ldpart 0.1.0__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.
- ldpart/__init__.py +36 -0
- ldpart/cli.py +115 -0
- ldpart/helpers.py +57 -0
- ldpart/partitioner.py +509 -0
- ldpart-0.1.0.dist-info/METADATA +151 -0
- ldpart-0.1.0.dist-info/RECORD +10 -0
- ldpart-0.1.0.dist-info/WHEEL +5 -0
- ldpart-0.1.0.dist-info/entry_points.txt +2 -0
- ldpart-0.1.0.dist-info/licenses/LICENSE +21 -0
- ldpart-0.1.0.dist-info/top_level.txt +1 -0
ldpart/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ldpart: certified multicollinearity partitioning of LD matrices.
|
|
3
|
+
|
|
4
|
+
Group SNPs into contiguous blocks of correlated SNPs from a square LD
|
|
5
|
+
matrix (r or r2 with 1 on the diagonal, SNPs ordered as the rows and
|
|
6
|
+
columns), with six greedy criteria built on classical
|
|
7
|
+
multicollinearity diagnostics: the determinant (fast Schur updates
|
|
8
|
+
and the LAPACK slogdet reference), the condition number, the maximal
|
|
9
|
+
variance inflation factor, the sum of reciprocal eigenvalues, and the
|
|
10
|
+
marginal pairwise r2 screen. Every saved block is certified: it
|
|
11
|
+
satisfies its criterion, and the monotone criteria additionally
|
|
12
|
+
certify maximality. See partitioner for the semantics.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .partitioner import (
|
|
16
|
+
partition_by_determinant,
|
|
17
|
+
partition_by_determinant_slogdet,
|
|
18
|
+
partition_by_condition,
|
|
19
|
+
partition_by_vif,
|
|
20
|
+
partition_by_reciprocal_eigenvalues,
|
|
21
|
+
partition_by_pairwise,
|
|
22
|
+
labels_to_blocks,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"partition_by_determinant",
|
|
29
|
+
"partition_by_determinant_slogdet",
|
|
30
|
+
"partition_by_condition",
|
|
31
|
+
"partition_by_vif",
|
|
32
|
+
"partition_by_reciprocal_eigenvalues",
|
|
33
|
+
"partition_by_pairwise",
|
|
34
|
+
"labels_to_blocks",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
ldpart/cli.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command line interface of ldpart: partition a square LD matrix into
|
|
3
|
+
blocks of correlated SNPs by a multicollinearity criterion. The input
|
|
4
|
+
file holds the matrix (1 on the diagonal, SNPs ordered as the rows and
|
|
5
|
+
columns) in HDF5; the output holds two columns, the zero-based SNP
|
|
6
|
+
index and the block label of every SNP, the format consumed by the
|
|
7
|
+
reproducibility layer of the ld-matrix-partition-01 project.
|
|
8
|
+
|
|
9
|
+
Installed as the console script `ldpart`:
|
|
10
|
+
|
|
11
|
+
ldpart -i LD.h5 -o labels.txt -m fast -d 0.001
|
|
12
|
+
ldpart -i LD.h5 -o labels.txt -m condition -c 1000
|
|
13
|
+
ldpart -i LD.h5 -o labels.txt -m vif -v 10
|
|
14
|
+
ldpart -i LD.h5 -o labels.txt -m reciprocal -r 5
|
|
15
|
+
ldpart -i LD.h5 -o labels.txt -m pairwise -p 0.8
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import h5py
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
from .helpers import (attach_matrix, check_input_files,
|
|
26
|
+
check_output_dir, show_time_elapsed)
|
|
27
|
+
from .partitioner import (
|
|
28
|
+
partition_by_determinant,
|
|
29
|
+
partition_by_determinant_slogdet,
|
|
30
|
+
partition_by_condition,
|
|
31
|
+
partition_by_vif,
|
|
32
|
+
partition_by_reciprocal_eigenvalues,
|
|
33
|
+
partition_by_pairwise,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Method -> (partition function, criterion argument, printed label)
|
|
37
|
+
METHODS = {
|
|
38
|
+
"fast": (partition_by_determinant, "min_det", "Minimal determinant"),
|
|
39
|
+
"slogdet": (partition_by_determinant_slogdet, "min_det",
|
|
40
|
+
"Minimal determinant"),
|
|
41
|
+
"condition": (partition_by_condition, "max_cond",
|
|
42
|
+
"Maximal condition number"),
|
|
43
|
+
"vif": (partition_by_vif, "max_vif", "Maximal VIF"),
|
|
44
|
+
"reciprocal": (partition_by_reciprocal_eigenvalues, "max_ratio",
|
|
45
|
+
"Maximal sum of reciprocal eigenvalues per block "
|
|
46
|
+
"size"),
|
|
47
|
+
"pairwise": (partition_by_pairwise, "min_r2",
|
|
48
|
+
"Minimal pairwise r2"),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parser_args(argv=None) -> argparse.Namespace:
|
|
53
|
+
parser = argparse.ArgumentParser(
|
|
54
|
+
prog="ldpart",
|
|
55
|
+
description="Partition a square LD matrix into blocks of "
|
|
56
|
+
"correlated SNPs by a multicollinearity criterion")
|
|
57
|
+
parser.add_argument("-i", "--input", required=True,
|
|
58
|
+
help="/path/to/input.h5 with the matrix in "
|
|
59
|
+
"HDF5 format")
|
|
60
|
+
parser.add_argument("-o", "--output", required=True,
|
|
61
|
+
help="/path/to/output.txt for the block labels")
|
|
62
|
+
parser.add_argument("-d", "--min_det", type=float, default=0.001,
|
|
63
|
+
help="minimal determinant of a block submatrix, "
|
|
64
|
+
"methods fast and slogdet (default 0.001)")
|
|
65
|
+
parser.add_argument("-c", "--max_cond", type=float, default=1000.0,
|
|
66
|
+
help="maximal condition number of a block "
|
|
67
|
+
"submatrix, method condition "
|
|
68
|
+
"(default 1000)")
|
|
69
|
+
parser.add_argument("-v", "--max_vif", type=float, default=10.0,
|
|
70
|
+
help="maximal variance inflation factor of a "
|
|
71
|
+
"block submatrix, method vif (default 10)")
|
|
72
|
+
parser.add_argument("-r", "--max_ratio", type=float, default=5.0,
|
|
73
|
+
help="maximal sum of reciprocal eigenvalues "
|
|
74
|
+
"per block size, method reciprocal "
|
|
75
|
+
"(default 5)")
|
|
76
|
+
parser.add_argument("-p", "--min_r2", type=float, default=0.8,
|
|
77
|
+
help="minimal pairwise r2 between the "
|
|
78
|
+
"candidate and a block member, method "
|
|
79
|
+
"pairwise (default 0.8)")
|
|
80
|
+
parser.add_argument("-m", "--method", choices=tuple(METHODS),
|
|
81
|
+
default="fast",
|
|
82
|
+
help="partition function: fast, slogdet, "
|
|
83
|
+
"condition, vif, reciprocal or pairwise "
|
|
84
|
+
"(default fast)")
|
|
85
|
+
parser.add_argument("--version", action="version",
|
|
86
|
+
version=f"ldpart {__version__}")
|
|
87
|
+
return parser.parse_args(argv)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main(argv=None) -> None:
|
|
91
|
+
args = parser_args(argv)
|
|
92
|
+
ts = time.time()
|
|
93
|
+
|
|
94
|
+
check_input_files([args.input])
|
|
95
|
+
check_output_dir(args.output)
|
|
96
|
+
|
|
97
|
+
print("Input:", args.input)
|
|
98
|
+
print("Output:", args.output)
|
|
99
|
+
print("Method:", args.method)
|
|
100
|
+
|
|
101
|
+
fun, arg_name, label = METHODS[args.method]
|
|
102
|
+
params = {arg_name: getattr(args, arg_name)}
|
|
103
|
+
print(label + ":", params[arg_name])
|
|
104
|
+
|
|
105
|
+
with h5py.File(args.input, "r") as f:
|
|
106
|
+
matrix = attach_matrix(f)
|
|
107
|
+
labels, nblocks = fun(matrix, **params)
|
|
108
|
+
|
|
109
|
+
np.savetxt(args.output, labels, delimiter=" ", fmt="%d")
|
|
110
|
+
print("Number of blocks:", nblocks)
|
|
111
|
+
show_time_elapsed(ts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
main()
|
ldpart/helpers.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assisting functions of the ldpart command line interface: the LD
|
|
3
|
+
matrix is read straight from its HDF5 dataset (no copy into memory),
|
|
4
|
+
and the small file and folder checks live here. Trimmed from the
|
|
5
|
+
helpers of the ld-matrix-partition-01 project, pandas free.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
import h5py
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def attach_matrix(file_obj: h5py.File) -> Union[h5py.Dataset,
|
|
17
|
+
h5py.Group]:
|
|
18
|
+
"""
|
|
19
|
+
The square matrix of the first object of an open HDF5 file: a
|
|
20
|
+
plain dataset, or a group of chunked pandas arrays
|
|
21
|
+
(block0_values).
|
|
22
|
+
"""
|
|
23
|
+
key = list(file_obj.keys())[0]
|
|
24
|
+
obj = file_obj[key]
|
|
25
|
+
if isinstance(obj, h5py.Dataset):
|
|
26
|
+
ds_obj = obj
|
|
27
|
+
elif isinstance(obj, h5py.Group):
|
|
28
|
+
ds_obj = obj["block0_values"]
|
|
29
|
+
else:
|
|
30
|
+
print("Unknown type of dataset")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
n, m = ds_obj.shape[0], ds_obj.shape[1]
|
|
34
|
+
if n != m:
|
|
35
|
+
print("Dataset is not a square matrix")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
print(f"Dataset shape: {n} x {m}")
|
|
38
|
+
return ds_obj
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_output_dir(output: str) -> None:
|
|
42
|
+
# Create the folder holding an output path when missing
|
|
43
|
+
dir_name = os.path.dirname(output)
|
|
44
|
+
if dir_name:
|
|
45
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_input_files(files: list) -> None:
|
|
49
|
+
for file in files:
|
|
50
|
+
if file and not os.path.isfile(file):
|
|
51
|
+
print(f"{file} doesn't exist")
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def show_time_elapsed(ts: float) -> None:
|
|
56
|
+
dur = time.strftime("%H:%M:%S", time.gmtime(time.time() - ts))
|
|
57
|
+
print("Time spent:", dur)
|
ldpart/partitioner.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Library of functions to group SNPs into blocks of correlated SNPs.
|
|
3
|
+
|
|
4
|
+
The input is a square symmetric LD matrix (r or r2 with 1 on the diagonal,
|
|
5
|
+
SNPs ordered as the rows/columns), either a numpy array or an h5py dataset.
|
|
6
|
+
Every grouping function returns (labels, nblocks), where labels is a per-SNP
|
|
7
|
+
list [index, block_label] with blocks numbered from 1. Each SNP gets exactly
|
|
8
|
+
one label, and blocks are contiguous ranges of indices. The two-column labels
|
|
9
|
+
can be saved and passed to 2hlist-02.py (det-01 project) to build *.hlist
|
|
10
|
+
files for haplotype inference and testing in PLINK.
|
|
11
|
+
|
|
12
|
+
Functions
|
|
13
|
+
---------
|
|
14
|
+
partition_by_determinant determinant criterion, incremental Schur
|
|
15
|
+
complement updates (fast)
|
|
16
|
+
partition_by_determinant_slogdet determinant criterion, full LAPACK slogdet
|
|
17
|
+
of the growing window at each step
|
|
18
|
+
(reference implementation)
|
|
19
|
+
partition_by_condition condition number criterion (squared
|
|
20
|
+
Belsley condition number), full LAPACK
|
|
21
|
+
eigvalsh of the growing window
|
|
22
|
+
partition_by_vif maximal variance inflation factor (VIF)
|
|
23
|
+
criterion, per-window eigendecomposition
|
|
24
|
+
partition_by_reciprocal_eigenvalues sum of reciprocal eigenvalues criterion
|
|
25
|
+
(= sum of VIF = trace of the inverse)
|
|
26
|
+
partition_by_pairwise pairwise r2 with the block (classic
|
|
27
|
+
|r| > 0.8 screen), incremental row maximum
|
|
28
|
+
labels_to_blocks convert labels into a list of blocks
|
|
29
|
+
|
|
30
|
+
Corrected semantics (see det-01/audit-partition-ld-03.md)
|
|
31
|
+
---------------------------------------------------------
|
|
32
|
+
Unlike det-01/partition-ld-03.py, the candidate SNP is always part of the
|
|
33
|
+
tested submatrix: the block starting at i1 is extended by SNP j only if
|
|
34
|
+
det(matrix[i1:j+1, i1:j+1]) > min_det with a positive sign. The first
|
|
35
|
+
candidate violating the condition seeds the next block, so every saved
|
|
36
|
+
block is certified: the determinant of its own submatrix exceeds min_det.
|
|
37
|
+
NaN values are treated as a failure to extend, never as an automatic pass.
|
|
38
|
+
|
|
39
|
+
All functions share the same greedy mechanism (grow the block, the first
|
|
40
|
+
failing candidate seeds the next one, NaN never extends a block) and the
|
|
41
|
+
same certified-block semantics; they differ in the tested criterion. Two
|
|
42
|
+
cut directions coexist: the determinant, condition number, VIF and
|
|
43
|
+
reciprocal eigenvalue criteria certify that a block is not TOO jointly
|
|
44
|
+
redundant (a block ends before becoming ill-conditioned), while the
|
|
45
|
+
pairwise criterion groups SNPs that ARE in LD (an uncorrelated SNP starts
|
|
46
|
+
a new block).
|
|
47
|
+
|
|
48
|
+
Command line wrapper: run-partition-01.py in this folder.
|
|
49
|
+
|
|
50
|
+
Part of the ld-matrix-partition-01 project.
|
|
51
|
+
Created: September 8, 2026
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extend_inverse(inv: np.ndarray, u: np.ndarray, s: float) -> np.ndarray:
|
|
58
|
+
"""
|
|
59
|
+
Inverse of the extended matrix [[M, c], [c.T, d]] given the inverse of M,
|
|
60
|
+
u = M^-1 c, and the Schur complement s = d - c.T u (Sherman-Morrison).
|
|
61
|
+
"""
|
|
62
|
+
k = u.shape[0]
|
|
63
|
+
w = u / s
|
|
64
|
+
new_inv = np.empty((k + 1, k + 1), dtype=np.float64)
|
|
65
|
+
new_inv[:k, :k] = inv + np.outer(w, u)
|
|
66
|
+
new_inv[:k, k] = -w
|
|
67
|
+
new_inv[k, :k] = -w
|
|
68
|
+
new_inv[k, k] = 1.0 / s
|
|
69
|
+
return new_inv
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_matrix(matrix) -> int:
|
|
73
|
+
# Get the size of a square 2D matrix
|
|
74
|
+
shape = matrix.shape
|
|
75
|
+
if len(shape) != 2 or shape[0] != shape[1]:
|
|
76
|
+
raise ValueError("Expected a square matrix")
|
|
77
|
+
return shape[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _seed(matrix, i: int):
|
|
81
|
+
# State of a block that consists of the single SNP i:
|
|
82
|
+
# sign and log|det| of its 1x1 submatrix, and its inverse
|
|
83
|
+
d = float(matrix[i, i])
|
|
84
|
+
if np.isnan(d) or d <= 0:
|
|
85
|
+
# Uncertifiable seed: the block stays a singleton
|
|
86
|
+
return 0, 0.0, np.ones((1, 1))
|
|
87
|
+
return 1, np.log(d), np.array([[1.0 / d]])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def partition_by_determinant(matrix, min_det: float = 0.001):
|
|
91
|
+
"""
|
|
92
|
+
Group SNPs into blocks by the determinant of the LD submatrix (fast).
|
|
93
|
+
|
|
94
|
+
The block starting at SNP i1 is extended by the candidate SNP j while
|
|
95
|
+
det(matrix[i1:j+1, i1:j+1]) > min_det holds with a positive sign. The
|
|
96
|
+
first candidate violating the condition seeds the next block. Since the
|
|
97
|
+
determinant of a correlation matrix can only decrease when a SNP is
|
|
98
|
+
added (each step multiplies it by 1 - R^2 <= 1), the greedy growth finds
|
|
99
|
+
the maximal block for the criterion.
|
|
100
|
+
|
|
101
|
+
The determinant is carried along incrementally with the Schur complement
|
|
102
|
+
det(M') = det(M) * (d - c.T M^-1 c), M' = [[M, c], [c.T, d]],
|
|
103
|
+
which needs one row of the matrix and O(k^2) work per step, instead of
|
|
104
|
+
a full O(k^3) factorization of the growing window (O(k^4) per block in
|
|
105
|
+
total). The explicit inverse is updated with the Sherman-Morrison
|
|
106
|
+
formula. On ill-conditioned blocks the accumulated value can differ
|
|
107
|
+
from the LAPACK determinant in the last bits; use
|
|
108
|
+
partition_by_determinant_slogdet() for the reference behaviour.
|
|
109
|
+
|
|
110
|
+
A NaN anywhere in the candidate's row (or a NaN, zero, or negative
|
|
111
|
+
Schur complement) fails the extension: the candidate starts a new
|
|
112
|
+
block. NaN never silently extends a block.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
matrix : h5py.Dataset or numpy.ndarray
|
|
117
|
+
Square symmetric LD matrix (r or r2) with 1 on the diagonal.
|
|
118
|
+
min_det : float
|
|
119
|
+
Minimal determinant of a block submatrix, within (0, 1).
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
list
|
|
124
|
+
Labels [index, block] of the matrix elements
|
|
125
|
+
int
|
|
126
|
+
Number of blocks
|
|
127
|
+
"""
|
|
128
|
+
n = _check_matrix(matrix)
|
|
129
|
+
if not 0.0 < min_det < 1.0:
|
|
130
|
+
raise ValueError(f"min_det must be within (0, 1), got {min_det}")
|
|
131
|
+
if n == 0:
|
|
132
|
+
return [], 0
|
|
133
|
+
|
|
134
|
+
log_min = np.log(min_det)
|
|
135
|
+
|
|
136
|
+
# Initiate the first block
|
|
137
|
+
b = 1
|
|
138
|
+
labels = [[0, b]]
|
|
139
|
+
i1 = 0
|
|
140
|
+
sign, logdet, inv = _seed(matrix, 0)
|
|
141
|
+
|
|
142
|
+
# The symmetry of the matrix is used to read the candidate's row
|
|
143
|
+
# (contiguous in memory) instead of its column
|
|
144
|
+
for j in range(1, n):
|
|
145
|
+
extend = False
|
|
146
|
+
if sign > 0:
|
|
147
|
+
row = np.asarray(matrix[j, i1:j + 1], dtype=np.float64)
|
|
148
|
+
if not np.isnan(row).any():
|
|
149
|
+
c, d = row[:-1], row[-1]
|
|
150
|
+
u = inv.dot(c)
|
|
151
|
+
s = d - c.dot(u)
|
|
152
|
+
if not np.isnan(s) and s > 0:
|
|
153
|
+
new_logdet = logdet + np.log(s)
|
|
154
|
+
if new_logdet > log_min:
|
|
155
|
+
extend = True
|
|
156
|
+
if extend:
|
|
157
|
+
# The candidate joins the current block
|
|
158
|
+
labels.append([j, b])
|
|
159
|
+
logdet = new_logdet
|
|
160
|
+
inv = _extend_inverse(inv, u, s)
|
|
161
|
+
else:
|
|
162
|
+
# The candidate fails: it seeds the next block
|
|
163
|
+
b += 1
|
|
164
|
+
labels.append([j, b])
|
|
165
|
+
i1 = j
|
|
166
|
+
sign, logdet, inv = _seed(matrix, j)
|
|
167
|
+
|
|
168
|
+
return labels, b
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def partition_by_determinant_slogdet(matrix, min_det: float = 0.001):
|
|
172
|
+
"""
|
|
173
|
+
Group SNPs into blocks by the determinant of the LD submatrix
|
|
174
|
+
(reference implementation).
|
|
175
|
+
|
|
176
|
+
Same criterion and semantics as partition_by_determinant(), but the
|
|
177
|
+
determinant of the growing window matrix[i1:j+1, i1:j+1] is recomputed
|
|
178
|
+
with np.linalg.slogdet at every step, which costs O(k^4) flops per
|
|
179
|
+
block. Kept to define the semantics and to verify the fast version.
|
|
180
|
+
Note that LAPACK can return a finite determinant for a window that
|
|
181
|
+
contains a NaN (the entry may stay off the pivoting path), so on a
|
|
182
|
+
matrix with NaNs the fast version, which checks the row explicitly,
|
|
183
|
+
is the authoritative one.
|
|
184
|
+
"""
|
|
185
|
+
n = _check_matrix(matrix)
|
|
186
|
+
if not 0.0 < min_det < 1.0:
|
|
187
|
+
raise ValueError(f"min_det must be within (0, 1), got {min_det}")
|
|
188
|
+
if n == 0:
|
|
189
|
+
return [], 0
|
|
190
|
+
|
|
191
|
+
log_min = np.log(min_det)
|
|
192
|
+
|
|
193
|
+
# Initiate the first block
|
|
194
|
+
b = 1
|
|
195
|
+
labels = [[0, b]]
|
|
196
|
+
i1 = 0
|
|
197
|
+
|
|
198
|
+
for j in range(1, n):
|
|
199
|
+
submatrix = np.asarray(matrix[i1:j + 1, i1:j + 1], dtype=np.float64)
|
|
200
|
+
sign, logdet = np.linalg.slogdet(submatrix)
|
|
201
|
+
if (not np.isfinite(logdet)) or sign <= 0 or logdet <= log_min:
|
|
202
|
+
# The candidate fails: it seeds the next block
|
|
203
|
+
b += 1
|
|
204
|
+
labels.append([j, b])
|
|
205
|
+
i1 = j
|
|
206
|
+
else:
|
|
207
|
+
labels.append([j, b])
|
|
208
|
+
|
|
209
|
+
return labels, b
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def partition_by_condition(matrix, max_cond: float = 1000.0):
|
|
213
|
+
"""
|
|
214
|
+
Group SNPs into blocks by the condition number of the LD submatrix.
|
|
215
|
+
|
|
216
|
+
The block starting at SNP i1 is extended by the candidate SNP j while
|
|
217
|
+
cond(matrix[i1:j+1, i1:j+1]) <= max_cond, where cond is the spectral
|
|
218
|
+
condition number lambda_max/|lambda_min| from np.linalg.eigvalsh. The
|
|
219
|
+
first candidate violating the condition seeds the next block, so every
|
|
220
|
+
saved block is certified: the condition number of its submatrix, and of
|
|
221
|
+
every prefix of it, does not exceed max_cond.
|
|
222
|
+
|
|
223
|
+
By the Cauchy interlacing theorem, appending a row and a column to a
|
|
224
|
+
symmetric matrix can only increase lambda_max and decrease lambda_min,
|
|
225
|
+
so the condition number of the growing window cannot recover after a
|
|
226
|
+
failure: the greedy growth finds the maximal block for the criterion.
|
|
227
|
+
A window that stops being positive definite (lambda_min <= 0) has an
|
|
228
|
+
infinite condition number and always fails, which turns numerical
|
|
229
|
+
singularity into an explicit cut.
|
|
230
|
+
|
|
231
|
+
Unlike the determinant, the condition number never underflows: it stays
|
|
232
|
+
meaningful far beyond the sizes where a determinant leaves the float64
|
|
233
|
+
range. Note the convention: the condition indices of Belsley et al. are
|
|
234
|
+
CI_q = sqrt(lambda_max/lambda_q) with CN = max_q CI_q = sqrt(kappa),
|
|
235
|
+
so this function's criterion kappa <= max_cond with the default 1000
|
|
236
|
+
corresponds to the classical CN = sqrt(1000) ~ 31.6, just above the
|
|
237
|
+
severe-multicollinearity level of 30.
|
|
238
|
+
|
|
239
|
+
A NaN anywhere in the candidate window fails the extension, never an
|
|
240
|
+
automatic pass. The eigenvalues are recomputed from the window with
|
|
241
|
+
LAPACK at every step (O(k^4) flops per block), mirroring
|
|
242
|
+
partition_by_determinant_slogdet().
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
matrix : h5py.Dataset or numpy.ndarray
|
|
247
|
+
Square symmetric LD matrix (r or r2) with 1 on the diagonal.
|
|
248
|
+
max_cond : float
|
|
249
|
+
Maximal condition number of a block submatrix, >= 1.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
list
|
|
254
|
+
Labels [index, block] of the matrix elements
|
|
255
|
+
int
|
|
256
|
+
Number of blocks
|
|
257
|
+
"""
|
|
258
|
+
n = _check_matrix(matrix)
|
|
259
|
+
if max_cond < 1.0:
|
|
260
|
+
raise ValueError(f"max_cond must be >= 1, got {max_cond}")
|
|
261
|
+
if n == 0:
|
|
262
|
+
return [], 0
|
|
263
|
+
|
|
264
|
+
# Initiate the first block
|
|
265
|
+
b = 1
|
|
266
|
+
labels = [[0, b]]
|
|
267
|
+
i1 = 0
|
|
268
|
+
|
|
269
|
+
for j in range(1, n):
|
|
270
|
+
submatrix = np.asarray(matrix[i1:j + 1, i1:j + 1], dtype=np.float64)
|
|
271
|
+
if np.isnan(submatrix).any():
|
|
272
|
+
extend = False
|
|
273
|
+
else:
|
|
274
|
+
eig = np.linalg.eigvalsh(submatrix)
|
|
275
|
+
lmin = abs(eig[0])
|
|
276
|
+
cond = eig[-1] / lmin if lmin > 0 else np.inf
|
|
277
|
+
extend = cond <= max_cond
|
|
278
|
+
if extend:
|
|
279
|
+
# The candidate joins the current block
|
|
280
|
+
labels.append([j, b])
|
|
281
|
+
else:
|
|
282
|
+
# The candidate fails: it seeds the next block
|
|
283
|
+
b += 1
|
|
284
|
+
labels.append([j, b])
|
|
285
|
+
i1 = j
|
|
286
|
+
|
|
287
|
+
return labels, b
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def partition_by_vif(matrix, max_vif: float = 10.0):
|
|
291
|
+
"""
|
|
292
|
+
Group SNPs into blocks by the maximal variance inflation factor (VIF)
|
|
293
|
+
of the block submatrix.
|
|
294
|
+
|
|
295
|
+
VIF_i = (R^-1)_ii = 1 / (1 - R2_i), where R2_i is the multiple
|
|
296
|
+
correlation of SNP i with the other SNPs of the block. The block
|
|
297
|
+
starting at SNP i1 is extended by the candidate SNP j while
|
|
298
|
+
max_i VIF_i <= max_vif over matrix[i1:j+1, i1:j+1]; the first
|
|
299
|
+
candidate violating the condition seeds the next block, so every saved
|
|
300
|
+
block is certified: the maximal VIF of its own submatrix (and of every
|
|
301
|
+
prefix of it) does not exceed max_vif. The equivalent tolerance
|
|
302
|
+
criterion 1/VIF >= 1/max_vif is the same test with an inverted
|
|
303
|
+
threshold. Classical reference levels: VIF 5 moderate, 10 severe
|
|
304
|
+
multicollinearity (tolerance 0.2 / 0.1).
|
|
305
|
+
|
|
306
|
+
Monotonicity: for a positive-definite extension M' = [[M, c], [c.T, d]]
|
|
307
|
+
with u = M^-1 c and Schur complement s = d - c.T u > 0, the updated
|
|
308
|
+
inverse is M'^-1 = [[M^-1 + uu.T/s, -u/s], [-u.T/s, 1/s]], so every
|
|
309
|
+
diagonal entry can only grow ((M'^-1)_ii = (M^-1)_ii + u_i^2/s for the
|
|
310
|
+
old indices, 1/s for the new one): the maximal VIF cannot recover
|
|
311
|
+
after a failure, and the greedy growth finds the maximal block.
|
|
312
|
+
|
|
313
|
+
The VIFs are read from the eigendecomposition R = Q diag(lambda) Q.T:
|
|
314
|
+
(R^-1)_ii = sum_q Q[i,q]^2 / lambda_q, computed fresh for every window
|
|
315
|
+
(O(k^4) flops per block), which also provides the positive-definite
|
|
316
|
+
check: a window with lambda_min <= 0 always fails. A NaN anywhere in
|
|
317
|
+
the window fails the extension, never an automatic pass.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
matrix : h5py.Dataset or numpy.ndarray
|
|
322
|
+
Square symmetric LD matrix (r or r2) with 1 on the diagonal.
|
|
323
|
+
max_vif : float
|
|
324
|
+
Maximal variance inflation factor of a block submatrix, >= 1.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
list
|
|
329
|
+
Labels [index, block] of the matrix elements
|
|
330
|
+
int
|
|
331
|
+
Number of blocks
|
|
332
|
+
"""
|
|
333
|
+
n = _check_matrix(matrix)
|
|
334
|
+
if max_vif < 1.0:
|
|
335
|
+
raise ValueError(f"max_vif must be >= 1, got {max_vif}")
|
|
336
|
+
if n == 0:
|
|
337
|
+
return [], 0
|
|
338
|
+
|
|
339
|
+
# Initiate the first block
|
|
340
|
+
b = 1
|
|
341
|
+
labels = [[0, b]]
|
|
342
|
+
i1 = 0
|
|
343
|
+
|
|
344
|
+
for j in range(1, n):
|
|
345
|
+
submatrix = np.asarray(matrix[i1:j + 1, i1:j + 1], dtype=np.float64)
|
|
346
|
+
if np.isnan(submatrix).any():
|
|
347
|
+
extend = False
|
|
348
|
+
else:
|
|
349
|
+
lam, q = np.linalg.eigh(submatrix)
|
|
350
|
+
if lam[0] > 0:
|
|
351
|
+
vif = (q * q / lam).sum(axis=1).max()
|
|
352
|
+
extend = vif <= max_vif
|
|
353
|
+
else:
|
|
354
|
+
extend = False
|
|
355
|
+
if extend:
|
|
356
|
+
# The candidate joins the current block
|
|
357
|
+
labels.append([j, b])
|
|
358
|
+
else:
|
|
359
|
+
# The candidate fails: it seeds the next block
|
|
360
|
+
b += 1
|
|
361
|
+
labels.append([j, b])
|
|
362
|
+
i1 = j
|
|
363
|
+
|
|
364
|
+
return labels, b
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def partition_by_reciprocal_eigenvalues(matrix, max_ratio: float = 5.0):
|
|
368
|
+
"""
|
|
369
|
+
Group SNPs into blocks by the sum of reciprocal eigenvalues of the
|
|
370
|
+
block submatrix (Kendall 1957; Silvey 1969).
|
|
371
|
+
|
|
372
|
+
The block starting at SNP i1 is extended by the candidate SNP j while
|
|
373
|
+
sum_q 1/lambda_q <= max_ratio * k over the k x k window
|
|
374
|
+
matrix[i1:j+1, i1:j+1]: the classical rule of Chatterjee & Price
|
|
375
|
+
(1977) and Dillon & Goldstein (1984) flags collinearity when the sum
|
|
376
|
+
of reciprocal eigenvalues is about five times the number of variables.
|
|
377
|
+
Since trace(R^-1) = sum_q 1/lambda_q = sum_i VIF_i, this is the
|
|
378
|
+
aggregate version of the VIF criterion, and every saved block is
|
|
379
|
+
certified: the sum does not exceed max_ratio * size at its final size
|
|
380
|
+
nor at any prefix.
|
|
381
|
+
|
|
382
|
+
Unlike the determinant, condition number and VIF criteria, the ratio
|
|
383
|
+
to the window size can recover after a failure (an almost uncorrelated
|
|
384
|
+
candidate raises k by 1 but the sum by barely more), so the greedy cut
|
|
385
|
+
is deterministic but the blocks are not certified maximal. A window
|
|
386
|
+
that stops being positive definite (lambda_min <= 0) always fails, a
|
|
387
|
+
NaN anywhere in the window fails the extension.
|
|
388
|
+
|
|
389
|
+
The eigenvalues are recomputed from the window with LAPACK at every
|
|
390
|
+
step (O(k^4) flops per block).
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
matrix : h5py.Dataset or numpy.ndarray
|
|
395
|
+
Square symmetric LD matrix (r or r2) with 1 on the diagonal.
|
|
396
|
+
max_ratio : float
|
|
397
|
+
Maximal sum of reciprocal eigenvalues per window size, >= 1.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
list
|
|
402
|
+
Labels [index, block] of the matrix elements
|
|
403
|
+
int
|
|
404
|
+
Number of blocks
|
|
405
|
+
"""
|
|
406
|
+
n = _check_matrix(matrix)
|
|
407
|
+
if max_ratio < 1.0:
|
|
408
|
+
raise ValueError(f"max_ratio must be >= 1, got {max_ratio}")
|
|
409
|
+
if n == 0:
|
|
410
|
+
return [], 0
|
|
411
|
+
|
|
412
|
+
# Initiate the first block
|
|
413
|
+
b = 1
|
|
414
|
+
labels = [[0, b]]
|
|
415
|
+
i1 = 0
|
|
416
|
+
|
|
417
|
+
for j in range(1, n):
|
|
418
|
+
submatrix = np.asarray(matrix[i1:j + 1, i1:j + 1], dtype=np.float64)
|
|
419
|
+
if np.isnan(submatrix).any():
|
|
420
|
+
extend = False
|
|
421
|
+
else:
|
|
422
|
+
lam = np.linalg.eigvalsh(submatrix)
|
|
423
|
+
if lam[0] > 0:
|
|
424
|
+
extend = (1.0 / lam).sum() <= max_ratio * lam.size
|
|
425
|
+
else:
|
|
426
|
+
extend = False
|
|
427
|
+
if extend:
|
|
428
|
+
# The candidate joins the current block
|
|
429
|
+
labels.append([j, b])
|
|
430
|
+
else:
|
|
431
|
+
# The candidate fails: it seeds the next block
|
|
432
|
+
b += 1
|
|
433
|
+
labels.append([j, b])
|
|
434
|
+
i1 = j
|
|
435
|
+
|
|
436
|
+
return labels, b
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def partition_by_pairwise(matrix, min_r2: float = 0.8):
|
|
440
|
+
"""
|
|
441
|
+
Group SNPs into blocks by pairwise LD with the block: the classic
|
|
442
|
+
pairwise correlation screen (absolute r above ~0.8, e.g. Gujarati &
|
|
443
|
+
Porter), applied to an r2 matrix.
|
|
444
|
+
|
|
445
|
+
The block starting at SNP i1 is extended by the candidate SNP j while
|
|
446
|
+
max over the block members of r2(candidate, member) >= min_r2. This
|
|
447
|
+
inverts the cut direction of the determinant, condition number, VIF
|
|
448
|
+
and reciprocal eigenvalue criteria: a SNP in LD with the block JOINS
|
|
449
|
+
it, and an uncorrelated SNP starts a new block. Every saved block is
|
|
450
|
+
certified in the pairwise sense: each member after the first has
|
|
451
|
+
r2 >= min_r2 with an earlier member of the same block. Only pairwise
|
|
452
|
+
(marginal) LD is tested, not the joint redundancy of the block.
|
|
453
|
+
|
|
454
|
+
The candidate needs one row segment and O(k) work per step, so this is
|
|
455
|
+
the cheapest criterion (comparable to PLINK-style LD pruning). A NaN
|
|
456
|
+
in the candidate's row segment fails the extension.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
matrix : h5py.Dataset or numpy.ndarray
|
|
461
|
+
Square symmetric LD matrix (r2, with 1 on the diagonal); for an r
|
|
462
|
+
matrix pass min_r2 as a threshold on r2 = value^2.
|
|
463
|
+
min_r2 : float
|
|
464
|
+
Minimal pairwise r2 between the candidate and a block member,
|
|
465
|
+
within (0, 1].
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
list
|
|
470
|
+
Labels [index, block] of the matrix elements
|
|
471
|
+
int
|
|
472
|
+
Number of blocks
|
|
473
|
+
"""
|
|
474
|
+
n = _check_matrix(matrix)
|
|
475
|
+
if not 0.0 < min_r2 <= 1.0:
|
|
476
|
+
raise ValueError(f"min_r2 must be within (0, 1], got {min_r2}")
|
|
477
|
+
if n == 0:
|
|
478
|
+
return [], 0
|
|
479
|
+
|
|
480
|
+
# Initiate the first block
|
|
481
|
+
b = 1
|
|
482
|
+
labels = [[0, b]]
|
|
483
|
+
i1 = 0
|
|
484
|
+
|
|
485
|
+
for j in range(1, n):
|
|
486
|
+
# The symmetry of the matrix is used to read the candidate's row
|
|
487
|
+
# (contiguous in memory) instead of its column
|
|
488
|
+
segment = np.asarray(matrix[j, i1:j], dtype=np.float64)
|
|
489
|
+
if segment.size and not np.isnan(segment).any() and segment.max() >= min_r2:
|
|
490
|
+
# The candidate joins the current block
|
|
491
|
+
labels.append([j, b])
|
|
492
|
+
else:
|
|
493
|
+
# The candidate fails: it seeds the next block
|
|
494
|
+
b += 1
|
|
495
|
+
labels.append([j, b])
|
|
496
|
+
i1 = j
|
|
497
|
+
|
|
498
|
+
return labels, b
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def labels_to_blocks(labels: list) -> list:
|
|
502
|
+
"""
|
|
503
|
+
Convert labels [index, block] into a list of (block, indices) tuples
|
|
504
|
+
ordered by the block number.
|
|
505
|
+
"""
|
|
506
|
+
blocks = {}
|
|
507
|
+
for i, b in labels:
|
|
508
|
+
blocks.setdefault(b, []).append(i)
|
|
509
|
+
return [(b, idx) for b, idx in sorted(blocks.items())]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ldpart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Certified multicollinearity partitioning of LD matrices into blocks of correlated SNPs
|
|
5
|
+
Author-email: Gennady Khvorykh <info@inzilico.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gkhvorykh/ldpart
|
|
8
|
+
Project-URL: Documentation, https://github.com/gkhvorykh/ldpart#readme
|
|
9
|
+
Keywords: linkage disequilibrium,LD blocks,multicollinearity,variance inflation factor,population genetics,genetics
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.20
|
|
23
|
+
Requires-Dist: h5py>=3.0
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Linkage Disequilibrium Partition (ldpart)
|
|
29
|
+
|
|
30
|
+
Certified multicollinearity partitioning of linkage disequilibrium (LD) matrices: group SNPs
|
|
31
|
+
into contiguous blocks of correlated SNPs with greedy criteria built
|
|
32
|
+
on classical multicollinearity diagnostics.
|
|
33
|
+
|
|
34
|
+
Every saved block is **certified** — its submatrix of the LD matrix
|
|
35
|
+
provably satisfies the criterion — and the monotone criteria
|
|
36
|
+
(determinant, condition number, maximal VIF) additionally certify that
|
|
37
|
+
every block is **maximal**: no neighbouring SNP could join it without
|
|
38
|
+
violating the criterion. NaN values never extend a block.
|
|
39
|
+
|
|
40
|
+
## Criteria
|
|
41
|
+
|
|
42
|
+
| function | method | criterion | certified | maximal | cost per candidate |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| `partition_by_determinant` | `fast` | det(block) > θ (Schur updates) | yes | yes (det is monotone under growth) | O(k²) |
|
|
45
|
+
| `partition_by_determinant_slogdet` | `slogdet` | det(block) > θ (LAPACK reference) | yes | yes | O(k³) |
|
|
46
|
+
| `partition_by_condition` | `condition` | κ(block) = λmax/\|λmin\| ≤ θ | yes | yes (Cauchy interlacing) | O(k³) |
|
|
47
|
+
| `partition_by_vif` | `vif` | max VIF = max diag(R⁻¹) ≤ θ | yes | yes (nested regressions) | O(k³) |
|
|
48
|
+
| `partition_by_reciprocal_eigenvalues` | `reciprocal` | Σ 1/λq ≤ θ·k | yes | no (the ratio can recover) | O(k³) |
|
|
49
|
+
| `partition_by_pairwise` | `pairwise` | max r2(candidate, member) ≥ θ | yes (pairwise sense) | — marginal screen | O(k) |
|
|
50
|
+
|
|
51
|
+
Classical operating points: det θ = 0.001, κ = 1000 (squared Belsley
|
|
52
|
+
CN ≈ 31.6), VIF 5/10, Σ1/λ ≈ 5·k (Chatterjee & Price), pairwise
|
|
53
|
+
r² = 0.8.
|
|
54
|
+
|
|
55
|
+
The determinant, condition, VIF and reciprocal criteria certify that
|
|
56
|
+
a block is **not too jointly redundant** (joint criteria); the
|
|
57
|
+
pairwise criterion groups SNPs that **are** in LD (marginal screen).
|
|
58
|
+
Their cuts land in different places — the joint criteria cut inside
|
|
59
|
+
strong LD where redundancy accumulates, the pairwise criterion cuts
|
|
60
|
+
where LD has decayed.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ldpart
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Python API
|
|
69
|
+
|
|
70
|
+
The input is a square symmetric LD matrix (r or r2, 1 on the
|
|
71
|
+
diagonal, SNPs ordered as the rows and columns) — a numpy array or an
|
|
72
|
+
open h5py dataset (the matrix is streamed row by row, never copied).
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import h5py
|
|
76
|
+
from ldpart import partition_by_determinant, labels_to_blocks
|
|
77
|
+
|
|
78
|
+
with h5py.File("ld.h5", "r") as f:
|
|
79
|
+
matrix = f["r2"]
|
|
80
|
+
labels, nblocks = partition_by_determinant(matrix, min_det=0.001)
|
|
81
|
+
|
|
82
|
+
blocks = labels_to_blocks(labels) # [(block, [snp indexes]), ...]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Every function returns `(labels, nblocks)`, where labels is a per-SNP
|
|
86
|
+
list of `[index, block]` with blocks numbered from 1.
|
|
87
|
+
|
|
88
|
+
## Command line
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ldpart -i ld.h5 -o labels.txt # det 0.001
|
|
92
|
+
ldpart -i ld.h5 -o labels.txt -m condition -c 1000
|
|
93
|
+
ldpart -i ld.h5 -o labels.txt -m vif -v 10
|
|
94
|
+
ldpart -i ld.h5 -o labels.txt -m reciprocal -r 5
|
|
95
|
+
ldpart -i ld.h5 -o labels.txt -m pairwise -p 0.8
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The output has two columns, the zero-based SNP index and the block
|
|
99
|
+
label.
|
|
100
|
+
|
|
101
|
+
## Semantics
|
|
102
|
+
|
|
103
|
+
- **Greedy growth:** the block starting at SNP i1 is extended by the
|
|
104
|
+
candidate SNP j only while the criterion holds over the window
|
|
105
|
+
matrix[i1:j+1, i1:j+1] *including the candidate*; the first failing
|
|
106
|
+
candidate seeds the next block, so blocks are contiguous and every
|
|
107
|
+
block satisfies its criterion at its final size and at every prefix.
|
|
108
|
+
- **Certified maximality:** the determinant can only decrease when a
|
|
109
|
+
SNP is added (Schur complement: each step multiplies it by
|
|
110
|
+
1 − R² ≤ 1), the condition number cannot recover (Cauchy
|
|
111
|
+
interlacing), the maximal VIF cannot recover (nested regressions) —
|
|
112
|
+
so the greedy cut yields the largest admissible block. The sum of
|
|
113
|
+
reciprocal eigenvalues per window size can recover, so its blocks
|
|
114
|
+
are certified but not maximal.
|
|
115
|
+
- **NaN semantics:** a NaN anywhere in the candidate window (or the
|
|
116
|
+
candidate's row segment for the pairwise screen) fails the
|
|
117
|
+
extension — NaN never silently extends a block. A SNP with a NaN
|
|
118
|
+
diagonal stays a singleton seed.
|
|
119
|
+
|
|
120
|
+
## Testing
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[test]"
|
|
124
|
+
pytest tests
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The suite holds property tests on random correlation matrices:
|
|
128
|
+
certification of every block by independent LAPACK recomputation,
|
|
129
|
+
maximality of the monotone criteria against brute force, the NaN
|
|
130
|
+
semantics, fast/slogdet agreement, numpy/h5py equivalence, and the
|
|
131
|
+
command line interface end to end.
|
|
132
|
+
|
|
133
|
+
## References
|
|
134
|
+
|
|
135
|
+
- Rogers & Huff (2008), *Heredity* — the r² estimator used in the
|
|
136
|
+
companion study.
|
|
137
|
+
- Belsley, Kuh & Welsch (1980) — regression diagnostics, condition
|
|
138
|
+
indices.
|
|
139
|
+
- Chatterjee & Price (1977); Dillon & Goldstein (1984) — the
|
|
140
|
+
sum-of-reciprocal-eigenvalues rule.
|
|
141
|
+
- Khvorykh, G., Khrunin, A., 2025. Determinant-based grouping of SNPs and its application for detecting disease-associated genomic loci. NAR Genomics and Bioinformatics 7. https://doi.org/10.1093/nargab/lqaf024
|
|
142
|
+
- The method is developed and validated in the LD matrix partition study (in preparation)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT © Gennady Khvorykh
|
|
148
|
+
|
|
149
|
+
## Author
|
|
150
|
+
|
|
151
|
+
Gennady Khvorykh, info [at] inzilico.com
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ldpart/__init__.py,sha256=0cYMpErfW5rhp4RQIMkEXL2Ga3-mmdE9PCcA_mlj2qY,1173
|
|
2
|
+
ldpart/cli.py,sha256=2AikozwXofKqa5z27x9e_wHW4NTekxsVqBhjJ6Yo9u4,4663
|
|
3
|
+
ldpart/helpers.py,sha256=JN2N1cg0_Judf50s2iUXBvfJssdr8nPC6JNoLxapCqQ,1601
|
|
4
|
+
ldpart/partitioner.py,sha256=0OSkPggAz6M9WS6okW-hef169E5hKGCVX_R8Nf_mElg,19345
|
|
5
|
+
ldpart-0.1.0.dist-info/licenses/LICENSE,sha256=AT8YUEsBDEKDq18sv0PVhMSX5DTd2qsDXVcAvqSViIQ,1073
|
|
6
|
+
ldpart-0.1.0.dist-info/METADATA,sha256=fB6xDP38wQihb6WHJ7A1N9NSQZZQBb3wAolhPBT9yAE,6169
|
|
7
|
+
ldpart-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
ldpart-0.1.0.dist-info/entry_points.txt,sha256=uwyEC-GYQIf4LaUkbEuWQafQVWjWaD2Gb5Sx4jhKgzs,43
|
|
9
|
+
ldpart-0.1.0.dist-info/top_level.txt,sha256=lQ0qqB2fAqWVrCR3-MVHn17FH0uUv6gOE-5pTM43MNo,7
|
|
10
|
+
ldpart-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gennady Khvorykh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ldpart
|