Skip to content

factrix.metrics.ic

IC (Information Coefficient) computation for cross-sectional panels.

Notes

Pipeline. Per-date Spearman rank IC (cross-section step) → IC time series, then non-overlapping cross-asset t or Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) t on its mean; the regime variant slices the same pipeline.

Input. DataFrame with date, asset_id, factor, forward_return.

Output. Time-indexed IC series (date, ic) that can be fed into any series/ tool (oos, trend, significance, positive_rate).

factrix.metrics.ic.ic

ic(ic_df: DataFrame, forward_periods: int = 5, inference: NonOverlapping | NeweyWest | StationaryBootstrap = NON_OVERLAPPING, expected_warnings: tuple[str, ...] = ()) -> MetricResult

Information coefficient (IC) mean significance: is mean IC significantly different from zero?

The periods floor is dynamic — the minimum input length scales with the forward_periods parameter and the inference method — so it is declared as a resolver (a callable sample_threshold) rather than a constant.

Parameters:

Name Type Description Default
ic_df DataFrame

Output of compute_ic().

required
forward_periods int

Overlap horizon of the forward returns; the non-overlapping stride and the HAC bandwidth floor both key off it.

5
inference NonOverlapping | NeweyWest | StationaryBootstrap

Significance-test method. fx.inference.NON_OVERLAPPING (default) runs an OLS t-test on a non-overlapping stride subsample; fx.inference.NEWEY_WEST keeps every observation and uses a Newey-West HAC standard error; fx.inference.STATIONARY_BOOTSTRAP also keeps every observation but replaces the HAC SE with a block-bootstrap empirical p, for a series too short or heavy-tailed for either t-test to be trusted. All three test the same \(H_0: \mathbb{E}[\mathrm{IC}] = 0\).

NON_OVERLAPPING

Returns:

Type Description
MetricResult

MetricResult with value=mean IC and the inference method's t/p.

Notes

Given the per-date IC series \(\mathrm{IC}_t\), \(H_0: \mathbb{E}[\mathrm{IC}] = 0\). The non-overlapping path strides the series at forward_periods (discarding \(h-1\) of every \(h\) observations) to avoid the lag floor implied by overlapping forward returns; the Newey-West path keeps every observation and absorbs the induced MA(\(h-1\)) autocorrelation through HAC standard errors (Bartlett kernel, NW1994 auto-bandwidth floored at \(h - 1\)).

References

Grinold 1989: IC as the canonical density-quality measure under the Fundamental Law of Active Management. Hansen-Hodrick 1980: K-period overlapping returns carry MA(K-1) autocorrelation — the motivation for the non-overlap stride used here.

Method selection

The default NON_OVERLAPPING path tests on roughly n / forward_periods effective observations; when that post-stride sample is thin it emits WarningCode.UNRELIABLE_SE_SHORT_PERIODS (now surfaced on the returned result's warning_codes). NEWEY_WEST keeps every observation and absorbs the overlap-induced autocorrelation in the HAC standard error, so on a thin series it retains more test power. The guidance is one-directional: prefer NEWEY_WEST when the non-overlapping effective sample is too thin; there is no symmetric reason to switch back to non-overlapping once the sample is ample. STATIONARY_BOOTSTRAP drops the HAC asymptotic-variance assumption entirely in favour of a block-bootstrap empirical p — prefer it when the IC series is heavy-tailed / skewed enough that a HAC t-test's normal-approximation p-value is itself suspect, not only when the sample is short. ic never changes inference for you — the choice stays explicit.

Examples:

Chain from :func:compute_ic output:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ic import compute_ic, ic
>>> panel = compute_forward_return(
...     fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
...     forward_periods=5,
... )
>>> ic_df = compute_ic(panel)["factor"]
>>> result = ic(ic_df, forward_periods=5)
>>> result.name == ""
True

factrix.metrics.ic.ic_ir

ic_ir(ic_df: DataFrame, expected_warnings: tuple[str, ...] = ()) -> MetricResult

\(\mathrm{ICIR} = \mathrm{mean}(\mathrm{IC}) / \mathrm{std}(\mathrm{IC})\).

Signed ratio — positive when information coefficient (IC) is consistently positive, negative when consistently negative. Analogous to a Sharpe ratio for the factor density.

This is a descriptive statistic, not a hypothesis test (t_stat=None). For significance testing, use ic().

Parameters:

Name Type Description Default
ic_df DataFrame

Output of compute_ic().

required

Returns:

Type Description
MetricResult

MetricResult with value=IC_IR (signed), t_stat=None.

Notes

\(\mathrm{ICIR} = \mathrm{mean}(\mathrm{IC}) / \mathrm{std}(\mathrm{IC})\) over the per-date IC series — a Sharpe-style ratio describing time-series stability of the density. Reported as a descriptive statistic; no inference is attached because the significance test on \(\mathrm{mean}(\mathrm{IC})\) lives in ic (optionally with inference=fx.inference.NEWEY_WEST for the HAC-corrected SE).

References

Grinold 1989: ICIR is the time-stability normalisation that completes the IR decomposition.

Examples:

Chain from :func:compute_ic output:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ic import compute_ic, ic_ir
>>> panel = compute_forward_return(
...     fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
...     forward_periods=5,
... )
>>> ic_df = compute_ic(panel)["factor"]
>>> result = ic_ir(ic_df)
>>> result.name == ""
True

Use cases

  • Compute per-date information coefficient (IC)


    Build the per-date Spearman IC series (with tie_ratio diagnostics) from a long-format panel before any inferential test. Pre-step for ic / ic_ir.

  • Mean-IC significance, non-overlapping


    Test \(H_0: \mathbb{E}[\mathrm{IC}] = 0\) on the every-forward_periods subsample to avoid the autocorrelation induced by overlapping forward returns. Default for the IC cell.

  • Mean-IC significance, heteroskedasticity-and-autocorrelation-consistent (HAC)


    Same null, but keep every overlapping observation and absorb the induced MA dependence through a Newey-West HAC standard error. Invoked via ic(inference=fx.inference.NEWEY_WEST).

  • IC stability (signed IR)


    mean(IC) / std(IC) over the per-date series — a Sharpe-style descriptive statistic for signal time-stability. No inference attached.

Choosing a function

Goal Function
Per-date IC table for downstream inspection / slicing compute_ic
Mean-IC significance (non-overlapping or Newey-West HAC) ic
Time-stability ratio (no inference) ic_ir

All three are invoked indirectly via evaluate(data, metrics={"ic": ic()}) — they're documented here for callers who want the standalone numerical output without the evaluation framing.

Worked example — per-date IC then mean significance

compute_ic → ic on a synthetic cross-sectional panel

import factrix as fx
from factrix.metrics.ic import compute_ic, ic
from factrix.preprocess import compute_forward_return

raw   = fx.datasets.make_cs_panel(
    n_assets=100, n_dates=500, ic_target=0.08, seed=2024,
)
panel = compute_forward_return(raw, forward_periods=5)

ic_df = compute_ic(panel)["factor"]
print(ic_df.head())
# ┌────────────┬───────────┬───────────┐
# │ date       ┆ ic        ┆ tie_ratio │
# ├────────────┼───────────┼───────────┤
# │ 2024-01-01 ┆ 0.083     ┆ 0.000     │
# │ 2024-01-02 ┆ 0.071     ┆ 0.000     │
# │ ...        ┆ ...       ┆ ...       │
# └────────────┴───────────┴───────────┘

out = ic(ic_df, forward_periods=5, inference=fx.inference.NEWEY_WEST)
print(out.value, out.stat, out.p_value)
# 0.0722  14.60  2.13e-40

Cross-slice IC analysis

For per-slice IC summaries (regime / universe / sector / ...), use by_slice on an IC frame joined with slice labels. For inferential contrasts (pairwise Wald χ² + Holm / Romano-Wolf adjusted p), use slice_pairwise_test. For a full walk-through of regime-conditioned IC, see the Slice analysis guide.

See also