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.
dftk/common/seq.py ADDED
@@ -0,0 +1,150 @@
1
+ """
2
+ dftk.common.seq — DNA/sequence utility functions.
3
+
4
+ Row-wise functions for use with pandas .apply(), importable by any command.
5
+
6
+ Available functions
7
+ -------------------
8
+ gc_content(x) GC percentage of a nucleotide string.
9
+ letter_count(x) Tuple (A, T, G, C) counts.
10
+ letter_total(x) Total ATGC count (non-ATGC characters excluded).
11
+ at_gc_ratio(x) (A+T) / (G+C) ratio.
12
+ count_motif(pattern, seq) Count (possibly overlapping) occurrences of a
13
+ nucleotide pattern; N in the pattern matches any base.
14
+ read_offset(read_pos, ref_pos, cigar, read_seq, count)
15
+ Extract bases from a short read at a given reference
16
+ position, accounting for CIGAR-encoded alignment gaps.
17
+ """
18
+
19
+ import re
20
+
21
+ import numpy as np
22
+
23
+ __all__ = [
24
+ "gc_content",
25
+ "letter_count",
26
+ "letter_total",
27
+ "at_gc_ratio",
28
+ "count_motif",
29
+ "read_offset",
30
+ ]
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Base composition
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ def letter_count(x: str) -> tuple[int, int, int, int]:
39
+ """Return (A, T, G, C) counts for nucleotide string *x*."""
40
+ letters = list(str(x).upper())
41
+ return (
42
+ letters.count("A"),
43
+ letters.count("T"),
44
+ letters.count("G"),
45
+ letters.count("C"),
46
+ )
47
+
48
+
49
+ def gc_content(x: str) -> float:
50
+ """GC percentage of nucleotide string *x*.
51
+
52
+ Returns NaN for strings that contain no ATGC characters.
53
+ """
54
+ A, T, G, C = letter_count(x)
55
+ total = A + T + G + C
56
+ return 100.0 * (G + C) / total if total > 0 else np.nan
57
+
58
+
59
+ def letter_total(x: str) -> int:
60
+ """Total ATGC character count in *x*."""
61
+ A, T, G, C = letter_count(x)
62
+ return A + T + G + C
63
+
64
+
65
+ def at_gc_ratio(x: str) -> float:
66
+ """(A+T) / (G+C) ratio of *x*.
67
+
68
+ Returns NaN when G+C == 0.
69
+ """
70
+ A, T, G, C = letter_count(x)
71
+ gc = G + C
72
+ return (A + T) / gc if gc > 0 else np.nan
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Motif counting
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ def count_motif(pattern: str, seq: str) -> int:
81
+ """Count occurrences of *pattern* in *seq*.
82
+
83
+ 'N' in *pattern* matches any single character (A, T, G, or C).
84
+ Overlapping matches are counted.
85
+ """
86
+ regex = pattern.replace("N", "(A|T|C|G)")
87
+ return len(re.findall(regex, str(seq)))
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # CIGAR-based read offset
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ def read_offset(
96
+ read_pos: int,
97
+ ref_pos: int,
98
+ cigar: str,
99
+ read_seq: str,
100
+ count: int = 1,
101
+ ) -> str:
102
+ """Extract *count* bases from a short read at reference position *ref_pos*.
103
+
104
+ Handles M (match/mismatch), I (insertion), D/N (deletion/skip), and
105
+ S (soft-clip) CIGAR operations. Returns '.' for positions outside the
106
+ aligned region and 'N' for positions inside a deletion.
107
+
108
+ Parameters
109
+ ----------
110
+ read_pos: leftmost mapping position of the read on the reference (0-based)
111
+ ref_pos: reference position to extract (0-based)
112
+ cigar: CIGAR string (e.g. "5S40M2I10M")
113
+ read_seq: read sequence string
114
+ count: number of consecutive reference bases to extract
115
+ """
116
+ cigar_re = re.compile(r"(\d+)([A-Z])")
117
+ offset_list: list[int] = []
118
+ last = -1
119
+ got_soft_clip = False
120
+
121
+ for steps_str, op in cigar_re.findall(cigar):
122
+ steps = int(steps_str)
123
+ if op == "M":
124
+ offset_list += [last + i + 1 for i in range(steps)]
125
+ last = offset_list[-1]
126
+ elif op == "I":
127
+ offset_list += [-1] * steps # insertions consume read but not reference
128
+ elif op == "S" and not got_soft_clip:
129
+ offset_list += [-1] * steps
130
+ last = offset_list[-1]
131
+ got_soft_clip = True
132
+ elif op in ("D", "N"):
133
+ last += steps
134
+
135
+ ref_pos_list = [x + read_pos if x >= 0 else x for x in offset_list]
136
+
137
+ bases: list[str] = []
138
+ for i in range(count):
139
+ target = ref_pos + i
140
+ try:
141
+ idx = ref_pos_list.index(target)
142
+ base = read_seq[idx]
143
+ except ValueError:
144
+ if ref_pos_list and ref_pos_list[0] < target < ref_pos_list[-1]:
145
+ base = "N"
146
+ else:
147
+ base = "."
148
+ bases.append(base)
149
+
150
+ return "".join(bases)
dftk/common/stats.py ADDED
@@ -0,0 +1,214 @@
1
+ """
2
+ dftk.common.stats — reusable row-wise statistical functions.
3
+
4
+ All functions here take a row (array-like indexed by column name) and return
5
+ a scalar. They are intended to be used as pandas .apply(func, axis=1) targets
6
+ via the dftk eval command, but are importable by any other command.
7
+
8
+ Available functions
9
+ -------------------
10
+ pval2se(row) Convert p-value and effect estimate to standard error.
11
+ t2pval(row) Two-sided p-value from t-statistic and degrees of freedom.
12
+ chi2_to_neglogp(row) Chi-squared + df → -log10(p), numerically stable.
13
+ neglogp_to_chi2(row) -log10(p) + df → chi-squared, numerically stable.
14
+ pval_to_chi2(row) p-value + df → chi-squared.
15
+ binom_test(row) Binomial test p-value.
16
+ fisher_test(row) Fisher exact test p-value (2×2 table).
17
+ fisher_OR(row) Fisher exact test odds ratio (2×2 table).
18
+ boschloo_test(row) Boschloo exact test p-value (2×2 table).
19
+ boschloo_OR(row) Odds ratio from Boschloo test (2×2 table).
20
+ generalized_poisson_nll(row) Negative log-likelihood for the Generalised Poisson.
21
+ """
22
+
23
+ import numpy as np
24
+ import scipy.stats as ss
25
+ from scipy.special import gammaln
26
+
27
+ __all__ = [
28
+ "pval2se",
29
+ "t2pval",
30
+ "chi2_to_neglogp",
31
+ "neglogp_to_chi2",
32
+ "pval_to_chi2",
33
+ "binom_test",
34
+ "fisher_test",
35
+ "fisher_OR",
36
+ "boschloo_test",
37
+ "boschloo_OR",
38
+ "generalized_poisson_nll",
39
+ ]
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # p-value / chi-squared conversions
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ def pval2se(row) -> float:
47
+ """Convert an effect estimate and its p-value to a standard error.
48
+
49
+ row[0]: effect estimate (beta / log-OR / etc.)
50
+ row[1]: two-sided p-value
51
+ """
52
+ return float(np.abs(row[0] / ss.norm.ppf(row[1] / 2)))
53
+
54
+
55
+ def t2pval(row) -> float:
56
+ """Two-sided p-value from a t-statistic and degrees of freedom.
57
+
58
+ row[0]: t-statistic
59
+ row[1]: degrees of freedom
60
+ """
61
+ return float(2 * ss.t.sf(np.abs(row[0]), row[1]))
62
+
63
+
64
+ def chi2_to_neglogp(row, threshold: float = 1380) -> float:
65
+ """Convert a chi-squared statistic and df to -log10(p).
66
+
67
+ Uses scipy.stats.chi2.logsf for moderate values; switches to an
68
+ asymptotic approximation above *threshold* to avoid underflow.
69
+
70
+ row[0]: chi-squared statistic
71
+ row[1]: degrees of freedom
72
+ """
73
+ chi_val = float(row[0])
74
+ df = float(row[1])
75
+ if chi_val < threshold:
76
+ log_p = ss.chi2.logsf(chi_val, df)
77
+ return float(-log_p / np.log(10))
78
+ else:
79
+ lin_term = chi_val / 2.0
80
+ log_term = -(df / 2 - 1) * np.log(chi_val / 2)
81
+ constant = gammaln(df / 2)
82
+ return float((lin_term + log_term + constant) / np.log(10))
83
+
84
+
85
+ def neglogp_to_chi2(
86
+ row, threshold: float = 300, max_iter: int = 50, tol: float = 1e-8
87
+ ) -> float:
88
+ """Convert -log10(p) and df to a chi-squared statistic.
89
+
90
+ Uses scipy.stats.chi2.isf for moderate -log10(p) values; switches to a
91
+ Newton-Raphson inversion of the asymptotic tail formula above *threshold*.
92
+ Closed-form approximation is used for df=2.
93
+
94
+ row[0]: -log10(p)
95
+ row[1]: degrees of freedom
96
+ """
97
+ neglogp = float(row[0])
98
+ df = int(row[1])
99
+
100
+ if neglogp < threshold:
101
+ p = 10 ** (-neglogp)
102
+ return float(ss.chi2.isf(p, df))
103
+
104
+ constant = gammaln(df / 2)
105
+ nlp = neglogp * np.log(10)
106
+ a = df / 2 - 1
107
+ x = 2 * (nlp - constant)
108
+ if df == 2:
109
+ return float(x)
110
+ for _ in range(max_iter):
111
+ f = nlp - constant + a * np.log(x / 2) - x / 2
112
+ fprime = a / x - 0.5
113
+ step = f / fprime
114
+ x_new = x - step
115
+ if abs(step) < tol:
116
+ return float(x_new)
117
+ x = x_new
118
+ return float(x)
119
+
120
+
121
+ def pval_to_chi2(row) -> float:
122
+ """Convert a p-value and df to a chi-squared statistic.
123
+
124
+ row[0]: p-value
125
+ row[1]: degrees of freedom
126
+ """
127
+ return float(ss.chi2.isf(float(row[0]), df=int(row[1])))
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Exact tests
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ def binom_test(row) -> float:
136
+ """Binomial test p-value.
137
+
138
+ row[0]: number of successes (int)
139
+ row[1]: number of trials (int)
140
+ row[2]: hypothesised success probability
141
+ """
142
+ return float(ss.binomtest(int(row[0]), int(row[1]), float(row[2])).pvalue)
143
+
144
+
145
+ def fisher_test(row) -> float:
146
+ """Fisher exact test p-value for a 2×2 contingency table.
147
+
148
+ row[0..3]: a, b, c, d (cells of the table, row-major)
149
+ """
150
+ return float(ss.fisher_exact([[row[0], row[1]], [row[2], row[3]]])[1])
151
+
152
+
153
+ def fisher_OR(row) -> float:
154
+ """Fisher exact test odds ratio for a 2×2 contingency table.
155
+
156
+ row[0..3]: a, b, c, d (cells of the table, row-major)
157
+ """
158
+ return float(ss.fisher_exact([[row[0], row[1]], [row[2], row[3]]])[0])
159
+
160
+
161
+ def boschloo_test(row) -> float:
162
+ """Boschloo exact test p-value for a 2×2 contingency table.
163
+
164
+ Boschloo's test is uniformly more powerful than Fisher's exact test for
165
+ testing independence when both margins are fixed.
166
+
167
+ row[0..3]: a, b, c, d (cells of the table, row-major)
168
+ """
169
+ table = [[int(row[0]), int(row[1])], [int(row[2]), int(row[3])]]
170
+ return float(ss.boschloo_exact(table).pvalue)
171
+
172
+
173
+ def boschloo_OR(row) -> float:
174
+ """Odds ratio from a 2×2 contingency table (same as Fisher OR).
175
+
176
+ Boschloo's test does not produce an OR directly; we return the
177
+ standard maximum-likelihood OR (ad/bc) instead.
178
+
179
+ row[0..3]: a, b, c, d (cells of the table, row-major)
180
+ """
181
+ a, b, c, d = float(row[0]), float(row[1]), float(row[2]), float(row[3])
182
+ denom = b * c
183
+ if denom == 0:
184
+ return float("inf") if a * d > 0 else float("nan")
185
+ return float((a * d) / denom)
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Probability distributions
190
+ # ---------------------------------------------------------------------------
191
+
192
+
193
+ def generalized_poisson_nll(row) -> float:
194
+ """Negative log-likelihood for the Generalised Poisson distribution.
195
+
196
+ Vectorised-safe: operates on scalar values extracted from a row.
197
+
198
+ row[0]: observed count x
199
+ row[1]: lambda parameter
200
+ row[2]: theta parameter
201
+
202
+ Returns -log P(x | lambda, theta), or +inf when lambda + theta*x ≤ 0.
203
+
204
+ log P = log(lambda) + (x-1)*log(lambda + theta*x)
205
+ - (lambda + theta*x) - log(x!)
206
+ """
207
+ x = float(row[0])
208
+ lam = float(row[1])
209
+ theta = float(row[2])
210
+ lam_theta_x = lam + theta * x
211
+ if lam_theta_x <= 0 or lam <= 0:
212
+ return float("inf")
213
+ log_pmf = np.log(lam) + (x - 1) * np.log(lam_theta_x) - lam_theta_x - gammaln(x + 1)
214
+ return float(-log_pmf)
dftk/io.py ADDED
@@ -0,0 +1,10 @@
1
+ # This module has moved to dftk.common.io.
2
+ # This file exists only as a compatibility shim and can be deleted once
3
+ # nothing imports from dftk.io directly.
4
+ from dftk.common.io import ( # noqa: F401
5
+ DFTK_TMPDIR,
6
+ _is_pipe_path,
7
+ _write_pipe_parquet,
8
+ globnames,
9
+ io,
10
+ )