Skip to content

factrix.metrics.event_quality

Per-event quality descriptive statistics for event signals.

All metrics operate on the signed_car (return x sign(factor)) of individual events. They describe the quality and shape of per-event outcomes — distinct from significance testing (caar.py) and path analysis (mfe_mae.py).

Metrics

event_hit_rate — fraction of correct-direction events (binomial test) event_ic — signal strength → return correlation (Spearman) signal_density — average time gap between events profit_factor — sum(gains) / sum(losses) event_skewness — skewness of signed_car distribution

Notes

Pipeline. Per-event scalar (hit / information coefficient (IC) / skew / density) computed on signed_car, then cross-event aggregation; binomial inference for hit rate, nonparametric for IC / skewness, descriptive elsewhere.

factrix.metrics.event_quality.event_hit_rate

event_hit_rate(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricOutput

Fraction of events where signed abnormal return > 0.

Parameters:

Name Type Description Default
df DataFrame

Panel with event signal and forward return.

required

Returns:

Type Description
MetricOutput

MetricOutput with value=hit_rate, stat=z from binomial test.

Notes

hits = sum_i 1{signed_car_i > 0}, rate = hits / N. Two-sided binomial test against H0: p = 0.5: exact below _BINOMIAL_EXACT_CUTOFF, normal-approximation z above (z = (hits - N/2) / (sqrt(N) / 2)).

factrix publishes stat consistent with the test branch (raw hit count for the exact path, z for the normal path) so an exact-binomial p is never paired with a Gaussian z label.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_quality import event_hit_rate
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = event_hit_rate(panel)
>>> result.name
'event_hit_rate'

factrix.metrics.event_quality.event_ic

event_ic(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricOutput

Signal strength → directional return correlation among events.

Spearman correlation between |factor| and signed_car (return × sign(factor)), computed only on event rows.

Unlike standard information coefficient (IC) (full cross-section per date), this measures whether signal magnitude predicts return magnitude among triggered events. Direction is already accounted for via sign().

Only meaningful when signal values have magnitude variance (not all ±1). Profile auto-skips when variance is absent.

Parameters:

Name Type Description Default
df DataFrame

Panel with event signal and forward return.

required

Returns:

Type Description
MetricOutput

MetricOutput with value=Spearman rho, stat=z from Fisher transform.

Notes

rho = Spearman(|factor|, signed_car) over event rows; Fisher z-transform z = atanh(rho) * sqrt(N - 3) against H0: rho = 0. Direction is already absorbed into signed_car so this isolates the magnitude-of-signal → magnitude-of-return link.

factrix short-circuits "not_applicable_discrete_signal" when |factor| lacks variance (e.g. {0, ±1} events): event-IC is undefined without magnitude variation, distinct from "too few events".

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_quality import event_ic
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = event_ic(panel)
>>> result.name
'event_ic'

factrix.metrics.event_quality.profit_factor

profit_factor(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricOutput

sum(positive signed_car) / sum(negative signed_car).

Per-event aggregate — no strategy assumptions. A profit factor > 1 means gross gains exceed gross losses across all events.

Parameters:

Name Type Description Default
df DataFrame

Panel with event signal and forward return.

required

Returns:

Type Description
MetricOutput

MetricOutput with value=profit_factor.

Notes

PF = sum(signed_car > 0) / |sum(signed_car < 0)|. Descriptive only; no formal H0 (the ratio's sampling distribution lacks a clean closed-form null without distributional assumptions). PF > 1 means gross gains exceed gross losses across all events; the metric ignores per-event variance.

factrix returns 0.0 rather than infinity when total losses are below EPSILON so downstream aggregators do not propagate non-finite floats.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_quality import profit_factor
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = profit_factor(panel)
>>> result.name
'profit_factor'

factrix.metrics.event_quality.event_skewness

event_skewness(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricOutput

Skewness of signed_car distribution.

Positive skew = occasional large gains, frequent small losses (desirable for event strategies). Uses scipy's Fisher skewness (bias-corrected).

Also tests H₀: skewness = 0 via D'Agostino's skew test.

Parameters:

Name Type Description Default
df DataFrame

Panel with event signal and forward return.

required

Returns:

Type Description
MetricOutput

MetricOutput with value=skewness, stat=z from D'Agostino test.

Notes

skew = m_3 / m_2^(3/2) (Fisher, bias-corrected via scipy.stats.skew(bias=False)); D'Agostino skew test gives z with H0: skew = 0 when N >= 20. Below 20 events, the test is not produced (stat=None) but the descriptive skewness is still returned.

factrix gates the inference branch at N >= 20 because the D'Agostino-Pearson normal approximation degrades sharply on small samples; reporting an unreliable z would invite false-positive significance.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_quality import event_skewness
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = event_skewness(panel)
>>> result.name
'event_skewness'

factrix.metrics.event_quality.signal_density

signal_density(df: DataFrame, *, factor_col: str = 'factor') -> MetricOutput

Average bars per event (inverse frequency).

Answers: "how frequently does this signal fire?"

Computed per-asset as total_bars / n_events (inverse event frequency), then averaged across assets. This is not the mean of actual inter-event gaps: bars-per-event depends only on counts, so clustered events and evenly-spaced events yield the same value. See clustering_diagnostic for event-date concentration.

Low density (large gaps) means the signal is selective; high density (small gaps) means the signal fires often — capacity is higher but independence may be weaker.

Parameters:

Name Type Description Default
df DataFrame

Panel with date, asset_id, factor.

required

Returns:

Type Description
MetricOutput

MetricOutput with value = mean bars-per-event across assets.

Notes

Per asset i: bars_per_event_i = total_bars_i / n_events_i; the headline is the cross-asset mean of this ratio. This is an inverse-frequency measure, not the mean of inter-event gaps: clustered and evenly-spaced events at the same total count map to the same value.

factrix exposes clustering_diagnostic for event-date concentration; pair the two when independence assumptions matter.

Examples:

>>> import factrix as fx
>>> from factrix.metrics.event_quality import signal_density
>>> panel = fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0)
>>> result = signal_density(panel)
>>> result.name
'signal_density'

Event-study contracts

These metrics use the sign-only form \(\text{signed\_car} = \text{forward\_return} \times \text{sign}(\text{factor})\) — distinct from caar's magnitude-weighted \(\text{forward\_return} \times \text{factor}\). See the abnormal-return table for the full per-metric contract and the confounded-event note for how the binomial / Spearman nulls behave under within-asset event clustering.

Use cases

  • Directional accuracy


    Fraction of events whose signed_car is positive, with a two-sided binomial test against \(H_0: p = 0.5\). Exact branch below the normal-approximation cutoff; \(z\) branch above. Headline statistic for "is the sign right more often than chance".

  • Magnitude → magnitude


    Among triggered events, does the signal's |factor| co-move with the realised signed_car? Spearman rank correlation with Fisher-\(z\) inference. Auto-skips on \(\{0, \pm 1\}\) inputs where |factor| has no variance.

  • Gain / loss ratio and shape


    profit_factor reports \(\sum\text{gains} / |\sum\text{losses}|\) as a descriptive gross ratio; event_skewness reports the Fisher- corrected skewness of the signed_car distribution with a D'Agostino test when \(N \geq 20\). Useful for screening fat-right-tail vs symmetric event payoffs.

  • Firing frequency


    signal_density reports mean bars-per-event per asset (inverse frequency). Pair with clustering_diagnostic when independence assumptions matter — bars-per-event ignores temporal clustering.

Choosing a function

Goal Function
Directional-accuracy binomial test event_hit_rate
Magnitude-of-signal → magnitude-of-return rank correlation event_ic
Gross gain / loss ratio (descriptive only) profit_factor
Tail asymmetry of signed_car with D'Agostino skew test event_skewness
Inverse firing frequency (bars per event) signal_density

Worked example — directional accuracy + tail shape

event_hit_rate + event_skewness on a synthetic event panel

import factrix as fx
from factrix.metrics.event_quality import (
    event_hit_rate, event_skewness, profit_factor,
)
from factrix.preprocess import compute_forward_return

raw   = fx.datasets.make_event_panel(
    n_assets=200, n_dates=500, event_rate=0.02,
    post_event_drift=0.004, seed=2024,
)
panel = compute_forward_return(raw, forward_periods=5)

hit = event_hit_rate(panel)
print(hit.value, hit.stat, hit.metadata["p_value"])
# 0.564  3.81  1.4e-04   (approximate)

sk = event_skewness(panel)
print(sk.value, sk.stat)
# 0.42  4.10   (approximate; stat is D'Agostino z when N >= 20)

pf = profit_factor(panel)
print(pf.value, pf.metadata["n_wins"], pf.metadata["n_losses"])
# 1.34  1131  864

See also