Can percentile-based predictions return useful original values?

Quantile
Represent amounts and costs by their positions in the training distribution, then convert predicted costs back to their original units.

Example

amount: 100.0
cost: 506.0

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

Score new records in source units and shuffle amounts while retaining their original costs. Check that evaluation does not change the learned distributions.

Result

P067 · Latest full run · Passing

Seed 6710 on cpu: 9 of 9 test checks met. 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
Original amounts 2.4% of baseline error
Shuffled amounts 139.5% of baseline error

Baseline error compares prediction error with a constant guess. 100% matches that guess; lower is better. Details specify the baseline and root-mean-square error calculation.

The learned relationship remains useful after conversion back. Shuffling removes accuracy; this does not establish behavior beyond the learned numerical range.

Experiment details and code

Latest full run

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

Source fingerprint: b711521548564b2bc08f47da3d5db43937e466804b2ab75ac3f15d2155612aaf. Python 3.12.10; Torch 2.12.0.

Measurement Value
steps 400
train_rows 4096
test_rows 2048
training_mean 1009.52
baseline_rmse 768.964
noise_rmse 10
rmse 18.1568
normalized_rmse 0.0236121
shuffled_rmse 1072.66
shuffled_normalized_rmse 1.39494
normalization /amount: count: 51200; compression: 100; minimum: 0.0462482; median: 175.7; maximum: 953.059; /cost: count: 51200; compression: 100; minimum: 184.852; median: 725.741; maximum: 3066.43
target_tampering_max_delta 0
checkpoint_max_delta 0
Behavioral checks
Behavioral check Outcome
Held-out source-unit normalized RMSE below 0.25 Met
Shuffled input normalized RMSE above 0.90 Met
Shuffling more than triples the RMSE Met
Predictions in original units are finite Met
Both digests count exactly the consumed training rows Met
Evaluation preserves both training distributions Met
Hidden target changes do not affect predictions Met
Checkpoint preserves both training distributions Met
Checkpoint predictions remain within 0.0001 source units Met

Full run history

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

Seed Outcome Recorded Code fingerprint
6701 met 2026-09-21T21:04:20.476298+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6702 met 2026-09-21T21:04:30.646891+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6703 met 2026-09-21T21:04:40.873029+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6704 met 2026-09-21T21:06:28.497766+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6705 met 2026-09-21T21:06:38.698478+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6706 met 2026-09-21T21:06:49.060937+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6707 met 2026-09-21T21:06:59.593858+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6708 met 2026-09-21T21:07:31.912640+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6701 met 2026-09-21T21:05:12.206814+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6709 met 2026-09-21T21:08:12.297121+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6
6710 met 2026-09-21T21:08:22.841737+00:00 f02866326306b441e46e0b02cbf1a7879c69210b2db1f450db31d00308fc32a6

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 P067

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

PYTHONPATH=proofs python proofs/quantile/rank_regression.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 observability

Draw an independent latent u ~ Uniform(0, 1) for each row, then set amount = 50 * (exp(3u) - 1) and cost = 200 + 3 * amount + Normal(0, 10). The positive amounts are skewed; the model sees only amount, never the generating u. Cost is always hidden from embedding. Its conditional mean and median coincide, and the known source-unit noise RMSE is 10. A random row split is appropriate because rows are independent and contain no persistent identities.

Training and controls

Train the xs preset for 400 AdamW updates, batch size 128 and learning rate 0.002, using 4,096 training rows and 1,024 independently seeded validation rows. Validation does not select a checkpoint: evaluation uses the fixed final update. Test 2,048 new rows once. Both inputs and targets use the default digest compression; the target objective is percentile MSE.

The predeclared gate is original-unit RMSE divided by the training-mean baseline RMSE below 0.25. Shuffling the visible input must give normalized RMSE above 0.90 and more than three times the intended error. The primary prediction receives only amount; a separate target-tampering check verifies the mask. Require finite predictions, exactly 128 digest observations per update, frozen evaluation state, and checkpoint prediction stability within 0.0001 source units. Counts include repeated training epochs.

Remaining work

The ten-seed calibration retained the predeclared gates and fixed training budget. The task tests one smooth relationship with moderate skew. It does not establish calibrated intervals, arbitrary tail accuracy, drift adaptation, or extrapolation beyond the fitted target distribution.

Complete experiment code

Code
"""P067: learn a skewed numeric relationship through Quantile representations."""

from collections.abc import Iterator
from functools import partial
from pathlib import Path
from tempfile import TemporaryDirectory

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

import relflow as rf

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


def records(*, rows: int, seed: int) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    amount = 50.0 * np.expm1(3.0 * rng.uniform(0.0, 1.0, rows))
    cost = 200.0 + 3.0 * amount + rng.normal(0.0, 10.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:
    predictions = model.predict(pa.Table.from_pylist(rows))["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}
    rows = list(records(rows=TEST_ROWS, seed=seed + 3))
    targets = np.asarray([row["cost"] for row in rows])
    visible = [{"amount": row["amount"]} 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 - targets) ** 2)))
    output = predict(model, visible)
    shuffled = np.random.default_rng(seed + 4).permutation(TEST_ROWS)
    control = predict(model, [visible[index] for index in shuffled])
    rmse = float(np.sqrt(np.mean((output - targets) ** 2)))
    control_rmse = float(np.sqrt(np.mean((control - targets) ** 2)))
    original = predict(model, rows[:128])
    tampered = predict(model, [{**row, "cost": row["cost"] + 1e6} for row in rows[:128]])
    target_delta = float(np.max(np.abs(original - tampered)))
    evaluation_frozen = normalization == {
        str(address): rf.Quantile.normalization(model, address) for address in addresses
    }
    with TemporaryDirectory(prefix="relflow-quantile-proof-") as directory:
        path = Path(directory) / "model.pt"
        model.save(path)
        restored = rf.Model.load(path).to(model.device)
        restored.eval()
        checkpoint_delta = float(np.max(np.abs(original - predict(restored, rows[:128]))))
        checkpoint_state = normalization == {
            str(address): rf.Quantile.normalization(restored, address) for address in addresses
        }
    metrics = {
        "steps": trainer.global_step,
        "train_rows": TRAIN_ROWS,
        "test_rows": TEST_ROWS,
        "training_mean": training_mean,
        "baseline_rmse": baseline_rmse,
        "noise_rmse": 10.0,
        "rmse": rmse,
        "normalized_rmse": rmse / baseline_rmse,
        "shuffled_rmse": control_rmse,
        "shuffled_normalized_rmse": control_rmse / baseline_rmse,
        "normalization": normalization,
        "target_tampering_max_delta": target_delta,
        "checkpoint_max_delta": checkpoint_delta,
    }
    checks = {
        "Held-out source-unit normalized RMSE below 0.25": rmse / baseline_rmse < 0.25,
        "Shuffled input normalized RMSE above 0.90": control_rmse / baseline_rmse > 0.90,
        "Shuffling more than triples the RMSE": control_rmse > 3.0 * rmse,
        "Predictions in original units are finite": bool(np.isfinite(output).all() and np.isfinite(control).all()),
        "Both digests count exactly the consumed training rows": all(
            state["count"] == trainer.global_step * BATCH_SIZE for state in normalization.values()
        ),
        "Evaluation preserves both training distributions": evaluation_frozen,
        "Hidden target changes do not affect predictions": target_delta < 1e-8,
        "Checkpoint preserves both training distributions": checkpoint_state,
        "Checkpoint predictions remain within 0.0001 source units": checkpoint_delta < 1e-4,
    }
    return metrics, checks


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