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
dftk/commands/fit_cmd.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.fit_cmd — dftk fit subcommand.
|
|
3
|
+
|
|
4
|
+
Port of dfsmfit.py: OLS / robust (RLM) / weighted (WLS) linear regression
|
|
5
|
+
using statsmodels with R-style formulas.
|
|
6
|
+
|
|
7
|
+
TODO: variable selection — add --select forward/backward/both to perform
|
|
8
|
+
stepwise AIC/BIC-driven predictor selection on top of OLS. The selected
|
|
9
|
+
formula would be logged to stderr and the output table would be identical
|
|
10
|
+
to a normal fit run. Lasso / Ridge / Elastic Net regularisation paths are
|
|
11
|
+
better handled as a separate command (e.g. dftk regpath) using sklearn,
|
|
12
|
+
since the output and cross-validation workflow differ fundamentally.
|
|
13
|
+
|
|
14
|
+
TODO: add support for additional model types via a --model flag:
|
|
15
|
+
--model logit / glm (+ --family binomial/poisson/gamma) — logistic and
|
|
16
|
+
generalised linear models via smf.logit() / smf.glm().
|
|
17
|
+
--model mixedlm (+ --groups COL) — mixed linear models with random effects
|
|
18
|
+
via smf.mixedlm(); output table would need extra columns for random
|
|
19
|
+
effects covariance. res2df() would need a branch for each new type
|
|
20
|
+
since result attributes (rsquared, condnum, etc.) differ.
|
|
21
|
+
|
|
22
|
+
Output modes
|
|
23
|
+
------------
|
|
24
|
+
Default Tidy coefficient table (one row per predictor).
|
|
25
|
+
--summary Print the full statsmodels summary text (no grouping).
|
|
26
|
+
--anova Print the ANOVA table (no grouping, OLS only).
|
|
27
|
+
|
|
28
|
+
Grouping (-g)
|
|
29
|
+
-------------
|
|
30
|
+
Runs the regression independently for each group and concatenates the
|
|
31
|
+
tidy coefficient tables into one output dataframe.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import logging
|
|
36
|
+
|
|
37
|
+
import pandas as pd
|
|
38
|
+
import statsmodels.api as sm
|
|
39
|
+
import statsmodels.formula.api as smf
|
|
40
|
+
import statsmodels.stats.anova as sa
|
|
41
|
+
|
|
42
|
+
from dftk.commands.base import BaseCommand
|
|
43
|
+
from dftk.common.io import check_cols, io
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Core regression helpers (also imported by scale_cmd)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def standard_model(df: pd.DataFrame, args: argparse.Namespace):
|
|
54
|
+
"""Return an OLS or RLM (robust) model, not yet fitted."""
|
|
55
|
+
if getattr(args, "robust", False):
|
|
56
|
+
return sm.robust.robust_linear_model.RLM.from_formula(
|
|
57
|
+
formula=args.formula, data=df, M=sm.robust.norms.HuberT()
|
|
58
|
+
)
|
|
59
|
+
return smf.ols(formula=args.formula, data=df)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def weighted_model(df: pd.DataFrame, args: argparse.Namespace):
|
|
63
|
+
"""Return a WLS model, not yet fitted."""
|
|
64
|
+
return sm.regression.linear_model.WLS.from_formula(
|
|
65
|
+
args.formula, df, weights=df[args.weights]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def regress_it(df: pd.DataFrame, args: argparse.Namespace):
|
|
70
|
+
"""Fit the appropriate model and return the fitted result object."""
|
|
71
|
+
if getattr(args, "weights", None) is not None:
|
|
72
|
+
mod = weighted_model(df, args)
|
|
73
|
+
else:
|
|
74
|
+
mod = standard_model(df, args)
|
|
75
|
+
return mod.fit()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Result → DataFrame
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def res2df(
|
|
84
|
+
res,
|
|
85
|
+
args: argparse.Namespace,
|
|
86
|
+
groupname=None,
|
|
87
|
+
) -> pd.DataFrame:
|
|
88
|
+
"""Convert a fitted statsmodels result to a tidy coefficient DataFrame."""
|
|
89
|
+
is_robust = getattr(args, "robust", False)
|
|
90
|
+
|
|
91
|
+
ci = res.conf_int()
|
|
92
|
+
out_dict = {
|
|
93
|
+
"tag": args.tag,
|
|
94
|
+
"variable": res.model.data.ynames,
|
|
95
|
+
"marker": res.model.data.xnames,
|
|
96
|
+
"effect": res.params,
|
|
97
|
+
"stdev": res.bse,
|
|
98
|
+
"pval": res.pvalues,
|
|
99
|
+
"nobs": res.nobs,
|
|
100
|
+
"cilo": ci[0],
|
|
101
|
+
"cihi": ci[1],
|
|
102
|
+
}
|
|
103
|
+
stat_cols = [
|
|
104
|
+
"tag",
|
|
105
|
+
"variable",
|
|
106
|
+
"marker",
|
|
107
|
+
"effect",
|
|
108
|
+
"stdev",
|
|
109
|
+
"pval",
|
|
110
|
+
"nobs",
|
|
111
|
+
"cilo",
|
|
112
|
+
"cihi",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
if not is_robust:
|
|
116
|
+
out_dict["rsquared"] = res.rsquared
|
|
117
|
+
out_dict["condnum"] = res.condition_number
|
|
118
|
+
stat_cols += ["rsquared", "condnum"]
|
|
119
|
+
|
|
120
|
+
if groupname is None:
|
|
121
|
+
return pd.DataFrame(out_dict)[stat_cols]
|
|
122
|
+
|
|
123
|
+
# Grouped: prepend group-column values
|
|
124
|
+
vals = list(groupname) if isinstance(groupname, tuple) else [groupname]
|
|
125
|
+
grpcols = args.groupcol[:]
|
|
126
|
+
n = len(res.params)
|
|
127
|
+
for grpcol, val in zip(grpcols, vals, strict=False):
|
|
128
|
+
out_dict[grpcol] = [val] * n
|
|
129
|
+
return pd.DataFrame(out_dict)[grpcols + stat_cols]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Command
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_EPILOG = """\
|
|
138
|
+
OUTPUT COLUMNS
|
|
139
|
+
--------------
|
|
140
|
+
tag Value of --tag (label for the result set).
|
|
141
|
+
variable Dependent variable name (left-hand side of formula).
|
|
142
|
+
marker Predictor name (Intercept plus each right-hand side term).
|
|
143
|
+
effect Estimated coefficient.
|
|
144
|
+
stdev Standard error of the coefficient.
|
|
145
|
+
pval Two-sided p-value.
|
|
146
|
+
nobs Number of observations used.
|
|
147
|
+
cilo Lower bound of the 95 % confidence interval.
|
|
148
|
+
cihi Upper bound of the 95 % confidence interval.
|
|
149
|
+
rsquared R² (OLS / WLS only).
|
|
150
|
+
condnum Condition number (OLS / WLS only).
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
EXAMPLES
|
|
154
|
+
--------
|
|
155
|
+
Simple OLS:
|
|
156
|
+
|
|
157
|
+
dftk fit data.tsv -f "y ~ x"
|
|
158
|
+
|
|
159
|
+
Grouped OLS (one regression per group):
|
|
160
|
+
|
|
161
|
+
dftk fit data.tsv -f "y ~ x" -g group
|
|
162
|
+
|
|
163
|
+
Weighted least squares:
|
|
164
|
+
|
|
165
|
+
dftk fit data.tsv -f "y ~ x" -w weight_col
|
|
166
|
+
|
|
167
|
+
Robust regression (HuberT):
|
|
168
|
+
|
|
169
|
+
dftk fit data.tsv -f "y ~ x" --robust
|
|
170
|
+
|
|
171
|
+
Print detailed summary (no grouping):
|
|
172
|
+
|
|
173
|
+
dftk fit data.tsv -f "y ~ x" --summary
|
|
174
|
+
|
|
175
|
+
ANOVA table (OLS, no grouping):
|
|
176
|
+
|
|
177
|
+
dftk fit data.tsv -f "y ~ x + z" --anova
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class FitCommand(BaseCommand):
|
|
182
|
+
name = "fit"
|
|
183
|
+
help = "OLS / robust / weighted linear regression (R-style formula)."
|
|
184
|
+
|
|
185
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
186
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
187
|
+
parser.epilog = _EPILOG
|
|
188
|
+
|
|
189
|
+
self.add_io_arguments(parser)
|
|
190
|
+
|
|
191
|
+
g = parser.add_argument_group("regression options")
|
|
192
|
+
g.add_argument(
|
|
193
|
+
"-f",
|
|
194
|
+
"--formula",
|
|
195
|
+
required=True,
|
|
196
|
+
metavar="FORMULA",
|
|
197
|
+
help="R-style formula, e.g. 'y ~ x + z'.",
|
|
198
|
+
)
|
|
199
|
+
g.add_argument(
|
|
200
|
+
"-w",
|
|
201
|
+
"--weights",
|
|
202
|
+
default=None,
|
|
203
|
+
metavar="COL",
|
|
204
|
+
help="Column name to use as observation weights (WLS).",
|
|
205
|
+
)
|
|
206
|
+
g.add_argument(
|
|
207
|
+
"--tag",
|
|
208
|
+
default="tag",
|
|
209
|
+
metavar="LABEL",
|
|
210
|
+
help="Label added to the 'tag' column in every output row (default: tag).",
|
|
211
|
+
)
|
|
212
|
+
g.add_argument(
|
|
213
|
+
"-r",
|
|
214
|
+
"--robust",
|
|
215
|
+
action="store_true",
|
|
216
|
+
help="Use robust linear regression (RLM with HuberT norm) instead of OLS.",
|
|
217
|
+
)
|
|
218
|
+
g.add_argument(
|
|
219
|
+
"-g",
|
|
220
|
+
"--groupcol",
|
|
221
|
+
nargs="+",
|
|
222
|
+
default=None,
|
|
223
|
+
metavar="COL",
|
|
224
|
+
help="Run a separate regression for each combination of these columns.",
|
|
225
|
+
)
|
|
226
|
+
g.add_argument(
|
|
227
|
+
"-s",
|
|
228
|
+
"--summary",
|
|
229
|
+
action="store_true",
|
|
230
|
+
help="Print the full statsmodels summary (overrides tabular output; no grouping).", # noqa: E501
|
|
231
|
+
)
|
|
232
|
+
g.add_argument(
|
|
233
|
+
"--anova",
|
|
234
|
+
action="store_true",
|
|
235
|
+
help="Print the ANOVA table (OLS only, no grouping).",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
239
|
+
df = io.read(args)
|
|
240
|
+
check_cols(df, args.groupcol, "-g/--groupcol")
|
|
241
|
+
if args.weights is not None:
|
|
242
|
+
check_cols(df, [args.weights], "-w/--weights")
|
|
243
|
+
|
|
244
|
+
if args.groupcol is None:
|
|
245
|
+
res = regress_it(df, args)
|
|
246
|
+
if args.summary:
|
|
247
|
+
print(res.summary())
|
|
248
|
+
return
|
|
249
|
+
if args.anova:
|
|
250
|
+
print(sa.anova_lm(res))
|
|
251
|
+
return
|
|
252
|
+
out = res2df(res, args)
|
|
253
|
+
else:
|
|
254
|
+
grouped = df.groupby(args.groupcol)
|
|
255
|
+
parts = []
|
|
256
|
+
for groupname, groupdf in grouped:
|
|
257
|
+
try:
|
|
258
|
+
res = regress_it(groupdf, args)
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
logger.error("Regression failed for group %s: %s", groupname, exc)
|
|
261
|
+
raise
|
|
262
|
+
parts.append(res2df(res, args, groupname))
|
|
263
|
+
out = pd.concat(parts, ignore_index=True)
|
|
264
|
+
|
|
265
|
+
io.printdf(out, args)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.func_cmd — dftk func subcommand.
|
|
3
|
+
|
|
4
|
+
Applies column transforms, optionally within groups. The main use case is
|
|
5
|
+
adding derived columns to a DataFrame: running totals, quantile bins, group-
|
|
6
|
+
level aggregates broadcast back to the original row count, etc.
|
|
7
|
+
|
|
8
|
+
TRANSFORMS (-t / --transform)
|
|
9
|
+
------------------------------
|
|
10
|
+
sum Group sum broadcast to each row (or simple column sum).
|
|
11
|
+
mean Group mean broadcast to each row.
|
|
12
|
+
min Group min broadcast to each row.
|
|
13
|
+
max Group max broadcast to each row.
|
|
14
|
+
count Non-null count per group broadcast to each row.
|
|
15
|
+
median Group median broadcast to each row.
|
|
16
|
+
std Group standard deviation broadcast to each row.
|
|
17
|
+
cumsum Cumulative sum (within group when -g is given).
|
|
18
|
+
rank Rank within group (average method, ascending).
|
|
19
|
+
pct_rank Percentile rank within group (0–1).
|
|
20
|
+
qcut:N Assign each value to one of N equal-frequency quantile bins.
|
|
21
|
+
The bin label is an integer 1..N.
|
|
22
|
+
|
|
23
|
+
EXAMPLES
|
|
24
|
+
--------
|
|
25
|
+
Cumulative sum of "value":
|
|
26
|
+
|
|
27
|
+
dftk func data.tsv -c value -t cumsum
|
|
28
|
+
|
|
29
|
+
Group mean broadcast (adds "value_mean" unless -d is given):
|
|
30
|
+
|
|
31
|
+
dftk func data.tsv -c value -g group -t mean
|
|
32
|
+
|
|
33
|
+
Quantile bins (quartiles):
|
|
34
|
+
|
|
35
|
+
dftk func data.tsv -c score -t qcut:4 -d score_quartile
|
|
36
|
+
|
|
37
|
+
Multiple transforms in one call:
|
|
38
|
+
|
|
39
|
+
dftk func data.tsv -c expr -t cumsum -d expr_cumsum \\
|
|
40
|
+
| dftk func - -c expr -t mean -g condition -d expr_grpmean
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import re
|
|
45
|
+
|
|
46
|
+
import pandas as pd
|
|
47
|
+
|
|
48
|
+
from dftk.commands.base import BaseCommand
|
|
49
|
+
from dftk.common.io import check_cols, io
|
|
50
|
+
|
|
51
|
+
# Transforms that use groupby().transform()
|
|
52
|
+
_GROUPBY_TRANSFORMS = {"sum", "mean", "min", "max", "count", "median", "std"}
|
|
53
|
+
|
|
54
|
+
# Transforms that use cumsum / rank within an optional group
|
|
55
|
+
_CUMULATIVE_TRANSFORMS = {"cumsum", "rank", "pct_rank"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _apply_transform(
|
|
59
|
+
df: pd.DataFrame,
|
|
60
|
+
col: str,
|
|
61
|
+
transform: str,
|
|
62
|
+
groupcols: list[str] | None,
|
|
63
|
+
destcol: str,
|
|
64
|
+
) -> pd.DataFrame:
|
|
65
|
+
"""Apply *transform* on *col* within optional *groupcols*, storing result in *destcol*.""" # noqa: E501
|
|
66
|
+
|
|
67
|
+
# --- qcut:N ---
|
|
68
|
+
m = re.fullmatch(r"qcut:(\d+)", transform)
|
|
69
|
+
if m:
|
|
70
|
+
n = int(m.group(1))
|
|
71
|
+
if n < 2:
|
|
72
|
+
raise ValueError("qcut requires N >= 2")
|
|
73
|
+
if groupcols:
|
|
74
|
+
|
|
75
|
+
def _qcut_group(s: pd.Series) -> pd.Series:
|
|
76
|
+
return (
|
|
77
|
+
pd.qcut(s, n, labels=False, duplicates="drop").astype("Int64") + 1
|
|
78
|
+
) # noqa: E501
|
|
79
|
+
|
|
80
|
+
df[destcol] = df.groupby(groupcols)[col].transform(_qcut_group)
|
|
81
|
+
else:
|
|
82
|
+
df[destcol] = (
|
|
83
|
+
pd.qcut(df[col], n, labels=False, duplicates="drop").astype("Int64") + 1
|
|
84
|
+
)
|
|
85
|
+
return df
|
|
86
|
+
|
|
87
|
+
# --- groupby().transform() aggregates ---
|
|
88
|
+
if transform in _GROUPBY_TRANSFORMS:
|
|
89
|
+
if groupcols:
|
|
90
|
+
df[destcol] = df.groupby(groupcols)[col].transform(transform)
|
|
91
|
+
else:
|
|
92
|
+
agg_val = getattr(df[col], transform)()
|
|
93
|
+
df[destcol] = agg_val
|
|
94
|
+
return df
|
|
95
|
+
|
|
96
|
+
# --- cumsum ---
|
|
97
|
+
if transform == "cumsum":
|
|
98
|
+
if groupcols:
|
|
99
|
+
df[destcol] = df.groupby(groupcols)[col].transform("cumsum")
|
|
100
|
+
else:
|
|
101
|
+
df[destcol] = df[col].cumsum()
|
|
102
|
+
return df
|
|
103
|
+
|
|
104
|
+
# --- rank ---
|
|
105
|
+
if transform == "rank":
|
|
106
|
+
if groupcols:
|
|
107
|
+
df[destcol] = df.groupby(groupcols)[col].rank(method="average")
|
|
108
|
+
else:
|
|
109
|
+
df[destcol] = df[col].rank(method="average")
|
|
110
|
+
return df
|
|
111
|
+
|
|
112
|
+
# --- pct_rank ---
|
|
113
|
+
if transform == "pct_rank":
|
|
114
|
+
if groupcols:
|
|
115
|
+
df[destcol] = df.groupby(groupcols)[col].rank(method="average", pct=True)
|
|
116
|
+
else:
|
|
117
|
+
df[destcol] = df[col].rank(method="average", pct=True)
|
|
118
|
+
return df
|
|
119
|
+
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Unknown transform {transform!r}. "
|
|
122
|
+
"Valid: sum, mean, min, max, count, median, std, cumsum, rank, pct_rank, qcut:N"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _default_destcol(col: str, transform: str) -> str:
|
|
127
|
+
"""Generate a destination column name from source column + transform."""
|
|
128
|
+
safe = re.sub(r"[^a-zA-Z0-9_]", "_", transform)
|
|
129
|
+
return f"{col}_{safe}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FuncCommand(BaseCommand):
|
|
133
|
+
name = "func"
|
|
134
|
+
help = "Apply column transforms (cumsum, group mean/sum, qcut, …)."
|
|
135
|
+
|
|
136
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
137
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
138
|
+
parser.epilog = __doc__
|
|
139
|
+
|
|
140
|
+
self.add_io_arguments(parser)
|
|
141
|
+
|
|
142
|
+
g = parser.add_argument_group("transform options")
|
|
143
|
+
g.add_argument(
|
|
144
|
+
"-c",
|
|
145
|
+
"--col",
|
|
146
|
+
required=True,
|
|
147
|
+
metavar="COL",
|
|
148
|
+
help="Source column to transform.",
|
|
149
|
+
)
|
|
150
|
+
g.add_argument(
|
|
151
|
+
"-t",
|
|
152
|
+
"--transform",
|
|
153
|
+
required=True,
|
|
154
|
+
metavar="TRANSFORM",
|
|
155
|
+
help=(
|
|
156
|
+
"Transform to apply: sum, mean, min, max, count, median, std, "
|
|
157
|
+
"cumsum, rank, pct_rank, or qcut:N."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
g.add_argument(
|
|
161
|
+
"-d",
|
|
162
|
+
"--destcol",
|
|
163
|
+
default=None,
|
|
164
|
+
metavar="NAME",
|
|
165
|
+
help="Name of the output column (default: <col>_<transform>).",
|
|
166
|
+
)
|
|
167
|
+
g.add_argument(
|
|
168
|
+
"-g",
|
|
169
|
+
"--groupcol",
|
|
170
|
+
nargs="+",
|
|
171
|
+
default=None,
|
|
172
|
+
metavar="COL",
|
|
173
|
+
help="Group by these column(s) before applying the transform.",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
177
|
+
df = io.read(args)
|
|
178
|
+
|
|
179
|
+
check_cols(df, [args.col], "-c/--col")
|
|
180
|
+
check_cols(df, args.groupcol, "-g/--groupcol")
|
|
181
|
+
|
|
182
|
+
destcol = args.destcol or _default_destcol(args.col, args.transform)
|
|
183
|
+
|
|
184
|
+
df = _apply_transform(df, args.col, args.transform, args.groupcol, destcol)
|
|
185
|
+
|
|
186
|
+
io.printdf(df, args)
|
dftk/commands/help.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.help — the ``dftk help`` subcommand.
|
|
3
|
+
|
|
4
|
+
dftk help — list all available subcommands
|
|
5
|
+
dftk help <command> — show full argparse help for one subcommand
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
from dftk.commands.base import BaseCommand
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HelpCommand(BaseCommand):
|
|
14
|
+
"""Show help for dftk subcommands."""
|
|
15
|
+
|
|
16
|
+
name = "help"
|
|
17
|
+
help = "Show detailed help for a subcommand"
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
available_commands: list[BaseCommand] | None = None,
|
|
22
|
+
command_groups: list[tuple[str, list[BaseCommand]]] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.available_commands: list[BaseCommand] = available_commands or []
|
|
25
|
+
self.command_groups: list[tuple[str, list[BaseCommand]]] = command_groups or []
|
|
26
|
+
|
|
27
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"command_name",
|
|
30
|
+
nargs="?",
|
|
31
|
+
metavar="COMMAND",
|
|
32
|
+
help="Name of the subcommand to get help for",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
36
|
+
if args.command_name is None:
|
|
37
|
+
if self.command_groups:
|
|
38
|
+
for header, cmds in self.command_groups:
|
|
39
|
+
print(f"\n{header}:")
|
|
40
|
+
for cmd in cmds:
|
|
41
|
+
print(f" {cmd.name:<16} {cmd.help}")
|
|
42
|
+
else:
|
|
43
|
+
print("Available commands:")
|
|
44
|
+
for cmd in self.available_commands:
|
|
45
|
+
print(f" {cmd.name:<16} {cmd.help}")
|
|
46
|
+
print("\nUse 'dftk help <command>' for detailed help on a command.")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
cmd = next(
|
|
50
|
+
(c for c in self.available_commands if c.name == args.command_name),
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
if cmd is None:
|
|
54
|
+
raise ValueError(f"Unknown command: {args.command_name!r}")
|
|
55
|
+
|
|
56
|
+
# Build a temporary parser for this command so argparse formats the
|
|
57
|
+
# help text exactly as it would appear during normal use.
|
|
58
|
+
temp_parser = argparse.ArgumentParser(
|
|
59
|
+
prog=f"dftk {cmd.name}",
|
|
60
|
+
description=cmd.help,
|
|
61
|
+
)
|
|
62
|
+
cmd.add_arguments(temp_parser)
|
|
63
|
+
temp_parser.print_help()
|