K Medoids
S
R
"""
k-Medoids Clustering Algorithm
For more details, see:
https://en.wikipedia.org/wiki/K-medoids
"""
import doctest
import numpy as np
from numpy import ndarray
from sklearn.datasets import load_iris
def _get_data() -> tuple[ndarray, ndarray]:
"""
Load the Iris dataset and return features and labels.
Returns:
tuple[ndarray, ndarray]: Feature matrix and target labels.
>>> features, labels = _get_data()
>>> features.shape
(150, 4)
>>> labels.shape
(150,)
"""
iris = load_iris()
return np.array(iris.data), np.array(iris.target)
def _compute_distances(data_matrix: ndarray, medoids: ndarray) -> ndarray:
"""
Compute pairwise distances between points and medoids.
Args:
data_matrix: Input dataset.
medoids: Indices of current medoids.
Returns:
ndarray: Distance matrix of shape (n_samples, n_clusters).
>>> x = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])
>>> d = _compute_distances(x, np.array([0, 2]))
>>> d.shape
(3, 2)
"""
return np.linalg.norm(data_matrix[:, np.newaxis] - data_matrix[medoids], axis=2)
def _assign_clusters(distances: ndarray) -> ndarray:
"""
Assign each data point to the nearest medoid.
Args:
distances: Pairwise distance matrix.
Returns:
ndarray: Cluster assignments.
>>> d = np.array([[0.1, 0.4], [0.2, 0.3], [0.9, 0.1]])
>>> _assign_clusters(d)
array([0, 0, 1])
"""
return np.argmin(distances, axis=1).astype(int)
def _initialize_medoids(
n_samples: int, n_clusters: int, random_state: int | None = None
) -> ndarray:
"""
Randomly select initial medoids.
Args:
n_samples: Total number of samples.
n_clusters: Number of clusters.
random_state: Optional random seed.
Returns:
ndarray: Indices of initial medoids.
>>> np.random.seed(42)
>>> _initialize_medoids(10, 3).shape
(3,)
"""
rng = np.random.default_rng(random_state)
return rng.choice(n_samples, n_clusters, replace=False)
def _update_medoids(
data_matrix: ndarray, clusters: ndarray, n_clusters: int
) -> ndarray:
"""
Update medoids by minimizing intra-cluster distances.
Args:
data_matrix: Dataset.
clusters: Cluster assignments.
n_clusters: Number of clusters.
Returns:
ndarray: Updated medoid indices.
>>> x = np.array([[0.0, 0.0], [1.0, 0.0], [5.0, 0.0]])
>>> clusters = np.array([0, 0, 1])
>>> _update_medoids(x, clusters, 2).shape
(2,)
"""
new_medoids = np.zeros(n_clusters, dtype=int)
for k in range(n_clusters):
cluster_points = np.where(clusters == k)[0]
if len(cluster_points) == 0:
continue
intra_distances = np.sum(
np.linalg.norm(
data_matrix[cluster_points][:, np.newaxis]
- data_matrix[cluster_points],
axis=2,
),
axis=1,
)
new_medoids[k] = cluster_points[np.argmin(intra_distances)]
return new_medoids
def apply_k_medoids(
data_matrix: ndarray,
n_clusters: int = 3,
max_iter: int = 100,
random_state: int | None = None,
) -> tuple[ndarray, ndarray]:
"""
Apply k-Medoids clustering to a dataset.
Args:
data_matrix: Input dataset.
n_clusters: Number of clusters.
max_iter: Maximum iterations.
random_state: Optional random seed.
Returns:
tuple[ndarray, ndarray]: Final medoids and cluster assignments.
>>> features, _ = _get_data()
>>> medoids, clusters = apply_k_medoids(features, n_clusters=3, max_iter=10)
>>> len(medoids)
3
"""
if n_clusters < 1 or max_iter < 1:
raise ValueError("n_clusters and max_iter must be >= 1")
n_samples = data_matrix.shape[0]
medoids = _initialize_medoids(n_samples, n_clusters, random_state)
for _ in range(max_iter):
distances = _compute_distances(data_matrix, medoids)
clusters = _assign_clusters(distances)
new_medoids = _update_medoids(data_matrix, clusters, n_clusters)
if np.array_equal(medoids, new_medoids):
break
medoids = new_medoids
return medoids, clusters
def main() -> None:
"""
Run k-Medoids on the Iris dataset and display results.
>>> main() # doctest: +ELLIPSIS
k-Medoids clustering (first 10 assignments):
[...]
"""
features, _ = _get_data()
_, clusters = apply_k_medoids(features, n_clusters=3, max_iter=50, random_state=42)
if not isinstance(clusters, np.ndarray):
raise TypeError("Cluster assignments must be an ndarray")
print("k-Medoids clustering (first 10 assignments):")
print(clusters[:10])
# Optional visualization
# import matplotlib.pyplot as plt
# plt.scatter(features[:, 0], features[:, 1], c=clusters, cmap="viridis", s=30)
# plt.scatter(
# features[medoids, 0],
# features[medoids, 1],
# c="red",
# marker="x",
# s=100,
# )
# plt.title("k-Medoids Clustering (Iris Dataset)")
# plt.xlabel("Feature 1")
# plt.ylabel("Feature 2")
# plt.show()
if __name__ == "__main__":
doctest.testmod()
main()