data-advisor 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.
- data_advisor/__init__.py +38 -0
- data_advisor/__main__.py +9 -0
- data_advisor/advisor.py +436 -0
- data_advisor/cli.py +160 -0
- data_advisor/codegen.py +515 -0
- data_advisor/rules.py +380 -0
- data_advisor/scanner.py +655 -0
- data_advisor/search.py +679 -0
- data_advisor/visualizer.py +236 -0
- data_advisor-0.1.0.dist-info/METADATA +303 -0
- data_advisor-0.1.0.dist-info/RECORD +14 -0
- data_advisor-0.1.0.dist-info/WHEEL +5 -0
- data_advisor-0.1.0.dist-info/entry_points.txt +2 -0
- data_advisor-0.1.0.dist-info/top_level.txt +1 -0
data_advisor/codegen.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codegen.py - Generates copy-pasteable, robust, executable Python scripts for timed exams.
|
|
3
|
+
|
|
4
|
+
Produces clean, PEP 8 compliant, production-grade Pandas, Scipy, and Scikit-Learn code
|
|
5
|
+
tailored specifically to the audited dataset.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
from data_advisor.visualizer import VisualizationAdvisor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeGenerator:
|
|
15
|
+
"""
|
|
16
|
+
Translates dataset scan and rule evaluations into sequential,
|
|
17
|
+
modular, executable Python code blocks and full pipeline scripts.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, scan_results: Dict[str, Any], data_source_str: str = "dataset.csv") -> None:
|
|
21
|
+
self.scan = scan_results
|
|
22
|
+
self.source = data_source_str
|
|
23
|
+
self.overview = scan_results.get("overview", {})
|
|
24
|
+
self.missing = scan_results.get("missing_values", {})
|
|
25
|
+
self.dtypes = scan_results.get("data_types", {})
|
|
26
|
+
self.distributions = scan_results.get("distributions", {})
|
|
27
|
+
self.categoricals = scan_results.get("categoricals", {})
|
|
28
|
+
self.target = scan_results.get("target")
|
|
29
|
+
self.hypothesis_tests = scan_results.get("hypothesis_tests", [])
|
|
30
|
+
|
|
31
|
+
# -------------------------------------------------------------------------
|
|
32
|
+
# 1. Environment & Ingestion Block
|
|
33
|
+
# -------------------------------------------------------------------------
|
|
34
|
+
def generate_imports_and_loading(self) -> str:
|
|
35
|
+
return f'''# =============================================================================
|
|
36
|
+
# STEP 1: ENVIRONMENT SETUP & DATA INGESTION
|
|
37
|
+
# =============================================================================
|
|
38
|
+
import numpy as np
|
|
39
|
+
import pandas as pd
|
|
40
|
+
import scipy.stats as stats
|
|
41
|
+
import matplotlib.pyplot as plt
|
|
42
|
+
import seaborn as sns
|
|
43
|
+
|
|
44
|
+
from sklearn.model_selection import train_test_split, cross_val_score, KFold, StratifiedKFold
|
|
45
|
+
from sklearn.preprocessing import StandardScaler, RobustScaler, OneHotEncoder, PowerTransformer
|
|
46
|
+
from sklearn.impute import SimpleImputer, KNNImputer
|
|
47
|
+
|
|
48
|
+
# Set presentation styles
|
|
49
|
+
pd.set_option('display.max_columns', 50)
|
|
50
|
+
pd.set_option('display.width', 1000)
|
|
51
|
+
sns.set_theme(style='whitegrid', palette='muted')
|
|
52
|
+
|
|
53
|
+
# Load the dataset
|
|
54
|
+
DATA_PATH = {repr(self.source)}
|
|
55
|
+
print(f"[*] Ingesting dataset from: {{DATA_PATH}}")
|
|
56
|
+
df = pd.read_csv(DATA_PATH) if DATA_PATH.endswith('.csv') else pd.read_excel(DATA_PATH)
|
|
57
|
+
|
|
58
|
+
print(f"[*] Raw Shape: {{df.shape[0]}} rows, {{df.shape[1]}} columns")
|
|
59
|
+
df.info()
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
# -------------------------------------------------------------------------
|
|
63
|
+
# 2. Data Cleaning Block
|
|
64
|
+
# -------------------------------------------------------------------------
|
|
65
|
+
def generate_cleaning_code(self) -> str:
|
|
66
|
+
lines = [
|
|
67
|
+
"# =============================================================================",
|
|
68
|
+
"# STEP 2: DATA CLEANING & AUDITING",
|
|
69
|
+
"# =============================================================================",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Duplicate Handling
|
|
73
|
+
dup_count = self.overview.get("duplicate_rows", 0)
|
|
74
|
+
if dup_count > 0:
|
|
75
|
+
lines.extend([
|
|
76
|
+
f"# Remove {dup_count} duplicated rows",
|
|
77
|
+
"initial_rows = len(df)",
|
|
78
|
+
"df = df.drop_duplicates().reset_index(drop=True)",
|
|
79
|
+
"print(f'[*] Deduplicated dataset: dropped {initial_rows - len(df)} duplicate rows.')",
|
|
80
|
+
"",
|
|
81
|
+
])
|
|
82
|
+
else:
|
|
83
|
+
lines.extend([
|
|
84
|
+
"# Verification: No duplicate rows detected",
|
|
85
|
+
"assert df.duplicated().sum() == 0, 'Unexpected duplicates found!'",
|
|
86
|
+
"",
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
# Drop Constant Columns
|
|
90
|
+
constant_cols = self.dtypes.get("constant_columns", [])
|
|
91
|
+
if constant_cols:
|
|
92
|
+
lines.extend([
|
|
93
|
+
f"# Drop zero-variance (constant) columns: {constant_cols}",
|
|
94
|
+
f"df = df.drop(columns={repr(constant_cols)})",
|
|
95
|
+
f'print(f"[*] Dropped constant columns: {constant_cols}")',
|
|
96
|
+
"",
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
# Drop High-Missing Columns (>60%)
|
|
100
|
+
high_missing_cols = [
|
|
101
|
+
c for c, info in self.missing.items() if info.get("null_pct", 0) >= 60.0
|
|
102
|
+
]
|
|
103
|
+
if high_missing_cols:
|
|
104
|
+
lines.extend([
|
|
105
|
+
f"# Drop high-sparsity columns (>60% missing): {high_missing_cols}",
|
|
106
|
+
f"df = df.drop(columns={repr(high_missing_cols)})",
|
|
107
|
+
f'print(f"[*] Dropped sparse columns: {high_missing_cols}")',
|
|
108
|
+
"",
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
# Date conversions & feature extraction
|
|
112
|
+
date_cols = self.dtypes.get("potential_dates", [])
|
|
113
|
+
if date_cols:
|
|
114
|
+
lines.extend([
|
|
115
|
+
"# Parse dates and extract temporal features",
|
|
116
|
+
])
|
|
117
|
+
for d_col in date_cols:
|
|
118
|
+
lines.extend([
|
|
119
|
+
f"df['{d_col}'] = pd.to_datetime(df['{d_col}'], errors='coerce')",
|
|
120
|
+
f"df['{d_col}_year'] = df['{d_col}'].dt.year",
|
|
121
|
+
f"df['{d_col}_month'] = df['{d_col}'].dt.month",
|
|
122
|
+
f"df['{d_col}_day'] = df['{d_col}'].dt.day",
|
|
123
|
+
f"df['{d_col}_dayofweek'] = df['{d_col}'].dt.dayofweek",
|
|
124
|
+
f"df = df.drop(columns=['{d_col}']) # Drop raw timestamp string",
|
|
125
|
+
])
|
|
126
|
+
lines.append("")
|
|
127
|
+
|
|
128
|
+
# Missing Value Imputation
|
|
129
|
+
numeric_imputes_median = []
|
|
130
|
+
numeric_imputes_mean = []
|
|
131
|
+
cat_imputes_mode = []
|
|
132
|
+
|
|
133
|
+
for c, info in self.missing.items():
|
|
134
|
+
pct = info.get("null_pct", 0)
|
|
135
|
+
if 0 < pct < 60.0 and c not in high_missing_cols:
|
|
136
|
+
if info.get("is_numeric", False):
|
|
137
|
+
skew = abs(self.distributions.get(c, {}).get("skewness", 0.0))
|
|
138
|
+
if skew > 1.0:
|
|
139
|
+
numeric_imputes_median.append(c)
|
|
140
|
+
else:
|
|
141
|
+
numeric_imputes_mean.append(c)
|
|
142
|
+
else:
|
|
143
|
+
cat_imputes_mode.append(c)
|
|
144
|
+
|
|
145
|
+
if numeric_imputes_median or numeric_imputes_mean or cat_imputes_mode:
|
|
146
|
+
lines.append("# Missing Value Imputation")
|
|
147
|
+
if numeric_imputes_median:
|
|
148
|
+
lines.extend([
|
|
149
|
+
f"# Median imputation for skewed numeric features: {numeric_imputes_median}",
|
|
150
|
+
f"for col in {repr(numeric_imputes_median)}:",
|
|
151
|
+
" df[col] = df[col].fillna(df[col].median())",
|
|
152
|
+
])
|
|
153
|
+
if numeric_imputes_mean:
|
|
154
|
+
lines.extend([
|
|
155
|
+
f"# Mean imputation for normal numeric features: {numeric_imputes_mean}",
|
|
156
|
+
f"for col in {repr(numeric_imputes_mean)}:",
|
|
157
|
+
" df[col] = df[col].fillna(df[col].mean())",
|
|
158
|
+
])
|
|
159
|
+
if cat_imputes_mode:
|
|
160
|
+
lines.extend([
|
|
161
|
+
f"# Mode imputation for categorical features: {cat_imputes_mode}",
|
|
162
|
+
f"for col in {repr(cat_imputes_mode)}:",
|
|
163
|
+
" mode_val = df[col].mode()[0] if not df[col].mode().empty else 'Missing'",
|
|
164
|
+
" df[col] = df[col].fillna(mode_val)",
|
|
165
|
+
])
|
|
166
|
+
lines.extend([
|
|
167
|
+
"print(f'[*] Remaining missing values: {df.isna().sum().sum()}')",
|
|
168
|
+
"",
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
# -------------------------------------------------------------------------
|
|
174
|
+
# 3. Distribution Transformations & Outlier Code
|
|
175
|
+
# -------------------------------------------------------------------------
|
|
176
|
+
def generate_transformations_code(self) -> str:
|
|
177
|
+
lines = [
|
|
178
|
+
"# =============================================================================",
|
|
179
|
+
"# STEP 3: DISTRIBUTION TRANSFORMATIONS & OUTLIER HANDLING",
|
|
180
|
+
"# =============================================================================",
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
has_transforms = False
|
|
184
|
+
for col, dist in self.distributions.items():
|
|
185
|
+
skew = dist.get("skewness", 0.0)
|
|
186
|
+
is_non_neg = dist.get("is_non_negative", True)
|
|
187
|
+
iqr_pct = dist.get("iqr_outlier_pct", 0.0)
|
|
188
|
+
lf = dist.get("lower_fence", 0.0)
|
|
189
|
+
uf = dist.get("upper_fence", 0.0)
|
|
190
|
+
|
|
191
|
+
# High skewness
|
|
192
|
+
if skew > 1.2 and is_non_neg:
|
|
193
|
+
has_transforms = True
|
|
194
|
+
lines.extend([
|
|
195
|
+
f"# Log1p transform for right-skewed column '{col}' (skewness={skew:.2f})",
|
|
196
|
+
f"df['{col}'] = np.log1p(df['{col}'])",
|
|
197
|
+
])
|
|
198
|
+
elif abs(skew) > 1.2 and not is_non_neg:
|
|
199
|
+
has_transforms = True
|
|
200
|
+
lines.extend([
|
|
201
|
+
f"# Yeo-Johnson transform for skewed column '{col}' containing negative values",
|
|
202
|
+
"pt = PowerTransformer(method='yeo-johnson')",
|
|
203
|
+
f"df['{col}'] = pt.fit_transform(df[['{col}']])",
|
|
204
|
+
])
|
|
205
|
+
|
|
206
|
+
# Extreme outliers clipping
|
|
207
|
+
if iqr_pct > 5.0:
|
|
208
|
+
has_transforms = True
|
|
209
|
+
lines.extend([
|
|
210
|
+
f"# Winsorize/Clip extreme outliers for '{col}' at IQR fences [{lf:.2f}, {uf:.2f}]",
|
|
211
|
+
f"df['{col}'] = df['{col}'].clip(lower={lf}, upper={uf})",
|
|
212
|
+
])
|
|
213
|
+
|
|
214
|
+
if not has_transforms:
|
|
215
|
+
lines.append("# No severe skewness or extreme outliers requiring destructive transformation.\n")
|
|
216
|
+
else:
|
|
217
|
+
lines.append("print('[*] Distribution normalization and outlier treatment applied.')\n")
|
|
218
|
+
|
|
219
|
+
return "\n".join(lines)
|
|
220
|
+
|
|
221
|
+
# -------------------------------------------------------------------------
|
|
222
|
+
# 4. Categorical Encoding Block
|
|
223
|
+
# -------------------------------------------------------------------------
|
|
224
|
+
def generate_encoding_code(self) -> str:
|
|
225
|
+
lines = [
|
|
226
|
+
"# =============================================================================",
|
|
227
|
+
"# STEP 4: CATEGORICAL FEATURE ENCODING",
|
|
228
|
+
"# =============================================================================",
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
target_col = self.target.get("target_column") if self.target else None
|
|
232
|
+
excluded_cols = set(
|
|
233
|
+
list(self.dtypes.get("potential_dates", []))
|
|
234
|
+
+ list(self.dtypes.get("constant_columns", []))
|
|
235
|
+
+ list(self.dtypes.get("potential_ids", []))
|
|
236
|
+
+ [c for c, info in self.missing.items() if info.get("null_pct", 0) >= 60.0]
|
|
237
|
+
)
|
|
238
|
+
if target_col:
|
|
239
|
+
excluded_cols.add(target_col)
|
|
240
|
+
|
|
241
|
+
binary_cols = []
|
|
242
|
+
low_card_cols = []
|
|
243
|
+
high_card_cols = []
|
|
244
|
+
|
|
245
|
+
for col, cat in self.categoricals.items():
|
|
246
|
+
if col in excluded_cols:
|
|
247
|
+
continue
|
|
248
|
+
cardinality = cat.get("cardinality", 0)
|
|
249
|
+
if cardinality == 2:
|
|
250
|
+
binary_cols.append(col)
|
|
251
|
+
elif 3 <= cardinality <= 10:
|
|
252
|
+
low_card_cols.append(col)
|
|
253
|
+
elif cardinality > 10:
|
|
254
|
+
high_card_cols.append(col)
|
|
255
|
+
|
|
256
|
+
if binary_cols:
|
|
257
|
+
lines.extend([
|
|
258
|
+
f"# Binary 0/1 Encoding for binary features: {binary_cols}",
|
|
259
|
+
f"for col in {repr(binary_cols)}:",
|
|
260
|
+
" if col in df.columns:",
|
|
261
|
+
" unique_vals = df[col].dropna().unique()",
|
|
262
|
+
" if len(unique_vals) == 2:",
|
|
263
|
+
" val_map = {unique_vals[0]: 0, unique_vals[1]: 1}",
|
|
264
|
+
" df[col] = df[col].map(val_map)",
|
|
265
|
+
' print(f"[*] Mapped {col}: {val_map}")',
|
|
266
|
+
"",
|
|
267
|
+
])
|
|
268
|
+
|
|
269
|
+
if low_card_cols:
|
|
270
|
+
lines.extend([
|
|
271
|
+
f"# One-Hot Encoding for low-cardinality features: {low_card_cols}",
|
|
272
|
+
f"ohe_cols = [c for c in {repr(low_card_cols)} if c in df.columns]",
|
|
273
|
+
"if ohe_cols:",
|
|
274
|
+
" df = pd.get_dummies(df, columns=ohe_cols, drop_first=True, dtype=int)",
|
|
275
|
+
" print(f'[*] Applied One-Hot Encoding on: {ohe_cols}')",
|
|
276
|
+
"",
|
|
277
|
+
])
|
|
278
|
+
|
|
279
|
+
if high_card_cols:
|
|
280
|
+
lines.extend([
|
|
281
|
+
f"# Frequency Encoding for high-cardinality features: {high_card_cols}",
|
|
282
|
+
f"for col in {repr(high_card_cols)}:",
|
|
283
|
+
" if col in df.columns:",
|
|
284
|
+
" freq_encoding = df[col].value_counts(normalize=True)",
|
|
285
|
+
" df[col] = df[col].map(freq_encoding)",
|
|
286
|
+
' print(f"[*] Applied Frequency Encoding on {col}")',
|
|
287
|
+
"",
|
|
288
|
+
])
|
|
289
|
+
|
|
290
|
+
if not (binary_cols or low_card_cols or high_card_cols):
|
|
291
|
+
lines.append("# No categorical columns requiring encoding.\n")
|
|
292
|
+
|
|
293
|
+
return "\n".join(lines)
|
|
294
|
+
|
|
295
|
+
# -------------------------------------------------------------------------
|
|
296
|
+
# 5. Hypothesis Testing Block
|
|
297
|
+
# -------------------------------------------------------------------------
|
|
298
|
+
def generate_hypothesis_testing_code(self) -> str:
|
|
299
|
+
lines = [
|
|
300
|
+
"# =============================================================================",
|
|
301
|
+
"# STEP 5: HYPOTHESIS TESTING & STATISTICAL INFERENCE",
|
|
302
|
+
"# =============================================================================",
|
|
303
|
+
"print('\\n' + '='*70)",
|
|
304
|
+
"print(' STATISTICAL HYPOTHESIS TESTING RESULTS')",
|
|
305
|
+
"print('='*70)",
|
|
306
|
+
"",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
if not self.hypothesis_tests:
|
|
310
|
+
lines.append("# No automated hypothesis tests generated.")
|
|
311
|
+
return "\n".join(lines)
|
|
312
|
+
|
|
313
|
+
for idx, test in enumerate(self.hypothesis_tests, 1):
|
|
314
|
+
f1 = test.get("feature_1")
|
|
315
|
+
f2 = test.get("feature_2")
|
|
316
|
+
test_type = test.get("test_type")
|
|
317
|
+
h0 = test.get("h0")
|
|
318
|
+
h1 = test.get("h1")
|
|
319
|
+
|
|
320
|
+
lines.extend([
|
|
321
|
+
f"# Test {idx}: {test_type} ({f1} vs {f2})",
|
|
322
|
+
f"if {repr(f1)} in df.columns and {repr(f2)} in df.columns:",
|
|
323
|
+
f" print('\\n' + '-'*50)",
|
|
324
|
+
f" print({repr(f'[TEST {idx}] {test_type}: {f1} vs {f2}')})",
|
|
325
|
+
f" print({repr(f'H0: {h0}')})",
|
|
326
|
+
f" print({repr(f'H1: {h1}')})",
|
|
327
|
+
])
|
|
328
|
+
|
|
329
|
+
if "Correlation" in test_type:
|
|
330
|
+
lines.extend([
|
|
331
|
+
f" valid_data = df[[{repr(f1)}, {repr(f2)}]].dropna()",
|
|
332
|
+
f" r, p_val = stats.pearsonr(valid_data[{repr(f1)}], valid_data[{repr(f2)}])",
|
|
333
|
+
f" rho, spearman_pval = stats.spearmanr(valid_data[{repr(f1)}], valid_data[{repr(f2)}])",
|
|
334
|
+
" print(f'Pearson r: {r:.4f} (p-value: {p_val:.4e})')",
|
|
335
|
+
" print(f'Spearman rho: {rho:.4f} (p-value: {spearman_pval:.4e})')",
|
|
336
|
+
" if p_val < 0.05:",
|
|
337
|
+
" print('=> CONCLUSION: Reject H0. Statistically significant correlation detected.')",
|
|
338
|
+
" else:",
|
|
339
|
+
" print('=> CONCLUSION: Fail to reject H0. No significant linear correlation.')",
|
|
340
|
+
])
|
|
341
|
+
elif "t-test" in test_type or "Mann-Whitney" in test_type:
|
|
342
|
+
lines.extend([
|
|
343
|
+
f" valid_data = df[[{repr(f1)}, {repr(f2)}]].dropna()",
|
|
344
|
+
f" classes = valid_data[{repr(f2)}].unique()",
|
|
345
|
+
f" if len(classes) >= 2:",
|
|
346
|
+
f" g1 = valid_data[valid_data[{repr(f2)}] == classes[0]][{repr(f1)}]",
|
|
347
|
+
f" g2 = valid_data[valid_data[{repr(f2)}] == classes[1]][{repr(f1)}]",
|
|
348
|
+
" t_stat, p_val = stats.ttest_ind(g1, g2, equal_var=False)",
|
|
349
|
+
" u_stat, u_pval = stats.mannwhitneyu(g1, g2, alternative='two-sided')",
|
|
350
|
+
" print(f'Welch t-test: t={t_stat:.4f}, p-value={p_val:.4e}')",
|
|
351
|
+
" print(f'Mann-Whitney U: U={u_stat:.4f}, p-value={u_pval:.4e}')",
|
|
352
|
+
" if p_val < 0.05:",
|
|
353
|
+
" print('=> CONCLUSION: Reject H0. Significant difference between class distributions.')",
|
|
354
|
+
" else:",
|
|
355
|
+
" print('=> CONCLUSION: Fail to reject H0. No significant difference.')",
|
|
356
|
+
])
|
|
357
|
+
elif "ANOVA" in test_type:
|
|
358
|
+
lines.extend([
|
|
359
|
+
f" valid_data = df[[{repr(f1)}, {repr(f2)}]].dropna()",
|
|
360
|
+
f" classes = valid_data[{repr(f2)}].unique()",
|
|
361
|
+
f" groups = [valid_data[valid_data[{repr(f2)}] == c][{repr(f1)}] for c in classes]",
|
|
362
|
+
" f_stat, p_val = stats.f_oneway(*groups)",
|
|
363
|
+
" print(f'One-way ANOVA: F={f_stat:.4f}, p-value={p_val:.4e}')",
|
|
364
|
+
" if p_val < 0.05:",
|
|
365
|
+
" print('=> CONCLUSION: Reject H0. Group means differ significantly.')",
|
|
366
|
+
" else:",
|
|
367
|
+
" print('=> CONCLUSION: Fail to reject H0. No significant difference in group means.')",
|
|
368
|
+
])
|
|
369
|
+
elif "Chi-Square" in test_type:
|
|
370
|
+
lines.extend([
|
|
371
|
+
f" valid_data = df[[{repr(f1)}, {repr(f2)}]].dropna()",
|
|
372
|
+
f" contingency_tab = pd.crosstab(valid_data[{repr(f1)}], valid_data[{repr(f2)}])",
|
|
373
|
+
" chi2, p_val, dof, _ = stats.chi2_contingency(contingency_tab)",
|
|
374
|
+
" n_obs = contingency_tab.sum().sum()",
|
|
375
|
+
" min_dim = min(contingency_tab.shape) - 1",
|
|
376
|
+
" cramer_v = np.sqrt(chi2 / (n_obs * min_dim)) if (n_obs * min_dim) > 0 else 0.0",
|
|
377
|
+
" print(f'Chi-Square: chi2={chi2:.4f}, dof={dof}, p-value={p_val:.4e}')",
|
|
378
|
+
" print(f'Cramer V Effect Size: {cramer_v:.4f}')",
|
|
379
|
+
" if p_val < 0.05:",
|
|
380
|
+
" print('=> CONCLUSION: Reject H0. Significant association/dependency between categorical features.')",
|
|
381
|
+
" else:",
|
|
382
|
+
" print('=> CONCLUSION: Fail to reject H0. Variables appear statistically independent.')",
|
|
383
|
+
])
|
|
384
|
+
lines.append("")
|
|
385
|
+
|
|
386
|
+
return "\n".join(lines)
|
|
387
|
+
|
|
388
|
+
# -------------------------------------------------------------------------
|
|
389
|
+
# 6. Baseline Modeling Block
|
|
390
|
+
# -------------------------------------------------------------------------
|
|
391
|
+
def generate_modeling_code(self) -> str:
|
|
392
|
+
if not self.target:
|
|
393
|
+
return "# No target variable detected; modeling pipeline skipped.\n"
|
|
394
|
+
|
|
395
|
+
target_col = self.target.get("target_column")
|
|
396
|
+
prob_type = self.target.get("problem_type")
|
|
397
|
+
id_cols = list(self.dtypes.get("potential_ids", []))
|
|
398
|
+
|
|
399
|
+
lines = [
|
|
400
|
+
"# =============================================================================",
|
|
401
|
+
f"# STEP 6: MACHINE LEARNING PIPELINE ({prob_type.upper()})",
|
|
402
|
+
"# =============================================================================",
|
|
403
|
+
f"TARGET = {repr(target_col)}",
|
|
404
|
+
f"ID_COLS = {repr(id_cols)}",
|
|
405
|
+
"",
|
|
406
|
+
"# Separate Feature Matrix (X) and Response Vector (y)",
|
|
407
|
+
"drop_cols = [TARGET] + [col for col in ID_COLS if col in df.columns]",
|
|
408
|
+
"X = df.drop(columns=[col for col in drop_cols if col in df.columns])",
|
|
409
|
+
"y = df[TARGET]",
|
|
410
|
+
"",
|
|
411
|
+
"# Align X with only numeric types (already encoded)",
|
|
412
|
+
"X = X.select_dtypes(include=[np.number])",
|
|
413
|
+
"print(f'[*] Modeling Matrix: X shape = {X.shape}, y shape = {y.shape}')",
|
|
414
|
+
"",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
if prob_type == "regression":
|
|
418
|
+
lines.extend([
|
|
419
|
+
"from sklearn.linear_model import LinearRegression, Ridge",
|
|
420
|
+
"from sklearn.ensemble import RandomForestRegressor",
|
|
421
|
+
"from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score",
|
|
422
|
+
"",
|
|
423
|
+
"# Train / Test Split (80/20)",
|
|
424
|
+
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)",
|
|
425
|
+
"",
|
|
426
|
+
"# Feature Scaling with RobustScaler (robust to remaining outliers)",
|
|
427
|
+
"scaler = RobustScaler()",
|
|
428
|
+
"X_train_scaled = scaler.fit_transform(X_train)",
|
|
429
|
+
"X_test_scaled = scaler.transform(X_test)",
|
|
430
|
+
"",
|
|
431
|
+
"# Model Baselines",
|
|
432
|
+
"models = {",
|
|
433
|
+
" 'Linear Regression': LinearRegression(),",
|
|
434
|
+
" 'Ridge Regression (L2)': Ridge(alpha=1.0),",
|
|
435
|
+
" 'Random Forest Regressor': RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1),",
|
|
436
|
+
"}",
|
|
437
|
+
"",
|
|
438
|
+
"print('\\n' + '='*75)",
|
|
439
|
+
"print(f'{\"Model\":<25} | {\"CV R2 (5-Fold)\":<15} | {\"Test RMSE\":<12} | {\"Test R2\":<10}')",
|
|
440
|
+
"print('='*75)",
|
|
441
|
+
"",
|
|
442
|
+
"for name, model in models.items():",
|
|
443
|
+
" # 5-Fold Cross Validation on Training set",
|
|
444
|
+
" cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='r2')",
|
|
445
|
+
" # Train on full train set and evaluate on test set",
|
|
446
|
+
" model.fit(X_train_scaled, y_train)",
|
|
447
|
+
" preds = model.predict(X_test_scaled)",
|
|
448
|
+
" # Calculate metrics",
|
|
449
|
+
" rmse = np.sqrt(mean_squared_error(y_test, preds))",
|
|
450
|
+
" r2 = r2_score(y_test, preds)",
|
|
451
|
+
" mae = mean_absolute_error(y_test, preds)",
|
|
452
|
+
" print(f'{name:<25} | {cv_scores.mean():.4f} (+/-{cv_scores.std():.3f}) | {rmse:<12.4f} | {r2:<10.4f}')",
|
|
453
|
+
"print('='*75)",
|
|
454
|
+
])
|
|
455
|
+
else:
|
|
456
|
+
# Classification
|
|
457
|
+
is_imbalanced = self.target.get("is_imbalanced", False)
|
|
458
|
+
cw_param = "'balanced'" if is_imbalanced else "None"
|
|
459
|
+
|
|
460
|
+
lines.extend([
|
|
461
|
+
"from sklearn.linear_model import LogisticRegression",
|
|
462
|
+
"from sklearn.ensemble import RandomForestClassifier",
|
|
463
|
+
"from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score",
|
|
464
|
+
"",
|
|
465
|
+
"# Stratified Train / Test Split (preserves class distribution)",
|
|
466
|
+
"X_train, X_test, y_train, y_test = train_test_split(",
|
|
467
|
+
" X, y, test_size=0.20, random_state=42, stratify=y",
|
|
468
|
+
")",
|
|
469
|
+
"",
|
|
470
|
+
"# Feature Scaling",
|
|
471
|
+
"scaler = StandardScaler()",
|
|
472
|
+
"X_train_scaled = scaler.fit_transform(X_train)",
|
|
473
|
+
"X_test_scaled = scaler.transform(X_test)",
|
|
474
|
+
"",
|
|
475
|
+
"# Classification Models",
|
|
476
|
+
"models = {",
|
|
477
|
+
f" 'Logistic Regression': LogisticRegression(class_weight={cw_param}, max_iter=1000, random_state=42),",
|
|
478
|
+
f" 'Random Forest Classifier': RandomForestClassifier(n_estimators=100, class_weight={cw_param}, random_state=42, n_jobs=-1),",
|
|
479
|
+
"}",
|
|
480
|
+
"",
|
|
481
|
+
"cv_strat = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)",
|
|
482
|
+
"",
|
|
483
|
+
"for name, model in models.items():",
|
|
484
|
+
" print(f'\\n>> Evaluating {name}:')",
|
|
485
|
+
" # 5-Fold Stratified Cross Validation",
|
|
486
|
+
" cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=cv_strat, scoring='f1_weighted')",
|
|
487
|
+
" print(f' 5-Fold CV Weighted F1: {cv_scores.mean():.4f} (+/- {cv_scores.std():.3f})')",
|
|
488
|
+
" # Fit and Predict on Test Set",
|
|
489
|
+
" model.fit(X_train_scaled, y_train)",
|
|
490
|
+
" preds = model.predict(X_test_scaled)",
|
|
491
|
+
" print(' Test Set Classification Report:')",
|
|
492
|
+
" print(classification_report(y_test, preds))",
|
|
493
|
+
" print(' Confusion Matrix:')",
|
|
494
|
+
" print(confusion_matrix(y_test, preds))",
|
|
495
|
+
])
|
|
496
|
+
|
|
497
|
+
return "\n".join(lines)
|
|
498
|
+
|
|
499
|
+
# -------------------------------------------------------------------------
|
|
500
|
+
# 7. Master Script Assembly
|
|
501
|
+
# -------------------------------------------------------------------------
|
|
502
|
+
def generate_full_script(self) -> str:
|
|
503
|
+
"""Assemble all modular blocks into one comprehensive, executable script."""
|
|
504
|
+
vis_advisor = VisualizationAdvisor(self.scan)
|
|
505
|
+
script_parts = [
|
|
506
|
+
self.generate_imports_and_loading(),
|
|
507
|
+
self.generate_cleaning_code(),
|
|
508
|
+
self.generate_transformations_code(),
|
|
509
|
+
self.generate_encoding_code(),
|
|
510
|
+
vis_advisor.generate_all_plots_code(),
|
|
511
|
+
self.generate_hypothesis_testing_code(),
|
|
512
|
+
self.generate_modeling_code(),
|
|
513
|
+
"\nprint('\\n[*] Complete Data Analysis & Modeling Pipeline Finished Successfully.')\n",
|
|
514
|
+
]
|
|
515
|
+
return "\n\n".join(script_parts)
|