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.
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/PKG-INFO +18 -6
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/README.md +17 -5
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/pyproject.toml.orig +1 -1
- dataframe_toolkit-0.9.0/src/dftk/commands/scat_cmd.py +454 -0
- dataframe_toolkit-0.8.0/src/dftk/commands/scat_cmd.py +0 -281
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/__init__.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/base.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/cleannames_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/corr_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/eval_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/merge_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/print_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/query_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/io.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.8.0 → dataframe_toolkit-0.9.0}/src/dftk/common/stats.py +0 -0
- {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.
|
|
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;
|
|
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;
|
|
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
|
|
@@ -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)
|
|
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
|