cleanflow-kit 1.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.
- cleanflow_kit-1.1.0.dist-info/METADATA +119 -0
- cleanflow_kit-1.1.0.dist-info/RECORD +17 -0
- cleanflow_kit-1.1.0.dist-info/WHEEL +5 -0
- cleanflow_kit-1.1.0.dist-info/licenses/LICENSE +21 -0
- cleanflow_kit-1.1.0.dist-info/top_level.txt +1 -0
- dataclean/__init__.py +44 -0
- dataclean/_compat.py +24 -0
- dataclean/data_cleaner.py +988 -0
- dataclean/data_loader.py +194 -0
- dataclean/drift_detector.py +111 -0
- dataclean/eda.py +400 -0
- dataclean/feature_engineer.py +608 -0
- dataclean/model_trainer.py +874 -0
- dataclean/pipeline.py +548 -0
- dataclean/py.typed +0 -0
- dataclean/report_generator.py +365 -0
- dataclean/synthetic_generator.py +97 -0
dataclean/eda.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exploratory Data Analysis (EDA) Module
|
|
3
|
+
======================================
|
|
4
|
+
Performs comprehensive EDA with visualizations and insights.
|
|
5
|
+
Memory-optimized: uses only matplotlib (no seaborn/scipy).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib
|
|
13
|
+
matplotlib.use('Agg')
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import warnings
|
|
18
|
+
import gc
|
|
19
|
+
|
|
20
|
+
warnings.filterwarnings('ignore')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EDAAnalyzer:
|
|
24
|
+
"""Perform exploratory data analysis on datasets."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, df: pd.DataFrame, target_col: Optional[str] = None):
|
|
27
|
+
self.df = df.copy()
|
|
28
|
+
self.target_col = target_col
|
|
29
|
+
self.insights: List[str] = []
|
|
30
|
+
self.stats: Dict[str, Any] = {}
|
|
31
|
+
|
|
32
|
+
def summary_statistics(self) -> pd.DataFrame:
|
|
33
|
+
"""Generate comprehensive summary statistics."""
|
|
34
|
+
numeric_df = self.df.select_dtypes(include=[np.number])
|
|
35
|
+
|
|
36
|
+
if len(numeric_df.columns) == 0:
|
|
37
|
+
return pd.DataFrame()
|
|
38
|
+
|
|
39
|
+
stats = numeric_df.describe().T
|
|
40
|
+
stats['missing'] = self.df[numeric_df.columns].isnull().sum()
|
|
41
|
+
stats['missing_pct'] = (stats['missing'] / len(self.df) * 100).round(2)
|
|
42
|
+
stats['skewness'] = numeric_df.skew()
|
|
43
|
+
stats['kurtosis'] = numeric_df.kurtosis()
|
|
44
|
+
|
|
45
|
+
self.stats['numeric_summary'] = stats
|
|
46
|
+
|
|
47
|
+
skewed = stats[stats['skewness'].abs() > 1].index.tolist()
|
|
48
|
+
if skewed:
|
|
49
|
+
self.insights.append(f"Highly skewed features detected: {skewed}")
|
|
50
|
+
|
|
51
|
+
return stats
|
|
52
|
+
|
|
53
|
+
def categorical_summary(self) -> Dict[str, pd.DataFrame]:
|
|
54
|
+
"""Summarize categorical columns."""
|
|
55
|
+
categorical_cols = self.df.select_dtypes(include=['object', 'category']).columns
|
|
56
|
+
summaries = {}
|
|
57
|
+
|
|
58
|
+
for col in categorical_cols:
|
|
59
|
+
value_counts = self.df[col].value_counts()
|
|
60
|
+
pct = (value_counts / len(self.df) * 100).round(2)
|
|
61
|
+
|
|
62
|
+
summary = pd.DataFrame({
|
|
63
|
+
'count': value_counts,
|
|
64
|
+
'percentage': pct
|
|
65
|
+
})
|
|
66
|
+
summaries[col] = summary
|
|
67
|
+
|
|
68
|
+
if len(value_counts) > 50:
|
|
69
|
+
self.insights.append(f"High cardinality in '{col}': {len(value_counts)} unique values")
|
|
70
|
+
|
|
71
|
+
self.stats['categorical_summary'] = summaries
|
|
72
|
+
return summaries
|
|
73
|
+
|
|
74
|
+
def correlation_analysis(self, method: str = 'pearson', threshold: float = 0.7) -> pd.DataFrame:
|
|
75
|
+
"""Compute correlation matrix and identify highly correlated features."""
|
|
76
|
+
numeric_df = self.df.select_dtypes(include=[np.number])
|
|
77
|
+
|
|
78
|
+
if len(numeric_df.columns) < 2:
|
|
79
|
+
return pd.DataFrame()
|
|
80
|
+
|
|
81
|
+
corr_matrix = numeric_df.corr(method=method)
|
|
82
|
+
self.stats['correlation_matrix'] = corr_matrix
|
|
83
|
+
|
|
84
|
+
high_corr_pairs = []
|
|
85
|
+
for i in range(len(corr_matrix.columns)):
|
|
86
|
+
for j in range(i + 1, len(corr_matrix.columns)):
|
|
87
|
+
if abs(corr_matrix.iloc[i, j]) > threshold:
|
|
88
|
+
high_corr_pairs.append({
|
|
89
|
+
'feature_1': corr_matrix.columns[i],
|
|
90
|
+
'feature_2': corr_matrix.columns[j],
|
|
91
|
+
'correlation': round(corr_matrix.iloc[i, j], 3)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
if high_corr_pairs:
|
|
95
|
+
self.stats['high_correlations'] = high_corr_pairs
|
|
96
|
+
self.insights.append(f"Found {len(high_corr_pairs)} highly correlated feature pairs (|r| > {threshold})")
|
|
97
|
+
|
|
98
|
+
return corr_matrix
|
|
99
|
+
|
|
100
|
+
def distribution_analysis(self) -> Dict[str, Dict[str, Any]]:
|
|
101
|
+
"""Analyze distributions of numeric features."""
|
|
102
|
+
numeric_cols = self.df.select_dtypes(include=[np.number]).columns
|
|
103
|
+
distributions = {}
|
|
104
|
+
|
|
105
|
+
for col in numeric_cols:
|
|
106
|
+
series = self.df[col].dropna()
|
|
107
|
+
distributions[col] = {
|
|
108
|
+
'mean': series.mean(),
|
|
109
|
+
'median': series.median(),
|
|
110
|
+
'std': series.std(),
|
|
111
|
+
'skewness': series.skew(),
|
|
112
|
+
'kurtosis': series.kurtosis(),
|
|
113
|
+
'q1': series.quantile(0.25),
|
|
114
|
+
'q3': series.quantile(0.75),
|
|
115
|
+
'iqr': series.quantile(0.75) - series.quantile(0.25),
|
|
116
|
+
'range': series.max() - series.min()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
self.stats['distributions'] = distributions
|
|
120
|
+
return distributions
|
|
121
|
+
|
|
122
|
+
def target_analysis(self) -> Dict[str, Any]:
|
|
123
|
+
"""Analyze target variable if specified."""
|
|
124
|
+
if self.target_col is None or self.target_col not in self.df.columns:
|
|
125
|
+
return {}
|
|
126
|
+
|
|
127
|
+
target = self.df[self.target_col]
|
|
128
|
+
analysis = {'column': self.target_col}
|
|
129
|
+
|
|
130
|
+
if pd.api.types.is_numeric_dtype(target):
|
|
131
|
+
analysis['type'] = 'continuous'
|
|
132
|
+
analysis['stats'] = {
|
|
133
|
+
'mean': target.mean(),
|
|
134
|
+
'median': target.median(),
|
|
135
|
+
'std': target.std(),
|
|
136
|
+
'min': target.min(),
|
|
137
|
+
'max': target.max()
|
|
138
|
+
}
|
|
139
|
+
else:
|
|
140
|
+
analysis['type'] = 'categorical'
|
|
141
|
+
value_counts = target.value_counts()
|
|
142
|
+
analysis['class_distribution'] = value_counts.to_dict()
|
|
143
|
+
analysis['class_balance'] = round(value_counts.min() / value_counts.max(), 3)
|
|
144
|
+
|
|
145
|
+
if analysis['class_balance'] < 0.5:
|
|
146
|
+
self.insights.append(f"Class imbalance detected in target: ratio = {analysis['class_balance']}")
|
|
147
|
+
|
|
148
|
+
self.stats['target_analysis'] = analysis
|
|
149
|
+
return analysis
|
|
150
|
+
|
|
151
|
+
def plot_distributions(self, columns=None, figsize=(15, 10), save_path=None):
|
|
152
|
+
"""Plot distribution histograms for numeric columns."""
|
|
153
|
+
if columns is None:
|
|
154
|
+
columns = self.df.select_dtypes(include=[np.number]).columns.tolist()
|
|
155
|
+
|
|
156
|
+
if len(columns) == 0:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
n_cols = min(3, len(columns))
|
|
160
|
+
n_rows = (len(columns) + n_cols - 1) // n_cols
|
|
161
|
+
|
|
162
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
163
|
+
axes = np.atleast_2d(axes).flatten()
|
|
164
|
+
|
|
165
|
+
for idx, col in enumerate(columns):
|
|
166
|
+
ax = axes[idx]
|
|
167
|
+
data = self.df[col].dropna()
|
|
168
|
+
ax.hist(data, bins=30, edgecolor='black', alpha=0.7, color='#4C72B0')
|
|
169
|
+
ax.axvline(data.mean(), color='red', linestyle='--', label=f'Mean: {data.mean():.2f}')
|
|
170
|
+
ax.axvline(data.median(), color='green', linestyle='--', label=f'Median: {data.median():.2f}')
|
|
171
|
+
ax.set_title(f'{col}\n(skew: {data.skew():.2f})')
|
|
172
|
+
ax.legend(fontsize=8)
|
|
173
|
+
|
|
174
|
+
for idx in range(len(columns), len(axes)):
|
|
175
|
+
axes[idx].set_visible(False)
|
|
176
|
+
|
|
177
|
+
plt.suptitle('Feature Distributions', fontsize=14, fontweight='bold')
|
|
178
|
+
plt.tight_layout()
|
|
179
|
+
|
|
180
|
+
if save_path:
|
|
181
|
+
plt.savefig(save_path, dpi=100, bbox_inches='tight')
|
|
182
|
+
|
|
183
|
+
plt.close(fig)
|
|
184
|
+
gc.collect()
|
|
185
|
+
|
|
186
|
+
def plot_boxplots(self, columns=None, figsize=(15, 10), save_path=None):
|
|
187
|
+
"""Plot boxplots for numeric columns to visualize outliers."""
|
|
188
|
+
if columns is None:
|
|
189
|
+
columns = self.df.select_dtypes(include=[np.number]).columns.tolist()
|
|
190
|
+
|
|
191
|
+
if len(columns) == 0:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
n_cols = min(4, len(columns))
|
|
195
|
+
n_rows = (len(columns) + n_cols - 1) // n_cols
|
|
196
|
+
|
|
197
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
198
|
+
axes = np.atleast_2d(axes).flatten()
|
|
199
|
+
|
|
200
|
+
for idx, col in enumerate(columns):
|
|
201
|
+
ax = axes[idx]
|
|
202
|
+
data = self.df[col].dropna()
|
|
203
|
+
bp = ax.boxplot(data, patch_artist=True)
|
|
204
|
+
bp['boxes'][0].set_facecolor('lightblue')
|
|
205
|
+
ax.set_title(col)
|
|
206
|
+
ax.set_xticks([])
|
|
207
|
+
|
|
208
|
+
for idx in range(len(columns), len(axes)):
|
|
209
|
+
axes[idx].set_visible(False)
|
|
210
|
+
|
|
211
|
+
plt.suptitle('Feature Boxplots (Outlier Detection)', fontsize=14, fontweight='bold')
|
|
212
|
+
plt.tight_layout()
|
|
213
|
+
|
|
214
|
+
if save_path:
|
|
215
|
+
plt.savefig(save_path, dpi=100, bbox_inches='tight')
|
|
216
|
+
|
|
217
|
+
plt.close(fig)
|
|
218
|
+
gc.collect()
|
|
219
|
+
|
|
220
|
+
def plot_correlation_heatmap(self, figsize=(12, 10), save_path=None):
|
|
221
|
+
"""Plot correlation heatmap using pure matplotlib (no seaborn)."""
|
|
222
|
+
if 'correlation_matrix' not in self.stats:
|
|
223
|
+
self.correlation_analysis()
|
|
224
|
+
|
|
225
|
+
corr_matrix = self.stats.get('correlation_matrix')
|
|
226
|
+
|
|
227
|
+
if corr_matrix is None or corr_matrix.empty:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
231
|
+
|
|
232
|
+
# Pure matplotlib heatmap
|
|
233
|
+
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
|
|
234
|
+
masked_corr = np.ma.masked_where(mask, corr_matrix.values)
|
|
235
|
+
|
|
236
|
+
cax = ax.imshow(masked_corr, cmap='RdBu_r', vmin=-1, vmax=1, aspect='auto')
|
|
237
|
+
fig.colorbar(cax, ax=ax, shrink=0.8)
|
|
238
|
+
|
|
239
|
+
# Labels
|
|
240
|
+
ax.set_xticks(range(len(corr_matrix.columns)))
|
|
241
|
+
ax.set_yticks(range(len(corr_matrix.columns)))
|
|
242
|
+
ax.set_xticklabels(corr_matrix.columns, rotation=45, ha='right', fontsize=8)
|
|
243
|
+
ax.set_yticklabels(corr_matrix.columns, fontsize=8)
|
|
244
|
+
|
|
245
|
+
# Annotate if small enough
|
|
246
|
+
if len(corr_matrix.columns) <= 15:
|
|
247
|
+
for i in range(len(corr_matrix.columns)):
|
|
248
|
+
for j in range(len(corr_matrix.columns)):
|
|
249
|
+
if not mask[i, j]:
|
|
250
|
+
ax.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}',
|
|
251
|
+
ha='center', va='center', fontsize=7)
|
|
252
|
+
|
|
253
|
+
plt.title('Feature Correlation Heatmap', fontsize=14, fontweight='bold')
|
|
254
|
+
plt.tight_layout()
|
|
255
|
+
|
|
256
|
+
if save_path:
|
|
257
|
+
plt.savefig(save_path, dpi=100, bbox_inches='tight')
|
|
258
|
+
|
|
259
|
+
plt.close(fig)
|
|
260
|
+
gc.collect()
|
|
261
|
+
|
|
262
|
+
def plot_categorical(self, columns=None, figsize=(15, 10), save_path=None):
|
|
263
|
+
"""Plot count plots for categorical columns."""
|
|
264
|
+
if columns is None:
|
|
265
|
+
columns = self.df.select_dtypes(include=['object', 'category']).columns.tolist()
|
|
266
|
+
|
|
267
|
+
if len(columns) == 0:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
columns = [c for c in columns if self.df[c].nunique() <= 20]
|
|
271
|
+
|
|
272
|
+
if len(columns) == 0:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
n_cols = min(2, len(columns))
|
|
276
|
+
n_rows = (len(columns) + n_cols - 1) // n_cols
|
|
277
|
+
|
|
278
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
279
|
+
axes = np.atleast_2d(axes).flatten()
|
|
280
|
+
|
|
281
|
+
for idx, col in enumerate(columns):
|
|
282
|
+
ax = axes[idx]
|
|
283
|
+
value_counts = self.df[col].value_counts()
|
|
284
|
+
ax.bar(range(len(value_counts)), value_counts.values, color='#4C72B0')
|
|
285
|
+
ax.set_xticks(range(len(value_counts)))
|
|
286
|
+
ax.set_xticklabels(value_counts.index, rotation=45, ha='right')
|
|
287
|
+
ax.set_title(f'{col} ({len(value_counts)} categories)')
|
|
288
|
+
ax.set_ylabel('Count')
|
|
289
|
+
|
|
290
|
+
for idx in range(len(columns), len(axes)):
|
|
291
|
+
axes[idx].set_visible(False)
|
|
292
|
+
|
|
293
|
+
plt.suptitle('Categorical Feature Distributions', fontsize=14, fontweight='bold')
|
|
294
|
+
plt.tight_layout()
|
|
295
|
+
|
|
296
|
+
if save_path:
|
|
297
|
+
plt.savefig(save_path, dpi=100, bbox_inches='tight')
|
|
298
|
+
|
|
299
|
+
plt.close(fig)
|
|
300
|
+
gc.collect()
|
|
301
|
+
|
|
302
|
+
def plot_target_relationships(self, figsize=(15, 10), save_path=None):
|
|
303
|
+
"""Plot relationships between features and target."""
|
|
304
|
+
if self.target_col is None:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
numeric_cols = [c for c in self.df.select_dtypes(include=[np.number]).columns
|
|
308
|
+
if c != self.target_col][:6]
|
|
309
|
+
|
|
310
|
+
if len(numeric_cols) == 0:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
n_cols = min(3, len(numeric_cols))
|
|
314
|
+
n_rows = (len(numeric_cols) + n_cols - 1) // n_cols
|
|
315
|
+
|
|
316
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
317
|
+
axes = np.atleast_2d(axes).flatten()
|
|
318
|
+
|
|
319
|
+
for idx, col in enumerate(numeric_cols):
|
|
320
|
+
ax = axes[idx]
|
|
321
|
+
ax.scatter(self.df[col], self.df[self.target_col], alpha=0.5, s=10, color='#4C72B0')
|
|
322
|
+
ax.set_xlabel(col)
|
|
323
|
+
ax.set_ylabel(self.target_col)
|
|
324
|
+
ax.set_title(f'{col} vs {self.target_col}')
|
|
325
|
+
|
|
326
|
+
for idx in range(len(numeric_cols), len(axes)):
|
|
327
|
+
axes[idx].set_visible(False)
|
|
328
|
+
|
|
329
|
+
plt.suptitle('Feature-Target Relationships', fontsize=14, fontweight='bold')
|
|
330
|
+
plt.tight_layout()
|
|
331
|
+
|
|
332
|
+
if save_path:
|
|
333
|
+
plt.savefig(save_path, dpi=100, bbox_inches='tight')
|
|
334
|
+
|
|
335
|
+
plt.close(fig)
|
|
336
|
+
gc.collect()
|
|
337
|
+
|
|
338
|
+
def run_full_analysis(self, show_plots=True, save_plots=False, output_dir=None):
|
|
339
|
+
"""Run complete EDA pipeline."""
|
|
340
|
+
print("=" * 60)
|
|
341
|
+
logger.info("EXPLORATORY DATA ANALYSIS")
|
|
342
|
+
print("=" * 60)
|
|
343
|
+
|
|
344
|
+
logger.info("\nš Summary Statistics:")
|
|
345
|
+
stats = self.summary_statistics()
|
|
346
|
+
if not stats.empty:
|
|
347
|
+
print(stats.round(2).to_string())
|
|
348
|
+
|
|
349
|
+
logger.info("\nš Categorical Columns:")
|
|
350
|
+
cat_summary = self.categorical_summary()
|
|
351
|
+
for col, summary in cat_summary.items():
|
|
352
|
+
logger.info(f"\n{col}:")
|
|
353
|
+
print(summary.head(5).to_string())
|
|
354
|
+
|
|
355
|
+
logger.info("\nš Correlation Analysis:")
|
|
356
|
+
corr = self.correlation_analysis()
|
|
357
|
+
if 'high_correlations' in self.stats:
|
|
358
|
+
for pair in self.stats['high_correlations'][:10]:
|
|
359
|
+
logger.info(f" {pair['feature_1']} ā {pair['feature_2']}: {pair['correlation']}")
|
|
360
|
+
|
|
361
|
+
if self.target_col:
|
|
362
|
+
logger.info(f"\nšÆ Target Variable Analysis ({self.target_col}):")
|
|
363
|
+
target_analysis = self.target_analysis()
|
|
364
|
+
if target_analysis:
|
|
365
|
+
if target_analysis['type'] == 'continuous':
|
|
366
|
+
for k, v in target_analysis['stats'].items():
|
|
367
|
+
logger.info(f" {k}: {v:.3f}")
|
|
368
|
+
else:
|
|
369
|
+
for cls, count in target_analysis['class_distribution'].items():
|
|
370
|
+
logger.info(f" {cls}: {count}")
|
|
371
|
+
|
|
372
|
+
if self.insights:
|
|
373
|
+
logger.info("\nš” Key Insights:")
|
|
374
|
+
for insight in self.insights:
|
|
375
|
+
logger.info(f" ā ļø {insight}")
|
|
376
|
+
|
|
377
|
+
if show_plots:
|
|
378
|
+
save_path = lambda name: f"{output_dir}/{name}.png" if save_plots and output_dir else None
|
|
379
|
+
|
|
380
|
+
logger.info("\nš Generating visualizations...")
|
|
381
|
+
self.plot_distributions(save_path=save_path('distributions'))
|
|
382
|
+
self.plot_boxplots(save_path=save_path('boxplots'))
|
|
383
|
+
self.plot_correlation_heatmap(save_path=save_path('correlation'))
|
|
384
|
+
self.plot_categorical(save_path=save_path('categorical'))
|
|
385
|
+
|
|
386
|
+
if self.target_col:
|
|
387
|
+
self.plot_target_relationships(save_path=save_path('target_relationships'))
|
|
388
|
+
|
|
389
|
+
print("\n" + "=" * 60)
|
|
390
|
+
|
|
391
|
+
gc.collect()
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
'stats': self.stats,
|
|
395
|
+
'insights': self.insights
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
def get_insights(self) -> List[str]:
|
|
399
|
+
"""Return list of insights discovered during analysis."""
|
|
400
|
+
return self.insights
|