{"id":"l-ml-3-1","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Train a logistic regression on the churn data, read its probabilities and coefficients, and use the probabilities to rank customers so marketing contacts the riskiest first.","html":"<p>The depth-3 tree from section 1 answered “churn” or “stay”. Marketing doesn’t really want that. They have budget for a few dozen win-back calls a week and want to know who to call first. That needs a score that ranks customers by risk, and the classic model for it is logistic regression.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">From a line to a probability</h2>\n<p>Logistic regression starts like linear regression: multiply each feature by a coefficient, add them up, add an intercept. That gives a score <code dir=\"ltr\">z</code> that can be any number. Then it squashes the score through the <strong>sigmoid</strong> function, <code dir=\"ltr\">1 / (1 + e^-z)</code>, which maps any number to a value between 0 and 1. Large positive scores give probabilities near 1, large negative scores near 0, and a score of exactly 0 gives 0.5.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 260\" role=\"img\" aria-labelledby=\"l-ml-3-1-t1\"><defs><marker id=\"arrow-l-ml-3-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-3-1-t1\">An S-shaped curve. The horizontal axis is the linear score z from minus 6 to plus 6; the vertical axis is probability from 0 to 1. The curve passes through 0.5 at z equals 0 and flattens toward 0 and 1 at the extremes.</title>\n  <line class=\"d-line\" x1=\"40\" y1=\"220\" x2=\"600\" y2=\"220\"/>\n  <line class=\"d-line\" x1=\"320\" y1=\"20\" x2=\"320\" y2=\"225\"/>\n  <line class=\"d-dashed d-line\" x1=\"40\" y1=\"40\" x2=\"600\" y2=\"40\"/>\n  <line class=\"d-dashed d-line\" x1=\"40\" y1=\"130\" x2=\"600\" y2=\"130\"/>\n  <text class=\"d-label-muted\" x=\"606\" y=\"44\">1.0</text>\n  <text class=\"d-label-muted\" x=\"606\" y=\"134\">0.5</text>\n  <text class=\"d-label-muted\" x=\"606\" y=\"224\">0.0</text>\n  <text class=\"d-label-muted\" x=\"40\" y=\"244\">z = −6</text>\n  <text class=\"d-label-muted\" x=\"320\" y=\"244\" text-anchor=\"middle\">0</text>\n  <text class=\"d-label-muted\" x=\"600\" y=\"244\" text-anchor=\"end\">+6</text>\n  <path class=\"d-arrow\" d=\"M40 219.6 C200 219 250 200 320 130 C390 60 440 41 600 40.4\" fill=\"none\"/>\n  <circle class=\"d-dot\" cx=\"320\" cy=\"130\" r=\"6\"/>\n  <text class=\"d-label\" x=\"80\" y=\"200\">likely stays</text>\n  <text class=\"d-label\" x=\"470\" y=\"70\">likely churns</text>\n</svg><figcaption>The sigmoid turns a score into a probability</figcaption></figure>\n<p>During training, the model adjusts the coefficients so that churners get high probabilities and non-churners low ones. The loss it minimises is <strong>log loss</strong>, which punishes confident wrong answers hard: predicting 0.99 for a customer who stayed costs far more than predicting 0.6. There’s no exact formula for the minimum, so scikit-learn finds it with an iterative optimiser, a refined cousin of the gradient descent you wrote last section.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Fitting it on churn</h2>\n<p>Because the optimiser is iterative and the model has a penalty (more on that below), scale the features first. A pipeline keeps the scaler and the model together.</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.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\"> 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>\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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test accuracy:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, model.score(X_test, y_test))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(model.predict_proba(X_test)[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(model.predict(X_test)[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</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><code dir=\"ltr\">predict_proba</code> returns two columns, one per class in the order of <code dir=\"ltr\">classes_</code>: probability of staying, probability of churning. They add up to 1. <code dir=\"ltr\">predict</code> is a shortcut that labels a row 1 when the churn probability is at least 0.5. That 0.5 is a default, not a law, and section 3’s last lesson is about choosing it on purpose.</p>\n<p>Accuracy is 0.72, level with the 80-day rule. Don’t stop reading there: the probabilities carry information accuracy throws away.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Reading the coefficients</h2>\n<p>With standardised features, each coefficient says how much one standard deviation of that feature moves the score <code dir=\"ltr\">z</code> (the log-odds of churn). That makes them comparable across features.</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.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:#1F2328;--shiki-dark:#E6EDF3\">coefs </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.Series(model[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].coef_[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </span><span style=\"color:#953800;--shiki-dark:#FFA657\">index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">features).sort_values()</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(coefs.round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\">ranked </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> X_test.assign(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">churn_prob</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">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:#953800;--shiki-dark:#FFA657\">churned</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y_test)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">top10 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ranked.sort_values(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn_prob\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">ascending</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</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\">(top10[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn_prob\"</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\">]].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\">\"share of the top 10 who really churned:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, top10[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].mean())</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>Recency (<code dir=\"ltr\">days_since_last_order</code>, +0.93) dominates, which matches what the rule baseline told you. Longer tenure also pushes towards churn, while more orders, more spend, coupon use and even more support tickets push away from it. Customers who contact support are engaged customers; it’s the silent ones who leave. That’s the kind of finding worth taking back to the business, with the caveat that it’s an association, not a cause.</p>\n<p>Then the payoff: of the ten customers the model ranks riskiest, nine really churned. A yes/no rule can’t produce that list; a probability can.</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>Skipping the scaler</span></p>\n<p>Fit <code dir=\"ltr\">LogisticRegression()</code> on the raw features and you get a <code dir=\"ltr\">ConvergenceWarning</code>: the optimiser hit its iteration limit before finishing, because <code dir=\"ltr\">total_spent</code> in thousands and <code dir=\"ltr\">used_coupon</code> in 0/1 make the loss surface a long, narrow valley. The coefficients you get are unfinished. Scale first; it also makes the penalty treat all features fairly.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">The penalty you didn’t ask for</h2>\n<p><code dir=\"ltr\">LogisticRegression</code> is regularised by default with an L2 penalty, the same idea as Ridge. Its strength is set by <code dir=\"ltr\">C</code>, which is the <strong>inverse</strong> of Ridge’s <code dir=\"ltr\">alpha</code>: small <code dir=\"ltr\">C</code> means a strong penalty, large <code dir=\"ltr\">C</code> means a weak one. The default <code dir=\"ltr\">C=1.0</code> is a reasonable start. On this data, values from 0.01 to 100 all land within a couple of points of each other, a sign that eight well-behaved features don’t give the model much room to overfit. With hundreds of features, <code dir=\"ltr\">C</code> matters a lot, and you’d tune it with the tools in section 4.</p>\n<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>When logistic regression is the right first model</span></p>\n<p>It’s fast, its probabilities are usually sensible out of the box, and its coefficients can be explained to a manager. Make it your first real model on any tabular classification problem, right after the baselines. More flexible models have to beat it to earn their place.</p>\n</aside>\n<p>Logistic regression draws one straight boundary through the feature space. Next lesson’s model makes no such assumption: it asks a series of yes/no questions and can carve the space into boxes.</p>\n","toc":[{"id":"sec-1","text":"From a line to a probability","level":2},{"id":"sec-2","text":"Fitting it on churn","level":2},{"id":"sec-3","text":"Reading the coefficients","level":2},{"id":"sec-4","text":"The penalty you didn’t ask for","level":2}],"takeaways":["Logistic regression computes a linear score and squashes it through the sigmoid into a probability between 0 and 1.","<code dir=\"ltr\">predict_proba</code> returns one probability per class; <code dir=\"ltr\">predict</code> applies a 0.5 cut-off to it.","On standardised features, each coefficient is the change in log-odds per standard deviation, so its sign and size are comparable across features.","<code dir=\"ltr\">LogisticRegression</code> is regularised by default; <code dir=\"ltr\">C</code> is the inverse of the penalty strength, so a smaller <code dir=\"ltr\">C</code> means a stronger penalty.","Ranking customers by probability is often more useful than a yes/no label, because it lets the business choose how many to act on."],"further":[{"title":"Logistic regression","url":"https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression"},{"title":"LogisticRegression","url":"https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html"}],"quiz":[{"q":"<p>A customer’s linear score inside a logistic regression is 0. What churn probability does the model give?</p>","options":[{"text":"0.5, because the sigmoid of 0 is exactly one half.","why":"Correct. A score of 0 means the evidence for and against churn balances."},{"text":"0, because a score of 0 means no churn.","why":"The probability only approaches 0 as the score goes far negative. Zero sits in the middle of the curve."},{"text":"It depends on the intercept, which is added after the sigmoid.","why":"The intercept is part of the linear score, added before the sigmoid. Once the score is 0, the output is fixed."},{"text":"1, because the sigmoid of 0 is 1.","why":"The sigmoid is <code dir=\"ltr\">1 / (1 + e^-z)</code>; with z = 0 that’s <code dir=\"ltr\">1 / 2</code>."}],"answer":0},{"q":"<p>On standardised churn features, <code dir=\"ltr\">days_since_last_order</code> has coefficient +0.93 and <code dir=\"ltr\">total_spent</code> has -0.26. What can you say?</p>","options":[{"text":"Each extra day of recency matters about 3.6 times as much as each extra dollar of spend.","why":"The coefficients are per standard deviation, not per dollar or per day, so a per-unit comparison doesn’t follow."},{"text":"Customers who spent more churn more often.","why":"A negative coefficient means more spending is associated with lower churn odds, holding the other features fixed."},{"text":"Raising spend by marketing would cut churn by a known amount.","why":"Coefficients describe associations in this data; they don’t tell you the effect of an intervention."},{"text":"A one-standard-deviation increase in recency raises churn odds far more than the same increase in spend lowers them.","why":"Correct. On a shared scale, the size of the coefficient compares effects in the fitted model."}],"answer":3},{"q":"<p>You fit <code dir=\"ltr\">LogisticRegression()</code> on the raw churn features and get a <code dir=\"ltr\">ConvergenceWarning</code>. What is the best first fix?</p>","options":[{"text":"Ignore it, because the accuracy looks fine.","why":"The optimiser stopped before finishing, so the coefficients are not the ones the model defines. Fix the cause."},{"text":"Lower <code dir=\"ltr\">C</code> to 0.0001 so the model has less to learn.","why":"A tiny <code dir=\"ltr\">C</code> hides the symptom by flattening the model, at the cost of a very different, underfit model."},{"text":"Add a <code dir=\"ltr\">StandardScaler</code> before the model in a pipeline.","why":"Correct. Features on wildly different scales make the optimiser’s job hard; scaling usually removes the warning."}],"answer":2},{"q":"<p>Marketing can call 30 customers this week. What should your model give them?</p>","options":[{"text":"The 30 customers with the highest predicted churn probability.","why":"Correct. Ranking by probability puts the limited budget where the model is most confident churn is coming."},{"text":"Any 30 customers that <code dir=\"ltr\">predict</code> labels as 1.","why":"Dozens of customers may be labelled 1. Picking 30 at random among them wastes the ranking the model already computed."},{"text":"The 30 customers whose probability is closest to 0.5.","why":"Those are the cases the model is least sure about, not the ones most likely to churn."}],"answer":0}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import train_test_split\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\n# 1. A pipeline: StandardScaler, then LogisticRegression; fit it on the training data\nmodel = LogisticRegression().fit(X_train, y_train)\n\n# 2. Churn probability (class 1) for every test customer\nchurn_prob = model.predict(X_test)\n\n# 3. customer_id of the 20 test customers with the highest churn probability\ncall_list = []\n\n# 4. Share of those 20 who really churned\nhit_rate = None\n","solution":"import pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import train_test_split\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\nmodel = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)\n\nchurn_prob = model.predict_proba(X_test)[:, 1]\n\nranked = churn.loc[X_test.index, [\"customer_id\", \"churned\"]].assign(prob=churn_prob)\ntop20 = ranked.sort_values(\"prob\", ascending=False).head(20)\ncall_list = top20[\"customer_id\"].tolist()\n\nhit_rate = top20[\"churned\"].mean()\nprint(call_list[:5], hit_rate)\n","checks":[{"id":"pipeline","test":"[type(s).__name__ for s in model] == ['StandardScaler', 'LogisticRegression']"},{"id":"probs","test":"len(churn_prob) == 100 and abs(float(churn_prob[0]) - float(__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.linear_model', fromlist=['x']).LogisticRegression()).fit(X_train, y_train).predict_proba(X_test)[:, 1][0])) < 1e-6 and 0 < float(min(churn_prob)) < float(max(churn_prob)) < 1"},{"id":"call-list","test":"len(call_list) == 20 and set(int(c) for c in call_list) == set(churn.loc[X_test.index, 'customer_id'].to_numpy()[__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.linear_model', fromlist=['x']).LogisticRegression()).fit(X_train, y_train).predict_proba(X_test)[:, 1].argsort()[::-1][:20]].tolist())"},{"id":"hit-rate","test":"hit_rate is not None and abs(float(hit_rate) - churn.set_index('customer_id').loc[[int(c) for c in call_list], 'churned'].mean()) < 1e-9 and float(hit_rate) > 0.8"}],"title":"Build Monday's call list","prompt":"<p>Marketing can call 20 customers. Give them the right 20.</p>\n<ol>\n<li><code dir=\"ltr\">model</code>: a pipeline of <code dir=\"ltr\">StandardScaler</code> then <code dir=\"ltr\">LogisticRegression</code>, fitted on the training data.</li>\n<li><code dir=\"ltr\">churn_prob</code>: the churn probability (class 1) for each test customer.</li>\n<li><code dir=\"ltr\">call_list</code>: the <code dir=\"ltr\">customer_id</code> values of the 20 test customers with the highest churn probability.</li>\n<li><code dir=\"ltr\">hit_rate</code>: the share of those 20 who actually churned.</li>\n</ol>\n","hints":["<code dir=\"ltr\">make_pipeline(StandardScaler(), LogisticRegression())</code>","<code dir=\"ltr\">predict</code> gives labels. <code dir=\"ltr\">predict_proba(X_test)[:, 1]</code> gives the churn column of probabilities.","<code dir=\"ltr\">X_test</code> keeps the original row index, so <code dir=\"ltr\">churn.loc[X_test.index, &quot;customer_id&quot;]</code> lines up with <code dir=\"ltr\">churn_prob</code>. Add the probabilities as a column, sort descending, take <code dir=\"ltr\">head(20)</code>."],"explanation":"<p>Ranking by probability concentrates the call budget: well over 80% of the 20 riskiest test customers really churned, against 55% for a random pick. <code dir=\"ltr\">predict</code> would only have said “1” for dozens of customers without any order among them. Keeping the original index from <code dir=\"ltr\">train_test_split</code> is what lets you join predictions back to ids.</p>\n","checkLabels":{"pipeline":"model is StandardScaler → LogisticRegression","probs":"churn_prob holds the class-1 probabilities","call-list":"call_list has the 20 highest-risk customer ids","hit-rate":"hit_rate is the share of the call list who churned"}},"words":690,"readMinutes":3}