Skip to content

factrix.MetricsBundle dataclass

MetricsBundle(identity: tuple[str, int], metrics: Mapping[str, MetricOutput] = dict(), skipped: Mapping[str, str] = dict(), context: Mapping[str, Any] = dict())

Cell-level descriptive metrics for one factor at one horizon.

Parallel to :class:factrix.FactorProfile (the primary inferential artifact). The bundle holds every standalone metric run_metrics successfully evaluated for the cell, plus a skipped map of metrics that could not auto-run (with reason).

Hashing is disabled (__hash__ = None) because the bundle holds MetricOutput instances whose metadata is a mutable dict. Group bundles by identity (a hashable tuple), not by the bundle itself.

Attributes:

Name Type Description
identity tuple[str, int]

(factor_id, forward_periods) — the hypothesis dimensions per #160. Aligns with FactorProfile.identity so downstream functions (compare / cross-slice analysis) can stack profiles and bundles by the same key.

metrics Mapping[str, MetricOutput]

Metric name → MetricOutput mapping for every metric that produced a value (including short-circuit MetricOutput entries — value=NaN with a reason in metadata["reason"]).

skipped Mapping[str, str]

Metric name → reason string for metrics that could not auto-run (excluded by the registry, requires explicit kwargs, stage-1 helper failed, …). v1 surfaces this through __repr__ / _repr_html_ and a single logging.info.

context Mapping[str, Any]

Sample-restriction / conditioning dimensions per #160. v1 always empty; downstream slicers (factrix.by_slice and the slice-test function pair) populate via dataclasses.replace once available, or callers stamp manually after panel-side filtering.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> bundle = fx.run_metrics(panel, fx.AnalysisConfig.individual_continuous())
>>> isinstance(bundle, fx.MetricsBundle)
True
>>> "ic" in bundle
True
>>> ic_output = bundle["ic"]
>>> bundle.identity == ("factor", 5)
True

to_frame

to_frame() -> DataFrame

Long-form view of the bundle: one row per metric.

Schema (8 columns, stable regardless of bundle contents):

column type source
factor_id str identity[0]
forward_periods int identity[1]
metric str mapping key
value float MetricOutput.value
stat float \ null
significance str \ null
p_value float \ null
short_circuit_reason str \ null

metadata is not flattened — the per-metric shape is heterogeneous (per-regime dicts, per-horizon entries, Kolari-Pynnönen source labels, …) and a flat schema would fight every consumer. Reach into bundle["name"].metadata for the rest.

context.* is not flattened in v1 — context is always empty here; downstream slicers populating context is a v1.x extension that may grow the schema.

skipped entries do not appear; only successful and short-circuited MetricOutput rows are emitted.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
>>> panel = compute_forward_return(raw, forward_periods=5)
>>> bundle = fx.run_metrics(panel, fx.AnalysisConfig.individual_continuous())
>>> df = bundle.to_frame()
>>> set(df.columns) >= {"factor_id", "metric", "value", "p_value"}
True
>>> df.height >= 1
True

Result type returned by run_metrics — holds every standalone metric the cell successfully evaluated for one factor at one horizon, plus a skipped map for metrics that could not auto-run.

Parallel to FactorProfile: both share the identity tuple (factor_id, forward_periods) so downstream functions (compare, survivors workflows) can join the two artifact families row-by-row.

Bundle Profile
run_metrics — descriptive surface (no false discovery rate (FDR) claim) evaluate — primary inferential decision (drives FDR)
Many metrics per cell, each a MetricOutput One primary stat + sidecar stats dict
compare(bundles) → cross-factor descriptive view compare(profiles) / compare(survivors)

Typical use

import factrix as fx

cfg    = fx.AnalysisConfig.individual_continuous(metric=fx.Metric.IC, forward_periods=5)
bundle = fx.run_metrics(panel, cfg, factor_col="momentum_12_1")

bundle.identity           # ('momentum_12_1', 5)
bundle.metrics            # dict[str, MetricOutput]
bundle.to_frame()         # pl.DataFrame — flat tabular view for compare / plotting
bundle.skipped            # {metric_name: reason} — metrics that short-circuited

For the wide multi-factor pattern (looping run_metrics with factor_col= over candidate signals) see the Batch screening guide.