Skip to content

Errors

How to read factrix errors and which exception class to catch.

TL;DR

Illustrative
import factrix as fx
from factrix.metrics import ic

try:
    results = fx.evaluate(
        data, 
        metrics={"ic": ic(inference=fx.inference.NEWEY_WEST)}, 
        factor_cols=["factor"]
    )
except fx.UserInputError as exc:
    # User typed the wrong thing — typo, unknown name, wrong column.
    # The message carries a fuzzy suggestion + a docs link.
    print(exc)
except fx.IncompatibleAxisError as exc:
    # Axis miswire.
    ...
except fx.InsufficientSampleError as exc:
    # A requested metric's effective sample is below its own hard floor.
    # exc.axis names the BINDING axis ("periods" / "assets" / "events" / ...),
    # exc.actual / exc.required carry the counts, and exc.shortfalls lists
    # one (label, reason, axis, actual, required) tuple per failing metric.
    print(f"{exc.axis}: had {exc.actual}, needed {exc.required}")
except fx.FactrixError as exc:
    # Catch-all for anything else factrix raises.
    ...

Every input mistake factrix rejects at an entry point — evaluate, a direct metric call, by_slice and the slice tests, the datasets builders — raises a FactrixError subclass, and the same mistake raises the same class whichever entry point you used: ic(ic_df, overlap_periods=0) and evaluate(..., overlap_periods=0) both raise UserInputError, n_groups=1 raises it from every bucketing metric, and an unvetted inference= raises IncompatibleInferenceError whether the sample-floor pre-flight or the metric body sees it first.

Metric knobs fail at construction

A metric's knobs are validated when the metric object is built, not when it runs. quantile_spread(n_groups=1) raises immediately; so does quantile_spread(panel, n_groups=1), because the direct-call form builds the same object first. Both raise the same class with the same .field, and a battery configured up front fails on the bad knob rather than three metrics into the evaluate:

Illustrative
metrics = {
    "ic": ic(),
    "spread": quantile_spread(n_groups=1),   # raises here, not in evaluate()
}

The three rules are the metric's Literal-annotated knobs (closed sets), its inference= allowlist, and its declared numeric bounds — see Custom metrics for @metric(validate=...). The panel's overlap_periods is not a knob: it is injected from the data at dispatch and validated there.

except fx.FactrixError does not yet block every library-raised failure. What still surfaces as a builtin:

Still builtin Where Why
TypeError An argument of the wrong Python type (by_slice(rows, ...) on a non-frame, a metric class passed where an instance is required, a positional metric knob) Wrong type is a TypeError by Python convention; wrong value is a UserInputError.
ValueError Library invariants outside the metric-knob / factrix.stats / result-exporter contract: factrix.metrics.register() re-registering a name, the metric-index declaration checks, the numeric kernels in factrix._stats, and the adapt / preprocess.orthogonalize schema guards Developer- or invariant-facing — a mis-declared metric, or a non-finite array reaching a kernel. Every metric knob, factrix.stats argument and to_frame / to_dict collision is a UserInputError.

UserInputError multi-inherits from ValueError, so except ValueError catches both it and everything in the table.

Exception hierarchy

FactrixError                       # base
├── IncompatibleAxisError          # (scope, density, metric) is not a legal cell
├── IncompatibleInferenceError     # inference= outside the metric's applicable-inference allowlist
├── InsufficientSampleError        # a metric's effective sample is below its own SampleThreshold floor
├── UserInputError                 # named-set typo / type mismatch / dataset schema error
└── CycleError                     # MetricSpec.requires declares a dependency cycle
Exception When you see it What it carries
IncompatibleAxisError (scope, density, metric) is not a legal cell —
IncompatibleInferenceError inference= outside the metric's applicable_inference allowlist .func_name, .value, .applicable
InsufficientSampleError Under strict=True, a requested metric short-circuited on a data shortage (an insufficient_* reason) .axis, .actual, .required, .shortfalls
UserInputError Unknown metric, column not in data, wrong type structured .field, .value, .candidates, .suggestions, .expected, .docs_url
CycleError A custom metric's MetricSpec.requires forms a dependency cycle —

Error → fix mapping

Concrete messages, what triggers them, and where to look for the fix.

Data-schema failures

Message hint Trigger Fix
factor_cols 'X' not in data columns Typo or wrong column name Check data.columns; pass the actual name to factor_cols=. See Data schema.
forward_return column missing Forgot the preprocess step compute_forward_return(raw, forward_periods=h) before evaluate. See Preparing data.
<metric>(): unknown factor_col='X' Typo in a column named on a direct metric call (factor_col / return_col / weight_col / factor_cols) Read .candidates / .suggestions — they are the frame's own columns. A mis-named column is a caller error, not a thin sample: it never returns a NaN "insufficient data" result.

Structural and sample failures

Exception / message Trigger Fix
IncompatibleAxisError: (scope, density, metric) is not a legal cell Combination that the dispatch table never registers Use compatible axes. Check list_metrics or inspect_data to find applicable metrics.
InsufficientSampleError: N metric(s) below their sample floor A requested metric's effective sample is below its own hard floor Read .axis first — the binding axis is not always periods. On periods, extend the window. On assets, either widen the universe or reconfigure the metric (monotonicity(n_groups=3), k_spread(k=2) for a small universe). strict=False returns NaN placeholders instead.

User-input failures (UserInputError)

Every UserInputError carries structured attributes (see Reading a UserInputError). Common triggers and fix paths:

Message hint Trigger Fix
unknown metrics='...' Typo or metric not applicable to the data inspect_data(data).usable enumerates the metrics applicable to the data shape. See list_metrics for the full catalog.
invalid expand_over=[...] One or more expand_over keys missing on some results' params The message lists every (factor, missing_key) pair in one pass. All results in the family must carry the key in .params; populate it consistently, or drop the key from expand_over. A key found on .metadata instead is called out separately — bookkeeping never partitions a family.
Expected: list[EvaluationResult], got ... Passing the wrong artifact type to a screening function Screening (bhy, partial_conjunction, bhy_hierarchical) consumes list[EvaluationResult].
unknown weight_by='...' / unknown tie_policy='...' Typo in a closed-set metric knob Every closed-set knob (weight_by, tie_policy, direction, center, method) is validated before any computation and the message lists its legal values. A typo raises rather than falling through to the default branch.
invalid q_top=..., Expected: a fraction strictly inside (0, 1) A fraction knob outside its bounds top_concentration(q_top=) needs a genuine sub-fraction of the cross-section: 0 leaves no top bucket, 1 selects all of it.

Reading a UserInputError

Every user-facing raise that takes a named input renders the same three-part message:

bhy(): unknown expand_over='univere_id'
  Did you mean: "universe_id"?
  Available: ['regime_id', 'sector', 'universe_id']
  Docs: https://awwesomeman.github.io/factrix/api/bhy#factrix.multi_factor.bhy
Line What to look at
<func_name>(): unknown <field>=<value> Which kwarg / column triggered the raise, and what value was received.
Did you mean: "..." Top-3 fuzzy candidates (omitted when nothing matches above the cutoff).
Available: [...] The full legal set.
Docs: https://... The function's deployed-docs anchor.

For type / shape mismatches, the second line reads Expected: <shape> instead of Did you mean: ....

Programmatic recovery

The structured attributes are the contract — read them, do not parse the rendered message:

Illustrative
import factrix as fx

bad: dict[str, object] = {}
for factor_col in candidates:
    try:
        results = fx.evaluate(data, metrics=metrics, factor_cols=[factor_col])
    except fx.UserInputError as exc:
        bad[exc.field] = exc.value
        # exc.suggestions carries top-3 fuzzy matches when applicable
Attribute Meaning
func_name The calling function (e.g. "bhy", "evaluate").
field The kwarg / column name that failed validation.
value The value the caller passed in.
candidates Sorted tuple of legal names (named-set branch); () otherwise.
suggestions difflib top-3 matches against candidates; () when none.
expected Human-readable shape (mismatch branch); None otherwise.
docs_url Resolved deployed-docs URL for the function.

Class reference

Autodoc anchors for cross-references of the form [FactrixError][factrix.FactrixError] from any docs page.

Base

factrix.FactrixError

Bases: Exception

Base for all factrix-raised errors.

User-input failures

factrix.UserInputError

UserInputError(*, func_name: str, field: str, value: object, candidates: Iterable[object] | None = None, expected: str | None = None, docs_path: str)

Bases: FactrixError, ValueError

User-supplied input does not match expected names or types.

Raised for typos in named-set kwargs (metric / estimator / params key / column name) or input-type mismatches. Multi-inherits from :class:ValueError so ecosystem code (pytest.raises(ValueError), generic except ValueError) keeps working.

Structured attributes carry the diagnostic so callers (sub-issue raises, LLM agents) do not parse the rendered message:

  • func_name: the calling function name (no parens)
  • field: the kwarg / column name that failed validation
  • value: the value the caller passed in
  • candidates: tuple of legal names (named-set branch); empty otherwise
  • suggestions: difflib top-3 fuzzy matches against candidates
  • expected: human-readable shape (type-mismatch branch); None otherwise
  • docs_url: deployed-docs URL for the function

Structural and sample failures

factrix.IncompatibleAxisError

Bases: FactrixError

(scope, density, metric) tuple is not a legal analysis cell.

Covers e.g. density=SPARSE paired with metric=IC, or (INDIVIDUAL, DENSE) with metric=None.

factrix.IncompatibleInferenceError

IncompatibleInferenceError(*, func_name: str, value: object, applicable: Iterable[str])

Bases: FactrixError

inference= is not in the metric's applicable-inference allowlist.

Each metric that exposes inference= declares an applicable_inference frozenset of the methods it actually dispatches. Passing anything outside it — a valid Inference the metric does not vet (e.g. HansenHodrick to ic) or a non- Inference object — raises here instead of silently running an unintended test or falling back to the default.

Structured attributes carry the diagnostic so callers do not parse the rendered message:

  • func_name: the calling metric name (no parens)
  • value: the value the caller passed as inference
  • applicable: tuple of allowed inference names for that metric

factrix.InsufficientSampleError

InsufficientSampleError(message: str, *, axis: str, actual: int | None, required: int | None, shortfalls: Iterable[tuple[str, str, str, int | None, int | None]] = ())

Bases: FactrixError

A requested metric's sample fell below its own hard floor, under strict=True.

Raised by :func:factrix.evaluate (and everything layered on it) when a metric that fits the data short-circuited on a data shortage — an insufficient_* reason. Schema / argument / configuration failures stay :class:UserInputError; the split exists so "your window is too short" and "you passed a bad column name" are not the same except clause.

The floor is per metric and per axis, not one global T. It is checked against the effective sample the estimator would use — the post-stride count for a non-overlapping test, the surviving cross-section for a bucketed one — not the raw row or date count. The same floor is what :func:factrix.inspect_data reports at pre-flight for the metric's default configuration, so pre-flight and run-time never disagree.

Structured attributes carry the diagnostic so callers do not parse the rendered message:

  • axis: the :data:~factrix._types.SampleAxis token the shortfall was measured on ("periods" / "assets" / "events" / "pairs" / "asset_pairs") — the binding axis, which is not always periods: a 10-bucket metric on an 8-name universe fails on "assets" however long the panel.
  • actual: the effective sample the metric had on that axis, or None when the producer reported no count.
  • required: the floor it needed, or None when the producer declared no numeric floor.
  • shortfalls: one (label, reason, axis, actual, required) tuple per failing metric, in request order — a battery can fail on more than one.

Custom-metric wiring failures

factrix.CycleError

Bases: FactrixError

Raised when MetricSpec.requires declares a dependency cycle.

A :class:FactrixError, so except factrix.FactrixError catches it alongside every other library-raised failure.