Rare observations can distort monitoring, fraud review, and data-quality workflows even when no anomaly labels exist. An Isolation Forest learns which numeric rows are easier to isolate from the rest and can flag those rows without a labeled target column.
Every training and incoming row must use the same ordered numeric features. Categorical fields need numeric encoding and missing measurements need imputation before fitting so the detector receives a consistent feature shape.
The contamination value sets the expected outlier share used to choose the decision threshold, while random_state makes the randomized tree splits reproducible. predict() returns 1 for an inlier and -1 for an outlier; a negative decision_function() value places a row on the outlier side of the threshold.
import numpy as np from sklearn.ensemble import IsolationForest training_names = np.array( [ "baseline-01", "baseline-02", "baseline-03", "baseline-04", "baseline-05", "baseline-06", "spike-01", ] ) training_rows = np.array( [ [10.0, 200.0], [10.4, 198.0], [9.8, 202.0], [10.2, 201.0], [10.1, 199.0], [10.5, 203.0], [42.0, 410.0], ] )
The first six rows form a tight baseline, while spike-01 supplies a known anomaly for the training smoke test.
evaluation_names = np.array(["incoming-normal", "incoming-spike"]) evaluation_rows = np.array( [ [10.3, 200.5], [35.0, 390.0], ] )
def print_results(title, names, labels, scores): print(title) for name, label, score in zip(names, labels, scores): state = "outlier" if label == -1 else "inlier" print(f"{name:15s} {state:7s} label={label:2d} score={score: .3f}")
detector = IsolationForest(contamination=0.15, random_state=42) detector.fit(training_rows)
A contamination value of 0.15 expects roughly one outlier among the seven controlled training rows. Production thresholds should reflect the share of rows that can receive anomaly review.
training_labels = detector.predict(training_rows) training_scores = detector.decision_function(training_rows) evaluation_labels = detector.predict(evaluation_rows) evaluation_scores = detector.decision_function(evaluation_rows)
print_results("Training rows", training_names, training_labels, training_scores) print() print_results( "Incoming rows", evaluation_names, evaluation_labels, evaluation_scores, )
assert training_labels[training_names == "spike-01"].item() == -1 assert evaluation_labels[evaluation_names == "incoming-spike"].item() == -1 assert evaluation_scores[evaluation_names == "incoming-spike"].item() < 0
$ python3 train_anomaly_detector.py Training rows baseline-01 inlier label= 1 score= 0.114 baseline-02 inlier label= 1 score= 0.052 baseline-03 inlier label= 1 score= 0.053 baseline-04 inlier label= 1 score= 0.123 baseline-05 inlier label= 1 score= 0.111 baseline-06 inlier label= 1 score= 0.031 spike-01 outlier label=-1 score=-0.280 Incoming rows incoming-normal inlier label= 1 score= 0.110 incoming-spike outlier label=-1 score=-0.206
The baseline-shaped incoming row remains an inlier, while both spike rows return -1 with negative decision scores. A failed assertion stops the script with a nonzero exit status.