serverlex 1.0.0__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.
- serverlex-1.0.0/Clean.py +261 -0
- serverlex-1.0.0/DataCheck.py +165 -0
- serverlex-1.0.0/DriftDetector.py +90 -0
- serverlex-1.0.0/EnsembleBuilder.py +78 -0
- serverlex-1.0.0/Evaluation.py +191 -0
- serverlex-1.0.0/Explainability.py +89 -0
- serverlex-1.0.0/FeatureSelector.py +90 -0
- serverlex-1.0.0/ModelDeployer.py +89 -0
- serverlex-1.0.0/ModelHyperparameter.py +162 -0
- serverlex-1.0.0/PKG-INFO +208 -0
- serverlex-1.0.0/Pipeline.py +251 -0
- serverlex-1.0.0/README.md +183 -0
- serverlex-1.0.0/Visualization.py +362 -0
- serverlex-1.0.0/__init__.py +27 -0
- serverlex-1.0.0/serverlex/__init__.py +34 -0
- serverlex-1.0.0/serverlex.egg-info/PKG-INFO +208 -0
- serverlex-1.0.0/serverlex.egg-info/SOURCES.txt +21 -0
- serverlex-1.0.0/serverlex.egg-info/dependency_links.txt +1 -0
- serverlex-1.0.0/serverlex.egg-info/requires.txt +6 -0
- serverlex-1.0.0/serverlex.egg-info/top_level.txt +12 -0
- serverlex-1.0.0/setup.cfg +4 -0
- serverlex-1.0.0/setup.py +43 -0
- serverlex-1.0.0/test_real_datasets.py +139 -0
serverlex-1.0.0/Clean.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
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):
|
|
14
|
+
self.path=path
|
|
15
|
+
self.data=None
|
|
16
|
+
def load(self):
|
|
17
|
+
try:
|
|
18
|
+
if not hasattr(self, 'data') or self.data is None:
|
|
19
|
+
self.data=pd.read_csv(self.path)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
print(e)
|
|
22
|
+
def seperate(self):
|
|
23
|
+
try:
|
|
24
|
+
self.load()
|
|
25
|
+
self.numerical_columns=self.data.select_dtypes(include='number')
|
|
26
|
+
self.categrorical_columns=self.data.select_dtypes(include=['object','category'])
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(e)
|
|
29
|
+
def outliers_IQR(self,param):
|
|
30
|
+
self.seperate()
|
|
31
|
+
Q1 = self.numerical_columns.quantile(0.25)
|
|
32
|
+
Q3 = self.numerical_columns.quantile(0.75)
|
|
33
|
+
IQR = Q3 - Q1
|
|
34
|
+
lower_bound = Q1 - 1.5 * IQR
|
|
35
|
+
upper_bound = Q3 + 1.5 * IQR
|
|
36
|
+
condition=((self.numerical_columns >=lower_bound) & (self.numerical_columns<=upper_bound)).all(axis=1)
|
|
37
|
+
if param=='r':
|
|
38
|
+
self.data = self.data.loc[condition]
|
|
39
|
+
self.numerical_columns = self.numerical_columns[condition]
|
|
40
|
+
return
|
|
41
|
+
if param=='m':
|
|
42
|
+
column_mean=self.numerical_columns.mean()
|
|
43
|
+
self.numerical_columns=self.numerical_columns.mask(~condition,column_mean,axis=1)
|
|
44
|
+
self.data[self.numerical_columns.columns] = self.numerical_columns
|
|
45
|
+
return
|
|
46
|
+
column_median=self.numerical_columns.median()
|
|
47
|
+
self.numerical_columns=self.numerical_columns.mask(~condition,column_median,axis=1)
|
|
48
|
+
self.data[self.numerical_columns.columns]=self.numerical_columns
|
|
49
|
+
def outliers_Z(self,param,threshold=None):
|
|
50
|
+
self.seperate()
|
|
51
|
+
mean=self.numerical_columns.mean()
|
|
52
|
+
x_diff_mean=(self.numerical_columns)-mean
|
|
53
|
+
std_dev=self.numerical_columns.std()
|
|
54
|
+
z_score=x_diff_mean/std_dev
|
|
55
|
+
t = threshold if threshold is not None else 3
|
|
56
|
+
is_valid = z_score.abs() < t
|
|
57
|
+
if param == 'r':
|
|
58
|
+
condition = is_valid.all(axis=1)
|
|
59
|
+
self.data = self.data.loc[condition]
|
|
60
|
+
self.numerical_columns = self.numerical_columns.loc[condition]
|
|
61
|
+
return
|
|
62
|
+
elif param == 'm':
|
|
63
|
+
self.numerical_columns = self.numerical_columns.mask(~is_valid, mean, axis=1)
|
|
64
|
+
self.data[self.numerical_columns.columns] = self.numerical_columns
|
|
65
|
+
return
|
|
66
|
+
column_median=self.numerical_columns.median()
|
|
67
|
+
self.numerical_columns=self.numerical_columns.mask(~is_valid,column_median,axis=1)
|
|
68
|
+
self.data[self.numerical_columns.columns]=self.numerical_columns
|
|
69
|
+
def Clean_string(self, data_time=None):
|
|
70
|
+
try:
|
|
71
|
+
self.seperate()
|
|
72
|
+
|
|
73
|
+
# Handle date time format from user: data_time=["column name of date time", format you want it to be] or else None
|
|
74
|
+
if data_time is not None:
|
|
75
|
+
col_name = data_time[0]
|
|
76
|
+
dt_format = data_time[1]
|
|
77
|
+
if col_name in self.data.columns:
|
|
78
|
+
parsed_dates = pd.to_datetime(self.data[col_name], format='mixed', errors='coerce')
|
|
79
|
+
self.data[col_name] = parsed_dates.dt.strftime(dt_format)
|
|
80
|
+
|
|
81
|
+
# Eliminate gaps between categories and lower them all for categorical columns
|
|
82
|
+
cat_cols = [c for c in self.categrorical_columns.columns if data_time is None or c != data_time[0]]
|
|
83
|
+
for col in cat_cols:
|
|
84
|
+
self.data[col] = self.data[col].astype(str).str.lower().str.replace(r"\s+", "", regex=True)
|
|
85
|
+
self.data[col] = self.data[col].mask(self.categrorical_columns[col].isna(), None)
|
|
86
|
+
self.categrorical_columns = self.data[self.categrorical_columns.columns]
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(e)
|
|
89
|
+
def delete_duplicates(self):
|
|
90
|
+
try:
|
|
91
|
+
self.load()
|
|
92
|
+
self.data = self.data.drop_duplicates()
|
|
93
|
+
self.seperate()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(e)
|
|
96
|
+
def fill_missing(self, param='m'):
|
|
97
|
+
try:
|
|
98
|
+
self.seperate()
|
|
99
|
+
if param in ['r', 'd', 'drop']:
|
|
100
|
+
self.data = self.data.dropna()
|
|
101
|
+
self.seperate()
|
|
102
|
+
return
|
|
103
|
+
if param in ['m', 'mean']:
|
|
104
|
+
fill_vals = self.numerical_columns.mean()
|
|
105
|
+
else:
|
|
106
|
+
fill_vals = self.numerical_columns.median()
|
|
107
|
+
self.numerical_columns = self.numerical_columns.fillna(fill_vals)
|
|
108
|
+
self.data[self.numerical_columns.columns] = self.numerical_columns
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(e)
|
|
111
|
+
def fill_categorical_missing(self, fill_val=None):
|
|
112
|
+
try:
|
|
113
|
+
self.seperate()
|
|
114
|
+
for col in self.categrorical_columns.columns:
|
|
115
|
+
val = fill_val if fill_val is not None else (self.data[col].mode()[0] if not self.data[col].mode().empty else "unknown")
|
|
116
|
+
self.data[col] = self.data[col].fillna(val)
|
|
117
|
+
self.categrorical_columns = self.data[self.categrorical_columns.columns]
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(e)
|
|
120
|
+
def clean_column_names(self):
|
|
121
|
+
try:
|
|
122
|
+
self.load()
|
|
123
|
+
self.data.columns = (
|
|
124
|
+
self.data.columns.str.strip()
|
|
125
|
+
.str.lower()
|
|
126
|
+
.str.replace(r"\s+", "_", regex=True)
|
|
127
|
+
.str.replace(r"[^\w_]", "", regex=True)
|
|
128
|
+
)
|
|
129
|
+
self.seperate()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(e)
|
|
132
|
+
def drop_uninformative(self, null_threshold=0.7):
|
|
133
|
+
try:
|
|
134
|
+
self.load()
|
|
135
|
+
null_pct = self.data.isnull().mean()
|
|
136
|
+
cols_to_drop = null_pct[null_pct > null_threshold].index.tolist()
|
|
137
|
+
zero_var = [col for col in self.data.columns if self.data[col].nunique(dropna=False) <= 1]
|
|
138
|
+
self.data = self.data.drop(columns=list(set(cols_to_drop + zero_var)))
|
|
139
|
+
self.seperate()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(e)
|
|
142
|
+
def clean_numeric_strings(self, columns):
|
|
143
|
+
try:
|
|
144
|
+
self.load()
|
|
145
|
+
for col in columns:
|
|
146
|
+
if col in self.data.columns:
|
|
147
|
+
cleaned = self.data[col].astype(str).str.replace(r"[\$,]", "", regex=True)
|
|
148
|
+
self.data[col] = pd.to_numeric(cleaned, errors="coerce")
|
|
149
|
+
self.seperate()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(e)
|
|
152
|
+
def low_corr(self, target=None, threshold=0.1):
|
|
153
|
+
try:
|
|
154
|
+
self.seperate()
|
|
155
|
+
corr_matrix = self.numerical_columns.corr().abs()
|
|
156
|
+
if target is not None:
|
|
157
|
+
if target in corr_matrix.columns:
|
|
158
|
+
low_corr_cols = corr_matrix[target][corr_matrix[target] < threshold].index.tolist()
|
|
159
|
+
self.data = self.data.drop(columns=low_corr_cols)
|
|
160
|
+
self.seperate()
|
|
161
|
+
else:
|
|
162
|
+
np.fill_diagonal(corr_matrix.values, 0)
|
|
163
|
+
low_corr_cols = corr_matrix.columns[(corr_matrix.max() < threshold)].tolist()
|
|
164
|
+
self.data = self.data.drop(columns=low_corr_cols)
|
|
165
|
+
self.seperate()
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(e)
|
|
168
|
+
def summary(self):
|
|
169
|
+
try:
|
|
170
|
+
self.load()
|
|
171
|
+
missing_cols = (self.data.isnull().sum() > 0).sum()
|
|
172
|
+
logger.info("Dataset summary: shape=%s, duplicates=%d, columns_with_missing=%d",
|
|
173
|
+
self.data.shape, self.data.duplicated().sum(), missing_cols)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error("Error in summary: %s", e)
|
|
176
|
+
def export(self, output_path):
|
|
177
|
+
try:
|
|
178
|
+
self.data.to_csv(output_path, index=False)
|
|
179
|
+
logger.info("Saved cleaned dataset to '%s'", output_path)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error("Error in export: %s", e)
|
|
182
|
+
def total_clean(
|
|
183
|
+
self,
|
|
184
|
+
data_time=None,
|
|
185
|
+
numeric_string_cols=None,
|
|
186
|
+
missing_param='m',
|
|
187
|
+
cat_fill_val=None,
|
|
188
|
+
outlier_method='IQR',
|
|
189
|
+
outlier_param='m',
|
|
190
|
+
outlier_threshold=None,
|
|
191
|
+
null_threshold=0.7,
|
|
192
|
+
corr_target=None,
|
|
193
|
+
corr_threshold=None,
|
|
194
|
+
export_path=None
|
|
195
|
+
):
|
|
196
|
+
try:
|
|
197
|
+
# 1. Load data
|
|
198
|
+
self.load()
|
|
199
|
+
|
|
200
|
+
# 2. Standardize column names
|
|
201
|
+
self.clean_column_names()
|
|
202
|
+
|
|
203
|
+
# Normalize user-passed column names to match cleaned column names if needed
|
|
204
|
+
def _normalize_col(c):
|
|
205
|
+
import re
|
|
206
|
+
return re.sub(r"[^\w_]", "", re.sub(r"\s+", "_", str(c).strip().lower()))
|
|
207
|
+
|
|
208
|
+
if data_time is not None:
|
|
209
|
+
c, fmt = data_time[0], data_time[1]
|
|
210
|
+
if c not in self.data.columns and _normalize_col(c) in self.data.columns:
|
|
211
|
+
data_time = [_normalize_col(c), fmt]
|
|
212
|
+
|
|
213
|
+
if numeric_string_cols:
|
|
214
|
+
numeric_string_cols = [
|
|
215
|
+
_normalize_col(c) if c not in self.data.columns and _normalize_col(c) in self.data.columns else c
|
|
216
|
+
for c in numeric_string_cols
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
if corr_target is not None:
|
|
220
|
+
if corr_target not in self.data.columns and _normalize_col(corr_target) in self.data.columns:
|
|
221
|
+
corr_target = _normalize_col(corr_target)
|
|
222
|
+
|
|
223
|
+
# 3. Remove duplicate rows
|
|
224
|
+
self.delete_duplicates()
|
|
225
|
+
|
|
226
|
+
# 4. Drop uninformative columns (high nulls or constant values)
|
|
227
|
+
if null_threshold is not None:
|
|
228
|
+
self.drop_uninformative(null_threshold=null_threshold)
|
|
229
|
+
|
|
230
|
+
# 5. Clean currency / formatted numeric strings into actual numbers
|
|
231
|
+
if numeric_string_cols:
|
|
232
|
+
self.clean_numeric_strings(numeric_string_cols)
|
|
233
|
+
|
|
234
|
+
# 6. Clean strings & format datetime
|
|
235
|
+
self.Clean_string(data_time=data_time)
|
|
236
|
+
|
|
237
|
+
# 7. Fill missing values (numerical)
|
|
238
|
+
self.fill_missing(param=missing_param)
|
|
239
|
+
|
|
240
|
+
# 8. Fill missing values (categorical)
|
|
241
|
+
self.fill_categorical_missing(fill_val=cat_fill_val)
|
|
242
|
+
|
|
243
|
+
# 9. Handle numerical outliers
|
|
244
|
+
if outlier_method == 'Z':
|
|
245
|
+
self.outliers_Z(param=outlier_param, threshold=outlier_threshold)
|
|
246
|
+
elif outlier_method == 'IQR':
|
|
247
|
+
self.outliers_IQR(param=outlier_param)
|
|
248
|
+
|
|
249
|
+
# 10. Drop low correlation columns if requested
|
|
250
|
+
if corr_threshold is not None:
|
|
251
|
+
self.low_corr(target=corr_target, threshold=corr_threshold)
|
|
252
|
+
|
|
253
|
+
# 11. Export if path specified
|
|
254
|
+
if export_path:
|
|
255
|
+
self.export(export_path)
|
|
256
|
+
|
|
257
|
+
return self.data
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print(e)
|
|
260
|
+
return self.data
|
|
261
|
+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
logger=logging.getLogger("DataGuard")
|
|
7
|
+
if not logger.handlers:
|
|
8
|
+
_h=logging.StreamHandler()
|
|
9
|
+
_h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
|
|
10
|
+
logger.addHandler(_h)
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataGuard:
|
|
15
|
+
def __init__(self, df=None):
|
|
16
|
+
self.data=df.copy() if df is not None else None
|
|
17
|
+
self.warnings=[]
|
|
18
|
+
self.id_cols=[]
|
|
19
|
+
self.leakage_cols=[]
|
|
20
|
+
self.high_cardinality_cols=[]
|
|
21
|
+
|
|
22
|
+
def set_data(self, df):
|
|
23
|
+
self.data=df.copy()
|
|
24
|
+
self.warnings=[]
|
|
25
|
+
|
|
26
|
+
# 1. Detect ID Columns
|
|
27
|
+
def check_id_columns(self):
|
|
28
|
+
try:
|
|
29
|
+
self.id_cols=[]
|
|
30
|
+
for col in self.data.columns:
|
|
31
|
+
unique_ratio=self.data[col].nunique(dropna=True) / len(self.data)
|
|
32
|
+
is_name_id=bool(re.search(r'(id|uuid|key|ssn|index|identifier)$', col.lower()))
|
|
33
|
+
|
|
34
|
+
if (unique_ratio > 0.95 and is_name_id) or (unique_ratio == 1.0 and self.data[col].dtype == 'object'):
|
|
35
|
+
self.id_cols.append(col)
|
|
36
|
+
logger.warning("Potential unique ID column detected: '%s' (uniqueness: %.1f%%)", col, unique_ratio * 100)
|
|
37
|
+
|
|
38
|
+
if self.id_cols:
|
|
39
|
+
self.warnings.append({
|
|
40
|
+
"Check": "ID Columns",
|
|
41
|
+
"Status": "WARN",
|
|
42
|
+
"Details": f"Columns {self.id_cols} appear to be unique IDs.",
|
|
43
|
+
"Fix": "Drop ID columns to prevent overfitting."
|
|
44
|
+
})
|
|
45
|
+
return self.id_cols
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error("Error in check_id_columns: %s", e)
|
|
48
|
+
|
|
49
|
+
# 2. Check Target Leakage
|
|
50
|
+
def check_target_leakage(self, target, threshold=0.95):
|
|
51
|
+
try:
|
|
52
|
+
self.leakage_cols=[]
|
|
53
|
+
if target not in self.data.columns:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
num_cols=self.data.select_dtypes(include='number')
|
|
57
|
+
if target in num_cols.columns:
|
|
58
|
+
corrs=num_cols.corr().abs()[target]
|
|
59
|
+
for col, val in corrs.items():
|
|
60
|
+
if col != target and val >= threshold:
|
|
61
|
+
self.leakage_cols.append((col, round(val, 4)))
|
|
62
|
+
logger.error("Suspicious target leakage: feature '%s' has correlation %.4f with target '%s'", col, val, target)
|
|
63
|
+
|
|
64
|
+
if self.leakage_cols:
|
|
65
|
+
self.warnings.append({
|
|
66
|
+
"Check": "Target Leakage",
|
|
67
|
+
"Status": "FAIL",
|
|
68
|
+
"Details": f"Features {self.leakage_cols} have correlation >= {threshold} with target '{target}'.",
|
|
69
|
+
"Fix": "Verify if these columns contain future data."
|
|
70
|
+
})
|
|
71
|
+
return self.leakage_cols
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error("Error in check_target_leakage: %s", e)
|
|
74
|
+
|
|
75
|
+
# 3. Check Class Imbalance
|
|
76
|
+
def check_class_imbalance(self, target):
|
|
77
|
+
try:
|
|
78
|
+
if target not in self.data.columns:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
counts=self.data[target].value_counts(normalize=True)
|
|
82
|
+
if len(counts) <= 20: # Classification
|
|
83
|
+
max_prop=counts.max()
|
|
84
|
+
min_prop=counts.min()
|
|
85
|
+
imbalance_ratio=round(max_prop / min_prop, 2)
|
|
86
|
+
|
|
87
|
+
if max_prop >= 0.70:
|
|
88
|
+
status="WARN" if max_prop < 0.90 else "FAIL"
|
|
89
|
+
logger.warning("Class imbalance in '%s': dominant class is %.1f%% (ratio %.2f:1)", target, max_prop * 100, imbalance_ratio)
|
|
90
|
+
self.warnings.append({
|
|
91
|
+
"Check": "Class Imbalance",
|
|
92
|
+
"Status": status,
|
|
93
|
+
"Details": f"Dominant class represents {max_prop:.1%} (ratio: {imbalance_ratio}:1).",
|
|
94
|
+
"Fix": "Use class_weight='balanced' or resampling."
|
|
95
|
+
})
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("Error in check_class_imbalance: %s", e)
|
|
98
|
+
|
|
99
|
+
# 4. Check High-Cardinality Categoricals
|
|
100
|
+
def check_cardinality(self, threshold=30):
|
|
101
|
+
try:
|
|
102
|
+
self.high_cardinality_cols=[]
|
|
103
|
+
cat_cols=self.data.select_dtypes(include=['object', 'category']).columns
|
|
104
|
+
|
|
105
|
+
for col in cat_cols:
|
|
106
|
+
n_unique=self.data[col].nunique()
|
|
107
|
+
if n_unique > threshold:
|
|
108
|
+
self.high_cardinality_cols.append((col, n_unique))
|
|
109
|
+
logger.warning("High cardinality in category '%s': %d unique values", col, n_unique)
|
|
110
|
+
|
|
111
|
+
if self.high_cardinality_cols:
|
|
112
|
+
self.warnings.append({
|
|
113
|
+
"Check": "High Cardinality",
|
|
114
|
+
"Status": "WARN",
|
|
115
|
+
"Details": f"Columns {self.high_cardinality_cols} exceed {threshold} unique values.",
|
|
116
|
+
"Fix": "Use label encoding or group rare categories."
|
|
117
|
+
})
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error("Error in check_cardinality: %s", e)
|
|
120
|
+
|
|
121
|
+
# 5. Check Infinite Values
|
|
122
|
+
def check_infinite_values(self):
|
|
123
|
+
try:
|
|
124
|
+
num_cols=self.data.select_dtypes(include='number').columns
|
|
125
|
+
inf_cols=[]
|
|
126
|
+
for col in num_cols:
|
|
127
|
+
if np.isinf(self.data[col]).any():
|
|
128
|
+
inf_cols.append(col)
|
|
129
|
+
logger.error("Infinite values detected in column '%s'", col)
|
|
130
|
+
|
|
131
|
+
if inf_cols:
|
|
132
|
+
self.warnings.append({
|
|
133
|
+
"Check": "Infinite Values",
|
|
134
|
+
"Status": "FAIL",
|
|
135
|
+
"Details": f"Columns {inf_cols} contain inf / -inf.",
|
|
136
|
+
"Fix": "Replace inf values with NaN or bounds."
|
|
137
|
+
})
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error("Error in check_infinite_values: %s", e)
|
|
140
|
+
|
|
141
|
+
# ==========================
|
|
142
|
+
# Master Health Audit Runner
|
|
143
|
+
# ==========================
|
|
144
|
+
def run_audit(self, target=None):
|
|
145
|
+
try:
|
|
146
|
+
self.warnings=[]
|
|
147
|
+
logger.info("Running data quality audit on %d rows, %d columns", len(self.data), len(self.data.columns))
|
|
148
|
+
|
|
149
|
+
self.check_id_columns()
|
|
150
|
+
self.check_infinite_values()
|
|
151
|
+
self.check_cardinality()
|
|
152
|
+
|
|
153
|
+
if target:
|
|
154
|
+
self.check_target_leakage(target)
|
|
155
|
+
self.check_class_imbalance(target)
|
|
156
|
+
|
|
157
|
+
if not self.warnings:
|
|
158
|
+
logger.info("Quality audit completed: all checks passed")
|
|
159
|
+
return pd.DataFrame([{"Check": "All Checks", "Status": "PASS", "Details": "No issues found", "Fix": "None"}])
|
|
160
|
+
|
|
161
|
+
report_df=pd.DataFrame(self.warnings)
|
|
162
|
+
logger.info("Quality audit completed: %d warnings/failures flagged", len(self.warnings))
|
|
163
|
+
return report_df
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error("Error in run_audit: %s", e)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.stats import ks_2samp, chi2_contingency
|
|
5
|
+
|
|
6
|
+
logger=logging.getLogger("DriftDetector")
|
|
7
|
+
if not logger.handlers:
|
|
8
|
+
_h=logging.StreamHandler()
|
|
9
|
+
_h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
|
|
10
|
+
logger.addHandler(_h)
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DriftDetector:
|
|
15
|
+
def __init__(self, reference_data, alpha=0.05):
|
|
16
|
+
"""
|
|
17
|
+
reference_data: Historical baseline (training data)
|
|
18
|
+
alpha: Significance threshold for statistical hypothesis tests (default: 0.05)
|
|
19
|
+
"""
|
|
20
|
+
self.ref_data=reference_data.copy() if isinstance(reference_data, pd.DataFrame) else pd.DataFrame(reference_data)
|
|
21
|
+
self.alpha=alpha
|
|
22
|
+
self.drift_results=[]
|
|
23
|
+
|
|
24
|
+
def detect_drift(self, current_data):
|
|
25
|
+
"""
|
|
26
|
+
Compares incoming production data against reference data:
|
|
27
|
+
- Numerical features: Two-Sample Kolmogorov-Smirnov test (ks_2samp)
|
|
28
|
+
- Categorical features: Chi-Square Contingency test
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
cur_data=current_data.copy() if isinstance(current_data, pd.DataFrame) else pd.DataFrame(current_data)
|
|
32
|
+
self.drift_results=[]
|
|
33
|
+
drift_count=0
|
|
34
|
+
|
|
35
|
+
common_cols=[c for c in self.ref_data.columns if c in cur_data.columns]
|
|
36
|
+
logger.info("Running drift detection across %d features (alpha=%.3f)", len(common_cols), self.alpha)
|
|
37
|
+
|
|
38
|
+
for col in common_cols:
|
|
39
|
+
# 1. Numerical Drift via Kolmogorov-Smirnov test
|
|
40
|
+
if pd.api.types.is_numeric_dtype(self.ref_data[col]):
|
|
41
|
+
ref_vals=self.ref_data[col].dropna()
|
|
42
|
+
cur_vals=cur_data[col].dropna()
|
|
43
|
+
|
|
44
|
+
if len(ref_vals) > 0 and len(cur_vals) > 0:
|
|
45
|
+
stat, p_val = ks_2samp(ref_vals, cur_vals)
|
|
46
|
+
is_drift = p_val < self.alpha
|
|
47
|
+
if is_drift:
|
|
48
|
+
drift_count += 1
|
|
49
|
+
logger.warning("Data drift detected in '%s' (KS=%.4f, p-value=%.5f)", col, stat, p_val)
|
|
50
|
+
|
|
51
|
+
self.drift_results.append({
|
|
52
|
+
"Feature": col,
|
|
53
|
+
"Type": "Numerical",
|
|
54
|
+
"Statistic": round(stat, 4),
|
|
55
|
+
"P_Value": round(p_val, 5),
|
|
56
|
+
"Drift_Detected": is_drift
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# 2. Categorical Drift via Chi-Square test
|
|
60
|
+
else:
|
|
61
|
+
ref_counts=self.ref_data[col].astype(str).value_counts()
|
|
62
|
+
cur_counts=cur_data[col].astype(str).value_counts()
|
|
63
|
+
|
|
64
|
+
# Align categories
|
|
65
|
+
all_cats=list(set(ref_counts.index).union(set(cur_counts.index)))
|
|
66
|
+
ref_aligned=[ref_counts.get(cat, 0) for cat in all_cats]
|
|
67
|
+
cur_aligned=[cur_counts.get(cat, 0) for cat in all_cats]
|
|
68
|
+
|
|
69
|
+
contingency_table=np.array([ref_aligned, cur_aligned])
|
|
70
|
+
if contingency_table.sum() > 0:
|
|
71
|
+
stat, p_val, _, _ = chi2_contingency(contingency_table)
|
|
72
|
+
is_drift = p_val < self.alpha
|
|
73
|
+
if is_drift:
|
|
74
|
+
drift_count += 1
|
|
75
|
+
logger.warning("Categorical shift detected in '%s' (Chi2=%.4f, p-value=%.5f)", col, stat, p_val)
|
|
76
|
+
|
|
77
|
+
self.drift_results.append({
|
|
78
|
+
"Feature": col,
|
|
79
|
+
"Type": "Categorical",
|
|
80
|
+
"Statistic": round(stat, 4),
|
|
81
|
+
"P_Value": round(p_val, 5),
|
|
82
|
+
"Drift_Detected": is_drift
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
report_df=pd.DataFrame(self.drift_results)
|
|
86
|
+
logger.info("Drift analysis complete: %d/%d features drifted", drift_count, len(common_cols))
|
|
87
|
+
return report_df
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error("Error in detect_drift: %s", e)
|
|
90
|
+
return pd.DataFrame()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from sklearn.ensemble import VotingClassifier, VotingRegressor, StackingClassifier, StackingRegressor
|
|
6
|
+
from sklearn.linear_model import LogisticRegression, Ridge
|
|
7
|
+
from sklearn.model_selection import cross_val_score, StratifiedKFold, KFold
|
|
8
|
+
|
|
9
|
+
logger=logging.getLogger("EnsembleBuilder")
|
|
10
|
+
if not logger.handlers:
|
|
11
|
+
_h=logging.StreamHandler()
|
|
12
|
+
_h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
|
|
13
|
+
logger.addHandler(_h)
|
|
14
|
+
logger.setLevel(logging.INFO)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnsembleBuilder:
|
|
18
|
+
"""
|
|
19
|
+
Automated Model Ensembling (Voting & Stacking):
|
|
20
|
+
Combines diverse algorithms to improve predictive performance and reduce variance.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, task="classification"):
|
|
24
|
+
self.task=task.lower()
|
|
25
|
+
|
|
26
|
+
def build_voting_ensemble(self, estimators, voting="soft"):
|
|
27
|
+
"""
|
|
28
|
+
Builds a Voting Ensemble:
|
|
29
|
+
Averages predictions across multiple models (soft = probability-weighted, hard = majority vote).
|
|
30
|
+
estimators: list of (name, model) tuples, e.g. [('rf', rf), ('gb', gb), ('lr', lr)]
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
logger.info("Building Voting Ensemble (%d estimators, mode=%s)", len(estimators), voting)
|
|
34
|
+
if self.task == "classification":
|
|
35
|
+
ensemble = VotingClassifier(estimators=estimators, voting=voting)
|
|
36
|
+
else:
|
|
37
|
+
ensemble = VotingRegressor(estimators=estimators)
|
|
38
|
+
return ensemble
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error("Error in build_voting_ensemble: %s", e)
|
|
41
|
+
|
|
42
|
+
def build_stacking_ensemble(self, estimators, final_estimator=None, cv=5):
|
|
43
|
+
"""
|
|
44
|
+
Builds a Stacking Ensemble:
|
|
45
|
+
Uses a meta-learner to learn how to best combine predictions from base models.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
logger.info("Building Stacking Ensemble (%d base estimators, cv=%d)", len(estimators), cv)
|
|
49
|
+
if self.task == "classification":
|
|
50
|
+
meta = final_estimator or LogisticRegression(max_iter=1000)
|
|
51
|
+
ensemble = StackingClassifier(estimators=estimators, final_estimator=meta, cv=cv, n_jobs=-1)
|
|
52
|
+
else:
|
|
53
|
+
meta = final_estimator or Ridge()
|
|
54
|
+
ensemble = StackingRegressor(estimators=estimators, final_estimator=meta, cv=cv, n_jobs=-1)
|
|
55
|
+
return ensemble
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error("Error in build_stacking_ensemble: %s", e)
|
|
58
|
+
|
|
59
|
+
def evaluate_ensemble(self, ensemble, X, y, cv=5):
|
|
60
|
+
"""Evaluates an ensemble model using cross-validation."""
|
|
61
|
+
try:
|
|
62
|
+
if not isinstance(y, pd.Series):
|
|
63
|
+
y = pd.Series(y)
|
|
64
|
+
|
|
65
|
+
if self.task == "classification":
|
|
66
|
+
kf = StratifiedKFold(n_splits=cv, shuffle=True, random_state=42)
|
|
67
|
+
acc = cross_val_score(ensemble, X, y, cv=kf, scoring="accuracy")
|
|
68
|
+
f1 = cross_val_score(ensemble, X, y, cv=kf, scoring="f1_weighted")
|
|
69
|
+
logger.info("Ensemble CV Accuracy: %.4f (+/- %.4f) | F1: %.4f", acc.mean(), acc.std(), f1.mean())
|
|
70
|
+
return {"Accuracy": round(acc.mean(), 4), "Std_Accuracy": round(acc.std(), 4), "F1": round(f1.mean(), 4)}
|
|
71
|
+
else:
|
|
72
|
+
kf = KFold(n_splits=cv, shuffle=True, random_state=42)
|
|
73
|
+
r2 = cross_val_score(ensemble, X, y, cv=kf, scoring="r2")
|
|
74
|
+
rmse = cross_val_score(ensemble, X, y, cv=kf, scoring="neg_root_mean_squared_error")
|
|
75
|
+
logger.info("Ensemble CV R2: %.4f (+/- %.4f) | RMSE: %.4f", r2.mean(), r2.std(), -rmse.mean())
|
|
76
|
+
return {"R2": round(r2.mean(), 4), "Std_R2": round(r2.std(), 4), "RMSE": round(-rmse.mean(), 4)}
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error("Error in evaluate_ensemble: %s", e)
|