Skip to content

fix: report an undefined cross-sectional IC as null, not NaN (release 0.1.2) - #44

Merged
stefan-jansen merged 2 commits into
mainfrom
fix/ic-nan-undefined-date
Aug 17, 2026
Merged

fix: report an undefined cross-sectional IC as null, not NaN (release 0.1.2)#44
stefan-jansen merged 2 commits into
mainfrom
fix/ic-nan-undefined-date

Conversation

@stefan-jansen

Copy link
Copy Markdown
Contributor

Closes #43.

The fix

cross_sectional_ic_series() builds its per-date IC with pl.corr over ranks.
The correlation is undefined when one side of a date has zero variance - every
prediction tied, or every return tied - and pl.corr returns NaN. Polars treats
NaN and null as different values, so the NaN survives drop_nulls("ic") and the
caller's mean IC comes back nan with nothing raising.

The correlation is now gated on is_finite() alongside min_obs, so an
undefined date carries null - the same as a date below min_obs, and the same
as what cross_sectional_ic() already reported in n_periods.

Verified end to end against the published 0.1.1 wheel with the issue's
reproducer:

0.1.1 this branch
ic on the tied date NaN null
drop_nulls("ic").mean() nan 0.244755
n_periods 2 2

Four new tests cover both correlation methods against both tied sides, asserting
null_count() == 1 and is_nan().sum() == 0. All four fail when the
is_finite() gate is removed.

Release 0.1.2

[tool.hatch.version] reads __version__ from
src/ml4t/diagnostic/__init__.py, not from the git tag, so the bump has to land
on a commit before v0.1.2 is tagged - tagging alone would rebuild 0.1.1 and
PyPI would reject the upload as a duplicate. That is what happened to the first
v0.1.1 tag.

The CHANGELOG had no 0.1.1 entry; one is added from that release commit so the
file does not jump 0.1.0b21 -> 0.1.2.

Local verification

ruff check, ruff format --check, and ty check clean; 5331 passed, 75
skipped. uv build plus scripts/verify_release_artifacts.py and
scripts/import_smoke.py pass against the built 0.1.2 wheel.

`cross_sectional_ic_series` builds its per-date IC with `pl.corr` over
ranks. Spearman and Pearson are both undefined when one side of a date
has zero variance - every prediction tied, or every return tied - and
`pl.corr` returns NaN there. Everything else in the same function already
uses null for a date it cannot compute: the `min_obs` gate writes
`.otherwise(None)`, and `cross_sectional_ic` reports such a date as
missing in `n_periods`. Only the raw `pl.corr` result escaped as NaN.

Polars treats NaN and null as different values, so `drop_nulls("ic")`
keeps the NaN and `.mean()` over the survivors is NaN. A weak model ties
on some dates, so a caller's headline metric silently becomes nan and
nothing raises. Measured in `12_gradient_boosting/07_hpo_comparison`
fold 0: 231 validation dates, 0 nulls, 52 NaN.

Gate the correlation on `is_finite()` alongside `min_obs`, so an
undefined date carries null. The three-date reproducer from the issue:

    date          n_obs   ic  (before)     ic (after)
    2024-01-01    12      -0.097902        -0.097902
    2024-01-02    12      NaN              null
    2024-01-03    12      -0.545455        -0.545455

    drop_nulls("ic").mean():  nan  ->  -0.321678

`n_periods` is 2 both ways; the aggregate wrapper already counted the
tied date as missing.

Tests cover both correlation methods against both tied sides, asserting
`null_count() == 1` and `is_nan().sum() == 0`. All four fail when the
`is_finite()` gate is removed.

Refs: stefan-jansen/machine-learning-for-trading#493
Carries the #493 fix: an undefined cross-sectional IC date now reports
null instead of NaN, so a `drop_nulls("ic").mean()` in a caller cannot
come back nan.

`[tool.hatch.version]` reads `__version__` from
`src/ml4t/diagnostic/__init__.py`, not from the git tag, so the bump has
to land on a commit before `v0.1.2` is tagged - tagging alone rebuilds
0.1.1 and PyPI rejects the upload as a duplicate. That is what happened
to the first v0.1.1 tag (see 3467a11).

The CHANGELOG had no 0.1.1 entry; added one from that release commit so
the file does not jump 0.1.0b21 -> 0.1.2.
Copilot AI lite review requested due to automatic review settings August 17, 2026 16:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@stefan-jansen
stefan-jansen merged commit 31fdf8e into main Aug 17, 2026
36 checks passed
@stefan-jansen
stefan-jansen deleted the fix/ic-nan-undefined-date branch August 17, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cross_sectional_ic_series reports an undefined date as NaN, poisoning the caller's mean IC

2 participants