dataframe-toolkit 0.5.1__tar.gz → 0.7.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.
Files changed (47) hide show
  1. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/PKG-INFO +1 -1
  2. dataframe_toolkit-0.7.0/pyproject.toml +82 -0
  3. dataframe_toolkit-0.5.1/pyproject.toml → dataframe_toolkit-0.7.0/pyproject.toml.orig +1 -1
  4. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/cli.py +9 -0
  5. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/__init__.py +2 -0
  6. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/annotate_cmd.py +4 -0
  7. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/binx_cmd.py +4 -2
  8. dataframe_toolkit-0.7.0/src/dftk/commands/chi_cmd.py +193 -0
  9. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/concat_cmd.py +15 -12
  10. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/corr_cmd.py +92 -9
  11. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/describe_cmd.py +4 -4
  12. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/eval_cmd.py +6 -5
  13. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/fit_cmd.py +6 -5
  14. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/func_cmd.py +6 -5
  15. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/hist_cmd.py +13 -11
  16. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/info_cmd.py +5 -1
  17. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/interp_cmd.py +35 -13
  18. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/line_cmd.py +25 -12
  19. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/melt_cmd.py +20 -10
  20. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/merge_cmd.py +34 -13
  21. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/pivot_cmd.py +34 -21
  22. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/print_cmd.py +1 -4
  23. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/query_cmd.py +31 -22
  24. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/randvar_cmd.py +5 -1
  25. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/sample_cmd.py +9 -7
  26. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/scale_cmd.py +24 -12
  27. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/scat_cmd.py +10 -7
  28. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/segid_cmd.py +7 -0
  29. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/split_cmd.py +8 -5
  30. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/stat_cmd.py +8 -2
  31. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/test_cmd.py +11 -2
  32. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/transpose_cmd.py +12 -0
  33. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/wstat_cmd.py +7 -7
  34. dataframe_toolkit-0.7.0/src/dftk/common/copula.py +213 -0
  35. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/io.py +11 -24
  36. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/plot.py +80 -7
  37. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/README.md +0 -0
  38. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/__init__.py +0 -0
  39. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/base.py +0 -0
  40. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/clean_cmd.py +0 -0
  41. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/dataset_cmd.py +0 -0
  42. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/commands/help.py +0 -0
  43. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/__init__.py +0 -0
  44. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/seed.py +0 -0
  45. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/seq.py +0 -0
  46. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.0}/src/dftk/common/stats.py +0 -0
  47. {dataframe_toolkit-0.5.1 → dataframe_toolkit-0.7.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.5.1
3
+ Version: 0.7.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
@@ -0,0 +1,82 @@
1
+ [project]
2
+ name = "dataframe-toolkit"
3
+ version = "0.7.0"
4
+ description = "A command-line toolkit for DataFrame analysis and manipulation"
5
+ readme = "README.md"
6
+ keywords = [
7
+ "dataframe",
8
+ "statistics",
9
+ "data analysis",
10
+ "CLI",
11
+ "TSV",
12
+ "pandas",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Scientific/Engineering :: Information Analysis",
19
+ "Topic :: Utilities",
20
+ "Programming Language :: Python :: 3",
21
+ ]
22
+ requires-python = ">=3.12"
23
+ dependencies = [
24
+ "pandas>=2.1",
25
+ "numpy>=1.24",
26
+ "pyarrow>=12.0",
27
+ "scipy>=1.10",
28
+ "statsmodels>=0.14",
29
+ "duckdb>=0.8",
30
+ "matplotlib>=3.7",
31
+ "seaborn>=0.12",
32
+ "tqdm>=4.65",
33
+ ]
34
+
35
+ [[project.authors]]
36
+ name = "Gunnar Pálsson"
37
+ email = "hemingur@gmail.com"
38
+
39
+ [project.license]
40
+ text = "MIT"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/hemingur/dataframe-toolkit"
44
+ Repository = "https://github.com/hemingur/dataframe-toolkit"
45
+
46
+ [project.scripts]
47
+ dftk = "dftk.cli:main"
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.10.4,<0.11.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.uv.build-backend]
54
+ module-name = "dftk"
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+
59
+ [tool.ruff]
60
+ line-length = 88
61
+ target-version = "py312"
62
+
63
+ [tool.ruff.lint]
64
+ select = [
65
+ "E",
66
+ "W",
67
+ "F",
68
+ "I",
69
+ "UP",
70
+ "B",
71
+ ]
72
+
73
+ [tool.ruff.lint.isort]
74
+ known-first-party = ["dftk"]
75
+
76
+ [dependency-groups]
77
+ dev = [
78
+ "pre-commit>=4.6.0",
79
+ "pytest>=8.0",
80
+ "pytest-cov>=7.1.0",
81
+ "ruff>=0.15.12",
82
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.5.1"
3
+ version = "0.7.0"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,6 +8,8 @@ logic and never needs to change when a new subcommand is added.
8
8
 
9
9
  import argparse
10
10
  import logging
11
+ import os
12
+ import signal
11
13
  import sys
12
14
 
13
15
  from dftk import __version__
@@ -47,6 +49,13 @@ def main() -> None:
47
49
 
48
50
  try:
49
51
  args.command_instance.execute(args)
52
+ sys.stdout.flush()
53
+ except BrokenPipeError:
54
+ # The reader closed the pipe early (e.g. `dftk ... | head`). Exit
55
+ # quietly with the SIGPIPE status like cat/grep, and point stdout at
56
+ # devnull so the interpreter's final flush doesn't raise again.
57
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
58
+ sys.exit(128 + signal.SIGPIPE)
50
59
  except FileNotFoundError as exc:
51
60
  print(f"File error: {exc}", file=sys.stderr)
52
61
  sys.exit(1)
@@ -8,6 +8,7 @@
8
8
 
9
9
  from dftk.commands.annotate_cmd import AnnotateCommand
10
10
  from dftk.commands.binx_cmd import BinxCommand
11
+ from dftk.commands.chi_cmd import ChiCommand
11
12
  from dftk.commands.clean_cmd import CleanCommand
12
13
  from dftk.commands.concat_cmd import ConcatCommand
13
14
  from dftk.commands.corr_cmd import CorrCommand
@@ -80,6 +81,7 @@ COMMAND_GROUPS: list[tuple[str, list]] = [
80
81
  ScatCommand(),
81
82
  LineCommand(),
82
83
  HistCommand(),
84
+ ChiCommand(),
83
85
  ],
84
86
  ),
85
87
  (
@@ -25,6 +25,10 @@ Usage examples:
25
25
  dftk eval data.tsv -f "z = x + y" -o result.parquet \\
26
26
  --meta genome=hg38 --meta source=my_pipeline
27
27
 
28
+ # Same, built from a bundled dataset:
29
+ dftk dataset iris -o | dftk eval ... -f "z = sepal_length + petal_length" \\
30
+ -o result.parquet --meta genome=hg38 --meta source=my_pipeline
31
+
28
32
  Annotations propagate through the parquet pipe: a file annotated at
29
33
  creation is re-annotated on every subsequent -o write unless explicitly
30
34
  overridden.
@@ -74,8 +74,10 @@ USEVALUE (--usevalue)
74
74
  EXAMPLES
75
75
  --------
76
76
  dftk binx data.tsv -c age -b 0:100:10
77
- dftk binx data.tsv -c score -b 0,25,50,75,100 -d quartile --usevalue m
78
- dftk binx data.tsv -c x -b -3:4:1 -d x_bin -o | dftk stat -g x_bin -s y
77
+ dftk dataset tips -o | dftk binx ... -c total_bill -b 0:60:10
78
+ dftk dataset tips -o | dftk binx ... -c tip -b 0,2,4,6,10 -d tip_bracket --usevalue m
79
+ dftk dataset tips -o | dftk binx ... -c total_bill -b 0:60:10 -d bill_bin -o \\
80
+ | dftk stat ... -c tip -g bill_bin
79
81
  """
80
82
 
81
83
 
@@ -0,0 +1,193 @@
1
+ """
2
+ dftk.commands.chi_cmd — dftk chi subcommand.
3
+
4
+ Corner-resolved chi-plot: a visual diagnostic for which direction (if any)
5
+ two variables have tail dependence in, complementing the numeric
6
+ lambda_lower/lambda_upper columns from `dftk corr --tail`.
7
+
8
+ Follows the structure of scat_cmd.py -- same --groupcol / --subgraphcol /
9
+ --size / --fontsize figure conventions as the rest of the Plots group.
10
+ """
11
+
12
+ import argparse
13
+
14
+ import numpy as np
15
+
16
+ from dftk.commands.base import BaseCommand
17
+ from dftk.common.copula import corner_chi, pseudo_observations
18
+ from dftk.common.io import check_cols, io
19
+ from dftk.common.plot import (
20
+ add_figure_arguments,
21
+ add_group_arguments,
22
+ add_legend_arguments,
23
+ add_xy_arguments,
24
+ apply_labels,
25
+ apply_style,
26
+ make_figure,
27
+ make_grouplabel,
28
+ save_or_show,
29
+ subgraph_groups,
30
+ subgraph_layout,
31
+ )
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Single-axes chi-plot (reused per subplot)
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ def _chi_ax(ax, df, args, title_suffix: str = ""):
39
+ xcol, ycol = args.xcol, args.ycol
40
+ legendloc = tuple(args.legendloc) if getattr(args, "legendloc", None) else "best"
41
+
42
+ def _draw(sub_df, label=None, **scatter_kw):
43
+ a, b = sub_df[[xcol, ycol]].dropna().values.T
44
+ if len(a) < 3:
45
+ return
46
+ u, v = pseudo_observations(a, b)
47
+ x, chi = corner_chi(u, v)
48
+ mask = np.abs(x) < 0.8 # trim the noisiest extreme points, standard practice
49
+ ax.scatter(x[mask], chi[mask], s=8, alpha=0.4, label=label, **scatter_kw)
50
+ ci = 1.78 / np.sqrt(len(u))
51
+ return ci
52
+
53
+ if args.groupcol is not None:
54
+ # Each group gets its OWN chi statistic computed within its own subset
55
+ # (mixing groups into one leave-one-out CDF would misrepresent both) --
56
+ # overlaid here the same way scat/hist overlay per-group series.
57
+ cis = []
58
+ for groupname, gdf in subgraph_groups(
59
+ df, args.groupcol, getattr(args, "groupcolorder", None)
60
+ ):
61
+ label = make_grouplabel(
62
+ groupname, args.groupcol, getattr(args, "groupcolformat", None)
63
+ )
64
+ ci = _draw(gdf, label=label)
65
+ if ci is not None:
66
+ cis.append(ci)
67
+ ax.legend(loc=legendloc, title=getattr(args, "legendtitle", None))
68
+ ci = max(cis) if cis else 0.0
69
+ else:
70
+ ci = _draw(df) or 0.0
71
+
72
+ ax.axhline(0, color="black", lw=0.8)
73
+ ax.axvline(0, color="gray", lw=0.6, ls=":")
74
+ if ci:
75
+ ax.axhline(ci, color="crimson", lw=1, ls="--", alpha=0.7)
76
+ ax.axhline(-ci, color="crimson", lw=1, ls="--", alpha=0.7)
77
+ ax.set_xlim(-1, 1)
78
+ ax.set_ylim(-0.2, 1)
79
+
80
+ apply_labels(
81
+ ax,
82
+ args,
83
+ default_xlabel="<- lower corner | upper corner ->",
84
+ default_ylabel=r"$\chi$ (local dependence)",
85
+ )
86
+ if title_suffix:
87
+ existing = ax.get_title()
88
+ ax.set_title(f"{existing} [{title_suffix}]" if existing else title_suffix)
89
+
90
+ ax.grid(True, linewidth=0.4, alpha=0.5)
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Command
95
+ # ---------------------------------------------------------------------------
96
+
97
+ _EPILOG = """\
98
+ Reads as: chi elevated (and rising) on the LEFT (negative x) only => lower-
99
+ tail dependence -- the two variables' worst values tend to co-occur, but
100
+ their best values don't (e.g. joint crashes but not joint rallies). Elevated
101
+ on the RIGHT only => upper-tail dependence (joint highs, not joint lows).
102
+ Elevated similarly on BOTH sides, decaying back toward 0 well before the
103
+ edges => generic correlation with no real tail dependence. Flat at 0
104
+ everywhere => independence. Dashed red lines mark an approximate 95%
105
+ control limit for "indistinguishable from independence".
106
+
107
+ This is a visual complement to `dftk corr --tail`'s numeric
108
+ lambda_lower/lambda_upper columns -- same underlying question, this command
109
+ answers it by eye and per-corner in one plot instead of at a chosen set of
110
+ thresholds. Needs a reasonable amount of data to be legible: a few hundred
111
+ points is usually enough to see the shape, but a strong one-sided asymmetry
112
+ is easiest to trust with several thousand.
113
+
114
+ EXAMPLES
115
+ --------
116
+ Basic chi-plot:
117
+
118
+ dftk chi data.tsv -x recomb_rate -y gc_content
119
+ dftk dataset tips -o | dftk chi ... -x total_bill -y tip
120
+
121
+ Split into a subplot grid by category, to check whether tail dependence is
122
+ consistent across the whole dataset or driven by a few subgroups (e.g.
123
+ chromosome, sector, region — here, diamond cut quality):
124
+
125
+ dftk dataset diamonds -o | dftk chi ... -x carat -y price --subgraphcol cut
126
+
127
+ Colour-code by group instead of splitting into subplots:
128
+
129
+ dftk dataset diamonds -o | dftk chi ... -x carat -y price -g cut
130
+
131
+ Publication figure (Nature single column, PDF):
132
+
133
+ dftk dataset tips -o | dftk chi ... -x total_bill -y tip \\
134
+ --size single --fontsize publication -f fig.pdf
135
+ """
136
+
137
+
138
+ class ChiCommand(BaseCommand):
139
+ name = "chi"
140
+ help = "Corner-resolved chi-plot: visualize lower vs upper tail dependence."
141
+
142
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
143
+ parser.formatter_class = argparse.RawDescriptionHelpFormatter
144
+ parser.epilog = _EPILOG
145
+
146
+ self.add_read_arguments(parser)
147
+ add_xy_arguments(parser)
148
+ add_figure_arguments(parser)
149
+ add_group_arguments(parser)
150
+ add_legend_arguments(parser)
151
+
152
+ def execute(self, args: argparse.Namespace) -> None:
153
+ if not args.xcol or not args.ycol:
154
+ raise ValueError("--xcol (-x) and --ycol (-y) are required.")
155
+
156
+ df = io.read(args)
157
+ check_cols(df, [args.xcol, args.ycol], "-x/-y")
158
+ check_cols(df, args.groupcol, "-g/--groupcol")
159
+ check_cols(df, args.subgraphcol, "--subgraphcol")
160
+
161
+ if args.file:
162
+ import matplotlib
163
+
164
+ matplotlib.use("Agg")
165
+
166
+ import matplotlib.pyplot as plt
167
+
168
+ if getattr(args, "usetex", False):
169
+ plt.rc("text", usetex=True)
170
+
171
+ apply_style(args)
172
+
173
+ if args.subgraphcol is not None:
174
+ groups = subgraph_groups(
175
+ df, args.subgraphcol, getattr(args, "subgraphorder", None)
176
+ )
177
+ nrows, ncols = subgraph_layout(len(groups), args.ncols)
178
+ fig, axes = make_figure(args, nrows=nrows, ncols=ncols)
179
+ for idx, (groupname, gdf) in enumerate(groups):
180
+ ax = axes[idx // ncols][idx % ncols]
181
+ suffix = make_grouplabel(
182
+ groupname, args.subgraphcol, getattr(args, "subgraphformat", None)
183
+ )
184
+ _chi_ax(ax, gdf, args, title_suffix=suffix)
185
+ for idx in range(len(groups), nrows * ncols):
186
+ axes[idx // ncols][idx % ncols].set_visible(False)
187
+ fig.tight_layout()
188
+ else:
189
+ fig, axes = make_figure(args)
190
+ _chi_ax(axes[0][0], df, args)
191
+ fig.tight_layout()
192
+
193
+ save_or_show(fig, args)
@@ -8,9 +8,11 @@ present in some but not all files are filled with NaN (or --fill value).
8
8
  Examples
9
9
  --------
10
10
  dftk concat a.tsv b.tsv c.tsv
11
- dftk concat a.tsv b.tsv --sourcecol source
12
- dftk concat a.parquet b.parquet -o
13
- dftk eval data.tsv -f "x=1" -o | dftk concat ... extra.tsv
11
+ dftk dataset penguins -o penguins.tsv
12
+ dftk concat penguins.tsv penguins.tsv --sourcecol source
13
+ dftk dataset penguins -o penguins.parquet
14
+ dftk concat penguins.parquet penguins.parquet -o
15
+ dftk dataset penguins -o | dftk eval ... -f "x=1" -o | dftk concat ... penguins.tsv
14
16
  """
15
17
 
16
18
  import argparse
@@ -43,9 +45,16 @@ COLUMN ALIGNMENT
43
45
  EXAMPLES
44
46
  --------
45
47
  dftk concat a.tsv b.tsv
46
- dftk concat a.tsv b.tsv c.tsv --sourcecol source
47
- dftk concat a.parquet b.parquet -o result.parquet
48
- dftk eval data.tsv -f "x=1" -o | dftk concat ... extra.tsv
48
+ dftk dataset penguins -o penguins.tsv
49
+ dftk concat penguins.tsv penguins.tsv --sourcecol source
50
+ dftk dataset penguins -o penguins.parquet
51
+ dftk concat penguins.parquet penguins.parquet -o result.parquet
52
+ dftk dataset penguins -o | dftk eval ... -f "x=1" -o | dftk concat ... penguins.tsv
53
+
54
+ Mismatched columns are outer-joined and filled (NaN by default, or --fill):
55
+
56
+ dftk dataset iris -o iris.tsv
57
+ dftk concat iris.tsv penguins.tsv --fill 0
49
58
  """
50
59
  g = parser.add_argument_group("concat options")
51
60
  g.add_argument(
@@ -86,12 +95,6 @@ EXAMPLES
86
95
  help="Input files have no header row; columns are named V1, V2, …",
87
96
  action="store_true",
88
97
  )
89
- r.add_argument(
90
- "--nrows",
91
- help="Maximum number of rows to read per file",
92
- type=int,
93
- default=None,
94
- )
95
98
  r.add_argument(
96
99
  "--delimiter",
97
100
  help="Column delimiter for TSV inputs (default: tab)",
@@ -18,6 +18,11 @@ import pandas as pd
18
18
  import scipy.stats as ss
19
19
 
20
20
  from dftk.commands.base import BaseCommand
21
+ from dftk.common.copula import (
22
+ MIN_TAIL_N,
23
+ empirical_tail_dependence,
24
+ pseudo_observations,
25
+ )
21
26
  from dftk.common.io import check_cols, io
22
27
  from dftk.common.seed import normalize_seed
23
28
 
@@ -112,6 +117,29 @@ def _bootstrap_stats(
112
117
  return p_perm, ci_lo, ci_hi
113
118
 
114
119
 
120
+ def _tail_stats(a: np.ndarray, b: np.ndarray, q_grid: np.ndarray) -> tuple:
121
+ """Return a flat tuple of (lambda_lower_qX, lambda_upper_qX, ...) for each q
122
+ in q_grid, in the same order, plus a stderr warning if any q is too small
123
+ for the sample size to resolve.
124
+ """
125
+ n = len(a)
126
+ u, v = pseudo_observations(a, b)
127
+ lam_L, lam_U = empirical_tail_dependence(u, v, q_grid)
128
+ thin = [q for q in q_grid if n * q < MIN_TAIL_N]
129
+ if thin:
130
+ logger.warning(
131
+ "tail dependence: n=%d gives fewer than %d points in the tail for "
132
+ "q=%s -- these estimates will be noisy. See --tail-q / MIN_TAIL_N.",
133
+ n,
134
+ MIN_TAIL_N,
135
+ ", ".join(f"{q:g}" for q in thin),
136
+ )
137
+ out = []
138
+ for lo, hi in zip(lam_L, lam_U, strict=True):
139
+ out.extend([float(lo), float(hi)])
140
+ return tuple(out)
141
+
142
+
115
143
  def _corr(
116
144
  df: pd.DataFrame, args: argparse.Namespace, rng: np.random.Generator
117
145
  ) -> pd.DataFrame:
@@ -122,12 +150,17 @@ def _corr(
122
150
  raise ValueError(f"-c pairs must be col1:col2, got: {bad}")
123
151
 
124
152
  ci = args.ci and args.method == "pearson"
153
+ tail = getattr(args, "tail", False)
154
+ q_grid = np.asarray(getattr(args, "tail_q", None) or [0.05])
125
155
 
126
156
  header = ["col1", "col2", "nobs", "correlation", "pvalue"]
127
157
  if ci:
128
158
  header += ["cilo", "cihi"]
129
159
  if args.bootstrap is not None:
130
160
  header += ["p_perm", "ci_boot_lo", "ci_boot_hi"]
161
+ if tail:
162
+ for q in q_grid:
163
+ header += [f"lambda_lower_q{q:g}", f"lambda_upper_q{q:g}"]
131
164
 
132
165
  rows = []
133
166
 
@@ -136,13 +169,14 @@ def _corr(
136
169
  pair_df = sub[[col1, col2]].dropna()
137
170
  a, b = pair_df[col1].values, pair_df[col2].values
138
171
  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(
172
+ extra = ()
173
+ if args.bootstrap is not None:
174
+ extra += _bootstrap_stats(
143
175
  a, b, method_fn, args.bootstrap, rng, args.confidence / 100.0
144
176
  )
145
- rows.append((*prefix, col1, col2, *cr, *boot))
177
+ if tail:
178
+ extra += _tail_stats(a, b, q_grid)
179
+ rows.append((*prefix, col1, col2, *cr, *extra))
146
180
 
147
181
  if args.groups:
148
182
  header = [*args.groups, *header]
@@ -161,7 +195,7 @@ COLUMN PAIRS (-c)
161
195
  Pairs are specified as col1:col2. Multiple pairs may be given:
162
196
 
163
197
  dftk corr data.tsv -c x:y
164
- dftk corr data.tsv -c x:y a:b
198
+ dftk dataset tips -o | dftk corr ... -c total_bill:tip tip:size
165
199
 
166
200
  METHODS (--method)
167
201
  ------------------
@@ -195,12 +229,45 @@ BOOTSTRAP (--bootstrap N)
195
229
 
196
230
  Use --randomseed for reproducibility.
197
231
 
232
+ TAIL DEPENDENCE (--tail / --tail-q)
233
+ ------------------------------------
234
+ Adds lambda_lower_qN / lambda_upper_qN columns: nonparametric estimates of
235
+ how likely one variable is to be extreme given the other is, separately
236
+ for the lower and upper tail, at each threshold N given to --tail-q.
237
+
238
+ A correlation coefficient describes overall association; it says nothing
239
+ about whether the *extremes* specifically move together more (or less)
240
+ than the bulk of the data. Two pairs can share the same Spearman rho and
241
+ have very different tail behavior -- this is what tells them apart. Two
242
+ markers can look equally correlated genome-wide while only one shares
243
+ its rare, extreme values; two assets can look equally correlated day to
244
+ day while only one actually crashes together.
245
+
246
+ Reading it: pass 2-3 values to --tail-q spanning an order of magnitude
247
+ (e.g. --tail-q 0.02 0.05 0.1). If both lambda columns shrink as q shrinks,
248
+ there is no real tail dependence (whatever correlation exists is spread
249
+ evenly, not concentrated in the extremes). If a lambda column stays
250
+ roughly flat across q instead of shrinking, that side has genuine tail
251
+ dependence. A single q in isolation is easy to misread, since even a pair
252
+ with NO tail dependence gives an elevated, nonzero reading at any finite
253
+ q if the pair is strongly correlated overall -- it is the trend across q
254
+ that carries the signal, not any one number.
255
+
256
+ This needs enough data in the tail region to be reliable: a warning is
257
+ printed to stderr when nobs * q for the smallest requested q falls below
258
+ 20, since anything thinner is dominated by sampling noise. Pair with
259
+ `dftk chi` for a visual, corner-resolved view of the same question.
260
+
198
261
  EXAMPLES
199
262
  --------
200
263
  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
264
+ dftk dataset tips -o | dftk corr ... -c total_bill:tip
265
+ dftk dataset tips -o | dftk corr ... -c total_bill:tip -g day --method spearman
266
+ dftk dataset tips -o | dftk corr ... -c total_bill:tip --ci
267
+ dftk dataset tips -o \\
268
+ | dftk corr ... -c total_bill:tip --method spearman \\
269
+ --bootstrap 2000 --randomseed 42
270
+ dftk dataset diamonds -o | dftk corr ... -c carat:price --tail --tail-q 0.02 0.05 0.1
204
271
  """
205
272
 
206
273
 
@@ -267,6 +334,22 @@ class CorrCommand(BaseCommand):
267
334
  metavar="SEED",
268
335
  help="Random seed for bootstrap/permutation (integer or string).",
269
336
  )
337
+ g.add_argument(
338
+ "--tail",
339
+ action="store_true",
340
+ help="Add nonparametric lower/upper tail-dependence coefficient "
341
+ "columns (see --tail-q).",
342
+ )
343
+ g.add_argument(
344
+ "--tail-q",
345
+ type=float,
346
+ nargs="+",
347
+ default=None,
348
+ metavar="Q",
349
+ help="Tail probability threshold(s) for --tail, e.g. 0.02 0.05 0.1 "
350
+ "(default: 0.05). Smaller q looks deeper into the tail but needs "
351
+ "more data to stay reliable -- see epilog.",
352
+ )
270
353
 
271
354
  def execute(self, args: argparse.Namespace) -> None:
272
355
  seed = normalize_seed(args.randomseed) if args.randomseed is not None else None
@@ -37,7 +37,7 @@ Optional flags
37
37
  Example
38
38
  -------
39
39
  dftk describe data.tsv
40
- dftk describe data.tsv --summary --correlations
40
+ dftk dataset iris -o | dftk describe ... --summary --correlations
41
41
  """
42
42
 
43
43
  import argparse
@@ -314,9 +314,9 @@ NOTES FLAGS
314
314
  EXAMPLES
315
315
  --------
316
316
  dftk describe data.tsv
317
- dftk describe data.tsv --summary --correlations
318
- dftk describe data.tsv --summary --correlations --corr-threshold 0.5
319
- dftk dataset iris | dftk describe - --summary --correlations
317
+ dftk dataset iris -o | dftk describe ...
318
+ dftk dataset iris -o | dftk describe ... --summary --correlations
319
+ dftk dataset iris -o | dftk describe ... --summary --correlations --corr-threshold 0.5
320
320
  """
321
321
  io.parser_read(parser)
322
322
 
@@ -43,7 +43,8 @@ Each -f argument is first passed to pandas df.eval(). This handles
43
43
  arithmetic and boolean expressions directly on column names:
44
44
 
45
45
  dftk eval data.tsv -f "z = x + y * 2"
46
- dftk eval data.tsv -f "flag = (pval < 0.05) & (effect > 0)"
46
+ dftk dataset tips -o | dftk eval ... -f "bill_and_tip = total_bill + tip * 2"
47
+ dftk dataset tips -o | dftk eval ... -f "good_tip = (tip > 3) & (total_bill < 30)"
47
48
 
48
49
  If df.eval() fails (e.g. for function calls), the expression is retried
49
50
  as a special function call using the syntax:
@@ -61,8 +62,8 @@ Available special functions:
61
62
  true_val and false_val can each be: a column name, a quoted
62
63
  string literal ("foo"), or a numeric literal.
63
64
  Example:
64
- dftk eval data.tsv \
65
- -f "big = value > 100" \
65
+ dftk dataset tips -o | dftk eval ... \
66
+ -f "big = total_bill > 30" \
66
67
  -f "label = where(big, \"large\", \"small\")"
67
68
 
68
69
  Bitwise:
@@ -119,8 +120,8 @@ CONSTANT COLUMNS (-c)
119
120
  ----------------------
120
121
  Add a column with a fixed value:
121
122
 
122
- dftk eval data.tsv -c "batch = 3"
123
- dftk eval data.tsv -c "label = control"
123
+ dftk dataset tips -o | dftk eval ... -c "batch = 3"
124
+ dftk dataset tips -o | dftk eval ... -c "region = northeast"
124
125
 
125
126
  The value is coerced to numeric when possible.
126
127
 
@@ -155,26 +155,27 @@ EXAMPLES
155
155
  Simple OLS:
156
156
 
157
157
  dftk fit data.tsv -f "y ~ x"
158
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower"
158
159
 
159
160
  Grouped OLS (one regression per group):
160
161
 
161
- dftk fit data.tsv -f "y ~ x" -g group
162
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower" -g origin
162
163
 
163
164
  Weighted least squares:
164
165
 
165
- dftk fit data.tsv -f "y ~ x" -w weight_col
166
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower" -w weight
166
167
 
167
168
  Robust regression (HuberT):
168
169
 
169
- dftk fit data.tsv -f "y ~ x" --robust
170
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower" --robust
170
171
 
171
172
  Print detailed summary (no grouping):
172
173
 
173
- dftk fit data.tsv -f "y ~ x" --summary
174
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower" --summary
174
175
 
175
176
  ANOVA table (OLS, no grouping):
176
177
 
177
- dftk fit data.tsv -f "y ~ x + z" --anova
178
+ dftk dataset mpg -o | dftk fit ... -f "mpg ~ horsepower + weight" --anova
178
179
  """
179
180
 
180
181