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.
@@ -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)