data-sampler 3.2.1__py3-none-any.whl
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.
- data_sampler/__init__.py +82 -0
- data_sampler/__main__.py +7 -0
- data_sampler/_logging.py +61 -0
- data_sampler/_names.py +60 -0
- data_sampler/anonymize.py +463 -0
- data_sampler/cli.py +363 -0
- data_sampler/engine.py +628 -0
- data_sampler/io.py +75 -0
- data_sampler/report.py +289 -0
- data_sampler/sampling.py +193 -0
- data_sampler/stats.py +174 -0
- data_sampler/tui/__init__.py +16 -0
- data_sampler/tui/app.py +904 -0
- data_sampler/workflow.py +285 -0
- data_sampler-3.2.1.dist-info/METADATA +427 -0
- data_sampler-3.2.1.dist-info/RECORD +19 -0
- data_sampler-3.2.1.dist-info/WHEEL +4 -0
- data_sampler-3.2.1.dist-info/entry_points.txt +3 -0
- data_sampler-3.2.1.dist-info/licenses/LICENSE +21 -0
data_sampler/stats.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Per-column statistics, modelled after the Data Wrangler column summary view."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
SPARK_CHARS = "▁▂▃▄▅▆▇█"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ColumnStats:
|
|
15
|
+
"""Summary statistics for a single DataFrame column."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
dtype: str
|
|
19
|
+
kind: str # "numeric" | "boolean" | "datetime" | "categorical" | "text" | "other"
|
|
20
|
+
count: int # non-null values
|
|
21
|
+
missing: int
|
|
22
|
+
missing_pct: float
|
|
23
|
+
unique: int
|
|
24
|
+
unique_pct: float
|
|
25
|
+
# numeric-only (None otherwise)
|
|
26
|
+
min: float | None = None
|
|
27
|
+
max: float | None = None
|
|
28
|
+
mean: float | None = None
|
|
29
|
+
std: float | None = None
|
|
30
|
+
median: float | None = None
|
|
31
|
+
# most frequent values as (value, count), up to 8
|
|
32
|
+
top_values: list[tuple[str, int]] = field(default_factory=list)
|
|
33
|
+
# distribution: 10-bin histogram counts for numeric, top-8 category counts otherwise
|
|
34
|
+
histogram: list[int] = field(default_factory=list)
|
|
35
|
+
# labels matching histogram entries (bin ranges or category names)
|
|
36
|
+
histogram_labels: list[str] = field(default_factory=list)
|
|
37
|
+
# whether the auto-stratifier would consider this column
|
|
38
|
+
stratifiable: bool = False
|
|
39
|
+
# True when unique/quantile figures are approximate (HLL / approx_quantile),
|
|
40
|
+
# as produced by the DuckDB engine over very large inputs
|
|
41
|
+
approximate: bool = False
|
|
42
|
+
|
|
43
|
+
def summary(self) -> str:
|
|
44
|
+
"""One-line summary suitable for a table cell."""
|
|
45
|
+
if self.kind == "numeric" and self.min is not None:
|
|
46
|
+
return f"{_fmt_num(self.min)} … {_fmt_num(self.max)} μ={_fmt_num(self.mean)}"
|
|
47
|
+
if self.top_values:
|
|
48
|
+
value, count = self.top_values[0]
|
|
49
|
+
pct = count / self.count * 100 if self.count else 0
|
|
50
|
+
return f"top: {value[:20]} ({pct:.0f}%)"
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fmt_num(x: float | None) -> str:
|
|
55
|
+
if x is None:
|
|
56
|
+
return "—"
|
|
57
|
+
if not np.isfinite(x):
|
|
58
|
+
return str(x)
|
|
59
|
+
if x == int(x) and abs(x) < 1e15:
|
|
60
|
+
return f"{int(x):,}"
|
|
61
|
+
return f"{x:,.2f}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sparkline(counts: list[int]) -> str:
|
|
65
|
+
"""Render a list of counts as a unicode sparkline (e.g. ``▂▅█▃▁``)."""
|
|
66
|
+
if not counts:
|
|
67
|
+
return ""
|
|
68
|
+
peak = max(counts)
|
|
69
|
+
if peak == 0:
|
|
70
|
+
return SPARK_CHARS[0] * len(counts)
|
|
71
|
+
out = []
|
|
72
|
+
for c in counts:
|
|
73
|
+
if c == 0:
|
|
74
|
+
out.append(SPARK_CHARS[0])
|
|
75
|
+
else:
|
|
76
|
+
idx = max(1, int(c / peak * (len(SPARK_CHARS) - 1)))
|
|
77
|
+
out.append(SPARK_CHARS[idx])
|
|
78
|
+
return "".join(out)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_stratifiable(series: pd.Series, n_rows: int, n_unique: int | None = None) -> bool:
|
|
82
|
+
"""Whether a column passes the auto-stratification candidate checks.
|
|
83
|
+
|
|
84
|
+
Mirrors the selection rules used by
|
|
85
|
+
:func:`data_sampler.sampling.find_stratification_columns`. Pass a
|
|
86
|
+
precomputed ``n_unique`` to avoid a second ``nunique`` hash pass.
|
|
87
|
+
"""
|
|
88
|
+
n_unique = series.nunique() if n_unique is None else n_unique
|
|
89
|
+
if n_unique > min(100, n_rows * 0.5):
|
|
90
|
+
return False
|
|
91
|
+
if n_unique < 2:
|
|
92
|
+
return False
|
|
93
|
+
if series.dtype == object or pd.api.types.is_string_dtype(series):
|
|
94
|
+
avg_len = series.dropna().astype(str).str.len().mean()
|
|
95
|
+
if avg_len > 50:
|
|
96
|
+
return False
|
|
97
|
+
if pd.api.types.is_numeric_dtype(series):
|
|
98
|
+
if n_unique > min(20, n_rows * 0.3):
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _classify(series: pd.Series) -> str:
|
|
104
|
+
if pd.api.types.is_bool_dtype(series):
|
|
105
|
+
return "boolean"
|
|
106
|
+
if pd.api.types.is_numeric_dtype(series):
|
|
107
|
+
return "numeric"
|
|
108
|
+
if pd.api.types.is_datetime64_any_dtype(series):
|
|
109
|
+
return "datetime"
|
|
110
|
+
if (
|
|
111
|
+
series.dtype == object
|
|
112
|
+
or pd.api.types.is_string_dtype(series)
|
|
113
|
+
or isinstance(series.dtype, pd.CategoricalDtype)
|
|
114
|
+
):
|
|
115
|
+
avg_len = series.dropna().astype(str).str.len().mean()
|
|
116
|
+
if pd.notna(avg_len) and avg_len > 50:
|
|
117
|
+
return "text"
|
|
118
|
+
return "categorical"
|
|
119
|
+
return "other"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def compute_column_stats(series: pd.Series, n_rows: int | None = None) -> ColumnStats:
|
|
123
|
+
"""Compute :class:`ColumnStats` for a single Series."""
|
|
124
|
+
n_rows = n_rows if n_rows is not None else len(series)
|
|
125
|
+
non_null = series.dropna()
|
|
126
|
+
count = len(non_null)
|
|
127
|
+
missing = len(series) - count
|
|
128
|
+
unique = series.nunique()
|
|
129
|
+
kind = _classify(series)
|
|
130
|
+
|
|
131
|
+
stats = ColumnStats(
|
|
132
|
+
name=str(series.name),
|
|
133
|
+
dtype=str(series.dtype),
|
|
134
|
+
kind=kind,
|
|
135
|
+
count=count,
|
|
136
|
+
missing=missing,
|
|
137
|
+
missing_pct=missing / len(series) * 100 if len(series) else 0.0,
|
|
138
|
+
unique=unique,
|
|
139
|
+
unique_pct=unique / count * 100 if count else 0.0,
|
|
140
|
+
stratifiable=is_stratifiable(series, n_rows, n_unique=unique),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if kind == "numeric" and count > 0:
|
|
144
|
+
stats.min = float(non_null.min())
|
|
145
|
+
stats.max = float(non_null.max())
|
|
146
|
+
stats.mean = float(non_null.mean())
|
|
147
|
+
stats.std = float(non_null.std()) if count > 1 else 0.0
|
|
148
|
+
stats.median = float(non_null.median())
|
|
149
|
+
finite = non_null.astype(float)
|
|
150
|
+
finite = finite[np.isfinite(finite)]
|
|
151
|
+
if len(finite) > 0:
|
|
152
|
+
counts, edges = np.histogram(finite, bins=10)
|
|
153
|
+
stats.histogram = [int(c) for c in counts]
|
|
154
|
+
stats.histogram_labels = [
|
|
155
|
+
f"{_fmt_num(float(edges[i]))} – {_fmt_num(float(edges[i + 1]))}"
|
|
156
|
+
for i in range(len(counts))
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
# top values only for non-numeric columns: numeric columns render their
|
|
160
|
+
# histogram instead, and stringifying millions of numbers just to throw
|
|
161
|
+
# the result away dominated this function's runtime (~60% on 1M rows)
|
|
162
|
+
if count > 0 and kind != "numeric":
|
|
163
|
+
vc = non_null.astype(str).value_counts().head(8)
|
|
164
|
+
stats.top_values = [(str(v), int(c)) for v, c in vc.items()]
|
|
165
|
+
stats.histogram = [int(c) for c in vc.values]
|
|
166
|
+
stats.histogram_labels = [str(v) for v in vc.index]
|
|
167
|
+
|
|
168
|
+
return stats
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compute_stats(df: pd.DataFrame) -> list[ColumnStats]:
|
|
172
|
+
"""Compute per-column statistics for every column of ``df``."""
|
|
173
|
+
n_rows = len(df)
|
|
174
|
+
return [compute_column_stats(df[col], n_rows) for col in df.columns]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Terminal UI for data-sampler (Textual)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_tui(path: str | None = None, sheet: str | None = None) -> None:
|
|
7
|
+
"""Launch the interactive terminal UI.
|
|
8
|
+
|
|
9
|
+
``path`` (optional) pre-loads a data file, skipping the file picker.
|
|
10
|
+
"""
|
|
11
|
+
from .app import DataSamplerApp
|
|
12
|
+
|
|
13
|
+
DataSamplerApp(path=path, sheet=sheet).run()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["run_tui"]
|