dataframe-toolkit 0.15.1__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.1 → dataframe_toolkit-0.16.0}/PKG-INFO +32 -2
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/README.md +31 -1
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/pyproject.toml.orig +1 -1
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/__init__.py +2 -0
- {dataframe_toolkit-0.15.1 → 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.16.0/src/dftk/commands/pairs_cmd.py +366 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/skills/dftk/SKILL.md +8 -3
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/base.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/cleannames_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/eval_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/explode_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/implode_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/merge_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/print_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/query_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/scat_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/skill_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/docformats.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/evalfuncs.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/io.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/common/stats.py +0 -0
- {dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/io.py +0 -0
- {dataframe_toolkit-0.15.1 → 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)
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.pairs_cmd — dftk pairs subcommand.
|
|
3
|
+
|
|
4
|
+
Scatter-plot matrix: every pair of columns as a small scatter plot, with each
|
|
5
|
+
column's distribution (KDE or histogram) on the diagonal. The upper triangle
|
|
6
|
+
repeats the scatter plots mirrored, or shows the correlation (--upper corr).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import fnmatch
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from dftk.commands.base import BaseCommand
|
|
16
|
+
from dftk.common.io import check_cols, io
|
|
17
|
+
from dftk.common.plot import (
|
|
18
|
+
_DEFAULT_SIZE,
|
|
19
|
+
WONG_PALETTE,
|
|
20
|
+
add_figure_arguments,
|
|
21
|
+
apply_style,
|
|
22
|
+
parse_figsize,
|
|
23
|
+
save_or_show,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_PANEL_INCHES = 2.2 # default size of one panel
|
|
27
|
+
_MAX_INCHES = 12.0 # default figure size cap
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _columns(df: pd.DataFrame, patterns: list[str] | None, exclude) -> list[str]:
|
|
31
|
+
"""-c names and globs (default: every numeric column), in that order."""
|
|
32
|
+
numeric = [
|
|
33
|
+
c
|
|
34
|
+
for c in df.columns
|
|
35
|
+
if c not in exclude
|
|
36
|
+
and pd.api.types.is_numeric_dtype(df[c])
|
|
37
|
+
and not pd.api.types.is_bool_dtype(df[c])
|
|
38
|
+
]
|
|
39
|
+
if not patterns:
|
|
40
|
+
cols = numeric
|
|
41
|
+
else:
|
|
42
|
+
cols = []
|
|
43
|
+
for pattern in patterns:
|
|
44
|
+
if set("*?[") & set(pattern) and pattern not in numeric:
|
|
45
|
+
matched = fnmatch.filter(numeric, pattern)
|
|
46
|
+
if not matched:
|
|
47
|
+
raise ValueError(f"-c: {pattern!r} matches no numeric column")
|
|
48
|
+
else:
|
|
49
|
+
check_cols(df, [pattern], "-c/--cols")
|
|
50
|
+
matched = [pattern]
|
|
51
|
+
cols += [c for c in matched if c not in cols]
|
|
52
|
+
if len(cols) < 2:
|
|
53
|
+
raise ValueError(f"pairs needs at least two numeric columns, got {cols}")
|
|
54
|
+
return cols
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _groups(df: pd.DataFrame, groupcol: str | None, order: list[str] | None):
|
|
58
|
+
"""[(label, sub-DataFrame)], in --groupcolorder, category or first-seen order."""
|
|
59
|
+
if groupcol is None:
|
|
60
|
+
return [(None, df)]
|
|
61
|
+
col = df[groupcol]
|
|
62
|
+
if order:
|
|
63
|
+
present = col.dropna().unique()
|
|
64
|
+
values = [v for v in present if str(v) in order]
|
|
65
|
+
values.sort(key=lambda v: order.index(str(v)))
|
|
66
|
+
values += [v for v in present if str(v) not in order]
|
|
67
|
+
elif isinstance(col.dtype, pd.CategoricalDtype):
|
|
68
|
+
values = [v for v in col.cat.categories if (col == v).any()]
|
|
69
|
+
else:
|
|
70
|
+
values = list(col.dropna().unique())
|
|
71
|
+
return [(str(v), df[col == v]) for v in values]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _limits(df: pd.DataFrame, col: str) -> tuple[float, float] | None:
|
|
75
|
+
"""Axis range of *col*: its data range plus 5% (None if all missing)."""
|
|
76
|
+
values = df[col].dropna().to_numpy(float)
|
|
77
|
+
if values.size == 0:
|
|
78
|
+
return None
|
|
79
|
+
lo, hi = values.min(), values.max()
|
|
80
|
+
pad = 0.05 * (hi - lo or abs(lo) or 1.0) # a constant column too
|
|
81
|
+
return lo - pad, hi + pad
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _diag(ax, groups, col: str, kind: str, colors) -> None:
|
|
85
|
+
"""One column's distribution: a KDE (or histogram) per group.
|
|
86
|
+
|
|
87
|
+
With groups, each KDE is scaled by the group's share of the rows, so the
|
|
88
|
+
areas compare as the histogram's counts do.
|
|
89
|
+
"""
|
|
90
|
+
from scipy.stats import gaussian_kde
|
|
91
|
+
|
|
92
|
+
ax.set_yticks([])
|
|
93
|
+
data = [g[col].dropna().to_numpy(float) for _, g in groups]
|
|
94
|
+
values = np.concatenate(data)
|
|
95
|
+
if values.size == 0:
|
|
96
|
+
return
|
|
97
|
+
if kind == "hist":
|
|
98
|
+
bins = np.histogram_bin_edges(values, bins="auto")
|
|
99
|
+
for (label, _), v, color in zip(groups, data, colors, strict=False):
|
|
100
|
+
ax.hist(v, bins=bins, color=color, alpha=0.5, label=label)
|
|
101
|
+
else:
|
|
102
|
+
kdes = [
|
|
103
|
+
(label, v, gaussian_kde(v), color)
|
|
104
|
+
for (label, _), v, color in zip(groups, data, colors, strict=False)
|
|
105
|
+
if v.size > 1 and np.ptp(v) > 0
|
|
106
|
+
]
|
|
107
|
+
if kdes:
|
|
108
|
+
# Run the curves 3 bandwidths past the data, where they reach ~0
|
|
109
|
+
bw = max(float(np.sqrt(kde.covariance[0, 0])) for _, _, kde, _ in kdes)
|
|
110
|
+
grid = np.linspace(values.min() - 3 * bw, values.max() + 3 * bw, 200)
|
|
111
|
+
for label, v, kde, color in kdes:
|
|
112
|
+
density = kde(grid) * v.size / values.size
|
|
113
|
+
ax.fill_between(grid, density, color=color, alpha=0.25)
|
|
114
|
+
ax.plot(grid, density, color=color, label=label)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _scatter(ax, groups, xcol: str, ycol: str, args, colors) -> None:
|
|
118
|
+
for (label, g), color in zip(groups, colors, strict=False):
|
|
119
|
+
ax.scatter(
|
|
120
|
+
g[xcol],
|
|
121
|
+
g[ycol],
|
|
122
|
+
s=args.markersize,
|
|
123
|
+
alpha=args.alpha,
|
|
124
|
+
color=color,
|
|
125
|
+
label=label,
|
|
126
|
+
linewidths=0,
|
|
127
|
+
rasterized=True, # keeps a PDF small with many points
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _r(df: pd.DataFrame, xcol: str, ycol: str, method: str) -> float:
|
|
132
|
+
pair = df[[xcol, ycol]].dropna()
|
|
133
|
+
return pair[xcol].corr(pair[ycol], method=method) if len(pair) > 1 else np.nan
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _corr_text(ax, df, groups, xcol: str, ycol: str, method: str, cmap, colors):
|
|
137
|
+
"""The correlation of the pair; with groups, also r within each group.
|
|
138
|
+
|
|
139
|
+
Without groups the cell is coloured by r. With groups, r over all rows
|
|
140
|
+
can differ even in sign from r within every group (Simpson's paradox),
|
|
141
|
+
so each group's r is written below it, in the group's colour.
|
|
142
|
+
"""
|
|
143
|
+
r = _r(df, xcol, ycol, method)
|
|
144
|
+
if len(groups) == 1:
|
|
145
|
+
if np.isfinite(r):
|
|
146
|
+
ax.set_facecolor(cmap((r + 1) / 2))
|
|
147
|
+
ax.text(
|
|
148
|
+
0.5,
|
|
149
|
+
0.5,
|
|
150
|
+
f"{r:.2f}",
|
|
151
|
+
transform=ax.transAxes,
|
|
152
|
+
ha="center",
|
|
153
|
+
va="center",
|
|
154
|
+
fontsize="large",
|
|
155
|
+
color="white" if abs(r) > 0.6 else "black",
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
lines = [("all", r, "black")] + [
|
|
159
|
+
(label, _r(g, xcol, ycol, method), color)
|
|
160
|
+
for (label, g), color in zip(groups, colors, strict=False)
|
|
161
|
+
]
|
|
162
|
+
step = 1 / (len(lines) + 1)
|
|
163
|
+
for n, (label, value, color) in enumerate(lines, start=1):
|
|
164
|
+
text = f"{value:.2f}" if np.isfinite(value) else "-"
|
|
165
|
+
ax.text(
|
|
166
|
+
0.5,
|
|
167
|
+
1 - n * step,
|
|
168
|
+
f"{label}: {text}",
|
|
169
|
+
transform=ax.transAxes,
|
|
170
|
+
ha="center",
|
|
171
|
+
va="center",
|
|
172
|
+
fontsize="small" if len(lines) <= 4 else "x-small",
|
|
173
|
+
color=color,
|
|
174
|
+
fontweight="bold" if n == 1 else "normal",
|
|
175
|
+
)
|
|
176
|
+
ax.set_xticks([])
|
|
177
|
+
ax.set_yticks([])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _figsize(args: argparse.Namespace, k: int) -> tuple[float, float]:
|
|
181
|
+
if args.size != _DEFAULT_SIZE:
|
|
182
|
+
return parse_figsize(args.size)
|
|
183
|
+
side = min(_PANEL_INCHES * k, _MAX_INCHES)
|
|
184
|
+
return side, side
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
_EPILOG = """\
|
|
188
|
+
LAYOUT
|
|
189
|
+
------
|
|
190
|
+
Row i, column j shows column j (x) against column i (y); the diagonal shows
|
|
191
|
+
each column's distribution, a KDE by default. All numeric columns are used
|
|
192
|
+
unless -c names some (globs allowed, in the order given):
|
|
193
|
+
|
|
194
|
+
dftk pairs data.tsv -c x y z
|
|
195
|
+
dftk dataset iris -o | dftk pairs -f iris_pairs.png
|
|
196
|
+
dftk dataset penguins -o | dftk pairs -c 'bill*' 'flipper*' body_mass_g \\
|
|
197
|
+
-f penguin_pairs.png
|
|
198
|
+
|
|
199
|
+
GROUPS
|
|
200
|
+
------
|
|
201
|
+
-g colours points by group, with one KDE per group on the diagonal. Each
|
|
202
|
+
KDE is scaled by its group's share of the rows, so a small group has a
|
|
203
|
+
small curve, as its histogram (--diag hist) would:
|
|
204
|
+
|
|
205
|
+
dftk dataset iris -o | dftk pairs -g species -f iris_species.png
|
|
206
|
+
|
|
207
|
+
UPPER TRIANGLE AND DIAGONAL
|
|
208
|
+
---------------------------
|
|
209
|
+
--upper corr writes the correlation (--method pearson, spearman or
|
|
210
|
+
kendall) in the upper cells, coloured blue (negative) to red (positive),
|
|
211
|
+
instead of repeating the scatter plots. With -g it writes r over all rows
|
|
212
|
+
and r within each group: they can differ, even in sign (in iris, sepal
|
|
213
|
+
length and width give -0.12 overall and +0.5 to +0.75 within each species).
|
|
214
|
+
--diag hist draws histograms:
|
|
215
|
+
|
|
216
|
+
dftk dataset iris -o | dftk pairs -g species --upper corr --diag hist \\
|
|
217
|
+
-f iris_corr.png
|
|
218
|
+
|
|
219
|
+
LARGE TABLES
|
|
220
|
+
------------
|
|
221
|
+
Points are rasterized, so a PDF stays small. With many points, lower
|
|
222
|
+
--alpha and --markersize, or plot a sample:
|
|
223
|
+
|
|
224
|
+
dftk dataset diamonds -o | dftk sample -n 5000 --randomseed 1 -o \\
|
|
225
|
+
| dftk pairs -c carat depth price --alpha 0.2 --upper corr \\
|
|
226
|
+
--method spearman -f diamonds_pairs.png
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PairsCommand(BaseCommand):
|
|
231
|
+
name = "pairs"
|
|
232
|
+
help = "Scatter-plot matrix of column pairs, with KDEs on the diagonal."
|
|
233
|
+
|
|
234
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
235
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
236
|
+
parser.epilog = _EPILOG
|
|
237
|
+
|
|
238
|
+
self.add_read_arguments(parser)
|
|
239
|
+
|
|
240
|
+
g = parser.add_argument_group("pairs")
|
|
241
|
+
g.add_argument(
|
|
242
|
+
"-c",
|
|
243
|
+
"--cols",
|
|
244
|
+
nargs="+",
|
|
245
|
+
default=None,
|
|
246
|
+
metavar="COL",
|
|
247
|
+
help="Columns to plot, globs allowed (default: all numeric columns).",
|
|
248
|
+
)
|
|
249
|
+
g.add_argument(
|
|
250
|
+
"-g",
|
|
251
|
+
"--groupcol",
|
|
252
|
+
default=None,
|
|
253
|
+
metavar="COL",
|
|
254
|
+
help="Colour by this column; one KDE per group on the diagonal.",
|
|
255
|
+
)
|
|
256
|
+
g.add_argument(
|
|
257
|
+
"--groupcolorder",
|
|
258
|
+
nargs="+",
|
|
259
|
+
default=None,
|
|
260
|
+
metavar="VAL",
|
|
261
|
+
help="Group order for -g; values not listed come last.",
|
|
262
|
+
)
|
|
263
|
+
g.add_argument(
|
|
264
|
+
"--diag",
|
|
265
|
+
choices=["kde", "hist"],
|
|
266
|
+
default="kde",
|
|
267
|
+
help="Distribution on the diagonal (default: kde).",
|
|
268
|
+
)
|
|
269
|
+
g.add_argument(
|
|
270
|
+
"--upper",
|
|
271
|
+
choices=["scatter", "corr"],
|
|
272
|
+
default="scatter",
|
|
273
|
+
help="Upper triangle: mirrored scatter plots, or the correlation "
|
|
274
|
+
"(default: scatter).",
|
|
275
|
+
)
|
|
276
|
+
g.add_argument(
|
|
277
|
+
"--method",
|
|
278
|
+
choices=["pearson", "spearman", "kendall"],
|
|
279
|
+
default="pearson",
|
|
280
|
+
help="Correlation for --upper corr (default: pearson).",
|
|
281
|
+
)
|
|
282
|
+
g.add_argument(
|
|
283
|
+
"--alpha",
|
|
284
|
+
type=float,
|
|
285
|
+
default=0.6,
|
|
286
|
+
help="Point opacity, 0-1 (default: 0.6).",
|
|
287
|
+
)
|
|
288
|
+
g.add_argument(
|
|
289
|
+
"--markersize",
|
|
290
|
+
type=float,
|
|
291
|
+
default=8.0,
|
|
292
|
+
help="Point size in points^2 (default: 8).",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
add_figure_arguments(parser)
|
|
296
|
+
|
|
297
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
298
|
+
df = io.read(args)
|
|
299
|
+
if args.groupcol is not None:
|
|
300
|
+
check_cols(df, [args.groupcol], "-g/--groupcol")
|
|
301
|
+
cols = _columns(df, args.cols, exclude={args.groupcol})
|
|
302
|
+
groups = _groups(df, args.groupcol, args.groupcolorder)
|
|
303
|
+
colors = [WONG_PALETTE[i % len(WONG_PALETTE)] for i in range(len(groups))]
|
|
304
|
+
if args.groupcol is None:
|
|
305
|
+
colors = [WONG_PALETTE[4]] # blue
|
|
306
|
+
|
|
307
|
+
if args.file:
|
|
308
|
+
import matplotlib
|
|
309
|
+
|
|
310
|
+
matplotlib.use("Agg")
|
|
311
|
+
|
|
312
|
+
import matplotlib.pyplot as plt
|
|
313
|
+
|
|
314
|
+
if getattr(args, "usetex", False):
|
|
315
|
+
plt.rc("text", usetex=True)
|
|
316
|
+
|
|
317
|
+
apply_style(args)
|
|
318
|
+
k = len(cols)
|
|
319
|
+
fig, axes = plt.subplots(
|
|
320
|
+
k, k, figsize=_figsize(args, k), squeeze=False, layout="constrained"
|
|
321
|
+
)
|
|
322
|
+
cmap = plt.get_cmap("RdBu_r")
|
|
323
|
+
|
|
324
|
+
for i, ycol in enumerate(cols):
|
|
325
|
+
for j, xcol in enumerate(cols):
|
|
326
|
+
ax = axes[i][j]
|
|
327
|
+
if i == j:
|
|
328
|
+
_diag(ax, groups, xcol, args.diag, colors)
|
|
329
|
+
elif i < j and args.upper == "corr":
|
|
330
|
+
_corr_text(ax, df, groups, xcol, ycol, args.method, cmap, colors)
|
|
331
|
+
else:
|
|
332
|
+
_scatter(ax, groups, xcol, ycol, args, colors)
|
|
333
|
+
# Labels on the outer edge only; columns share x, rows share y
|
|
334
|
+
if i == k - 1:
|
|
335
|
+
ax.set_xlabel(xcol)
|
|
336
|
+
else:
|
|
337
|
+
ax.tick_params(labelbottom=False)
|
|
338
|
+
if j == 0:
|
|
339
|
+
ax.set_ylabel(ycol)
|
|
340
|
+
if j != 0 and i != j:
|
|
341
|
+
ax.tick_params(labelleft=False)
|
|
342
|
+
|
|
343
|
+
# The column's range down each column of panels, and along each row
|
|
344
|
+
limits = [_limits(df, col) for col in cols]
|
|
345
|
+
for i in range(k):
|
|
346
|
+
for j in range(k):
|
|
347
|
+
if i < j and args.upper == "corr":
|
|
348
|
+
continue
|
|
349
|
+
if limits[j] is not None:
|
|
350
|
+
axes[i][j].set_xlim(limits[j])
|
|
351
|
+
if i != j and limits[i] is not None:
|
|
352
|
+
axes[i][j].set_ylim(limits[i])
|
|
353
|
+
|
|
354
|
+
if args.groupcol is not None:
|
|
355
|
+
# From a scatter panel: every group is there, with or without a KDE
|
|
356
|
+
handles, labels = axes[1][0].get_legend_handles_labels()
|
|
357
|
+
fig.legend(
|
|
358
|
+
handles,
|
|
359
|
+
labels,
|
|
360
|
+
title=args.groupcol,
|
|
361
|
+
loc="outside right center",
|
|
362
|
+
)
|
|
363
|
+
if args.title:
|
|
364
|
+
fig.suptitle(args.title)
|
|
365
|
+
fig.align_ylabels(axes[:, 0])
|
|
366
|
+
save_or_show(fig, args)
|
|
@@ -135,13 +135,18 @@ Statistics (all accept `-g` for per-group results):
|
|
|
135
135
|
| Weighted versions | `wstat` |
|
|
136
136
|
| Linear, robust and weighted regression (R-style formula) | `fit -f "y ~ x + z"` |
|
|
137
137
|
| p-values for column pairs (t, Mann-Whitney, KS, …) | `test -c A:B -d p --test NAME` |
|
|
138
|
-
| Correlations with CIs, bootstrap, tail dependence | `corr -c A:B` |
|
|
138
|
+
| Correlations with CIs, bootstrap, tail dependence | `corr -c A:B` (globs: `-c '*:*'` every pair once) |
|
|
139
|
+
| Correlation matrix, every pair both ways plus the diagonal | `corr --all [COL…]` |
|
|
139
140
|
|
|
140
141
|
Plots (always with `-f FILE`): `scat` (scatter; `--fit` for an OLS line,
|
|
141
142
|
`--fit lowess` for a smooth curve, per group with `-g`; `--hexbin` for
|
|
142
143
|
tables too large for points), `line`, `hist`
|
|
143
|
-
(also KDE), `chi` (tail-dependence chi-plot)
|
|
144
|
-
|
|
144
|
+
(also KDE), `chi` (tail-dependence chi-plot), `heatmap` (coloured grid from a
|
|
145
|
+
long table `-x COL -y COL -v VALUE`, or a wide one with `-y ROWS`; a
|
|
146
|
+
correlogram is `corr --all -o | heatmap -x col1 -y col2 -v correlation
|
|
147
|
+
--annot --vmin -1 --vmax 1`), `pairs` (scatter-plot matrix with KDEs on the diagonal; `-c COL…`,
|
|
148
|
+
`-g COL`, `--upper corr`). `-g COL` overlays groups, `--subgraphcol COL`
|
|
149
|
+
makes panels.
|
|
145
150
|
|
|
146
151
|
Other: `dataset NAME` loads a bundled example table, `annotate FILE.parquet`
|
|
147
152
|
reads and writes provenance metadata, `clean` removes leftover temp pipe
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataframe_toolkit-0.15.1 → dataframe_toolkit-0.16.0}/src/dftk/skills/dftk/references/snakemake.md
RENAMED
|
File without changes
|