Common mistakes and best practices for DBSCAN
DBSCAN finds connected dense regions under one metric, one neighborhood radius, and one density threshold. Its output is therefore inseparable from those choices.
1. Guessing eps in unscaled units
eps is a distance in the fitted feature space. If features use incompatible units, large-range features dominate. Scale only after deciding what equal distance should mean, and fit that transformation on the reference/training population for deployment.
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
X, _ = make_moons(n_samples=300, noise=0.06, random_state=42)
X_scaled = StandardScaler().fit_transform(X)
labels = DBSCAN(eps=0.30, min_samples=5).fit_predict(X_scaled)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
assert n_clusters == 2
A sorted k-nearest-neighbor distance plot can suggest candidate radii, but its elbow is subjective and should be followed by sensitivity analysis.
import matplotlib.pyplot as plt
from sklearn.neighbors import NearestNeighbors
min_samples = 5
neighbors = NearestNeighbors(n_neighbors=min_samples).fit(X_scaled)
distances, _ = neighbors.kneighbors(X_scaled)
k_distances = np.sort(distances[:, -1])
fig, ax = plt.subplots()
ax.plot(k_distances)
ax.axhline(0.30, color="red", linestyle="--", label="candidate eps")
ax.set(title="Sorted 5-neighbor distances", xlabel="ordered observation", ylabel="distance")
ax.legend()
fig.tight_layout()
plt.close(fig)
2. Treating min_samples as a cosmetic option
The observation itself counts toward min_samples. Larger values demand denser, more robust cores; smaller values admit fragile chains. Dimensionality-based rules are starting points, not guarantees, because sample size, duplication, noise, and metric all affect neighborhood counts.
cluster_counts = {}
for min_samples in (3, 5, 10):
candidate = DBSCAN(eps=0.30, min_samples=min_samples).fit_predict(X_scaled)
cluster_counts[min_samples] = len(set(candidate)) - (1 if -1 in candidate else 0)
assert all(count >= 1 for count in cluster_counts.values())
3. Calling every -1 label an anomaly
-1 means the point was not density-reachable under the chosen configuration. It may be a valid sparse observation, a boundary case, a sensor error, or a true anomaly. Investigate it with domain evidence instead of discarding it automatically. Border-point assignment can also depend on traversal when a border point is reachable from multiple clusters; core structure is the more stable object.
4. Ignoring varying density and high dimension
One global eps may merge a dense cluster before it recovers a sparse one. Compare OPTICS or HDBSCAN for varying densities. In high dimensions, distance concentration can undermine neighborhoods; PCA may help when a linear lower-dimensional representation is substantively defensible, but t-SNE coordinates distort distances and are usually unsafe as a default clustering space.
5. Misusing internal metrics
Silhouette is defined only when there are at least two labels. If noise is included, it is treated as another cluster. For a cluster-shape diagnostic, state explicitly that noise is excluded; report the noise fraction separately.
from sklearn.metrics import silhouette_score
non_noise = labels != -1
non_noise_labels = labels[non_noise]
assert np.unique(non_noise_labels).size == 2
silhouette = silhouette_score(X_scaled[non_noise], non_noise_labels)
noise_fraction = np.mean(~non_noise)
assert 0.20 < silhouette < 1.0
assert 0.0 <= noise_fraction < 0.20
Silhouette still prefers compact groups and is not a universal judge of density-connected shapes. Add parameter stability, resampling, domain review, and external labels when legitimate.
6. Assuming scaling is automatically favorable
Scikit-learn's DBSCAN bulk-computes neighborhoods and can approach quadratic memory when eps is large and min_samples is low. Favorable spatial indexing depends on metric, dimension, and data. For large data, consider sparse radius-neighbor graphs, duplicate compression with sample_weight, or OPTICS; HDBSCAN addresses varying density but is not simply a universally faster DBSCAN.
7. Visualize core, border, and noise points
model = DBSCAN(eps=0.30, min_samples=5).fit(X_scaled)
core_mask = np.zeros(model.labels_.shape, dtype=bool)
core_mask[model.core_sample_indices_] = True
fig, ax = plt.subplots()
ax.scatter(X_scaled[~core_mask, 0], X_scaled[~core_mask, 1], c=model.labels_[~core_mask], cmap="viridis", s=18)
ax.scatter(X_scaled[core_mask, 0], X_scaled[core_mask, 1], c=model.labels_[core_mask], cmap="viridis", s=48, edgecolor="black")
ax.set(title="DBSCAN core points (outlined), border points, and noise")
fig.tight_layout()
plt.close(fig)
Conclusion
Record the metric, preprocessing, eps, min_samples, noise fraction, and stability range. DBSCAN can recover non-convex density-connected groups, but only when a single density scale is a reasonable model of the data.