ECOD - Empirical Cumulative Distribution based Outlier Detection.
Classes
ECOD detects outliers from per-dimension empirical tail probabilities.
__init__( self, contamination: float = 0.1, )
Overview
- For each dimension, build the empirical CDF of the training values.
- For a point, read off its left-tail probability
- Convert each to a surprise, -\log(\text{probability}), and sum
- Score the point by the largest of three aggregates: left-tail only,
Theory
The left and right tail probabilities of dimension j are estimated as
and the three aggregate scores are
where s_j follows the sign of the dimension's skewness \gamma_j: a left-skewed dimension is scored on its left tail, a right-skewed one on its right. The final score is \max(O^{-}, O^{+}, O^{a}).
Summing -\log probabilities is the independence assumption made explicit: it treats dimensions as independent, which is why ECOD is fast and dimension-scalable, and also why it cannot see an outlier that is only unusual in the joint distribution — a point at (tall, light) whose height and weight are each perfectly ordinary.
Parameters
contamination
Attributes
X_train_
skewness_
threshold_
n_features_in_
fit.
Notes
Complexity. Training is O(n d \log n) — one sort per dimension — and prediction is O(m d \log n) by binary search. Memory is O(n d) because the training matrix is retained. Both the sort and the search run in the shared C++ kernel tuiml._cpp_ext.stats.tail_probabilities.
When to use. ECOD is the right first thing to try on tabular data: nothing to tune, no scaling required — it is invariant to any monotone per-feature transform — and it scales to high dimension where distance-based detectors collapse. Use LocalOutlierFactorDetector or IsolationForestDetector instead when anomalies are defined by feature interactions rather than by extremeness in individual features.
References
See Also
>>> import numpy as np
>>> from tuiml.algorithms.anomaly import ECODDetector
>>> rng = np.random.default_rng(0)
>>> X = np.vstack([rng.normal(0, 1, (200, 3)), rng.normal(8, 1, (10, 3))])
>>> detector = ECODDetector(contamination=0.05).fit(X)
>>> predictions = detector.predict(X)
>>> int((predictions[-10:] == -1).sum()) # the injected outliers
10
The per-dimension contributions explain why a point was flagged:
>>> contributions = detector.feature_contributions(X[-1:])
>>> contributions.shape
(1, 3)
Methods
fit
(self, X: np.ndarray, _y: Optional[np.ndarray]=None) -> 'ECODDetector'
fit
(self, X: np.ndarray, _y: Optional[np.ndarray]=None) -> 'ECODDetector'
Fit the ECOD detector.
Parameters
X
_y
Returns
self
feature_contributions
(self, X: np.ndarray) -> np.ndarray
feature_contributions
(self, X: np.ndarray) -> np.ndarray
Return each feature's contribution to a sample's outlier score.
Parameters
X
Returns
contributions