dataframe-toolkit 0.15.0__tar.gz → 0.16.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.15.0 → dataframe_toolkit-0.16.0}/PKG-INFO +32 -2
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/README.md +31 -1
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/pyproject.toml.orig +1 -1
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/__init__.py +2 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/corr_cmd.py +169 -23
- dataframe_toolkit-0.16.0/src/dftk/commands/heatmap_cmd.py +253 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/merge_cmd.py +20 -0
- dataframe_toolkit-0.16.0/src/dftk/commands/pairs_cmd.py +366 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/docformats.py +24 -8
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/skills/dftk/SKILL.md +8 -3
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/base.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/cleannames_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/eval_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/explode_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/implode_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/print_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/query_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/scat_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/skill_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/evalfuncs.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/io.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/stats.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/io.py +0 -0
- {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/skills/dftk/references/snakemake.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dataframe-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -139,7 +139,7 @@ dftk dataset tips -o \
|
|
|
139
139
|
| `wstat` | Weighted descriptive statistics (wmean, wstd, weighted quantile CI) |
|
|
140
140
|
| `fit` | OLS/robust/weighted regression via R-style formulas; tidy table, `--summary`, `--anova` |
|
|
141
141
|
| `test` | P-values between column pairs: t-test, Mann-Whitney, Wilcoxon, KS, correlations, bootstrap; groups |
|
|
142
|
-
| `corr` | Pairwise column correlations (Pearson/Spearman/Kendall) with optional BCa bootstrap CI
|
|
142
|
+
| `corr` | Pairwise column correlations (Pearson/Spearman/Kendall) with optional BCa bootstrap CI; globs (`-c '*:*'`) or a full matrix (`--all`) |
|
|
143
143
|
| `describe` | Quick column-level summary (dtype, n, n_unique, n_null, sample values) |
|
|
144
144
|
| `info` | Per-column dtype, null counts, and memory usage; `--summary` for dataset-level totals |
|
|
145
145
|
| `randvar` | Sample from a distribution and append as a new column (norm, alpha, beta, …) |
|
|
@@ -157,6 +157,8 @@ publication-quality output.
|
|
|
157
157
|
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
|
|
158
158
|
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
|
|
159
159
|
| `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
|
|
160
|
+
| `heatmap` | Coloured grid of values from a long (`-x -y -v`) or wide (`-y`) table; correlograms from `corr --all`, `--annot`, `--lower` |
|
|
161
|
+
| `pairs` | Scatter-plot matrix with KDEs (or histograms) on the diagonal; `-g` colours groups, `--upper corr` puts r in the upper triangle |
|
|
160
162
|
|
|
161
163
|
### Utilities
|
|
162
164
|
|
|
@@ -409,6 +411,34 @@ dftk eval segments.tsv -f 'gap = Begin - End.groupby(Chr).shift()'
|
|
|
409
411
|
Without `groupby(Chr)` the first segment of a chromosome is compared with
|
|
410
412
|
the last one of the previous chromosome.
|
|
411
413
|
|
|
414
|
+
### Correlograms and heatmaps
|
|
415
|
+
|
|
416
|
+
`corr --all` correlates every pair of numeric columns both ways round, plus the diagonal, which is the square table `heatmap` draws. Values of both signs get diverging colours centred on 0; `--vmin -1 --vmax 1` keeps the full scale for r, so a weak correlation doesn't look strong. `--annot` writes the values in the cells and `--lower` keeps the lower triangle:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
dftk dataset iris -o | dftk corr --all -o \
|
|
420
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --vmin -1 --vmax 1 -f iris_corr.png
|
|
421
|
+
dftk dataset diamonds -o | dftk corr --all --method spearman -o \
|
|
422
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --lower --square \
|
|
423
|
+
--vmin -1 --vmax 1 -f diamonds_corr.png
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Missing values are dropped pair by pair, so `nobs` can differ between cells; `dftk print --dropna -o | dftk corr --all` uses the complete rows throughout. p-values are not adjusted for multiple testing.
|
|
427
|
+
|
|
428
|
+
For the shape of each relationship, not just r, `pairs` draws every pair of columns as a scatter plot with each column's KDE on the diagonal; `-g` colours groups (each group's KDE scaled by its share of the rows) and `--upper corr` writes r in the upper triangle, with `-g` also r within each group, which can differ even in sign from r over all rows:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
dftk dataset iris -o | dftk pairs -g species -f iris_pairs.png
|
|
432
|
+
dftk dataset iris -o | dftk pairs -g species --upper corr --diag hist -f iris_pairs_r.png
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`heatmap` draws any table with one row per cell (`-x -y -v`), or a wide one with `-y` naming the row labels and the other numeric columns as heatmap columns, as `pivot` makes it:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
dftk dataset penguins -o | dftk pivot -i species -g island -v body_mass_g -f count -o \
|
|
439
|
+
| dftk heatmap -y species --annot --cmap Blues -f penguin_counts.png
|
|
440
|
+
```
|
|
441
|
+
|
|
412
442
|
### Bootstrap confidence intervals
|
|
413
443
|
|
|
414
444
|
Bootstrap mode repeats the full stat computation N times on resampled data
|
|
@@ -109,7 +109,7 @@ dftk dataset tips -o \
|
|
|
109
109
|
| `wstat` | Weighted descriptive statistics (wmean, wstd, weighted quantile CI) |
|
|
110
110
|
| `fit` | OLS/robust/weighted regression via R-style formulas; tidy table, `--summary`, `--anova` |
|
|
111
111
|
| `test` | P-values between column pairs: t-test, Mann-Whitney, Wilcoxon, KS, correlations, bootstrap; groups |
|
|
112
|
-
| `corr` | Pairwise column correlations (Pearson/Spearman/Kendall) with optional BCa bootstrap CI
|
|
112
|
+
| `corr` | Pairwise column correlations (Pearson/Spearman/Kendall) with optional BCa bootstrap CI; globs (`-c '*:*'`) or a full matrix (`--all`) |
|
|
113
113
|
| `describe` | Quick column-level summary (dtype, n, n_unique, n_null, sample values) |
|
|
114
114
|
| `info` | Per-column dtype, null counts, and memory usage; `--summary` for dataset-level totals |
|
|
115
115
|
| `randvar` | Sample from a distribution and append as a new column (norm, alpha, beta, …) |
|
|
@@ -127,6 +127,8 @@ publication-quality output.
|
|
|
127
127
|
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
|
|
128
128
|
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
|
|
129
129
|
| `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
|
|
130
|
+
| `heatmap` | Coloured grid of values from a long (`-x -y -v`) or wide (`-y`) table; correlograms from `corr --all`, `--annot`, `--lower` |
|
|
131
|
+
| `pairs` | Scatter-plot matrix with KDEs (or histograms) on the diagonal; `-g` colours groups, `--upper corr` puts r in the upper triangle |
|
|
130
132
|
|
|
131
133
|
### Utilities
|
|
132
134
|
|
|
@@ -379,6 +381,34 @@ dftk eval segments.tsv -f 'gap = Begin - End.groupby(Chr).shift()'
|
|
|
379
381
|
Without `groupby(Chr)` the first segment of a chromosome is compared with
|
|
380
382
|
the last one of the previous chromosome.
|
|
381
383
|
|
|
384
|
+
### Correlograms and heatmaps
|
|
385
|
+
|
|
386
|
+
`corr --all` correlates every pair of numeric columns both ways round, plus the diagonal, which is the square table `heatmap` draws. Values of both signs get diverging colours centred on 0; `--vmin -1 --vmax 1` keeps the full scale for r, so a weak correlation doesn't look strong. `--annot` writes the values in the cells and `--lower` keeps the lower triangle:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
dftk dataset iris -o | dftk corr --all -o \
|
|
390
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --vmin -1 --vmax 1 -f iris_corr.png
|
|
391
|
+
dftk dataset diamonds -o | dftk corr --all --method spearman -o \
|
|
392
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --lower --square \
|
|
393
|
+
--vmin -1 --vmax 1 -f diamonds_corr.png
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Missing values are dropped pair by pair, so `nobs` can differ between cells; `dftk print --dropna -o | dftk corr --all` uses the complete rows throughout. p-values are not adjusted for multiple testing.
|
|
397
|
+
|
|
398
|
+
For the shape of each relationship, not just r, `pairs` draws every pair of columns as a scatter plot with each column's KDE on the diagonal; `-g` colours groups (each group's KDE scaled by its share of the rows) and `--upper corr` writes r in the upper triangle, with `-g` also r within each group, which can differ even in sign from r over all rows:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
dftk dataset iris -o | dftk pairs -g species -f iris_pairs.png
|
|
402
|
+
dftk dataset iris -o | dftk pairs -g species --upper corr --diag hist -f iris_pairs_r.png
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`heatmap` draws any table with one row per cell (`-x -y -v`), or a wide one with `-y` naming the row labels and the other numeric columns as heatmap columns, as `pivot` makes it:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
dftk dataset penguins -o | dftk pivot -i species -g island -v body_mass_g -f count -o \
|
|
409
|
+
| dftk heatmap -y species --annot --cmap Blues -f penguin_counts.png
|
|
410
|
+
```
|
|
411
|
+
|
|
382
412
|
### Bootstrap confidence intervals
|
|
383
413
|
|
|
384
414
|
Bootstrap mode repeats the full stat computation N times on resampled data
|
|
@@ -62,6 +62,8 @@ COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
|
62
62
|
("line", "line_cmd:LineCommand"),
|
|
63
63
|
("hist", "hist_cmd:HistCommand"),
|
|
64
64
|
("chi", "chi_cmd:ChiCommand"),
|
|
65
|
+
("heatmap", "heatmap_cmd:HeatmapCommand"),
|
|
66
|
+
("pairs", "pairs_cmd:PairsCommand"),
|
|
65
67
|
],
|
|
66
68
|
),
|
|
67
69
|
(
|
|
@@ -11,6 +11,8 @@ Bootstrap mode (--bootstrap N) produces a single summary row per pair with:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import fnmatch
|
|
15
|
+
import itertools
|
|
14
16
|
import logging
|
|
15
17
|
|
|
16
18
|
import numpy as np
|
|
@@ -140,14 +142,82 @@ def _tail_stats(a: np.ndarray, b: np.ndarray, q_grid: np.ndarray) -> tuple:
|
|
|
140
142
|
return tuple(out)
|
|
141
143
|
|
|
142
144
|
|
|
145
|
+
_GLOB_CHARS = frozenset("*?[")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _numeric_columns(df: pd.DataFrame, groups: list[str]) -> list[str]:
|
|
149
|
+
"""Numeric, non-boolean columns of *df* other than the group columns."""
|
|
150
|
+
return [
|
|
151
|
+
c
|
|
152
|
+
for c in df.columns
|
|
153
|
+
if c not in groups
|
|
154
|
+
and pd.api.types.is_numeric_dtype(df[c])
|
|
155
|
+
and not pd.api.types.is_bool_dtype(df[c])
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_glob(pattern: str, numeric: list[str]) -> bool:
|
|
160
|
+
"""A glob pattern, unless it is the name of a column (e.g. x[1])."""
|
|
161
|
+
return bool(_GLOB_CHARS & set(pattern)) and pattern not in numeric
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _match(pattern: str, numeric: list[str], option: str) -> list[str]:
|
|
165
|
+
"""A column name as is, or the numeric columns a glob pattern matches."""
|
|
166
|
+
if not _is_glob(pattern, numeric):
|
|
167
|
+
return [pattern]
|
|
168
|
+
matched = fnmatch.filter(numeric, pattern)
|
|
169
|
+
if not matched:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"{option}: {pattern!r} matches no numeric column (numeric columns: "
|
|
172
|
+
f"{numeric})"
|
|
173
|
+
)
|
|
174
|
+
return matched
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _expand_pairs(cols: list[str], numeric: list[str]) -> list[tuple[str, str]]:
|
|
178
|
+
"""-c COL1:COL2 items as pairs; a glob on either side expands.
|
|
179
|
+
|
|
180
|
+
A pair from a glob is left out when both sides are the same column or
|
|
181
|
+
when the pair already came up the other way round, so -c '*:*' gives
|
|
182
|
+
every pair once. Pairs named in full are kept as given.
|
|
183
|
+
"""
|
|
184
|
+
bad = [c for c in cols if len(c.split(":", maxsplit=1)) != 2]
|
|
185
|
+
if bad:
|
|
186
|
+
raise ValueError(f"-c pairs must be col1:col2, got: {bad}")
|
|
187
|
+
pairs: list[tuple[str, str]] = []
|
|
188
|
+
for item in cols:
|
|
189
|
+
left, right = item.split(":", maxsplit=1)
|
|
190
|
+
if not (_is_glob(left, numeric) or _is_glob(right, numeric)):
|
|
191
|
+
pairs.append((left, right))
|
|
192
|
+
continue
|
|
193
|
+
for a, b in itertools.product(
|
|
194
|
+
_match(left, numeric, "-c"), _match(right, numeric, "-c")
|
|
195
|
+
):
|
|
196
|
+
if a != b and (a, b) not in pairs and (b, a) not in pairs:
|
|
197
|
+
pairs.append((a, b))
|
|
198
|
+
return pairs
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _all_pairs(cols: list[str], numeric: list[str]) -> list[tuple[str, str]]:
|
|
202
|
+
"""--all [COL ...]: every ordered pair, diagonal included (a square matrix)."""
|
|
203
|
+
names: list[str] = []
|
|
204
|
+
for pattern in cols or ["*"]:
|
|
205
|
+
names += [c for c in _match(pattern, numeric, "--all") if c not in names]
|
|
206
|
+
if len(names) < 2:
|
|
207
|
+
raise ValueError(f"--all needs at least two numeric columns, got {names}")
|
|
208
|
+
return list(itertools.product(names, names))
|
|
209
|
+
|
|
210
|
+
|
|
143
211
|
def _corr(
|
|
144
|
-
df: pd.DataFrame,
|
|
212
|
+
df: pd.DataFrame,
|
|
213
|
+
args: argparse.Namespace,
|
|
214
|
+
rng: np.random.Generator,
|
|
215
|
+
pairs: list[tuple[str, str]] | None = None,
|
|
145
216
|
) -> pd.DataFrame:
|
|
217
|
+
"""Correlations for *pairs* (default: args.cols, globs expanded)."""
|
|
218
|
+
if pairs is None:
|
|
219
|
+
pairs = _expand_pairs(args.cols, _numeric_columns(df, args.groups))
|
|
146
220
|
method_fn = _METHOD_FUNC[args.method]
|
|
147
|
-
pairs = [c.split(":", maxsplit=1) for c in args.cols]
|
|
148
|
-
bad = [c for c, p in zip(args.cols, pairs, strict=False) if len(p) != 2]
|
|
149
|
-
if bad:
|
|
150
|
-
raise ValueError(f"-c pairs must be col1:col2, got: {bad}")
|
|
151
221
|
|
|
152
222
|
ci = args.ci and args.method == "pearson"
|
|
153
223
|
tail = getattr(args, "tail", False)
|
|
@@ -163,20 +233,49 @@ def _corr(
|
|
|
163
233
|
header += [f"lambda_lower_q{q:g}", f"lambda_upper_q{q:g}"]
|
|
164
234
|
|
|
165
235
|
rows = []
|
|
236
|
+
n_stats = len(header) - 2 # nobs onwards
|
|
166
237
|
|
|
167
238
|
def _add_pairs(sub: pd.DataFrame, prefix: tuple = ()):
|
|
239
|
+
# Every statistic is symmetric in the two columns, so --all's mirrored
|
|
240
|
+
# pair (b, a) reuses the result for (a, b)
|
|
241
|
+
done: dict[frozenset, tuple] = {}
|
|
168
242
|
for col1, col2 in pairs:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
a, b
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
243
|
+
key = frozenset((col1, col2))
|
|
244
|
+
if key not in done:
|
|
245
|
+
if col1 == col2: # the diagonal of --all; sub[[c, c]] is 2-D
|
|
246
|
+
a = b = sub[col1].dropna().values
|
|
247
|
+
else:
|
|
248
|
+
pair_df = sub[[col1, col2]].dropna()
|
|
249
|
+
a, b = pair_df[col1].values, pair_df[col2].values
|
|
250
|
+
if len(a) < 2:
|
|
251
|
+
where = f" (group {', '.join(map(str, prefix))})" if prefix else ""
|
|
252
|
+
logger.warning(
|
|
253
|
+
f"corr: {col1}:{col2}{where} has {len(a)} complete "
|
|
254
|
+
"row(s); statistics left empty"
|
|
255
|
+
)
|
|
256
|
+
stats = (len(a),) + (np.nan,) * (n_stats - 1)
|
|
257
|
+
elif col1 == col2:
|
|
258
|
+
# r is 1 by definition; a p-value or interval of a column
|
|
259
|
+
# with itself means nothing and would count as a finding,
|
|
260
|
+
# and ties push the tail statistics above 1
|
|
261
|
+
r = _compute_correlation(a, b, method_fn, False)[1]
|
|
262
|
+
r = 1.0 if np.isfinite(r) else r # NaN: a constant column
|
|
263
|
+
stats = (len(a), r) + (np.nan,) * (n_stats - 2)
|
|
264
|
+
else:
|
|
265
|
+
stats = _compute_correlation(a, b, method_fn, ci)
|
|
266
|
+
if args.bootstrap is not None:
|
|
267
|
+
stats += _bootstrap_stats(
|
|
268
|
+
a,
|
|
269
|
+
b,
|
|
270
|
+
method_fn,
|
|
271
|
+
args.bootstrap,
|
|
272
|
+
rng,
|
|
273
|
+
args.confidence / 100.0,
|
|
274
|
+
)
|
|
275
|
+
if tail:
|
|
276
|
+
stats += _tail_stats(a, b, q_grid)
|
|
277
|
+
done[key] = stats
|
|
278
|
+
rows.append((*prefix, col1, col2, *done[key]))
|
|
180
279
|
|
|
181
280
|
if args.groups:
|
|
182
281
|
header = [*args.groups, *header]
|
|
@@ -197,6 +296,36 @@ COLUMN PAIRS (-c)
|
|
|
197
296
|
dftk corr data.tsv -c x:y
|
|
198
297
|
dftk dataset tips -o | dftk corr -c total_bill:tip tip:size
|
|
199
298
|
|
|
299
|
+
A glob on either side expands to the numeric columns it matches (quote
|
|
300
|
+
it). '*:*' gives every pair once; 'sepal*:petal*' every sepal column
|
|
301
|
+
with every petal column:
|
|
302
|
+
|
|
303
|
+
dftk dataset iris -o | dftk corr -c '*:*'
|
|
304
|
+
dftk dataset iris -o | dftk corr -c 'sepal*:petal*' --method spearman
|
|
305
|
+
|
|
306
|
+
CORRELATION MATRIX (--all)
|
|
307
|
+
--------------------------
|
|
308
|
+
--all gives every pair both ways round plus the diagonal, in column order:
|
|
309
|
+
the square table a correlogram needs. Name columns (globs allowed) to
|
|
310
|
+
limit it; -g gives one matrix per group. Each pair is computed once. On
|
|
311
|
+
the diagonal r is 1 and the other statistics are empty.
|
|
312
|
+
|
|
313
|
+
dftk dataset iris -o | dftk corr --all
|
|
314
|
+
dftk dataset iris -o | dftk corr --all -o \\
|
|
315
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --vmin -1 --vmax 1
|
|
316
|
+
dftk dataset diamonds -o | dftk corr --all carat depth price -g cut
|
|
317
|
+
|
|
318
|
+
Missing values are dropped pair by pair, so nobs can differ from cell to
|
|
319
|
+
cell, and the matrix need not be a valid correlation matrix. For one set
|
|
320
|
+
of rows throughout, drop incomplete rows first:
|
|
321
|
+
|
|
322
|
+
dftk dataset penguins -o | dftk print --dropna -o | dftk corr --all
|
|
323
|
+
|
|
324
|
+
p-values are not adjusted for multiple testing, and --all lists each
|
|
325
|
+
p-value twice. Adjust over one triangle:
|
|
326
|
+
|
|
327
|
+
dftk dataset iris -o | dftk corr --all -o | dftk query -q 'col1 < col2'
|
|
328
|
+
|
|
200
329
|
METHODS (--method)
|
|
201
330
|
------------------
|
|
202
331
|
pearson Pearson r (default)
|
|
@@ -287,13 +416,25 @@ class CorrCommand(BaseCommand):
|
|
|
287
416
|
self.add_io_arguments(parser)
|
|
288
417
|
|
|
289
418
|
g = parser.add_argument_group("corr options")
|
|
290
|
-
g.
|
|
419
|
+
which = g.add_mutually_exclusive_group(required=True)
|
|
420
|
+
which.add_argument(
|
|
291
421
|
"-c",
|
|
292
422
|
"--cols",
|
|
293
423
|
nargs="+",
|
|
294
|
-
required=True,
|
|
295
424
|
metavar="COL1:COL2",
|
|
296
|
-
help="Column pair(s) to correlate, e.g. x:y."
|
|
425
|
+
help="Column pair(s) to correlate, e.g. x:y. A glob on either "
|
|
426
|
+
"side expands to numeric columns: '*:*' is every pair once, "
|
|
427
|
+
"'sepal*:petal*' every sepal column with every petal column.",
|
|
428
|
+
)
|
|
429
|
+
which.add_argument(
|
|
430
|
+
"--all",
|
|
431
|
+
nargs="*",
|
|
432
|
+
default=None,
|
|
433
|
+
metavar="COL",
|
|
434
|
+
help="Correlation matrix: every pair of the named columns (globs "
|
|
435
|
+
"allowed; default: all numeric columns) both ways round, plus the "
|
|
436
|
+
"diagonal, in column order. Pipe it to dftk heatmap for a "
|
|
437
|
+
"correlogram.",
|
|
297
438
|
)
|
|
298
439
|
g.add_argument(
|
|
299
440
|
"-g",
|
|
@@ -355,10 +496,15 @@ class CorrCommand(BaseCommand):
|
|
|
355
496
|
seed = normalize_seed(args.randomseed) if args.randomseed is not None else None
|
|
356
497
|
rng = np.random.default_rng(seed)
|
|
357
498
|
|
|
358
|
-
all_cols = [c for pair in args.cols for c in pair.split(":", maxsplit=1)]
|
|
359
499
|
df = io.read(args)
|
|
360
|
-
check_cols(df, all_cols, "-c/--cols")
|
|
361
500
|
check_cols(df, args.groups, "-g/--groups")
|
|
362
|
-
|
|
363
|
-
|
|
501
|
+
numeric = _numeric_columns(df, args.groups)
|
|
502
|
+
if args.all is not None:
|
|
503
|
+
pairs = _all_pairs(args.all, numeric)
|
|
504
|
+
check_cols(df, sorted({c for p in pairs for c in p}), "--all")
|
|
505
|
+
else:
|
|
506
|
+
pairs = _expand_pairs(args.cols, numeric)
|
|
507
|
+
check_cols(df, [c for p in pairs for c in p], "-c/--cols")
|
|
508
|
+
|
|
509
|
+
result = _corr(df, args, rng, pairs)
|
|
364
510
|
io.printdf(result, args)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.heatmap_cmd — dftk heatmap subcommand.
|
|
3
|
+
|
|
4
|
+
A coloured grid of values, e.g. a correlogram from ``dftk corr --all``.
|
|
5
|
+
|
|
6
|
+
Two input shapes:
|
|
7
|
+
|
|
8
|
+
long (-x COL -y COL -v COL) one row per cell, as corr and stat give it;
|
|
9
|
+
rows and columns keep their first-seen order
|
|
10
|
+
wide (-y COL) one row per heatmap row; every other numeric
|
|
11
|
+
column (or --select) is a heatmap column,
|
|
12
|
+
as pivot gives it or a matrix file holds it
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from dftk.commands.base import BaseCommand
|
|
21
|
+
from dftk.common.io import check_cols, io
|
|
22
|
+
from dftk.common.plot import (
|
|
23
|
+
add_figure_arguments,
|
|
24
|
+
apply_style,
|
|
25
|
+
make_figure,
|
|
26
|
+
save_or_show,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _matrix(df: pd.DataFrame, args: argparse.Namespace) -> pd.DataFrame:
|
|
31
|
+
"""The heatmap as a DataFrame: index = y labels, columns = x labels."""
|
|
32
|
+
if args.xcol is None and args.value is None:
|
|
33
|
+
if args.ycol is None:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"give -x, -y and -v (one row per cell), or -y alone (one row "
|
|
36
|
+
"per heatmap row, other numeric columns as heatmap columns)"
|
|
37
|
+
)
|
|
38
|
+
check_cols(df, [args.ycol], "-y/--ycol")
|
|
39
|
+
values = [
|
|
40
|
+
c
|
|
41
|
+
for c in df.columns
|
|
42
|
+
if c != args.ycol
|
|
43
|
+
and pd.api.types.is_numeric_dtype(df[c])
|
|
44
|
+
and not pd.api.types.is_bool_dtype(df[c])
|
|
45
|
+
]
|
|
46
|
+
if not values:
|
|
47
|
+
raise ValueError(f"no numeric columns besides -y {args.ycol}")
|
|
48
|
+
if df[args.ycol].duplicated().any():
|
|
49
|
+
raise ValueError(f"-y {args.ycol} has repeated values: one row each")
|
|
50
|
+
return df.set_index(args.ycol)[values].astype(float)
|
|
51
|
+
|
|
52
|
+
if None in (args.xcol, args.ycol, args.value):
|
|
53
|
+
raise ValueError("long input needs all of -x, -y and -v")
|
|
54
|
+
check_cols(df, [args.xcol, args.ycol, args.value], "-x/-y/-v")
|
|
55
|
+
keys = [args.ycol, args.xcol]
|
|
56
|
+
if df.duplicated(keys).any():
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"several rows for the same {args.xcol} and {args.ycol}: summarise "
|
|
59
|
+
"first (dftk pivot), or filter (--prequery)"
|
|
60
|
+
)
|
|
61
|
+
xs = pd.unique(df[args.xcol])
|
|
62
|
+
ys = pd.unique(df[args.ycol])
|
|
63
|
+
matrix = df.pivot(index=args.ycol, columns=args.xcol, values=args.value)
|
|
64
|
+
return matrix.reindex(index=ys, columns=xs).astype(float)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fmt(values: np.ndarray, fmt: str | None) -> str:
|
|
68
|
+
"""--fmt, or g for whole numbers (counts) and .2f otherwise."""
|
|
69
|
+
if fmt:
|
|
70
|
+
return fmt
|
|
71
|
+
finite = values[np.isfinite(values)]
|
|
72
|
+
return "g" if finite.size and np.all(finite == np.round(finite)) else ".2f"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _heatmap(ax, fig, matrix: pd.DataFrame, args: argparse.Namespace) -> None:
|
|
76
|
+
values = matrix.to_numpy()
|
|
77
|
+
shown = np.ma.masked_invalid(values)
|
|
78
|
+
if args.lower:
|
|
79
|
+
if matrix.shape[0] != matrix.shape[1]:
|
|
80
|
+
raise ValueError("--lower needs a square heatmap (as corr --all gives)")
|
|
81
|
+
shown = np.ma.masked_where(np.triu(np.ones_like(values), k=1) > 0, shown)
|
|
82
|
+
|
|
83
|
+
vmin, vmax, center = args.vmin, args.vmax, args.center
|
|
84
|
+
finite = values[np.isfinite(values)]
|
|
85
|
+
if center is None and finite.size and finite.min() < 0 < finite.max():
|
|
86
|
+
center = 0.0 # values of both signs: diverging colours around 0
|
|
87
|
+
cmap = args.cmap or ("RdBu_r" if center is not None else "viridis")
|
|
88
|
+
if center is not None and finite.size:
|
|
89
|
+
# Symmetric limits so that center gets the middle colour
|
|
90
|
+
half = max(abs(finite.max() - center), abs(finite.min() - center))
|
|
91
|
+
vmin = center - half if vmin is None else vmin
|
|
92
|
+
vmax = center + half if vmax is None else vmax
|
|
93
|
+
|
|
94
|
+
mesh = ax.pcolormesh(shown, cmap=cmap, vmin=vmin, vmax=vmax)
|
|
95
|
+
fig.colorbar(mesh, ax=ax, label=args.value or "")
|
|
96
|
+
|
|
97
|
+
ax.set_xticks(np.arange(matrix.shape[1]) + 0.5)
|
|
98
|
+
ax.set_xticklabels([str(c) for c in matrix.columns], rotation=45, ha="right")
|
|
99
|
+
ax.set_yticks(np.arange(matrix.shape[0]) + 0.5)
|
|
100
|
+
ax.set_yticklabels([str(i) for i in matrix.index])
|
|
101
|
+
ax.invert_yaxis() # first row at the top, as in the table
|
|
102
|
+
ax.set_xlabel(args.xlabel if args.xlabel is not None else (args.xcol or ""))
|
|
103
|
+
ax.set_ylabel(args.ylabel if args.ylabel is not None else (args.ycol or ""))
|
|
104
|
+
ax.set_title(args.title or "")
|
|
105
|
+
ax.set_aspect("equal" if args.square else "auto")
|
|
106
|
+
|
|
107
|
+
if args.annot:
|
|
108
|
+
fmt = _fmt(values, args.fmt)
|
|
109
|
+
for (i, j), v in np.ndenumerate(shown.filled(np.nan)):
|
|
110
|
+
if np.isfinite(v):
|
|
111
|
+
# Dark text on light cells, light text on dark cells
|
|
112
|
+
r, g, b, _ = mesh.cmap(mesh.norm(v))
|
|
113
|
+
light = 0.299 * r + 0.587 * g + 0.114 * b > 0.5
|
|
114
|
+
ax.text(
|
|
115
|
+
j + 0.5,
|
|
116
|
+
i + 0.5,
|
|
117
|
+
format(v, fmt),
|
|
118
|
+
ha="center",
|
|
119
|
+
va="center",
|
|
120
|
+
color="black" if light else "white",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_EPILOG = """\
|
|
125
|
+
INPUT
|
|
126
|
+
-----
|
|
127
|
+
Long, one row per cell (-x, -y, -v): rows and columns of the heatmap keep
|
|
128
|
+
the order in which their labels first appear.
|
|
129
|
+
|
|
130
|
+
dftk heatmap data.tsv -x sample -y gene -v expression
|
|
131
|
+
dftk dataset tips -o | dftk pivot -i day time -v tip -f mean -o \\
|
|
132
|
+
| dftk heatmap -x time -y day -v mean_tip --annot
|
|
133
|
+
|
|
134
|
+
Wide, one row per heatmap row (-y alone): every other numeric column is a
|
|
135
|
+
heatmap column (--select picks and orders them):
|
|
136
|
+
|
|
137
|
+
dftk dataset tips -o | dftk pivot -i day -g time -v tip -f mean -o \\
|
|
138
|
+
| dftk heatmap -y day --annot
|
|
139
|
+
|
|
140
|
+
CORRELOGRAM
|
|
141
|
+
-----------
|
|
142
|
+
corr --all gives every pair of columns both ways round, which is a square
|
|
143
|
+
heatmap. Values of both signs get diverging colours centred on 0. Give
|
|
144
|
+
--vmin -1 --vmax 1 for correlations: otherwise the scale stretches to the
|
|
145
|
+
largest |r|, and a weak correlation looks as strong as a strong one would:
|
|
146
|
+
|
|
147
|
+
dftk dataset iris -o | dftk corr --all -o \\
|
|
148
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --vmin -1 --vmax 1
|
|
149
|
+
|
|
150
|
+
--lower keeps the lower triangle and the diagonal; --square makes the
|
|
151
|
+
cells square:
|
|
152
|
+
|
|
153
|
+
dftk dataset diamonds -o | dftk corr --all --method spearman -o \\
|
|
154
|
+
| dftk heatmap -x col1 -y col2 -v correlation --annot --lower --square \\
|
|
155
|
+
--vmin -1 --vmax 1
|
|
156
|
+
|
|
157
|
+
COLOURS AND NUMBERS
|
|
158
|
+
-------------------
|
|
159
|
+
--cmap takes any matplotlib colour map (viridis, magma, RdBu_r, coolwarm,
|
|
160
|
+
...). --annot writes each value in its cell, with --fmt as a Python format
|
|
161
|
+
(.2f, .1%, g; default g for whole numbers, else .2f). Empty cells stay
|
|
162
|
+
blank.
|
|
163
|
+
|
|
164
|
+
dftk dataset penguins -o | dftk pivot -i species -g island -v body_mass_g \\
|
|
165
|
+
-f count -o \\
|
|
166
|
+
| dftk heatmap -y species --annot --cmap Blues -f counts.png
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class HeatmapCommand(BaseCommand):
|
|
171
|
+
name = "heatmap"
|
|
172
|
+
help = "Coloured grid of values, e.g. a correlogram from corr --all."
|
|
173
|
+
|
|
174
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
175
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
176
|
+
parser.epilog = _EPILOG
|
|
177
|
+
|
|
178
|
+
self.add_read_arguments(parser)
|
|
179
|
+
|
|
180
|
+
g = parser.add_argument_group("heatmap")
|
|
181
|
+
g.add_argument("-x", "--xcol", metavar="COL", help="Heatmap column labels.")
|
|
182
|
+
g.add_argument(
|
|
183
|
+
"-y",
|
|
184
|
+
"--ycol",
|
|
185
|
+
metavar="COL",
|
|
186
|
+
help="Heatmap row labels. Alone, without -x and -v: the other "
|
|
187
|
+
"numeric columns are the heatmap columns.",
|
|
188
|
+
)
|
|
189
|
+
g.add_argument("-v", "--value", metavar="COL", help="Cell values.")
|
|
190
|
+
g.add_argument(
|
|
191
|
+
"--select",
|
|
192
|
+
nargs="+",
|
|
193
|
+
default=None,
|
|
194
|
+
metavar="COL",
|
|
195
|
+
help="Wide input: the value columns to show, in this order.",
|
|
196
|
+
)
|
|
197
|
+
g.add_argument("--xlabel", default=None, help="X-axis label (default: -x).")
|
|
198
|
+
g.add_argument("--ylabel", default=None, help="Y-axis label (default: -y).")
|
|
199
|
+
g.add_argument("--annot", action="store_true", help="Write values in cells.")
|
|
200
|
+
g.add_argument(
|
|
201
|
+
"--fmt",
|
|
202
|
+
default=None,
|
|
203
|
+
metavar="FMT",
|
|
204
|
+
help="Format of --annot values, e.g. .2f or .1%% (default: g for "
|
|
205
|
+
"whole numbers, else .2f).",
|
|
206
|
+
)
|
|
207
|
+
g.add_argument(
|
|
208
|
+
"--cmap",
|
|
209
|
+
default=None,
|
|
210
|
+
help="Matplotlib colour map (default: RdBu_r around --center, "
|
|
211
|
+
"else viridis).",
|
|
212
|
+
)
|
|
213
|
+
g.add_argument(
|
|
214
|
+
"--center",
|
|
215
|
+
type=float,
|
|
216
|
+
default=None,
|
|
217
|
+
help="Value that gets the middle colour (default: 0 when values "
|
|
218
|
+
"have both signs).",
|
|
219
|
+
)
|
|
220
|
+
g.add_argument("--vmin", type=float, default=None, help="Colour scale minimum.")
|
|
221
|
+
g.add_argument("--vmax", type=float, default=None, help="Colour scale maximum.")
|
|
222
|
+
g.add_argument(
|
|
223
|
+
"--lower",
|
|
224
|
+
action="store_true",
|
|
225
|
+
help="Show only the lower triangle and diagonal (square heatmaps).",
|
|
226
|
+
)
|
|
227
|
+
g.add_argument("--square", action="store_true", help="Square cells.")
|
|
228
|
+
|
|
229
|
+
add_figure_arguments(parser)
|
|
230
|
+
|
|
231
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
232
|
+
df = io.read(args)
|
|
233
|
+
if args.select:
|
|
234
|
+
check_cols(df, args.select, "--select")
|
|
235
|
+
keep = [args.ycol] if args.ycol else []
|
|
236
|
+
df = df[keep + [c for c in args.select if c not in keep]]
|
|
237
|
+
matrix = _matrix(df, args)
|
|
238
|
+
|
|
239
|
+
if args.file:
|
|
240
|
+
import matplotlib
|
|
241
|
+
|
|
242
|
+
matplotlib.use("Agg")
|
|
243
|
+
|
|
244
|
+
import matplotlib.pyplot as plt
|
|
245
|
+
|
|
246
|
+
if getattr(args, "usetex", False):
|
|
247
|
+
plt.rc("text", usetex=True)
|
|
248
|
+
|
|
249
|
+
apply_style(args)
|
|
250
|
+
fig, axes = make_figure(args)
|
|
251
|
+
_heatmap(axes[0][0], fig, matrix, args)
|
|
252
|
+
fig.tight_layout()
|
|
253
|
+
save_or_show(fig, args)
|
|
@@ -29,6 +29,7 @@ import uuid
|
|
|
29
29
|
import pandas as pd
|
|
30
30
|
|
|
31
31
|
from dftk.commands.base import BaseCommand
|
|
32
|
+
from dftk.common import docformats
|
|
32
33
|
from dftk.common.io import check_cols, io
|
|
33
34
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
@@ -113,6 +114,24 @@ def _add_cross_col(
|
|
|
113
114
|
args.drop = drop
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
def _carry_json_columns(
|
|
118
|
+
merged: pd.DataFrame,
|
|
119
|
+
left: pd.DataFrame,
|
|
120
|
+
right: pd.DataFrame,
|
|
121
|
+
suffixes: tuple[str, str],
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Mark the JSON-text columns of both inputs in *merged* (see docformats)."""
|
|
124
|
+
names = []
|
|
125
|
+
for side, other, suffix in ((left, right, suffixes[0]), (right, left, suffixes[1])):
|
|
126
|
+
for col in docformats.json_columns(side):
|
|
127
|
+
name = col
|
|
128
|
+
if col in other.columns and f"{col}{suffix}" in merged.columns:
|
|
129
|
+
name = f"{col}{suffix}"
|
|
130
|
+
if name in merged.columns and name not in names:
|
|
131
|
+
names.append(name)
|
|
132
|
+
docformats.mark_json_columns(merged, names)
|
|
133
|
+
|
|
134
|
+
|
|
116
135
|
def _gap_sides(args: argparse.Namespace) -> tuple[bool, bool]:
|
|
117
136
|
"""Return (left, right): which side's columns can get missing values.
|
|
118
137
|
|
|
@@ -227,6 +246,7 @@ def _do_merge(
|
|
|
227
246
|
if "_merge" in df_merged.columns:
|
|
228
247
|
df_merged = df_merged.drop(columns=["_merge"])
|
|
229
248
|
|
|
249
|
+
_carry_json_columns(df_merged, df_left, df_right, suffixes)
|
|
230
250
|
return df_merged
|
|
231
251
|
|
|
232
252
|
|