{"id":"l-ml-3-4","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Read a confusion matrix, compute precision, recall, F1 and ROC AUC with scikit-learn, and pick the metric that matches what each kind of mistake costs the business.","html":"<p>Logistic regression and the 80-day rule both score 0.72 accuracy on the churn test set. Are they equally good? Accuracy says yes. Marketing would disagree as soon as they see who each one flags. Accuracy blends two very different mistakes into one number, and throws away the ranking that made the call list useful. This lesson takes them apart.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Four outcomes</h2>\n<p>Every prediction on a yes/no problem lands in one of four cells. Call churners <strong>positives</strong> (they’re the class you’re trying to find).</p>\n<ul>\n<li><strong>True positive (TP)</strong>: predicted churn, did churn. Marketing reached the right person.</li>\n<li><strong>False positive (FP)</strong>: predicted churn, stayed. An offer wasted on a loyal customer.</li>\n<li><strong>False negative (FN)</strong>: predicted stay, churned. A customer lost without a fight.</li>\n<li><strong>True negative (TN)</strong>: predicted stay, stayed. Correctly left alone.</li>\n</ul>\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.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.metrics </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> confusion_matrix</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 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\">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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pred </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model.predict(X_test)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rule </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (X_test[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 80</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).astype(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></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\">\"model:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">\\n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, confusion_matrix(y_test, pred))</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\">\"rule:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">\\n</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, confusion_matrix(y_test, rule))</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>scikit-learn lays the matrix out with actual classes as rows and predicted classes as columns, both in sorted order (0 then 1). So the model’s <code dir=\"ltr\">[[35, 10], [18, 37]]</code> reads like this:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 560 280\" role=\"img\" aria-labelledby=\"l-ml-3-4-t1\"><defs><marker id=\"arrow-l-ml-3-4-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-3-4-t1\">Two by two grid. Actual stayed, predicted stay: 35 true negatives. Actual stayed, predicted churn: 10 false positives. Actual churned, predicted stay: 18 false negatives. Actual churned, predicted churn: 37 true positives.</title>\n  <text class=\"d-label-strong\" x=\"330\" y=\"24\" text-anchor=\"middle\">predicted</text>\n  <text class=\"d-label\" x=\"250\" y=\"50\" text-anchor=\"middle\">stay (0)</text>\n  <text class=\"d-label\" x=\"420\" y=\"50\" text-anchor=\"middle\">churn (1)</text>\n  <text class=\"d-label-strong\" x=\"40\" y=\"170\" text-anchor=\"middle\" transform=\"rotate(-90 40 170)\">actual</text>\n  <text class=\"d-label\" x=\"150\" y=\"115\" text-anchor=\"end\">stayed (0)</text>\n  <text class=\"d-label\" x=\"150\" y=\"215\" text-anchor=\"end\">churned (1)</text>\n  <rect class=\"d-box-success\" x=\"170\" y=\"64\" width=\"160\" height=\"90\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"250\" y=\"105\" text-anchor=\"middle\">35</text>\n  <text class=\"d-label\" x=\"250\" y=\"130\" text-anchor=\"middle\">true negatives</text>\n  <rect class=\"d-box-warn\" x=\"340\" y=\"64\" width=\"160\" height=\"90\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"420\" y=\"105\" text-anchor=\"middle\">10</text>\n  <text class=\"d-label\" x=\"420\" y=\"130\" text-anchor=\"middle\">false positives</text>\n  <rect class=\"d-box-warn\" x=\"170\" y=\"164\" width=\"160\" height=\"90\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"250\" y=\"205\" text-anchor=\"middle\">18</text>\n  <text class=\"d-label\" x=\"250\" y=\"230\" text-anchor=\"middle\">false negatives</text>\n  <rect class=\"d-box-success\" x=\"340\" y=\"164\" width=\"160\" height=\"90\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"420\" y=\"205\" text-anchor=\"middle\">37</text>\n  <text class=\"d-label\" x=\"420\" y=\"230\" text-anchor=\"middle\">true positives</text>\n</svg><figcaption>The logistic regression’s confusion matrix on 100 test customers</figcaption></figure>\n<p>The rule’s matrix is <code dir=\"ltr\">[[31, 14], [14, 41]]</code>. Same 72 correct predictions, different mistakes: the model wastes fewer offers (10 against 14) and misses more churners (18 against 14). Which is better depends on what each mistake costs, and accuracy can’t tell you.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Precision, recall and F1</h2>\n<p>Two ratios turn the matrix into questions a manager asks:</p>\n<ul>\n<li><strong>Precision</strong> = TP / (TP + FP): of the customers we flagged, how many were really leaving? The model: 37 / 47 = 0.79. The rule: 0.75.</li>\n<li><strong>Recall</strong> = TP / (TP + FN): of the customers who left, how many did we flag? The model: 37 / 55 = 0.67. The rule: 0.75.</li>\n</ul>\n<p>There’s a built-in tension. Flag more people and recall rises while precision usually falls; flag fewer and the reverse. <strong>F1</strong> is the harmonic mean of the two, <code dir=\"ltr\">2 * P * R / (P + R)</code>. It’s high only when both are, so a model can’t game it by maximising one. Use it when you need a single number and both errors matter roughly equally.</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.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.metrics </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> classification_report, roc_auc_score</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 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\">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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(classification_report(y_test, model.predict(X_test), </span><span style=\"color:#953800;--shiki-dark:#FFA657\">target_names</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"stayed\"</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>\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\">\"model ROC AUC:\"</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\">(roc_auc_score(y_test, model.predict_proba(X_test)[:, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]), </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\">\"rule  ROC AUC:\"</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\">(roc_auc_score(y_test, X_test[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]), </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><code dir=\"ltr\">classification_report</code> prints precision, recall and F1 for each class, with support (the number of true cases). Read the “churned” row for the business question; the “stayed” row is the same analysis with the roles swapped.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">ROC AUC: grading the ranking</h2>\n<p>Precision and recall depend on the 0.5 cut-off. <strong>ROC AUC</strong> doesn’t use a cut-off at all. It asks: if you pick one churner and one stayer at random, how often does the model give the churner the higher score? 0.5 means the scores are no better than a coin; 1.0 means every churner outranks every stayer.</p>\n<p><code dir=\"ltr\">roc_auc_score</code> takes scores, not labels: pass <code dir=\"ltr\">predict_proba(...)[:, 1]</code>. Any score that orders customers works, so you can grade the rule by its raw <code dir=\"ltr\">days_since_last_order</code>. The rule gets 0.77; logistic regression gets 0.81. That gap is the real difference between them, hidden by the accuracy tie: the model’s ranking is meaningfully better, which is why its top-10 list in <a href=\"/lesson/ml-crash-course/l-ml-3-1\">the logistic regression lesson</a> was so accurate.</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>Not knowing which class is “positive”</span></p>\n<p><code dir=\"ltr\">precision_score</code> and <code dir=\"ltr\">recall_score</code> treat label 1 as positive unless you say otherwise. In <code dir=\"ltr\">load_breast_cancer</code>, 0 is malignant and 1 is benign, so a reported recall of 0.99 is recall for <strong>benign</strong> tumours. For the class a doctor cares about, pass <code dir=\"ltr\">pos_label=0</code> or read the malignant row of <code dir=\"ltr\">classification_report</code>. Check <code dir=\"ltr\">target_names</code> before quoting any per-class metric.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing a metric</h2>\n<p>Start from the cost of each mistake, then pick:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Situation</th>\n<th>Watch</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Missing a case is costly (cancer screening, fraud)</td>\n<td>Recall</td>\n<td>Every false negative is the expensive error</td>\n</tr>\n<tr>\n<td>Acting on a case is costly (pricey offers, account bans)</td>\n<td>Precision</td>\n<td>Every false positive wastes money or trust</td>\n</tr>\n<tr>\n<td>You’ll act on the top N by score</td>\n<td>ROC AUC, or the hit rate in the top N</td>\n<td>Ranking quality is what matters</td>\n</tr>\n<tr>\n<td>Both errors matter, one number needed</td>\n<td>F1</td>\n<td>Punishes neglecting either</td>\n</tr>\n</tbody>\n</table></div><aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Report a pair, not a single score</span></p>\n<p>“Precision 0.79 and recall 0.67 at the default threshold, ROC AUC 0.81” tells a stakeholder what the model does and leaves room to talk about trade-offs. A lone F1 or accuracy figure ends the conversation too early.</p>\n</aside>\n<p>Precision and recall moved in opposite directions between the model and the rule because they effectively use different cut-offs. You can move that cut-off yourself, on purpose, and that’s next lesson.</p>\n","toc":[{"id":"sec-1","text":"Four outcomes","level":2},{"id":"sec-2","text":"Precision, recall and F1","level":2},{"id":"sec-3","text":"ROC AUC: grading the ranking","level":2},{"id":"sec-4","text":"Choosing a metric","level":2}],"takeaways":["A confusion matrix counts true negatives, false positives, false negatives and true positives; in scikit-learn rows are actual classes and columns are predicted classes.","Precision is the share of flagged cases that are real; recall is the share of real cases that get flagged.","F1 is the harmonic mean of precision and recall, useful when you need one number that punishes neglecting either.","ROC AUC measures ranking quality across all thresholds: the chance that a random positive gets a higher score than a random negative.","Always check which class is the positive one: in <code dir=\"ltr\">load_breast_cancer</code>, malignant is 0, so recall for malignant needs <code dir=\"ltr\">pos_label=0</code>."],"further":[{"title":"Classification metrics","url":"https://scikit-learn.org/stable/modules/model_evaluation.html#classification-metrics"},{"title":"confusion_matrix","url":"https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html"},{"title":"roc_auc_score","url":"https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">confusion_matrix(y_test, pred)</code> returns <code dir=\"ltr\">[[35, 10], [18, 37]]</code> for churn (1 = churned). How many churners did the model miss?</p>","options":[{"text":"10","why":"That’s the top-right cell, customers who stayed but were flagged as churners (false positives)."},{"text":"35","why":"That’s the top-left cell, customers who stayed and were correctly predicted to stay."},{"text":"37","why":"That’s the bottom-right cell, churners the model caught."},{"text":"18","why":"Correct. Row 1 is actual churners; column 0 is “predicted to stay”. Those 18 are false negatives."}],"answer":3},{"q":"<p>Each win-back offer costs $40, so marketing only wants to contact customers who are very likely to churn. Which metric should you watch most closely?</p>","options":[{"text":"Precision, because it measures how many contacted customers really were churning.","why":"Correct. Every false positive is $40 spent on a customer who would have stayed anyway."},{"text":"Recall, because it counts how many churners you reach.","why":"Recall rewards contacting more people, which is exactly what an expensive offer argues against."},{"text":"Accuracy, because it balances both kinds of error.","why":"Accuracy weighs both errors equally, but here false positives cost money and false negatives cost nothing extra."}],"answer":0},{"q":"<p>Model A has ROC AUC 0.81 and model B has 0.77, but both have accuracy 0.72 at the default threshold. What does that tell you?</p>","options":[{"text":"The AUC difference must be a calculation error, since accuracy is equal.","why":"The two metrics measure different things. Accuracy looks at one threshold; AUC looks at the whole ranking."},{"text":"B is better because it reaches the same accuracy with a weaker ranking.","why":"A weaker ranking is a disadvantage; it means B’s scores separate churners from stayers less well."},{"text":"A ranks customers better, so it will give a better call list at most list sizes.","why":"Correct. AUC summarises ranking quality, which is what a top-N list depends on."}],"answer":2},{"q":"<p>A model on <code dir=\"ltr\">load_breast_cancer</code> reports <code dir=\"ltr\">recall_score(y_test, pred) = 0.99</code>. Why might that number mislead a doctor?</p>","options":[{"text":"Recall can’t be above 0.95 on medical data, so it’s a bug.","why":"There’s no such ceiling; this dataset is genuinely easy to separate."},{"text":"Label 1 is benign here, so that’s recall for benign tumours, not the malignant ones the doctor cares about.","why":"Correct. Pass <code dir=\"ltr\">pos_label=0</code> (or read the per-class report) to get recall for malignant."},{"text":"Recall ignores the false negatives, which are what matter in medicine.","why":"Recall is built from false negatives; the problem is which class it’s computed for."}],"answer":1}],"exercise":{"kind":"python","starter":"from sklearn.datasets import load_breast_cancer\nfrom sklearn.metrics import confusion_matrix, precision_score, recall_score, roc_auc_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.tree import DecisionTreeClassifier\n\ndata = load_breast_cancer()\nprint(data.target_names)          # index 0 = malignant, 1 = benign\nX, y = load_breast_cancer(return_X_y=True, as_frame=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)\n\nmodel = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)\npred = model.predict(X_test)\n\n# 1. Confusion matrix (rows = actual, columns = predicted)\ncm = None\n\n# 2. Recall and precision for MALIGNANT tumours\nrecall_malignant = recall_score(y_test, pred)\nprecision_malignant = precision_score(y_test, pred)\n\n# 3. ROC AUC of the model's scores\nauc = None\n","solution":"from sklearn.datasets import load_breast_cancer\nfrom sklearn.metrics import confusion_matrix, precision_score, recall_score, roc_auc_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.tree import DecisionTreeClassifier\n\ndata = load_breast_cancer()\nprint(data.target_names)          # index 0 = malignant, 1 = benign\nX, y = load_breast_cancer(return_X_y=True, as_frame=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)\n\nmodel = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)\npred = model.predict(X_test)\n\ncm = confusion_matrix(y_test, pred)\n\nrecall_malignant = recall_score(y_test, pred, pos_label=0)\nprecision_malignant = precision_score(y_test, pred, pos_label=0)\n\nauc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])\nprint(cm, recall_malignant, precision_malignant, auc)\n","checks":[{"id":"cm","test":"cm is not None and [list(map(int, r)) for r in cm] == [[50, 3], [10, 80]]"},{"id":"recall","test":"abs(float(recall_malignant) - 50 / 53) < 1e-6"},{"id":"precision","test":"abs(float(precision_malignant) - 50 / 60) < 1e-6"},{"id":"auc","test":"auc is not None and 0.9 < float(auc) < 0.97"}],"title":"Metrics for the class that matters","prompt":"<p>A depth-2 tree screens breast tumours. In this dataset <strong>0 means malignant</strong> and 1 means benign. Measure the model from a doctor’s point of view.</p>\n<ol>\n<li><code dir=\"ltr\">cm</code>: the confusion matrix of <code dir=\"ltr\">y_test</code> against <code dir=\"ltr\">pred</code>.</li>\n<li><code dir=\"ltr\">recall_malignant</code> and <code dir=\"ltr\">precision_malignant</code>: recall and precision with malignant as the positive class.</li>\n<li><code dir=\"ltr\">auc</code>: ROC AUC from the model’s predicted probabilities.</li>\n</ol>\n","hints":["<code dir=\"ltr\">confusion_matrix(y_test, pred)</code>: true labels first.","The starter computes the metrics for label 1 (benign). Add <code dir=\"ltr\">pos_label=0</code>.","<code dir=\"ltr\">roc_auc_score</code> wants scores for the positive label of <code dir=\"ltr\">y_test</code> (1). Use <code dir=\"ltr\">model.predict_proba(X_test)[:, 1]</code>, not <code dir=\"ltr\">predict</code>."],"explanation":"<p>The matrix <code dir=\"ltr\">[[50, 3], [10, 80]]</code> shows 3 malignant tumours called benign: the dangerous error. Recall for malignant is 50/53 ≈ 0.94; precision is 50/60 ≈ 0.83, because 10 benign tumours were flagged. With the default <code dir=\"ltr\">pos_label=1</code> you’d have reported benign recall (0.89) as if it were about cancer. AUC is about 0.94: the tree’s four leaves give a coarse but useful ranking.</p>\n","checkLabels":{"cm":"cm is the confusion matrix","recall":"recall_malignant uses malignant (0) as positive","precision":"precision_malignant uses malignant (0) as positive","auc":"auc is computed from probabilities"}},"words":720,"readMinutes":3}