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