Many small corrections, added together
Think back to the last lesson. A random forest grows lots of trees at the same time, each looking at the problem a little differently, and then it averages their votes — like asking a whole room for their opinion at once.
Gradient Boosting is different. It works one teammate at a time, in a line. Tree 1 makes a first rough guess. Tree 2 looks only at where tree 1 went wrong and learns a small correction. Tree 3 fixes whatever is still off. Each new tree studies the leftover mistakes and patches them, and their small fixes add up into a sharp final answer.
A random forest is a crowd voting all at once. Gradient Boosting is a relay team where each runner studies the previous runner's mistakes and closes the gap a little more.
This model has two main dials you can turn. Here is what each one does in plain language.
Number of rounds
How many correction trees join the relay. A few teammates might not fix enough; a huge number can just memorise the practice data and waste time. This is the n_estimators dial.
Size of each correction
How big a step each new teammate is allowed to take. Small careful steps are safer but usually need more teammates to get there. This is the learning_rate dial.
These two dials pull against each other: gentle steps (a small learning rate) usually want more rounds, while bigger steps want fewer. In Step 2 you will let cross-validation try several combinations and pick the pair that works for your data.
Gradient Boosting works for both jobs — sorting things into groups (classification) and predicting a number (regression). It does not care whether your numbers are on the same scale, so we skip that step, but your shared preprocessor still quietly fills in missing values and turns text categories into numbers behind the scenes.
Set up the model for your task
This cell first does a quick safety check that your setup cells ran, then builds a pipeline: the cleaning steps and the model wrapped together so they always run in the right order. It automatically picks the right version of Gradient Boosting for your task — the sorting-into-groups version or the predicting-a-number version.
A pipeline is like a coffee machine: you pour in raw beans (your data), and grinding, brewing, and pouring all happen in one go — you never have to remember the order.
What this does: Confirms your setup cells ran, then builds the Gradient Boosting model paired with your data-cleaning steps.
# Check that the shared foundation is available.
required_names = [
"TASK", "X_train", "X_test", "y_train", "y_test",
"make_preprocessor", "make_cv", "PRIMARY_SCORING",
"CV_FIT_PARAMS", "evaluate_predictions", "results_log",
]
missing_names = [name for name in required_names if name not in globals()]
if missing_names:
raise RuntimeError(
"Run the Start Here foundation cells first. Missing: "
+ ", ".join(missing_names)
)
from sklearn.ensemble import (
GradientBoostingClassifier,
GradientBoostingRegressor,
)
from sklearn.pipeline import Pipeline
if TASK == "classification":
gradient_estimator = GradientBoostingClassifier(
random_state=RANDOM_STATE
)
else:
gradient_estimator = GradientBoostingRegressor(
random_state=RANDOM_STATE
)
gradient_pipeline = Pipeline([
("prepare", make_preprocessor(scale_numeric=False)),
("model", gradient_estimator),
])
print("Pipeline ready for:", TASK)
A short line like Pipeline ready for: classification (or regression). That should match the task you chose in your setup cells. If a red error appears instead saying to run the Start Here cells, that is just the safety check being helpful — click Runtime → Run all and try again. If the printed task is not the one you meant, fix it back in your setup cell rather than here.
Let the model try dial settings on practice data
Now we let the computer try several combinations of the two dials and see which works best — but only using cross-validation on the practice data. Your real test rows stay sealed in an envelope, untouched, so the final grade later is honest.
Cross-validation is like taking several small practice quizzes using rows the model has not seen yet, then averaging the scores. It tells you how the settings tend to do — without ever peeking at the real exam.
Each combination of "number of rounds" and "size of each correction" is one hyperparameter choice. Start with the modest set of options below; if your dataset is large this can take a few minutes, which is completely normal — let it run.
What this does: Quietly tries several dial settings on practice data and keeps the combination that scored best.
from sklearn.model_selection import GridSearchCV
parameter_grid = {
"model__n_estimators": [50, 100, 200],
"model__learning_rate": [0.03, 0.1, 0.3],
"model__max_depth": [2, 3],
}
gradient_search = GridSearchCV(
estimator=gradient_pipeline,
param_grid=parameter_grid,
scoring=PRIMARY_SCORING,
cv=make_cv(),
n_jobs=-1,
refit=True,
)
gradient_search.fit(X_train, y_train, **CV_FIT_PARAMS)
print("Best training-CV settings:")
for name, value in gradient_search.best_params_.items():
print(f" {name}: {value}")
if TASK == "classification":
print(f"Best mean CV balanced accuracy: {gradient_search.best_score_:.3f}")
else:
print(f"Best mean CV MAE: {-gradient_search.best_score_:.3f}")
The winning dial settings printed out (which number of rounds, which correction size, which tree depth), plus the average practice-quiz score for that winner. If your project is a sorting-into-groups task you will see a balanced-accuracy score (higher is better); if it is a predict-a-number task you will see a line labelled MAE. Jot the winning pair down. Do not assume "more rounds" or "bigger steps" is automatically better — the whole point of this search is to let your data decide. Your own numbers will look different from anyone else's, and that is fine.
MAE stands for "mean absolute error." It is simply the average size of the model's misses, measured in the same units as the thing you are predicting. If you are predicting a score out of 100 and the MAE is about 8, the model is off by roughly 8 points on a typical row. Smaller is better.
If this search feels too slow
No problem — you can hand-edit the lists in the cell to try fewer options: [50, 100] for the rounds, [0.05, 0.1] for the correction size, and [2] for depth. Trimming the search to fit your time is a normal, sensible choice. The one thing to keep is the honest practice-then-test approach — never skip validation just to go faster.
Open the sealed test — just once
All the choosing is done. Now the winning model sits the exact same held-out exam that your baseline and your earlier models took. We open that envelope only once so the score stays trustworthy.
The test rows are like exam questions the model has never seen. Grading it on those is the only fair way to tell whether it really learned something or just memorised the practice sheet.
What this does: Asks the winning model to predict the sealed test rows and shows how well those predictions match the real answers.
best_gradient = gradient_search.best_estimator_
gradient_predictions = best_gradient.predict(X_test)
evaluate_predictions(
"Gradient Boosting",
y_test,
gradient_predictions,
show_details=True,
)
A score for Gradient Boosting on your test rows. On the example study-habits data, a classification run lands around 70% accuracy — comfortably above the roughly 64% baseline that just guesses the most common answer. Your own data will give a different number, and that is expected. Compare it to your baseline using the measure that matters for your problem. If Gradient Boosting does not win, that is still a real, useful result — not a broken lesson.
- How much better is this than your baseline — a lot, a little, or not at all?
- Which group, or which range of numbers, does it still get wrong most often?
- Would that kind of mistake be acceptable in the real decision you wrote down at the start?
See how each dial setting did
Before you opened the test, the search quietly quizzed every dial combination on practice data. This cell lays those results out in a table, best at the top, so you can see the pattern instead of guessing.
What this does: Shows a tidy table of every dial combination the search tried and how each one scored on the practice quizzes.
cv_results = pd.DataFrame(gradient_search.cv_results_)
columns_to_show = [
"param_model__n_estimators",
"param_model__learning_rate",
"param_model__max_depth",
"mean_test_score",
"std_test_score",
"mean_fit_time",
]
cv_summary = cv_results[columns_to_show].copy()
if TASK == "regression":
cv_summary["mean_test_score"] *= -1
cv_summary = cv_summary.rename(
columns={"mean_test_score": "mean_validation_MAE"}
)
cv_summary = cv_summary.sort_values("mean_validation_MAE")
else:
cv_summary = cv_summary.rename(
columns={"mean_test_score": "mean_validation_balanced_accuracy"}
)
cv_summary = cv_summary.sort_values(
"mean_validation_balanced_accuracy", ascending=False
)
display(cv_summary.head(10))
A table of up to ten dial combinations sorted from best to worst, each with its average practice score, how much that score wobbled between quizzes, and roughly how long it took to train. Look past just the top row: if several settings score almost the same, it is wiser to pick the smaller or faster one than to treat a tiny difference as if it were meaningful.
See which columns the model leans on
Which of your columns actually helped? This cell finds out with a clever trick called permutation importance: it scrambles one column at a time and watches how much the score falls. If scrambling a column barely changes the score, the model was not really using it. If the score collapses, that column was doing a lot of work.
Imagine covering up one column with correction tape and asking the model to guess again. The columns that make its answers fall apart when hidden are the ones it truly relies on.
What this does: Scrambles each column in turn, measures how much the score drops, and draws a bar chart ranking your columns from most to least useful.
from sklearn.inspection import permutation_importance
gradient_importance = permutation_importance(
best_gradient,
X_test,
y_test,
scoring=PRIMARY_SCORING,
n_repeats=5,
random_state=RANDOM_STATE,
n_jobs=-1,
)
importance_table = pd.DataFrame({
"feature": X_test.columns,
"importance": gradient_importance.importances_mean,
"variation": gradient_importance.importances_std,
}).sort_values("importance", ascending=False)
display(importance_table)
importance_table.sort_values("importance").plot.barh(
x="feature",
y="importance",
xerr="variation",
legend=False,
figsize=(8, max(4, len(importance_table) * 0.35)),
)
plt.xlabel("Score lost after shuffling this feature")
plt.title("Gradient Boosting permutation importance")
plt.tight_layout()
plt.show()
A table and a bar chart of your columns, longest bars on top. The little error whiskers on each bar show how much the result wobbled across repeated shuffles. On the example study data, a column like previous score or attendance tends to matter most — but your own columns will rank differently, and there is no "correct" ranking.
Important does not mean it causes the outcome
A column can look important just because it travels alongside the real cause, or because it quietly leaks a hint about the answer. When two columns say similar things, they may also split the credit between them. So read this chart as "what this particular model leaned on," not as a law about how the world works.
Your own scoreboard for the whole course
This is the moment the course has been building toward. Your results_log has been quietly collecting a row for every model you tried in this notebook — the baseline and each lesson — all graded on the very same sealed test. This cell lays them out as one scoreboard so you can finally compare them fairly.
Because every model sat the exact same exam on the exact same rows, this is a fair comparison. And the "winner" is not a universal ranking of algorithms — it is whichever model fits your data and cares most about the mistake that would cost you most.
What this does: Gathers every model you have run into one table and sorts it, best first, by the measure that fits your task.
leaderboard = pd.DataFrame(results_log)
if TASK == "classification":
leaderboard = leaderboard.sort_values(
"balanced_accuracy", ascending=False
)
else:
leaderboard = leaderboard.sort_values("MAE")
display(leaderboard.reset_index(drop=True))
One row per model you actually ran — your baseline plus every lesson you finished — sorted best to worst. It only holds your models on your data; there is no course-wide score to compare against and nothing trained on someone else's dataset. Your score is evidence about your problem, not a grade, so treat the top row as a candidate to examine rather than an automatic winner.
Before you crown a winner, it helps to ask a few honest questions. The highest score is not always the best choice — here is why the answer can change once you look past the number.
| Question | Why it can change the winner |
|---|---|
| Does it beat the baseline by enough to matter? | A measurable gain may still be too small to justify operational complexity. |
| Which mistake does it favour? | Two models with similar headline scores can create very different real-world costs. |
| Can the decision be explained? | A slightly weaker linear model or small tree may be preferable when people must challenge decisions. |
| How expensive is prediction and retraining? | Latency, memory, energy, and maintenance are part of model quality. |
| Will the data process change? | The best historical model can fail first when categories, behaviour, or measurement rules drift. |
A peek at the model's "how sure" numbers
This one is optional — skip it with a clear conscience if you are ready to wrap up. If your project is a classification (sorting-into-groups) task, the model can also tell you how strongly it leans toward each group for a given row. These are handy for ranking rows by how sure the model seems, but they are estimates, not guarantees, so never read them as certainty.
What this does: For a classification project, shows the first ten test rows next to the true answer, the model's guess, and how strongly it leaned toward that guess.
if TASK != "classification":
print("Probability output applies to classification only.")
else:
class_names = best_gradient.named_steps["model"].classes_
probabilities = best_gradient.predict_proba(X_test)
probability_preview = X_test.head(10).copy()
probability_preview["truth"] = y_test.head(10).values
probability_preview["prediction"] = gradient_predictions[:10]
probability_preview["largest_estimated_probability"] = (
probabilities[:10].max(axis=1)
)
print("Probability column order:", list(class_names))
display(probability_preview)
If your task is regression, a short note that this preview is for classification only — nothing to worry about. If it is classification, a small table of ten rows showing the truth, the model's guess, and its strongest "how sure" number for each. Your own rows will differ, of course.
Going deeper (optional): why we say "estimated" and not "confident"
A model can print 0.90 for a row and still turn out right far less than 90% of the time. Whether those numbers can be trusted at face value is a separate question — you would have to check it, on its own, using validation data. So the number describes the model's internal scale, not how trustworthy the data or the real world actually is. When in doubt, treat it as a ranking hint, not a promise.
The real lesson was the routine, not any one model
Here is the honest secret of the whole course: the individual models matter less than the steady routine you now know by heart. If you can follow these seven steps, you can pick up almost any new model and use it safely.
- Decide what one row means, what you are predicting, which columns you get to use, and which mistake would hurt most.
- Split the data in a way that mirrors how it will really be used — by chance, by time, or by group.
- Learn all cleaning steps from the practice data only, kept inside a pipeline so nothing leaks.
- Build a plain baseline first, so every fancy model has something honest to beat.
- Tune the dials with cross-validation on practice data — never on the final test.
- Open the test just once, and look at where it goes wrong, not only the headline score.
- Pick the model whose trade-offs actually fit the decision you have to make.
- Which model would you pick today, and what is your reason?
- What would you need to see to change your mind?
- What new data would be worth collecting next?
- Who could be hurt if a prediction is wrong, and how would you catch it?
YOUR PACE, YOUR PROJECT
You did it — save your notebook and your final note.
Your scoreboard is a snapshot of one dataset and one split, not a universal ranking of models. That is exactly why it is yours. Well done getting all the way through.