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,281 @@
1
+ """
2
+ dftk.commands.corr_cmd — dftk corr subcommand.
3
+
4
+ Port of dfcorr.py: pairwise column correlations with optional grouping,
5
+ analytical confidence intervals (Pearson), and BCa bootstrap inference.
6
+
7
+ Bootstrap mode (--bootstrap N) produces a single summary row per pair with:
8
+ p_perm — permutation p-value (fraction of |r_null| >= |r_obs|)
9
+ ci_boot_lo — BCa 95% CI lower bound (all methods)
10
+ ci_boot_hi — BCa 95% CI upper bound (all methods)
11
+ """
12
+
13
+ import argparse
14
+ import logging
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ import scipy.stats as ss
19
+
20
+ from dftk.commands.base import BaseCommand
21
+ from dftk.common.io import check_cols, io
22
+ from dftk.common.seed import normalize_seed
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ METHODS = ["pearson", "spearman", "kendall"]
27
+ _METHOD_FUNC = {
28
+ "pearson": ss.pearsonr,
29
+ "spearman": ss.spearmanr,
30
+ "kendall": ss.kendalltau,
31
+ }
32
+
33
+
34
+ def _compute_correlation(a: np.ndarray, b: np.ndarray, method_fn, ci: bool) -> tuple:
35
+ result = method_fn(a, b)
36
+ row = (len(a), result.statistic, result.pvalue)
37
+ if ci:
38
+ interval = result.confidence_interval()
39
+ row = (*row, interval.low, interval.high)
40
+ return row
41
+
42
+
43
+ # BCa jackknife acceleration is estimated from leave-one-out resamples.
44
+ # scipy.stats.bootstrap materialises an (n × n-1) array for this step,
45
+ # which OOMs at large n. We stream one leave-one-out at a time and cap
46
+ # at _MAX_JACK observations so peak memory is O(min(n, _MAX_JACK)).
47
+ _MAX_JACK = 500
48
+
49
+
50
+ def _bootstrap_stats(
51
+ a: np.ndarray,
52
+ b: np.ndarray,
53
+ method_fn,
54
+ n_boot: int,
55
+ rng: np.random.Generator,
56
+ confidence: float = 0.95,
57
+ ) -> tuple[float, float, float]:
58
+ """Return (p_perm, ci_boot_lo, ci_boot_hi) via permutation test and BCa CI."""
59
+ n = len(a)
60
+ r_obs = method_fn(a, b).statistic
61
+
62
+ r_obs_abs = abs(r_obs)
63
+ n_extreme = 0
64
+ for _ in range(n_boot):
65
+ if abs(method_fn(a, rng.permutation(b)).statistic) >= r_obs_abs:
66
+ n_extreme += 1
67
+ p_perm = n_extreme / n_boot
68
+
69
+ theta_boot = np.empty(n_boot)
70
+ for i in range(n_boot):
71
+ idx = rng.integers(0, n, size=n)
72
+ theta_boot[i] = method_fn(a[idx], b[idx]).statistic
73
+
74
+ if n <= _MAX_JACK:
75
+ jack_indices = np.arange(n)
76
+ else:
77
+ jack_indices = rng.choice(n, size=_MAX_JACK, replace=False)
78
+
79
+ theta_jack = np.empty(len(jack_indices))
80
+ mask = np.ones(n, dtype=bool)
81
+ for j, i in enumerate(jack_indices):
82
+ mask[i] = False
83
+ theta_jack[j] = method_fn(a[mask], b[mask]).statistic
84
+ mask[i] = True
85
+
86
+ jmean = theta_jack.mean()
87
+ diff = jmean - theta_jack
88
+ denom = np.sum(diff**2)
89
+ a_hat = np.sum(diff**3) / (6.0 * denom**1.5) if denom > 1e-20 else 0.0
90
+
91
+ prop = np.clip((theta_boot < r_obs).mean(), 1e-10, 1 - 1e-10)
92
+ z0 = ss.norm.ppf(prop)
93
+
94
+ alpha = (1.0 - confidence) / 2.0
95
+ z_lo, z_hi = ss.norm.ppf(alpha), ss.norm.ppf(1.0 - alpha)
96
+
97
+ def _adj(z: float) -> float:
98
+ denom = 1.0 - a_hat * (z0 + z)
99
+ if abs(denom) < 1e-10:
100
+ return float("nan")
101
+ return ss.norm.cdf(z0 + (z0 + z) / denom)
102
+
103
+ a1, a2 = _adj(z_lo), _adj(z_hi)
104
+
105
+ if np.isnan(a1) or np.isnan(a2):
106
+ ci_lo = float(np.percentile(theta_boot, 100.0 * alpha))
107
+ ci_hi = float(np.percentile(theta_boot, 100.0 * (1.0 - alpha)))
108
+ else:
109
+ ci_lo = float(np.percentile(theta_boot, 100.0 * a1))
110
+ ci_hi = float(np.percentile(theta_boot, 100.0 * a2))
111
+
112
+ return p_perm, ci_lo, ci_hi
113
+
114
+
115
+ def _corr(
116
+ df: pd.DataFrame, args: argparse.Namespace, rng: np.random.Generator
117
+ ) -> pd.DataFrame:
118
+ method_fn = _METHOD_FUNC[args.method]
119
+ pairs = [c.split(":", maxsplit=1) for c in args.cols]
120
+ bad = [c for c, p in zip(args.cols, pairs, strict=False) if len(p) != 2]
121
+ if bad:
122
+ raise ValueError(f"-c pairs must be col1:col2, got: {bad}")
123
+
124
+ ci = args.ci and args.method == "pearson"
125
+
126
+ header = ["col1", "col2", "nobs", "correlation", "pvalue"]
127
+ if ci:
128
+ header += ["cilo", "cihi"]
129
+ if args.bootstrap is not None:
130
+ header += ["p_perm", "ci_boot_lo", "ci_boot_hi"]
131
+
132
+ rows = []
133
+
134
+ def _add_pairs(sub: pd.DataFrame, prefix: tuple = ()):
135
+ for col1, col2 in pairs:
136
+ pair_df = sub[[col1, col2]].dropna()
137
+ a, b = pair_df[col1].values, pair_df[col2].values
138
+ cr = _compute_correlation(a, b, method_fn, ci)
139
+ if args.bootstrap is None:
140
+ rows.append((*prefix, col1, col2, *cr))
141
+ else:
142
+ boot = _bootstrap_stats(
143
+ a, b, method_fn, args.bootstrap, rng, args.confidence / 100.0
144
+ )
145
+ rows.append((*prefix, col1, col2, *cr, *boot))
146
+
147
+ if args.groups:
148
+ header = [*args.groups, *header]
149
+ for group, gdf in df.groupby(args.groups):
150
+ prefix = group if isinstance(group, tuple) else (group,)
151
+ _add_pairs(gdf, prefix)
152
+ else:
153
+ _add_pairs(df)
154
+
155
+ return pd.DataFrame(rows, columns=header)
156
+
157
+
158
+ _EPILOG = """\
159
+ COLUMN PAIRS (-c)
160
+ -----------------
161
+ Pairs are specified as col1:col2. Multiple pairs may be given:
162
+
163
+ dftk corr data.tsv -c x:y
164
+ dftk corr data.tsv -c x:y a:b
165
+
166
+ METHODS (--method)
167
+ ------------------
168
+ pearson Pearson r (default)
169
+ spearman Spearman rank correlation
170
+ kendall Kendall tau
171
+
172
+ CONFIDENCE INTERVAL (--ci)
173
+ --------------------------
174
+ Adds cilo and cihi columns using the analytical Fisher z-transform formula.
175
+ Only available for Pearson; silently ignored for Spearman/Kendall.
176
+ Use --bootstrap to get non-parametric CIs for any method.
177
+
178
+ BOOTSTRAP (--bootstrap N)
179
+ --------------------------
180
+ Runs N bootstrap resamples and N permutation draws, adding three columns:
181
+
182
+ p_perm Permutation p-value: fraction of |r_null| >= |r_obs| when b
183
+ is repeatedly shuffled to break the pairing with a. This is
184
+ a non-parametric test of the null hypothesis r = 0.
185
+
186
+ ci_boot_lo BCa (bias-corrected and accelerated) CI lower bound.
187
+ ci_boot_hi BCa CI upper bound.
188
+
189
+ The CI level defaults to 95% and is controlled by --confidence.
190
+ BCa corrects for both bias (z0) and skewness (acceleration factor a_hat)
191
+ in the bootstrap distribution. The acceleration factor is estimated via a
192
+ streaming jackknife: leave-one-out statistics are computed one at a time
193
+ (capped at 500 observations when n > 500) so peak memory is O(n) rather
194
+ than O(n^2). Falls back to percentile CI if BCa is degenerate.
195
+
196
+ Use --randomseed for reproducibility.
197
+
198
+ EXAMPLES
199
+ --------
200
+ dftk corr data.tsv -c x:y
201
+ dftk corr data.tsv -c x:y -g group --method spearman
202
+ dftk corr data.tsv -c x:y --ci
203
+ dftk corr data.tsv -c x:y --method spearman --bootstrap 2000 --randomseed 42
204
+ """
205
+
206
+
207
+ class CorrCommand(BaseCommand):
208
+ @property
209
+ def name(self) -> str:
210
+ return "corr"
211
+
212
+ @property
213
+ def help(self) -> str:
214
+ return "Pairwise column correlations (Pearson, Spearman, Kendall) with optional bootstrap." # noqa: E501
215
+
216
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
217
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
218
+ parser.epilog = _EPILOG
219
+
220
+ self.add_io_arguments(parser)
221
+
222
+ g = parser.add_argument_group("corr options")
223
+ g.add_argument(
224
+ "-c",
225
+ "--cols",
226
+ nargs="+",
227
+ required=True,
228
+ metavar="COL1:COL2",
229
+ help="Column pair(s) to correlate, e.g. x:y.",
230
+ )
231
+ g.add_argument(
232
+ "-g",
233
+ "--groups",
234
+ nargs="+",
235
+ default=[],
236
+ metavar="COL",
237
+ help="Group column(s); correlation is computed within each group.",
238
+ )
239
+ g.add_argument(
240
+ "--method",
241
+ choices=METHODS,
242
+ default="pearson",
243
+ help="Correlation method (default: pearson).",
244
+ )
245
+ g.add_argument(
246
+ "--ci",
247
+ action="store_true",
248
+ help="Include analytical 95%% CI columns cilo/cihi (Pearson only).",
249
+ )
250
+ g.add_argument(
251
+ "--bootstrap",
252
+ type=int,
253
+ default=None,
254
+ metavar="N",
255
+ help="Bootstrap N resamples; adds p_perm and BCa CI columns.",
256
+ )
257
+ g.add_argument(
258
+ "--confidence",
259
+ type=float,
260
+ default=95.0,
261
+ metavar="PCT",
262
+ help="CI level in percent for BCa bootstrap bounds (default: 95.0).",
263
+ )
264
+ g.add_argument(
265
+ "--randomseed",
266
+ default=None,
267
+ metavar="SEED",
268
+ help="Random seed for bootstrap/permutation (integer or string).",
269
+ )
270
+
271
+ def execute(self, args: argparse.Namespace) -> None:
272
+ seed = normalize_seed(args.randomseed) if args.randomseed is not None else None
273
+ rng = np.random.default_rng(seed)
274
+
275
+ all_cols = [c for pair in args.cols for c in pair.split(":", maxsplit=1)]
276
+ df = io.read(args)
277
+ check_cols(df, all_cols, "-c/--cols")
278
+ check_cols(df, args.groups, "-g/--groups")
279
+
280
+ result = _corr(df, args, rng)
281
+ io.printdf(result, args)
@@ -0,0 +1,181 @@
1
+ """
2
+ dftk.commands.dataset_cmd — the ``dftk dataset`` subcommand.
3
+
4
+ Provides access to curated example datasets from seaborn, statsmodels,
5
+ and (optionally) pydataset. Datasets are written to stdout as TSV or
6
+ into the dftk parquet pipe with -o.
7
+
8
+ Examples
9
+ --------
10
+ dftk dataset iris
11
+ dftk dataset iris --source seaborn
12
+ dftk dataset longley --source statsmodels
13
+ dftk dataset --list
14
+ dftk dataset --list --source seaborn
15
+ dftk dataset iris -o | dftk stat -c sepal_length -g species
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+
21
+ import pandas as pd
22
+
23
+ from dftk.commands.base import BaseCommand
24
+ from dftk.common.io import io
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Per-source load/list helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def _load_seaborn(name: str) -> pd.DataFrame:
32
+ import seaborn as sns
33
+
34
+ return sns.load_dataset(name)
35
+
36
+
37
+ def _list_seaborn() -> list[tuple[str, str, str]]:
38
+ import seaborn as sns
39
+
40
+ return [("seaborn", n, "") for n in sorted(sns.get_dataset_names())]
41
+
42
+
43
+ def _load_statsmodels(name: str) -> pd.DataFrame:
44
+ import statsmodels.api as sm
45
+
46
+ mod = getattr(sm.datasets, name, None)
47
+ if mod is not None and hasattr(mod, "load_pandas"):
48
+ return mod.load_pandas().data
49
+ raise ValueError(f"statsmodels dataset {name!r} not found")
50
+
51
+
52
+ def _list_statsmodels() -> list[tuple[str, str, str]]:
53
+ import statsmodels.api as sm
54
+
55
+ rows = []
56
+ for name in sorted(dir(sm.datasets)):
57
+ if name.startswith("_"):
58
+ continue
59
+ mod = getattr(sm.datasets, name, None)
60
+ if mod is None or not hasattr(mod, "load_pandas"):
61
+ continue
62
+ title = (getattr(mod, "TITLE", "") or "").strip()
63
+ desc = title or (getattr(mod, "DESCRSHORT", "") or "").strip()
64
+ rows.append(("statsmodels", name, desc))
65
+ return rows
66
+
67
+
68
+ try:
69
+ import pydataset as _pydataset # noqa: F401
70
+
71
+ _HAS_PYDATASET = True
72
+ except ImportError:
73
+ _HAS_PYDATASET = False
74
+
75
+
76
+ def _load_pydataset(name: str) -> pd.DataFrame:
77
+ import pydataset
78
+
79
+ return pydataset.data(name)
80
+
81
+
82
+ def _list_pydataset() -> list[tuple[str, str, str]]:
83
+ import pydataset
84
+
85
+ index = pydataset.data()
86
+ return [
87
+ ("pydataset", str(row["item"]), str(row.get("title", "")))
88
+ for _, row in index.iterrows()
89
+ ]
90
+
91
+
92
+ # Registry — built at import time so choices are accurate
93
+ _SOURCES: dict[str, tuple] = {
94
+ "seaborn": (_load_seaborn, _list_seaborn),
95
+ "statsmodels": (_load_statsmodels, _list_statsmodels),
96
+ }
97
+ if _HAS_PYDATASET:
98
+ _SOURCES["pydataset"] = (_load_pydataset, _list_pydataset)
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Command
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ class DatasetCommand(BaseCommand):
107
+ name = "dataset"
108
+ help = "Load a curated example dataset (seaborn, statsmodels, pydataset)"
109
+
110
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
111
+ io.parser_output(parser)
112
+
113
+ g = parser.add_argument_group("dataset options")
114
+ g.add_argument(
115
+ "NAME",
116
+ nargs="?",
117
+ default=None,
118
+ help=(
119
+ "Dataset name (e.g. 'iris', 'longley'). "
120
+ "Omit with --list to show all available datasets."
121
+ ),
122
+ )
123
+ g.add_argument(
124
+ "--source",
125
+ nargs="+",
126
+ default=None,
127
+ choices=list(_SOURCES),
128
+ metavar="SOURCE",
129
+ help=(
130
+ f"Source(s) to search: {', '.join(_SOURCES)}. "
131
+ "Default: try all sources in order."
132
+ ),
133
+ )
134
+ g.add_argument(
135
+ "--list",
136
+ action="store_true",
137
+ dest="list_datasets",
138
+ help="List available datasets as TSV (source, name, description).",
139
+ )
140
+
141
+ def execute(self, args: argparse.Namespace) -> None:
142
+ sources = args.source or list(_SOURCES)
143
+
144
+ if args.list_datasets:
145
+ self._print_list(sources)
146
+ return
147
+
148
+ if args.NAME is None:
149
+ raise ValueError(
150
+ "Provide a dataset NAME, or use --list to browse available datasets."
151
+ )
152
+
153
+ df = self._load(args.NAME, sources)
154
+ io.printdf(df, args)
155
+
156
+ # ------------------------------------------------------------------ #
157
+ # Helpers #
158
+ # ------------------------------------------------------------------ #
159
+
160
+ def _load(self, name: str, sources: list[str]) -> pd.DataFrame:
161
+ errors: list[str] = []
162
+ for src in sources:
163
+ load_fn, _ = _SOURCES[src]
164
+ try:
165
+ return load_fn(name)
166
+ except Exception as exc:
167
+ errors.append(f" {src}: {exc}")
168
+ raise ValueError(
169
+ f"Dataset {name!r} not found in: {', '.join(sources)}.\n"
170
+ + "\n".join(errors)
171
+ )
172
+
173
+ def _print_list(self, sources: list[str]) -> None:
174
+ print("source\tname\tdescription")
175
+ for src in sources:
176
+ _, list_fn = _SOURCES[src]
177
+ try:
178
+ for source, name, desc in list_fn():
179
+ print(f"{source}\t{name}\t{desc}")
180
+ except Exception as exc:
181
+ print(f"Warning: could not list {src} datasets: {exc}", file=sys.stderr)