Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions doc/source/tutorials/notebooks/7_outlier_detection.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Anomaly Detection with the Local Outlier Factor\n",
"\n",
"The **Local Outlier Factor (LOF)** is a density-based anomaly detection algorithm that quantifies how isolated each data point is relative to its local neighborhood.\n",
"Instead of applying a single global distance threshold, LOF estimates the **local density** around every sample and compares it with the densities of its nearest neighbors.\n",
"Points located in regions that are substantially less dense than those of their neighbors are considered potential outliers.\n",
"\n",
"\n",
"This localized approach makes LOF particularly effective for datasets with **non-uniform density**, where clusters vary in compactness or spread.\n",
"The algorithm produces a **LOF score** for each sample, which can be interpreted as follows:\n",
"\n",
"\n",
"- **LOF < 1:** The sample lies in a denser region than its neighbors (potentially part of a very compact cluster).\n",
"- **LOF ≈ 1:** The sample has a density comparable to its neighbors and is considered *normal*.\n",
"- **LOF >> 1:** The sample resides in a region of much lower density and is likely an *outlier*.\n",
"\n",
"\n",
"In this way, LOF captures *relative density deviations* rather than assuming a single global notion of normality, enabling robust detection of local anomalies in complex datasets.\n",
"\n",
"\n",
"This notebook shows how to:\n",
"1. Generate a small synthetic dataset with normal points and outliers.\n",
"2. Apply Heat’s distributed LOF implementation.\n",
"3. Visualize and interpret the resulting anomaly scores."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import heat as ht\n",
"import numpy as np\n",
"import torch\n",
"from heat.classification.localoutlierfactor import LocalOutlierFactor\n",
"from mpi4py import MPI\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# Uncomment the following line to enable GPU usage if necessary\n",
"# ht.use_device(\"gpu\")\n",
"\n",
"print(f\"Running on device: {ht.get_device()}\")\n",
"print(f\"MPI world size: {ht.MPI_WORLD.size}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### About the setup\n",
"\n",
"The code below initializes all required modules:\n",
"- **Heat (ht)** for distributed array operations\n",
"- **NumPy** for basic data generation\n",
"- **Matplotlib** for visualization\n",
"\n",
"> 💡 You can uncomment `ht.use_device(\"gpu\")` to run this example on a GPU if available."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Generate synthetic data\n",
"\n",
"To illustrate the LOF algorithm, we will create a small two-dimensional dataset.\n",
"It consists of two dense clusters of points and a few uniformly distributed **outliers**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def generate_test_data(random_seed:int = 42, points: int = 100, outliers: int = 20):\n",
" np.random.seed(random_seed)\n",
"\n",
" # Generate two Gaussian clusters\n",
" X_inliers = 0.3 * np.random.randn(points, 2)\n",
" X_inliers = np.r_[X_inliers + 2, X_inliers - 2]\n",
"\n",
" # Add uniformly distributed outliers\n",
" X_outliers = np.random.uniform(low=-4, high=4, size=(outliers, 2))\n",
" X = np.r_[X_outliers, X_inliers]\n",
"\n",
" # Convert to Heat tensor (distributed array)\n",
" ht_X = ht.array(X, split=0)\n",
" return ht_X\n",
"\n",
"ht_data = generate_test_data()\n",
"data = ht_data.numpy()\n",
"print(f\"Generated dataset with shape: {ht_data.shape}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> 🔍 The dataset contains 220 samples in total: 200 inliers forming two clusters and 20 outliers scattered randomly."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Compute Local Outlier Factor\n",
"\n",
"We now fit the LOF model to the generated data.\n",
"\n",
"The current implementation allows for adjusting two parameters:\n",
"- **`n_neighbors`**: number of neighboring points used to estimate local density.\n",
"- **`threshold`**: points with a higher LOF score thean thus cut-off value are labeled as anomalies.\n",
"\n",
"The Heat implementation distributes both the neighbor search and LOF computation across all available MPI processes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"lof = LocalOutlierFactor(n_neighbors=10, threshold=2)\n",
"lof.fit(ht_data)\n",
"\n",
"lof_scores = lof.lof_scores.numpy()\n",
"anomaly = lof.anomaly.numpy()\n",
"print(f\"Computed LOF scores. Number of detected outliers: {np.sum(np.where(anomaly==-1, 0, 1))}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"After fitting, each point receives a **LOF score**:\n",
"- Values around **1** correspond to normal points (similar local density).\n",
"- Values significantly larger than **1** indicate potential outliers.\n",
"\n",
"The `threshold` parameter determines which points are finally classified as anomalies."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Visualize LOF scores\n",
"\n",
"The plot below shows the data points colored by their LOF scores.\n",
"- The color intensity represents how “different” a point’s local density is compared to its neighbors.\n",
"- Points outlined with a black circle are flagged as **outliers**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_lof(data: np.array, lof_scores: np.array, anomaly: np.array): \n",
" plt.rc(\"font\", family=\"serif\")\n",
" plt.figure(figsize=(10, 6))\n",
" scatter = plt.scatter(\n",
" data[:, 0], data[:, 1], c=lof_scores, cmap=\"coolwarm\", edgecolors=\"k\", s=60, alpha=0.8\n",
" )\n",
"\n",
" outlier_indices = np.where(anomaly == 1)[0]\n",
"\n",
" plt.scatter(\n",
" data[outlier_indices, 0],\n",
" data[outlier_indices, 1],\n",
" facecolors=\"none\",\n",
" edgecolors=\"black\",\n",
" s=120,\n",
" linewidths=2,\n",
" label=\"Outliers\",\n",
" )\n",
"\n",
" plt.colorbar(scatter, label=\"LOF Score\")\n",
" plt.xlabel(\"Feature 1\")\n",
" plt.ylabel(\"Feature 2\")\n",
" plt.title(\"Local Outlier Factor (LOF) - Anomaly Detection\")\n",
" plt.legend()\n",
" if ht.MPI_WORLD.rank == 0:\n",
" plt.show()\n",
"\n",
"plot_lof(data, lof_scores, anomaly)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> 💡 Interpretation: Points with darker (red) colors have higher LOF scores and are more likely to be outliers. The black-circled points are those identified as anomalies according to the chosen threshold."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "heatenv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}