Data schema
Single-source contract for every factrix entry point that consumes a panel. Every dispatch cell evaluate runs floors its input schema at the same four columns described here. Per-cell extensions (optional weight / price columns) are listed under Optional columns.
Four-column contract¶
| Column | dtype | Semantics |
|---|---|---|
date |
Date or Datetime (required — a String or integer date is rejected) |
Observation timestamp. Ordering key only — the horizon is measured in periods of the panel's own distinct-date grid, never in calendar time (Period grid, not calendar). |
asset_id |
Utf8 / Categorical |
Cross-section identifier. Identical for COMMON-scope factors. Scope detection reads finite cells only, so a missing cell does not make a broadcast factor individual (df.filter(pl.col("factor").cast(pl.Float64).is_finite()).group_by("date").agg((pl.col("factor").n_unique() == 1).alias("is_common"))["is_common"].all() is True). |
factor |
numeric (Int* / Float*) |
The signal value. Dense: real-valued exposure (z-score, IC-rankable). Sparse: zero-encoded {0, R} event trigger, where 0 marks non-events. |
forward_return |
Float64 |
Look-ahead return over the horizon used at evaluate time. Attach via compute_forward_return so the horizon is explicit and aligned with forward_periods. |
The minimal panel is therefore long-format (date, asset_id, factor, forward_return). Two assets over three periods:
Ingestion contract¶
Every entry point that consumes a panel (evaluate, evaluate_horizons,
inspect_data, by_slice, compute_forward_return) normalises it once at
the boundary. Three rules are enforced, and one rewrite is applied:
| Rule | On violation |
|---|---|
date is Date or Datetime |
UserInputError — a String date sorts lexicographically and silently reorders any non-ISO format; parse it first (pl.col("date").str.to_date(fmt)). |
(date, asset_id) is unique |
UserInputError — a duplicate makes one asset-period observation ambiguous and can fan out keyed joins or double-count that asset downstream. |
| Per-asset period grids agree | ragged_period_grid warning — horizons stay on the shared panel grid; an asset missing the required entry or exit period has no return for that pair. |
| Non-finite float cells | NaN and ±inf in every float column are rewritten to null (integer, boolean, Decimal and string columns are untouched). A +inf price therefore becomes a gap rather than a fabricated -100 % return; a NaN factor cell is a missing value like null. |
The normalisation is idempotent and cheap (≈0.15 s on 3 M rows), so
compute_forward_return followed by evaluate runs it twice without cost.
For sparse factors, null factor cells are missing values, not non-events, and
are excluded from sparse-ratio detection. Fill missing values to 0 only when
that is the intended event contract; see
Sparse and event signals.
import polars as pl
from datetime import date
panel = pl.DataFrame({
"date": [date(2024, 1, 1), date(2024, 1, 1),
date(2024, 1, 2), date(2024, 1, 2),
date(2024, 1, 3), date(2024, 1, 3)],
"asset_id": ["A", "B", "A", "B", "A", "B"],
"factor": [0.12, -0.08, 0.20, 0.04, -0.15, 0.18],
"forward_return": [0.01, 0.00, 0.02, 0.00, -0.01, 0.03],
})
The two synthetic dataset generators emit this layout (plus a price column) ready for compute_forward_return: fx.datasets.make_cs_panel (cross-sectional) and fx.datasets.make_event_panel (event-study).
Accepted input type: DataInput¶
Every data-consuming entry point annotates its first argument as DataInput — an eager pl.DataFrame or a pl.LazyFrame. A LazyFrame is collected internally, so the choice is purely ergonomic; the schema contract above is identical either way.
factrix.DataInput ¶
Accepted panel input type for every data-consuming entry point.
Either an eager pl.DataFrame or a pl.LazyFrame carrying the panel
schema (see Data schema). A LazyFrame is
collected internally, so passing one is purely an ergonomic convenience —
the validation and dispatch contract is identical.
factor_cols= — signal column names¶
Panels often arrive with the signal column named something other than "factor" (e.g. "alpha", "score", "momentum_12_1"). Pass a list of column names in factor_cols= to fx.evaluate to evaluate them:
from factrix.metrics import ic
results = fx.evaluate(
panel,
metrics={"ic": ic(inference=fx.inference.NEWEY_WEST)},
factor_cols=["momentum_12_1"],
forward_periods=5,
)
Behaviour:
evaluateprojects each entry infactor_colsto the canonical"factor"name internally so every metric callable still sees the four-column schema.factor_cols=[...]accepts a list of column names — IC stage-1 and batch-native primitives share one polars query across the batch.- Each returned
EvaluationResulthasresult.factorandresult.forward_periods.
Error cases (both raise UserInputError):
| Trigger | Message hint |
|---|---|
factor_cols not present on the panel |
Lists the actual columns; suggests a fuzzy match. |
Optional columns¶
Per-cell extensions activate additional standalone metrics when present and short-circuit (NaN with reason) when absent — they never gate the core procedure.
A direct call to a raw-panel metric validates the key columns date + asset_id before running and raises the same UserInputError as evaluate when one is missing; see Standalone metrics.
| Column | Activates | Cell |
|---|---|---|
market_cap |
quantile_spread_vw value-weighting |
Individual × Continuous |
price |
event_around_return, mfe_mae, event-window diagnostics |
Individual × Sparse |
Through evaluate the column must carry its declared name above: the panel is
projected per factor before a metric's keyword arguments are known, so a
weight_col= override only applies to a direct quantile_spread_vw(...) call.
inspect_data reports quantile_spread_vw as unusable, with a blocker naming
the column, on a panel that has no market_cap.
For event paths, a price column on the evaluation panel can only cover that
panel's retained rows. Pass the original full panel separately as
evaluate(..., price_data=raw)
when preprocessing removed the forward-return tail or selected a coarser
evaluation grid. data owns events and return samples; price_data owns only
the complete price grid.
Reserved columns¶
compute_forward_return stamps two constant Int32
columns on the panel. They are never treated as factor columns, are read by
evaluate / by_slice / sample_requirements / the slice_period_* tests
as the single source of truth, and are stripped before dispatch, so they never
reach a metric or to_frame(). Do not write them by hand.
| Column | Carries | Surfaces as |
|---|---|---|
_forward_periods |
The return horizon — the forward_periods the return was built with, in periods of the price grid. Names the hypothesis. |
EvaluationResult.forward_periods; the (factor, forward_periods, *params) identity |
_overlap_periods |
The overlap of adjacent observations on the evaluation grid — what inference consumes (HAC bandwidth and df, non-overlapping stride, stride-scaled floors). Equal to the horizon on the full grid; derived from dates= on a coarser one (Evaluating on a coarser grid). |
EvaluationResult.overlap_periods (bookkeeping, not identity); metadata["overlap_periods"] on every metric |
A constant column is the one carrier that survives the ordinary polars
transforms a panel goes through between construction and evaluation
(with_columns, joins, partition_by in by_slice); DataFrame-level
metadata does not. A self-attached forward_return panel carries neither
stamp and declares forward_periods= (and, on a coarser grid,
overlap_periods=) on evaluate.
Every present stamp must remain a non-null positive integer with one value
over the complete panel. Public entry points reject mixed or invalid stamp
columns before dispatch (and before by_slice partitions the frame), because
otherwise row or slice order could select a different hypothesis and inference
configuration. Evaluate different return horizons as separate panels with
evaluate_horizons; do not concatenate their stamped rows
into one panel.
Common errors¶
Schema-related failures and their fix paths:
| Message | Trigger | Fix |
|---|---|---|
factor_cols 'X' not in panel columns |
Typo / wrong column name | Check panel.columns; pass the actual name to factor_cols=. |
forward_return column missing |
Forgot the preprocess step | panel = compute_forward_return(raw, forward_periods=h) before evaluate. |
Full error taxonomy and recovery patterns: Errors.
Preprocess pipeline¶
The canonical pipeline from raw price/event data to evaluate-ready panel:
raw panel ──compute_forward_return(h)──▶ evaluation + forward-return sample
└──────────────── price_data ────────────────┐
▼
evaluate(...)
Pre-attachment helpers live in factrix.preprocess; synthetic panels in factrix.datasets. Wide-format multi-factor inputs are handled by passing the column names through factor_cols= on a single evaluate call rather than by reshaping the panel.