{"id":"l-ml-5-1","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Group customers with k-means, profile the clusters to see whether they mean anything, and compress many correlated columns into a few with PCA.","html":"<p>Not every question comes with labels. Cartwheel’s marketing team asks: “What kinds of customers do we have?” Nobody has tagged customers with types, so there’s no target to predict. <strong>Unsupervised learning</strong> looks for structure in the features alone. The two tools you’ll use most are clustering, which groups similar rows, and dimensionality reduction, which summarises many columns with a few.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">k-means in two moves</h2>\n<p>k-means needs one decision from you: the number of clusters, k. Then it repeats two steps:</p>\n<ol>\n<li><strong>Assign</strong>: put each customer in the cluster whose centre is nearest.</li>\n<li><strong>Update</strong>: move each centre to the average of the customers assigned to it.</li>\n</ol>\n<p>When assignments stop changing, it’s done. Different starting centres can give different results, so scikit-learn runs the whole thing <code dir=\"ltr\">n_init</code> times and keeps the tightest solution.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 240\" role=\"img\" aria-labelledby=\"l-ml-5-1-t1\"><defs><marker id=\"arrow-l-ml-5-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-5-1-t1\">Left panel: points coloured by their nearest of two centres, shown as large rings. Right panel: each centre has moved to the middle of its assigned points. The two steps repeat until the assignments stop changing.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"20\" width=\"300\" height=\"180\" rx=\"10\"/>\n  <rect class=\"d-box\" x=\"360\" y=\"20\" width=\"300\" height=\"180\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"170\" y=\"225\" text-anchor=\"middle\">1. assign to nearest centre</text>\n  <text class=\"d-label-strong\" x=\"510\" y=\"225\" text-anchor=\"middle\">2. move centre to the mean</text>\n  <circle class=\"d-dot\" cx=\"60\" cy=\"60\" r=\"5\"/><circle class=\"d-dot\" cx=\"85\" cy=\"80\" r=\"5\"/><circle class=\"d-dot\" cx=\"70\" cy=\"110\" r=\"5\"/><circle class=\"d-dot\" cx=\"105\" cy=\"65\" r=\"5\"/>\n  <rect class=\"d-box-accent\" x=\"215\" y=\"120\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"245\" y=\"150\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"270\" y=\"125\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"230\" y=\"170\" width=\"10\" height=\"10\"/>\n  <circle class=\"d-box-primary\" cx=\"140\" cy=\"110\" r=\"14\"/>\n  <circle class=\"d-box-accent\" cx=\"200\" cy=\"110\" r=\"14\"/>\n  <circle class=\"d-dot\" cx=\"400\" cy=\"60\" r=\"5\"/><circle class=\"d-dot\" cx=\"425\" cy=\"80\" r=\"5\"/><circle class=\"d-dot\" cx=\"410\" cy=\"110\" r=\"5\"/><circle class=\"d-dot\" cx=\"445\" cy=\"65\" r=\"5\"/>\n  <rect class=\"d-box-accent\" x=\"555\" y=\"120\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"585\" y=\"150\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"610\" y=\"125\" width=\"10\" height=\"10\"/><rect class=\"d-box-accent\" x=\"570\" y=\"170\" width=\"10\" height=\"10\"/>\n  <circle class=\"d-box-primary\" cx=\"420\" cy=\"79\" r=\"14\"/>\n  <circle class=\"d-box-accent\" cx=\"585\" cy=\"146\" r=\"14\"/>\n  <path class=\"d-arrow\" d=\"M330 110 L350 110\" marker-end=\"url(#arrow-l-ml-5-1-1)\"/>\n</svg><figcaption>One round of k-means</figcaption></figure>\n<p>Because “nearest” is a distance, the same lesson from k-nearest neighbours applies: scale first, or <code dir=\"ltr\">total_spent</code> in thousands decides everything.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Segmenting Cartwheel’s customers</h2>\n<p>Use behaviour columns only and leave <code dir=\"ltr\">churned</code> out: a segmentation should describe customers, not their outcome. You’ll look at churn afterwards to see whether the segments mean anything.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.cluster </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> KMeans</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.metrics </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> silhouette_score</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.pipeline </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StandardScaler</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">behaviour </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_order_value\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">             \"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StandardScaler().fit_transform(churn[behaviour])</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> k </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> range</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">7</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    labels </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> KMeans(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_clusters</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">k, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_init</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit_predict(X)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"k=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">k</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  silhouette </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">silhouette_score(X, labels)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">segments </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), KMeans(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_clusters</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_init</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"segment_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> segments.fit_predict(churn[behaviour])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">profile </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> churn.groupby(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"segment_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).agg(</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    customers</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"size\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#953800;--shiki-dark:#FFA657\">orders</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mean\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    avg_order</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_order_value\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mean\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#953800;--shiki-dark:#FFA657\">days_since</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mean\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    tenure</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mean\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#953800;--shiki-dark:#FFA657\">churn_rate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mean\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(profile.round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>First, the honest part. The <strong>silhouette score</strong> measures how much closer each customer is to its own cluster than to the next one, from -1 to 1. Scores around 0.25 for every k mean the customers don’t fall into crisp natural groups; they form a continuum. That’s typical for behavioural data. It doesn’t make clustering useless, but it means k is your choice, made for usefulness, not a fact you discovered.</p>\n<p>Then the useful part. Profiling the four clusters gives segments a marketer can name:</p>\n<ul>\n<li><strong>Lapsed occasionals</strong> (129): two orders, last one about 310 days ago. 79% churn.</li>\n<li><strong>Big-ticket buyers</strong> (41): few orders averaging $600. 66% churn.</li>\n<li><strong>Regulars</strong> (79): eight orders, the last about 100 days ago. 33% churn.</li>\n<li><strong>New and recent</strong> (149): short tenure, ordered about two months ago. 44% churn.</li>\n</ul>\n<p>Churn rates differ sharply between segments even though k-means never saw <code dir=\"ltr\">churned</code>. That’s evidence the segments capture something real, and it gives marketing four different conversations to have instead of one.</p>\n<p>k-means isn’t the only option. It assumes roughly round clusters of similar size, an assumption that is usually good enough for behavioural segments. Hierarchical clustering (<code dir=\"ltr\">AgglomerativeClustering</code>) shows how groups merge at different levels, and <code dir=\"ltr\">DBSCAN</code> finds dense regions of any shape and labels outliers as noise. Start with k-means for segmentation; switch when its assumptions clearly don’t fit your data.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Naming clusters before profiling them</span></p>\n<p>k-means always returns k groups, even from random noise. Cluster numbers have no meaning, and they can change between runs. Before you call a cluster “VIPs”, print its averages and sizes, check it’s stable across a couple of <code dir=\"ltr\">random_state</code> values, and confirm it matters for something the business cares about.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">PCA: fewer columns, most of the information</h2>\n<p>Many datasets have dozens of correlated columns. The breast cancer data has 30 measurements of cell nuclei, many of them variations on size. <strong>Principal component analysis</strong> finds new axes, each a weighted mix of the original columns, ordered by how much of the data’s variation they capture. Keep the first few and you have a compact summary.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> numpy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_breast_cancer</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.decomposition </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> PCA</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.pipeline </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StandardScaler</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_breast_cancer(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pca </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), PCA()).fit(X)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">ratios </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pca[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].explained_variance_ratio_</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"first two components keep:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, ratios[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"=\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(ratios[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"components needed for 90%:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(np.argmax(np.cumsum(ratios) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.90</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">coords </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), PCA(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_components</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).fit_transform(X)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> label, name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"malignant\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"benign\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)]:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(name, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"average position:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, coords[y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> label].mean(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">axis</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>Two components keep 63% of the variation in 30 scaled columns, and seven keep 90%. Plot the two coordinates and malignant and benign tumours sit on opposite sides of the first axis, even though PCA never saw the labels. That’s why a 2D PCA plot is a standard first look at any wide dataset.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>When PCA helps a model</span></p>\n<p>PCA can also be a pipeline step before a model, to cut noise or speed up training on very wide data. On tables with a dozen meaningful columns, like churn, it usually costs interpretability and gains nothing. Use it for exploration first; add it to a model only when cross-validation says it helps.</p>\n</aside>\n<p>Both techniques here are classic and cheap. The next lesson is about models that learn their own features from raw inputs: neural networks.</p>\n","toc":[{"id":"sec-1","text":"k-means in two moves","level":2},{"id":"sec-2","text":"Segmenting Cartwheel’s customers","level":2},{"id":"sec-3","text":"PCA: fewer columns, most of the information","level":2}],"takeaways":["k-means alternates two steps, assign each point to its nearest centre and move each centre to the mean of its points, until nothing changes.","Clustering uses distances, so scale features first, and set <code dir=\"ltr\">n_init</code> and <code dir=\"ltr\">random_state</code> for repeatable results.","Clusters are only useful if you can describe them; profile each one with group means before giving it a name.","A low silhouette score means the data has no crisp natural groups; segments can still be useful, but they’re a choice, not a discovery.","PCA finds new axes that capture the most variance; <code dir=\"ltr\">explained_variance_ratio_</code> tells you how much each component keeps."],"further":[{"title":"Clustering: K-means","url":"https://scikit-learn.org/stable/modules/clustering.html#k-means"},{"title":"KMeans","url":"https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html"},{"title":"Principal component analysis (PCA)","url":"https://scikit-learn.org/stable/modules/decomposition.html#pca"}],"quiz":[{"q":"<p>You run k-means on raw churn columns without scaling. Which column will dominate the clusters?</p>","options":[{"text":"<code dir=\"ltr\">used_coupon</code>, because binary columns separate groups cleanly.","why":"A 0/1 column contributes at most 1 to a squared distance, which is nothing next to dollar amounts."},{"text":"<code dir=\"ltr\">days_since_last_order</code>, because it’s the best churn predictor.","why":"k-means never sees the target; it only sees distances, which are driven by scale."},{"text":"All columns contribute equally, because k-means normalises internally.","why":"KMeans does no scaling of its own; you have to add a <code dir=\"ltr\">StandardScaler</code>."},{"text":"<code dir=\"ltr\">total_spent</code>, because its values in the thousands swamp every other distance.","why":"Correct. Unscaled, k-means mostly clusters customers by spend."}],"answer":3},{"q":"<p>Silhouette scores for k = 2 to 7 all sit between 0.25 and 0.27. What does that tell you?</p>","options":[{"text":"The data has no strong natural grouping, so pick k for usefulness and explainability.","why":"Correct. Flat, low scores mean no k stands out; the segmentation is a modelling choice."},{"text":"k = 7 is best because it has the highest score.","why":"Differences this small are noise, and seven segments are harder for a team to use than four."},{"text":"k-means has failed and must be replaced by a supervised model.","why":"Weak cluster structure doesn’t make clustering useless, and a supervised model needs labels you don’t have for segments."}],"answer":0},{"q":"<p>PCA on the 30 breast cancer features gives <code dir=\"ltr\">explained_variance_ratio_</code> of 0.44 and 0.19 for the first two components. What does a 2D plot of them show?</p>","options":[{"text":"The two most important original features.","why":"Components are weighted combinations of all 30 features, not two of the originals."},{"text":"A view that keeps about 63% of the variation in the scaled data.","why":"Correct. It’s a useful map, but over a third of the variation is not shown."},{"text":"The two classes, perfectly separated, because PCA uses the labels.","why":"PCA is unsupervised; it never sees the labels. Any separation you see comes from the data’s structure."}],"answer":1}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import numpy as np\nimport pandas as pd\nfrom sklearn.cluster import KMeans\nfrom sklearn.datasets import load_wine\nfrom sklearn.decomposition import PCA\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nbehaviour = [\"orders\", \"total_spent\", \"avg_order_value\", \"days_since_last_order\",\n             \"tenure_days\", \"mobile_share\"]\n\n# 1. Segment customers into 4 clusters: StandardScaler, then KMeans(n_clusters=4, n_init=10, random_state=42)\nsegments = make_pipeline(KMeans(n_clusters=4, n_init=10, random_state=42))\nlabels = segments.fit_predict(churn[behaviour])\n\n# 2. Churn rate per cluster (a Series indexed by cluster label), and the highest rate\nchurn_by_cluster = None\nhighest_rate = None\n\n# 3. PCA on the wine data (13 scaled features): share of variance kept by the first 2 components\nX_wine, y_wine = load_wine(return_X_y=True)\nwine_var_2 = None\n","solution":"import numpy as np\nimport pandas as pd\nfrom sklearn.cluster import KMeans\nfrom sklearn.datasets import load_wine\nfrom sklearn.decomposition import PCA\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nbehaviour = [\"orders\", \"total_spent\", \"avg_order_value\", \"days_since_last_order\",\n             \"tenure_days\", \"mobile_share\"]\n\nsegments = make_pipeline(StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=42))\nlabels = segments.fit_predict(churn[behaviour])\n\nchurn_by_cluster = churn.groupby(labels)[\"churned\"].mean()\nhighest_rate = churn_by_cluster.max()\n\nX_wine, y_wine = load_wine(return_X_y=True)\npca = make_pipeline(StandardScaler(), PCA(n_components=2)).fit(X_wine)\nwine_var_2 = pca[-1].explained_variance_ratio_.sum()\nprint(churn_by_cluster.round(2).to_dict(), round(wine_var_2, 3))\n","checks":[{"id":"scaled","test":"[type(s).__name__ for s in segments] == ['StandardScaler', 'KMeans']"},{"id":"clusters","test":"labels is not None and len(labels) == 398 and len(set(labels)) == 4 and sorted(__import__('collections').Counter(labels).values()) == sorted(__import__('collections').Counter(__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.cluster', fromlist=['x']).KMeans(n_clusters=4, n_init=10, random_state=42)).fit_predict(churn[behaviour])).values()) and min(__import__('collections').Counter(labels).values()) > 20"},{"id":"churn-rates","test":"churn_by_cluster is not None and len(churn_by_cluster) == 4 and sorted(float(v) for v in churn_by_cluster) == sorted(float(v) for v in churn.groupby(labels)['churned'].mean()) and abs(float(highest_rate) - float(max(churn_by_cluster))) < 1e-9 and 0.7 < float(highest_rate) < 0.9"},{"id":"pca","test":"wine_var_2 is not None and abs(float(wine_var_2) - 0.554) < 0.005"}],"title":"Segments and a compressed view","prompt":"<ol>\n<li>Fix <code dir=\"ltr\">segments</code> so the customers are scaled before k-means (4 clusters, <code dir=\"ltr\">n_init=10</code>, <code dir=\"ltr\">random_state=42</code>), and get one label per customer in <code dir=\"ltr\">labels</code>.</li>\n<li><code dir=\"ltr\">churn_by_cluster</code>: the churn rate of each cluster, as a Series indexed by cluster label. <code dir=\"ltr\">highest_rate</code>: the largest of those rates.</li>\n<li><code dir=\"ltr\">wine_var_2</code>: the share of variance that the first two principal components keep for the wine dataset’s 13 features, after standard scaling.</li>\n</ol>\n","hints":["Put <code dir=\"ltr\">StandardScaler()</code> before <code dir=\"ltr\">KMeans(...)</code> in <code dir=\"ltr\">make_pipeline</code>.","<code dir=\"ltr\">churn.groupby(labels)[&quot;churned&quot;].mean()</code> groups by the label array directly.","Fit <code dir=\"ltr\">make_pipeline(StandardScaler(), PCA(n_components=2))</code> on <code dir=\"ltr\">X_wine</code> and sum <code dir=\"ltr\">pipe[-1].explained_variance_ratio_</code>."],"explanation":"<p>Scaled k-means finds the four segments from the lesson (129, 41, 79 and 149 customers), and the lapsed occasionals churn at 79% even though the target was never used for clustering. Unscaled, <code dir=\"ltr\">total_spent</code> would have dominated and the segments would mostly sort customers by spend. On the wine data, two components keep about 55% of the variance in 13 measurements: enough for a useful map, not enough to replace the original columns.</p>\n","checkLabels":{"scaled":"segments scales before clustering","clusters":"labels hold the four expected segments","churn-rates":"churn_by_cluster and highest_rate are correct","pca":"wine_var_2 is the variance kept by two components"}},"words":671,"readMinutes":3}