III-25 · Boosting and Gradient Boosting

Fit a tree to the current loss direction

Derive squared-error negative gradients and inspect the actual residual tree, learning-rate update and objective at every stage.

Chapters in Part IV
  1. III-20 · Decision Trees and Candidate Splits
  2. III-21 · Entropy, Gini Impurity and Information Gain
  3. III-22 · Regression Trees, Tree Depth and Pruning
  4. III-23 · Bagging and Bootstrap Sampling
  5. III-24 · Random Forests
  6. III-25 · Boosting and Gradient Boosting
TREE / ENSEMBLE SHEET

Fit the next squared-error gradient

Hand-authored data · two features · exact row IDs and input values. Computed outputs marked ≈ use six significant digits; tables retain the underlying numbers.

Fit configuration
Source observations and edge cases
Inspect fitted tree and query
4 source rows. Query prediction: 0.5. 1 fitted residual stages.
Target and actual prediction by source row indexFilled blue circles are targets; hollow rust diamonds are actual predictions; purple vertical segments are residuals. Horizontal positions are row indices, not feature distances. Coincident coordinates may overlap; the numeric table preserves every row.Source row indexa1: x [1, 0]; target 0; prediction 0.5a2: x [2, 0]; target 0; prediction 0.5a3: x [3, 1]; target 2; prediction 1.5a4: x [4, 1]; target 2; prediction 1.5
Targets ●, predictions ◇, residual segments. The x axis enumerates source rows; it does not interpolate a function between samples. X range ≈ [0.5, 4.5]; vertical target / prediction range ≈ [-0.5, 2.5]. Labels are approximate. Input coordinates and row identities remain exact in the source table.
ℓi=12(yi−Fi)2,−∂Fiℓi=yi−Fi=ri,Fm=Fm−1+0.5hm\ell_i=\tfrac12(y_i-F_i)^2,\quad -\partial_{F_i}\ell_i=y_i-F_i=r_i,\quad F_m=F_{m-1}+0.5h_m
Lm=12n∑i(yi−Fm(xi))2,m=1,Lm≈0.125L_m=\frac{1}{2n}\sum_i(y_i-F_m(x_i))^2,\quad m=1,\quad L_m\approx0.125
X · observations by featuresRows are samples; no intercept column.
4 × 2
y · targetsFinite scalar targets.
4 × 1
Prediction · queryx = [2.5, 0]
0.5
Half MSE objectiveThese hand-authored rows are not a population estimate.
≈ 0.125
Input provenanceSeed used only for randomized feature subsets; greedy all-feature trees use no draws.
Hand-authored; seed 7

Query result ≈ 0.5; baseline plus the learning-rate-scaled sum of residual trees.

Half MSE ≈ 0.125

Legal candidates at root

Considered feature indices (zero based): [0, 1]. Exact computed-gain ties choose lower feature index, then lower threshold. Training stops at nonpositive gain.

Only partitions meeting minimum leaf count are candidates. All row IDs include bootstrap draw identities where relevant.
FeatureThresholdLeft IDs / nRight IDs / nWeighted impurityGainChosen
x11.5a1 / 1a2, a3, a4 / 3≈ 0.666667≈ 0.333333no
x12.5a1, a2 / 2a3, a4 / 2≈ 0≈ 1yes
x13.5a1, a2, a3 / 3a4 / 1≈ 0.666667≈ 0.333333no
x20.5a1, a2 / 2a3, a4 / 2≈ 0≈ 1no

Actual recursive tree

  • root · depth 0n 4 · mean ≈ 0 · SSE ≈ 4x1 ≤ 2.5 → left; otherwise rightSplit accepted.
    • rootL · depth 1n 2 · mean ≈ -1 · SSE ≈ 0Leaf prediction: -1Constant regression targets: zero SSE.
    • rootR · depth 1n 2 · mean ≈ 1 · SSE ≈ 0Leaf prediction: 1Constant regression targets: zero SSE.
Row multiplicities affect every node count. Feature subsets are recorded at each eligible split, in preorder.
NodeRowsSource IDs with multiplicityFeaturesLeaf / branch
roota1, a2, a3, a4a1, a2, a3, a4[0, 1]x1 ≤ 2.5
rootLa1, a2a1, a2[]-1
rootRa3, a4a3, a4[]1

Sequential squared-error stages

Initial baseline ≈ 1. Every stage fits a new tree to y − F before that stage. Only squared-error regression is implemented here.

Training objective is half MSE; evaluation MSE is twice this number.
StageBefore objectiveAfter objective
1≈ 0.5≈ 0.125
Each current prediction uses all previous stages. The displayed recursive tree is the last residual tree.
Stage / sourceyF beforer = y − Fh(x)F after
1 / a10≈ 1≈ -1≈ -1≈ 0.5
1 / a20≈ 1≈ -1≈ -1≈ 0.5
1 / a32≈ 1≈ 1≈ 1≈ 1.5
1 / a42≈ 1≈ 1≈ 1≈ 1.5
Exact inputs; actual model outputs and prediction-minus-target residuals are approximate displays.
IDx1x2TargetPredictionResidual
a11000.50.5
a22000.50.5
a33121.5-0.5
a44121.5-0.5

Limits: 32 rows, 1–4 kernel features (the sheet uses 2), depth ≤ 5, at most 6 trees or stages. Empty-node impurity/risk is conventionally 0; its prediction and empty normalized evaluation are undefined. The sheet bounds features, targets and queries to −10…10. Edits use the selected replay frame, exit replay and leave saved history unchanged.

Start with the controls. Every branch, score, bootstrap draw and stage output comes from the active numeric state; Reset restores the hand-authored inputs.

Follow the first correction numerically

The opening four rows have targets [0,0,2,2]. Their initial mean predictor is 1. At learning rate 0.5, the first stump fits residuals [−1,−1,1,1] and updates predictions to [0.5,0.5,1.5,1.5]. The displayed half-MSE is 0.125. Press Fit one more stage: a new tree fits the new residuals, rather than reusing the first tree’s target.

Replay 0–3 stages. Frame 0 is the mean-only baseline; frame 1 includes one fitted correction. Read the residual fitting ledger across frames, inspect the actual last-stage tree and change a query. Its prediction sums all fitted stages, not only the displayed last tree. Editing a selected frame exits replay and starts from that frame’s data, rate and stage count.

An ordered additive model

In this lesson, boosting is squared-error regression:

F0(x)=yˉ,Fm(x)=Fm−1(x)+ηhm(x),0<η≤1.F_0(x)=\bar y,\qquad F_m(x)=F_{m-1}(x)+\eta h_m(x),\quad 0<\eta\le1.

Each hm is a new regression tree. Its training targets depend on Fm−1, so stage order matters. Bagging uses resamples of the original targets and averages separately fitted models; this boosting procedure uses all original rows and accumulates learning-rate-scaled corrections.

The experiment is deterministic all-feature squared-error boosting. It does not add bootstrap sampling, random feature subsets or a classification variant. Many other boosting algorithms use other objectives, gradients, line searches or weighting schemes. Only the mechanism derived here is claimed.

Derive the residual target from the loss

For a single row, choose half squared error:

ℓi(Fi)=12(yi−Fi)2,−∂ℓi∂Fi=yi−Fi.\ell_i(F_i)=\frac12(y_i-F_i)^2,\qquad -\frac{\partial\ell_i}{\partial F_i}=y_i-F_i.

The negative per-example gradient is exactly the residual ri=yi−Fi. The training objective is L=(1/n)sum elli=half MSE. Its vector gradient has entries (Fi−yi)/n; using residuals as the fitted direction absorbs this common positive scaling into the step convention.

Fit hm to these residual targets by minimizing squared error within a bounded regression-tree family. Its leaves predict residual means. Then update all predictions simultaneously using the old Fi and this new hm. A residual vector, fitted correction vector and prediction vector are conceptual n by 1 columns; X remains n by 2. There is no coefficient matrix or softmax dimension hidden in this experiment.

Why the rate controls the correction

Write r for the old residual vector and h for the new fitted leaf-mean vector. The new residual is r−ηh, giving

L(F+ηh)−L(F)=−ηnrTh+η22nhTh.L(F+\eta h)-L(F) =-\frac{\eta}{n}r^\mathsf T h+\frac{\eta^2}{2n}h^\mathsf T h.

Within each fitted leaf, h is its residual mean, so the leaf’s residual sum equals its count times h. Summing leaves gives r transpose h = h transpose h. Consequently

L(F+ηh)−L(F)=−η(2−η)2n∥h∥2.L(F+\eta h)-L(F)= -\frac{\eta(2-\eta)}{2n}\|h\|^2.

For 0<η≤10<\eta\le1 this is nonpositive in exact arithmetic. A zero correction leaves the objective unchanged. This argument applies to least-squares leaf means on the current rows; it is not a universal guarantee for arbitrary objectives or different stage solvers. Lower training loss also does not imply better validation performance.

Work three tiny stages

The canonical opening rows have feature pairs [1,0], [2,0], [3,1], [4,1]. Either feature can separate the two target groups; the exact tie chooses x1 at 2.5. A depth-1 stump fits each residual group exactly.

StageResiduals before fittingFitted hPredictions after η=0.5Half MSE
0——[1,1,1,1]0.5
1[−1,−1,1,1][−1,−1,1,1][0.5,0.5,1.5,1.5]0.125
2[−0.5,−0.5,0.5,0.5]same as residuals[0.25,0.25,1.75,1.75]0.03125
3[−0.25,−0.25,0.25,0.25]same as residuals[0.125,0.125,1.875,1.875]0.0078125

Residual magnitude halves each stage here, so squared loss becomes one quarter. At η=1, the first stump solves this particular four-row problem exactly and later stages fit zero corrections. This special example does not imply that every dataset is solved by one stump.

Make the weak learner’s limits visible

Set Maximum depth to 0. The mean-initialized residuals sum to zero, so a root-only residual tree predicts zero and cannot change the original mean. Increase depth to 1 and observe a real correction. Set constant features while retaining varying targets: no separating test exists, and again the residual mean is zero. Constant targets are already solved by the baseline.

Empty data have no baseline, residual stages or loss. Invalid numeric edits preserve the last accepted state. The pure kernels reject nonfinite inputs and nonfinite residual squares or sums. Python observations are normalized to binary64 consistently with the browser, and seed/count/depth arguments remain integer indices after validation.

The sheet permits rates 0.01…1, up to six stages and depth five; the pure boosting kernel allows every finite rate in (0,1]. The practical number of stages, depth and rate should be selected with validation. Repeated small corrections can overfit despite a monotone training objective.

Connections and independent proof

For logistic BCE, the negative derivative with respect to a logit is y−sigmoid(z), not the scalar-regression residual y−z. Other losses have their own derivatives and may require different terminal-value optimization. This lesson implements none of those variants; III-13 supplies the distinct logistic derivation.

The independent tiny-stage oracle retains Fraction means, residuals, updates and losses throughout, while a separate exhaustive partition search chooses each residual tree. The actual Python/NumPy source executes these stages, and committed expectations compare every before/residual/fitted/after vector with TypeScript. The row-index plot displays actual final predictions and residual segments, rather than an invented generic convergence curve.

Execute the actual Python and NumPy

The imported file below is the canonical runnable source used by the numerical tests. Read the named functions for this chapter; the same recursive kernel supports the Part’s later ensembles. From the book root:

.venv/bin/python -B code/python/p04/trees_ensembles.py

That default command prints the six-row squared-error boosting demonstration as JSON. To run this chapter’s tree or sampling operation, pass a JSON request on stdin with operation equal to tree, bootstrap, bagging or boosting, the displayed rows, and the options keys in DEFAULT_OPTIONS. The independent fixture file contains complete runnable requests for all six lessons. Core lessons require no network or model service.

For this chapter’s exact committed example, run the independent fixture request through the actual canonical module:

.venv/bin/python -B - <<'PYTHON'
import json, sys
from pathlib import Path
sys.path.insert(0, "code/python/p04")
from trees_ensembles import dispatch
cases = json.loads(Path("fixtures/p04/trees.fixtures.json").read_text())["cases"]
request = next(c["input"] for c in cases if c["input"]["name"] == "boosting-tiny")
print(json.dumps(dispatch(request), allow_nan=False))
PYTHON
code/python/p04/trees_ensembles.py
"""From-scratch binary CART, bootstrap ensembles and squared-error boosting.
X is an n x d row matrix; y is n x 1 conceptually. No sklearn or hosted model.
Public input: 0..32 rows, 1..4 features, depth 0..5, minLeaf 1..32.
Run: .venv/bin/python code/python/p04/trees_ensembles.py
Or send one JSON request on stdin with operation tree/bootstrap/bagging/boosting.
"""
import json
import math
import sys
import numpy as np

DEFAULT_OPTIONS = dict(task="classification", criterion="gini", dimensions=2,
                       maxDepth=3, minLeaf=1, mtry=0, seed=7)
CLASS_ROWS = [dict(id=f"s{i+1}", x=[i+1, 1-i % 2], y=y)
              for i, y in enumerate([0, 0, 1, 1, 0, 1])]
REG_ROWS = [dict(r, y=2*r["y"]) for r in CLASS_ROWS]
MOD = 2**32

def finite(value, name):
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
        raise ValueError(f"{name} must be a finite number, not bool or string.")
    return float(value)

def integer(value, lower, upper, name):
    finite(value, name)
    if int(value) != value or not lower <= value <= upper:
        raise ValueError(f"{name} must be an integer in [{lower},{upper}].")
    return int(value)

def validate_rows(rows, o):
    if not isinstance(o, dict) or o.get("task") not in ("classification", "regression") or o.get("criterion") not in ("gini", "entropy", "sse") or (o["task"] == "regression") != (o["criterion"] == "sse"):
        raise ValueError("Classification requires Gini/entropy; regression requires SSE.")
    for key, lo, hi in [("dimensions",1,4),("maxDepth",0,5),("minLeaf",1,32),("seed",0,MOD-1)]:
        integer(o.get(key), lo, hi, key)
    integer(o.get("mtry"), 0, o["dimensions"], "mtry")
    if not isinstance(rows, list) or len(rows) > 32:
        raise ValueError("Trees support 0..32 observation rows.")
    ids = set()
    for r in rows:
        if not isinstance(r, dict) or not isinstance(r.get("id"), str) or not r["id"].strip() or r["id"] in ids:
            raise ValueError("Row IDs must be unique nonempty strings.")
        ids.add(r["id"])
        if "sourceId" in r and (not isinstance(r["sourceId"], str) or not r["sourceId"].strip()):
            raise ValueError("Source IDs must be nonempty strings.")
        if not isinstance(r.get("x"), list) or len(r["x"]) != o["dimensions"]:
            raise ValueError("Each row needs the declared number of feature entries.")
        for v in r["x"]:
            finite(v, "Feature")
        finite(r.get("y"), "Target")
        if o["task"] == "classification" and r["y"] not in (0, 1):
            raise ValueError("Binary labels must be 0 or 1.")

def normalized(rows, o):
    """Detached binary64 observations; integer options stay integer-valued.
    Python arbitrary-precision ints are rounded once to the browser's numeric
    domain before identity, unique thresholds, NumPy masks or recursion.
    """
    validate_rows(rows, o)
    options = dict(o)
    for k in ("dimensions", "maxDepth", "minLeaf", "mtry", "seed"):
        options[k] = int(o[k])
    data = [dict(r, x=[float(v) for v in r["x"]], y=float(r["y"])) for r in rows]
    return data, options

def finite_mean(values, name):
    if not isinstance(values, list) or not values:
        raise ValueError("A mean needs at least one value.")
    data = [finite(v, name) for v in values]
    if all(v == data[0] for v in data):
        return data[0]  # exact constant mean; no accumulating addition needed
    with np.errstate(over="ignore", invalid="ignore"):
        return finite(float(np.mean(np.asarray(data, dtype=float))), name)

def stats(rows, o):
    rows, o = normalized(rows, o)
    n = len(rows)
    if not n:
        return dict(n=0, counts=[0,0] if o["task"] == "classification" else None,
                    mean=None, value=None, probability=None, impurity=0, risk=0)
    # These are real NumPy arrays used in canonical calculations, not display code.
    y = np.asarray([r["y"] for r in rows], dtype=np.float64).reshape(n, 1)
    if o["task"] == "classification":
        positive = int(np.count_nonzero(y == 1))
        counts, p = [n-positive, positive], positive/n
        impurity = 2*p*(1-p) if o["criterion"] == "gini" else -sum(c/n*math.log2(c/n) for c in counts if c)
        return dict(n=n, counts=counts, mean=None, value=int(positive > n-positive),
                    probability=p, impurity=impurity, risk=finite(n*impurity, "Node risk"))
    with np.errstate(over="ignore", invalid="ignore"):
        mean = finite_mean([r["y"] for r in rows], "Leaf mean")
        residual = y[:,0] - mean
        risk = finite(float(residual @ residual), "Leaf SSE")
    return dict(n=n, counts=None, mean=mean, value=mean, probability=None,
                impurity=finite(risk/n, "Node MSE"), risk=risk)

def evaluate_split(rows, o, feature, threshold):
    rows, o = normalized(rows, o)
    feature = integer(feature, 0, o["dimensions"]-1, "Feature")
    threshold = finite(threshold, "Threshold")
    X = np.asarray([r["x"] for r in rows], dtype=float).reshape(len(rows), o["dimensions"])
    mask = X[:,feature] <= threshold
    left_rows = [r for i, r in enumerate(rows) if mask[i]]
    right_rows = [r for i, r in enumerate(rows) if not mask[i]]
    parent, left, right = stats(rows,o), stats(left_rows,o), stats(right_rows,o)
    legal = left["n"] >= o["minLeaf"] and right["n"] >= o["minLeaf"]
    weighted = finite((left["risk"]+right["risk"])/len(rows), "Weighted child impurity") if rows else None
    gain = finite(parent["impurity"]-weighted, "Gain") if legal else None
    return dict(feature=feature, threshold=threshold, lower=None, upper=None,
                parent=parent, left=left, right=right, leftIds=[r["id"] for r in left_rows],
                rightIds=[r["id"] for r in right_rows], weighted=weighted, gain=gain,
                legal=legal, reason=None if legal else "Both children must meet the minimum leaf count; empty children are illegal.")

def candidates(rows, o, features=None):
    rows, o = normalized(rows, o)
    features = list(range(o["dimensions"])) if features is None else features
    if not isinstance(features, list) or any(isinstance(f,bool) or not isinstance(f,(int,float)) for f in features) or len(set(features)) != len(features):
        raise ValueError("Candidate features must be a unique index list.")
    result = []
    for f in sorted(features):
        f = integer(f, 0, o["dimensions"]-1, "Candidate feature")
        values = sorted(set(r["x"][f] for r in rows))
        for lo, hi in zip(values, values[1:]):
            t = lo/2 + hi/2
            if t < lo or t >= hi:
                t = lo  # adjacent floats: retain the exact <= partition
            s = evaluate_split(rows,o,f,t)
            if s["legal"]:
                result.append(dict(s, lower=lo, upper=hi))
    return result

def best_split(splits):
    best = None
    for s in sorted(splits, key=lambda s: (s["feature"],s["threshold"])):
        if s["legal"] and s["gain"] is not None and (best is None or s["gain"] > best["gain"]):
            best = s
    return best  # exact computed-gain ties: lower feature, then lower threshold

def lcg(seed):
    seed = integer(seed, 0, MOD-1, "Seed")
    state = seed
    while True:
        state = (1664525*state+1013904223) % MOD
        yield state

def train_tree(rows, o):
    rows, o = normalized(rows, o)
    rng = lcg(o["seed"])
    def grow(data, depth, node_id):
        s = stats(data,o)
        leaf = dict(id=node_id, dimensions=o["dimensions"], depth=depth,
                    rowIds=[r["id"] for r in data], sourceIds=[r.get("sourceId",r["id"]) for r in data],
                    stats=s, features=[], candidates=[], split=None, left=None, right=None, reason="")
        stop = ("Empty node: no prediction." if not data else
                ("Pure labels: zero impurity." if o["task"] == "classification" else
                 "Constant regression targets: zero SSE." if all(r["y"] == data[0]["y"] for r in data) else
                 "Computed SSE underflowed to zero; finer target variation is numerically unresolved.") if s["risk"] == 0 else
                "Maximum depth reached." if depth >= o["maxDepth"] else
                "Too few rows for two legal leaves." if len(data) < 2*o["minLeaf"] else None)
        if stop:
            return dict(leaf, reason=stop)
        features = list(range(o["dimensions"]))
        m = o["mtry"]
        if 0 < m < len(features):
            for i in range(m):
                j = i + (next(rng)*(len(features)-i))//MOD
                features[i], features[j] = features[j], features[i]
            features = sorted(features[:m])
        splits = candidates(data,o,features)
        split = best_split(splits)
        if split is None or split["gain"] <= 0:
            return dict(leaf, features=features, candidates=splits, reason="No legal positive-gain split in the considered features.")
        return dict(leaf, features=features, candidates=splits, split=split, reason="Split accepted.",
                    left=grow([r for r in data if r["x"][split["feature"]] <= split["threshold"]],depth+1,node_id+"L"),
                    right=grow([r for r in data if r["x"][split["feature"]] > split["threshold"]],depth+1,node_id+"R"))
    return grow(rows,0,"root")

def predict(tree, x):
    if not isinstance(x,list) or len(x) != tree["dimensions"]:
        raise ValueError("Prediction needs exactly the declared number of features.")
    x = [finite(v,"Prediction feature") for v in x]
    node = tree
    while node["split"] is not None:
        s = node["split"]
        node = node["left"] if x[s["feature"]] <= s["threshold"] else node["right"]
    return dict(value=node["stats"]["value"], probability=node["stats"]["probability"],
                leaf=node["id"], reason="Empty training node has no prediction." if node["stats"]["value"] is None else None)

def nodes(tree):
    return [tree] + (nodes(tree["left"])+nodes(tree["right"]) if tree["split"] else [])

def prune_tree(tree, alpha):
    alpha = finite(alpha,"Pruning alpha")
    if not 0 <= alpha <= 10:
        raise ValueError("Pruning alpha must be in [0,10].")
    n = tree["stats"]["n"]
    if not n:
        return dict(tree=tree, leaves=1, risk=None, objective=None, reason="Empty training data: normalized pruning objective is undefined.")
    def solve(node):
        risk = finite(node["stats"]["risk"]/n,"Normalized leaf risk")
        cost = finite(risk+alpha,"Pruning cost")
        if not node["split"]:
            return dict(tree=node, leaves=1, risk=risk, objective=cost)
        left,right = solve(node["left"]),solve(node["right"])
        split_cost = finite(left["objective"]+right["objective"],"Subtree cost")
        # Selection-only, eight binary64 epsilons at the compared cost scale.
        # Reported SSE/risk/objective values are never rounded to enforce a tie.
        tol = 8*sys.float_info.epsilon*max(abs(cost),abs(split_cost))
        if cost <= split_cost or cost-split_cost <= tol:
            return dict(tree=dict(node,split=None,left=None,right=None,reason="Collapsed by cost complexity (ties prefer fewer leaves)."),
                        leaves=1,risk=risk,objective=cost)
        return dict(tree=dict(node,left=left["tree"],right=right["tree"]),leaves=left["leaves"]+right["leaves"],
                    risk=finite(left["risk"]+right["risk"],"Subtree risk"),objective=split_cost)
    return dict(solve(tree),reason=None)

def bootstrap(rows,o,seed,draws=None):
    rows, o = normalized(rows,o)
    seed = integer(seed,0,MOD-1,"Bootstrap seed")
    draws = len(rows) if draws is None else draws
    draws = integer(draws,0,32,"Bootstrap draws")
    if not rows and draws:
        raise ValueError("Cannot draw from an empty source.")
    rng = lcg(seed)
    indices = [next(rng)*len(rows)//MOD for _ in range(draws)]
    counts = [dict(id=r["id"],count=indices.count(i)) for i,r in enumerate(rows)]
    return dict(seed=seed,indices=indices,counts=counts,
                draws=[dict(draw=j+1,index=i,sourceId=rows[i]["id"]) for j,i in enumerate(indices)],
                rows=[dict(rows[i],id=f"draw{j+1}",sourceId=rows[i]["id"]) for j,i in enumerate(indices)],
                uniqueIds=[r["id"] for r in counts if r["count"]],oobIds=[r["id"] for r in counts if not r["count"]])

def aggregate(values,task):
    if not values:
        return dict(value=None,reason="No eligible tree predictions.")
    mean = finite_mean(values,"Prediction mean")
    return dict(value=int(mean > .5) if task == "classification" else mean,reason=None)

def bagging(rows,o,seed,count):
    rows, o = normalized(rows,o)
    seed = integer(seed,0,MOD-1,"Ensemble seed")
    count = integer(count,0,6,"Tree count")
    master, state, offset = lcg(seed), seed, 0
    budget = (2**o["maxDepth"]-1)*o["mtry"] if 0 < o["mtry"] < o["dimensions"] else 0
    trees = []
    for i in range(count):
        bootstrap_seed,start = state,offset
        for _ in range(len(rows)):
            state = next(master)
        offset += len(rows)
        feature_seed,feature_start = state,offset
        for _ in range(budget):
            state = next(master)
        offset += budget
        sample = bootstrap(rows,o,bootstrap_seed)
        tree = train_tree(sample["rows"],dict(o,seed=feature_seed))
        trees.append(dict(id=f"tree{i+1}",bootstrapSeed=bootstrap_seed,featureSeed=feature_seed,
                          bootstrapStart=start,featureStart=feature_start,featureBudget=budget,sample=sample,tree=tree))
    predictions,oob = [],[]
    for r in rows:
        values = [predict(t["tree"],r["x"])["value"] for t in trees]
        values = [v for v in values if v is not None]
        predictions.append(dict(id=r["id"],values=values,**aggregate(values,o["task"]),
                                voteFraction=sum(v == 1 for v in values)/len(values) if values and o["task"] == "classification" else None))
        eligible = [t for t in trees if r["id"] in t["sample"]["oobIds"]]
        vs = [predict(t["tree"],r["x"])["value"] for t in eligible]
        vs = [v for v in vs if v is not None]
        oob.append(dict(id=r["id"],treeIds=[t["id"] for t in eligible],values=vs,**aggregate(vs,o["task"])))
    evaluated = [(p["value"],r["y"]) for p,r in zip(oob,rows) if p["value"] is not None]
    value = (sum(v==y for v,y in evaluated)/len(evaluated) if o["task"] == "classification" else
             finite(sum((v-y)**2 for v,y in evaluated)/len(evaluated),"OOB MSE")) if evaluated else None
    return dict(streamPolicy="Disjoint reserved LCG segments: n bootstrap draws, then at most (2^depth−1)·mtry feature draws per tree.",
                trees=trees,predictions=predictions,oob=oob,eligible=len(evaluated),
                oobMetric=dict(value=value,reason=None if evaluated else "No source row has an eligible OOB prediction."))

def boosting(rows,o,rate,stages):
    rows, o = normalized(rows,o)
    if o["task"] != "regression" or o["mtry"] != 0:
        raise ValueError("This boosting experiment uses squared-error regression with all features.")
    rate = finite(rate,"Learning rate")
    if not 0 < rate <= 1:
        raise ValueError("Boosting rate must be in (0,1].")
    stages = integer(stages,0,6,"Stage count")
    if not rows:
        return dict(baseline=None,predictions=[],stages=[],loss=dict(value=None,reason="Empty data: squared-error loss and baseline are undefined."))
    baseline = stats(rows,o)["mean"]
    values = [baseline for _ in rows]
    def loss(v):
        with np.errstate(over="ignore",invalid="ignore"):
            r = np.asarray(v,dtype=float)-np.asarray([r["y"] for r in rows],dtype=float)
            return finite(float(r @ r)/(2*len(rows)),"Half MSE")
    trace = []
    for m in range(stages):
        before = values[:]
        residual = [finite(r["y"]-v,"Negative per-example gradient") for r,v in zip(rows,before)]
        tree = train_tree([dict(r,y=v) for r,v in zip(rows,residual)],o)
        fitted = [predict(tree,r["x"])["value"] for r in rows]
        values = [finite(v+rate*h,"Boosted prediction") for v,h in zip(before,fitted)]
        trace.append(dict(stage=m+1,before=before,residuals=residual,tree=tree,fitted=fitted,after=values[:],
                          lossBefore=loss(before),lossAfter=loss(values)))
    return dict(baseline=baseline,predictions=values,stages=trace,loss=dict(value=loss(values),reason=None))

def dispatch(request):
    rows,o = request.get("rows",CLASS_ROWS),request.get("options",DEFAULT_OPTIONS)
    op = request.get("operation","tree")
    if op == "tree":
        tree = train_tree(rows,o)
        result = dict(tree=tree,candidates=candidates(rows,o),best=best_split(candidates(rows,o)),
                      predictions=[predict(tree,r["x"]) for r in rows])
        if "probe" in request:
            result["probe"] = predict(tree,request["probe"])
        if "alpha" in request:
            result["pruned"] = prune_tree(tree,request["alpha"])
        return result
    if op == "bootstrap":
        return bootstrap(rows,o,request["seed"],request.get("draws"))
    if op == "bagging":
        return bagging(rows,o,request["seed"],request["count"])
    if op == "boosting":
        return boosting(rows,o,request["rate"],request["stages"])
    raise ValueError("Unknown operation.")

if __name__ == "__main__":
    raw = "" if sys.stdin.isatty() else sys.stdin.read()
    request = json.loads(raw) if raw.strip() else dict(operation="boosting",rows=REG_ROWS,
              options=dict(DEFAULT_OPTIONS,task="regression",criterion="sse",maxDepth=1),rate=.5,stages=3)
    try:
        print(json.dumps(dispatch(request),allow_nan=False))
    except (ValueError,OverflowError,TypeError) as error:
        print(json.dumps(dict(error=str(error)),allow_nan=False))
        sys.exit(2)

Five graded investigations

For half squared error, what is the negative derivative with respect to prediction F?
F=1, fitted h=−1 and η=0.5. What is the updated F?
Four residuals have magnitude 0.5. What is half MSE?
A root-only residual tree follows a mean-only baseline. What correction does it fit?
Does every boosting objective use target minus raw prediction as its gradient?