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]
|
|
forward_periods |
int
|
The economic return horizon — the |
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 |
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 |
n_assets |
int
|
Unique assets in the panel (cell-invariant;
|
metrics |
Mapping[str, MetricResult]
|
Read-only |
params |
Mapping[str, Hashable]
|
Caller-supplied hypothesis parameters — the sweep knobs
that decide which hypothesis this result is (e.g.
|
metadata |
Mapping[str, Any]
|
Caller-supplied bookkeeping labels that do not define
the hypothesis (e.g. |
warnings |
list[Warning]
|
Flat list of :class: |
plan |
str
|
Multi-line DAG execution plan (topological order,
|
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 ¶
JSON-friendly nested dict view.
Layout (top-level keys, stable order):
factor/cell/forward_periods/overlap_periods/n_periods/n_pairs/n_assets/params/metadatametrics: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 ¶
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_namepairing — 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 norebalance_lag); - only scalar values are exported —
bool/int/float/str(NaN/Infbecomenulllikevalue; numpy scalars are unwrapped). A list, dict, tuple or other nested value raisesUserInputErrornaming the metric and key — those are what :meth:to_dictis for; - a key colliding with a fixed column, a :attr:
paramskey or a repeated key raisesUserInputError, so metadata never shadows identity or the fixed schema; - dtypes are inferred per column from the values present.
Raises:
| Type | Description |
|---|---|
UserInputError
|
a :attr: |
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_namepairing, 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/Infbecomenulllikevalue, numpy scalars are unwrapped). A list, dict or other nested value raisesUserInputErrornaming the metric and key — that is whatto_dict()is for; - a key that collides with a fixed column, a
paramskey, or repeats raisesUserInputError, 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. |
alternative |
PValueAlternative | None
|
Alternative-hypothesis direction used to construct
|
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). |
n_obs_axis |
SampleAxis | None
|
Sample dimension |
stat |
float | None
|
Test statistic (t, z, W, chi2, ...), when applicable. Must be
finite; |
metadata |
dict[str, object]
|
Estimator-specific context beyond the top-level fields
( |
warning_codes |
tuple[str, ...]
|
Per-metric advisory :class: |
name |
str
|
Metric name stamped by the DAG executor at dispatch time. Empty string for outputs constructed outside the registry. |
is_applicable
property
¶
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
¶
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: readwarning_codesand the calibration limits before using it for a decision.alternative(str|None): The corresponding alternative hypothesis (two-sided,greater, orless).p_valueandalternativemust either both be present or both beNone. > [!IMPORTANT] >p_valueis 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 byn_obs_axis.n_obs_axis(str|None): Sample dimensionn_obscounts along — see Sample axes. It must be one of the documented sample-axis tokens.n_obsandn_obs_axisare an atomic pair: both are present, or both areNone.is_applicable(bool):Falseforstrict=Falseshort-circuit placeholders, so reporting code can filter them without inspectingmetadata["reason"].reason(str|None): Stable short-circuit reason, copied frommetadata["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: |
source |
str | None
|
Metric label that emitted the warning, or |
message |
str
|
Human-readable detail. |
expected |
bool
|
|
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,sourceisNone.message(str): A human-readable detail message describing the issue.