G2PInsight 1.0.0__tar.gz → 1.0.2__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.
- g2pinsight-1.0.2/G2PInsight/__init__.py +3 -0
- g2pinsight-1.0.2/G2PInsight/bin/font_utils.py +74 -0
- g2pinsight-1.0.2/G2PInsight/bin/gemma_gwas.py +190 -0
- g2pinsight-1.0.2/G2PInsight/bin/modeltraining.py +2432 -0
- g2pinsight-1.0.2/G2PInsight/bin/plink_ld.py +225 -0
- g2pinsight-1.0.2/G2PInsight/bin/preprocess.py +1894 -0
- g2pinsight-1.0.2/G2PInsight/bin/visualization.py +1783 -0
- g2pinsight-1.0.2/G2PInsight/main.py +276 -0
- g2pinsight-1.0.2/G2PInsight.egg-info/PKG-INFO +487 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight.egg-info/SOURCES.txt +1 -11
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight.egg-info/requires.txt +10 -0
- g2pinsight-1.0.2/MANIFEST.in +4 -0
- g2pinsight-1.0.2/PKG-INFO +487 -0
- g2pinsight-1.0.2/README.md +442 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/pyproject.toml +40 -3
- g2pinsight-1.0.2/setup.py +3 -0
- g2pinsight-1.0.0/G2PInsight/__init__.py +0 -10
- g2pinsight-1.0.0/G2PInsight/bin/font_utils.py +0 -135
- g2pinsight-1.0.0/G2PInsight/bin/gemma_gwas.py +0 -402
- g2pinsight-1.0.0/G2PInsight/bin/modeltraining.py +0 -2671
- g2pinsight-1.0.0/G2PInsight/bin/plink_ld.py +0 -433
- g2pinsight-1.0.0/G2PInsight/bin/preprocess.py +0 -2903
- g2pinsight-1.0.0/G2PInsight/bin/visualization.py +0 -1692
- g2pinsight-1.0.0/G2PInsight/main.py +0 -453
- g2pinsight-1.0.0/G2PInsight.egg-info/PKG-INFO +0 -451
- g2pinsight-1.0.0/PKG-INFO +0 -451
- g2pinsight-1.0.0/README.md +0 -416
- g2pinsight-1.0.0/setup.py +0 -30
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight/bin/__init__.py +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight/bin/software/gemma-0.98.5-linux-static-AMD64 +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight/bin/software/plink +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight.egg-info/dependency_links.txt +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight.egg-info/entry_points.txt +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/G2PInsight.egg-info/top_level.txt +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/LICENSE +0 -0
- {g2pinsight-1.0.0 → g2pinsight-1.0.2}/setup.cfg +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import importlib
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Optional, Tuple, cast
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
_FONT_MANAGER_LOGGER = 'matplotlib.font_manager'
|
|
8
|
+
|
|
9
|
+
def _load_module(name: str) -> Any:
|
|
10
|
+
return importlib.import_module(name)
|
|
11
|
+
|
|
12
|
+
def _collect_matplotlib_font_names() -> set:
|
|
13
|
+
fm = _load_module('matplotlib.font_manager')
|
|
14
|
+
font_manager = getattr(fm, 'fontManager', None)
|
|
15
|
+
if font_manager is None:
|
|
16
|
+
font_manager = cast(Any, fm.FontManager)()
|
|
17
|
+
mpl_logger = logging.getLogger(_FONT_MANAGER_LOGGER)
|
|
18
|
+
prev_level = mpl_logger.level
|
|
19
|
+
mpl_logger.setLevel(logging.WARNING)
|
|
20
|
+
try:
|
|
21
|
+
return {f.name for f in font_manager.ttflist}
|
|
22
|
+
finally:
|
|
23
|
+
mpl_logger.setLevel(prev_level)
|
|
24
|
+
|
|
25
|
+
@lru_cache(maxsize=1)
|
|
26
|
+
def detect_available_fonts() -> Tuple[Optional[str], Optional[str]]:
|
|
27
|
+
try:
|
|
28
|
+
mpl_fonts = _collect_matplotlib_font_names()
|
|
29
|
+
chinese_font_candidates = ['SimHei', 'SimSun', 'Microsoft YaHei', 'KaiTi', 'FangSong', 'STSong', 'STKaiti']
|
|
30
|
+
english_fonts = ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Times New Roman', 'Calibri']
|
|
31
|
+
chinese_font = None
|
|
32
|
+
for font_name in chinese_font_candidates:
|
|
33
|
+
if font_name in mpl_fonts:
|
|
34
|
+
chinese_font = font_name
|
|
35
|
+
break
|
|
36
|
+
english_font = None
|
|
37
|
+
for font_name in english_fonts:
|
|
38
|
+
if font_name in mpl_fonts:
|
|
39
|
+
english_font = font_name
|
|
40
|
+
break
|
|
41
|
+
if not chinese_font:
|
|
42
|
+
chinese_font = 'DejaVu Sans'
|
|
43
|
+
if not english_font:
|
|
44
|
+
english_font = 'DejaVu Sans'
|
|
45
|
+
return (chinese_font, english_font)
|
|
46
|
+
except Exception:
|
|
47
|
+
return ('DejaVu Sans', 'DejaVu Sans')
|
|
48
|
+
|
|
49
|
+
def setup_matplotlib_font(chinese_font: Optional[str]=None, english_font: Optional[str]=None) -> None:
|
|
50
|
+
try:
|
|
51
|
+
mpl = _load_module('matplotlib')
|
|
52
|
+
mpl.use('Agg')
|
|
53
|
+
plt = _load_module('matplotlib.pyplot')
|
|
54
|
+
if chinese_font is None or english_font is None:
|
|
55
|
+
chinese_font, english_font = detect_available_fonts()
|
|
56
|
+
default_font = chinese_font if chinese_font else english_font
|
|
57
|
+
plt.rcParams['font.sans-serif'] = [default_font, english_font, 'DejaVu Sans']
|
|
58
|
+
plt.rcParams['axes.unicode_minus'] = False
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def setup_plotly_font(chinese_font: Optional[str]=None, english_font: Optional[str]=None) -> dict:
|
|
63
|
+
try:
|
|
64
|
+
if chinese_font is None or english_font is None:
|
|
65
|
+
chinese_font, english_font = detect_available_fonts()
|
|
66
|
+
default_font = chinese_font if chinese_font else english_font
|
|
67
|
+
return {'family': default_font, 'size': 12}
|
|
68
|
+
except Exception:
|
|
69
|
+
return {'family': 'Arial', 'size': 12}
|
|
70
|
+
try:
|
|
71
|
+
_cn, _en = detect_available_fonts()
|
|
72
|
+
setup_matplotlib_font(_cn, _en)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import subprocess
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import shutil
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
bin_dir = Path(__file__).parent
|
|
13
|
+
software_dir = bin_dir / 'software'
|
|
14
|
+
_bundled_plink = software_dir / 'plink'
|
|
15
|
+
_bundled_gemma = software_dir / 'gemma-0.98.5-linux-static-AMD64'
|
|
16
|
+
|
|
17
|
+
def _get_plink_executable() -> str:
|
|
18
|
+
if _bundled_plink.exists():
|
|
19
|
+
return str(_bundled_plink)
|
|
20
|
+
found = shutil.which('plink')
|
|
21
|
+
if found:
|
|
22
|
+
logger.info(f'Using system PLINK executable from PATH: {found}')
|
|
23
|
+
return found
|
|
24
|
+
raise FileNotFoundError(f'PLINK executable not found. Checked bundled path: {_bundled_plink} and system PATH.')
|
|
25
|
+
|
|
26
|
+
def _get_gemma_executable() -> str:
|
|
27
|
+
if _bundled_gemma.exists():
|
|
28
|
+
return str(_bundled_gemma)
|
|
29
|
+
found = shutil.which('gemma')
|
|
30
|
+
if found:
|
|
31
|
+
logger.info(f'Using system GEMMA executable from PATH: {found}')
|
|
32
|
+
return found
|
|
33
|
+
raise FileNotFoundError(f'GEMMA executable not found. Checked bundled path: {_bundled_gemma} and system PATH.')
|
|
34
|
+
plink_executable = _get_plink_executable()
|
|
35
|
+
gemma_executable = _get_gemma_executable()
|
|
36
|
+
REQUIRED_INPUT_FILES = {'bed': '.bed', 'bim': '.bim', 'fam': '.fam'}
|
|
37
|
+
|
|
38
|
+
def validate_input_files(input_prefix: str) -> None:
|
|
39
|
+
if not isinstance(input_prefix, str) or not input_prefix.strip():
|
|
40
|
+
raise ValueError('input_prefix must be a non-empty string')
|
|
41
|
+
missing_files = []
|
|
42
|
+
for file_type, suffix in REQUIRED_INPUT_FILES.items():
|
|
43
|
+
file_path = f'{input_prefix}{suffix}'
|
|
44
|
+
if not Path(file_path).exists():
|
|
45
|
+
missing_files.append(file_path)
|
|
46
|
+
if missing_files:
|
|
47
|
+
raise FileNotFoundError(f"Missing PLINK binary files: {', '.join(missing_files)}")
|
|
48
|
+
logger.info(f'Input file validation passed: {input_prefix}')
|
|
49
|
+
|
|
50
|
+
def validate_phenotype_file(phenotype_file: str) -> pd.DataFrame:
|
|
51
|
+
if not isinstance(phenotype_file, str) or not phenotype_file.strip():
|
|
52
|
+
raise ValueError('phenotype_file must be a non-empty string')
|
|
53
|
+
if not Path(phenotype_file).exists():
|
|
54
|
+
raise FileNotFoundError(f'Phenotype file not found: {phenotype_file}')
|
|
55
|
+
try:
|
|
56
|
+
pheno_df = pd.read_csv(phenotype_file, sep='\\s+', header=None, usecols=[0, 1], skipinitialspace=True)
|
|
57
|
+
pheno_df.columns = ['IID', 'PHENO']
|
|
58
|
+
if pheno_df['IID'].isnull().any() or pheno_df['PHENO'].isnull().any():
|
|
59
|
+
logger.warning('The first two columns in the phenotype file contain missing values')
|
|
60
|
+
return pheno_df
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise RuntimeError(f'Failed to read phenotype file: {str(e)}')
|
|
63
|
+
|
|
64
|
+
def run_shell_command(cmd: list, step_name: str) -> None:
|
|
65
|
+
cmd_str = ' '.join(cmd)
|
|
66
|
+
logger.info(f'[RUN] {step_name}: {cmd_str}')
|
|
67
|
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8')
|
|
68
|
+
stderr_content = ''
|
|
69
|
+
if result.stderr:
|
|
70
|
+
stderr_content = result.stderr[:1000]
|
|
71
|
+
lines = stderr_content.splitlines()
|
|
72
|
+
filtered_lines = []
|
|
73
|
+
for line in lines:
|
|
74
|
+
stripped = line.strip()
|
|
75
|
+
if stripped and all((ch in '=%0123456789' for ch in stripped)):
|
|
76
|
+
continue
|
|
77
|
+
filtered_lines.append(line)
|
|
78
|
+
stderr_content = '\n'.join(filtered_lines).strip()
|
|
79
|
+
if stderr_content:
|
|
80
|
+
if result.returncode == 0:
|
|
81
|
+
error_keywords = ['ERROR', 'FATAL', 'FAIL', 'Exception', 'Traceback']
|
|
82
|
+
if any((keyword in stderr_content.upper() for keyword in error_keywords)):
|
|
83
|
+
logger.warning(f'stderr (potential issue): {stderr_content}')
|
|
84
|
+
else:
|
|
85
|
+
logger.info(f'stderr (informational): {stderr_content}')
|
|
86
|
+
else:
|
|
87
|
+
logger.error(f'stderr: {stderr_content}')
|
|
88
|
+
if result.returncode != 0:
|
|
89
|
+
raise RuntimeError(f'{step_name} failed with return code: {result.returncode}')
|
|
90
|
+
logger.info(f'[DONE] {step_name}')
|
|
91
|
+
|
|
92
|
+
def plink_quality_control(input_prefix: str, output_prefix: str) -> None:
|
|
93
|
+
cmd = [plink_executable, '--bfile', input_prefix, '--maf', '0.05', '--geno', '0.2', '--allow-extra-chr', '--allow-no-sex', '--make-bed', '--out', output_prefix]
|
|
94
|
+
run_shell_command(cmd, 'PLINK quality control')
|
|
95
|
+
|
|
96
|
+
def merge_phenotype_to_fam(pheno_df: pd.DataFrame, fam_file: str) -> None:
|
|
97
|
+
bak_fam = f'{fam_file}.bak'
|
|
98
|
+
if not Path(bak_fam).exists():
|
|
99
|
+
cmd = ['mv', fam_file, bak_fam]
|
|
100
|
+
run_shell_command(cmd, 'Backup FAM file')
|
|
101
|
+
pheno_dict = dict(zip(pheno_df['IID'].astype(str).str.strip(), pheno_df['PHENO'].astype(str).str.strip()))
|
|
102
|
+
fam_df = pd.read_csv(bak_fam, sep='\\s+', header=None, names=['FID', 'IID', 'PAT', 'MAT', 'SEX', 'OLD_PHENO'])
|
|
103
|
+
fam_df['IID'] = fam_df['IID'].astype(str).str.strip()
|
|
104
|
+
fam_df['PHENO'] = fam_df['IID'].map(pheno_dict).fillna('-9')
|
|
105
|
+
new_fam_df = fam_df[['FID', 'IID', 'PAT', 'MAT', 'SEX', 'PHENO']]
|
|
106
|
+
new_fam_df.to_csv(fam_file, sep=' ', header=False, index=False)
|
|
107
|
+
matched = (fam_df['PHENO'] != '-9').sum()
|
|
108
|
+
total = len(fam_df)
|
|
109
|
+
logger.info(f'Phenotype matching completed: {matched}/{total} samples matched (-9 used for missing phenotypes)')
|
|
110
|
+
|
|
111
|
+
def calculate_pca(input_prefix: str, output_prefix: str) -> None:
|
|
112
|
+
cmd = [plink_executable, '--bfile', input_prefix, '--pca', '5', '--allow-extra-chr', '--allow-no-sex', '--out', output_prefix]
|
|
113
|
+
run_shell_command(cmd, 'PCA computation')
|
|
114
|
+
|
|
115
|
+
def filter_valid_samples(pheno_df: pd.DataFrame, fam_file: str, output_sample_file: str) -> None:
|
|
116
|
+
fam_df = pd.read_csv(fam_file, sep='\\s+', header=None, names=['FID', 'IID', 'PAT', 'MAT', 'SEX', 'PHENO'])
|
|
117
|
+
valid_iids = set(pheno_df['IID'].astype(str).str.strip())
|
|
118
|
+
fam_df['IID'] = fam_df['IID'].astype(str).str.strip()
|
|
119
|
+
valid_df = fam_df[fam_df['IID'].isin(valid_iids)][['FID', 'IID']]
|
|
120
|
+
valid_df.to_csv(output_sample_file, sep=' ', header=False, index=False)
|
|
121
|
+
logger.info(f'Valid samples saved to: {output_sample_file} (n={len(valid_df)})')
|
|
122
|
+
|
|
123
|
+
def filter_plink_samples(input_prefix: str, sample_file: str, output_prefix: str) -> None:
|
|
124
|
+
cmd = [plink_executable, '--bfile', input_prefix, '--keep', sample_file, '--allow-extra-chr', '--allow-no-sex', '--make-bed', '--out', output_prefix]
|
|
125
|
+
run_shell_command(cmd, 'PLINK sample filtering')
|
|
126
|
+
|
|
127
|
+
def filter_pca_by_samples(pca_file: str, sample_file: str, output_pca_file: str) -> None:
|
|
128
|
+
valid_df = pd.read_csv(sample_file, sep='\\s+', header=None, names=['FID', 'IID'])
|
|
129
|
+
valid_set = set(zip(valid_df['FID'].astype(str).str.strip(), valid_df['IID'].astype(str).str.strip()))
|
|
130
|
+
pca_df = pd.read_csv(pca_file, sep='\\s+', header=None)
|
|
131
|
+
pca_df['key'] = list(zip(pca_df[0].astype(str).str.strip(), pca_df[1].astype(str).str.strip()))
|
|
132
|
+
filtered_df = pca_df[pca_df['key'].isin(valid_set)].drop('key', axis=1)
|
|
133
|
+
filtered_df.to_csv(output_pca_file, sep=' ', header=False, index=False)
|
|
134
|
+
logger.info(f'Filtered PCA saved to: {output_pca_file} (n={len(filtered_df)} samples)')
|
|
135
|
+
|
|
136
|
+
def extract_pca_covariates(pca_file: str, output_cov_file: str) -> None:
|
|
137
|
+
pca_df = pd.read_csv(pca_file, sep='\\s+', header=None)
|
|
138
|
+
cov_df = pca_df.iloc[:, 2:]
|
|
139
|
+
cov_df.to_csv(output_cov_file, sep=' ', header=False, index=False)
|
|
140
|
+
logger.info(f'PCA covariates saved to: {output_cov_file} (PCs={cov_df.shape[1]})')
|
|
141
|
+
|
|
142
|
+
def calculate_kinship_matrix(genotype_prefix: str, output_prefix: str) -> str:
|
|
143
|
+
base_prefix = Path(output_prefix).name
|
|
144
|
+
kinship_prefix = f'{base_prefix}_kinship'
|
|
145
|
+
cmd = [gemma_executable, '-bfile', genotype_prefix, '-gk', '1', '-o', kinship_prefix]
|
|
146
|
+
run_shell_command(cmd, 'Kinship matrix computation')
|
|
147
|
+
kinship_file = Path('output') / f'{kinship_prefix}.cXX.txt'
|
|
148
|
+
if not kinship_file.exists():
|
|
149
|
+
raise FileNotFoundError(f'Kinship matrix file was not generated: {kinship_file}')
|
|
150
|
+
return kinship_file.absolute().as_posix()
|
|
151
|
+
|
|
152
|
+
def run_gemma_gwas(genotype_prefix: str, kinship_file: str, cov_file: str, output_prefix: str) -> None:
|
|
153
|
+
base_prefix = Path(output_prefix).name
|
|
154
|
+
gwas_prefix = f'{base_prefix}_gwas'
|
|
155
|
+
current_dir = os.getcwd()
|
|
156
|
+
logger.info(f'GWAS working directory: {current_dir}')
|
|
157
|
+
logger.info(f'GWAS output prefix: {gwas_prefix} (results expected under {current_dir}/output/)')
|
|
158
|
+
cmd = [gemma_executable, '-bfile', genotype_prefix, '-k', kinship_file, '-c', cov_file, '-lmm', '1', '-o', gwas_prefix]
|
|
159
|
+
run_shell_command(cmd, 'GWAS association analysis')
|
|
160
|
+
expected_result_file = Path(current_dir) / 'output' / f'{gwas_prefix}.assoc.txt'
|
|
161
|
+
if expected_result_file.exists():
|
|
162
|
+
logger.info(f'GWAS result file generated: {expected_result_file}')
|
|
163
|
+
else:
|
|
164
|
+
logger.warning(f'GWAS result file not found at expected path: {expected_result_file}')
|
|
165
|
+
|
|
166
|
+
def run_complete_gwas_pipeline(input_plink_prefix: str, phenotype_file: str, output_prefix: str, perform_plink_qc: bool=True) -> None:
|
|
167
|
+
validate_input_files(input_plink_prefix)
|
|
168
|
+
pheno_df = validate_phenotype_file(phenotype_file)
|
|
169
|
+
if perform_plink_qc:
|
|
170
|
+
clean_geno_prefix = f'{output_prefix}_clean_geno'
|
|
171
|
+
plink_quality_control(input_plink_prefix, clean_geno_prefix)
|
|
172
|
+
else:
|
|
173
|
+
logger.info('Skipping PLINK QC in GWAS step based on upstream configuration')
|
|
174
|
+
clean_geno_prefix = input_plink_prefix
|
|
175
|
+
pca_prefix = f'{output_prefix}_geno_pca'
|
|
176
|
+
calculate_pca(clean_geno_prefix, pca_prefix)
|
|
177
|
+
valid_sample_file = f'{output_prefix}_valid_samples.txt'
|
|
178
|
+
filter_valid_samples(pheno_df, f'{clean_geno_prefix}.fam', valid_sample_file)
|
|
179
|
+
filtered_geno_prefix = f'{output_prefix}_clean_geno_filtered'
|
|
180
|
+
filter_plink_samples(clean_geno_prefix, valid_sample_file, filtered_geno_prefix)
|
|
181
|
+
filtered_fam_file = f'{filtered_geno_prefix}.fam'
|
|
182
|
+
merge_phenotype_to_fam(pheno_df, filtered_fam_file)
|
|
183
|
+
pca_eigenvec_file = f'{pca_prefix}.eigenvec'
|
|
184
|
+
filtered_pca_file = f'{output_prefix}_geno_pca_filtered.eigenvec'
|
|
185
|
+
filter_pca_by_samples(pca_eigenvec_file, valid_sample_file, filtered_pca_file)
|
|
186
|
+
cov_file = f'{output_prefix}_covariates_filtered.txt'
|
|
187
|
+
extract_pca_covariates(filtered_pca_file, cov_file)
|
|
188
|
+
kinship_file = calculate_kinship_matrix(filtered_geno_prefix, output_prefix)
|
|
189
|
+
run_gemma_gwas(filtered_geno_prefix, kinship_file, cov_file, output_prefix)
|
|
190
|
+
logger.info('GWAS pipeline completed')
|