{"id":"l-ml-1-2","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Turn the churn table into a feature matrix X and a target y, then split it with train_test_split so you can measure a model on customers it has never seen.","html":"<p>A model that scores 99% is either very good or has seen the answers. You can’t tell which from the number alone. The single most useful habit in machine learning is setting aside data the model never sees during training, so that its score on that data means something.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The workflow, start to finish</h2>\n<p>Every supervised project you’ll work on follows roughly the same loop. The steps are boring on purpose; skipping one is how projects fail.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 200\" role=\"img\" aria-labelledby=\"l-ml-1-2-t1\"><defs><marker id=\"arrow-l-ml-1-2-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-1-2-t1\">Seven steps in a loop: frame the question, collect labelled data, split, baseline, train, evaluate, then ship and monitor, with an arrow from evaluate back to train for iteration.</title>\n  <rect class=\"d-box-primary\" x=\"10\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"53\" y=\"63\" text-anchor=\"middle\">Frame</text>\n  <rect class=\"d-box\" x=\"108\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"151\" y=\"63\" text-anchor=\"middle\">Collect</text>\n  <rect class=\"d-box-warn\" x=\"206\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"249\" y=\"63\" text-anchor=\"middle\">Split</text>\n  <rect class=\"d-box\" x=\"304\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"347\" y=\"63\" text-anchor=\"middle\">Baseline</text>\n  <rect class=\"d-box\" x=\"402\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"445\" y=\"63\" text-anchor=\"middle\">Train</text>\n  <rect class=\"d-box\" x=\"500\" y=\"30\" width=\"86\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"543\" y=\"63\" text-anchor=\"middle\">Evaluate</text>\n  <rect class=\"d-box-success\" x=\"598\" y=\"30\" width=\"92\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"644\" y=\"63\" text-anchor=\"middle\">Ship</text>\n  <path class=\"d-arrow\" d=\"M96 58 L106 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M194 58 L204 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M292 58 L302 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 58 L400 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M488 58 L498 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M586 58 L596 58\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M543 90 C543 150 445 150 445 92\" marker-end=\"url(#arrow-l-ml-1-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"494\" y=\"165\" text-anchor=\"middle\">iterate</text>\n</svg><figcaption>The supervised learning workflow</figcaption></figure>\n<p><strong>Frame</strong> the question precisely: what is predicted, for whom, at what moment, and what action follows. For Cartwheel: “On 30 September, for every customer who has ordered before, predict whether they will order in the next 90 days, so marketing can target win-back offers.” <strong>Collect</strong> examples where the answer is known: that’s <code dir=\"ltr\">churn.csv</code>. <strong>Split</strong> off a test set. Build a <strong>baseline</strong> (next lesson but one). <strong>Train</strong> models, <strong>evaluate</strong> them, iterate, and only then <strong>ship</strong> and keep watching.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Features and target</h2>\n<p>scikit-learn expects two things: a feature matrix <code dir=\"ltr\">X</code>, one row per example and one column per feature, and a target <code dir=\"ltr\">y</code> with one answer per row. A pandas DataFrame works as <code dir=\"ltr\">X</code> and a Series works as <code dir=\"ltr\">y</code>.</p>\n<p>For now you’ll use the eight numeric columns. Text columns such as <code dir=\"ltr\">country</code> need encoding first (section 4 handles that), and <code dir=\"ltr\">avg_satisfaction</code> has gaps that some models can’t accept yet.</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>\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\">features </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\">\"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>\n<span class=\"line\"><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\">\"used_coupon\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"support_tickets\"</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\"> churn[features]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</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\">(X.shape, y.shape)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(y.value_counts())</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><code dir=\"ltr\">customer_id</code> stays out. An id is an arbitrary label, and a flexible model could still find splits on it that fit the training rows by coincidence. Anything that wouldn’t make sense to a person explaining churn shouldn’t be a feature.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Holding out a test set</h2>\n<p><code dir=\"ltr\">train_test_split</code> shuffles the rows and cuts them into two parts. You train on one and measure on the other.</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\"> 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.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</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\">features </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\">\"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>\n<span class=\"line\"><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\">\"used_coupon\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"support_tickets\"</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, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> churn[features], churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</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\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    X, y, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</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 style=\"color:#953800;--shiki-dark:#FFA657\">stratify</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y</span></span>\n<span class=\"line\"><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\">\"train:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, X_train.shape, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn rate\"</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\">(y_train.mean(), </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\">\"test: \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, X_test.shape, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn rate\"</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\">(y_test.mean(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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>Three arguments do the work:</p>\n<ul>\n<li><code dir=\"ltr\">test_size=0.25</code> keeps a quarter of the rows back: 100 customers for testing, 298 for training. With a few hundred rows, 20–30% is a sensible test share; with millions, 1–5% is plenty.</li>\n<li><code dir=\"ltr\">random_state=42</code> fixes the shuffle so you get the same split every run. Without it, every rerun reshuffles and your score wobbles for reasons that have nothing to do with your model.</li>\n<li><code dir=\"ltr\">stratify=y</code> keeps the churn rate the same in both parts (55%). With 100 test rows, an unlucky shuffle could otherwise give you a test set that’s 45% or 62% churned, and every number you compute on it would shift.</li>\n</ul>\n<p>The order of the four returned values is fixed: <code dir=\"ltr\">X_train, X_test, y_train, y_test</code>. Mixing them up is a common source of nonsense scores.</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>Peeking at the test set</span></p>\n<p>The test set is only honest the first time you use it to make a decision. If you try ten ideas and keep whichever scores best on the test set, you have tuned to those 100 customers, and the score you report will be too optimistic. Make choices with the training data (section 4 shows how with cross-validation) and look at the test score once, at the end.</p>\n</aside>\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>Random splits assume the future looks like the past</span></p>\n<p>A random split mixes old and new rows. If your data changes over time, a random split lets the model peek at patterns from “later”. For forecasting-style problems, split by date: train on earlier periods, test on later ones. The leakage lesson in section 4 comes back to this.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">What the split buys you</h2>\n<p>Think of the split as a contract with your future self. Everything you do from here on (choosing features, choosing models, tuning settings) happens on the training part. The test part stays sealed until you’re ready to report a result. With <code dir=\"ltr\">X_train</code> and <code dir=\"ltr\">y_train</code> you can fit any model you like. With <code dir=\"ltr\">X_test</code> and <code dir=\"ltr\">y_test</code> you get one honest answer to “how well does this work on customers it hasn’t seen?”. That’s the number that predicts what happens when marketing uses your model in December. Next lesson you’ll fit the first model and see how big the gap between those two numbers can be.</p>\n","toc":[{"id":"sec-1","text":"The workflow, start to finish","level":2},{"id":"sec-2","text":"Features and target","level":2},{"id":"sec-3","text":"Holding out a test set","level":2},{"id":"sec-4","text":"What the split buys you","level":2}],"takeaways":["Every supervised project follows the same loop: frame, collect labelled data, split, baseline, train, evaluate, then ship and monitor.","X is a table of features with one row per example; y is the matching column of answers.","A model’s score on its own training data says how well it memorised, not how well it predicts.","Use <code dir=\"ltr\">stratify=y</code> for classification so the train and test sets keep the same class balance, and set <code dir=\"ltr\">random_state</code> so the split is repeatable.","The test set is for the final check; looking at it while you make decisions turns it into training data."],"further":[{"title":"train_test_split","url":"https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html"},{"title":"Common pitfalls and recommended practices","url":"https://scikit-learn.org/stable/common_pitfalls.html"}],"quiz":[{"q":"<p>You fit a model on all 398 churn rows and it scores 0.99 on those same rows. What can you conclude?</p>","options":[{"text":"The model will be about 99% accurate on next month’s customers.","why":"The model has seen every one of those answers. A high score on seen data is consistent with pure memorisation."},{"text":"Very little about future performance; you need rows the model never saw.","why":"Correct. Only held-out data tells you how the model generalises to new customers."},{"text":"The model is overfitting, so you should delete features until the score drops.","why":"You can’t diagnose overfitting without a held-out score to compare against, and deleting features blindly is not a fix."}],"answer":1},{"q":"<p>What does <code dir=\"ltr\">stratify=y</code> change in <code dir=\"ltr\">train_test_split(X, y, test_size=0.25, stratify=y)</code>?</p>","options":[{"text":"It sorts the rows by the target before splitting.","why":"Sorting would put all of one class in one part. Stratifying does the opposite; it keeps classes evenly spread."},{"text":"It balances the classes so each part is 50% churned.","why":"Stratifying preserves the original ratio (55% here); it does not rebalance to 50/50."},{"text":"It keeps the share of each class the same in the train and test parts.","why":"Correct. With small datasets an unlucky split can skew the test set; stratifying prevents that."}],"answer":2},{"q":"<p>Why drop <code dir=\"ltr\">customer_id</code> from the features?</p>","options":[{"text":"It is an identifier with no causal link to churn, so a model could only memorise it.","why":"Correct. Ids are arbitrary labels. A flexible model can still split on them and learn noise that won’t hold for new customers."},{"text":"scikit-learn refuses integer columns named <code dir=\"ltr\">id</code>.","why":"scikit-learn doesn’t inspect column names for meaning. It would happily use the id, which is the problem."},{"text":"It is the target, so it must not be in X.","why":"The target is <code dir=\"ltr\">churned</code>. The id is a row label, not an answer."},{"text":"Ids make training slower because they are large numbers.","why":"Large numbers don’t slow training meaningfully. The reason is about what the model can learn, not speed."}],"answer":0},{"q":"<p>You try twenty model settings, keep the one with the best test score, and report that score. What is wrong?</p>","options":[{"text":"Nothing; that is what the test set is for.","why":"The test set is for one final, unbiased estimate. Choosing among twenty options by it makes it part of training."},{"text":"Twenty settings is too few to find a good model.","why":"The number of settings isn’t the issue. Any selection based on the test set biases the reported score upward."},{"text":"The reported score is optimistic, because you picked the setting that happened to suit this test set.","why":"Correct. Use a validation split or cross-validation to choose, then touch the test set once."}],"answer":2}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nchurn = pd.read_csv(\"churn.csv\")\n\n# 1. Feature matrix: the eight numeric feature columns (no id, no target)\nX = churn.drop(columns=[\"country\", \"segment\", \"avg_satisfaction\"])\n# 2. Target\ny = churn[\"churned\"]\n\n# 3. Split: 25% test, random_state=42, keep the churn rate equal in both parts\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n","solution":"import pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nchurn = pd.read_csv(\"churn.csv\")\n\nX = churn.drop(columns=[\"customer_id\", \"country\", \"segment\", \"avg_satisfaction\", \"churned\"])\ny = churn[\"churned\"]\n\nX_train, X_test, y_train, y_test = train_test_split(\n    X, y, test_size=0.25, random_state=42, stratify=y\n)\n","checks":[{"id":"features","test":"set(X.columns) == {'tenure_days', 'orders', 'total_spent', 'avg_order_value', 'days_since_last_order', 'used_coupon', 'support_tickets', 'mobile_share'}"},{"id":"target","test":"list(y.unique()) != [] and set(y.unique()) == {0, 1} and len(y) == 398"},{"id":"sizes","test":"X_train.shape == (298, 8) and X_test.shape == (100, 8) and len(y_test) == 100"},{"id":"stratified","test":"abs(float(y_test.mean()) - float(y.mean())) < 0.01 and abs(float(y_train.mean()) - float(y.mean())) < 0.01"}],"title":"Build X and y, then split","prompt":"<p>Prepare the churn data for modelling.</p>\n<ol>\n<li><code dir=\"ltr\">X</code>: the eight numeric feature columns: everything except <code dir=\"ltr\">customer_id</code>, <code dir=\"ltr\">country</code>, <code dir=\"ltr\">segment</code>, <code dir=\"ltr\">avg_satisfaction</code> and the target.</li>\n<li><code dir=\"ltr\">y</code>: the <code dir=\"ltr\">churned</code> column.</li>\n<li>Split into <code dir=\"ltr\">X_train, X_test, y_train, y_test</code> with a 25% test set, <code dir=\"ltr\">random_state=42</code>, and the churn rate kept equal in both parts.</li>\n</ol>\n","hints":["<code dir=\"ltr\">customer_id</code> and <code dir=\"ltr\">churned</code> are still inside <code dir=\"ltr\">X</code> in the starter. Drop them too.","<code dir=\"ltr\">test_size=0.25</code> gives 100 test rows from 398.","Pass <code dir=\"ltr\">stratify=y</code> so both parts keep the 55% churn rate."],"explanation":"<p>Dropping the id and the target leaves eight numeric features. <code dir=\"ltr\">test_size=0.25</code> holds back 100 customers, <code dir=\"ltr\">random_state=42</code> makes the split repeatable, and <code dir=\"ltr\">stratify=y</code> keeps the churn rate at 55% in both parts so the test score isn’t skewed by an unlucky shuffle. Leaving <code dir=\"ltr\">churned</code> inside <code dir=\"ltr\">X</code> would let any model read the answer straight off the features, which is the most extreme form of leakage.</p>\n","checkLabels":{"features":"X holds exactly the eight numeric features","target":"y is the churned column (398 values of 0 and 1)","sizes":"298 training rows and 100 test rows","stratified":"Train and test keep the overall churn rate"}},"words":671,"readMinutes":3}