User guide
scikit-clarans provides estimators for $k$-medoids clustering. This guide covers algorithm selection, parameter tuning, and search mechanics.
Why use k-medoids?
Standard $k$-means calculates cluster centers by averaging coordinates in Euclidean space, which creates synthetic centroids. In contrast, $k$-medoids restricts cluster centers to actual observations from the dataset:
Outlier resistance: Medoids minimize absolute pairwise distances (\(\sum d\)) rather than squared Euclidean distances (\(\sum d^2\)), keeping centers stable when the dataset contains extreme values.
Custom distance metrics: Supports metrics such as Manhattan distance, cosine dissimilarity, or precomputed distance matrices, whereas standard $k$-means requires Euclidean space.
Interpretability: Every medoid corresponds to a real record in the input data, such as a patient profile, an exemplary document, or a molecule.
Note
In scikit-learn’s KMeans, inertia_ represents the sum of squared Euclidean distances. In scikit-clarans, inertia_ represents the sum of unsquared distances from each sample to its assigned medoid according to the chosen metric.
Quick start
Clustering with CLARANS:
from clarans import CLARANS
from sklearn.datasets import make_blobs
# 1. Generate sample data
X, _ = make_blobs(n_samples=1000, centers=4, n_features=2, random_state=42)
# 2. Instantiate and fit model
model = CLARANS(
n_clusters=4,
num_local=3,
init='k-medoids++',
random_state=42
)
model.fit(X)
# 3. Inspect results
print("Medoid Indices:", model.medoid_indices_)
print("Medoid Coordinates:\n", model.cluster_centers_)
print("Labels (first 10):", model.labels_[:10])
print("Inertia (Total Distance Cost):", model.inertia_)
Choosing an estimator
scikit-clarans includes two estimators:
Feature |
CLARANS |
FastCLARANS |
|---|---|---|
Reference |
Ng & Han (2002) |
Schubert & Rousseeuw (2021) |
Candidate sampling |
Random (medoid, non-medoid) pairs |
Non-medoid samples tested against all \(k\) medoids |
Swap evaluation |
Single swap evaluation (\(O(n)\) with |
FastPAM1 vectorized delta cost across all \(k\) medoids |
Working memory |
\(O(n)\) |
\(O(n)\) |
Primary use |
Baseline and algorithm comparison |
General clustering workflows and benchmarks |
Note
Both estimators compute distances dynamically with an \(O(n)\) memory footprint rather than storing an \(O(n^2)\) matrix. They run in a single process and typically handle tens of thousands of samples efficiently.
FastCLARANS example:
from clarans import FastCLARANS
fast_model = FastCLARANS(n_clusters=4, num_local=3, random_state=42)
fast_model.fit(X)
Configuration and parameter tuning
Both estimators share core parameters, with CLARANS providing an additional cost_evaluation option:
Parameter |
Estimator |
Default |
Description |
|---|---|---|---|
|
Both |
|
Number of clusters (medoids) to find (\(k\)). |
|
Both |
|
Number of local searches (random restarts). Higher values explore more local minima. |
|
Both |
|
Maximum non-improving neighbors to check per search. Defaults to \(\max(250, 1.25\% \times k(n-k))\) in CLARANS and \(\max(1, \lfloor 250/k \rfloor, 2.5\% \times (n-k))\) in FastCLARANS. |
|
Both |
|
Initialization strategy ( |
|
Both |
|
Distance metric to use (e.g., |
|
Both |
|
Additional keyword arguments for the metric function (e.g., |
|
CLARANS only |
|
Swap evaluation method ( |
|
Both |
|
Verbosity level ( |
Tuning guidelines
- Cost evaluation in CLARANS:
CLARANSsupportscost_evaluation='delta'(default) andcost_evaluation='brute_force'. FastCLARANS does not expose this parameter because its FastPAM1 formulation always calculates deltas.cost_evaluation='delta'caches nearest (\(d_1\)) and second-nearest (\(d_2\)) medoid distances for all samples. Each candidate swap runs in \(O(n \cdot d)\) operations without recomputing distances to unchanged medoids. This gives a 1.5x to 2.5x speedup with identical clustering results.cost_evaluation='brute_force'recalculates total clustering cost from scratch in \(O(n \cdot k \cdot d)\), reproducing the original formulation of Ng & Han (2002).
- Initialization strategy (
init): 'k-medoids++'(default): Selects initial medoids probabilistically based on squared distances. Fast and requires \(O(n \cdot k)\) memory.'random': Uniform random sampling. Very fast, though it may require increasingnum_localto reach comparable solution quality.'build': Greedy initialization from classic PAM. Provides good starting points on small datasets, but computes the full pairwise distance matrix (\(O(n^2)\) time and memory). Avoid for datasets larger than 5,000 samples.'heuristic': Selects the \(k\) samples with the smallest sum of distances to all other points (\(O(n^2)\) time and memory). Avoid for datasets larger than 5,000 samples.Array-like: User-provided array of initial medoid coordinates or indices.
- Number of restarts (
num_local): For randomized initialization (
'k-medoids++'or'random'), increasingnum_localto 3 to 5 helps find better local minima when solutions vary across runs.Note
Deterministic initialization methods such as
'build','heuristic', or explicit coordinate arrays always produce the same starting points. With deterministic seeding, setnum_local=1to avoid repeated identical searches.- Neighbor sampling (
max_neighbors): The default
max_neighbors='auto'scales with the dataset size according to formulas from the original papers (\(\max(250, 1.25\% \times k(n-k))\) in CLARANS and \(\max(1, \lfloor 250/k \rfloor, 2.5\% \times (n-k))\) in FastCLARANS). An explicit integer value (such asmax_neighbors=200) is mainly useful when you need a fixed ceiling on search steps for very large inputs.
Distance metrics and routing
scikit-clarans automatically routes distance computations based on the input data type, matrix sparsity, and chosen metric:
Dense array metrics: Standard distance functions (
'euclidean','manhattan','cosine','chebyshev','minkowski','mahalanobis') are routed to SciPy’s optimizedcdist. Metric aliases are mapped automatically (such as'cityblock'and'l1'to'manhattan', or'l2'to'euclidean'). Keyword parameters such as{'p': 3}or{'VI': VI}are passed viametric_params.Sparse matrix inputs (scipy.sparse): When
Xis a CSR or CSC sparse matrix, distance evaluations are routed to scikit-learn’sDistanceMetricwithout densifying the full matrix, maintaining an \(O(n)\) memory footprint.Missing data (NaN) handling: When inputs contain missing values (NaN), configure
metric='nan_euclidean'. Any dimension where either sample is missing is excluded from the distance sum, and the Euclidean distance is scaled by \(\sqrt{d / d_{\text{valid}}}\).Custom callable functions: Supply any Python distance function
metric=funcaccepting two 1D vectors(u, v) -> float. For example, a custom Haversine function can cluster geographical latitude and longitude coordinates on the Earth’s surface.Precomputed distance matrices: Pass a pairwise distance matrix with
metric='precomputed'. The algorithm validates symmetry using fast Cython kernels (is_matrix_symmetric) and handles both symmetric and directed/asymmetric matrices (\(D \neq D^T\)). When performing inference withpredict()ortransform()on new query points, you can provide either the full matrix of shape \((n_{\text{test}}, n_{\text{train}})\) or a compact submatrix of shape \((n_{\text{test}}, k)\) matching only the \(k\) medoids.
Fitted attributes
After calling fit(X), both CLARANS and FastCLARANS store the clustering results and search statistics in estimator attributes:
Attribute |
Type |
Description |
|---|---|---|
|
ndarray or None |
Coordinates of the selected medoids with shape |
|
ndarray |
Row indices of the selected medoids in the training data |
|
ndarray |
Cluster labels assigned to each sample in the training set with shape |
|
float |
Sum of distances from each point to its assigned medoid according to the chosen metric. |
|
int |
Effective threshold of non-improving neighbor evaluations per restart. Equals the resolved integer value when |
|
int |
Search neighborhood size: \(k(n-k)\) in |
|
int |
Total number of candidate evaluations tested across all |
|
int |
Total number of accepted medoid swaps across all |
|
int |
Number of input features seen during |
Attribute details and conventions
- Inertia calculation:
inertia_represents the sum of unsquared distances \(\sum_{i=1}^n \min_{m \in M} d(x_i, m)\) using the chosen distance metric. StandardKMeansin scikit-learn computes the sum of squared Euclidean distances (\(\sum d^2\)). When comparing inertia across estimators or distance functions, account for the difference between linear and squared scale.- Coordinates with precomputed metrics:
When
metric='precomputed', the inputXis a distance matrix of shape(n_samples, n_samples). Because the original feature coordinates are not passed tofit,cluster_centers_is set toNone. Usemedoid_indices_to retrieve the row positions of the selected medoids in your original data.- Neighbor attributes and auto-scaling:
The constructor parameter
max_neighborsaccepts an integer or the string'auto'. The fitted attributemax_neighbors_(with a trailing underscore) always stores the concrete integer limit used during the search.The total neighborhood size
n_neighbors_differs between estimators because of their search formulation:In
CLARANS, each search step samples a pair of (medoid, non-medoid), son_neighbors_equals the number of edges in graph \(G_{n,k}\), namely \(k(n-k)\).In
FastCLARANS, each step samples a non-medoid candidate point and evaluates all \(k\) medoid swaps simultaneously, son_neighbors_equals \(n-k\).
- Iteration and swap counters:
In scikit-learn clusterers such as
KMeans,n_iter_typically records the iteration count of the best restart. Inscikit-clarans,n_iter_records the cumulative count of candidate evaluations tested across allnum_localrestarts. Accepted medoid replacements are tracked separately inn_swaps_.
Monitoring search progress
Both CLARANS and FastCLARANS provide console logging through the verbose parameter to monitor local searches during fit.
Summary table (verbose=1)
Setting verbose=1 (or verbose=True) prints a configuration header, a summary table with one row per local search, and a final line marking the best restart:
[CLARANS] n=1000, k=4, metric=euclidean, num_local=3, max_neighbors=250/3984 (6.3%)
# Cost Swaps Evals Time(s)
1 150.12345* 4 80 0.012s converged
2 148.54321* 5 110 0.015s converged
3 152.00000 2 65 0.009s converged
Best: #2 | Totals: 11 swaps, 255 evals, 0.038s
The header reports the dataset size (\(n\)), number of clusters (\(k\)), distance metric, total restarts (num_local), and the sampled neighbor budget (max_neighbors) relative to the total neighborhood size.
Table columns:
#: 1-based index of the restart.Cost: Final clustering inertia (sum of distances) for that restart. An asterisk (*) flags an iteration that set a new lowest cost across all completed restarts.Swaps: Number of accepted medoid swaps.Evals: Total candidate evaluations tested during the restart.Time(s): Wall-clock duration of the restart.Stopping status:
converged: The search evaluatedmax_neighborsconsecutive non-improving candidates without finding a cost reduction.exhausted: The search tested every available non-medoid sample before reachingmax_neighbors.
The summary footer displays the restart index that achieved the lowest cost, followed by cumulative totals for accepted swaps, evaluations, and overall execution time.
Step-by-step traces (verbose=2)
Setting verbose=2 prints each accepted swap in real time:
[CLARANS] n=1000, k=4, metric=euclidean, num_local=2, max_neighbors=250/3984 (6.3%)
Restart 1/2 (init cost: 234.56789):
swap 1 | eval 8 | cost 210.12345 | diff -24.44444
swap 2 | eval 25 | cost 180.50000 | diff -29.62345
1 180.50000* swaps=2, evals=275, 0.021s (converged)
Restart 2/2 (init cost: 245.11111):
swap 1 | eval 12 | cost 195.43210 | diff -49.67901
2 195.43210 swaps=1, evals=262, 0.019s (converged)
Best: #1 | Totals: 3 swaps, 537 evals, 0.041s
In addition to the restart summary, this level records:
init cost: Starting inertia computed right after medoid initialization.swap: Sequence number of the accepted swap within the current restart.eval: Running count of neighbor evaluations examined when the improving swap was found.cost: New total distance cost after making the swap.diff: Cost change (\(\Delta < 0\)) produced by the swap.
Search mechanics
The k-medoids search space can be viewed as an undirected graph \(G_{n,k} = (V, E)\):
Vertices: Each vertex \(v \in V\) represents a set of \(k\) medoids from \(n\) samples. The total number of vertices is \(\binom{n}{k}\).
Edges: An edge connects two vertices if their medoid sets differ by exactly one point (one swap \(m_j \leftrightarrow x_c\)). Each vertex has \(k(n-k)\) neighbors.
Exhaustive search: Standard PAM checks all \(k(n-k)\) neighbors at each step to find the steepest descent, which costs \(O(k(n-k)^2)\) per iteration.
CLARANS search: CLARANS checks randomly sampled neighbors. As soon as a neighbor reduces total distance cost, it makes the swap immediately (first-choice hill climbing). If
max_neighborsconsecutive samples yield no improvement, the search stops at a local minimum.FastCLARANS search: FastCLARANS samples candidate replacement points and evaluates the swap delta against all \(k\) current medoids simultaneously using FastPAM1 equations. A single \(O(n)\) pass over the data evaluates \(k\) neighbors at once.