This page shows the chapter notebook with its saved results. Constructed examples do not measure competition gains.
Chapter 16: The Fourth-Root Blend
Try the decision first
How alike can three models' predictions be before a blend stops beating the best single model, and do the weights matter?
Write your expectation before opening the worked results. Use the chapter activity to compare supported inputs. To change the calculation itself, run the notebook on GitHub.
Your answers stay on this page until you download them. They are not sent to a server.
Worked notebook and saved results
How alike can three models' predictions be before a blend stops beating the best single model, and do the weights matter?
The chapter treats a blend as a candidate that must beat the strongest component and cost less than it gains. Prediction correlation is its screen, so the useful number is where the blend's gain disappears.
Constructed data with models that differ only in noise. The crossover correlation depends on how different the models' strengths are; with models of equal strength an equal blend would not fall below the best one.
The experiment
A constructed binary task. A hidden signal drives the label; three logistic regressions each see a noisy copy of it (noise 1.0, 1.15 and 1.3), and the share of that noise common to the three copies is the control. Models are fitted on 600 rows. On 300 development rows the strongest model is chosen, fourth-root weights are computed from each model's log loss against a measured constant-prediction baseline, and a hill-climbing search fits weights. Everything is scored by log loss on 20,000 assessment rows. Forty independent datasets are averaged.
Calculation code
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
from kaggle_companion.activities._common import clean
SEED = 16
REPLICATES = 40 # independent constructed datasets; every estimate is their mean
NOISE = np.array([1.0, 1.15, 1.3]) # how noisy each model's view of the hidden signal is: three similar-strength models
N_TRAIN, N_DEV, N_ASSESS = 600, 300, 20000
EPS = 1e-4
def generate(rng, n, shared):
"""A hidden signal drives the label. Each model sees a noisy copy; `shared` is the share of each copy's noise
variance that is common to all three models, so it sets how alike their errors are."""
signal = rng.normal(size=n)
y = (rng.random(n) < 1 / (1 + np.exp(-1.6 * signal))).astype(int)
common = rng.normal(size=n)
views = [signal + sd * (np.sqrt(shared) * common + np.sqrt(1 - shared) * rng.normal(size=n)) for sd in NOISE]
return np.column_stack(views), y
def fourth_root_weights(scores, baseline, maximize=True):
"""The chapter's heuristic: compress each positive margin over a measured baseline with a fourth root."""
scores = np.asarray(scores, dtype=float)
margin = scores - baseline if maximize else baseline - scores
raw = np.maximum(margin, 0) ** 0.25
return raw / raw.sum()
def greedy_weights(y, P, step=0.1, max_iterations=30):
"""Hill-climbing on development log loss: start from the best model, move weight to whichever model helps most."""
loss = lambda q: log_loss(y, np.clip(q, EPS, 1 - EPS))
weights = np.eye(P.shape[1])[int(np.argmin([loss(P[:, j]) for j in range(P.shape[1])]))]
current = P @ weights
for _ in range(max_iterations):
trial = [loss((1 - step) * current + step * P[:, j]) for j in range(P.shape[1])]
j = int(np.argmin(trial))
if trial[j] >= loss(current) - 1e-12:
break
weights = weights * (1 - step)
weights[j] += step
current = P @ weights
return weights
def run(shared):
loss = lambda y, q: log_loss(y, np.clip(q, EPS, 1 - EPS))
rows = []
for rep in range(REPLICATES):
rng = np.random.default_rng(SEED * 100 + rep)
(X, y), (Xd, yd), (Xa, ya) = [generate(rng, n, shared) for n in (N_TRAIN, N_DEV, N_ASSESS)]
models = [LogisticRegression().fit(X[:, [m]], y) for m in range(3)] # one model per view
Pd = np.column_stack([models[m].predict_proba(Xd[:, [m]])[:, 1] for m in range(3)])
Pa = np.column_stack([models[m].predict_proba(Xa[:, [m]])[:, 1] for m in range(3)])
dev_loss = np.array([loss(yd, Pd[:, m]) for m in range(3)])
dummy = loss(yd, np.full(N_DEV, y.mean())) # measured baseline: a constant-prior predictor
w_root = fourth_root_weights(dev_loss, dummy, maximize=False)
w_greedy = greedy_weights(yd, Pd)
logits = np.log(Pd / (1 - Pd))
corr = np.corrcoef(logits.T)
rows.append({
"best": loss(ya, Pa[:, int(np.argmin(dev_loss))]), # strongest model, chosen on development rows
"equal": loss(ya, Pa.mean(axis=1)), "root": loss(ya, Pa @ w_root), "greedy": loss(ya, Pa @ w_greedy),
"equal_dev": loss(yd, Pd.mean(axis=1)), "greedy_dev": loss(yd, Pd @ w_greedy),
"correlation": (corr[0, 1] + corr[0, 2] + corr[1, 2]) / 3, "root_spread": w_root.max() - w_root.min(),
"w_root": w_root, "w_greedy": w_greedy,
})
col = lambda k: np.array([r[k] for r in rows])
gain = {k: col("best") - col(k) for k in ("equal", "root", "greedy")} # positive: the blend beats the best single model
return clean({
"shared_noise": shared, "correlation": float(col("correlation").mean()), "root_weight_spread": float(col("root_spread").mean()),
"assessment_loss": {k: float(col(k).mean()) for k in ("best", "equal", "root", "greedy")},
"gain_over_best": {k: float(v.mean()) for k, v in gain.items()},
"gain_standard_error": {k: float(v.std() / np.sqrt(REPLICATES)) for k, v in gain.items()},
"share_equal_beats_best": float((gain["equal"] > 0).mean()),
"mean_weights": {"root": np.mean([r["w_root"] for r in rows], axis=0), "greedy": np.mean([r["w_greedy"] for r in rows], axis=0)},
"greedy_vs_equal": {"development": float((col("equal_dev") - col("greedy_dev")).mean()),
"assessment": float((col("equal") - col("greedy")).mean())},
"replicates": REPLICATES, "assessment_rows": N_ASSESS, "development_rows": N_DEV,
})
Measure it across the control
The control is share of each model's noise that is common to all three. Each value below runs the whole experiment from the generator up.
Calculation code
import pandas as pd
from kaggle_companion.activities.ch16 import explain
VALUES = [0.0, 0.3, 0.7, 0.95]
results = {value: run(value) for value in VALUES}
table = pd.DataFrame({str(v): explain(results[v], v)['metrics'] for v in VALUES})
print(table.to_string())0.0 0.3 0.7 0.95 Prediction correlation 0.43 0.60 0.83 0.97 Best single model 0.619 0.616 0.615 0.613 Equal blend 0.598 0.606 0.616 0.623 Fourth-root blend 0.598 0.606 0.616 0.623 Fitted blend 0.599 0.605 0.612 0.613
Look at it
The figure shows the default setting, share of each model's noise that is common to all three = 0.95.
Calculation code
import matplotlib.pyplot as plt
from kaggle_companion.activities.ch16 import draw, NCOLS, HEIGHT
DEFAULT = 0.95
fig, axes = plt.subplots(1, NCOLS, figsize=(6.6 if NCOLS == 1 else 10.4, HEIGHT), layout='constrained')
draw(axes, results[DEFAULT], DEFAULT)
plt.show()What the result says
At prediction correlation 0.97, the equal blend does worse than the best single model: 0.613 - 0.623 = -0.010 log loss on 20,000 assessment rows, and it beats the best model in 0% of 40 datasets. Fourth-root weights score 0.623 (weights differ by 0.028 on average). Against equal weights, fitted weights change log loss by -0.0100 on the rows the weights were fitted to and by -0.0096 on assessment rows (negative means fitted weights do better).
- Equal blend: 0.613 - 0.623 = -0.010 (best model minus blend; positive means the blend wins).
- Fourth-root blend: 0.613 - 0.623 = -0.010.
- Fitted blend: 0.613 - 0.613 = 0.000.
- Fourth-root weights differ from each other by 0.028 on average (equal weights differ by 0).
Apply this to your competition
- Screen the candidates by prediction correlation, but decide by the blend's score on assessment rows, not by a correlation cutoff.
- Compare equal weights, the fourth-root heuristic and any fitted weights against the strongest single model, which is the real baseline.
- Use a measured dummy predictor as the fourth-root baseline, and expect weights close to equal.
- Keep the single model when the blend does not beat it, and count the extra fits and inference as cost.
Book location: Chapter 16, The Fourth-Root Weight Formula.
Apply a lesson from this chapter
Select a lesson whose conditions match your project. State how you would test it and what would make you reject it.
Average probabilities before binary decisions
Conditions
- binary classifiers produce aligned positive-class probabilities
Action
- Proposed: average component probabilities before applying the binary decision threshold.
Check
- Proposed: assert matching IDs and positive-class definitions in every file; compare average probabilities with hard-label voting on legitimate validation.
Limits
- The source assumes row ordering and fabricates sequential IDs rather than joining recorded IDs. The fixed threshold needs task-matched testing.
Average class probabilities before pair scoring and evaluate unseen classes
Conditions
- classification-based descriptors score millions of same-identity pairs
Action
- Proposed adaptation of source observation: Compare ensemble membership and averaging order, then evaluate seen and unseen identities separately.
Check
- Proposed verification: Use identity-disjoint validation for transfer and compare probability-based scores with identity-independent embeddings.
Limits
- Unknown-artist weakness is explicit; averaging pair scores differs mathematically from averaging distributions first.
Keep equal averaging as a benchmark for learned ensemble weights
Conditions
- diverse models already provide usable predictions
Action
- Proposed conditional application: Compare greedy equal-weight averaging with ridge-learned weights before adding meta-model complexity.
Check
- Proposed check: Compare equal weights and regularized weights on nested out-of-fold predictions, including fit variance and inference cost.
Limits
- One failed ridge comparison does not imply learned weights never help; validation predictions must remain untouched by base-model fitting.
Keep an equal-weight rank-fusion baseline
Conditions
- complementary rankers must be fused with limited validation data
Action
- Proposed adaptation of source observation: Compare unweighted reciprocal rank fusion against tuned weights and extra weak rankers; retain simple configurations when improvements fail to transfer.
Check
- Proposed verification: Use an independent query split and report ranker subset, k, local score and external score.
Limits
- Overfitting is the authors’ interpretation, not proven causal attribution; k=1 is not a universal default.
Check whether the final ensemble actually beats its strongest model
Conditions
- an ensemble is being selected from several strong image models
Action
- Proposed recipe: compare it with the strongest individual candidate rather than assuming model count guarantees improvement.
Check
- Proposed check: Compare ensemble and best component using legitimate untouched preselection validation, measure uncertainty and inference cost, and preserve final-score timing.
Limits
- Private scores became available after competition; the source does not establish predeadline superiority of B7.
Keep logit blending distinct from probability averaging
Conditions
- several image classifiers produce comparable logits
Action
- Proposed recipe: compare a weighted logit sum followed by softmax with individual predictions.
Check
- Proposed check: Compare logit and probability blending using identical out-of-fold scores, calibrate scales, and evaluate macroF1and serving cost.
Limits
- Logit scales vary by backbone and calibration; weights can overfit reused validation.