Skip to content

Commit fd03b1a

Browse files
committed
Rephrasing in cluster_kmeans_sol_01.py
1 parent 7374ae5 commit fd03b1a

1 file changed

Lines changed: 73 additions & 64 deletions

File tree

python_scripts/clustering_kmeans_sol_01.py

Lines changed: 73 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
data
2929

3030
# %% [markdown]
31-
# We can explore the data using a seborn `pairplot`.
31+
# We can explore the data using a seaborn `pairplot`.
3232

3333
# %%
3434
import seaborn as sns
@@ -52,7 +52,7 @@
5252
n_clusters_values = [2, 3, 4]
5353

5454
for n_clusters in n_clusters_values:
55-
model = KMeans(n_clusters=n_clusters, random_state=0)
55+
model = KMeans(n_clusters=n_clusters)
5656
clustered_data = data.copy()
5757
clustered_data["cluster label"] = model.fit_predict(data)
5858
sns.pairplot(clustered_data, hue="cluster label", palette="tab10")
@@ -127,27 +127,34 @@ def plot_n_clusters_scores(
127127
from sklearn.pipeline import make_pipeline
128128
from sklearn.preprocessing import StandardScaler
129129

130-
model = make_pipeline(StandardScaler(), KMeans(random_state=0))
130+
model = make_pipeline(StandardScaler(), KMeans())
131131
model
132132

133133
# %%
134134
# solution
135135
plot_n_clusters_scores(model, data, score_type="inertia")
136136

137+
# %% [markdown] tags=["solution"]
138+
#
139+
# The WCSS plot no strong elbow but it might depend of the random
140+
# initialization of the centroids in k-means.
141+
137142
# %% [markdown]
138-
# Let's check if the best choice of n_clusters remains stable when resampling
139-
# the dataset. For such purpose:
140-
# - Keep a fixed `random_state` for the `KMeans` step to isolate the effect of
141-
# data resampling.
142-
# - Generate resamplings consisting of 50% of the data by using
143+
# Let's if we can find one or more stable candidates for `n_clusters` using the
144+
# elbow method when resampling the dataset. For such purpose:
145+
# - Generate randomly resampled data consisting of 50% of the data by using
143146
# `train_test_split` with `train_size=0.5`. Changing the `random_state`
144-
# to do the split leads to different resamplings.
147+
# to do the split leads to different samples.
145148
# - Use the `plot_n_clusters_scores` function inside a `for` loop to make
146149
# multiple overlapping plots of the inertia, each time using a different
147-
# resampling. 10 resamplings should be enough to draw conclusions.
150+
# resampling. 10 resampling iterations should be enough to draw conclusions.
151+
# - You can choose to set the `random_state` value of the `KMeans` step, but be
152+
# aware that even if we fix `random_state=0` in all resampling iterations,
153+
# k-means will still choose different initial centroids for different data
154+
# samples, so fixing it or not should not change the conclusions w.r.t. to
155+
# stability to resampling.
148156
#
149-
# Is the elbow (optimal number of clusters) stable across all different
150-
# resamplings?
157+
# Is the elbow (optimal number of clusters) stable when resampling?
151158

152159
# %%
153160
# solution
@@ -185,9 +192,9 @@ def plot_n_clusters_scores(
185192
# data is unevenly distributed) where increasing `n_init` may help ensuring a
186193
# global minimal inertia.
187194
#
188-
# Repeat the previous example but setting `n_init=5`. Remeber to fix the
195+
# Repeat the previous example but setting `n_init=5`. Remember to fix the
189196
# `random_state` for the `KMeans` initialization to only estimate the
190-
# variability related to resamplings of the data. Are the resulting inertia
197+
# variability related to the resampling of the data. Are the resulting inertia
191198
# curves more stable?
192199

193200
# %%
@@ -244,11 +251,19 @@ def plot_n_clusters_scores(
244251
# pipeline.
245252

246253
# %% [markdown]
254+
#
247255
# Once again repeat the experiment to determine the stability of the optimal
248256
# number of clusters. This time, instead of using a `StandardScaler`, use a
249-
# `QuantileTransformer` with default parameters as the preprocessing step in the
250-
# pipeline. For the `KMeans` step, keep `n_init=5` and a fixed `random_state`.
251-
# What happens in terms of silhouette score?
257+
# `QuantileTransformer` with default parameters as the preprocessing step in
258+
# the pipeline. Contrary to `StandardScaler`, `QuantileTransformer` is a
259+
# nonlinear transformation that maps the features with a long tail
260+
# distributions to a uniform distribution, which is the case for the
261+
# "frequency" and "monetary" features in the RFM dataset.
262+
#
263+
# For the `KMeans` step, keep `n_init=5`.
264+
#
265+
# What happens in terms of silhouette score? Does this make it possible to
266+
# identify stable and qualitatively interesting clusters in this data?
252267

253268
# %%
254269
from sklearn.preprocessing import QuantileTransformer
@@ -270,84 +285,78 @@ def plot_n_clusters_scores(
270285
)
271286

272287
# %% [markdown] tags=["solution"]
273-
# The silhouette score is much more stable across resamplings. Moreover, the
274-
# optimal number of clusters seems to be 2, as it provides the highest score,
275-
# indicating that the data points are well-separated and correctly grouped with
276-
# good cohesion. However 4 or 6 clusters may still make sense if the clustering
277-
# has specific use cases or domain relevance.
288+
#
289+
# The silhouette score is a bit more stable under resampling. Moreover, the
290+
# optimal number of clusters seems to be 2, as it provides the highest score.
291+
# However 4 or 6 clusters may still make sense if the clustering has specific
292+
# use cases or domain relevance.
278293
#
279294
# Notice that you should still be cautious as the relatively low values of the
280-
# silhouette scores suggest that some points may be "misassigned". To verify
281-
# this, we can plot the labels when setting `n_clusters=6`.
295+
# silhouette scores suggest that clusters are not well separated or not dense
296+
# enough. To verify this, we can plot the labels when setting `n_clusters=6`.
282297

283298
# %% tags=["solution"]
284299
n_clusters = 6
285300
model = make_pipeline(
286301
QuantileTransformer(),
287302
KMeans(n_init=5, n_clusters=n_clusters, random_state=0),
288-
)
303+
).set_output(transform="pandas")
289304
model
290305

291306
# %% tags=["solution"]
292-
clustered_data = data.copy()
293-
clustered_data["cluster label"] = model.fit_predict(data)
307+
cluster_labels = model.fit_predict(data)
294308

295-
sns.pairplot(clustered_data, hue="cluster label", palette="tab10")
296-
plt.title(f"n_clusters={n_clusters}")
309+
_ = sns.pairplot(
310+
model[:-1].transform(data).assign(cluster_labels=cluster_labels),
311+
hue="cluster_labels",
312+
palette="tab10",
313+
)
297314

298315
# %% [markdown] tags=["solution"]
299-
# Indeed, "monetary" exactly equal to 0 is divided into 2 clusters (the plot in
300-
# the middle of the `pairplot`), whereas it would feel more reasonably to have
301-
# those points form a single cluster.
302316
#
303-
# Alternatively, we can plot the labels in the transformed space to better
304-
# observe that some data points seem to be misassigned.
317+
# Since we have 3 dimensions, we can try to visualize the cluster labels using
318+
# a 3D projection directly:
305319

306320
# %% tags=["solution"]
307321
from matplotlib.colors import ListedColormap
308322

309-
cluster_labels = model.fit_predict(data)
310-
cmap = ListedColormap(plt.get_cmap("tab10").colors[:n_clusters])
311-
312323
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": "3d"})
324+
ax.view_init(azim=80)
325+
326+
cmap = ListedColormap(plt.get_cmap("tab10").colors[:n_clusters])
313327
scatter = ax.scatter(
314-
*model[:-1].transform(data).T,
328+
*model[:-1].transform(data).values.T,
315329
c=cluster_labels,
316330
cmap=cmap,
317331
s=50,
318332
alpha=0.7,
319333
)
320334
ax.set_box_aspect(None, zoom=0.84)
321-
ax.set_xlabel("Transformed Monetary", labelpad=15)
322-
ax.set_ylabel("Transformed Frequency", labelpad=15)
323-
ax.set_zlabel("Transformed Recency", labelpad=15)
335+
ax.set_xlabel(f"Transformed {data.columns[0]}", labelpad=15)
336+
ax.set_ylabel(f"Transformed {data.columns[1]}", labelpad=15)
337+
ax.set_zlabel(f"Transformed {data.columns[2]}", labelpad=15)
324338
ax.set_title("Clusters in quantile-transformed space", y=0.99)
325339
_ = plt.tight_layout()
326340

327341
# %% [markdown] tags=["solution"]
328-
# Observe that the clusters at "transformed monetary" exactly equals zero are
329-
# grouped together with data points with non-zero "transformed monetary" that
330-
# would more naturally belong to other clusters. What happens here is that data
331-
# points at "transformed monetary" equals zero lie on a flat region. i.e. they
332-
# vary only in "transformed recency" and "transformed frequency".
333342
#
334-
# Remember that k-means consists of minimizing the squared distance from each
335-
# point to its assigned centroid. This makes it more suited to data where
336-
# clusters are roughly spherical and evenly distributed in all directions of the
337-
# feature space.
338-
339-
# When the data doesn't have this kind of structure.k-means may not perform
340-
# well. In such cases, we can consider:
343+
# The general impression from this study is that k-means fails to find
344+
# well-separated clusters in this dataset regardless of the preprocessing
345+
# method used.
346+
#
347+
# We can observe that the quantile-transformed data has a structure of layered
348+
# planes because of the discrete integer levels for the lowest values of the
349+
# "Frequency" feature which are overrepresented in the dataset.
341350
#
342-
# - using other clustering algorithms that handle more complex shapes, such as
343-
# HDBSCAN (which we will cover in a future notebook) or Gaussian Mixture
344-
# Models (GMMs);
345-
# - focusing on a subset of features where the cluster structure is clearer, as
346-
# we did by separating penguins into 6 groups (3 species × 2 sexes) in a
347-
# previous notebook;
348-
# - Or even applying k-means with a larger number of clusters, even if they are
349-
# not interpretable, and use the distance to centroids as preprocessing for
350-
# another task.
351+
# One could try more advanced kinds of preprocessing, or even, clustering
352+
# algorithms that favor different kinds of shapes, however, by looking at the
353+
# pairplot above we can draw the following conclusions:
354+
# - The discrete layers visible for the lowest values of the "frequency"
355+
# feature are not that interesting to treat as clusters by themselves because
356+
# they do not relate to visible structure involving the other two features;
357+
# - If we ignore the "frequency" feature, we can observe no significant cluster
358+
# structure in the "recency" and "monetary" 2D space.
351359
#
352-
# It all depends on the specific application domain and the downstream use of
353-
# the resulting clusters.
360+
# In conclusion, the data does not have a clear cluster structure, and this
361+
# explains why we could not find strong and stable values for the silhouette
362+
# score under resampling.

0 commit comments

Comments
 (0)