statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
clustering.hpp
Go to the documentation of this file.
1
8#pragma once
9
10#include <algorithm>
11#include <cmath>
12#include <cstddef>
13#include <limits>
14#include <map>
15#include <numeric>
16#include <random>
17#include <stdexcept>
18#include <utility>
19#include <vector>
20
22
23namespace statcpp {
24
25// ============================================================================
26// Distance Functions
27// ============================================================================
28
39inline double euclidean_distance(const std::vector<double>& a, const std::vector<double>& b)
40{
41 if (a.size() != b.size()) {
42 throw std::invalid_argument("statcpp::euclidean_distance: dimension mismatch");
43 }
44
45 double sum = 0.0;
46 for (std::size_t i = 0; i < a.size(); ++i) {
47 double diff = a[i] - b[i];
48 sum += diff * diff;
49 }
50 return std::sqrt(sum);
51}
52
63inline double manhattan_distance(const std::vector<double>& a, const std::vector<double>& b)
64{
65 if (a.size() != b.size()) {
66 throw std::invalid_argument("statcpp::manhattan_distance: dimension mismatch");
67 }
68
69 double sum = 0.0;
70 for (std::size_t i = 0; i < a.size(); ++i) {
71 sum += std::abs(a[i] - b[i]);
72 }
73 return sum;
74}
75
76// ============================================================================
77// K-means Clustering
78// ============================================================================
79
84 std::vector<std::size_t> labels;
85 std::vector<std::vector<double>> centroids;
86 double inertia;
87 std::size_t n_iter;
88};
89
101inline std::vector<std::vector<double>> kmeans_plusplus_init(
102 const std::vector<std::vector<double>>& data,
103 std::size_t k)
104{
105 std::size_t n = data.size();
106
107 std::vector<std::vector<double>> centroids;
108 centroids.reserve(k);
109
110 auto& rng = get_random_engine();
111 std::uniform_int_distribution<std::size_t> init_dist(0, n - 1);
112
113 // Randomly select the first centroid
114 centroids.push_back(data[init_dist(rng)]);
115
116 // Probabilistically select remaining centroids
117 std::vector<double> distances(n);
118
119 for (std::size_t c = 1; c < k; ++c) {
120 double total_dist = 0.0;
121
122 for (std::size_t i = 0; i < n; ++i) {
123 double min_dist = std::numeric_limits<double>::max();
124 for (const auto& centroid : centroids) {
125 double dist = euclidean_distance(data[i], centroid);
126 min_dist = std::min(min_dist, dist);
127 }
128 distances[i] = min_dist * min_dist;
129 total_dist += distances[i];
130 }
131
132 // Fallback when all data points coincide with existing centroids
133 if (total_dist <= 0.0) {
134 std::uniform_int_distribution<std::size_t> rand_dist(0, n - 1);
135 centroids.push_back(data[rand_dist(rng)]);
136 continue;
137 }
138
139 // Probabilistically select the next centroid
140 std::uniform_real_distribution<double> prob_dist(0.0, total_dist);
141 double threshold = prob_dist(rng);
142 double cumsum = 0.0;
143
144 for (std::size_t i = 0; i < n; ++i) {
145 cumsum += distances[i];
146 if (cumsum >= threshold) {
147 centroids.push_back(data[i]);
148 break;
149 }
150 }
151 }
152
153 return centroids;
154}
155
169 const std::vector<std::vector<double>>& data,
170 std::size_t k,
171 std::size_t max_iter = 100,
172 double tol = 1e-6)
173{
174 if (data.empty()) {
175 throw std::invalid_argument("statcpp::kmeans: empty data");
176 }
177 if (k == 0) {
178 throw std::invalid_argument("statcpp::kmeans: k must be positive");
179 }
180 if (k > data.size()) {
181 throw std::invalid_argument("statcpp::kmeans: k exceeds number of data points");
182 }
183
184 std::size_t n = data.size();
185 std::size_t dim = data[0].size();
186
187 // K-means++ initialization
188 auto centroids = kmeans_plusplus_init(data, k);
189
190 std::vector<std::size_t> labels(n);
191 std::size_t iter = 0;
192
193 for (iter = 0; iter < max_iter; ++iter) {
194 // Assignment step
195 for (std::size_t i = 0; i < n; ++i) {
196 double min_dist = std::numeric_limits<double>::max();
197 std::size_t best_cluster = 0;
198
199 for (std::size_t c = 0; c < k; ++c) {
200 double dist = euclidean_distance(data[i], centroids[c]);
201 if (dist < min_dist) {
202 min_dist = dist;
203 best_cluster = c;
204 }
205 }
206 labels[i] = best_cluster;
207 }
208
209 // Update step
210 std::vector<std::vector<double>> new_centroids(k, std::vector<double>(dim, 0.0));
211 std::vector<std::size_t> cluster_sizes(k, 0);
212
213 for (std::size_t i = 0; i < n; ++i) {
214 std::size_t c = labels[i];
215 cluster_sizes[c]++;
216 for (std::size_t d = 0; d < dim; ++d) {
217 new_centroids[c][d] += data[i][d];
218 }
219 }
220
221 for (std::size_t c = 0; c < k; ++c) {
222 if (cluster_sizes[c] > 0) {
223 for (std::size_t d = 0; d < dim; ++d) {
224 new_centroids[c][d] /= static_cast<double>(cluster_sizes[c]);
225 }
226 } else {
227 // Empty cluster: reinitialize with the farthest data point
228 double max_dist = -1.0;
229 std::size_t farthest = 0;
230 for (std::size_t i = 0; i < n; ++i) {
231 double dist = euclidean_distance(data[i], centroids[labels[i]]);
232 if (dist > max_dist) {
233 max_dist = dist;
234 farthest = i;
235 }
236 }
237 new_centroids[c] = data[farthest];
238 }
239 }
240
241 // Convergence check
242 double max_shift = 0.0;
243 for (std::size_t c = 0; c < k; ++c) {
244 double shift = euclidean_distance(centroids[c], new_centroids[c]);
245 max_shift = std::max(max_shift, shift);
246 }
247
248 centroids = std::move(new_centroids);
249
250 if (max_shift < tol) {
251 iter++;
252 break;
253 }
254 }
255
256 // Calculate inertia
257 double inertia = 0.0;
258 for (std::size_t i = 0; i < n; ++i) {
259 double dist = euclidean_distance(data[i], centroids[labels[i]]);
260 inertia += dist * dist;
261 }
262
263 return {labels, centroids, inertia, iter};
264}
265
266// ============================================================================
267// Hierarchical Clustering
268// ============================================================================
269
273enum class linkage_type {
274 single,
275 complete,
276 average,
277 ward
278};
279
284 std::size_t left;
285 std::size_t right;
286 double distance;
287 std::size_t count;
288};
289
302inline std::vector<dendrogram_node> hierarchical_clustering(
303 const std::vector<std::vector<double>>& data,
305{
306 if (data.empty()) {
307 throw std::invalid_argument("statcpp::hierarchical_clustering: empty data");
308 }
309
310 std::size_t n = data.size();
311
312 // Calculate distance matrix
313 std::vector<std::vector<double>> dist_matrix(n, std::vector<double>(n, 0.0));
314 for (std::size_t i = 0; i < n; ++i) {
315 for (std::size_t j = i + 1; j < n; ++j) {
316 double d = euclidean_distance(data[i], data[j]);
317 dist_matrix[i][j] = d;
318 dist_matrix[j][i] = d;
319 }
320 }
321
322 // Active clusters
323 std::vector<bool> active(2 * n - 1, false);
324 for (std::size_t i = 0; i < n; ++i) {
325 active[i] = true;
326 }
327
328 // Cluster sizes
329 std::vector<std::size_t> cluster_size(2 * n - 1, 1);
330
331 // Dendrogram
332 std::vector<dendrogram_node> dendrogram;
333 dendrogram.reserve(n - 1);
334
335 // Extended cluster distance matrix
336 // For Ward's method, store squared distances; for others, store distances.
337 bool use_squared = (linkage == linkage_type::ward);
338 std::vector<std::vector<double>> cluster_dist(2 * n - 1, std::vector<double>(2 * n - 1, std::numeric_limits<double>::max()));
339 // Copy initial distance matrix
340 for (std::size_t i = 0; i < n; ++i) {
341 for (std::size_t j = 0; j < n; ++j) {
342 double d = dist_matrix[i][j];
343 cluster_dist[i][j] = use_squared ? d * d : d;
344 }
345 }
346
347 for (std::size_t step = 0; step < n - 1; ++step) {
348 // Find the closest pair
349 double min_dist = std::numeric_limits<double>::max();
350 std::size_t min_i = 0, min_j = 0;
351
352 for (std::size_t i = 0; i < n + step; ++i) {
353 if (!active[i]) continue;
354 for (std::size_t j = i + 1; j < n + step; ++j) {
355 if (!active[j]) continue;
356 if (cluster_dist[i][j] < min_dist) {
357 min_dist = cluster_dist[i][j];
358 min_i = i;
359 min_j = j;
360 }
361 }
362 }
363
364 // Create new cluster
365 std::size_t new_cluster = n + step;
366 active[min_i] = false;
367 active[min_j] = false;
368 active[new_cluster] = true;
369
370 cluster_size[new_cluster] = cluster_size[min_i] + cluster_size[min_j];
371
372 // Store Euclidean distance in dendrogram (take sqrt for Ward's squared distances)
373 double dendro_dist = use_squared ? std::sqrt(min_dist) : min_dist;
374 dendrogram.push_back({min_i, min_j, dendro_dist, cluster_size[new_cluster]});
375
376 // Update distances between new cluster and other clusters
377 for (std::size_t k = 0; k < new_cluster; ++k) {
378 if (!active[k]) continue;
379
380 double new_dist;
381 switch (linkage) {
383 new_dist = std::min(cluster_dist[min_i][k], cluster_dist[min_j][k]);
384 break;
386 new_dist = std::max(cluster_dist[min_i][k], cluster_dist[min_j][k]);
387 break;
389 new_dist = (cluster_size[min_i] * cluster_dist[min_i][k] +
390 cluster_size[min_j] * cluster_dist[min_j][k]) /
391 static_cast<double>(cluster_size[min_i] + cluster_size[min_j]);
392 break;
393 case linkage_type::ward: {
394 // Lance-Williams recurrence for Ward's method on squared distances
395 double ni = static_cast<double>(cluster_size[min_i]);
396 double nj = static_cast<double>(cluster_size[min_j]);
397 double nk = static_cast<double>(cluster_size[k]);
398 double nijk = ni + nj + nk;
399 new_dist = ((ni + nk) * cluster_dist[min_i][k] +
400 (nj + nk) * cluster_dist[min_j][k] -
401 nk * min_dist) / nijk;
402 break;
403 }
404 }
405
406 cluster_dist[new_cluster][k] = new_dist;
407 cluster_dist[k][new_cluster] = new_dist;
408 }
409 }
410
411 return dendrogram;
412}
413
425inline std::vector<std::size_t> cut_dendrogram(
426 const std::vector<dendrogram_node>& dendrogram,
427 std::size_t n_data,
428 std::size_t k)
429{
430 if (k == 0 || k > n_data) {
431 throw std::invalid_argument("statcpp::cut_dendrogram: invalid k");
432 }
433
434 std::vector<std::size_t> labels(n_data);
435 std::iota(labels.begin(), labels.end(), 0);
436
437 if (k == n_data) {
438 return labels;
439 }
440
441 // Apply n_data - k merges
442 std::size_t n_merges = n_data - k;
443
444 std::vector<std::size_t> cluster_map(2 * n_data - 1);
445 std::iota(cluster_map.begin(), cluster_map.end(), 0);
446
447 for (std::size_t i = 0; i < n_merges; ++i) {
448 const auto& node = dendrogram[i];
449 std::size_t new_cluster = n_data + i;
450
451 // Assign merged clusters the same label
452 std::size_t left_label = cluster_map[node.left];
453 std::size_t right_label = cluster_map[node.right];
454
455 for (std::size_t j = 0; j < 2 * n_data - 1; ++j) {
456 if (cluster_map[j] == right_label) {
457 cluster_map[j] = left_label;
458 }
459 }
460 cluster_map[new_cluster] = left_label;
461 }
462
463 // Normalize labels to 0 through k-1
464 for (std::size_t i = 0; i < n_data; ++i) {
465 labels[i] = cluster_map[i];
466 }
467
468 // Convert labels to consecutive integers
469 std::map<std::size_t, std::size_t> label_map;
470 std::size_t next_label = 0;
471 for (std::size_t i = 0; i < n_data; ++i) {
472 if (label_map.find(labels[i]) == label_map.end()) {
473 label_map[labels[i]] = next_label++;
474 }
475 labels[i] = label_map[labels[i]];
476 }
477
478 return labels;
479}
480
481// ============================================================================
482// Silhouette Score
483// ============================================================================
484
498inline double silhouette_score(
499 const std::vector<std::vector<double>>& data,
500 const std::vector<std::size_t>& labels)
501{
502 if (data.empty()) {
503 throw std::invalid_argument("statcpp::silhouette_score: empty data");
504 }
505 if (data.size() != labels.size()) {
506 throw std::invalid_argument("statcpp::silhouette_score: data and labels size mismatch");
507 }
508
509 std::size_t n = data.size();
510
511 // Check number of clusters
512 std::size_t k = *std::max_element(labels.begin(), labels.end()) + 1;
513 if (k == 1) {
514 return 0.0; // Single cluster case
515 }
516
517 double total_silhouette = 0.0;
518
519 for (std::size_t i = 0; i < n; ++i) {
520 // a(i): Average distance to points in the same cluster
521 double a = 0.0;
522 std::size_t same_cluster_count = 0;
523
524 for (std::size_t j = 0; j < n; ++j) {
525 if (i != j && labels[j] == labels[i]) {
526 a += euclidean_distance(data[i], data[j]);
527 same_cluster_count++;
528 }
529 }
530 if (same_cluster_count > 0) {
531 a /= static_cast<double>(same_cluster_count);
532 }
533
534 // b(i): Average distance to points in the nearest other cluster
535 double b = std::numeric_limits<double>::max();
536
537 for (std::size_t c = 0; c < k; ++c) {
538 if (c == labels[i]) continue;
539
540 double cluster_dist = 0.0;
541 std::size_t cluster_count = 0;
542
543 for (std::size_t j = 0; j < n; ++j) {
544 if (labels[j] == c) {
545 cluster_dist += euclidean_distance(data[i], data[j]);
546 cluster_count++;
547 }
548 }
549
550 if (cluster_count > 0) {
551 cluster_dist /= static_cast<double>(cluster_count);
552 b = std::min(b, cluster_dist);
553 }
554 }
555
556 // Silhouette value
557 double s = 0.0;
558 if (same_cluster_count > 0) {
559 s = (b - a) / std::max(a, b);
560 }
561 total_silhouette += s;
562 }
563
564 return total_silhouette / static_cast<double>(n);
565}
566
567} // namespace statcpp
auto sum(Iterator first, Iterator last)
Sum.
kmeans_result kmeans(const std::vector< std::vector< double > > &data, std::size_t k, std::size_t max_iter=100, double tol=1e-6)
K-means clustering.
std::vector< std::vector< double > > kmeans_plusplus_init(const std::vector< std::vector< double > > &data, std::size_t k)
K-means++ initialization.
linkage_type
Linkage types.
@ ward
Ward's method.
@ average
Average linkage.
@ complete
Complete linkage.
@ single
Single linkage.
double silhouette_score(const std::vector< std::vector< double > > &data, const std::vector< std::size_t > &labels)
Calculate silhouette score.
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
double euclidean_distance(const std::vector< double > &a, const std::vector< double > &b)
Euclidean distance.
default_random_engine & get_random_engine()
Singleton accessor for global random engine.
std::vector< std::size_t > cut_dendrogram(const std::vector< dendrogram_node > &dendrogram, std::size_t n_data, std::size_t k)
Extract k clusters from dendrogram.
double manhattan_distance(const std::vector< double > &a, const std::vector< double > &b)
Manhattan distance.
std::vector< dendrogram_node > hierarchical_clustering(const std::vector< std::vector< double > > &data, linkage_type linkage=linkage_type::single)
Hierarchical clustering.
Random engine wrapper and utilities.
double distance
Merge distance.
std::size_t count
Number of data points in cluster.
std::size_t right
Right child.
std::size_t left
Left child (cluster or data point)
K-means clustering result.
double inertia
Inertia (within-cluster sum of squares)
std::vector< std::vector< double > > centroids
Cluster centroids.
std::size_t n_iter
Number of iterations until convergence.
std::vector< std::size_t > labels
Cluster assignments.