Skip to content
41 changes: 41 additions & 0 deletions src/cluster/agglomerative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl Default for AgglomerativeClusteringParameters {
pub struct AgglomerativeClustering<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>> {
/// The cluster label assigned to each sample.
pub labels: Vec<usize>,
parameters: Option<AgglomerativeClusteringParameters>,
_phantom_tx: PhantomData<TX>,
_phantom_ty: PhantomData<TY>,
_phantom_x: PhantomData<X>,
Expand Down Expand Up @@ -176,12 +177,21 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>> AgglomerativeClusteri
}
Ok(AgglomerativeClustering {
labels,
parameters: Some(parameters),
_phantom_tx: PhantomData,
_phantom_ty: PhantomData,
_phantom_x: PhantomData,
_phantom_y: PhantomData,
})
}

/// Getter for parameters used in the model
///
/// # Returns
/// `Some` with the parameters used to configure the model, or `None` if unavailable.
pub fn parameters(&self) -> Option<&AgglomerativeClusteringParameters> {
self.parameters.as_ref()
}
}

impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>>
Expand Down Expand Up @@ -314,4 +324,35 @@ mod tests {

assert!(result.is_err());
}

#[test]
fn test_can_get_assigned_parameters() {
let data = vec![0.0, 0.0, 5.0, 5.0, 10.0, 10.0];
let matrix = DenseMatrix::new(3, 2, data, false).unwrap();
let parameters = AgglomerativeClusteringParameters::default().with_n_clusters(1);
let expected_parameters = parameters.clone();
let clustering = AgglomerativeClustering::<f64, f64, DenseMatrix<f64>, Vec<f64>>::fit(
&matrix, parameters,
)
.unwrap();

let actual_parameters = clustering
.parameters()
.expect("parameters should be set after fitting");
assert_eq!(actual_parameters.n_clusters, expected_parameters.n_clusters);
}

#[test]
fn test_returns_none_on_no_parameters() {
let clustering = AgglomerativeClustering::<f64, f64, DenseMatrix<f64>, Vec<f64>> {
labels: Vec::new(),
parameters: None,
_phantom_tx: PhantomData,
_phantom_ty: PhantomData,
_phantom_x: PhantomData,
_phantom_y: PhantomData,
};

assert!(clustering.parameters().is_none());
}
}
122 changes: 119 additions & 3 deletions src/cluster/dbscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ pub struct DBSCAN<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>, D: Dista
num_classes: usize,
knn_algorithm: KNNAlgorithm<TX, D>,
eps: f64,
parameters: Option<DBSCANParameters<TX, D>>,
_phantom_ty: PhantomData<TY>,
_phantom_x: PhantomData<X>,
_phantom_y: PhantomData<Y>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Debug, Clone)]
/// DBSCAN clustering algorithm parameters
pub struct DBSCANParameters<T: Number, D: Distance<Vec<T>>> {
Expand All @@ -91,6 +92,43 @@ pub struct DBSCANParameters<T: Number, D: Distance<Vec<T>>> {
_phantom_t: PhantomData<T>,
}

// SERDE: a manual implementation avoids the implicit `Default` bounds that
// `#[derive(Deserialize)]` would add to generic parameter types.
#[cfg(feature = "serde")]
impl<'de, T, D> Deserialize<'de> for DBSCANParameters<T, D>
where
T: Number,
D: Distance<Vec<T>> + Deserialize<'de>,
{
fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
where
De: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct DBSCANParametersData<D> {
distance: D,
#[serde(default)]
min_samples: usize,
#[serde(default)]
eps: f64,
#[serde(default)]
algorithm: KNNAlgorithmName,
#[serde(default)]
_phantom_t: PhantomData<()>,
}

let data = DBSCANParametersData::deserialize(deserializer)?;

Ok(Self {
distance: data.distance,
min_samples: data.min_samples,
eps: data.eps,
algorithm: data.algorithm,
_phantom_t: PhantomData,
})
}
}

impl<T: Number, D: Distance<Vec<T>>> DBSCANParameters<T, D> {
/// a function that defines a distance between each pair of point in training data.
/// This function should extend [`Distance`](../../math/distance/trait.Distance.html) trait.
Expand Down Expand Up @@ -295,7 +333,7 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>, D: Distance<Vec<TX>>>
x.row_iter()
.map(|row| row.iterator(0).cloned().collect())
.collect(),
parameters.distance,
parameters.distance.clone(),
)?;

let mut row = vec![TX::zero(); x.shape().1];
Expand Down Expand Up @@ -353,6 +391,7 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>, D: Distance<Vec<TX>>>
num_classes: k as usize,
knn_algorithm: algo,
eps: parameters.eps,
parameters: Some(parameters),
_phantom_ty: PhantomData,
_phantom_x: PhantomData,
_phantom_y: PhantomData,
Expand Down Expand Up @@ -392,6 +431,14 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>, D: Distance<Vec<TX>>>

Ok(result)
}

/// Getter for parameters used in the model
///
/// # Returns
/// `Some` with the parameters used to configure the model, or `None` if unavailable.
pub fn parameters(&self) -> Option<&DBSCANParameters<TX, D>> {
self.parameters.as_ref()
}
}

#[cfg(test)]
Expand Down Expand Up @@ -491,12 +538,34 @@ mod tests {
])
.unwrap();

let dbscan = DBSCAN::fit(&x, Default::default()).unwrap();
let parameters = DBSCANParameters::default()
.with_eps(0.5)
.with_min_samples(2)
.with_algorithm(KNNAlgorithmName::LinearSearch);
let expected_parameters = parameters.clone();
let dbscan = DBSCAN::fit(&x, parameters).unwrap();

let deserialized_dbscan: DBSCAN<f32, f32, DenseMatrix<f32>, Vec<f32>, Euclidian<f32>> =
serde_json::from_str(&serde_json::to_string(&dbscan).unwrap()).unwrap();

assert_eq!(dbscan, deserialized_dbscan);

let actual_parameters = deserialized_dbscan
.parameters()
.expect("parameters should survive serialization");
assert_eq!(
format!("{:?}", actual_parameters.distance),
format!("{:?}", expected_parameters.distance)
);
assert_eq!(
actual_parameters.min_samples,
expected_parameters.min_samples
);
assert_eq!(actual_parameters.eps, expected_parameters.eps);
assert_eq!(
std::mem::discriminant(&actual_parameters.algorithm),
std::mem::discriminant(&expected_parameters.algorithm)
);
}

#[cfg(feature = "datasets")]
Expand All @@ -514,4 +583,51 @@ mod tests {

println!("{labels:?}");
}

#[test]
fn test_can_get_assigned_parameters() {
let data = vec![0.0, 0.0, 0.5, 0.5, 10.0, 10.0];
let matrix = DenseMatrix::new(3, 2, data, false).unwrap();
let parameters: DBSCANParameters<f64, Euclidian<f64>> = DBSCANParameters::default()
.with_eps(1.0)
.with_min_samples(1);
let expected_parameters = parameters.clone();
let clustering = DBSCAN::<f64, f64, DenseMatrix<f64>, Vec<f64>, Euclidian<f64>>::fit(
&matrix, parameters,
)
.unwrap();

let actual_parameters = clustering
.parameters()
.expect("parameters should be set after fitting");
assert_eq!(
format!("{:?}", actual_parameters.distance),
format!("{:?}", expected_parameters.distance)
);
assert_eq!(
actual_parameters.min_samples,
expected_parameters.min_samples
);
assert_eq!(actual_parameters.eps, expected_parameters.eps);
assert_eq!(
std::mem::discriminant(&actual_parameters.algorithm),
std::mem::discriminant(&expected_parameters.algorithm)
);
}

#[test]
fn test_returns_none_on_no_parameters() {
let data = vec![0.0, 0.0, 0.5, 0.5, 10.0, 10.0];
let matrix = DenseMatrix::new(3, 2, data, false).unwrap();
let parameters: DBSCANParameters<f64, Euclidian<f64>> = DBSCANParameters::default()
.with_eps(1.0)
.with_min_samples(1);
let mut clustering = DBSCAN::<f64, f64, DenseMatrix<f64>, Vec<f64>, Euclidian<f64>>::fit(
&matrix, parameters,
)
.unwrap();
clustering.parameters = None;

assert!(clustering.parameters().is_none());
}
}
39 changes: 39 additions & 0 deletions src/cluster/kmeans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub struct KMeans<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>> {
size: Vec<usize>,
_distortion: f64,
centroids: Vec<Vec<f64>>,
parameters: Option<KMeansParameters>,
_phantom_tx: PhantomData<TX>,
_phantom_ty: PhantomData<TY>,
_phantom_x: PhantomData<X>,
Expand Down Expand Up @@ -315,6 +316,7 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>> KMeans<TX, TY, X, Y>
size,
_distortion: distortion,
centroids,
parameters: Some(parameters),
_phantom_tx: PhantomData,
_phantom_ty: PhantomData,
_phantom_x: PhantomData,
Expand Down Expand Up @@ -411,6 +413,14 @@ impl<TX: Number, TY: Number, X: Array2<TX>, Y: Array1<TY>> KMeans<TX, TY, X, Y>

y
}

/// Getter for parameters used in the model
///
/// # Returns
/// `Some` with the parameters used to configure the model, or `None` if unavailable.
pub fn parameters(&self) -> Option<&KMeansParameters> {
self.parameters.as_ref()
}
}

#[cfg(test)]
Expand Down Expand Up @@ -543,4 +553,33 @@ mod tests {

assert_eq!(kmeans, deserialized_kmeans);
}

#[test]
fn test_can_get_assigned_parameters() {
let data = vec![0.0, 0.0, 5.0, 5.0, 10.0, 10.0];
let matrix = DenseMatrix::new(3, 2, data, false).unwrap();
let parameters = KMeansParameters::default().with_k(2);
let expected_parameters = parameters.clone();
let clustering =
KMeans::<f64, f64, DenseMatrix<f64>, Vec<f64>>::fit(&matrix, parameters).unwrap();

let actual_parameters = clustering
.parameters()
.expect("parameters should be set after fitting");
assert_eq!(actual_parameters.k, expected_parameters.k);
assert_eq!(actual_parameters.max_iter, expected_parameters.max_iter);
assert_eq!(actual_parameters.seed, expected_parameters.seed);
}

#[test]
fn test_returns_none_on_no_parameters() {
let data = vec![0.0, 0.0, 5.0, 5.0, 10.0, 10.0];
let matrix = DenseMatrix::new(3, 2, data, false).unwrap();
let parameters = KMeansParameters::default().with_k(2);
let mut clustering =
KMeans::<f64, f64, DenseMatrix<f64>, Vec<f64>>::fit(&matrix, parameters).unwrap();
clustering.parameters = None;

assert!(clustering.parameters().is_none());
}
}
42 changes: 42 additions & 0 deletions src/decomposition/pca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub struct PCA<T: Number + RealNumber, X: Array2<T> + SVDDecomposable<T> + EVDDe
eigenvectors: X,
eigenvalues: Vec<T>,
projection: X,
parameters: Option<PCAParameters>,
mu: Vec<T>,
pmu: Vec<T>,
}
Expand Down Expand Up @@ -329,6 +330,7 @@ impl<T: Number + RealNumber, X: Array2<T> + SVDDecomposable<T> + EVDDecomposable
eigenvectors,
eigenvalues,
projection: projection.transpose(),
parameters: Some(parameters),
mu,
pmu,
})
Expand Down Expand Up @@ -360,6 +362,14 @@ impl<T: Number + RealNumber, X: Array2<T> + SVDDecomposable<T> + EVDDecomposable
pub fn components(&self) -> &X {
&self.projection
}

/// Getter for parameters used in the model
///
/// # Returns
/// `Some` with the parameters used to configure the model, or `None` if unavailable.
pub fn parameters(&self) -> Option<&PCAParameters> {
self.parameters.as_ref()
}
}

#[cfg(test)]
Expand Down Expand Up @@ -747,4 +757,36 @@ mod tests {

// assert_eq!(pca, deserialized_pca);
// }

#[test]
fn test_can_get_assigned_parameters() {
let data = vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0];
let matrix = DenseMatrix::new(4, 2, data, false).unwrap();
let parameters = PCAParameters::default().with_n_components(1);
let expected_parameters = parameters.clone();
let pca = PCA::<f64, DenseMatrix<f64>>::fit(&matrix, parameters).unwrap();

let actual_parameters = pca
.parameters()
.expect("parameters should be set after fitting");
assert_eq!(
actual_parameters.n_components,
expected_parameters.n_components
);
assert_eq!(
actual_parameters.use_correlation_matrix,
expected_parameters.use_correlation_matrix
);
}

#[test]
fn test_returns_none_on_no_parameters() {
let data = vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0];
let matrix = DenseMatrix::new(4, 2, data, false).unwrap();
let parameters = PCAParameters::default().with_n_components(1);
let mut pca = PCA::<f64, DenseMatrix<f64>>::fit(&matrix, parameters).unwrap();
pca.parameters = None;

assert!(pca.parameters().is_none());
}
}
Loading