Skip to content

factrix.inspect_data

inspect_data(data: Any, factor_cols: Sequence[str] | None = None, *, expected_warnings: tuple[str, ...] = ()) -> DataInspection

Inspect data and return typed dispatch-axis + per-metric verdict.

Pre-flight introspection: typed detection plus, for every role=SpecRole.METRIC :class:MetricSpec in the registry, a :class:MetricApplicability verdict combining (a) cell-match against the detected (scope, density, structure) and (b) the spec's optional :class:SampleThreshold against the data's n_periods / n_assets / n_pairs / n_events.

The n_events floor is pre-flighted against the non-zero factor count for event-driven metrics; a metric with a dynamic event floor (caar) is pre-flighted at its default config, and its in-body short-circuit on the actual run-time params stays authoritative.

Missing cells never move an axis. Scope is read from finite factor cells only — a broadcast common factor with a gap on one asset stays COMMON, and a period left with a single finite cell is still compatible with COMMON. A period with no finite cell carries no evidence and is ignored; if every period is missing, the axis is unidentifiable, routing falls back to the unrestricted FactorScope.INDIVIDUAL, and a FACTOR_SCOPE_UNIDENTIFIABLE data-level warning names the count the fallback was taken on. evaluate dispatches through the same detector, so pre-flight scope and the cell a run is routed to cannot disagree.

Content-based gates beyond cell and sample shape follow the run-time contract. A metric declaring requires_continuous_magnitude (e.g. event_ic) is blocked on a discrete ±k signal (|factor| constant across events, such as a ternary {-1, 0, +1} indicator), matching its run-time not_applicable_discrete_signal short-circuit. For a COMMON factor, common_quantile_spread is blocked when its usable per-date history has fewer than 2 * n_groups distinct values. A one-sided factor leaves directional_hit_rate's point estimate defined but its test degenerate, so that state is reported as a degraded DEGENERATE_VARIANCE advisory rather than as an unusable metric.

Multi-factor input is inspected per column, the granularity :func:factrix.evaluate dispatches at: :attr:DataInspection.factors maps each inspected column to a :class:FactorInspection carrying that column's scope, density, counts, stage-one profiles, warnings and full metric verdicts — nothing there is inherited from a sibling column.

Two granularities rather than one, deliberately: a screen over dozens of candidate columns still wants a single answer to "what is this panel", so :attr:~DataInspection.properties, :attr:~DataInspection.metrics and the usable / degraded / unusable partitions remain the concise aggregate and describe the first inspected column (the whole panel when there is only one). They are exactly factors[<first column>], so single-factor callers never touch the mapping and multi-factor callers never have to guess which column an aggregate refers to. When columns disagree on FactorDensity or FactorScope, a CROSS_FACTOR_*_MISMATCH data-level warning names the basis column, the columns that disagree with it and the value each carries.

warnings on the result is the first column's warnings plus those cross-factor mismatches; a later column's own advisories live on factors[col].warnings rather than being merged into one stream where the column they belong to would be lost.

One panel-level caveat: n_periods and n_assets are properties of the data, so every :class:FactorInspection reports the panel's counts, not the periods that column happens to cover. Where a column's own coverage gates a metric it does so through the stage-one profile, which is computed per column (a column whose IC cross-sections survive on 20 periods is blocked at ic's 50-period floor while its sibling is not). Common-beta consumers likewise apply their asset floor to the per-asset regressions surviving compute_common_betas, not the raw universe.

Parameters:

Name Type Description Default
data Any

Long-format factor data with the canonical columns. The baseline columns date / asset_id / forward_return and the optional schema columns price / market_cap are reserved and not treated as factors.

required
factor_cols Sequence[str] | None

Optional list of factor columns to check. When None (default), auto-detects all columns except the reserved columns as factor candidates.

None
expected_warnings tuple[str, ...]

:class:~factrix.WarningCode values declaring known pre-flight regimes. Matching records remain present with expected=True; only their UserWarning echoes stop.

()

Returns:

Type Description
DataInspection

class:DataInspection.

Examples:

>>> import factrix as fx
>>> import polars as pl
>>> raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
>>> info = fx.inspect_data(raw)
>>> all(m.spec.cell.matches(info.properties.scope, info.properties.density, info.properties.structure) for m in info.usable)
True

Per-column verdicts for a heterogeneous panel:

>>> data = raw.with_columns(pl.col("factor").mean().over("date").alias("macro"))
>>> info = fx.inspect_data(data, factor_cols=["factor", "macro"])
>>> info.factors["factor"].properties.scope.value
'individual'
>>> info.factors["macro"].properties.scope.value
'common'
>>> "ic" in info.factors["macro"].unusable.names
True

Warning declarations

inspect_data surfaces every pre-flight advisory both as a structured Warning record and as a framed UserWarning. Use the same declaration as an evaluation when a regime is intentional:

import factrix as fx

panel = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
info = fx.inspect_data(
    panel,
    expected_warnings=("few_assets", "unreliable_se_short_periods"),
)

The records remain in info.warnings, info.factors[col].warnings, and each metric verdict's warnings, marked with expected=True; only their stderr echoes stop. Unknown codes are rejected so a typo cannot silently suppress nothing.


Usability Tiers

inspect_data partitions public metrics into three distinct groups based on the inspected data shape and the metric's declarative sample_threshold:

  • Usable: The metric is fully applicable and the data shape satisfies all warning thresholds (warn_*). This is the safest set to run out-of-the-box.
  • Degraded: The metric is applicable but runs with a warning because the sample size is borderline (falls between the hard min_* floor and the soft warn_* threshold).
  • Unusable: The metric cannot be run on this data, either because of a cell mismatch or because a hard sample floor (min_*) is violated.

Scalar-input helpers such as breakeven_cost and net_spread are also listed as unusable for panel data. They consume already computed scalar values (quantile_spread.value, notional_turnover.value) rather than a panel, so run the upstream diagnostics first and call the helper directly.


Resolved floor for a configured metric

The tiers above read each metric's default-configuration sample_threshold (as do list_metrics / metrics_summary). The floor a run gates on can differ: the configuration changes it (ic(inference=NEWEY_WEST) needs 20 periods, the default non-overlapping t-test 50), and stride-scaled floors follow the panel's overlap_periods (positive_rate() needs 10 periods at overlap_periods=1, 50 at 5). sample_requirements resolves the floor for an instance at a horizon — the same resolution evaluate and the slice_period_* tests apply — so a coverage audit (regime slices, IS/OOS splits) can be planned against the number the run will actually use. evaluate(strict=True) raises on a hard min_* breach; strict=False short-circuits the metric to NaN with a metric_unavailable warning; the soft warn_* tier always returns a value and attaches the axis' degraded-tier warning code.

factrix.sample_requirements

sample_requirements(metric: Any, *, data: DataFrame | None = None, overlap_periods: int | None = None) -> SampleThreshold

Resolve a configured metric's sample floor at a panel's overlap.

The floor a metric gates on depends on its configuration (e.g. ic(inference=NeweyWest()) needs fewer periods than the default non-overlapping t-test) and, for metrics that sub-sample at the overlap stride, on the panel's overlap_periods. list_metrics / metrics_summary / inspect_data report the default-configuration floor; this is the run-time one — the same resolution :func:evaluate and the slice_period_* tests apply.

Parameters:

Name Type Description Default
metric Any

A metric instance (ic(), positive_rate(), …).

required
data DataFrame | None

Panel whose stamped overlap (set by :func:factrix.preprocess.compute_forward_return) resolves the floor. Resolved exactly as :func:evaluate does: an explicit overlap_periods must agree with the stamp; an unstamped panel requires it.

None
overlap_periods int | None

The evaluation-grid overlap, when no panel is at hand (or the panel is unstamped). Validated exactly as :func:evaluate validates a declared overlap — a positive int (bool rejected). With neither data nor overlap_periods the floor is resolved at the metric's default overlap — the same value metric.spec().sample_threshold carries.

None

Returns:

Type Description
SampleThreshold

class:SampleThreshold with the hard min_* and soft warn_*

SampleThreshold

floors per axis. evaluate(strict=True) raises on a hard-floor

SampleThreshold

breach; strict=False short-circuits the metric to NaN with a

SampleThreshold

metric_unavailable warning; the soft tier always returns a value

SampleThreshold

and attaches the axis' degraded-tier warning code.

Raises:

Type Description
UserInputError

metric is not a metric instance; overlap_periods is not a positive int; or data is given and its stamp disagrees with an explicit overlap_periods (unstamped, with no explicit value and no horizon stamp, it is required).

Examples:

>>> import factrix as fx
>>> from factrix.metrics import ic, positive_rate
>>> fx.sample_requirements(ic()).min_periods
50
>>> fx.sample_requirements(ic(inference=fx.inference.NEWEY_WEST)).min_periods
20
>>> fx.sample_requirements(positive_rate(), overlap_periods=1).min_periods
10
>>> raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
>>> panel = fx.preprocess.compute_forward_return(raw, forward_periods=5)
>>> fx.sample_requirements(positive_rate(), data=panel).min_periods
50

Scope detection and missing cells

FactorScope asks one question: does the factor take the same value across every asset at a given period (COMMON, a broadcast macro series) or vary across the cross-section (INDIVIDUAL)?

The question is answered from finite cells only. A null, NaN or ±inf factor cell is a missing observation, not a distinct factor value, so it cannot manufacture cross-sectional variation:

  • A broadcast factor with a gap on one asset stays COMMON.
  • A period left with a single finite cell is still compatible with COMMON — one observation cannot contradict a broadcast structure.
  • An asset with no row at all on a period is likewise not variation.
  • A period with no finite cell carries no evidence either way and is ignored. properties.scope_reason reports how many of the panel's periods the decision was read from, so an ignored period is visible rather than silent.

If every period is unidentifiable — the column has no finite cell anywhere — the broadcast property cannot be established at all. Routing then falls back to the unrestricted FactorScope.INDIVIDUAL (COMMON is its special case, and claiming it would assert a structure no observation supports) and a factor_scope_unidentifiable data-level warning is emitted. Every sample floor is violated at n_pairs = 0 in that state, so read the warning as a missing factor column rather than as a scope verdict.

evaluate dispatches each factor column through the same detector, so the pre-flight scope and the cell a run is routed to cannot disagree.


Result structure

inspect_data returns a DataInspection carrying the detected data properties (properties), the per-metric applicability verdicts (metrics, plus the usable / degraded / unusable partitions), and any data-level warnings. Each entry in the metrics group is a MetricApplicability.

The tier groups are MetricApplicabilityGroup objects: they expose .names and .to_metrics_dict(), and slicing or concatenating them with + preserves those helpers.


Multi-factor input: per-factor results

evaluate dispatches each factor column independently — one EvaluationResult per column, each routed to its own (scope, density, structure) cell. Inspection reports at the same granularity, so a preflight verdict maps one-to-one onto the evaluate output it predicts:

inspect_data(data, factor_cols=cols) evaluate(data, ..., factor_cols=cols)
info.factors[col].properties the cell results[col] was dispatched to
info.factors[col].usable / .degraded / .unusable which entries of results[col].metrics return a value, a degraded value, or a metric_unavailable NaN
info.factors[col].warnings the data-level warnings that column raises
info.factors[col].usable.to_metrics_dict() the metrics= argument to run that column safely
import factrix as fx
import polars as pl
from factrix.preprocess import compute_forward_return

raw = fx.datasets.make_cs_panel(n_assets=20, n_dates=120)
data = compute_forward_return(
    raw.with_columns(pl.col("factor").mean().over("date").alias("macro")),
    forward_periods=5,
)
info = fx.inspect_data(data, factor_cols=["factor", "macro"])

for col, f in info.factors.items():
    print(col, f.properties.scope.value, f.properties.density.value, f.usable.names)

# Run each column with the metrics its own verdict cleared. strict=False keeps
# a metric whose run-time floor binds tighter than the pre-flight one as a NaN
# placeholder with a reason, rather than raising for the whole batch.
for col, f in info.factors.items():
    results = fx.evaluate(
        data,
        metrics=f.usable.to_metrics_dict(),
        factor_cols=[col],
        forward_periods=5,
        strict=False,
    )

Two granularities travel together on purpose. A screen over dozens of candidate columns still wants one answer to "what is this panel", so properties, metrics and the tier partitions remain a concise aggregate and describe the first inspected column — exactly factors[<first column>]. Single-factor callers therefore never touch the mapping; multi-factor callers never have to guess which column an aggregate refers to.

When columns disagree on FactorScope or FactorDensity, the aggregate warnings carry a cross_factor_*_mismatch naming the basis column, the columns that disagree with it, and the value each one carries. A later column's own advisories (low cardinality, frequent events, scope unidentifiable, sample shape) live on factors[col].warnings rather than being merged into one stream where the column they belong to would be lost.

One panel-level caveat: n_periods and n_assets are properties of the data, so every FactorInspection reports the panel's counts rather than the periods that column happens to cover. Where a column's own coverage gates a metric it does so through the stage-one profile, which is per column — a column whose IC cross-sections survive on 20 periods is blocked at ic's 50-period floor while its sibling in the same panel is not. The same rule applies to time-series-first common betas: their n_assets floors use only the per-asset regressions that survive compute_common_betas' complete-pair, minimum-history, and factor-variation filters, rather than the raw panel universe. A metric whose run-time gate is data content rather than sample shape is mirrored the same way: common_quantile_spread is reported unusable when the per-period factor history carries fewer than n_groups * 2 distinct values, the comparison it short-circuits insufficient_factor_variation on.

factrix.DataInspection dataclass

DataInspection(properties: DataProperties, metrics: list[MetricApplicability], warnings: list[Warning] = list(), factors: dict[str, FactorInspection] = dict())

Bases: _MetricPartitionView

Result of :func:inspect_data.

Pure data — no execution methods.

Two granularities travel together. :attr:factors carries one :class:FactorInspection per inspected column — the granularity evaluate dispatches at. :attr:properties / :attr:metrics and the :attr:usable / :attr:degraded / :attr:unusable partitions are the concise aggregate view and describe the first inspected column, which is the whole panel when there is only one; they are exactly factors[<first column>], so a single-factor caller never has to reach into the mapping.

Attributes:

Name Type Description
properties DataProperties

:class:DataProperties with typed enum axes, the per-axis rationale strings, and shape numerics — for the first inspected column.

metrics list[MetricApplicability]

Flat list[MetricApplicability] — one verdict per visibility=PUBLIC spec the inspector considered, for the first inspected column. Single source of truth for the aggregate view; the :attr:usable / :attr:degraded / :attr:unusable properties expose it as a mutually exclusive partition.

warnings list[Warning]

Data-level sample-shape diagnostics (NW HAC SE unreliable, cross-asset df low). source=None on every entry because these are data-level, not per-metric. Per-metric degraded warnings live inside each :class:MetricApplicability. Carries the first column's warnings plus the cross-factor mismatch warnings, which name the disagreeing columns; a later column's own advisories live on factors[col].warnings.

factors dict[str, FactorInspection]

{column name: FactorInspection} in inspected order — per-column scope, density, counts, stage-one profiles, warnings and metric verdicts. Always populated, including for single-factor input.

to_dict

to_dict() -> dict[str, Any]

JSON-friendly nested dict view.

Layout (top-level keys, stable order):

  • properties: {scope, density, structure, n_assets, n_periods, n_pairs, sparse_ratio} — enum fields rendered as their .value string; sparse_ratio NaN emitted as None.
  • reasoning: {scope, density, structure}.
  • metrics: list of per-spec dicts {name, cell, usable, warnings, blockers} — same row shape suits pl.from_dicts for cross-data audit.
  • warnings: data-level [{code, source, message, expected}, ...].
  • factors: {column name: FactorInspection.to_dict()} in inspected order — the same four keys per column, plus factor. factors[<first column>] restates the four keys above.

Mirrors :meth:factrix.EvaluationResult.to_dict shape — single to_dict convention across the public result-type group so log / parquet sinks treat them uniformly.


factrix.FactorInspection dataclass

FactorInspection(factor: str, properties: DataProperties, metrics: list[MetricApplicability], warnings: list[Warning] = list())

Bases: _MetricPartitionView

Pre-flight result for one factor column.

evaluate dispatches every factor column independently, so inspection reports at the same granularity: one of these per inspected column, keyed by column name on :attr:DataInspection.factors. Every axis, count, stage-one profile, advisory warning and metric verdict here is computed from :attr:factor alone — nothing is inherited from a sibling column.

Attributes:

Name Type Description
factor str

The inspected column's name.

properties DataProperties

:class:DataProperties detected on this column. The panel-level fields (structure / n_assets / n_periods) are properties of the data and are therefore shared with every sibling; the factor-level fields (scope / density / n_pairs / n_events / sparse_ratio) are this column's.

metrics list[MetricApplicability]

One :class:MetricApplicability per public spec, judged against this column. Partitioned by :attr:usable / :attr:degraded / :attr:unusable.

warnings list[Warning]

Data-level diagnostics raised by this column (sample shape, scope unidentifiable, low-cardinality / frequent-event advisories, single-asset event data).

to_dict

to_dict() -> dict[str, Any]

JSON-friendly view: {factor, properties, reasoning, metrics, warnings}.

Same row shape as :meth:DataInspection.to_dict minus factors, plus the column name, so a multi-factor audit flattens straight into pl.from_dicts.


factrix.DataProperties dataclass

DataProperties(scope: FactorScope, scope_reason: str, density: FactorDensity, density_reason: str, structure: DataStructure, structure_reason: str, n_assets: int, n_periods: int, n_pairs: int, n_events: int, sparse_ratio: float)

Inspected data properties driving cell dispatch.

Carries the dispatch axes (scope / density / structure as typed enums), the human-readable rationale for each axis decision (scope_reason / density_reason / structure_reason), and the data-shape numerics the user typically wants next to them (n_assets / n_periods / n_pairs / sparse_ratio). Named Properties rather than Axes because the numeric fields are not axes — they are observations supporting the axis decisions.

Each *_reason sits next to the enum it explains so detection verdict and rationale travel together as one value.

Attributes:

Name Type Description
scope FactorScope

Detected :class:FactorScope. Read from finite factor cells only: null / NaN / ±inf are missing observations, not distinct factor values, so a gap in a broadcast factor cannot manufacture cross-sectional variation. A period with no finite cell is ignored; when no period has one the axis is unidentifiable and falls back to INDIVIDUAL under a FACTOR_SCOPE_UNIDENTIFIABLE warning.

scope_reason str

Human-readable rationale for scope, including how many of the panel's periods the decision was actually read from.

density FactorDensity

Detected :class:FactorDensity.

density_reason str

Human-readable rationale for density.

structure DataStructure

Detected :class:DataStructure — TIMESERIES iff n_assets == 1 (single-asset data), PANEL otherwise.

structure_reason str

Human-readable rationale for structure.

n_assets int

Unique asset_id count in the panel.

n_periods int

Unique date count in the panel.

n_pairs int

Finite (date, asset_id) factor observation count — the upper bound on usable sample size for any pair-counting metric (IC, rank-IC, FM cross-section). Equals data.height for a dense panel; smaller when the factor column has non-finite values.

n_events int

Finite, non-zero factor observation count — the event sample size for event-driven metrics (CAAR, MFE/MAE, corrado-rank, event-quality). Finite AND non-zero cells, matching those metrics' factor != 0 event filter. For a dense continuous factor this is ~n_pairs (the event axis only gates SPARSE-cell metrics). caar counts event periods rather than rows, so its pre-flight reads this as a loose upper bound; its in-body short-circuit on event periods stays authoritative.

sparse_ratio float

Zero-ratio in the factor column (denominator is the finite cell count). math.nan when none is finite.


factrix.MetricApplicabilityGroup

Bases: list['MetricApplicability']

A tier partition of :class:MetricApplicability verdicts.

A list subclass — every list operation works — with discovery helpers layered on. Returned by :attr:DataInspection.usable / degraded / unusable.

names property

names: list[str]

The metric name of each verdict, in order.

to_metrics_dict

to_metrics_dict() -> dict[str, MetricBase]

Normalise the group into {name: metric_instance} for evaluate.

The canonical discovery bridge: pick a tier (usually usable) and feed its default-constructed instances straight to :func:factrix.evaluate::

info = fx.inspect_data(data)
results = fx.evaluate(data, metrics=info.usable.to_metrics_dict(),
                      factor_cols=[...], forward_periods=5)

Scalar-input utilities are kept out of usable and therefore out of this bridge: compute their upstream values first, then call the helper directly.


factrix.MetricApplicability dataclass

MetricApplicability(metric: type[MetricBase], name: str, spec: MetricSpec, usable: bool, warnings: list[Warning] = list(), blockers: list[str] = list())

Per-metric pre-flight verdict against one data's properties.

Attributes:

Name Type Description
metric type[MetricBase]

The :class:~factrix.metrics._base.MetricBase subclass this verdict is about — the callable a caller would instantiate to run it. Lets consumers reach the class (and its compute / params) without going through :attr:spec.

name str

The metric's registry name (metric.__name__ == spec.name), surfaced directly so callers can key on it without reaching through :attr:spec.

spec MetricSpec

The :class:MetricSpec being evaluated.

usable bool

True iff the metric passes cell match, every min_* floor on its :class:SampleThreshold, and any content gate that runtime can pre-flight. warn_* violations and content states that preserve a point estimate do NOT flip usable to False — they attach a degraded :class:Warning to :attr:warnings.

warnings list[Warning]

warn_*-tier and content diagnostics (the metric will run but its inference is degraded). Empty when no advisory applies.

blockers list[str]

Concrete reasons the metric is unusable (cell mismatch, min_* floor violation, or a content gate that prevents a point estimate). Empty when usable is True.