Skip to content

factrix.EvaluationResult dataclass

EvaluationResult(factor: str, cell: tuple[FactorScope, FactorDensity, DataStructure], forward_periods: int, overlap_periods: int, n_periods: int, n_pairs: int, n_assets: int, metrics: Mapping[str, MetricResult], plan: str, params: Mapping[str, Hashable] = dict(), metadata: Mapping[str, Any] = dict(), warnings: list[Warning] = list())

Bundle-level result for one factor.

Attributes:

Name Type Description
factor str

Factor column name from the source panel.

cell tuple[FactorScope, FactorDensity, DataStructure]

(scope, density, structure) tuple derived from the panel structure at dispatch time. structure is DataStructure.PANEL or DataStructure.TIMESERIES resolved from the panel's asset count; scope and density default to INDIVIDUAL / DENSE.

forward_periods int

The economic return horizon — the forward_periods forward_return was built with, in periods of the price grid. Read from the panel's compute_forward_return stamp (or the declared fallback for a self-attached panel). A property of the data, not a per-metric knob; joins the hypothesis identity.

overlap_periods int

The overlap of adjacent observations on the evaluation grid — the quantity inference consumed (HAC bandwidth and effective df, non-overlapping stride, stride-scaled floors). Equal to forward_periods on the full grid; smaller on a coarser evaluation grid (compute_forward_return(..., dates=)). Bookkeeping only — it does not join the hypothesis identity, because the same horizon evaluated on two grids is one hypothesis estimated twice, not two hypotheses.

n_periods int

Number of unique dates in the factor panel where the factor column is finite. A panel structural property — independent of any individual metric's estimator.

n_pairs int

Number of finite (date, asset_id) factor observations in the factor panel. A panel structural property.

n_assets int

Unique assets in the panel (cell-invariant; 1 is legal for TIMESERIES).

metrics Mapping[str, MetricResult]

Read-only label -> MetricResult mapping carrying per-metric outputs, keyed by the caller-supplied label. Structural metrics (e.g. greedy_forward_selection) carry their non-scalar payload in MetricResult.metadata — the value is still a :class:MetricResult.

params Mapping[str, Hashable]

Caller-supplied hypothesis parameters — the sweep knobs that decide which hypothesis this result is (e.g. {"timeframe": "1h", "universe": "tw50"}). Every value joins the hypothesis identifier, so two results that differ only in params are two distinct hypotheses. expand_over may name these keys to partition a family.

metadata Mapping[str, Any]

Caller-supplied bookkeeping labels that do not define the hypothesis (e.g. {"run_id": ..., "vintage": ...}). Never joins the identifier and never partitions a family, so two results differing only in metadata still collide as a duplicate hypothesis.

warnings list[Warning]

Flat list of :class:Warning records. Per-metric entries carry source=label; cross-metric or pre-dispatch entries carry source=None.

plan str

Multi-line DAG execution plan (topological order, [batchable] / [per-factor] annotations).

unexpected_warnings property

unexpected_warnings: list[Warning]

Warnings the caller did not declare via expected_warnings.

The alert view for pipelines and humans: :attr:warnings is the complete record (declared regimes included, flagged expected=True); this subset is what still deserves attention.

metric

metric(label: str) -> MetricResult

Return the :class:MetricResult for label.

Convenience over result.metrics[label] — the same lookup with a message that lists the available labels on a miss, so a typo in an interactive session fails loudly instead of with a bare KeyError.

to_dict

to_dict() -> dict[str, Any]

JSON-friendly nested dict view.

Layout (top-level keys, stable order):

  • factor / cell / forward_periods / overlap_periods / n_periods / n_pairs / n_assets / params / metadata
  • metrics: label -> {value, p_value, alternative, stat, n_obs, n_obs_axis, is_applicable, reason, metadata}
  • warnings: list of {code, source, message, expected}
  • plan

Nested mappings, dataclass instances, lists, and tuples are normalized recursively. Mapping keys accepted by the standard JSON encoder are converted to strings; Python and numpy values become JSON-native scalars; non-finite floats become None. Unsupported values raise a path-aware :class:UserInputError.

to_frame

to_frame(*, metadata: Sequence[str] = ()) -> DataFrame

One row per produced metric, prefixed with bundle identity.

Schema (column order is stable; metadata= columns, if any, follow warning_codes in the order requested):

column dtype source
factor str :attr:factor
forward_periods i64 :attr:forward_periods
one column per :attr:params key inferred :attr:params value
overlap_periods i64 :attr:overlap_periods (bookkeeping, not identity)
n_assets i64 :attr:n_assets
metric_name str MetricResult.name
value f64 | null MetricResult.value
p_value f64 | null MetricResult.p_value
alternative str | null MetricResult.alternative
stat f64 | null MetricResult.stat
n_obs i64 | null MetricResult.n_obs — estimator effective sample size
n_obs_axis str | null MetricResult.n_obs_axis — axis n_obs counts along (periods / events / pairs / asset_pairs / assets)
is_applicable bool false for strict=False short-circuits
reason str | null short-circuit reason when not applicable
warning_codes list[str] per-metric warning codes — bundle records sourced on this metric, unioned (de-duplicated, first-seen order) with MetricResult.warning_codes

The leading factor / forward_periods / :attr:params block is the hypothesis identity — the same tuple :meth:to_dict and :func:factrix.compare carry. Without it, stacking the three results of an evaluate_horizons sweep produced three indistinguishable rows.

Designed for stacking across factors: pl.concat([r.to_frame() for r in results.values()]) (results whose :attr:params keys differ need pl.concat(..., how="diagonal"), as in :func:factrix.compare).

Exporting estimator metadata. The fixed schema is the stable cross-metric contract; estimator-specific definitions (a spread's n_groups, a turnover's rebalance_lag / mean_tail_size) live in MetricResult.metadata and do not leak into it by default. metadata= names the keys to carry along, one column per key, so a CSV written from the stacked frame can be audited without the MetricResult objects:

  • the column is named after the key and keeps the row's factor / metric_name pairing — stacking many results keeps every metadata cell next to the metric it came from;
  • a metric that does not carry the key gets null (a spread row has no rebalance_lag);
  • only scalar values are exported — bool / int / float / str (NaN / Inf become null like value; numpy scalars are unwrapped). A list, dict, tuple or other nested value raises UserInputError naming the metric and key — those are what :meth:to_dict is for;
  • a key colliding with a fixed column, a :attr:params key or a repeated key raises UserInputError, so metadata never shadows identity or the fixed schema;
  • dtypes are inferred per column from the values present.

Raises:

Type Description
UserInputError

a :attr:params key collides with a fixed column name; a metadata key collides, repeats, or names a non-scalar value on some metric.


Serialization Methods

EvaluationResult provides two main methods to serialize evaluation outputs:

to_frame()

Converts the metric results into a stable, long-form Polars pl.DataFrame. This makes it easy to stack results across multiple factors using pl.concat([r.to_frame() for r in results]) before writing to disk (e.g., Parquet).

The leading factor / forward_periods / params block is the hypothesis identity — the same (factor, forward_periods, *params) tuple to_dict() and compare() carry. Without it, the three results of an evaluate_horizons sweep stack into three indistinguishable rows.

Schema: - factor (str): The factor column name. - forward_periods (i64): The return horizon — the hypothesis. - one column per params key (dtype inferred): the caller-supplied hypothesis knobs. A params key colliding with a fixed column name above raises UserInputError. Results whose params keys differ need pl.concat(..., how="diagonal"). - overlap_periods (i64): The evaluation-grid overlap inference consumed (equal to forward_periods on the full grid). Bookkeeping, not identity. - n_assets (i64): Total unique assets. - metric_name (str): The metric identifier. - value (f64 | null): The calculated metric value (NaN/Inf are normalized to null). - p_value (f64 | null): The p-value, if applicable. - alternative (str | null): The tested alternative (two-sided, greater, or less), present exactly when p_value is present. - stat (f64 | null): The underlying test statistic, if applicable. - n_obs (i64 | null): Effective sample size the metric's estimator used. null only where a single integer count is not meaningful (e.g. a multi-window CAAR series). - n_obs_axis (str | null): The dimension n_obs counts along — see Sample axes below. A bare count is uninterpretable without it (a Fama-MacBeth n_obs is periods; a pooled-OLS one is (date, asset) pairs). null exactly when n_obs is. - is_applicable (bool): false when strict=False returned a short-circuit placeholder for an unsupported metric/input combination. - reason (str | null): Short-circuit reason when is_applicable is false. - warning_codes (list[str]): Warnings attached to the metric — the bundle-level Warning records sourced on it, unioned (de-duplicated, first-seen order) with the producer's own MetricResult.warning_codes.

Exporting estimator metadata (metadata=)

The fixed schema is the cross-metric contract; estimator-specific definitions (a spread's n_groups, a turnover's rebalance_lag or mean_tail_size) stay in MetricResult.metadata and are not expanded by default. to_frame(metadata=(...)) names the keys to carry along — one trailing column per key, in the order requested — so a CSV written from the stacked frame can be audited without the MetricResult objects:

  • the column keeps the row's factor / metric_name pairing, so stacking many results keeps every cell next to the metric it came from;
  • a metric that does not carry the key gets null;
  • only scalar values export (bool / int / float / str; NaN / Inf become null like value, numpy scalars are unwrapped). A list, dict or other nested value raises UserInputError naming the metric and key — that is what to_dict() is for;
  • a key that collides with a fixed column, a params key, or repeats raises UserInputError, so metadata never shadows the identity block or the fixed schema;
  • dtypes are inferred per column.

Tradability audit — is the turnover pricing the same book as the spread?

import factrix as fx
from factrix.metrics import notional_turnover, quantile_spread
from factrix.preprocess import compute_forward_return

raw = fx.datasets.make_cs_panel(n_assets=6, n_dates=120, rng=0)
panel = compute_forward_return(raw, forward_periods=5)
result = fx.evaluate(
    panel,
    metrics={
        "spread": quantile_spread(n_groups=2),
        "turnover": notional_turnover(n_groups=2, rebalance_lag=1),
    },
    factor_cols=["factor"],
)["factor"]

audit = result.to_frame(metadata=("n_groups", "rebalance_lag", "mean_tail_size"))
print(audit.select("metric_name", "value", "n_groups", "rebalance_lag", "mean_tail_size"))
# spread    ...  2  null  3.0
# turnover  ...  2  1     3.0

Both rows show n_groups = 2, so the spread and the turnover describe the same top-half / bottom-half book; the spread row's rebalance_lag is null because a spread carries no such key.

to_dict()

Converts the complete result into a JSON-native nested dictionary. JSON scalar values (None, bool, int, float, and str) and their numpy scalar equivalents are normalized recursively, with NaN and Inf becoming None. Mappings, dataclass instances, lists, and tuples are supported at every depth; dataclasses become mappings of their declared fields and tuples become JSON arrays. Mapping keys follow the standard JSON encoder: strings, integers, finite floats, booleans, and None become JSON object keys. This policy applies equally to bundle metadata, metric metadata, params, and the rest of the payload, so the result can be serialized strictly with json.dumps(result.to_dict(), allow_nan=False).

Sets, unsupported mapping keys, custom objects, cyclic containers, and other unsupported values raise UserInputError. Its structured field identifies the exact payload path (for example, metadata['diagnostics']['model']) so the invalid bookkeeping value can be corrected before it reaches a JSON encoder.


Sample axes

n_obs_axis names the unit n_obs counts. It is the only thing that makes a bare count comparable across metrics in a stacked to_frame() — on one N=12, T=239 panel the values below differ by ~60x.

token unit metrics
periods dates, or adjacent-period transitions for a turnover metric ic, fm_beta, quantile_spread, k_spread, monotonicity, rank_turnover, notional_turnover
assets cross-sectional names common_beta, asset-axis short-circuits
events event observations (non-zero factor cells) caar, event_hit_rate, event_ic, corrado_rank, ...
pairs pooled (date, asset) observations, each treated as one independent draw pooled_beta, directional_hit_rate
asset_pairs within-period unordered asset couples (C(n_assets, 2) per date) directional_pair_accuracy

pairs and asset_pairs are deliberately separate tokens: a pooled (date, asset) count and a within-period ordering-pair count are different units, and sharing one label made a stacked table unreadable.


The metrics mapping

EvaluationResult.metrics is a read-only Mapping[str, MetricResult] (a MappingProxyType) keyed by the caller-supplied metric label, including short-circuit NaN outputs. Access it like any dict — result.metrics["ic"] returns the MetricResult for "ic", and keys() / values() / items() / get() / in / len() / iteration all work as usual. The mapping is immutable; attempting to assign into it raises TypeError.

result.metric("ic") is a convenience over result.metrics["ic"]: the same lookup, but a miss raises KeyError listing the available labels (no metric 'sharpe' on this result; available: ic, ic_ir) instead of a bare key — handy in interactive sessions where a typo would otherwise fail opaquely.


factrix.MetricResult dataclass

MetricResult(value: float, p_value: float | None = None, alternative: PValueAlternative | None = None, n_obs: int | None = None, n_obs_axis: SampleAxis | None = None, stat: float | None = None, metadata: dict[str, object] = dict(), warning_codes: tuple[str, ...] = (), name: str = '')

Single-metric result produced by a factrix.metrics.* primitive.

Attributes:

Name Type Description
value float

Raw metric value.

p_value float | None

Reported p-value for the metric's hypothesis test. None when no test result is available, including descriptive metrics and tests whose statistic is withheld.

alternative PValueAlternative | None

Alternative-hypothesis direction used to construct p_value. Present exactly when p_value is present.

n_obs int | None

Effective sample size the estimator actually used (e.g. number of non-overlapping IC periods, number of events, number of bootstrap windows). None where a single integer count is not meaningful (e.g. multi-window CAAR series). Non-negative integer-valued Python and numpy real scalars are normalized to a Python int; booleans, fractional values, strings, and non-finite values are rejected.

n_obs_axis SampleAxis | None

Sample dimension n_obs counts along — one of "periods" / "events" / "pairs" / "asset_pairs" / "assets". A bare count is uninterpretable without its axis (a Fama-MacBeth n_obs is periods; a pooled-OLS one is (date, asset) pairs), so producers stamp the axis alongside the count. None exactly when n_obs is.

stat float | None

Test statistic (t, z, W, chi2, ...), when applicable. Must be finite; NaN is accepted as the "estimator ran but could not form the statistic" marker, ±Inf is rejected (it only ever arises from an unguarded division by a zero standard error).

metadata dict[str, object]

Estimator-specific context beyond the top-level fields (stat_type, h0, method are the standard keys).

warning_codes tuple[str, ...]

Per-metric advisory :class:WarningCode values (as strings) the producer attached to this output. Empty tuple when the metric raised no advisory.

name str

Metric name stamped by the DAG executor at dispatch time. Empty string for outputs constructed outside the registry.

is_applicable property

is_applicable: bool

Whether the metric produced a usable result for this input.

strict=False short-circuits unsupported metrics into placeholder outputs with metadata["reason"]. This flag lets reporting and grid-search code filter those rows without relying on metadata shape.

reason property

reason: str | None

Stable short-circuit reason, when the metric did not run cleanly.


Key Fields

MetricResult represents the outcome of a single metric calculation. Its primary fields are:

  • value (float): The calculated numeric output of the metric.
  • p_value (float | None): The reported p-value for the metric's hypothesis test. When present, it is finite and in [0, 1]. A valid number does not by itself establish calibration: read warning_codes and the calibration limits before using it for a decision.
  • alternative (str | None): The corresponding alternative hypothesis (two-sided, greater, or less). p_value and alternative must either both be present or both be None. > [!IMPORTANT] > p_value is the canonical field for metric p-values.
  • stat (float | None): The test statistic (e.g. t-statistic, z-statistic).
  • n_obs (int | None): Non-negative effective sample size used by the estimator. Its unit is carried by n_obs_axis.
  • n_obs_axis (str | None): Sample dimension n_obs counts along — see Sample axes. It must be one of the documented sample-axis tokens. n_obs and n_obs_axis are an atomic pair: both are present, or both are None.
  • is_applicable (bool): False for strict=False short-circuit placeholders, so reporting code can filter them without inspecting metadata["reason"].
  • reason (str | None): Stable short-circuit reason, copied from metadata["reason"] when present.
  • metadata (dict): Underlying dictionary of metric-specific metadata.
  • warning_codes (tuple[str, ...]): Advisory warnings attached to the metric.
  • name (str): The name of the metric, automatically populated during dispatch.

factrix.Warning dataclass

Warning(code: WarningCode, source: str | None = None, message: str = '', expected: bool = False)

Flat per-evaluation diagnostic record.

Source convention: a per-metric warning carries source == <metric label>; a panel-level / cross-metric warning carries source is None.

Attributes:

Name Type Description
code WarningCode

The :class:WarningCode enum member.

source str | None

Metric label that emitted the warning, or None for bundle-level diagnostics.

message str

Human-readable detail.

expected bool

True when the caller declared this code as the study's design via evaluate(..., expected_warnings=(...,)) or the pre-flight design via inspect_data(..., expected_warnings=(...,)). The record is never dropped — the flag says "acknowledged", not "absent" — so human-facing channels (stderr echo, repr emphasis) can go quiet while the audit trail stays complete.


Fields

A Warning object represents a warning emitted during data inspection or metric evaluation:

  • code (WarningCode): The enum identifier for the warning type (e.g. WarningCode.UNRELIABLE_SE_SHORT_PERIODS).
  • source (str | None): The name of the metric that generated the warning. If the warning is data-level or cross-metric, source is None.
  • message (str): A human-readable detail message describing the issue.