dataframe-toolkit 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/PKG-INFO +18 -6
  2. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/README.md +17 -5
  3. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/pyproject.toml +1 -1
  4. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/pyproject.toml.orig +1 -1
  5. dataframe_toolkit-0.9.0/src/dftk/commands/scat_cmd.py +454 -0
  6. dataframe_toolkit-0.8.0/src/dftk/commands/scat_cmd.py +0 -281
  7. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/__init__.py +0 -0
  8. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/cli.py +0 -0
  9. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/__init__.py +0 -0
  10. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/annotate_cmd.py +0 -0
  11. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/base.py +0 -0
  12. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/binx_cmd.py +0 -0
  13. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/chi_cmd.py +0 -0
  14. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/clean_cmd.py +0 -0
  15. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/cleannames_cmd.py +0 -0
  16. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/concat_cmd.py +0 -0
  17. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/corr_cmd.py +0 -0
  18. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/dataset_cmd.py +0 -0
  19. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/describe_cmd.py +0 -0
  20. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/eval_cmd.py +0 -0
  21. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/fit_cmd.py +0 -0
  22. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/func_cmd.py +0 -0
  23. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/help.py +0 -0
  24. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/hist_cmd.py +0 -0
  25. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/info_cmd.py +0 -0
  26. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/interp_cmd.py +0 -0
  27. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/line_cmd.py +0 -0
  28. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/melt_cmd.py +0 -0
  29. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/merge_cmd.py +0 -0
  30. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/pivot_cmd.py +0 -0
  31. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/print_cmd.py +0 -0
  32. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/query_cmd.py +0 -0
  33. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/randvar_cmd.py +0 -0
  34. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/sample_cmd.py +0 -0
  35. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/scale_cmd.py +0 -0
  36. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/segid_cmd.py +0 -0
  37. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/split_cmd.py +0 -0
  38. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/stat_cmd.py +0 -0
  39. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/test_cmd.py +0 -0
  40. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/transpose_cmd.py +0 -0
  41. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/wstat_cmd.py +0 -0
  42. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/__init__.py +0 -0
  43. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/copula.py +0 -0
  44. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/io.py +0 -0
  45. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/plot.py +0 -0
  46. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/seed.py +0 -0
  47. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/seq.py +0 -0
  48. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/stats.py +0 -0
  49. {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dataframe-toolkit
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -145,11 +145,12 @@ interactively otherwise. They support `--groupcol` for colour grouping,
145
145
  `--subgraphcol` for subplot grids, and figure/font presets for
146
146
  publication-quality output.
147
147
 
148
- | Command | Description |
149
- |---------|-------------------------------------------------------------------------------------------------|
150
- | `scat` | Scatter plot; optional OLS/robust fit overlay, bubble size (`--sizecol`), colour (`--colorcol`) |
151
- | `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
152
- | `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
148
+ | Command | Description |
149
+ |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
150
+ | `scat` | Scatter plot; OLS/robust or LOWESS fit (`--fit lowess`, one per group with `-g`), hexagonal binning for large tables (`--hexbin`), bubble size (`--sizecol`), colour (`--colorcol`) |
151
+ | `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
152
+ | `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
153
+ | `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
153
154
 
154
155
  ### Utilities
155
156
 
@@ -186,6 +187,17 @@ dftk dataset fertility -o \
186
187
 
187
188
  For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
188
189
 
190
+ ### Large tables: hexbin instead of points
191
+
192
+ With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
193
+
194
+ ```bash
195
+ dftk dataset diamonds -o \
196
+ | dftk scat ... -x carat -y price --hexbin --hexlog --logx --logy -f diamonds.png
197
+ ```
198
+
199
+ `--colorcol COL` colours each hexagon by the mean of `COL` instead of the count; with `--subgraphcol` all panels share one colour scale.
200
+
189
201
  ### Group summary then plot
190
202
 
191
203
  ```bash
@@ -116,11 +116,12 @@ interactively otherwise. They support `--groupcol` for colour grouping,
116
116
  `--subgraphcol` for subplot grids, and figure/font presets for
117
117
  publication-quality output.
118
118
 
119
- | Command | Description |
120
- |---------|-------------------------------------------------------------------------------------------------|
121
- | `scat` | Scatter plot; optional OLS/robust fit overlay, bubble size (`--sizecol`), colour (`--colorcol`) |
122
- | `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
123
- | `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
119
+ | Command | Description |
120
+ |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
121
+ | `scat` | Scatter plot; OLS/robust or LOWESS fit (`--fit lowess`, one per group with `-g`), hexagonal binning for large tables (`--hexbin`), bubble size (`--sizecol`), colour (`--colorcol`) |
122
+ | `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
123
+ | `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
124
+ | `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
124
125
 
125
126
  ### Utilities
126
127
 
@@ -157,6 +158,17 @@ dftk dataset fertility -o \
157
158
 
158
159
  For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
159
160
 
161
+ ### Large tables: hexbin instead of points
162
+
163
+ With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
164
+
165
+ ```bash
166
+ dftk dataset diamonds -o \
167
+ | dftk scat ... -x carat -y price --hexbin --hexlog --logx --logy -f diamonds.png
168
+ ```
169
+
170
+ `--colorcol COL` colours each hexagon by the mean of `COL` instead of the count; with `--subgraphcol` all panels share one colour scale.
171
+
160
172
  ### Group summary then plot
161
173
 
162
174
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.8.0"
3
+ version = "0.9.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.8.0"
3
+ version = "0.9.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,454 @@
1
+ """
2
+ dftk.commands.scat_cmd — dftk scat subcommand.
3
+
4
+ Scatter plot with optional OLS / robust regression line overlay.
5
+
6
+ Port of dfscat.py with improvements:
7
+ - --groupcol colour-codes points by a column
8
+ - --subgraphcol splits into a subplot grid
9
+ - --size / --fontsize publication-quality figure presets
10
+ - --fit overlays an OLS (or robust) regression line via fit_cmd.regress_it,
11
+ or a LOWESS curve (--fit lowess); with --groupcol, one per group
12
+ - --hexbin hexagonal binning for large tables (port of dfhexb.py)
13
+ - --sizecol point size from a data column (was --area)
14
+ - --colorcol continuous colour mapping from a data column (was --color)
15
+ """
16
+
17
+ import argparse
18
+ import sys
19
+
20
+ import matplotlib.lines as mlines
21
+ import numpy as np
22
+
23
+ from dftk.commands.base import BaseCommand
24
+ from dftk.common.io import check_cols, io
25
+ from dftk.common.plot import (
26
+ add_figure_arguments,
27
+ add_group_arguments,
28
+ add_legend_arguments,
29
+ add_xy_arguments,
30
+ apply_labels,
31
+ apply_limits,
32
+ apply_style,
33
+ apply_ticks,
34
+ make_figure,
35
+ make_grouplabel,
36
+ save_or_show,
37
+ subgraph_groups,
38
+ subgraph_layout,
39
+ )
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Fit overlay
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ def _ols_curve(x, y, args, label: str):
47
+ """Fit OLS (or RLM) of y on x; return (x_plot, y_plot, legend text)."""
48
+ import pandas as pd
49
+
50
+ from dftk.commands.fit_cmd import regress_it
51
+
52
+ fit_args = argparse.Namespace(
53
+ formula="_y ~ _x" if not args.nointercept else "_y ~ _x - 1",
54
+ weights=None,
55
+ robust=getattr(args, "robust", False),
56
+ )
57
+ res = regress_it(pd.DataFrame({"_x": x, "_y": y}), fit_args)
58
+ if label:
59
+ print(f"== {label}", file=sys.stderr)
60
+ print(res.summary(), file=sys.stderr)
61
+
62
+ slope = res.params["_x"]
63
+ intercept = 0.0 if args.nointercept else res.params["Intercept"]
64
+ if args.nointercept:
65
+ text = f"{slope:+.3g} × {args.xcol}"
66
+ else:
67
+ text = f"{intercept:.3g}{slope:+.3g} × {args.xcol}"
68
+ if getattr(args, "pvalue", False):
69
+ pval = res.pvalues.get("_x", res.pvalues.iloc[-1])
70
+ text += f" -log10(p)={-np.log10(pval):.2g}"
71
+ return slope, intercept, text
72
+
73
+
74
+ def _lowess_curve(x, y, args):
75
+ """Return the LOWESS curve of y on x as (x_sorted, y_smoothed)."""
76
+ from statsmodels.nonparametric.smoothers_lowess import lowess
77
+
78
+ # delta: fit only every 1% of the x range and interpolate in between.
79
+ # Without it LOWESS is quadratic in n (10,000 points take seconds,
80
+ # 200,000 do not finish); with it 1,000,000 points take a few seconds.
81
+ delta = 0.01 * (x.max() - x.min())
82
+ curve = lowess(y, x, frac=args.frac, delta=delta)
83
+ return curve[:, 0], curve[:, 1]
84
+
85
+
86
+ def _fit_overlay(ax, x_series, y_series, args, color=None, label: str = ""):
87
+ """Overlay the --fit line or curve on ax.
88
+
89
+ Without *color* (no --groupcol) the fit gets its own red legend entry,
90
+ as before. With *color* it is drawn in the group's colour, over that
91
+ group's x range, and the group legend already identifies it.
92
+ """
93
+ import pandas as pd
94
+
95
+ data = pd.DataFrame({"x": x_series.values, "y": y_series.values}).dropna()
96
+ data = data.astype(float)
97
+ if len(data) < 3:
98
+ print(
99
+ f"--fit: skipped {label or 'data'} (fewer than 3 points)", file=sys.stderr
100
+ )
101
+ return
102
+ x, y = data["x"].to_numpy(), data["y"].to_numpy()
103
+
104
+ if args.fit == "lowess":
105
+ x_plot, y_plot = _lowess_curve(x, y, args)
106
+ text = f"LOWESS (frac={args.frac:.2g})"
107
+ else:
108
+ slope, intercept, text = _ols_curve(x, y, args, label)
109
+ xmin, xmax = (x.min(), x.max()) if color is not None else ax.get_xlim()
110
+ x_plot = np.linspace(xmin, xmax, 200)
111
+ y_plot = intercept + slope * x_plot
112
+
113
+ if color is None:
114
+ ax.plot(x_plot, y_plot, "r--", linewidth=1)
115
+ ax.legend(
116
+ handles=[mlines.Line2D([], [], color="red", linestyle="--", label=text)],
117
+ loc=0,
118
+ )
119
+ else:
120
+ ax.plot(x_plot, y_plot, "--", color=color, linewidth=1.2)
121
+
122
+
123
+ def _hexbin_ax(ax, df, args):
124
+ """Draw a hexagonal-bin density plot (or mean of --colorcol per hexagon)."""
125
+ import matplotlib.pyplot as plt
126
+
127
+ cols = [args.xcol, args.ycol] + ([args.colorcol] if args.colorcol else [])
128
+ data = df[cols].dropna()
129
+ for flag, col in (("logx", args.xcol), ("logy", args.ycol)):
130
+ if getattr(args, flag, False):
131
+ positive = data[col] > 0
132
+ if not positive.all():
133
+ print(
134
+ f"--hexbin --{flag}: dropped {(~positive).sum()} rows with "
135
+ f"{col} <= 0",
136
+ file=sys.stderr,
137
+ )
138
+ data = data[positive]
139
+
140
+ kw = dict(
141
+ gridsize=args.gridsize,
142
+ mincnt=1,
143
+ cmap="viridis",
144
+ # bin on the log scale, so hexagons are regular on log axes
145
+ xscale="log" if getattr(args, "logx", False) else "linear",
146
+ yscale="log" if getattr(args, "logy", False) else "linear",
147
+ )
148
+ if args.hexlog:
149
+ kw["bins"] = "log"
150
+ if args.colorcol:
151
+ kw["C"] = data[args.colorcol]
152
+ kw["reduce_C_function"] = np.mean
153
+ label = f"mean {args.colorcol}"
154
+ else:
155
+ label = "count"
156
+ hb = ax.hexbin(data[args.xcol], data[args.ycol], **kw)
157
+ plt.colorbar(hb, ax=ax, label=label)
158
+
159
+
160
+ def _share_hexbin_colours(axes) -> None:
161
+ """Give every --subgraphcol panel the same colour scale, so they compare."""
162
+ from matplotlib.collections import PolyCollection
163
+
164
+ hexbins = [
165
+ c
166
+ for row in axes
167
+ for ax in row
168
+ for c in ax.collections
169
+ if isinstance(c, PolyCollection) and c.get_array() is not None
170
+ ]
171
+ values = [hb.get_array() for hb in hexbins if hb.get_array().size]
172
+ if not values:
173
+ return
174
+ vmin = min(float(v.min()) for v in values)
175
+ vmax = max(float(v.max()) for v in values)
176
+ for hb in hexbins:
177
+ hb.set_clim(vmin, vmax)
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Single-axes scatter (reused per subplot)
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ def _scatter_ax(ax, df, args, title_suffix: str = ""):
186
+ """Draw scatter on a single Axes, with optional --groupcol colouring."""
187
+ import matplotlib.pyplot as plt
188
+
189
+ xcol = args.xcol
190
+ ycol = args.ycol
191
+ legendloc = tuple(args.legendloc) if args.legendloc else "best"
192
+
193
+ scatter_kw = dict(marker=getattr(args, "marker", "o"), alpha=0.7)
194
+
195
+ if getattr(args, "hexbin", False):
196
+ _hexbin_ax(ax, df, args)
197
+ if getattr(args, "fit", None):
198
+ _fit_overlay(ax, df[xcol], df[ycol], args)
199
+ elif args.groupcol is not None:
200
+ for groupname, gdf in subgraph_groups(
201
+ df, args.groupcol, getattr(args, "groupcolorder", None)
202
+ ):
203
+ label = make_grouplabel(
204
+ groupname, args.groupcol, getattr(args, "groupcolformat", None)
205
+ )
206
+ kw = dict(scatter_kw)
207
+ if args.sizecol:
208
+ kw["s"] = gdf[args.sizecol]
209
+ points = ax.scatter(gdf[xcol], gdf[ycol], label=label, **kw)
210
+ if getattr(args, "fit", None):
211
+ color = points.get_facecolor()[0]
212
+ _fit_overlay(ax, gdf[xcol], gdf[ycol], args, color=color, label=label)
213
+ ax.legend(loc=legendloc, title=args.legendtitle)
214
+ else:
215
+ kw = dict(scatter_kw)
216
+ if args.sizecol:
217
+ kw["s"] = df[args.sizecol]
218
+ if args.colorcol:
219
+ sc = ax.scatter(
220
+ df[xcol], df[ycol], c=df[args.colorcol], cmap="viridis", **kw
221
+ )
222
+ plt.colorbar(sc, ax=ax, label=args.colorcol)
223
+ else:
224
+ label = args.legend or ""
225
+ ax.scatter(df[xcol], df[ycol], label=label if label else None, **kw)
226
+ if label:
227
+ ax.legend(loc=legendloc, title=args.legendtitle)
228
+
229
+ if getattr(args, "fit", None):
230
+ _fit_overlay(ax, df[xcol], df[ycol], args)
231
+
232
+ apply_limits(ax, args)
233
+ apply_ticks(ax, args)
234
+ apply_labels(ax, args, default_xlabel=xcol or "", default_ylabel=ycol or "")
235
+ if title_suffix:
236
+ existing = ax.get_title()
237
+ ax.set_title(f"{existing} [{title_suffix}]" if existing else title_suffix)
238
+
239
+ ax.grid(True, linewidth=0.4, alpha=0.5)
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Command
244
+ # ---------------------------------------------------------------------------
245
+
246
+ _EPILOG = """\
247
+ EXAMPLES
248
+ --------
249
+ Basic scatter:
250
+
251
+ dftk scat data.tsv -x weight -y height
252
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip
253
+
254
+ Colour by group:
255
+
256
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip -g day
257
+
258
+ Subplot grid by time-of-day, colour by smoker:
259
+
260
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip \\
261
+ --subgraphcol time -g smoker
262
+
263
+ Fit OLS line, or one per group in the group's colour:
264
+
265
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip --fit
266
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip -g smoker --fit
267
+
268
+ LOWESS curve (local regression; smaller --frac follows the data more
269
+ closely). At the ends of the x range, where few points are left, the curve
270
+ can overshoot the data; a larger --frac or --xlim helps:
271
+
272
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip --fit lowess
273
+
274
+ Hexagonal binning for large tables (54,000 diamonds), log colour scale and
275
+ log axes; hexagons are binned on the log scale:
276
+
277
+ dftk dataset diamonds -o | dftk scat ... -x carat -y price \
278
+ --hexbin --hexlog --logx --logy
279
+
280
+ Colour each hexagon by the mean of another column instead of the count:
281
+
282
+ dftk dataset diamonds -o | dftk scat ... -x carat -y price \
283
+ --hexbin --colorcol depth --gridsize 50
284
+
285
+ One hexbin panel per group, all on the same colour scale:
286
+
287
+ dftk dataset diamonds -o | dftk scat ... -x carat -y price \
288
+ --hexbin --hexlog --subgraphcol cut --gridsize 40
289
+
290
+ Publication figure (Nature single column, PDF):
291
+
292
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip \\
293
+ --size single --fontsize publication -f fig.pdf
294
+
295
+ Bubble chart (point size from column):
296
+
297
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip --sizecol size
298
+
299
+ Continuous colour mapping:
300
+
301
+ dftk dataset tips -o | dftk scat ... -x total_bill -y tip --colorcol tip
302
+ """
303
+
304
+
305
+ class ScatCommand(BaseCommand):
306
+ name = "scat"
307
+ help = "Scatter plot with optional regression line overlay."
308
+
309
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
310
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
311
+ parser.epilog = _EPILOG
312
+
313
+ self.add_read_arguments(parser)
314
+ add_xy_arguments(parser)
315
+ add_figure_arguments(parser)
316
+ add_group_arguments(parser)
317
+ add_legend_arguments(parser)
318
+
319
+ s = parser.add_argument_group("scatter options")
320
+ s.add_argument(
321
+ "-m",
322
+ "--marker",
323
+ default="o",
324
+ metavar="MARKER",
325
+ help="Matplotlib marker style (default: o).",
326
+ )
327
+ s.add_argument(
328
+ "--sizecol",
329
+ default=None,
330
+ metavar="COL",
331
+ help="Column to use for point sizes (bubble chart).",
332
+ )
333
+ s.add_argument(
334
+ "--colorcol",
335
+ default=None,
336
+ metavar="COL",
337
+ help="Column for continuous colour mapping.",
338
+ )
339
+
340
+ h = parser.add_argument_group("hexbin options")
341
+ h.add_argument(
342
+ "--hexbin",
343
+ action="store_true",
344
+ help="Hexagonal binning instead of points, for large tables: "
345
+ "colour shows the number of points per hexagon, or with "
346
+ "--colorcol the mean of that column. Not with -g or --sizecol.",
347
+ )
348
+ h.add_argument(
349
+ "--gridsize",
350
+ type=int,
351
+ default=100,
352
+ metavar="N",
353
+ help="Number of hexagons across the x axis (default: 100).",
354
+ )
355
+ h.add_argument(
356
+ "--hexlog",
357
+ action="store_true",
358
+ help="Log colour scale, so sparse areas stay visible.",
359
+ )
360
+
361
+ f = parser.add_argument_group("fit options")
362
+ f.add_argument(
363
+ "--fit",
364
+ nargs="?",
365
+ const="ols",
366
+ default=None,
367
+ choices=["ols", "lowess"],
368
+ help="Overlay a fit: ols (the default for a bare --fit; a "
369
+ "regression line, summary to stderr) or lowess (a smooth curve). "
370
+ "With -g, one per group in the group's colour.",
371
+ )
372
+ f.add_argument(
373
+ "--frac",
374
+ type=float,
375
+ default=2 / 3,
376
+ metavar="F",
377
+ help="Share of the data used for each local fit in --fit lowess "
378
+ "(default: 2/3). Smaller values follow the data more closely.",
379
+ )
380
+ f.add_argument(
381
+ "-r",
382
+ "--robust",
383
+ action="store_true",
384
+ help="Use robust regression (RLM) for --fit ols.",
385
+ )
386
+ f.add_argument(
387
+ "--nointercept", action="store_true", help="Suppress intercept in --fit."
388
+ )
389
+ f.add_argument(
390
+ "--pvalue",
391
+ action="store_true",
392
+ help="Add −log₁₀(p) to the fit legend label.",
393
+ )
394
+
395
+ def execute(self, args: argparse.Namespace) -> None:
396
+ if not args.xcol or not args.ycol:
397
+ raise ValueError("--xcol (-x) and --ycol (-y) are required.")
398
+
399
+ df = io.read(args)
400
+ check_cols(df, [args.xcol, args.ycol], "-x/-y")
401
+ check_cols(df, args.groupcol, "-g/--groupcol")
402
+ check_cols(df, args.subgraphcol, "--subgraphcol")
403
+ if args.sizecol:
404
+ check_cols(df, [args.sizecol], "--sizecol")
405
+ if args.colorcol:
406
+ check_cols(df, [args.colorcol], "--colorcol")
407
+ if args.hexbin:
408
+ if args.groupcol:
409
+ raise ValueError(
410
+ "--hexbin cannot be combined with -g/--groupcol: overlaid "
411
+ "densities cannot be told apart. Use --subgraphcol instead."
412
+ )
413
+ if args.sizecol:
414
+ raise ValueError("--hexbin cannot be combined with --sizecol.")
415
+ if args.fit == "lowess" and not 0 < args.frac <= 1:
416
+ raise ValueError(f"--frac must be in (0, 1], got {args.frac}")
417
+
418
+ # Use non-interactive backend when saving to file
419
+ if args.file:
420
+ import matplotlib
421
+
422
+ matplotlib.use("Agg")
423
+
424
+ import matplotlib.pyplot as plt
425
+
426
+ if args.usetex:
427
+ plt.rc("text", usetex=True)
428
+
429
+ apply_style(args)
430
+
431
+ if args.subgraphcol is not None:
432
+ groups = subgraph_groups(
433
+ df, args.subgraphcol, getattr(args, "subgraphorder", None)
434
+ )
435
+ nrows, ncols = subgraph_layout(len(groups), args.ncols)
436
+ fig, axes = make_figure(args, nrows=nrows, ncols=ncols)
437
+ for idx, (groupname, gdf) in enumerate(groups):
438
+ ax = axes[idx // ncols][idx % ncols]
439
+ suffix = make_grouplabel(
440
+ groupname, args.subgraphcol, getattr(args, "subgraphformat", None)
441
+ )
442
+ _scatter_ax(ax, gdf, args, title_suffix=suffix)
443
+ # Hide unused axes
444
+ for idx in range(len(groups), nrows * ncols):
445
+ axes[idx // ncols][idx % ncols].set_visible(False)
446
+ if args.hexbin:
447
+ _share_hexbin_colours(axes)
448
+ fig.tight_layout()
449
+ else:
450
+ fig, axes = make_figure(args)
451
+ _scatter_ax(axes[0][0], df, args)
452
+ fig.tight_layout()
453
+
454
+ save_or_show(fig, args)
@@ -1,281 +0,0 @@
1
- """
2
- dftk.commands.scat_cmd — dftk scat subcommand.
3
-
4
- Scatter plot with optional OLS / robust regression line overlay.
5
-
6
- Port of dfscat.py with improvements:
7
- - --groupcol colour-codes points by a column
8
- - --subgraphcol splits into a subplot grid
9
- - --size / --fontsize publication-quality figure presets
10
- - --fit overlays an OLS (or robust) regression line via fit_cmd.regress_it
11
- - --sizecol point size from a data column (was --area)
12
- - --colorcol continuous colour mapping from a data column (was --color)
13
- """
14
-
15
- import argparse
16
- import sys
17
-
18
- import matplotlib.lines as mlines
19
- import numpy as np
20
-
21
- from dftk.commands.base import BaseCommand
22
- from dftk.common.io import check_cols, io
23
- from dftk.common.plot import (
24
- add_figure_arguments,
25
- add_group_arguments,
26
- add_legend_arguments,
27
- add_xy_arguments,
28
- apply_labels,
29
- apply_limits,
30
- apply_style,
31
- apply_ticks,
32
- make_figure,
33
- make_grouplabel,
34
- save_or_show,
35
- subgraph_groups,
36
- subgraph_layout,
37
- )
38
-
39
- # ---------------------------------------------------------------------------
40
- # Fit overlay
41
- # ---------------------------------------------------------------------------
42
-
43
-
44
- def _fit_overlay(ax, x_series, y_series, args):
45
- """Fit a regression line and overlay it on ax. Prints summary to stdout."""
46
- import pandas as pd
47
-
48
- from dftk.commands.fit_cmd import regress_it
49
-
50
- df_fit = pd.DataFrame({"_x": x_series.values, "_y": y_series.values}).dropna()
51
- fit_args = argparse.Namespace(
52
- formula="_y ~ _x" if not args.nointercept else "_y ~ _x - 1",
53
- weights=None,
54
- robust=getattr(args, "robust", False),
55
- )
56
- res = regress_it(df_fit, fit_args)
57
- print(res.summary(), file=sys.stderr)
58
-
59
- xmin, xmax = ax.get_xlim()
60
- X_plot = np.linspace(xmin, xmax, 200)
61
-
62
- if args.nointercept:
63
- slope = res.params["_x"]
64
- Y_plot = slope * X_plot
65
- label_text = f"{slope:+.3g} × {args.xcol}"
66
- else:
67
- intercept = res.params["Intercept"]
68
- slope = res.params["_x"]
69
- Y_plot = intercept + slope * X_plot
70
- label_text = f"{intercept:.3g}{slope:+.3g} × {args.xcol}"
71
-
72
- if getattr(args, "pvalue", False):
73
- pval = res.pvalues.get("_x", res.pvalues.iloc[-1])
74
- label_text += f" -log10(p)={-np.log10(pval):.2g}"
75
-
76
- ax.plot(X_plot, Y_plot, "r--", linewidth=1)
77
- ax.legend(
78
- handles=[mlines.Line2D([], [], color="red", linestyle="--", label=label_text)],
79
- loc=0,
80
- )
81
-
82
-
83
- # ---------------------------------------------------------------------------
84
- # Single-axes scatter (reused per subplot)
85
- # ---------------------------------------------------------------------------
86
-
87
-
88
- def _scatter_ax(ax, df, args, title_suffix: str = ""):
89
- """Draw scatter on a single Axes, with optional --groupcol colouring."""
90
- import matplotlib.pyplot as plt
91
-
92
- xcol = args.xcol
93
- ycol = args.ycol
94
- legendloc = tuple(args.legendloc) if args.legendloc else "best"
95
-
96
- scatter_kw = dict(marker=getattr(args, "marker", "o"), alpha=0.7)
97
-
98
- if args.groupcol is not None:
99
- for groupname, gdf in subgraph_groups(
100
- df, args.groupcol, getattr(args, "groupcolorder", None)
101
- ):
102
- label = make_grouplabel(
103
- groupname, args.groupcol, getattr(args, "groupcolformat", None)
104
- )
105
- kw = dict(scatter_kw)
106
- if args.sizecol:
107
- kw["s"] = gdf[args.sizecol]
108
- ax.scatter(gdf[xcol], gdf[ycol], label=label, **kw)
109
- ax.legend(loc=legendloc, title=args.legendtitle)
110
- else:
111
- kw = dict(scatter_kw)
112
- if args.sizecol:
113
- kw["s"] = df[args.sizecol]
114
- if args.colorcol:
115
- sc = ax.scatter(
116
- df[xcol], df[ycol], c=df[args.colorcol], cmap="viridis", **kw
117
- )
118
- plt.colorbar(sc, ax=ax, label=args.colorcol)
119
- else:
120
- label = args.legend or ""
121
- ax.scatter(df[xcol], df[ycol], label=label if label else None, **kw)
122
- if label:
123
- ax.legend(loc=legendloc, title=args.legendtitle)
124
-
125
- if getattr(args, "fit", False) and args.groupcol is None:
126
- _fit_overlay(ax, df[xcol], df[ycol], args)
127
-
128
- apply_limits(ax, args)
129
- apply_ticks(ax, args)
130
- apply_labels(ax, args, default_xlabel=xcol or "", default_ylabel=ycol or "")
131
- if title_suffix:
132
- existing = ax.get_title()
133
- ax.set_title(f"{existing} [{title_suffix}]" if existing else title_suffix)
134
-
135
- ax.grid(True, linewidth=0.4, alpha=0.5)
136
-
137
-
138
- # ---------------------------------------------------------------------------
139
- # Command
140
- # ---------------------------------------------------------------------------
141
-
142
- _EPILOG = """\
143
- EXAMPLES
144
- --------
145
- Basic scatter:
146
-
147
- dftk scat data.tsv -x weight -y height
148
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip
149
-
150
- Colour by group:
151
-
152
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip -g day
153
-
154
- Subplot grid by time-of-day, colour by smoker:
155
-
156
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip \\
157
- --subgraphcol time -g smoker
158
-
159
- Fit OLS line:
160
-
161
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip --fit
162
-
163
- Publication figure (Nature single column, PDF):
164
-
165
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip \\
166
- --size single --fontsize publication -f fig.pdf
167
-
168
- Bubble chart (point size from column):
169
-
170
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip --sizecol size
171
-
172
- Continuous colour mapping:
173
-
174
- dftk dataset tips -o | dftk scat ... -x total_bill -y tip --colorcol tip
175
- """
176
-
177
-
178
- class ScatCommand(BaseCommand):
179
- name = "scat"
180
- help = "Scatter plot with optional regression line overlay."
181
-
182
- def add_arguments(self, parser: argparse.ArgumentParser) -> None:
183
- parser.formatter_class = argparse.RawDescriptionHelpFormatter
184
- parser.epilog = _EPILOG
185
-
186
- self.add_read_arguments(parser)
187
- add_xy_arguments(parser)
188
- add_figure_arguments(parser)
189
- add_group_arguments(parser)
190
- add_legend_arguments(parser)
191
-
192
- s = parser.add_argument_group("scatter options")
193
- s.add_argument(
194
- "-m",
195
- "--marker",
196
- default="o",
197
- metavar="MARKER",
198
- help="Matplotlib marker style (default: o).",
199
- )
200
- s.add_argument(
201
- "--sizecol",
202
- default=None,
203
- metavar="COL",
204
- help="Column to use for point sizes (bubble chart).",
205
- )
206
- s.add_argument(
207
- "--colorcol",
208
- default=None,
209
- metavar="COL",
210
- help="Column for continuous colour mapping.",
211
- )
212
-
213
- f = parser.add_argument_group("fit options")
214
- f.add_argument(
215
- "--fit",
216
- action="store_true",
217
- help="Overlay an OLS regression line. Prints summary to stderr.",
218
- )
219
- f.add_argument(
220
- "-r",
221
- "--robust",
222
- action="store_true",
223
- help="Use robust regression (RLM) for --fit.",
224
- )
225
- f.add_argument(
226
- "--nointercept", action="store_true", help="Suppress intercept in --fit."
227
- )
228
- f.add_argument(
229
- "--pvalue",
230
- action="store_true",
231
- help="Add −log₁₀(p) to the fit legend label.",
232
- )
233
-
234
- def execute(self, args: argparse.Namespace) -> None:
235
- if not args.xcol or not args.ycol:
236
- raise ValueError("--xcol (-x) and --ycol (-y) are required.")
237
-
238
- df = io.read(args)
239
- check_cols(df, [args.xcol, args.ycol], "-x/-y")
240
- check_cols(df, args.groupcol, "-g/--groupcol")
241
- check_cols(df, args.subgraphcol, "--subgraphcol")
242
- if args.sizecol:
243
- check_cols(df, [args.sizecol], "--sizecol")
244
- if args.colorcol:
245
- check_cols(df, [args.colorcol], "--colorcol")
246
-
247
- # Use non-interactive backend when saving to file
248
- if args.file:
249
- import matplotlib
250
-
251
- matplotlib.use("Agg")
252
-
253
- import matplotlib.pyplot as plt
254
-
255
- if args.usetex:
256
- plt.rc("text", usetex=True)
257
-
258
- apply_style(args)
259
-
260
- if args.subgraphcol is not None:
261
- groups = subgraph_groups(
262
- df, args.subgraphcol, getattr(args, "subgraphorder", None)
263
- )
264
- nrows, ncols = subgraph_layout(len(groups), args.ncols)
265
- fig, axes = make_figure(args, nrows=nrows, ncols=ncols)
266
- for idx, (groupname, gdf) in enumerate(groups):
267
- ax = axes[idx // ncols][idx % ncols]
268
- suffix = make_grouplabel(
269
- groupname, args.subgraphcol, getattr(args, "subgraphformat", None)
270
- )
271
- _scatter_ax(ax, gdf, args, title_suffix=suffix)
272
- # Hide unused axes
273
- for idx in range(len(groups), nrows * ncols):
274
- axes[idx // ncols][idx % ncols].set_visible(False)
275
- fig.tight_layout()
276
- else:
277
- fig, axes = make_figure(args)
278
- _scatter_ax(axes[0][0], df, args)
279
- fig.tight_layout()
280
-
281
- save_or_show(fig, args)