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 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
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,8 @@
1
+ pandas>=1.3
2
+ matplotlib>=3.4
3
+ seaborn>=0.11
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ build
8
+ twine
@@ -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