Machine Learning › Unsupervised Learning › Day 184
Hands-on lab — Day 184: Hierarchical Clustering and DBSCAN
- ← Back to the Day 184 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-184-hierarchical-clustering-and-dbscan/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/hierarchical_clustering_and_dbscan_lib.py Test
./tests/run_tests.sh File tree
examples/hierarchical_clustering_and_dbscan_lib.py examples/test_hierarchical_clustering_and_dbscan_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/hierarchical_clustering_and_dbscan_lib.py starter/test_hierarchical_clustering_and_dbscan_lib.py tests/run_tests.sh tests/test_hierarchical_clustering_and_dbscan_lib.py troubleshooting.md
Lab README
Lab: Day 184 -- Hierarchical Clustering and DBSCAN
Lesson
Day number: 184 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Hierarchical Clustering and DBSCAN.
Purpose
Build a complete, pure NumPy implementation of the DBSCAN density clustering algorithm from scratch. You will implement epsilon-neighborhood range queries, core point identification, breadth-first density cluster expansion, and noise classification.
Learning objectives
- Implement epsilon-radius Euclidean neighborhood queries.
- Identify core, border, and noise observations.
- Expand density clusters using iterative queue traversal.
- Benchmark clustering performance on non-spherical datasets with noise.
Prerequisites
- Linear algebra: Pairwise Euclidean distance matrices.
- Data structures: Queue-based breadth-first traversal.
- 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/hierarchical_clustering_and_dbscan_lib.py: Student scaffold file.examples/hierarchical_clustering_and_dbscan_lib.py: Complete reference implementation.tests/test_hierarchical_clustering_and_dbscan_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/hierarchical_clustering_and_dbscan_lib.py
What the commands do
- Generates two Gaussian clusters with 100 points plus 3 isolated outlier noise points.
- Runs
DBSCANFromScratchwith epsilon=0.8 and MinPts=5. - Logs the number of discovered clusters and verified noise points.
Expected output
DBSCAN Demo: Discovered 2 clusters with 3 noise points.
Validation steps
- Verify that all points in core neighborhoods are assigned to matching cluster IDs.
- Verify that isolated outliers receive label
-1. - 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
- All Points Noise: Epsilon radius is too small. Check scale of features.
- Single Giant Cluster: Epsilon radius is too large, causing clusters to merge across noise.
Security notes
All computations run strictly on local CPU memory without network transmission.
Extension exercises
- Implement KD-Tree spatial indexing to optimize neighborhood range queries from O(N^2) to O(N log N).
- Implement Ward hierarchical clustering dendrogram tree building.
Navigation
- Lesson title: Hierarchical Clustering and DBSCAN
- Day number: 184 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-184-hierarchical-clustering-and-dbscan
- 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-184-hierarchical-clustering-and-dbscanwhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 184
- `Discovered Clusters`: Integer count of non-noise clusters.
- `Noise Points`: Integer count of points assigned label -1.
- `Labels`: Array of cluster indices of shape (N,).
examples-run.txt
DBSCAN Demo: Discovered 2 clusters with 3 noise points.
measured-values.txt
Discovered Clusters: 2
Noise Points: 3
Total Samples: 103
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 2 items
tests/test_hierarchical_clustering_and_dbscan_lib.py::test_dbscan_cluster_discovery PASSED [ 50%]
tests/test_hierarchical_clustering_and_dbscan_lib.py::test_dbscan_noise_filtering PASSED [100%]
============================== 2 passed in 0.08s ===============================
Source files
examples/hierarchical_clustering_and_dbscan_lib.py (1889 bytes)
import numpy as np
class DBSCANFromScratch:
def __init__(self, eps=0.5, min_samples=5):
self.eps = eps
self.min_samples = min_samples
self.labels_ = None
def fit(self, X):
n_samples = len(X)
self.labels_ = np.full(n_samples, -1)
cluster_id = 0
dists = np.linalg.norm(X[:, np.newaxis, :] - X[np.newaxis, :, :], axis=2)
for i in range(n_samples):
if self.labels_[i] != -1:
continue
neighbors = np.where(dists[i] <= self.eps)[0]
if len(neighbors) < self.min_samples:
continue
self.labels_[i] = cluster_id
queue = list(neighbors[neighbors != i])
while queue:
current_point = queue.pop(0)
if self.labels_[current_point] == -1:
self.labels_[current_point] = cluster_id
curr_neighbors = np.where(dists[current_point] <= self.eps)[0]
if len(curr_neighbors) >= self.min_samples:
for n in curr_neighbors:
if self.labels_[n] == -1:
self.labels_[n] = cluster_id
queue.append(n)
cluster_id += 1
return self
def run_dbscan_demo():
np.random.seed(42)
c1 = np.random.normal(loc=[-3.0, 0.0], scale=0.3, size=(50, 2))
c2 = np.random.normal(loc=[3.0, 0.0], scale=0.3, size=(50, 2))
noise = np.array([[10.0, 10.0], [-10.0, 10.0], [0.0, -10.0]])
X = np.vstack([c1, c2, noise])
db = DBSCANFromScratch(eps=0.8, min_samples=5).fit(X)
n_clusters = len(set(db.labels_) - {-1})
n_noise = int(np.sum(db.labels_ == -1))
print(f"DBSCAN Demo: Discovered {n_clusters} clusters with {n_noise} noise points.")
return db, n_clusters, n_noise
if __name__ == "__main__":
run_dbscan_demo()
examples/test_hierarchical_clustering_and_dbscan_lib.py (879 bytes)
import pytest
import numpy as np
from examples.hierarchical_clustering_and_dbscan_lib import DBSCANFromScratch
def test_dbscan_cluster_discovery():
np.random.seed(42)
c1 = np.random.normal(loc=[-4.0, 0.0], scale=0.2, size=(40, 2))
c2 = np.random.normal(loc=[4.0, 0.0], scale=0.2, size=(40, 2))
X = np.vstack([c1, c2])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
unique_labels = set(db.labels_) - {-1}
assert len(unique_labels) == 2
assert db.labels_[0] != db.labels_[45]
def test_dbscan_noise_filtering():
np.random.seed(42)
c1 = np.random.normal(loc=[0.0, 0.0], scale=0.2, size=(30, 2))
noise = np.array([[15.0, 15.0], [-15.0, -15.0]])
X = np.vstack([c1, noise])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
assert db.labels_[-1] == -1
assert db.labels_[-2] == -1
assert db.labels_[0] != -1
metadata.yml (447 bytes)
lesson_id: D184
day: 184
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/hierarchical_clustering_and_dbscan_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/hierarchical_clustering_and_dbscan_lib.py (295 bytes)
import numpy as np
class DBSCANFromScratch:
def __init__(self, eps=0.5, min_samples=5):
self.eps = eps
self.min_samples = min_samples
self.labels_ = None
def fit(self, X):
# TODO: Implement DBSCAN density reachability and cluster expansion
pass
starter/test_hierarchical_clustering_and_dbscan_lib.py (879 bytes)
import pytest
import numpy as np
from examples.hierarchical_clustering_and_dbscan_lib import DBSCANFromScratch
def test_dbscan_cluster_discovery():
np.random.seed(42)
c1 = np.random.normal(loc=[-4.0, 0.0], scale=0.2, size=(40, 2))
c2 = np.random.normal(loc=[4.0, 0.0], scale=0.2, size=(40, 2))
X = np.vstack([c1, c2])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
unique_labels = set(db.labels_) - {-1}
assert len(unique_labels) == 2
assert db.labels_[0] != db.labels_[45]
def test_dbscan_noise_filtering():
np.random.seed(42)
c1 = np.random.normal(loc=[0.0, 0.0], scale=0.2, size=(30, 2))
noise = np.array([[15.0, 15.0], [-15.0, -15.0]])
X = np.vstack([c1, noise])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
assert db.labels_[-1] == -1
assert db.labels_[-2] == -1
assert db.labels_[0] != -1
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 184 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_hierarchical_clustering_and_dbscan_lib.py (879 bytes)
import pytest
import numpy as np
from examples.hierarchical_clustering_and_dbscan_lib import DBSCANFromScratch
def test_dbscan_cluster_discovery():
np.random.seed(42)
c1 = np.random.normal(loc=[-4.0, 0.0], scale=0.2, size=(40, 2))
c2 = np.random.normal(loc=[4.0, 0.0], scale=0.2, size=(40, 2))
X = np.vstack([c1, c2])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
unique_labels = set(db.labels_) - {-1}
assert len(unique_labels) == 2
assert db.labels_[0] != db.labels_[45]
def test_dbscan_noise_filtering():
np.random.seed(42)
c1 = np.random.normal(loc=[0.0, 0.0], scale=0.2, size=(30, 2))
noise = np.array([[15.0, 15.0], [-15.0, -15.0]])
X = np.vstack([c1, noise])
db = DBSCANFromScratch(eps=0.8, min_samples=4).fit(X)
assert db.labels_[-1] == -1
assert db.labels_[-2] == -1
assert db.labels_[0] != -1
Troubleshooting
Troubleshooting: Day 184 - Hierarchical Clustering and DBSCAN
Common Issues
- Queue Infinite Loop:
- Ensure points already added to cluster labels are not re-enqueued redundantly.
Security notes
Security & Privacy: Day 184 - Hierarchical Clustering and DBSCAN
Security Guidance
- All clustering calculations execute locally without network transmission.