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 — density 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(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricResult
Fraction of events with return in expected direction.
The static event floor (sample_threshold=SampleThreshold(min_events=MIN_EVENTS_HARD)) gates the hit-rate binomial test on the count of non-zero (event) observations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Panel with event density and forward return. |
required |
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult 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.
return_col must be sign-symmetric around zero — signed_car =
return_col * sign(factor_col), so an always-positive magnitude
target (realised volatility, turnover) collapses sign(signed_car)
to sign(factor_col) and silently turns the hit rate into a count
of positive-factor events, with no error raised. Use
:func:~factrix.metrics.ic.ic or
:func:~factrix.metrics.monotonicity.monotonicity for those targets
instead.
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 == ""
True
factrix.metrics.event_quality.event_ic ¶
event_ic(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricResult
Spearman correlation between factor value and realised forward return.
The static event floor (sample_threshold=SampleThreshold(min_events=MIN_EVENTS_HARD)) gates the rank correlation on the count of non-zero (event) observations.
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 density magnitude predicts return magnitude among triggered events. Direction is already accounted for via sign().
Only meaningful when density values have magnitude variance (not all ±1). The metric returns a not-applicable result when variance is absent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Panel with event density and forward return. |
required |
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult 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-density → 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 == ""
True
factrix.metrics.event_quality.profit_factor ¶
profit_factor(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricResult
Profit factor = sum(gains) / sum(|losses|) across events.
The static event floor (sample_threshold=SampleThreshold(min_events=MIN_EVENTS_HARD)) gates the ratio on the count of non-zero (event) observations.
Per-event aggregate — no strategy assumptions. A profit factor > 1 means gross gains exceed gross losses across all events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Panel with event density and forward return. |
required |
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with value=profit_factor. |
Notes
PF = sum(signed_car_i * 1{signed_car_i > 0}) /
|sum(signed_car_i * 1{signed_car_i < 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.
When gains are positive and losses are zero, factrix returns
inf: the gross gain/loss ratio is unbounded, not zero. When
both gains and losses are zero, the ratio is undefined and the
metric returns NaN with metadata["profit_factor_status"].
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 == ""
True
factrix.metrics.event_quality.event_skewness ¶
event_skewness(data: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricResult
Skewness of signed event return distribution.
The static event floor (sample_threshold=SampleThreshold(min_events=MIN_EVENTS_HARD)) gates the descriptive skewness on the count of non-zero (event) observations.
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 |
|---|---|---|---|
data
|
DataFrame
|
Panel with event density and forward return. |
required |
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult 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_events >= 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_events >= 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 == ""
True
factrix.metrics.event_quality.signal_density ¶
signal_density(data: DataFrame, *, factor_col: str = 'factor') -> MetricResult
Average bars per event (inverse frequency).
No event-axis sample_threshold is declared (sample_threshold=SampleThreshold()) because the in-body floor is a degeneracy guard (>= 2 events overall, then >= 2 per asset) needed to define the per-asset bars-per-event ratio, not the statistical MIN_EVENTS_HARD floor the other event metrics gate on; like other math-degeneracy guards it stays in the body.
Answers: "how frequently does this density 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_hhi for event-date concentration.
Low density (large gaps) means the density is selective; high density (small gaps) means the density fires often — capacity is higher but independence may be weaker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Panel with |
required |
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult 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_hhi 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. If gains are positive and losses are zero, the ratio is unbounded (value = inf,profit_factor_status = "unbounded_no_losses"); if both are zero, the ratio is undefined (value = NaN).event_skewnessreports the Fisher- corrected skewness of thesigned_cardistribution with a D'Agostino test whenn_events >= 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_hhiwhen 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,
event_magnitude_jitter=0.5, post_event_drift_bps=40.0, seed=2024,
)
panel = compute_forward_return(raw, forward_periods=5)
hit = event_hit_rate(panel)
print(hit.value, hit.stat, hit.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_events >= 20)
pf = profit_factor(panel)
print(pf.value, pf.metadata["n_wins"], pf.metadata["n_losses"])
# 1.34 1131 864
See also¶
-
caar/bmp_z
Mean-CAAR significance and BMP variance-robust \(z\) on the same event sample.
-
clustering_hhi
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.