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,657 @@
1
+ """
2
+ dftk.commands.eval_cmd — dftk eval subcommand.
3
+
4
+ Evaluates formulas, constants, and string/row functions against a DataFrame.
5
+ Three formula types are supported, each with its own flag:
6
+
7
+ -f pandas eval expressions (df.eval, falls back to special_function)
8
+ -c constant column assignments
9
+ -s string / row functions
10
+ """
11
+
12
+ import argparse
13
+ import fnmatch
14
+ import hashlib
15
+ import logging
16
+ import os
17
+ from functools import partial
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+ from scipy import special as _sp
23
+
24
+ from dftk.commands.base import BaseCommand
25
+ from dftk.common import seq as _seq
26
+ from dftk.common import stats as _stats
27
+ from dftk.common.io import io
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Optional dependency: python-Levenshtein
32
+ try:
33
+ import Levenshtein as _lev
34
+
35
+ _HAS_LEVENSHTEIN = True
36
+ except ImportError:
37
+ _HAS_LEVENSHTEIN = False
38
+
39
+ _EPILOG = """\
40
+ FORMULA EXPRESSIONS (-f)
41
+ -------------------------
42
+ Each -f argument is first passed to pandas df.eval(). This handles
43
+ arithmetic and boolean expressions directly on column names:
44
+
45
+ dftk eval data.tsv -f "z = x + y * 2"
46
+ dftk eval data.tsv -f "flag = (pval < 0.05) & (effect > 0)"
47
+
48
+ If df.eval() fails (e.g. for function calls), the expression is retried
49
+ as a special function call using the syntax:
50
+
51
+ dest = func(col1, col2, ...)
52
+
53
+ Available special functions:
54
+
55
+ Row aggregation (applied across named columns):
56
+ sum mean std min max median
57
+
58
+ Conditional assignment:
59
+ where(cond_col, true_val, false_val)
60
+ — np.where-style conditional; cond_col must be a boolean column.
61
+ true_val and false_val can each be: a column name, a quoted
62
+ string literal ("foo"), or a numeric literal.
63
+ Example:
64
+ dftk eval data.tsv \
65
+ -f "big = value > 100" \
66
+ -f "label = where(big, \"large\", \"small\")"
67
+
68
+ Bitwise:
69
+ applymask(col, mask_col) — col & mask_col
70
+ overlap(lo1, hi1, lo2, hi2) — length of interval overlap
71
+
72
+ Column aggregation (glob pattern):
73
+ colsum(col*) — sum of all columns matching the glob pattern
74
+
75
+ NumPy math (element-wise):
76
+ sign(col) cbrt(col) log1p(col) expm1(col)
77
+
78
+ scipy.special — log-gamma / digamma:
79
+ gamma(col) lgamma(col) digamma(col)
80
+ beta(a, b) betaln(a, b)
81
+
82
+ scipy.special — error functions:
83
+ erf(col) erfc(col) erfinv(col)
84
+
85
+ scipy.special — sigmoid / logit:
86
+ expit(col) logit(col)
87
+
88
+ scipy.special — Bessel functions (fixed order):
89
+ j0(col) j1(col) — Bessel J, order 0 / 1
90
+ y0(col) y1(col) — Bessel Y, order 0 / 1
91
+ i0(col) i1(col) — modified Bessel I, order 0 / 1
92
+ k0(col) k1(col) — modified Bessel K, order 0 / 1
93
+
94
+ scipy.special — Bessel functions (parametric order):
95
+ jn(n, col) — Bessel J, integer order n
96
+ yn(n, col) — Bessel Y, integer order n
97
+ iv(v, col) — modified Bessel I, real order v
98
+ kv(v, col) — modified Bessel K, real order v
99
+ (n/v is a numeric literal, e.g. result = jn(3, x))
100
+
101
+ Path functions (applied element-wise):
102
+ basename dirname exists getsize realpath
103
+
104
+ Statistical functions (see common/stats.py):
105
+ binom_test(k, n, p)
106
+ fisher_test(a, b, c, d) — p-value
107
+ fisher_OR(a, b, c, d) — odds ratio
108
+ boschloo_test(a, b, c, d) — p-value
109
+ boschloo_OR(a, b, c, d) — odds ratio
110
+ pval2se(effect, pval)
111
+ t2pval(t, df)
112
+ chi2_to_neglogp(chi2, df)
113
+ neglogp_to_chi2(neglogp, df)
114
+ pval_to_chi2(pval, df)
115
+ generalized_poisson_nll(x, lam, theta)
116
+
117
+
118
+ CONSTANT COLUMNS (-c)
119
+ ----------------------
120
+ Add a column with a fixed value:
121
+
122
+ dftk eval data.tsv -c "batch = 3"
123
+ dftk eval data.tsv -c "label = control"
124
+
125
+ The value is coerced to numeric when possible.
126
+
127
+
128
+ STRING / ROW FUNCTIONS (-s)
129
+ ----------------------------
130
+ Apply a string or row-level function using the same dest = func(cols) syntax:
131
+
132
+ String operations:
133
+ translate(col, from, to)
134
+ len(col)
135
+ join(col1, col2, ...) — joined with --joinsep (default: "")
136
+ lower(col) upper(col)
137
+ zfill(col, width)
138
+ find(col, substr)
139
+ match(col, pattern_col)
140
+ substr(col, start, length)
141
+ substring(col, start_col, length)
142
+ replace(col, old, new)
143
+ leftsplit(col, sep, idx) — split on first sep; idx=0 left part, 1 right
144
+ rightsplit(col, sep, idx) — split on last sep
145
+ sort(col) — sort characters in string
146
+ ord(col) — minimum ord value of characters
147
+ template(...) — Python str.format_map across all columns
148
+
149
+ Path operations:
150
+ basename(col) stem(col) realpath(col) isfile(col) exists(col)
151
+
152
+ Crypto / hash:
153
+ md5(col)
154
+
155
+ DNA / sequence (see common/seq.py):
156
+ gccontent(col)
157
+ basecount(col)
158
+ motifcount(pattern, seq_col)
159
+ readoffset(read_pos, ref_pos, cigar, seq, count)
160
+
161
+ Edit distance (requires python-Levenshtein):
162
+ levenshtein(col1, col2)
163
+ """
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Formula parser
167
+ # ---------------------------------------------------------------------------
168
+
169
+
170
+ def _parse_formula(formula: str) -> tuple[str, str, list[str], str]:
171
+ """Parse 'dest = func(col1, col2, ...)' into (dest, func, cols, body).
172
+
173
+ Returns the raw argument string as *body* so callers can use it as a
174
+ glob pattern (e.g. for colsum).
175
+ """
176
+ try:
177
+ dest, right = (x.strip() for x in formula.split("=", 1))
178
+ func, right = (x.strip() for x in right.split("(", 1))
179
+ body, _ = (x.strip() for x in right.rsplit(")", 1))
180
+ except Exception:
181
+ raise ValueError(
182
+ f"Bad formula syntax: {formula!r} Expected: dest = func(col1, col2, ...)"
183
+ ) from None
184
+ cols = [x.strip() for x in body.split(",")]
185
+ return dest, func, cols, body
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Special functions (used as pandas agg fallback inside -f)
190
+ # ---------------------------------------------------------------------------
191
+
192
+ _STATS_DISPATCH = {
193
+ "binom_test": _stats.binom_test,
194
+ "fisher_test": _stats.fisher_test,
195
+ "fisher_OR": _stats.fisher_OR,
196
+ "boschloo_test": _stats.boschloo_test,
197
+ "boschloo_OR": _stats.boschloo_OR,
198
+ "pval2se": _stats.pval2se,
199
+ "t2pval": _stats.t2pval,
200
+ "chi2_to_neglogp": _stats.chi2_to_neglogp,
201
+ "neglogp_to_chi2": _stats.neglogp_to_chi2,
202
+ "pval_to_chi2": _stats.pval_to_chi2,
203
+ "generalized_poisson_nll": _stats.generalized_poisson_nll,
204
+ }
205
+
206
+ _PATH_FUNCS = {"basename", "dirname", "exists", "getsize", "realpath"}
207
+ _NP_FUNCS = {"sign", "cbrt", "log1p", "expm1"}
208
+ _ROW_AGG_FUNCS = {"sum", "mean", "std", "min", "max", "median"}
209
+ _ROW_IDX_FUNCS = {"idxmax", "idxmin"}
210
+
211
+ # scipy.special single-column functions (element-wise, vectorised)
212
+ _SCIPY_SINGLE = {
213
+ "gamma": _sp.gamma, # Γ(x)
214
+ "lgamma": _sp.gammaln, # log Γ(x)
215
+ "digamma": _sp.digamma, # ψ(x) (log-derivative of Γ)
216
+ "erf": _sp.erf, # error function
217
+ "erfc": _sp.erfc, # complementary error function
218
+ "erfinv": _sp.erfinv, # inverse error function
219
+ "expit": _sp.expit, # logistic sigmoid 1/(1+exp(-x))
220
+ "logit": _sp.logit, # log(x/(1-x))
221
+ # Bessel J (first kind), fixed order
222
+ "j0": _sp.j0,
223
+ "j1": _sp.j1,
224
+ # Bessel Y (second kind), fixed order
225
+ "y0": _sp.y0,
226
+ "y1": _sp.y1,
227
+ # Modified Bessel I, fixed order
228
+ "i0": _sp.i0,
229
+ "i1": _sp.i1,
230
+ # Modified Bessel K, fixed order
231
+ "k0": _sp.k0,
232
+ "k1": _sp.k1,
233
+ }
234
+
235
+ # scipy.special two-column functions: func(col_a, col_b)
236
+ _SCIPY_DUAL = {
237
+ "beta": lambda x: _sp.beta(x[0], x[1]), # B(a, b)
238
+ "betaln": lambda x: _sp.betaln(x[0], x[1]), # log B(a, b)
239
+ }
240
+
241
+ # scipy.special parametric Bessel: func_name(order_literal, col)
242
+ # Covers all-order variants: jn(n,x), yn(n,x), iv(v,x), kv(v,x)
243
+ _SCIPY_BESSEL_ORDER = {"jn", "yn", "iv", "kv"}
244
+
245
+
246
+ class _FrameFunc:
247
+ """Callable that applies a named pandas DataFrame method along axis=1.
248
+
249
+ Used for idxmax/idxmin, which need the column labels and therefore cannot
250
+ go through the standard ``func(row.to_numpy())`` dispatch path.
251
+ """
252
+
253
+ def __init__(self, method: str) -> None:
254
+ self.method = method
255
+
256
+ def __call__(self, df_subset):
257
+ return getattr(df_subset, self.method)(axis=1)
258
+
259
+
260
+ def _resolve_where_val(val: str, df: pd.DataFrame):
261
+ """Resolve a where() argument to a scalar, Series, or literal string.
262
+
263
+ Resolution order:
264
+ 1. Quoted string literal ("foo" or 'foo')
265
+ 2. Column name present in *df*
266
+ 3. Integer literal
267
+ 4. Float literal
268
+ 5. Bare string (returned as-is)
269
+ """
270
+ s = val.strip()
271
+ if (s.startswith('"') and s.endswith('"')) or (
272
+ s.startswith("'") and s.endswith("'")
273
+ ):
274
+ return s[1:-1]
275
+ if s in df.columns:
276
+ return df[s]
277
+ try:
278
+ return int(s)
279
+ except ValueError:
280
+ pass
281
+ try:
282
+ return float(s)
283
+ except ValueError:
284
+ pass
285
+ return s
286
+
287
+
288
+ class _WhereFunc:
289
+ """Callable for ``where(cond_col, true_val, false_val)`` using ``np.where``.
290
+
291
+ Receives the full DataFrame so that *true_val* and *false_val* can be
292
+ resolved as column references, quoted string literals, or numeric literals.
293
+ """
294
+
295
+ def __init__(self, cond_col: str, true_val: str, false_val: str) -> None:
296
+ self.cond_col = cond_col
297
+ self.true_val = true_val
298
+ self.false_val = false_val
299
+
300
+ def __call__(self, df: pd.DataFrame):
301
+ cond = df[self.cond_col]
302
+ true_res = _resolve_where_val(self.true_val, df)
303
+ false_res = _resolve_where_val(self.false_val, df)
304
+ return np.where(cond, true_res, false_res)
305
+
306
+
307
+ _VALID_SPECIAL = (
308
+ {
309
+ "sum",
310
+ "mean",
311
+ "std",
312
+ "min",
313
+ "max",
314
+ "median",
315
+ "overlap",
316
+ "applymask",
317
+ "colsum",
318
+ "where",
319
+ }
320
+ | _ROW_IDX_FUNCS
321
+ | _PATH_FUNCS
322
+ | _NP_FUNCS
323
+ | set(_STATS_DISPATCH)
324
+ | set(_SCIPY_SINGLE)
325
+ | set(_SCIPY_DUAL)
326
+ | _SCIPY_BESSEL_ORDER
327
+ )
328
+
329
+
330
+ def _special_function(formula: str, df_columns: list[str]):
331
+ """Return (dest, func, cols) for a special-function formula."""
332
+ dest, func_name, cols, body = _parse_formula(formula)
333
+
334
+ if func_name not in _VALID_SPECIAL:
335
+ raise ValueError(
336
+ f"Function {func_name!r} is not available as a "
337
+ "special function. See dftk eval --help."
338
+ )
339
+
340
+ if func_name == "where":
341
+ if len(cols) != 3:
342
+ raise ValueError(
343
+ "where() requires exactly 3 arguments: "
344
+ "where(cond_col, true_val, false_val)"
345
+ )
346
+ return dest, _WhereFunc(cols[0], cols[1], cols[2]), []
347
+
348
+ if func_name == "overlap":
349
+ func = partial(lambda x: max(0, min(x[1], x[3]) - max(x[0] - 1, x[2] - 1)))
350
+ elif func_name == "applymask":
351
+ func = partial(lambda x: x[0] & x[1])
352
+ elif func_name == "colsum":
353
+ cols = fnmatch.filter(df_columns, body)
354
+ func = np.sum
355
+ elif func_name in _ROW_AGG_FUNCS:
356
+ func = getattr(np, func_name)
357
+ # Each argument may be a glob pattern: expand to matching column names
358
+ cols = [c for pat in cols for c in (fnmatch.filter(df_columns, pat) or [pat])]
359
+ elif func_name in _ROW_IDX_FUNCS:
360
+ func = _FrameFunc(func_name)
361
+ cols = [c for pat in cols for c in (fnmatch.filter(df_columns, pat) or [pat])]
362
+ elif func_name in _NP_FUNCS:
363
+ func = getattr(np, func_name)
364
+ elif func_name in _PATH_FUNCS:
365
+ func = np.frompyfunc(getattr(os.path, func_name), 1, 1)
366
+ elif func_name in _STATS_DISPATCH:
367
+ func = _STATS_DISPATCH[func_name]
368
+ elif func_name in _SCIPY_SINGLE:
369
+ func = _SCIPY_SINGLE[func_name]
370
+ elif func_name in _SCIPY_DUAL:
371
+ func = _SCIPY_DUAL[func_name]
372
+ elif func_name in _SCIPY_BESSEL_ORDER:
373
+ try:
374
+ order = float(cols[0])
375
+ except (ValueError, IndexError):
376
+ raise ValueError(
377
+ f"{func_name}() first argument must be a numeric order literal, "
378
+ f"e.g. result = {func_name}(2, xcol)"
379
+ ) from None
380
+ func = partial(getattr(_sp, func_name), order)
381
+ cols = cols[1:]
382
+ else:
383
+ func = func_name # bare string passed to pandas agg
384
+
385
+ return dest, func, cols
386
+
387
+
388
+ # ---------------------------------------------------------------------------
389
+ # String / row functions (used by -s)
390
+ # ---------------------------------------------------------------------------
391
+
392
+
393
+ def _string_function(formula: str, joinsep: str):
394
+ """Return (dest, func, cols) for a string/row-function formula."""
395
+ dest, func_name, cols, _body = _parse_formula(formula)
396
+
397
+ if func_name == "translate":
398
+ trs = str.maketrans(*cols[1:])
399
+ return dest, partial(lambda x: x.translate(trs)), cols[0:1]
400
+ if func_name == "len":
401
+ return dest, partial(lambda x: len(str(x))), cols
402
+ if func_name == "join":
403
+ return dest, partial(lambda x: joinsep.join(str(v) for v in x)), cols
404
+ if func_name == "lower":
405
+ return dest, partial(lambda x: str(x).lower()), cols
406
+ if func_name == "upper":
407
+ return dest, partial(lambda x: str(x).upper()), cols
408
+ if func_name == "zfill":
409
+ return dest, partial(lambda x: str(x).zfill(int(cols[1]))), cols[0:1]
410
+ if func_name == "find":
411
+ return dest, partial(lambda x: str(x).find(str(cols[1]))), cols[0:1]
412
+ if func_name == "match":
413
+ return dest, partial(lambda x: str(x[0]).find(str(x[1]))), cols
414
+ if func_name == "substr":
415
+ start, length = int(cols[1]), int(cols[2])
416
+ return dest, partial(lambda x: str(x)[start : start + length]), cols[0:1]
417
+ if func_name == "substring":
418
+ length = int(cols[2])
419
+ return dest, partial(lambda x: str(x[0])[x[1] : x[1] + length]), cols[0:2]
420
+ if func_name == "replace":
421
+ return dest, partial(lambda x: str(x).replace(cols[1], cols[2])), cols[0:1]
422
+
423
+ if func_name == "leftsplit":
424
+ sep, idx = cols[1], int(cols[2])
425
+
426
+ def _leftsplit(x):
427
+ s = str(x)
428
+ if sep in s:
429
+ parts = s.split(sep, 1)
430
+ return parts[0] if idx == 0 else parts[1]
431
+ return s if idx == 0 else ""
432
+
433
+ return dest, _leftsplit, cols[0:1]
434
+
435
+ if func_name == "rightsplit":
436
+ sep, idx = cols[1], int(cols[2])
437
+
438
+ def _rightsplit(x):
439
+ s = str(x)
440
+ if sep in s:
441
+ parts = s.rsplit(sep, 1)
442
+ return parts[0] if idx == 0 else parts[1]
443
+ return s if idx == 0 else ""
444
+
445
+ return dest, _rightsplit, cols[0:1]
446
+
447
+ if func_name == "levenshtein":
448
+ if not _HAS_LEVENSHTEIN:
449
+ raise ImportError(
450
+ "levenshtein() requires the python-Levenshtein package. "
451
+ "Install it with: uv add python-Levenshtein"
452
+ )
453
+ return dest, partial(lambda x: _lev.distance(str(x[0]), str(x[1]))), cols[0:2]
454
+
455
+ if func_name == "sort":
456
+ return dest, partial(lambda x: "".join(sorted(str(x)))), cols
457
+ if func_name == "ord":
458
+
459
+ def _myord(x):
460
+ try:
461
+ return min(ord(c) for c in str(x))
462
+ except (TypeError, ValueError):
463
+ return 0
464
+
465
+ return dest, _myord, cols[0:1]
466
+ if func_name == "template":
467
+ return dest, partial(lambda x: _body.format(**x)), []
468
+ if func_name == "md5":
469
+ return (
470
+ dest,
471
+ partial(lambda x: hashlib.md5(str(x).encode()).hexdigest()),
472
+ cols[0:1],
473
+ )
474
+
475
+ # Path operations
476
+ if func_name == "basename":
477
+ return dest, partial(lambda x: os.path.basename(str(x))), cols
478
+ if func_name == "stem":
479
+ return dest, partial(lambda x: Path(str(x)).stem), cols
480
+ if func_name == "realpath":
481
+ return dest, partial(lambda x: str(Path(str(x)).resolve())), cols
482
+ if func_name == "isfile":
483
+ return dest, partial(lambda x: int(os.path.isfile(str(x)))), cols
484
+ if func_name == "exists":
485
+ return dest, partial(lambda x: int(os.path.exists(str(x)))), cols
486
+
487
+ # DNA / sequence operations
488
+ if func_name == "gccontent":
489
+ return dest, partial(lambda x: _seq.gc_content(x)), cols
490
+ if func_name == "basecount":
491
+ return dest, partial(lambda x: _seq.letter_total(x)), cols
492
+ if func_name == "motifcount":
493
+ pattern = cols[0]
494
+ return dest, partial(lambda x: _seq.count_motif(pattern, x)), cols[1:]
495
+ if func_name == "readoffset":
496
+ return dest, partial(lambda x: _seq.read_offset(*x)), cols
497
+
498
+ raise ValueError(
499
+ f"String function {func_name!r} is not available. See dftk eval --help."
500
+ )
501
+
502
+
503
+ # ---------------------------------------------------------------------------
504
+ # Core eval logic
505
+ # ---------------------------------------------------------------------------
506
+
507
+
508
+ def _eval(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
509
+ joinsep: str = getattr(args, "joinsep", "")
510
+ intbool: bool = getattr(args, "intbool", False)
511
+ existing = set(df.columns)
512
+
513
+ # -c: constant columns
514
+ for expr in getattr(args, "constant", []):
515
+ colname, constval_str = (x.strip() for x in expr.split("=", 1))
516
+ try:
517
+ constval: int | float | str = int(constval_str)
518
+ except ValueError:
519
+ try:
520
+ constval = float(constval_str)
521
+ except ValueError:
522
+ constval = constval_str
523
+ df[colname] = constval
524
+
525
+ # -f: pandas eval with special-function fallback
526
+ for formula in getattr(args, "formula", []):
527
+ try:
528
+ df.eval(formula, inplace=True)
529
+ except (ValueError, SyntaxError):
530
+ logger.debug(f"df.eval failed for {formula!r}, trying special_function")
531
+ dest, func, cols = _special_function(formula, list(df.columns))
532
+ if len(df) == 0:
533
+ df[dest] = True
534
+ elif isinstance(func, _WhereFunc):
535
+ df[dest] = func(df)
536
+ elif isinstance(func, _FrameFunc):
537
+ # Frame-level functions (idxmax/idxmin) need column labels
538
+ df[dest] = func(df[cols])
539
+ elif len(cols) == 1:
540
+ # Element-wise on a single column (e.g. np.sign, os.path.*)
541
+ df[dest] = df[cols[0]].apply(func)
542
+ else:
543
+ # Multi-column row function: pass numpy array so positional
544
+ # indexing (row[0], row[1], ...) works in pandas 2.x
545
+ df[dest] = df[cols].apply(lambda row, f=func: f(row.to_numpy()), axis=1)
546
+ except Exception:
547
+ if len(df) == 0:
548
+ dest = formula.split("=", 1)[0].strip()
549
+ df[dest] = True
550
+ else:
551
+ logger.error(f"Could not evaluate formula: {formula!r}")
552
+ raise
553
+
554
+ # -s: string / row functions
555
+ for strfunc in getattr(args, "strfunc", []):
556
+ try:
557
+ dest, func, cols = _string_function(strfunc, joinsep)
558
+ if len(df) == 0:
559
+ df[dest] = True
560
+ elif len(cols) == 0:
561
+ df[dest] = df.apply(func, axis=1)
562
+ elif len(cols) == 1:
563
+ df[dest] = df[cols[0]].apply(func)
564
+ else:
565
+ df[dest] = df[cols].apply(func, axis=1)
566
+ except Exception:
567
+ if len(df) == 0:
568
+ dest = strfunc.split("=", 1)[0].strip()
569
+ df[dest] = True
570
+ else:
571
+ logger.error(f"Could not evaluate string function: {strfunc!r}")
572
+ raise
573
+
574
+ # --intbool: convert newly created boolean columns to int
575
+ if intbool:
576
+ new_cols = set(df.columns) - existing
577
+ for col in new_cols:
578
+ if df[col].dtype == np.bool_:
579
+ df[col] = df[col].astype(np.int64)
580
+
581
+ return df
582
+
583
+
584
+ # ---------------------------------------------------------------------------
585
+ # Command
586
+ # ---------------------------------------------------------------------------
587
+
588
+
589
+ class EvalCommand(BaseCommand):
590
+ @property
591
+ def name(self) -> str:
592
+ return "eval"
593
+
594
+ @property
595
+ def help(self) -> str:
596
+ return "Evaluate formulas, constants, and string functions on a DataFrame"
597
+
598
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
599
+ import argparse as ap
600
+
601
+ parser.formatter_class = ap.RawDescriptionHelpFormatter
602
+ parser.epilog = _EPILOG
603
+
604
+ io.parser_read(parser)
605
+
606
+ g = parser.add_argument_group("formula evaluation")
607
+ g.add_argument(
608
+ "-f",
609
+ "--formula",
610
+ help=(
611
+ "pandas eval expression, or special function call "
612
+ "(dest = func(col1, col2, ...)). Repeatable."
613
+ ),
614
+ default=[],
615
+ action="append",
616
+ metavar="EXPR",
617
+ )
618
+ g.add_argument(
619
+ "-c",
620
+ "--constant",
621
+ help=(
622
+ "Add a constant column (dest = value). "
623
+ "Value is coerced to numeric when possible. Repeatable."
624
+ ),
625
+ default=[],
626
+ action="append",
627
+ metavar="EXPR",
628
+ )
629
+ g.add_argument(
630
+ "-s",
631
+ "--strfunc",
632
+ help=(
633
+ "String or row function (dest = func(col1, col2, ...)). "
634
+ "Repeatable. See --help for full function list."
635
+ ),
636
+ default=[],
637
+ action="append",
638
+ metavar="EXPR",
639
+ )
640
+ g.add_argument(
641
+ "--joinsep",
642
+ help="Separator used by the join() string function (default: empty).",
643
+ default="",
644
+ metavar="SEP",
645
+ )
646
+ g.add_argument(
647
+ "--intbool",
648
+ help="Convert newly created boolean columns to int (False=0, True=1).",
649
+ action="store_true",
650
+ )
651
+
652
+ io.parser_output(parser)
653
+
654
+ def execute(self, args: argparse.Namespace) -> None:
655
+ df = io.read(args)
656
+ df = _eval(df, args)
657
+ io.printdf(df, args)