bitbullet 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. bitbullet/__init__.py +1 -0
  2. bitbullet/cluster/__init__.py +34 -0
  3. bitbullet/cluster/algorithms/__init__.py +0 -0
  4. bitbullet/cluster/algorithms/density/__init__.py +0 -0
  5. bitbullet/cluster/algorithms/density/dbscan.py +252 -0
  6. bitbullet/cluster/algorithms/model_based/__init__.py +0 -0
  7. bitbullet/cluster/algorithms/model_based/gmm.py +283 -0
  8. bitbullet/cluster/algorithms/partitional/__init__.py +19 -0
  9. bitbullet/cluster/algorithms/partitional/kmeans.py +157 -0
  10. bitbullet/cluster/algorithms/partitional/kmodes.py +334 -0
  11. bitbullet/cluster/algorithms/partitional/kprototypes.py +281 -0
  12. bitbullet/cluster/core/__init__.py +74 -0
  13. bitbullet/cluster/core/autopipeline.py +355 -0
  14. bitbullet/cluster/core/base.py +348 -0
  15. bitbullet/cluster/core/registry.py +61 -0
  16. bitbullet/cluster/distance/__init__.py +23 -0
  17. bitbullet/cluster/distance/mixed.py +178 -0
  18. bitbullet/cluster/distances/__init__.py +0 -0
  19. bitbullet/cluster/distances/memory_efficient.py +297 -0
  20. bitbullet/cluster/evaluation/__init__.py +66 -0
  21. bitbullet/cluster/evaluation/metrics.py +537 -0
  22. bitbullet/cluster/evaluation/profiling.py +602 -0
  23. bitbullet/cluster/evaluation/selection.py +214 -0
  24. bitbullet/cluster/utils/__init__.py +13 -0
  25. bitbullet/cluster/utils/sample_size.py +655 -0
  26. bitbullet/cluster/visualization/__init__.py +54 -0
  27. bitbullet/cluster/visualization/decomposition.py +324 -0
  28. bitbullet/cluster/visualization/interactive.py +496 -0
  29. bitbullet/cluster/weights/__init__.py +0 -0
  30. bitbullet/cluster/weights/categorical_weights.py +295 -0
  31. bitbullet/cluster/weights/gamma_estimation.py +273 -0
  32. bitbullet/evaluate/__init__.py +41 -0
  33. bitbullet/evaluate/classification.py +181 -0
  34. bitbullet/evaluate/config.py +50 -0
  35. bitbullet/evaluate/evaluator.py +322 -0
  36. bitbullet/evaluate/regression.py +136 -0
  37. bitbullet/model/__init__.py +55 -0
  38. bitbullet/model/core/__init__.py +1 -0
  39. bitbullet/model/core/base.py +213 -0
  40. bitbullet/model/core/metadata.py +335 -0
  41. bitbullet/model/core/registry.py +127 -0
  42. bitbullet/model/serialization/__init__.py +1 -0
  43. bitbullet/model/serialization/model_serializer.py +378 -0
  44. bitbullet/model/wrappers/__init__.py +39 -0
  45. bitbullet/model/wrappers/lgbm_wrapper.py +311 -0
  46. bitbullet/model/wrappers/xgb_wrapper.py +181 -0
  47. bitbullet/train/__init__.py +57 -0
  48. bitbullet/train/callbacks/__init__.py +0 -0
  49. bitbullet/train/core/__init__.py +1 -0
  50. bitbullet/train/core/base.py +526 -0
  51. bitbullet/train/core/config.py +216 -0
  52. bitbullet/train/core/state.py +149 -0
  53. bitbullet/train/evaluation/__init__.py +0 -0
  54. bitbullet/train/evaluation/threshold_optimizer.py +153 -0
  55. bitbullet/train/explainability/__init__.py +19 -0
  56. bitbullet/train/explainability/shap_explainer.py +581 -0
  57. bitbullet/train/feature_selection/__init__.py +0 -0
  58. bitbullet/train/feature_selection/selector_factory.py +609 -0
  59. bitbullet/train/optimizers/__init__.py +0 -0
  60. bitbullet/train/optimizers/hyperparameter_grids.py +511 -0
  61. bitbullet/train/optimizers/optuna_optimizer.py +252 -0
  62. bitbullet/train/reports/__init__.py +19 -0
  63. bitbullet/train/reports/feature_report.py +365 -0
  64. bitbullet/train/reports/training_report.py +325 -0
  65. bitbullet/train/trainers/__init__.py +0 -0
  66. bitbullet/train/trainers/optuna_trainer.py +1044 -0
  67. bitbullet/train/utils/__init__.py +0 -0
  68. bitbullet/train/utils/sample_weights.py +174 -0
  69. bitbullet/transform/__init__.py +29 -0
  70. bitbullet/transform/core/__init__.py +7 -0
  71. bitbullet/transform/core/base.py +308 -0
  72. bitbullet/transform/core/pipeline.py +518 -0
  73. bitbullet/transform/core/registry.py +241 -0
  74. bitbullet/transform/eda.py +148 -0
  75. bitbullet/transform/transformers/__init__.py +13 -0
  76. bitbullet/transform/transformers/categorical.py +344 -0
  77. bitbullet/transform/transformers/datetime.py +191 -0
  78. bitbullet/transform/transformers/numerical.py +407 -0
  79. bitbullet/transform/transformers/target.py +487 -0
  80. bitbullet-0.1.0.dist-info/METADATA +296 -0
  81. bitbullet-0.1.0.dist-info/RECORD +84 -0
  82. bitbullet-0.1.0.dist-info/WHEEL +5 -0
  83. bitbullet-0.1.0.dist-info/licenses/LICENSE +21 -0
  84. bitbullet-0.1.0.dist-info/top_level.txt +1 -0
bitbullet/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,34 @@
1
+ """
2
+ BitBullet Cluster - Production-grade clustering engine for mixed data types.
3
+
4
+ This package provides a comprehensive clustering solution with:
5
+ - Support for mixed numerical and categorical data
6
+ - Multiple clustering algorithms (K-Modes, K-Prototypes, K-Means, DBSCAN, GMM)
7
+ - Data-driven feature weighting
8
+ - Memory-efficient distance computation
9
+ - Production-ready serialization
10
+ - Comprehensive evaluation metrics
11
+ - Beautiful visualizations
12
+ """
13
+
14
+ from bitbullet.cluster.core.base import BaseClusterer, ClusterConfig, ClusterState
15
+ from bitbullet.cluster.weights.categorical_weights import CategoricalWeights
16
+ from bitbullet.cluster.weights.gamma_estimation import GammaEstimator
17
+ from bitbullet.cluster.utils.sample_size import (
18
+ SampleSizeEstimator,
19
+ SampleSizeEstimate,
20
+ estimate_sample_size,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "BaseClusterer",
27
+ "ClusterConfig",
28
+ "ClusterState",
29
+ "CategoricalWeights",
30
+ "GammaEstimator",
31
+ "SampleSizeEstimator",
32
+ "SampleSizeEstimate",
33
+ "estimate_sample_size",
34
+ ]
File without changes
File without changes
@@ -0,0 +1,252 @@
1
+ """
2
+ DBSCAN density-based clustering algorithm.
3
+
4
+ Finds arbitrarily-shaped clusters and identifies outliers.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from sklearn.cluster import DBSCAN as SKLearnDBSCAN
12
+
13
+ from bitbullet.cluster.core.base import BaseClusterer
14
+
15
+
16
+ class DBSCANClusterer(BaseClusterer):
17
+ """DBSCAN clustering for arbitrary-shaped clusters.
18
+
19
+ Features:
20
+ - Density-based clustering (no need to specify n_clusters)
21
+ - Automatic outlier detection (noise points labeled as -1)
22
+ - Works with arbitrary cluster shapes
23
+ - Supports custom distance metrics
24
+
25
+ Note:
26
+ DBSCAN does not support prediction on new data. Use fit_predict
27
+ on all data at once, or use a classifier trained on DBSCAN labels.
28
+ """
29
+
30
+ def _validate_config(self) -> None:
31
+ """Validate DBSCAN specific configuration."""
32
+ # DBSCAN doesn't require n_clusters (it's determined automatically)
33
+ # So we don't validate n_clusters
34
+
35
+ # Must have eps and min_samples
36
+ eps = self._config.params.get("eps")
37
+ min_samples = self._config.params.get("min_samples")
38
+
39
+ if eps is None:
40
+ raise ValueError("DBSCAN requires 'eps' parameter in config.params")
41
+
42
+ if min_samples is None:
43
+ raise ValueError("DBSCAN requires 'min_samples' parameter in config.params")
44
+
45
+ if eps <= 0:
46
+ raise ValueError(f"eps must be positive, got {eps}")
47
+
48
+ if min_samples < 1:
49
+ raise ValueError(f"min_samples must be >= 1, got {min_samples}")
50
+
51
+ def _fit(self, data: pd.DataFrame, verbose: bool = False) -> None:
52
+ """Fit DBSCAN to data.
53
+
54
+ Args:
55
+ data: Input data
56
+ verbose: Print progress messages
57
+ """
58
+ # Extract parameters
59
+ eps = self._config.params.get("eps", 0.5)
60
+ min_samples = self._config.params.get("min_samples", 5)
61
+ metric = self._config.params.get("metric", "euclidean")
62
+ algorithm = self._config.params.get("algorithm", "auto")
63
+ leaf_size = self._config.params.get("leaf_size", 30)
64
+
65
+ if verbose:
66
+ print(f"Fitting DBSCAN (eps={eps}, min_samples={min_samples}, metric={metric})...")
67
+
68
+ # ── Mixed-distance path ───────────────────────────────────────────────
69
+ # When metric == 'mixed', DBSCAN consumes a precomputed pairwise
70
+ # distance matrix built from the same k-prototypes hybrid distance
71
+ # formula (numerical Euclidean + γ · weighted-matching) — the same
72
+ # math the Clusterability Diagnostic γ slider previews. This is how
73
+ # DBSCAN supports mixed numerical+categorical data without dragging
74
+ # in Gower as an external dep.
75
+ if metric == "mixed":
76
+ from bitbullet.cluster.distance import compute_mixed_distance_matrix
77
+ from bitbullet.cluster.weights.categorical_weights import CategoricalWeights
78
+ from bitbullet.cluster.weights.gamma_estimation import GammaEstimator
79
+
80
+ numerical_cols = self._config.numerical_columns or []
81
+ categorical_cols = self._config.categorical_columns or []
82
+ if not numerical_cols and not categorical_cols:
83
+ raise ValueError(
84
+ "DBSCAN with metric='mixed' requires at least one numerical "
85
+ "or categorical feature selection."
86
+ )
87
+
88
+ num_df = data[numerical_cols] if numerical_cols else None
89
+ cat_df = data[categorical_cols] if categorical_cols else None
90
+
91
+ # Resolve γ: heuristic string ('huang' / 'variance_matching') or
92
+ # a concrete float (manual). Mirrors k-prototypes' contract so a
93
+ # user toggling between the two algorithms sees identical γ
94
+ # semantics.
95
+ gamma_raw = self._config.params.get("gamma", "huang")
96
+ gamma_mode = "fixed"
97
+ if isinstance(gamma_raw, str):
98
+ gamma_mode = gamma_raw
99
+ gamma_value = GammaEstimator.calculate(
100
+ data, numerical_cols, categorical_cols, method=gamma_raw
101
+ ) if numerical_cols and categorical_cols else 1.0
102
+ else:
103
+ gamma_value = float(gamma_raw)
104
+
105
+ # Categorical weights — same factory k-prototypes uses.
106
+ cat_weights_raw = self._config.params.get("categorical_weights", "relevance")
107
+ cat_weights_method = "manual"
108
+ if isinstance(cat_weights_raw, str):
109
+ cat_weights_method = cat_weights_raw
110
+ if categorical_cols:
111
+ cat_weights = CategoricalWeights.calculate(
112
+ data, method=cat_weights_raw, categorical_columns=categorical_cols
113
+ )
114
+ else:
115
+ cat_weights = None
116
+ elif cat_weights_raw is None:
117
+ cat_weights = None
118
+ cat_weights_method = "none"
119
+ else:
120
+ cat_weights = CategoricalWeights.validate_manual_weights(
121
+ cat_weights_raw, categorical_cols
122
+ )
123
+
124
+ distance_matrix = compute_mixed_distance_matrix(
125
+ num_df=num_df,
126
+ cat_df=cat_df,
127
+ gamma=float(gamma_value),
128
+ cat_weights=cat_weights,
129
+ )
130
+
131
+ sklearn_metric = "precomputed"
132
+ sklearn_input = distance_matrix
133
+ mixed_metadata = {
134
+ "gamma_value": float(gamma_value),
135
+ "gamma_mode": gamma_mode,
136
+ "categorical_weighting_method": cat_weights_method,
137
+ "categorical_weights": cat_weights,
138
+ }
139
+ else:
140
+ sklearn_metric = metric
141
+ sklearn_input = data
142
+ mixed_metadata = None
143
+
144
+ # Create DBSCAN instance
145
+ clusterer = SKLearnDBSCAN(
146
+ eps=eps,
147
+ min_samples=min_samples,
148
+ metric=sklearn_metric,
149
+ algorithm=algorithm,
150
+ leaf_size=leaf_size,
151
+ n_jobs=-1,
152
+ )
153
+
154
+ # Fit the model
155
+ labels = clusterer.fit_predict(sklearn_input)
156
+
157
+ # Count clusters (excluding noise points labeled -1)
158
+ n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
159
+ n_noise = np.sum(labels == -1)
160
+
161
+ # Store results
162
+ self._state.labels = labels
163
+ self._state.n_clusters_actual = n_clusters
164
+
165
+ # DBSCAN doesn't have explicit centroids, but we can calculate them
166
+ # as the mean of points in each cluster (excluding noise). Categorical
167
+ # columns get the mode; only numerical columns get a true mean.
168
+ centroids = []
169
+ numerical_only = data.select_dtypes(include="number")
170
+ for cluster_id in range(n_clusters):
171
+ mask = labels == cluster_id
172
+ if np.any(mask) and not numerical_only.empty:
173
+ centroid = numerical_only[mask].mean(axis=0).values
174
+ centroids.append(centroid)
175
+
176
+ if centroids:
177
+ self._state.cluster_centers = np.array(centroids)
178
+ else:
179
+ self._state.cluster_centers = None
180
+
181
+ # Store fitted parameters
182
+ self._state.fitted_params = {
183
+ "eps": eps,
184
+ "min_samples": min_samples,
185
+ "metric": metric,
186
+ "n_noise_points": int(n_noise),
187
+ "n_clusters_found": n_clusters,
188
+ "core_sample_indices": clusterer.core_sample_indices_.tolist() if len(clusterer.core_sample_indices_) > 0 else [],
189
+ **({"mixed_distance": mixed_metadata} if mixed_metadata else {}),
190
+ }
191
+
192
+ if verbose:
193
+ print(f"Found {n_clusters} clusters and {n_noise} noise points")
194
+
195
+ def _predict(self, data: pd.DataFrame) -> np.ndarray:
196
+ """DBSCAN does not support prediction on new data.
197
+
198
+ Args:
199
+ data: New data
200
+
201
+ Raises:
202
+ NotImplementedError: DBSCAN does not support prediction
203
+ """
204
+ raise NotImplementedError(
205
+ "DBSCAN does not support prediction on new data. "
206
+ "Either:\n"
207
+ "1. Include all data in fit_predict()\n"
208
+ "2. Train a classifier (e.g., KNN) on DBSCAN labels to predict new data\n"
209
+ "3. Use a different algorithm that supports prediction (e.g., K-Means)"
210
+ )
211
+
212
+ def get_noise_points(self) -> np.ndarray:
213
+ """Get indices of points classified as noise.
214
+
215
+ Returns:
216
+ Array of indices where labels == -1
217
+
218
+ Raises:
219
+ RuntimeError: If not fitted
220
+ """
221
+ if not self.is_fitted:
222
+ raise RuntimeError("Must fit before getting noise points")
223
+
224
+ return np.where(self._state.labels == -1)[0]
225
+
226
+ def get_core_points(self) -> np.ndarray:
227
+ """Get indices of core points (points with >= min_samples neighbors).
228
+
229
+ Returns:
230
+ Array of core point indices
231
+
232
+ Raises:
233
+ RuntimeError: If not fitted
234
+ """
235
+ if not self.is_fitted:
236
+ raise RuntimeError("Must fit before getting core points")
237
+
238
+ return np.array(self._state.fitted_params["core_sample_indices"])
239
+
240
+ def get_n_clusters(self) -> int:
241
+ """Get number of clusters found (excluding noise).
242
+
243
+ Returns:
244
+ Number of clusters
245
+
246
+ Raises:
247
+ RuntimeError: If not fitted
248
+ """
249
+ if not self.is_fitted:
250
+ raise RuntimeError("Must fit before getting n_clusters")
251
+
252
+ return self._state.n_clusters_actual
File without changes
@@ -0,0 +1,283 @@
1
+ """
2
+ Gaussian Mixture Model (GMM) clustering algorithm.
3
+
4
+ Probabilistic soft clustering assuming Gaussian distributions.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from sklearn.mixture import GaussianMixture
12
+
13
+ from bitbullet.cluster.core.base import BaseClusterer
14
+
15
+
16
+ class GMMClusterer(BaseClusterer):
17
+ """Gaussian Mixture Model for probabilistic clustering.
18
+
19
+ Features:
20
+ - Soft clustering (probabilities for each cluster)
21
+ - Models overlapping clusters
22
+ - Multiple covariance types (full, tied, diag, spherical)
23
+ - Supports convergence diagnostics
24
+
25
+ Note:
26
+ GMM only works with numerical features. For mixed data,
27
+ use K-Prototypes instead.
28
+ """
29
+
30
+ def _validate_config(self) -> None:
31
+ """Validate GMM specific configuration."""
32
+ if self._config.n_clusters is None or self._config.n_clusters < 1:
33
+ raise ValueError(
34
+ f"n_clusters must be >= 1 for GMM, got {self._config.n_clusters}"
35
+ )
36
+
37
+ # GMM doesn't support categorical columns
38
+ if self._config.categorical_columns:
39
+ raise ValueError(
40
+ "GMM does not support categorical columns. "
41
+ "Use K-Prototypes for mixed data types."
42
+ )
43
+
44
+ def _fit(self, data: pd.DataFrame, verbose: bool = False) -> None:
45
+ """Fit GMM to numerical data.
46
+
47
+ Args:
48
+ data: Input data with only numerical features
49
+ verbose: Print progress messages
50
+ """
51
+ # Extract parameters
52
+ covariance_type = self._config.params.get("covariance_type", "full")
53
+ n_init = self._config.params.get("n_init", 1)
54
+ max_iter = self._config.params.get("max_iter", 100)
55
+ tol = self._config.params.get("tol", 1e-3)
56
+ reg_covar = self._config.params.get("reg_covar", 1e-6)
57
+ init_params = self._config.params.get("init_params", "kmeans")
58
+
59
+ if verbose:
60
+ print(f"Fitting GMM (n_components={self._config.n_clusters}, "
61
+ f"covariance_type={covariance_type})...")
62
+
63
+ # Create GMM instance
64
+ gmm = GaussianMixture(
65
+ n_components=self._config.n_clusters,
66
+ covariance_type=covariance_type,
67
+ n_init=n_init,
68
+ max_iter=max_iter,
69
+ tol=tol,
70
+ reg_covar=reg_covar,
71
+ init_params=init_params,
72
+ random_state=self._config.random_state,
73
+ verbose=int(verbose),
74
+ verbose_interval=10,
75
+ )
76
+
77
+ # Fit the model
78
+ gmm.fit(data)
79
+ labels = gmm.predict(data)
80
+
81
+ # Store results
82
+ self._state.labels = labels
83
+ self._state.n_clusters_actual = self._config.n_clusters
84
+ self._state.cluster_centers = gmm.means_
85
+
86
+ # Store fitted parameters (needed for prediction)
87
+ self._state.fitted_params = {
88
+ "converged": bool(gmm.converged_),
89
+ "n_iter": int(gmm.n_iter_),
90
+ "lower_bound": float(gmm.lower_bound_),
91
+ "covariances": gmm.covariances_.tolist(),
92
+ "weights": gmm.weights_.tolist(),
93
+ "precisions_cholesky": gmm.precisions_cholesky_.tolist(),
94
+ "covariance_type": covariance_type,
95
+ }
96
+
97
+ if verbose:
98
+ converged = "converged" if gmm.converged_ else "did NOT converge"
99
+ print(f"GMM {converged} after {gmm.n_iter_} iterations. "
100
+ f"Log-likelihood: {gmm.lower_bound_:.2f}")
101
+
102
+ def _predict(self, data: pd.DataFrame) -> np.ndarray:
103
+ """Assign new data to clusters using hard assignment.
104
+
105
+ Args:
106
+ data: New data to assign to clusters
107
+
108
+ Returns:
109
+ Cluster labels (hard assignment)
110
+ """
111
+ # Recreate GMM from fitted parameters
112
+ gmm = GaussianMixture(n_components=self._config.n_clusters)
113
+ gmm.means_ = self._state.cluster_centers
114
+ gmm.covariances_ = np.array(self._state.fitted_params["covariances"])
115
+ gmm.weights_ = np.array(self._state.fitted_params["weights"])
116
+ gmm.precisions_cholesky_ = np.array(self._state.fitted_params["precisions_cholesky"])
117
+ gmm.covariance_type = self._state.fitted_params["covariance_type"]
118
+
119
+ # Predict
120
+ labels = gmm.predict(data)
121
+
122
+ return labels
123
+
124
+ def predict_proba(self, data: pd.DataFrame) -> np.ndarray:
125
+ """Get probability of each cluster for each data point.
126
+
127
+ Args:
128
+ data: Data to get probabilities for
129
+
130
+ Returns:
131
+ Array of shape (n_samples, n_clusters) with probabilities
132
+
133
+ Raises:
134
+ RuntimeError: If not fitted
135
+ """
136
+ if not self.is_fitted:
137
+ raise RuntimeError("Must fit before prediction")
138
+
139
+ # Recreate GMM from fitted parameters
140
+ gmm = GaussianMixture(n_components=self._config.n_clusters)
141
+ gmm.means_ = self._state.cluster_centers
142
+ gmm.covariances_ = np.array(self._state.fitted_params["covariances"])
143
+ gmm.weights_ = np.array(self._state.fitted_params["weights"])
144
+ gmm.precisions_cholesky_ = np.array(self._state.fitted_params["precisions_cholesky"])
145
+ gmm.covariance_type = self._state.fitted_params["covariance_type"]
146
+
147
+ # Get probabilities
148
+ probas = gmm.predict_proba(data)
149
+
150
+ return probas
151
+
152
+ def get_centroids(self) -> pd.DataFrame:
153
+ """Get cluster centroids (means) as DataFrame.
154
+
155
+ Returns:
156
+ DataFrame with centroids
157
+
158
+ Raises:
159
+ RuntimeError: If not fitted
160
+ """
161
+ if not self.is_fitted:
162
+ raise RuntimeError("Must fit before getting centroids")
163
+
164
+ centroids_df = pd.DataFrame(
165
+ self._state.cluster_centers,
166
+ columns=self._state.feature_names,
167
+ index=[f"cluster_{i}" for i in range(self._state.n_clusters_actual)]
168
+ )
169
+
170
+ return centroids_df
171
+
172
+ def get_covariances(self) -> np.ndarray:
173
+ """Get covariance matrices for each cluster.
174
+
175
+ Returns:
176
+ Covariance matrices (shape depends on covariance_type)
177
+
178
+ Raises:
179
+ RuntimeError: If not fitted
180
+ """
181
+ if not self.is_fitted:
182
+ raise RuntimeError("Must fit before getting covariances")
183
+
184
+ return np.array(self._state.fitted_params["covariances"])
185
+
186
+ def get_weights(self) -> np.ndarray:
187
+ """Get mixture weights (prior probabilities of each cluster).
188
+
189
+ Returns:
190
+ Array of weights summing to 1.0
191
+
192
+ Raises:
193
+ RuntimeError: If not fitted
194
+ """
195
+ if not self.is_fitted:
196
+ raise RuntimeError("Must fit before getting weights")
197
+
198
+ return np.array(self._state.fitted_params["weights"])
199
+
200
+ def score(self, data: pd.DataFrame) -> float:
201
+ """Compute log-likelihood of data under the model.
202
+
203
+ Args:
204
+ data: Data to score
205
+
206
+ Returns:
207
+ Log-likelihood score (higher is better)
208
+
209
+ Raises:
210
+ RuntimeError: If not fitted
211
+ """
212
+ if not self.is_fitted:
213
+ raise RuntimeError("Must fit before scoring")
214
+
215
+ # Recreate GMM from fitted parameters
216
+ gmm = GaussianMixture(n_components=self._config.n_clusters)
217
+ gmm.means_ = self._state.cluster_centers
218
+ gmm.covariances_ = np.array(self._state.fitted_params["covariances"])
219
+ gmm.weights_ = np.array(self._state.fitted_params["weights"])
220
+ gmm.precisions_cholesky_ = np.array(self._state.fitted_params["precisions_cholesky"])
221
+ gmm.covariance_type = self._state.fitted_params["covariance_type"]
222
+
223
+ return gmm.score(data)
224
+
225
+ def get_bic(self, data: pd.DataFrame) -> float:
226
+ """Compute Bayesian Information Criterion (BIC) for model selection.
227
+
228
+ BIC balances model fit with complexity. Lower is better.
229
+ Use this for elbow-like plots when selecting optimal number of clusters.
230
+
231
+ BIC = -2 * log_likelihood + n_params * log(n_samples)
232
+
233
+ Args:
234
+ data: Data used for fitting (same data used in fit())
235
+
236
+ Returns:
237
+ BIC score (lower is better)
238
+
239
+ Raises:
240
+ RuntimeError: If not fitted
241
+ """
242
+ if not self.is_fitted:
243
+ raise RuntimeError("Must fit before computing BIC")
244
+
245
+ # Recreate GMM from fitted parameters
246
+ gmm = GaussianMixture(n_components=self._config.n_clusters)
247
+ gmm.means_ = self._state.cluster_centers
248
+ gmm.covariances_ = np.array(self._state.fitted_params["covariances"])
249
+ gmm.weights_ = np.array(self._state.fitted_params["weights"])
250
+ gmm.precisions_cholesky_ = np.array(self._state.fitted_params["precisions_cholesky"])
251
+ gmm.covariance_type = self._state.fitted_params["covariance_type"]
252
+
253
+ return gmm.bic(data)
254
+
255
+ def get_aic(self, data: pd.DataFrame) -> float:
256
+ """Compute Akaike Information Criterion (AIC) for model selection.
257
+
258
+ AIC is similar to BIC but with less penalty for model complexity.
259
+ Lower is better.
260
+
261
+ AIC = -2 * log_likelihood + 2 * n_params
262
+
263
+ Args:
264
+ data: Data used for fitting (same data used in fit())
265
+
266
+ Returns:
267
+ AIC score (lower is better)
268
+
269
+ Raises:
270
+ RuntimeError: If not fitted
271
+ """
272
+ if not self.is_fitted:
273
+ raise RuntimeError("Must fit before computing AIC")
274
+
275
+ # Recreate GMM from fitted parameters
276
+ gmm = GaussianMixture(n_components=self._config.n_clusters)
277
+ gmm.means_ = self._state.cluster_centers
278
+ gmm.covariances_ = np.array(self._state.fitted_params["covariances"])
279
+ gmm.weights_ = np.array(self._state.fitted_params["weights"])
280
+ gmm.precisions_cholesky_ = np.array(self._state.fitted_params["precisions_cholesky"])
281
+ gmm.covariance_type = self._state.fitted_params["covariance_type"]
282
+
283
+ return gmm.aic(data)
@@ -0,0 +1,19 @@
1
+ """Partitional clustering algorithms."""
2
+
3
+ __all__ = ["KMeansClusterer", "KModesClusterer", "KPrototypesClusterer"]
4
+
5
+
6
+ def __getattr__(name: str):
7
+ if name == "KMeansClusterer":
8
+ from bitbullet.cluster.algorithms.partitional.kmeans import KMeansClusterer
9
+
10
+ return KMeansClusterer
11
+ if name == "KModesClusterer":
12
+ from bitbullet.cluster.algorithms.partitional.kmodes import KModesClusterer
13
+
14
+ return KModesClusterer
15
+ if name == "KPrototypesClusterer":
16
+ from bitbullet.cluster.algorithms.partitional.kprototypes import KPrototypesClusterer
17
+
18
+ return KPrototypesClusterer
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")