flasheda 0.1.0__tar.gz → 0.1.3__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.
- flasheda-0.1.3/PKG-INFO +49 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/__init__.py +6 -11
- flasheda-0.1.3/flasheda/report.py +522 -0
- flasheda-0.1.3/flasheda.egg-info/PKG-INFO +49 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda.egg-info/requires.txt +1 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/setup.py +8 -3
- flasheda-0.1.0/PKG-INFO +0 -19
- flasheda-0.1.0/flasheda/report.py +0 -348
- flasheda-0.1.0/flasheda.egg-info/PKG-INFO +0 -19
- {flasheda-0.1.0 → flasheda-0.1.3}/README.md +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/__init__.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/categorical.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/correlation.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/missing.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/numeric.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/analyzers/overview.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda/sampler.py +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda.egg-info/SOURCES.txt +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda.egg-info/dependency_links.txt +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/flasheda.egg-info/top_level.txt +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/pyproject.toml +0 -0
- {flasheda-0.1.0 → flasheda-0.1.3}/setup.cfg +0 -0
flasheda-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flasheda
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Constant-time EDA for any dataset size — one line, fixed time.
|
|
5
|
+
Home-page: https://github.com/sjapanjots/flasheda
|
|
6
|
+
Author: Japanjot Singh
|
|
7
|
+
Author-email: sjapanjots@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pandas>=1.3.0
|
|
13
|
+
Requires-Dist: numpy>=1.21.0
|
|
14
|
+
Requires-Dist: scipy>=1.7.0
|
|
15
|
+
Requires-Dist: rich>=12.0.0
|
|
16
|
+
Requires-Dist: jinja2>=3.0.0
|
|
17
|
+
Requires-Dist: matplotlib>=3.4.0
|
|
18
|
+
Requires-Dist: fpdf2>=2.7.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
# ⚡ FlashEDA
|
|
30
|
+
|
|
31
|
+
Constant-time Exploratory Data Analysis — one line, fixed time, any dataset size.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
```bash
|
|
35
|
+
pip install flasheda
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
```python
|
|
40
|
+
import flasheda
|
|
41
|
+
|
|
42
|
+
report = flasheda.analyze(df) # works on 1K or 50M rows in the same time
|
|
43
|
+
report.show() # rich console output
|
|
44
|
+
report.save_html("report.html") # browser report
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
FlashEDA uses **reservoir sampling** to always analyse exactly 5,000 rows,
|
|
49
|
+
regardless of dataset size. All analyzers run in parallel via ThreadPoolExecutor.
|
|
@@ -11,7 +11,7 @@ from .sampler import sample, get_sample_size, set_sample_size
|
|
|
11
11
|
from .analyzers import overview, missing, numeric, categorical, correlation
|
|
12
12
|
from .report import EDAReport
|
|
13
13
|
|
|
14
|
-
__version__ = "0.1.
|
|
14
|
+
__version__ = "0.1.2"
|
|
15
15
|
__all__ = ["analyze", "set_sample_size", "get_sample_size", "EDAReport"]
|
|
16
16
|
|
|
17
17
|
|
|
@@ -21,6 +21,8 @@ def analyze(
|
|
|
21
21
|
*,
|
|
22
22
|
show: bool = False,
|
|
23
23
|
random_state: int = 42,
|
|
24
|
+
save_pdf: bool = True,
|
|
25
|
+
pdf_path: str = "flasheda_report.pdf",
|
|
24
26
|
) -> EDAReport:
|
|
25
27
|
|
|
26
28
|
t0 = time.perf_counter()
|
|
@@ -85,14 +87,7 @@ def analyze(
|
|
|
85
87
|
if show:
|
|
86
88
|
report.show()
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
if save_pdf:
|
|
91
|
+
report.save_pdf(pdf_path)
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
def get_sample_size() -> int:
|
|
92
|
-
from .sampler import get_sample_size as _get
|
|
93
|
-
return _get()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def set_sample_size(n: int) -> None:
|
|
97
|
-
from .sampler import set_sample_size as _set
|
|
98
|
-
_set(n)
|
|
93
|
+
return report
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class EDAReport:
|
|
9
|
+
overview: Dict[str, Any]
|
|
10
|
+
missing: Dict[str, Any]
|
|
11
|
+
numeric: Dict[str, Any]
|
|
12
|
+
categorical: Dict[str, Any]
|
|
13
|
+
correlation: Dict[str, Any]
|
|
14
|
+
sample_size: int
|
|
15
|
+
elapsed_seconds: float
|
|
16
|
+
original_shape: tuple
|
|
17
|
+
warnings: list[str] = field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Console
|
|
21
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def show(self) -> None:
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich import box
|
|
27
|
+
from rich.panel import Panel
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
orig_rows = self.original_shape[0]
|
|
31
|
+
ncols = self.original_shape[1]
|
|
32
|
+
|
|
33
|
+
console.print(
|
|
34
|
+
Panel(
|
|
35
|
+
f"[bold cyan]FlashEDA Report[/bold cyan]\n"
|
|
36
|
+
f"Dataset: [green]{orig_rows:,}[/green] rows × [green]{ncols}[/green] cols "
|
|
37
|
+
f"| Analysed on [yellow]{self.sample_size:,}[/yellow] sampled rows "
|
|
38
|
+
f"| ⏱ [white]{self.elapsed_seconds:.2f}s[/white]",
|
|
39
|
+
expand=False,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if self.warnings:
|
|
44
|
+
for w in self.warnings:
|
|
45
|
+
console.print(f" [bold yellow]⚠ {w}[/bold yellow]")
|
|
46
|
+
console.print()
|
|
47
|
+
|
|
48
|
+
miss = self.missing
|
|
49
|
+
console.print(
|
|
50
|
+
f"[bold]Missing values[/bold] overall: [red]{miss['overall_null_pct']}%[/red] "
|
|
51
|
+
f"| rows with any null: [red]{miss['rows_with_any_null_pct']}%[/red]"
|
|
52
|
+
)
|
|
53
|
+
if miss["critical_columns"]:
|
|
54
|
+
console.print(f" Critical (>50% null): [red]{', '.join(miss['critical_columns'])}[/red]")
|
|
55
|
+
if miss["warn_columns"]:
|
|
56
|
+
console.print(f" Warn (>20% null): [yellow]{', '.join(miss['warn_columns'])}[/yellow]")
|
|
57
|
+
console.print()
|
|
58
|
+
|
|
59
|
+
num_cols = self.numeric.get("numeric_col_names", [])
|
|
60
|
+
if num_cols:
|
|
61
|
+
t = Table(title="Numeric columns", box=box.SIMPLE, show_lines=False)
|
|
62
|
+
for h in ["Column", "Mean", "Std", "Min", "Median", "Max", "Outlier %", "Skew"]:
|
|
63
|
+
t.add_column(h, style="white")
|
|
64
|
+
for col in num_cols:
|
|
65
|
+
s = self.numeric["columns"].get(col, {})
|
|
66
|
+
if "error" in s:
|
|
67
|
+
continue
|
|
68
|
+
flag = "[yellow]⚠[/yellow] " if s.get("skewed") else ""
|
|
69
|
+
t.add_row(
|
|
70
|
+
col,
|
|
71
|
+
str(s.get("mean", "")),
|
|
72
|
+
str(s.get("std", "")),
|
|
73
|
+
str(s.get("min", "")),
|
|
74
|
+
str(s.get("median", "")),
|
|
75
|
+
str(s.get("max", "")),
|
|
76
|
+
f"{s.get('outlier_pct', 0)}%",
|
|
77
|
+
f"{flag}{s.get('skewness', '')}",
|
|
78
|
+
)
|
|
79
|
+
console.print(t)
|
|
80
|
+
|
|
81
|
+
cat_cols = self.categorical.get("categorical_col_names", [])
|
|
82
|
+
if cat_cols:
|
|
83
|
+
t = Table(title="Categorical columns", box=box.SIMPLE, show_lines=False)
|
|
84
|
+
for h in ["Column", "Unique", "Cardinality ratio", "Mode", "Mode %", "Flag"]:
|
|
85
|
+
t.add_column(h)
|
|
86
|
+
for col in cat_cols:
|
|
87
|
+
s = self.categorical["columns"].get(col, {})
|
|
88
|
+
if "error" in s:
|
|
89
|
+
continue
|
|
90
|
+
flag = ""
|
|
91
|
+
if s.get("likely_id_column"):
|
|
92
|
+
flag = "[red]likely ID[/red]"
|
|
93
|
+
elif s.get("high_cardinality"):
|
|
94
|
+
flag = "[yellow]high cardinality[/yellow]"
|
|
95
|
+
t.add_row(
|
|
96
|
+
col,
|
|
97
|
+
str(s.get("unique_count", "")),
|
|
98
|
+
str(s.get("cardinality_ratio", "")),
|
|
99
|
+
str(s.get("mode", ""))[:30],
|
|
100
|
+
f"{s.get('mode_pct', '')}%",
|
|
101
|
+
flag,
|
|
102
|
+
)
|
|
103
|
+
console.print(t)
|
|
104
|
+
|
|
105
|
+
strong = self.correlation.get("numeric_strong_pairs", [])
|
|
106
|
+
if strong:
|
|
107
|
+
console.print("[bold]Strong numeric correlations (|r| ≥ 0.8)[/bold]")
|
|
108
|
+
for pair in strong[:10]:
|
|
109
|
+
console.print(f" {pair['col_a']} ↔ {pair['col_b']} r={pair['pearson_r']}")
|
|
110
|
+
console.print()
|
|
111
|
+
|
|
112
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
113
|
+
# HTML
|
|
114
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def to_html(self) -> str:
|
|
117
|
+
return _render_html(self)
|
|
118
|
+
|
|
119
|
+
def save_html(self, path: str = "flasheda_report.html") -> Path:
|
|
120
|
+
p = Path(path)
|
|
121
|
+
p.write_text(self.to_html(), encoding="utf-8")
|
|
122
|
+
print(f"HTML report saved → {p.resolve()}")
|
|
123
|
+
return p
|
|
124
|
+
|
|
125
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
126
|
+
# PDF ← THE METHOD THAT WAS MISSING
|
|
127
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
def save_pdf(self, path: str = "flasheda_report.pdf") -> Path:
|
|
130
|
+
"""
|
|
131
|
+
Save a full EDA report as a PDF file.
|
|
132
|
+
Requires: pip install fpdf2
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
from fpdf import FPDF
|
|
136
|
+
except ImportError:
|
|
137
|
+
raise ImportError(
|
|
138
|
+
"fpdf2 is required for PDF output. "
|
|
139
|
+
"Install it with: pip install fpdf2"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
pdf = FPDF()
|
|
143
|
+
pdf.set_auto_page_break(auto=True, margin=15)
|
|
144
|
+
pdf.add_page()
|
|
145
|
+
|
|
146
|
+
orig_rows, orig_cols = self.original_shape
|
|
147
|
+
|
|
148
|
+
# ── Title bar ──────────────────────────────────────────────────────
|
|
149
|
+
pdf.set_font("Helvetica", "B", 20)
|
|
150
|
+
pdf.set_fill_color(26, 26, 46)
|
|
151
|
+
pdf.set_text_color(255, 255, 255)
|
|
152
|
+
pdf.cell(0, 14, "FlashEDA Report",
|
|
153
|
+
new_x="LMARGIN", new_y="NEXT", fill=True, align="C")
|
|
154
|
+
|
|
155
|
+
pdf.set_font("Helvetica", "", 9)
|
|
156
|
+
pdf.set_fill_color(40, 40, 70)
|
|
157
|
+
pdf.cell(
|
|
158
|
+
0, 8,
|
|
159
|
+
f"Rows: {orig_rows:,} | Columns: {orig_cols} | "
|
|
160
|
+
f"Sample: {self.sample_size:,} | Time: {self.elapsed_seconds:.2f}s",
|
|
161
|
+
new_x="LMARGIN", new_y="NEXT", fill=True, align="C"
|
|
162
|
+
)
|
|
163
|
+
pdf.ln(4)
|
|
164
|
+
pdf.set_text_color(0, 0, 0)
|
|
165
|
+
|
|
166
|
+
# ── Warnings ───────────────────────────────────────────────────────
|
|
167
|
+
if self.warnings:
|
|
168
|
+
_pdf_section_title(pdf, "Warnings")
|
|
169
|
+
pdf.set_font("Helvetica", "", 9)
|
|
170
|
+
pdf.set_text_color(180, 100, 0)
|
|
171
|
+
for w in self.warnings:
|
|
172
|
+
pdf.cell(0, 6, f" ! {w}", new_x="LMARGIN", new_y="NEXT")
|
|
173
|
+
pdf.set_text_color(0, 0, 0)
|
|
174
|
+
pdf.ln(2)
|
|
175
|
+
|
|
176
|
+
# ── Overview ───────────────────────────────────────────────────────
|
|
177
|
+
_pdf_section_title(pdf, "Dataset Overview")
|
|
178
|
+
_pdf_kv_table(pdf, [
|
|
179
|
+
["Total rows", f"{orig_rows:,}"],
|
|
180
|
+
["Total columns", str(orig_cols)],
|
|
181
|
+
["Sample size used", f"{self.sample_size:,}"],
|
|
182
|
+
["Analysis time", f"{self.elapsed_seconds:.3f}s"],
|
|
183
|
+
["Overall null %", f"{self.missing.get('overall_null_pct', 0)}%"],
|
|
184
|
+
["Duplicate rows (sample)", f"{self.overview.get('duplicate_pct_in_sample', 0)}%"],
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
# ── Missing values ─────────────────────────────────────────────────
|
|
188
|
+
_pdf_section_title(pdf, "Missing Values")
|
|
189
|
+
miss_rows = [
|
|
190
|
+
[col,
|
|
191
|
+
str(info.get("null_count", 0)),
|
|
192
|
+
f"{info.get('null_pct', 0)}%",
|
|
193
|
+
info.get("severity", "ok").upper()]
|
|
194
|
+
for col, info in self.missing.get("per_column", {}).items()
|
|
195
|
+
]
|
|
196
|
+
_pdf_table(pdf, ["Column", "Null Count", "Null %", "Status"], miss_rows)
|
|
197
|
+
|
|
198
|
+
# ── Numeric columns ────────────────────────────────────────────────
|
|
199
|
+
num_cols = self.numeric.get("numeric_col_names", [])
|
|
200
|
+
if num_cols:
|
|
201
|
+
_pdf_section_title(pdf, "Numeric Columns")
|
|
202
|
+
num_rows = []
|
|
203
|
+
for col in num_cols:
|
|
204
|
+
s = self.numeric["columns"].get(col, {})
|
|
205
|
+
if "error" in s:
|
|
206
|
+
continue
|
|
207
|
+
num_rows.append([
|
|
208
|
+
col,
|
|
209
|
+
str(s.get("mean", "")),
|
|
210
|
+
str(s.get("std", "")),
|
|
211
|
+
str(s.get("min", "")),
|
|
212
|
+
str(s.get("median", "")),
|
|
213
|
+
str(s.get("max", "")),
|
|
214
|
+
f"{s.get('outlier_pct', 0)}%",
|
|
215
|
+
str(s.get("skewness", "")),
|
|
216
|
+
])
|
|
217
|
+
_pdf_table(pdf,
|
|
218
|
+
["Column", "Mean", "Std", "Min", "Median",
|
|
219
|
+
"Max", "Outlier%", "Skew"],
|
|
220
|
+
num_rows)
|
|
221
|
+
|
|
222
|
+
# ── Categorical columns ────────────────────────────────────────────
|
|
223
|
+
cat_cols = self.categorical.get("categorical_col_names", [])
|
|
224
|
+
if cat_cols:
|
|
225
|
+
_pdf_section_title(pdf, "Categorical Columns")
|
|
226
|
+
cat_rows = []
|
|
227
|
+
for col in cat_cols:
|
|
228
|
+
s = self.categorical["columns"].get(col, {})
|
|
229
|
+
if "error" in s:
|
|
230
|
+
continue
|
|
231
|
+
flag = ("LIKELY ID" if s.get("likely_id_column")
|
|
232
|
+
else "HIGH CARD" if s.get("high_cardinality") else "")
|
|
233
|
+
cat_rows.append([
|
|
234
|
+
col,
|
|
235
|
+
str(s.get("unique_count", "")),
|
|
236
|
+
str(s.get("cardinality_ratio", "")),
|
|
237
|
+
str(s.get("mode", ""))[:20],
|
|
238
|
+
f"{s.get('mode_pct', '')}%",
|
|
239
|
+
flag,
|
|
240
|
+
])
|
|
241
|
+
_pdf_table(pdf,
|
|
242
|
+
["Column", "Unique", "Cardinality",
|
|
243
|
+
"Mode", "Mode %", "Flag"],
|
|
244
|
+
cat_rows)
|
|
245
|
+
|
|
246
|
+
# ── Correlations ───────────────────────────────────────────────────
|
|
247
|
+
strong = self.correlation.get("numeric_strong_pairs", [])
|
|
248
|
+
if strong:
|
|
249
|
+
_pdf_section_title(pdf, "Strong Correlations (|r| >= 0.8)")
|
|
250
|
+
_pdf_table(pdf,
|
|
251
|
+
["Column A", "Column B", "Pearson r"],
|
|
252
|
+
[[p["col_a"], p["col_b"], str(p["pearson_r"])]
|
|
253
|
+
for p in strong[:20]])
|
|
254
|
+
|
|
255
|
+
# ── Footer ─────────────────────────────────────────────────────────
|
|
256
|
+
pdf.ln(6)
|
|
257
|
+
pdf.set_font("Helvetica", "I", 8)
|
|
258
|
+
pdf.set_text_color(150, 150, 150)
|
|
259
|
+
pdf.cell(0, 6,
|
|
260
|
+
"Generated by FlashEDA — constant-time EDA for any dataset size",
|
|
261
|
+
align="C", new_x="LMARGIN", new_y="NEXT")
|
|
262
|
+
|
|
263
|
+
p = Path(path)
|
|
264
|
+
pdf.output(str(p))
|
|
265
|
+
print(f"PDF report saved -> {p.resolve()}")
|
|
266
|
+
return p
|
|
267
|
+
|
|
268
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
269
|
+
# Serialisation
|
|
270
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
273
|
+
return {
|
|
274
|
+
"overview": self.overview,
|
|
275
|
+
"missing": self.missing,
|
|
276
|
+
"numeric": self.numeric,
|
|
277
|
+
"categorical": self.categorical,
|
|
278
|
+
"correlation": self.correlation,
|
|
279
|
+
"sample_size": self.sample_size,
|
|
280
|
+
"elapsed_seconds": self.elapsed_seconds,
|
|
281
|
+
"original_shape": list(self.original_shape),
|
|
282
|
+
"warnings": self.warnings,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
def to_json(self, indent: int = 2) -> str:
|
|
286
|
+
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
287
|
+
|
|
288
|
+
def __repr__(self) -> str:
|
|
289
|
+
rows, cols = self.original_shape
|
|
290
|
+
return (
|
|
291
|
+
f"EDAReport(rows={rows:,}, cols={cols}, "
|
|
292
|
+
f"sample={self.sample_size:,}, elapsed={self.elapsed_seconds:.2f}s)"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
297
|
+
# PDF helpers
|
|
298
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
def _pdf_section_title(pdf, title: str) -> None:
|
|
301
|
+
pdf.set_font("Helvetica", "B", 11)
|
|
302
|
+
pdf.set_fill_color(230, 235, 245)
|
|
303
|
+
pdf.set_text_color(26, 26, 46)
|
|
304
|
+
pdf.cell(0, 8, f" {title}",
|
|
305
|
+
new_x="LMARGIN", new_y="NEXT", fill=True)
|
|
306
|
+
pdf.set_text_color(0, 0, 0)
|
|
307
|
+
pdf.ln(2)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _pdf_kv_table(pdf, rows: list) -> None:
|
|
311
|
+
pdf.set_font("Helvetica", "", 9)
|
|
312
|
+
col_w = 70
|
|
313
|
+
for key, val in rows:
|
|
314
|
+
pdf.set_fill_color(248, 249, 252)
|
|
315
|
+
pdf.cell(col_w, 7, f" {key}", border=1, fill=True)
|
|
316
|
+
pdf.cell(col_w, 7, f" {val}", border=1,
|
|
317
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
318
|
+
pdf.ln(4)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _pdf_table(pdf, headers: list, rows: list) -> None:
|
|
322
|
+
if not rows:
|
|
323
|
+
pdf.set_font("Helvetica", "I", 9)
|
|
324
|
+
pdf.cell(0, 6, " No data.",
|
|
325
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
326
|
+
pdf.ln(2)
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
page_w = pdf.w - pdf.l_margin - pdf.r_margin
|
|
330
|
+
col_w = page_w / len(headers)
|
|
331
|
+
|
|
332
|
+
# Header
|
|
333
|
+
pdf.set_font("Helvetica", "B", 8)
|
|
334
|
+
pdf.set_fill_color(26, 26, 46)
|
|
335
|
+
pdf.set_text_color(255, 255, 255)
|
|
336
|
+
for h in headers:
|
|
337
|
+
pdf.cell(col_w, 7, f" {h}", border=1, fill=True)
|
|
338
|
+
pdf.ln()
|
|
339
|
+
|
|
340
|
+
# Rows
|
|
341
|
+
pdf.set_font("Helvetica", "", 8)
|
|
342
|
+
pdf.set_text_color(0, 0, 0)
|
|
343
|
+
for i, row in enumerate(rows):
|
|
344
|
+
pdf.set_fill_color(255, 255, 255) if i % 2 == 0 \
|
|
345
|
+
else pdf.set_fill_color(245, 247, 252)
|
|
346
|
+
for cell in row:
|
|
347
|
+
pdf.cell(col_w, 6, f" {str(cell)[:22]}", border=1, fill=True)
|
|
348
|
+
pdf.ln()
|
|
349
|
+
|
|
350
|
+
pdf.ln(4)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
# HTML renderer
|
|
355
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
def _render_html(r: EDAReport) -> str:
|
|
358
|
+
num_stats = r.numeric.get("numeric_col_names", [])
|
|
359
|
+
cat_stats = r.categorical.get("categorical_col_names", [])
|
|
360
|
+
strong_corr = r.correlation.get("numeric_strong_pairs", [])
|
|
361
|
+
miss_per_col = r.missing.get("per_column", {})
|
|
362
|
+
|
|
363
|
+
num_rows_html = ""
|
|
364
|
+
for col in num_stats:
|
|
365
|
+
s = r.numeric["columns"].get(col, {})
|
|
366
|
+
if "error" in s:
|
|
367
|
+
continue
|
|
368
|
+
skew_badge = '<span class="badge warn">skewed</span>' if s.get("skewed") else ""
|
|
369
|
+
out_badge = '<span class="badge warn">outliers</span>' if s.get("outlier_pct", 0) > 5 else ""
|
|
370
|
+
num_rows_html += f"""
|
|
371
|
+
<tr>
|
|
372
|
+
<td><strong>{col}</strong></td>
|
|
373
|
+
<td>{s.get('mean','')}</td><td>{s.get('std','')}</td>
|
|
374
|
+
<td>{s.get('min','')}</td><td>{s.get('median','')}</td>
|
|
375
|
+
<td>{s.get('max','')}</td>
|
|
376
|
+
<td>{s.get('outlier_pct','')}%</td>
|
|
377
|
+
<td>{s.get('skewness','')} {skew_badge}</td>
|
|
378
|
+
<td>{out_badge}</td>
|
|
379
|
+
</tr>"""
|
|
380
|
+
|
|
381
|
+
cat_rows_html = ""
|
|
382
|
+
for col in cat_stats:
|
|
383
|
+
s = r.categorical["columns"].get(col, {})
|
|
384
|
+
if "error" in s:
|
|
385
|
+
continue
|
|
386
|
+
flag = ""
|
|
387
|
+
if s.get("likely_id_column"):
|
|
388
|
+
flag = '<span class="badge danger">likely ID</span>'
|
|
389
|
+
elif s.get("high_cardinality"):
|
|
390
|
+
flag = '<span class="badge warn">high cardinality</span>'
|
|
391
|
+
top = s.get("top_values", [])[:5]
|
|
392
|
+
top_str = " · ".join(f"{t['value']} ({t['pct']}%)" for t in top)
|
|
393
|
+
cat_rows_html += f"""
|
|
394
|
+
<tr>
|
|
395
|
+
<td><strong>{col}</strong></td>
|
|
396
|
+
<td>{s.get('unique_count','')}</td>
|
|
397
|
+
<td>{s.get('cardinality_ratio','')}</td>
|
|
398
|
+
<td>{str(s.get('mode',''))[:40]}</td>
|
|
399
|
+
<td>{s.get('mode_pct','')}%</td>
|
|
400
|
+
<td class="muted">{top_str[:80]}</td>
|
|
401
|
+
<td>{flag}</td>
|
|
402
|
+
</tr>"""
|
|
403
|
+
|
|
404
|
+
miss_rows_html = ""
|
|
405
|
+
for col, info in miss_per_col.items():
|
|
406
|
+
sev = info.get("severity", "ok")
|
|
407
|
+
color = {"ok": "#27ae60", "warn": "#f39c12",
|
|
408
|
+
"critical": "#e74c3c"}.get(sev, "#888")
|
|
409
|
+
bar_w = min(100, int(info.get("null_pct", 0)))
|
|
410
|
+
miss_rows_html += f"""
|
|
411
|
+
<tr>
|
|
412
|
+
<td>{col}</td>
|
|
413
|
+
<td>{info.get('null_count', 0)}</td>
|
|
414
|
+
<td>
|
|
415
|
+
<div class="bar-bg">
|
|
416
|
+
<div class="bar-fill" style="width:{bar_w}%;background:{color}"></div>
|
|
417
|
+
</div>
|
|
418
|
+
{info.get('null_pct', 0)}%
|
|
419
|
+
</td>
|
|
420
|
+
<td><span style="color:{color};font-weight:500">{sev}</span></td>
|
|
421
|
+
</tr>"""
|
|
422
|
+
|
|
423
|
+
corr_html = ""
|
|
424
|
+
for p in strong_corr[:15]:
|
|
425
|
+
corr_html += (f"<tr><td>{p['col_a']}</td>"
|
|
426
|
+
f"<td>{p['col_b']}</td>"
|
|
427
|
+
f"<td>{p['pearson_r']}</td></tr>")
|
|
428
|
+
|
|
429
|
+
warn_html = "".join(
|
|
430
|
+
f'<div class="warn-banner">⚠ {w}</div>'
|
|
431
|
+
for w in r.warnings
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
orig_rows, orig_cols = r.original_shape
|
|
435
|
+
|
|
436
|
+
return f"""<!DOCTYPE html>
|
|
437
|
+
<html lang="en">
|
|
438
|
+
<head>
|
|
439
|
+
<meta charset="UTF-8">
|
|
440
|
+
<title>FlashEDA Report</title>
|
|
441
|
+
<style>
|
|
442
|
+
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
443
|
+
body{{font-family:system-ui,sans-serif;background:#f5f7fa;color:#222;font-size:14px}}
|
|
444
|
+
.container{{max-width:1100px;margin:0 auto;padding:24px 16px}}
|
|
445
|
+
header{{background:#1a1a2e;color:#fff;padding:24px 32px;border-radius:10px;margin-bottom:24px}}
|
|
446
|
+
header h1{{font-size:22px;font-weight:600;margin-bottom:6px}}
|
|
447
|
+
header .meta{{font-size:13px;color:#aac}}
|
|
448
|
+
.card{{background:#fff;border-radius:10px;padding:20px 24px;margin-bottom:20px;box-shadow:0 1px 4px rgba(0,0,0,.07)}}
|
|
449
|
+
.card h2{{font-size:15px;font-weight:600;margin-bottom:14px;border-bottom:1px solid #eee;padding-bottom:8px}}
|
|
450
|
+
table{{width:100%;border-collapse:collapse;font-size:13px}}
|
|
451
|
+
th{{text-align:left;padding:6px 10px;background:#f0f2f5;font-weight:500}}
|
|
452
|
+
td{{padding:6px 10px;border-top:1px solid #f0f0f0;vertical-align:middle}}
|
|
453
|
+
.badge{{display:inline-block;padding:2px 7px;border-radius:12px;font-size:11px;font-weight:600}}
|
|
454
|
+
.badge.warn{{background:#fff3cd;color:#856404}}
|
|
455
|
+
.badge.danger{{background:#f8d7da;color:#842029}}
|
|
456
|
+
.muted{{color:#888;font-size:12px}}
|
|
457
|
+
.bar-bg{{display:inline-block;width:80px;height:8px;background:#eee;border-radius:4px;vertical-align:middle;margin-right:6px}}
|
|
458
|
+
.bar-fill{{height:8px;border-radius:4px}}
|
|
459
|
+
.warn-banner{{background:#fff3cd;color:#856404;padding:10px 14px;border-radius:8px;margin-bottom:10px;font-size:13px}}
|
|
460
|
+
.stats-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:4px}}
|
|
461
|
+
.stat-box{{background:#f8f9fc;border-radius:8px;padding:14px 16px}}
|
|
462
|
+
.stat-box .label{{font-size:11px;color:#888;margin-bottom:4px}}
|
|
463
|
+
.stat-box .value{{font-size:20px;font-weight:600;color:#1a1a2e}}
|
|
464
|
+
</style>
|
|
465
|
+
</head>
|
|
466
|
+
<body>
|
|
467
|
+
<div class="container">
|
|
468
|
+
<header>
|
|
469
|
+
<h1>FlashEDA Report</h1>
|
|
470
|
+
<div class="meta">
|
|
471
|
+
{orig_rows:,} rows x {orig_cols} columns |
|
|
472
|
+
Analysed on {r.sample_size:,} sampled rows |
|
|
473
|
+
Completed in {r.elapsed_seconds:.2f}s
|
|
474
|
+
</div>
|
|
475
|
+
</header>
|
|
476
|
+
{warn_html}
|
|
477
|
+
<div class="card">
|
|
478
|
+
<h2>Dataset overview</h2>
|
|
479
|
+
<div class="stats-grid">
|
|
480
|
+
<div class="stat-box"><div class="label">Total rows</div><div class="value">{orig_rows:,}</div></div>
|
|
481
|
+
<div class="stat-box"><div class="label">Columns</div><div class="value">{orig_cols}</div></div>
|
|
482
|
+
<div class="stat-box"><div class="label">Sample size</div><div class="value">{r.sample_size:,}</div></div>
|
|
483
|
+
<div class="stat-box"><div class="label">Analysis time</div><div class="value">{r.elapsed_seconds:.2f}s</div></div>
|
|
484
|
+
<div class="stat-box"><div class="label">Overall null %</div><div class="value">{r.missing.get('overall_null_pct',0)}%</div></div>
|
|
485
|
+
<div class="stat-box"><div class="label">Duplicates (sample)</div><div class="value">{r.overview.get('duplicate_pct_in_sample',0)}%</div></div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="card">
|
|
489
|
+
<h2>Missing values</h2>
|
|
490
|
+
<table>
|
|
491
|
+
<thead><tr><th>Column</th><th>Null count</th><th>Null %</th><th>Status</th></tr></thead>
|
|
492
|
+
<tbody>{miss_rows_html}</tbody>
|
|
493
|
+
</table>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="card">
|
|
496
|
+
<h2>Numeric columns</h2>
|
|
497
|
+
<table>
|
|
498
|
+
<thead><tr><th>Column</th><th>Mean</th><th>Std</th><th>Min</th><th>Median</th><th>Max</th><th>Outlier %</th><th>Skewness</th><th>Flags</th></tr></thead>
|
|
499
|
+
<tbody>{num_rows_html}</tbody>
|
|
500
|
+
</table>
|
|
501
|
+
</div>
|
|
502
|
+
<div class="card">
|
|
503
|
+
<h2>Categorical columns</h2>
|
|
504
|
+
<table>
|
|
505
|
+
<thead><tr><th>Column</th><th>Unique</th><th>Cardinality</th><th>Mode</th><th>Mode %</th><th>Top values</th><th>Flags</th></tr></thead>
|
|
506
|
+
<tbody>{cat_rows_html}</tbody>
|
|
507
|
+
</table>
|
|
508
|
+
</div>
|
|
509
|
+
{"" if not corr_html else f'''
|
|
510
|
+
<div class="card">
|
|
511
|
+
<h2>Strong correlations (|r| >= 0.8)</h2>
|
|
512
|
+
<table>
|
|
513
|
+
<thead><tr><th>Column A</th><th>Column B</th><th>Pearson r</th></tr></thead>
|
|
514
|
+
<tbody>{corr_html}</tbody>
|
|
515
|
+
</table>
|
|
516
|
+
</div>'''}
|
|
517
|
+
<p class="muted" style="text-align:center;margin-top:16px">
|
|
518
|
+
Generated by FlashEDA
|
|
519
|
+
</p>
|
|
520
|
+
</div>
|
|
521
|
+
</body>
|
|
522
|
+
</html>"""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flasheda
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Constant-time EDA for any dataset size — one line, fixed time.
|
|
5
|
+
Home-page: https://github.com/sjapanjots/flasheda
|
|
6
|
+
Author: Japanjot Singh
|
|
7
|
+
Author-email: sjapanjots@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pandas>=1.3.0
|
|
13
|
+
Requires-Dist: numpy>=1.21.0
|
|
14
|
+
Requires-Dist: scipy>=1.7.0
|
|
15
|
+
Requires-Dist: rich>=12.0.0
|
|
16
|
+
Requires-Dist: jinja2>=3.0.0
|
|
17
|
+
Requires-Dist: matplotlib>=3.4.0
|
|
18
|
+
Requires-Dist: fpdf2>=2.7.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
# ⚡ FlashEDA
|
|
30
|
+
|
|
31
|
+
Constant-time Exploratory Data Analysis — one line, fixed time, any dataset size.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
```bash
|
|
35
|
+
pip install flasheda
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
```python
|
|
40
|
+
import flasheda
|
|
41
|
+
|
|
42
|
+
report = flasheda.analyze(df) # works on 1K or 50M rows in the same time
|
|
43
|
+
report.show() # rich console output
|
|
44
|
+
report.save_html("report.html") # browser report
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
FlashEDA uses **reservoir sampling** to always analyse exactly 5,000 rows,
|
|
49
|
+
regardless of dataset size. All analyzers run in parallel via ThreadPoolExecutor.
|
|
@@ -2,9 +2,13 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="flasheda",
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.3",
|
|
6
6
|
description="Constant-time EDA for any dataset size — one line, fixed time.",
|
|
7
|
-
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Japanjot Singh",
|
|
10
|
+
author_email="sjapanjots@gmail.com",
|
|
11
|
+
url="https://github.com/sjapanjots/flasheda",
|
|
8
12
|
packages=find_packages(),
|
|
9
13
|
python_requires=">=3.8",
|
|
10
14
|
install_requires=[
|
|
@@ -14,9 +18,10 @@ setup(
|
|
|
14
18
|
"rich>=12.0.0",
|
|
15
19
|
"jinja2>=3.0.0",
|
|
16
20
|
"matplotlib>=3.4.0",
|
|
21
|
+
"fpdf2>=2.7.0",
|
|
17
22
|
],
|
|
18
23
|
classifiers=[
|
|
19
24
|
"Programming Language :: Python :: 3",
|
|
20
|
-
"
|
|
25
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
21
26
|
],
|
|
22
27
|
)
|
flasheda-0.1.0/PKG-INFO
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: flasheda
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Constant-time EDA for any dataset size — one line, fixed time.
|
|
5
|
-
Author: Your Name
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
-
Requires-Python: >=3.8
|
|
9
|
-
Requires-Dist: pandas>=1.3.0
|
|
10
|
-
Requires-Dist: numpy>=1.21.0
|
|
11
|
-
Requires-Dist: scipy>=1.7.0
|
|
12
|
-
Requires-Dist: rich>=12.0.0
|
|
13
|
-
Requires-Dist: jinja2>=3.0.0
|
|
14
|
-
Requires-Dist: matplotlib>=3.4.0
|
|
15
|
-
Dynamic: author
|
|
16
|
-
Dynamic: classifier
|
|
17
|
-
Dynamic: requires-dist
|
|
18
|
-
Dynamic: requires-python
|
|
19
|
-
Dynamic: summary
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
flasheda.report
|
|
3
|
-
~~~~~~~~~~~~~~~
|
|
4
|
-
EDAReport: the single object returned by flasheda.analyze().
|
|
5
|
-
Supports:
|
|
6
|
-
- .show() → rich console table
|
|
7
|
-
- .to_html() → self-contained HTML report
|
|
8
|
-
- .to_dict() → plain dict
|
|
9
|
-
- .to_json() → JSON string
|
|
10
|
-
- .save_html() → write HTML to disk
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
import time
|
|
17
|
-
from dataclasses import dataclass, field
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import Any, Dict, Optional
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@dataclass
|
|
23
|
-
class EDAReport:
|
|
24
|
-
overview: Dict[str, Any]
|
|
25
|
-
missing: Dict[str, Any]
|
|
26
|
-
numeric: Dict[str, Any]
|
|
27
|
-
categorical: Dict[str, Any]
|
|
28
|
-
correlation: Dict[str, Any]
|
|
29
|
-
sample_size: int
|
|
30
|
-
elapsed_seconds: float
|
|
31
|
-
original_shape: tuple
|
|
32
|
-
warnings: list[str] = field(default_factory=list)
|
|
33
|
-
|
|
34
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
35
|
-
# Console output
|
|
36
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
def show(self) -> None:
|
|
39
|
-
"""Print a rich summary to the console."""
|
|
40
|
-
from rich.console import Console
|
|
41
|
-
from rich.table import Table
|
|
42
|
-
from rich import box
|
|
43
|
-
from rich.panel import Panel
|
|
44
|
-
|
|
45
|
-
console = Console()
|
|
46
|
-
|
|
47
|
-
# ── Header ──
|
|
48
|
-
orig_rows = self.original_shape[0]
|
|
49
|
-
ncols = self.original_shape[1]
|
|
50
|
-
console.print(
|
|
51
|
-
Panel(
|
|
52
|
-
f"[bold cyan]FlashEDA Report[/bold cyan]\n"
|
|
53
|
-
f"Dataset: [green]{orig_rows:,}[/green] rows × [green]{ncols}[/green] cols "
|
|
54
|
-
f"| Analysed on [yellow]{self.sample_size:,}[/yellow] sampled rows "
|
|
55
|
-
f"| ⏱ [white]{self.elapsed_seconds:.2f}s[/white]",
|
|
56
|
-
expand=False,
|
|
57
|
-
)
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# ── Warnings ──
|
|
61
|
-
if self.warnings:
|
|
62
|
-
for w in self.warnings:
|
|
63
|
-
console.print(f" [bold yellow]⚠ {w}[/bold yellow]")
|
|
64
|
-
console.print()
|
|
65
|
-
|
|
66
|
-
# ── Missing values ──
|
|
67
|
-
miss = self.missing
|
|
68
|
-
console.print(f"[bold]Missing values[/bold] overall: [red]{miss['overall_null_pct']}%[/red] "
|
|
69
|
-
f"| rows with any null: [red]{miss['rows_with_any_null_pct']}%[/red]")
|
|
70
|
-
if miss["critical_columns"]:
|
|
71
|
-
console.print(f" Critical (>50% null): [red]{', '.join(miss['critical_columns'])}[/red]")
|
|
72
|
-
if miss["warn_columns"]:
|
|
73
|
-
console.print(f" Warn (>20% null): [yellow]{', '.join(miss['warn_columns'])}[/yellow]")
|
|
74
|
-
console.print()
|
|
75
|
-
|
|
76
|
-
# ── Numeric summary table ──
|
|
77
|
-
num_cols = self.numeric.get("numeric_col_names", [])
|
|
78
|
-
if num_cols:
|
|
79
|
-
t = Table(title="Numeric columns", box=box.SIMPLE, show_lines=False)
|
|
80
|
-
for h in ["Column", "Mean", "Std", "Min", "Median", "Max", "Outlier %", "Skew"]:
|
|
81
|
-
t.add_column(h, style="white")
|
|
82
|
-
for col in num_cols:
|
|
83
|
-
s = self.numeric["columns"].get(col, {})
|
|
84
|
-
if "error" in s:
|
|
85
|
-
continue
|
|
86
|
-
flag = "[yellow]⚠[/yellow] " if s.get("skewed") else ""
|
|
87
|
-
t.add_row(
|
|
88
|
-
col,
|
|
89
|
-
str(s.get("mean", "")),
|
|
90
|
-
str(s.get("std", "")),
|
|
91
|
-
str(s.get("min", "")),
|
|
92
|
-
str(s.get("median", "")),
|
|
93
|
-
str(s.get("max", "")),
|
|
94
|
-
f"{s.get('outlier_pct', 0)}%",
|
|
95
|
-
f"{flag}{s.get('skewness', '')}",
|
|
96
|
-
)
|
|
97
|
-
console.print(t)
|
|
98
|
-
|
|
99
|
-
# ── Categorical summary table ──
|
|
100
|
-
cat_cols = self.categorical.get("categorical_col_names", [])
|
|
101
|
-
if cat_cols:
|
|
102
|
-
t = Table(title="Categorical columns", box=box.SIMPLE, show_lines=False)
|
|
103
|
-
for h in ["Column", "Unique", "Cardinality ratio", "Mode", "Mode %", "Flag"]:
|
|
104
|
-
t.add_column(h)
|
|
105
|
-
for col in cat_cols:
|
|
106
|
-
s = self.categorical["columns"].get(col, {})
|
|
107
|
-
if "error" in s:
|
|
108
|
-
continue
|
|
109
|
-
flag = ""
|
|
110
|
-
if s.get("likely_id_column"):
|
|
111
|
-
flag = "[red]likely ID[/red]"
|
|
112
|
-
elif s.get("high_cardinality"):
|
|
113
|
-
flag = "[yellow]high cardinality[/yellow]"
|
|
114
|
-
t.add_row(
|
|
115
|
-
col,
|
|
116
|
-
str(s.get("unique_count", "")),
|
|
117
|
-
str(s.get("cardinality_ratio", "")),
|
|
118
|
-
str(s.get("mode", ""))[:30],
|
|
119
|
-
f"{s.get('mode_pct', '')}%",
|
|
120
|
-
flag,
|
|
121
|
-
)
|
|
122
|
-
console.print(t)
|
|
123
|
-
|
|
124
|
-
# ── Strong correlations ──
|
|
125
|
-
strong = self.correlation.get("numeric_strong_pairs", [])
|
|
126
|
-
if strong:
|
|
127
|
-
console.print("[bold]Strong numeric correlations (|r| ≥ 0.8)[/bold]")
|
|
128
|
-
for pair in strong[:10]:
|
|
129
|
-
console.print(f" {pair['col_a']} ↔ {pair['col_b']} r={pair['pearson_r']}")
|
|
130
|
-
console.print()
|
|
131
|
-
|
|
132
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
133
|
-
# HTML report
|
|
134
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
def to_html(self) -> str:
|
|
137
|
-
"""Return a self-contained HTML report string."""
|
|
138
|
-
return _render_html(self)
|
|
139
|
-
|
|
140
|
-
def save_html(self, path: str = "flasheda_report.html") -> Path:
|
|
141
|
-
"""Save the HTML report to disk and return the path."""
|
|
142
|
-
p = Path(path)
|
|
143
|
-
p.write_text(self.to_html(), encoding="utf-8")
|
|
144
|
-
return p
|
|
145
|
-
|
|
146
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
147
|
-
# Serialisation
|
|
148
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
151
|
-
return {
|
|
152
|
-
"overview": self.overview,
|
|
153
|
-
"missing": self.missing,
|
|
154
|
-
"numeric": self.numeric,
|
|
155
|
-
"categorical": self.categorical,
|
|
156
|
-
"correlation": self.correlation,
|
|
157
|
-
"sample_size": self.sample_size,
|
|
158
|
-
"elapsed_seconds": self.elapsed_seconds,
|
|
159
|
-
"original_shape": list(self.original_shape),
|
|
160
|
-
"warnings": self.warnings,
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
def to_json(self, indent: int = 2) -> str:
|
|
164
|
-
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
165
|
-
|
|
166
|
-
def __repr__(self) -> str:
|
|
167
|
-
rows, cols = self.original_shape
|
|
168
|
-
return (
|
|
169
|
-
f"EDAReport(rows={rows:,}, cols={cols}, "
|
|
170
|
-
f"sample={self.sample_size:,}, elapsed={self.elapsed_seconds:.2f}s)"
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
175
|
-
# HTML renderer
|
|
176
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
def _render_html(r: EDAReport) -> str:
|
|
179
|
-
num_stats = r.numeric.get("numeric_col_names", [])
|
|
180
|
-
cat_stats = r.categorical.get("categorical_col_names", [])
|
|
181
|
-
strong_corr = r.correlation.get("numeric_strong_pairs", [])
|
|
182
|
-
miss_per_col = r.missing.get("per_column", {})
|
|
183
|
-
|
|
184
|
-
# Build numeric rows
|
|
185
|
-
num_rows_html = ""
|
|
186
|
-
for col in num_stats:
|
|
187
|
-
s = r.numeric["columns"].get(col, {})
|
|
188
|
-
if "error" in s:
|
|
189
|
-
continue
|
|
190
|
-
skew_badge = '<span class="badge warn">skewed</span>' if s.get("skewed") else ""
|
|
191
|
-
out_badge = '<span class="badge warn">outliers</span>' if s.get("outlier_pct", 0) > 5 else ""
|
|
192
|
-
num_rows_html += f"""
|
|
193
|
-
<tr>
|
|
194
|
-
<td><strong>{col}</strong></td>
|
|
195
|
-
<td>{s.get('mean', '')}</td><td>{s.get('std', '')}</td>
|
|
196
|
-
<td>{s.get('min', '')}</td><td>{s.get('median', '')}</td><td>{s.get('max', '')}</td>
|
|
197
|
-
<td>{s.get('outlier_pct', '')}%</td>
|
|
198
|
-
<td>{s.get('skewness', '')} {skew_badge}</td>
|
|
199
|
-
<td>{out_badge}</td>
|
|
200
|
-
</tr>"""
|
|
201
|
-
|
|
202
|
-
# Build categorical rows
|
|
203
|
-
cat_rows_html = ""
|
|
204
|
-
for col in cat_stats:
|
|
205
|
-
s = r.categorical["columns"].get(col, {})
|
|
206
|
-
if "error" in s:
|
|
207
|
-
continue
|
|
208
|
-
flag = ""
|
|
209
|
-
if s.get("likely_id_column"):
|
|
210
|
-
flag = '<span class="badge danger">likely ID</span>'
|
|
211
|
-
elif s.get("high_cardinality"):
|
|
212
|
-
flag = '<span class="badge warn">high cardinality</span>'
|
|
213
|
-
top = s.get("top_values", [])[:5]
|
|
214
|
-
top_str = " · ".join(f"{t['value']} ({t['pct']}%)" for t in top)
|
|
215
|
-
cat_rows_html += f"""
|
|
216
|
-
<tr>
|
|
217
|
-
<td><strong>{col}</strong></td>
|
|
218
|
-
<td>{s.get('unique_count', '')}</td>
|
|
219
|
-
<td>{s.get('cardinality_ratio', '')}</td>
|
|
220
|
-
<td>{s.get('mode', '')[:40]}</td>
|
|
221
|
-
<td>{s.get('mode_pct', '')}%</td>
|
|
222
|
-
<td class="muted">{top_str[:80]}</td>
|
|
223
|
-
<td>{flag}</td>
|
|
224
|
-
</tr>"""
|
|
225
|
-
|
|
226
|
-
# Missing table
|
|
227
|
-
miss_rows_html = ""
|
|
228
|
-
for col, info in miss_per_col.items():
|
|
229
|
-
sev = info.get("severity", "ok")
|
|
230
|
-
color = {"ok": "#27ae60", "warn": "#f39c12", "critical": "#e74c3c"}.get(sev, "#888")
|
|
231
|
-
bar_w = min(100, int(info.get("null_pct", 0)))
|
|
232
|
-
miss_rows_html += f"""
|
|
233
|
-
<tr>
|
|
234
|
-
<td>{col}</td>
|
|
235
|
-
<td>{info.get('null_count', 0)}</td>
|
|
236
|
-
<td>
|
|
237
|
-
<div class="bar-bg">
|
|
238
|
-
<div class="bar-fill" style="width:{bar_w}%;background:{color}"></div>
|
|
239
|
-
</div>
|
|
240
|
-
{info.get('null_pct', 0)}%
|
|
241
|
-
</td>
|
|
242
|
-
<td><span style="color:{color};font-weight:500">{sev}</span></td>
|
|
243
|
-
</tr>"""
|
|
244
|
-
|
|
245
|
-
# Correlation rows
|
|
246
|
-
corr_html = ""
|
|
247
|
-
for p in strong_corr[:15]:
|
|
248
|
-
corr_html += f"<tr><td>{p['col_a']}</td><td>{p['col_b']}</td><td>{p['pearson_r']}</td></tr>"
|
|
249
|
-
|
|
250
|
-
# Warnings
|
|
251
|
-
warn_html = ""
|
|
252
|
-
for w in r.warnings:
|
|
253
|
-
warn_html += f'<div class="warn-banner">⚠ {w}</div>'
|
|
254
|
-
|
|
255
|
-
orig_rows, orig_cols = r.original_shape
|
|
256
|
-
|
|
257
|
-
return f"""<!DOCTYPE html>
|
|
258
|
-
<html lang="en">
|
|
259
|
-
<head>
|
|
260
|
-
<meta charset="UTF-8">
|
|
261
|
-
<title>FlashEDA Report</title>
|
|
262
|
-
<style>
|
|
263
|
-
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
264
|
-
body {{ font-family: system-ui, sans-serif; background: #f5f7fa; color: #222; font-size: 14px; }}
|
|
265
|
-
.container {{ max-width: 1100px; margin: 0 auto; padding: 24px 16px; }}
|
|
266
|
-
header {{ background: #1a1a2e; color: #fff; padding: 24px 32px; border-radius: 10px; margin-bottom: 24px; }}
|
|
267
|
-
header h1 {{ font-size: 22px; font-weight: 600; margin-bottom: 6px; }}
|
|
268
|
-
header .meta {{ font-size: 13px; color: #aac; }}
|
|
269
|
-
.card {{ background: #fff; border-radius: 10px; padding: 20px 24px; margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.07); }}
|
|
270
|
-
.card h2 {{ font-size: 15px; font-weight: 600; margin-bottom: 14px; border-bottom: 1px solid #eee; padding-bottom: 8px; }}
|
|
271
|
-
table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
|
|
272
|
-
th {{ text-align: left; padding: 6px 10px; background: #f0f2f5; font-weight: 500; }}
|
|
273
|
-
td {{ padding: 6px 10px; border-top: 1px solid #f0f0f0; vertical-align: middle; }}
|
|
274
|
-
.badge {{ display: inline-block; padding: 2px 7px; border-radius: 12px; font-size: 11px; font-weight: 600; }}
|
|
275
|
-
.badge.warn {{ background: #fff3cd; color: #856404; }}
|
|
276
|
-
.badge.danger {{ background: #f8d7da; color: #842029; }}
|
|
277
|
-
.muted {{ color: #888; font-size: 12px; }}
|
|
278
|
-
.bar-bg {{ display: inline-block; width: 80px; height: 8px; background: #eee; border-radius: 4px; vertical-align: middle; margin-right: 6px; }}
|
|
279
|
-
.bar-fill {{ height: 8px; border-radius: 4px; }}
|
|
280
|
-
.warn-banner {{ background: #fff3cd; color: #856404; padding: 10px 14px; border-radius: 8px; margin-bottom: 10px; font-size: 13px; }}
|
|
281
|
-
.stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 4px; }}
|
|
282
|
-
.stat-box {{ background: #f8f9fc; border-radius: 8px; padding: 14px 16px; }}
|
|
283
|
-
.stat-box .label {{ font-size: 11px; color: #888; margin-bottom: 4px; }}
|
|
284
|
-
.stat-box .value {{ font-size: 20px; font-weight: 600; color: #1a1a2e; }}
|
|
285
|
-
</style>
|
|
286
|
-
</head>
|
|
287
|
-
<body>
|
|
288
|
-
<div class="container">
|
|
289
|
-
<header>
|
|
290
|
-
<h1>⚡ FlashEDA Report</h1>
|
|
291
|
-
<div class="meta">
|
|
292
|
-
{orig_rows:,} rows × {orig_cols} columns |
|
|
293
|
-
Analysed on {r.sample_size:,} sampled rows |
|
|
294
|
-
Completed in {r.elapsed_seconds:.2f}s
|
|
295
|
-
</div>
|
|
296
|
-
</header>
|
|
297
|
-
|
|
298
|
-
{warn_html}
|
|
299
|
-
|
|
300
|
-
<div class="card">
|
|
301
|
-
<h2>Dataset overview</h2>
|
|
302
|
-
<div class="stats-grid">
|
|
303
|
-
<div class="stat-box"><div class="label">Total rows</div><div class="value">{orig_rows:,}</div></div>
|
|
304
|
-
<div class="stat-box"><div class="label">Columns</div><div class="value">{orig_cols}</div></div>
|
|
305
|
-
<div class="stat-box"><div class="label">Sample size</div><div class="value">{r.sample_size:,}</div></div>
|
|
306
|
-
<div class="stat-box"><div class="label">Analysis time</div><div class="value">{r.elapsed_seconds:.2f}s</div></div>
|
|
307
|
-
<div class="stat-box"><div class="label">Overall null %</div><div class="value">{r.missing.get('overall_null_pct', 0)}%</div></div>
|
|
308
|
-
<div class="stat-box"><div class="label">Duplicate rows (sample)</div><div class="value">{r.overview.get('duplicate_pct_in_sample', 0)}%</div></div>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
<div class="card">
|
|
313
|
-
<h2>Missing values</h2>
|
|
314
|
-
<table>
|
|
315
|
-
<thead><tr><th>Column</th><th>Null count</th><th>Null %</th><th>Status</th></tr></thead>
|
|
316
|
-
<tbody>{miss_rows_html}</tbody>
|
|
317
|
-
</table>
|
|
318
|
-
</div>
|
|
319
|
-
|
|
320
|
-
<div class="card">
|
|
321
|
-
<h2>Numeric columns</h2>
|
|
322
|
-
<table>
|
|
323
|
-
<thead><tr><th>Column</th><th>Mean</th><th>Std</th><th>Min</th><th>Median</th><th>Max</th><th>Outlier %</th><th>Skewness</th><th>Flags</th></tr></thead>
|
|
324
|
-
<tbody>{num_rows_html}</tbody>
|
|
325
|
-
</table>
|
|
326
|
-
</div>
|
|
327
|
-
|
|
328
|
-
<div class="card">
|
|
329
|
-
<h2>Categorical columns</h2>
|
|
330
|
-
<table>
|
|
331
|
-
<thead><tr><th>Column</th><th>Unique</th><th>Cardinality ratio</th><th>Mode</th><th>Mode %</th><th>Top values</th><th>Flags</th></tr></thead>
|
|
332
|
-
<tbody>{cat_rows_html}</tbody>
|
|
333
|
-
</table>
|
|
334
|
-
</div>
|
|
335
|
-
|
|
336
|
-
{"" if not corr_html else f'''
|
|
337
|
-
<div class="card">
|
|
338
|
-
<h2>Strong correlations (|r| ≥ 0.8)</h2>
|
|
339
|
-
<table>
|
|
340
|
-
<thead><tr><th>Column A</th><th>Column B</th><th>Pearson r</th></tr></thead>
|
|
341
|
-
<tbody>{corr_html}</tbody>
|
|
342
|
-
</table>
|
|
343
|
-
</div>'''}
|
|
344
|
-
|
|
345
|
-
<p class="muted" style="text-align:center;margin-top:16px">Generated by FlashEDA — constant-time EDA for any dataset size</p>
|
|
346
|
-
</div>
|
|
347
|
-
</body>
|
|
348
|
-
</html>"""
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: flasheda
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Constant-time EDA for any dataset size — one line, fixed time.
|
|
5
|
-
Author: Your Name
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
-
Requires-Python: >=3.8
|
|
9
|
-
Requires-Dist: pandas>=1.3.0
|
|
10
|
-
Requires-Dist: numpy>=1.21.0
|
|
11
|
-
Requires-Dist: scipy>=1.7.0
|
|
12
|
-
Requires-Dist: rich>=12.0.0
|
|
13
|
-
Requires-Dist: jinja2>=3.0.0
|
|
14
|
-
Requires-Dist: matplotlib>=3.4.0
|
|
15
|
-
Dynamic: author
|
|
16
|
-
Dynamic: classifier
|
|
17
|
-
Dynamic: requires-dist
|
|
18
|
-
Dynamic: requires-python
|
|
19
|
-
Dynamic: summary
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|