{"id":"l-ml-3-2","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Grow decision trees on the churn data, read them as plain if/else rules, control their size with max_depth and min_samples_leaf, and know when their probabilities can’t be trusted.","html":"<p>Ask a support lead how they spot customers who are about to leave and you’ll get something like: “If they haven’t ordered in months, they’re gone. If they’re recent but only bought once or twice, it’s a toss-up. Regulars who ordered recently are fine.” That’s a decision tree. This lesson grows one from data and checks whether it agrees.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Twenty questions, chosen by data</h2>\n<p>A decision tree predicts by asking yes/no questions about one feature at a time. To build it, the algorithm looks at every feature and every possible threshold, and picks the question that splits the training customers into the two purest groups: ideally all churners on one side and all stayers on the other. Purity is measured with the <strong>Gini impurity</strong> by default (0 for a group of one class only, 0.5 for a 50/50 mix). Then it repeats the search inside each group, and so on down.</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.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.tree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DecisionTreeClassifier, export_text</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\">tree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</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\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit(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\">(export_text(tree, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">feature_names</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">features, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">show_weights</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></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><code dir=\"ltr\">export_text</code> prints the rules, and <code dir=\"ltr\">show_weights=True</code> adds how many training customers of each class (stayed, churned) reached each leaf.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 270\" role=\"img\" aria-labelledby=\"l-ml-3-2-t1\"><defs><marker id=\"arrow-l-ml-3-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-3-2-t1\">Root question: days since last order at most 236.5. If yes, ask orders at most 3.5: yes gives a leaf of 60 stayed and 67 churned, no gives 60 stayed and 13 churned. If no, ask tenure at most 637.5: yes gives 11 stayed and 84 churned, no gives 2 stayed and 1 churned.</title>\n  <rect class=\"d-box-primary\" x=\"235\" y=\"10\" width=\"230\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"350\" y=\"37\" text-anchor=\"middle\">days_since_last ≤ 236.5?</text>\n  <path class=\"d-arrow\" d=\"M300 54 L180 100\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M400 54 L520 100\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"220\" y=\"80\">yes</text>\n  <text class=\"d-label-muted\" x=\"470\" y=\"80\">no</text>\n  <rect class=\"d-box\" x=\"80\" y=\"104\" width=\"200\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"180\" y=\"131\" text-anchor=\"middle\">orders ≤ 3.5?</text>\n  <rect class=\"d-box\" x=\"420\" y=\"104\" width=\"200\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"520\" y=\"131\" text-anchor=\"middle\">tenure_days ≤ 637.5?</text>\n  <path class=\"d-arrow\" d=\"M140 148 L80 196\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M220 148 L270 196\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M480 148 L430 196\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M560 148 L610 196\" marker-end=\"url(#arrow-l-ml-3-2-1)\"/>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"200\" width=\"140\" height=\"54\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"80\" y=\"222\" text-anchor=\"middle\">60 stay / 67 churn</text>\n  <text class=\"d-label-strong\" x=\"80\" y=\"243\" text-anchor=\"middle\">toss-up</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"200\" width=\"140\" height=\"54\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"270\" y=\"222\" text-anchor=\"middle\">60 stay / 13 churn</text>\n  <text class=\"d-label-strong\" x=\"270\" y=\"243\" text-anchor=\"middle\">stays</text>\n  <rect class=\"d-box-accent\" x=\"360\" y=\"200\" width=\"140\" height=\"54\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"430\" y=\"222\" text-anchor=\"middle\">11 stay / 84 churn</text>\n  <text class=\"d-label-strong\" x=\"430\" y=\"243\" text-anchor=\"middle\">churns</text>\n  <rect class=\"d-box\" x=\"540\" y=\"200\" width=\"140\" height=\"54\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"610\" y=\"222\" text-anchor=\"middle\">2 stay / 1 churn</text>\n  <text class=\"d-label-strong\" x=\"610\" y=\"243\" text-anchor=\"middle\">3 customers!</text>\n</svg><figcaption>The depth-2 churn tree</figcaption></figure>\n<p>Three leaves match the support lead almost word for word. Long-silent customers churn (84 of 95). Recent regulars with four or more orders stay (60 of 73). Recent customers with few orders are a toss-up (67 of 127 churn). The tree also discovered an <strong>interaction</strong> nobody had to code: order count matters, but only among recent customers.</p>\n<p>The fourth leaf is a warning. The tree spent a whole question isolating three very long-tenured customers. That’s the greedy search at work: it picks the split that helps most right now, even if the help comes from a group too small to mean anything.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">How a tree predicts probabilities</h2>\n<p>A customer who falls into a leaf gets that leaf’s class shares as probabilities. Land in the “regulars” leaf and <code dir=\"ltr\">predict_proba</code> returns about <code dir=\"ltr\">[0.82, 0.18]</code>. So a depth-2 tree can only ever output four distinct probabilities, one per leaf. That’s coarse, but honest. The trouble starts when the tree has no limits.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Growing too far, and how to stop it</h2>\n<p>An unconstrained tree keeps splitting until every leaf is pure. On the churn data that’s 64 leaves, 11 levels deep, and 100% training accuracy. Every leaf is pure, so every probability is exactly 0 or 1: the tree claims total certainty about every test customer while being right only 72% of the time. Ranked by such probabilities, almost all customers tie, and the call list from last lesson becomes a lottery.</p>\n<p>Two hyperparameters do most of the work of keeping a tree honest:</p>\n<ul>\n<li><code dir=\"ltr\">max_depth</code> caps how many questions in a row the tree can ask.</li>\n<li><code dir=\"ltr\">min_samples_leaf</code> refuses any split that would leave fewer than that many training customers in a leaf. It targets exactly the problem in the fourth leaf above, and it lets the tree grow deep where there’s plenty of data and stay shallow where there isn’t.</li>\n</ul>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"><span 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:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> settings </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [{}, {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"max_depth\"</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"min_samples_leaf\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    t </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DecisionTreeClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">**</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">settings).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    distinct </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(t.predict_proba(X_test)[:, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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:#0550AE;--shiki-dark:#79C0FF\">str</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(settings)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:24}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> leaves </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t.get_n_leaves()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:3}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  train </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t.score(X_train, y_train)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  \"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">          f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t.score(X_test, y_test)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  distinct probabilities </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">distinct</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DecisionTreeClassifier(</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\">20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit(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\">(pd.Series(t.feature_importances_, </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 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\">).round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p><code dir=\"ltr\">feature_importances_</code> says how much each feature reduced impurity across all its splits, normalised to sum to 1. Recency takes over half, order count comes second; coupons and support tickets get zero because the tree never used them. Treat these numbers as a description of this tree, not of the world: features the tree didn’t need can still be informative, and the next lesson shows a more trustworthy way to measure importance.</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>Trusting one tree’s structure</span></p>\n<p>Change <code dir=\"ltr\">random_state</code> in <code dir=\"ltr\">train_test_split</code> and refit: the root question may stay, but the branches below it often change. A tree’s first split decides everything beneath it, so a small change in the data can produce a different-looking tree with a similar score. Read a single tree as one plausible story, not as the truth about your customers.</p>\n</aside>\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>What trees don’t need</span></p>\n<p>Trees split on the order of values, so they need no scaling, they shrug off outliers in the features, and they handle a mix of large and small numbers without complaint. That’s why tree-based models are the default for tabular data in practice, as long as you use many of them together.</p>\n</aside>\n<p>That instability is also an opportunity. If one tree is noisy, average many trees grown on slightly different data and the noise cancels out. That’s the idea behind random forests and gradient boosting, next.</p>\n","toc":[{"id":"sec-1","text":"Twenty questions, chosen by data","level":2},{"id":"sec-2","text":"How a tree predicts probabilities","level":2},{"id":"sec-3","text":"Growing too far, and how to stop it","level":2}],"takeaways":["A decision tree splits the data with one yes/no question at a time, choosing at each step the question that makes the groups purest.","A fitted tree is a set of readable rules; <code dir=\"ltr\">export_text</code> prints them.","Unconstrained trees grow until every leaf is pure, which memorises the training data and produces overconfident 0 or 1 probabilities.","<code dir=\"ltr\">max_depth</code> and <code dir=\"ltr\">min_samples_leaf</code> limit growth; <code dir=\"ltr\">min_samples_leaf</code> stops the tree from spending a split on a handful of customers.","Trees need no feature scaling and capture interactions automatically, but a single tree is unstable: small data changes can reshape it."],"further":[{"title":"Decision Trees","url":"https://scikit-learn.org/stable/modules/tree.html"},{"title":"export_text","url":"https://scikit-learn.org/stable/modules/generated/sklearn.tree.export_text.html"}],"quiz":[{"q":"<p>A leaf of a churn tree holds 60 customers who stayed and 13 who churned. What does <code dir=\"ltr\">predict_proba</code> return for a customer who lands there?</p>","options":[{"text":"About <code dir=\"ltr\">[0.82, 0.18]</code>: the class shares in that leaf.","why":"Correct. A tree’s probability is the fraction of training customers of each class in the leaf."},{"text":"<code dir=\"ltr\">[1.0, 0.0]</code>, because the leaf’s majority class is 0.","why":"That’s what <code dir=\"ltr\">predict</code> reports as a label. <code dir=\"ltr\">predict_proba</code> keeps the proportions."},{"text":"<code dir=\"ltr\">[0.5, 0.5]</code>, because trees don’t estimate probabilities.","why":"Trees do estimate them, from leaf proportions, though coarsely."},{"text":"It depends on how far the customer is from the split threshold.","why":"Inside a leaf every customer gets the same answer; distance from the threshold plays no part."}],"answer":0},{"q":"<p>An unlimited tree’s test predictions contain only two distinct probabilities, 0.0 and 1.0. Why is that a problem?</p>","options":[{"text":"It means the tree has a bug and must be refitted with another <code dir=\"ltr\">random_state</code>.","why":"It’s expected behaviour, not a bug. Every leaf of a fully grown tree is pure."},{"text":"Probabilities of exactly 0 or 1 can’t be used for ranking customers.","why":"They can be sorted, but with only two values almost every customer ties, so the ranking carries no information."},{"text":"The tree claims total certainty about every customer, which its test accuracy of 0.72 shows is false.","why":"Correct. Pure leaves produce overconfident probabilities, and the ranking you’d build from them is nearly useless."}],"answer":2},{"q":"<p>Which is a genuine advantage of trees over logistic regression for the churn data?</p>","options":[{"text":"Trees always generalise better because they can model curves.","why":"Flexibility cuts both ways. On this data a constrained logistic regression does as well or better."},{"text":"Trees give smoother, better-calibrated probabilities.","why":"Tree probabilities are coarse leaf proportions, usually less smooth than logistic regression’s."},{"text":"Trees are stable; retraining on slightly different data gives nearly the same tree.","why":"The opposite is true. A different first split changes everything below it."},{"text":"Trees capture interactions such as “few orders matters only for recent customers” without you creating the feature.","why":"Correct. Each branch asks its own follow-up questions, so interactions come for free."}],"answer":3}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.tree import DecisionTreeClassifier, export_text\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\ntree = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)\nprint(export_text(tree, feature_names=features))\n\n# 1. Rewrite the printed tree as plain Python rules. Return 1 for churn, 0 for stay.\ndef rule_predict(row):\n    if row[\"days_since_last_order\"] <= 236.5:\n        return 1\n    return 1\n\nrule_preds = [rule_predict(row) for _, row in X_test.iterrows()]\n\n# 2. A tree limited by min_samples_leaf=20 (random_state=42), fitted on the training data\nleafy = None\n\n# 3. The name of the feature with the highest importance in `leafy`\ntop_feature = None\n","solution":"import pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.tree import DecisionTreeClassifier, export_text\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\ntree = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)\nprint(export_text(tree, feature_names=features))\n\ndef rule_predict(row):\n    if row[\"days_since_last_order\"] <= 236.5:\n        if row[\"orders\"] <= 3.5:\n            return 1\n        return 0\n    if row[\"tenure_days\"] <= 637.5:\n        return 1\n    return 0\n\nrule_preds = [rule_predict(row) for _, row in X_test.iterrows()]\n\nleafy = DecisionTreeClassifier(min_samples_leaf=20, random_state=42).fit(X_train, y_train)\nimportances = pd.Series(leafy.feature_importances_, index=features)\ntop_feature = importances.idxmax()\nprint(top_feature)\n","checks":[{"id":"rules-test","test":"list(map(int, rule_preds)) == tree.predict(X_test).tolist()"},{"id":"rules-train","test":"[int(rule_predict(r)) for _, r in X_train.iterrows()] == tree.predict(X_train).tolist()"},{"id":"leafy","test":"leafy is not None and hasattr(leafy, 'tree_') and leafy.min_samples_leaf == 20 and leafy.get_n_leaves() == __import__('sklearn.tree', fromlist=['x']).DecisionTreeClassifier(min_samples_leaf=20, random_state=42).fit(X_train, y_train).get_n_leaves()"},{"id":"top-feature","test":"top_feature == 'days_since_last_order'"}],"title":"Turn a tree into rules","prompt":"<p>A tree is a stack of if/else statements. Prove it.</p>\n<ol>\n<li>Run the code to print the depth-2 tree. Complete <code dir=\"ltr\">rule_predict(row)</code> so it returns exactly what the tree predicts (1 = churn, 0 = stay) for any customer row. The checks compare it with <code dir=\"ltr\">tree.predict</code> on every training and test row.</li>\n<li><code dir=\"ltr\">leafy</code>: a <code dir=\"ltr\">DecisionTreeClassifier(min_samples_leaf=20, random_state=42)</code> fitted on the training data.</li>\n<li><code dir=\"ltr\">top_feature</code>: the name of the feature with the largest <code dir=\"ltr\">feature_importances_</code> value in <code dir=\"ltr\">leafy</code>.</li>\n</ol>\n","hints":["The printed tree has two levels. Each <code dir=\"ltr\">|---</code> line is a condition; the <code dir=\"ltr\">class:</code> lines are the return values.","Under <code dir=\"ltr\">days_since_last_order &lt;= 236.5</code> the tree asks about <code dir=\"ltr\">orders</code>; under the other branch it asks about <code dir=\"ltr\">tenure_days</code>.","<code dir=\"ltr\">pd.Series(leafy.feature_importances_, index=features).idxmax()</code> returns the top feature’s name."],"explanation":"<p>Four leaves become four <code dir=\"ltr\">return</code> statements. The recent-customer branch depends on order count, and the long-silent branch is almost always churn except for the tiny group of three very long-tenured customers. That’s the interaction the tree found on its own. Code like this could run in a SQL <code dir=\"ltr\">CASE</code> expression, which is one reason shallow trees are easy to deploy. <code dir=\"ltr\">days_since_last_order</code> dominates the importances of the bigger tree too.</p>\n","checkLabels":{"rules-test":"rule_predict matches the tree on every test customer","rules-train":"rule_predict matches the tree on every training customer","leafy":"leafy is fitted with min_samples_leaf=20","top-feature":"top_feature names the most important feature"}},"words":711,"readMinutes":3}