flasheda 0.4.0__tar.gz → 0.6.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.
- {flasheda-0.4.0 → flasheda-0.6.0}/PKG-INFO +57 -3
- {flasheda-0.4.0 → flasheda-0.6.0}/README.md +48 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/__init__.py +31 -6
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/__init__.py +2 -2
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/categorical.py +4 -3
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/correlation.py +38 -2
- flasheda-0.6.0/flasheda/analyzers/text.py +134 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/clean/detectors.py +13 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/__init__.py +5 -2
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/steps.py +20 -2
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/base.py +12 -2
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/duckdb_writer.py +5 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/pandas_writer.py +23 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/polars_writer.py +5 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/pyspark_writer.py +5 -1
- flasheda-0.6.0/flasheda/compare/__init__.py +224 -0
- flasheda-0.6.0/flasheda/contract/__init__.py +29 -0
- flasheda-0.6.0/flasheda/contract/check.py +251 -0
- flasheda-0.6.0/flasheda/contract/rules.py +227 -0
- flasheda-0.6.0/flasheda/contract/script.py +227 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/engine/__init__.py +3 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/engine/sampler.py +4 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/engine/source.py +13 -0
- flasheda-0.6.0/flasheda/engine/sql.py +129 -0
- flasheda-0.6.0/flasheda/export/__init__.py +9 -0
- flasheda-0.6.0/flasheda/export/notebook.py +154 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/features/__init__.py +20 -1
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/report.py +53 -9
- flasheda-0.6.0/flasheda/timeseries/__init__.py +313 -0
- flasheda-0.6.0/flasheda/timeseries/forecast.py +138 -0
- flasheda-0.6.0/flasheda/timeseries/script.py +117 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda.egg-info/PKG-INFO +57 -3
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda.egg-info/SOURCES.txt +24 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda.egg-info/requires.txt +8 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/setup.py +5 -4
- flasheda-0.6.0/tests/test_compare.py +90 -0
- flasheda-0.6.0/tests/test_contract.py +157 -0
- flasheda-0.6.0/tests/test_contract_script.py +81 -0
- flasheda-0.6.0/tests/test_forecast.py +93 -0
- flasheda-0.6.0/tests/test_html_escape.py +19 -0
- flasheda-0.6.0/tests/test_no_side_effects.py +19 -0
- flasheda-0.6.0/tests/test_notebook.py +70 -0
- flasheda-0.6.0/tests/test_point_biserial.py +41 -0
- flasheda-0.6.0/tests/test_sql.py +81 -0
- flasheda-0.6.0/tests/test_text.py +77 -0
- flasheda-0.6.0/tests/test_text_features.py +76 -0
- flasheda-0.6.0/tests/test_timeseries.py +109 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/MIT.License +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/missing.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/numeric.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/overview.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/analyzers/target.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/clean/__init__.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/planner.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/validate.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/codegen/writers/__init__.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/engine/backends.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/modeling/__init__.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/modeling/script.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/modeling/specs.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda/sampler.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda.egg-info/dependency_links.txt +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/flasheda.egg-info/top_level.txt +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/pyproject.toml +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/setup.cfg +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/tests/test_clean_target.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/tests/test_codegen.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/tests/test_engine.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/tests/test_features_model.py +0 -0
- {flasheda-0.4.0 → flasheda-0.6.0}/tests/test_writers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flasheda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Fixed-time EDA for any dataset size, plus validated cleaning, feature and training code for pandas, polars, DuckDB and PySpark.
|
|
5
5
|
Home-page: https://github.com/sjapanjots/Flash-EDA
|
|
6
6
|
Author: Japanjot Singh
|
|
@@ -9,7 +9,7 @@ License: MIT
|
|
|
9
9
|
Project-URL: Source, https://github.com/sjapanjots/Flash-EDA
|
|
10
10
|
Project-URL: Changelog, https://github.com/sjapanjots/Flash-EDA/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/sjapanjots/Flash-EDA/issues
|
|
12
|
-
Keywords: eda,data-cleaning,pandas,polars,duckdb,pyspark,code-generation,feature-engineering,automl
|
|
12
|
+
Keywords: eda,data-cleaning,pandas,polars,duckdb,pyspark,code-generation,feature-engineering,automl,data-quality,data-drift,time-series,forecasting,pii
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -35,17 +35,24 @@ Requires-Dist: pyspark>=3.4; extra == "spark"
|
|
|
35
35
|
Provides-Extra: ml
|
|
36
36
|
Requires-Dist: scikit-learn>=1.2; extra == "ml"
|
|
37
37
|
Requires-Dist: pyarrow>=12; extra == "ml"
|
|
38
|
+
Provides-Extra: sql
|
|
39
|
+
Requires-Dist: sqlalchemy>=1.4; extra == "sql"
|
|
40
|
+
Requires-Dist: pyarrow>=12; extra == "sql"
|
|
38
41
|
Provides-Extra: all
|
|
39
42
|
Requires-Dist: duckdb>=0.10; extra == "all"
|
|
40
43
|
Requires-Dist: polars>=0.20; extra == "all"
|
|
41
44
|
Requires-Dist: pyarrow>=12; extra == "all"
|
|
42
45
|
Requires-Dist: scikit-learn>=1.2; extra == "all"
|
|
46
|
+
Requires-Dist: sqlalchemy>=1.4; extra == "all"
|
|
43
47
|
Provides-Extra: dev
|
|
44
48
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
45
49
|
Requires-Dist: duckdb>=0.10; extra == "dev"
|
|
46
50
|
Requires-Dist: polars>=0.20; extra == "dev"
|
|
47
51
|
Requires-Dist: pyarrow>=12; extra == "dev"
|
|
48
52
|
Requires-Dist: scikit-learn>=1.2; extra == "dev"
|
|
53
|
+
Requires-Dist: pandera; extra == "dev"
|
|
54
|
+
Requires-Dist: sqlalchemy>=1.4; extra == "dev"
|
|
55
|
+
Requires-Dist: nbformat; extra == "dev"
|
|
49
56
|
Requires-Dist: build; extra == "dev"
|
|
50
57
|
Requires-Dist: twine; extra == "dev"
|
|
51
58
|
Dynamic: author
|
|
@@ -72,7 +79,7 @@ Dynamic: summary
|
|
|
72
79
|
[](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml)
|
|
73
80
|
[](MIT.License)
|
|
74
81
|
|
|
75
|
-
**Current version: 0.
|
|
82
|
+
**Current version: 0.6.0** · [Changelog](CHANGELOG.md)
|
|
76
83
|
|
|
77
84
|
Analyse any dataset in a few seconds, then get **validated cleaning, feature and training code** that runs on the full data, in the engine that fits its size: pandas, polars, DuckDB or PySpark.
|
|
78
85
|
|
|
@@ -84,6 +91,7 @@ FlashEDA analyses a fixed-size random sample, so analysis time barely changes be
|
|
|
84
91
|
pip install flasheda # core
|
|
85
92
|
pip install "flasheda[fast]" # + polars, DuckDB, pyarrow (recommended for big files)
|
|
86
93
|
pip install "flasheda[ml]" # + scikit-learn, for flasheda.train
|
|
94
|
+
pip install "flasheda[sql]" # + SQLAlchemy, for database tables
|
|
87
95
|
pip install "flasheda[spark]" # + PySpark
|
|
88
96
|
pip install "flasheda[all]" # everything except PySpark
|
|
89
97
|
```
|
|
@@ -106,6 +114,27 @@ code.save("clean_orders.py") # then: python clean_orders.py
|
|
|
106
114
|
result = fe.train("orders.csv", target="churn", engine="polars")
|
|
107
115
|
result.show() # leaderboard
|
|
108
116
|
result.save("prep.py", "train.py") # python prep.py && python train.py → model.joblib
|
|
117
|
+
|
|
118
|
+
# 4. Guard: learn rules from good data, check every new file
|
|
119
|
+
contract = fe.analyze("orders_jan.csv").contract()
|
|
120
|
+
fe.check("orders_feb.csv", contract).show()
|
|
121
|
+
open("check.py", "w").write(contract.script()) # python check.py new.csv → exit 1 on failure
|
|
122
|
+
|
|
123
|
+
# 5. Compare: what changed between two datasets
|
|
124
|
+
fe.compare("orders_jan.csv", "orders_feb.csv").show()
|
|
125
|
+
|
|
126
|
+
# Databases work like files
|
|
127
|
+
src = fe.sql("postgresql://user:pass@host/shop", table="orders")
|
|
128
|
+
fe.analyze(src, target="churn")
|
|
129
|
+
|
|
130
|
+
# 6. Time series: trend, seasonality, forecast
|
|
131
|
+
ts = fe.timeseries("orders.parquet", time="created_at", value="amount")
|
|
132
|
+
ts.show()
|
|
133
|
+
fc = ts.forecast() # backtested baselines
|
|
134
|
+
open("forecast.py", "w").write(fc.script()) # python forecast.py → forecast.csv
|
|
135
|
+
|
|
136
|
+
# 7. Everything as one runnable notebook
|
|
137
|
+
report.to_notebook("orders.ipynb", train=True, time="created_at")
|
|
109
138
|
```
|
|
110
139
|
|
|
111
140
|
## What it does
|
|
@@ -120,6 +149,12 @@ result.save("prep.py", "train.py") # python prep.py && python train.py → model
|
|
|
120
149
|
| **Code generation** | one cleaning plan → pandas, polars (lazy, streaming), DuckDB SQL (larger than memory) or PySpark |
|
|
121
150
|
| **Features** | date parts, one-hot / frequency encoding, ratios that beat their inputs |
|
|
122
151
|
| **Models** | baseline, linear, random forest, gradient boosting compared with the right metric and split; training script for the winner |
|
|
152
|
+
| **Data contracts** | rules learned from good data; full-data checks; standalone check script for CI; pandera export |
|
|
153
|
+
| **Drift** | schema changes and per-column drift (PSI, KS, new categories, null-rate change) between two datasets |
|
|
154
|
+
| **Databases** | any SQLAlchemy URL; sampling runs in the database; passwords never stored |
|
|
155
|
+
| **Text** | personal data (emails, phones, card numbers…) flagged; free-text stats; length/word features |
|
|
156
|
+
| **Time series** | full-data aggregation per hour/day/week; gaps, trend, seasonality, anomalies; backtested forecast + script |
|
|
157
|
+
| **Notebook** | analysis, cleaning, training, forecast and contract check as one runnable `.ipynb` |
|
|
123
158
|
| **Validation** | generated code is run on the sample and compared with a reference result before you get it |
|
|
124
159
|
|
|
125
160
|
### Cleaning steps
|
|
@@ -141,6 +176,17 @@ Each step in the generated code has a comment saying why it was added:
|
|
|
141
176
|
|
|
142
177
|
The metric and split follow the target: PR-AUC for imbalanced binary targets, MAE for skewed regression targets, time-based splits when there's a date column, and so on.
|
|
143
178
|
|
|
179
|
+
### Data contracts and drift
|
|
180
|
+
A **contract** describes what good data looks like, learned from a sample with tolerances so normal variation passes:
|
|
181
|
+
type, missing %, 1st/99th percentile range (catches unit changes like ×100), no negatives, allowed values, uniqueness of IDs.
|
|
182
|
+
`fe.check()` checks every row of a file in one DuckDB pass; `contract.script()` writes the same check as a standalone script that exits with code 1 on failure, ready for a pipeline or CI job.
|
|
183
|
+
|
|
184
|
+
`fe.compare(old, new)` reports added/removed columns, type changes and how much each column shifted (PSI: < 0.1 stable, 0.1–0.25 moderate, ≥ 0.25 major). Dates and IDs are shown as range shifts, since they always move.
|
|
185
|
+
|
|
186
|
+
### Time series and forecasting
|
|
187
|
+
A random sample breaks time order, so `fe.timeseries()` first aggregates **every row** per period (one DuckDB `GROUP BY`, about 0.7 s for 5M rows), then reports gaps, trend (% per year), daily/weekly/yearly seasonality, the busiest weekday or hour, and anomalies.
|
|
188
|
+
`ts.forecast()` backtests five simple baselines (naive, seasonal naive, moving average, drift, trend + season) and keeps the most accurate. These are the numbers any fancier model has to beat. `fc.script()` writes it as a standalone script.
|
|
189
|
+
|
|
144
190
|
### Engine choice
|
|
145
191
|
|
|
146
192
|
| Rows | Engine |
|
|
@@ -163,12 +209,20 @@ Measured on synthetic messy data:
|
|
|
163
209
|
| Run the generated polars script on 1M rows | 0.9 s |
|
|
164
210
|
| `fe.train()` on a 1M-row CSV (plan, features, 4 models, checks) | 6.7 s |
|
|
165
211
|
| Generated `prep.py` + `train.py` on 1M rows | 1.3 s + 20 s |
|
|
212
|
+
| Learn a contract from 1M rows / run the check script on 1M rows | 0.8 s / 0.6–0.9 s |
|
|
213
|
+
| Compare two 1M-row files | 0.6 s |
|
|
214
|
+
| Analyse a 1M-row SQLite table (5,000 rows transferred) | 0.3 s |
|
|
215
|
+
| Time-series profile of a 5.3M-row Parquet file | 0.7 s |
|
|
216
|
+
| Generated forecast script on 5.3M rows | 0.6 s |
|
|
217
|
+
| Notebook with cleaning, features, training, forecast and contract | 5 s |
|
|
166
218
|
|
|
167
219
|
## Examples
|
|
168
220
|
|
|
169
221
|
- [`examples/phase1_demo.py`](examples/phase1_demo.py): engine choice and code generation at 0.1M / 1M / 5M rows
|
|
170
222
|
- [`examples/phase2_demo.py`](examples/phase2_demo.py): target analysis and cleaning on a 1M-row churn CSV
|
|
171
223
|
- [`examples/phase3_demo.py`](examples/phase3_demo.py): features, model comparison and training on 1M rows
|
|
224
|
+
- [`examples/phase4_demo.py`](examples/phase4_demo.py): contracts, check script, drift and a database on 1M rows
|
|
225
|
+
- [`examples/phase5_demo.py`](examples/phase5_demo.py): text, time series, forecast and notebook on 5M rows
|
|
172
226
|
|
|
173
227
|
## Development
|
|
174
228
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/sjapanjots/Flash-EDA/actions/workflows/tests.yml)
|
|
8
8
|
[](MIT.License)
|
|
9
9
|
|
|
10
|
-
**Current version: 0.
|
|
10
|
+
**Current version: 0.6.0** · [Changelog](CHANGELOG.md)
|
|
11
11
|
|
|
12
12
|
Analyse any dataset in a few seconds, then get **validated cleaning, feature and training code** that runs on the full data, in the engine that fits its size: pandas, polars, DuckDB or PySpark.
|
|
13
13
|
|
|
@@ -19,6 +19,7 @@ FlashEDA analyses a fixed-size random sample, so analysis time barely changes be
|
|
|
19
19
|
pip install flasheda # core
|
|
20
20
|
pip install "flasheda[fast]" # + polars, DuckDB, pyarrow (recommended for big files)
|
|
21
21
|
pip install "flasheda[ml]" # + scikit-learn, for flasheda.train
|
|
22
|
+
pip install "flasheda[sql]" # + SQLAlchemy, for database tables
|
|
22
23
|
pip install "flasheda[spark]" # + PySpark
|
|
23
24
|
pip install "flasheda[all]" # everything except PySpark
|
|
24
25
|
```
|
|
@@ -41,6 +42,27 @@ code.save("clean_orders.py") # then: python clean_orders.py
|
|
|
41
42
|
result = fe.train("orders.csv", target="churn", engine="polars")
|
|
42
43
|
result.show() # leaderboard
|
|
43
44
|
result.save("prep.py", "train.py") # python prep.py && python train.py → model.joblib
|
|
45
|
+
|
|
46
|
+
# 4. Guard: learn rules from good data, check every new file
|
|
47
|
+
contract = fe.analyze("orders_jan.csv").contract()
|
|
48
|
+
fe.check("orders_feb.csv", contract).show()
|
|
49
|
+
open("check.py", "w").write(contract.script()) # python check.py new.csv → exit 1 on failure
|
|
50
|
+
|
|
51
|
+
# 5. Compare: what changed between two datasets
|
|
52
|
+
fe.compare("orders_jan.csv", "orders_feb.csv").show()
|
|
53
|
+
|
|
54
|
+
# Databases work like files
|
|
55
|
+
src = fe.sql("postgresql://user:pass@host/shop", table="orders")
|
|
56
|
+
fe.analyze(src, target="churn")
|
|
57
|
+
|
|
58
|
+
# 6. Time series: trend, seasonality, forecast
|
|
59
|
+
ts = fe.timeseries("orders.parquet", time="created_at", value="amount")
|
|
60
|
+
ts.show()
|
|
61
|
+
fc = ts.forecast() # backtested baselines
|
|
62
|
+
open("forecast.py", "w").write(fc.script()) # python forecast.py → forecast.csv
|
|
63
|
+
|
|
64
|
+
# 7. Everything as one runnable notebook
|
|
65
|
+
report.to_notebook("orders.ipynb", train=True, time="created_at")
|
|
44
66
|
```
|
|
45
67
|
|
|
46
68
|
## What it does
|
|
@@ -55,6 +77,12 @@ result.save("prep.py", "train.py") # python prep.py && python train.py → model
|
|
|
55
77
|
| **Code generation** | one cleaning plan → pandas, polars (lazy, streaming), DuckDB SQL (larger than memory) or PySpark |
|
|
56
78
|
| **Features** | date parts, one-hot / frequency encoding, ratios that beat their inputs |
|
|
57
79
|
| **Models** | baseline, linear, random forest, gradient boosting compared with the right metric and split; training script for the winner |
|
|
80
|
+
| **Data contracts** | rules learned from good data; full-data checks; standalone check script for CI; pandera export |
|
|
81
|
+
| **Drift** | schema changes and per-column drift (PSI, KS, new categories, null-rate change) between two datasets |
|
|
82
|
+
| **Databases** | any SQLAlchemy URL; sampling runs in the database; passwords never stored |
|
|
83
|
+
| **Text** | personal data (emails, phones, card numbers…) flagged; free-text stats; length/word features |
|
|
84
|
+
| **Time series** | full-data aggregation per hour/day/week; gaps, trend, seasonality, anomalies; backtested forecast + script |
|
|
85
|
+
| **Notebook** | analysis, cleaning, training, forecast and contract check as one runnable `.ipynb` |
|
|
58
86
|
| **Validation** | generated code is run on the sample and compared with a reference result before you get it |
|
|
59
87
|
|
|
60
88
|
### Cleaning steps
|
|
@@ -76,6 +104,17 @@ Each step in the generated code has a comment saying why it was added:
|
|
|
76
104
|
|
|
77
105
|
The metric and split follow the target: PR-AUC for imbalanced binary targets, MAE for skewed regression targets, time-based splits when there's a date column, and so on.
|
|
78
106
|
|
|
107
|
+
### Data contracts and drift
|
|
108
|
+
A **contract** describes what good data looks like, learned from a sample with tolerances so normal variation passes:
|
|
109
|
+
type, missing %, 1st/99th percentile range (catches unit changes like ×100), no negatives, allowed values, uniqueness of IDs.
|
|
110
|
+
`fe.check()` checks every row of a file in one DuckDB pass; `contract.script()` writes the same check as a standalone script that exits with code 1 on failure, ready for a pipeline or CI job.
|
|
111
|
+
|
|
112
|
+
`fe.compare(old, new)` reports added/removed columns, type changes and how much each column shifted (PSI: < 0.1 stable, 0.1–0.25 moderate, ≥ 0.25 major). Dates and IDs are shown as range shifts, since they always move.
|
|
113
|
+
|
|
114
|
+
### Time series and forecasting
|
|
115
|
+
A random sample breaks time order, so `fe.timeseries()` first aggregates **every row** per period (one DuckDB `GROUP BY`, about 0.7 s for 5M rows), then reports gaps, trend (% per year), daily/weekly/yearly seasonality, the busiest weekday or hour, and anomalies.
|
|
116
|
+
`ts.forecast()` backtests five simple baselines (naive, seasonal naive, moving average, drift, trend + season) and keeps the most accurate. These are the numbers any fancier model has to beat. `fc.script()` writes it as a standalone script.
|
|
117
|
+
|
|
79
118
|
### Engine choice
|
|
80
119
|
|
|
81
120
|
| Rows | Engine |
|
|
@@ -98,12 +137,20 @@ Measured on synthetic messy data:
|
|
|
98
137
|
| Run the generated polars script on 1M rows | 0.9 s |
|
|
99
138
|
| `fe.train()` on a 1M-row CSV (plan, features, 4 models, checks) | 6.7 s |
|
|
100
139
|
| Generated `prep.py` + `train.py` on 1M rows | 1.3 s + 20 s |
|
|
140
|
+
| Learn a contract from 1M rows / run the check script on 1M rows | 0.8 s / 0.6–0.9 s |
|
|
141
|
+
| Compare two 1M-row files | 0.6 s |
|
|
142
|
+
| Analyse a 1M-row SQLite table (5,000 rows transferred) | 0.3 s |
|
|
143
|
+
| Time-series profile of a 5.3M-row Parquet file | 0.7 s |
|
|
144
|
+
| Generated forecast script on 5.3M rows | 0.6 s |
|
|
145
|
+
| Notebook with cleaning, features, training, forecast and contract | 5 s |
|
|
101
146
|
|
|
102
147
|
## Examples
|
|
103
148
|
|
|
104
149
|
- [`examples/phase1_demo.py`](examples/phase1_demo.py): engine choice and code generation at 0.1M / 1M / 5M rows
|
|
105
150
|
- [`examples/phase2_demo.py`](examples/phase2_demo.py): target analysis and cleaning on a 1M-row churn CSV
|
|
106
151
|
- [`examples/phase3_demo.py`](examples/phase3_demo.py): features, model comparison and training on 1M rows
|
|
152
|
+
- [`examples/phase4_demo.py`](examples/phase4_demo.py): contracts, check script, drift and a database on 1M rows
|
|
153
|
+
- [`examples/phase5_demo.py`](examples/phase5_demo.py): text, time series, forecast and notebook on 5M rows
|
|
107
154
|
|
|
108
155
|
## Development
|
|
109
156
|
|
|
@@ -10,21 +10,29 @@ import pandas as pd
|
|
|
10
10
|
from . import codegen, engine
|
|
11
11
|
from .clean import clean
|
|
12
12
|
from .modeling import train
|
|
13
|
-
from .
|
|
13
|
+
from .contract import check
|
|
14
|
+
from .compare import compare
|
|
15
|
+
from .timeseries import timeseries
|
|
16
|
+
from .engine import available_engines, choose_engine, inspect_source, sample, get_sample_size, set_sample_size, sql
|
|
14
17
|
from .analyzers import overview, missing, numeric, categorical, correlation
|
|
15
18
|
from .analyzers import target as target_analyzer
|
|
19
|
+
from .analyzers import text as text_analyzer
|
|
16
20
|
from .report import EDAReport
|
|
17
21
|
|
|
18
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.6.0"
|
|
19
23
|
__all__ = [
|
|
20
24
|
"analyze",
|
|
21
25
|
"available_engines",
|
|
26
|
+
"check",
|
|
22
27
|
"clean",
|
|
28
|
+
"compare",
|
|
23
29
|
"codegen",
|
|
24
30
|
"engine",
|
|
25
31
|
"get_sample_size",
|
|
26
32
|
"inspect_source",
|
|
27
33
|
"set_sample_size",
|
|
34
|
+
"sql",
|
|
35
|
+
"timeseries",
|
|
28
36
|
"train",
|
|
29
37
|
"EDAReport",
|
|
30
38
|
]
|
|
@@ -36,13 +44,17 @@ def analyze(
|
|
|
36
44
|
*,
|
|
37
45
|
show: bool = False,
|
|
38
46
|
random_state: int = 42,
|
|
39
|
-
save_pdf: bool =
|
|
47
|
+
save_pdf: bool = False,
|
|
40
48
|
pdf_path: str = "flasheda_report.pdf",
|
|
41
49
|
engine: Optional[str] = None,
|
|
42
50
|
target: Optional[str] = None,
|
|
43
51
|
) -> EDAReport:
|
|
44
52
|
"""
|
|
45
|
-
Analyse a fixed-size sample of `source
|
|
53
|
+
Analyse a fixed-size sample of `source`: a DataFrame, a CSV/TSV or Parquet
|
|
54
|
+
path, or a database table/query from flasheda.sql().
|
|
55
|
+
|
|
56
|
+
Nothing is written to disk unless you ask: pass save_pdf=True (to
|
|
57
|
+
pdf_path) or call report.save_pdf() / report.save_html().
|
|
46
58
|
|
|
47
59
|
`target` names the column you want to predict; it adds task detection,
|
|
48
60
|
class balance, per-feature predictive power and leakage checks.
|
|
@@ -65,12 +77,13 @@ def analyze(
|
|
|
65
77
|
"numeric": lambda: numeric.analyze(sampled),
|
|
66
78
|
"categorical": lambda: categorical.analyze(sampled),
|
|
67
79
|
"correlation": lambda: correlation.analyze(sampled),
|
|
80
|
+
"text": lambda: text_analyzer.analyze(sampled),
|
|
68
81
|
}
|
|
69
82
|
if target is not None:
|
|
70
83
|
tasks["target"] = lambda: target_analyzer.analyze(sampled, target, random_state)
|
|
71
84
|
|
|
72
85
|
results: dict = {}
|
|
73
|
-
with ThreadPoolExecutor(max_workers=
|
|
86
|
+
with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
|
|
74
87
|
future_map = {pool.submit(fn): name for name, fn in tasks.items()}
|
|
75
88
|
for future in as_completed(future_map):
|
|
76
89
|
name = future_map[future]
|
|
@@ -92,6 +105,14 @@ def analyze(
|
|
|
92
105
|
corr = results.get("correlation", {})
|
|
93
106
|
if corr.get("numeric_strong_pairs"):
|
|
94
107
|
warnings.append(f"{len(corr['numeric_strong_pairs'])} strongly correlated pair(s) found")
|
|
108
|
+
for p in corr.get("numeric_boolean_strong_pairs", []):
|
|
109
|
+
warnings.append(f"{p['numeric']!r} almost determines {p['boolean']!r} "
|
|
110
|
+
f"(point-biserial r = {p['point_biserial_r']})")
|
|
111
|
+
|
|
112
|
+
txt = results.get("text", {})
|
|
113
|
+
for col in txt.get("personal_data_columns", []):
|
|
114
|
+
kinds = ", ".join(f"{k} {v}%" for k, v in txt["columns"][col]["personal_data"].items())
|
|
115
|
+
warnings.append(f"Possible personal data in {col!r}: {kinds} of non-empty values")
|
|
95
116
|
|
|
96
117
|
tgt = results.get("target", {})
|
|
97
118
|
if "error" in tgt:
|
|
@@ -115,13 +136,17 @@ def analyze(
|
|
|
115
136
|
numeric=results.get("numeric", {}),
|
|
116
137
|
categorical=results.get("categorical", {}),
|
|
117
138
|
correlation=results.get("correlation", {}),
|
|
139
|
+
text=txt,
|
|
118
140
|
sample_size=len(sampled),
|
|
119
141
|
elapsed_seconds=elapsed,
|
|
120
142
|
original_shape=original_shape,
|
|
121
143
|
warnings=warnings,
|
|
122
144
|
target=tgt,
|
|
123
145
|
source=str(info.path) if info.path else None,
|
|
124
|
-
|
|
146
|
+
sql_source=info.sql,
|
|
147
|
+
# database sources are read with pandas (or exported with .to_parquet())
|
|
148
|
+
recommended_engine=choose_engine(
|
|
149
|
+
info.n_rows, prefer=engine or ("pandas" if info.sql is not None else None)),
|
|
125
150
|
sample=sampled,
|
|
126
151
|
)
|
|
127
152
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from . import overview, missing, numeric, categorical, correlation, target
|
|
1
|
+
from . import overview, missing, numeric, categorical, correlation, target, text
|
|
2
2
|
|
|
3
|
-
__all__ = ["overview", "missing", "numeric", "categorical", "correlation", "target"]
|
|
3
|
+
__all__ = ["overview", "missing", "numeric", "categorical", "correlation", "target", "text"]
|
|
@@ -11,7 +11,7 @@ import pandas as pd
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
from typing import Dict, Any, List
|
|
13
13
|
|
|
14
|
-
from ..clean.detectors import looks_like_datetime
|
|
14
|
+
from ..clean.detectors import is_free_text, looks_like_datetime
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
HIGH_CARDINALITY_RATIO = 0.5 # unique/total > 50% → high cardinality warning
|
|
@@ -54,8 +54,9 @@ def analyze(df: pd.DataFrame) -> Dict[str, Any]:
|
|
|
54
54
|
mode_pct = round(vc.iloc[0] / n * 100, 2) if len(vc) > 0 else 0.0
|
|
55
55
|
|
|
56
56
|
# Detect potential ID columns (very high cardinality); unique
|
|
57
|
-
# timestamps are high-cardinality too, but
|
|
58
|
-
likely_id = cardinality_ratio > 0.95 and not looks_like_datetime(df[col])
|
|
57
|
+
# timestamps and free text are high-cardinality too, but not identifiers
|
|
58
|
+
likely_id = (cardinality_ratio > 0.95 and not looks_like_datetime(df[col])
|
|
59
|
+
and not is_free_text(df[col]))
|
|
59
60
|
|
|
60
61
|
results[col] = {
|
|
61
62
|
"count": n,
|
|
@@ -4,7 +4,7 @@ flasheda.analyzers.correlation
|
|
|
4
4
|
Computes:
|
|
5
5
|
- Pearson correlation for numeric–numeric pairs
|
|
6
6
|
- Cramér's V for categorical–categorical pairs
|
|
7
|
-
- Point-biserial for numeric–boolean pairs
|
|
7
|
+
- Point-biserial for numeric–boolean pairs (Pearson with the boolean as 0/1)
|
|
8
8
|
|
|
9
9
|
Flags strongly correlated pairs (potential redundant features).
|
|
10
10
|
Always operates on the fixed sample — constant time.
|
|
@@ -21,6 +21,7 @@ from itertools import combinations
|
|
|
21
21
|
STRONG_CORR_THRESHOLD = 0.80
|
|
22
22
|
MAX_COLS_FOR_HEATMAP = 20 # skip heatmap if too many cols (slow to render)
|
|
23
23
|
MAX_CRAMER_LEVELS = 100 # Cramér's V is meaningless (and slow) for ID-like columns
|
|
24
|
+
MAX_BOOL_COLS = 10
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def analyze(df: pd.DataFrame) -> Dict[str, Any]:
|
|
@@ -66,10 +67,20 @@ def analyze(df: pd.DataFrame) -> Dict[str, Any]:
|
|
|
66
67
|
"matrix": [[round(float(hm_corr.loc[r, c]), 3) for c in heatmap_cols] for r in heatmap_cols],
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
# ── Point-biserial for numeric–boolean pairs ────────────────────────────
|
|
71
|
+
biserial_pairs: List[Dict[str, Any]] = []
|
|
72
|
+
for b in [c for c in df.columns if _is_boolean(df[c])][:MAX_BOOL_COLS]:
|
|
73
|
+
flag = _as_01(df[b])
|
|
74
|
+
for a in num_cols[:MAX_COLS_FOR_HEATMAP]:
|
|
75
|
+
r = _point_biserial(df[a], flag)
|
|
76
|
+
if r is not None and abs(r) >= STRONG_CORR_THRESHOLD:
|
|
77
|
+
biserial_pairs.append({"numeric": a, "boolean": b, "point_biserial_r": round(r, 4)})
|
|
78
|
+
|
|
69
79
|
return {
|
|
70
80
|
"pearson_matrix": pearson_matrix,
|
|
71
81
|
"numeric_strong_pairs": strong_pairs,
|
|
72
82
|
"categorical_strong_pairs": cramer_pairs,
|
|
83
|
+
"numeric_boolean_strong_pairs": biserial_pairs,
|
|
73
84
|
"heatmap": heatmap_data,
|
|
74
85
|
"numeric_col_names": num_cols,
|
|
75
86
|
}
|
|
@@ -92,4 +103,29 @@ def _cramers_v(a: pd.Series, b: pd.Series) -> float | None:
|
|
|
92
103
|
return None
|
|
93
104
|
return float(np.sqrt(phi2_corr / denom))
|
|
94
105
|
except Exception:
|
|
95
|
-
return None
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _is_boolean(s: pd.Series) -> bool:
|
|
109
|
+
"""bool dtype, or text holding only true/false."""
|
|
110
|
+
if pd.api.types.is_bool_dtype(s):
|
|
111
|
+
return True
|
|
112
|
+
values = s.dropna()
|
|
113
|
+
if not len(values) or pd.api.types.is_numeric_dtype(values):
|
|
114
|
+
return False
|
|
115
|
+
return bool(values.astype(str).str.strip().str.lower().isin(["true", "false"]).all())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _as_01(s: pd.Series) -> pd.Series:
|
|
119
|
+
if pd.api.types.is_bool_dtype(s):
|
|
120
|
+
return s.astype(float)
|
|
121
|
+
text = s.astype("string").str.strip().str.lower()
|
|
122
|
+
return text.map({"true": 1.0, "false": 0.0}).astype(float)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _point_biserial(x: pd.Series, flag: pd.Series) -> float | None:
|
|
126
|
+
"""Point-biserial r: Pearson between a numeric column and a 0/1 flag."""
|
|
127
|
+
pair = pd.DataFrame({"x": pd.to_numeric(x, errors="coerce"), "b": flag}).dropna()
|
|
128
|
+
if len(pair) < 3 or pair["x"].std() == 0 or pair["b"].nunique() < 2:
|
|
129
|
+
return None
|
|
130
|
+
from scipy.stats import pointbiserialr
|
|
131
|
+
return float(pointbiserialr(pair["b"], pair["x"]).statistic)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flasheda.analyzers.text
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Text columns:
|
|
5
|
+
|
|
6
|
+
personal data emails, phone numbers, URLs, IP addresses and card numbers
|
|
7
|
+
(Luhn-checked) found in any text column, as a share of the
|
|
8
|
+
column's non-empty values
|
|
9
|
+
free text for columns that read like sentences (≥ 3 words on average
|
|
10
|
+
or ≥ 30 characters): length and word statistics, empty and
|
|
11
|
+
duplicate values, all-caps share, most common words
|
|
12
|
+
|
|
13
|
+
Regex-based and dependency-free; runs on the fixed sample.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from typing import Any, Dict, List
|
|
21
|
+
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from ..clean.detectors import is_free_text, is_text
|
|
25
|
+
|
|
26
|
+
PII_PATTERNS = {
|
|
27
|
+
"email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
|
|
28
|
+
"phone": r"(?<![\w.])\+?\d[\d\s().-]{7,}\d(?![\w.])",
|
|
29
|
+
"url": r"\bhttps?://[^\s]+|\bwww\.[^\s]+",
|
|
30
|
+
"ip_address": r"(?<![\d.])(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)(?![\d.])",
|
|
31
|
+
"card_number": r"\b(?:\d[ -]?){13,19}\b",
|
|
32
|
+
}
|
|
33
|
+
PII_MIN_PCT = 1.0 # report a type when at least 1% of values contain it
|
|
34
|
+
TOP_WORDS = 10
|
|
35
|
+
STOPWORDS = frozenset("""a an and are as at be but by for from has have i in is it its of on or
|
|
36
|
+
that the this to was were will with you your we our they their he she not no so if when what how
|
|
37
|
+
my me all just can do did been than then there about up out very""".split())
|
|
38
|
+
WORD = re.compile(r"[a-z][a-z']+")
|
|
39
|
+
DATE_LIKE = re.compile(r"\d{4}[-/.]\d{1,2}[-/.]\d{1,2}|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4}|\d:\d{2}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def analyze(df: pd.DataFrame) -> Dict[str, Any]:
|
|
43
|
+
columns: Dict[str, Dict[str, Any]] = {}
|
|
44
|
+
for col in df.columns:
|
|
45
|
+
s = df[col]
|
|
46
|
+
if not is_text(s):
|
|
47
|
+
continue
|
|
48
|
+
values = s.dropna().astype(str)
|
|
49
|
+
info: Dict[str, Any] = {}
|
|
50
|
+
pii = personal_data(values)
|
|
51
|
+
if pii:
|
|
52
|
+
info["personal_data"] = pii
|
|
53
|
+
if is_free_text(s):
|
|
54
|
+
info["free_text"] = _free_text_stats(values, values.str.len(), values.str.count(r"\S+"))
|
|
55
|
+
if info:
|
|
56
|
+
columns[str(col)] = info
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"columns": columns,
|
|
60
|
+
"personal_data_columns": [c for c, v in columns.items() if "personal_data" in v],
|
|
61
|
+
"free_text_columns": [c for c, v in columns.items() if "free_text" in v],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def personal_data(values: pd.Series) -> Dict[str, float]:
|
|
66
|
+
"""{kind: % of non-empty values containing it}, for kinds in ≥ PII_MIN_PCT of them."""
|
|
67
|
+
if not len(values):
|
|
68
|
+
return {}
|
|
69
|
+
found = {}
|
|
70
|
+
for kind, pattern in PII_PATTERNS.items():
|
|
71
|
+
hits = values.str.contains(pattern, regex=True)
|
|
72
|
+
if kind == "card_number" and hits.any():
|
|
73
|
+
hits = hits & values.map(_has_luhn_number)
|
|
74
|
+
if kind == "phone" and hits.any():
|
|
75
|
+
hits = hits & values.map(_looks_like_phone) & \
|
|
76
|
+
~values.str.contains(PII_PATTERNS["ip_address"], regex=True)
|
|
77
|
+
pct = round(float(hits.mean() * 100), 2)
|
|
78
|
+
if pct >= PII_MIN_PCT:
|
|
79
|
+
found[kind] = pct
|
|
80
|
+
if "card_number" in found:
|
|
81
|
+
found.pop("phone", None) # card numbers also match the phone pattern
|
|
82
|
+
return found
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _looks_like_phone(text: str) -> bool:
|
|
86
|
+
"""9–15 digits, written with a + or separators, or exactly 10 plain digits.
|
|
87
|
+
Plain digit runs of other lengths are usually order or account numbers."""
|
|
88
|
+
for m in re.finditer(PII_PATTERNS["phone"], text):
|
|
89
|
+
raw = m.group()
|
|
90
|
+
digits = sum(ch.isdigit() for ch in raw)
|
|
91
|
+
formatted = raw.startswith("+") or any(ch in raw for ch in " ().-")
|
|
92
|
+
if DATE_LIKE.search(raw) or re.fullmatch(r"\d+\.\d+", raw):
|
|
93
|
+
continue # dates, times and decimals have digit runs too
|
|
94
|
+
if 9 <= digits <= 15 and (formatted or digits == 10):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _has_luhn_number(text: str) -> bool:
|
|
100
|
+
for m in re.finditer(PII_PATTERNS["card_number"], text):
|
|
101
|
+
digits = [int(d) for d in re.sub(r"\D", "", m.group())]
|
|
102
|
+
if 13 <= len(digits) <= 19 and _luhn(digits):
|
|
103
|
+
return True
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _luhn(digits: List[int]) -> bool:
|
|
108
|
+
total = 0
|
|
109
|
+
for i, d in enumerate(reversed(digits)):
|
|
110
|
+
if i % 2:
|
|
111
|
+
d *= 2
|
|
112
|
+
d -= 9 if d > 9 else 0
|
|
113
|
+
total += d
|
|
114
|
+
return total % 10 == 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _free_text_stats(values: pd.Series, lengths: pd.Series, words: pd.Series) -> Dict[str, Any]:
|
|
118
|
+
stripped = values.str.strip()
|
|
119
|
+
letters = stripped.str.replace(r"[^A-Za-z]", "", regex=True)
|
|
120
|
+
counter: Counter = Counter()
|
|
121
|
+
for text in values.str.lower().head(2_000):
|
|
122
|
+
counter.update(w for w in WORD.findall(text) if w not in STOPWORDS)
|
|
123
|
+
n = len(values)
|
|
124
|
+
return {
|
|
125
|
+
"mean_chars": round(float(lengths.mean()), 1),
|
|
126
|
+
"median_chars": float(lengths.median()),
|
|
127
|
+
"max_chars": int(lengths.max()),
|
|
128
|
+
"mean_words": round(float(words.mean()), 1),
|
|
129
|
+
"empty_pct": round(float((stripped == "").mean() * 100), 2),
|
|
130
|
+
"duplicate_pct": round(float(values.duplicated().mean() * 100), 2),
|
|
131
|
+
"all_caps_pct": round(float(((letters.str.len() > 3) & (letters == letters.str.upper())).mean() * 100), 2),
|
|
132
|
+
"top_words": [w for w, _ in counter.most_common(TOP_WORDS)],
|
|
133
|
+
"rows": n,
|
|
134
|
+
}
|
|
@@ -41,6 +41,19 @@ def looks_like_datetime(s: pd.Series, min_ratio: float = 0.95) -> bool:
|
|
|
41
41
|
return bool(parsed.notna().mean() >= min_ratio)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
FREE_TEXT_MIN_WORDS = 3.0
|
|
45
|
+
FREE_TEXT_MIN_CHARS = 30.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_free_text(s: pd.Series) -> bool:
|
|
49
|
+
"""Text that reads like sentences: ≥ 3 words or ≥ 30 characters on average."""
|
|
50
|
+
if not is_text(s):
|
|
51
|
+
return False
|
|
52
|
+
values = s.dropna().astype(str).head(2_000)
|
|
53
|
+
return bool(values.str.count(r"\S+").mean() >= FREE_TEXT_MIN_WORDS
|
|
54
|
+
or values.str.len().mean() >= FREE_TEXT_MIN_CHARS)
|
|
55
|
+
|
|
56
|
+
|
|
44
57
|
def numeric_text_ratio(s: pd.Series) -> Optional[float]:
|
|
45
58
|
"""Share of non-empty text values that parse as numbers, or None if it's not text."""
|
|
46
59
|
if not is_text(s):
|
|
@@ -18,7 +18,7 @@ from .planner import plan_from_report
|
|
|
18
18
|
from .steps import (
|
|
19
19
|
ClipOutliers, DateParts, DropColumns, DropDuplicates, DropNullRows, FillNull,
|
|
20
20
|
FrequencyEncode, GroupRare, Log1p, MissingIndicator, NormalizeText, OneHot, Plan, Ratio,
|
|
21
|
-
ToNumeric,
|
|
21
|
+
TextStats, ToNumeric,
|
|
22
22
|
)
|
|
23
23
|
from .validate import ValidationResult, compare_frames, run_generated, validate_code
|
|
24
24
|
from .writers import WRITERS, get_writer
|
|
@@ -58,10 +58,12 @@ def generate(
|
|
|
58
58
|
output: str = "cleaned.parquet",
|
|
59
59
|
sample: Optional[pd.DataFrame] = None,
|
|
60
60
|
columns: Optional[List[str]] = None,
|
|
61
|
+
sql: Any = None,
|
|
61
62
|
) -> GeneratedCode:
|
|
63
|
+
"""`sql` is a flasheda.sql() source to read from instead of a file."""
|
|
62
64
|
if columns is None and sample is not None:
|
|
63
65
|
columns = [str(c) for c in sample.columns]
|
|
64
|
-
code = get_writer(engine).render(plan, source, output, columns)
|
|
66
|
+
code = get_writer(engine).render(plan, source, output, columns, sql=sql)
|
|
65
67
|
return GeneratedCode(engine, code, plan, sample)
|
|
66
68
|
|
|
67
69
|
|
|
@@ -81,6 +83,7 @@ __all__ = [
|
|
|
81
83
|
"OneHot",
|
|
82
84
|
"Plan",
|
|
83
85
|
"Ratio",
|
|
86
|
+
"TextStats",
|
|
84
87
|
"ToNumeric",
|
|
85
88
|
"ValidationResult",
|
|
86
89
|
"WRITERS",
|