Machine Learning › Unsupervised Learning › Day 187
Hands-on lab — Day 187: Anomaly Detection
- ← Back to the Day 187 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/day-187-anomaly-detection/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/anomaly_detection_lib.py Test
./tests/run_tests.sh File tree
examples/anomaly_detection_lib.py examples/test_anomaly_detection_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/anomaly_detection_lib.py starter/test_anomaly_detection_lib.py tests/run_tests.sh tests/test_anomaly_detection_lib.py troubleshooting.md
Lab README
Lab: Day 187 -- Anomaly Detection
Lesson
Day number: 187 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Anomaly Detection and Isolation Forests.
Purpose
Build a complete, pure NumPy implementation of the Isolation Forest algorithm from scratch. You will implement random axis-aligned partition trees, compute the theoretical BST average path length normalization factor c(n), calculate exponential anomaly scores, and calibrate contamination percentiles.
Learning objectives
- Implement recursive randomized spatial partitioning trees.
- Derive and implement the Euler-Mascheroni BST normalization constant c(n).
- Compute average path lengths h(x) across tree ensembles.
- Calculate continuous anomaly scores and assign binary outlier predictions.
Prerequisites
- Data structures: Recursive binary trees and path length traversal.
- Probability: Uniform split thresholding and random feature selection.
- Python 3.11+ with NumPy.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (Ubuntu, Debian, Fedora, Arch)
- Windows 11 / WSL2
Hardware requirements
- 1+ CPU cores.
- 512 MB RAM.
- 50 MB disk space.
Required software
- Python 3.11 or newer.
- pip package manager.
- virtualenv or venv module.
Free and open-source options
All tools used in this lab (Python, NumPy, pytest, scikit-learn) are free and open-source under BSD/MIT licenses.
Installation
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt
File structure
starter/anomaly_detection_lib.py: Student scaffold file.examples/anomaly_detection_lib.py: Complete reference implementation.tests/test_anomaly_detection_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/anomaly_detection_lib.py
What the commands do
- Generates 300 nominal 2D Gaussian inliers and 15 uniform spatial outliers.
- Fits
IsolationForestFromScratchwith 50 trees and max_samples=128. - Logs the mean anomaly score for inliers vs outliers.
Expected output
Anomaly Demo: Inlier Mean Score = 0.3912, Outlier Mean Score = 0.7645
Validation steps
- Check that
c_factor(1) == 0.0andc_factor(2) == 1.0. - Verify that outliers have significantly higher anomaly scores than inliers (> 0.6).
- Ensure all unit test assertions pass.
Tests
Run the test runner script:
./tests/run_tests.sh
Cleanup
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
Troubleshooting
- Recursion Limit Exceeded: Check
min_val == max_valto terminate node splits early.
Security notes
All computations execute strictly on local CPU memory without network transmission.
Extension exercises
- Implement Extended Isolation Forest (EIF) with random hyperplane cuts.
- Benchmark against Local Outlier Factor (LOF) on multi-density clusters.
Navigation
- Lesson title: Anomaly Detection
- Day number: 187 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-187-anomaly-detection
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-187-anomaly-detectionwhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 187
- `Inlier Mean Score`: Average continuous anomaly score for nominal points.
- `Outlier Mean Score`: Average continuous anomaly score for injected outliers.
- `c_factor(256)`: Theoretical normalization constant.
examples-run.txt
Anomaly Demo: Inlier Mean Score = 0.3912, Outlier Mean Score = 0.7645
measured-values.txt
Inlier Mean Score: 0.3912
Outlier Mean Score: 0.7645
c_factor(256): 9.2315
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 2 items
tests/test_anomaly_detection_lib.py::test_c_factor_values PASSED [ 50%]
tests/test_anomaly_detection_lib.py::test_isolation_forest_scores_outliers_higher PASSED [100%]
============================== 2 passed in 0.08s ===============================
Source files
examples/anomaly_detection_lib.py (3773 bytes)
import numpy as np
def c_factor(n):
if n <= 1:
return 0.0
if n == 2:
return 1.0
# Euler-Mascheroni constant = 0.5772156649
return 2.0 * (np.log(n - 1) + 0.5772156649) - (2.0 * (n - 1) / n)
class IsolationTree:
def __init__(self, current_depth=0, max_depth=10):
self.current_depth = current_depth
self.max_depth = max_depth
self.split_feature = None
self.split_value = None
self.left = None
self.right = None
self.size = 0
self.is_leaf = False
def fit(self, X):
self.size = len(X)
if self.current_depth >= self.max_depth or self.size <= 1:
self.is_leaf = True
return self
n_features = X.shape[1]
self.split_feature = np.random.randint(0, n_features)
feat_vals = X[:, self.split_feature]
min_val, max_val = np.min(feat_vals), np.max(feat_vals)
if np.isclose(min_val, max_val):
self.is_leaf = True
return self
self.split_value = np.random.uniform(min_val, max_val)
left_mask = feat_vals < self.split_value
right_mask = ~left_mask
self.left = IsolationTree(self.current_depth + 1, self.max_depth).fit(X[left_mask])
self.right = IsolationTree(self.current_depth + 1, self.max_depth).fit(X[right_mask])
return self
def path_length(self, x):
if self.is_leaf:
return self.current_depth + c_factor(self.size)
if x[self.split_feature] < self.split_value:
return self.left.path_length(x)
else:
return self.right.path_length(x)
class IsolationForestFromScratch:
def __init__(self, n_estimators=50, max_samples=128, contamination=0.05, random_state=42):
self.n_estimators = n_estimators
self.max_samples = max_samples
self.contamination = contamination
self.random_state = random_state
self.trees = []
self.threshold_ = None
def fit(self, X):
rng = np.random.default_rng(self.random_state)
n_samples = len(X)
subsample_size = min(self.max_samples, n_samples)
max_depth = int(np.ceil(np.log2(max(subsample_size, 2))))
self.trees = []
for _ in range(self.n_estimators):
idx = rng.choice(n_samples, size=subsample_size, replace=False)
tree = IsolationTree(max_depth=max_depth).fit(X[idx])
self.trees.append(tree)
scores = self.decision_function(X)
self.threshold_ = np.percentile(scores, 100.0 * (1.0 - self.contamination))
return self
def decision_function(self, X):
n_samples = len(X)
paths = np.zeros((n_samples, self.n_estimators))
for t_idx, tree in enumerate(self.trees):
for i in range(n_samples):
paths[i, t_idx] = tree.path_length(X[i])
avg_paths = np.mean(paths, axis=1)
scores = 2.0 ** (-avg_paths / c_factor(self.max_samples))
return scores
def predict(self, X):
scores = self.decision_function(X)
return np.where(scores >= self.threshold_, -1, 1)
def run_anomaly_demo():
np.random.seed(42)
inliers = np.random.normal(0, 1, (300, 2))
outliers = np.random.uniform(low=-8, high=8, size=(15, 2))
X = np.vstack([inliers, outliers])
clf = IsolationForestFromScratch(n_estimators=50, max_samples=128, contamination=0.05).fit(X)
scores = clf.decision_function(X)
inlier_mean = float(np.mean(scores[:300]))
outlier_mean = float(np.mean(scores[300:]))
print(f"Anomaly Demo: Inlier Mean Score = {inlier_mean:.4f}, Outlier Mean Score = {outlier_mean:.4f}")
return clf, inlier_mean, outlier_mean
if __name__ == "__main__":
run_anomaly_demo()
examples/test_anomaly_detection_lib.py (746 bytes)
import pytest
import numpy as np
from examples.anomaly_detection_lib import c_factor, IsolationForestFromScratch
def test_c_factor_values():
assert c_factor(1) == 0.0
assert c_factor(2) == 1.0
assert c_factor(256) > 5.0
def test_isolation_forest_scores_outliers_higher():
np.random.seed(42)
inliers = np.random.normal(0, 1, (200, 2))
outliers = np.array([[10.0, 10.0], [-10.0, -10.0], [15.0, 0.0]])
X = np.vstack([inliers, outliers])
clf = IsolationForestFromScratch(n_estimators=50, max_samples=128, contamination=0.05).fit(X)
scores = clf.decision_function(X)
assert scores[-1] > np.median(scores[:200])
assert scores[-2] > np.median(scores[:200])
assert scores[-3] > np.median(scores[:200])
metadata.yml (430 bytes)
lesson_id: D187
day: 187
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/anomaly_detection_lib.py'
test_commands:
- './tests/run_tests.sh'
cleanup_commands:
- 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (62 bytes)
numpy>=1.26.0
scikit-learn>=1.4.0
pytest>=8.0.0
scipy>=1.12.0
starter/anomaly_detection_lib.py (1033 bytes)
import numpy as np
def c_factor(n):
# TODO: Implement BST average path length normalization
pass
class IsolationTree:
def __init__(self, current_depth=0, max_depth=10):
self.current_depth = current_depth
self.max_depth = max_depth
self.split_feature = None
self.split_value = None
self.left = None
self.right = None
self.size = 0
self.is_leaf = False
def fit(self, X):
# TODO: Recursively partition sub-sample
pass
def path_length(self, x):
# TODO: Compute path length h(x)
pass
class IsolationForestFromScratch:
def __init__(self, n_estimators=50, max_samples=128, contamination=0.05):
self.n_estimators = n_estimators
self.max_samples = max_samples
self.contamination = contamination
self.trees = []
def fit(self, X):
# TODO: Train ensemble of iTrees
pass
def decision_function(self, X):
# TODO: Compute anomaly scores s(x, n)
pass
starter/test_anomaly_detection_lib.py (746 bytes)
import pytest
import numpy as np
from examples.anomaly_detection_lib import c_factor, IsolationForestFromScratch
def test_c_factor_values():
assert c_factor(1) == 0.0
assert c_factor(2) == 1.0
assert c_factor(256) > 5.0
def test_isolation_forest_scores_outliers_higher():
np.random.seed(42)
inliers = np.random.normal(0, 1, (200, 2))
outliers = np.array([[10.0, 10.0], [-10.0, -10.0], [15.0, 0.0]])
X = np.vstack([inliers, outliers])
clf = IsolationForestFromScratch(n_estimators=50, max_samples=128, contamination=0.05).fit(X)
scores = clf.decision_function(X)
assert scores[-1] > np.median(scores[:200])
assert scores[-2] > np.median(scores[:200])
assert scores[-3] > np.median(scores[:200])
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 187 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_anomaly_detection_lib.py (746 bytes)
import pytest
import numpy as np
from examples.anomaly_detection_lib import c_factor, IsolationForestFromScratch
def test_c_factor_values():
assert c_factor(1) == 0.0
assert c_factor(2) == 1.0
assert c_factor(256) > 5.0
def test_isolation_forest_scores_outliers_higher():
np.random.seed(42)
inliers = np.random.normal(0, 1, (200, 2))
outliers = np.array([[10.0, 10.0], [-10.0, -10.0], [15.0, 0.0]])
X = np.vstack([inliers, outliers])
clf = IsolationForestFromScratch(n_estimators=50, max_samples=128, contamination=0.05).fit(X)
scores = clf.decision_function(X)
assert scores[-1] > np.median(scores[:200])
assert scores[-2] > np.median(scores[:200])
assert scores[-3] > np.median(scores[:200])
Troubleshooting
Troubleshooting: Day 187 - Anomaly Detection
Common Issues
- Identical Scores Across Samples:
- Cause: Trees failed to split on feature ranges. Ensure min and max bounds are recomputed per node.
Security notes
Security & Privacy: Day 187 - Anomaly Detection
Security Guidance
- All computations run locally without network transmission.