From 6c3a78fd2d03b2b4a9c52826c95988132a891dd0 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Sun, 24 Nov 2024 13:58:09 -0800 Subject: [PATCH 01/13] Initial commit for implementation of Affinity Propagation --- src/MLJClusteringInterface.jl | 79 +++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 4e9b17e..407358b 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -16,7 +16,7 @@ using Distances # =================================================================== ## EXPORTS -export KMeans, KMedoids, DBSCAN, HierarchicalClustering +export KMeans, KMedoids, AffinityPropagation, DBSCAN, HierarchicalClustering # =================================================================== ## CONSTANTS @@ -95,10 +95,69 @@ function MMI.transform(model::KMedoids, fitresult, X) return MMI.table(X̃, prototype=X) end +# # AFFINITY_PROPAGATION -# # PREDICT FOR K_MEANS AND K_MEDOIDS +@mlj_model mutable struct AffinityPropagation <: MMI.Unsupervised + damp::Float64 = 0.5::(0.0 ≤ _ < 1.0) + maxiter::Int = 200::(_ > 0) + tol::Float64 = 1e-6::(_ > 0) + preference::Union{Nothing,Float64} = nothing + metric::SemiMetric = SqEuclidean() +end + +function MMI.fit(model::AffinityPropagation, verbosity::Int, X) + Xarray = MMI.matrix(X)' + + # Compute similarity matrix using negative pairwise distances + S = -pairwise(model.metric, Xarray, dims=2) + + # Set preferences on diagonal if specified + if !isnothing(model.preference) + fill!(view(S, diagind(S)), model.preference) + end + + result = Cl.affinityprop( + S, + maxiter=model.maxiter, + tol=model.tol, + damp=model.damp + ) -function MMI.predict(model::Union{KMeans,KMedoids}, fitresult, Xnew) + # Get number of clusters and labels + exemplars = result.exemplars + k = length(exemplars) + cluster_labels = MMI.categorical(1:k) + + # Store exemplar points as centers (similar to KMeans/KMedoids) + centers = view(Xarray, :, exemplars) + + fitresult = (centers, cluster_labels) + cache = nothing + report = ( + assignments=result.assignments, + cluster_labels=cluster_labels, + iterations=result.iterations, + converged=result.converged + ) + + return fitresult, cache, report +end + +MMI.fitted_params(::AffinityPropagation, fitresult) = (exemplars=fitresult[1],) + +function MMI.transform(model::AffinityPropagation, fitresult, X) + # negative pairwise distance from samples to exemplars + X̃ = -pairwise( + model.metric, + MMI.matrix(X)', + fitresult[1], dims=2 + ) + return MMI.table(X̃, prototype=X) +end + +# # PREDICT FOR K_MEANS, K_MEDOIDS and AFFINITY_PROPAGATION + +function MMI.predict(model::Union{KMeans,KMedoids,AffinityPropagation}, fitresult, Xnew) locations, cluster_labels = fitresult Xarray = MMI.matrix(Xnew) (n, p), k = size(Xarray), model.k @@ -211,7 +270,7 @@ MMI.reporting_operations(::Type{<:HierarchicalClustering}) = (:predict,) # # METADATA metadata_pkg.( - (KMeans, KMedoids, DBSCAN, HierarchicalClustering), + (KMeans, KMedoids, DBSCAN, HierarchicalClustering, AffinityPropagation), name="Clustering", uuid="aaaa29a8-35af-508c-8bc3-b662a17a0fe5", url="https://github.com/JuliaStats/Clustering.jl", @@ -251,6 +310,18 @@ metadata_model( path = "$(PKG).HierarchicalClustering" ) +metadata_model( + AffinityPropagation, + human_name = "Affinity Propagation clusterer", + input_scitype = Tuple{MMI.Table(Continuous)}, # Not sure about this part + path = "$(PKG).AffinityPropagation" +) + +""" +$(MMI.doc_header(AffinityPropagation)) +To be added later! +""" + """ $(MMI.doc_header(KMeans)) From 5ff901300778ca45d69ff4a48b1bd1770b0b4c27 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Sun, 24 Nov 2024 14:15:09 -0800 Subject: [PATCH 02/13] Fix the input_scitype --- src/MLJClusteringInterface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 407358b..fb89248 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -313,7 +313,7 @@ metadata_model( metadata_model( AffinityPropagation, human_name = "Affinity Propagation clusterer", - input_scitype = Tuple{MMI.Table(Continuous)}, # Not sure about this part + input_scitype = MMI.Table(Continuous), path = "$(PKG).AffinityPropagation" ) From 222e9197451c1423ba02db3ca8a7c3dd77175711 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Mon, 2 Dec 2024 15:28:24 -0800 Subject: [PATCH 03/13] Make AffinityPropagation a Static model --- src/MLJClusteringInterface.jl | 113 +++++++++++++++------------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index fb89248..7ec7bb5 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -95,69 +95,9 @@ function MMI.transform(model::KMedoids, fitresult, X) return MMI.table(X̃, prototype=X) end -# # AFFINITY_PROPAGATION - -@mlj_model mutable struct AffinityPropagation <: MMI.Unsupervised - damp::Float64 = 0.5::(0.0 ≤ _ < 1.0) - maxiter::Int = 200::(_ > 0) - tol::Float64 = 1e-6::(_ > 0) - preference::Union{Nothing,Float64} = nothing - metric::SemiMetric = SqEuclidean() -end - -function MMI.fit(model::AffinityPropagation, verbosity::Int, X) - Xarray = MMI.matrix(X)' - - # Compute similarity matrix using negative pairwise distances - S = -pairwise(model.metric, Xarray, dims=2) - - # Set preferences on diagonal if specified - if !isnothing(model.preference) - fill!(view(S, diagind(S)), model.preference) - end - - result = Cl.affinityprop( - S, - maxiter=model.maxiter, - tol=model.tol, - damp=model.damp - ) - - # Get number of clusters and labels - exemplars = result.exemplars - k = length(exemplars) - cluster_labels = MMI.categorical(1:k) - - # Store exemplar points as centers (similar to KMeans/KMedoids) - centers = view(Xarray, :, exemplars) - - fitresult = (centers, cluster_labels) - cache = nothing - report = ( - assignments=result.assignments, - cluster_labels=cluster_labels, - iterations=result.iterations, - converged=result.converged - ) - - return fitresult, cache, report -end - -MMI.fitted_params(::AffinityPropagation, fitresult) = (exemplars=fitresult[1],) +# # PREDICT FOR K_MEANS AND K_MEDOIDS -function MMI.transform(model::AffinityPropagation, fitresult, X) - # negative pairwise distance from samples to exemplars - X̃ = -pairwise( - model.metric, - MMI.matrix(X)', - fitresult[1], dims=2 - ) - return MMI.table(X̃, prototype=X) -end - -# # PREDICT FOR K_MEANS, K_MEDOIDS and AFFINITY_PROPAGATION - -function MMI.predict(model::Union{KMeans,KMedoids,AffinityPropagation}, fitresult, Xnew) +function MMI.predict(model::Union{KMeans,KMedoids}, fitresult, Xnew) locations, cluster_labels = fitresult Xarray = MMI.matrix(Xnew) (n, p), k = size(Xarray), model.k @@ -267,6 +207,55 @@ end MMI.reporting_operations(::Type{<:HierarchicalClustering}) = (:predict,) +# # AFFINITY_PROPAGATION + +@mlj_model mutable struct AffinityPropagation <: MMI.Static + damp::Float64 = 0.5::(0.0 ≤ _ < 1.0) + maxiter::Int = 200::(_ > 0) + tol::Float64 = 1e-6::(_ > 0) + preference::Union{Nothing,Float64} = nothing + metric::SemiMetric = SqEuclidean() +end + +function MMI.predict(model::AffinityPropagation, ::Nothing, X) + Xarray = MMI.matrix(X)' + + # Compute similarity matrix using negative pairwise distances + S = -pairwise(model.metric, Xarray, dims=2) + + # Set preferences on diagonal if specified + if !isnothing(model.preference) + fill!(view(S, diagind(S)), model.preference) + end + + result = Cl.affinityprop( + S, + maxiter=model.maxiter, + tol=model.tol, + damp=model.damp + ) + + # Get number of clusters and labels + exemplars = result.exemplars + k = length(exemplars) + cluster_labels = MMI.categorical(1:k) + + # Store exemplar points as centers (similar to KMeans/KMedoids) + centers = view(Xarray, :, exemplars) + + report = ( + exemplars=exemplars, + centers=centers, + cluster_labels=cluster_labels, + iterations=result.iterations, + converged=result.converged + ) + + return MMI.caterogical(result.assignments), report +end + +MMI.reporting_operations(::Type{<:AffinityPropagation}) = (:predict,) + # # METADATA metadata_pkg.( From 2f187355d5db77f088f677fc669810112e978acd Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Mon, 2 Dec 2024 16:44:56 -0800 Subject: [PATCH 04/13] Fix missing dep --- Project.toml | 2 ++ src/MLJClusteringInterface.jl | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 56b46d6..0845e0a 100644 --- a/Project.toml +++ b/Project.toml @@ -6,10 +6,12 @@ version = "0.1.11" [deps] Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLJModelInterface = "e80e1ace-859a-464e-9ed9-23947d8ae3ea" [compat] Clustering = "0.15" Distances = "0.9, 0.10" +LinearAlgebra = "1.11.0" MLJModelInterface = "1.4" julia = "1.6" diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 7ec7bb5..828c218 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -13,6 +13,7 @@ import MLJModelInterface: Continuous, Count, Finite, Multiclass, Table, OrderedF @mlj_model, metadata_model, metadata_pkg using Distances +using LinearAlgebra # =================================================================== ## EXPORTS @@ -251,7 +252,7 @@ function MMI.predict(model::AffinityPropagation, ::Nothing, X) converged=result.converged ) - return MMI.caterogical(result.assignments), report + return MMI.categorical(result.assignments), report end MMI.reporting_operations(::Type{<:AffinityPropagation}) = (:predict,) From d1b95e93d5c4c6dcd1ce72e5e704ac456afb77d6 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Mon, 2 Dec 2024 16:46:21 -0800 Subject: [PATCH 05/13] Add doc --- src/MLJClusteringInterface.jl | 74 ++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 828c218..dbd86d2 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -307,11 +307,6 @@ metadata_model( path = "$(PKG).AffinityPropagation" ) -""" -$(MMI.doc_header(AffinityPropagation)) -To be added later! -""" - """ $(MMI.doc_header(KMeans)) @@ -675,4 +670,73 @@ report(mach).cutter(h = 2.5) """ HierarchicalClustering +""" +$(MMI.doc_header(AffinityPropagation)) + +[Affinity Propagation](https://en.wikipedia.org/wiki/Affinity_propagation) is a clustering algorithm based on the concept of "message passing" between data points. More information is available at the [Clustering.jl documentation](https://juliastats.org/Clustering.jl/stable/index.html). Use `predict` to get cluster assignments. Indices of the exemplars, their values, etc, are accessed from the machine report (see below). + +This is a static implementation, i.e., it does not generalize to new data instances, and +there is no training data. For clusterers that do generalize, see [`KMeans`](@ref) or +[`KMedoids`](@ref). + +In MLJ or MLJBase, create a machine with + + mach = machine(model) + +# Hyper-parameters + +- `damp = 0.5`: damping factor + +- `maxiter = 200`: maximum number of iteration + +- `tol = 1e-6`: tolerance for converenge + +- `preference = nothing`: the value of the diagonal elements of the similarity matrix + +- `metric = SqEuclidean`: metric (see `Distances.jl` for available metrics) + +# Operations + +- `predict(mach, X)`: return cluster label assignments, as an unordered + `CategoricalVector`. Here `X` is any table of input features (eg, a `DataFrame`) whose + columns are of scitype `Continuous`; check column scitypes with `schema(X)`. + +# Report + +After calling `predict(mach)`, the fields of `report(mach)` are: + +- exemplars: indices of the data picked as exemplars in `X` + +- centers: positions of the exemplars in the feature space + +- cluster_labels: labels of clusters given to each datum in `X` + +- iterations: the number of iteration run by the algorithm + +- converged: whether or not the algorithm converges by the maximum iteration + +# Examples + +``` +using MLJ, MLJClusteringInterface + +X, labels = make_moons(400, noise=0.9, rng=1) + +AffinityPropagation = @load AffinityPropagation pkg=Clustering +model = AffinityPropagation(preferences=-10.0) +mach = machine(model) + +# compute and output cluster assignments for observations in `X`: +yhat = predict(mach, X) + +# Get the positions of the exemplars +report(mach).centers + +# Plot clustering result +using GLMakie +scatter(MLJ.matrix(X)', color=yhat.refs) +``` +""" +AffinityPropagation + end # module From c2f5d18c5a39eaf3a1a95569cd2616818394a786 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Mon, 2 Dec 2024 17:00:49 -0800 Subject: [PATCH 06/13] Fix typo of a var in doc --- src/MLJClusteringInterface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index dbd86d2..c4745d0 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -723,7 +723,7 @@ using MLJ, MLJClusteringInterface X, labels = make_moons(400, noise=0.9, rng=1) AffinityPropagation = @load AffinityPropagation pkg=Clustering -model = AffinityPropagation(preferences=-10.0) +model = AffinityPropagation(preference=-10.0) mach = machine(model) # compute and output cluster assignments for observations in `X`: From 7d0453c583e169c192bad603d1fe15dc1ae2eb24 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Mon, 2 Dec 2024 18:04:15 -0800 Subject: [PATCH 07/13] Set default preference to be median similarity of all pairs --- Project.toml | 2 ++ src/MLJClusteringInterface.jl | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 0845e0a..73bea52 100644 --- a/Project.toml +++ b/Project.toml @@ -8,10 +8,12 @@ Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLJModelInterface = "e80e1ace-859a-464e-9ed9-23947d8ae3ea" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [compat] Clustering = "0.15" Distances = "0.9, 0.10" LinearAlgebra = "1.11.0" MLJModelInterface = "1.4" +StatsBase = "0.34.3" julia = "1.6" diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index c4745d0..45f9496 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -14,6 +14,7 @@ import MLJModelInterface: Continuous, Count, Finite, Multiclass, Table, OrderedF using Distances using LinearAlgebra +using StatsBase # =================================================================== ## EXPORTS @@ -224,11 +225,18 @@ function MMI.predict(model::AffinityPropagation, ::Nothing, X) # Compute similarity matrix using negative pairwise distances S = -pairwise(model.metric, Xarray, dims=2) - # Set preferences on diagonal if specified - if !isnothing(model.preference) - fill!(view(S, diagind(S)), model.preference) + diagonal_element = if !isnothing(model.preference) + model.preference + else + # Get the median out of all pairs of similarity, that is, values above + # the diagonal line. + # Such default choice is mentioned in the algorithm's wiki article + iuppertri = triu!(trues(size(S)),1) + median(S[iuppertri]) end + fill!(view(S, diagind(S)), diagonal_element) + result = Cl.affinityprop( S, maxiter=model.maxiter, From 20b3b8dd6dc908598c2de3943eebcfc1336f23fd Mon Sep 17 00:00:00 2001 From: "Leon (Yuan-Ru) Lin" Date: Thu, 5 Dec 2024 16:03:32 -0800 Subject: [PATCH 08/13] Remove redundant import of MLJClusteringInterface Co-authored-by: Anthony Blaom, PhD --- src/MLJClusteringInterface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 45f9496..43f2cfe 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -726,7 +726,7 @@ After calling `predict(mach)`, the fields of `report(mach)` are: # Examples ``` -using MLJ, MLJClusteringInterface +using MLJ X, labels = make_moons(400, noise=0.9, rng=1) From 06a2d885f549781ecc84499d67eb6803f92f8ba3 Mon Sep 17 00:00:00 2001 From: "Leon (Yuan-Ru) Lin" Date: Thu, 5 Dec 2024 16:04:09 -0800 Subject: [PATCH 09/13] Specify where SqEuclidean() lives Co-authored-by: Anthony Blaom, PhD --- src/MLJClusteringInterface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 43f2cfe..68747a9 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -701,7 +701,7 @@ In MLJ or MLJBase, create a machine with - `preference = nothing`: the value of the diagonal elements of the similarity matrix -- `metric = SqEuclidean`: metric (see `Distances.jl` for available metrics) +- `metric = Distances.SqEuclidean()`: metric (see `Distances.jl` for available metrics) # Operations From 9b489262431fe1fc55e3a561aa21c5380e806429 Mon Sep 17 00:00:00 2001 From: "Leon (Yuan-Ru) Lin" Date: Thu, 5 Dec 2024 16:05:03 -0800 Subject: [PATCH 10/13] Loose versions of newly added deps Co-authored-by: Anthony Blaom, PhD --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 73bea52..da904eb 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [compat] Clustering = "0.15" Distances = "0.9, 0.10" -LinearAlgebra = "1.11.0" +LinearAlgebra = "1" MLJModelInterface = "1.4" -StatsBase = "0.34.3" +StatsBase = "0.34" julia = "1.6" From 45dd5839f4f23b2cb62d3eff30265da3361588f9 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Thu, 5 Dec 2024 16:17:27 -0800 Subject: [PATCH 11/13] Document how an unspecified preference is handled --- src/MLJClusteringInterface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJClusteringInterface.jl b/src/MLJClusteringInterface.jl index 68747a9..3dc83e0 100644 --- a/src/MLJClusteringInterface.jl +++ b/src/MLJClusteringInterface.jl @@ -699,7 +699,7 @@ In MLJ or MLJBase, create a machine with - `tol = 1e-6`: tolerance for converenge -- `preference = nothing`: the value of the diagonal elements of the similarity matrix +- `preference = nothing`: the (single float) value of the diagonal elements of the similarity matrix. If unspecified, choose median (negative) similarity of all pairs as mentioned [here](https://en.wikipedia.org/wiki/Affinity_propagation#Algorithm) - `metric = Distances.SqEuclidean()`: metric (see `Distances.jl` for available metrics) From fc87b92781ff1d254e2026282569faa64678d997 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Thu, 5 Dec 2024 16:19:42 -0800 Subject: [PATCH 12/13] Bump supported Julia version to 1.10 --- .github/workflows/ci.yml | 2 +- Project.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3713c1b..361c6c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.6' + - '1.10' - '1' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index da904eb..9f813f0 100644 --- a/Project.toml +++ b/Project.toml @@ -16,4 +16,4 @@ Distances = "0.9, 0.10" LinearAlgebra = "1" MLJModelInterface = "1.4" StatsBase = "0.34" -julia = "1.6" +julia = "1.10" From da6e69cdde8f7e0082cb61d09d64d14576c16bd3 Mon Sep 17 00:00:00 2001 From: Yuan-Ru Lin Date: Thu, 5 Dec 2024 16:54:52 -0800 Subject: [PATCH 13/13] Add test for Affinity Propagation --- test/runtests.jl | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index b0bc903..b42088e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -150,8 +150,40 @@ end @test report(mach).dendrogram.heights == dendro.heights end +# # AffinityPropagation + +@testset "AffinityPropagation" begin + X = table(stack(Iterators.partition(0.5:0.5:20, 5))') + + # Test case 1: preference == median (negative) similarity (i.e. unspecified) + mach = machine(AffinityPropagation()) + + yhat = predict(mach, X) + @test yhat == [1, 1, 1, 1, 2, 2, 2, 2] + + _report = report(mach) + @test _report.exemplars == [2, 7] + @test _report.centers == [3.0 15.5; 3.5 16.0; 4.0 16.5; 4.5 17.0; 5.0 17.5] + @test _report.cluster_labels == [1, 2] + @test _report.iterations == 50 + @test _report.converged == true + + # Test case 2: |preference| too large + mach2 = machine(AffinityPropagation(preference=-20.0)) + + yhat = predict(mach2, X) + @test yhat == [1, 2, 3, 4, 5, 6, 7, 8] + + _report = report(mach2) + @test _report.exemplars == [1, 2, 3, 4, 5, 6, 7, 8] + @test _report.centers == matrix(X)' + @test _report.cluster_labels == [1, 2, 3, 4, 5, 6, 7, 8] + @test _report.iterations == 32 + @test _report.converged == true +end + @testset "MLJ interface" begin - models = [KMeans, KMedoids, DBSCAN, HierarchicalClustering] + models = [KMeans, KMedoids, DBSCAN, HierarchicalClustering, AffinityPropagation] failures, summary = MLJTestInterface.test( models, X;