{"id":"l-ml-3-3","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Combine many trees with random forests and gradient boosting, use HistGradientBoostingClassifier’s built-in handling of missing values, and measure feature importance with permutation importance.","html":"<p>A single decision tree is easy to read and easy to break: change a few training rows and the branches rearrange. The cure is an old statistical trick. Ask many independent, slightly different estimators and combine their answers. The errors of each one partly cancel, and what’s left is more reliable than any single member. Models built this way are <strong>ensembles</strong>, and for tabular data like churn they’re what most practitioners reach for after the baselines and a linear model.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Random forests: many trees, averaged</h2>\n<p>A random forest grows a few hundred deep trees, each on a different <strong>bootstrap sample</strong> (the training rows sampled with replacement, so each tree sees a slightly different dataset) and, at every split, only a random subset of the features. That second trick stops every tree from opening with the same dominant feature, which makes the trees more different from each other and their mistakes less correlated. To predict, the forest averages the trees’ probabilities.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Gradient boosting: trees that fix each other</h2>\n<p>Boosting takes the opposite approach. It builds shallow trees one at a time. The first makes rough predictions; the second is fitted to the errors the first one left; the third to what the first two still get wrong, and so on. Each tree’s contribution is scaled down by the <code dir=\"ltr\">learning_rate</code>, so the ensemble creeps towards a good answer instead of lurching. More trees (<code dir=\"ltr\">max_iter</code>) and a higher learning rate fit the training data more closely, and eventually overfit it.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-ml-3-3-t1\"><defs><marker id=\"arrow-l-ml-3-3-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-3-t1\">Top row, random forest: three trees trained independently on different samples feed into an average. Bottom row, gradient boosting: tree 1 feeds its errors to tree 2, which feeds its remaining errors to tree 3, and their scaled outputs are summed.</title>\n  <text class=\"d-label-strong\" x=\"10\" y=\"24\">Random forest (bagging)</text>\n  <rect class=\"d-box\" x=\"20\" y=\"40\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"75\" y=\"65\" text-anchor=\"middle\">tree on sample 1</text>\n  <rect class=\"d-box\" x=\"150\" y=\"40\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"205\" y=\"65\" text-anchor=\"middle\">tree on sample 2</text>\n  <rect class=\"d-box\" x=\"280\" y=\"40\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"335\" y=\"65\" text-anchor=\"middle\">tree on sample 3</text>\n  <path class=\"d-arrow\" d=\"M390 60 L480 60\" marker-end=\"url(#arrow-l-ml-3-3-1)\"/>\n  <rect class=\"d-box-primary\" x=\"485\" y=\"40\" width=\"190\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"580\" y=\"65\" text-anchor=\"middle\">average the votes</text>\n  <text class=\"d-label-strong\" x=\"10\" y=\"140\">Gradient boosting</text>\n  <rect class=\"d-box\" x=\"20\" y=\"160\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"75\" y=\"185\" text-anchor=\"middle\">tree 1</text>\n  <path class=\"d-arrow\" d=\"M130 180 L150 180\" marker-end=\"url(#arrow-l-ml-3-3-1)\"/>\n  <rect class=\"d-box\" x=\"155\" y=\"160\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"210\" y=\"185\" text-anchor=\"middle\">tree 2 on errors</text>\n  <path class=\"d-arrow\" d=\"M265 180 L285 180\" marker-end=\"url(#arrow-l-ml-3-3-1)\"/>\n  <rect class=\"d-box\" x=\"290\" y=\"160\" width=\"110\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"345\" y=\"185\" text-anchor=\"middle\">tree 3 on errors</text>\n  <path class=\"d-arrow\" d=\"M400 180 L480 180\" marker-end=\"url(#arrow-l-ml-3-3-1)\"/>\n  <rect class=\"d-box-accent\" x=\"485\" y=\"160\" width=\"190\" height=\"40\" rx=\"8\"/><text class=\"d-label\" x=\"580\" y=\"185\" text-anchor=\"middle\">sum, scaled by learning rate</text>\n  <text class=\"d-label-muted\" x=\"210\" y=\"228\" text-anchor=\"middle\">each tree depends on the ones before it</text>\n</svg><figcaption>Bagging averages in parallel; boosting corrects in sequence</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Both on churn, with the missing column back</h2>\n<p>So far you’ve left out <code dir=\"ltr\">avg_satisfaction</code> because half its values are blank. <code dir=\"ltr\">HistGradientBoostingClassifier</code>, scikit-learn’s fast boosting implementation, handles missing values natively: at each split it learns which side the blanks should go to. Recent versions of <code dir=\"ltr\">DecisionTreeClassifier</code> and <code dir=\"ltr\">RandomForestClassifier</code> accept NaN too. <code dir=\"ltr\">LogisticRegression</code> and k-NN still don’t; they raise <code dir=\"ltr\">ValueError: Input X contains NaN</code>.</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.ensemble </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> HistGradientBoostingClassifier, RandomForestClassifier</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.linear_model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LogisticRegression</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 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:#0A3069;--shiki-dark:#A5D6FF\">            \"avg_satisfaction\"</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\">no_gaps </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> features[:</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\">]  </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># logistic regression can't take the NaNs in avg_satisfaction</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">models </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"logistic (no satisfaction)\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (make_pipeline(StandardScaler(), LogisticRegression()), no_gaps),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"random forest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (RandomForestClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_estimators</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">min_samples_leaf</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><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\">), features),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"boosting, defaults\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (HistGradientBoostingClassifier(</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\">), features),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"boosting, gentle\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: (HistGradientBoostingClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">max_depth</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">learning_rate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">                                                        max_iter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</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\">), features),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> name, (model, cols) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> models.items():</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    model.fit(X_train[cols], 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:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:27}</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\">model.score(X_train[cols], 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\">model.score(X_test[cols], y_test)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>The forest reaches 0.76 and gently tuned boosting 0.78, against 0.72 for logistic regression and the rule. Boosting with default settings fits the training set almost perfectly (0.99) and does worse than the gentle version: on 298 rows, the defaults are too eager. Shallower trees and a smaller learning rate keep it in check.</p>\n<p>Hold these numbers loosely. On a 100-customer test set, two points is two customers. The next lesson adds a metric for ranking quality, and section 4’s cross-validation will tell you whether the ensembles’ edge survives a different shuffle.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">What do the trees rely on?</h2>\n<p>Forests offer <code dir=\"ltr\">feature_importances_</code>, but those numbers are computed on the training data from how much each feature reduced impurity, and they’re biased towards continuous features with many possible thresholds. <strong>Permutation importance</strong> asks a sharper question on held-out data: if I shuffle this one column, breaking its link with the target, how much does the score drop?</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.ensemble </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> RandomForestClassifier</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.inspection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> permutation_importance</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">features </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_order_value\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">            \"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"used_coupon\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"support_tickets\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">            \"avg_satisfaction\"</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\">forest </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> RandomForestClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_estimators</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">min_samples_leaf</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit(X_train, y_train)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">result </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> permutation_importance(forest, X_test, y_test, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_repeats</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">table </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame({</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"impurity (train)\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: forest.feature_importances_,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">                      \"permutation (test)\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: result.importances_mean}, </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)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(table.sort_values(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"permutation (test)\"</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\">).round(</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>Impurity importance ranks <code dir=\"ltr\">tenure_days</code> second. Permutation importance on the test set says it barely matters: the forest used it to fit noise in the training data. Shuffling recency costs about 13 points of accuracy; shuffling satisfaction costs nothing (the small negative values are noise). The honest summary for Cartwheel: recency and order count drive the predictions, and the satisfaction survey, in its current half-empty state, adds nothing.</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>Reporting impurity importances as “what drives churn”</span></p>\n<p>Impurity importances describe how one model grew its trees on training data. They inflate continuous, high-cardinality features and split credit arbitrarily between correlated ones. When a stakeholder asks what matters, use permutation importance on held-out data, and still phrase the answer as “what the model relies on”, not “what causes churn”.</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>Default choices for tabular data</span></p>\n<p>After baselines and a logistic regression, try <code dir=\"ltr\">HistGradientBoostingClassifier</code> with a modest <code dir=\"ltr\">learning_rate</code> (0.05–0.1) and shallow trees. It’s fast, handles missing values, and is hard to beat on tables. Use <code dir=\"ltr\">RandomForestClassifier</code> when you want a strong model with almost no tuning.</p>\n</aside>\n<p>Accuracy has carried you this far, but it treats a missed churner and a false alarm as equally bad, and it ignores the ranking you built for marketing. Next lesson replaces it with metrics that look at each kind of mistake separately.</p>\n","toc":[{"id":"sec-1","text":"Random forests: many trees, averaged","level":2},{"id":"sec-2","text":"Gradient boosting: trees that fix each other","level":2},{"id":"sec-3","text":"Both on churn, with the missing column back","level":2},{"id":"sec-4","text":"What do the trees rely on?","level":2}],"takeaways":["A random forest trains many deep trees on bootstrap samples with random feature subsets and averages their votes, which cancels much of a single tree’s variance.","Gradient boosting builds shallow trees one after another, each fitted to the errors of the ensemble so far; <code dir=\"ltr\">learning_rate</code> and <code dir=\"ltr\">max_iter</code> trade off speed and overfitting.","<code dir=\"ltr\">HistGradientBoostingClassifier</code> is scikit-learn’s fast boosting model and accepts missing values (NaN) directly.","Impurity-based <code dir=\"ltr\">feature_importances_</code> favour features with many distinct values; permutation importance on held-out data measures what the model really relies on.","On small tabular datasets, ensembles often add only a few points over a good linear model, so compare them against it rather than assuming they win."],"further":[{"title":"Ensembles: Gradient boosting, random forests, bagging","url":"https://scikit-learn.org/stable/modules/ensemble.html"},{"title":"HistGradientBoostingClassifier","url":"https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.HistGradientBoostingClassifier.html"},{"title":"Permutation feature importance","url":"https://scikit-learn.org/stable/modules/permutation_importance.html"}],"quiz":[{"q":"<p>Why does a random forest usually beat a single unconstrained tree on new data?</p>","options":[{"text":"Each of its trees is shallower and therefore cannot overfit.","why":"Forest trees are typically grown deep. The benefit comes from averaging, not from limiting each tree."},{"text":"It uses gradient descent to tune the trees together.","why":"Forest trees are trained independently; nothing ties them together during training."},{"text":"It trains on more data than the single tree.","why":"Each tree sees a bootstrap sample of the same training set, so no extra data is involved."},{"text":"Averaging many trees trained on different samples and feature subsets cancels out their individual mistakes.","why":"Correct. Each tree overfits in its own way; the errors are partly independent, so the average is more stable."}],"answer":3},{"q":"<p>In gradient boosting, what does each new tree learn?</p>","options":[{"text":"To correct the errors that the ensemble built so far still makes.","why":"Correct. Boosting is sequential; every tree targets what’s left over."},{"text":"The same target as the first tree, on a fresh bootstrap sample.","why":"That describes bagging, the idea behind random forests."},{"text":"A random subset of the features, ignoring previous trees.","why":"Random feature subsets are a forest trick. Boosting trees depend on the trees before them."}],"answer":0},{"q":"<p><code dir=\"ltr\">avg_satisfaction</code> is blank for half the customers. Which model can you fit on it with no extra preprocessing?</p>","options":[{"text":"<code dir=\"ltr\">LogisticRegression</code>, because it ignores missing values.","why":"It raises <code dir=\"ltr\">ValueError: Input X contains NaN</code>. You would need an imputer first."},{"text":"<code dir=\"ltr\">HistGradientBoostingClassifier</code>, which learns which branch missing values should follow at each split.","why":"Correct. Native NaN support is one of its main practical advantages."},{"text":"<code dir=\"ltr\">KNeighborsClassifier</code>, because it skips missing columns when measuring distance.","why":"k-NN raises an error on NaN too; distances can’t be computed with gaps."}],"answer":1},{"q":"<p>A forest’s <code dir=\"ltr\">feature_importances_</code> ranks <code dir=\"ltr\">tenure_days</code> second, but permutation importance on the test set gives it almost zero. What’s the most likely explanation?</p>","options":[{"text":"Permutation importance is broken for random forests.","why":"It works for any model; it only needs predictions and a score."},{"text":"The test set is too small to contain <code dir=\"ltr\">tenure_days</code>.","why":"Every row has the feature. Small test sets add noise, but this pattern has a known cause."},{"text":"The forest used <code dir=\"ltr\">tenure_days</code> in many splits that fit training noise, which inflates impurity importance but doesn’t help on new data.","why":"Correct. Impurity importance is computed on training data and favours continuous features with many possible thresholds."},{"text":"Shuffling <code dir=\"ltr\">tenure_days</code> made the model more accurate, which proves the feature is harmful.","why":"A near-zero result means shuffling changed little, not that the model improved."}],"answer":2}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import pandas as pd\nfrom sklearn.ensemble import HistGradientBoostingClassifier\nfrom sklearn.inspection import permutation_importance\nfrom sklearn.model_selection import train_test_split\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\",\n            \"avg_satisfaction\"]\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. Gradient boosting on all nine features (missing values included):\n#    max_depth=3, learning_rate=0.05, max_iter=200, random_state=42\nbooster = HistGradientBoostingClassifier(random_state=42)\nbooster.fit(X_train.drop(columns=\"avg_satisfaction\"), y_train)\n\n# 2. Test accuracy\ntest_acc = None\n\n# 3. Permutation importance on the TEST set (n_repeats=5, random_state=0),\n#    then the feature names sorted from most to least important\nranking = []\n","solution":"import pandas as pd\nfrom sklearn.ensemble import HistGradientBoostingClassifier\nfrom sklearn.inspection import permutation_importance\nfrom sklearn.model_selection import train_test_split\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\",\n            \"avg_satisfaction\"]\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\nbooster = HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200,\n                                         random_state=42)\nbooster.fit(X_train, y_train)\n\ntest_acc = booster.score(X_test, y_test)\n\nresult = permutation_importance(booster, X_test, y_test, n_repeats=5, random_state=0)\nranking = pd.Series(result.importances_mean, index=features).sort_values(ascending=False).index.tolist()\nprint(test_acc, ranking[:3])\n","checks":[{"id":"nine-features","test":"booster.n_features_in_ == 9"},{"id":"settings","test":"booster.max_depth == 3 and booster.learning_rate == 0.05 and booster.max_iter == 200"},{"id":"accuracy","test":"test_acc is not None and 0.74 <= float(test_acc) <= 0.82 and abs(float(test_acc) - booster.score(X_test, y_test)) < 1e-9"},{"id":"ranking","test":"len(ranking) == 9 and set(ranking) == set(features) and ranking[0] == 'days_since_last_order'"}],"title":"Boost with the gaps left in","prompt":"<p>Fit a gradient boosting model on all nine features, including the half-empty <code dir=\"ltr\">avg_satisfaction</code>, and find out what it relies on.</p>\n<ol>\n<li><code dir=\"ltr\">booster</code>: <code dir=\"ltr\">HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200, random_state=42)</code> fitted on all of <code dir=\"ltr\">X_train</code> (no columns dropped).</li>\n<li><code dir=\"ltr\">test_acc</code>: its test accuracy.</li>\n<li><code dir=\"ltr\">ranking</code>: a list of the nine feature names sorted by permutation importance on the test set (<code dir=\"ltr\">n_repeats=5, random_state=0</code>), most important first.</li>\n</ol>\n","hints":["No imputation is needed: <code dir=\"ltr\">HistGradientBoostingClassifier</code> routes missing values itself.","<code dir=\"ltr\">permutation_importance(booster, X_test, y_test, n_repeats=5, random_state=0)</code> returns an object with <code dir=\"ltr\">importances_mean</code>.","Wrap the means in a <code dir=\"ltr\">pd.Series</code> indexed by <code dir=\"ltr\">features</code>, sort descending, and take <code dir=\"ltr\">.index.tolist()</code>."],"explanation":"<p>Gentle boosting lands around 0.78 on the test set, a few points above logistic regression. Permutation importance puts <code dir=\"ltr\">days_since_last_order</code> far ahead of everything else, and <code dir=\"ltr\">avg_satisfaction</code> near zero or below: the model can use the column, but the survey as collected doesn’t help. Computing importance on the test set measures what generalises, not what the trees memorised.</p>\n","checkLabels":{"nine-features":"The model is trained on all nine features","settings":"The booster uses the gentle settings","accuracy":"test_acc is the test accuracy","ranking":"ranking lists all nine features by permutation importance, recency first"}},"words":692,"readMinutes":3}