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,278 @@
1
+ """
2
+ dftk.commands.scat_cmd — dftk scat subcommand.
3
+
4
+ Scatter plot with optional OLS / robust regression line overlay.
5
+
6
+ Port of dfscat.py with improvements:
7
+ - --groupcol colour-codes points by a column
8
+ - --subgraphcol splits into a subplot grid
9
+ - --size / --fontsize publication-quality figure presets
10
+ - --fit overlays an OLS (or robust) regression line via fit_cmd.regress_it
11
+ - --sizecol point size from a data column (was --area)
12
+ - --colorcol continuous colour mapping from a data column (was --color)
13
+ """
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ import matplotlib.lines as mlines
19
+ import numpy as np
20
+
21
+ from dftk.commands.base import BaseCommand
22
+ from dftk.common.io import check_cols, io
23
+ from dftk.common.plot import (
24
+ add_figure_arguments,
25
+ add_group_arguments,
26
+ add_legend_arguments,
27
+ add_xy_arguments,
28
+ apply_labels,
29
+ apply_limits,
30
+ apply_style,
31
+ apply_ticks,
32
+ make_figure,
33
+ make_grouplabel,
34
+ save_or_show,
35
+ subgraph_groups,
36
+ subgraph_layout,
37
+ )
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Fit overlay
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ def _fit_overlay(ax, x_series, y_series, args):
45
+ """Fit a regression line and overlay it on ax. Prints summary to stdout."""
46
+ import pandas as pd
47
+
48
+ from dftk.commands.fit_cmd import regress_it
49
+
50
+ df_fit = pd.DataFrame({"_x": x_series.values, "_y": y_series.values}).dropna()
51
+ fit_args = argparse.Namespace(
52
+ formula="_y ~ _x" if not args.nointercept else "_y ~ _x - 1",
53
+ weights=None,
54
+ robust=getattr(args, "robust", False),
55
+ )
56
+ res = regress_it(df_fit, fit_args)
57
+ print(res.summary(), file=sys.stderr)
58
+
59
+ xmin, xmax = ax.get_xlim()
60
+ X_plot = np.linspace(xmin, xmax, 200)
61
+
62
+ if args.nointercept:
63
+ slope = res.params["_x"]
64
+ Y_plot = slope * X_plot
65
+ label_text = f"{slope:+.3g} × {args.xcol}"
66
+ else:
67
+ intercept = res.params["Intercept"]
68
+ slope = res.params["_x"]
69
+ Y_plot = intercept + slope * X_plot
70
+ label_text = f"{intercept:.3g}{slope:+.3g} × {args.xcol}"
71
+
72
+ if getattr(args, "pvalue", False):
73
+ pval = res.pvalues.get("_x", res.pvalues.iloc[-1])
74
+ label_text += f" -log10(p)={-np.log10(pval):.2g}"
75
+
76
+ ax.plot(X_plot, Y_plot, "r--", linewidth=1)
77
+ ax.legend(
78
+ handles=[mlines.Line2D([], [], color="red", linestyle="--", label=label_text)],
79
+ loc=0,
80
+ )
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Single-axes scatter (reused per subplot)
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ def _scatter_ax(ax, df, args, title_suffix: str = ""):
89
+ """Draw scatter on a single Axes, with optional --groupcol colouring."""
90
+ import matplotlib.pyplot as plt
91
+
92
+ xcol = args.xcol
93
+ ycol = args.ycol
94
+ legendloc = tuple(args.legendloc) if args.legendloc else "best"
95
+
96
+ scatter_kw = dict(marker=getattr(args, "marker", "o"), alpha=0.7)
97
+
98
+ if args.groupcol is not None:
99
+ for groupname, gdf in subgraph_groups(
100
+ df, args.groupcol, getattr(args, "groupcolorder", None)
101
+ ):
102
+ label = make_grouplabel(
103
+ groupname, args.groupcol, getattr(args, "groupcolformat", None)
104
+ )
105
+ kw = dict(scatter_kw)
106
+ if args.sizecol:
107
+ kw["s"] = gdf[args.sizecol]
108
+ ax.scatter(gdf[xcol], gdf[ycol], label=label, **kw)
109
+ ax.legend(loc=legendloc, title=args.legendtitle)
110
+ else:
111
+ kw = dict(scatter_kw)
112
+ if args.sizecol:
113
+ kw["s"] = df[args.sizecol]
114
+ if args.colorcol:
115
+ sc = ax.scatter(
116
+ df[xcol], df[ycol], c=df[args.colorcol], cmap="viridis", **kw
117
+ )
118
+ plt.colorbar(sc, ax=ax, label=args.colorcol)
119
+ else:
120
+ label = args.legend or ""
121
+ ax.scatter(df[xcol], df[ycol], label=label if label else None, **kw)
122
+ if label:
123
+ ax.legend(loc=legendloc, title=args.legendtitle)
124
+
125
+ if getattr(args, "fit", False) and args.groupcol is None:
126
+ _fit_overlay(ax, df[xcol], df[ycol], args)
127
+
128
+ apply_limits(ax, args)
129
+ apply_ticks(ax, args)
130
+ apply_labels(ax, args, default_xlabel=xcol or "", default_ylabel=ycol or "")
131
+ if title_suffix:
132
+ existing = ax.get_title()
133
+ ax.set_title(f"{existing} [{title_suffix}]" if existing else title_suffix)
134
+
135
+ ax.grid(True, linewidth=0.4, alpha=0.5)
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Command
140
+ # ---------------------------------------------------------------------------
141
+
142
+ _EPILOG = """\
143
+ EXAMPLES
144
+ --------
145
+ Basic scatter:
146
+
147
+ dftk scat data.tsv -x weight -y height
148
+
149
+ Colour by group:
150
+
151
+ dftk scat data.tsv -x x -y y -g group
152
+
153
+ Subplot grid by condition, colour by treatment:
154
+
155
+ dftk scat data.tsv -x x -y y --subgraphcol condition -g treatment
156
+
157
+ Fit OLS line:
158
+
159
+ dftk scat data.tsv -x x -y y --fit
160
+
161
+ Publication figure (Nature single column, PDF):
162
+
163
+ dftk scat data.tsv -x x -y y --size single --fontsize publication -f fig.pdf
164
+
165
+ Bubble chart (point size from column):
166
+
167
+ dftk scat data.tsv -x x -y y --sizecol area_col
168
+
169
+ Continuous colour mapping:
170
+
171
+ dftk scat data.tsv -x x -y y --colorcol score
172
+ """
173
+
174
+
175
+ class ScatCommand(BaseCommand):
176
+ name = "scat"
177
+ help = "Scatter plot with optional regression line overlay."
178
+
179
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
180
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
181
+ parser.epilog = _EPILOG
182
+
183
+ self.add_read_arguments(parser)
184
+ add_xy_arguments(parser)
185
+ add_figure_arguments(parser)
186
+ add_group_arguments(parser)
187
+ add_legend_arguments(parser)
188
+
189
+ s = parser.add_argument_group("scatter options")
190
+ s.add_argument(
191
+ "-m",
192
+ "--marker",
193
+ default="o",
194
+ metavar="MARKER",
195
+ help="Matplotlib marker style (default: o).",
196
+ )
197
+ s.add_argument(
198
+ "--sizecol",
199
+ default=None,
200
+ metavar="COL",
201
+ help="Column to use for point sizes (bubble chart).",
202
+ )
203
+ s.add_argument(
204
+ "--colorcol",
205
+ default=None,
206
+ metavar="COL",
207
+ help="Column for continuous colour mapping.",
208
+ )
209
+
210
+ f = parser.add_argument_group("fit options")
211
+ f.add_argument(
212
+ "--fit",
213
+ action="store_true",
214
+ help="Overlay an OLS regression line. Prints summary to stderr.",
215
+ )
216
+ f.add_argument(
217
+ "-r",
218
+ "--robust",
219
+ action="store_true",
220
+ help="Use robust regression (RLM) for --fit.",
221
+ )
222
+ f.add_argument(
223
+ "--nointercept", action="store_true", help="Suppress intercept in --fit."
224
+ )
225
+ f.add_argument(
226
+ "--pvalue",
227
+ action="store_true",
228
+ help="Add −log₁₀(p) to the fit legend label.",
229
+ )
230
+
231
+ def execute(self, args: argparse.Namespace) -> None:
232
+ if not args.xcol or not args.ycol:
233
+ raise ValueError("--xcol (-x) and --ycol (-y) are required.")
234
+
235
+ df = io.read(args)
236
+ check_cols(df, [args.xcol, args.ycol], "-x/-y")
237
+ check_cols(df, args.groupcol, "-g/--groupcol")
238
+ check_cols(df, args.subgraphcol, "--subgraphcol")
239
+ if args.sizecol:
240
+ check_cols(df, [args.sizecol], "--sizecol")
241
+ if args.colorcol:
242
+ check_cols(df, [args.colorcol], "--colorcol")
243
+
244
+ # Use non-interactive backend when saving to file
245
+ if args.file:
246
+ import matplotlib
247
+
248
+ matplotlib.use("Agg")
249
+
250
+ import matplotlib.pyplot as plt
251
+
252
+ if args.usetex:
253
+ plt.rc("text", usetex=True)
254
+
255
+ apply_style(args)
256
+
257
+ if args.subgraphcol is not None:
258
+ groups = subgraph_groups(
259
+ df, args.subgraphcol, getattr(args, "subgraphorder", None)
260
+ )
261
+ nrows, ncols = subgraph_layout(len(groups), args.ncols)
262
+ fig, axes = make_figure(args, nrows=nrows, ncols=ncols)
263
+ for idx, (groupname, gdf) in enumerate(groups):
264
+ ax = axes[idx // ncols][idx % ncols]
265
+ suffix = make_grouplabel(
266
+ groupname, args.subgraphcol, getattr(args, "subgraphformat", None)
267
+ )
268
+ _scatter_ax(ax, gdf, args, title_suffix=suffix)
269
+ # Hide unused axes
270
+ for idx in range(len(groups), nrows * ncols):
271
+ axes[idx // ncols][idx % ncols].set_visible(False)
272
+ fig.tight_layout()
273
+ else:
274
+ fig, axes = make_figure(args)
275
+ _scatter_ax(axes[0][0], df, args)
276
+ fig.tight_layout()
277
+
278
+ save_or_show(fig, args)
@@ -0,0 +1,130 @@
1
+ """
2
+ dftk.commands.split_cmd — dftk split subcommand.
3
+
4
+ Port of dfsplit.py: split a dataframe into one TSV file per group.
5
+ """
6
+
7
+ import argparse
8
+ import copy
9
+ import os
10
+
11
+ from dftk.commands.base import BaseCommand
12
+ from dftk.common.io import check_cols, io
13
+
14
+
15
+ def _make_filename(groupname, prefix: str, suffix: str) -> str:
16
+ tag = (
17
+ "_".join(str(x) for x in groupname)
18
+ if isinstance(groupname, tuple)
19
+ else str(groupname)
20
+ )
21
+ return f"{prefix}{tag}{suffix}"
22
+
23
+
24
+ class _SafeDict(dict):
25
+ """Format-map dict that leaves unknown keys unreplaced."""
26
+
27
+ def __missing__(self, key):
28
+ return "{" + key + "}"
29
+
30
+
31
+ def _split(df, args: argparse.Namespace) -> None:
32
+ for groupname, groupdf in df.groupby(args.groups):
33
+ if args.template is not None:
34
+ key = (groupname,) if not isinstance(groupname, tuple) else groupname
35
+ group_dict = dict(zip(args.groups, key, strict=False))
36
+ filename = args.template.format_map(_SafeDict(group_dict))
37
+ else:
38
+ filename = _make_filename(groupname, args.prefix, args.suffix)
39
+
40
+ if args.noclobber and os.path.exists(filename):
41
+ continue
42
+
43
+ file_args = copy.copy(args)
44
+ file_args.output = filename
45
+ io.printdf(groupdf, file_args)
46
+
47
+
48
+ _EPILOG = """\
49
+ FILENAME PATTERNS
50
+ -----------------
51
+ By default output files are named: <prefix><group_key><suffix>
52
+
53
+ dftk split data.tsv -g country -p out/ -s .tsv
54
+ # → out/Iceland.tsv, out/Norway.tsv, ...
55
+
56
+ With --template, use {col} placeholders for the group column values:
57
+
58
+ dftk split data.tsv -g year month --template data_{year}_{month}.tsv
59
+
60
+ Unknown placeholders are left as-is (safe format_map).
61
+
62
+ OPTIONS
63
+ -------
64
+ --noclobber Skip files that already exist instead of overwriting.
65
+
66
+ Note: -o/--output is not used by split; output paths are always determined
67
+ by -p/--prefix, -s/--suffix, or --template.
68
+
69
+ EXAMPLES
70
+ --------
71
+ dftk split data.tsv -g group
72
+ dftk split data.tsv -g country year -p results/ -s .tsv
73
+ dftk split data.tsv -g category --template out/{category}.tsv
74
+ """
75
+
76
+
77
+ class SplitCommand(BaseCommand):
78
+ @property
79
+ def name(self) -> str:
80
+ return "split"
81
+
82
+ @property
83
+ def help(self) -> str:
84
+ return "Split a dataframe into one file per group."
85
+
86
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
87
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
88
+ parser.epilog = _EPILOG
89
+
90
+ self.add_io_arguments(parser)
91
+
92
+ g = parser.add_argument_group("split options")
93
+ g.add_argument(
94
+ "-g",
95
+ "--groups",
96
+ nargs="+",
97
+ required=True,
98
+ metavar="COL",
99
+ help="Column(s) to group and split by.",
100
+ )
101
+ g.add_argument(
102
+ "-p",
103
+ "--prefix",
104
+ default="split-",
105
+ metavar="PREFIX",
106
+ help="Filename prefix (default: 'split-').",
107
+ )
108
+ g.add_argument(
109
+ "-s",
110
+ "--suffix",
111
+ default="",
112
+ metavar="SUFFIX",
113
+ help="Filename suffix / extension (default: none).",
114
+ )
115
+ g.add_argument(
116
+ "--template",
117
+ default=None,
118
+ metavar="TEMPLATE",
119
+ help="Filename template with {col} placeholders, e.g. out/{group}.tsv.",
120
+ )
121
+ g.add_argument(
122
+ "--noclobber",
123
+ action="store_true",
124
+ help="Skip output files that already exist.",
125
+ )
126
+
127
+ def execute(self, args: argparse.Namespace) -> None:
128
+ df = io.read(args)
129
+ check_cols(df, args.groups, "-g/--groups")
130
+ _split(df, args)
@@ -0,0 +1,289 @@
1
+ """
2
+ dftk.commands.stat_cmd — the ``dftk stat`` subcommand.
3
+
4
+ Computes descriptive statistics for one or more numeric columns, optionally
5
+ within groups. Output always includes a ``name`` column identifying which
6
+ input column each row describes, giving a consistent schema regardless of
7
+ how many columns are requested.
8
+
9
+ Output columns
10
+ --------------
11
+ [groupcol(s),] name, count, sum, mean, std, min, max, median,
12
+ cilo, cihi, sem, skew, kurt
13
+
14
+ Bootstrap mode
15
+ --------------
16
+ With ``--bootstrap N`` the full stat computation is repeated N times on
17
+ resampled data. Each iteration appends a ``samplenum`` column and output
18
+ blocks are streamed without repeating the header, making it easy to pipe
19
+ into ``dftk pivot`` to aggregate across samples.
20
+
21
+ Example
22
+ -------
23
+ dftk stat data.tsv -c height weight -g sex
24
+ dftk stat data.tsv -c value -g group --bootstrap 1000 --randomseed 42 -o \\
25
+ | dftk pivot -i group -v value_mean -o \\
26
+ | dftk print
27
+ """
28
+
29
+ import argparse
30
+ import logging
31
+
32
+ import numpy as np
33
+ import pandas as pd
34
+
35
+ from dftk.commands.base import BaseCommand
36
+ from dftk.common.io import check_cols, io
37
+ from dftk.common.seed import normalize_seed
38
+
39
+ logger = logging.getLogger("dftk")
40
+
41
+ _CI_METHODS = ["linear", "lower", "higher", "midpoint", "nearest"]
42
+
43
+ # Canonical order of statistic columns in the output.
44
+ _STAT_NAMES = [
45
+ "count",
46
+ "sum",
47
+ "mean",
48
+ "std",
49
+ "min",
50
+ "max",
51
+ "median",
52
+ "cilo",
53
+ "cihi",
54
+ "sem",
55
+ "skew",
56
+ "kurt",
57
+ ]
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Helpers
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def _samplefraction(x: str) -> float:
66
+ """argparse type validator for --samplefrac."""
67
+ try:
68
+ x = float(x)
69
+ except ValueError:
70
+ raise argparse.ArgumentTypeError(f"{x!r} is not a valid float") from None
71
+ if not (0.0 <= x <= 1.0):
72
+ raise argparse.ArgumentTypeError(f"{x} is not in range [0.0, 1.0]")
73
+ return x
74
+
75
+
76
+ def _ci(name: str, level: float, method: str):
77
+ """Return a named percentile aggregation function for use with pandas agg.
78
+
79
+ The function name is set explicitly so pandas uses it as the output
80
+ column name rather than the generic ``<lambda>`` or ``cifunction``.
81
+ """
82
+
83
+ def cifunction(a):
84
+ return np.percentile(a, q=level, method=method)
85
+
86
+ cifunction.__name__ = name
87
+ return cifunction
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Core computation
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ def _compute_stats(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
96
+ """Return a dataframe of descriptive statistics.
97
+
98
+ Always produces a ``name`` column identifying the source column, giving
99
+ a consistent schema regardless of how many columns are in ``args.cols``.
100
+ """
101
+ cols: list[str] = args.cols
102
+ groupcol: list[str] | None = args.groupcol
103
+
104
+ conf_lo = (100.0 - args.confidencelevel) / 2
105
+ conf_hi = 100.0 - conf_lo
106
+ cilo_fn = _ci("cilo", conf_lo, args.confidencemethod)
107
+ cihi_fn = _ci("cihi", conf_hi, args.confidencemethod)
108
+
109
+ agg_funcs = [
110
+ "count",
111
+ "sum",
112
+ "mean",
113
+ "std",
114
+ "min",
115
+ "max",
116
+ "median",
117
+ cilo_fn,
118
+ cihi_fn,
119
+ "sem",
120
+ "skew",
121
+ "kurt",
122
+ ]
123
+
124
+ if groupcol is None:
125
+ # Ungrouped — agg returns shape (n_stats, n_cols); transpose to
126
+ # (n_cols, n_stats) then reset the column index to get a 'name' row.
127
+ stats = (
128
+ df[cols].agg(agg_funcs).T.reset_index().rename(columns={"index": "name"})
129
+ )
130
+ return stats[["name"] + _STAT_NAMES]
131
+
132
+ else:
133
+ # Grouped — agg returns MultiIndex columns (col, stat).
134
+ # Reshape to long format so every (group, col) combination is a row.
135
+ name_col = "name"
136
+ if name_col in groupcol:
137
+ name_col = "name_mangled"
138
+ logger.warning(
139
+ 'A group column is named "name"; '
140
+ 'stat name column renamed to "name_mangled"'
141
+ )
142
+
143
+ stats = (
144
+ df.groupby(groupcol)[cols]
145
+ .agg(agg_funcs)
146
+ .rename(columns={"amin": "min", "amax": "max"}) # numpy compat
147
+ )
148
+
149
+ # swaplevel: (col, stat) → (stat, col)
150
+ # stack: fold the col level into the row index
151
+ # reset_index + rename: flatten to a plain DataFrame
152
+ level_col = f"level_{len(groupcol)}"
153
+ stats = (
154
+ stats.swaplevel(0, 1, axis=1)
155
+ .stack(future_stack=True)
156
+ .reset_index()
157
+ .rename(columns={level_col: name_col})
158
+ .sort_values(by=[name_col] + groupcol)
159
+ )
160
+
161
+ out_cols = groupcol + [name_col] + _STAT_NAMES
162
+ return stats[out_cols].reset_index(drop=True)
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Command
167
+ # ---------------------------------------------------------------------------
168
+
169
+
170
+ class StatCommand(BaseCommand):
171
+ """Compute descriptive statistics for one or more columns."""
172
+
173
+ name = "stat"
174
+ help = "Compute descriptive statistics (mean, std, CI, …) for one or more columns"
175
+
176
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
177
+ self.add_io_arguments(parser)
178
+
179
+ g = parser.add_argument_group("statistics")
180
+ g.add_argument(
181
+ "-c",
182
+ "--cols",
183
+ help="Column(s) to compute statistics on",
184
+ nargs="+",
185
+ required=True,
186
+ metavar="COL",
187
+ )
188
+ g.add_argument(
189
+ "-g",
190
+ "--groupcol",
191
+ help="Group-by column(s)",
192
+ nargs="+",
193
+ default=None,
194
+ metavar="COL",
195
+ )
196
+ g.add_argument(
197
+ "--confidencelevel",
198
+ help="Confidence level in percent (default: 95.0)",
199
+ type=float,
200
+ default=95.0,
201
+ metavar="PCT",
202
+ )
203
+ g.add_argument(
204
+ "--confidencemethod",
205
+ help="Percentile interpolation method (default: linear)",
206
+ choices=_CI_METHODS,
207
+ default="linear",
208
+ )
209
+
210
+ bs = parser.add_argument_group("bootstrap")
211
+ bs.add_argument(
212
+ "--bootstrap",
213
+ help="Number of bootstrap samples to draw",
214
+ type=int,
215
+ default=None,
216
+ metavar="N",
217
+ )
218
+ bs.add_argument(
219
+ "--randomseed",
220
+ help=(
221
+ "Random seed — integer, or a string which is MD5-hashed "
222
+ "to a reproducible integer"
223
+ ),
224
+ default=None,
225
+ metavar="SEED",
226
+ )
227
+ size_grp = bs.add_mutually_exclusive_group()
228
+ size_grp.add_argument(
229
+ "--samplesize",
230
+ help="Rows per bootstrap sample (default: same size as input)",
231
+ type=int,
232
+ default=None,
233
+ metavar="N",
234
+ )
235
+ size_grp.add_argument(
236
+ "--samplefrac",
237
+ help="Fraction of rows per bootstrap sample, 0.0–1.0 (default: 1.0)",
238
+ type=_samplefraction,
239
+ default=1.0,
240
+ metavar="FRAC",
241
+ )
242
+ bs.add_argument(
243
+ "--noreplace",
244
+ help="Sample without replacement (default: with replacement)",
245
+ dest="replace",
246
+ action="store_false",
247
+ )
248
+ parser.set_defaults(replace=True)
249
+
250
+ def execute(self, args: argparse.Namespace) -> None:
251
+ args.randomseed = normalize_seed(args.randomseed)
252
+ np.random.seed(args.randomseed)
253
+
254
+ df = io.read(args)
255
+ if df.empty:
256
+ raise ValueError("Input data is empty")
257
+
258
+ check_cols(df, args.cols, "-c/--cols")
259
+ check_cols(df, args.groupcol, "-g/--groupcol")
260
+
261
+ if args.bootstrap is None:
262
+ result = _compute_stats(df, args)
263
+ io.printdf(result, args)
264
+
265
+ else:
266
+ logger.info(
267
+ f"Bootstrapping: {args.bootstrap} samples, "
268
+ f"seed={args.randomseed}, replace={args.replace}"
269
+ )
270
+ sample_kwargs: dict = {"replace": args.replace}
271
+ if args.samplesize is not None:
272
+ sample_kwargs["n"] = args.samplesize
273
+ else:
274
+ sample_kwargs["frac"] = args.samplefrac
275
+
276
+ frames = []
277
+ for i in range(1, args.bootstrap + 1):
278
+ if args.groupcol is None:
279
+ sample = df.sample(**sample_kwargs)
280
+ else:
281
+ sample = df.groupby(args.groupcol, group_keys=False).apply(
282
+ lambda x: x.sample(**sample_kwargs)
283
+ )
284
+
285
+ result = _compute_stats(sample, args)
286
+ result["samplenum"] = i
287
+ frames.append(result)
288
+
289
+ io.printdf(pd.concat(frames, ignore_index=True), args)