edasnap 0.1.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.
- edasnap-0.1.0/LICENSE +21 -0
- edasnap-0.1.0/PKG-INFO +162 -0
- edasnap-0.1.0/README.md +131 -0
- edasnap-0.1.0/pyproject.toml +46 -0
- edasnap-0.1.0/setup.cfg +4 -0
- edasnap-0.1.0/src/edasnap/__init__.py +51 -0
- edasnap-0.1.0/src/edasnap/compare.py +64 -0
- edasnap-0.1.0/src/edasnap/overview.py +54 -0
- edasnap-0.1.0/src/edasnap/plots.py +136 -0
- edasnap-0.1.0/src/edasnap/quality.py +103 -0
- edasnap-0.1.0/src/edasnap/report.py +51 -0
- edasnap-0.1.0/src/edasnap/stats.py +35 -0
- edasnap-0.1.0/src/edasnap.egg-info/PKG-INFO +162 -0
- edasnap-0.1.0/src/edasnap.egg-info/SOURCES.txt +21 -0
- edasnap-0.1.0/src/edasnap.egg-info/dependency_links.txt +1 -0
- edasnap-0.1.0/src/edasnap.egg-info/requires.txt +8 -0
- edasnap-0.1.0/src/edasnap.egg-info/top_level.txt +1 -0
- edasnap-0.1.0/tests/test_compare.py +38 -0
- edasnap-0.1.0/tests/test_overview.py +38 -0
- edasnap-0.1.0/tests/test_plots.py +30 -0
- edasnap-0.1.0/tests/test_quality.py +69 -0
- edasnap-0.1.0/tests/test_report.py +19 -0
- edasnap-0.1.0/tests/test_stats.py +22 -0
edasnap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sivaraam.kr
|
|
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.
|
edasnap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edasnap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fast, honest first look at any pandas DataFrame — one-liner EDA helpers, no bloat.
|
|
5
|
+
Author: sivaraam.kr
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sivaraam-kr/edasnap
|
|
8
|
+
Project-URL: Issues, https://github.com/sivaraam-kr/edasnap/issues
|
|
9
|
+
Keywords: eda,exploratory-data-analysis,pandas,data-science,data-quality
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pandas>=1.3
|
|
24
|
+
Requires-Dist: matplotlib>=3.4
|
|
25
|
+
Requires-Dist: seaborn>=0.11
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# edasnap
|
|
33
|
+
|
|
34
|
+
**A fast, honest first look at any pandas DataFrame — in one line each.**
|
|
35
|
+
|
|
36
|
+
`edasnap` answers the questions you ask every single time you load a new
|
|
37
|
+
dataset: What's the shape? What's null, and how much? What are my numeric
|
|
38
|
+
vs. categorical columns? Are there duplicate rows, constant columns, or
|
|
39
|
+
columns that are secretly IDs? Do `"USA"`, `"usa "`, and `"U.S.A"` need to
|
|
40
|
+
be merged into one category? Will this merge silently blow up my row count?
|
|
41
|
+
|
|
42
|
+
It's built to be the opposite of a giant auto-generated HTML report:
|
|
43
|
+
|
|
44
|
+
- **One function, one job.** No monolithic `report()` that dumps 40 sections
|
|
45
|
+
you didn't ask for — call only what you need.
|
|
46
|
+
- **Every function returns real data** (a `dict`, a `DataFrame`, a
|
|
47
|
+
matplotlib `Figure`) — never just a printout you can't reuse.
|
|
48
|
+
- **Never mutates your DataFrame.** Everything is read-only analysis.
|
|
49
|
+
- **Small on purpose.** Just pandas EDA — no bundled ML training, no
|
|
50
|
+
encoding pipelines, no computer vision, no LLM calls. It's a lens on your
|
|
51
|
+
data, not a pipeline.
|
|
52
|
+
- **Doesn't crash on messy real data** — mixed types, weird strings, and
|
|
53
|
+
edge cases (empty df, all-null column, single row) are handled, not
|
|
54
|
+
fatal.
|
|
55
|
+
- **Readable source.** Every function is short enough to read in ten
|
|
56
|
+
seconds if you're curious what it's actually doing — handy if you're
|
|
57
|
+
still learning pandas.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install edasnap
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import pandas as pd
|
|
69
|
+
import edasnap as es
|
|
70
|
+
|
|
71
|
+
df = pd.read_csv("data.csv")
|
|
72
|
+
|
|
73
|
+
es.quick_report(df) # runs the core checks below, in a sensible order
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## All functions
|
|
77
|
+
|
|
78
|
+
### Structure & column types
|
|
79
|
+
|
|
80
|
+
| Function | What it tells you |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `es.overview(df)` | Row/column count, total + per-column memory usage, duplicate row count |
|
|
83
|
+
| `es.dtypes_report(df)` | Buckets every column into `numeric`, `categorical`, `datetime`, `boolean`, or `text` |
|
|
84
|
+
| `es.get_numeric_cols(df)` | List of numeric column names — ready to hand to a plot or model |
|
|
85
|
+
| `es.get_categorical_cols(df)` | List of categorical column names |
|
|
86
|
+
| `es.get_datetime_cols(df)` | List of datetime column names |
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
es.overview(df)
|
|
90
|
+
# {'rows': 891, 'columns': 12, 'duplicate_rows': 0,
|
|
91
|
+
# 'total_memory_mb': 0.29, 'memory_by_column_mb': {...}}
|
|
92
|
+
|
|
93
|
+
es.dtypes_report(df)
|
|
94
|
+
# {'numeric': ['Age', 'Fare'], 'categorical': ['Sex', 'Embarked'], ...}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Data quality
|
|
98
|
+
|
|
99
|
+
| Function | What it tells you |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `es.nulls(df)` | % missing per column, worst first — only columns that actually have nulls |
|
|
102
|
+
| `es.duplicates(df, subset=None)` | Duplicate row count, % of the dataset, and a preview |
|
|
103
|
+
| `es.constant_and_id_cols(df)` | Columns with only one value (dead weight), and columns that are likely IDs (`nunique == len(df)`) — both are common "don't feed this to a model" traps |
|
|
104
|
+
| `es.inconsistent_categories(df)` | Flags values in text columns that are probably the same category typed differently — `"USA"` / `"usa "` / `"U.S.A"` — a check the bigger EDA tools skip |
|
|
105
|
+
| `es.outlier_report(df, method="iqr")` | Outlier count and % per numeric column, via IQR or z-score — as a table, no plot needed |
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
es.nulls(df)
|
|
109
|
+
# null_count null_pct
|
|
110
|
+
# Cabin 687 77.10
|
|
111
|
+
# Age 177 19.87
|
|
112
|
+
|
|
113
|
+
es.inconsistent_categories(df)
|
|
114
|
+
# {'country': {'usa': ['USA', 'usa ', 'U.S.A'], 'uk': ['UK', 'uk']}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Summary stats
|
|
118
|
+
|
|
119
|
+
| Function | What it tells you |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `es.describe_plus(df)` | `.describe()` plus null %, skew, kurtosis, and dtype in one table |
|
|
122
|
+
| `es.unique_report(df)` | `nunique` + a few example values per column |
|
|
123
|
+
|
|
124
|
+
### Plots (each returns the matplotlib `Figure`)
|
|
125
|
+
|
|
126
|
+
| Function | What it draws |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `es.plot_numeric_distributions(df)` | Histogram + boxplot grid, one pair per numeric column |
|
|
129
|
+
| `es.plot_categorical_counts(df, top_n=10)` | Bar chart grid per categorical column, capped at the top N values |
|
|
130
|
+
| `es.plot_correlation(df)` | Masked correlation heatmap over numeric columns |
|
|
131
|
+
| `es.plot_target_relationship(df, target="price")` | Auto-picks boxplot/violin/scatter per feature against your target column |
|
|
132
|
+
| `es.plot_missing(df)` | Heatmap of where nulls occur across the whole DataFrame |
|
|
133
|
+
|
|
134
|
+
### Comparing two DataFrames
|
|
135
|
+
|
|
136
|
+
| Function | What it tells you |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `es.compare_dfs(train_df, test_df)` | Shared/added/removed columns and dtype mismatches between two DataFrames — e.g. train vs. test drift |
|
|
139
|
+
| `es.check_merge_keys(orders_df, users_df, on="user_id")` | Before you `merge()`: dtype mismatch on the key, % of keys missing on each side, and duplicate-key counts on each side — the usual cause of a merge silently multiplying your row count |
|
|
140
|
+
|
|
141
|
+
### The one convenience function
|
|
142
|
+
|
|
143
|
+
| Function | What it does |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `es.quick_report(df)` | Chains `overview` → `nulls` → `dtypes_report` → `duplicates` → `constant_and_id_cols` → a numeric-distribution plot, prints a readable summary, and returns everything as a dict. Every piece it calls also works standalone. |
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/sivaraam-kr/edasnap
|
|
151
|
+
cd edasnap
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Publishing
|
|
157
|
+
|
|
158
|
+
See [PUBLISHING.md](PUBLISHING.md) for the release checklist.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
edasnap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# edasnap
|
|
2
|
+
|
|
3
|
+
**A fast, honest first look at any pandas DataFrame — in one line each.**
|
|
4
|
+
|
|
5
|
+
`edasnap` answers the questions you ask every single time you load a new
|
|
6
|
+
dataset: What's the shape? What's null, and how much? What are my numeric
|
|
7
|
+
vs. categorical columns? Are there duplicate rows, constant columns, or
|
|
8
|
+
columns that are secretly IDs? Do `"USA"`, `"usa "`, and `"U.S.A"` need to
|
|
9
|
+
be merged into one category? Will this merge silently blow up my row count?
|
|
10
|
+
|
|
11
|
+
It's built to be the opposite of a giant auto-generated HTML report:
|
|
12
|
+
|
|
13
|
+
- **One function, one job.** No monolithic `report()` that dumps 40 sections
|
|
14
|
+
you didn't ask for — call only what you need.
|
|
15
|
+
- **Every function returns real data** (a `dict`, a `DataFrame`, a
|
|
16
|
+
matplotlib `Figure`) — never just a printout you can't reuse.
|
|
17
|
+
- **Never mutates your DataFrame.** Everything is read-only analysis.
|
|
18
|
+
- **Small on purpose.** Just pandas EDA — no bundled ML training, no
|
|
19
|
+
encoding pipelines, no computer vision, no LLM calls. It's a lens on your
|
|
20
|
+
data, not a pipeline.
|
|
21
|
+
- **Doesn't crash on messy real data** — mixed types, weird strings, and
|
|
22
|
+
edge cases (empty df, all-null column, single row) are handled, not
|
|
23
|
+
fatal.
|
|
24
|
+
- **Readable source.** Every function is short enough to read in ten
|
|
25
|
+
seconds if you're curious what it's actually doing — handy if you're
|
|
26
|
+
still learning pandas.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install edasnap
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import pandas as pd
|
|
38
|
+
import edasnap as es
|
|
39
|
+
|
|
40
|
+
df = pd.read_csv("data.csv")
|
|
41
|
+
|
|
42
|
+
es.quick_report(df) # runs the core checks below, in a sensible order
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## All functions
|
|
46
|
+
|
|
47
|
+
### Structure & column types
|
|
48
|
+
|
|
49
|
+
| Function | What it tells you |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `es.overview(df)` | Row/column count, total + per-column memory usage, duplicate row count |
|
|
52
|
+
| `es.dtypes_report(df)` | Buckets every column into `numeric`, `categorical`, `datetime`, `boolean`, or `text` |
|
|
53
|
+
| `es.get_numeric_cols(df)` | List of numeric column names — ready to hand to a plot or model |
|
|
54
|
+
| `es.get_categorical_cols(df)` | List of categorical column names |
|
|
55
|
+
| `es.get_datetime_cols(df)` | List of datetime column names |
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
es.overview(df)
|
|
59
|
+
# {'rows': 891, 'columns': 12, 'duplicate_rows': 0,
|
|
60
|
+
# 'total_memory_mb': 0.29, 'memory_by_column_mb': {...}}
|
|
61
|
+
|
|
62
|
+
es.dtypes_report(df)
|
|
63
|
+
# {'numeric': ['Age', 'Fare'], 'categorical': ['Sex', 'Embarked'], ...}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Data quality
|
|
67
|
+
|
|
68
|
+
| Function | What it tells you |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `es.nulls(df)` | % missing per column, worst first — only columns that actually have nulls |
|
|
71
|
+
| `es.duplicates(df, subset=None)` | Duplicate row count, % of the dataset, and a preview |
|
|
72
|
+
| `es.constant_and_id_cols(df)` | Columns with only one value (dead weight), and columns that are likely IDs (`nunique == len(df)`) — both are common "don't feed this to a model" traps |
|
|
73
|
+
| `es.inconsistent_categories(df)` | Flags values in text columns that are probably the same category typed differently — `"USA"` / `"usa "` / `"U.S.A"` — a check the bigger EDA tools skip |
|
|
74
|
+
| `es.outlier_report(df, method="iqr")` | Outlier count and % per numeric column, via IQR or z-score — as a table, no plot needed |
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
es.nulls(df)
|
|
78
|
+
# null_count null_pct
|
|
79
|
+
# Cabin 687 77.10
|
|
80
|
+
# Age 177 19.87
|
|
81
|
+
|
|
82
|
+
es.inconsistent_categories(df)
|
|
83
|
+
# {'country': {'usa': ['USA', 'usa ', 'U.S.A'], 'uk': ['UK', 'uk']}}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Summary stats
|
|
87
|
+
|
|
88
|
+
| Function | What it tells you |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `es.describe_plus(df)` | `.describe()` plus null %, skew, kurtosis, and dtype in one table |
|
|
91
|
+
| `es.unique_report(df)` | `nunique` + a few example values per column |
|
|
92
|
+
|
|
93
|
+
### Plots (each returns the matplotlib `Figure`)
|
|
94
|
+
|
|
95
|
+
| Function | What it draws |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `es.plot_numeric_distributions(df)` | Histogram + boxplot grid, one pair per numeric column |
|
|
98
|
+
| `es.plot_categorical_counts(df, top_n=10)` | Bar chart grid per categorical column, capped at the top N values |
|
|
99
|
+
| `es.plot_correlation(df)` | Masked correlation heatmap over numeric columns |
|
|
100
|
+
| `es.plot_target_relationship(df, target="price")` | Auto-picks boxplot/violin/scatter per feature against your target column |
|
|
101
|
+
| `es.plot_missing(df)` | Heatmap of where nulls occur across the whole DataFrame |
|
|
102
|
+
|
|
103
|
+
### Comparing two DataFrames
|
|
104
|
+
|
|
105
|
+
| Function | What it tells you |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `es.compare_dfs(train_df, test_df)` | Shared/added/removed columns and dtype mismatches between two DataFrames — e.g. train vs. test drift |
|
|
108
|
+
| `es.check_merge_keys(orders_df, users_df, on="user_id")` | Before you `merge()`: dtype mismatch on the key, % of keys missing on each side, and duplicate-key counts on each side — the usual cause of a merge silently multiplying your row count |
|
|
109
|
+
|
|
110
|
+
### The one convenience function
|
|
111
|
+
|
|
112
|
+
| Function | What it does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `es.quick_report(df)` | Chains `overview` → `nulls` → `dtypes_report` → `duplicates` → `constant_and_id_cols` → a numeric-distribution plot, prints a readable summary, and returns everything as a dict. Every piece it calls also works standalone. |
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git clone https://github.com/sivaraam-kr/edasnap
|
|
120
|
+
cd edasnap
|
|
121
|
+
pip install -e ".[dev]"
|
|
122
|
+
pytest
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Publishing
|
|
126
|
+
|
|
127
|
+
See [PUBLISHING.md](PUBLISHING.md) for the release checklist.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "edasnap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A fast, honest first look at any pandas DataFrame — one-liner EDA helpers, no bloat."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "sivaraam.kr" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["eda", "exploratory-data-analysis", "pandas", "data-science", "data-quality"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Intended Audience :: Education",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pandas>=1.3",
|
|
30
|
+
"matplotlib>=3.4",
|
|
31
|
+
"seaborn>=0.11",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.0",
|
|
37
|
+
"build",
|
|
38
|
+
"twine",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/sivaraam-kr/edasnap"
|
|
43
|
+
Issues = "https://github.com/sivaraam-kr/edasnap/issues"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
edasnap-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""edasnap: fast, simple, one-liner EDA helpers for pandas DataFrames."""
|
|
2
|
+
|
|
3
|
+
from .overview import (
|
|
4
|
+
dtypes_report,
|
|
5
|
+
get_categorical_cols,
|
|
6
|
+
get_datetime_cols,
|
|
7
|
+
get_numeric_cols,
|
|
8
|
+
overview,
|
|
9
|
+
)
|
|
10
|
+
from .quality import (
|
|
11
|
+
constant_and_id_cols,
|
|
12
|
+
duplicates,
|
|
13
|
+
inconsistent_categories,
|
|
14
|
+
nulls,
|
|
15
|
+
outlier_report,
|
|
16
|
+
)
|
|
17
|
+
from .stats import describe_plus, unique_report
|
|
18
|
+
from .plots import (
|
|
19
|
+
plot_categorical_counts,
|
|
20
|
+
plot_correlation,
|
|
21
|
+
plot_missing,
|
|
22
|
+
plot_numeric_distributions,
|
|
23
|
+
plot_target_relationship,
|
|
24
|
+
)
|
|
25
|
+
from .compare import check_merge_keys, compare_dfs
|
|
26
|
+
from .report import quick_report
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"overview",
|
|
32
|
+
"dtypes_report",
|
|
33
|
+
"get_numeric_cols",
|
|
34
|
+
"get_categorical_cols",
|
|
35
|
+
"get_datetime_cols",
|
|
36
|
+
"nulls",
|
|
37
|
+
"duplicates",
|
|
38
|
+
"constant_and_id_cols",
|
|
39
|
+
"inconsistent_categories",
|
|
40
|
+
"outlier_report",
|
|
41
|
+
"describe_plus",
|
|
42
|
+
"unique_report",
|
|
43
|
+
"plot_numeric_distributions",
|
|
44
|
+
"plot_categorical_counts",
|
|
45
|
+
"plot_correlation",
|
|
46
|
+
"plot_target_relationship",
|
|
47
|
+
"plot_missing",
|
|
48
|
+
"compare_dfs",
|
|
49
|
+
"check_merge_keys",
|
|
50
|
+
"quick_report",
|
|
51
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Two-dataframe comparison and merge-key diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compare_dfs(df1: pd.DataFrame, df2: pd.DataFrame) -> dict:
|
|
9
|
+
"""Shared/added/removed columns, dtype mismatches on shared columns, row-count diff."""
|
|
10
|
+
cols1, cols2 = set(df1.columns), set(df2.columns)
|
|
11
|
+
shared = sorted(cols1 & cols2)
|
|
12
|
+
|
|
13
|
+
dtype_mismatches = {
|
|
14
|
+
col: {"df1": str(df1[col].dtype), "df2": str(df2[col].dtype)}
|
|
15
|
+
for col in shared
|
|
16
|
+
if df1[col].dtype != df2[col].dtype
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
"shared_columns": shared,
|
|
21
|
+
"only_in_df1": sorted(cols1 - cols2),
|
|
22
|
+
"only_in_df2": sorted(cols2 - cols1),
|
|
23
|
+
"dtype_mismatches": dtype_mismatches,
|
|
24
|
+
"row_count_df1": len(df1),
|
|
25
|
+
"row_count_df2": len(df2),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_merge_keys(df1: pd.DataFrame, df2: pd.DataFrame, on: str | list) -> dict:
|
|
30
|
+
"""Diagnose a proposed join key: dtype mismatch, missing keys each side, duplicate keys each side."""
|
|
31
|
+
keys = [on] if isinstance(on, str) else list(on)
|
|
32
|
+
for key in keys:
|
|
33
|
+
if key not in df1.columns:
|
|
34
|
+
raise ValueError(f"'{key}' not found in df1.")
|
|
35
|
+
if key not in df2.columns:
|
|
36
|
+
raise ValueError(f"'{key}' not found in df2.")
|
|
37
|
+
|
|
38
|
+
dtype_mismatches = {
|
|
39
|
+
key: {"df1": str(df1[key].dtype), "df2": str(df2[key].dtype)}
|
|
40
|
+
for key in keys
|
|
41
|
+
if df1[key].dtype != df2[key].dtype
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
left_keys = df1[keys].drop_duplicates()
|
|
45
|
+
right_keys = df2[keys].drop_duplicates()
|
|
46
|
+
merged = left_keys.merge(right_keys, on=keys, how="outer", indicator=True)
|
|
47
|
+
|
|
48
|
+
missing_in_df2 = int((merged["_merge"] == "left_only").sum())
|
|
49
|
+
missing_in_df1 = int((merged["_merge"] == "right_only").sum())
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"keys": keys,
|
|
53
|
+
"dtype_mismatches": dtype_mismatches,
|
|
54
|
+
"unique_keys_missing_in_df2": missing_in_df2,
|
|
55
|
+
"unique_keys_missing_in_df2_pct": round(missing_in_df2 / len(left_keys) * 100, 2)
|
|
56
|
+
if len(left_keys)
|
|
57
|
+
else 0.0,
|
|
58
|
+
"unique_keys_missing_in_df1": missing_in_df1,
|
|
59
|
+
"unique_keys_missing_in_df1_pct": round(missing_in_df1 / len(right_keys) * 100, 2)
|
|
60
|
+
if len(right_keys)
|
|
61
|
+
else 0.0,
|
|
62
|
+
"duplicate_key_rows_df1": int(df1.duplicated(subset=keys).sum()),
|
|
63
|
+
"duplicate_key_rows_df2": int(df2.duplicated(subset=keys).sum()),
|
|
64
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Structural overview of a DataFrame: shape, dtypes, memory, column buckets."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def overview(df: pd.DataFrame) -> dict:
|
|
7
|
+
"""Shape, memory usage, and duplicate-row count in one dict."""
|
|
8
|
+
mem = df.memory_usage(deep=True)
|
|
9
|
+
return {
|
|
10
|
+
"rows": df.shape[0],
|
|
11
|
+
"columns": df.shape[1],
|
|
12
|
+
"duplicate_rows": int(df.duplicated().sum()),
|
|
13
|
+
"total_memory_mb": round(float(mem.sum()) / 1_000_000, 3),
|
|
14
|
+
"memory_by_column_mb": (mem.drop("Index", errors="ignore") / 1_000_000)
|
|
15
|
+
.round(4)
|
|
16
|
+
.sort_values(ascending=False)
|
|
17
|
+
.to_dict(),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dtypes_report(df: pd.DataFrame, high_cardinality_threshold: int = 50) -> dict:
|
|
22
|
+
"""Bucket columns into numeric / categorical / datetime / boolean / text."""
|
|
23
|
+
buckets = {
|
|
24
|
+
"numeric": [],
|
|
25
|
+
"categorical": [],
|
|
26
|
+
"datetime": [],
|
|
27
|
+
"boolean": [],
|
|
28
|
+
"text": [],
|
|
29
|
+
}
|
|
30
|
+
for col in df.columns:
|
|
31
|
+
series = df[col]
|
|
32
|
+
if pd.api.types.is_bool_dtype(series):
|
|
33
|
+
buckets["boolean"].append(col)
|
|
34
|
+
elif pd.api.types.is_datetime64_any_dtype(series):
|
|
35
|
+
buckets["datetime"].append(col)
|
|
36
|
+
elif pd.api.types.is_numeric_dtype(series):
|
|
37
|
+
buckets["numeric"].append(col)
|
|
38
|
+
elif series.nunique(dropna=True) <= high_cardinality_threshold:
|
|
39
|
+
buckets["categorical"].append(col)
|
|
40
|
+
else:
|
|
41
|
+
buckets["text"].append(col)
|
|
42
|
+
return buckets
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_numeric_cols(df: pd.DataFrame) -> list:
|
|
46
|
+
return dtypes_report(df)["numeric"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_categorical_cols(df: pd.DataFrame) -> list:
|
|
50
|
+
return dtypes_report(df)["categorical"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_datetime_cols(df: pd.DataFrame) -> list:
|
|
54
|
+
return dtypes_report(df)["datetime"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Auto-grid plotting helpers. Every function returns the matplotlib Figure it built."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .overview import get_categorical_cols, get_numeric_cols
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _grid_shape(n: int, max_cols: int = 3) -> tuple:
|
|
14
|
+
ncols = min(max_cols, n) or 1
|
|
15
|
+
nrows = math.ceil(n / ncols)
|
|
16
|
+
return nrows, ncols
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def plot_numeric_distributions(df: pd.DataFrame, columns: list | None = None, max_cols: int = 3):
|
|
20
|
+
"""Histogram + boxplot grid for every numeric column."""
|
|
21
|
+
cols = columns or get_numeric_cols(df)
|
|
22
|
+
if not cols:
|
|
23
|
+
raise ValueError("No numeric columns found to plot.")
|
|
24
|
+
|
|
25
|
+
nrows, ncols = _grid_shape(len(cols), max_cols)
|
|
26
|
+
fig, axes = plt.subplots(nrows, ncols * 2, figsize=(4.5 * ncols * 2, 3.5 * nrows))
|
|
27
|
+
axes = axes.reshape(nrows, ncols * 2)
|
|
28
|
+
|
|
29
|
+
for i, col in enumerate(cols):
|
|
30
|
+
r, c = divmod(i, ncols)
|
|
31
|
+
hist_ax, box_ax = axes[r, c * 2], axes[r, c * 2 + 1]
|
|
32
|
+
df[col].dropna().hist(ax=hist_ax, bins=30)
|
|
33
|
+
hist_ax.set_title(f"{col} (hist)")
|
|
34
|
+
box_ax.boxplot(df[col].dropna())
|
|
35
|
+
box_ax.set_title(f"{col} (box)")
|
|
36
|
+
|
|
37
|
+
for j in range(len(cols), nrows * ncols):
|
|
38
|
+
r, c = divmod(j, ncols)
|
|
39
|
+
axes[r, c * 2].axis("off")
|
|
40
|
+
axes[r, c * 2 + 1].axis("off")
|
|
41
|
+
|
|
42
|
+
fig.tight_layout()
|
|
43
|
+
return fig
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def plot_categorical_counts(df: pd.DataFrame, columns: list | None = None, top_n: int = 10, max_cols: int = 3):
|
|
47
|
+
"""Bar chart grid for every categorical column, capped at top_n categories."""
|
|
48
|
+
cols = columns or get_categorical_cols(df)
|
|
49
|
+
if not cols:
|
|
50
|
+
raise ValueError("No categorical columns found to plot.")
|
|
51
|
+
|
|
52
|
+
nrows, ncols = _grid_shape(len(cols), max_cols)
|
|
53
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 3.5 * nrows), squeeze=False)
|
|
54
|
+
|
|
55
|
+
for i, col in enumerate(cols):
|
|
56
|
+
r, c = divmod(i, ncols)
|
|
57
|
+
counts = df[col].value_counts().head(top_n)
|
|
58
|
+
counts.plot(kind="bar", ax=axes[r, c])
|
|
59
|
+
axes[r, c].set_title(col)
|
|
60
|
+
axes[r, c].tick_params(axis="x", rotation=45)
|
|
61
|
+
|
|
62
|
+
for j in range(len(cols), nrows * ncols):
|
|
63
|
+
r, c = divmod(j, ncols)
|
|
64
|
+
axes[r, c].axis("off")
|
|
65
|
+
|
|
66
|
+
fig.tight_layout()
|
|
67
|
+
return fig
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def plot_correlation(df: pd.DataFrame, method: str = "pearson", sample: int | None = None):
|
|
71
|
+
"""Masked correlation heatmap over numeric columns."""
|
|
72
|
+
import numpy as np
|
|
73
|
+
import seaborn as sns
|
|
74
|
+
|
|
75
|
+
data = df if sample is None or len(df) <= sample else df.sample(sample, random_state=0)
|
|
76
|
+
numeric_cols = get_numeric_cols(data)
|
|
77
|
+
if len(numeric_cols) < 2:
|
|
78
|
+
raise ValueError("Need at least 2 numeric columns to plot a correlation heatmap.")
|
|
79
|
+
|
|
80
|
+
corr = data[numeric_cols].corr(method=method)
|
|
81
|
+
mask = np.triu(np.ones_like(corr, dtype=bool))
|
|
82
|
+
|
|
83
|
+
fig, ax = plt.subplots(figsize=(0.8 * len(numeric_cols) + 2, 0.8 * len(numeric_cols) + 2))
|
|
84
|
+
sns.heatmap(corr, mask=mask, annot=True, fmt=".2f", cmap="coolwarm", center=0, ax=ax)
|
|
85
|
+
fig.tight_layout()
|
|
86
|
+
return fig
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def plot_target_relationship(df: pd.DataFrame, target: str, max_cols: int = 3):
|
|
90
|
+
"""Boxplot/violin (categorical feature) or scatter (numeric feature) vs. target."""
|
|
91
|
+
if target not in df.columns:
|
|
92
|
+
raise ValueError(f"'{target}' is not a column in the DataFrame.")
|
|
93
|
+
|
|
94
|
+
feature_cols = [c for c in df.columns if c != target]
|
|
95
|
+
numeric_target = pd.api.types.is_numeric_dtype(df[target])
|
|
96
|
+
|
|
97
|
+
nrows, ncols = _grid_shape(len(feature_cols), max_cols)
|
|
98
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 3.5 * nrows), squeeze=False)
|
|
99
|
+
|
|
100
|
+
for i, col in enumerate(feature_cols):
|
|
101
|
+
r, c = divmod(i, ncols)
|
|
102
|
+
ax = axes[r, c]
|
|
103
|
+
is_numeric_feature = pd.api.types.is_numeric_dtype(df[col])
|
|
104
|
+
|
|
105
|
+
if is_numeric_feature and numeric_target:
|
|
106
|
+
ax.scatter(df[col], df[target], alpha=0.4, s=10)
|
|
107
|
+
ax.set_xlabel(col)
|
|
108
|
+
ax.set_ylabel(target)
|
|
109
|
+
elif not is_numeric_feature and numeric_target:
|
|
110
|
+
df.boxplot(column=target, by=col, ax=ax)
|
|
111
|
+
ax.set_title(f"{target} by {col}")
|
|
112
|
+
elif is_numeric_feature and not numeric_target:
|
|
113
|
+
df.boxplot(column=col, by=target, ax=ax)
|
|
114
|
+
ax.set_title(f"{col} by {target}")
|
|
115
|
+
else:
|
|
116
|
+
pd.crosstab(df[col], df[target]).plot(kind="bar", stacked=True, ax=ax, legend=False)
|
|
117
|
+
ax.set_title(f"{col} vs {target}")
|
|
118
|
+
|
|
119
|
+
for j in range(len(feature_cols), nrows * ncols):
|
|
120
|
+
r, c = divmod(j, ncols)
|
|
121
|
+
axes[r, c].axis("off")
|
|
122
|
+
|
|
123
|
+
fig.suptitle("")
|
|
124
|
+
fig.tight_layout()
|
|
125
|
+
return fig
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def plot_missing(df: pd.DataFrame):
|
|
129
|
+
"""Heatmap of null locations across the DataFrame."""
|
|
130
|
+
import seaborn as sns
|
|
131
|
+
|
|
132
|
+
fig, ax = plt.subplots(figsize=(0.3 * len(df.columns) + 4, 5))
|
|
133
|
+
sns.heatmap(df.isna(), cbar=False, yticklabels=False, ax=ax)
|
|
134
|
+
ax.set_title("Missing value pattern")
|
|
135
|
+
fig.tight_layout()
|
|
136
|
+
return fig
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Data-quality checks: nulls, duplicates, constant/ID columns, messy categories, outliers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .overview import get_numeric_cols
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def nulls(df: pd.DataFrame) -> pd.DataFrame:
|
|
13
|
+
"""% missing per column, sorted descending, nulls-only columns."""
|
|
14
|
+
counts = df.isna().sum()
|
|
15
|
+
counts = counts[counts > 0]
|
|
16
|
+
if counts.empty:
|
|
17
|
+
return pd.DataFrame(columns=["null_count", "null_pct"])
|
|
18
|
+
pct = (counts / len(df) * 100).round(2)
|
|
19
|
+
result = pd.DataFrame({"null_count": counts, "null_pct": pct})
|
|
20
|
+
return result.sort_values("null_pct", ascending=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def duplicates(df: pd.DataFrame, subset: list | None = None) -> dict:
|
|
24
|
+
"""Count and preview of duplicate rows, optionally restricted to a column subset."""
|
|
25
|
+
mask = df.duplicated(subset=subset, keep=False)
|
|
26
|
+
dup_rows = df[mask]
|
|
27
|
+
return {
|
|
28
|
+
"duplicate_row_count": int(df.duplicated(subset=subset).sum()),
|
|
29
|
+
"duplicate_pct": round(df.duplicated(subset=subset).sum() / len(df) * 100, 2)
|
|
30
|
+
if len(df)
|
|
31
|
+
else 0.0,
|
|
32
|
+
"preview": dup_rows.head(10),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def constant_and_id_cols(df: pd.DataFrame) -> dict:
|
|
37
|
+
"""Flag constant columns (nunique==1) and likely-ID columns (nunique==len(df))."""
|
|
38
|
+
n = len(df)
|
|
39
|
+
constant_cols, id_cols = [], []
|
|
40
|
+
for col in df.columns:
|
|
41
|
+
nunique = df[col].nunique(dropna=False)
|
|
42
|
+
if nunique <= 1:
|
|
43
|
+
constant_cols.append(col)
|
|
44
|
+
elif n > 0 and nunique == n:
|
|
45
|
+
id_cols.append(col)
|
|
46
|
+
return {"constant_columns": constant_cols, "likely_id_columns": id_cols}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize(value: str) -> str:
|
|
50
|
+
value = value.strip().lower()
|
|
51
|
+
return re.sub(r"[^a-z0-9]", "", value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inconsistent_categories(df: pd.DataFrame, columns: list | None = None) -> dict:
|
|
55
|
+
"""Flag values that likely represent the same category due to case/whitespace/punctuation."""
|
|
56
|
+
target_cols = columns or [
|
|
57
|
+
c for c in df.columns if df[c].dtype == object or str(df[c].dtype) == "category"
|
|
58
|
+
]
|
|
59
|
+
findings = {}
|
|
60
|
+
for col in target_cols:
|
|
61
|
+
values = df[col].dropna().astype(str).unique()
|
|
62
|
+
groups: dict[str, list] = {}
|
|
63
|
+
for v in values:
|
|
64
|
+
key = _normalize(v)
|
|
65
|
+
groups.setdefault(key, []).append(v)
|
|
66
|
+
messy = {k: sorted(v) for k, v in groups.items() if len(v) > 1}
|
|
67
|
+
if messy:
|
|
68
|
+
findings[col] = messy
|
|
69
|
+
return findings
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def outlier_report(df: pd.DataFrame, method: str = "iqr") -> pd.DataFrame:
|
|
73
|
+
"""Per numeric column, count/percentage of outliers via IQR or z-score."""
|
|
74
|
+
if method not in {"iqr", "zscore"}:
|
|
75
|
+
raise ValueError("method must be 'iqr' or 'zscore'")
|
|
76
|
+
|
|
77
|
+
rows = []
|
|
78
|
+
for col in get_numeric_cols(df):
|
|
79
|
+
series = df[col].dropna()
|
|
80
|
+
if series.empty:
|
|
81
|
+
continue
|
|
82
|
+
if method == "iqr":
|
|
83
|
+
q1, q3 = series.quantile(0.25), series.quantile(0.75)
|
|
84
|
+
iqr = q3 - q1
|
|
85
|
+
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
|
|
86
|
+
outlier_mask = (series < lower) | (series > upper)
|
|
87
|
+
else:
|
|
88
|
+
std = series.std()
|
|
89
|
+
if std == 0 or pd.isna(std):
|
|
90
|
+
outlier_mask = pd.Series(False, index=series.index)
|
|
91
|
+
else:
|
|
92
|
+
z = (series - series.mean()) / std
|
|
93
|
+
outlier_mask = z.abs() > 3
|
|
94
|
+
|
|
95
|
+
count = int(outlier_mask.sum())
|
|
96
|
+
rows.append(
|
|
97
|
+
{
|
|
98
|
+
"column": col,
|
|
99
|
+
"outlier_count": count,
|
|
100
|
+
"outlier_pct": round(count / len(series) * 100, 2),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return pd.DataFrame(rows).sort_values("outlier_pct", ascending=False).reset_index(drop=True)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""One guided function that chains the atomic checks in a sensible order."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .overview import dtypes_report, overview
|
|
6
|
+
from .quality import constant_and_id_cols, duplicates, nulls
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from IPython.display import display
|
|
10
|
+
|
|
11
|
+
_IN_NOTEBOOK = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
_IN_NOTEBOOK = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _show(label, value):
|
|
17
|
+
print(f"\n--- {label} ---")
|
|
18
|
+
if _IN_NOTEBOOK and isinstance(value, pd.DataFrame):
|
|
19
|
+
display(value)
|
|
20
|
+
else:
|
|
21
|
+
print(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def quick_report(df: pd.DataFrame, plot: bool = True) -> dict:
|
|
25
|
+
"""Run overview, nulls, dtypes, duplicates, and constant/ID checks in one call.
|
|
26
|
+
|
|
27
|
+
Prints a readable summary and returns everything as a dict so each
|
|
28
|
+
piece stays usable on its own.
|
|
29
|
+
"""
|
|
30
|
+
result = {
|
|
31
|
+
"overview": overview(df),
|
|
32
|
+
"nulls": nulls(df),
|
|
33
|
+
"dtypes": dtypes_report(df),
|
|
34
|
+
"duplicates": duplicates(df),
|
|
35
|
+
"constant_and_id": constant_and_id_cols(df),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_show("Overview", result["overview"])
|
|
39
|
+
_show("Nulls", result["nulls"] if not result["nulls"].empty else "No missing values.")
|
|
40
|
+
_show("Column types", result["dtypes"])
|
|
41
|
+
_show("Duplicate rows", result["duplicates"]["duplicate_row_count"])
|
|
42
|
+
_show("Constant / likely-ID columns", result["constant_and_id"])
|
|
43
|
+
|
|
44
|
+
if plot:
|
|
45
|
+
from .plots import plot_numeric_distributions
|
|
46
|
+
|
|
47
|
+
numeric_cols = result["dtypes"]["numeric"]
|
|
48
|
+
if numeric_cols:
|
|
49
|
+
result["numeric_distributions_fig"] = plot_numeric_distributions(df, columns=numeric_cols)
|
|
50
|
+
|
|
51
|
+
return result
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Summary statistics beyond plain .describe()."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .overview import get_numeric_cols
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def describe_plus(df: pd.DataFrame) -> pd.DataFrame:
|
|
9
|
+
""".describe() plus null %, skew, kurtosis, and dtype, for numeric columns."""
|
|
10
|
+
numeric_cols = get_numeric_cols(df)
|
|
11
|
+
if not numeric_cols:
|
|
12
|
+
return pd.DataFrame()
|
|
13
|
+
|
|
14
|
+
base = df[numeric_cols].describe().T
|
|
15
|
+
base["null_pct"] = (df[numeric_cols].isna().sum() / len(df) * 100).round(2)
|
|
16
|
+
base["skew"] = df[numeric_cols].skew()
|
|
17
|
+
base["kurtosis"] = df[numeric_cols].kurt()
|
|
18
|
+
base["dtype"] = df[numeric_cols].dtypes.astype(str)
|
|
19
|
+
return base
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def unique_report(df: pd.DataFrame, examples: int = 3) -> pd.DataFrame:
|
|
23
|
+
"""nunique + example values per column."""
|
|
24
|
+
rows = []
|
|
25
|
+
for col in df.columns:
|
|
26
|
+
series = df[col]
|
|
27
|
+
sample = series.dropna().unique()[:examples]
|
|
28
|
+
rows.append(
|
|
29
|
+
{
|
|
30
|
+
"column": col,
|
|
31
|
+
"nunique": series.nunique(dropna=True),
|
|
32
|
+
"examples": list(sample),
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
return pd.DataFrame(rows).sort_values("nunique", ascending=False).reset_index(drop=True)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edasnap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fast, honest first look at any pandas DataFrame — one-liner EDA helpers, no bloat.
|
|
5
|
+
Author: sivaraam.kr
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sivaraam-kr/edasnap
|
|
8
|
+
Project-URL: Issues, https://github.com/sivaraam-kr/edasnap/issues
|
|
9
|
+
Keywords: eda,exploratory-data-analysis,pandas,data-science,data-quality
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pandas>=1.3
|
|
24
|
+
Requires-Dist: matplotlib>=3.4
|
|
25
|
+
Requires-Dist: seaborn>=0.11
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# edasnap
|
|
33
|
+
|
|
34
|
+
**A fast, honest first look at any pandas DataFrame — in one line each.**
|
|
35
|
+
|
|
36
|
+
`edasnap` answers the questions you ask every single time you load a new
|
|
37
|
+
dataset: What's the shape? What's null, and how much? What are my numeric
|
|
38
|
+
vs. categorical columns? Are there duplicate rows, constant columns, or
|
|
39
|
+
columns that are secretly IDs? Do `"USA"`, `"usa "`, and `"U.S.A"` need to
|
|
40
|
+
be merged into one category? Will this merge silently blow up my row count?
|
|
41
|
+
|
|
42
|
+
It's built to be the opposite of a giant auto-generated HTML report:
|
|
43
|
+
|
|
44
|
+
- **One function, one job.** No monolithic `report()` that dumps 40 sections
|
|
45
|
+
you didn't ask for — call only what you need.
|
|
46
|
+
- **Every function returns real data** (a `dict`, a `DataFrame`, a
|
|
47
|
+
matplotlib `Figure`) — never just a printout you can't reuse.
|
|
48
|
+
- **Never mutates your DataFrame.** Everything is read-only analysis.
|
|
49
|
+
- **Small on purpose.** Just pandas EDA — no bundled ML training, no
|
|
50
|
+
encoding pipelines, no computer vision, no LLM calls. It's a lens on your
|
|
51
|
+
data, not a pipeline.
|
|
52
|
+
- **Doesn't crash on messy real data** — mixed types, weird strings, and
|
|
53
|
+
edge cases (empty df, all-null column, single row) are handled, not
|
|
54
|
+
fatal.
|
|
55
|
+
- **Readable source.** Every function is short enough to read in ten
|
|
56
|
+
seconds if you're curious what it's actually doing — handy if you're
|
|
57
|
+
still learning pandas.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install edasnap
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import pandas as pd
|
|
69
|
+
import edasnap as es
|
|
70
|
+
|
|
71
|
+
df = pd.read_csv("data.csv")
|
|
72
|
+
|
|
73
|
+
es.quick_report(df) # runs the core checks below, in a sensible order
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## All functions
|
|
77
|
+
|
|
78
|
+
### Structure & column types
|
|
79
|
+
|
|
80
|
+
| Function | What it tells you |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `es.overview(df)` | Row/column count, total + per-column memory usage, duplicate row count |
|
|
83
|
+
| `es.dtypes_report(df)` | Buckets every column into `numeric`, `categorical`, `datetime`, `boolean`, or `text` |
|
|
84
|
+
| `es.get_numeric_cols(df)` | List of numeric column names — ready to hand to a plot or model |
|
|
85
|
+
| `es.get_categorical_cols(df)` | List of categorical column names |
|
|
86
|
+
| `es.get_datetime_cols(df)` | List of datetime column names |
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
es.overview(df)
|
|
90
|
+
# {'rows': 891, 'columns': 12, 'duplicate_rows': 0,
|
|
91
|
+
# 'total_memory_mb': 0.29, 'memory_by_column_mb': {...}}
|
|
92
|
+
|
|
93
|
+
es.dtypes_report(df)
|
|
94
|
+
# {'numeric': ['Age', 'Fare'], 'categorical': ['Sex', 'Embarked'], ...}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Data quality
|
|
98
|
+
|
|
99
|
+
| Function | What it tells you |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `es.nulls(df)` | % missing per column, worst first — only columns that actually have nulls |
|
|
102
|
+
| `es.duplicates(df, subset=None)` | Duplicate row count, % of the dataset, and a preview |
|
|
103
|
+
| `es.constant_and_id_cols(df)` | Columns with only one value (dead weight), and columns that are likely IDs (`nunique == len(df)`) — both are common "don't feed this to a model" traps |
|
|
104
|
+
| `es.inconsistent_categories(df)` | Flags values in text columns that are probably the same category typed differently — `"USA"` / `"usa "` / `"U.S.A"` — a check the bigger EDA tools skip |
|
|
105
|
+
| `es.outlier_report(df, method="iqr")` | Outlier count and % per numeric column, via IQR or z-score — as a table, no plot needed |
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
es.nulls(df)
|
|
109
|
+
# null_count null_pct
|
|
110
|
+
# Cabin 687 77.10
|
|
111
|
+
# Age 177 19.87
|
|
112
|
+
|
|
113
|
+
es.inconsistent_categories(df)
|
|
114
|
+
# {'country': {'usa': ['USA', 'usa ', 'U.S.A'], 'uk': ['UK', 'uk']}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Summary stats
|
|
118
|
+
|
|
119
|
+
| Function | What it tells you |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `es.describe_plus(df)` | `.describe()` plus null %, skew, kurtosis, and dtype in one table |
|
|
122
|
+
| `es.unique_report(df)` | `nunique` + a few example values per column |
|
|
123
|
+
|
|
124
|
+
### Plots (each returns the matplotlib `Figure`)
|
|
125
|
+
|
|
126
|
+
| Function | What it draws |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `es.plot_numeric_distributions(df)` | Histogram + boxplot grid, one pair per numeric column |
|
|
129
|
+
| `es.plot_categorical_counts(df, top_n=10)` | Bar chart grid per categorical column, capped at the top N values |
|
|
130
|
+
| `es.plot_correlation(df)` | Masked correlation heatmap over numeric columns |
|
|
131
|
+
| `es.plot_target_relationship(df, target="price")` | Auto-picks boxplot/violin/scatter per feature against your target column |
|
|
132
|
+
| `es.plot_missing(df)` | Heatmap of where nulls occur across the whole DataFrame |
|
|
133
|
+
|
|
134
|
+
### Comparing two DataFrames
|
|
135
|
+
|
|
136
|
+
| Function | What it tells you |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `es.compare_dfs(train_df, test_df)` | Shared/added/removed columns and dtype mismatches between two DataFrames — e.g. train vs. test drift |
|
|
139
|
+
| `es.check_merge_keys(orders_df, users_df, on="user_id")` | Before you `merge()`: dtype mismatch on the key, % of keys missing on each side, and duplicate-key counts on each side — the usual cause of a merge silently multiplying your row count |
|
|
140
|
+
|
|
141
|
+
### The one convenience function
|
|
142
|
+
|
|
143
|
+
| Function | What it does |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `es.quick_report(df)` | Chains `overview` → `nulls` → `dtypes_report` → `duplicates` → `constant_and_id_cols` → a numeric-distribution plot, prints a readable summary, and returns everything as a dict. Every piece it calls also works standalone. |
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/sivaraam-kr/edasnap
|
|
151
|
+
cd edasnap
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Publishing
|
|
157
|
+
|
|
158
|
+
See [PUBLISHING.md](PUBLISHING.md) for the release checklist.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/edasnap/__init__.py
|
|
5
|
+
src/edasnap/compare.py
|
|
6
|
+
src/edasnap/overview.py
|
|
7
|
+
src/edasnap/plots.py
|
|
8
|
+
src/edasnap/quality.py
|
|
9
|
+
src/edasnap/report.py
|
|
10
|
+
src/edasnap/stats.py
|
|
11
|
+
src/edasnap.egg-info/PKG-INFO
|
|
12
|
+
src/edasnap.egg-info/SOURCES.txt
|
|
13
|
+
src/edasnap.egg-info/dependency_links.txt
|
|
14
|
+
src/edasnap.egg-info/requires.txt
|
|
15
|
+
src/edasnap.egg-info/top_level.txt
|
|
16
|
+
tests/test_compare.py
|
|
17
|
+
tests/test_overview.py
|
|
18
|
+
tests/test_plots.py
|
|
19
|
+
tests/test_quality.py
|
|
20
|
+
tests/test_report.py
|
|
21
|
+
tests/test_stats.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
edasnap
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import edasnap as es
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_compare_dfs_columns():
|
|
7
|
+
df1 = pd.DataFrame({"a": [1], "b": [2]})
|
|
8
|
+
df2 = pd.DataFrame({"a": [1], "c": [3]})
|
|
9
|
+
result = es.compare_dfs(df1, df2)
|
|
10
|
+
assert result["shared_columns"] == ["a"]
|
|
11
|
+
assert result["only_in_df1"] == ["b"]
|
|
12
|
+
assert result["only_in_df2"] == ["c"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_compare_dfs_dtype_mismatch():
|
|
16
|
+
df1 = pd.DataFrame({"a": [1, 2]})
|
|
17
|
+
df2 = pd.DataFrame({"a": ["1", "2"]})
|
|
18
|
+
result = es.compare_dfs(df1, df2)
|
|
19
|
+
assert "a" in result["dtype_mismatches"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_check_merge_keys_detects_missing_and_duplicates():
|
|
23
|
+
df1 = pd.DataFrame({"id": [1, 2, 2, 3]})
|
|
24
|
+
df2 = pd.DataFrame({"id": [2, 3, 4]})
|
|
25
|
+
result = es.check_merge_keys(df1, df2, on="id")
|
|
26
|
+
assert result["unique_keys_missing_in_df2"] == 1
|
|
27
|
+
assert result["unique_keys_missing_in_df1"] == 1
|
|
28
|
+
assert result["duplicate_key_rows_df1"] == 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_check_merge_keys_missing_column_raises():
|
|
32
|
+
df1 = pd.DataFrame({"id": [1]})
|
|
33
|
+
df2 = pd.DataFrame({"other": [1]})
|
|
34
|
+
try:
|
|
35
|
+
es.check_merge_keys(df1, df2, on="id")
|
|
36
|
+
assert False, "expected ValueError"
|
|
37
|
+
except ValueError:
|
|
38
|
+
pass
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import edasnap as es
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_overview_basic(sample_df):
|
|
7
|
+
result = es.overview(sample_df)
|
|
8
|
+
assert result["rows"] == 10
|
|
9
|
+
assert result["columns"] == 6
|
|
10
|
+
assert result["duplicate_rows"] == 0
|
|
11
|
+
assert result["total_memory_mb"] > 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_overview_empty(empty_df):
|
|
15
|
+
result = es.overview(empty_df)
|
|
16
|
+
assert result["rows"] == 0
|
|
17
|
+
assert result["columns"] == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_dtypes_report_buckets(sample_df):
|
|
21
|
+
report = es.dtypes_report(sample_df)
|
|
22
|
+
assert "age" in report["numeric"]
|
|
23
|
+
assert "score" in report["numeric"]
|
|
24
|
+
assert "flag" in report["boolean"]
|
|
25
|
+
assert "country" in report["categorical"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_get_numeric_cols(sample_df):
|
|
29
|
+
assert set(es.get_numeric_cols(sample_df)) >= {"id", "age", "score"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_get_categorical_cols(sample_df):
|
|
33
|
+
assert "country" in es.get_categorical_cols(sample_df)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_dtypes_report_datetime():
|
|
37
|
+
df = pd.DataFrame({"d": pd.date_range("2024-01-01", periods=5)})
|
|
38
|
+
assert es.get_datetime_cols(df) == ["d"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import matplotlib
|
|
2
|
+
|
|
3
|
+
matplotlib.use("Agg")
|
|
4
|
+
|
|
5
|
+
import edasnap as es
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_plot_numeric_distributions_returns_figure(sample_df):
|
|
9
|
+
fig = es.plot_numeric_distributions(sample_df)
|
|
10
|
+
assert fig is not None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_plot_categorical_counts_returns_figure(sample_df):
|
|
14
|
+
fig = es.plot_categorical_counts(sample_df)
|
|
15
|
+
assert fig is not None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_plot_correlation_returns_figure(sample_df):
|
|
19
|
+
fig = es.plot_correlation(sample_df)
|
|
20
|
+
assert fig is not None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_plot_target_relationship_numeric_target(sample_df):
|
|
24
|
+
fig = es.plot_target_relationship(sample_df.drop(columns=["constant"]), target="score")
|
|
25
|
+
assert fig is not None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_plot_missing_returns_figure(sample_df):
|
|
29
|
+
fig = es.plot_missing(sample_df)
|
|
30
|
+
assert fig is not None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import edasnap as es
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_nulls_reports_only_null_columns(sample_df):
|
|
7
|
+
result = es.nulls(sample_df)
|
|
8
|
+
assert set(result.index) == {"age", "score"}
|
|
9
|
+
assert result.loc["age", "null_count"] == 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_nulls_empty_when_no_nulls():
|
|
13
|
+
df = pd.DataFrame({"a": [1, 2, 3]})
|
|
14
|
+
result = es.nulls(df)
|
|
15
|
+
assert result.empty
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_duplicates_counts_correctly():
|
|
19
|
+
df = pd.DataFrame({"a": [1, 1, 2, 3], "b": [1, 1, 2, 3]})
|
|
20
|
+
result = es.duplicates(df)
|
|
21
|
+
assert result["duplicate_row_count"] == 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_duplicates_on_empty_df(empty_df):
|
|
25
|
+
result = es.duplicates(empty_df)
|
|
26
|
+
assert result["duplicate_row_count"] == 0
|
|
27
|
+
assert result["duplicate_pct"] == 0.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_constant_and_id_cols(sample_df):
|
|
31
|
+
result = es.constant_and_id_cols(sample_df)
|
|
32
|
+
assert "constant" in result["constant_columns"]
|
|
33
|
+
assert "id" in result["likely_id_columns"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_inconsistent_categories_groups_variants(sample_df):
|
|
37
|
+
findings = es.inconsistent_categories(sample_df, columns=["country"])
|
|
38
|
+
assert "country" in findings
|
|
39
|
+
groups = findings["country"]
|
|
40
|
+
usa_group = [g for g in groups.values() if "USA" in g][0]
|
|
41
|
+
assert set(usa_group) == {"USA", "usa ", "U.S.A"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_inconsistent_categories_no_false_positive():
|
|
45
|
+
df = pd.DataFrame({"c": ["apple", "banana", "cherry"]})
|
|
46
|
+
findings = es.inconsistent_categories(df)
|
|
47
|
+
assert findings == {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_outlier_report_iqr(sample_df):
|
|
51
|
+
result = es.outlier_report(sample_df, method="iqr")
|
|
52
|
+
row = result[result["column"] == "age"].iloc[0]
|
|
53
|
+
assert row["outlier_count"] >= 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_outlier_report_invalid_method(sample_df):
|
|
57
|
+
try:
|
|
58
|
+
es.outlier_report(sample_df, method="bogus")
|
|
59
|
+
assert False, "expected ValueError"
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_quality_functions_handle_messy_data(messy_df):
|
|
65
|
+
es.nulls(messy_df)
|
|
66
|
+
es.duplicates(messy_df)
|
|
67
|
+
es.constant_and_id_cols(messy_df)
|
|
68
|
+
es.inconsistent_categories(messy_df)
|
|
69
|
+
es.outlier_report(messy_df)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import matplotlib
|
|
2
|
+
|
|
3
|
+
matplotlib.use("Agg")
|
|
4
|
+
|
|
5
|
+
import edasnap as es
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_quick_report_returns_all_pieces(sample_df):
|
|
9
|
+
result = es.quick_report(sample_df)
|
|
10
|
+
assert "overview" in result
|
|
11
|
+
assert "nulls" in result
|
|
12
|
+
assert "dtypes" in result
|
|
13
|
+
assert "duplicates" in result
|
|
14
|
+
assert "constant_and_id" in result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_quick_report_without_plot(sample_df):
|
|
18
|
+
result = es.quick_report(sample_df, plot=False)
|
|
19
|
+
assert "numeric_distributions_fig" not in result
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import edasnap as es
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_describe_plus_has_expected_columns(sample_df):
|
|
7
|
+
result = es.describe_plus(sample_df)
|
|
8
|
+
for col in ["null_pct", "skew", "kurtosis", "dtype"]:
|
|
9
|
+
assert col in result.columns
|
|
10
|
+
assert "age" in result.index
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_describe_plus_no_numeric_cols():
|
|
14
|
+
df = pd.DataFrame({"c": ["a", "b"]})
|
|
15
|
+
result = es.describe_plus(df)
|
|
16
|
+
assert result.empty
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_unique_report(sample_df):
|
|
20
|
+
result = es.unique_report(sample_df)
|
|
21
|
+
row = result[result["column"] == "constant"].iloc[0]
|
|
22
|
+
assert row["nunique"] == 1
|