dataframe-toolkit 0.4.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.
- dataframe_toolkit-0.4.1.dist-info/METADATA +230 -0
- dataframe_toolkit-0.4.1.dist-info/RECORD +42 -0
- dataframe_toolkit-0.4.1.dist-info/WHEEL +4 -0
- dataframe_toolkit-0.4.1.dist-info/entry_points.txt +3 -0
- dftk/__init__.py +4 -0
- dftk/cli.py +72 -0
- dftk/commands/__init__.py +100 -0
- dftk/commands/annotate_cmd.py +165 -0
- dftk/commands/base.py +85 -0
- dftk/commands/binx_cmd.py +130 -0
- dftk/commands/clean_cmd.py +51 -0
- dftk/commands/concat_cmd.py +165 -0
- dftk/commands/corr_cmd.py +281 -0
- dftk/commands/dataset_cmd.py +181 -0
- dftk/commands/describe_cmd.py +360 -0
- dftk/commands/eval_cmd.py +657 -0
- dftk/commands/fit_cmd.py +265 -0
- dftk/commands/func_cmd.py +186 -0
- dftk/commands/help.py +63 -0
- dftk/commands/hist_cmd.py +321 -0
- dftk/commands/interp_cmd.py +271 -0
- dftk/commands/line_cmd.py +320 -0
- dftk/commands/melt_cmd.py +78 -0
- dftk/commands/merge_cmd.py +373 -0
- dftk/commands/pivot_cmd.py +438 -0
- dftk/commands/print_cmd.py +36 -0
- dftk/commands/query_cmd.py +230 -0
- dftk/commands/randvar_cmd.py +199 -0
- dftk/commands/sample_cmd.py +126 -0
- dftk/commands/scale_cmd.py +299 -0
- dftk/commands/scat_cmd.py +278 -0
- dftk/commands/split_cmd.py +130 -0
- dftk/commands/stat_cmd.py +289 -0
- dftk/commands/test_cmd.py +235 -0
- dftk/commands/wstat_cmd.py +163 -0
- dftk/common/__init__.py +8 -0
- dftk/common/io.py +690 -0
- dftk/common/plot.py +566 -0
- dftk/common/seed.py +25 -0
- dftk/common/seq.py +150 -0
- dftk/common/stats.py +214 -0
- dftk/io.py +10 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.describe_cmd — dftk describe subcommand.
|
|
3
|
+
|
|
4
|
+
Profiles each column in a DataFrame: data type, basic statistics, missing
|
|
5
|
+
values, and automatically generated quality/distribution notes.
|
|
6
|
+
|
|
7
|
+
Output columns (one row per input column)
|
|
8
|
+
------------------------------------------
|
|
9
|
+
name, type, count, missing_pct, n_unique,
|
|
10
|
+
mean, std, min, p25, median, p75, max, skew, kurtosis,
|
|
11
|
+
top, top_freq_pct, notes
|
|
12
|
+
|
|
13
|
+
Numeric columns populate mean…kurtosis; top/top_freq_pct are blank.
|
|
14
|
+
Categorical/boolean columns populate top/top_freq_pct; numeric stats are blank.
|
|
15
|
+
|
|
16
|
+
Notes flags
|
|
17
|
+
-----------
|
|
18
|
+
all_missing — column has no non-null values
|
|
19
|
+
high_missing — > 20 % missing
|
|
20
|
+
constant — only one distinct value
|
|
21
|
+
near_constant — dominant value covers > 95 % of non-null rows
|
|
22
|
+
possible_id — every non-null value is unique (n_unique == n_rows)
|
|
23
|
+
high_cardinality — categorical with > 50 distinct values
|
|
24
|
+
outliers_iqr — at least one value outside 1.5 × IQR fences
|
|
25
|
+
approx_normal — |skew| < 0.5 and |excess_kurtosis| < 1
|
|
26
|
+
right_skewed — skew > 1
|
|
27
|
+
left_skewed — skew < -1
|
|
28
|
+
heavy_tailed — excess kurtosis > 3
|
|
29
|
+
bimodal_hint — bimodality coefficient > 0.555 (Pfister et al.)
|
|
30
|
+
|
|
31
|
+
Optional flags
|
|
32
|
+
--------------
|
|
33
|
+
--summary Print a human-readable summary paragraph to stderr.
|
|
34
|
+
--correlations Include pairwise correlation analysis in the summary.
|
|
35
|
+
--corr-threshold Minimum |r| to report (default: 0.7).
|
|
36
|
+
|
|
37
|
+
Example
|
|
38
|
+
-------
|
|
39
|
+
dftk describe data.tsv
|
|
40
|
+
dftk describe data.tsv --summary --correlations
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import sys
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
import pandas as pd
|
|
48
|
+
|
|
49
|
+
from dftk.commands.base import BaseCommand
|
|
50
|
+
from dftk.common.io import io
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Column type detection
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _col_type(col: pd.Series) -> str:
|
|
58
|
+
if pd.api.types.is_bool_dtype(col):
|
|
59
|
+
return "boolean"
|
|
60
|
+
if pd.api.types.is_numeric_dtype(col):
|
|
61
|
+
return "numeric"
|
|
62
|
+
if pd.api.types.is_datetime64_any_dtype(col):
|
|
63
|
+
return "datetime"
|
|
64
|
+
return "categorical"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Single-column profiling
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _profile_col(col: pd.Series, n_rows: int) -> dict:
|
|
73
|
+
"""Return a dict describing one column."""
|
|
74
|
+
col_type = _col_type(col)
|
|
75
|
+
n_missing = int(col.isna().sum())
|
|
76
|
+
missing_pct = round(100.0 * n_missing / n_rows, 1) if n_rows > 0 else 0.0
|
|
77
|
+
n_valid = n_rows - n_missing
|
|
78
|
+
n_unique = int(col.nunique())
|
|
79
|
+
|
|
80
|
+
row: dict = dict(
|
|
81
|
+
name=col.name,
|
|
82
|
+
type=col_type,
|
|
83
|
+
count=n_valid,
|
|
84
|
+
missing_pct=missing_pct,
|
|
85
|
+
n_unique=n_unique,
|
|
86
|
+
mean=np.nan,
|
|
87
|
+
std=np.nan,
|
|
88
|
+
min=np.nan,
|
|
89
|
+
p25=np.nan,
|
|
90
|
+
median=np.nan,
|
|
91
|
+
p75=np.nan,
|
|
92
|
+
max=np.nan,
|
|
93
|
+
skew=np.nan,
|
|
94
|
+
kurtosis=np.nan,
|
|
95
|
+
top="",
|
|
96
|
+
top_freq_pct=np.nan,
|
|
97
|
+
notes="",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
notes: list[str] = []
|
|
101
|
+
|
|
102
|
+
if missing_pct == 100.0:
|
|
103
|
+
notes.append("all_missing")
|
|
104
|
+
row["notes"] = ",".join(notes)
|
|
105
|
+
return row
|
|
106
|
+
if missing_pct > 20.0:
|
|
107
|
+
notes.append("high_missing")
|
|
108
|
+
|
|
109
|
+
if n_unique == 0:
|
|
110
|
+
row["notes"] = ",".join(notes)
|
|
111
|
+
return row
|
|
112
|
+
|
|
113
|
+
if n_unique == 1:
|
|
114
|
+
notes.append("constant")
|
|
115
|
+
|
|
116
|
+
if n_unique == n_rows and n_missing == 0:
|
|
117
|
+
notes.append("possible_id")
|
|
118
|
+
|
|
119
|
+
if col_type == "numeric":
|
|
120
|
+
vals = col.dropna()
|
|
121
|
+
|
|
122
|
+
row["mean"] = float(vals.mean())
|
|
123
|
+
row["std"] = float(vals.std())
|
|
124
|
+
row["min"] = float(vals.min())
|
|
125
|
+
row["p25"] = float(vals.quantile(0.25))
|
|
126
|
+
row["median"] = float(vals.median())
|
|
127
|
+
row["p75"] = float(vals.quantile(0.75))
|
|
128
|
+
row["max"] = float(vals.max())
|
|
129
|
+
|
|
130
|
+
if len(vals) >= 4:
|
|
131
|
+
skew = float(vals.skew())
|
|
132
|
+
kurt = float(vals.kurtosis()) # excess kurtosis (normal = 0)
|
|
133
|
+
row["skew"] = round(skew, 4)
|
|
134
|
+
row["kurtosis"] = round(kurt, 4)
|
|
135
|
+
|
|
136
|
+
# IQR outlier detection
|
|
137
|
+
q1, q3 = row["p25"], row["p75"]
|
|
138
|
+
iqr = q3 - q1
|
|
139
|
+
if (
|
|
140
|
+
iqr > 0
|
|
141
|
+
and int(((vals < q1 - 1.5 * iqr) | (vals > q3 + 1.5 * iqr)).sum()) > 0
|
|
142
|
+
):
|
|
143
|
+
notes.append("outliers_iqr")
|
|
144
|
+
|
|
145
|
+
# Distribution shape
|
|
146
|
+
if abs(skew) < 0.5 and abs(kurt) < 1.0:
|
|
147
|
+
notes.append("approx_normal")
|
|
148
|
+
else:
|
|
149
|
+
if skew > 1.0:
|
|
150
|
+
notes.append("right_skewed")
|
|
151
|
+
elif skew < -1.0:
|
|
152
|
+
notes.append("left_skewed")
|
|
153
|
+
if kurt > 3.0:
|
|
154
|
+
notes.append("heavy_tailed")
|
|
155
|
+
|
|
156
|
+
# Bimodality coefficient (Pfister et al. 2013)
|
|
157
|
+
n = len(vals)
|
|
158
|
+
if n > 5:
|
|
159
|
+
denom = kurt + 3.0 * (n - 1) ** 2 / ((n - 2) * (n - 3))
|
|
160
|
+
if abs(denom) > 1e-9 and (skew**2 + 1.0) / denom > 0.555:
|
|
161
|
+
notes.append("bimodal_hint")
|
|
162
|
+
|
|
163
|
+
else:
|
|
164
|
+
vc = col.value_counts()
|
|
165
|
+
if len(vc) > 0:
|
|
166
|
+
row["top"] = str(vc.index[0])
|
|
167
|
+
row["top_freq_pct"] = (
|
|
168
|
+
round(100.0 * int(vc.iloc[0]) / n_valid, 1) if n_valid > 0 else 0.0
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if n_unique > 1 and row["top_freq_pct"] > 95.0:
|
|
172
|
+
notes.append("near_constant")
|
|
173
|
+
|
|
174
|
+
if col_type == "categorical" and n_unique > 50:
|
|
175
|
+
notes.append("high_cardinality")
|
|
176
|
+
|
|
177
|
+
row["notes"] = ",".join(notes)
|
|
178
|
+
return row
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Correlation analysis
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _correlations(df: pd.DataFrame, threshold: float = 0.7) -> list[tuple]:
|
|
187
|
+
"""Return (col_a, col_b, r) pairs where |r| >= threshold, sorted by |r|."""
|
|
188
|
+
num_cols = df.select_dtypes(include="number").columns.tolist()
|
|
189
|
+
if len(num_cols) < 2:
|
|
190
|
+
return []
|
|
191
|
+
corr = df[num_cols].corr()
|
|
192
|
+
pairs = []
|
|
193
|
+
for i, a in enumerate(num_cols):
|
|
194
|
+
for b in num_cols[i + 1 :]:
|
|
195
|
+
r = corr.loc[a, b]
|
|
196
|
+
if not np.isnan(r) and abs(r) >= threshold:
|
|
197
|
+
pairs.append((a, b, round(float(r), 4)))
|
|
198
|
+
pairs.sort(key=lambda x: -abs(x[2]))
|
|
199
|
+
return pairs
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Human-readable summary
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _flag_names(profile: pd.DataFrame, flag: str) -> list[str]:
|
|
208
|
+
return [str(r["name"]) for _, r in profile.iterrows() if flag in str(r["notes"])]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _generate_summary(
|
|
212
|
+
profile: pd.DataFrame,
|
|
213
|
+
corr_pairs: list[tuple],
|
|
214
|
+
n_rows: int,
|
|
215
|
+
n_cols: int,
|
|
216
|
+
) -> str:
|
|
217
|
+
lines = [f"Dataset: {n_rows} rows × {n_cols} columns"]
|
|
218
|
+
|
|
219
|
+
missing = profile[profile["missing_pct"] > 0].sort_values(
|
|
220
|
+
"missing_pct", ascending=False
|
|
221
|
+
)
|
|
222
|
+
if len(missing):
|
|
223
|
+
parts = [f"'{r['name']}' ({r['missing_pct']}%)" for _, r in missing.iterrows()]
|
|
224
|
+
lines.append("Missing data: " + ", ".join(parts))
|
|
225
|
+
|
|
226
|
+
for flag, label in [
|
|
227
|
+
("constant", "Constant columns"),
|
|
228
|
+
("near_constant", "Near-constant columns"),
|
|
229
|
+
("possible_id", "Possible identifiers (all unique)"),
|
|
230
|
+
("high_cardinality", "High cardinality"),
|
|
231
|
+
]:
|
|
232
|
+
cols = _flag_names(profile, flag)
|
|
233
|
+
if cols:
|
|
234
|
+
lines.append(f"{label}: " + ", ".join(f"'{c}'" for c in cols))
|
|
235
|
+
|
|
236
|
+
dist_parts = []
|
|
237
|
+
for _, r in profile[profile["type"] == "numeric"].iterrows():
|
|
238
|
+
n = str(r["notes"])
|
|
239
|
+
tags = []
|
|
240
|
+
if "approx_normal" in n:
|
|
241
|
+
tags.append("approx. normal")
|
|
242
|
+
if "right_skewed" in n:
|
|
243
|
+
tags.append("right-skewed")
|
|
244
|
+
if "left_skewed" in n:
|
|
245
|
+
tags.append("left-skewed")
|
|
246
|
+
if "heavy_tailed" in n:
|
|
247
|
+
tags.append("heavy-tailed")
|
|
248
|
+
if "bimodal_hint" in n:
|
|
249
|
+
tags.append("possibly bimodal")
|
|
250
|
+
if "outliers_iqr" in n:
|
|
251
|
+
tags.append("has outliers (IQR)")
|
|
252
|
+
if tags:
|
|
253
|
+
dist_parts.append(f"'{r['name']}': {', '.join(tags)}")
|
|
254
|
+
if dist_parts:
|
|
255
|
+
lines.append("Distributions: " + "; ".join(dist_parts))
|
|
256
|
+
|
|
257
|
+
if corr_pairs:
|
|
258
|
+
parts = [f"'{a}' ↔ '{b}' (r={r})" for a, b, r in corr_pairs]
|
|
259
|
+
lines.append("High correlations: " + ", ".join(parts))
|
|
260
|
+
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Command
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class DescribeCommand(BaseCommand):
|
|
270
|
+
@property
|
|
271
|
+
def name(self) -> str:
|
|
272
|
+
return "describe"
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def help(self) -> str:
|
|
276
|
+
return "Profile DataFrame columns: types, distributions, and quality flags"
|
|
277
|
+
|
|
278
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
279
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
280
|
+
parser.epilog = """\
|
|
281
|
+
OUTPUT COLUMNS
|
|
282
|
+
--------------
|
|
283
|
+
One row per input column:
|
|
284
|
+
|
|
285
|
+
name column name
|
|
286
|
+
type numeric | categorical | boolean | datetime
|
|
287
|
+
count number of non-null values
|
|
288
|
+
missing_pct percentage of null values
|
|
289
|
+
n_unique number of distinct values
|
|
290
|
+
mean std (numeric only)
|
|
291
|
+
min p25 median p75 max
|
|
292
|
+
skew kurtosis excess kurtosis (normal distribution = 0)
|
|
293
|
+
top most frequent value (categorical/boolean)
|
|
294
|
+
top_freq_pct frequency of top value as a percentage
|
|
295
|
+
notes comma-separated quality/distribution flags (see below)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
NOTES FLAGS
|
|
299
|
+
-----------
|
|
300
|
+
all_missing no non-null values
|
|
301
|
+
high_missing > 20 % of values are null
|
|
302
|
+
constant only one distinct value
|
|
303
|
+
near_constant dominant value covers > 95 % of non-null rows
|
|
304
|
+
possible_id every non-null value is unique (likely an identifier)
|
|
305
|
+
high_cardinality categorical column with > 50 distinct values
|
|
306
|
+
outliers_iqr at least one value outside the 1.5 × IQR fences
|
|
307
|
+
approx_normal |skew| < 0.5 and |excess kurtosis| < 1
|
|
308
|
+
right_skewed skew > 1
|
|
309
|
+
left_skewed skew < -1
|
|
310
|
+
heavy_tailed excess kurtosis > 3
|
|
311
|
+
bimodal_hint bimodality coefficient > 0.555 (Pfister et al. 2013)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
EXAMPLES
|
|
315
|
+
--------
|
|
316
|
+
dftk describe data.tsv
|
|
317
|
+
dftk describe data.tsv --summary --correlations
|
|
318
|
+
dftk describe data.tsv --summary --correlations --corr-threshold 0.5
|
|
319
|
+
dftk dataset iris | dftk describe - --summary --correlations
|
|
320
|
+
"""
|
|
321
|
+
io.parser_read(parser)
|
|
322
|
+
|
|
323
|
+
g = parser.add_argument_group("describe options")
|
|
324
|
+
g.add_argument(
|
|
325
|
+
"--summary",
|
|
326
|
+
help="Print a human-readable summary paragraph to stderr.",
|
|
327
|
+
action="store_true",
|
|
328
|
+
)
|
|
329
|
+
g.add_argument(
|
|
330
|
+
"--correlations",
|
|
331
|
+
help="Compute pairwise correlations and include them in --summary output.",
|
|
332
|
+
action="store_true",
|
|
333
|
+
)
|
|
334
|
+
g.add_argument(
|
|
335
|
+
"--corr-threshold",
|
|
336
|
+
help="Minimum |r| to report as a high correlation (default: 0.7).",
|
|
337
|
+
type=float,
|
|
338
|
+
default=0.7,
|
|
339
|
+
metavar="FLOAT",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
io.parser_output(parser)
|
|
343
|
+
|
|
344
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
345
|
+
df = io.read(args)
|
|
346
|
+
n_rows, n_cols = df.shape
|
|
347
|
+
|
|
348
|
+
profile = pd.DataFrame([_profile_col(df[c], n_rows) for c in df.columns])
|
|
349
|
+
|
|
350
|
+
corr_pairs: list[tuple] = []
|
|
351
|
+
if getattr(args, "correlations", False):
|
|
352
|
+
threshold = getattr(args, "corr_threshold", 0.7)
|
|
353
|
+
corr_pairs = _correlations(df, threshold)
|
|
354
|
+
|
|
355
|
+
if getattr(args, "summary", False):
|
|
356
|
+
print(
|
|
357
|
+
_generate_summary(profile, corr_pairs, n_rows, n_cols), file=sys.stderr
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
io.printdf(profile, args)
|