gpse 0.0.2__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.
- gpse/__init__.py +11 -0
- gpse/batch/__init__.py +3 -0
- gpse/batch/cli.py +72 -0
- gpse/batch/runner.py +206 -0
- gpse/cli.py +215 -0
- gpse/config/__init__.py +15 -0
- gpse/config/_topsis_config.py +205 -0
- gpse/config/constants.py +127 -0
- gpse/config/default.yaml +15 -0
- gpse/config/software.yaml +20 -0
- gpse/config/topsis.yaml +18 -0
- gpse/convert/__init__.py +11 -0
- gpse/convert/cli.py +65 -0
- gpse/convert/external.py +400 -0
- gpse/convert/genotype_matrix.py +349 -0
- gpse/convert/phenotype.py +409 -0
- gpse/convert/processor.py +815 -0
- gpse/convert/qc.py +424 -0
- gpse/convert/validators.py +181 -0
- gpse/convert/workflow.py +214 -0
- gpse/models/__init__.py +30 -0
- gpse/models/classification_model_optimizer.py +393 -0
- gpse/models/classification_models.py +8 -0
- gpse/models/model_optimizers.py +14 -0
- gpse/models/regression_model_optimizer.py +679 -0
- gpse/predict/__init__.py +5 -0
- gpse/predict/__main__.py +3 -0
- gpse/predict/cli.py +95 -0
- gpse/predict/core.py +315 -0
- gpse/tasks/__init__.py +12 -0
- gpse/tasks/classification.py +565 -0
- gpse/tools/__init__.py +1 -0
- gpse/tools/analyze_phenotypes.py +140 -0
- gpse/train/__init__.py +26 -0
- gpse/train/_cv_manager.py +234 -0
- gpse/train/_data_io.py +261 -0
- gpse/train/_ensemble.py +235 -0
- gpse/train/_feature_selection.py +287 -0
- gpse/train/_fold_training.py +304 -0
- gpse/train/_model_pipeline.py +125 -0
- gpse/train/_model_tools.py +97 -0
- gpse/train/_optimization.py +244 -0
- gpse/train/_pipeline.py +490 -0
- gpse/train/_repeat_training.py +665 -0
- gpse/train/_results.py +262 -0
- gpse/train/cli.py +408 -0
- gpse/train/predictor.py +305 -0
- gpse/train/stacking.py +230 -0
- gpse/train/topsis.py +214 -0
- gpse/train/workflow.py +416 -0
- gpse/utils/__init__.py +54 -0
- gpse/utils/cli_display.py +529 -0
- gpse/utils/configuration.py +192 -0
- gpse/utils/dependency_checker.py +280 -0
- gpse/utils/feature_manifest.py +63 -0
- gpse/utils/genomic_utils.py +812 -0
- gpse/utils/log_utils.py +569 -0
- gpse/utils/logo.py +235 -0
- gpse/utils/paralle.py +266 -0
- gpse/utils/print_utils.py +75 -0
- gpse/utils/snp_ids.py +61 -0
- gpse/utils/version.py +159 -0
- gpse-0.0.2.dist-info/METADATA +1123 -0
- gpse-0.0.2.dist-info/RECORD +67 -0
- gpse-0.0.2.dist-info/WHEEL +4 -0
- gpse-0.0.2.dist-info/entry_points.txt +3 -0
- gpse-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Genotype format conversion and matrix construction utilities.
|
|
5
|
+
|
|
6
|
+
Pure functions for converting between VCF, PLINK binary, PED/MAP, and
|
|
7
|
+
numeric CSV matrix formats. No class state required — the caller
|
|
8
|
+
(typically ``GenomicDataProcessor``) passes in the logger and tool paths.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import glob
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from gpse.convert.external import resolve_configured_tool, run_command, ensure_log_dir
|
|
16
|
+
from gpse.utils.feature_manifest import write_feature_manifest
|
|
17
|
+
from gpse.utils.snp_ids import canonical_ids_from_map_file
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from gpse.utils.log_utils import logger as _default_logger
|
|
21
|
+
except Exception: # pragma: no cover
|
|
22
|
+
import logging
|
|
23
|
+
_default_logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Additive genotype encoding map
|
|
26
|
+
GENO_DICT = {
|
|
27
|
+
'00': '0', # Homozygous reference
|
|
28
|
+
'01': '1', # Heterozygous
|
|
29
|
+
'10': '1', # Heterozygous
|
|
30
|
+
'11': '2', # Homozygous alternate
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Centered additive encoding, Azodi et al. 2019 style: [-1, 0, 1] = [aa, Aa, AA]
|
|
34
|
+
GENO_DICT_CENTERED = {
|
|
35
|
+
'00': '-1', # Homozygous reference (aa)
|
|
36
|
+
'01': '0', # Heterozygous (Aa)
|
|
37
|
+
'10': '0', # Heterozygous (Aa)
|
|
38
|
+
'11': '1', # Homozygous alternate (AA)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Supported genotype encodings for convert_to_matrix().
|
|
42
|
+
GENO_ENCODINGS = {
|
|
43
|
+
'012': GENO_DICT,
|
|
44
|
+
'-101': GENO_DICT_CENTERED,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_plink(plink_path, config_path=None, auto_project_config=False):
|
|
49
|
+
"""Resolve the PLINK executable from config or command override."""
|
|
50
|
+
return resolve_configured_tool(
|
|
51
|
+
"plink",
|
|
52
|
+
command_override=plink_path,
|
|
53
|
+
config_path=config_path,
|
|
54
|
+
auto_project_config=auto_project_config,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _timestamp_plink_log(out_prefix, logger=None):
|
|
59
|
+
"""Move PLINK's auto-generated ``<prefix>.log`` into the ``log/`` sub-directory.
|
|
60
|
+
|
|
61
|
+
If a log with the same destination name already exists, a timestamp is
|
|
62
|
+
appended to avoid collisions.
|
|
63
|
+
"""
|
|
64
|
+
log_path = f"{out_prefix}.log"
|
|
65
|
+
if not os.path.exists(log_path):
|
|
66
|
+
return
|
|
67
|
+
log_dir = ensure_log_dir(out_prefix)
|
|
68
|
+
base_name = os.path.basename(out_prefix)
|
|
69
|
+
dest = os.path.join(log_dir, f"{base_name}.log")
|
|
70
|
+
if os.path.exists(dest):
|
|
71
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
72
|
+
dest = os.path.join(log_dir, f"{base_name}_{timestamp}.log")
|
|
73
|
+
os.rename(log_path, dest)
|
|
74
|
+
if logger is not None:
|
|
75
|
+
logger.info(f"PLINK log moved to: {dest}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# VCF → PLINK BED
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def vcf_to_plink(
|
|
83
|
+
vcf_file,
|
|
84
|
+
out_prefix,
|
|
85
|
+
*,
|
|
86
|
+
plink_path="plink",
|
|
87
|
+
config_path=None,
|
|
88
|
+
auto_project_config=False,
|
|
89
|
+
allow_extra_chr=False,
|
|
90
|
+
logger=None,
|
|
91
|
+
):
|
|
92
|
+
"""Convert a VCF file to PLINK binary format (BED/BIM/FAM)."""
|
|
93
|
+
log = logger or _default_logger
|
|
94
|
+
log.info(f"Converting VCF file {vcf_file} to PLINK binary format...")
|
|
95
|
+
|
|
96
|
+
# Skip when output already exists.
|
|
97
|
+
if all(os.path.exists(f"{out_prefix}{ext}") for ext in (".bed", ".bim", ".fam")):
|
|
98
|
+
log.info(f"PLINK binary files already exist: {out_prefix}")
|
|
99
|
+
log.info("Skipping conversion step...")
|
|
100
|
+
return out_prefix
|
|
101
|
+
|
|
102
|
+
plink = _resolve_plink(plink_path, config_path, auto_project_config)
|
|
103
|
+
cmd = [plink, "--vcf", vcf_file, "--make-bed", "--out", out_prefix, "--double-id"]
|
|
104
|
+
if allow_extra_chr:
|
|
105
|
+
cmd.extend(["--allow-extra-chr"])
|
|
106
|
+
run_command(cmd, logger=log)
|
|
107
|
+
_timestamp_plink_log(out_prefix, logger=log)
|
|
108
|
+
|
|
109
|
+
log.info(f"VCF conversion completed: {out_prefix}.bed, {out_prefix}.bim, {out_prefix}.fam")
|
|
110
|
+
return out_prefix
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# BED → PED/MAP (with SNP extraction)
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def extract_snps(
|
|
118
|
+
bfile,
|
|
119
|
+
extract_file,
|
|
120
|
+
out_prefix,
|
|
121
|
+
*,
|
|
122
|
+
plink_path="plink",
|
|
123
|
+
config_path=None,
|
|
124
|
+
auto_project_config=False,
|
|
125
|
+
allow_extra_chr=False,
|
|
126
|
+
logger=None,
|
|
127
|
+
):
|
|
128
|
+
"""Extract selected SNPs from a PLINK binary dataset to PED/MAP."""
|
|
129
|
+
log = logger or _default_logger
|
|
130
|
+
log.info(f"Extracting SNPs from {bfile}...")
|
|
131
|
+
|
|
132
|
+
if os.path.exists(f"{out_prefix}.ped") and os.path.exists(f"{out_prefix}.map"):
|
|
133
|
+
log.info(f"PLINK PED/MAP files already exist: {out_prefix}")
|
|
134
|
+
log.info("Skipping extraction step...")
|
|
135
|
+
return out_prefix
|
|
136
|
+
|
|
137
|
+
plink = _resolve_plink(plink_path, config_path, auto_project_config)
|
|
138
|
+
cmd = [
|
|
139
|
+
plink,
|
|
140
|
+
"--bfile", bfile,
|
|
141
|
+
"--out", out_prefix,
|
|
142
|
+
"--extract", extract_file,
|
|
143
|
+
"--recode", "compound-genotypes", "01",
|
|
144
|
+
"--output-missing-genotype", "3",
|
|
145
|
+
]
|
|
146
|
+
if allow_extra_chr:
|
|
147
|
+
cmd.extend(["--allow-extra-chr"])
|
|
148
|
+
run_command(cmd, logger=log)
|
|
149
|
+
_timestamp_plink_log(out_prefix, logger=log)
|
|
150
|
+
|
|
151
|
+
log.info(f"SNP extraction completed: {out_prefix}.ped, {out_prefix}.map")
|
|
152
|
+
return out_prefix
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# BED → PED/MAP (full, no filtering)
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def convert_bfile_to_ped(
|
|
160
|
+
bfile,
|
|
161
|
+
out_prefix,
|
|
162
|
+
*,
|
|
163
|
+
plink_path="plink",
|
|
164
|
+
config_path=None,
|
|
165
|
+
auto_project_config=False,
|
|
166
|
+
allow_extra_chr=False,
|
|
167
|
+
logger=None,
|
|
168
|
+
):
|
|
169
|
+
"""Convert PLINK binary files directly to PED/MAP without SNP filtering."""
|
|
170
|
+
log = logger or _default_logger
|
|
171
|
+
log.info(f"Converting PLINK binary dataset {bfile} to PED/MAP format...")
|
|
172
|
+
|
|
173
|
+
if os.path.exists(f"{out_prefix}.ped") and os.path.exists(f"{out_prefix}.map"):
|
|
174
|
+
log.info(f"PLINK PED/MAP files already exist: {out_prefix}")
|
|
175
|
+
log.info("Skipping conversion step...")
|
|
176
|
+
return out_prefix
|
|
177
|
+
|
|
178
|
+
plink = _resolve_plink(plink_path, config_path, auto_project_config)
|
|
179
|
+
cmd = [
|
|
180
|
+
plink,
|
|
181
|
+
"--bfile", bfile,
|
|
182
|
+
"--out", out_prefix,
|
|
183
|
+
"--recode", "compound-genotypes", "01",
|
|
184
|
+
"--output-missing-genotype", "3",
|
|
185
|
+
]
|
|
186
|
+
if allow_extra_chr:
|
|
187
|
+
cmd.extend(["--allow-extra-chr"])
|
|
188
|
+
run_command(cmd, logger=log)
|
|
189
|
+
_timestamp_plink_log(out_prefix, logger=log)
|
|
190
|
+
|
|
191
|
+
log.info(f"Conversion completed: {out_prefix}.ped, {out_prefix}.map")
|
|
192
|
+
return out_prefix
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# PED/MAP → numeric CSV matrix
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def convert_to_matrix(fileprefix, out_file=None, *, out_format="parquet", geno_encoding="012", logger=None):
|
|
200
|
+
"""Convert PLINK PED/MAP genotype data to a numeric CSV or binary matrix.
|
|
201
|
+
|
|
202
|
+
Encoding (``geno_encoding="012"``): ``00→0, 01→1, 10→1, 11→2``,
|
|
203
|
+
missing → ``3``. With ``geno_encoding="-101"`` (Azodi et al. 2019
|
|
204
|
+
style): ``00→-1, 01→0, 10→0, 11→1``, missing → ``3``.
|
|
205
|
+
"""
|
|
206
|
+
log = logger or _default_logger
|
|
207
|
+
if geno_encoding not in GENO_ENCODINGS:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Unknown geno_encoding '{geno_encoding}'. "
|
|
210
|
+
f"Supported: {sorted(GENO_ENCODINGS)}"
|
|
211
|
+
)
|
|
212
|
+
geno_dict = GENO_ENCODINGS[geno_encoding]
|
|
213
|
+
log.info(f"Converting {fileprefix}.ped and {fileprefix}.map to matrix format...")
|
|
214
|
+
log.info(f"Genotype encoding: {geno_encoding}")
|
|
215
|
+
|
|
216
|
+
# Detect pyarrow
|
|
217
|
+
out_format = out_format.lower()
|
|
218
|
+
if out_format in ('parquet', 'feather'):
|
|
219
|
+
try:
|
|
220
|
+
import pyarrow
|
|
221
|
+
except ImportError:
|
|
222
|
+
log.warning(
|
|
223
|
+
f"Output format '{out_format}' requires the 'pyarrow' package, which is not installed. "
|
|
224
|
+
"Falling back to 'csv'. Please run 'pip install pyarrow' to enable highly optimized binary formats."
|
|
225
|
+
)
|
|
226
|
+
out_format = 'csv'
|
|
227
|
+
|
|
228
|
+
ped_path = fileprefix + '.ped'
|
|
229
|
+
map_path = fileprefix + '.map'
|
|
230
|
+
|
|
231
|
+
# Try alternate paths when the prefix contains a directory component.
|
|
232
|
+
if not os.path.exists(ped_path) and '/' in fileprefix:
|
|
233
|
+
base_name = os.path.basename(fileprefix)
|
|
234
|
+
dir_name = os.path.dirname(fileprefix)
|
|
235
|
+
alt_ped = os.path.join(dir_name, base_name + '.ped')
|
|
236
|
+
alt_map = os.path.join(dir_name, base_name + '.map')
|
|
237
|
+
if os.path.exists(alt_ped) and os.path.exists(alt_map):
|
|
238
|
+
ped_path = alt_ped
|
|
239
|
+
map_path = alt_map
|
|
240
|
+
|
|
241
|
+
if out_file is None:
|
|
242
|
+
ext = '.parquet' if out_format == 'parquet' else '.feather' if out_format == 'feather' else '.csv'
|
|
243
|
+
out_file = fileprefix + ext
|
|
244
|
+
|
|
245
|
+
manifest_file = os.path.splitext(out_file)[0] + ".features.json"
|
|
246
|
+
if os.path.exists(out_file) and os.path.exists(manifest_file):
|
|
247
|
+
log.info(f"Matrix file already exists: {out_file}")
|
|
248
|
+
log.info("Skipping conversion step...")
|
|
249
|
+
return out_file
|
|
250
|
+
if os.path.exists(out_file):
|
|
251
|
+
log.warning("Existing matrix has no feature manifest; regenerating it with canonical SNP IDs.")
|
|
252
|
+
|
|
253
|
+
if not os.path.exists(ped_path) or not os.path.exists(map_path):
|
|
254
|
+
raise FileNotFoundError(f"Input file not found: {ped_path} or {map_path}")
|
|
255
|
+
|
|
256
|
+
# Use stable UCSC-style IDs from chromosome and base-pair coordinates.
|
|
257
|
+
snpid_list = canonical_ids_from_map_file(map_path)
|
|
258
|
+
|
|
259
|
+
# Read sample IDs and genotypes from .ped.
|
|
260
|
+
sample_genotypes = []
|
|
261
|
+
with open(ped_path) as ped_file:
|
|
262
|
+
for row in ped_file:
|
|
263
|
+
row = row.strip().split()
|
|
264
|
+
sample_id = row[1]
|
|
265
|
+
|
|
266
|
+
# Normalize sample IDs produced from VCF conversion.
|
|
267
|
+
if sample_id.endswith('_'):
|
|
268
|
+
sample_id = sample_id.rstrip('_')
|
|
269
|
+
|
|
270
|
+
geno = row[6:] # Genotypes start at column 7.
|
|
271
|
+
encoded_geno = [geno_dict.get(g, '3') for g in geno]
|
|
272
|
+
sample_genotypes.append((sample_id, encoded_geno))
|
|
273
|
+
|
|
274
|
+
# Write matrix based on format.
|
|
275
|
+
if out_format in ('parquet', 'feather'):
|
|
276
|
+
import pandas as pd
|
|
277
|
+
data = {sample_id: genotypes for sample_id, genotypes in sample_genotypes}
|
|
278
|
+
df = pd.DataFrame.from_dict(data, orient='index', columns=snpid_list)
|
|
279
|
+
df.index.name = 'ID'
|
|
280
|
+
df_reset = df.reset_index()
|
|
281
|
+
if out_format == 'parquet':
|
|
282
|
+
df_reset.to_parquet(out_file, index=False)
|
|
283
|
+
else:
|
|
284
|
+
df_reset.to_feather(out_file)
|
|
285
|
+
else:
|
|
286
|
+
# Write CSV matrix.
|
|
287
|
+
with open(out_file, 'w') as csv_file:
|
|
288
|
+
csv_file.write("ID," + ",".join(snpid_list) + '\n')
|
|
289
|
+
for sample_id, genotypes in sample_genotypes:
|
|
290
|
+
csv_file.write(sample_id + "," + ",".join(genotypes) + '\n')
|
|
291
|
+
|
|
292
|
+
manifest_path = write_feature_manifest(
|
|
293
|
+
os.path.dirname(out_file) or ".",
|
|
294
|
+
snpid_list,
|
|
295
|
+
source_file=out_file,
|
|
296
|
+
filename=os.path.basename(manifest_file),
|
|
297
|
+
)
|
|
298
|
+
log.info(f"Feature manifest written: {manifest_path}")
|
|
299
|
+
log.info(f"Matrix conversion completed: {out_file}")
|
|
300
|
+
return out_file
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Batch processing: SNP directory
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def process_snp_dir(
|
|
309
|
+
bfile,
|
|
310
|
+
snp_dir,
|
|
311
|
+
out_dir,
|
|
312
|
+
*,
|
|
313
|
+
plink_path="plink",
|
|
314
|
+
config_path=None,
|
|
315
|
+
auto_project_config=False,
|
|
316
|
+
allow_extra_chr=False,
|
|
317
|
+
geno_encoding="012",
|
|
318
|
+
logger=None,
|
|
319
|
+
):
|
|
320
|
+
"""Process all SNP list files (*.txt) in a directory."""
|
|
321
|
+
log = logger or _default_logger
|
|
322
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
323
|
+
|
|
324
|
+
snp_files = glob.glob(os.path.join(snp_dir, "*.txt"))
|
|
325
|
+
if not snp_files:
|
|
326
|
+
log.warning(f"No .txt files found in {snp_dir}")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
log.info(f"Found {len(snp_files)} SNP list file(s). Starting batch processing...")
|
|
330
|
+
|
|
331
|
+
for snp_file in snp_files:
|
|
332
|
+
phenotype = os.path.basename(snp_file).replace('.txt', '')
|
|
333
|
+
log.info("")
|
|
334
|
+
log.info(f"Processing phenotype: {phenotype}")
|
|
335
|
+
|
|
336
|
+
out_prefix = os.path.join(out_dir, phenotype)
|
|
337
|
+
try:
|
|
338
|
+
extract_snps(
|
|
339
|
+
bfile, snp_file, out_prefix,
|
|
340
|
+
plink_path=plink_path,
|
|
341
|
+
config_path=config_path,
|
|
342
|
+
auto_project_config=auto_project_config,
|
|
343
|
+
allow_extra_chr=allow_extra_chr,
|
|
344
|
+
logger=log,
|
|
345
|
+
)
|
|
346
|
+
matrix_file = convert_to_matrix(out_prefix, geno_encoding=geno_encoding, logger=log)
|
|
347
|
+
log.info(f"Phenotype {phenotype} completed. CSV matrix: {matrix_file}")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
log.error(f"Error while processing phenotype {phenotype}: {str(e)}")
|