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.
@@ -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()