| Signature | Description | Parameters |
|---|---|---|
include <DataFrame/DataFrameMLVisitors.h> template<typename T, typename I = unsigned long, std::size_t A = 0> struct DaviesBouldinIndexVisitor; // ----------------------------------------- template<typename T, typename I = unsigned long, std::size_t A = 0> using db_index_v = DaviesBouldinIndexVisitor<T, I, A>; |
Davies-Bouldin Cluster Validity Index The Davies-Bouldin index (DB) measures the average similarity between each cluster and its most similar neighbour, where similarity is the ratio of intra-cluster scatter to inter-cluster centroid separation: For each cluster i: s(i) = (1/ni) . Σi∈Ci dist(xi, µi) [mean intra-cluster distance] R(i, j) = (s(i) + s(j)) / d(μi, μi) [cluster similarity ratio] Di = max_{j != i} R(i, j) [worst-case similarity for i]
DB = (1/k) . Σi Di [index = mean of worst cases]
DB ∈ [0, ∞):DB = 0: perfect separation (never reached in practice) Lower: tighter, better-separated clusters — better result Higher: diffuse or poorly separated clusters — worse result Unlike Silhouette (which is O(n2)), DB is O(n.k) once centroids are known, making it practical for large datasets or many clusters. Special cases: Noise points (label −1 or −2, DBSCAN convention) are excluded from all centroid and scatter calculations. Singleton clusters: s(i) = 0. The ratio R(i, j) = s(j) / d(µi, µj), which is well-defined as long as the two centroids differ. Coincident centroids (d(µi, µi) = 0): R(i, j) is set to a large sentinel (std::numeric_limits Fewer than 2 non-noise clusters: DB = 0 (undefined, same as Silhouette). The visitor uses the same two-column operator() pattern as SilhouetteScoreVisitor: column 1 (T): data values (scalar or MD container) column 2 (long): cluster label per point (−1/−2 = noise) The distance function (dist) is used for BOTH: (a) intra-cluster scatter: dist(xi, µi) (b) centroid separation: dist(µi, µj) so the metric is consistent throughout. The default matches SilhouetteScoreVisitor: squared Euclidean for scalar T, Euclidean for MD T. Per-cluster results (scatter si, worst-case ratio Di, centroid μi) are available via accessors for diagnostic use.
References:
Davies, D.L. and Bouldin, D.W. (1979). "A cluster separation measure",
IEEE Transactions on Pattern Analysis and Machine Intelligence
PAMI-1(2): 224–227.
explicit SilhouetteScoreVisitor(distance_func f = default_dist_()); using distance_func = std::function<double(const value_type &, const value_type &)>; inline static distance_func default_dist_() { if constexpr (! is_md_) return ([](const T &x, const T &y) -> double { const double d { static_cast<double>(x - y) }; return (d * d); }); else return ([](const T &x, const T &y) -> double { double sum { 0.0 }; for (size_type i { 0 }; i < size_type(x.size()); ++i) { const double diff { static_cast<double>(x[i]) - static_cast<double>(y[i]) }; sum += diff * diff; } return (std::sqrt(sum)); }); }get_results() The Davies-Bouldin index ∈ [0, ∞). Lower is better. get_scatter() Per-cluster intra-cluster scatter s(i) = mean dist(point, centroid). Size == number of non-noise clusters. get_worst_ratio() Per-cluster worst-case similarity ratio Dᵢ = max_{j != i} R(i, j). Size == number of non-noise clusters. get_centroids() Per-cluster centroids μi. Type is double for scalar T, std::vector |
T: Column data type I: Index type A: Memory alignment boundary for vectors. Default is system default alignment |
static void test_DaviesBouldinIndexVisitor() { std::cout << "\nTesting DaviesBouldinIndexVisitor{ } ..." << std::endl; using MyDataFrame = StdDataFrame<unsigned long>; constexpr std::size_t col_s { 100 }; std::vector<unsigned long> idx(col_s); std::iota(idx.begin(), idx.end(), 0UL); MyDataFrame df; df.load_index(std::move(idx)); // Analytics // { df.load_column("x1", std::vector<double>{ 0.0, 2.0, 10.0, 12.0 }, nan_policy::dont_pad_with_nans); df.load_column("lbl1", std::vector<long>{ 0, 0, 1, 1 }, nan_policy::dont_pad_with_nans); db_index_v<double> db; df.single_act_visit<double, long>("x1", "lbl1", db); // DB index // assert(std::abs(db.get_result() - 0.02) < 1e-9); assert(db.get_scatter().size() == 2); assert(std::abs(db.get_scatter()[0] - 1.0) < 1e-9); assert(std::abs(db.get_scatter()[1] - 1.0) < 1e-9); assert(db.get_worst_ratio().size() == 2); assert(std::abs(db.get_worst_ratio()[0] - 0.02) < 1e-9); assert(std::abs(db.get_worst_ratio()[1] - 0.02) < 1e-9); assert(db.get_centroids().size() == 2); assert(std::abs(db.get_centroids()[0] - 1.0) < 1e-9); assert(std::abs(db.get_centroids()[1] - 11.0) < 1e-9); } // Well separated // { df.load_column("x2", std::vector<double>{ 0.0, 0.1, 0.2, 100.0, 100.1, 100.2, 200.0, 200.1, 200.2 }, nan_policy::dont_pad_with_nans); df.load_column("lbl2", std::vector<long>{ 0, 0, 0, 1, 1, 1, 2, 2, 2 }, nan_policy::dont_pad_with_nans); db_index_v<double> db; df.single_act_visit<double, long>("x2", "lbl2", db); // Three clusters very far apart, each tight -> DB close to 0 // assert(std::abs(db.get_result() - 1.33333e-06) < 1e-9); assert(db.get_scatter().size() == 3); assert(std::abs(db.get_scatter()[0] - 0.00666667) < 1e-8); assert(std::abs(db.get_scatter()[2] - 0.00666667) < 1e-8); assert(db.get_worst_ratio().size() == 3); assert(std::abs(db.get_worst_ratio()[0] - 1.33333e-06) < 1e-9); assert(std::abs(db.get_worst_ratio()[2] - 1.33333e-06) < 1e-9); assert(db.get_centroids().size() == 3); assert(std::abs(db.get_centroids()[0] - 0.1) < 1e-6); assert(std::abs(db.get_centroids()[1] - 100.1) < 1e-6); assert(std::abs(db.get_centroids()[2] - 200.1) < 1e-6); } // Poor separation // { df.load_column("x3", std::vector<double>{ 0, 10, 20, 30, 1, 11, 21, 31 }, nan_policy::dont_pad_with_nans); df.load_column("lbl3", std::vector<long>{ 0, 0, 0, 0, 1, 1, 1, 1 }, nan_policy::dont_pad_with_nans); db_index_v<double> db; df.single_act_visit<double, long>("x3", "lbl3", db); // Centroids: u0=15, u1=16 -> very close, large scatter -> large DB // assert(std::abs(db.get_result() - 250.0) < 1e-6); assert(db.get_scatter().size() == 2); assert(std::abs(db.get_scatter()[0] - 125.0) < 1e-6); assert(std::abs(db.get_scatter()[1] - 125.0) < 1e-6); assert(db.get_worst_ratio().size() == 2); assert(std::abs(db.get_worst_ratio()[0] - 250.0) < 1e-6); assert(std::abs(db.get_worst_ratio()[1] - 250.0) < 1e-6); assert(db.get_centroids().size() == 2); assert(std::abs(db.get_centroids()[0] - 15.0) < 1e-6); assert(std::abs(db.get_centroids()[1] - 16.0) < 1e-6); } // Singleton // { df.load_column("x4", std::vector<double>{ 5.0, 10.0, 12.0 }, nan_policy::dont_pad_with_nans); df.load_column("lbl4", std::vector<long>{ 0, 1, 1 }, nan_policy::dont_pad_with_nans); db_index_v<double> db; df.single_act_visit<double, long>("x4", "lbl4", db); assert(std::abs(db.get_result() - 0.0277778) < 1e-7); assert(db.get_scatter().size() == 2); assert(std::abs(db.get_scatter()[0] - 0.0) < 1e-9); assert(std::abs(db.get_scatter()[1] - 1.0) < 1e-9); assert(db.get_worst_ratio().size() == 2); assert(std::abs(db.get_worst_ratio()[0] - 0.0277778) < 1e-7); assert(std::abs(db.get_worst_ratio()[1] - 0.0277778) < 1e-7); assert(db.get_centroids().size() == 2); assert(std::abs(db.get_centroids()[0] - 5.0) < 1e-6); assert(std::abs(db.get_centroids()[1] - 11.0) < 1e-6); } // Noise points // { df.load_column("x5_noise", std::vector<double>{ 5.0, 0.0, 2.0, 10.0, 12.0 }, nan_policy::dont_pad_with_nans); df.load_column("lbl5_noise", std::vector<long>{ -1, 0, 0, 1, 1 }, nan_policy::dont_pad_with_nans); df.load_column("x5_clean", std::vector<double>{ 0.0, 2.0, 10.0, 12.0 }, nan_policy::dont_pad_with_nans); df.load_column("lbl5_clean", std::vector<long>{ 0, 0, 1, 1 }, nan_policy::dont_pad_with_nans); db_index_v<double> db_noise; db_index_v<double> db_clean; df.single_act_visit<double, long>("x5_noise", "lbl5_noise", db_noise); df.single_act_visit<double, long>("x5_clean", "lbl5_clean", db_clean); assert(std::abs(db_noise.get_result() - db_clean.get_result()) < 1e-9); assert(std::abs(db_noise.get_centroids()[0] - db_clean.get_centroids()[0]) < 1e-9); assert(std::abs(db_noise.get_centroids()[1] - db_clean.get_centroids()[1]) < 1e-9); assert(std::abs(db_noise.get_worst_ratio()[0] - db_clean.get_worst_ratio()[0]) < 1e-9); assert(std::abs(db_noise.get_worst_ratio()[1] - db_clean.get_worst_ratio()[1]) < 1e-9); } // Custom distance // { df.load_column("x6", std::vector<double>{ 0.0, 2.0, 10.0, 12.0 }, nan_policy::dont_pad_with_nans); df.load_column("lbl6", std::vector<long>{ 0, 0, 1, 1 }, nan_policy::dont_pad_with_nans); // Absolute difference: dist(x, y) = |x - y| // auto abs_dist = [](const double &x, const double &y) -> double { return (std::abs(x - y)); }; DaviesBouldinIndexVisitor<double> db_def; DaviesBouldinIndexVisitor<double> db_abs { abs_dist }; df.single_act_visit<double, long>("x6", "lbl6", db_def); df.single_act_visit<double, long>("x6", "lbl6", db_abs); assert(std::abs(db_def.get_result() - 0.02) < 1e-6); assert(std::abs(db_abs.get_result() - 0.2) < 1e-6); assert(std::abs(db_def.get_scatter()[0] - 1.0) < 1e-6); assert(std::abs(db_def.get_scatter()[1] - 1.0) < 1e-6); assert(std::abs(db_abs.get_scatter()[0] - 1.0) < 1e-6); assert(std::abs(db_abs.get_scatter()[1] - 1.0) < 1e-6); assert(std::abs(db_def.get_worst_ratio()[0] - 0.02) < 1e-6); assert(std::abs(db_def.get_worst_ratio()[1] - 0.02) < 1e-6); assert(std::abs(db_abs.get_worst_ratio()[0] - 0.2) < 1e-6); assert(std::abs(db_abs.get_worst_ratio()[1] - 0.2) < 1e-6); assert(std::abs(db_def.get_centroids()[0] - 1.0) < 1e-6); assert(std::abs(db_def.get_centroids()[1] - 11.0) < 1e-6); assert(std::abs(db_abs.get_centroids()[0] - 1.0) < 1e-6); assert(std::abs(db_abs.get_centroids()[1] - 11.0) < 1e-6); } }