dataframe-toolkit 0.4.1__py3-none-any.whl
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.4.1.dist-info/METADATA +230 -0
- dataframe_toolkit-0.4.1.dist-info/RECORD +42 -0
- dataframe_toolkit-0.4.1.dist-info/WHEEL +4 -0
- dataframe_toolkit-0.4.1.dist-info/entry_points.txt +3 -0
- dftk/__init__.py +4 -0
- dftk/cli.py +72 -0
- dftk/commands/__init__.py +100 -0
- dftk/commands/annotate_cmd.py +165 -0
- dftk/commands/base.py +85 -0
- dftk/commands/binx_cmd.py +130 -0
- dftk/commands/clean_cmd.py +51 -0
- dftk/commands/concat_cmd.py +165 -0
- dftk/commands/corr_cmd.py +281 -0
- dftk/commands/dataset_cmd.py +181 -0
- dftk/commands/describe_cmd.py +360 -0
- dftk/commands/eval_cmd.py +657 -0
- dftk/commands/fit_cmd.py +265 -0
- dftk/commands/func_cmd.py +186 -0
- dftk/commands/help.py +63 -0
- dftk/commands/hist_cmd.py +321 -0
- dftk/commands/interp_cmd.py +271 -0
- dftk/commands/line_cmd.py +320 -0
- dftk/commands/melt_cmd.py +78 -0
- dftk/commands/merge_cmd.py +373 -0
- dftk/commands/pivot_cmd.py +438 -0
- dftk/commands/print_cmd.py +36 -0
- dftk/commands/query_cmd.py +230 -0
- dftk/commands/randvar_cmd.py +199 -0
- dftk/commands/sample_cmd.py +126 -0
- dftk/commands/scale_cmd.py +299 -0
- dftk/commands/scat_cmd.py +278 -0
- dftk/commands/split_cmd.py +130 -0
- dftk/commands/stat_cmd.py +289 -0
- dftk/commands/test_cmd.py +235 -0
- dftk/commands/wstat_cmd.py +163 -0
- dftk/common/__init__.py +8 -0
- dftk/common/io.py +690 -0
- dftk/common/plot.py +566 -0
- dftk/common/seed.py +25 -0
- dftk/common/seq.py +150 -0
- dftk/common/stats.py +214 -0
- dftk/io.py +10 -0
dftk/common/plot.py
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.common.plot — shared matplotlib infrastructure for dftk plot commands.
|
|
3
|
+
|
|
4
|
+
All plot commands (scat, line, hist) import from here for:
|
|
5
|
+
- Figure / axes creation with publication-quality defaults
|
|
6
|
+
- Font size presets (publication / screen / presentation)
|
|
7
|
+
- Figure size presets (single / double / full column, or explicit WxH)
|
|
8
|
+
- Colourblind-friendly default palette (Wong 2011)
|
|
9
|
+
- Shared argument groups for argparse
|
|
10
|
+
- Axis limit helpers
|
|
11
|
+
- Save-or-show helper
|
|
12
|
+
|
|
13
|
+
Font size hierarchy
|
|
14
|
+
-------------------
|
|
15
|
+
Each preset maps three semantic roles to point sizes:
|
|
16
|
+
|
|
17
|
+
small → tick label numbers on axes
|
|
18
|
+
medium → axis labels, legend text
|
|
19
|
+
large → figure title, legend title
|
|
20
|
+
|
|
21
|
+
Presets:
|
|
22
|
+
|
|
23
|
+
publication 6 / 8 / 9 pt (Nature / Science figure standards)
|
|
24
|
+
screen 9 / 11 / 13 pt (default — readable on screen)
|
|
25
|
+
presentation 12 / 14 / 16 pt (slides)
|
|
26
|
+
|
|
27
|
+
Figure size presets (width × auto-height at golden ratio)
|
|
28
|
+
----------------------------------------------------------
|
|
29
|
+
single 3.50" × 2.16" Nature single column (89 mm)
|
|
30
|
+
double 7.20" × 4.45" Nature double column (183 mm)
|
|
31
|
+
full 7.20" × 4.45" alias for double
|
|
32
|
+
WxH explicit inches, e.g. "5x3.5"
|
|
33
|
+
default: 5x3.5
|
|
34
|
+
|
|
35
|
+
Colour palette
|
|
36
|
+
--------------
|
|
37
|
+
Wong (2011) colourblind-safe 8-colour palette, also print-safe in greyscale.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import logging
|
|
44
|
+
import math
|
|
45
|
+
|
|
46
|
+
import matplotlib
|
|
47
|
+
import matplotlib.ticker as ticker
|
|
48
|
+
import numpy as np
|
|
49
|
+
import pandas as pd
|
|
50
|
+
|
|
51
|
+
# matplotlib.pyplot is imported lazily (inside functions) so that
|
|
52
|
+
# matplotlib.use() can be called in execute() before pyplot initialises
|
|
53
|
+
# the backend. Never add "import matplotlib.pyplot as plt" at module level.
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Colourblind-safe palette — Wong (2011) Nature Methods
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
WONG_PALETTE = [
|
|
62
|
+
"#E69F00", # orange
|
|
63
|
+
"#56B4E9", # sky blue
|
|
64
|
+
"#009E73", # bluish green
|
|
65
|
+
"#F0E442", # yellow
|
|
66
|
+
"#0072B2", # blue
|
|
67
|
+
"#D55E00", # vermilion
|
|
68
|
+
"#CC79A7", # reddish purple
|
|
69
|
+
"#000000", # black
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Font size presets
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
_FONT_PRESETS: dict[str, tuple[int, int, int]] = {
|
|
77
|
+
# small medium large
|
|
78
|
+
"publication": (6, 8, 9),
|
|
79
|
+
"screen": (9, 11, 13),
|
|
80
|
+
"presentation": (12, 14, 16),
|
|
81
|
+
}
|
|
82
|
+
_DEFAULT_FONTSIZE = "screen"
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Figure size presets (width_inches, height_inches)
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
_GOLDEN = (1 + math.sqrt(5)) / 2 # ≈ 1.618
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _auto_height(w: float) -> float:
|
|
92
|
+
return round(w / _GOLDEN, 2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
_SIZE_PRESETS: dict[str, tuple[float, float]] = {
|
|
96
|
+
"single": (3.50, _auto_height(3.50)),
|
|
97
|
+
"double": (7.20, _auto_height(7.20)),
|
|
98
|
+
"full": (7.20, _auto_height(7.20)),
|
|
99
|
+
}
|
|
100
|
+
_DEFAULT_SIZE = "10x6"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def parse_figsize(size_str: str) -> tuple[float, float]:
|
|
104
|
+
"""Parse --size argument into (width, height) inches."""
|
|
105
|
+
if size_str in _SIZE_PRESETS:
|
|
106
|
+
return _SIZE_PRESETS[size_str]
|
|
107
|
+
try:
|
|
108
|
+
w_str, h_str = size_str.lower().split("x")
|
|
109
|
+
return float(w_str), float(h_str)
|
|
110
|
+
except (ValueError, AttributeError):
|
|
111
|
+
logger.warning("Cannot parse --size %r; using default 5x3.5", size_str)
|
|
112
|
+
return 5.0, 3.5
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# rcParams application
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def apply_style(args: argparse.Namespace) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Apply font sizes, palette, and optional user stylesheets to rcParams.
|
|
123
|
+
Call once before creating the figure.
|
|
124
|
+
"""
|
|
125
|
+
import matplotlib.pyplot as plt
|
|
126
|
+
|
|
127
|
+
preset = getattr(args, "fontsize", _DEFAULT_FONTSIZE)
|
|
128
|
+
if preset not in _FONT_PRESETS:
|
|
129
|
+
logger.warning("Unknown --fontsize %r; using 'screen'", preset)
|
|
130
|
+
preset = "screen"
|
|
131
|
+
|
|
132
|
+
small, medium, large = _FONT_PRESETS[preset]
|
|
133
|
+
|
|
134
|
+
matplotlib.rcParams.update(
|
|
135
|
+
{
|
|
136
|
+
"font.family": "sans-serif",
|
|
137
|
+
"font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"],
|
|
138
|
+
"xtick.labelsize": small,
|
|
139
|
+
"ytick.labelsize": small,
|
|
140
|
+
"axes.labelsize": medium,
|
|
141
|
+
"legend.fontsize": medium,
|
|
142
|
+
"axes.titlesize": large,
|
|
143
|
+
"legend.title_fontsize": large,
|
|
144
|
+
"figure.dpi": 100,
|
|
145
|
+
"savefig.dpi": 300,
|
|
146
|
+
"axes.prop_cycle": matplotlib.cycler(color=WONG_PALETTE),
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Optional user stylesheets (applied after our base to allow overrides)
|
|
151
|
+
styles = getattr(args, "styles", None) or []
|
|
152
|
+
valid = [s for s in styles if s in plt.style.available]
|
|
153
|
+
if valid:
|
|
154
|
+
plt.style.use(valid)
|
|
155
|
+
# Re-enforce tick visibility which some styles suppress
|
|
156
|
+
matplotlib.rcParams["xtick.bottom"] = True
|
|
157
|
+
matplotlib.rcParams["ytick.left"] = True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Figure / axes creation
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
_MAX_SCREEN_W = 16.0 # inches — cap for interactive multi-panel figures
|
|
165
|
+
_MAX_SCREEN_H = 10.0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def make_figure(
|
|
169
|
+
args: argparse.Namespace,
|
|
170
|
+
nrows: int = 1,
|
|
171
|
+
ncols: int = 1,
|
|
172
|
+
) -> tuple[matplotlib.figure.Figure, np.ndarray]:
|
|
173
|
+
"""
|
|
174
|
+
Create a figure with nrows × ncols axes.
|
|
175
|
+
|
|
176
|
+
Returns (fig, axes) where axes is always a 2-D numpy array of shape
|
|
177
|
+
(nrows, ncols) for uniform indexing even when nrows=ncols=1.
|
|
178
|
+
|
|
179
|
+
For publication presets (single/double/full) the per-panel size is
|
|
180
|
+
respected exactly. For freeform sizes (WxH, including the default)
|
|
181
|
+
the total figure is capped at _MAX_SCREEN_W × _MAX_SCREEN_H so that
|
|
182
|
+
large subplot grids remain at a comfortable screen size.
|
|
183
|
+
"""
|
|
184
|
+
import matplotlib.pyplot as plt
|
|
185
|
+
|
|
186
|
+
size_str = getattr(args, "size", _DEFAULT_SIZE)
|
|
187
|
+
w, h = parse_figsize(size_str)
|
|
188
|
+
|
|
189
|
+
w_total = w * ncols
|
|
190
|
+
h_total = h * nrows
|
|
191
|
+
|
|
192
|
+
# Cap total size for freeform (non-preset) sizes so grids don't balloon.
|
|
193
|
+
if size_str not in _SIZE_PRESETS:
|
|
194
|
+
w_total = min(w_total, _MAX_SCREEN_W)
|
|
195
|
+
h_total = min(h_total, _MAX_SCREEN_H)
|
|
196
|
+
|
|
197
|
+
fig, axes = plt.subplots(
|
|
198
|
+
nrows,
|
|
199
|
+
ncols,
|
|
200
|
+
figsize=(w_total, h_total),
|
|
201
|
+
squeeze=False,
|
|
202
|
+
)
|
|
203
|
+
return fig, axes
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# Subplot grid for --subgraphcol
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def subgraph_layout(n_groups: int, ncols_hint: int | None = None) -> tuple[int, int]:
|
|
212
|
+
"""Return (nrows, ncols) for a grid of n_groups subplots."""
|
|
213
|
+
if ncols_hint is not None:
|
|
214
|
+
ncols = max(1, ncols_hint)
|
|
215
|
+
else:
|
|
216
|
+
ncols = min(n_groups, 3) # default: at most 3 columns
|
|
217
|
+
nrows = math.ceil(n_groups / ncols)
|
|
218
|
+
return nrows, ncols
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def subgraph_groups(
|
|
222
|
+
df: pd.DataFrame,
|
|
223
|
+
cols: list[str],
|
|
224
|
+
order: list[str] | None = None,
|
|
225
|
+
) -> list[tuple]:
|
|
226
|
+
"""Return list of (key, subdf) pairs from df.groupby(cols).
|
|
227
|
+
|
|
228
|
+
If *order* is given the panels are sorted to match it; values not listed
|
|
229
|
+
appear at the end in their original groupby order.
|
|
230
|
+
"""
|
|
231
|
+
groups = list(df.groupby(cols))
|
|
232
|
+
if order:
|
|
233
|
+
rank = {v: i for i, v in enumerate(order)}
|
|
234
|
+
n = len(order)
|
|
235
|
+
|
|
236
|
+
def _rank(g):
|
|
237
|
+
key = g[0]
|
|
238
|
+
# groupby with a list always returns tuple keys; unwrap single-col case
|
|
239
|
+
if isinstance(key, tuple) and len(key) == 1:
|
|
240
|
+
key = key[0]
|
|
241
|
+
return rank.get(str(key), n)
|
|
242
|
+
|
|
243
|
+
groups.sort(key=_rank)
|
|
244
|
+
return groups
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Axis helpers
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _to_float(s: str, fallback: float) -> float:
|
|
253
|
+
try:
|
|
254
|
+
return float(s)
|
|
255
|
+
except (TypeError, ValueError):
|
|
256
|
+
return fallback
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def apply_limits(ax: matplotlib.axes.Axes, args: argparse.Namespace) -> None:
|
|
260
|
+
"""Apply --xlim / --ylim / --logx / --logy / --xmargin / --ymargin."""
|
|
261
|
+
xlim = getattr(args, "xlim", None)
|
|
262
|
+
ylim = getattr(args, "ylim", None)
|
|
263
|
+
logx = getattr(args, "logx", False)
|
|
264
|
+
logy = getattr(args, "logy", False)
|
|
265
|
+
xmargin = getattr(args, "xmargin", False)
|
|
266
|
+
ymargin = getattr(args, "ymargin", False)
|
|
267
|
+
|
|
268
|
+
if logx:
|
|
269
|
+
ax.set_xscale("log")
|
|
270
|
+
if logy:
|
|
271
|
+
ax.set_yscale("log")
|
|
272
|
+
|
|
273
|
+
if xmargin:
|
|
274
|
+
xmin, xmax = ax.get_xlim()
|
|
275
|
+
r = xmax - xmin
|
|
276
|
+
ax.set_xlim(xmin - 0.01 * r, xmax + 0.01 * r)
|
|
277
|
+
if ymargin:
|
|
278
|
+
ymin, ymax = ax.get_ylim()
|
|
279
|
+
r = ymax - ymin
|
|
280
|
+
ax.set_ylim(ymin - 0.01 * r, ymax + 0.01 * r)
|
|
281
|
+
|
|
282
|
+
if xlim is not None:
|
|
283
|
+
xmin, xmax = ax.get_xlim()
|
|
284
|
+
ax.set_xlim(
|
|
285
|
+
_to_float(xlim[0], xmin),
|
|
286
|
+
_to_float(xlim[1], xmax),
|
|
287
|
+
)
|
|
288
|
+
if ylim is not None:
|
|
289
|
+
ymin, ymax = ax.get_ylim()
|
|
290
|
+
ax.set_ylim(
|
|
291
|
+
_to_float(ylim[0], ymin),
|
|
292
|
+
_to_float(ylim[1], ymax),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def apply_ticks(ax: matplotlib.axes.Axes, args: argparse.Namespace) -> None:
|
|
297
|
+
"""Apply --xticks / --yticks major/minor locators."""
|
|
298
|
+
xticks = getattr(args, "xticks", None)
|
|
299
|
+
yticks = getattr(args, "yticks", None)
|
|
300
|
+
if xticks is not None:
|
|
301
|
+
ax.xaxis.set_major_locator(ticker.MultipleLocator(xticks[0]))
|
|
302
|
+
ax.xaxis.set_minor_locator(ticker.MultipleLocator(xticks[1]))
|
|
303
|
+
if yticks is not None:
|
|
304
|
+
ax.yaxis.set_major_locator(ticker.MultipleLocator(yticks[0]))
|
|
305
|
+
ax.yaxis.set_minor_locator(ticker.MultipleLocator(yticks[1]))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def apply_labels(
|
|
309
|
+
ax: matplotlib.axes.Axes,
|
|
310
|
+
args: argparse.Namespace,
|
|
311
|
+
default_xlabel: str = "",
|
|
312
|
+
default_ylabel: str = "",
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Apply title, xlabel, ylabel, defaulting to column names."""
|
|
315
|
+
xlabel = getattr(args, "xlabel", None) or default_xlabel
|
|
316
|
+
ylabel = getattr(args, "ylabel", None) or default_ylabel
|
|
317
|
+
title = getattr(args, "title", "") or ""
|
|
318
|
+
ax.set_xlabel(xlabel)
|
|
319
|
+
ax.set_ylabel(ylabel)
|
|
320
|
+
ax.set_title(title)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# Group label
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def make_grouplabel(
|
|
329
|
+
groupname,
|
|
330
|
+
groupcols: list[str] | None = None,
|
|
331
|
+
fmt: str | None = None,
|
|
332
|
+
) -> str:
|
|
333
|
+
"""Format a groupby key as a legend label or subplot title.
|
|
334
|
+
|
|
335
|
+
Default: value(s) only, joined by " " for multi-column groups.
|
|
336
|
+
With *fmt*: an f-string template where column names are the placeholders,
|
|
337
|
+
e.g. ``"{treatment} ({dose} mg)"`` for groupcols=["treatment", "dose"].
|
|
338
|
+
"""
|
|
339
|
+
if isinstance(groupname, tuple):
|
|
340
|
+
vals = dict(zip(groupcols or [], groupname, strict=False)) if groupcols else {}
|
|
341
|
+
else:
|
|
342
|
+
vals = {groupcols[0]: groupname} if groupcols else {}
|
|
343
|
+
|
|
344
|
+
if fmt:
|
|
345
|
+
try:
|
|
346
|
+
return fmt.format(**vals)
|
|
347
|
+
except KeyError:
|
|
348
|
+
pass # fall through to default if template references unknown key
|
|
349
|
+
|
|
350
|
+
if isinstance(groupname, tuple):
|
|
351
|
+
return " ".join(str(v) for v in groupname)
|
|
352
|
+
return str(groupname)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
# Save / show
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def save_or_show(fig: matplotlib.figure.Figure, args: argparse.Namespace) -> None:
|
|
361
|
+
"""Save figure to --file (300 dpi) or display interactively."""
|
|
362
|
+
import matplotlib.pyplot as plt
|
|
363
|
+
|
|
364
|
+
outfile = getattr(args, "file", None)
|
|
365
|
+
if outfile:
|
|
366
|
+
fig.savefig(outfile, bbox_inches="tight", dpi=300)
|
|
367
|
+
logger.info("Saved figure to %s", outfile)
|
|
368
|
+
else:
|
|
369
|
+
backend = matplotlib.get_backend().lower()
|
|
370
|
+
if backend in ("agg", "cairo", "pdf", "ps", "svg", "pgf"):
|
|
371
|
+
import sys
|
|
372
|
+
|
|
373
|
+
print(
|
|
374
|
+
f"Warning: no interactive display (backend: {backend}). "
|
|
375
|
+
"Use -f/--file to save the figure to a file.",
|
|
376
|
+
file=sys.stderr,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
plt.show()
|
|
380
|
+
plt.close(fig)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# Shared argparse argument groups
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def add_xy_arguments(parser: argparse.ArgumentParser) -> None:
|
|
389
|
+
"""Add x/y column, label, limit, log-scale, margin, tick args."""
|
|
390
|
+
g = parser.add_argument_group("axes")
|
|
391
|
+
g.add_argument("-x", "--xcol", metavar="COL", help="X-axis column.")
|
|
392
|
+
g.add_argument("-y", "--ycol", metavar="COL", help="Y-axis column.")
|
|
393
|
+
g.add_argument(
|
|
394
|
+
"-xl",
|
|
395
|
+
"--xlabel",
|
|
396
|
+
metavar="TEXT",
|
|
397
|
+
default=None,
|
|
398
|
+
help="X-axis label (default: column name).",
|
|
399
|
+
)
|
|
400
|
+
g.add_argument(
|
|
401
|
+
"-yl",
|
|
402
|
+
"--ylabel",
|
|
403
|
+
metavar="TEXT",
|
|
404
|
+
default=None,
|
|
405
|
+
help="Y-axis label (default: column name).",
|
|
406
|
+
)
|
|
407
|
+
g.add_argument(
|
|
408
|
+
"-xm",
|
|
409
|
+
"--xlim",
|
|
410
|
+
nargs=2,
|
|
411
|
+
metavar=("LO", "HI"),
|
|
412
|
+
default=None,
|
|
413
|
+
help="X-axis limits.",
|
|
414
|
+
)
|
|
415
|
+
g.add_argument(
|
|
416
|
+
"-ym",
|
|
417
|
+
"--ylim",
|
|
418
|
+
nargs=2,
|
|
419
|
+
metavar=("LO", "HI"),
|
|
420
|
+
default=None,
|
|
421
|
+
help="Y-axis limits.",
|
|
422
|
+
)
|
|
423
|
+
g.add_argument("-lx", "--logx", action="store_true", help="Logarithmic x axis.")
|
|
424
|
+
g.add_argument("-ly", "--logy", action="store_true", help="Logarithmic y axis.")
|
|
425
|
+
g.add_argument(
|
|
426
|
+
"--xmargin", action="store_true", help="Add 1 %% margin to x limits."
|
|
427
|
+
)
|
|
428
|
+
g.add_argument(
|
|
429
|
+
"--ymargin", action="store_true", help="Add 1 %% margin to y limits."
|
|
430
|
+
)
|
|
431
|
+
g.add_argument(
|
|
432
|
+
"-xt",
|
|
433
|
+
"--xticks",
|
|
434
|
+
nargs=2,
|
|
435
|
+
type=float,
|
|
436
|
+
metavar=("MAJOR", "MINOR"),
|
|
437
|
+
default=None,
|
|
438
|
+
help="X-axis major and minor tick spacing.",
|
|
439
|
+
)
|
|
440
|
+
g.add_argument(
|
|
441
|
+
"-yt",
|
|
442
|
+
"--yticks",
|
|
443
|
+
nargs=2,
|
|
444
|
+
type=float,
|
|
445
|
+
metavar=("MAJOR", "MINOR"),
|
|
446
|
+
default=None,
|
|
447
|
+
help="Y-axis major and minor tick spacing.",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def add_figure_arguments(parser: argparse.ArgumentParser) -> None:
|
|
452
|
+
"""Add figure-level args: title, size, fontsize, styles, file."""
|
|
453
|
+
g = parser.add_argument_group("figure")
|
|
454
|
+
g.add_argument("-t", "--title", default="", metavar="TEXT", help="Figure title.")
|
|
455
|
+
g.add_argument(
|
|
456
|
+
"--size",
|
|
457
|
+
default=_DEFAULT_SIZE,
|
|
458
|
+
metavar="PRESET|WxH",
|
|
459
|
+
help='Figure size: single (3.5" col), double (7.2" col), or WxH '
|
|
460
|
+
f"in inches (default: {_DEFAULT_SIZE}).",
|
|
461
|
+
)
|
|
462
|
+
g.add_argument(
|
|
463
|
+
"--fontsize",
|
|
464
|
+
choices=list(_FONT_PRESETS),
|
|
465
|
+
default=_DEFAULT_FONTSIZE,
|
|
466
|
+
help="Font size preset (default: screen). "
|
|
467
|
+
"small=tick labels, medium=axis labels/legend, "
|
|
468
|
+
"large=title/legend title.",
|
|
469
|
+
)
|
|
470
|
+
g.add_argument(
|
|
471
|
+
"--styles",
|
|
472
|
+
nargs="*",
|
|
473
|
+
metavar="STYLE",
|
|
474
|
+
default=None,
|
|
475
|
+
help="Matplotlib stylesheets to apply (e.g. seaborn-v0_8).",
|
|
476
|
+
)
|
|
477
|
+
g.add_argument(
|
|
478
|
+
"-f",
|
|
479
|
+
"--file",
|
|
480
|
+
default=None,
|
|
481
|
+
metavar="PATH",
|
|
482
|
+
help="Save figure to file instead of displaying it. "
|
|
483
|
+
"Format inferred from extension (.png, .pdf, .svg, …).",
|
|
484
|
+
)
|
|
485
|
+
g.add_argument(
|
|
486
|
+
"--usetex",
|
|
487
|
+
action="store_true",
|
|
488
|
+
help="Render text with LaTeX (requires a TeX installation).",
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def add_group_arguments(parser: argparse.ArgumentParser) -> None:
|
|
493
|
+
"""Add --groupcol and --subgraphcol."""
|
|
494
|
+
g = parser.add_argument_group("grouping")
|
|
495
|
+
g.add_argument(
|
|
496
|
+
"-g",
|
|
497
|
+
"--groupcol",
|
|
498
|
+
nargs="+",
|
|
499
|
+
default=None,
|
|
500
|
+
metavar="COL",
|
|
501
|
+
help="Colour-code by this column (one series per value).",
|
|
502
|
+
)
|
|
503
|
+
g.add_argument(
|
|
504
|
+
"--groupcolorder",
|
|
505
|
+
nargs="+",
|
|
506
|
+
default=None,
|
|
507
|
+
metavar="VAL",
|
|
508
|
+
help="Series order for --groupcol (e.g. control treated …). "
|
|
509
|
+
"Values not listed appear last in their original order.",
|
|
510
|
+
)
|
|
511
|
+
g.add_argument(
|
|
512
|
+
"--subgraphcol",
|
|
513
|
+
nargs="+",
|
|
514
|
+
default=None,
|
|
515
|
+
metavar="COL",
|
|
516
|
+
help="Split into a subplot grid by this column.",
|
|
517
|
+
)
|
|
518
|
+
g.add_argument(
|
|
519
|
+
"--groupcolformat",
|
|
520
|
+
default=None,
|
|
521
|
+
metavar="TEMPLATE",
|
|
522
|
+
help="Legend label template for --groupcol. "
|
|
523
|
+
"Use {colname} placeholders, e.g. '{treatment} ({dose} mg)'.",
|
|
524
|
+
)
|
|
525
|
+
g.add_argument(
|
|
526
|
+
"--subgraphorder",
|
|
527
|
+
nargs="+",
|
|
528
|
+
default=None,
|
|
529
|
+
metavar="VAL",
|
|
530
|
+
help="Panel order for --subgraphcol (e.g. Jan Feb Mar …). "
|
|
531
|
+
"Values not listed appear last in their original order.",
|
|
532
|
+
)
|
|
533
|
+
g.add_argument(
|
|
534
|
+
"--subgraphformat",
|
|
535
|
+
default=None,
|
|
536
|
+
metavar="TEMPLATE",
|
|
537
|
+
help="Subplot title template for --subgraphcol. "
|
|
538
|
+
"Use {colname} placeholders, e.g. 'Month: {month}'.",
|
|
539
|
+
)
|
|
540
|
+
g.add_argument(
|
|
541
|
+
"--ncols",
|
|
542
|
+
type=int,
|
|
543
|
+
default=None,
|
|
544
|
+
metavar="N",
|
|
545
|
+
help="Number of columns in the subplot grid (default: min(groups, 3)).",
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def add_legend_arguments(parser: argparse.ArgumentParser) -> None:
|
|
550
|
+
"""Add --legend / --legendtitle / --legendloc."""
|
|
551
|
+
g = parser.add_argument_group("legend")
|
|
552
|
+
g.add_argument(
|
|
553
|
+
"--legend",
|
|
554
|
+
default=None,
|
|
555
|
+
metavar="TEXT",
|
|
556
|
+
help="Legend label (when not using --groupcol).",
|
|
557
|
+
)
|
|
558
|
+
g.add_argument("--legendtitle", default=None, metavar="TEXT", help="Legend title.")
|
|
559
|
+
g.add_argument(
|
|
560
|
+
"--legendloc",
|
|
561
|
+
nargs=2,
|
|
562
|
+
type=float,
|
|
563
|
+
metavar=("X", "Y"),
|
|
564
|
+
default=None,
|
|
565
|
+
help="Legend position as (x, y) in axes coordinates.",
|
|
566
|
+
)
|
dftk/common/seed.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.common.seed — random seed normalisation.
|
|
3
|
+
|
|
4
|
+
The dftk scripts accept seeds as either integers or arbitrary strings.
|
|
5
|
+
Strings are deterministically mapped to 32-bit unsigned integers via MD5
|
|
6
|
+
so that friendly names like ``"experiment-1"`` work as reproducible seeds.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_seed(seed: str | int | None) -> int | None:
|
|
13
|
+
"""Return a 32-bit integer seed suitable for ``numpy.random.seed()``.
|
|
14
|
+
|
|
15
|
+
* ``None`` → ``None`` (numpy draws from system entropy)
|
|
16
|
+
* integer or digit str → the integer value
|
|
17
|
+
* any other string → MD5-hashed to a 32-bit unsigned integer
|
|
18
|
+
"""
|
|
19
|
+
if seed is None:
|
|
20
|
+
return None
|
|
21
|
+
try:
|
|
22
|
+
return int(seed)
|
|
23
|
+
except (ValueError, TypeError):
|
|
24
|
+
encoded = str(seed).encode("utf-8")
|
|
25
|
+
return int(hashlib.md5(encoded).hexdigest(), 16) & ((1 << 32) - 1)
|