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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.hist_cmd — dftk hist subcommand.
|
|
3
|
+
|
|
4
|
+
Histogram or kernel density estimate (KDE).
|
|
5
|
+
|
|
6
|
+
Port of dfhist.py with improvements:
|
|
7
|
+
- --groupcol overlaid histograms / KDEs per group (colours from Wong palette)
|
|
8
|
+
- --subgraphcol splits into a subplot grid
|
|
9
|
+
- --size / --fontsize publication-quality figure presets
|
|
10
|
+
- --bins / --binwidth bin control (mutual exclusion preserved)
|
|
11
|
+
- --kde kernel density estimate via seaborn.kdeplot
|
|
12
|
+
- --stats add mean ± σ to subplot title
|
|
13
|
+
- -y/--weightcol renamed from --ycol for clarity (weights column)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from dftk.commands.base import BaseCommand
|
|
21
|
+
from dftk.common.io import check_cols, io
|
|
22
|
+
from dftk.common.plot import (
|
|
23
|
+
add_figure_arguments,
|
|
24
|
+
add_group_arguments,
|
|
25
|
+
add_legend_arguments,
|
|
26
|
+
apply_labels,
|
|
27
|
+
apply_limits,
|
|
28
|
+
apply_style,
|
|
29
|
+
make_figure,
|
|
30
|
+
make_grouplabel,
|
|
31
|
+
save_or_show,
|
|
32
|
+
subgraph_groups,
|
|
33
|
+
subgraph_layout,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Single-axes histogram
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _hist_ax(ax, df, args, title_suffix: str = ""):
|
|
42
|
+
xcol = args.xcol
|
|
43
|
+
legendloc = tuple(args.legendloc) if args.legendloc else "best"
|
|
44
|
+
|
|
45
|
+
# Build bins
|
|
46
|
+
data_all = df[xcol].dropna()
|
|
47
|
+
bins = args.bins
|
|
48
|
+
if args.binwidth is not None and not args.kde:
|
|
49
|
+
lo = (data_all.min() // args.binwidth) * args.binwidth
|
|
50
|
+
hi = data_all.max() + args.binwidth
|
|
51
|
+
bins = np.arange(lo, hi, args.binwidth)
|
|
52
|
+
|
|
53
|
+
kde_bw = args.binwidth if args.kde and args.binwidth is not None else "scott"
|
|
54
|
+
|
|
55
|
+
hist_kw = dict(
|
|
56
|
+
alpha=0.4,
|
|
57
|
+
cumulative=args.cumulative,
|
|
58
|
+
density=args.normed,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if args.groupcol is not None:
|
|
62
|
+
for groupname, gdf in subgraph_groups(
|
|
63
|
+
df, args.groupcol, getattr(args, "groupcolorder", None)
|
|
64
|
+
):
|
|
65
|
+
label = make_grouplabel(
|
|
66
|
+
groupname, args.groupcol, getattr(args, "groupcolformat", None)
|
|
67
|
+
)
|
|
68
|
+
data = gdf[xcol].dropna()
|
|
69
|
+
weights = gdf[args.weightcol].values if args.weightcol else None
|
|
70
|
+
if args.kde:
|
|
71
|
+
import seaborn as sns
|
|
72
|
+
|
|
73
|
+
sns.kdeplot(
|
|
74
|
+
data,
|
|
75
|
+
bw_method=kde_bw,
|
|
76
|
+
label=label,
|
|
77
|
+
cumulative=args.cumulative,
|
|
78
|
+
cut=0,
|
|
79
|
+
ax=ax,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
ax.hist(data, bins=bins, weights=weights, label=label, **hist_kw)
|
|
83
|
+
ax.legend(loc=legendloc, title=args.legendtitle)
|
|
84
|
+
else:
|
|
85
|
+
data = data_all
|
|
86
|
+
weights = df[args.weightcol].values if args.weightcol else None
|
|
87
|
+
label = args.legend or None
|
|
88
|
+
|
|
89
|
+
if args.stats:
|
|
90
|
+
mu, sigma = data.mean(), data.std()
|
|
91
|
+
stats_str = f"μ={mu:.3g}, σ={sigma:.3g}"
|
|
92
|
+
existing_title = title_suffix or args.title or ""
|
|
93
|
+
title_suffix = (
|
|
94
|
+
f"{existing_title} ({stats_str})" if existing_title else stats_str
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if args.kde:
|
|
98
|
+
import seaborn as sns
|
|
99
|
+
|
|
100
|
+
sns.kdeplot(
|
|
101
|
+
data,
|
|
102
|
+
bw_method=kde_bw,
|
|
103
|
+
label=label,
|
|
104
|
+
cumulative=args.cumulative,
|
|
105
|
+
cut=0,
|
|
106
|
+
ax=ax,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
ax.hist(data, bins=bins, weights=weights, label=label, **hist_kw)
|
|
110
|
+
|
|
111
|
+
if label:
|
|
112
|
+
ax.legend(loc=legendloc, title=args.legendtitle)
|
|
113
|
+
|
|
114
|
+
apply_limits(ax, args)
|
|
115
|
+
apply_labels(
|
|
116
|
+
ax,
|
|
117
|
+
args,
|
|
118
|
+
default_xlabel=xcol or "",
|
|
119
|
+
default_ylabel="density" if args.normed else "count",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
title = args.title or ""
|
|
123
|
+
if title_suffix:
|
|
124
|
+
title = f"{title} [{title_suffix}]" if title else title_suffix
|
|
125
|
+
ax.set_title(title)
|
|
126
|
+
|
|
127
|
+
ax.grid(True, linewidth=0.4, alpha=0.5)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Command
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
_EPILOG = """\
|
|
135
|
+
EXAMPLES
|
|
136
|
+
--------
|
|
137
|
+
Basic histogram (10 bins):
|
|
138
|
+
|
|
139
|
+
dftk hist data.tsv -x value
|
|
140
|
+
|
|
141
|
+
Fixed bin width:
|
|
142
|
+
|
|
143
|
+
dftk hist data.tsv -x value --binwidth 0.5
|
|
144
|
+
|
|
145
|
+
Normalised density:
|
|
146
|
+
|
|
147
|
+
dftk hist data.tsv -x value --normed
|
|
148
|
+
|
|
149
|
+
Cumulative:
|
|
150
|
+
|
|
151
|
+
dftk hist data.tsv -x value --cumulative
|
|
152
|
+
|
|
153
|
+
KDE (kernel density estimate):
|
|
154
|
+
|
|
155
|
+
dftk hist data.tsv -x value --kde
|
|
156
|
+
|
|
157
|
+
Grouped (overlaid):
|
|
158
|
+
|
|
159
|
+
dftk hist data.tsv -x value -g group
|
|
160
|
+
|
|
161
|
+
Subplot grid by condition:
|
|
162
|
+
|
|
163
|
+
dftk hist data.tsv -x value --subgraphcol condition -g group
|
|
164
|
+
|
|
165
|
+
Add mean ± σ to title:
|
|
166
|
+
|
|
167
|
+
dftk hist data.tsv -x value --stats
|
|
168
|
+
|
|
169
|
+
Weighted histogram:
|
|
170
|
+
|
|
171
|
+
dftk hist data.tsv -x value -y weight_col
|
|
172
|
+
|
|
173
|
+
Publication figure (Nature single column, PDF):
|
|
174
|
+
|
|
175
|
+
dftk hist data.tsv -x value --size single --fontsize publication -f fig.pdf
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class HistCommand(BaseCommand):
|
|
180
|
+
name = "hist"
|
|
181
|
+
help = "Histogram or kernel density estimate (KDE)."
|
|
182
|
+
|
|
183
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
184
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
185
|
+
parser.epilog = _EPILOG
|
|
186
|
+
|
|
187
|
+
self.add_read_arguments(parser)
|
|
188
|
+
add_figure_arguments(parser)
|
|
189
|
+
add_group_arguments(parser)
|
|
190
|
+
add_legend_arguments(parser)
|
|
191
|
+
|
|
192
|
+
g = parser.add_argument_group("histogram options")
|
|
193
|
+
g.add_argument(
|
|
194
|
+
"-x", "--xcol", metavar="COL", required=True, help="Column to histogram."
|
|
195
|
+
)
|
|
196
|
+
g.add_argument(
|
|
197
|
+
"-y",
|
|
198
|
+
"--weightcol",
|
|
199
|
+
default=None,
|
|
200
|
+
metavar="COL",
|
|
201
|
+
help="Column to use as observation weights.",
|
|
202
|
+
)
|
|
203
|
+
g.add_argument(
|
|
204
|
+
"-xl",
|
|
205
|
+
"--xlabel",
|
|
206
|
+
default=None,
|
|
207
|
+
metavar="TEXT",
|
|
208
|
+
help="X-axis label (default: column name).",
|
|
209
|
+
)
|
|
210
|
+
g.add_argument(
|
|
211
|
+
"-yl",
|
|
212
|
+
"--ylabel",
|
|
213
|
+
default=None,
|
|
214
|
+
metavar="TEXT",
|
|
215
|
+
help="Y-axis label (default: count or density).",
|
|
216
|
+
)
|
|
217
|
+
g.add_argument(
|
|
218
|
+
"-xm",
|
|
219
|
+
"--xlim",
|
|
220
|
+
nargs=2,
|
|
221
|
+
metavar=("LO", "HI"),
|
|
222
|
+
default=None,
|
|
223
|
+
help="X-axis limits.",
|
|
224
|
+
)
|
|
225
|
+
g.add_argument(
|
|
226
|
+
"-ym",
|
|
227
|
+
"--ylim",
|
|
228
|
+
nargs=2,
|
|
229
|
+
metavar=("LO", "HI"),
|
|
230
|
+
default=None,
|
|
231
|
+
help="Y-axis limits.",
|
|
232
|
+
)
|
|
233
|
+
g.add_argument("-lx", "--logx", action="store_true", help="Logarithmic x axis.")
|
|
234
|
+
g.add_argument("-ly", "--logy", action="store_true", help="Logarithmic y axis.")
|
|
235
|
+
g.add_argument(
|
|
236
|
+
"--xmargin", action="store_true", help="Add 1 %% margin to x limits."
|
|
237
|
+
)
|
|
238
|
+
g.add_argument(
|
|
239
|
+
"--ymargin", action="store_true", help="Add 1 %% margin to y limits."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
g.add_argument(
|
|
243
|
+
"-n",
|
|
244
|
+
"--normed",
|
|
245
|
+
action="store_true",
|
|
246
|
+
help="Normalise so area sums to 1 (density).",
|
|
247
|
+
)
|
|
248
|
+
g.add_argument(
|
|
249
|
+
"-c", "--cumulative", action="store_true", help="Plot cumulative histogram."
|
|
250
|
+
)
|
|
251
|
+
g.add_argument(
|
|
252
|
+
"-k",
|
|
253
|
+
"--kde",
|
|
254
|
+
action="store_true",
|
|
255
|
+
help="Plot kernel density estimate instead of histogram "
|
|
256
|
+
"(uses seaborn.kdeplot).",
|
|
257
|
+
)
|
|
258
|
+
g.add_argument(
|
|
259
|
+
"--stats", action="store_true", help="Add mean ± σ to the subplot title."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
bins = parser.add_argument_group("bin options (mutually exclusive)")
|
|
263
|
+
mx = bins.add_mutually_exclusive_group()
|
|
264
|
+
mx.add_argument(
|
|
265
|
+
"-b",
|
|
266
|
+
"--bins",
|
|
267
|
+
type=int,
|
|
268
|
+
default=10,
|
|
269
|
+
metavar="N",
|
|
270
|
+
help="Number of bins (default: 10).",
|
|
271
|
+
)
|
|
272
|
+
mx.add_argument(
|
|
273
|
+
"-w",
|
|
274
|
+
"--binwidth",
|
|
275
|
+
type=float,
|
|
276
|
+
default=None,
|
|
277
|
+
metavar="W",
|
|
278
|
+
help="Bin width (overrides --bins; also sets KDE bandwidth).",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
282
|
+
df = io.read(args)
|
|
283
|
+
check_cols(df, [args.xcol], "-x/--xcol")
|
|
284
|
+
check_cols(df, args.groupcol, "-g/--groupcol")
|
|
285
|
+
check_cols(df, args.subgraphcol, "--subgraphcol")
|
|
286
|
+
if args.weightcol:
|
|
287
|
+
check_cols(df, [args.weightcol], "-y/--weightcol")
|
|
288
|
+
|
|
289
|
+
if args.file:
|
|
290
|
+
import matplotlib
|
|
291
|
+
|
|
292
|
+
matplotlib.use("Agg")
|
|
293
|
+
|
|
294
|
+
import matplotlib.pyplot as plt
|
|
295
|
+
|
|
296
|
+
if getattr(args, "usetex", False):
|
|
297
|
+
plt.rc("text", usetex=True)
|
|
298
|
+
|
|
299
|
+
apply_style(args)
|
|
300
|
+
|
|
301
|
+
if args.subgraphcol is not None:
|
|
302
|
+
groups = subgraph_groups(
|
|
303
|
+
df, args.subgraphcol, getattr(args, "subgraphorder", None)
|
|
304
|
+
)
|
|
305
|
+
nrows, ncols = subgraph_layout(len(groups), args.ncols)
|
|
306
|
+
fig, axes = make_figure(args, nrows=nrows, ncols=ncols)
|
|
307
|
+
for idx, (groupname, gdf) in enumerate(groups):
|
|
308
|
+
ax = axes[idx // ncols][idx % ncols]
|
|
309
|
+
suffix = make_grouplabel(
|
|
310
|
+
groupname, args.subgraphcol, getattr(args, "subgraphformat", None)
|
|
311
|
+
)
|
|
312
|
+
_hist_ax(ax, gdf, args, title_suffix=suffix)
|
|
313
|
+
for idx in range(len(groups), nrows * ncols):
|
|
314
|
+
axes[idx // ncols][idx % ncols].set_visible(False)
|
|
315
|
+
fig.tight_layout()
|
|
316
|
+
else:
|
|
317
|
+
fig, axes = make_figure(args)
|
|
318
|
+
_hist_ax(axes[0][0], df, args)
|
|
319
|
+
fig.tight_layout()
|
|
320
|
+
|
|
321
|
+
save_or_show(fig, args)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.interp_cmd — dftk interp subcommand.
|
|
3
|
+
|
|
4
|
+
Enriches a data table by interpolating values from a reference curve.
|
|
5
|
+
|
|
6
|
+
The reference file (--ref) defines a 1-D function: a sorted x-column and one
|
|
7
|
+
or more y-columns. For each row in DATAFILE the command looks up the x-value
|
|
8
|
+
in the reference and interpolates the corresponding y-value(s), appending them
|
|
9
|
+
as new column(s).
|
|
10
|
+
|
|
11
|
+
Grouping (-g) can be used when both files contain a grouping column (e.g. a
|
|
12
|
+
chromosome or sample ID): interpolation is then performed independently within
|
|
13
|
+
each matching group.
|
|
14
|
+
|
|
15
|
+
INTERPOLATION METHODS (--method)
|
|
16
|
+
---------------------------------
|
|
17
|
+
linear (default) Piecewise-linear interpolation.
|
|
18
|
+
nearest Nearest-neighbour (step function).
|
|
19
|
+
cubic Cubic spline (requires >= 4 reference points per group).
|
|
20
|
+
zero Zero-order hold (previous value).
|
|
21
|
+
slinear Linear in the B-spline sense.
|
|
22
|
+
quadratic / cubic Higher-order B-splines.
|
|
23
|
+
|
|
24
|
+
Out-of-range behaviour (--fill)
|
|
25
|
+
--------------------------------
|
|
26
|
+
nan (default) Values outside the reference x-range are NaN.
|
|
27
|
+
edge Clamp to the boundary values of the reference curve.
|
|
28
|
+
|
|
29
|
+
KNOWN LIMITATION
|
|
30
|
+
-----------------
|
|
31
|
+
When the reference curve has a flat segment (zero slope, e.g. a recombination-
|
|
32
|
+
cold spot in a genetic map), interpolation returns the left-boundary value for
|
|
33
|
+
all query points within that segment. Round-trip conversions that pass through
|
|
34
|
+
a flat segment require manual handling — this command does not attempt to
|
|
35
|
+
correct for it.
|
|
36
|
+
|
|
37
|
+
EXAMPLES
|
|
38
|
+
--------
|
|
39
|
+
Basic interpolation — add "concentration" to samples by interpolating a
|
|
40
|
+
standard curve:
|
|
41
|
+
|
|
42
|
+
dftk interp samples.tsv --ref stdcurve.tsv -x fluorescence -v concentration
|
|
43
|
+
|
|
44
|
+
Different x-column names in each file:
|
|
45
|
+
|
|
46
|
+
dftk interp samples.tsv --ref stdcurve.tsv \\
|
|
47
|
+
-x sample_fluor --refx std_fluor -v concentration
|
|
48
|
+
|
|
49
|
+
Interpolate multiple y-columns at once:
|
|
50
|
+
|
|
51
|
+
dftk interp data.tsv --ref ref.tsv -x pos -v y1 y2 -d col1 col2
|
|
52
|
+
|
|
53
|
+
Per-chromosome genetic-map lookup (grouped):
|
|
54
|
+
|
|
55
|
+
dftk interp intervals.tsv --ref genmap.tsv \\
|
|
56
|
+
-x bp_mid --refx pos_bp -v pos_cM -d cM_mid -g chrom
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
import argparse
|
|
60
|
+
|
|
61
|
+
import numpy as np
|
|
62
|
+
import pandas as pd
|
|
63
|
+
|
|
64
|
+
from dftk.commands.base import BaseCommand
|
|
65
|
+
from dftk.common.io import check_cols, io
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Core interpolation
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _interp_one(
|
|
73
|
+
ref: pd.DataFrame,
|
|
74
|
+
query: pd.DataFrame,
|
|
75
|
+
x_ref: str,
|
|
76
|
+
x_query: str,
|
|
77
|
+
val_cols: list[str],
|
|
78
|
+
dest_cols: list[str],
|
|
79
|
+
method: str,
|
|
80
|
+
fill: str,
|
|
81
|
+
) -> pd.DataFrame:
|
|
82
|
+
"""Interpolate *val_cols* from *ref* at *x_query* positions in *query*.
|
|
83
|
+
|
|
84
|
+
Returns a copy of *query* with *dest_cols* added.
|
|
85
|
+
"""
|
|
86
|
+
import scipy.interpolate
|
|
87
|
+
|
|
88
|
+
query = query.copy()
|
|
89
|
+
x_new = query[x_query].to_numpy(dtype=float)
|
|
90
|
+
|
|
91
|
+
for src, dest in zip(val_cols, dest_cols, strict=False):
|
|
92
|
+
x_ref_arr = ref[x_ref].to_numpy(dtype=float)
|
|
93
|
+
y_ref_arr = ref[src].to_numpy(dtype=float)
|
|
94
|
+
|
|
95
|
+
if fill == "edge":
|
|
96
|
+
fv = (y_ref_arr[0], y_ref_arr[-1])
|
|
97
|
+
else:
|
|
98
|
+
fv = np.nan
|
|
99
|
+
|
|
100
|
+
fx = scipy.interpolate.interp1d(
|
|
101
|
+
x_ref_arr,
|
|
102
|
+
y_ref_arr,
|
|
103
|
+
kind=method,
|
|
104
|
+
bounds_error=False,
|
|
105
|
+
fill_value=fv,
|
|
106
|
+
assume_sorted=True,
|
|
107
|
+
)
|
|
108
|
+
query[dest] = fx(x_new)
|
|
109
|
+
|
|
110
|
+
return query
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _interp(
|
|
114
|
+
ref: pd.DataFrame,
|
|
115
|
+
data: pd.DataFrame,
|
|
116
|
+
x_ref: str,
|
|
117
|
+
x_query: str,
|
|
118
|
+
val_cols: list[str],
|
|
119
|
+
dest_cols: list[str],
|
|
120
|
+
method: str,
|
|
121
|
+
fill: str,
|
|
122
|
+
groupcols: list[str] | None,
|
|
123
|
+
) -> pd.DataFrame:
|
|
124
|
+
"""Run interpolation, optionally within groups."""
|
|
125
|
+
|
|
126
|
+
if groupcols:
|
|
127
|
+
parts = []
|
|
128
|
+
# Both ref and data are grouped with the same groupcols, so keys match directly
|
|
129
|
+
ref_groups = {k: v for k, v in ref.groupby(groupcols)}
|
|
130
|
+
for grp_key, grp_data in data.groupby(groupcols, sort=False):
|
|
131
|
+
if grp_key not in ref_groups:
|
|
132
|
+
# Group present in data but not in ref — fill with NaN
|
|
133
|
+
grp_data = grp_data.copy()
|
|
134
|
+
for dest in dest_cols:
|
|
135
|
+
grp_data[dest] = np.nan
|
|
136
|
+
parts.append(grp_data)
|
|
137
|
+
continue
|
|
138
|
+
parts.append(
|
|
139
|
+
_interp_one(
|
|
140
|
+
ref_groups[grp_key],
|
|
141
|
+
grp_data,
|
|
142
|
+
x_ref,
|
|
143
|
+
x_query,
|
|
144
|
+
val_cols,
|
|
145
|
+
dest_cols,
|
|
146
|
+
method,
|
|
147
|
+
fill,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return pd.concat(parts, ignore_index=True)
|
|
151
|
+
|
|
152
|
+
return _interp_one(ref, data, x_ref, x_query, val_cols, dest_cols, method, fill)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Command
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class InterpCommand(BaseCommand):
|
|
161
|
+
name = "interp"
|
|
162
|
+
help = "Enrich a table by interpolating values from a reference curve."
|
|
163
|
+
|
|
164
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
165
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
166
|
+
parser.epilog = __doc__
|
|
167
|
+
|
|
168
|
+
self.add_io_arguments(parser)
|
|
169
|
+
|
|
170
|
+
g = parser.add_argument_group("interpolation options")
|
|
171
|
+
g.add_argument(
|
|
172
|
+
"--ref",
|
|
173
|
+
required=True,
|
|
174
|
+
metavar="FILE",
|
|
175
|
+
help="Reference file containing the curve to interpolate from.",
|
|
176
|
+
)
|
|
177
|
+
g.add_argument(
|
|
178
|
+
"-x",
|
|
179
|
+
"--xcol",
|
|
180
|
+
required=True,
|
|
181
|
+
metavar="COL",
|
|
182
|
+
help="X-axis column in DATAFILE (the query positions).",
|
|
183
|
+
)
|
|
184
|
+
g.add_argument(
|
|
185
|
+
"--refx",
|
|
186
|
+
default=None,
|
|
187
|
+
metavar="COL",
|
|
188
|
+
help="X-axis column in the reference file (default: same as -x/--xcol).",
|
|
189
|
+
)
|
|
190
|
+
g.add_argument(
|
|
191
|
+
"-v",
|
|
192
|
+
"--val",
|
|
193
|
+
required=True,
|
|
194
|
+
nargs="+",
|
|
195
|
+
metavar="COL",
|
|
196
|
+
help="Y-axis column(s) in the reference file to interpolate.",
|
|
197
|
+
)
|
|
198
|
+
g.add_argument(
|
|
199
|
+
"-d",
|
|
200
|
+
"--destcol",
|
|
201
|
+
default=None,
|
|
202
|
+
nargs="+",
|
|
203
|
+
metavar="NAME",
|
|
204
|
+
help="Output column name(s) in DATAFILE (default: same as -v/--val).",
|
|
205
|
+
)
|
|
206
|
+
g.add_argument(
|
|
207
|
+
"-g",
|
|
208
|
+
"--groupcol",
|
|
209
|
+
nargs="+",
|
|
210
|
+
default=None,
|
|
211
|
+
metavar="COL",
|
|
212
|
+
help=(
|
|
213
|
+
"Interpolate independently within each group. "
|
|
214
|
+
"Must be present in both DATAFILE and the reference file."
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
g.add_argument(
|
|
218
|
+
"--method",
|
|
219
|
+
default="linear",
|
|
220
|
+
metavar="METHOD",
|
|
221
|
+
choices=["linear", "nearest", "zero", "slinear", "quadratic", "cubic"],
|
|
222
|
+
help="Interpolation method (default: linear).",
|
|
223
|
+
)
|
|
224
|
+
g.add_argument(
|
|
225
|
+
"--fill",
|
|
226
|
+
default="nan",
|
|
227
|
+
choices=["nan", "edge"],
|
|
228
|
+
help="Out-of-range fill: nan (default) or edge (clamp to boundary).",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
232
|
+
# Read the data file (DATAFILE positional arg, handled by io.read)
|
|
233
|
+
data = io.read(args)
|
|
234
|
+
|
|
235
|
+
# Read the reference file using the same read settings
|
|
236
|
+
import copy
|
|
237
|
+
|
|
238
|
+
ref_args = copy.copy(args)
|
|
239
|
+
ref_args.DATAFILE = args.ref
|
|
240
|
+
ref = io.read(ref_args)
|
|
241
|
+
|
|
242
|
+
x_ref = args.refx if args.refx is not None else args.xcol
|
|
243
|
+
dest_cols = args.destcol if args.destcol is not None else list(args.val)
|
|
244
|
+
|
|
245
|
+
if len(dest_cols) != len(args.val):
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"--destcol has {len(dest_cols)} name(s) but "
|
|
248
|
+
f"--val has {len(args.val)} column(s). They must match."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Validate columns
|
|
252
|
+
check_cols(data, [args.xcol], "-x/--xcol")
|
|
253
|
+
check_cols(data, args.groupcol, "-g/--groupcol")
|
|
254
|
+
check_cols(ref, [x_ref], "--refx")
|
|
255
|
+
check_cols(ref, args.val, "-v/--val")
|
|
256
|
+
if args.groupcol:
|
|
257
|
+
check_cols(ref, args.groupcol, "-g/--groupcol (in ref file)")
|
|
258
|
+
|
|
259
|
+
result = _interp(
|
|
260
|
+
ref=ref,
|
|
261
|
+
data=data,
|
|
262
|
+
x_ref=x_ref,
|
|
263
|
+
x_query=args.xcol,
|
|
264
|
+
val_cols=list(args.val),
|
|
265
|
+
dest_cols=dest_cols,
|
|
266
|
+
method=args.method,
|
|
267
|
+
fill=args.fill,
|
|
268
|
+
groupcols=args.groupcol,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
io.printdf(result, args)
|