ECOD - Empirical Cumulative Distribution based Outlier Detection.

Classes

ECODDetector

class algorithms.anomaly.ecod.ECODDetector(Classifier)

ECOD detects outliers from per-dimension empirical tail probabilities.

ECOD is parameter-free: it has nothing to tune, no distance metric, no neighbourhood size, no kernel. It asks one question per dimension — how far into the tail does this value sit? — and adds the surprise up across dimensions. Despite that simplicity it is at or near the top of large outlier-detection benchmarks, and it is the rare detector that can say which feature made a point look anomalous.
Constructor
__init__(
    self,
    contamination: float = 0.1,
)

Overview

  1. For each dimension, build the empirical CDF of the training values.
  2. For a point, read off its left-tail probability
\hat{F}_j(x_j) and right-tail probability 1 - \hat{F}_j(x_j^-).
  1. Convert each to a surprise, -\log(\text{probability}), and sum
across dimensions.
  1. Score the point by the largest of three aggregates: left-tail only,
right-tail only, and a skewness-guided choice that picks the tail each dimension is actually skewed towards.

Theory

The left and right tail probabilities of dimension j are estimated as

\hat{F}_j^{-}(x) = \frac{1}{n} \sum_{i=1}^{n} \mathbb{1}\{X_{ij} \leq x\}, \quad \hat{F}_j^{+}(x) = \frac{1}{n} \sum_{i=1}^{n} \mathbb{1}\{X_{ij} \geq x\}

and the three aggregate scores are

O^{-}(x) = -\sum_j \log \hat{F}_j^{-}(x_j), \quad O^{+}(x) = -\sum_j \log \hat{F}_j^{+}(x_j), \quad O^{a}(x) = -\sum_j \log \hat{F}_j^{s_j}(x_j)

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
float = 0.1
Expected proportion of outliers. Sets the decision threshold; it does not affect the scores themselves.

Attributes

X_train_
np.ndarray of shape (n_samples, n_features)
Training data retained to evaluate the empirical CDF at predict time.
skewness_
np.ndarray of shape (n_features,)
Per-dimension adjusted Fisher-Pearson skewness, which selects the tail used by the skewness-guided aggregate.
threshold_
float
Decision-function value separating inliers from outliers.
n_features_in_
int
Number of features seen during 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

Li2022
Li, Z., Zhao, Y., Hu, X., Botta, N., Ionescu, C., & Chen, G. H. (2022). ECOD: Unsupervised Outlier Detection Using Empirical Cumulative Distribution Functions. IEEE Transactions on Knowledge and Data Engineering, 35(12), 12181-12193. :doi:`10.1109/TKDE.2022.3159580`
python
>>> 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:

python
>>> contributions = detector.feature_contributions(X[-1:])
>>> contributions.shape
(1, 3)

Methods

get_parameter_schema (cls) -> Dict[str, Dict[str, Any]]

Return JSON Schema for algorithm parameters.

get_capabilities (cls) -> List[str]

Return supported capabilities.

get_complexity (cls) -> str

Return complexity analysis.

get_references (cls) -> List[str]

Return academic citations.

fit (self, X: np.ndarray, _y: Optional[np.ndarray]=None) -> 'ECODDetector'

Fit the ECOD detector.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Training data. Labels are ignored; the method is unsupervised.
_y
np.ndarray
Ignored, present for API consistency.
Returns
self
ECODDetector
The fitted detector.
decision_function (self, X: np.ndarray) -> np.ndarray

Compute anomaly scores for samples.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Input samples.
Returns
scores
np.ndarray of shape (n_samples,)
Anomaly scores. Lower scores indicate anomalies.
predict (self, X: np.ndarray) -> np.ndarray

Predict if samples are anomalies or not.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Input samples.
Returns
predictions
np.ndarray of shape (n_samples,)
-1 for anomalies, 1 for normal instances.
score_samples (self, X: np.ndarray) -> np.ndarray

Alias for decision_function for compatibility.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Input samples.
Returns
scores
np.ndarray of shape (n_samples,)
Anomaly scores. Lower scores indicate anomalies.
feature_contributions (self, X: np.ndarray) -> np.ndarray

Return each feature's contribution to a sample's outlier score.

Parameters
X
np.ndarray of shape (n_samples, n_features)
Input samples.
Returns
contributions
np.ndarray of shape (n_samples, n_features)
Per-dimension :math:`-\log` tail probability, on the tail chosen by that dimension's skewness. Row sums equal the skewness-guided aggregate score.
__repr__ (self) -> str

Return a readable representation of the detector.