yaeda 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.
- yaeda/__init__.py +28 -0
- yaeda/analyze.py +354 -0
- yaeda/charts.py +1260 -0
- yaeda/clustering.py +232 -0
- yaeda/correlation.py +238 -0
- yaeda/exports/export.py +172 -0
- yaeda/exports/html.py +357 -0
- yaeda/exports/markdown.py +108 -0
- yaeda/exports/pdf.py +446 -0
- yaeda/exports/tabs/base.py +31 -0
- yaeda/exports/tabs/charts.py +37 -0
- yaeda/exports/tabs/cluster.py +125 -0
- yaeda/exports/tabs/correlation.py +35 -0
- yaeda/exports/tabs/feature_importance.py +76 -0
- yaeda/exports/tabs/features.py +169 -0
- yaeda/exports/tabs/health.py +71 -0
- yaeda/exports/tabs/interactions.py +166 -0
- yaeda/exports/tabs/model_diagnostics.py +157 -0
- yaeda/exports/tabs/pdp.py +120 -0
- yaeda/extract.py +242 -0
- yaeda/feature_importance.py +300 -0
- yaeda/interaction.py +158 -0
- yaeda/model_diagnostics.py +357 -0
- yaeda-0.1.0.dist-info/METADATA +118 -0
- yaeda-0.1.0.dist-info/RECORD +27 -0
- yaeda-0.1.0.dist-info/WHEEL +4 -0
- yaeda-0.1.0.dist-info/licenses/LICENSE +21 -0
yaeda/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""tabulareda - Automated Tabular EDA & Golden Feature Engineering Toolkit."""
|
|
2
|
+
|
|
3
|
+
from .analyze import TabularEDA
|
|
4
|
+
from .clustering import ClusterReport, TabularClusterAnalyzer
|
|
5
|
+
from .correlation import CorrelationReport, FeatureTargetAnalyzer
|
|
6
|
+
from .extract import FeatureProfile, TableProfile, TabularDataProfiler
|
|
7
|
+
from .feature_importance import FeatureImportanceAnalyzer, FeatureImportanceReport
|
|
8
|
+
from .interaction import FeatureInteractionAnalyzer, InteractionReport
|
|
9
|
+
from .model_diagnostics import ModelDiagnosticsAnalyzer, ModelDiagnosticsReport
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ClusterReport",
|
|
13
|
+
"CorrelationReport",
|
|
14
|
+
"FeatureImportanceAnalyzer",
|
|
15
|
+
"FeatureImportanceReport",
|
|
16
|
+
"FeatureInteractionAnalyzer",
|
|
17
|
+
"FeatureProfile",
|
|
18
|
+
"FeatureTargetAnalyzer",
|
|
19
|
+
"InteractionReport",
|
|
20
|
+
"ModelDiagnosticsAnalyzer",
|
|
21
|
+
"ModelDiagnosticsReport",
|
|
22
|
+
"TableProfile",
|
|
23
|
+
"TabularClusterAnalyzer",
|
|
24
|
+
"TabularDataProfiler",
|
|
25
|
+
"TabularEDA",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
yaeda/analyze.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
from typing import Literal, Any
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
# import weasyprint
|
|
7
|
+
|
|
8
|
+
from .extract import TabularDataProfiler, TableProfile
|
|
9
|
+
from .correlation import FeatureTargetAnalyzer, CorrelationReport
|
|
10
|
+
from .feature_importance import FeatureImportanceAnalyzer, FeatureImportanceReport
|
|
11
|
+
from .interaction import FeatureInteractionAnalyzer, InteractionReport
|
|
12
|
+
|
|
13
|
+
from .clustering import TabularClustersCall, ClusterReport
|
|
14
|
+
from .model_diagnostics import (
|
|
15
|
+
ModelDiagnosticsCall,
|
|
16
|
+
ModelDiagnosticsReport,
|
|
17
|
+
ModelDiagnosticsInputs,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .charts import EDAChartGenerator
|
|
21
|
+
from .exports.html import EDAHTMLDashboardBuilder
|
|
22
|
+
from .exports.markdown import EDAMarkdownReportBuilder
|
|
23
|
+
|
|
24
|
+
# from .exports.pdf import EDAPDFReportBuilder
|
|
25
|
+
from .exports.export import StructuredDataExporter
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TabularEDA:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
df: pd.DataFrame,
|
|
32
|
+
target: str,
|
|
33
|
+
features: list[str] | None = None,
|
|
34
|
+
*,
|
|
35
|
+
diagnostics: dict[str, Any] | None = None,
|
|
36
|
+
target_type: Literal["classification", "regression"] | None = None,
|
|
37
|
+
n_clusters: list[int] | None = None,
|
|
38
|
+
n_frequent: int = 3,
|
|
39
|
+
n_extremes: int = 3,
|
|
40
|
+
outlier_irq_factor: float = 1.5,
|
|
41
|
+
collinear_threshold: float = 0.80,
|
|
42
|
+
test_size: float = 0.25,
|
|
43
|
+
shap_sample_limit: int = 500,
|
|
44
|
+
seed: int = 42,
|
|
45
|
+
):
|
|
46
|
+
self._df = df
|
|
47
|
+
self._target = target
|
|
48
|
+
self._features = features
|
|
49
|
+
|
|
50
|
+
self._diagnostics = (
|
|
51
|
+
[ModelDiagnosticsInputs(**input) for input in diagnostics]
|
|
52
|
+
if diagnostics is not None
|
|
53
|
+
else None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self._n_frequent = n_frequent
|
|
57
|
+
self._n_extremes = n_extremes
|
|
58
|
+
self._outlier_irq_factor = outlier_irq_factor
|
|
59
|
+
|
|
60
|
+
self._target_type = target_type
|
|
61
|
+
self._seed = seed
|
|
62
|
+
|
|
63
|
+
self._collinear_threshold = collinear_threshold
|
|
64
|
+
|
|
65
|
+
self._test_size = test_size
|
|
66
|
+
self._shap_sample_limit = shap_sample_limit
|
|
67
|
+
|
|
68
|
+
self._n_clusters = n_clusters if n_clusters is not None else [4]
|
|
69
|
+
|
|
70
|
+
self._profile: TableProfile | None = None
|
|
71
|
+
self._corr_report: CorrelationReport | None = None
|
|
72
|
+
self._feature_importance: FeatureImportanceReport | None = None
|
|
73
|
+
self._cluster_report: list[ClusterReport] | None = None
|
|
74
|
+
self._diagnostics_report: ModelDiagnosticsReport = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def stats(self) -> TableProfile:
|
|
78
|
+
if self._profile is not None:
|
|
79
|
+
return self._profile
|
|
80
|
+
|
|
81
|
+
profiler = TabularDataProfiler(
|
|
82
|
+
self._df,
|
|
83
|
+
target=self._target,
|
|
84
|
+
columns=self._features,
|
|
85
|
+
n_frequent=self._n_frequent,
|
|
86
|
+
n_extremes=self._n_extremes,
|
|
87
|
+
outlier_iqr_factor=self._outlier_irq_factor,
|
|
88
|
+
)
|
|
89
|
+
self._profile = profiler.run()
|
|
90
|
+
return self._profile
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def correlation(self) -> CorrelationReport:
|
|
94
|
+
if self._corr_report is not None:
|
|
95
|
+
return self._corr_report
|
|
96
|
+
|
|
97
|
+
analyzer = FeatureTargetAnalyzer(
|
|
98
|
+
df=self._df,
|
|
99
|
+
target=self._target,
|
|
100
|
+
features=self._features,
|
|
101
|
+
collinear_threshold=self._collinear_threshold,
|
|
102
|
+
random_state=self._seed,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self._corr_report = analyzer.run()
|
|
106
|
+
return self._corr_report
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def feature_importance(self) -> FeatureImportanceReport:
|
|
110
|
+
if self._feature_importance is not None:
|
|
111
|
+
return self._feature_importance
|
|
112
|
+
|
|
113
|
+
analyzer = FeatureImportanceAnalyzer(
|
|
114
|
+
df=self._df,
|
|
115
|
+
features=self._features,
|
|
116
|
+
target=self._target,
|
|
117
|
+
target_type=self._target_type,
|
|
118
|
+
test_size=self._test_size,
|
|
119
|
+
shap_sample_limit=self._shap_sample_limit,
|
|
120
|
+
random_state=self._seed,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self._feature_importance = analyzer.run()
|
|
124
|
+
return self._feature_importance
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def clusters(self) -> list[ClusterReport]:
|
|
128
|
+
if self._cluster_report is not None:
|
|
129
|
+
return list(self._cluster_report.values())
|
|
130
|
+
|
|
131
|
+
prominent = self.select_prominent_features(top_n=8)
|
|
132
|
+
analyzer = TabularClustersCall(
|
|
133
|
+
df=self._df,
|
|
134
|
+
target=self._target,
|
|
135
|
+
features=prominent,
|
|
136
|
+
target_type=self._target_type
|
|
137
|
+
or (
|
|
138
|
+
"classification"
|
|
139
|
+
if self.correlation.target_type == "classification"
|
|
140
|
+
else "regression"
|
|
141
|
+
),
|
|
142
|
+
n_clusters=self._n_clusters,
|
|
143
|
+
random_state=self._seed,
|
|
144
|
+
)
|
|
145
|
+
self._cluster_report = analyzer.run()
|
|
146
|
+
return list(self._cluster_report.values())
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def diagnostics(self) -> list[ModelDiagnosticsReport] | None:
|
|
150
|
+
if self._diagnostics_report is not None:
|
|
151
|
+
return list(self._diagnostics_report.values())
|
|
152
|
+
|
|
153
|
+
if self._diagnostics is None:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
fi = self.feature_importance
|
|
157
|
+
analyzer = ModelDiagnosticsCall(
|
|
158
|
+
df=self._df,
|
|
159
|
+
target=self._target,
|
|
160
|
+
inputs=self._diagnostics,
|
|
161
|
+
features=fi.feature_names,
|
|
162
|
+
target_type=self._target_type or fi.target_type,
|
|
163
|
+
preprocessed_X=fi.preprocessed_X,
|
|
164
|
+
random_state=self._seed,
|
|
165
|
+
)
|
|
166
|
+
self._diagnostics_report = analyzer.run()
|
|
167
|
+
return list(self._diagnostics_report.values())
|
|
168
|
+
|
|
169
|
+
def plot_partial_dependence(
|
|
170
|
+
self,
|
|
171
|
+
top_n: int = 6,
|
|
172
|
+
kind: str = "both",
|
|
173
|
+
deduplicate_collinear: bool = True,
|
|
174
|
+
) -> str:
|
|
175
|
+
"""Returns base64 Partial Dependence chart for the top N prominent features."""
|
|
176
|
+
fi = self.feature_importance
|
|
177
|
+
prominent = self.select_prominent_features(
|
|
178
|
+
top_n=top_n, deduplicate_collinear=deduplicate_collinear
|
|
179
|
+
)
|
|
180
|
+
print(prominent, type(prominent))
|
|
181
|
+
chart_engine = EDAChartGenerator()
|
|
182
|
+
return chart_engine.plot_partial_dependence(
|
|
183
|
+
model=fi.fitted_model,
|
|
184
|
+
X=fi.preprocessed_X,
|
|
185
|
+
feature_names=fi.feature_names,
|
|
186
|
+
features_to_plot=prominent,
|
|
187
|
+
target_type=fi.target_type,
|
|
188
|
+
kind=kind,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def analyze_all(
|
|
192
|
+
self,
|
|
193
|
+
) -> tuple[TableProfile, CorrelationReport, FeatureImportanceReport]:
|
|
194
|
+
return self.stats, self.correlation, self.feature_importance
|
|
195
|
+
|
|
196
|
+
def select_prominent_features(
|
|
197
|
+
self,
|
|
198
|
+
top_n: int = 6,
|
|
199
|
+
include_auxiliary: bool = True,
|
|
200
|
+
) -> list[str]:
|
|
201
|
+
"""
|
|
202
|
+
Guarantees selection of all Tier 1 (Golden) and Tier 2 (Strong) features,
|
|
203
|
+
followed by Tier 3 (Auxiliary) features up to top_n.
|
|
204
|
+
"""
|
|
205
|
+
stats, _corr, fi = self.analyze_all()
|
|
206
|
+
|
|
207
|
+
unhealthy = {
|
|
208
|
+
feat
|
|
209
|
+
for feat, p in stats.features.items()
|
|
210
|
+
if p.missing_percentage > 80.0 or (p.is_numeric and p.std_dev == 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
golden_feats = [
|
|
214
|
+
m.feature for m in fi.importances if "Tier 1" in m.tier and m.feature not in unhealthy
|
|
215
|
+
]
|
|
216
|
+
strong_feats = [
|
|
217
|
+
m.feature for m in fi.importances if "Tier 2" in m.tier and m.feature not in unhealthy
|
|
218
|
+
]
|
|
219
|
+
aux_feats = [
|
|
220
|
+
m.feature for m in fi.importances if "Tier 3" in m.tier and m.feature not in unhealthy
|
|
221
|
+
]
|
|
222
|
+
other_feats = [
|
|
223
|
+
m.feature
|
|
224
|
+
for m in fi.importances
|
|
225
|
+
if m.feature not in golden_feats
|
|
226
|
+
and m.feature not in strong_feats
|
|
227
|
+
and m.feature not in aux_feats
|
|
228
|
+
and m.feature not in unhealthy
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
# 1. Always prioritize Golden and Strong features
|
|
232
|
+
selected = list(golden_feats)
|
|
233
|
+
for f in strong_feats:
|
|
234
|
+
if f not in selected:
|
|
235
|
+
selected.append(f)
|
|
236
|
+
|
|
237
|
+
# 2. Add Auxiliary features
|
|
238
|
+
if include_auxiliary:
|
|
239
|
+
for f in aux_feats:
|
|
240
|
+
if f not in selected:
|
|
241
|
+
selected.append(f)
|
|
242
|
+
if len(selected) >= max(top_n, len(golden_feats) + len(strong_feats) + 1):
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
# 3. Fill up to top_n if capacity remains
|
|
246
|
+
if len(selected) < top_n:
|
|
247
|
+
for f in other_feats:
|
|
248
|
+
if f not in selected:
|
|
249
|
+
selected.append(f)
|
|
250
|
+
if len(selected) == top_n:
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
return selected
|
|
254
|
+
|
|
255
|
+
def interactions(self, top_n: int = 6, max_pairs: int | None = None) -> InteractionReport:
|
|
256
|
+
prominent = self.select_prominent_features(top_n=top_n, include_auxiliary=True)
|
|
257
|
+
analyzer = FeatureInteractionAnalyzer(
|
|
258
|
+
df=self._df,
|
|
259
|
+
target=self._target,
|
|
260
|
+
features=prominent,
|
|
261
|
+
target_type=self._target_type
|
|
262
|
+
or (
|
|
263
|
+
"classification"
|
|
264
|
+
if self.correlation.target_type == "classification"
|
|
265
|
+
else "regression"
|
|
266
|
+
),
|
|
267
|
+
max_pairs=max_pairs,
|
|
268
|
+
random_state=self._seed,
|
|
269
|
+
)
|
|
270
|
+
return analyzer.run()
|
|
271
|
+
|
|
272
|
+
def to_markdown(self, output: Path | str | None = None) -> str:
|
|
273
|
+
stats, corr, fi = self.analyze_all()
|
|
274
|
+
md_builder = EDAMarkdownReportBuilder(
|
|
275
|
+
table_profile=stats,
|
|
276
|
+
corr_report=corr,
|
|
277
|
+
importance_report=fi,
|
|
278
|
+
)
|
|
279
|
+
md_data = md_builder.generate(output)
|
|
280
|
+
return md_data
|
|
281
|
+
|
|
282
|
+
def to_html(
|
|
283
|
+
self,
|
|
284
|
+
output: Path | str | None = None,
|
|
285
|
+
top_n_features: int = 6,
|
|
286
|
+
top_n_interactions: int = 20,
|
|
287
|
+
) -> str:
|
|
288
|
+
stats, corr, fi = self.analyze_all()
|
|
289
|
+
prominent = self.select_prominent_features(top_n=top_n_features, include_auxiliary=True)
|
|
290
|
+
interaction_rep = self.interactions(top_n=top_n_features, max_pairs=None)
|
|
291
|
+
cluster_rep = self.clusters
|
|
292
|
+
diag_rep = self.diagnostics
|
|
293
|
+
|
|
294
|
+
chart_engine = EDAChartGenerator()
|
|
295
|
+
html_builder = EDAHTMLDashboardBuilder(
|
|
296
|
+
table_profile=stats,
|
|
297
|
+
corr_report=corr,
|
|
298
|
+
importance_report=fi,
|
|
299
|
+
chart_generator=chart_engine,
|
|
300
|
+
df=self._df,
|
|
301
|
+
prominent_features=prominent,
|
|
302
|
+
interaction_report=interaction_rep,
|
|
303
|
+
cluster_report=cluster_rep,
|
|
304
|
+
diagnostics_report=diag_rep,
|
|
305
|
+
)
|
|
306
|
+
return html_builder.generate(
|
|
307
|
+
output_path=output,
|
|
308
|
+
max_interaction_cards=top_n_interactions,
|
|
309
|
+
max_table_rows=top_n_interactions,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def to_json(self, output: Path | str | None = None) -> dict[str, Any]:
|
|
313
|
+
stats, corr, fi = self.analyze_all()
|
|
314
|
+
cluster_rep = self.clusters
|
|
315
|
+
diagnostics = self.diagnostics
|
|
316
|
+
json_builder = StructuredDataExporter(
|
|
317
|
+
table_profile=stats,
|
|
318
|
+
corr_report=corr,
|
|
319
|
+
importance_report=fi,
|
|
320
|
+
cluster_report=cluster_rep,
|
|
321
|
+
diagnostic_report=diagnostics,
|
|
322
|
+
)
|
|
323
|
+
return json_builder.export_json(output)
|
|
324
|
+
|
|
325
|
+
# def to_pdf(self, output: Path | str | None = None) -> weasyprint.HTML:
|
|
326
|
+
# stats, corr, fi = self.analyze_all()
|
|
327
|
+
# chart_engine = EDAChartGenerator()
|
|
328
|
+
# pdf_builder = EDAPDFReportBuilder(
|
|
329
|
+
# table_profile=stats,
|
|
330
|
+
# corr_report=corr,
|
|
331
|
+
# importance_report=fi,
|
|
332
|
+
# chart_generator=chart_engine,
|
|
333
|
+
# )
|
|
334
|
+
# pdf_data = pdf_builder.generate(output)
|
|
335
|
+
# return pdf_data
|
|
336
|
+
|
|
337
|
+
def to_csv(self, output: Path | str | None = None) -> dict[str, Any]:
|
|
338
|
+
stats, corr, fi = self.analyze_all()
|
|
339
|
+
csv_builder = StructuredDataExporter(
|
|
340
|
+
table_profile=stats,
|
|
341
|
+
corr_report=corr,
|
|
342
|
+
importance_report=fi,
|
|
343
|
+
)
|
|
344
|
+
csv_data = csv_builder.export_csvs(output)
|
|
345
|
+
return csv_data
|
|
346
|
+
|
|
347
|
+
def to_notebook(self, top_n_features: int = 6, top_n_interactions: int = 6):
|
|
348
|
+
from IPython.display import display, HTML
|
|
349
|
+
|
|
350
|
+
html = self.to_html(
|
|
351
|
+
top_n_features=top_n_features,
|
|
352
|
+
top_n_interactions=top_n_interactions,
|
|
353
|
+
)
|
|
354
|
+
return display(HTML(html))
|