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.
@@ -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)