Skip to content

factrix.multi_factor.bhy_across_metrics

bhy_across_metrics(results: list[EvaluationResult], *, metrics: list[str], expand_over: tuple[str, ...] = (), q: float = 0.05) -> 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.

()
q float

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

0.05

Returns:

Name Type Description
A CrossMetricBhyResult

class:CrossMetricBhyResult containing every submitted cell.

Raises:

Type Description
UserInputError

Inputs do not define a valid, traceable family or any non-short-circuited metric cell has an invalid p-value.


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.

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

screen.n_tests       # {(): n_results * 2}, less data-shortage cells
screen.to_frame()    # factor | 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 insufficient_* cell stays in entries with active=False and adj_p=NaN, but does not enter the active family or n_tests.
  • Other missing or invalid p-values raise rather than silently changing the family.

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 inactive cells
survivors / adj_p Passing cell hypotheses and their adjusted p-values
metrics Metric labels in declared order
expand_over Keys partitioning separately reported families
n_tests Active factor × metric family size per bucket

factrix.multi_factor.CrossMetricBhyResult dataclass

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

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 an insufficient_* data-shortage cell. The record remains auditable but is excluded from the BHY denominator.