TL;DR
- One unpruned decision tree scored 1.0000 on training folds and 0.6222 on held out folds. That gap is the whole reason ensembles exist.
- A random forest averages many overfitted trees. Our forest went from 0.6222 to 0.8156 with no tuning at all.
- Gradient boosting builds trees in sequence, each correcting the last. On 486 rows it did not repay the extra care.
- Impurity based feature importance ranked a random customer ID fourth out of sixteen features. Permutation importance did not fall for it.
- Best ensemble reached 0.8362 AUC against logistic regression at 0.8306. Fifty six ten thousandths, for a model nobody can read.
Gradient boosting is the default winner on tabular data, and on this dataset it lost. Not narrowly, either. Out of the box it scored 0.8104 against a logistic regression that had already posted 0.8306 in Part 11, and it took three rounds of tuning to claw back to roughly where the linear model started.
That result is not a knock on boosting. It is a knock on the habit of reaching for the strongest available model before checking whether the dataset can feed it. Tree ensembles earn their reputation at scale, on wide feature sets with interactions a linear model cannot express. Give one 486 rows and fifteen mostly independent columns and it has very little to find that logistic regression missed. Both facts are worth carrying, and this part demonstrates them on our own data rather than asserting them.
Where the churn project stands after Part 11
Last part we stopped trusting single split scores and built an honest evaluation protocol: five fold stratified cross validation, every fitted step inside the pipeline, thresholds swept on out of fold predictions. That protocol handed us a defensible baseline of 0.8306 AUC with a standard deviation of 0.0253, and an average precision of 0.6121 against a 0.2716 base rate.
This part swaps the model and keeps everything else frozen. Same 486 row extract of the public Telco Customer Churn file, same 132 churners, same six categorical and three numeric columns from Part 6, same folds, same seed. Only the estimator changes. Freezing everything but one variable is the only way a model comparison means anything, and it is a discipline more teams skip than admit.
One decision tree, and how it overfits
A decision tree asks a sequence of yes or no questions about a customer and follows the answers to a leaf, which holds a predicted churn probability. Is the contract month to month? Is tenure below 15? Each split is chosen greedily, meaning the algorithm picks whichever single question best separates churners from stayers right now, then repeats inside each branch without ever reconsidering an earlier choice.
Left unconstrained, splitting continues until every leaf is pure. With 486 customers a tree can memorise all of them, and that is exactly what happens. Watch train and test scores together, which is a habit worth building because a single number cannot show you this failure at all.
Best single tree lands at depth 3 with 0.7953, which loses to logistic regression by three and a half points. Note also how noisy the shallow trees are: a standard deviation of 0.0411 at depth 3 against 0.0253 for the linear model. Trees are high variance estimators. Change a handful of rows and the top split can flip, and every split beneath it changes with it. Instability of that kind is the property ensembles are built to attack, and understanding it makes the rest of this part obvious rather than magical.
Bagging and random forests
Bagging, short for bootstrap aggregating, trains many trees on random samples of the rows drawn with replacement, then averages their predictions. Individual trees stay deep and overfitted on purpose. Because each one overfits a different random sample, their errors are partly independent, and averaging cancels a good share of them. A random forest adds a second source of randomness: at every split each tree may only consider a random subset of the features, which stops every tree from opening with the same dominant column and keeps them genuinely different from one another.
Effect on our data is immediate and large. Trees that individually score 0.62 combine into a forest at 0.8156, beating the best single tree by more than two points and arriving there without a single tuned parameter.
Two practical readings come out of that curve. First, n_estimators is not a parameter you tune, it is a budget you set: pick a number past the elbow and forget it, since more trees never hurt accuracy, only wall clock. Going from 300 to 800 trees cost roughly three times the fit time and moved AUC by minus 0.0018, which is noise pointing the wrong way. Second, and easy to miss, fold to fold variance drops alongside the mean rising. Averaging buys stability as well as accuracy, and stability is what makes a number safe to quote to somebody who will hold you to it.
Gradient boosting, and how it differs from a forest
Boosting inverts the arrangement. Rather than growing many independent trees and averaging, it grows them one after another, each new tree fitted to the errors the ensemble has made so far. Start with a constant prediction, measure how wrong it is on every customer, fit a small tree to those errors, add a shrunken fraction of that tree to the running prediction, measure the new errors, repeat. Gradient refers to nothing more exotic than the direction of those current errors.
Consequences follow directly from the sequence. Boosted trees are deliberately weak, often just a few levels deep, because each one only needs to fix a slice of remaining error. Learning rate controls how much of each tree gets added, defaulting to 0.1 in scikit-learn, and it trades against the number of iterations: halve the rate and you need roughly twice the trees. Most importantly, boosting has no averaging step to protect it, so it will happily keep fitting until it has memorised your noise. Where a forest is close to safe by construction, boosting requires you to say when to stop.
| Model | Train AUC | Test AUC | Fold std | Avg precision | Fit time |
|---|---|---|---|---|---|
| Tree, unpruned | 1.0000 | 0.6222 | 0.0566 | 0.3607 | 0.04s |
| Tree, depth 3 | 0.8495 | 0.7953 | 0.0411 | 0.5126 | 0.04s |
| Logistic regression, Part 11 | n/a | 0.8306 | 0.0253 | 0.6121 | 0.05s |
| Random forest, defaults | 1.0000 | 0.8156 | 0.0252 | 0.6069 | 2.09s |
| Random forest, min_samples_leaf 10 | 0.9031 | 0.8362 | 0.0242 | 0.6415 | 2.13s |
| HistGradientBoosting, defaults | 0.9995 | 0.8104 | 0.0313 | 0.5872 | 0.39s |
| HistGradientBoosting, regularised | 0.9732 | 0.8203 | 0.0343 | 0.6052 | 0.34s |
| HistGradientBoosting, early stopping | 0.9387 | 0.8210 | 0.0271 | 0.5957 | 0.24s |
| GradientBoostingClassifier, defaults | 0.9936 | 0.8139 | 0.0456 | 0.6099 | 0.49s |
Read that table honestly and the headline is uncomfortable for anyone who reaches for boosting reflexively. Every boosting variant lost to logistic regression. Regularising helped, moving 0.8104 to 0.8203, and early stopping landed in the same place at 0.8210 while cutting training AUC from 0.9995 to 0.9387, which is a healthier looking model even though the held out score barely moved. Only the constrained random forest cleared the linear baseline at all, by 0.0056 AUC.
Root cause is sample size, not the algorithm. Boosting fits residual structure, and with 486 rows and 132 positives there is barely any residual structure left over once contract type, tenure and internet service have been accounted for. Feed the same pipeline the full 7,043 row file and the ordering shifts, as it does on most real churn tables. Both results belong in your head at once: boosting usually wins on tabular data, and usually is doing real work in that sentence.
War story
On a pricing model years back I replaced a logistic regression with a boosted ensemble and reported a gain of 1.4 points of AUC. Nobody questioned it, and we shipped. Six weeks later the finance lead asked why a particular account had been scored high risk, and I could not answer in the time she had, because the answer lived across 400 trees. We spent the next sprint bolting on explanation tooling, roughly nine engineering days, to recover something the linear model had given us for free in its coefficients. Model was rolled back the following quarter, not on accuracy grounds but because the review committee would not sign an unexplainable score. What I do now is count the explanation cost as part of the model cost up front, and 1.4 points would not survive that arithmetic today.
One error worth showing, because it catches everybody exactly once. Trees do not need scaling, which tempts people to skip the preprocessing pipeline entirely and hand scikit-learn the raw frame. Categorical columns are still strings, and the message is blunt about it:
Native categorical support is genuinely convenient, and it avoids widening 15 columns into a sparser matrix on higher cardinality data. On our features it scored 0.8181 against 0.8203 for the one hot version, so the convenience is free rather than an accuracy win. Reach for it when a column has dozens of levels, not to squeeze out points.
Feature importance that misleads
Every fitted forest exposes feature_importances_, and reading it is how most people first form an opinion about what drives churn. That attribute measures mean impurity decrease, meaning how much each feature reduced node impurity across all trees. It carries a bias that is well documented and still catches teams constantly: features with many distinct values get more chances to produce a lucky split, so continuous and high cardinality columns score high regardless of whether they mean anything.
Demonstrating this takes one extra column. I added customer_ref, a six digit random integer with no relationship to churn whatsoever, and refitted the constrained forest.
Permutation importance answers a better question. Shuffle one column on held out data, refit nothing, and see how far the score falls. A column the model genuinely relies on will hurt when scrambled; a column it merely split on by accident will not. Two cautions come with it. Run it on test data, since permuting on training data measures memorisation rather than dependence. And treat correlated features carefully, because shuffling one of a correlated pair lets the model recover through the other, so both look unimportant. You can see that above: Contract two year scores 0.0500 on impurity and 0.0091 on permutation, largely because tenure already encodes contract length. Neither method is a causal statement, which is a distinction worth rereading in correlation versus causation before you present importances to anyone who might act on them.
Tuning that paid, and tuning that did not
Across roughly thirty configurations run for this part, gains clustered in one place. Constraining leaf size on the forest, min_samples_leaf moving from the default of 1 to 5 and then 10, produced 0.8156 then 0.8343 then 0.8362. That is the entire story of forest tuning on a dataset this size, and it took under a minute to find. Everything else moved the fourth decimal place.
Boosting was fussier and repaid the fuss less. Dropping learning rate to 0.05 while capping max_leaf_nodes at 15 and adding l2_regularization of 1.0 bought 0.0099 AUC over defaults. Early stopping bought about the same, arrived faster, and produced the more trustworthy training curve. Worth noting that early stopping carves its validation slice out of the training fold, so the reported score stays honest, but you are training on 85 percent of an already small fold. Below a thousand rows I would rather regularise explicitly than let early stopping shrink the training data further.
My take
Fit a random forest with 300 trees and min_samples_leaf around 5 to 10 as your second model, always, immediately after a linear baseline. Two lines, a couple of seconds, and it tells you within one run whether the problem holds interaction structure a linear model cannot reach. If the forest does not clear your baseline by a margin larger than the fold standard deviation, and here it cleared 0.8306 by 0.0056 against a standard deviation of 0.0242, then the honest reading is that the ensemble found nothing. Ship the model you can explain. Everything in Part 11 about comparing differences against fold noise applies with more force here, because ensembles fit harder and therefore fool you harder.
Model I would ship for churn, and the one I would avoid
For this extract I would ship the logistic regression from Part 10 and keep the forest as a diagnostic. Fifty six ten thousandths of AUC does not justify trading coefficients a retention manager can read for an ensemble that needs tooling to explain. Average precision does favour the forest more clearly, 0.6415 against 0.6121, which is a real 5 percent relative improvement in the cleanliness of the flagged list, so on a larger extract I would revisit that call. On the full 7,043 row file I expect the forest to win properly, and the correct move then is to ship it with permutation importances and partial dependence computed as part of the release rather than promised afterwards.
Model I would avoid outright is a single decision tree. It is the one people gravitate to because a tree diagram is easy to show in a meeting, and it was the worst performer here at every depth. Interpretability from a lone tree is largely an illusion anyway, since resampling the data reorders the splits and the picture you presented last month no longer holds. Where you truly need a readable model, a linear one is both more stable and more honest. Between the two boosting implementations, use HistGradientBoostingClassifier rather than the older GradientBoostingClassifier: it was faster here, 0.24 to 0.39 seconds against 0.49, scored better, and it handles categoricals and missing values natively.
Our churn project now has four candidate models scored on identical folds and a clear picture of what each costs to explain. Part 13 turns to unsupervised learning, clustering and dimensionality reduction, where there is no label to score against and evaluation becomes considerably harder than anything in this part. Before moving on, add customer_ref to your own feature frame and compare the two importance rankings yourself. Seeing a random number outrank contract type on your own screen is what makes the lesson stick.
References
- scikit-learn user guide, Ensembles: gradient boosting, random forests, bagging, for the bootstrap and random feature subset description and the recommendation to prefer histogram based boosting on larger samples.
- scikit-learn, HistGradientBoostingClassifier API reference, for the learning_rate default of 0.1, max_iter default of 100, and native categorical_features support.
- scikit-learn user guide, Permutation feature importance, for the impurity bias toward high cardinality features and the warning about correlated predictors.
- scikit-learn, RandomForestClassifier API reference, for min_samples_leaf, max_features and n_estimators defaults.
- Telco Customer Churn dataset, IBM sample data on GitHub, 7043 rows, the file used throughout this series.


DrJha