flasheda 0.1.2__tar.gz → 0.3.0__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.
Files changed (48) hide show
  1. flasheda-0.3.0/MIT.License +21 -0
  2. flasheda-0.3.0/PKG-INFO +153 -0
  3. flasheda-0.3.0/README.md +97 -0
  4. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/__init__.py +131 -109
  5. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/__init__.py +3 -3
  6. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/categorical.py +78 -75
  7. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/correlation.py +94 -90
  8. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/missing.py +53 -53
  9. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/numeric.py +70 -70
  10. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/analyzers/overview.py +65 -65
  11. flasheda-0.3.0/flasheda/analyzers/target.py +230 -0
  12. flasheda-0.3.0/flasheda/clean/__init__.py +45 -0
  13. flasheda-0.3.0/flasheda/clean/detectors.py +125 -0
  14. flasheda-0.3.0/flasheda/codegen/__init__.py +86 -0
  15. flasheda-0.3.0/flasheda/codegen/planner.py +170 -0
  16. flasheda-0.3.0/flasheda/codegen/steps.py +200 -0
  17. flasheda-0.3.0/flasheda/codegen/validate.py +107 -0
  18. flasheda-0.3.0/flasheda/codegen/writers/__init__.py +22 -0
  19. flasheda-0.3.0/flasheda/codegen/writers/base.py +86 -0
  20. flasheda-0.3.0/flasheda/codegen/writers/duckdb_writer.py +124 -0
  21. flasheda-0.3.0/flasheda/codegen/writers/pandas_writer.py +69 -0
  22. flasheda-0.3.0/flasheda/codegen/writers/polars_writer.py +99 -0
  23. flasheda-0.3.0/flasheda/codegen/writers/pyspark_writer.py +76 -0
  24. flasheda-0.3.0/flasheda/engine/__init__.py +23 -0
  25. flasheda-0.3.0/flasheda/engine/backends.py +67 -0
  26. flasheda-0.3.0/flasheda/engine/sampler.py +125 -0
  27. flasheda-0.3.0/flasheda/engine/source.py +110 -0
  28. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda/report.py +669 -521
  29. flasheda-0.3.0/flasheda/sampler.py +9 -0
  30. flasheda-0.3.0/flasheda.egg-info/PKG-INFO +153 -0
  31. flasheda-0.3.0/flasheda.egg-info/SOURCES.txt +39 -0
  32. flasheda-0.3.0/flasheda.egg-info/requires.txt +23 -0
  33. {flasheda-0.1.2 → flasheda-0.3.0}/pyproject.toml +2 -2
  34. {flasheda-0.1.2 → flasheda-0.3.0}/setup.cfg +4 -4
  35. flasheda-0.3.0/setup.py +49 -0
  36. flasheda-0.3.0/tests/test_clean_target.py +167 -0
  37. flasheda-0.3.0/tests/test_codegen.py +66 -0
  38. flasheda-0.3.0/tests/test_engine.py +77 -0
  39. flasheda-0.3.0/tests/test_writers.py +120 -0
  40. flasheda-0.1.2/PKG-INFO +0 -49
  41. flasheda-0.1.2/README.md +0 -21
  42. flasheda-0.1.2/flasheda/sampler.py +0 -124
  43. flasheda-0.1.2/flasheda.egg-info/PKG-INFO +0 -49
  44. flasheda-0.1.2/flasheda.egg-info/SOURCES.txt +0 -17
  45. flasheda-0.1.2/flasheda.egg-info/requires.txt +0 -7
  46. flasheda-0.1.2/setup.py +0 -27
  47. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda.egg-info/dependency_links.txt +0 -0
  48. {flasheda-0.1.2 → flasheda-0.3.0}/flasheda.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: flasheda
3
+ Version: 0.3.0
4
+ Summary: Fixed-time EDA for any dataset size, plus validated cleaning code for pandas, polars, DuckDB and PySpark.
5
+ Home-page: https://github.com/sjapanjots/Flash-EDA
6
+ Author: Japanjot Singh
7
+ Author-email: sjapanjots@gmail.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/sjapanjots/Flash-EDA
10
+ Project-URL: Changelog, https://github.com/sjapanjots/Flash-EDA/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/sjapanjots/Flash-EDA/issues
12
+ Keywords: eda,data-cleaning,pandas,polars,duckdb,pyspark,code-generation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: MIT.License
22
+ Requires-Dist: pandas>=2.0
23
+ Requires-Dist: numpy>=1.22
24
+ Requires-Dist: scipy>=1.7.0
25
+ Requires-Dist: rich>=12.0.0
26
+ Requires-Dist: jinja2>=3.0.0
27
+ Requires-Dist: matplotlib>=3.4.0
28
+ Requires-Dist: fpdf2>=2.7.0
29
+ Provides-Extra: fast
30
+ Requires-Dist: duckdb>=0.10; extra == "fast"
31
+ Requires-Dist: polars>=0.20; extra == "fast"
32
+ Requires-Dist: pyarrow>=12; extra == "fast"
33
+ Provides-Extra: spark
34
+ Requires-Dist: pyspark>=3.4; extra == "spark"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7; extra == "dev"
37
+ Requires-Dist: duckdb>=0.10; extra == "dev"
38
+ Requires-Dist: polars>=0.20; extra == "dev"
39
+ Requires-Dist: pyarrow>=12; extra == "dev"
40
+ Requires-Dist: build; extra == "dev"
41
+ Requires-Dist: twine; extra == "dev"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: description-content-type
47
+ Dynamic: home-page
48
+ Dynamic: keywords
49
+ Dynamic: license
50
+ Dynamic: license-file
51
+ Dynamic: project-url
52
+ Dynamic: provides-extra
53
+ Dynamic: requires-dist
54
+ Dynamic: requires-python
55
+ Dynamic: summary
56
+
57
+ <img width="1254" height="1254" alt="image" src="https://github.com/user-attachments/assets/a87d02e0-7a18-41e0-844d-a2b9c3d6f769" />
58
+
59
+ # FlashEDA
60
+
61
+ [![PyPI](https://img.shields.io/pypi/v/flasheda)](https://pypi.org/project/flasheda/)
62
+ [![Python](https://img.shields.io/pypi/pyversions/flasheda)](https://pypi.org/project/flasheda/)
63
+ [![Tests](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml/badge.svg)](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml)
64
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](MIT.License)
65
+
66
+ **Current version: 0.3.0** · [Changelog](CHANGELOG.md)
67
+
68
+ Analyse any dataset in a few seconds, then get **validated cleaning code** that runs on the full data, in the engine that fits its size: pandas, polars, DuckDB or PySpark.
69
+
70
+ FlashEDA analyses a fixed-size random sample, so analysis time barely changes between 100 thousand and 100 million rows.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install flasheda # core
76
+ pip install "flasheda[fast]" # + polars, DuckDB, pyarrow (recommended for big files)
77
+ pip install "flasheda[spark]" # + PySpark
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ ```python
83
+ import flasheda as fe
84
+
85
+ # 1. Analyse: any size, a few seconds
86
+ report = fe.analyze("orders.csv", target="churn", save_pdf=False)
87
+ report.show() # console report
88
+ report.save_html("report.html") # or report.save_pdf("report.pdf")
89
+
90
+ # 2. Clean: get a script for the full data, already checked on the sample
91
+ code = fe.clean("orders.csv", target="churn", engine="polars")
92
+ code.save("clean_orders.py") # then: python clean_orders.py
93
+ ```
94
+
95
+ ## What it does
96
+
97
+ | | |
98
+ |---|---|
99
+ | **Overview** | true row count (read from file metadata, no full load), types, memory, duplicates |
100
+ | **Data quality** | missing values, outliers, skew, likely ID columns, numbers stored as text, spelling variants, rare categories |
101
+ | **Target analysis** | task type, class balance, per-feature predictive power, **possible leakage**, recommended metric and validation split |
102
+ | **Correlations** | Pearson (numeric) and Cramér's V (categorical) |
103
+ | **Reports** | console, HTML, PDF, JSON |
104
+ | **Code generation** | one cleaning plan → pandas, polars (lazy, streaming), DuckDB SQL (larger than memory) or PySpark |
105
+ | **Validation** | generated code is run on the sample and compared with a reference result before you get it |
106
+
107
+ ### Cleaning steps
108
+ Each step in the generated code has a comment saying why it was added:
109
+
110
+ - drop likely-ID, mostly-empty, constant and leaky columns
111
+ - drop rows where the target is missing, and drop duplicate rows
112
+ - convert numbers stored as text (`"12.5"`, `"N/A"`) to numbers
113
+ - merge spelling variants (`"Delhi"`, `" delhi"`, `"DELHI"`)
114
+ - group rare categories into `other`
115
+ - add `<col>_was_missing` flags when missingness predicts the target
116
+ - fill nulls (median / mode), log-transform skewed columns, clip outliers
117
+
118
+ ### Engine choice
119
+
120
+ | Rows | Engine |
121
+ |---|---|
122
+ | < 1M | pandas |
123
+ | 1M – 200M | polars or DuckDB |
124
+ | > 200M | PySpark (if installed) |
125
+
126
+ Override with `fe.analyze(..., engine="duckdb")` or `report.codegen("duckdb")`.
127
+
128
+ ## Performance
129
+
130
+ Measured on synthetic messy data:
131
+
132
+ | Task | Time |
133
+ |---|---|
134
+ | Analyse a 5.1M-row Parquet file | 0.56 s |
135
+ | Sample a 1M-row CSV (0.1.x → 0.2.0) | 37.3 s → 0.65 s |
136
+ | Analyse + plan + generate + validate, 1M-row CSV with target | 2.5 s |
137
+ | Run the generated polars script on 1M rows | 0.9 s |
138
+
139
+ ## Examples
140
+
141
+ - [`examples/phase1_demo.py`](examples/phase1_demo.py): engine choice and code generation at 0.1M / 1M / 5M rows
142
+ - [`examples/phase2_demo.py`](examples/phase2_demo.py): target analysis and cleaning on a 1M-row churn CSV
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ pip install -e ".[dev]"
148
+ pytest
149
+ ```
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,97 @@
1
+ <img width="1254" height="1254" alt="image" src="https://github.com/user-attachments/assets/a87d02e0-7a18-41e0-844d-a2b9c3d6f769" />
2
+
3
+ # FlashEDA
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/flasheda)](https://pypi.org/project/flasheda/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/flasheda)](https://pypi.org/project/flasheda/)
7
+ [![Tests](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml/badge.svg)](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](MIT.License)
9
+
10
+ **Current version: 0.3.0** · [Changelog](CHANGELOG.md)
11
+
12
+ Analyse any dataset in a few seconds, then get **validated cleaning code** that runs on the full data, in the engine that fits its size: pandas, polars, DuckDB or PySpark.
13
+
14
+ FlashEDA analyses a fixed-size random sample, so analysis time barely changes between 100 thousand and 100 million rows.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install flasheda # core
20
+ pip install "flasheda[fast]" # + polars, DuckDB, pyarrow (recommended for big files)
21
+ pip install "flasheda[spark]" # + PySpark
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ import flasheda as fe
28
+
29
+ # 1. Analyse: any size, a few seconds
30
+ report = fe.analyze("orders.csv", target="churn", save_pdf=False)
31
+ report.show() # console report
32
+ report.save_html("report.html") # or report.save_pdf("report.pdf")
33
+
34
+ # 2. Clean: get a script for the full data, already checked on the sample
35
+ code = fe.clean("orders.csv", target="churn", engine="polars")
36
+ code.save("clean_orders.py") # then: python clean_orders.py
37
+ ```
38
+
39
+ ## What it does
40
+
41
+ | | |
42
+ |---|---|
43
+ | **Overview** | true row count (read from file metadata, no full load), types, memory, duplicates |
44
+ | **Data quality** | missing values, outliers, skew, likely ID columns, numbers stored as text, spelling variants, rare categories |
45
+ | **Target analysis** | task type, class balance, per-feature predictive power, **possible leakage**, recommended metric and validation split |
46
+ | **Correlations** | Pearson (numeric) and Cramér's V (categorical) |
47
+ | **Reports** | console, HTML, PDF, JSON |
48
+ | **Code generation** | one cleaning plan → pandas, polars (lazy, streaming), DuckDB SQL (larger than memory) or PySpark |
49
+ | **Validation** | generated code is run on the sample and compared with a reference result before you get it |
50
+
51
+ ### Cleaning steps
52
+ Each step in the generated code has a comment saying why it was added:
53
+
54
+ - drop likely-ID, mostly-empty, constant and leaky columns
55
+ - drop rows where the target is missing, and drop duplicate rows
56
+ - convert numbers stored as text (`"12.5"`, `"N/A"`) to numbers
57
+ - merge spelling variants (`"Delhi"`, `" delhi"`, `"DELHI"`)
58
+ - group rare categories into `other`
59
+ - add `<col>_was_missing` flags when missingness predicts the target
60
+ - fill nulls (median / mode), log-transform skewed columns, clip outliers
61
+
62
+ ### Engine choice
63
+
64
+ | Rows | Engine |
65
+ |---|---|
66
+ | < 1M | pandas |
67
+ | 1M – 200M | polars or DuckDB |
68
+ | > 200M | PySpark (if installed) |
69
+
70
+ Override with `fe.analyze(..., engine="duckdb")` or `report.codegen("duckdb")`.
71
+
72
+ ## Performance
73
+
74
+ Measured on synthetic messy data:
75
+
76
+ | Task | Time |
77
+ |---|---|
78
+ | Analyse a 5.1M-row Parquet file | 0.56 s |
79
+ | Sample a 1M-row CSV (0.1.x → 0.2.0) | 37.3 s → 0.65 s |
80
+ | Analyse + plan + generate + validate, 1M-row CSV with target | 2.5 s |
81
+ | Run the generated polars script on 1M rows | 0.9 s |
82
+
83
+ ## Examples
84
+
85
+ - [`examples/phase1_demo.py`](examples/phase1_demo.py): engine choice and code generation at 0.1M / 1M / 5M rows
86
+ - [`examples/phase2_demo.py`](examples/phase2_demo.py): target analysis and cleaning on a 1M-row churn CSV
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ pip install -e ".[dev]"
92
+ pytest
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -1,110 +1,132 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from concurrent.futures import ThreadPoolExecutor, as_completed
5
- from pathlib import Path
6
- from typing import Union, Optional
7
-
8
- import pandas as pd
9
-
10
- from .sampler import sample, get_sample_size, set_sample_size
11
- from .analyzers import overview, missing, numeric, categorical, correlation
12
- from .report import EDAReport
13
-
14
- __version__ = "0.1.2"
15
- __all__ = ["analyze", "set_sample_size", "get_sample_size", "EDAReport"]
16
-
17
-
18
- def analyze(
19
- source: Union[pd.DataFrame, str, Path],
20
- n: Optional[int] = None,
21
- *,
22
- show: bool = False,
23
- random_state: int = 42,
24
- save_pdf: bool = True, # ← NEW: auto-save PDF
25
- pdf_path: str = "flasheda_report.pdf", # ← NEW: PDF file name
26
- ) -> EDAReport:
27
- """
28
- Perform constant-time EDA on any dataset.
29
-
30
- Parameters
31
- ----------
32
- source : pandas DataFrame or path to CSV / Parquet file
33
- n : sample size (default 5000)
34
- show : print rich console summary
35
- random_state : RNG seed
36
- save_pdf : automatically save PDF report (default True)
37
- pdf_path : path/name of the PDF file to save
38
-
39
- Example
40
- -------
41
- >>> report = flasheda.analyze(df)
42
- # flasheda_report.pdf is saved automatically
43
- """
44
- t0 = time.perf_counter()
45
- n = n or get_sample_size()
46
-
47
- if isinstance(source, pd.DataFrame):
48
- original_shape = source.shape
49
- else:
50
- original_shape = (-1, -1)
51
-
52
- sampled = sample(source, n=n, random_state=random_state)
53
-
54
- if original_shape == (-1, -1):
55
- original_shape = sampled.shape
56
-
57
- tasks = {
58
- "overview": lambda: overview.analyze(sampled, original_shape),
59
- "missing": lambda: missing.analyze(sampled),
60
- "numeric": lambda: numeric.analyze(sampled),
61
- "categorical": lambda: categorical.analyze(sampled),
62
- "correlation": lambda: correlation.analyze(sampled),
63
- }
64
-
65
- results: dict = {}
66
- with ThreadPoolExecutor(max_workers=5) as pool:
67
- future_map = {pool.submit(fn): name for name, fn in tasks.items()}
68
- for future in as_completed(future_map):
69
- name = future_map[future]
70
- try:
71
- results[name] = future.result()
72
- except Exception as exc:
73
- results[name] = {"error": str(exc)}
74
-
75
- warnings: list[str] = []
76
- miss = results.get("missing", {})
77
- if miss.get("critical_columns"):
78
- warnings.append(f"Columns with >50% missing: {', '.join(miss['critical_columns'])}")
79
- num = results.get("numeric", {})
80
- if num.get("skewed_columns"):
81
- warnings.append(f"Highly skewed columns: {', '.join(num['skewed_columns'])}")
82
- cat = results.get("categorical", {})
83
- if cat.get("likely_id_columns"):
84
- warnings.append(f"Likely ID columns: {', '.join(cat['likely_id_columns'])}")
85
- corr = results.get("correlation", {})
86
- if corr.get("numeric_strong_pairs"):
87
- warnings.append(f"{len(corr['numeric_strong_pairs'])} strongly correlated pair(s) found")
88
-
89
- elapsed = round(time.perf_counter() - t0, 3)
90
-
91
- report = EDAReport(
92
- overview=results.get("overview", {}),
93
- missing=results.get("missing", {}),
94
- numeric=results.get("numeric", {}),
95
- categorical=results.get("categorical", {}),
96
- correlation=results.get("correlation", {}),
97
- sample_size=len(sampled),
98
- elapsed_seconds=elapsed,
99
- original_shape=original_shape,
100
- warnings=warnings,
101
- )
102
-
103
- if show:
104
- report.show()
105
-
106
- # ── Auto-save PDF immediately after analysis ───────────────────────────
107
- if save_pdf:
108
- report.save_pdf(pdf_path)
109
-
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from concurrent.futures import ThreadPoolExecutor, as_completed
5
+ from pathlib import Path
6
+ from typing import Union, Optional
7
+
8
+ import pandas as pd
9
+
10
+ from . import codegen, engine
11
+ from .clean import clean
12
+ from .engine import available_engines, choose_engine, inspect_source, sample, get_sample_size, set_sample_size
13
+ from .analyzers import overview, missing, numeric, categorical, correlation
14
+ from .analyzers import target as target_analyzer
15
+ from .report import EDAReport
16
+
17
+ __version__ = "0.3.0"
18
+ __all__ = [
19
+ "analyze",
20
+ "available_engines",
21
+ "clean",
22
+ "codegen",
23
+ "engine",
24
+ "get_sample_size",
25
+ "inspect_source",
26
+ "set_sample_size",
27
+ "EDAReport",
28
+ ]
29
+
30
+
31
+ def analyze(
32
+ source: Union[pd.DataFrame, str, Path],
33
+ n: Optional[int] = None,
34
+ *,
35
+ show: bool = False,
36
+ random_state: int = 42,
37
+ save_pdf: bool = True,
38
+ pdf_path: str = "flasheda_report.pdf",
39
+ engine: Optional[str] = None,
40
+ target: Optional[str] = None,
41
+ ) -> EDAReport:
42
+ """
43
+ Analyse a fixed-size sample of `source` (DataFrame, CSV/TSV or Parquet path).
44
+
45
+ `target` names the column you want to predict; it adds task detection,
46
+ class balance, per-feature predictive power and leakage checks.
47
+
48
+ `engine` forces the engine recommended for full-size work; by default it
49
+ is chosen from the dataset's row count.
50
+ """
51
+ t0 = time.perf_counter()
52
+ n = n or get_sample_size()
53
+
54
+ info = inspect_source(source)
55
+ if target is not None and target not in info.columns:
56
+ raise KeyError(f"Target column {target!r} not found. Columns: {info.columns}")
57
+ sampled = sample(source, n=n, random_state=random_state)
58
+ original_shape = info.shape
59
+
60
+ tasks = {
61
+ "overview": lambda: overview.analyze(sampled, original_shape),
62
+ "missing": lambda: missing.analyze(sampled),
63
+ "numeric": lambda: numeric.analyze(sampled),
64
+ "categorical": lambda: categorical.analyze(sampled),
65
+ "correlation": lambda: correlation.analyze(sampled),
66
+ }
67
+ if target is not None:
68
+ tasks["target"] = lambda: target_analyzer.analyze(sampled, target, random_state)
69
+
70
+ results: dict = {}
71
+ with ThreadPoolExecutor(max_workers=5) as pool:
72
+ future_map = {pool.submit(fn): name for name, fn in tasks.items()}
73
+ for future in as_completed(future_map):
74
+ name = future_map[future]
75
+ try:
76
+ results[name] = future.result()
77
+ except Exception as exc:
78
+ results[name] = {"error": str(exc)}
79
+
80
+ warnings: list[str] = []
81
+ miss = results.get("missing", {})
82
+ if miss.get("critical_columns"):
83
+ warnings.append(f"Columns with >50% missing: {', '.join(miss['critical_columns'])}")
84
+ num = results.get("numeric", {})
85
+ if num.get("skewed_columns"):
86
+ warnings.append(f"Highly skewed columns: {', '.join(num['skewed_columns'])}")
87
+ cat = results.get("categorical", {})
88
+ if cat.get("likely_id_columns"):
89
+ warnings.append(f"Likely ID columns: {', '.join(cat['likely_id_columns'])}")
90
+ corr = results.get("correlation", {})
91
+ if corr.get("numeric_strong_pairs"):
92
+ warnings.append(f"{len(corr['numeric_strong_pairs'])} strongly correlated pair(s) found")
93
+
94
+ tgt = results.get("target", {})
95
+ if "error" in tgt:
96
+ raise RuntimeError(f"Target analysis failed: {tgt['error']}")
97
+ if tgt.get("leakage_columns"):
98
+ cols = ", ".join(str(l["column"]) for l in tgt["leakage_columns"])
99
+ warnings.append(f"Possible target leakage: {cols}")
100
+ if tgt.get("imbalanced"):
101
+ warnings.append(
102
+ f"Imbalanced target: minority class is {tgt['minority_pct']}% "
103
+ f"(ratio {tgt['imbalance_ratio']}:1)"
104
+ )
105
+ if tgt.get("null_count"):
106
+ warnings.append(f"Target {target!r} is null in {tgt['null_pct']}% of sampled rows")
107
+
108
+ elapsed = round(time.perf_counter() - t0, 3)
109
+
110
+ report = EDAReport(
111
+ overview=results.get("overview", {}),
112
+ missing=results.get("missing", {}),
113
+ numeric=results.get("numeric", {}),
114
+ categorical=results.get("categorical", {}),
115
+ correlation=results.get("correlation", {}),
116
+ sample_size=len(sampled),
117
+ elapsed_seconds=elapsed,
118
+ original_shape=original_shape,
119
+ warnings=warnings,
120
+ target=tgt,
121
+ source=str(info.path) if info.path else None,
122
+ recommended_engine=choose_engine(info.n_rows, prefer=engine),
123
+ sample=sampled,
124
+ )
125
+
126
+ if show:
127
+ report.show()
128
+
129
+ if save_pdf:
130
+ report.save_pdf(pdf_path)
131
+
110
132
  return report
@@ -1,3 +1,3 @@
1
- from . import overview, missing, numeric, categorical, correlation
2
-
3
- __all__ = ["overview", "missing", "numeric", "categorical", "correlation"]
1
+ from . import overview, missing, numeric, categorical, correlation, target
2
+
3
+ __all__ = ["overview", "missing", "numeric", "categorical", "correlation", "target"]