Skip to content

factrix.slice_pairwise_test

slice_pairwise_test(metric: Callable, df: DataFrame, *, label: str, estimator: Estimator | None = None, multiple_testing: MultipleTestingMethod | None = None) -> DataFrame

Cross-slice pairwise Wald contrasts on a per-date metric panel.

Parameters:

Name Type Description Default
metric Callable

Metric callable whose module declares per_date_series.

required
df DataFrame

Input frame for the metric, containing label.

required
label str

Column whose values define the slice partition.

required
estimator Estimator | None

Inference estimator. None resolves to :class:WaldNWCluster (analytic Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) + 1-way cluster on slice). :class:BlockBootstrap triggers the joint block-bootstrap path and changes the default multiple_testing to "romano_wolf".

None
multiple_testing MultipleTestingMethod | None

P-value adjustment family. None follows the estimator default — "holm" for WaldNWCluster, "romano_wolf" for BlockBootstrap. "romano_wolf" with an analytic estimator raises ValueError (RW requires a bootstrap distribution).

None

Returns:

Type Description
DataFrame

Long-form pl.DataFrame with columns

DataFrame

(slice_a, slice_b, n_obs, stat, p_raw, p_adj); one row per

DataFrame

ordered slice pair (a, b) with a before b in the

DataFrame

partition's iteration order. stat carries the Wald χ² under

DataFrame

WaldNWCluster and the signed mean diff under

DataFrame

BlockBootstrap — bootstrap p-values are based on

DataFrame

|mean diff|.

Raises:

Type Description
ValueError

Fewer than two slice values, fewer than two dates aligned across all slices, or an estimator / multiple- testing combination that has no calibrated p-value path.

TypeError

Metric is not slice-test-eligible (no per_date_series capability).

NotImplementedError

Estimator outside WaldNWCluster / BlockBootstrap.

Notes

BlockBootstrap reproducibility — pass an explicit rng_seed on the :class:BlockBootstrap instance to fix the bootstrap draw. Bootstrap metadata (resolved block length, scheme, seed) is not attached to the returned DataFrame in this release; callers wanting it can either reconstruct from the estimator config or use :func:factrix._stats.bootstrap._block_bootstrap_diff_p directly per pair.

Examples:

Pairwise information coefficient (IC) contrasts across two sub-universes. The canonical pattern is to compute the per-date metric series per slice upstream and concatenate with a label column — slices must share dates, so date-disjoint labels (e.g. calendar year) do not apply:

>>> import polars as pl
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics import ic, compute_ic
>>> raw = fx.datasets.make_cs_panel(n_assets=100, n_dates=500)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> assets = panel["asset_id"].unique().sort().to_list()
>>> half = len(assets) // 2
>>> sector_map = {a: ("tech" if i < half else "fin")
...               for i, a in enumerate(assets)}
>>> panel_sec = panel.with_columns(
...     pl.col("asset_id").replace_strict(sector_map).alias("sector")
... )
>>> per_sector_ic = pl.concat([
...     compute_ic(panel_sec.filter(pl.col("sector") == s))
...        .with_columns(pl.lit(s).alias("sector"))
...     for s in ("tech", "fin")
... ])
>>> pairs = fx.slice_pairwise_test(ic, per_sector_ic, label="sector")

Block-bootstrap path (auto-switches to Romano-Wolf multiple-testing):

>>> from factrix.stats import BlockBootstrap
>>> pairs_bb = fx.slice_pairwise_test(
...     ic, per_sector_ic, label="sector",
...     estimator=BlockBootstrap(rng_seed=0),
... )

factrix.slice_joint_test

slice_joint_test(metric: Callable, df: DataFrame, *, label: str, estimator: Estimator | None = None) -> DataFrame

Omnibus Wald χ² that all K slice means are equal.

The joint restriction is β_0 = β_1 = … = β_{K-1}, encoded as K-1 contrasts against the first slice; the Wald statistic follows χ²_{K-1} under H₀.

Parameters:

Name Type Description Default
metric Callable

Metric callable whose module declares per_date_series.

required
df DataFrame

Input frame for the metric, containing label.

required
label str

Column whose values define the slice partition.

required
estimator Estimator | None

Inference estimator. None resolves to :class:WaldNWCluster. Other estimators raise NotImplementedError pending follow-up batches.

None

Returns:

Type Description
DataFrame

Single-row pl.DataFrame with columns

DataFrame

(n_obs, k_slices, df, stat, p). df is the restriction

DataFrame

rank (K-1); stat is the joint Wald χ²; p is the

DataFrame

chi-squared survival function. No multiple_testing kwarg —

DataFrame

a single omnibus has no family-internal correction to apply.

Raises:

Type Description
ValueError

Fewer than two slice values, or fewer than two dates aligned across all slices.

TypeError

Metric is not slice-test-eligible (no per_date_series capability).

NotImplementedError

Non-WaldNWCluster estimator passed before the block-bootstrap batch lands.

Examples:

Joint omnibus test that mean information coefficient (IC) is identical across two sub-universes (see :func:slice_pairwise_test for the per-sector ic panel construction):

>>> import polars as pl
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics import ic, compute_ic
>>> raw = fx.datasets.make_cs_panel(n_assets=100, n_dates=500)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> assets = panel["asset_id"].unique().sort().to_list()
>>> half = len(assets) // 2
>>> sector_map = {a: ("tech" if i < half else "fin")
...               for i, a in enumerate(assets)}
>>> panel_sec = panel.with_columns(
...     pl.col("asset_id").replace_strict(sector_map).alias("sector")
... )
>>> per_sector_ic = pl.concat([
...     compute_ic(panel_sec.filter(pl.col("sector") == s))
...        .with_columns(pl.lit(s).alias("sector"))
...     for s in ("tech", "fin")
... ])
>>> joint = fx.slice_joint_test(ic, per_sector_ic, label="sector")

Cross-slice statistical-test function pair. Both consume a metric callable and a date-keyed DataFrame whose label column carries the slice identifier; the functions partition by label, line up per-date metric series across slices, and report inference on whether the slices' means differ.

The two functions answer different statistical questions:

Function Question Output shape
slice_pairwise_test "Which pairs differ?" — K(K−1)/2 contrasts with family-internal multiple-testing correction One row per pair: (slice_a, slice_b, n_obs, stat, p_raw, p_adj)
slice_joint_test "Do any slices differ at all?" — single omnibus Wald χ² One row: (n_obs, k_slices, df, stat, p)

Both functions sit in the View class (per #148 function classification): their headline output is a comparison test result. They do not participate in Benjamini-Hochberg-Yekutieli (BHY) family expansion — adjusted p is a within-slice-family closure, not a cell-level discovery commitment.

Metric capability requirement

The metric callable's module must declare per_date_series (a top-level capability function returning a (date, value) long-form frame); information coefficient (IC), Fama-MacBeth, and hit_rate ship with this declaration. A metric without it raises TypeError at the function call site.

See the docstring Examples blocks above for the canonical per-sub-universe construction (compute_ic per sector, concatenated with a sector label column).

Date alignment is required

Both functions join all slices on date and run inference on the intersected rows. Joint Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC) over the (T, K) per-date metric panel needs aligned rows so cross-slice covariance enters through the joint kernel. Slices with disjoint date supports (e.g. regimes split by time period) yield zero aligned rows and the functions raise ValueError.

For genuinely time-disjoint slices, run inference per slice and compare summaries upstream (or wait for the future factor_decomposition function, which adopts a different SE geometry). Date-shared slices — universe, sector, market-cap tier — are the intended use case.

Estimator dispatch

Estimator Inference path stat column carries
WaldNWCluster (default) Joint NW HAC over the (T, K) per-date metric panel; per-pair Wald χ² via single-row restriction matrix on the joint variance Wald χ²
BlockBootstrap Joint block-bootstrap on the same panel; per-pair p from \|mean diff\| against the bootstrap null distribution Signed mean diff

BlockBootstrap shares one set of block indices across all pair diffs per draw, so the bootstrap distribution preserves cross-pair dependence — the joint structure Romano-Wolf step-down relies on.

slice_joint_test accepts only WaldNWCluster; the omnibus Wald χ² has no canonical bootstrap analogue, so the function steers callers to slice_pairwise_test if a bootstrap path is wanted.

Multiple-testing correction (slice_pairwise_test only)

Method Default for Notes
"holm" WaldNWCluster (default) Holm step-down — conservative under arbitrary dependence
"romano_wolf" BlockBootstrap Step-down using the joint bootstrap distribution; near-optimal for date-shared slices (universe / sector)
"bonferroni" Manual opt-in For literature / cross-tool reproduction

multiple_testing="romano_wolf" with an analytic estimator raises ValueError — RW needs a bootstrap distribution that analytic estimators do not produce.

Cross-axis composition

The functions accept a single label column. For cross-axis slice analysis (regime × universe), compose a composite label upstream with pl.concat_str(...):

ic_df = ic_df.with_columns(
    pl.concat_str(["regime", "universe"], separator="_").alias("regime_x_universe")
)
slice_pairwise_test(ic, ic_df, label="regime_x_universe")

Two-way interaction decomposition (main effect + interaction with double-clustered SE) is a different statistical object and is reserved for the future factor_decomposition function.

Responsibility boundaries

Need Use
Descriptive per-slice metric values (no test) by_slice
Which slice pairs differ statistically slice_pairwise_test
Whether any slice differs (omnibus) slice_joint_test
FDR-adjusted survivor selection across factors bhy(profiles, ...)
Multi-factor leaderboard rendering compare(...)