Skip to content

factrix.multi_factor.partial_conjunction_across_metrics

partial_conjunction_across_metrics(results: list[EvaluationResult], *, metrics: list[str], min_pass: int, family_size: int | Mapping[tuple[Any, ...], int] | None = None, q: float = 0.05, inactive_policy: InactivePolicy = 'count') -> CrossMetricPartialConjunctionResult

Test whether each factor identity passes at least k of m metrics.

Metric labels are the predeclared condition axis. For each result, the Bonferroni-style partial-conjunction p-value is computed across the fixed m endpoints, then BHY runs across factor identities. Inactive endpoints (a data-shortage short-circuit or a degenerate_variance result) are governed by inactive_policy: under the default "count" they stay in m at an inert p = 1; under "exclude" submitted inactive endpoints leave m. Identities with fewer than min_pass computed plus declared-unsubmitted endpoints remain in the audit output but do not enter the outer BHY family. min_pass must be declared before the p-values are seen — (m - k + 1) * p_((k)) is not monotone in k.

Parameters:

Name Type Description Default
results list[EvaluationResult]

Unique :class:EvaluationResult hypotheses. Horizon and all params remain part of each factor identity.

required
metrics list[str]

At least two unique, predeclared inferential metric labels.

required
min_pass int

k in the claim "at least k of m metrics carry signal". Must satisfy 2 <= min_pass <= len(metrics). Declared before the p-values are seen.

required
family_size int | Mapping[tuple[Any, ...], int] | None

Complete metric-endpoint family size per factor identity. A scalar applies to every identity; a mapping declares heterogeneous sizes keyed like the returned family_size. Declared-but-unsubmitted endpoints enter k-of-m as inert non-rejections. None uses len(metrics).

None
q float

Nominal FDR target for BHY across factor identities.

0.05
inactive_policy InactivePolicy

"count" (default) keeps an inactive endpoint in the k-of-m denominator at an inert p = 1; "exclude" drops it from m under an independent or pre-specified activity filter. See :data:InactivePolicy.

'count'

Returns:

Name Type Description
A CrossMetricPartialConjunctionResult

class:CrossMetricPartialConjunctionResult with factor-level

CrossMetricPartialConjunctionResult

survivors and the underlying metric hypotheses retained for audit.

Raises:

Type Description
UserInputError

Inputs, metric p-values, or min_pass violate the declared k-of-m contract.


When to use it

Use this procedure for a predeclared factor-level claim such as "the factor has signal on at least two of IC, beta, and spread." Metric labels are the fixed condition axis: each factor identity receives one k-of-m partial-conjunction p-value, followed by BHY across identities.

Illustrative
screen = fx.multi_factor.partial_conjunction_across_metrics(
    results,
    metrics=["ic", "beta", "spread"],
    min_pass=2,
    q=0.05,
)

screen.to_frame()
# factor | forward_periods | <sorted params> | pc_p | adj_p
#        | survived | eligible | family_size | n_passed_uncorr

This differs from bhy_across_metrics: pooled BHY selects factor × metric cells, while partial conjunction returns factor identities supported by at least k endpoints.

Inactive endpoints and m

The declared metric list fixes the endpoints you may claim on. An inactive endpoint (insufficient_* short-circuit or degenerate_variance) never ran a test, and inactive_policy says whether it still counts toward the k-of-m denominator — the same declaration every other screening function takes, so the same degenerate cell costs the same whichever verb sees it. See inactive candidates.

Under the default "count" the endpoint stays in m at an inert p = 1. Under "exclude" it leaves m; if fewer than min_pass endpoints remain, that identity cannot support the claim: it stays visible with eligible=False and empty PC/adjusted p-values, and does not enter the outer BHY family. Either way the endpoint stays in hypotheses for audit and family reports the counts.

min_pass must be declared before the p-values are seen — (m - k + 1) * p_(k) is not monotone in k (see partial_conjunction).

Descriptive endpoints and other invalid p-values fail loudly. The function does not implement min_pass=1 any-metric promotion.

Result fields

Field Meaning
entries Every tested factor identity, in input order
hypotheses Underlying result × metric cells retained for audit
pc_p_all Raw k-of-m p-value per identity; NaN when fewer than k endpoints are active
adj_p_all BHY-adjusted PC p-value per identity
survivors / adj_p Passing factor identities and their adjusted p-values
metrics / min_pass Declared m endpoints and required k
family_size The k-of-m denominator m per identity, including declared-unsubmitted endpoints
family Declared / computed / inactive / unsubmitted / adjusted endpoint counts and the inactive_policy used
n_identities Identities entering the outer BHY family

The frame's identity prefix is complete: same-named factors at different horizons or parameter settings remain separate rows.

factrix.multi_factor.CrossMetricPartialConjunctionResult dataclass

CrossMetricPartialConjunctionResult(entries: list[EvaluationResult], hypotheses: list[MetricHypothesis], adj_p_all: ndarray, pc_p_all: ndarray, q: float, metrics: tuple[str, ...], min_pass: int, family_size: Mapping[tuple[Any, ...], int], n_identities: int, n_passed_uncorr_all: ndarray, family: FamilyAccounting)

Bases: _ScreenResultMixin

Factor-level k-of-m metric confirmation followed by BHY.

to_frame

to_frame() -> DataFrame

Return one row per factor identity, including ineligible rows.