Unsupervised clustering groups rows when no target labels exist yet. KMeans in scikit-learn fits a fixed number of centroid-based clusters, which makes it a quick way to smoke-test whether numeric rows separate into recognizable segments.
The estimator stores the fitted row assignments in labels_, the learned centroids in cluster_centers_, and the within-cluster sum of squared distances in inertia_. Setting random_state and n_init keeps the small check reproducible while still letting KMeans try multiple centroid seeds.
Cluster numbers are identifiers, not class names. Scale numeric columns before fitting when the units differ, inspect the centers before naming the groups, and treat inertia as a comparison signal between related runs rather than a standalone score.
import numpy as np import sklearn from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler customer_names = np.array( [ "trial-01", "trial-02", "trial-03", "steady-01", "steady-02", "steady-03", "premium-01", "premium-02", "premium-03", ] ) features = np.array( [ [1.2, 18.0], [1.8, 22.0], [2.0, 24.0], [5.8, 47.0], [6.4, 52.0], [6.9, 49.0], [10.2, 85.0], [10.8, 91.0], [11.4, 88.0], ], dtype=float, ) scaler = StandardScaler() features_scaled = scaler.fit_transform(features) kmeans = KMeans(n_clusters=3, n_init=10, random_state=42) labels = kmeans.fit_predict(features_scaled) centers = scaler.inverse_transform(kmeans.cluster_centers_) print(f"scikit-learn {sklearn.__version__}") print("Cluster assignments:") for customer, label in zip(customer_names, labels): print(f"- {customer}: cluster {label}") print("\nCluster centers in original units:") for cluster_id, center in enumerate(centers): visits, spend = center print(f"- cluster {cluster_id}: visits={visits:.1f}, avg_order=${spend:.2f}") print(f"\nInertia: {kmeans.inertia_:.3f}") print(f"Iterations: {kmeans.n_iter_}") new_rows = np.array( [ [2.3, 23.0], [10.5, 89.0], ], dtype=float, ) new_labels = kmeans.predict(scaler.transform(new_rows)) print("\nNew row clusters:") for row, label in zip(new_rows, new_labels): visits, spend = row print(f"- visits={visits:.1f}, avg_order=${spend:.2f}: cluster {label}")
The sample uses two numeric features, monthly visits and average order value. StandardScaler keeps both columns on comparable scale before KMeans uses Euclidean distance.
$ python3 cluster_kmeans.py scikit-learn 1.9.0 Cluster assignments: - trial-01: cluster 2 - trial-02: cluster 2 - trial-03: cluster 2 - steady-01: cluster 0 - steady-02: cluster 0 - steady-03: cluster 0 - premium-01: cluster 1 - premium-02: cluster 1 - premium-03: cluster 1 Cluster centers in original units: - cluster 0: visits=6.4, avg_order=$49.33 - cluster 1: visits=10.8, avg_order=$88.00 - cluster 2: visits=1.7, avg_order=$21.33 Inertia: 0.184 Iterations: 2 New row clusters: - visits=2.3, avg_order=$23.00: cluster 2 - visits=10.5, avg_order=$89.00: cluster 1
Cluster assignments: - trial-01: cluster 2 - trial-02: cluster 2 - trial-03: cluster 2 - steady-01: cluster 0 - steady-02: cluster 0 - steady-03: cluster 0 - premium-01: cluster 1 - premium-02: cluster 1 - premium-03: cluster 1
The cluster IDs can change when the data, seed, or number of clusters changes. Name segments from their centers and source rows, not from the numeric label alone.
Cluster centers in original units: - cluster 0: visits=6.4, avg_order=$49.33 - cluster 1: visits=10.8, avg_order=$88.00 - cluster 2: visits=1.7, avg_order=$21.33 Inertia: 0.184 Iterations: 2
The centers were transformed back to the original feature units for readability. Inertia is computed in the scaled feature space, so compare it only with related KMeans runs that use the same preprocessing.
New row clusters: - visits=2.3, avg_order=$23.00: cluster 2 - visits=10.5, avg_order=$89.00: cluster 1
Call predict() on rows transformed by the same scaler that was fitted before KMeans. Refitting the scaler or the clusterer on incoming rows changes the cluster boundary.
$ rm cluster_kmeans.py