Skip to course content
Free Python statistics course

Statistical Data Analytics with Python

Unit 10.04: Predict on the link scale, convert afterwards

Nobody outside statistics thinks in log-odds. Convert to probabilities at concrete values, and build the interval before converting.

Predict on the link scale, convert afterwards

The readable output of a logistic model is a table of predicted probabilities at values a reader recognises — at 0 months, at 12, at 24.

The technical point is the order of operations. model.get_prediction(grid).summary_frame() computes on the link scale and converts both endpoints afterwards, so the intervals never leave [0, 1]. Building an interval on the probability scale directly can produce bounds below 0 or above 1.

The resulting probabilities also make the model's non-linearity visible: the same change in the predictor moves the probability by different amounts depending on where you are on the curve.

This block predicts churn probability across the tenure range and measures the step size.

import numpy as np, pandas as pd
import statsmodels.formula.api as smf

rng = np.random.default_rng(705)
n = 600
d = pd.DataFrame({"tenure": rng.uniform(0, 48, n)})
d["churn"] = rng.binomial(1, 1 / (1 + np.exp(-(1.5 - 0.07 * d.tenure))))
model = smf.logit("churn ~ tenure", data=d).fit(disp=False)

grid = pd.DataFrame({"tenure": [0, 6, 12, 24, 36, 48]})
pred = model.get_prediction(grid).summary_frame(alpha=0.05)

print(f"{'tenure':>8s}{'P(churn)':>12s}{'95% CI':>22s}")
for t, row in zip(grid.tenure, pred.itertuples()):
    print(f"{t:8d}{row.predicted:12.3f}"
          f"      [{row.ci_lower:.3f}, {row.ci_upper:.3f}]")

print("\nThe interval was built on the log-odds scale and converted afterwards,")
print("which is why it never leaves [0, 1].\n")
fine = pd.DataFrame({"tenure": np.arange(0, 49, 6)})
q = model.predict(fine).values
steps = -np.diff(q)                      # fall in probability per 6 months
print("The SAME 6-month step moves the probability by different amounts:")
for lo, hi, s in zip(fine.tenure[:-1], fine.tenure[1:], steps):
    print(f"  {lo:2d} -> {hi:2d} months : {s:.3f}")
print(f"\nSteepest step {steps.max():.3f}, shallowest {steps.min():.3f}"
      f" -- a factor of {steps.max() / steps.min():.1f}.")
print("The curve is steepest near p = 0.5 and flattens at both ends, which is")
print("why one coefficient cannot be read as a change in probability.")
print("\nReport probabilities, not coefficients, to non-technical readers.")

Churn probability falls from 0.860 at tenure 0 to 0.126 at 48 months, every interval inside [0, 1]. The step table is the lesson: the same six-month step moves the probability by 0.067 at the start, rises to 0.116 between 18 and 24 months, and falls to 0.061 at the end — steepest near p = 0.5, flattening at both ends, a factor of 1.9 between the extremes.

The mistake this prevents

The mistake is converting the endpoints of a probability-scale interval, or reporting a single probability without one. A predicted probability of 0.13 with an interval from 0.09 to 0.18 is a different message from 0.13 alone.

Takeaway

Produce a small table of predicted probabilities at meaningful predictor values with get_prediction(...).summary_frame(). Give that table to non-technical readers instead of the coefficients.