serverlex 1.2.0__tar.gz → 1.2.1__tar.gz

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 (48) hide show
  1. {serverlex-1.2.0 → serverlex-1.2.1}/PKG-INFO +1 -1
  2. {serverlex-1.2.0 → serverlex-1.2.1}/pyproject.toml +1 -1
  3. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/__init__.py +1 -1
  4. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/diagnostics/drift.py +65 -24
  5. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/diagnostics/evaluation.py +68 -12
  6. serverlex-1.2.1/serverlex/preprocessing/clean.py +306 -0
  7. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/preprocessing/pipeline.py +30 -13
  8. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/serving/deployer.py +105 -27
  9. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/visualization/eda.py +61 -32
  10. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex.egg-info/PKG-INFO +1 -1
  11. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex.egg-info/SOURCES.txt +1 -11
  12. serverlex-1.2.1/serverlex.egg-info/top_level.txt +1 -0
  13. {serverlex-1.2.0 → serverlex-1.2.1}/setup.py +1 -14
  14. serverlex-1.2.1/tests/test_audit_remediation.py +190 -0
  15. serverlex-1.2.0/Clean.py +0 -2
  16. serverlex-1.2.0/DataCheck.py +0 -2
  17. serverlex-1.2.0/DriftDetector.py +0 -2
  18. serverlex-1.2.0/EnsembleBuilder.py +0 -2
  19. serverlex-1.2.0/Evaluation.py +0 -2
  20. serverlex-1.2.0/Explainability.py +0 -2
  21. serverlex-1.2.0/FeatureSelector.py +0 -2
  22. serverlex-1.2.0/ModelDeployer.py +0 -2
  23. serverlex-1.2.0/ModelHyperparameter.py +0 -2
  24. serverlex-1.2.0/Pipeline.py +0 -2
  25. serverlex-1.2.0/Visualization.py +0 -2
  26. serverlex-1.2.0/serverlex/preprocessing/clean.py +0 -267
  27. serverlex-1.2.0/serverlex.egg-info/top_level.txt +0 -12
  28. {serverlex-1.2.0 → serverlex-1.2.1}/README.md +0 -0
  29. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/diagnostics/__init__.py +0 -0
  30. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/diagnostics/dataguard.py +0 -0
  31. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/diagnostics/explainability.py +0 -0
  32. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/modeling/__init__.py +0 -0
  33. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/modeling/ensemble.py +0 -0
  34. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/modeling/hyperparameter.py +0 -0
  35. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/modeling/selector.py +0 -0
  36. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/preprocessing/__init__.py +0 -0
  37. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/serving/__init__.py +0 -0
  38. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex/visualization/__init__.py +0 -0
  39. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex.egg-info/dependency_links.txt +0 -0
  40. {serverlex-1.2.0 → serverlex-1.2.1}/serverlex.egg-info/requires.txt +0 -0
  41. {serverlex-1.2.0 → serverlex-1.2.1}/setup.cfg +0 -0
  42. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_clean.py +0 -0
  43. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_dataguard.py +0 -0
  44. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_deployer.py +0 -0
  45. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_drift.py +0 -0
  46. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_leakage.py +0 -0
  47. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_parity.py +0 -0
  48. {serverlex-1.2.0 → serverlex-1.2.1}/tests/test_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverlex
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Production-Safe ML Preprocessing, Diagnostics, Tuning, and Serving Platform
5
5
  Author: Serverlex Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "serverlex"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Production-Safe ML Preprocessing, Diagnostics, Tuning, and Serving Platform"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Serverlex Team" }]
@@ -24,4 +24,4 @@ __all__ = [
24
24
  "ModelDeployer",
25
25
  ]
26
26
 
27
- __version__ = "1.2.0"
27
+ __version__ = "1.2.1"
@@ -87,19 +87,46 @@ class DriftDetector:
87
87
  logger.info("Running drift detection across %d features (alpha=%.3f, psi_threshold=%.2f)", len(ref_cols), self.alpha, self.psi_threshold)
88
88
 
89
89
  for col in common_cols:
90
- # 2. Numerical Drift
91
- if pd.api.types.is_numeric_dtype(self.ref_data[col]):
92
- ref_vals=self.ref_data[col].dropna()
93
- cur_vals=cur_data[col].dropna()
90
+ ref_series = self.ref_data[col].dropna()
91
+ cur_series = cur_data[col].dropna()
92
+
93
+ if len(ref_series) == 0 or len(cur_series) == 0:
94
+ continue
94
95
 
95
- if len(ref_vals) > 0 and len(cur_vals) > 0:
96
- stat, p_val = ks_2samp(ref_vals, cur_vals)
97
- psi = self.calculate_psi(ref_vals, cur_vals)
98
- w_dist = float(np.round(wasserstein_distance(ref_vals, cur_vals), 4))
96
+ # 2. Check Constant Shift (was constant in reference, altered in current)
97
+ if ref_series.nunique() <= 1:
98
+ ref_val = ref_series.iloc[0] if len(ref_series) > 0 else None
99
+ is_shifted = (cur_series.nunique() > 1) or (cur_series.nunique() == 1 and cur_series.iloc[0] != ref_val)
100
+ if is_shifted:
101
+ drift_count += 1
102
+ logger.warning("Constant feature shift detected in '%s' (ref=%s, cur_unique=%s)", col, ref_val, cur_series.unique())
103
+ self.drift_results.append({
104
+ "Feature": col,
105
+ "Type": "Constant Shift",
106
+ "Statistic": None,
107
+ "P_Value": 0.0,
108
+ "PSI": 1.0,
109
+ "Wasserstein": None,
110
+ "Drift_Detected": True,
111
+ "Severity": "High"
112
+ })
113
+ continue
99
114
 
100
- # Avoid false positives: require practical shift (PSI >= threshold) OR severe shift
115
+ # 3. Numerical Drift
116
+ if pd.api.types.is_numeric_dtype(self.ref_data[col]):
117
+ ref_vals = ref_series
118
+ cur_vals = cur_series
119
+
120
+ stat, p_val = ks_2samp(ref_vals, cur_vals)
121
+ psi = self.calculate_psi(ref_vals, cur_vals)
122
+ w_dist = float(np.round(wasserstein_distance(ref_vals, cur_vals), 4))
123
+
124
+ # Guard against small sample noise: KS p-value requires sample size >= 10
125
+ if len(cur_vals) < 10:
126
+ is_drift = False
127
+ severity = "Insufficient Samples (< 10)"
128
+ else:
101
129
  is_drift = (psi >= self.psi_threshold) or (p_val < self.alpha and psi >= 0.10)
102
-
103
130
  if psi >= 0.25:
104
131
  severity = "High"
105
132
  elif psi >= 0.10:
@@ -107,26 +134,40 @@ class DriftDetector:
107
134
  else:
108
135
  severity = "None"
109
136
 
110
- if is_drift:
111
- drift_count += 1
112
- logger.warning("Data drift detected in '%s' (KS=%.4f, p=%.5f, PSI=%.4f, Wasserstein=%.4f, Severity=%s)", col, stat, p_val, psi, w_dist, severity)
137
+ if is_drift:
138
+ drift_count += 1
139
+ logger.warning("Data drift detected in '%s' (KS=%.4f, p=%.5f, PSI=%.4f, Wasserstein=%.4f, Severity=%s)", col, stat, p_val, psi, w_dist, severity)
113
140
 
114
- self.drift_results.append({
115
- "Feature": col,
116
- "Type": "Numerical",
117
- "Statistic": round(stat, 4),
118
- "P_Value": round(p_val, 5),
119
- "PSI": psi,
120
- "Wasserstein": w_dist,
121
- "Drift_Detected": is_drift,
122
- "Severity": severity
123
- })
141
+ self.drift_results.append({
142
+ "Feature": col,
143
+ "Type": "Numerical",
144
+ "Statistic": round(stat, 4),
145
+ "P_Value": round(p_val, 5),
146
+ "PSI": psi,
147
+ "Wasserstein": w_dist,
148
+ "Drift_Detected": is_drift,
149
+ "Severity": severity
150
+ })
124
151
 
125
- # 3. Categorical Drift
152
+ # 4. Categorical Drift
126
153
  else:
127
154
  ref_counts=self.ref_data[col].astype(str).value_counts()
128
155
  cur_counts=cur_data[col].astype(str).value_counts()
129
156
 
157
+ # Guard against tiny batches: Chi-square and PSI are unstable for n < 10
158
+ if len(cur_series) < 10:
159
+ self.drift_results.append({
160
+ "Feature": col,
161
+ "Type": "Categorical",
162
+ "Statistic": None,
163
+ "P_Value": 1.0,
164
+ "PSI": 0.0,
165
+ "Wasserstein": None,
166
+ "Drift_Detected": False,
167
+ "Severity": "Insufficient Samples (< 10)"
168
+ })
169
+ continue
170
+
130
171
  all_cats=list(set(ref_counts.index).union(set(cur_counts.index)))
131
172
  ref_aligned=np.array([ref_counts.get(cat, 0) for cat in all_cats], dtype=float)
132
173
  cur_aligned=np.array([cur_counts.get(cat, 0) for cat in all_cats], dtype=float)
@@ -34,7 +34,9 @@ class ModelEvaluator:
34
34
  # ==============================================================
35
35
  # 1. Classification Diagnostics
36
36
  # ==============================================================
37
- def plot_confusion_matrix(self, y_true, y_pred, labels=None, title="Confusion Matrix"):
37
+ # 1. Classification Diagnostics
38
+ # ==============================================================
39
+ def plot_confusion_matrix(self, y_true, y_pred, labels=None, title="Confusion Matrix", save_path=None):
38
40
  try:
39
41
  cm=confusion_matrix(y_true, y_pred, labels=labels)
40
42
  fig, ax=plt.subplots(figsize=(6, 5))
@@ -58,12 +60,17 @@ class ModelEvaluator:
58
60
  ax.set_ylabel("Actual Label")
59
61
  ax.set_xlabel("Predicted Label")
60
62
  plt.tight_layout()
61
- plt.show()
63
+ if save_path:
64
+ plt.savefig(save_path)
65
+ plt.close(fig)
66
+ else:
67
+ plt.show()
62
68
  return cm
63
69
  except Exception as e:
64
70
  logger.error("Error in plot_confusion_matrix: %s", e)
71
+ raise
65
72
 
66
- def plot_roc_curve(self, y_true, y_prob, title="ROC Curve"):
73
+ def plot_roc_curve(self, y_true, y_prob, title="ROC Curve", save_path=None):
67
74
  try:
68
75
  y_true_arr = np.asarray(y_true)
69
76
  y_prob_arr = np.asarray(y_prob)
@@ -98,14 +105,18 @@ class ModelEvaluator:
98
105
  ax.legend(loc="lower right")
99
106
  ax.grid(True, linestyle="--", alpha=0.5)
100
107
  plt.tight_layout()
101
- plt.show()
108
+ if save_path:
109
+ plt.savefig(save_path)
110
+ plt.close(fig)
111
+ else:
112
+ plt.show()
102
113
  logger.info("ROC-AUC score: %.4f", roc_auc)
103
114
  return roc_auc
104
115
  except Exception as e:
105
116
  logger.error("Error in plot_roc_curve: %s", e)
106
117
  raise
107
118
 
108
- def plot_precision_recall(self, y_true, y_prob, title="Precision-Recall Curve"):
119
+ def plot_precision_recall(self, y_true, y_prob, title="Precision-Recall Curve", save_path=None):
109
120
  try:
110
121
  precision, recall, _ = precision_recall_curve(y_true, y_prob)
111
122
  fig, ax=plt.subplots(figsize=(6, 5))
@@ -115,14 +126,19 @@ class ModelEvaluator:
115
126
  ax.set_title(title, fontsize=12, fontweight="bold")
116
127
  ax.grid(True, linestyle="--", alpha=0.5)
117
128
  plt.tight_layout()
118
- plt.show()
129
+ if save_path:
130
+ plt.savefig(save_path)
131
+ plt.close(fig)
132
+ else:
133
+ plt.show()
119
134
  except Exception as e:
120
135
  logger.error("Error in plot_precision_recall: %s", e)
136
+ raise
121
137
 
122
138
  # ==============================================================
123
139
  # 2. Regression Diagnostics
124
140
  # ==============================================================
125
- def plot_actual_vs_predicted(self, y_true, y_pred, title="Actual vs. Predicted"):
141
+ def plot_actual_vs_predicted(self, y_true, y_pred, title="Actual vs. Predicted", save_path=None):
126
142
  try:
127
143
  fig, ax=plt.subplots(figsize=(6, 5))
128
144
  ax.scatter(y_true, y_pred, alpha=0.6, edgecolors="k", color="royalblue")
@@ -137,11 +153,16 @@ class ModelEvaluator:
137
153
  ax.legend()
138
154
  ax.grid(True, linestyle="--", alpha=0.5)
139
155
  plt.tight_layout()
140
- plt.show()
156
+ if save_path:
157
+ plt.savefig(save_path)
158
+ plt.close(fig)
159
+ else:
160
+ plt.show()
141
161
  except Exception as e:
142
162
  logger.error("Error in plot_actual_vs_predicted: %s", e)
163
+ raise
143
164
 
144
- def plot_residuals(self, y_true, y_pred, title="Residual Analysis"):
165
+ def plot_residuals(self, y_true, y_pred, title="Residual Analysis", save_path=None):
145
166
  try:
146
167
  residuals = np.array(y_true) - np.array(y_pred)
147
168
  fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))
@@ -161,9 +182,14 @@ class ModelEvaluator:
161
182
 
162
183
  plt.suptitle(title, fontsize=13, fontweight="bold")
163
184
  plt.tight_layout()
164
- plt.show()
185
+ if save_path:
186
+ plt.savefig(save_path)
187
+ plt.close(fig)
188
+ else:
189
+ plt.show()
165
190
  except Exception as e:
166
191
  logger.error("Error in plot_residuals: %s", e)
192
+ raise
167
193
 
168
194
  # ==============================================================
169
195
  # 3. Deep End-to-End Diagnostics Runner
@@ -183,14 +209,33 @@ class ModelEvaluator:
183
209
  logger.info("Evaluating model predictions (task=%s, samples=%d)", self.task, len(self.y_true))
184
210
 
185
211
  if is_class:
212
+ from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
186
213
  logger.info("Classification report:\n%s", classification_report(self.y_true, self.y_pred))
187
214
  self.plot_confusion_matrix(self.y_true, self.y_pred)
188
215
 
216
+ acc = accuracy_score(self.y_true, self.y_pred)
217
+ prec = precision_score(self.y_true, self.y_pred, average='weighted', zero_division=0)
218
+ rec = recall_score(self.y_true, self.y_pred, average='weighted', zero_division=0)
219
+ f1_w = f1_score(self.y_true, self.y_pred, average='weighted', zero_division=0)
220
+ f1_m = f1_score(self.y_true, self.y_pred, average='macro', zero_division=0)
221
+ roc_auc = None
222
+
189
223
  if len(np.unique(self.y_true)) == 2 and hasattr(model, "predict_proba"):
190
224
  self.y_prob = model.predict_proba(X)[:, 1]
191
- self.plot_roc_curve(self.y_true, self.y_prob)
225
+ roc_auc = self.plot_roc_curve(self.y_true, self.y_prob)
192
226
  self.plot_precision_recall(self.y_true, self.y_prob)
193
227
 
228
+ self.metrics = {
229
+ "task": "Classification",
230
+ "accuracy": round(float(acc), 4),
231
+ "precision_weighted": round(float(prec), 4),
232
+ "recall_weighted": round(float(rec), 4),
233
+ "f1_weighted": round(float(f1_w), 4),
234
+ "f1_macro": round(float(f1_m), 4),
235
+ "roc_auc": round(float(roc_auc), 4) if roc_auc is not None else None
236
+ }
237
+ return self.metrics
238
+
194
239
  else:
195
240
  r2 = r2_score(self.y_true, self.y_pred)
196
241
  rmse = np.sqrt(mean_squared_error(self.y_true, self.y_pred))
@@ -198,7 +243,7 @@ class ModelEvaluator:
198
243
  mape = mean_absolute_percentage_error(self.y_true, self.y_pred)
199
244
 
200
245
  n = len(self.y_true)
201
- p = X.shape[1]
246
+ p = X.shape[1] if hasattr(X, "shape") and len(X.shape) > 1 else 1
202
247
  adj_r2 = 1 - (1 - r2) * (n - 1) / (n - p - 1) if (n - p - 1) > 0 else r2
203
248
 
204
249
  logger.info("Regression metrics: R2=%.4f, Adj_R2=%.4f, RMSE=%.4f, MAE=%.4f, MAPE=%.2f%%",
@@ -207,5 +252,16 @@ class ModelEvaluator:
207
252
  self.plot_actual_vs_predicted(self.y_true, self.y_pred)
208
253
  self.plot_residuals(self.y_true, self.y_pred)
209
254
 
255
+ self.metrics = {
256
+ "task": "Regression",
257
+ "r2": round(float(r2), 4),
258
+ "adj_r2": round(float(adj_r2), 4),
259
+ "rmse": round(float(rmse), 4),
260
+ "mae": round(float(mae), 4),
261
+ "mape": round(float(mape), 4)
262
+ }
263
+ return self.metrics
264
+
210
265
  except Exception as e:
211
266
  logger.error("Error in evaluate: %s", e)
267
+ raise
@@ -0,0 +1,306 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import logging
4
+
5
+ logger=logging.getLogger("Clean")
6
+ if not logger.handlers:
7
+ _h=logging.StreamHandler()
8
+ _h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
9
+ logger.addHandler(_h)
10
+ logger.setLevel(logging.INFO)
11
+
12
+ class Clean:
13
+ def __init__(self, path_or_df):
14
+ if isinstance(path_or_df, pd.DataFrame):
15
+ self.path = None
16
+ self.data = path_or_df.copy()
17
+ else:
18
+ self.path = path_or_df
19
+ self.data = None
20
+
21
+ def load(self):
22
+ if not hasattr(self, 'data') or self.data is None:
23
+ if self.path is not None:
24
+ self.data = pd.read_csv(self.path)
25
+ else:
26
+ raise ValueError("No dataset or file path provided to Clean.")
27
+ return self.data
28
+
29
+ def separate(self):
30
+ self.load()
31
+ self.numerical_columns = self.data.select_dtypes(include='number')
32
+ self.categorical_columns = self.data.select_dtypes(include=['object', 'category', 'string'])
33
+ self.categrorical_columns = self.categorical_columns # Backward-compatibility typo alias
34
+ return self.numerical_columns, self.categorical_columns
35
+
36
+ seperate = separate # Backward-compatibility typo alias
37
+
38
+ def outliers_IQR(self, param='m', exclude=()):
39
+ self.separate()
40
+ exclude = set(exclude) if exclude else set()
41
+ cols = [c for c in self.numerical_columns.columns if c not in exclude]
42
+ if not cols:
43
+ return self.data
44
+
45
+ num = self.numerical_columns[cols]
46
+ q1 = num.quantile(0.25)
47
+ q3 = num.quantile(0.75)
48
+ iqr = q3 - q1
49
+ lower_bound = q1 - 1.5 * iqr
50
+ upper_bound = q3 + 1.5 * iqr
51
+
52
+ bad = (num < lower_bound) | (num > upper_bound)
53
+
54
+ if param in ['r', 'drop', 'remove']:
55
+ self.data = self.data.loc[~bad.any(axis=1)]
56
+ self.separate()
57
+ return self.data
58
+
59
+ if param in ['m', 'mean']:
60
+ fill = num.mean()
61
+ else:
62
+ fill = num.median()
63
+
64
+ self.data[cols] = num.mask(bad, fill, axis=1)
65
+ self.separate()
66
+ return self.data
67
+
68
+ def outliers_Z(self, param='m', threshold=None, exclude=()):
69
+ self.separate()
70
+ exclude = set(exclude) if exclude else set()
71
+ cols = [c for c in self.numerical_columns.columns if c not in exclude]
72
+ if not cols:
73
+ return self.data
74
+
75
+ num = self.numerical_columns[cols]
76
+ mean = num.mean()
77
+ std_dev = num.std()
78
+ std_dev = std_dev.replace(0, 1.0)
79
+ z_score = (num - mean) / std_dev
80
+ t = threshold if threshold is not None else 3.0
81
+
82
+ bad = z_score.abs() >= t
83
+
84
+ if param in ['r', 'drop', 'remove']:
85
+ self.data = self.data.loc[~bad.any(axis=1)]
86
+ self.separate()
87
+ return self.data
88
+
89
+ if param in ['m', 'mean']:
90
+ fill = mean
91
+ else:
92
+ fill = num.median()
93
+
94
+ self.data[cols] = num.mask(bad, fill, axis=1)
95
+ self.separate()
96
+ return self.data
97
+
98
+ def Clean_string(self, data_time=None):
99
+ self.separate()
100
+
101
+ # Handle date time format: data_time="col" or data_time=["col"] or data_time=["col", format]
102
+ if data_time is not None:
103
+ if isinstance(data_time, str):
104
+ col_name, dt_format = data_time, None
105
+ elif len(data_time) == 1:
106
+ col_name, dt_format = data_time[0], None
107
+ else:
108
+ col_name, dt_format = data_time[0], data_time[1]
109
+
110
+ if col_name in self.data.columns:
111
+ parsed_dates = pd.to_datetime(self.data[col_name], format='mixed', errors='coerce')
112
+ if dt_format:
113
+ self.data[col_name] = parsed_dates.dt.strftime(dt_format)
114
+ else:
115
+ self.data[col_name] = parsed_dates
116
+
117
+ # Standardize strings: strip leading/trailing, normalize internal whitespace to single space, lowercase
118
+ cat_cols = [c for c in self.categorical_columns.columns if data_time is None or c != data_time[0]]
119
+ for col in cat_cols:
120
+ orig_na = self.data[col].isna()
121
+ cleaned = self.data[col].astype(str).str.strip().str.lower().str.replace(r"\s+", " ", regex=True)
122
+ self.data[col] = cleaned.mask(orig_na, None)
123
+
124
+ self.separate()
125
+ return self.data
126
+
127
+ def delete_duplicates(self):
128
+ self.load()
129
+ self.data = self.data.drop_duplicates()
130
+ self.separate()
131
+ return self.data
132
+ def fill_missing(self, param='m'):
133
+ self.separate()
134
+ if param in ['r', 'd', 'drop']:
135
+ self.data = self.data.dropna()
136
+ self.separate()
137
+ return self.data
138
+ if param in ['m', 'mean']:
139
+ fill_vals = self.numerical_columns.mean()
140
+ else:
141
+ fill_vals = self.numerical_columns.median()
142
+ self.numerical_columns = self.numerical_columns.fillna(fill_vals)
143
+ self.data[self.numerical_columns.columns] = self.numerical_columns
144
+ self.separate()
145
+ return self.data
146
+
147
+ def fill_categorical_missing(self, fill_val=None):
148
+ self.separate()
149
+ for col in self.categorical_columns.columns:
150
+ val = fill_val if fill_val is not None else (self.data[col].mode()[0] if not self.data[col].mode().empty else "unknown")
151
+ self.data[col] = self.data[col].fillna(val)
152
+ self.separate()
153
+ return self.data
154
+
155
+ def clean_column_names(self):
156
+ self.load()
157
+ self.data.columns = (
158
+ self.data.columns.str.strip()
159
+ .str.lower()
160
+ .str.replace(r"\s+", "_", regex=True)
161
+ .str.replace(r"[^\w_]", "", regex=True)
162
+ )
163
+ self.separate()
164
+ return self.data
165
+
166
+ def drop_uninformative(self, null_threshold=0.7):
167
+ self.load()
168
+ null_pct = self.data.isnull().mean()
169
+ cols_to_drop = null_pct[null_pct > null_threshold].index.tolist()
170
+ zero_var = [col for col in self.data.columns if self.data[col].nunique(dropna=False) <= 1]
171
+ all_drop = list(set(cols_to_drop + zero_var))
172
+ if all_drop:
173
+ self.data = self.data.drop(columns=all_drop)
174
+ self.separate()
175
+ return self.data
176
+
177
+ def clean_numeric_strings(self, columns):
178
+ self.load()
179
+ for col in columns:
180
+ if col in self.data.columns:
181
+ cleaned = self.data[col].astype(str).str.replace(r"[\$,]", "", regex=True).str.strip()
182
+ self.data[col] = pd.to_numeric(cleaned, errors="coerce")
183
+ self.separate()
184
+ return self.data
185
+
186
+ def low_corr(self, target=None, threshold=0.1):
187
+ self.separate()
188
+ corr_matrix = self.numerical_columns.corr().abs()
189
+ if target is not None:
190
+ if target in corr_matrix.columns:
191
+ low_corr_cols = corr_matrix[target][corr_matrix[target] < threshold].index.tolist()
192
+ self.data = self.data.drop(columns=low_corr_cols)
193
+ self.separate()
194
+ else:
195
+ np.fill_diagonal(corr_matrix.values, 0)
196
+ low_corr_cols = corr_matrix.columns[(corr_matrix.max() < threshold)].tolist()
197
+ if low_corr_cols:
198
+ self.data = self.data.drop(columns=low_corr_cols)
199
+ self.separate()
200
+ return self.data
201
+
202
+ def summary(self):
203
+ self.load()
204
+ missing_cols = (self.data.isnull().sum() > 0).sum()
205
+ logger.info("Dataset summary: shape=%s, duplicates=%d, columns_with_missing=%d",
206
+ self.data.shape, self.data.duplicated().sum(), missing_cols)
207
+ return {
208
+ "shape": self.data.shape,
209
+ "duplicates": int(self.data.duplicated().sum()),
210
+ "columns_with_missing": int(missing_cols)
211
+ }
212
+
213
+ def export(self, output_path):
214
+ self.load()
215
+ self.data.to_csv(output_path, index=False)
216
+ logger.info("Saved cleaned dataset to '%s'", output_path)
217
+
218
+ def total_clean(
219
+ self,
220
+ data_time=None,
221
+ numeric_string_cols=None,
222
+ missing_param='m',
223
+ cat_fill_val=None,
224
+ outlier_method='IQR',
225
+ outlier_param='m',
226
+ outlier_threshold=None,
227
+ exclude_outliers=None,
228
+ null_threshold=0.7,
229
+ corr_target=None,
230
+ corr_threshold=None,
231
+ export_path=None
232
+ ):
233
+ # 1. Load data
234
+ self.load()
235
+
236
+ # 2. Standardize column names
237
+ self.clean_column_names()
238
+
239
+ def _normalize_col(c):
240
+ import re
241
+ return re.sub(r"[^\w_]", "", re.sub(r"\s+", "_", str(c).strip().lower()))
242
+
243
+ if data_time is not None:
244
+ if isinstance(data_time, str):
245
+ c, fmt = data_time, None
246
+ elif len(data_time) == 1:
247
+ c, fmt = data_time[0], None
248
+ else:
249
+ c, fmt = data_time[0], data_time[1]
250
+
251
+ if c not in self.data.columns and _normalize_col(c) in self.data.columns:
252
+ data_time = [_normalize_col(c), fmt] if fmt else [_normalize_col(c)]
253
+
254
+ if numeric_string_cols:
255
+ numeric_string_cols = [
256
+ _normalize_col(c) if c not in self.data.columns and _normalize_col(c) in self.data.columns else c
257
+ for c in numeric_string_cols
258
+ ]
259
+
260
+ if corr_target is not None:
261
+ if corr_target not in self.data.columns and _normalize_col(corr_target) in self.data.columns:
262
+ corr_target = _normalize_col(corr_target)
263
+
264
+ # 3. Remove duplicate rows
265
+ self.delete_duplicates()
266
+
267
+ # 4. Drop uninformative columns (high nulls or constant values)
268
+ if null_threshold is not None:
269
+ self.drop_uninformative(null_threshold=null_threshold)
270
+
271
+ # 5. Clean currency / formatted numeric strings into actual numbers
272
+ if numeric_string_cols:
273
+ self.clean_numeric_strings(numeric_string_cols)
274
+
275
+ # 6. Clean strings & format datetime
276
+ self.Clean_string(data_time=data_time)
277
+
278
+ # 7. Fill missing values (numerical)
279
+ self.fill_missing(param=missing_param)
280
+
281
+ # 8. Fill missing values (categorical)
282
+ self.fill_categorical_missing(fill_val=cat_fill_val)
283
+
284
+ # 9. Handle numerical outliers with target/exclude protection
285
+ exclude_set = set()
286
+ if exclude_outliers:
287
+ exclude_set.update([_normalize_col(c) if _normalize_col(c) in self.data.columns else c for c in exclude_outliers])
288
+ if corr_target:
289
+ exclude_set.add(corr_target)
290
+
291
+ if outlier_method and str(outlier_method).lower() != 'none':
292
+ if outlier_method == 'Z':
293
+ self.outliers_Z(param=outlier_param, threshold=outlier_threshold, exclude=exclude_set)
294
+ elif outlier_method == 'IQR':
295
+ self.outliers_IQR(param=outlier_param, exclude=exclude_set)
296
+
297
+ # 10. Drop low correlation columns if requested
298
+ if corr_threshold is not None:
299
+ self.low_corr(target=corr_target, threshold=corr_threshold)
300
+
301
+ # 11. Export if path specified
302
+ if export_path:
303
+ self.export(export_path)
304
+
305
+ return self.data
306
+