dataframe-toolkit 0.10.0__tar.gz → 0.11.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.
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/PKG-INFO +34 -1
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/README.md +33 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/pyproject.toml.orig +1 -1
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/__init__.py +1 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/eval_cmd.py +148 -23
- dataframe_toolkit-0.11.0/src/dftk/commands/implode_cmd.py +189 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/base.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/cleannames_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/corr_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/explode_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/merge_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/print_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/query_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/scat_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/io.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.0}/src/dftk/common/stats.py +0 -0
- {dataframe_toolkit-0.10.0 → dataframe_toolkit-0.11.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.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -117,6 +117,7 @@ dftk dataset tips -o \
|
|
|
117
117
|
| `concat` | Concatenate two or more tables row-wise |
|
|
118
118
|
| `melt` | Reshape wide-to-long (`pd.melt`) |
|
|
119
119
|
| `explode` | Split separated values in a cell (`a,b`) into one row per value |
|
|
120
|
+
| `implode` | Join a column's values within groups into one cell, the inverse of `explode` |
|
|
120
121
|
| `pivot` | Reshape long-to-wide with per-cell aggregation |
|
|
121
122
|
| `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
|
|
122
123
|
| `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
|
|
@@ -223,6 +224,38 @@ printf "sample\tfluor\ns1\t60\ns2\t110\n" \
|
|
|
223
224
|
| dftk interp - --ref stdcurve.tsv -x fluor --refx fluor -v conc -d conc_ng_ul
|
|
224
225
|
```
|
|
225
226
|
|
|
227
|
+
### Filling missing values
|
|
228
|
+
|
|
229
|
+
`eval -f` takes pandas column methods (`COL.method(...)`), and `fillna`,
|
|
230
|
+
`ffill`, `bfill` and `interpolate` fill empty cells. Defaults from another
|
|
231
|
+
table, after a merge (a column in both files gets the suffix `_r`):
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
dftk merge data.tsv -r defaults.tsv -k type -t left -o \
|
|
235
|
+
| dftk eval ... -f "depth = depth.fillna(depth_r)" --drop depth_r
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The group mean, a fixed value, or the last value above (`ffill`) within
|
|
239
|
+
each group:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
dftk dataset penguins -o | dftk eval ... \
|
|
243
|
+
-f "mean = body_mass_g.groupby(species).transform('mean')" \
|
|
244
|
+
-f "body_mass_g = body_mass_g.fillna(mean)" --drop mean \
|
|
245
|
+
-f "sex = sex.fillna('unknown')"
|
|
246
|
+
|
|
247
|
+
dftk dataset healthexp -o \
|
|
248
|
+
| dftk eval ... -f "le = Life_Expectancy.where(Year % 10 != 5)" -o \
|
|
249
|
+
| dftk eval ... -f "le_filled = le.groupby(Country).ffill()"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`ffill`, `bfill` and `interpolate` use the rows in the order they are in:
|
|
253
|
+
sort in a step of its own first (`dftk eval data.tsv --sortasc site day -o
|
|
254
|
+
| dftk eval ... -f ...`), and use `groupby(...)` when the table holds
|
|
255
|
+
several series, or values are taken from the neighbouring group.
|
|
256
|
+
`interpolate()` has no grouped form and works by row position. `dftk eval
|
|
257
|
+
-h` covers these cases, with examples.
|
|
258
|
+
|
|
226
259
|
### Bootstrap confidence intervals
|
|
227
260
|
|
|
228
261
|
Bootstrap mode repeats the full stat computation N times on resampled data
|
|
@@ -88,6 +88,7 @@ dftk dataset tips -o \
|
|
|
88
88
|
| `concat` | Concatenate two or more tables row-wise |
|
|
89
89
|
| `melt` | Reshape wide-to-long (`pd.melt`) |
|
|
90
90
|
| `explode` | Split separated values in a cell (`a,b`) into one row per value |
|
|
91
|
+
| `implode` | Join a column's values within groups into one cell, the inverse of `explode` |
|
|
91
92
|
| `pivot` | Reshape long-to-wide with per-cell aggregation |
|
|
92
93
|
| `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
|
|
93
94
|
| `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
|
|
@@ -194,6 +195,38 @@ printf "sample\tfluor\ns1\t60\ns2\t110\n" \
|
|
|
194
195
|
| dftk interp - --ref stdcurve.tsv -x fluor --refx fluor -v conc -d conc_ng_ul
|
|
195
196
|
```
|
|
196
197
|
|
|
198
|
+
### Filling missing values
|
|
199
|
+
|
|
200
|
+
`eval -f` takes pandas column methods (`COL.method(...)`), and `fillna`,
|
|
201
|
+
`ffill`, `bfill` and `interpolate` fill empty cells. Defaults from another
|
|
202
|
+
table, after a merge (a column in both files gets the suffix `_r`):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
dftk merge data.tsv -r defaults.tsv -k type -t left -o \
|
|
206
|
+
| dftk eval ... -f "depth = depth.fillna(depth_r)" --drop depth_r
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The group mean, a fixed value, or the last value above (`ffill`) within
|
|
210
|
+
each group:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
dftk dataset penguins -o | dftk eval ... \
|
|
214
|
+
-f "mean = body_mass_g.groupby(species).transform('mean')" \
|
|
215
|
+
-f "body_mass_g = body_mass_g.fillna(mean)" --drop mean \
|
|
216
|
+
-f "sex = sex.fillna('unknown')"
|
|
217
|
+
|
|
218
|
+
dftk dataset healthexp -o \
|
|
219
|
+
| dftk eval ... -f "le = Life_Expectancy.where(Year % 10 != 5)" -o \
|
|
220
|
+
| dftk eval ... -f "le_filled = le.groupby(Country).ffill()"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`ffill`, `bfill` and `interpolate` use the rows in the order they are in:
|
|
224
|
+
sort in a step of its own first (`dftk eval data.tsv --sortasc site day -o
|
|
225
|
+
| dftk eval ... -f ...`), and use `groupby(...)` when the table holds
|
|
226
|
+
several series, or values are taken from the neighbouring group.
|
|
227
|
+
`interpolate()` has no grouped form and works by row position. `dftk eval
|
|
228
|
+
-h` covers these cases, with examples.
|
|
229
|
+
|
|
197
230
|
### Bootstrap confidence intervals
|
|
198
231
|
|
|
199
232
|
Bootstrap mode repeats the full stat computation N times on resampled data
|
|
@@ -31,6 +31,7 @@ COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
|
31
31
|
("concat", "concat_cmd:ConcatCommand"),
|
|
32
32
|
("melt", "melt_cmd:MeltCommand"),
|
|
33
33
|
("explode", "explode_cmd:ExplodeCommand"),
|
|
34
|
+
("implode", "implode_cmd:ImplodeCommand"),
|
|
34
35
|
("pivot", "pivot_cmd:PivotCommand"),
|
|
35
36
|
("func", "func_cmd:FuncCommand"),
|
|
36
37
|
("scale", "scale_cmd:ScaleCommand"),
|
|
@@ -14,6 +14,7 @@ import fnmatch
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
|
+
import re
|
|
17
18
|
from functools import partial
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
@@ -44,6 +45,88 @@ arithmetic and boolean expressions directly on column names:
|
|
|
44
45
|
dftk dataset tips -o | dftk eval ... -f "bill_and_tip = total_bill + tip * 2"
|
|
45
46
|
dftk dataset tips -o | dftk eval ... -f "good_tip = (tip > 3) & (total_bill < 30)"
|
|
46
47
|
|
|
48
|
+
Column methods are pandas Series methods, written COL.method(...); text
|
|
49
|
+
methods are under .str (name.str.lower(), not name.lower() or tolower()):
|
|
50
|
+
|
|
51
|
+
x = x.abs() x = x.round(1) x = x.clip(lower=0) y = name.str.upper()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
FILLING MISSING VALUES (-f)
|
|
55
|
+
---------------------------
|
|
56
|
+
fillna() fills the empty cells of a column; the other cells are kept:
|
|
57
|
+
|
|
58
|
+
dftk eval data.tsv -f "x = x.fillna(y)" from another column
|
|
59
|
+
-f "x = x.fillna(0)" -f "sex = sex.fillna('unknown')" a fixed value
|
|
60
|
+
-f "x = x.fillna(y).fillna(0)" y first, then 0
|
|
61
|
+
-f "x_filled = x.fillna(y)" keep x, add a new column
|
|
62
|
+
-f "missing = x.isna()" mark the empty cells
|
|
63
|
+
|
|
64
|
+
Defaults from another table: a column that is in both files gets the
|
|
65
|
+
suffix _r from merge, so fill from it and drop it. Here the defaults are
|
|
66
|
+
each species' median body mass:
|
|
67
|
+
|
|
68
|
+
dftk dataset penguins -o | dftk pivot ... -i species -v body_mass_g \\
|
|
69
|
+
-f median --move median_body_mass_g:body_mass_g -o defaults.parquet
|
|
70
|
+
dftk dataset penguins -o \\
|
|
71
|
+
| dftk merge ... -r defaults.parquet -k species -t left -o \\
|
|
72
|
+
| dftk eval ... -f "body_mass_g = body_mass_g.fillna(body_mass_g_r)" \\
|
|
73
|
+
--drop body_mass_g_r
|
|
74
|
+
|
|
75
|
+
The group mean (or 'median', 'min', ...), in one step:
|
|
76
|
+
|
|
77
|
+
dftk dataset penguins -o | dftk eval ... \\
|
|
78
|
+
-f "mean = body_mass_g.groupby(species).transform('mean')" \\
|
|
79
|
+
-f "body_mass_g = body_mass_g.fillna(mean)" --drop mean
|
|
80
|
+
|
|
81
|
+
From neighbouring rows (the co2 dataset is a weekly series with gaps):
|
|
82
|
+
|
|
83
|
+
dftk dataset co2 -o | dftk eval ... -f "ffill = co2.ffill()" \\
|
|
84
|
+
-f "bfill = co2.bfill()" -f "linear = co2.interpolate()"
|
|
85
|
+
|
|
86
|
+
ffill() the last value above; the first rows stay empty
|
|
87
|
+
bfill() the next value below; the last rows stay empty
|
|
88
|
+
ffill(limit=1) fill at most one row after each value
|
|
89
|
+
interpolate() a straight line between the values above and below, by
|
|
90
|
+
row position: rows must be evenly spaced (for x-based
|
|
91
|
+
lookup from a reference curve, see dftk interp)
|
|
92
|
+
|
|
93
|
+
Row order matters: these methods use the rows in the order they are in.
|
|
94
|
+
Sort first, in a step of its own (--sortasc is applied to the output, so
|
|
95
|
+
in the same command it comes after the fill):
|
|
96
|
+
|
|
97
|
+
dftk eval data.tsv --sortasc site day -o | dftk eval ... -f "..."
|
|
98
|
+
|
|
99
|
+
Several series in one table (sites, countries, samples) must be filled
|
|
100
|
+
within each group, or values are taken from the neighbouring group:
|
|
101
|
+
|
|
102
|
+
-f "x = x.groupby(site).ffill()"
|
|
103
|
+
-f "x = x.groupby(site).bfill()"
|
|
104
|
+
-f "x = x.groupby(site).ffill().fillna(x.groupby(site).bfill())"
|
|
105
|
+
|
|
106
|
+
A group's rows don't have to be next to each other, but within each group
|
|
107
|
+
they must be in order. In healthexp, rows are sorted by Year with the
|
|
108
|
+
countries mixed together, so a grouped ffill is right as it is:
|
|
109
|
+
|
|
110
|
+
dftk dataset healthexp -o \\
|
|
111
|
+
| dftk eval ... -f "le = Life_Expectancy.where(Year % 10 != 5)" -o \\
|
|
112
|
+
| dftk eval ... -f "le_filled = le.groupby(Country).ffill()"
|
|
113
|
+
|
|
114
|
+
interpolate() has no grouped form. Sort so that each group's rows are
|
|
115
|
+
together; then gaps inside a group are right, but a gap at the start or
|
|
116
|
+
end of a group is filled across the boundary. To leave those empty, keep
|
|
117
|
+
only values with something before and after them in the same group:
|
|
118
|
+
|
|
119
|
+
dftk dataset healthexp -o \\
|
|
120
|
+
| dftk eval ... -f "le = Life_Expectancy.where(Year % 10 != 5)" \\
|
|
121
|
+
--sortasc Country Year -o \\
|
|
122
|
+
| dftk eval ... -f "before = le.groupby(Country).ffill().notna()" \\
|
|
123
|
+
-f "after = le.groupby(Country).bfill().notna()" \\
|
|
124
|
+
-f "le_linear = le.interpolate().where(before & after)" \\
|
|
125
|
+
--drop before after
|
|
126
|
+
|
|
127
|
+
Only empty cells are filled; missing rows (a year with no row at all) are
|
|
128
|
+
not added.
|
|
129
|
+
|
|
47
130
|
If df.eval() fails (e.g. for function calls), the expression is retried
|
|
48
131
|
as a special function call using the syntax:
|
|
49
132
|
|
|
@@ -516,6 +599,68 @@ def _string_function(formula: str, joinsep: str):
|
|
|
516
599
|
# ---------------------------------------------------------------------------
|
|
517
600
|
|
|
518
601
|
|
|
602
|
+
def _formula_error(formula: str, exc: Exception) -> str:
|
|
603
|
+
"""Return a short message for a mistake pandas found in an -f formula."""
|
|
604
|
+
msg = str(exc)
|
|
605
|
+
m = re.search(r"'Series' object has no attribute '(\w+)'", msg)
|
|
606
|
+
if m:
|
|
607
|
+
name = m.group(1)
|
|
608
|
+
col = re.search(rf"(\w+)\.{name}\b", formula)
|
|
609
|
+
col = col.group(1) if col else "COL"
|
|
610
|
+
if hasattr(pd.Series.str, name):
|
|
611
|
+
hint = f"Text methods are under .str: {col}.str.{name}()."
|
|
612
|
+
else:
|
|
613
|
+
hint = f"Text methods are under .str, e.g. {col}.str.lower()."
|
|
614
|
+
return (
|
|
615
|
+
f"-f {formula!r}: column has no method {name!r}. {hint} See dftk eval -h."
|
|
616
|
+
)
|
|
617
|
+
return f"-f {formula!r}: {msg}"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _apply_special_function(df: pd.DataFrame, formula: str) -> None:
|
|
621
|
+
"""Evaluate a special-function formula and store its column in *df*."""
|
|
622
|
+
dest, func, cols = _special_function(formula, list(df.columns))
|
|
623
|
+
if len(df) == 0:
|
|
624
|
+
df[dest] = True
|
|
625
|
+
elif isinstance(func, _WhereFunc):
|
|
626
|
+
df[dest] = func(df)
|
|
627
|
+
elif isinstance(func, _FrameFunc):
|
|
628
|
+
# Frame-level functions (idxmax/idxmin) need column labels
|
|
629
|
+
df[dest] = func(df[cols])
|
|
630
|
+
elif len(cols) == 1:
|
|
631
|
+
# Element-wise on a single column (e.g. np.sign, os.path.*)
|
|
632
|
+
df[dest] = df[cols[0]].apply(func)
|
|
633
|
+
else:
|
|
634
|
+
# Multi-column row function: pass numpy array so positional
|
|
635
|
+
# indexing (row[0], row[1], ...) works in pandas 2.x
|
|
636
|
+
df[dest] = df[cols].apply(lambda row, f=func: f(row.to_numpy()), axis=1)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _eval_formula(df: pd.DataFrame, formula: str) -> pd.DataFrame:
|
|
640
|
+
"""Evaluate one -f formula: pandas df.eval(), else a special function."""
|
|
641
|
+
try:
|
|
642
|
+
# Not inplace: that writes into the existing column, which must keep
|
|
643
|
+
# its dtype, so x = x.fillna(y) or x = x.isna() failed whenever the
|
|
644
|
+
# result had another dtype
|
|
645
|
+
return df.eval(formula)
|
|
646
|
+
except (AttributeError, TypeError, NameError, KeyError) as exc:
|
|
647
|
+
# pandas ran the expression: a mistake in it, not a dftk bug
|
|
648
|
+
raise ValueError(_formula_error(formula, exc)) from exc
|
|
649
|
+
except (ValueError, SyntaxError) as eval_exc:
|
|
650
|
+
if re.match(r"\s*\w+\s*=\s*\w+\.", formula):
|
|
651
|
+
# dest = col.method(...) is never a special function
|
|
652
|
+
raise ValueError(_formula_error(formula, eval_exc)) from eval_exc
|
|
653
|
+
logger.debug(f"df.eval failed for {formula!r}, trying special_function")
|
|
654
|
+
try:
|
|
655
|
+
_apply_special_function(df, formula)
|
|
656
|
+
except ValueError as exc:
|
|
657
|
+
raise ValueError(f"{exc} (pandas df.eval: {eval_exc})") from exc
|
|
658
|
+
except Exception:
|
|
659
|
+
logger.error(f"Could not evaluate formula: {formula!r}")
|
|
660
|
+
raise
|
|
661
|
+
return df
|
|
662
|
+
|
|
663
|
+
|
|
519
664
|
def _eval(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
|
|
520
665
|
joinsep: str = getattr(args, "joinsep", "")
|
|
521
666
|
intbool: bool = getattr(args, "intbool", False)
|
|
@@ -536,31 +681,11 @@ def _eval(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
|
|
|
536
681
|
# -f: pandas eval with special-function fallback
|
|
537
682
|
for formula in getattr(args, "formula", []):
|
|
538
683
|
try:
|
|
539
|
-
df
|
|
540
|
-
except (ValueError, SyntaxError):
|
|
541
|
-
logger.debug(f"df.eval failed for {formula!r}, trying special_function")
|
|
542
|
-
dest, func, cols = _special_function(formula, list(df.columns))
|
|
543
|
-
if len(df) == 0:
|
|
544
|
-
df[dest] = True
|
|
545
|
-
elif isinstance(func, _WhereFunc):
|
|
546
|
-
df[dest] = func(df)
|
|
547
|
-
elif isinstance(func, _FrameFunc):
|
|
548
|
-
# Frame-level functions (idxmax/idxmin) need column labels
|
|
549
|
-
df[dest] = func(df[cols])
|
|
550
|
-
elif len(cols) == 1:
|
|
551
|
-
# Element-wise on a single column (e.g. np.sign, os.path.*)
|
|
552
|
-
df[dest] = df[cols[0]].apply(func)
|
|
553
|
-
else:
|
|
554
|
-
# Multi-column row function: pass numpy array so positional
|
|
555
|
-
# indexing (row[0], row[1], ...) works in pandas 2.x
|
|
556
|
-
df[dest] = df[cols].apply(lambda row, f=func: f(row.to_numpy()), axis=1)
|
|
684
|
+
df = _eval_formula(df, formula)
|
|
557
685
|
except Exception:
|
|
558
|
-
if len(df)
|
|
559
|
-
dest = formula.split("=", 1)[0].strip()
|
|
560
|
-
df[dest] = True
|
|
561
|
-
else:
|
|
562
|
-
logger.error(f"Could not evaluate formula: {formula!r}")
|
|
686
|
+
if len(df) != 0:
|
|
563
687
|
raise
|
|
688
|
+
df[formula.split("=", 1)[0].strip()] = True
|
|
564
689
|
|
|
565
690
|
# -s: string / row functions
|
|
566
691
|
for strfunc in getattr(args, "strfunc", []):
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.implode_cmd — dftk implode subcommand.
|
|
3
|
+
|
|
4
|
+
The inverse of explode: join the values of a column within each group of
|
|
5
|
+
rows into one cell ("GO:1,GO:2"). Several columns are joined in the same
|
|
6
|
+
row order, so explode -c a b followed by implode -c a b gives back the
|
|
7
|
+
original rows.
|
|
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 breast
|
|
22
|
+
BRCA1 GO:2 breast
|
|
23
|
+
TP53 GO:3 colon
|
|
24
|
+
|
|
25
|
+
dftk implode data.tsv -c go
|
|
26
|
+
|
|
27
|
+
gene go tissue
|
|
28
|
+
BRCA1 GO:1,GO:2 breast
|
|
29
|
+
TP53 GO:3 colon
|
|
30
|
+
|
|
31
|
+
By default rows are grouped by all the other columns, so rows are only
|
|
32
|
+
joined when everything else in them is the same. With -i, rows are grouped
|
|
33
|
+
by those columns only and the other columns are dropped. Rows with an
|
|
34
|
+
empty key form a group of their own.
|
|
35
|
+
|
|
36
|
+
Values are joined in the order of the rows; --sortvalues sorts them and
|
|
37
|
+
--unique drops repeats. Empty values are left out, and a group with none
|
|
38
|
+
gets an empty cell. Whole numbers are written without ".0" when every
|
|
39
|
+
value in the column is one (pandas reads integers with gaps as floats).
|
|
40
|
+
|
|
41
|
+
Several columns are joined row by row, so their n-th items still belong
|
|
42
|
+
together; --unique and --sortvalues then work on whole rows.
|
|
43
|
+
|
|
44
|
+
Runnable version: each species' islands, one cell per species:
|
|
45
|
+
|
|
46
|
+
dftk dataset penguins -o \\
|
|
47
|
+
| dftk implode ... -c island -i species --unique --sortvalues
|
|
48
|
+
|
|
49
|
+
Numbers, with another separator: the party sizes seen on each day:
|
|
50
|
+
|
|
51
|
+
dftk dataset tips -o \\
|
|
52
|
+
| dftk implode ... -c size -i day --unique --sortvalues --sep '|'
|
|
53
|
+
|
|
54
|
+
Undoing explode: --poscol puts the items back in their original order,
|
|
55
|
+
here after shuffling the rows, and is dropped from the output:
|
|
56
|
+
|
|
57
|
+
dftk dataset penguins -o \\
|
|
58
|
+
| dftk implode ... -c island -i species --unique -o \\
|
|
59
|
+
| dftk explode ... -c island --poscol n -o \\
|
|
60
|
+
| dftk sample ... -f 1 --randomseed 1 -o \\
|
|
61
|
+
| dftk implode ... -c island --poscol n
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _as_text(s: pd.Series) -> pd.Series:
|
|
66
|
+
"""Return *s* as strings, keeping missing values missing."""
|
|
67
|
+
values = s.dropna()
|
|
68
|
+
if (
|
|
69
|
+
pd.api.types.is_float_dtype(s)
|
|
70
|
+
and len(values)
|
|
71
|
+
and (values == values.round()).all()
|
|
72
|
+
):
|
|
73
|
+
# Integers with gaps are read as floats: write 10, not 10.0
|
|
74
|
+
s = s.astype("Int64")
|
|
75
|
+
text = s.astype(object).where(s.notna())
|
|
76
|
+
text = text.map(lambda v: v if pd.isna(v) else str(v))
|
|
77
|
+
return text.where(text != "")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _implode(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
|
|
81
|
+
cols = args.cols
|
|
82
|
+
poscol = args.poscol
|
|
83
|
+
if len(set(cols)) != len(cols):
|
|
84
|
+
raise ValueError(f"-c/--cols lists a column more than once: {cols}")
|
|
85
|
+
if poscol is not None and poscol in cols:
|
|
86
|
+
raise ValueError(f"--poscol {poscol!r} is also in -c/--cols")
|
|
87
|
+
|
|
88
|
+
if args.indexcols is not None:
|
|
89
|
+
keys = list(args.indexcols)
|
|
90
|
+
overlap = [c for c in keys if c in cols or c == poscol]
|
|
91
|
+
if overlap:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"-i/--indexcols overlaps -c/--cols or --poscol: {overlap}"
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
keys = [c for c in df.columns if c not in cols and c != poscol]
|
|
97
|
+
|
|
98
|
+
order = [c for c in df.columns if c in keys or c in cols]
|
|
99
|
+
work = df[keys + cols + ([poscol] if poscol else [])].reset_index(drop=True)
|
|
100
|
+
if keys:
|
|
101
|
+
work["_gid"] = work.groupby(keys, sort=False, dropna=False).ngroup()
|
|
102
|
+
else:
|
|
103
|
+
work["_gid"] = 0
|
|
104
|
+
|
|
105
|
+
if args.sortvalues:
|
|
106
|
+
work = work.sort_values(["_gid", *cols], kind="stable", na_position="last")
|
|
107
|
+
elif poscol:
|
|
108
|
+
work = work.sort_values(["_gid", poscol], kind="stable")
|
|
109
|
+
else:
|
|
110
|
+
work = work.sort_values("_gid", kind="stable")
|
|
111
|
+
if args.unique:
|
|
112
|
+
work = work.drop_duplicates(subset=["_gid", *cols])
|
|
113
|
+
|
|
114
|
+
for col in cols:
|
|
115
|
+
work[col] = _as_text(work[col])
|
|
116
|
+
grouped = work.groupby("_gid", sort=True)
|
|
117
|
+
joined = grouped[cols].agg(lambda s: args.sep.join(s.dropna()))
|
|
118
|
+
joined = joined.where(joined != "")
|
|
119
|
+
result = grouped[keys].first().join(joined) if keys else joined
|
|
120
|
+
return result.reset_index(drop=True)[order]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ImplodeCommand(BaseCommand):
|
|
124
|
+
@property
|
|
125
|
+
def name(self) -> str:
|
|
126
|
+
return "implode"
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def help(self) -> str:
|
|
130
|
+
return "Join a column's values within groups of rows into one cell"
|
|
131
|
+
|
|
132
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
133
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
134
|
+
parser.epilog = _EPILOG
|
|
135
|
+
|
|
136
|
+
self.add_io_arguments(parser)
|
|
137
|
+
|
|
138
|
+
g = parser.add_argument_group("implode options")
|
|
139
|
+
g.add_argument(
|
|
140
|
+
"-c",
|
|
141
|
+
"--cols",
|
|
142
|
+
nargs="+",
|
|
143
|
+
required=True,
|
|
144
|
+
metavar="COL",
|
|
145
|
+
help="Column(s) whose values are joined. Several columns are "
|
|
146
|
+
"joined in the same row order.",
|
|
147
|
+
)
|
|
148
|
+
g.add_argument(
|
|
149
|
+
"-i",
|
|
150
|
+
"--indexcols",
|
|
151
|
+
nargs="+",
|
|
152
|
+
default=None,
|
|
153
|
+
metavar="COL",
|
|
154
|
+
help="Group by these columns and drop the others "
|
|
155
|
+
"(default: group by all columns not in -c or --poscol).",
|
|
156
|
+
)
|
|
157
|
+
g.add_argument(
|
|
158
|
+
"--sep",
|
|
159
|
+
default=",",
|
|
160
|
+
metavar="SEP",
|
|
161
|
+
help="Separator between joined values (default: ,).",
|
|
162
|
+
)
|
|
163
|
+
g.add_argument(
|
|
164
|
+
"--poscol",
|
|
165
|
+
default=None,
|
|
166
|
+
metavar="NAME",
|
|
167
|
+
help="Join the values in the order of column NAME (e.g. the "
|
|
168
|
+
"positions from explode --poscol) and drop it.",
|
|
169
|
+
)
|
|
170
|
+
g.add_argument(
|
|
171
|
+
"--unique",
|
|
172
|
+
action="store_true",
|
|
173
|
+
help="Drop repeated values (with several -c columns, repeated rows).",
|
|
174
|
+
)
|
|
175
|
+
g.add_argument(
|
|
176
|
+
"--sortvalues",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="Sort the values in each cell (with several -c columns, "
|
|
179
|
+
"sort whole rows by them).",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
183
|
+
df = io.read(args)
|
|
184
|
+
check_cols(df, args.cols, "-c/--cols")
|
|
185
|
+
check_cols(df, args.indexcols, "-i/--indexcols")
|
|
186
|
+
if args.poscol is not None:
|
|
187
|
+
check_cols(df, [args.poscol], "--poscol")
|
|
188
|
+
result = _implode(df, args)
|
|
189
|
+
io.printdf(result, args)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|