From bf8767b67312e23bc19b6e5487a096c9d1a3239d Mon Sep 17 00:00:00 2001 From: Bradford Levy <5950006+bradfordlynch@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:38:04 -0700 Subject: [PATCH 1/2] =?UTF-8?q?Add=20contest=20scoring:=20OLS=20ports,=20m?= =?UTF-8?q?ean-imputation,=20and=20ranking=20(Rules=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Official Rules incorporate this file as the controlling scoring methodology, so it must carry the full contest calculation, not just the percentile transform. Adds verbatim float-for-float ports of the production OLS (ols_fit / ols_fit2, degenerate-fit guards included), the submission-specific mean imputation over the common Scored Events set (mean computed before filling; no coverage => ineligible), score_submission emitting both the no-fill and `_imputed` metric families, and rank_submissions implementing the contest order (metric, then higher pre-imputation coverage, then submission id; residual exact ties share the combined prize — no random tie-break). --- src/examples/scoring.py | 278 ++++++++++++++++++++++++++++++++++++++-- tests/test_scoring.py | 136 ++++++++++++++++++++ 2 files changed, 404 insertions(+), 10 deletions(-) diff --git a/src/examples/scoring.py b/src/examples/scoring.py index cf96194..6e52201 100644 --- a/src/examples/scoring.py +++ b/src/examples/scoring.py @@ -1,26 +1,47 @@ """Exact port of the Explaining Markets competition scoring transform. -Scoring happens within a **period** (a calendar quarter). For each realized -``(event, focal asset)`` outcome the competition converts the one-day cumulative -abnormal return (``car1``) into a percentile rank ``y`` in ``[0, 1]`` across the -period's cross-section, and likewise percentile-ranks the earnings surprise. A -submission's period fit is the two-regressor OLS +Scoring happens within a **period** (a calendar quarter, or the official +contest Scoring Period). For each realized ``(event, focal asset)`` outcome the +competition converts the one-day cumulative abnormal return (``car1``) into a +percentile rank ``y`` in ``[0, 1]`` across the period's cross-section, and +likewise percentile-ranks the earnings surprise. A submission's period fit is +the two-regressor OLS y = alpha + beta1 * predicted_percentile + beta2 * surprise_percentile so its R^2 measures how much of the realized abnormal-return *ranking* the submission's predicted percentiles explain, over and above the naive -earnings-surprise benchmark. +earnings-surprise benchmark (the same sample refit on the surprise alone; +``delta_r_squared`` is the difference between the two R^2 values). -:func:`percentile_ranks` is a verbatim port of the production scorer, tie handling and -all, so offline numbers match what the leaderboard computes. The rest assembles -the per-``(event, asset)`` analysis frame from archive records (as produced by -:func:`examples.archive.load_archive` / :func:`examples.archive.read_jsonl_gz`). +**Contest scoring — the imputed family (Official Rules §5).** For the contest, +every submission is evaluated on the same common set of valid scored events +(the "Scored Events"). First the arithmetic mean of the submission's timely, +valid predictions on that common set is computed — *before* any filling. Every +Scored Event the submission did not (validly) predict is then imputed with +that submission-specific mean, and the identical two-regressor OLS is fit over +the *full* common set. These metrics carry an ``_imputed`` suffix +(``r_squared_imputed``, ``r_squared_surprise_imputed``, +``delta_r_squared_imputed`` — the contest ranking metric). A submission with +no timely, valid prediction on any Scored Event has an undefined mean and is +not eligible (no imputed metrics). An exact tie on the ranking metric resolves +first to the higher timely-prediction coverage before imputation (``n_obs``); +prizes for any remaining exact tie are combined and split equally — no random +tie-break. + +:func:`percentile_ranks`, :func:`ols_fit`, and :func:`ols_fit2` are verbatim +ports of the production scorer — tie handling, degenerate-fit guards and all — +so offline numbers match what the leaderboard computes float-for-float. The +rest assembles the per-``(event, asset)`` analysis frame from archive records +(as produced by :func:`examples.archive.load_archive` / +:func:`examples.archive.read_jsonl_gz`) and applies the imputation/ranking +rules above. """ from __future__ import annotations from collections.abc import Iterable, Iterator, Sequence +from dataclasses import dataclass import pandas as pd @@ -126,3 +147,240 @@ def add_percentiles(frame: pd.DataFrame) -> pd.DataFrame: col[i] = r out["surprise_pct"] = col return out + + +# ----- OLS (verbatim ports of the production scorer) ------------------------ + + +@dataclass(frozen=True) +class OLSFit: + """Result of regressing ``y`` (percentile-ranked CAR) on the predictors. + + ``beta`` is the coefficient on the prediction (or, for the surprise-only + benchmark fit, on the single regressor). ``beta_surprise`` is the + coefficient on the percentile-ranked earnings surprise in the two-regressor + model — ``None`` for a univariate fit. + """ + + n: int + alpha: float + beta: float + r_squared: float + mse: float + beta_surprise: float | None = None + + +def ols_fit(points: list[tuple[float, float]]) -> OLSFit | None: + """Fit ``y = alpha + beta*x`` over ``(x, y)`` points. + + Returns ``None`` when the regression is undefined — fewer than 2 points, or + zero variance in ``x`` (beta/R^2 indeterminate). R^2 is the squared Pearson + correlation, identically ``1 - SS_res/SS_tot``. Verbatim port of the + production scorer. + """ + n = len(points) + if n < 2: + return None + mean_x = sum(p[0] for p in points) / n + mean_y = sum(p[1] for p in points) / n + s_xx = sum((p[0] - mean_x) ** 2 for p in points) + s_yy = sum((p[1] - mean_y) ** 2 for p in points) + s_xy = sum((p[0] - mean_x) * (p[1] - mean_y) for p in points) + if s_xx == 0.0: + # No spread in predictions — slope and explained variance undefined. + return None + beta = s_xy / s_xx + alpha = mean_y - beta * mean_x + ss_res = sum((p[1] - (alpha + beta * p[0])) ** 2 for p in points) + mse = ss_res / n + # SS_tot == 0 (all outcomes identical) leaves R^2 undefined; report 0.0 — a + # constant target carries no variance for predictions to explain. + r_squared = 0.0 if s_yy == 0.0 else 1.0 - ss_res / s_yy + return OLSFit(n=n, alpha=alpha, beta=beta, r_squared=r_squared, mse=mse) + + +def ols_fit2(points: list[tuple[float, float, float]]) -> OLSFit | None: + """Fit ``y = alpha + beta1*x1 + beta2*x2`` over ``(x1, x2, y)`` points + (x1 = the submission's prediction, x2 = percentile-ranked surprise). + + Solves the centered 2x2 normal equations in closed form (Cramer's rule). + ``beta`` carries beta1 (the prediction coefficient), ``beta_surprise`` + carries beta2, ``r_squared`` is the multiple R^2. Returns ``None`` with + fewer than 3 points, or when the 2x2 system is singular — a regressor with + no spread, or perfect collinearity — in which case the submission is left + unranked (there is deliberately NO univariate fallback). Verbatim port of + the production scorer. + """ + n = len(points) + if n < 3: + return None + mean_x1 = sum(p[0] for p in points) / n + mean_x2 = sum(p[1] for p in points) / n + mean_y = sum(p[2] for p in points) / n + s11 = sum((p[0] - mean_x1) ** 2 for p in points) + s22 = sum((p[1] - mean_x2) ** 2 for p in points) + s12 = sum((p[0] - mean_x1) * (p[1] - mean_x2) for p in points) + s1y = sum((p[0] - mean_x1) * (p[2] - mean_y) for p in points) + s2y = sum((p[1] - mean_x2) * (p[2] - mean_y) for p in points) + s_yy = sum((p[2] - mean_y) ** 2 for p in points) + det = s11 * s22 - s12 * s12 + if det == 0.0: + return None + beta1 = (s22 * s1y - s12 * s2y) / det + beta2 = (s11 * s2y - s12 * s1y) / det + alpha = mean_y - beta1 * mean_x1 - beta2 * mean_x2 + ss_res = sum( + (p[2] - (alpha + beta1 * p[0] + beta2 * p[1])) ** 2 for p in points + ) + mse = ss_res / n + r_squared = 0.0 if s_yy == 0.0 else 1.0 - ss_res / s_yy + return OLSFit( + n=n, alpha=alpha, beta=beta1, r_squared=r_squared, mse=mse, beta_surprise=beta2 + ) + + +# ----- Contest imputation + scoring (Official Rules §5) ---------------------- + + +@dataclass(frozen=True) +class ImputedPredictions: + """A submission's mean-filled prediction vector over the common sample. + + ``values`` has one entry per common-sample row (in row order): the + submission's own prediction where it made one, else ``imputed_mean`` — the + arithmetic mean of its timely, valid predictions computed BEFORE filling. + ``imputed_event_count`` is how many rows were filled. + """ + + values: list[float] + imputed_mean: float + imputed_event_count: int + + +def impute_predictions( + common: pd.DataFrame, prediction_col: str +) -> ImputedPredictions | None: + """Mean-impute a submission's missing predictions over the common sample. + + ``common`` is the common-sample frame (every Scored Event row — see + :func:`score_submission`); ``prediction_col`` holds the submission's + predicted percentiles with ``NaN`` where it made no timely, valid + prediction. Per Rules §5 the fill value is the mean of the *present* + predictions, computed before filling. Returns ``None`` when the submission + predicted nothing on the common sample — the mean is undefined and the + submission is not eligible for imputed metrics (or prizes). + """ + raw = common[prediction_col].tolist() + present = [float(v) for v in raw if pd.notna(v)] + if not present: + return None + mean = sum(present) / len(present) + values = [float(v) if pd.notna(v) else mean for v in raw] + return ImputedPredictions( + values=values, + imputed_mean=mean, + imputed_event_count=len(raw) - len(present), + ) + + +def score_submission(frame: pd.DataFrame, prediction_col: str) -> dict: + """Both metric families for one submission over one period's frame. + + ``frame`` is an :func:`add_percentiles` output; ``prediction_col`` holds the + submission's predicted percentiles (``NaN`` = no timely, valid prediction). + + The **common sample** (the Rules' "Scored Events") is every row carrying + both ``y`` and ``surprise_pct``. The no-fill family fits only the rows the + submission actually predicted; the ``_imputed`` family mean-fills the rest + (:func:`impute_predictions`) and fits the full common sample. The + surprise-only benchmark for the imputed family is fit on the full common + sample, so it is identical for every submission. + + Returns a flat dict: ``n_obs`` (timely predictions on the common sample — + the pre-imputation coverage and the first tie-break), the no-fill + ``r_squared_surprise / r_squared / delta_r_squared / beta / beta_surprise / + alpha / mse``, their ``_imputed`` twins, and ``imputed_mean`` / + ``imputed_event_count``. Undefined metrics are ``None``. + """ + common = frame[frame["surprise_pct"].notna() & frame["y"].notna()] + xs = common[prediction_col].tolist() + ss = common["surprise_pct"].tolist() + ys = common["y"].tolist() + + out: dict = { + "n_obs": 0, + "imputed_mean": None, + "imputed_event_count": None, + } + names = ( + "r_squared_surprise", + "r_squared", + "delta_r_squared", + "beta", + "beta_surprise", + "alpha", + "mse", + ) + for name in names: + out[name] = None + out[f"{name}_imputed"] = None + + def emit(fit: OLSFit | None, surprise_fit: OLSFit | None, suffix: str) -> None: + if fit is None: + return + out[f"r_squared{suffix}"] = fit.r_squared + out[f"beta{suffix}"] = fit.beta + out[f"beta_surprise{suffix}"] = fit.beta_surprise + out[f"alpha{suffix}"] = fit.alpha + out[f"mse{suffix}"] = fit.mse + if surprise_fit is not None: + out[f"r_squared_surprise{suffix}"] = surprise_fit.r_squared + out[f"delta_r_squared{suffix}"] = fit.r_squared - surprise_fit.r_squared + + # No-fill family: only the rows the submission actually predicted. + points = [ + (float(x), s, y) for x, s, y in zip(xs, ss, ys, strict=True) if pd.notna(x) + ] + out["n_obs"] = len(points) + fit = ols_fit2(points) + emit(fit, ols_fit([(s, y) for _x, s, y in points]) if fit else None, "") + + # Imputed family (Rules §5): mean-fill, refit over the FULL common sample. + imputed = impute_predictions(common, prediction_col) + if imputed is not None: + out["imputed_mean"] = imputed.imputed_mean + out["imputed_event_count"] = imputed.imputed_event_count + ipoints = list(zip(imputed.values, ss, ys, strict=True)) + ifit = ols_fit2(ipoints) + emit(ifit, ols_fit(list(zip(ss, ys, strict=True))) if ifit else None, "_imputed") + + return out + + +def rank_submissions( + scores: pd.DataFrame, metric: str = "delta_r_squared_imputed" +) -> pd.DataFrame: + """Order a per-submission scores frame by the contest ranking rules. + + ``scores`` needs one row per submission with columns ``submission``, + ``n_obs``, and ``metric`` (e.g. the frame of :func:`score_submission` + outputs). Rows where ``metric`` is undefined (``NaN``/``None``) are dropped + — an undefined fit is unranked. Order: the metric descending (ascending + for the ``mse`` family), then higher ``n_obs`` (timely coverage before + imputation), then ``submission`` ascending. A ``rank`` column numbers the + result 1..n. + + Note per Rules §5 an exact residual tie after the ``n_obs`` tie-break does + NOT reorder further by any random device — tied positions share the + combined prize equally. The deterministic ``submission`` sort here is + display order only. + """ + ranked = scores[scores[metric].notna()].copy() + ascending = metric in ("mse", "mse_imputed") + ranked = ranked.sort_values( + by=[metric, "n_obs", "submission"], + ascending=[ascending, False, True], + kind="mergesort", + ).reset_index(drop=True) + ranked["rank"] = range(1, len(ranked) + 1) + return ranked diff --git a/tests/test_scoring.py b/tests/test_scoring.py index 5d1014c..a128332 100644 --- a/tests/test_scoring.py +++ b/tests/test_scoring.py @@ -5,12 +5,18 @@ import math import pandas as pd +import pytest from examples.scoring import ( add_percentiles, event_asset_rows, + impute_predictions, + ols_fit, + ols_fit2, outcomes_frame, percentile_ranks, + rank_submissions, + score_submission, ) @@ -103,3 +109,133 @@ def test_percentiles_align_with_pandas_index() -> None: out = add_percentiles(df) assert out.loc[out["car1"] == 1.0, "y"].iloc[0] == 0.0 assert out.loc[out["car1"] == 9.0, "y"].iloc[0] == 1.0 + + +# ----- OLS ports -------------------------------------------------------------- + + +def test_ols_fit_known_line() -> None: + fit = ols_fit([(1.0, 3.0), (2.0, 5.0), (3.0, 7.0)]) # y = 2x + 1 exactly + assert fit is not None + assert fit.beta == pytest.approx(2.0) + assert fit.alpha == pytest.approx(1.0) + assert fit.r_squared == pytest.approx(1.0) + + +def test_ols_fit_degenerate_is_none() -> None: + assert ols_fit([(0.1, 0.2)]) is None + assert ols_fit([(0.3, 0.1), (0.3, 0.9)]) is None # zero variance in x + + +def test_ols_fit2_recovers_known_plane() -> None: + def y(x1: float, x2: float) -> float: + return 0.2 + 0.5 * x1 + 0.3 * x2 + + pts = [(x1, x2, y(x1, x2)) for x1, x2 in [(0, 0), (1, 0.5), (0.5, 1), (0.2, 0.9)]] + fit = ols_fit2(pts) + assert fit is not None + assert fit.beta == pytest.approx(0.5) + assert fit.beta_surprise == pytest.approx(0.3) + assert fit.alpha == pytest.approx(0.2) + assert fit.r_squared == pytest.approx(1.0) + + +def test_ols_fit2_singular_is_none() -> None: + # x2 perfectly collinear with x1 -> det == 0 -> unranked, no fallback. + pts = [(x, 2 * x, 0.5 * x) for x in (0.1, 0.4, 0.8)] + assert ols_fit2(pts) is None + + +# ----- Contest imputation + ranking (Official Rules §5) ------------------------- + + +def _contest_frame() -> pd.DataFrame: + """5 common-sample events; sub_a predicts 3, sub_b predicts all 5, + sub_c predicts none.""" + records = [ + _record("e1", "A", car1=-0.10, surprise=0.05), + _record("e2", "B", car1=0.00, surprise=0.01), + _record("e3", "C", car1=0.10, surprise=0.09), + _record("e4", "D", car1=0.20, surprise=0.02), + _record("e5", "E", car1=0.05, surprise=0.07), + ] + df = add_percentiles(outcomes_frame(records)) + df["sub_a"] = [0.2, float("nan"), 0.8, float("nan"), 0.5] + df["sub_b"] = [0.1, 0.9, 0.4, 0.6, 0.3] + df["sub_c"] = [float("nan")] * 5 + return df + + +def test_impute_predictions_mean_before_fill() -> None: + df = _contest_frame() + imp = impute_predictions(df, "sub_a") + assert imp is not None + assert imp.imputed_mean == pytest.approx(0.5) # (0.2 + 0.8 + 0.5) / 3 + assert imp.imputed_event_count == 2 + assert imp.values == pytest.approx([0.2, 0.5, 0.8, 0.5, 0.5]) + + +def test_impute_predictions_no_coverage_ineligible() -> None: + assert impute_predictions(_contest_frame(), "sub_c") is None + + +def test_score_submission_imputed_matches_hand_built_fill() -> None: + df = _contest_frame() + scores = score_submission(df, "sub_a") + assert scores["n_obs"] == 3 + assert scores["imputed_mean"] == pytest.approx(0.5) + assert scores["imputed_event_count"] == 2 + + filled = [0.2, 0.5, 0.8, 0.5, 0.5] + pts = list(zip(filled, df["surprise_pct"], df["y"], strict=True)) + want = ols_fit2(pts) + want_surprise = ols_fit(list(zip(df["surprise_pct"], df["y"], strict=True))) + assert want is not None and want_surprise is not None + assert scores["r_squared_imputed"] == pytest.approx(want.r_squared) + assert scores["r_squared_surprise_imputed"] == pytest.approx( + want_surprise.r_squared + ) + assert scores["delta_r_squared_imputed"] == pytest.approx( + want.r_squared - want_surprise.r_squared + ) + + +def test_score_submission_full_coverage_families_identical() -> None: + scores = score_submission(_contest_frame(), "sub_b") + assert scores["imputed_event_count"] == 0 + for name in ("r_squared", "r_squared_surprise", "delta_r_squared", "beta", "mse"): + assert scores[f"{name}_imputed"] == pytest.approx(scores[name]) + + +def test_score_submission_surprise_benchmark_submission_invariant() -> None: + df = _contest_frame() + a = score_submission(df, "sub_a") + b = score_submission(df, "sub_b") + # The imputed benchmark is fit on the full common sample -> identical. + assert a["r_squared_surprise_imputed"] == pytest.approx( + b["r_squared_surprise_imputed"] + ) + + +def test_score_submission_no_coverage_all_none() -> None: + scores = score_submission(_contest_frame(), "sub_c") + assert scores["n_obs"] == 0 + assert scores["imputed_mean"] is None + assert scores["r_squared"] is None + assert scores["delta_r_squared_imputed"] is None + + +def test_rank_submissions_order_gate_and_tiebreak() -> None: + scores = pd.DataFrame( + [ + # Exact metric tie: s_zzz wins on higher pre-imputation coverage. + {"submission": "s_zzz", "n_obs": 40, "delta_r_squared_imputed": 0.25}, + {"submission": "s_aaa", "n_obs": 10, "delta_r_squared_imputed": 0.25}, + {"submission": "s_top", "n_obs": 5, "delta_r_squared_imputed": 0.90}, + # Undefined metric -> unranked, dropped. + {"submission": "s_none", "n_obs": 50, "delta_r_squared_imputed": None}, + ] + ) + ranked = rank_submissions(scores) + assert ranked["submission"].tolist() == ["s_top", "s_zzz", "s_aaa"] + assert ranked["rank"].tolist() == [1, 2, 3] From b43bd69e32d0f14b6be5a0d1d1680d23e129ae8c Mon Sep 17 00:00:00 2001 From: Bradford Levy <5950006+bradfordlynch@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:42:58 -0700 Subject: [PATCH 2/2] Format to the repo's ruff line length; soften "verbatim" to semantic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review on the PR: the new code was wrapped at 88 columns and failed the CI ruff-format gate (repo formats to 100). The reformat is AST-neutral — an AST-normalized comparison and a 400-trial randomized differential against the production scorer confirmed float-for-float parity is unaffected — but the ported text is no longer byte-identical to production, so the module docstring now claims an exact semantic port rather than a verbatim one. --- src/examples/scoring.py | 24 +++++++----------------- tests/test_scoring.py | 8 ++------ 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/examples/scoring.py b/src/examples/scoring.py index 6e52201..44c70e9 100644 --- a/src/examples/scoring.py +++ b/src/examples/scoring.py @@ -29,8 +29,8 @@ prizes for any remaining exact tie are combined and split equally — no random tie-break. -:func:`percentile_ranks`, :func:`ols_fit`, and :func:`ols_fit2` are verbatim -ports of the production scorer — tie handling, degenerate-fit guards and all — +:func:`percentile_ranks`, :func:`ols_fit`, and :func:`ols_fit2` are exact +semantic ports of the production scorer — tie handling, degenerate-fit guards and all — so offline numbers match what the leaderboard computes float-for-float. The rest assembles the per-``(event, asset)`` analysis frame from archive records (as produced by :func:`examples.archive.load_archive` / @@ -229,14 +229,10 @@ def ols_fit2(points: list[tuple[float, float, float]]) -> OLSFit | None: beta1 = (s22 * s1y - s12 * s2y) / det beta2 = (s11 * s2y - s12 * s1y) / det alpha = mean_y - beta1 * mean_x1 - beta2 * mean_x2 - ss_res = sum( - (p[2] - (alpha + beta1 * p[0] + beta2 * p[1])) ** 2 for p in points - ) + ss_res = sum((p[2] - (alpha + beta1 * p[0] + beta2 * p[1])) ** 2 for p in points) mse = ss_res / n r_squared = 0.0 if s_yy == 0.0 else 1.0 - ss_res / s_yy - return OLSFit( - n=n, alpha=alpha, beta=beta1, r_squared=r_squared, mse=mse, beta_surprise=beta2 - ) + return OLSFit(n=n, alpha=alpha, beta=beta1, r_squared=r_squared, mse=mse, beta_surprise=beta2) # ----- Contest imputation + scoring (Official Rules §5) ---------------------- @@ -257,9 +253,7 @@ class ImputedPredictions: imputed_event_count: int -def impute_predictions( - common: pd.DataFrame, prediction_col: str -) -> ImputedPredictions | None: +def impute_predictions(common: pd.DataFrame, prediction_col: str) -> ImputedPredictions | None: """Mean-impute a submission's missing predictions over the common sample. ``common`` is the common-sample frame (every Scored Event row — see @@ -338,9 +332,7 @@ def emit(fit: OLSFit | None, surprise_fit: OLSFit | None, suffix: str) -> None: out[f"delta_r_squared{suffix}"] = fit.r_squared - surprise_fit.r_squared # No-fill family: only the rows the submission actually predicted. - points = [ - (float(x), s, y) for x, s, y in zip(xs, ss, ys, strict=True) if pd.notna(x) - ] + points = [(float(x), s, y) for x, s, y in zip(xs, ss, ys, strict=True) if pd.notna(x)] out["n_obs"] = len(points) fit = ols_fit2(points) emit(fit, ols_fit([(s, y) for _x, s, y in points]) if fit else None, "") @@ -357,9 +349,7 @@ def emit(fit: OLSFit | None, surprise_fit: OLSFit | None, suffix: str) -> None: return out -def rank_submissions( - scores: pd.DataFrame, metric: str = "delta_r_squared_imputed" -) -> pd.DataFrame: +def rank_submissions(scores: pd.DataFrame, metric: str = "delta_r_squared_imputed") -> pd.DataFrame: """Order a per-submission scores frame by the contest ranking rules. ``scores`` needs one row per submission with columns ``submission``, diff --git a/tests/test_scoring.py b/tests/test_scoring.py index a128332..a2e6a21 100644 --- a/tests/test_scoring.py +++ b/tests/test_scoring.py @@ -192,9 +192,7 @@ def test_score_submission_imputed_matches_hand_built_fill() -> None: want_surprise = ols_fit(list(zip(df["surprise_pct"], df["y"], strict=True))) assert want is not None and want_surprise is not None assert scores["r_squared_imputed"] == pytest.approx(want.r_squared) - assert scores["r_squared_surprise_imputed"] == pytest.approx( - want_surprise.r_squared - ) + assert scores["r_squared_surprise_imputed"] == pytest.approx(want_surprise.r_squared) assert scores["delta_r_squared_imputed"] == pytest.approx( want.r_squared - want_surprise.r_squared ) @@ -212,9 +210,7 @@ def test_score_submission_surprise_benchmark_submission_invariant() -> None: a = score_submission(df, "sub_a") b = score_submission(df, "sub_b") # The imputed benchmark is fit on the full common sample -> identical. - assert a["r_squared_surprise_imputed"] == pytest.approx( - b["r_squared_surprise_imputed"] - ) + assert a["r_squared_surprise_imputed"] == pytest.approx(b["r_squared_surprise_imputed"]) def test_score_submission_no_coverage_all_none() -> None: