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.
dftk/commands/base.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ dftk.commands.base — abstract base class for all dftk subcommands.
3
+
4
+ Every subcommand must:
5
+
6
+ 1. Inherit from BaseCommand.
7
+ 2. Define the ``name`` and ``help`` class attributes (or properties).
8
+ 3. Implement ``add_arguments(parser)`` to declare its CLI arguments.
9
+ 4. Implement ``execute(args)`` to run the command.
10
+
11
+ Most commands also call ``self.add_io_arguments(parser)`` inside
12
+ ``add_arguments`` to attach the standard dftk read + output argument groups.
13
+
14
+ Example skeleton::
15
+
16
+ from dftk.commands.base import BaseCommand
17
+ from dftk.common.io import io
18
+
19
+ class MyCommand(BaseCommand):
20
+ name = "mycommand"
21
+ help = "Does something useful"
22
+
23
+ def add_arguments(self, parser):
24
+ self.add_io_arguments(parser) # adds DATAFILE, -o, etc.
25
+ g = parser.add_argument_group("mycommand options")
26
+ g.add_argument("-x", "--xcol", ...)
27
+
28
+ def execute(self, args):
29
+ df = io.read(args)
30
+ # ... transform df ...
31
+ io.printdf(df, args)
32
+ """
33
+
34
+ import argparse
35
+ from abc import ABC, abstractmethod
36
+
37
+
38
+ class BaseCommand(ABC):
39
+ """Abstract base class for all dftk subcommands."""
40
+
41
+ @property
42
+ @abstractmethod
43
+ def name(self) -> str:
44
+ """Command name as it appears on the CLI (e.g. ``"stat"``)."""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def help(self) -> str:
49
+ """One-line description shown in ``dftk --help``."""
50
+
51
+ @abstractmethod
52
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
53
+ """Declare all CLI arguments for this command on *parser*."""
54
+
55
+ @abstractmethod
56
+ def execute(self, args: argparse.Namespace) -> None:
57
+ """Run the command using the parsed *args*."""
58
+
59
+ # ------------------------------------------------------------------ #
60
+ # Shared helpers #
61
+ # ------------------------------------------------------------------ #
62
+
63
+ def add_io_arguments(self, parser: argparse.ArgumentParser) -> None:
64
+ """Attach the standard dftk read + output argument groups to *parser*.
65
+
66
+ Adds:
67
+ * All ``io.parser_read`` arguments (DATAFILE, --backend, --noheader, …)
68
+ * All ``io.parser_output`` arguments (-o/--output, --select, --drop, …)
69
+
70
+ Call this at the top of ``add_arguments`` for any command that reads
71
+ tabular data and produces tabular output.
72
+ """
73
+ from dftk.common.io import io
74
+ io.parser_read(parser)
75
+ io.parser_output(parser)
76
+
77
+ def add_read_arguments(self, parser: argparse.ArgumentParser) -> None:
78
+ """Attach only the read argument group to *parser* (no output args).
79
+
80
+ Use this for commands that read tabular data but produce non-tabular
81
+ output (e.g. plots), so that --select, --drop, --round, etc. are not
82
+ exposed.
83
+ """
84
+ from dftk.common.io import io
85
+ io.parser_read(parser)
@@ -0,0 +1,130 @@
1
+ """
2
+ dftk.commands.binx_cmd — dftk binx subcommand.
3
+
4
+ Port of dfbinx.py: assign each row a bin index based on a column value and
5
+ a set of bin edges, with an optional replacement by the lower, middle, or
6
+ upper edge value of that bin.
7
+ """
8
+
9
+ import argparse
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
+
17
+
18
+ def _parse_binspec(binspec: str) -> list[float]:
19
+ """Parse 'min:max:step' or 'e1,e2,...,eN' into a list of bin edges."""
20
+ if "," in binspec:
21
+ return [float(x) for x in binspec.split(",")]
22
+ parts = binspec.split(":")
23
+ if len(parts) != 3:
24
+ raise ValueError(
25
+ f"binspec must be 'min:max:step' or comma-separated edges, got: {binspec!r}"
26
+ )
27
+ bmin, bmax, bstep = float(parts[0]), float(parts[1]), float(parts[2])
28
+ return list(np.arange(bmin, bmax, bstep))
29
+
30
+
31
+ def _binx(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
32
+ bins = _parse_binspec(args.binspec)
33
+ if len(bins) < 2:
34
+ raise ValueError(
35
+ f"binspec must produce at least 2 edges; got {len(bins)}: {bins}"
36
+ )
37
+
38
+ dest = args.destcol or f"{args.col}_bin"
39
+ df[dest] = pd.cut(df[args.col], bins, labels=False, include_lowest=True)
40
+
41
+ if args.usevalue is not None:
42
+ frac = {"l": 0.0, "m": 0.5, "u": 1.0}[args.usevalue]
43
+ edges = bins
44
+
45
+ def _to_edge(idx: float) -> float:
46
+ if pd.isna(idx):
47
+ return float("nan")
48
+ i = int(idx)
49
+ return (1.0 - frac) * edges[i] + frac * edges[i + 1]
50
+
51
+ df[dest] = df[dest].apply(_to_edge)
52
+
53
+ return df
54
+
55
+
56
+ _EPILOG = """\
57
+ BIN SPECIFICATION (--binspec)
58
+ ------------------------------
59
+ min:max:step Generate edges with np.arange(min, max, step).
60
+ Note: max is exclusive, same as Python range().
61
+ e1,e2,...,eN Explicit comma-separated edge values.
62
+
63
+ Examples:
64
+ 0:100:10 edges [0, 10, 20, ..., 90] → 9 bins
65
+ 0,5,10,50 3 bins: [0,5), [5,10), [10,50]
66
+
67
+ USEVALUE (--usevalue)
68
+ ---------------------
69
+ Replaces the integer bin index with the actual edge value:
70
+ l lower edge of the bin
71
+ m midpoint between lower and upper edges
72
+ u upper edge of the bin
73
+
74
+ EXAMPLES
75
+ --------
76
+ dftk binx data.tsv -c age -b 0:100:10
77
+ dftk binx data.tsv -c score -b 0,25,50,75,100 -d quartile --usevalue m
78
+ dftk binx data.tsv -c x -b -3:4:1 -d x_bin -o | dftk stat -g x_bin -s y
79
+ """
80
+
81
+
82
+ class BinxCommand(BaseCommand):
83
+ @property
84
+ def name(self) -> str:
85
+ return "binx"
86
+
87
+ @property
88
+ def help(self) -> str:
89
+ return "Assign bin indices to a column based on explicit or generated edges."
90
+
91
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
92
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
93
+ parser.epilog = _EPILOG
94
+
95
+ self.add_io_arguments(parser)
96
+
97
+ g = parser.add_argument_group("binx options")
98
+ g.add_argument(
99
+ "-c",
100
+ "--col",
101
+ required=True,
102
+ metavar="COL",
103
+ help="Column to bin.",
104
+ )
105
+ g.add_argument(
106
+ "-d",
107
+ "--destcol",
108
+ default=None,
109
+ metavar="DESTCOL",
110
+ help="Name of the output bin column (default: <col>_bin).",
111
+ )
112
+ g.add_argument(
113
+ "-b",
114
+ "--binspec",
115
+ required=True,
116
+ metavar="SPEC",
117
+ help="Bin edges: 'min:max:step' or 'e1,e2,...,eN'.",
118
+ )
119
+ g.add_argument(
120
+ "--usevalue",
121
+ choices=["l", "m", "u"],
122
+ default=None,
123
+ help="Replace bin index with bin edge value: l=lower, m=mid, u=upper.",
124
+ )
125
+
126
+ def execute(self, args: argparse.Namespace) -> None:
127
+ df = io.read(args)
128
+ check_cols(df, [args.col], "-c/--col")
129
+ result = _binx(df, args)
130
+ io.printdf(result, args)
@@ -0,0 +1,51 @@
1
+ """
2
+ dftk.commands.clean_cmd — the ``dftk clean`` subcommand.
3
+
4
+ Removes all temp parquet pipe files left in DFTK_TMPDIR. Useful after a
5
+ pipeline is interrupted before the final ``dftk print`` stage has had a
6
+ chance to consume (and therefore delete) them.
7
+
8
+ dftk clean
9
+ """
10
+
11
+ import argparse
12
+ import os
13
+ import sys
14
+
15
+ from dftk.commands.base import BaseCommand
16
+ from dftk.common.io import DFTK_TMPDIR
17
+
18
+
19
+ class CleanCommand(BaseCommand):
20
+ """Remove leftover temp parquet pipe files."""
21
+
22
+ name = "clean"
23
+ help = "Remove temp parquet pipe files from the dftk temp directory"
24
+
25
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
26
+ # No arguments needed — always cleans DFTK_TMPDIR.
27
+ pass
28
+
29
+ def execute(self, args: argparse.Namespace) -> None:
30
+ if not os.path.isdir(DFTK_TMPDIR):
31
+ print(
32
+ f"Nothing to clean (directory does not exist: {DFTK_TMPDIR})",
33
+ file=sys.stderr,
34
+ )
35
+ return
36
+
37
+ files = [f for f in os.listdir(DFTK_TMPDIR) if f.endswith(".parquet")]
38
+ errors = 0
39
+ for f in files:
40
+ try:
41
+ os.unlink(os.path.join(DFTK_TMPDIR, f))
42
+ except OSError as exc:
43
+ print(f"Warning: could not remove {f}: {exc}", file=sys.stderr)
44
+ errors += 1
45
+
46
+ removed = len(files) - errors
47
+ print(
48
+ f"Removed {removed} file(s) from {DFTK_TMPDIR}"
49
+ + (f" ({errors} error(s))" if errors else ""),
50
+ file=sys.stderr,
51
+ )
@@ -0,0 +1,165 @@
1
+ """
2
+ dftk.commands.concat_cmd — dftk concat subcommand.
3
+
4
+ Concatenate two or more input files (TSV or parquet) row-wise into a single
5
+ DataFrame. Column alignment follows pandas outer-join semantics: columns
6
+ present in some but not all files are filled with NaN (or --fill value).
7
+
8
+ Examples
9
+ --------
10
+ dftk concat a.tsv b.tsv c.tsv
11
+ dftk concat a.tsv b.tsv --sourcecol source
12
+ dftk concat a.parquet b.parquet -o
13
+ dftk eval data.tsv -f "x=1" -o | dftk concat ... extra.tsv
14
+ """
15
+
16
+ import argparse
17
+ import copy
18
+
19
+ import pandas as pd
20
+
21
+ from dftk.commands.base import BaseCommand
22
+ from dftk.common.io import io
23
+
24
+
25
+ class ConcatCommand(BaseCommand):
26
+ @property
27
+ def name(self) -> str:
28
+ return "concat"
29
+
30
+ @property
31
+ def help(self) -> str:
32
+ return "Concatenate multiple input files row-wise"
33
+
34
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
35
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
36
+ parser.epilog = """\
37
+ COLUMN ALIGNMENT
38
+ ----------------
39
+ Columns present in all files are aligned by name.
40
+ Columns missing from some files are filled with NaN (or --fill value).
41
+ Use --select to enforce a fixed output column set.
42
+
43
+ EXAMPLES
44
+ --------
45
+ dftk concat a.tsv b.tsv
46
+ dftk concat a.tsv b.tsv c.tsv --sourcecol source
47
+ dftk concat a.parquet b.parquet -o result.parquet
48
+ dftk eval data.tsv -f "x=1" -o | dftk concat ... extra.tsv
49
+ """
50
+ g = parser.add_argument_group("concat options")
51
+ g.add_argument(
52
+ "DATAFILES",
53
+ nargs="+",
54
+ metavar="FILE",
55
+ help=(
56
+ "Input files to concatenate (TSV or .parquet). "
57
+ "Use - for TSV stdin or ... for a parquet pipe path on stdin. "
58
+ "At most one stdin source is allowed."
59
+ ),
60
+ )
61
+ g.add_argument(
62
+ "--sourcecol",
63
+ default=None,
64
+ metavar="COL",
65
+ help="Add a column with this name containing the source filename.",
66
+ )
67
+ g.add_argument(
68
+ "--fill",
69
+ default=None,
70
+ metavar="VALUE",
71
+ help=(
72
+ "Fill value for columns absent in some files "
73
+ "(default: NaN). Use 0 or '' for numeric/string defaults."
74
+ ),
75
+ )
76
+
77
+ r = parser.add_argument_group("data input")
78
+ r.add_argument(
79
+ "--backend",
80
+ help="Backend for reading data (default: pandas)",
81
+ choices=["pandas", "duckdb"],
82
+ default="pandas",
83
+ )
84
+ r.add_argument(
85
+ "--noheader",
86
+ help="Input files have no header row; columns are named V1, V2, …",
87
+ action="store_true",
88
+ )
89
+ r.add_argument(
90
+ "--nrows",
91
+ help="Maximum number of rows to read per file",
92
+ type=int,
93
+ default=None,
94
+ )
95
+ r.add_argument(
96
+ "--delimiter",
97
+ help="Column delimiter for TSV inputs (default: tab)",
98
+ default=None,
99
+ )
100
+ r.add_argument(
101
+ "--readasobject",
102
+ help=(
103
+ "Read named columns as object dtype. "
104
+ "Omit column names to apply to all columns."
105
+ ),
106
+ nargs="*",
107
+ default=None,
108
+ metavar="COL",
109
+ )
110
+ r.add_argument(
111
+ "--prequery",
112
+ help="Pandas query expression after reading each file. Repeatable.",
113
+ default=[],
114
+ action="append",
115
+ metavar="EXPR",
116
+ )
117
+
118
+ io.parser_output(parser)
119
+
120
+ def execute(self, args: argparse.Namespace) -> None:
121
+ stdin_used = False
122
+ dfs: list[pd.DataFrame] = []
123
+
124
+ for path in args.DATAFILES:
125
+ if path in ("-", "..."):
126
+ if stdin_used:
127
+ raise ValueError(
128
+ "stdin (-/...) can only appear once in a concat file list"
129
+ )
130
+ stdin_used = True
131
+
132
+ # Build a per-file args with DATAFILE set to this path.
133
+ file_args = copy.copy(args)
134
+ file_args.DATAFILE = path
135
+
136
+ try:
137
+ df = io.read(file_args)
138
+ except pd.errors.EmptyDataError:
139
+ import logging
140
+
141
+ logging.warning(f"concat: skipping empty file {path!r}")
142
+ continue
143
+
144
+ if args.sourcecol:
145
+ df[args.sourcecol] = path
146
+
147
+ dfs.append(df)
148
+
149
+ if not dfs:
150
+ raise ValueError("concat: no data read from any input file")
151
+
152
+ result = pd.concat(dfs, ignore_index=True)
153
+
154
+ if args.fill is not None:
155
+ fill: int | float | str
156
+ try:
157
+ fill = int(args.fill)
158
+ except ValueError:
159
+ try:
160
+ fill = float(args.fill)
161
+ except ValueError:
162
+ fill = args.fill
163
+ result = result.fillna(fill)
164
+
165
+ io.printdf(result, args)