Skip to content

factrix.multi_factor.bhy_across_metrics

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

Pool factor x metric hypotheses into one BHY FDR family.

The survivor unit is one (EvaluationResult, metric label) cell. This controls hypothesis-level FDR; deduplicating survivors by factor does not create a factor-level FDR guarantee.

Parameters:

Name Type Description Default
results list[EvaluationResult]

Unique :class:EvaluationResult hypotheses.

required
metrics list[str]

At least two unique inferential metric labels, in the desired audit order.

required
expand_over tuple[str, ...]

Result fields or params keys that partition the input into separately reported families. Metrics stay pooled inside each bucket.

()
family_size int | Mapping[tuple[Any, ...], int] | None

Complete factor-by-metric family size. A scalar applies to every expand_over bucket; a mapping declares heterogeneous bucket sizes. Missing cells are inert non-rejections and always remain in m. None uses the submitted cell count.

None
q float

Nominal FDR target in the open interval (0, 1).

0.05
inactive_policy InactivePolicy

"count" (default) keeps an inactive cell in the pooled family 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 CrossMetricBhyResult

class:CrossMetricBhyResult containing every submitted cell.

Raises:

Type Description
UserInputError

Inputs do not define a valid, traceable family, any non-short-circuited metric cell has an invalid p-value, or inactive_policy is not 'count' / 'exclude'.


When to use it

Use bhy_across_metrics when the research process may select any tested factor × metric cell. Unlike bhy, which deliberately runs one screen per metric label, this function flattens all declared labels into one Benjamini-Hochberg-Yekutieli family.

Illustrative
screen = fx.multi_factor.bhy_across_metrics(
    results,
    metrics=["ic", "spread"],
    q=0.05,
)

screen.family_size   # {(): n_results * 2} under the default policy
screen.family        # declared / computed / inactive / unsubmitted / adjusted + policy
screen.to_frame()
# factor | forward_periods | <sorted params> | metric
#        | p_value | adj_p | survived | active

The survivor unit is one (EvaluationResult, metric label) hypothesis. Taking the unique factor names afterward does not provide factor-level FDR control. For the claim that a factor works on at least k predeclared endpoints, use partial_conjunction_across_metrics.

Family and audit contract

  • metrics must contain at least two unique inferential labels. A descriptive output with p_value=None fails loudly.
  • Entry order is result-major, then caller-supplied metric order.
  • expand_over retains the existing bhy meaning: it partitions by a result field or params key; metric labels remain pooled inside each bucket.
  • An inactive cell (insufficient_* or degenerate_variance) stays in entries with active=False. Under the default inactive_policy="count" it stays in family_size at an inert p = 1; under "exclude" it leaves the family and carries adj_p=NaN — see inactive candidates.
  • Other missing or invalid p-values raise rather than silently changing the family.
  • to_frame() preserves the underlying result identity (factor, horizon, and sorted params) before the metric-level audit columns, so repeated factor names remain traceable across sweeps.

Result fields

Field Meaning
entries Every traceable MetricHypothesis, including inactive and eliminated cells
adj_p_all BHY-adjusted p-values aligned with entries; NaN for a cell that entered no family
survivors / adj_p Passing cell hypotheses and their adjusted p-values
metrics Metric labels in declared order
expand_over Keys partitioning separately reported families
family_size Factor × metric family size per bucket — the m each step-up ran on, including declared-unsubmitted cells
family Declared / computed / inactive / unsubmitted / adjusted counts and the inactive_policy used

factrix.multi_factor.CrossMetricBhyResult dataclass

CrossMetricBhyResult(entries: list[MetricHypothesis], adj_p_all: ndarray, q: float, metrics: tuple[str, ...], expand_over: tuple[str, ...], family_size: Mapping[tuple[Any, ...], int], family: FamilyAccounting)

Bases: _ScreenResultMixin

One BHY screen over a pooled factor x metric hypothesis family.

to_frame

to_frame() -> DataFrame

Return every tested cell, including inactive and eliminated rows.

factrix.multi_factor.MetricHypothesis dataclass

MetricHypothesis(result: EvaluationResult, metric_name: str, p_value: float, identifier: tuple[Any, ...], expand_over_values: tuple[Any, ...], is_active: bool)

One traceable EvaluationResult x metric hypothesis.

is_active=False marks a placeholder cell (data-shortage short-circuit or degenerate variance). The record remains auditable but is not a hypothesis, so it is excluded from every family.