What happens beyond the learned percentile range?

Quantile
Train a Quantile relationship within a bounded amount range, then supply much smaller and larger amounts.

Example

amount: 20.0
cost: 180.5

The answer is shown here for explanation; it is hidden from the model when scored.

Model tree with order, amount, cost. Amber cards are hidden prediction targets. Other fields provide the input.

Model tree with order, amount, cost. Amber cards are hidden prediction targets. Other fields provide the input.

Figure 1: Amber cards are hidden prediction targets. Other fields provide the input.

Comparison

Compare in-range error with each outer range. Check whether distinct amounts in a tail receive the same prediction.

Result

P068 · Latest full run · Limited

Seed 6810 on cpu: 17 of 17 test checks met. This experiment demonstrates a limitation. History: 11 of 11 full runs met every check, across 10 distinct seeds. History can include earlier source versions. The experiment code has changed since this run; these measurements describe its earlier version.

Comparison Latest measurement
In-range error, original units 0.7486
Below-range error, original units 84.71
Above-range error, original units 84.17

Outside the learned support, percentile inputs and outputs hit their boundaries. Tail predictions become constant; accepting a number does not guarantee extrapolation.

Experiment details and code

Latest full run

Seed 6810, cpu, recorded 2026-09-21T21:08:24.119856+00:00. Outcome: met.

Source fingerprint: f28d94920b6442c9b40a486e3a2581a96957f4d7c922b5326d41883ed76d8ae8. Python 3.12.10; Torch 2.12.0.

Measurement Value
steps 400
train_rows 4096
test_rows_per_condition 2048
training_mean 180.328
noise_rmse 0.57735
in_range_rmse 0.748561
in_range_baseline_rmse 23.1165
in_range_normalized_rmse 0.0323821
shuffled_rmse 32.524
shuffled_normalized_rmse 1.40696
normalization /amount: count: 51200; compression: 100; minimum: 10.0036; median: 20.0784; maximum: 29.9902; /cost: count: 51200; compression: 100; minimum: 139.339; median: 180.298; maximum: 220.722
below shift: -30; target_minimum: 19.4841; target_maximum: 100.853; prediction_mean: 141.624; prediction_spread: 0; rmse: 84.7131; baseline_rmse: 122.405; normalized_rmse: 0.692073; support_clipping_rmse_lower_bound: 82.5173; rmse_above_clipping_bound: 2.19576; endpoint_distance_fraction: 0.0280748
above shift: 30; target_minimum: 259.484; target_maximum: 340.853; prediction_mean: 219.19; prediction_spread: 0; rmse: 84.1717; baseline_rmse: 122.007; normalized_rmse: 0.689891; support_clipping_rmse_lower_bound: 82.6994; rmse_above_clipping_bound: 1.47229; endpoint_distance_fraction: 0.0188279
Behavioral checks
Behavioral check Outcome
In-range source-unit normalized RMSE below 0.20 Met
Shuffled input normalized RMSE above 0.90 Met
In-range predictions are finite Met
Both digests count exactly the consumed training rows Met
below: out-of-range finite inputs produce finite predictions Met
below: predictions stay inside learned target support Met
below: observed error meets the unavoidable clipping lower bound Met
below: input percentiles collapse to the same tail Met
below: predictions saturate within 0.00001 source units Met
below: mean prediction stays within 15 percent of the corresponding edge Met
above: out-of-range finite inputs produce finite predictions Met
above: predictions stay inside learned target support Met
above: observed error meets the unavoidable clipping lower bound Met
above: input percentiles collapse to the same tail Met
above: predictions saturate within 0.00001 source units Met
above: mean prediction stays within 15 percent of the corresponding edge Met
Evaluation preserves both training distributions Met

Full run history

Runs may include earlier source versions. Repeating a seed is not an independent seed check.

Seed Outcome Recorded Code fingerprint
6801 met 2026-09-21T21:04:20.476299+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6802 met 2026-09-21T21:04:31.731243+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6803 met 2026-09-21T21:04:42.000642+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6804 met 2026-09-21T21:06:28.497767+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6805 met 2026-09-21T21:06:39.884073+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6806 met 2026-09-21T21:06:50.249869+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6807 met 2026-09-21T21:07:00.772390+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6808 met 2026-09-21T21:07:33.019706+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6801 met 2026-09-21T21:05:25.573719+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6809 met 2026-09-21T21:08:13.229106+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b
6810 met 2026-09-21T21:08:24.119856+00:00 309c5014798f0cfab3b17fbd43c2aab2834ef5fd2c452810e4dd5a1b8881332b

Recorded results.

Run this experiment

Proof execution is optional and separate from building this site. Use a RelFlow build matching the recorded provenance; replace the wheel path below with that artifact. Run from this documentation repository’s root:

uv run --with /path/to/matching-relflow.whl python proofs/run.py P068

Or, from a prepared proof environment, run the self-contained script directly:

PYTHONPATH=proofs python proofs/quantile/clipped_extrapolation.py

Add --accelerator gpu for CUDA or --seed 42 for another seeded experiment. --steps 2 checks execution with a short training budget; it is recorded as a smoke run.

Download the complete proof.

Synthetic process and paired observations

Each independent row has amount ~ Uniform(10, 30) and cost = 100 + 4 * amount + Uniform(-1, 1). Only amount is visible; the conditional mean is identifiable, with noise RMSE 1 / sqrt(3). There are no identities or shared row-level latent variables across splits.

Training, metric, and boundary gates

Use 4,096 training rows, 1,024 validation rows, and 2,048 independently generated test rows per evaluation condition. The in-range and shifted tests share latent draws only with each other. Train xs for 400 AdamW updates at learning rate 0.002 and batch size 128. Use the fixed final update, with percentile MSE and default digest compression.

Predeclare in-range normalized RMSE below 0.20 against the training-mean baseline, and shuffled-input normalized RMSE above 0.90. For each shifted test, compute the best possible bounded output separately for every target: clip(cost, learned_minimum, learned_maximum). Its RMSE is an exact lower bound for every predictor confined to that support, even one with an oracle target. The learned predictor must stay within support and incur at least that lower-bound error. Same-tail inputs must produce the same percentile and predictions within 0.00001 source units, with mean predictions within 15% of the support width of the corresponding endpoint. This gate records expected saturation as successful evidence of a limitation.

Remaining work

The ten-seed calibration retained the predeclared gates and fixed training budget. This proof establishes the boundary of this Quantile schema, not the extrapolation ability of Number or models with additional informative fields. Accepting an out-of-support value gives no OOD warning or calibrated uncertainty.

Complete experiment code

Code
"""P068: distinguish learned in-range prediction from bounded Quantile extrapolation."""

from collections.abc import Iterator
from functools import partial

import lightning.pytorch as lit
import numpy as np
import pyarrow as pa
from reporting import report

import relflow as rf

PROOF_ID = "P068"
BUDGET = 400
BATCH_SIZE = 128
TRAIN_ROWS = 4096
TEST_ROWS = 2048


def records(*, rows: int, seed: int, shift: float = 0.0) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    amount = rng.uniform(10.0, 30.0, rows) + shift
    cost = 100.0 + 4.0 * amount + rng.uniform(-1.0, 1.0, rows)
    for value, target in zip(amount, cost, strict=True):
        yield {"amount": float(value), "cost": float(target)}


def build() -> rf.Model:
    model = rf.Model.xs(
        batch_size=BATCH_SIZE,
        amount=rf.Quantile,
        cost=rf.Quantile(objective="mse", mask=True),
    )
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def predict(model: rf.Model, rows: list[dict]) -> np.ndarray:
    visible = pa.Table.from_pylist([{"amount": row["amount"]} for row in rows])
    predictions = model.predict(visible)["predictions"].to_pylist()
    return np.asarray([row["cost"]["content"] for row in predictions], dtype=np.float64)


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    lit.seed_everything(seed, workers=True)
    model = build()
    data = rf.SyntheticDataModule(
        model=model,
        train=partial(records, rows=TRAIN_ROWS, seed=seed + 1),
        validate=partial(records, rows=1024, seed=seed + 2),
        seed=seed,
    )
    trainer = lit.Trainer(
        accelerator=accelerator,
        devices=1,
        max_epochs=-1,
        max_steps=BUDGET if steps is None else min(steps, BUDGET),
        logger=False,
        enable_checkpointing=False,
        enable_progress_bar=False,
        enable_model_summary=False,
        deterministic=True,
        num_sanity_val_steps=0,
    )
    trainer.fit(model, datamodule=data)
    model.eval()
    addresses = (rf.Address("amount"), rf.Address("cost"))
    normalization = {str(address): rf.Quantile.normalization(model, address) for address in addresses}
    lower = float(normalization["/cost"]["minimum"])
    upper = float(normalization["/cost"]["maximum"])
    width = upper - lower
    rows = list(records(rows=TEST_ROWS, seed=seed + 3))
    target = np.asarray([row["cost"] for row in rows])
    training_mean = float(np.mean([row["cost"] for row in records(rows=TRAIN_ROWS, seed=seed + 1)]))
    baseline_rmse = float(np.sqrt(np.mean((training_mean - target) ** 2)))
    output = predict(model, rows)
    rmse = float(np.sqrt(np.mean((output - target) ** 2)))
    permutation = np.random.default_rng(seed + 4).permutation(TEST_ROWS)
    shuffled = predict(model, [rows[index] for index in permutation])
    shuffled_rmse = float(np.sqrt(np.mean((shuffled - target) ** 2)))
    metrics = {
        "steps": trainer.global_step,
        "train_rows": TRAIN_ROWS,
        "test_rows_per_condition": TEST_ROWS,
        "training_mean": training_mean,
        "noise_rmse": float(1 / np.sqrt(3)),
        "in_range_rmse": rmse,
        "in_range_baseline_rmse": baseline_rmse,
        "in_range_normalized_rmse": rmse / baseline_rmse,
        "shuffled_rmse": shuffled_rmse,
        "shuffled_normalized_rmse": shuffled_rmse / baseline_rmse,
        "normalization": normalization,
    }
    checks = {
        "In-range source-unit normalized RMSE below 0.20": rmse / baseline_rmse < 0.20,
        "Shuffled input normalized RMSE above 0.90": shuffled_rmse / baseline_rmse > 0.90,
        "In-range predictions are finite": bool(np.isfinite(output).all()),
        "Both digests count exactly the consumed training rows": all(
            state["count"] == trainer.global_step * BATCH_SIZE for state in normalization.values()
        ),
    }
    for name, shift, percentile, endpoint in (("below", -30.0, 0.0, lower), ("above", 30.0, 1.0, upper)):
        shifted = list(records(rows=TEST_ROWS, seed=seed + 3, shift=shift))
        targets = np.asarray([row["cost"] for row in shifted])
        predictions = predict(model, shifted)
        baseline = float(np.sqrt(np.mean((training_mean - targets) ** 2)))
        observed_rmse = float(np.sqrt(np.mean((predictions - targets) ** 2)))
        bounded_oracle = np.clip(targets, lower, upper)
        lower_bound = float(np.sqrt(np.mean((bounded_oracle - targets) ** 2)))
        spread = float(np.ptp(predictions))
        distance = float(abs(predictions.mean() - endpoint) / width)
        fields = model.encode(pa.Table.from_pylist(shifted), strata="test")
        metrics[name] = {
            "shift": shift,
            "target_minimum": float(targets.min()),
            "target_maximum": float(targets.max()),
            "prediction_mean": float(predictions.mean()),
            "prediction_spread": spread,
            "rmse": observed_rmse,
            "baseline_rmse": baseline,
            "normalized_rmse": observed_rmse / baseline,
            "support_clipping_rmse_lower_bound": lower_bound,
            "rmse_above_clipping_bound": observed_rmse - lower_bound,
            "endpoint_distance_fraction": distance,
        }
        checks[f"{name}: out-of-range finite inputs produce finite predictions"] = bool(np.isfinite(predictions).all())
        checks[f"{name}: predictions stay inside learned target support"] = bool(
            np.all(predictions >= lower) and np.all(predictions <= upper)
        )
        checks[f"{name}: observed error meets the unavoidable clipping lower bound"] = (
            lower_bound > 50.0 and observed_rmse >= lower_bound - 1e-8
        )
        checks[f"{name}: input percentiles collapse to the same tail"] = bool(
            fields["/amount"].content.eq(percentile).all()
        )
        checks[f"{name}: predictions saturate within 0.00001 source units"] = spread < 1e-5
        checks[f"{name}: mean prediction stays within 15 percent of the corresponding edge"] = distance < 0.15
    checks["Evaluation preserves both training distributions"] = normalization == {
        str(address): rf.Quantile.normalization(model, address) for address in addresses
    }
    return metrics, checks


if __name__ == "__main__":
    report(PROOF_ID, run, seed=6801)