Errors
How to read factrix errors and which exception class to catch.
TL;DR¶
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:
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:
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 validationvalue: the value the caller passed incandidates: tuple of legal names (named-set branch); empty otherwisesuggestions: difflib top-3 fuzzy matches againstcandidatesexpected: human-readable shape (type-mismatch branch);Noneotherwisedocs_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 ¶
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 asinferenceapplicable: 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.SampleAxistoken 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, orNonewhen the producer reported no count.required: the floor it needed, orNonewhen 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.