Skip to content

The cross-slice inference surface is two function pairs, split on whether the slices share dates:

  • Cross-sectional / date-aligned — slice_pairwise_test / slice_joint_test (sector, size bucket, liquidity tier).
  • Date-disjoint — slice_period_pairwise_test / slice_period_joint_test (market regime, calendar period, in/out-of-sample). See Date supports: aligned vs disjoint.

factrix.slice_pairwise_test

slice_pairwise_test(data: DataFrame, metric: MetricBase, *, by: str, factor_col: str, overlap_periods: int | None = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> DataFrame

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

Data-first counterpart of :func:factrix.by_slice: partitions a raw panel on by, builds each slice's per-date metric series via the metric's producer, aligns on date, and runs the analytic Newey-West HAC + slice-cluster Wald on every slice pair. Cross-sectional only (slices must share dates).

Parameters:

Name Type Description Default
data DataFrame

Raw long-format panel — same input contract as :func:factrix.evaluate (date, asset_id, <factor_col>, forward_return). Must contain by; compose it upstream if needed.

required
metric MetricBase

A metric instance whose module declares per_date_series (ic() / fm_beta() / positive_rate()). The bare class is rejected.

required
by str

Column whose values define the slice partition.

required
factor_col str

The single factor column to score per slice.

required
overlap_periods int | None

The evaluation-grid overlap the joint HAC bandwidth resolves at (this test has no sample floor to gate). Normally omitted — :func:factrix.preprocess.compute_forward_return stamps it on the panel and it is read from there. Declare it for a self-attached forward_return panel that carries no stamp; the contract is :func:factrix.evaluate's — a value disagreeing with the stamp is rejected, and an unstamped panel with no declaration is an error rather than a silent default.

None
strict bool

Accepted so the four slice-test entry points share one signature shape; inert here. This path gates no slice on the metric's SampleThreshold floor — a slice too thin to yield a per-date series collapses the inner-join and raises <2 aligned dates, which is not a shortage strict=False could admit as an unavailable row. Its live counterpart is :func:factrix.slice_period_pairwise_test's.

True
expected_warnings tuple[str, ...]

:class:~factrix.WarningCode values the caller declares as the study's design. Validated on the same contract as :func:factrix.evaluate's (an unknown code is rejected, since a declaration that marks nothing is always a typo); the cross- sectional path raises no code of its own, so there is nothing for a declaration to keep quiet yet.

()

Returns:

Type Description
DataFrame

Long-form pl.DataFrame with columns ``(slice_a, slice_b,

DataFrame

n_obs, mean_diff, stat, p_raw, p_adj, stat_type, reference_dist,

DataFrame

df_num, df_denom, multiplicity)``; one row per ordered slice pair

DataFrame

(a, b) with a before b in the partition's iteration

DataFrame

order. mean_diff is the signed μ_a − μ_b (direction /

DataFrame

effect size), stat the Wald statistic, and p_adj the Holm

DataFrame

step-down family-wise correction across the K(K-1)/2 pairs. The

DataFrame

trailing five columns disclose the active mechanism (constant

DataFrame

across rows): stat_type="wald"; reference_dist="F" with

DataFrame

df_num=1 (single contrast) and df_denom=n_obs-1 — the

DataFrame

date-cluster count is T=n_obs, so the finite-sample

DataFrame

F_{1, T-1} reference is used in place of the over-rejecting

DataFrame

asymptotic χ²; multiplicity="holm".

Raises:

Type Description
UserInputError

metric is not a metric instance, factor_col is absent, or overlap_periods is not a positive int / disagrees with the panel's stamp / is missing on an unstamped panel. Also raised for fewer than two slice values, or fewer than two dates aligned across all slices (e.g. a date-disjoint partition).

TypeError

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

Examples:

Pairwise information coefficient (IC) contrasts across two sectors on a synthetic cross-sectional panel — partition on a sector column, score ic per sector, contrast the per-date series:

>>> import polars as pl
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics import ic
>>> raw = fx.datasets.make_cs_panel(n_assets=100, n_dates=250)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> assets = panel["asset_id"].unique().sort().to_list()
>>> sector = {a: ("tech" if i % 2 else "fin")
...           for i, a in enumerate(assets)}
>>> panel = panel.with_columns(
...     pl.col("asset_id").replace_strict(sector).alias("sector")
... )
>>> pairs = fx.slice_pairwise_test(
...     panel, ic(), by="sector", factor_col="factor"
... )
>>> pairs.columns
['slice_a', 'slice_b', 'n_obs', 'mean_diff', 'stat', 'p_raw', 'p_adj', 'stat_type', 'reference_dist', 'df_num', 'df_denom', 'multiplicity']

factrix.slice_joint_test

slice_joint_test(data: DataFrame, metric: MetricBase, *, by: str, factor_col: str, overlap_periods: int | None = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> 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₀. Same data-first contract and cross-sectional (shared-date) limitation as :func:slice_pairwise_test.

Parameters:

Name Type Description Default
data DataFrame

Raw long-format panel (see :func:slice_pairwise_test).

required
metric MetricBase

A metric instance whose module declares per_date_series. The bare class is rejected.

required
by str

Column whose values define the slice partition.

required
factor_col str

The single factor column to score per slice.

required
overlap_periods int | None

The evaluation-grid overlap the joint HAC bandwidth resolves at (this test has no sample floor to gate). Normally omitted — :func:factrix.preprocess.compute_forward_return stamps it on the panel and it is read from there. Declare it for a self-attached forward_return panel that carries no stamp; the contract is :func:factrix.evaluate's — a value disagreeing with the stamp is rejected, and an unstamped panel with no declaration is an error rather than a silent default.

None
strict bool

Accepted so the four slice-test entry points share one signature shape; inert here. This path gates no slice on the metric's SampleThreshold floor — a slice too thin to yield a per-date series collapses the inner-join and raises <2 aligned dates, which is not a shortage strict=False could admit as an unavailable row. Its live counterpart is :func:factrix.slice_period_pairwise_test's.

True
expected_warnings tuple[str, ...]

:class:~factrix.WarningCode values the caller declares as the study's design. Validated on the same contract as :func:factrix.evaluate's (an unknown code is rejected, since a declaration that marks nothing is always a typo); the cross- sectional path raises no code of its own, so there is nothing for a declaration to keep quiet yet.

()

Returns:

Type Description
DataFrame

Single-row pl.DataFrame with columns ``(n_obs, k_slices, stat,

DataFrame

p_value, stat_type, reference_dist, df_num, df_denom,

DataFrame

multiplicity).stat`` is the joint Wald statistic. The

DataFrame

mechanism columns disclose the reference: stat_type="wald",

DataFrame

reference_dist="F", df_num=K-1 (restriction rank) and

DataFrame

df_denom=n_obs-1 — p_value is the finite-sample

DataFrame

F_{K-1, T-1} survival of stat / (K-1) (the date-cluster

DataFrame

count is T=n_obs, so the asymptotic χ² reference would

DataFrame

over-reject). multiplicity is None — a single omnibus has

DataFrame

no family-internal correction to apply.

Raises:

Type Description
UserInputError

metric is not a metric instance, factor_col is absent, or overlap_periods is not a positive int / disagrees with the panel's stamp / is missing on an unstamped panel. Also raised for fewer than two slice values, or fewer than two dates aligned across all slices.

TypeError

Metric is not slice-test-eligible.

Examples:

Joint omnibus test that mean information coefficient (IC) is identical across two sectors (see :func:slice_pairwise_test for the panel construction):

>>> import polars as pl
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics import ic
>>> raw = fx.datasets.make_cs_panel(n_assets=100, n_dates=250)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> assets = panel["asset_id"].unique().sort().to_list()
>>> sector = {a: ("tech" if i % 2 else "fin")
...           for i, a in enumerate(assets)}
>>> panel = panel.with_columns(
...     pl.col("asset_id").replace_strict(sector).alias("sector")
... )
>>> joint = fx.slice_joint_test(
...     panel, ic(), by="sector", factor_col="factor"
... )
>>> joint["df_num"][0]
1

factrix.slice_period_pairwise_test

slice_period_pairwise_test(data: DataFrame, metric: MetricBase, *, by: str, factor_col: str, method: Method = 'bootstrap', overlap_periods: int | None = None, n_resamples: int = 999, rng: Rng = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> DataFrame

Pairwise cross-slice contrasts for a date-disjoint partition.

Date-disjoint counterpart of :func:slice_pairwise_test: partitions a raw panel on by, builds each slice's per-period metric series via the metric's producer, and contrasts every slice pair as independent samples (no date inner-join). The right tool for regime analysis (bull / bear, high-vol / low-vol) and other time-disjoint splits (calendar period, in/out-of-sample), where the cross-sectional pair would raise <2 aligned dates.

Parameters:

Name Type Description Default
data DataFrame

Raw long-format panel — same input contract as :func:factrix.evaluate (date, asset_id, <factor_col>, forward_return). Must contain by; compose it upstream if needed.

required
metric MetricBase

A metric instance whose module declares per_date_series (ic() / fm_beta() / positive_rate()). The bare class is rejected.

required
by str

Column whose values define the slice partition (regime label, calendar bucket, …).

required
factor_col str

The single factor column to score per slice.

required
method Method

"bootstrap" (default) runs an independent stationary block bootstrap per slice with Romano-Wolf step-down p_adj; "analytic" runs Newey-West HAC per slice with Welch-style pairwise contrasts and Holm p_adj. Neither dominates every finite sample: at K=5 the measured short-slice grid favours the analytic Holm path. See reference/inference-calibration.

'bootstrap'
overlap_periods int | None

The evaluation-grid overlap the sample floor, bootstrap block-length floor, and analytic per-slice HAC bandwidth resolve at. Normally omitted — :func:factrix.preprocess.compute_forward_return stamps it on the panel and it is read from there. Declare it for a self-attached forward_return panel that carries no stamp; the contract is :func:factrix.evaluate's — a value disagreeing with the stamp is rejected, and an unstamped panel with no declaration is an error rather than a silent default.

None
n_resamples int

B for the "bootstrap" path (ignored by "analytic"). Must be at least BOOTSTRAP_RESAMPLES_FLOOR — the shared floor every factrix entry point reporting an inference drawn from resamples enforces. The default 999 gives an empirical p-value grid of roughly 0.001; automatic block-length selection is a separate Politis-White step, floored at the known overlap_periods horizon.

999
rng Rng

Reproducibility seed for the "bootstrap" path (ignored by "analytic"). None draws from system entropy and the resolved int is reported in the seed column; an int is reported unchanged; a numpy.random.Generator is used as-is and advanced by the call, and the column is null because only its owner can reproduce the draw. Under method="analytic" the column is null throughout.

None
strict bool

True (default) raises ValueError when any slice is below the metric's sample floor. False keeps the schema and returns every pair touching a thin slice as an unavailable row (stat / p_raw / p_adj NaN, reason= "insufficient_periods"); the remaining pairs are tested and their multiplicity family is the tested pairs only. The strict=False counterpart of :func:factrix.evaluate's metric_unavailable short-circuit, for batch regime research where one thin regime must not abort the sweep.

True
expected_warnings tuple[str, ...]

:class:~factrix.WarningCode values the caller declares as the study's design. Validated on the same contract as :func:factrix.evaluate's (an unknown code is rejected, since a declaration that marks nothing is always a typo); the pairwise path raises no code of its own — the short-slice SHORT_SLICE_JOINT_TEST echo belongs to :func:slice_period_joint_test, whose K-1 inverted restrictions cause it — so there is nothing for a declaration to keep quiet yet.

()

Returns:

Name Type Description
DataFrame

Long-form pl.DataFrame with columns ``(slice_a, slice_b,

DataFrame

n_periods_a, n_periods_b, mean_diff, stat, p_raw, p_adj, stat_type,

DataFrame

reference_dist, df_num, df_denom, multiplicity, min_periods, seed,

DataFrame

reason); one row per ordered slice pair(a, b).n_periods_*``

DataFrame

are each slice's own date counts (disjoint spans differ in length)

DataFrame

and min_periods the floor they were gated on (resolved at the

DataFrame

panel's stamped or declared overlap_periods). reason is null on a tested

DataFrame

pair, "degenerate_variance" when the contrast variance collapsed

DataFrame

(NaN stat / p_raw / p_adj — no test, not a

DataFrame

non-rejection), or "insufficient_periods" for a pair admitted by

DataFrame

strict=False; filter on reason.is_null() before folding rows

DataFrame

into a wider family. mean_diff is the

DataFrame

signed μ_a − μ_b; stat the studentized contrast

DataFrame

(μ_a − μ_b)² / (v_a + v_b). The mechanism columns disclose the

path DataFrame

stat_type="wald", df_num=1; reference_dist is

DataFrame

"bootstrap_null" (method="bootstrap", df_denom=None) or

DataFrame

"f" (method="analytic": F_{1, ν} with the per-pair

DataFrame

Welch-Satterthwaite ν in df_denom — two disjoint slices with

DataFrame

their own HAC variances is the Welch two-sample setting, and the

DataFrame

asymptotic χ²₁ used earlier over-rejected at short slices); and

DataFrame

multiplicity the family-wise correction ("romano_wolf" for

DataFrame

"bootstrap", "holm" for "analytic").

Raises:

Type Description
UserInputError

metric is not a metric instance, factor_col is absent, method is invalid, overlap_periods is not a positive int / disagrees with the panel's stamp / is missing on an unstamped panel, or by names a date-aligned partition — any two slices sharing ≥2 dates are not independent samples and belong to :func:factrix.slice_pairwise_test / :func:factrix.slice_joint_test.

ValueError

Fewer than two slice values, any slice with fewer than two dates, or (strict=True) any slice whose per-period series is below the metric's own SampleThreshold floor resolved at the panel's stamped or declared overlap_periods (the size at which :func:factrix.by_slice short-circuits the metric to NaN; see :func:factrix.sample_requirements).

TypeError

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

factrix.slice_period_joint_test

slice_period_joint_test(data: DataFrame, metric: MetricBase, *, by: str, factor_col: str, method: Method = 'bootstrap', overlap_periods: int | None = None, n_resamples: int = 999, rng: Rng = None, strict: bool = True, expected_warnings: tuple[str, ...] = ()) -> DataFrame

Omnibus block-diagonal Wald χ² that all K disjoint-slice means are equal.

Date-disjoint counterpart of :func:slice_joint_test. The restriction is μ_0 = μ_1 = … = μ_{K-1} (K-1 contrasts against the first slice); because the slices are independent samples, the cross-slice covariance is block-diagonal — Var(μ_k) on the diagonal, zero off it. Both methods share the same Wald quadratic form; they differ in how the null is referenced, mirroring the pairwise path: "analytic" uses an F_{K-1, ν} reference with a Satterthwaite ν, while "bootstrap" calibrates the statistic against its own block-bootstrap null. The bootstrap path is still known to over-reject in the measured K=5 short-slice cells. Useful for regime analysis: a single test of "does this factor's edge differ across regimes at all?" before drilling into pairs.

Parameters:

Name Type Description Default
data DataFrame

Raw long-format panel (see :func:slice_period_pairwise_test).

required
metric MetricBase

A metric instance whose module declares per_date_series. The bare class is rejected.

required
by str

Column whose values define the slice partition.

required
factor_col str

The single factor column to score per slice.

required
method Method

"bootstrap" (default) sources each Var(μ_k) from an independent stationary block bootstrap; "analytic" from a per-slice Newey-West HAC. See :func:slice_period_pairwise_test.

'bootstrap'
overlap_periods int | None

The evaluation-grid overlap the sample floor, bootstrap block-length floor, and analytic per-slice HAC bandwidth resolve at. Normally omitted — :func:factrix.preprocess.compute_forward_return stamps it on the panel and it is read from there. Declare it for a self-attached forward_return panel that carries no stamp; the contract is :func:factrix.evaluate's — a value disagreeing with the stamp is rejected, and an unstamped panel with no declaration is an error rather than a silent default.

None
n_resamples int

B for the "bootstrap" path (ignored by "analytic"). Must be at least BOOTSTRAP_RESAMPLES_FLOOR — the shared floor every factrix entry point reporting an inference drawn from resamples enforces. The default 999 gives an empirical p-value grid of roughly 0.001; automatic block-length selection is a separate Politis-White step, floored at the known overlap_periods horizon.

999
rng Rng

Reproducibility seed for the "bootstrap" path (ignored by "analytic"). None draws from system entropy and the resolved int is reported in the seed column; an int is reported unchanged; a numpy.random.Generator is used as-is and advanced by the call, and the column is null because only its owner can reproduce the draw. Under method="analytic" the column is null throughout.

None
strict bool

True (default) raises ValueError when any slice is below the metric's sample floor. False returns the same single-row schema as an unavailable row instead (stat / p_value NaN, reason="insufficient_periods"): the omnibus restriction spans every slice, so one thin slice makes the whole test unavailable. The strict=False counterpart of :func:factrix.evaluate's metric_unavailable short-circuit.

True
expected_warnings tuple[str, ...]

:class:~factrix.WarningCode values declaring warning regimes that are the study's design — the same contract as :func:factrix.evaluate. A batch of regime tests on short samples trips short_slice_joint_test on every cell by construction; declaring it keeps the record (the code stays in warning_codes, leaves unexpected_warning_codes) and stops the per-call UserWarning echo. Unknown codes are rejected.

()

Returns:

Type Description
DataFrame

Single-row pl.DataFrame with columns ``(k_slices, n_periods_min,

DataFrame

stat, p_value, stat_type, reference_dist, df_num, df_denom,

DataFrame

multiplicity, min_periods, seed, short_slice_periods,

DataFrame

warning_codes, unexpected_warning_codes, reason).stat`` is the joint Wald

DataFrame

statistic; n_periods_min the shortest slice's date count and

DataFrame

min_periods the floor it was gated on (resolved at the panel's

DataFrame

stamped or declared overlap_periods); reason is null when the test ran

DataFrame

and "insufficient_periods" on a strict=False unavailable

DataFrame

row. warning_codes lists every :class:~factrix.WarningCode

DataFrame

the test raised (today only short_slice_joint_test) and

DataFrame

unexpected_warning_codes the subset not declared via

DataFrame

expected_warnings — the audit pair of

DataFrame

attr:~factrix.EvaluationResult.warnings /

DataFrame

attr:~factrix.EvaluationResult.unexpected_warnings;

DataFrame

short_slice_periods is the calibration threshold the short-slice

DataFrame

code is gated on, next to the k_slices / n_periods_min it was

DataFrame

read against. The mechanism

DataFrame

columns

DataFrame

disclose the reference: stat_type="wald", df_num=K-1

DataFrame

(restriction rank); reference_dist is "f" for

DataFrame

method="analytic" — p_value from F_{K-1, ν} with the

DataFrame

Satterthwaite ν in df_denom, the K-slice form of the

DataFrame

pairwise Welch reference — or "bootstrap_null" (df_denom

DataFrame

None) for method="bootstrap". multiplicity is None — a single

DataFrame

omnibus has no family-internal correction.

Raises:

Type Description
UserInputError

metric is not a metric instance, factor_col is absent, method is invalid, overlap_periods is not a positive int / disagrees with the panel's stamp / is missing on an unstamped panel, or by names a date-aligned partition — any two slices sharing ≥2 dates are not independent samples and belong to :func:factrix.slice_pairwise_test / :func:factrix.slice_joint_test.

ValueError

Fewer than two slice values, any slice with fewer than two dates, or (strict=True) any slice whose per-period series is below the metric's own SampleThreshold floor resolved at the panel's stamped or declared overlap_periods (the size at which :func:factrix.by_slice short-circuits the metric to NaN; see :func:factrix.sample_requirements).

TypeError

Metric is not slice-test-eligible.

Warns:

Type Description
UserWarning

K >= 3 and the shortest slice has fewer than 150 periods — :class:~factrix.WarningCode.SHORT_SLICE_JOINT_TEST, recorded in the row's warning_codes and echoed unless declared in expected_warnings. Measured size on a true null (iid per-period IC, nominal 5%, 500 seeds per cell), rows K, columns T::

K / T     50     90    150    250
2        .046   .058   .052   .046
3        .062   .078   .054   .038
5        .094   .082   .054   .056

The excess is not the reference distribution (F and χ² agree at these ν) and not the aggregation (with the true variances the same statistic rejects 3.8%): each slice's Bartlett HAC variance is a noisy small-sample estimate — effective df ≈ 21 at T = 50 rather than T - 1 — and inverting K-1 of them inflates the Wald. The bootstrap path carries the same noise (10–15% at K=5, T=50–90), so switching method does not help; a longer slice does. Prewhitening, plug-in bandwidths and finite-sample F references were each measured and none calibrates this case. Pairwise contrasts on the same slices sit at 5–6% and are the better-calibrated read.

Cross-slice statistical-test function pair. Both take a date-keyed DataFrame (data-first) and a metric callable; the by column carries the slice identifier; the functions partition by by, 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, mean_diff, stat, p_raw, p_adj, stat_type, reference_dist, df_num, df_denom, multiplicity)
slice_joint_test "Do any slices differ at all?" — single omnibus Wald χ² One row: (n_obs, k_slices, stat, p_value, stat_type, reference_dist, df_num, df_denom, multiplicity)

Both functions sit in the View class: 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.

One signature shape

All four entry points take the same parameters, so moving a call between the cross-sectional and the date-disjoint path is a one-word edit:

Illustrative
def slice_test(data, metric, *, by, factor_col,
               overlap_periods=None, strict=True, expected_warnings=()): ...

The slice_period_* pair adds the three knobs its resampling machinery needs — method, n_resamples, rng — and nothing else differs.

Two of the shared knobs are inert on the cross-sectional pair, kept so the shape holds rather than because they do work there:

Knob slice_period_* slice_pairwise_test / slice_joint_test
strict gates each slice on the metric's SampleThreshold floor; False returns unavailable rows (see below) inert — this path gates no slice on a floor; a slice too thin to produce a per-date series collapses the inner join and raises <2 aligned dates, which is not a shortage strict=False could admit
expected_warnings live on slice_period_joint_test (see below); validated but silent on slice_period_pairwise_test validated, but this path raises no WarningCode of its own, so there is nothing to keep quiet

Validation is the part that is never inert: an unknown code is rejected at every one of the four, on the same contract as evaluate, because a declaration that marks nothing is always a typo.

Evaluation-grid overlap (overlap_periods)

The joint NW HAC bandwidth is floored at the evaluation-grid overlap — overlapping forward returns make each per-date series autocorrelated, and a kernel that does not cover the overlap under-estimates the variance. There is no sample floor to gate here, so the overlap drives the bandwidth alone. Resolution follows evaluate's contract, the same one the slice_period_* pair applies: the stamp left by compute_forward_return is the truth, a declared overlap_periods= disagreeing with it is rejected, and an unstamped (self-attached forward_return) panel must declare it rather than fall back to a silent default.

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 positive_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 supports: aligned vs disjoint

slice_pairwise_test / slice_joint_test 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 these functions raise UserInputError on by (<2 aligned dates). Date-shared slices — universe, sector, market-cap tier — are their intended use case.

A <2 aligned dates error has two distinct causes, and the message distinguishes them:

  • Date-disjoint partition — the slices share fewer than two raw dates by construction (the case above). The message names the date-disjoint partition and points at slice_period_*.
  • Date-aligned but metric-dropped — the slices do share dates, but the per-slice metric dropped most of its per-date values, so the joined panel still collapses below two rows. The usual cause is too few assets per slice (e.g. ic drops any date below MIN_IC_ASSETS_HARD); a sector cut with thin cross-sections triggers it. The message reports the raw shared-date count and blames the thin universe — widen each slice's asset universe or use a coarser partition.

The refusal is symmetric

Each pair refuses the other's partition, before its metric runs, and names the entry point that fits:

You called With slices that You get
slice_pairwise_test / slice_joint_test share <2 dates (date-disjoint) UserInputError on by: <2 aligned dates … use slice_period_pairwise_test / slice_period_joint_test for date-disjoint partitions.
slice_period_pairwise_test / slice_period_joint_test share ≥2 dates in any pair (date-aligned) UserInputError on by: slices 'tech' and 'fin' share 120 dates … use slice_pairwise_test / slice_joint_test for date-aligned partitions.

The period-family refusal is a hard error, not a warning: those tests treat each slice as an independent sample with block-diagonal cross-slice covariance, so slices sharing dates would have their common shocks counted as independent evidence and the p-values would be anticonservative with nothing in the result marking it. Partially overlapping spans are refused on the same rule — the shared periods carry the shared shock whether they are 10% of the span or all of it. Slices sharing exactly one date are tolerated: a date-axis partition truncated at a regime boundary can leave one common period, which slice_boundary_truncation already describes.

For genuinely time-disjoint slices, reach for slice_period_pairwise_test / slice_period_joint_test. They build the same per-slice per-date series but do not inner-join — each slice is treated as an independent sample with block-diagonal cross-slice covariance. A two-valued method flag selects the estimator:

method Per-slice SE Pairwise p_adj Finite-sample note
"bootstrap" (default) Independent stationary block bootstrap (Politis-White block length, floored at overlap_periods); pairwise bootstrap-t uses a per-draw block SE Romano-Wolf step-down on the same joint studentized roots Pairwise overlap cells are near nominal; the omnibus still over-rejects in measured K=5 short-slice cells
"analytic" Per-slice Newey-West HAC, Welch-style pairwise contrast Holm step-down Fast and deterministic; better calibrated for measured K=5 pairwise contrasts

Under method="bootstrap" both take n_resamples= (default 999, floored at 199) and rng= — the library-wide resampling knobs, see Resampling knobs. "analytic" ignores both.

Neither method is uniformly better in small samples. See Joint period tests on short slices before interpreting a short-regime omnibus or choosing the pairwise path.

Each slice's per-period series must clear the metric's own sample floor, resolved at the panel's stamped or declared overlap_periods — the same floor by_slice short-circuits on. The bootstrap block length is also never shorter than that known dependence horizon. Otherwise the test raises UserInputError on by rather than return an uncalibrated contrast. Plan the partition against sample_requirements (e.g. positive_rate() needs 10 periods per slice at overlap_periods=1, 50 at the default 5). Resolution follows evaluate's contract: the stamp left by compute_forward_return is the truth, a declared overlap_periods= disagreeing with it is rejected, and an unstamped (self-attached forward_return) panel must declare it — the same value by_slice(..., overlap_periods=) takes, so the descriptive and the inferential path never gate at different floors.

Pairwise output is (slice_a, slice_b, n_periods_a, n_periods_b, mean_diff, stat, p_raw, p_adj, stat_type, reference_dist, df_num, df_denom, multiplicity, min_periods, reason) — per-slice n_periods_* because disjoint spans differ in length, min_periods the floor they were gated on. In either slice test a pair whose contrast variance collapses carries NaN in stat / p_raw / p_adj (no test, not a non-rejection) and is left out of the Holm family; the period tests name it reason="degenerate_variance". The omnibus is a block-diagonal Wald χ² returning (k_slices, n_periods_min, stat, p_value, stat_type, reference_dist, df_num, df_denom, multiplicity, min_periods, reason).

Non-strict batch mode (slice_period_* only)

strict=False is the counterpart of evaluate(strict=False) for batch regime research, where one thin regime must not abort a sweep over factors × samples. A slice below the floor no longer raises; the affected rows come back in the same schema with reason="insufficient_periods" and NaN in stat / p_*:

  • joint — the omnibus restriction spans every slice, so one thin slice makes the whole test unavailable: a single row with n_periods_min (the shortest slice) against min_periods.
  • pairwise — every pair touching a thin slice is an unavailable row; the remaining pairs are tested and form their own multiplicity family (identical to running the test on the valid slices alone).

reason is null on a tested row, so reason.is_null() is the filter to apply before folding rows from many runs into one family.

Warning contract (slice_period_joint_test)

The slicing functions follow the same marked, never dropped contract as evaluate(..., expected_warnings=). The omnibus has one advisory — short_slice_joint_test, fired when K >= 3 and the shortest slice has fewer than 150 periods, the regime where the joint Wald over-rejects (8–9% analytic and 10–15% bootstrap at a nominal 5% for K=5 with 50–90-period slices; see the function's Warns: block for the measured grid). It is delivered on the row, not only on stderr:

Column Meaning
warning_codes every WarningCode the test raised (list[str], empty when clean)
unexpected_warning_codes the subset not declared via expected_warnings — the alert view
short_slice_periods the calibration threshold (150) the code is gated on

k_slices and n_periods_min already carry the values it was read against, so a stacked frame from a batch of regime tests answers which cells tripped it, whether the caller had declared it, and why. Declaring the code — slice_period_joint_test(..., expected_warnings=("short_slice_joint_test",)) — keeps it in warning_codes, drops it from unexpected_warning_codes and stops the per-call UserWarning echo, so a sweep over candidates and rebalance frequencies prints nothing for an accepted limitation while the audit record survives. Undeclared codes keep the echo. Unknown codes are rejected. by_slice takes the same argument and forwards it to every per-slice evaluate, applying it to its own slice_boundary_truncation record as well. slice_period_pairwise_test and the cross-sectional slice tests accept the same argument on the same contract — a typo is refused there too — but raise no advisory today, so they carry no such columns and a declaration has nothing to mark.

Inference path (cross-sectional slice tests)

slice_pairwise_test and slice_joint_test run one analytic path and take no estimator or multiple-testing argument:

Function Inference Multiplicity stat column carries
slice_pairwise_test Joint Newey-West HAC over the (T, K) per-date metric panel, slice-clustered; per-pair Wald via a single-row restriction on the joint variance Holm step-down on p_raw → p_adj (multiplicity="holm") Wald statistic; mean_diff carries the signed effect
slice_joint_test Same joint HAC; omnibus Wald over all K−1 contrasts — (one test) Omnibus Wald statistic

Holm is applied internally because the pairwise family is generated by the function itself; the user-assembled families that bhy_adjust serves are a different object. A bootstrap path exists only where slices do not share dates: slice_period_pairwise_test(method="bootstrap") and slice_period_joint_test(method="bootstrap") resample each period slice independently (see Date supports: aligned vs disjoint).

Cross-axis composition

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

Illustrative
ic_df = ic_df.with_columns(
    pl.concat_str(["regime", "universe"], separator="_").alias("regime_x_universe")
)
slice_pairwise_test(ic_df, ic, by="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(results, ...)
Multi-factor leaderboard rendering compare(...)