Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
}

// A manual implementation avoids adding an unnecessary `Default` bound to
// generic parameter types while preserving their serialized representation.
#[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
Loading