2828data
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# %%
3434import seaborn as sns
5252n_clusters_values = [2 , 3 , 4 ]
5353
5454for 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(
127127from sklearn .pipeline import make_pipeline
128128from sklearn .preprocessing import StandardScaler
129129
130- model = make_pipeline (StandardScaler (), KMeans (random_state = 0 ))
130+ model = make_pipeline (StandardScaler (), KMeans ())
131131model
132132
133133# %%
134134# solution
135135plot_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# %%
254269from 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"]
284299n_clusters = 6
285300model = make_pipeline (
286301 QuantileTransformer (),
287302 KMeans (n_init = 5 , n_clusters = n_clusters , random_state = 0 ),
288- )
303+ ). set_output ( transform = "pandas" )
289304model
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"]
307321from matplotlib .colors import ListedColormap
308322
309- cluster_labels = model .fit_predict (data )
310- cmap = ListedColormap (plt .get_cmap ("tab10" ).colors [:n_clusters ])
311-
312323fig , 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 ])
313327scatter = 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)
320334ax .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 )
324338ax .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