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/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"]