Can one number representation serve additive and multiplicative structure?

Representation
Compare Number, which embeds raw and log lanes and reads out standardized log-scale deviations, with Quantile ranks on additive, moderate-tail, heavy-tail, and far-from-zero targets.

Example

price: 18.4
rate: 0.62
ltv: 15.1

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

Model tree with customer, price, rate, ltv. Amber cards are hidden prediction targets. Each arm changes only how numbers are represented.

Model tree with customer, price, rate, ltv. Amber cards are hidden prediction targets. Each arm changes only how numbers are represented.

Figure 1: Amber cards are hidden prediction targets. Each arm changes only how numbers are represented.

Comparison

Hold the data, model width, optimizer, and update budget fixed. Change only the numeric datatype: Number, which embeds a raw lane and a signed-log lane and reads targets out as standardized deviations on the signed-log scale, or Quantile, which represents values by their training percentile. Score every arm in source units against the known conditional median, on the bulk of the distribution and on its tails, for an additive task, for multiplicative tasks with moderate and very heavy tails, and for a target that sits near 1,000.

Result

P079 · Latest full run · Passing

Seed 7901 on cpu: 10 of 10 test checks met. History: 3 of 4 full runs met every check, across 1 distinct seeds. History can include earlier source versions.

Comparison Latest measurement
Heavy tails, Number 31.9% of baseline error
Far from zero, Number 6.0% of baseline error
Additive, Number 5.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.

Number has no scale option. Its two lanes make additive and multiplicative structure both readable: with moderate tails they are interchangeable, and only when the largest input is ten thousand times the median does a raw lane alone degrade, because standardization is then set by a handful of extreme rows. Its readout starts at the training mean on the signed-log scale and predicts standardized deviations, which is what lets a target near 1,000 or a heavy-tailed target train at the same budget as a centered one; a plain raw-unit head cannot grow a readout that spans either. A monotone target transform leaves the median-optimal MAE answer unchanged, so these are claims about optimization at a budget, not about the estimand. Quantile ranks are a fair input but a poor heavy-tailed target, because the inverse continues linearly past the learned support and a slightly high rank becomes an enormous source value.

Experiment details and code

Latest full run

Seed 7901, cpu, recorded 2026-10-22T21:46:40.431590+00:00. Outcome: met.

Source fingerprint: a3b4aab6a7c9f749f6aefc17337b546a239b74bd1c76ad6a718e0169e6324465. Python 3.12.10; Torch 2.12.0.

Measurement Value
train_rows 8192
test_rows 4096
budget 500
long_budget 1500
heavy oracle_mae: 20.0493; baseline_mae: 79.1491; training_median: 19.4939; training_support: 0.0212963, 12924.5; bulk_rows: 2572; tail_rows: 422; number_number: mae: 25.2418; oracle_ratio: 1.25898; normalized_mae: 0.318914; median_distance: 12.5744; shuffled_normalized_mae: 1.53812; beyond_support_rows: 6; beyond_support_error_share: 1.09874e-05; finite: True; bulk_oracle_ratio: 1.01147; tail_oracle_ratio: 1.45982; steps: 500; rank_number: mae: 26.6722; oracle_ratio: 1.33033; normalized_mae: 0.336987; median_distance: 16.3935; shuffled_normalized_mae: 1.51633; beyond_support_rows: 7; beyond_support_error_share: 1.57577e-05; finite: True; bulk_oracle_ratio: 1.0785; tail_oracle_ratio: 1.52874; steps: 500; number_rank: mae: 679.021; oracle_ratio: 33.8675; normalized_mae: 8.57901; median_distance: 679.171; shuffled_normalized_mae: 10.0841; beyond_support_rows: 167; beyond_support_error_share: 0.922241; finite: True; bulk_oracle_ratio: 4.96722; tail_oracle_ratio: 47.3447; steps: 500; number_number_long: mae: 24.2516; oracle_ratio: 1.2096; normalized_mae: 0.306404; median_distance: 13.5902; shuffled_normalized_mae: 1.73498; beyond_support_rows: 0; beyond_support_error_share: 0; finite: True; bulk_oracle_ratio: 1.13304; tail_oracle_ratio: 1.27353; steps: 1500
moderate oracle_mae: 13.6754; baseline_mae: 49.1659; training_median: 19.9681; training_support: 0.113902, 6132.08; bulk_rows: 2618; tail_rows: 353; number_number: mae: 15.3844; oracle_ratio: 1.12497; normalized_mae: 0.312907; median_distance: 5.83052; shuffled_normalized_mae: 1.52824; beyond_support_rows: 7; beyond_support_error_share: 2.05953e-05; finite: True; bulk_oracle_ratio: 1.0489; tail_oracle_ratio: 1.23042; steps: 500; rank_number: mae: 15.2002; oracle_ratio: 1.1115; normalized_mae: 0.309162; median_distance: 5.12388; shuffled_normalized_mae: 1.5344; beyond_support_rows: 3; beyond_support_error_share: 3.36829e-06; finite: True; bulk_oracle_ratio: 1.01363; tail_oracle_ratio: 1.26312; steps: 500
additive oracle_mae: 1.59083; baseline_mae: 33.2309; training_median: 1.02628; training_support: -98.104, 100.625; bulk_rows: None; tail_rows: None; number_number: mae: 1.83023; oracle_ratio: 1.15049; normalized_mae: 0.0550761; median_distance: 0.887503; shuffled_normalized_mae: 1.38533; beyond_support_rows: 0; beyond_support_error_share: 0; finite: True; steps: 500; rank_number: mae: 2.31764; oracle_ratio: 1.45688; normalized_mae: 0.0697436; median_distance: 1.70544; shuffled_normalized_mae: 1.41197; beyond_support_rows: 0; beyond_support_error_share: 0; finite: True; steps: 500
narrow oracle_mae: 1.56035; baseline_mae: 33.8514; training_median: 1000.09; training_support: 900.774, 1101.75; bulk_rows: None; tail_rows: None; number_number: mae: 2.0455; oracle_ratio: 1.31093; normalized_mae: 0.0604259; median_distance: 1.37; shuffled_normalized_mae: 1.39944; beyond_support_rows: 0; beyond_support_error_share: 0; finite: True; steps: 500; rank_number: mae: 3.09951; oracle_ratio: 1.98642; normalized_mae: 0.0915622; median_distance: 2.89733; shuffled_normalized_mae: 1.41378; beyond_support_rows: 0; beyond_support_error_share: 0; finite: True; steps: 500
Behavioral checks
Behavioral check Outcome
Heavy tails: Number reaches 1.5 times the oracle MAE Met
Heavy tails: Number stays within 10 percent of Quantile inputs Met
Moderate tails: Number reaches 1.5 times the oracle MAE Met
Moderate tails: Number stays within 10 percent of Quantile inputs Met
Additive: Number reaches 1.5 times the oracle MAE Met
Additive: Number stays within 10 percent of Quantile inputs Met
Far from zero: Number reaches 1.5 times the oracle MAE Met
Far from zero: Number stays within 10 percent of Quantile inputs Met
Shuffled inputs lose the relationship in every arm Met
Every arm produces finite predictions Met

Full run history

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

Seed Outcome Recorded Code fingerprint
7901 not met 2026-10-22T17:03:36.628414+00:00 d4c2be0da6b78d35a23973d940341d570e2738e403342ed6665cef797fd22457
7901 met 2026-10-22T17:15:15.584262+00:00 305bb2b60c05017743618988cda615d1261964e75491c28fb7a7213cfc58a9bc
7901 met 2026-10-22T18:29:18.256168+00:00 ac220ea14f44d43e81d06d7812db6c73cb2aebe9601efa3a45ac4dc4b5ea0dd2
7901 met 2026-10-22T21:46:40.431590+00:00 ac220ea14f44d43e81d06d7812db6c73cb2aebe9601efa3a45ac4dc4b5ea0dd2

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 P079

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

PYTHONPATH=proofs python proofs/representation/number_lanes.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 processes and oracles

The additive task draws left and right uniformly on [-50, 50] and sets total = left + right + 2 z; the far-from-zero task adds 1,000 to that total, so its target spans roughly 900 to 1,100. The multiplicative tasks draw price = exp(3 + 1.2 z1) and rate = exp(s z2) from independent standard normals, then ltv = price * sqrt(rate) * exp(0.3 z3). Prices have a median near 20 and a long tail; rates have a median of 1. The heavy task uses s = 2.5, where the largest of 8,192 training rates is about ten thousand times the median, the regime of RelBench’s study-adverse count. The moderate task uses s = 1.5, where that ratio is a few hundred. The noise is symmetric in log units, so the conditional median is exactly price * sqrt(rate), and the evaluator computes it from the visible inputs without any latent variable.

Oracle MAE is the test-set error of the conditional median. Baseline MAE is the error of the training-median constant. The multiplicative bulk is the set of test rows whose price and rate both fall between the 10th and 90th training percentiles; the tail is any row above the 95th percentile of either input. Every arm also records how much of its absolute error comes from predictions outside the training target range.

Arms and budget

Inputs embed as Number or as Quantile ranks; targets train as a Number or as a Quantile with the MAE objective. Every task compares the two input choices with a Number target; the heavy task also compares the two target choices with Number inputs, and gives the Number arm three times the budget once.

Every arm uses the xs preset, 8,192 training rows, 1,024 validation rows, batch size 128, learning rate 0.002, and 500 AdamW updates (1,500 for the long arms), scored once on 4,096 fresh rows. A shuffled-input control pairs each test target with another row’s inputs. Lane weight norms of the first projection are recorded for the combined arms as a diagnostic.

Gates

Two earlier full runs ablated the lanes and the target scale through options that no longer exist. The first, on moderate tails only, refuted a bulk-blur gate for the raw lane; a sweep located the raw lane’s degradation at s >= 2.5; the second met every gate with both regimes and showed a raw-unit target lagging a log-unit target even at three times the budget. Those runs remain in the evidence under their own code fingerprints, and their findings became the datatype’s defaults. This version gates the contract of the resulting Number against Quantile.

Gates remain provisional until ten-seed calibration. Number must reach at most 1.5 times the oracle MAE on every task and stay within 10 percent of Quantile inputs on every task. Every arm must produce finite predictions and lose the relationship under shuffled inputs. The Quantile target, the long-budget arm, and the beyond-support error shares are diagnostics without gates.

Remaining work

The tasks are two-input and noise-free apart from symmetric log or additive noise. They do not cover bounded or bimodal bulks, where a rank lane may add information beyond the log lane, or mixed-sign heavy tails. Mixed precision is covered only by the recorded side experiments: an fp32 embedder and readout under bf16 autocast matched fp32 scores, which is now the default.

Complete experiment code

Code
"""P079: compare Number's lanes and log-scale readout with Quantile ranks across numeric regimes."""

from collections.abc import Callable, Iterator
from functools import partial

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

PROOF_ID = "P079"
BUDGET = 500
LONG_BUDGET = 1500
BATCH_SIZE = 128
TRAIN_ROWS = 8192
TEST_ROWS = 4096
PRICE_SCALE = 1.2
HEAVY_SCALE = 2.5
MODERATE_SCALE = 1.5
NOISE = 0.3
SPAN = 50.0
ADDITIVE_NOISE = 2.0
OFFSET = 1000.0
INPUTS = {
    "heavy": ("price", "rate"),
    "moderate": ("price", "rate"),
    "additive": ("left", "right"),
    "narrow": ("left", "right"),
}
TARGETS = {"heavy": "ltv", "moderate": "ltv", "additive": "total", "narrow": "total"}
ARMS = (
    ("heavy", "number", "number", BUDGET),
    ("heavy", "rank", "number", BUDGET),
    ("heavy", "number", "rank", BUDGET),
    ("heavy", "number", "number", LONG_BUDGET),
    ("moderate", "number", "number", BUDGET),
    ("moderate", "rank", "number", BUDGET),
    ("additive", "number", "number", BUDGET),
    ("additive", "rank", "number", BUDGET),
    ("narrow", "number", "number", BUDGET),
    ("narrow", "rank", "number", BUDGET),
)


def multiplicative(*, rows: int, seed: int, scale: float = HEAVY_SCALE) -> Iterator[dict]:
    """Lognormal prices and rates multiply under symmetric log noise; ``scale`` sets the rate tail."""
    rng = np.random.default_rng(seed)
    price = np.exp(3.0 + PRICE_SCALE * rng.standard_normal(rows))
    rate = np.exp(scale * rng.standard_normal(rows))
    ltv = price * np.sqrt(rate) * np.exp(NOISE * rng.standard_normal(rows))
    for index in range(rows):
        yield {"price": float(price[index]), "rate": float(rate[index]), "ltv": float(ltv[index])}


def additive(*, rows: int, seed: int, offset: float = 0.0) -> Iterator[dict]:
    """Two uniform terms add under symmetric noise; ``offset`` moves the total away from zero."""
    rng = np.random.default_rng(seed)
    left = rng.uniform(-SPAN, SPAN, rows)
    right = rng.uniform(-SPAN, SPAN, rows)
    total = offset + left + right + ADDITIVE_NOISE * rng.standard_normal(rows)
    for index in range(rows):
        yield {"left": float(left[index]), "right": float(right[index]), "total": float(total[index])}


RECORDS: dict[str, Callable[..., Iterator[dict]]] = {
    "heavy": multiplicative,
    "moderate": partial(multiplicative, scale=MODERATE_SCALE),
    "additive": additive,
    "narrow": partial(additive, offset=OFFSET),
}


def median(task: str, rows: list[dict]) -> np.ndarray:
    """Conditional median of the target given the visible inputs."""
    if task == "additive":
        return np.asarray([row["left"] + row["right"] for row in rows])
    if task == "narrow":
        return np.asarray([OFFSET + row["left"] + row["right"] for row in rows])
    return np.asarray([row["price"] * np.sqrt(row["rate"]) for row in rows])


def build(task: str, inputs: str, target: str) -> rf.Model:
    scalar = rf.Quantile if inputs == "rank" else rf.Number
    fields: dict[str, object] = {name: scalar for name in INPUTS[task]}
    if target == "rank":
        fields[TARGETS[task]] = rf.Quantile(objective="mae", mask=True)
    else:
        fields[TARGETS[task]] = rf.Number(mask=True)
    return rf.Model.xs(fields=fields)


def fit(model: rf.Model, train, validate, seed: int, steps: int | None, accelerator: str, budget: int) -> int:
    data = rf.SyntheticDataModule(train=train, validate=validate, seed=seed)
    model.fit(
        data,
        batch_size=BATCH_SIZE,
        learning_rate=0.002,
        early_stopping=False,
        rollback=False,
        precision="32-true",
        strategy="auto",
        accelerator=accelerator,
        devices=1,
        max_epochs=-1,
        max_steps=budget if steps is None else min(steps, budget),
        deterministic=True,
        logger=False,
        enable_checkpointing=False,
        enable_progress_bar=False,
        enable_model_summary=False,
        num_sanity_val_steps=0,
    )
    model.eval()
    return model.trainer.global_step


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


def evaluate(output: np.ndarray, control: np.ndarray, reference: dict, bulk: np.ndarray | None) -> dict:
    target, oracle = reference["target"], reference["oracle"]
    error = np.abs(output - target)
    mae = float(error.mean())
    beyond = (output < reference["support"][0]) | (output > reference["support"][1])
    result = {
        "mae": mae,
        "oracle_ratio": mae / reference["oracle_mae"],
        "normalized_mae": mae / reference["baseline_mae"],
        "median_distance": float(np.mean(np.abs(output - oracle))),
        "shuffled_normalized_mae": float(np.mean(np.abs(control - target))) / reference["baseline_mae"],
        "beyond_support_rows": int(beyond.sum()),
        "beyond_support_error_share": float(error[beyond].sum() / error.sum()) if error.sum() else 0.0,
        "finite": bool(np.isfinite(output).all() and np.isfinite(control).all()),
    }
    if bulk is not None:
        tail = reference["tail"]
        irreducible = np.abs(target - oracle)
        result["bulk_oracle_ratio"] = float(error[bulk].mean() / irreducible[bulk].mean())
        result["tail_oracle_ratio"] = float(error[tail].mean() / irreducible[tail].mean())
    return result


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    stream = seed * 100
    metrics: dict = {"train_rows": TRAIN_ROWS, "test_rows": TEST_ROWS, "budget": BUDGET, "long_budget": LONG_BUDGET}
    for offset, task in ((0, "heavy"), (20, "moderate"), (10, "additive"), (30, "narrow")):
        records, name = RECORDS[task], TARGETS[task]
        training = list(records(rows=TRAIN_ROWS, seed=stream + offset + 1))
        test = list(records(rows=TEST_ROWS, seed=stream + offset + 3))
        target = np.asarray([row[name] for row in test])
        oracle = median(task, test)
        observed = np.asarray([row[name] for row in training])
        constant = float(np.median(observed))
        reference = {
            "target": target,
            "oracle": oracle,
            "oracle_mae": float(np.mean(np.abs(target - oracle))),
            "baseline_mae": float(np.mean(np.abs(target - constant))),
            "support": (float(observed.min()), float(observed.max())),
        }
        bulk = None
        if task in ("heavy", "moderate"):
            columns = {key: np.asarray([row[key] for row in training]) for key in INPUTS[task]}
            low = {key: np.percentile(values, 10) for key, values in columns.items()}
            high = {key: np.percentile(values, 90) for key, values in columns.items()}
            extreme = {key: np.percentile(values, 95) for key, values in columns.items()}
            bulk = np.all([[low[key] <= row[key] <= high[key] for key in INPUTS[task]] for row in test], axis=1)
            reference["tail"] = np.any([[row[key] > extreme[key] for key in INPUTS[task]] for row in test], axis=1)
        permutation = np.random.default_rng(stream + offset + 4).permutation(TEST_ROWS)
        shuffled = [{**test[index], name: row[name]} for row, index in zip(test, permutation, strict=True)]
        metrics[task] = {
            "oracle_mae": reference["oracle_mae"],
            "baseline_mae": reference["baseline_mae"],
            "training_median": constant,
            "training_support": list(reference["support"]),
            "bulk_rows": None if bulk is None else int(bulk.sum()),
            "tail_rows": None if bulk is None else int(reference["tail"].sum()),
        }
        for arm_task, inputs, space, budget in ARMS:
            if arm_task != task:
                continue
            lit.seed_everything(seed, workers=True)
            model = build(task, inputs, space)
            updates = fit(
                model,
                partial(records, rows=TRAIN_ROWS, seed=stream + offset + 1),
                partial(records, rows=1024, seed=stream + offset + 2),
                seed,
                steps,
                accelerator,
                budget,
            )
            result = evaluate(predict(model, task, test), predict(model, task, shuffled), reference, bulk)
            result["steps"] = updates
            metrics[task][f"{inputs}_{space}" + ("_long" if budget != BUDGET else "")] = result
    arms = [value for task in TARGETS for value in metrics[task].values() if isinstance(value, dict)]
    checks = {}
    for task in TARGETS:
        label = {
            "heavy": "Heavy tails",
            "moderate": "Moderate tails",
            "additive": "Additive",
            "narrow": "Far from zero",
        }[task]
        checks[f"{label}: Number reaches 1.5 times the oracle MAE"] = (
            metrics[task]["number_number"]["oracle_ratio"] <= 1.5
        )
        checks[f"{label}: Number stays within 10 percent of Quantile inputs"] = (
            metrics[task]["number_number"]["mae"] <= 1.10 * metrics[task]["rank_number"]["mae"]
        )
    checks["Shuffled inputs lose the relationship in every arm"] = all(
        arm["shuffled_normalized_mae"] >= 0.90 for arm in arms
    )
    checks["Every arm produces finite predictions"] = all(arm["finite"] for arm in arms)
    return metrics, checks


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