{"id":"l-ml-4-1","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Replace a single train/test split with k-fold cross-validation, read the mean and spread of the fold scores, and decide whether a difference between models is real or noise.","html":"<p>Through section 3 every comparison came with a hedge: “on 100 test customers, two points is two customers”. The forest scored 0.76, boosting 0.78, logistic regression 0.72. Was boosting really better, or did it get a lucky test set? One split can’t tell you. You need several, and that’s what cross-validation gives you.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Five splits instead of one</h2>\n<p><strong>k-fold cross-validation</strong> cuts the training data into k equal folds (5 is the usual default). It trains k models: each one learns from k−1 folds and is scored on the fold it didn’t see. You end up with k scores, and every training row has been in a validation fold exactly once.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 260\" role=\"img\" aria-labelledby=\"l-ml-4-1-t1\"><defs><marker id=\"arrow-l-ml-4-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-4-1-t1\">Five rows, one per round. In each row the training data is shown as five blocks; a different block is highlighted as the validation fold each time, and the other four are used for training. Each round produces one score.</title>\n  <text class=\"d-label-muted\" x=\"20\" y=\"20\">round</text>\n  <text class=\"d-label-muted\" x=\"590\" y=\"20\">score</text>\n  <text class=\"d-label\" x=\"30\" y=\"52\">1</text>\n  <rect class=\"d-box-warn\" x=\"60\" y=\"34\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"160\" y=\"34\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"260\" y=\"34\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"360\" y=\"34\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"460\" y=\"34\" width=\"96\" height=\"28\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"590\" y=\"53\">0.72</text>\n  <text class=\"d-label\" x=\"30\" y=\"92\">2</text>\n  <rect class=\"d-box\" x=\"60\" y=\"74\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box-warn\" x=\"160\" y=\"74\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"260\" y=\"74\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"360\" y=\"74\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"460\" y=\"74\" width=\"96\" height=\"28\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"590\" y=\"93\">0.75</text>\n  <text class=\"d-label\" x=\"30\" y=\"132\">3</text>\n  <rect class=\"d-box\" x=\"60\" y=\"114\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"160\" y=\"114\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box-warn\" x=\"260\" y=\"114\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"360\" y=\"114\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"460\" y=\"114\" width=\"96\" height=\"28\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"590\" y=\"133\">0.68</text>\n  <text class=\"d-label\" x=\"30\" y=\"172\">4</text>\n  <rect class=\"d-box\" x=\"60\" y=\"154\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"160\" y=\"154\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"260\" y=\"154\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box-warn\" x=\"360\" y=\"154\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"460\" y=\"154\" width=\"96\" height=\"28\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"590\" y=\"173\">0.81</text>\n  <text class=\"d-label\" x=\"30\" y=\"212\">5</text>\n  <rect class=\"d-box\" x=\"60\" y=\"194\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"160\" y=\"194\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"260\" y=\"194\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box\" x=\"360\" y=\"194\" width=\"96\" height=\"28\" rx=\"4\"/><rect class=\"d-box-warn\" x=\"460\" y=\"194\" width=\"96\" height=\"28\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"590\" y=\"213\">0.68</text>\n  <text class=\"d-label-muted\" x=\"60\" y=\"248\">highlighted = validation fold, others = training folds</text>\n</svg><figcaption>5-fold cross-validation</figcaption></figure>\n<p>The mean of the k scores is a steadier estimate than any single split. The spread between them is just as valuable: it shows how much a score can move for reasons that have nothing to do with the model.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Cross-validating the churn models</h2>\n<p><code dir=\"ltr\">cross_val_score</code> runs the whole loop for one metric; <code dir=\"ltr\">cross_validate</code> handles several metrics at once. For classification, pass a <code dir=\"ltr\">StratifiedKFold</code> with <code dir=\"ltr\">shuffle=True</code> so each fold keeps the 55% churn rate and the rows are mixed first. Create the splitter once and reuse it for every model so they all face exactly the same folds.</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.dummy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DummyClassifier</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.ensemble </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> HistGradientBoostingClassifier, RandomForestClassifier</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.linear_model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LogisticRegression</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\"> StratifiedKFold, cross_validate, train_test_split</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.tree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DecisionTreeClassifier</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_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\">    churn[features], churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </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>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    stratify</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StratifiedKFold(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_splits</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">shuffle</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 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\">models </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"dummy\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: DummyClassifier(),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"logistic\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: make_pipeline(StandardScaler(), LogisticRegression()),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"tree depth 3\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: DecisionTreeClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">max_depth</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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:#0A3069;--shiki-dark:#A5D6FF\">    \"forest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: RandomForestClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_estimators</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">min_samples_leaf</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</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:#0A3069;--shiki-dark:#A5D6FF\">    \"boosting\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: HistGradientBoostingClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">max_depth</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">learning_rate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">                                               max_iter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</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\">}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> name, model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> models.items():</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    r </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_validate(model, X_train, y_train, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cv</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">scoring</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"accuracy\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"roc_auc\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    acc, auc </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> r[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test_accuracy\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], r[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test_roc_auc\"</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:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:13}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> accuracy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">acc.mean()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ± </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">acc.std()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">   ROC AUC </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">auc.mean()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ± </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">auc.std()</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></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Read the spreads first. Logistic regression’s accuracy ranges from 0.68 to 0.81 across folds, a 13-point swing from nothing but which customers landed where. Against that noise, the accuracy differences between the four real models (0.73 to 0.76) mean very little: every gap is smaller than one standard deviation.</p>\n<p>ROC AUC, which grades the full ranking, is more decisive: logistic regression and the forest sit around 0.81, boosting around 0.79, and the depth-3 tree trails at 0.74 with the largest spread. That’s a pattern you can act on.</p>\n<p>For the 80-day rule, cross-validation means re-choosing the cut-off on each set of training folds and scoring it on the held-out fold. Done that way it averages 0.70 accuracy. Every real model beats it, by a few points.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">What you can conclude</h2>\n<p>Put together, cross-validation tells a calmer story than section 3’s single split did. Boosting’s 0.78 test accuracy was partly luck; across five folds it’s 0.75, within noise of everything else, and it ranks worse than logistic regression. The forest and logistic regression are tied on ranking. Given a tie, prefer the simpler model: logistic regression trains in milliseconds, its coefficients can be explained, and it has nothing to tune but <code dir=\"ltr\">C</code>.</p>\n<p>That’s the skeptic’s payoff. Without cross-validation you’d have shipped boosting because of a lucky number.</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>Comparing models on different splits</span></p>\n<p>Calling <code dir=\"ltr\">cross_val_score(model, X, y, cv=5)</code> separately for each model is fine for classifiers (an integer <code dir=\"ltr\">cv</code> gives the same unshuffled stratified folds each time). But mixing splitters, using <code dir=\"ltr\">shuffle=True</code> without a <code dir=\"ltr\">random_state</code>, or comparing one model’s CV score with another’s single test score makes the comparison noisy or unfair. Create one splitter and pass it to everything.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing k, and what CV doesn’t fix</h2>\n<ul>\n<li><strong>k = 5</strong> is the standard. <strong>k = 10</strong> gives each model more training data and a slightly less pessimistic estimate, at twice the cost. With very small datasets (a few dozen rows), more folds help; with large ones, even 3 folds are plenty.</li>\n<li>The fold scores aren’t independent (the training folds overlap), so treat the standard deviation as a rough guide to noise, not a formal error bar.</li>\n<li>Cross-validation doesn’t protect you from leakage. If information from the validation fold sneaks into training, every fold is optimistic. <a href=\"/lesson/ml-crash-course/l-ml-4-4\">The leakage lesson</a> is about exactly that.</li>\n</ul>\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>Where the test set fits now</span></p>\n<p>Cross-validation runs on <code dir=\"ltr\">X_train</code> only. It’s your tool for choosing models, features and settings. When you’ve chosen, fit the winner on all of <code dir=\"ltr\">X_train</code> and score it once on <code dir=\"ltr\">X_test</code>. That single number is what you report.</p>\n</aside>\n<p>So far every model has used the eight numeric columns. The data also has countries, segments and a half-empty satisfaction score that need encoding and imputing first. Next lesson builds a pipeline that does all of that inside each fold, so cross-validation stays honest.</p>\n","toc":[{"id":"sec-1","text":"Five splits instead of one","level":2},{"id":"sec-2","text":"Cross-validating the churn models","level":2},{"id":"sec-3","text":"What you can conclude","level":2},{"id":"sec-4","text":"Choosing k, and what CV doesn’t fix","level":2}],"takeaways":["k-fold cross-validation trains k models, each validated on a different fold, so every training row is used for validation exactly once.","The mean of the fold scores estimates performance; their standard deviation tells you how much a single split could mislead you.","Use <code dir=\"ltr\">StratifiedKFold</code> with <code dir=\"ltr\">shuffle=True</code> and a fixed <code dir=\"ltr\">random_state</code> for classification, and pass the same splitter to every model you compare.","<code dir=\"ltr\">cross_validate</code> returns several metrics at once, plus fit and score times.","Cross-validate on the training set to choose; keep the test set for one final check."],"further":[{"title":"Cross-validation: evaluating estimator performance","url":"https://scikit-learn.org/stable/modules/cross_validation.html"},{"title":"cross_validate","url":"https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_validate.html"},{"title":"StratifiedKFold","url":"https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedKFold.html"}],"quiz":[{"q":"<p>In 5-fold cross-validation on 298 training rows, how many models are fitted and how many rows does each one train on?</p>","options":[{"text":"One model, trained on all 298 rows and scored five times.","why":"Scoring one model five times on data it trained on would measure memorisation, not generalisation."},{"text":"Five models, each trained on about 60 rows and validated on the rest.","why":"That’s backwards. Each model validates on one fold (about 60 rows) and trains on the other four."},{"text":"Five models, each trained on about 238 rows and validated on the remaining 60.","why":"Correct. Every row sits in the validation fold once and in the training folds four times."},{"text":"Twenty-five models, one for every pair of folds.","why":"Plain k-fold fits one model per fold. Repeated or nested schemes fit more, but that’s not what this is."}],"answer":2},{"q":"<p>Model A scores 0.75 ± 0.04 accuracy across five folds; model B scores 0.73 ± 0.05. What’s the fair conclusion?</p>","options":[{"text":"The difference is small compared with the fold-to-fold spread, so treat them as roughly tied.","why":"Correct. A two-point gap inside a four-to-five-point spread could easily reverse with another shuffle."},{"text":"A is clearly better, because its mean is higher.","why":"Means alone ignore the noise. With spreads this size, the ranking isn’t reliable."},{"text":"B is better, because a larger standard deviation means it adapts more.","why":"A larger spread means less stable performance, not a strength."}],"answer":0},{"q":"<p>Why use the same <code dir=\"ltr\">StratifiedKFold(..., random_state=42)</code> object for every model you compare?</p>","options":[{"text":"Different splitters would make some models train faster.","why":"Speed is unaffected; the concern is fairness of the comparison."},{"text":"scikit-learn requires a single splitter per script.","why":"There’s no such rule; you can create as many as you like."},{"text":"It shuffles the data differently for each model, which is fairer.","why":"The opposite. Different shuffles add noise to the comparison."},{"text":"Every model is then scored on exactly the same folds, so differences come from the models, not the splits.","why":"Correct. Paired comparisons on identical folds are much less noisy."}],"answer":3},{"q":"<p>After cross-validation picks logistic regression, what should you do with the test set?</p>","options":[{"text":"Add it to the cross-validation to get more folds.","why":"Then nothing is left for an unbiased final estimate."},{"text":"Fit the chosen model on all training data and score it on the test set once.","why":"Correct. The test score is the number you report, and it hasn’t influenced any decision."},{"text":"Skip it, since the cross-validation score is already unbiased.","why":"The CV score was used to choose between models, so it’s slightly optimistic for the winner."}],"answer":1}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import StratifiedKFold, cross_validate, train_test_split\nfrom sklearn.neighbors import KNeighborsClassifier\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nfeatures = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n            \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\nX_train, X_test, y_train, y_test = train_test_split(\n    churn[features], churn[\"churned\"], test_size=0.25, random_state=42,\n    stratify=churn[\"churned\"])\n\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n\n# 1. Cross-validate both models on the TRAINING data with the same `cv`,\n#    scoring both \"accuracy\" and \"roc_auc\"\nlogistic = make_pipeline(StandardScaler(), LogisticRegression())\nknn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=15))\n\n# 2. Mean ROC AUC for each model, and the standard deviation of k-NN's fold AUCs\nlogistic_auc = None\nknn_auc = None\nknn_auc_std = None\n\n# 3. Fit the model with the higher mean AUC on all training data; score accuracy on the test set once\nfinal_test_acc = None\n","solution":"import pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import StratifiedKFold, cross_validate, train_test_split\nfrom sklearn.neighbors import KNeighborsClassifier\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nfeatures = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n            \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\nX_train, X_test, y_train, y_test = train_test_split(\n    churn[features], churn[\"churned\"], test_size=0.25, random_state=42,\n    stratify=churn[\"churned\"])\n\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n\nlogistic = make_pipeline(StandardScaler(), LogisticRegression())\nknn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=15))\n\nr_log = cross_validate(logistic, X_train, y_train, cv=cv, scoring=[\"accuracy\", \"roc_auc\"])\nr_knn = cross_validate(knn, X_train, y_train, cv=cv, scoring=[\"accuracy\", \"roc_auc\"])\n\nlogistic_auc = r_log[\"test_roc_auc\"].mean()\nknn_auc = r_knn[\"test_roc_auc\"].mean()\nknn_auc_std = r_knn[\"test_roc_auc\"].std()\n\nwinner = logistic if logistic_auc >= knn_auc else knn\nfinal_test_acc = winner.fit(X_train, y_train).score(X_test, y_test)\nprint(round(logistic_auc, 3), round(knn_auc, 3), round(knn_auc_std, 3), final_test_acc)\n","checks":[{"id":"logistic-auc","test":"logistic_auc is not None and abs(float(logistic_auc) - 0.807) < 0.002"},{"id":"knn-auc","test":"knn_auc is not None and abs(float(knn_auc) - float(__import__('sklearn.model_selection', fromlist=['x']).cross_val_score(__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.neighbors', fromlist=['x']).KNeighborsClassifier(n_neighbors=15)), X_train, y_train, cv=__import__('sklearn.model_selection', fromlist=['x']).StratifiedKFold(n_splits=5, shuffle=True, random_state=42), scoring='roc_auc').mean())) < 1e-6"},{"id":"knn-std","test":"knn_auc_std is not None and abs(float(knn_auc_std) - float(__import__('sklearn.model_selection', fromlist=['x']).cross_val_score(__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.neighbors', fromlist=['x']).KNeighborsClassifier(n_neighbors=15)), X_train, y_train, cv=__import__('sklearn.model_selection', fromlist=['x']).StratifiedKFold(n_splits=5, shuffle=True, random_state=42), scoring='roc_auc').std())) < 1e-6"},{"id":"final","test":"final_test_acc is not None and abs(float(final_test_acc) - 0.72) < 0.005"}],"title":"Logistic regression or k-NN?","prompt":"<p>Settle a model choice with cross-validation, then use the test set once.</p>\n<ol>\n<li>Cross-validate <code dir=\"ltr\">logistic</code> and <code dir=\"ltr\">knn</code> on the training data with the provided <code dir=\"ltr\">cv</code>, scoring <code dir=\"ltr\">&quot;accuracy&quot;</code> and <code dir=\"ltr\">&quot;roc_auc&quot;</code>.</li>\n<li><code dir=\"ltr\">logistic_auc</code> and <code dir=\"ltr\">knn_auc</code>: each model’s mean ROC AUC across the folds. <code dir=\"ltr\">knn_auc_std</code>: the standard deviation of k-NN’s fold AUCs.</li>\n<li><code dir=\"ltr\">final_test_acc</code>: fit whichever model has the higher mean AUC on all of <code dir=\"ltr\">X_train</code>, then compute its test accuracy.</li>\n</ol>\n","hints":["<code dir=\"ltr\">cross_validate(model, X_train, y_train, cv=cv, scoring=[&quot;accuracy&quot;, &quot;roc_auc&quot;])</code> returns a dict with a <code dir=\"ltr\">&quot;test_roc_auc&quot;</code> array.","<code dir=\"ltr\">.mean()</code> and <code dir=\"ltr\">.std()</code> work directly on those arrays.","Pick the winner with a plain <code dir=\"ltr\">if</code>, then <code dir=\"ltr\">winner.fit(X_train, y_train).score(X_test, y_test)</code>."],"explanation":"<p>Scaled k-NN with 15 neighbours ranks customers a little worse than logistic regression on average, and its fold scores vary noticeably. Because both models faced identical folds, the comparison is fair. The winner is refitted on all training rows and scored once on the test set (0.72): that’s the number you report, and it played no part in the choice.</p>\n","checkLabels":{"logistic-auc":"logistic_auc is the mean cross-validated ROC AUC","knn-auc":"knn_auc is k-NN’s mean ROC AUC on the same folds","knn-std":"knn_auc_std is the spread of k-NN’s fold AUCs","final":"final_test_acc is the winner’s test accuracy"}},"words":680,"readMinutes":3}