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.
Files changed (57) hide show
  1. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/PKG-INFO +32 -2
  2. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/README.md +31 -1
  3. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/pyproject.toml +1 -1
  4. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/pyproject.toml.orig +1 -1
  5. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/__init__.py +2 -0
  6. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/corr_cmd.py +169 -23
  7. dataframe_toolkit-0.16.0/src/dftk/commands/heatmap_cmd.py +253 -0
  8. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/merge_cmd.py +20 -0
  9. dataframe_toolkit-0.16.0/src/dftk/commands/pairs_cmd.py +366 -0
  10. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/docformats.py +24 -8
  11. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/skills/dftk/SKILL.md +8 -3
  12. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/__init__.py +0 -0
  13. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/cli.py +0 -0
  14. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/annotate_cmd.py +0 -0
  15. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/base.py +0 -0
  16. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/binx_cmd.py +0 -0
  17. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/chi_cmd.py +0 -0
  18. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/clean_cmd.py +0 -0
  19. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/cleannames_cmd.py +0 -0
  20. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/concat_cmd.py +0 -0
  21. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/dataset_cmd.py +0 -0
  22. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/describe_cmd.py +0 -0
  23. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/eval_cmd.py +0 -0
  24. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/explode_cmd.py +0 -0
  25. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/fit_cmd.py +0 -0
  26. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/func_cmd.py +0 -0
  27. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/help.py +0 -0
  28. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/hist_cmd.py +0 -0
  29. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/implode_cmd.py +0 -0
  30. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/info_cmd.py +0 -0
  31. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/interp_cmd.py +0 -0
  32. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/line_cmd.py +0 -0
  33. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/melt_cmd.py +0 -0
  34. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/pivot_cmd.py +0 -0
  35. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/print_cmd.py +0 -0
  36. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/query_cmd.py +0 -0
  37. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/randvar_cmd.py +0 -0
  38. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/sample_cmd.py +0 -0
  39. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/scale_cmd.py +0 -0
  40. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/scat_cmd.py +0 -0
  41. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/segid_cmd.py +0 -0
  42. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/skill_cmd.py +0 -0
  43. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/split_cmd.py +0 -0
  44. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/stat_cmd.py +0 -0
  45. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/test_cmd.py +0 -0
  46. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/transpose_cmd.py +0 -0
  47. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/commands/wstat_cmd.py +0 -0
  48. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/__init__.py +0 -0
  49. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/copula.py +0 -0
  50. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/evalfuncs.py +0 -0
  51. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/io.py +0 -0
  52. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/plot.py +0 -0
  53. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/seed.py +0 -0
  54. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/seq.py +0 -0
  55. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/common/stats.py +0 -0
  56. {dataframe_toolkit-0.15.0 → dataframe_toolkit-0.16.0}/src/dftk/io.py +0 -0
  57. {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.15.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.15.0"
3
+ version = "0.16.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.15.0"
3
+ version = "0.16.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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, args: argparse.Namespace, rng: np.random.Generator
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
- pair_df = sub[[col1, col2]].dropna()
170
- a, b = pair_df[col1].values, pair_df[col2].values
171
- cr = _compute_correlation(a, b, method_fn, ci)
172
- extra = ()
173
- if args.bootstrap is not None:
174
- extra += _bootstrap_stats(
175
- a, b, method_fn, args.bootstrap, rng, args.confidence / 100.0
176
- )
177
- if tail:
178
- extra += _tail_stats(a, b, q_grid)
179
- rows.append((*prefix, col1, col2, *cr, *extra))
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.add_argument(
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
- result = _corr(df, args, rng)
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