THE IDEA
How it decides, in everyday terms
Think of the model as a friendly judge weighing evidence. It looks at each clue in a row (each feature), decides how much each clue matters, and adds them up into a single running total. Then it converts that total into a probability — a number between 0 and 1 — that leans toward one group or another. Whichever group ends up most likely becomes its answer.
Each clue nudges the answer a little. A high total pushes toward one group; a low total pushes toward the other. The model smoothly slides from "probably this group" to "probably that group" rather than flipping abruptly — like a dimmer switch, not an on/off switch.
You'll also meet a setting the code calls C. It controls how much freedom the model has to react strongly to any one clue. A smaller C keeps the model calmer and simpler; a larger C lets it react more. Neither is automatically better, so instead of guessing, you'll let the data tell you which works best — using only the practice data, never the final exam.
A probability is a considered opinion, not a promise
When the model says "about 80% likely to pass," treat that as its best-informed guess, not a guarantee. It's a considered opinion from what it has seen so far. It can be off — especially when you have only a little data, when the future turns out different from the past, or when something important simply wasn't recorded.
Get the model ready
This first cell double-checks that your foundation setup ran, then assembles the model so it's ready to learn. It puts two steps together in order: first it puts every number on a comparable footing (called scaling), then it hands the result to the model. Bundling these steps together is called a pipeline.
Scaling means measuring every clue with the same-sized ruler, so a column like "attendance percent" doesn't overpower a column like "sleep hours" just because its numbers happen to be bigger. The pipeline makes sure this same preparation happens every single time, automatically.
What this does: Checks that your setup cells ran, confirms you're doing a category-prediction task, and builds the model together with its number-scaling step.
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
required_foundation = [
"TASK", "RANDOM_STATE", "FEATURES", "X_train", "X_test",
"y_train", "y_test", "make_preprocessor", "make_cv",
"CV_FIT_PARAMS", "PRIMARY_SCORING", "evaluate_predictions",
"results_log",
]
missing_foundation = [
name for name in required_foundation if name not in globals()
]
if missing_foundation:
raise NameError(
"Run every Foundation cell first. Missing: "
+ ", ".join(missing_foundation)
)
if TASK != "classification":
raise ValueError(
"Logistic regression predicts categories. "
"Set TASK to 'classification' or choose a regression lesson."
)
logistic_pipeline = Pipeline([
("prepare", make_preprocessor(scale_numeric=True)),
("model", LogisticRegression(
max_iter=3000,
random_state=RANDOM_STATE,
)),
])
print("Training rows:", len(X_train))
print("Held-out test rows:", len(X_test))
print("Classes in training:", sorted(y_train.unique(), key=str))
A count of your practice rows and your set-aside test rows, plus a list of the groups the model will choose between (on the example students, "passed" is true or false — two groups). Your own data will differ. You just want to see at least two groups here. If a group is missing or has only a single row, that's a gentle nudge to revisit how you defined your target before going further. And if the cell complains that setup is missing, no worries — click Runtime then Run all and try again.
Let the data pick the best setting
Remember that C setting? Rather than guessing it, we try a handful of values and let the practice data show which works best. The trick is a fair, careful trial called cross-validation.
Cross-validation is like giving the model several small practice quizzes. Each time, it studies most of the practice data and gets quizzed on the slice it hasn't seen yet — then we rotate which slice is held back. Averaging those quiz scores gives an honest sense of how each setting performs. Crucially, the real final exam (your test set) stays sealed the whole time.
What this does: Tries five different C values, quizzes each one fairly on the practice data, and keeps the setting that scored best — then retrains that winner on all the practice data.
search = GridSearchCV(
estimator=logistic_pipeline,
param_grid={
"model__C": [0.01, 0.1, 1.0, 10.0, 100.0],
},
scoring=PRIMARY_SCORING,
cv=make_cv(),
n_jobs=-1,
refit=True,
return_train_score=True,
)
search.fit(X_train, y_train, **CV_FIT_PARAMS)
best_logistic = search.best_estimator_
cv_table = pd.DataFrame(search.cv_results_)[[
"param_model__C",
"mean_train_score",
"mean_test_score",
"std_test_score",
"rank_test_score",
]].sort_values("rank_test_score")
display(cv_table)
print("Chosen settings:", search.best_params_)
print("Validation metric:", PRIMARY_SCORING)
A small table ranking the five C values by their average quiz score, and a note of which one won. Don't chase a dazzling winning number — look for a score that stays steady. If several settings land almost neck-and-neck, that's genuinely useful: it tells you the exact winner might wobble a bit, which is honest information about how sure we can be. Your own data will differ, and there's no single "right" number here.
- Did the calmer, simpler setting or the more reactive one work better on your practice quizzes?
- How close were the top settings to each other, compared with the gap to the rest?
Take one honest look at the final exam
The model and its setting are now locked in. This is the moment we open the sealed test set — the rows the model has never seen — and let it make its predictions, just once. We add the result to the same log as your baseline and any earlier lessons.
The baseline is the "do-nothing" comparison — it just guesses the most common answer every time. On the example students, always guessing "pass" is right about 64% of the time. That's the bar to clear. If your model can't beat guessing the obvious, it isn't yet earning its keep.
What this does: Shows the locked model the test rows for the first and only time, scores how well it did, and records that score next to your baseline.
logistic_predictions = best_logistic.predict(X_test)
logistic_result = evaluate_predictions(
"Logistic regression",
y_test,
logistic_predictions,
)
print("Compare this row with the baseline using the metric that fits your problem.")
A single score for how often the model was right on the test rows. On the example students it usually lands around the mid-70s in percent — comfortably above the roughly 64% baseline. Your own data will differ, and there's no "correct" score to aim for: the honest question is simply "did it beat my baseline, and where does it still slip up?" A single overall number can quietly hide poor performance on a group that's rare but really matters, so it's worth a closer look.
Resist the urge to keep tweaking now
It's tempting, but if you change C, your features, or your preparation because of what you just saw on the test score, the test set quietly stops being a fair final exam. That's a subtle trap. Instead, jot the idea down as a future experiment — one you'd run with a fresh, untouched test set.
See how confident the model was
This is the model's special gift. Instead of only saying "pass," it can say "about 80% likely to pass." This cell lays those confidence numbers out in a table next to what actually happened, so you can see not just what the model guessed, but how sure it was.
For each row, the model spreads 100% of its confidence across the possible answers. If it's 80% sure of "pass," the remaining 20% goes to "not pass" — the numbers in a row always add up to a whole. A high number means "quite sure," a number near the middle means "honestly, this one's a toss-up."
What this does: Builds a table showing, for each test row, what actually happened, what the model guessed, and how confident it was in each possible answer.
probability_estimates = best_logistic.predict_proba(X_test)
class_labels = best_logistic.named_steps["model"].classes_
probability_table = pd.DataFrame(
probability_estimates,
columns=[f"estimated_p({label})" for label in class_labels],
index=X_test.index,
)
probability_table.insert(0, "predicted", logistic_predictions)
probability_table.insert(0, "actual", y_test)
display(probability_table.head(12))
print(
"Largest row-sum error:",
abs(probability_estimates.sum(axis=1) - 1).max(),
)
The first several test rows, each with the true answer, the model's guess, and its confidence numbers — plus a tiny check confirming each row's confidence adds up to a whole. Your own data will differ. The interesting rows to hunt for are the two honest surprises: one where the model was unsure yet still got it right, and one where it was very confident yet wrong. Those two tell you far more than the easy rows it always nails.
- In a real decision, would you just take the model's top pick — or set your own cutoff based on how costly each kind of mistake is?
- What might make these confidence numbers less trustworthy once you use the model on brand-new situations?
Peek at which clues mattered
One lovely thing about this model is that you can look inside and see what it learned. Each clue gets a weight — the code calls it a coefficient. The sign tells you which group the clue leans toward, and the size hints at how strongly it pushed.
A coefficient is just "how much this clue nudged the answer, and in which direction." A positive weight pushes toward one group; a negative weight pushes toward the other. On the study example, you might see that more hours studied and higher attendance push toward "pass" — which matches common sense, and that's reassuring.
What this does: Lists the clues by how strongly they swayed the model, and notes which group the positive ones lean toward.
prepared_feature_names = (
best_logistic.named_steps["prepare"].get_feature_names_out()
)
fitted_logistic = best_logistic.named_steps["model"]
if len(fitted_logistic.classes_) == 2:
coefficient_table = pd.DataFrame({
"transformed_feature": prepared_feature_names,
"coefficient": fitted_logistic.coef_[0],
})
coefficient_table["absolute_size"] = (
coefficient_table["coefficient"].abs()
)
coefficient_table = coefficient_table.sort_values(
"absolute_size", ascending=False
)
print(
"Positive coefficients shift predictions toward:",
fitted_logistic.classes_[1],
)
display(coefficient_table.head(20))
else:
coefficient_table = pd.DataFrame(
fitted_logistic.coef_.T,
index=prepared_feature_names,
columns=[str(label) for label in fitted_logistic.classes_],
).rename_axis("transformed_feature").reset_index()
coefficient_long = coefficient_table.melt(
id_vars="transformed_feature",
var_name="class",
value_name="coefficient",
)
coefficient_long["absolute_size"] = (
coefficient_long["coefficient"].abs()
)
top_by_class = (
coefficient_long
.sort_values(["class", "absolute_size"], ascending=[True, False])
.groupby("class", group_keys=False)
.head(10)
)
display(top_by_class)
A table of clues sorted by how strongly they swayed the model, with a note telling you which group the positive weights point toward. Your own data will differ, so read it as a story about your columns, not a verdict.
Going deeper (optional)
A couple of honest cautions if you want to read the weights closely. When your target has two groups, the plus/minus signs are all relative to the second group the model lists. When it has three or more groups, read each group's column on its own. Also, the number clues were put on the same-sized ruler earlier (scaled), while yes/no category clues were not — so you can't line up all the sizes as one tidy ranking of importance. Compare like with like.
"Mattered to the model" is not the same as "caused it"
A weight describes a pattern the model noticed, holding the other clues steady — it does not prove that clue causes the outcome. Clues that travel together, things you never recorded, how categories were coded, and the calming effect of that C setting can all shift a weight's size or even flip its sign. So read weights as interesting hints, not proof.
DEEPER PRACTICE
Explore at your pace
None of this is required — it's just here if you're curious and want to poke around. Here are a few gentle experiments and what to watch for with each. There's no single right answer; the habit to keep is always comparing back to your baseline.
| Try | Observe | Keep honest |
|---|---|---|
| Remove one suspicious feature | CV stability and coefficient changes | Choose with training CV only |
| Change the classification threshold | Which errors trade places | Use a validation plan, not the test score |
| Check probability calibration | Whether predicted frequencies match outcomes | Use out-of-fold or separate validation predictions |
- Which metric matters for your real decision, and why?
- Where does the model make confident mistakes?
- What is your next training-only experiment?
YOUR PACE, YOUR PROJECT
Save the evidence, not just the score.
Your score is evidence, not a grade — so jot down the story around it: which C won, how close the top settings were, your one honest test result, where the model made costly mistakes, and one reason its confidence numbers might not be fully trustworthy.