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,438 @@
1
+ """
2
+ dftk.commands.pivot_cmd — dftk pivot subcommand.
3
+
4
+ Wraps pandas pivot_table with optional bootstrap resampling and three
5
+ sampling strategies for hierarchical data.
6
+ """
7
+
8
+ import argparse
9
+ import logging
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from dftk.commands.base import BaseCommand
15
+ from dftk.common.io import check_cols, io
16
+ from dftk.common.seed import normalize_seed
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _EPILOG = """\
21
+ AGGREGATION FUNCTIONS (-f / --aggfunc)
22
+ ---------------------------------------
23
+ Pass one or more names from this list:
24
+
25
+ count min max mean median mode sum std var skew kurt prod
26
+ cilo cihi (confidence-interval bounds, level set by --confidencelevel)
27
+ bitwise_and bitwise_or bitwise_xor
28
+
29
+ Bare names apply the same function(s) to every value column:
30
+
31
+ dftk pivot data.tsv -v sales cost -i region -f mean std
32
+
33
+ Per-column functions use COL:FUNC syntax — all items must use this form if
34
+ any do:
35
+
36
+ dftk pivot data.tsv -v sales cost -i region -f sales:mean cost:sum
37
+
38
+ When -f is omitted, pandas default aggregation (mean) is used.
39
+
40
+
41
+ BOOTSTRAP SAMPLING MODES (--bootstrap N)
42
+ -----------------------------------------
43
+ Three sampling strategies are available, determined by which flags are given.
44
+
45
+ 1. Full resampling (--fullsampling)
46
+ Sample every row in the dataset with replacement, ignoring group
47
+ structure. Use this when there is no meaningful grouping to preserve.
48
+
49
+ dftk pivot data.tsv -v value -i subject -f mean \\
50
+ --bootstrap 500 --fullsampling
51
+
52
+ 2. Group-based row sampling (--samplinggroup COLS)
53
+ Resample rows with replacement *within* each group defined by
54
+ COLS. Group sizes are preserved across samples.
55
+
56
+ dftk pivot data.tsv -v value -i timepoint -g condition -f mean \\
57
+ --bootstrap 500 --samplinggroup subject
58
+
59
+ If neither --fullsampling nor --samplinggroup nor --samplingcols is
60
+ given, the sampling groups default to the union of --index and
61
+ --groupcols (the pivot dimensions themselves).
62
+
63
+ 3. Hierarchical (parent/child) sampling (--samplingcols COLS)
64
+ Use when each logical unit has multiple data rows (e.g. a subject has
65
+ many measurements). Sampling parents and pulling all their children
66
+ keeps the within-unit structure intact.
67
+
68
+ Steps:
69
+ a. Build a sampling set of unique combinations of --samplingcols
70
+ (the parent identifiers).
71
+ b. Bootstrap-resample that set with replacement — optionally within
72
+ groups defined by --samplinggroup.
73
+ c. Merge the resampled set back against the full DataFrame, pulling
74
+ in all child rows for each selected parent.
75
+
76
+ If parent P is drawn twice, all of P's rows appear twice in the
77
+ bootstrap sample; if Q is not drawn, none of Q's rows appear.
78
+
79
+ # Sample subjects (parents) within each sex group;
80
+ # each subject has multiple visit rows (children).
81
+ dftk pivot data.tsv -v score -i visit -g condition -f mean \\
82
+ --bootstrap 500 \\
83
+ --samplingcols subject_id \\
84
+ --samplinggroup sex
85
+
86
+ Note: --samplingcols values are combined with --samplinggroup values
87
+ to form the merge key, so the group column must also be in the data.
88
+
89
+
90
+ PIPE MODE
91
+ ---------
92
+ Use -o alone to pass data to the next dftk command without writing to a
93
+ file:
94
+
95
+ cat data.tsv \\
96
+ | dftk pivot - -v value -i subject -g condition -f mean -o \\
97
+ | dftk stat - -c condition_A condition_B
98
+ """
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Helpers
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ def _ci(name: str, level: float, method: str):
107
+ """Return a named percentile function for use as a pandas agg function."""
108
+
109
+ def cifunction(a):
110
+ return np.percentile(a, q=level, method=method)
111
+
112
+ cifunction.__name__ = name
113
+ return cifunction
114
+
115
+
116
+ def _bitwise(op: str):
117
+ """Return a named bitwise-reduce function."""
118
+
119
+ def bitfunction(a):
120
+ if op == "and":
121
+ return np.bitwise_and.reduce(a)
122
+ if op == "or":
123
+ return np.bitwise_or.reduce(a)
124
+ if op == "xor":
125
+ return np.bitwise_xor.reduce(a)
126
+
127
+ bitfunction.__name__ = op
128
+ return bitfunction
129
+
130
+
131
+ def _prepare_funcs(
132
+ aggfuncs: list[str] | None, confidencelevel: float, confidencemethod: str
133
+ ):
134
+ """Convert aggfunc name strings into a pandas-compatible aggfunc argument.
135
+
136
+ Returns:
137
+ None → pandas default (mean)
138
+ list → same function(s) applied to all value columns
139
+ dict → per-column functions, keyed by column name
140
+ """
141
+ if aggfuncs is None:
142
+ return None
143
+
144
+ funclist: list = []
145
+ funcdict: dict = {}
146
+
147
+ for item in aggfuncs:
148
+ colname = None
149
+ funcname = item
150
+ if ":" in item:
151
+ colname, funcname = item.split(":", 1)
152
+ funcdict.setdefault(colname, [])
153
+
154
+ if funcname in {
155
+ "count",
156
+ "min",
157
+ "max",
158
+ "mean",
159
+ "median",
160
+ "mode",
161
+ "sum",
162
+ "std",
163
+ "var",
164
+ "skew",
165
+ "kurt",
166
+ "prod",
167
+ }:
168
+ func = funcname
169
+ elif funcname == "cilo":
170
+ lo = (100.0 - confidencelevel) / 2.0
171
+ func = _ci("cilo", lo, confidencemethod)
172
+ elif funcname == "cihi":
173
+ hi = 100.0 - (100.0 - confidencelevel) / 2.0
174
+ func = _ci("cihi", hi, confidencemethod)
175
+ elif funcname in {"bitwise_and", "bitwise_or", "bitwise_xor"}:
176
+ func = _bitwise(funcname.split("_", 1)[1])
177
+ else:
178
+ try:
179
+ func = getattr(np, funcname)
180
+ except AttributeError:
181
+ raise ValueError( # noqa: E501
182
+ f"Unknown aggregation function: {funcname!r}"
183
+ ) from None
184
+
185
+ if funcdict and colname is None:
186
+ raise ValueError(
187
+ "Mixed aggfunc spec: either all items must use COL:FUNC "
188
+ "syntax or none must."
189
+ )
190
+
191
+ if colname is not None:
192
+ funcdict[colname].append(func)
193
+ else:
194
+ funclist.append(func)
195
+
196
+ return funcdict if funcdict else funclist
197
+
198
+
199
+ def _flatten_columns(cols) -> list[str]:
200
+ """Flatten MultiIndex column tuples to underscore-joined strings."""
201
+ result = []
202
+ for col in cols:
203
+ if isinstance(col, str):
204
+ result.append(col)
205
+ else:
206
+ result.append("_".join(str(x) for x in col if str(x) != ""))
207
+ return result
208
+
209
+
210
+ def _do_pivot(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
211
+ """Run pivot_table and flatten column names."""
212
+ funcs = _prepare_funcs(
213
+ getattr(args, "aggfunc", None),
214
+ getattr(args, "confidencelevel", 95.0),
215
+ getattr(args, "confidencemethod", "linear"),
216
+ )
217
+
218
+ index = args.index or None
219
+ columns = args.groupcols or None
220
+
221
+ pivoted = df.pivot_table(
222
+ values=args.values,
223
+ index=index,
224
+ columns=columns,
225
+ aggfunc=funcs if funcs is not None else "mean",
226
+ )
227
+
228
+ cols = pivoted.columns
229
+ pivoted.columns = _flatten_columns(cols.values if hasattr(cols, "values") else cols)
230
+
231
+ if getattr(args, "fillzero", False) and hasattr(pivoted.index, "levels"):
232
+ pidx = pd.MultiIndex.from_product(
233
+ pivoted.index.levels, names=pivoted.index.names
234
+ )
235
+ pivoted = pivoted.reindex(pidx, fill_value=0, copy=False)
236
+
237
+ return pivoted.reset_index()
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Command
242
+ # ---------------------------------------------------------------------------
243
+
244
+
245
+ class PivotCommand(BaseCommand):
246
+ @property
247
+ def name(self) -> str:
248
+ return "pivot"
249
+
250
+ @property
251
+ def help(self) -> str:
252
+ return "Pivot / aggregate a DataFrame (pivot_table)"
253
+
254
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
255
+ import argparse as ap
256
+
257
+ parser.formatter_class = ap.RawDescriptionHelpFormatter
258
+ parser.epilog = _EPILOG
259
+
260
+ io.parser_read(parser)
261
+
262
+ g = parser.add_argument_group("pivot options")
263
+ g.add_argument(
264
+ "-v",
265
+ "--values",
266
+ help="Value column(s) to aggregate.",
267
+ nargs="+",
268
+ required=True,
269
+ metavar="COL",
270
+ )
271
+ g.add_argument(
272
+ "-i",
273
+ "--index",
274
+ help="Column(s) that become row labels in the pivot table.",
275
+ nargs="*",
276
+ default=[],
277
+ metavar="COL",
278
+ )
279
+ g.add_argument(
280
+ "-g",
281
+ "--groupcols",
282
+ help="Column(s) whose unique values are spread into new column headers.",
283
+ nargs="*",
284
+ default=[],
285
+ metavar="COL",
286
+ )
287
+ g.add_argument(
288
+ "-f",
289
+ "--aggfunc",
290
+ help=(
291
+ "Aggregation function(s). "
292
+ "Use bare names (mean std) or COL:FUNC pairs (sales:mean cost:sum). "
293
+ "See --help for full list. Default: mean."
294
+ ),
295
+ nargs="*",
296
+ default=None,
297
+ metavar="FUNC",
298
+ )
299
+ g.add_argument(
300
+ "-z",
301
+ "--fillzero",
302
+ help="Reindex to all (index × columns) combinations, fill missing with 0.",
303
+ action="store_true",
304
+ )
305
+ g.add_argument(
306
+ "--confidencelevel",
307
+ help="Confidence level for cilo/cihi aggfuncs, in percent (default: 95.0).",
308
+ type=float,
309
+ default=95.0,
310
+ metavar="PCT",
311
+ )
312
+ g.add_argument(
313
+ "--confidencemethod",
314
+ help="Interpolation method for cilo/cihi (default: linear).",
315
+ default="linear",
316
+ metavar="METHOD",
317
+ )
318
+
319
+ bs = parser.add_argument_group("bootstrap options")
320
+ bs.add_argument(
321
+ "--bootstrap",
322
+ help=(
323
+ "Number of bootstrap samples. "
324
+ "Produces a single DataFrame with a samplenum column."
325
+ ),
326
+ type=int,
327
+ default=None,
328
+ metavar="N",
329
+ )
330
+ bs.add_argument(
331
+ "--randomseed",
332
+ help="Random seed. Accepts an integer or a string (hashed to uint32).",
333
+ default=None,
334
+ metavar="SEED",
335
+ )
336
+ bs.add_argument(
337
+ "--fullsampling",
338
+ help="Resample all rows with replacement, ignoring group structure.",
339
+ action="store_true",
340
+ )
341
+ bs.add_argument(
342
+ "--samplinggroup",
343
+ help=(
344
+ "Column(s) defining groups within which rows are resampled. "
345
+ "Also used as the within-group dimension when --samplingcols is given."
346
+ ),
347
+ nargs="*",
348
+ default=[],
349
+ metavar="COL",
350
+ )
351
+ bs.add_argument(
352
+ "--samplingcols",
353
+ help=(
354
+ "Column(s) identifying the sampling unit (e.g. a subject ID). "
355
+ "Enables hierarchical bootstrap: unique units are resampled and "
356
+ "all their rows are pulled in via merge. "
357
+ "See --help for a detailed example."
358
+ ),
359
+ nargs="*",
360
+ default=[],
361
+ metavar="COL",
362
+ )
363
+
364
+ io.parser_output(parser)
365
+
366
+ def execute(self, args: argparse.Namespace) -> None:
367
+ args.randomseed = normalize_seed(args.randomseed)
368
+ np.random.seed(args.randomseed)
369
+
370
+ df = io.read(args)
371
+
372
+ check_cols(df, args.values, "-v/--values")
373
+ check_cols(df, args.index, "-i/--index")
374
+ check_cols(df, args.groupcols, "-g/--groupcols")
375
+ check_cols(df, args.samplingcols, "--samplingcols")
376
+ check_cols(df, args.samplinggroup, "--samplinggroup")
377
+
378
+ if args.bootstrap is None:
379
+ result = _do_pivot(df, args)
380
+ io.printdf(result, args)
381
+ return
382
+
383
+ # ------------------------------------------------------------------
384
+ # Bootstrap
385
+ # ------------------------------------------------------------------
386
+
387
+ logger.info(
388
+ f"Bootstrap pivot: {args.bootstrap} samples, seed={args.randomseed}"
389
+ )
390
+
391
+ # Determine sampling mode and build sampling set if needed.
392
+ if args.fullsampling:
393
+ mode = "full"
394
+ s = None
395
+ samplinggroup: list[str] = []
396
+ elif args.samplingcols:
397
+ mode = "hierarchical"
398
+ # The merge key is samplingcols + samplinggroup so the group
399
+ # column is preserved through the merge.
400
+ merge_cols = args.samplingcols + args.samplinggroup
401
+ s = df[merge_cols].drop_duplicates().reset_index(drop=True)
402
+ samplinggroup = args.samplinggroup
403
+ else:
404
+ mode = "grouped"
405
+ s = None
406
+ samplinggroup = args.samplinggroup or (args.index + args.groupcols)
407
+
408
+ logger.info(f"Sampling mode: {mode}")
409
+
410
+ frames: list[pd.DataFrame] = []
411
+
412
+ for ns in range(1, args.bootstrap + 1):
413
+ if mode == "full":
414
+ bf = df.sample(frac=1, replace=True)
415
+
416
+ elif mode == "hierarchical":
417
+ if samplinggroup:
418
+ ss = (
419
+ s.groupby(samplinggroup, group_keys=False)[s.columns.tolist()]
420
+ .apply(lambda x: x.sample(frac=1.0, replace=True))
421
+ .reset_index(drop=True)
422
+ )
423
+ else:
424
+ ss = s.sample(frac=1.0, replace=True).reset_index(drop=True)
425
+ bf = pd.merge(ss, df)
426
+
427
+ else: # grouped
428
+ bf = (
429
+ df.groupby(samplinggroup, group_keys=False)[df.columns.tolist()]
430
+ .apply(lambda x: x.sample(frac=1.0, replace=True))
431
+ .reset_index(drop=True)
432
+ )
433
+
434
+ result = _do_pivot(bf, args)
435
+ result["samplenum"] = ns
436
+ frames.append(result)
437
+
438
+ io.printdf(pd.concat(frames, ignore_index=True), args)
@@ -0,0 +1,36 @@
1
+ """
2
+ dftk.commands.print_cmd — the ``dftk print`` subcommand.
3
+
4
+ Reads any dftk-compatible input (TSV file, TSV on stdin, or a parquet pipe
5
+ path on stdin) and writes TSV to stdout. Acts as the terminal stage of a
6
+ dftk pipeline:
7
+
8
+ dftk stat -g grp -s val -o \\
9
+ | dftk pivot -i grp -v val_mean -o \\
10
+ | dftk print
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+
16
+ from dftk.commands.base import BaseCommand
17
+ from dftk.common.io import io
18
+
19
+
20
+ class PrintCommand(BaseCommand):
21
+ """Read any dftk input and write TSV to stdout."""
22
+
23
+ name = "print"
24
+ help = "Convert any input (TSV or parquet pipe) to TSV on stdout"
25
+
26
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
27
+ # Only the read-side arguments — no -o/--output, since the whole
28
+ # point of this command is to force TSV to stdout.
29
+ io.parser_read(parser)
30
+
31
+ def execute(self, args: argparse.Namespace) -> None:
32
+ df = io.read(args)
33
+ try:
34
+ df.to_csv(sys.stdout, sep="\t", index=False)
35
+ except (OSError, BrokenPipeError):
36
+ sys.stderr.close()
@@ -0,0 +1,230 @@
1
+ """
2
+ dftk.commands.query_cmd — dftk query subcommand.
3
+
4
+ Filter (and optionally reshape) a dataframe using either pandas query
5
+ expressions or full DuckDB SQL.
6
+
7
+ Two modes
8
+ ---------
9
+ pandas mode (-q / --query, repeatable)
10
+ Applies pandas DataFrame.query() expressions in sequence. Each
11
+ expression is a boolean row filter; all expressions are ANDed together.
12
+
13
+ dftk query data.tsv -q "x > 0" -q "group == 'A'"
14
+
15
+ sql mode (--sql)
16
+ Passes a full SQL statement to DuckDB. The input data is available as
17
+ a virtual table named "data" (override with --table). Any SQL that
18
+ DuckDB supports is valid: SELECT, aggregation, window functions, CTEs.
19
+
20
+ dftk query data.tsv --sql "SELECT grp, AVG(x) AS mean FROM data GROUP BY grp"
21
+
22
+ When the input is a named .parquet file, DuckDB queries it natively
23
+ without loading the full file into memory — suitable for large datasets.
24
+
25
+ Requires: pip install duckdb
26
+ """
27
+
28
+ import argparse
29
+ import logging
30
+ import os
31
+
32
+ import pandas as pd
33
+
34
+ from dftk.commands.base import BaseCommand
35
+ from dftk.common.io import io
36
+
37
+ try:
38
+ import duckdb
39
+
40
+ _HAS_DUCKDB = True
41
+ except ImportError:
42
+ duckdb = None
43
+ _HAS_DUCKDB = False
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Core functions
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _pandas_query(df: pd.DataFrame, expressions: list[str]) -> pd.DataFrame:
54
+ """Apply each expression as a pandas boolean row filter (AND logic)."""
55
+ for expr in expressions:
56
+ try:
57
+ df = df.query(expr)
58
+ except Exception as exc:
59
+ raise ValueError(f"pandas query failed ({expr!r}): {exc}") from exc
60
+ return df
61
+
62
+
63
+ def _sql_query(args: argparse.Namespace, sql: str, table: str) -> pd.DataFrame:
64
+ """Run *sql* against the input data registered as a DuckDB virtual table.
65
+
66
+ For named .parquet inputs the file is queried as a lazy DuckDB relation —
67
+ DuckDB pushes down filters and projections so the full dataset need not
68
+ fit in memory. All other inputs (TSV files, stdin, parquet-pipe) are
69
+ read into a pandas DataFrame first and then registered with DuckDB.
70
+ """
71
+ if not _HAS_DUCKDB:
72
+ raise ImportError("--sql requires the duckdb package: pip install duckdb")
73
+
74
+ conn = duckdb.connect()
75
+ filename: str | None = getattr(args, "DATAFILE", None)
76
+
77
+ # Named parquet: let DuckDB read it lazily (potentially out-of-core).
78
+ direct_parquet = (
79
+ filename
80
+ and filename not in (None, "-")
81
+ and filename.endswith(".parquet")
82
+ and os.path.isfile(filename)
83
+ )
84
+
85
+ if direct_parquet:
86
+ rel = conn.read_parquet(filename)
87
+ conn.register(table, rel)
88
+ else:
89
+ # stdin, TSV files, or parquet-pipe: read into pandas first.
90
+ df = io.read(args)
91
+ conn.register(table, df)
92
+
93
+ try:
94
+ return conn.execute(sql).df()
95
+ except Exception as exc:
96
+ raise ValueError(f"DuckDB SQL failed: {exc}") from exc
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Command
101
+ # ---------------------------------------------------------------------------
102
+
103
+ _EPILOG = """\
104
+ PANDAS QUERY MODE (-q / --query)
105
+ ----------------------------------
106
+ Each expression is applied as a boolean row filter in sequence; multiple
107
+ -q flags are ANDed together. Syntax mirrors Python comparisons on column
108
+ names:
109
+
110
+ dftk query data.tsv -q "x > 0"
111
+ dftk query data.tsv -q "group == 'A'" -q "score >= 10"
112
+ dftk query data.tsv -q "x > 0 and y < 100"
113
+ dftk query data.tsv -q "label.str.startswith('foo')"
114
+
115
+ Backtick-quoting handles column names with spaces or special characters:
116
+
117
+ dftk query data.tsv -q "`my col` > 0"
118
+
119
+ This is a thin wrapper around pandas DataFrame.query().
120
+
121
+
122
+ SQL MODE (--sql)
123
+ ------------------
124
+ A full SQL statement is passed to DuckDB. The input data is available as
125
+ a virtual table named "data" (or the name given by --table).
126
+
127
+ Simple filtering:
128
+
129
+ dftk query data.tsv --sql "SELECT * FROM data WHERE x > 0"
130
+ cat data.tsv | dftk query --sql "SELECT * FROM data WHERE x > 0" -o
131
+
132
+ Aggregation (replaces dftk pivot for simple cases):
133
+
134
+ dftk query data.tsv --sql \\
135
+ "SELECT group, AVG(x) AS mean, STDDEV(x) AS std, COUNT(*) AS n
136
+ FROM data GROUP BY group"
137
+
138
+ Window functions:
139
+
140
+ dftk query data.tsv --sql \\
141
+ "SELECT *, ROW_NUMBER() OVER (PARTITION BY group ORDER BY x DESC) AS rank
142
+ FROM data"
143
+
144
+ CTEs:
145
+
146
+ dftk query data.tsv --sql \\
147
+ "WITH ranked AS (
148
+ SELECT *, RANK() OVER (PARTITION BY group ORDER BY score DESC) AS rank
149
+ FROM data
150
+ )
151
+ SELECT * FROM ranked WHERE rank <= 3"
152
+
153
+ OUT-OF-CORE LARGE FILES
154
+ ------------------------
155
+ When the input is a named .parquet file, DuckDB queries it natively without
156
+ loading the full dataset into memory. Filter pushdown and column pruning
157
+ mean that only the rows and columns needed by the query are read:
158
+
159
+ dftk query huge.parquet --sql "SELECT * FROM data WHERE x > 0"
160
+ dftk query huge.parquet --sql \\
161
+ "SELECT group, AVG(x) AS mean FROM data GROUP BY group"
162
+
163
+ For piped input (stdin or parquet-pipe), the data is loaded into pandas
164
+ first and then registered with DuckDB — in-memory, same as pandas mode.
165
+ Use a named checkpoint file (-o huge.parquet) when you need out-of-core
166
+ queries on a large intermediate result:
167
+
168
+ dftk eval "y = x * 2" huge.tsv -o huge.parquet
169
+ dftk query huge.parquet --sql "SELECT * FROM data WHERE y > 1000"
170
+
171
+ Requires: pip install duckdb
172
+ """
173
+
174
+
175
+ class QueryCommand(BaseCommand):
176
+ name = "query"
177
+ help = "Filter rows with pandas expressions (-q) or full DuckDB SQL (--sql)."
178
+
179
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
180
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
181
+ parser.epilog = _EPILOG
182
+
183
+ self.add_io_arguments(parser)
184
+
185
+ g = parser.add_argument_group("query options")
186
+ g.add_argument(
187
+ "-q",
188
+ "--query",
189
+ action="append",
190
+ default=[],
191
+ metavar="EXPR",
192
+ help=(
193
+ "Pandas boolean expression for row filtering. "
194
+ "Repeatable; all expressions are ANDed together. "
195
+ "Cannot be combined with --sql."
196
+ ),
197
+ )
198
+ g.add_argument(
199
+ "--sql",
200
+ default=None,
201
+ metavar="SQL",
202
+ help=(
203
+ "Full DuckDB SQL query. The input is registered as a virtual "
204
+ "table named 'data' (see --table). Named .parquet inputs are "
205
+ "queried natively without full load into memory. "
206
+ "Cannot be combined with -q. Requires duckdb."
207
+ ),
208
+ )
209
+ g.add_argument(
210
+ "--table",
211
+ default="data",
212
+ metavar="NAME",
213
+ help=(
214
+ "Name of the virtual table in --sql queries (default: data). "
215
+ "Change this if your data has a column named 'data'."
216
+ ),
217
+ )
218
+
219
+ def execute(self, args: argparse.Namespace) -> None:
220
+ if args.sql is not None and args.query:
221
+ raise ValueError("Cannot combine -q/--query and --sql.")
222
+
223
+ if args.sql is not None:
224
+ df = _sql_query(args, args.sql, args.table)
225
+ else:
226
+ df = io.read(args)
227
+ if args.query:
228
+ df = _pandas_query(df, args.query)
229
+
230
+ io.printdf(df, args)