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 |
required |
df
|
DataFrame
|
Input frame for the metric, containing |
required |
label
|
str
|
Column whose values define the slice partition. |
required |
estimator
|
Estimator | None
|
Inference estimator. |
None
|
multiple_testing
|
MultipleTestingMethod | None
|
P-value adjustment family. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Long-form |
DataFrame
|
|
DataFrame
|
ordered slice pair |
DataFrame
|
partition's iteration order. |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
NotImplementedError
|
Estimator outside |
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):
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 |
required |
df
|
DataFrame
|
Input frame for the metric, containing |
required |
label
|
str
|
Column whose values define the slice partition. |
required |
estimator
|
Estimator | None
|
Inference estimator. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Single-row |
DataFrame
|
|
DataFrame
|
rank ( |
DataFrame
|
chi-squared survival function. No |
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
|
NotImplementedError
|
Non- |
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(...) |