dataframe-toolkit 0.9.1__tar.gz → 0.10.0__tar.gz

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.
Files changed (49) hide show
  1. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/PKG-INFO +2 -1
  2. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/README.md +1 -0
  3. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/pyproject.toml +1 -1
  4. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/pyproject.toml.orig +1 -1
  5. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/__init__.py +1 -0
  6. dataframe_toolkit-0.10.0/src/dftk/commands/explode_cmd.py +180 -0
  7. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/__init__.py +0 -0
  8. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/cli.py +0 -0
  9. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/annotate_cmd.py +0 -0
  10. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/base.py +0 -0
  11. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/binx_cmd.py +0 -0
  12. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/chi_cmd.py +0 -0
  13. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/clean_cmd.py +0 -0
  14. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/cleannames_cmd.py +0 -0
  15. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/concat_cmd.py +0 -0
  16. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/corr_cmd.py +0 -0
  17. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/dataset_cmd.py +0 -0
  18. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/describe_cmd.py +0 -0
  19. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/eval_cmd.py +0 -0
  20. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/fit_cmd.py +0 -0
  21. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/func_cmd.py +0 -0
  22. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/help.py +0 -0
  23. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/hist_cmd.py +0 -0
  24. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/info_cmd.py +0 -0
  25. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/interp_cmd.py +0 -0
  26. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/line_cmd.py +0 -0
  27. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/melt_cmd.py +0 -0
  28. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/merge_cmd.py +0 -0
  29. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/pivot_cmd.py +0 -0
  30. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/print_cmd.py +0 -0
  31. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/query_cmd.py +0 -0
  32. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/randvar_cmd.py +0 -0
  33. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/sample_cmd.py +0 -0
  34. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/scale_cmd.py +0 -0
  35. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/scat_cmd.py +0 -0
  36. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/segid_cmd.py +0 -0
  37. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/split_cmd.py +0 -0
  38. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/stat_cmd.py +0 -0
  39. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/test_cmd.py +0 -0
  40. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/transpose_cmd.py +0 -0
  41. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/commands/wstat_cmd.py +0 -0
  42. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/__init__.py +0 -0
  43. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/copula.py +0 -0
  44. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/io.py +0 -0
  45. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/plot.py +0 -0
  46. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/seed.py +0 -0
  47. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/seq.py +0 -0
  48. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/common/stats.py +0 -0
  49. {dataframe_toolkit-0.9.1 → dataframe_toolkit-0.10.0}/src/dftk/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dataframe-toolkit
3
- Version: 0.9.1
3
+ Version: 0.10.0
4
4
  Summary: A command-line toolkit for DataFrame analysis and manipulation
5
5
  Keywords: dataframe,statistics,data analysis,CLI,TSV,pandas
6
6
  Author: Gunnar Pálsson
@@ -116,6 +116,7 @@ dftk dataset tips -o \
116
116
  | `merge` | Join two tables on key columns (inner/left/right/outer) |
117
117
  | `concat` | Concatenate two or more tables row-wise |
118
118
  | `melt` | Reshape wide-to-long (`pd.melt`) |
119
+ | `explode` | Split separated values in a cell (`a,b`) into one row per value |
119
120
  | `pivot` | Reshape long-to-wide with per-cell aggregation |
120
121
  | `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
121
122
  | `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
@@ -87,6 +87,7 @@ dftk dataset tips -o \
87
87
  | `merge` | Join two tables on key columns (inner/left/right/outer) |
88
88
  | `concat` | Concatenate two or more tables row-wise |
89
89
  | `melt` | Reshape wide-to-long (`pd.melt`) |
90
+ | `explode` | Split separated values in a cell (`a,b`) into one row per value |
90
91
  | `pivot` | Reshape long-to-wide with per-cell aggregation |
91
92
  | `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
92
93
  | `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,6 +30,7 @@ COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
30
30
  ("merge", "merge_cmd:MergeCommand"),
31
31
  ("concat", "concat_cmd:ConcatCommand"),
32
32
  ("melt", "melt_cmd:MeltCommand"),
33
+ ("explode", "explode_cmd:ExplodeCommand"),
33
34
  ("pivot", "pivot_cmd:PivotCommand"),
34
35
  ("func", "func_cmd:FuncCommand"),
35
36
  ("scale", "scale_cmd:ScaleCommand"),
@@ -0,0 +1,180 @@
1
+ """
2
+ dftk.commands.explode_cmd — dftk explode subcommand.
3
+
4
+ Split a column of separated values (e.g. "GO:1, GO:2") into one row per
5
+ value, repeating the other columns on each new row (pandas str.split +
6
+ explode). Several columns are split in parallel: their n-th items end up
7
+ on the same row, so every row must have the same number of items in them.
8
+ """
9
+
10
+ import argparse
11
+
12
+ import pandas as pd
13
+
14
+ from dftk.commands.base import BaseCommand
15
+ from dftk.common.io import check_cols, io
16
+
17
+ _EPILOG = """\
18
+ EXAMPLE
19
+ -------
20
+ gene go tissue
21
+ BRCA1 GO:1, GO:2 breast
22
+ TP53 GO:3 colon
23
+
24
+ dftk explode data.tsv -c go
25
+
26
+ gene go tissue
27
+ BRCA1 GO:1 breast
28
+ BRCA1 GO:2 breast
29
+ TP53 GO:3 colon
30
+
31
+ Spaces around each item are removed. An empty cell stays as one row with
32
+ an empty value; --dropempty removes such rows. If all the new values in a
33
+ split column are numbers, the column becomes numeric (integers stay
34
+ integers); columns named in --readasobject stay text.
35
+
36
+ Several columns are split in parallel, item by item. For every
37
+ combination instead, run explode once per column:
38
+
39
+ id a b
40
+ 1 x,y 10,20
41
+
42
+ dftk explode data.tsv -c a b -> (1, x, 10), (1, y, 20)
43
+
44
+ Runnable version: collect each species' islands into one comma-separated
45
+ cell with SQL, then split them back into rows, numbering each item:
46
+
47
+ dftk dataset penguins -o \\
48
+ | dftk query ... -o --sql "SELECT species,
49
+ string_agg(DISTINCT island, ',' ORDER BY island) AS islands
50
+ FROM data GROUP BY species ORDER BY species" \\
51
+ | dftk explode ... -c islands --poscol n
52
+
53
+ Another separator, keeping only some columns:
54
+
55
+ dftk dataset tips -o \\
56
+ | dftk query ... -o --sql "SELECT day,
57
+ string_agg(DISTINCT size::VARCHAR, '|' ORDER BY size::VARCHAR) AS sizes
58
+ FROM data GROUP BY day ORDER BY day" \\
59
+ | dftk explode ... -c sizes --sep '|' --select sizes day
60
+ """
61
+
62
+
63
+ def _split_cell(value, sep: str):
64
+ if pd.isna(value):
65
+ return value
66
+ return [item.strip() for item in str(value).split(sep)]
67
+
68
+
69
+ def _to_numeric(s: pd.Series) -> pd.Series:
70
+ """Return *s* as integers or floats if every non-empty value is one."""
71
+ values = s.dropna()
72
+ if values.empty:
73
+ return s
74
+ text = values.astype(str)
75
+ if text.str.fullmatch(r"[+-]?\d+").all():
76
+ return s.astype("Int64") if s.isna().any() else s.astype("int64")
77
+ try:
78
+ return pd.to_numeric(s)
79
+ except (ValueError, TypeError):
80
+ return s
81
+
82
+
83
+ def _explode(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
84
+ cols = args.cols
85
+ if not args.sep:
86
+ raise ValueError("--sep must not be empty")
87
+ if len(set(cols)) != len(cols):
88
+ raise ValueError(f"-c/--cols lists a column more than once: {cols}")
89
+ if args.poscol is not None and args.poscol in df.columns:
90
+ raise ValueError(f"--poscol {args.poscol!r} is already a column")
91
+
92
+ df = df.reset_index(drop=True)
93
+ for col in cols:
94
+ df[col] = df[col].map(lambda v: _split_cell(v, args.sep)).astype(object)
95
+
96
+ if len(cols) > 1:
97
+ counts = pd.DataFrame(
98
+ {c: df[c].map(lambda v: len(v) if isinstance(v, list) else 1) for c in cols}
99
+ )
100
+ bad = counts.index[counts.nunique(axis=1) > 1]
101
+ if len(bad):
102
+ shown = "; ".join(
103
+ f"row {i + 1} ("
104
+ + ", ".join(f"{c}: {counts.at[i, c]}" for c in cols)
105
+ + ")"
106
+ for i in bad[:5]
107
+ )
108
+ more = f" and {len(bad) - 5} more" if len(bad) > 5 else ""
109
+ raise ValueError(
110
+ f"Rows with different numbers of items in -c/--cols: {shown}{more}. "
111
+ "Run explode once per column for every combination."
112
+ )
113
+
114
+ result = df.explode(cols)
115
+ if args.poscol is not None:
116
+ result[args.poscol] = result.groupby(level=0).cumcount() + 1
117
+ result = result.reset_index(drop=True)
118
+
119
+ for col in cols:
120
+ result[col] = result[col].replace("", pd.NA)
121
+ if args.dropempty:
122
+ result = result[result[cols].notna().any(axis=1)].reset_index(drop=True)
123
+
124
+ readasobject = getattr(args, "readasobject", None)
125
+ for col in cols:
126
+ if readasobject is not None and (not readasobject or col in readasobject):
127
+ continue
128
+ result[col] = _to_numeric(result[col])
129
+ return result
130
+
131
+
132
+ class ExplodeCommand(BaseCommand):
133
+ @property
134
+ def name(self) -> str:
135
+ return "explode"
136
+
137
+ @property
138
+ def help(self) -> str:
139
+ return "Split separated values in a column into one row per value"
140
+
141
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
142
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
143
+ parser.epilog = _EPILOG
144
+
145
+ self.add_io_arguments(parser)
146
+
147
+ g = parser.add_argument_group("explode options")
148
+ g.add_argument(
149
+ "-c",
150
+ "--cols",
151
+ nargs="+",
152
+ required=True,
153
+ metavar="COL",
154
+ help="Column(s) to split. Several columns are split in parallel "
155
+ "and must have the same number of items on each row.",
156
+ )
157
+ g.add_argument(
158
+ "--sep",
159
+ default=",",
160
+ metavar="SEP",
161
+ help="Separator between items, a plain string, not a regex (default: ,).",
162
+ )
163
+ g.add_argument(
164
+ "--poscol",
165
+ default=None,
166
+ metavar="NAME",
167
+ help="Add a column NAME with each item's position in its cell (1, 2, ...).",
168
+ )
169
+ g.add_argument(
170
+ "--dropempty",
171
+ action="store_true",
172
+ help="Drop rows whose split columns are all empty "
173
+ "(default: keep them, so no input row is lost).",
174
+ )
175
+
176
+ def execute(self, args: argparse.Namespace) -> None:
177
+ df = io.read(args)
178
+ check_cols(df, args.cols, "-c/--cols")
179
+ result = _explode(df, args)
180
+ io.printdf(result, args)