se-dat 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.
se_dat-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: se-dat
3
+ Version: 0.1.0
4
+ Summary: Simple Exploratory Data Analysis tool: column profiling, correlation analysis, and encoding suggestions.
5
+ Author-email: Haim Feldman <haimfeld@gmail.com>
6
+ License: MIT
7
+ Keywords: eda,data-analysis,pandas,correlation,encoding
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: numpy>=1.23
14
+ Requires-Dist: pandas>=1.5
15
+ Requires-Dist: scipy>=1.9
16
+ Requires-Dist: matplotlib>=3.6
17
+ Requires-Dist: seaborn>=0.12
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7; extra == "dev"
20
+ Requires-Dist: pytest-cov; extra == "dev"
21
+
22
+ # se-dat
23
+
24
+ Simple Exploratory Data Analysis tool. Profiles your columns, measures
25
+ associations across numeric/categorical data, and suggests encodings so you can
26
+ move from raw data to a model-ready frame in one call.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install -e .
32
+ ```
33
+
34
+ ## Features
35
+
36
+ 1. **Column type profiling** — per column: inferred type (`numeric`, `boolean`,
37
+ `categorical`, `string`, `datetime`, `id`), missing %, unique count,
38
+ cardinality ratio, and a confidence flag when the inference is ambiguous
39
+ (numeric-looking strings, `0/1` vs `yes`/`no` booleans, etc.).
40
+ 2. **Correlation analysis** — Pearson/Spearman matrices + heatmap
41
+ (numeric-numeric), Cramér's V (categorical-categorical), correlation ratio
42
+ eta (numeric-categorical), plus multicollinearity flags above a threshold.
43
+ 3. **Encoding suggestions + auto-transform** — detects binary-like strings
44
+ (`yes`/`no`, `true`/`false`, `0/1`), suggests one-hot for low-cardinality
45
+ categoricals, and target/ordinal encoding for high-cardinality ones (with a
46
+ dimensionality warning). Apply suggestions individually or all at once.
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import pandas as pd
52
+ import sedat
53
+
54
+ df = pd.DataFrame({
55
+ "id": range(500),
56
+ "age": ...,
57
+ "income": ["45000", "67000", ...], # numeric stored as string
58
+ "smoker": ["yes", "no", ...], # binary-like
59
+ "region": ["north", "south", ...],
60
+ "target": ..., # numeric target
61
+ })
62
+
63
+ report = sedat.EDAReport.create(df, target=df["target"])
64
+ print(report.profile.summary) # column type profiling
65
+ print(report.correlations.flagged_pairs) # multicollinearity warnings
66
+ print(report.encoding_summary) # suggested encodings
67
+
68
+ model_ready = report.apply_all_encodings() # apply every suggestion
69
+ ```
70
+
71
+ ### Granular APIs
72
+
73
+ ```python
74
+ profile = sedat.profile_dataframe(df)
75
+ pearson = sedat.numeric_correlation(df, method="pearson")
76
+ cramers = sedat.categorical_correlation(df) # Cramér's V matrix
77
+ eta = sedat.numeric_categorical_correlation(df) # correlation ratio matrix
78
+ fig = sedat.correlation_heatmap(pearson, title="Numeric correlations")
79
+
80
+ plan = sedat.suggest_encodings(df, target=df["target"], cardinality_threshold=10)
81
+ only_smoker = next(s for s in plan.suggestions if s.column == "smoker")
82
+ one_hot_step = only_smoker.apply(df) # accept one suggestion
83
+ all_encoded = plan.apply_all(df) # accept all suggestions
84
+ ```
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pip install -e ".[dev]"
90
+ python -m pytest
91
+ ```
se_dat-0.1.0/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # se-dat
2
+
3
+ Simple Exploratory Data Analysis tool. Profiles your columns, measures
4
+ associations across numeric/categorical data, and suggests encodings so you can
5
+ move from raw data to a model-ready frame in one call.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install -e .
11
+ ```
12
+
13
+ ## Features
14
+
15
+ 1. **Column type profiling** — per column: inferred type (`numeric`, `boolean`,
16
+ `categorical`, `string`, `datetime`, `id`), missing %, unique count,
17
+ cardinality ratio, and a confidence flag when the inference is ambiguous
18
+ (numeric-looking strings, `0/1` vs `yes`/`no` booleans, etc.).
19
+ 2. **Correlation analysis** — Pearson/Spearman matrices + heatmap
20
+ (numeric-numeric), Cramér's V (categorical-categorical), correlation ratio
21
+ eta (numeric-categorical), plus multicollinearity flags above a threshold.
22
+ 3. **Encoding suggestions + auto-transform** — detects binary-like strings
23
+ (`yes`/`no`, `true`/`false`, `0/1`), suggests one-hot for low-cardinality
24
+ categoricals, and target/ordinal encoding for high-cardinality ones (with a
25
+ dimensionality warning). Apply suggestions individually or all at once.
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import pandas as pd
31
+ import sedat
32
+
33
+ df = pd.DataFrame({
34
+ "id": range(500),
35
+ "age": ...,
36
+ "income": ["45000", "67000", ...], # numeric stored as string
37
+ "smoker": ["yes", "no", ...], # binary-like
38
+ "region": ["north", "south", ...],
39
+ "target": ..., # numeric target
40
+ })
41
+
42
+ report = sedat.EDAReport.create(df, target=df["target"])
43
+ print(report.profile.summary) # column type profiling
44
+ print(report.correlations.flagged_pairs) # multicollinearity warnings
45
+ print(report.encoding_summary) # suggested encodings
46
+
47
+ model_ready = report.apply_all_encodings() # apply every suggestion
48
+ ```
49
+
50
+ ### Granular APIs
51
+
52
+ ```python
53
+ profile = sedat.profile_dataframe(df)
54
+ pearson = sedat.numeric_correlation(df, method="pearson")
55
+ cramers = sedat.categorical_correlation(df) # Cramér's V matrix
56
+ eta = sedat.numeric_categorical_correlation(df) # correlation ratio matrix
57
+ fig = sedat.correlation_heatmap(pearson, title="Numeric correlations")
58
+
59
+ plan = sedat.suggest_encodings(df, target=df["target"], cardinality_threshold=10)
60
+ only_smoker = next(s for s in plan.suggestions if s.column == "smoker")
61
+ one_hot_step = only_smoker.apply(df) # accept one suggestion
62
+ all_encoded = plan.apply_all(df) # accept all suggestions
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ pip install -e ".[dev]"
69
+ python -m pytest
70
+ ```
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "se-dat"
7
+ version = "0.1.0"
8
+ description = "Simple Exploratory Data Analysis tool: column profiling, correlation analysis, and encoding suggestions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Haim Feldman", email = "haimfeld@gmail.com" }]
13
+ keywords = ["eda", "data-analysis", "pandas", "correlation", "encoding"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "numpy>=1.23",
21
+ "pandas>=1.5",
22
+ "scipy>=1.9",
23
+ "matplotlib>=3.6",
24
+ "seaborn>=0.12",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=7", "pytest-cov"]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
se_dat-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: se-dat
3
+ Version: 0.1.0
4
+ Summary: Simple Exploratory Data Analysis tool: column profiling, correlation analysis, and encoding suggestions.
5
+ Author-email: Haim Feldman <haimfeld@gmail.com>
6
+ License: MIT
7
+ Keywords: eda,data-analysis,pandas,correlation,encoding
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: numpy>=1.23
14
+ Requires-Dist: pandas>=1.5
15
+ Requires-Dist: scipy>=1.9
16
+ Requires-Dist: matplotlib>=3.6
17
+ Requires-Dist: seaborn>=0.12
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7; extra == "dev"
20
+ Requires-Dist: pytest-cov; extra == "dev"
21
+
22
+ # se-dat
23
+
24
+ Simple Exploratory Data Analysis tool. Profiles your columns, measures
25
+ associations across numeric/categorical data, and suggests encodings so you can
26
+ move from raw data to a model-ready frame in one call.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install -e .
32
+ ```
33
+
34
+ ## Features
35
+
36
+ 1. **Column type profiling** — per column: inferred type (`numeric`, `boolean`,
37
+ `categorical`, `string`, `datetime`, `id`), missing %, unique count,
38
+ cardinality ratio, and a confidence flag when the inference is ambiguous
39
+ (numeric-looking strings, `0/1` vs `yes`/`no` booleans, etc.).
40
+ 2. **Correlation analysis** — Pearson/Spearman matrices + heatmap
41
+ (numeric-numeric), Cramér's V (categorical-categorical), correlation ratio
42
+ eta (numeric-categorical), plus multicollinearity flags above a threshold.
43
+ 3. **Encoding suggestions + auto-transform** — detects binary-like strings
44
+ (`yes`/`no`, `true`/`false`, `0/1`), suggests one-hot for low-cardinality
45
+ categoricals, and target/ordinal encoding for high-cardinality ones (with a
46
+ dimensionality warning). Apply suggestions individually or all at once.
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import pandas as pd
52
+ import sedat
53
+
54
+ df = pd.DataFrame({
55
+ "id": range(500),
56
+ "age": ...,
57
+ "income": ["45000", "67000", ...], # numeric stored as string
58
+ "smoker": ["yes", "no", ...], # binary-like
59
+ "region": ["north", "south", ...],
60
+ "target": ..., # numeric target
61
+ })
62
+
63
+ report = sedat.EDAReport.create(df, target=df["target"])
64
+ print(report.profile.summary) # column type profiling
65
+ print(report.correlations.flagged_pairs) # multicollinearity warnings
66
+ print(report.encoding_summary) # suggested encodings
67
+
68
+ model_ready = report.apply_all_encodings() # apply every suggestion
69
+ ```
70
+
71
+ ### Granular APIs
72
+
73
+ ```python
74
+ profile = sedat.profile_dataframe(df)
75
+ pearson = sedat.numeric_correlation(df, method="pearson")
76
+ cramers = sedat.categorical_correlation(df) # Cramér's V matrix
77
+ eta = sedat.numeric_categorical_correlation(df) # correlation ratio matrix
78
+ fig = sedat.correlation_heatmap(pearson, title="Numeric correlations")
79
+
80
+ plan = sedat.suggest_encodings(df, target=df["target"], cardinality_threshold=10)
81
+ only_smoker = next(s for s in plan.suggestions if s.column == "smoker")
82
+ one_hot_step = only_smoker.apply(df) # accept one suggestion
83
+ all_encoded = plan.apply_all(df) # accept all suggestions
84
+ ```
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pip install -e ".[dev]"
90
+ python -m pytest
91
+ ```
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/se_dat.egg-info/PKG-INFO
4
+ src/se_dat.egg-info/SOURCES.txt
5
+ src/se_dat.egg-info/dependency_links.txt
6
+ src/se_dat.egg-info/requires.txt
7
+ src/se_dat.egg-info/top_level.txt
8
+ src/sedat/__init__.py
9
+ src/sedat/correlations.py
10
+ src/sedat/encoding.py
11
+ src/sedat/profile.py
12
+ src/sedat/report.py
13
+ src/sedat/types.py
14
+ tests/test_se_dat.py
@@ -0,0 +1,9 @@
1
+ numpy>=1.23
2
+ pandas>=1.5
3
+ scipy>=1.9
4
+ matplotlib>=3.6
5
+ seaborn>=0.12
6
+
7
+ [dev]
8
+ pytest>=7
9
+ pytest-cov
@@ -0,0 +1 @@
1
+ sedat
@@ -0,0 +1,51 @@
1
+ """se-dat: Simple Exploratory Data Analysis tool.
2
+
3
+ Provides column type profiling, correlation analysis (numeric, categorical and
4
+ mixed) and encoding suggestions with automatic transforms.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .correlations import (
10
+ CorrelationReport,
11
+ categorical_correlation,
12
+ correlation_heatmap,
13
+ correlation_ratio,
14
+ correlation_report,
15
+ cramers_v,
16
+ numeric_categorical_correlation,
17
+ numeric_correlation,
18
+ )
19
+ from .encoding import (
20
+ EncodingPlan,
21
+ EncodingSuggestion,
22
+ apply_encodings,
23
+ suggest_encodings,
24
+ )
25
+ from .profile import ColumnProfile, DataFrameProfile, profile_column, profile_dataframe
26
+ from .report import EDAReport
27
+ from .types import infer_column_type, is_binary_like
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ "ColumnProfile",
33
+ "CorrelationReport",
34
+ "DataFrameProfile",
35
+ "EDAReport",
36
+ "EncodingPlan",
37
+ "EncodingSuggestion",
38
+ "apply_encodings",
39
+ "categorical_correlation",
40
+ "correlation_heatmap",
41
+ "correlation_ratio",
42
+ "correlation_report",
43
+ "cramers_v",
44
+ "infer_column_type",
45
+ "is_binary_like",
46
+ "numeric_categorical_correlation",
47
+ "numeric_correlation",
48
+ "profile_column",
49
+ "profile_dataframe",
50
+ "suggest_encodings",
51
+ ]
@@ -0,0 +1,227 @@
1
+ """Correlation analysis.
2
+
3
+ Numeric-numeric: Pearson/Spearman matrices plus a heatmap.
4
+ Categorical-categorical: Cramer's V.
5
+ Numeric-categorical: correlation ratio (eta), an ANOVA-style association measure.
6
+
7
+ Highly correlated pairs above a threshold are flagged as potential
8
+ multicollinearity.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Iterator
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ from scipy.stats import chi2_contingency
19
+
20
+ from .types import binary_mapping
21
+
22
+ CORRELATION_TYPES = ("numeric", "boolean", "categorical")
23
+
24
+
25
+ def _numeric_df(df: pd.DataFrame) -> pd.DataFrame:
26
+ return df.select_dtypes(include=[np.number])
27
+
28
+
29
+ def _categorical_df(df: pd.DataFrame) -> pd.DataFrame:
30
+ picked = []
31
+ for col in df.columns:
32
+ dtype = df[col].dtype
33
+ if (
34
+ pd.api.types.is_bool_dtype(dtype)
35
+ or isinstance(dtype, pd.CategoricalDtype)
36
+ or dtype == object
37
+ or isinstance(dtype, pd.StringDtype)
38
+ ):
39
+ picked.append(col)
40
+ return df[picked]
41
+
42
+
43
+ def numeric_correlation(df: pd.DataFrame, method: str = "pearson") -> pd.DataFrame:
44
+ """Pearson or Spearman correlation matrix over numeric columns."""
45
+ num = _numeric_df(df)
46
+ if num.shape[1] < 2:
47
+ return pd.DataFrame()
48
+ return num.corr(method=method)
49
+
50
+
51
+ def cramers_v(a: pd.Series, b: pd.Series, correction: bool = True) -> float:
52
+ """Cramer's V association between two categorical series, in [0, 1]."""
53
+ if a.name is not None and b.name is not None and a.name == b.name:
54
+ return 1.0
55
+ cross = pd.crosstab(a.astype(str), b.astype(str))
56
+ if min(cross.shape) < 2:
57
+ return float("nan")
58
+ chi2, _, _, _ = chi2_contingency(cross, correction=correction)
59
+ n = int(cross.to_numpy().sum())
60
+ phi2 = chi2 / n
61
+ r, k = cross.shape
62
+ if correction:
63
+ phi2 = max(0.0, phi2 - ((k - 1) * (r - 1)) / (n - 1))
64
+ r = max(0, r - ((r - 1) ** 2) / (n - 1))
65
+ k = max(0, k - ((k - 1) ** 2) / (n - 1))
66
+ denom = min(k - 1, r - 1)
67
+ if denom <= 0:
68
+ return float("nan")
69
+ return float(np.sqrt(phi2 / denom))
70
+
71
+
72
+ def categorical_correlation(df: pd.DataFrame) -> pd.DataFrame:
73
+ """Cramer's V matrix over categorical/boolean columns."""
74
+ cats = _categorical_df(df)
75
+ if cats.shape[1] < 2:
76
+ return pd.DataFrame()
77
+ mat = pd.DataFrame(index=cats.columns, columns=cats.columns, dtype=float)
78
+ for i, a in enumerate(cats.columns):
79
+ for j, b in enumerate(cats.columns):
80
+ if i <= j:
81
+ continue
82
+ v = cramers_v(cats[a], cats[b])
83
+ mat.loc[a, b] = v
84
+ mat.loc[b, a] = v
85
+ for i in range(mat.shape[0]):
86
+ mat.iat[i, i] = 1.0
87
+ return mat
88
+
89
+
90
+ def correlation_ratio(numeric: pd.Series, categorical: pd.Series) -> float:
91
+ """Eta correlation ratio between one numeric and one categorical series."""
92
+ y = pd.to_numeric(numeric, errors="coerce")
93
+ g = categorical.astype(str)
94
+ d = pd.DataFrame({"y": y, "g": g}).dropna()
95
+ if len(d) < 2 or d["g"].nunique() < 2:
96
+ return float("nan")
97
+ grand_mean = float(d["y"].mean())
98
+ group = d.groupby("g")["y"].agg(["mean", "count"])
99
+ ss_between = float(((group["mean"] - grand_mean) ** 2 * group["count"]).sum())
100
+ ss_total = float(((d["y"] - grand_mean) ** 2).sum())
101
+ if ss_total <= 0:
102
+ return float("nan")
103
+ return float(np.sqrt(ss_between / ss_total))
104
+
105
+
106
+ def numeric_categorical_correlation(df: pd.DataFrame) -> pd.DataFrame:
107
+ """Eta matrix with numeric columns as rows and categorical columns as cols."""
108
+ num = _numeric_df(df)
109
+ cats = _categorical_df(df)
110
+ if num.shape[1] < 1 or cats.shape[1] < 1:
111
+ return pd.DataFrame()
112
+ rows = []
113
+ for n in num.columns:
114
+ for c in cats.columns:
115
+ rows.append({"numeric": n, "categorical": c, "eta": correlation_ratio(num[n], cats[c])})
116
+ if not rows:
117
+ return pd.DataFrame()
118
+ return pd.DataFrame(rows).pivot(index="numeric", columns="categorical", values="eta")
119
+
120
+
121
+ def _upper_pairs(mat: pd.DataFrame) -> Iterator[tuple[str, str, float]]:
122
+ cols = list(mat.columns)
123
+ for i in range(len(cols)):
124
+ for j in range(i + 1, len(cols)):
125
+ v = mat.iat[i, j]
126
+ if np.isnan(v):
127
+ continue
128
+ yield cols[i], cols[j], float(v)
129
+
130
+
131
+ @dataclass
132
+ class CorrelationReport:
133
+ numeric: pd.DataFrame
134
+ numeric_spearman: pd.DataFrame
135
+ categorical: pd.DataFrame
136
+ numeric_categorical: pd.DataFrame
137
+ method_numeric: str = "pearson"
138
+ threshold: float = 0.7
139
+
140
+ @property
141
+ def summary(self) -> pd.DataFrame:
142
+ rows = []
143
+ for mat, kind, method in (
144
+ (self.numeric, "numeric", self.method_numeric),
145
+ (self.numeric_spearman, "numeric", "spearman"),
146
+ (self.categorical, "categorical", "cramers_v"),
147
+ (self.numeric_categorical, "numeric-categorical", "eta"),
148
+ ):
149
+ if mat.empty:
150
+ continue
151
+ if kind == "numeric-categorical":
152
+ for num_col in mat.index:
153
+ for cat_col in mat.columns:
154
+ v = mat.loc[num_col, cat_col]
155
+ if np.isnan(v):
156
+ continue
157
+ rows.append(
158
+ {
159
+ "column_a": num_col,
160
+ "column_b": cat_col,
161
+ "kind": kind,
162
+ "method": method,
163
+ "value": round(float(v), 4),
164
+ "flagged": abs(float(v)) >= self.threshold,
165
+ }
166
+ )
167
+ else:
168
+ for a, b, v in _upper_pairs(mat):
169
+ rows.append(
170
+ {
171
+ "column_a": a,
172
+ "column_b": b,
173
+ "kind": kind,
174
+ "method": method,
175
+ "value": round(v, 4),
176
+ "flagged": abs(v) >= self.threshold,
177
+ }
178
+ )
179
+ return pd.DataFrame(rows)
180
+
181
+ @property
182
+ def flagged_pairs(self) -> pd.DataFrame:
183
+ s = self.summary
184
+ return s[s["flagged"]] if not s.empty else s
185
+
186
+
187
+ def correlation_report(
188
+ df: pd.DataFrame,
189
+ method: str = "pearson",
190
+ threshold: float = 0.7,
191
+ ) -> CorrelationReport:
192
+ return CorrelationReport(
193
+ numeric=numeric_correlation(df, method=method),
194
+ numeric_spearman=numeric_correlation(df, method="spearman"),
195
+ categorical=categorical_correlation(df),
196
+ numeric_categorical=numeric_categorical_correlation(df),
197
+ method_numeric=method,
198
+ threshold=threshold,
199
+ )
200
+
201
+
202
+ def _binary_numeric_map(df: pd.DataFrame) -> dict[str, dict[str, int]]:
203
+ return {
204
+ col: mapping
205
+ for col in df.columns
206
+ if (mapping := binary_mapping(df[col])) is not None
207
+ }
208
+
209
+
210
+ def encode_binary_columns(df: pd.DataFrame) -> pd.DataFrame:
211
+ """Encode binary-like object columns to 0/1 numeric for correlation use."""
212
+ out = df.copy()
213
+ for col, mapping in _binary_numeric_map(df).items():
214
+ out[col] = out[col].astype(str).str.strip().str.lower().map(mapping)
215
+ return out
216
+
217
+
218
+ def correlation_heatmap(matrix: pd.DataFrame, title: str = "Correlation matrix") -> object:
219
+ """Plot a heatmap for a correlation matrix; returns a matplotlib Figure."""
220
+ import matplotlib.pyplot as plt
221
+ import seaborn as sns
222
+
223
+ fig, ax = plt.subplots(figsize=(max(6, matrix.shape[1] * 0.7), max(5, matrix.shape[0] * 0.6)))
224
+ sns.heatmap(matrix, annot=True, fmt=".2f", cmap="coolwarm", vmin=-1, vmax=1, ax=ax)
225
+ ax.set_title(title)
226
+ fig.tight_layout()
227
+ return fig
@@ -0,0 +1,237 @@
1
+ """Encoding suggestions and automatic transforms.
2
+
3
+ Detects binary-like string columns ("yes"/"no", "true"/"false", 0/1) and offers
4
+ an .encode()-style 0/1 map. Suggests one-hot encoding for low-cardinality
5
+ categoricals and warns/suggests target or ordinal encoding for high-cardinality
6
+ ones to avoid a dimensionality explosion.
7
+
8
+ Suggestions can be applied individually (``suggestion.apply``) or all at once
9
+ (``EncodingPlan.apply_all``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from .profile import profile_dataframe
20
+ from .types import binary_mapping
21
+
22
+ BINARY_STRATEGY = "binary_encode"
23
+ ONE_HOT_STRATEGY = "one_hot"
24
+ ORDINAL_STRATEGY = "ordinal_encode"
25
+ TARGET_STRATEGY = "target_encode"
26
+ NONE_STRATEGY = "none"
27
+
28
+ DEFAULT_CARDINALITY_THRESHOLD = 10
29
+
30
+
31
+ @dataclass
32
+ class EncodingSuggestion:
33
+ column: str
34
+ current_type: str
35
+ suggested_type: str
36
+ strategy: str
37
+ cardinality: int
38
+ rationale: str
39
+ categories: list[str] = field(default_factory=list)
40
+ target_means: dict[str, float] = field(default_factory=dict)
41
+
42
+ def apply(self, df: pd.DataFrame, target: pd.Series | None = None) -> pd.DataFrame:
43
+ out = df.copy()
44
+ col = self.column
45
+ if col not in out.columns:
46
+ return out
47
+ if self.strategy == NONE_STRATEGY:
48
+ return out
49
+ if self.strategy == BINARY_STRATEGY:
50
+ mapping = binary_mapping(out[col])
51
+ if mapping is None:
52
+ ordered = sorted(out[col].dropna().unique())
53
+ mapping = {v: i for i, v in enumerate(ordered)}
54
+ out[col] = out[col].astype(str).str.strip().str.lower().map(mapping)
55
+ elif self.strategy == ONE_HOT_STRATEGY:
56
+ dummies = pd.get_dummies(out[col], prefix=col, dtype=int)
57
+ out = out.drop(columns=[col])
58
+ out = pd.concat([out, dummies], axis=1)
59
+ elif self.strategy == ORDINAL_STRATEGY:
60
+ ordered = sorted(out[col].dropna().astype(str).unique())
61
+ out[col] = out[col].astype(str).map({v: i for i, v in enumerate(ordered)})
62
+ elif self.strategy == TARGET_STRATEGY:
63
+ if target is not None:
64
+ means = target.groupby(out[col].astype(str)).transform("mean")
65
+ out[col] = pd.to_numeric(means, errors="coerce")
66
+ elif self.target_means:
67
+ out[col] = out[col].astype(str).map(self.target_means)
68
+ return out
69
+
70
+
71
+ @dataclass
72
+ class EncodingPlan:
73
+ suggestions: list[EncodingSuggestion]
74
+ cardinality_threshold: int = DEFAULT_CARDINALITY_THRESHOLD
75
+ target: pd.Series | None = None
76
+
77
+ @property
78
+ def summary(self) -> pd.DataFrame:
79
+ rows = [
80
+ {
81
+ "column": s.column,
82
+ "current_type": s.current_type,
83
+ "suggested_type": s.suggested_type,
84
+ "strategy": s.strategy,
85
+ "cardinality": s.cardinality,
86
+ "rationale": s.rationale,
87
+ }
88
+ for s in self.suggestions
89
+ ]
90
+ return pd.DataFrame(rows)
91
+
92
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
93
+ return self.apply_all(df)
94
+
95
+ def apply_all(self, df: pd.DataFrame, target: pd.Series | None = None) -> pd.DataFrame:
96
+ result = df.copy()
97
+ tgt = target if target is not None else self.target
98
+ for suggestion in self.suggestions:
99
+ result = suggestion.apply(result, target=tgt)
100
+ return result
101
+
102
+ def __repr__(self) -> str:
103
+ return repr(self.summary)
104
+
105
+
106
+ def _warn(msg: str) -> None:
107
+ import warnings
108
+
109
+ warnings.warn(msg, UserWarning, stacklevel=3)
110
+
111
+
112
+ def suggest_encodings(
113
+ df: pd.DataFrame,
114
+ target: pd.Series | None = None,
115
+ cardinality_threshold: int = DEFAULT_CARDINALITY_THRESHOLD,
116
+ ) -> EncodingPlan:
117
+ """Build an :class:`EncodingPlan` of encoding suggestions for ``df``.
118
+
119
+ Parameters
120
+ ----------
121
+ df:
122
+ Input DataFrame.
123
+ target:
124
+ Optional numeric target Series used for target-encoding suggestions on
125
+ high-cardinality categoricals. The Series index must match ``df``.
126
+ cardinality_threshold:
127
+ Categoricals with at most this many distinct values get a one-hot
128
+ suggestion; above it they get target/ordinal encoding.
129
+ """
130
+ profile = profile_dataframe(df)
131
+ suggestions: list[EncodingSuggestion] = []
132
+
133
+ for col_profile in profile.columns:
134
+ col = col_profile.name
135
+ series = df[col]
136
+ cardinality = col_profile.n_unique
137
+ current_type = col_profile.inferred_type
138
+
139
+ if current_type == "boolean":
140
+ if series.dtype == object or isinstance(series.dtype, pd.StringDtype):
141
+ suggestions.append(
142
+ EncodingSuggestion(
143
+ column=col,
144
+ current_type=current_type,
145
+ suggested_type="boolean (0/1)",
146
+ strategy=BINARY_STRATEGY,
147
+ cardinality=cardinality,
148
+ rationale="binary-like strings map cleanly to 0/1",
149
+ categories=sorted(series.dropna().unique().tolist()),
150
+ )
151
+ )
152
+ continue
153
+
154
+ if current_type not in ("categorical",):
155
+ continue
156
+
157
+ if cardinality == 0:
158
+ continue
159
+
160
+ if cardinality == 2:
161
+ suggestions.append(
162
+ EncodingSuggestion(
163
+ column=col,
164
+ current_type=current_type,
165
+ suggested_type="boolean (0/1)",
166
+ strategy=BINARY_STRATEGY,
167
+ cardinality=cardinality,
168
+ rationale="two distinct values: binary encode",
169
+ categories=sorted(series.dropna().unique().tolist()),
170
+ )
171
+ )
172
+ elif cardinality <= cardinality_threshold:
173
+ suggestions.append(
174
+ EncodingSuggestion(
175
+ column=col,
176
+ current_type=current_type,
177
+ suggested_type="one-hot (dummy) columns",
178
+ strategy=ONE_HOT_STRATEGY,
179
+ cardinality=cardinality,
180
+ rationale=f"low-cardinality categorical (k={cardinality})",
181
+ categories=sorted(series.dropna().unique().tolist()),
182
+ )
183
+ )
184
+ else:
185
+ if target is not None:
186
+ suggestions.append(
187
+ EncodingSuggestion(
188
+ column=col,
189
+ current_type=current_type,
190
+ suggested_type="target-encoded numeric",
191
+ strategy=TARGET_STRATEGY,
192
+ cardinality=cardinality,
193
+ rationale=(
194
+ f"high-cardinality categorical (k={cardinality}): "
195
+ "one-hot would explode dimensionality"
196
+ ),
197
+ categories=sorted(series.dropna().unique().tolist()),
198
+ target_means={
199
+ str(k): float(v)
200
+ for k, v in target.groupby(series.astype(str)).mean().items()
201
+ },
202
+ )
203
+ )
204
+ else:
205
+ _warn(
206
+ f"Column '{col}' has high cardinality ({cardinality}). "
207
+ "Pass a `target` Series to enable target encoding; "
208
+ "falling back to ordinal encoding."
209
+ )
210
+ suggestions.append(
211
+ EncodingSuggestion(
212
+ column=col,
213
+ current_type=current_type,
214
+ suggested_type="ordinal-encoded numeric",
215
+ strategy=ORDINAL_STRATEGY,
216
+ cardinality=cardinality,
217
+ rationale=(
218
+ f"high-cardinality categorical (k={cardinality}): "
219
+ "one-hot would explode dimensionality"
220
+ ),
221
+ categories=sorted(series.dropna().astype(str).unique().tolist()),
222
+ )
223
+ )
224
+
225
+ return EncodingPlan(suggestions=suggestions, cardinality_threshold=cardinality_threshold, target=target)
226
+
227
+
228
+ def apply_encodings(
229
+ df: pd.DataFrame,
230
+ plan: EncodingPlan | None = None,
231
+ target: pd.Series | None = None,
232
+ cardinality_threshold: int = DEFAULT_CARDINALITY_THRESHOLD,
233
+ ) -> pd.DataFrame:
234
+ """Shortcut: build a plan (if not given) and apply every suggestion."""
235
+ if plan is None:
236
+ plan = suggest_encodings(df, target=target, cardinality_threshold=cardinality_threshold)
237
+ return plan.apply_all(df, target=target)
@@ -0,0 +1,75 @@
1
+ """Column type profiling.
2
+
3
+ Produces a per-column profile with the inferred semantic type, basic stats
4
+ (missing %, unique count, cardinality ratio) and a confidence flag when the
5
+ inference is ambiguous.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ import pandas as pd
13
+
14
+ from .types import infer_column_type
15
+
16
+
17
+ @dataclass
18
+ class ColumnProfile:
19
+ name: str
20
+ dtype: str
21
+ inferred_type: str
22
+ n_non_null: int
23
+ n_unique: int
24
+ missing_pct: float
25
+ cardinality_ratio: float
26
+ confidence: str
27
+ notes: list[str] = field(default_factory=list)
28
+
29
+ @property
30
+ def ambiguous(self) -> bool:
31
+ return self.confidence in ("low", "medium")
32
+
33
+
34
+ @dataclass
35
+ class DataFrameProfile:
36
+ columns: list[ColumnProfile]
37
+
38
+ @property
39
+ def summary(self) -> pd.DataFrame:
40
+ rows = [
41
+ {
42
+ "column": c.name,
43
+ "dtype": c.dtype,
44
+ "inferred_type": c.inferred_type,
45
+ "missing_pct": c.missing_pct,
46
+ "n_unique": c.n_unique,
47
+ "cardinality_ratio": c.cardinality_ratio,
48
+ "confidence": c.confidence,
49
+ "notes": "; ".join(c.notes),
50
+ }
51
+ for c in self.columns
52
+ ]
53
+ return pd.DataFrame(rows).set_index("column")
54
+
55
+ def __repr__(self) -> str:
56
+ return repr(self.summary)
57
+
58
+
59
+ def profile_column(series: pd.Series) -> ColumnProfile:
60
+ info = infer_column_type(series)
61
+ return ColumnProfile(
62
+ name=str(series.name),
63
+ dtype=str(series.dtype),
64
+ inferred_type=info["inferred_type"],
65
+ n_non_null=info["n_non_null"],
66
+ n_unique=info["n_unique"],
67
+ missing_pct=info["missing_pct"],
68
+ cardinality_ratio=info["cardinality_ratio"],
69
+ confidence=info["confidence"],
70
+ notes=info["notes"],
71
+ )
72
+
73
+
74
+ def profile_dataframe(df: pd.DataFrame) -> DataFrameProfile:
75
+ return DataFrameProfile([profile_column(df[col]) for col in df.columns])
@@ -0,0 +1,72 @@
1
+ """High-level facade combining profiling, correlations and encoding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import pandas as pd
8
+
9
+ from .correlations import CorrelationReport, correlation_report
10
+ from .encoding import (
11
+ DEFAULT_CARDINALITY_THRESHOLD,
12
+ EncodingPlan,
13
+ suggest_encodings,
14
+ )
15
+ from .profile import DataFrameProfile, profile_dataframe
16
+
17
+
18
+ @dataclass
19
+ class EDAReport:
20
+ """One object summarizing profiling, correlations and encodings for a frame."""
21
+
22
+ df: pd.DataFrame
23
+ profile: DataFrameProfile
24
+ correlations: CorrelationReport
25
+ encodings: EncodingPlan
26
+ corr_threshold: float = 0.7
27
+ cardinality_threshold: int = DEFAULT_CARDINALITY_THRESHOLD
28
+
29
+ @classmethod
30
+ def create(
31
+ cls,
32
+ df: pd.DataFrame,
33
+ corr_threshold: float = 0.7,
34
+ cardinality_threshold: int = DEFAULT_CARDINALITY_THRESHOLD,
35
+ target: pd.Series | None = None,
36
+ ) -> "EDAReport":
37
+ return cls(
38
+ df=df,
39
+ profile=profile_dataframe(df),
40
+ correlations=correlation_report(df, threshold=corr_threshold),
41
+ encodings=suggest_encodings(
42
+ df, target=target, cardinality_threshold=cardinality_threshold
43
+ ),
44
+ corr_threshold=corr_threshold,
45
+ cardinality_threshold=cardinality_threshold,
46
+ )
47
+
48
+ @property
49
+ def summary(self) -> pd.DataFrame:
50
+ return self.profile.summary
51
+
52
+ @property
53
+ def correlation_summary(self) -> pd.DataFrame:
54
+ return self.correlations.summary
55
+
56
+ @property
57
+ def encoding_summary(self) -> pd.DataFrame:
58
+ return self.encodings.summary
59
+
60
+ def apply_all_encodings(self, target: pd.Series | None = None) -> pd.DataFrame:
61
+ return self.encodings.apply_all(self.df, target=target)
62
+
63
+ def __repr__(self) -> str:
64
+ lines = [repr(self.profile)]
65
+ corr = self.correlations.summary
66
+ if not corr.empty:
67
+ flagged = corr[corr["flagged"]]
68
+ if not flagged.empty:
69
+ lines.append("\nFlagged highly-correlated pairs:")
70
+ lines.append(repr(flagged))
71
+ lines.append("\n" + repr(self.encodings))
72
+ return "\n".join(lines)
@@ -0,0 +1,152 @@
1
+ """Column type inference helpers.
2
+
3
+ Infers a semantic type for each column: numeric, boolean, categorical,
4
+ string/text, datetime or id (high-cardinality unique values), and reports a
5
+ confidence level plus explanatory notes when the inference is ambiguous.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ BINARY_VALUE_SETS: dict[str, set[str]] = {
16
+ "yes/no": {"yes", "no", "y", "n"},
17
+ "true/false": {"true", "false", "t", "f"},
18
+ "0/1": {"0", "1"},
19
+ }
20
+
21
+ _BINARY_MAPS: dict[frozenset[str], dict[str, int]] = {
22
+ frozenset({"yes", "no", "y", "n"}): {"yes": 1, "no": 0, "y": 1, "n": 0},
23
+ frozenset({"true", "false", "t", "f"}): {"true": 1, "false": 0, "t": 1, "f": 0},
24
+ frozenset({"0", "1"}): {"0": 1, "1": 0},
25
+ }
26
+
27
+
28
+ def binary_mapping(series: pd.Series) -> dict[str, int] | None:
29
+ """Return a {value: 0/1} mapping if ``series`` is binary-like, else None."""
30
+ unique = set(series.dropna().astype(str).str.strip().str.lower().unique())
31
+ if not unique:
32
+ return None
33
+ for allowed, mapping in _BINARY_MAPS.items():
34
+ if unique.issubset(allowed):
35
+ return {v: mapping[v] for v in unique}
36
+ return None
37
+
38
+
39
+ def is_binary_like(series: pd.Series) -> bool:
40
+ return binary_mapping(series) is not None
41
+
42
+
43
+ def _parseable_as_datetime(series: pd.Series, threshold: float = 0.9) -> bool:
44
+ sample = series.dropna().astype(str).head(200)
45
+ if sample.empty:
46
+ return False
47
+ try:
48
+ with pd.option_context("mode.chained_assignment", None):
49
+ import warnings
50
+
51
+ with warnings.catch_warnings():
52
+ warnings.simplefilter("ignore")
53
+ parsed = pd.to_datetime(sample, errors="coerce")
54
+ except (TypeError, ValueError, OverflowError):
55
+ return False
56
+ return float(parsed.notna().mean()) >= threshold
57
+
58
+
59
+ def _numeric_like_string(series: pd.Series, threshold: float = 0.9) -> bool:
60
+ sample = series.dropna().head(500)
61
+ if sample.empty:
62
+ return False
63
+ coerced = pd.to_numeric(sample, errors="coerce")
64
+ return float(coerced.notna().mean()) >= threshold
65
+
66
+
67
+ def _looks_like_identifier(series: pd.Series, threshold: float = 0.95) -> bool:
68
+ sample = series.dropna().astype(str).head(500)
69
+ if sample.empty:
70
+ return False
71
+ no_whitespace = ~sample.str.contains(r"\s", regex=True)
72
+ return float(no_whitespace.mean()) >= threshold
73
+
74
+
75
+ def _basic_stats(series: pd.Series) -> dict[str, float | int]:
76
+ n_non_null = int(series.notna().sum())
77
+ n_rows = len(series)
78
+ n_unique = int(series.nunique(dropna=True))
79
+ missing_pct = 0.0 if n_rows == 0 else (n_rows - n_non_null) / n_rows * 100
80
+ cardinality_ratio = n_unique / n_non_null if n_non_null else 0.0
81
+ return {
82
+ "n_non_null": n_non_null,
83
+ "n_unique": n_unique,
84
+ "missing_pct": round(missing_pct, 2),
85
+ "cardinality_ratio": round(cardinality_ratio, 4),
86
+ }
87
+
88
+
89
+ def infer_column_type(series: pd.Series) -> dict[str, Any]:
90
+ """Infer the semantic type of a single ``Series``.
91
+
92
+ Returns a dict with keys: inferred_type, confidence, notes, plus the basic
93
+ stats (n_non_null, n_unique, missing_pct, cardinality_ratio).
94
+ """
95
+ stats = _basic_stats(series)
96
+ notes: list[str] = []
97
+ inferred_type: str
98
+ confidence: str = "high"
99
+
100
+ if isinstance(series.dtype, pd.CategoricalDtype):
101
+ inferred_type = "categorical"
102
+ elif pd.api.types.is_datetime64_any_dtype(series.dtype):
103
+ inferred_type = "datetime"
104
+ elif pd.api.types.is_bool_dtype(series.dtype):
105
+ inferred_type = "boolean"
106
+ elif pd.api.types.is_numeric_dtype(series.dtype):
107
+ mapping = binary_mapping(series)
108
+ if mapping is not None and stats["n_unique"] <= 2:
109
+ inferred_type = "boolean"
110
+ confidence = "medium"
111
+ notes.append("numeric 0/1 column: ambiguous boolean vs numeric")
112
+ elif (
113
+ pd.api.types.is_integer_dtype(series.dtype)
114
+ and stats["cardinality_ratio"] >= 0.95
115
+ ):
116
+ inferred_type = "id"
117
+ notes.append("high-cardinality unique integer values")
118
+ else:
119
+ inferred_type = "numeric"
120
+ else:
121
+ if stats["n_non_null"] == 0:
122
+ inferred_type = "string"
123
+ confidence = "low"
124
+ notes.append("empty column")
125
+ else:
126
+ mapping = binary_mapping(series)
127
+ if mapping is not None:
128
+ inferred_type = "boolean"
129
+ confidence = "medium"
130
+ notes.append(f"stored as strings ({', '.join(sorted(mapping))})")
131
+ elif _parseable_as_datetime(series):
132
+ inferred_type = "datetime"
133
+ confidence = "medium"
134
+ notes.append("parsed from object dtype")
135
+ elif _numeric_like_string(series):
136
+ inferred_type = "numeric"
137
+ confidence = "low"
138
+ notes.append("numeric-looking values stored as string")
139
+ elif stats["cardinality_ratio"] >= 0.95:
140
+ if _looks_like_identifier(series):
141
+ inferred_type = "id"
142
+ notes.append("high-cardinality unique values")
143
+ else:
144
+ inferred_type = "string"
145
+ notes.append("high-cardinality free text")
146
+ elif stats["cardinality_ratio"] >= 0.5:
147
+ inferred_type = "string"
148
+ notes.append("many unique values")
149
+ else:
150
+ inferred_type = "categorical"
151
+
152
+ return {**stats, "inferred_type": inferred_type, "confidence": confidence, "notes": notes}
@@ -0,0 +1,176 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import pytest
4
+
5
+ import sedat
6
+
7
+
8
+ def make_df(n=1000, seed=0):
9
+ rng = np.random.default_rng(seed)
10
+ df = pd.DataFrame(
11
+ {
12
+ "id": np.arange(n),
13
+ "age": rng.integers(18, 80, n),
14
+ "height": rng.normal(170, 10, n).round(2),
15
+ "income": [str(x) for x in rng.integers(30000, 120000, n)],
16
+ "gender": rng.choice(["M", "F"], n),
17
+ "smoker": rng.choice(["yes", "no"], n),
18
+ "active": rng.choice([True, False], n),
19
+ "flag01": rng.integers(0, 2, n),
20
+ "score_cat": rng.choice(["low", "med", "high"], n),
21
+ "free_text": [f"user comment number {i}" for i in range(n)],
22
+ "joined": pd.to_datetime("2020-01-01")
23
+ + pd.to_timedelta(rng.integers(0, 365 * 5, n), unit="D"),
24
+ "large_card": rng.choice([f"sku_{i}" for i in range(50)], n),
25
+ "target": rng.normal(0, 1, n),
26
+ }
27
+ )
28
+ df.loc[rng.choice(n, 25, replace=False), "age"] = np.nan
29
+ return df
30
+
31
+
32
+ def type_map(profile):
33
+ return {c.name: c.inferred_type for c in profile.columns}
34
+
35
+
36
+ def test_profile_infers_types():
37
+ df = make_df()
38
+ profile = sedat.profile_dataframe(df)
39
+ types = type_map(profile)
40
+ assert types["id"] == "id"
41
+ assert types["age"] == "numeric"
42
+ assert types["height"] == "numeric"
43
+ assert types["income"] == "numeric"
44
+ assert types["gender"] == "categorical"
45
+ assert types["smoker"] == "boolean"
46
+ assert types["active"] == "boolean"
47
+ assert types["flag01"] == "boolean"
48
+ assert types["free_text"] == "string"
49
+ assert types["joined"] == "datetime"
50
+ assert types["target"] == "numeric"
51
+
52
+
53
+ def test_profile_confidence_flags():
54
+ df = make_df()
55
+ profile = sedat.profile_dataframe(df)
56
+ by_name = {c.name: c for c in profile.columns}
57
+ assert by_name["income"].confidence == "low"
58
+ assert by_name["income"].ambiguous
59
+ assert by_name["smoker"].confidence == "medium"
60
+ assert by_name["active"].confidence == "high"
61
+ assert "stored as string" in by_name["income"].notes[0]
62
+
63
+
64
+ def test_profile_basic_stats():
65
+ df = make_df()
66
+ profile = sedat.profile_dataframe(df)
67
+ by_name = {c.name: c for c in profile.columns}
68
+ assert by_name["age"].missing_pct == pytest.approx(2.5, abs=0.05)
69
+ assert by_name["id"].cardinality_ratio == pytest.approx(1.0)
70
+ assert by_name["gender"].n_unique == 2
71
+
72
+
73
+ def test_numeric_correlation():
74
+ df = make_df()
75
+ pearson = sedat.numeric_correlation(df)
76
+ assert {"age", "height"} <= set(pearson.columns)
77
+ assert abs(pearson.loc["height", "age"]) < 0.3
78
+ spearman = sedat.numeric_correlation(df, method="spearman")
79
+ assert "age" in spearman.index
80
+
81
+
82
+ def test_cramers_v_range():
83
+ df = make_df()
84
+ mat = sedat.categorical_correlation(df)
85
+ assert {"gender", "smoker", "score_cat"} <= set(mat.columns)
86
+ assert mat.loc["gender", "gender"] == 1.0
87
+ for col in mat.columns:
88
+ v = mat.loc["gender", col]
89
+ assert np.isnan(v) or 0.0 <= v <= 1.0
90
+
91
+
92
+ def test_eta_range():
93
+ df = make_df()
94
+ mat = sedat.numeric_categorical_correlation(df)
95
+ assert "gender" in mat.columns
96
+ assert "age" in mat.index
97
+ v = mat.loc["age", "gender"]
98
+ assert np.isnan(v) or 0.0 <= v <= 1.0
99
+
100
+
101
+ def test_correlation_report_flags():
102
+ df = make_df()
103
+ df2 = df.copy()
104
+ df2["height2"] = df["height"] * 1.5 + 3
105
+ report = sedat.correlation_report(df2, threshold=0.95)
106
+ flagged = report.flagged_pairs
107
+ assert not flagged.empty
108
+ pair = flagged[(flagged["column_a"] == "height") & (flagged["column_b"] == "height2")]
109
+ assert not pair.empty
110
+ assert pair["value"].iloc[0] == pytest.approx(1.0, abs=0.01)
111
+
112
+
113
+ def test_encode_binary_suggestion():
114
+ df = make_df()
115
+ plan = sedat.suggest_encodings(df)
116
+ by_col = {s.column: s for s in plan.suggestions}
117
+ assert by_col["smoker"].strategy == "binary_encode"
118
+ assert "active" not in by_col
119
+ assert by_col["gender"].strategy == "binary_encode"
120
+ assert by_col["score_cat"].strategy == "one_hot"
121
+ assert by_col["large_card"].strategy == "ordinal_encode"
122
+
123
+
124
+ def test_apply_encodings_all():
125
+ df = make_df()
126
+ plan = sedat.suggest_encodings(df)
127
+ out = plan.apply_all(df)
128
+ object_cols = [c for c in out.columns if out[c].dtype == object]
129
+ assert object_cols == []
130
+ assert out["smoker"].isin([0, 1]).all()
131
+ assert out["score_cat_low"].dtype == int
132
+ assert "large_card" in out.columns and out["large_card"].dtype in (int, float)
133
+
134
+
135
+ def test_target_encoding_high_cardinality():
136
+ df = make_df()
137
+ plan = sedat.suggest_encodings(df, target=df["target"])
138
+ by_col = {s.column: s for s in plan.suggestions}
139
+ assert by_col["large_card"].strategy == "target_encode"
140
+ out = plan.apply_all(df)
141
+ assert out["large_card"].dtype == float
142
+
143
+
144
+ def test_apply_single_suggestion():
145
+ df = make_df()
146
+ plan = sedat.suggest_encodings(df)
147
+ smoker = next(s for s in plan.suggestions if s.column == "smoker")
148
+ out = smoker.apply(df)
149
+ assert out["smoker"].isin([0, 1]).all()
150
+
151
+
152
+ def test_report_end_to_end():
153
+ df = make_df()
154
+ report = sedat.EDAReport.create(df, target=df["target"])
155
+ assert isinstance(report.profile, sedat.DataFrameProfile)
156
+ assert report.encoding_summary is not None
157
+ out = report.apply_all_encodings()
158
+ assert object not in out.dtypes.values
159
+ assert "score_cat" not in out.columns
160
+ assert "score_cat_low" in out.columns
161
+
162
+
163
+ def test_binary_mapping_helpers():
164
+ s = pd.Series(["yes", "no", "yes"])
165
+ assert sedat.is_binary_like(s)
166
+ s2 = pd.Series([0, 1, 1])
167
+ assert sedat.is_binary_like(s2)
168
+ s3 = pd.Series(["alpha", "beta", "gamma"])
169
+ assert not sedat.is_binary_like(s3)
170
+
171
+
172
+ def test_infer_column_type_direct():
173
+ s = pd.Series(["1.5", "2.5", "3.0"])
174
+ info = sedat.infer_column_type(s)
175
+ assert info["inferred_type"] == "numeric"
176
+ assert info["confidence"] == "low"