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 |
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:
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_caris 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 realisedsigned_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_factorreports \(\sum\text{gains} / |\sum\text{losses}|\) as a descriptive gross ratio;event_skewnessreports the Fisher- corrected skewness of thesigned_cardistribution with a D'Agostino test when \(N \geq 20\). Useful for screening fat-right-tail vs symmetric event payoffs. -
Firing frequency
signal_densityreports mean bars-per-event per asset (inverse frequency). Pair withclustering_diagnosticwhen 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¶
-
caar/bmp_test
Mean-CAAR significance and BMP variance-robust \(z\) on the same event sample.
-
clustering_diagnostic
Event-date concentration index — read alongside
signal_densitywhen independence matters. -
by_slice
Per-slice event-quality summaries (regime / universe / sector).
-
Statistical methods
Binomial test branches, Fisher-\(z\) Spearman, D'Agostino skew test.
-
Metric applicability reference
Sample-size guards and
signed_carcontracts for the sign-only family. -
Individual × Sparse landing
Adjacent event-study metrics in the same cell.