pheval 0.1.0__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of pheval might be problematic. Click here for more details.
- pheval/__init__.py +0 -5
- pheval/analyse/__init__.py +0 -0
- pheval/analyse/analysis.py +703 -0
- pheval/analyse/generate_plots.py +312 -0
- pheval/analyse/generate_summary_outputs.py +186 -0
- pheval/analyse/rank_stats.py +61 -0
- pheval/cli.py +22 -7
- pheval/cli_pheval.py +37 -12
- pheval/cli_pheval_utils.py +225 -8
- pheval/config_parser.py +36 -0
- pheval/constants.py +1 -0
- pheval/implementations/__init__.py +1 -3
- pheval/post_processing/__init__.py +0 -0
- pheval/post_processing/post_processing.py +210 -0
- pheval/prepare/__init__.py +0 -0
- pheval/prepare/create_noisy_phenopackets.py +173 -0
- pheval/prepare/create_spiked_vcf.py +366 -0
- pheval/prepare/custom_exceptions.py +47 -0
- pheval/prepare/update_phenopacket.py +53 -0
- pheval/resources/alternate_ouputs/CADA_results.txt +11 -0
- pheval/resources/alternate_ouputs/DeepPVP_results.txt +22 -0
- pheval/resources/alternate_ouputs/OVA_results.txt +11 -0
- pheval/resources/alternate_ouputs/Phen2Gene_results.json +814 -0
- pheval/resources/alternate_ouputs/Phenolyzer_results.txt +12 -0
- pheval/resources/alternate_ouputs/lirical_results.tsv +152 -0
- pheval/resources/alternate_ouputs/svanna_results.tsv +9 -0
- pheval/resources/hgnc_complete_set_2022-10-01.txt +43222 -0
- pheval/run_metadata.py +27 -0
- pheval/runners/runner.py +92 -11
- pheval/utils/__init__.py +0 -0
- pheval/utils/docs_gen.py +105 -0
- pheval/utils/docs_gen.sh +18 -0
- pheval/utils/file_utils.py +88 -0
- pheval/utils/phenopacket_utils.py +356 -0
- pheval/utils/semsim_utils.py +156 -0
- {pheval-0.1.0.dist-info → pheval-0.2.0.dist-info}/METADATA +12 -4
- pheval-0.2.0.dist-info/RECORD +41 -0
- {pheval-0.1.0.dist-info → pheval-0.2.0.dist-info}/WHEEL +1 -1
- pheval/utils.py +0 -7
- pheval-0.1.0.dist-info/RECORD +0 -13
- {pheval-0.1.0.dist-info → pheval-0.2.0.dist-info}/LICENSE +0 -0
- {pheval-0.1.0.dist-info → pheval-0.2.0.dist-info}/entry_points.txt +0 -0
pheval/cli_pheval.py
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Monarch Initiative
|
|
3
3
|
"""
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
8
|
from pheval.implementations import get_implementation_resolver
|
|
9
|
+
from pheval.utils.file_utils import write_metadata
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@click.command()
|
|
11
13
|
@click.option(
|
|
12
|
-
"--
|
|
14
|
+
"--input-dir",
|
|
13
15
|
"-i",
|
|
14
16
|
metavar="INPUTDIR",
|
|
15
17
|
required=True,
|
|
16
18
|
help="The input directory (relative path: e.g exomiser-13.11)",
|
|
19
|
+
type=Path,
|
|
17
20
|
)
|
|
18
21
|
@click.option(
|
|
19
|
-
"--
|
|
22
|
+
"--testdata-dir",
|
|
20
23
|
"-t",
|
|
21
24
|
metavar="TESTDATA",
|
|
22
25
|
required=True,
|
|
23
26
|
help="The input directory (relative path: e.g ./data)",
|
|
27
|
+
type=Path,
|
|
24
28
|
)
|
|
25
29
|
@click.option(
|
|
26
30
|
"--runner",
|
|
@@ -30,18 +34,20 @@ from pheval.implementations import get_implementation_resolver
|
|
|
30
34
|
help="Runner implementation (e.g exomiser-13.11)",
|
|
31
35
|
)
|
|
32
36
|
@click.option(
|
|
33
|
-
"--
|
|
37
|
+
"--tmp-dir",
|
|
34
38
|
"-m",
|
|
35
39
|
metavar="TMPDIR",
|
|
36
40
|
required=False,
|
|
37
41
|
help="The path of the temporary directory (optional)",
|
|
42
|
+
type=Path,
|
|
38
43
|
)
|
|
39
44
|
@click.option(
|
|
40
|
-
"--
|
|
45
|
+
"--output-dir",
|
|
41
46
|
"-o",
|
|
42
47
|
metavar="OUTPUTDIR",
|
|
43
48
|
required=True,
|
|
44
49
|
help="The path of the output directory",
|
|
50
|
+
type=Path,
|
|
45
51
|
)
|
|
46
52
|
@click.option(
|
|
47
53
|
"--config",
|
|
@@ -49,20 +55,39 @@ from pheval.implementations import get_implementation_resolver
|
|
|
49
55
|
metavar="CONFIG",
|
|
50
56
|
required=False,
|
|
51
57
|
help="The path of the configuration file (optional e.g config.yaml)",
|
|
58
|
+
type=Path,
|
|
52
59
|
)
|
|
53
|
-
|
|
60
|
+
@click.option(
|
|
61
|
+
"--version",
|
|
62
|
+
"-v",
|
|
63
|
+
required=False,
|
|
64
|
+
help="Version of the tool implementation.",
|
|
65
|
+
type=str,
|
|
66
|
+
)
|
|
67
|
+
def run(
|
|
68
|
+
input_dir: Path,
|
|
69
|
+
testdata_dir: Path,
|
|
70
|
+
runner: str,
|
|
71
|
+
tmp_dir: Path,
|
|
72
|
+
output_dir: Path,
|
|
73
|
+
config: Path,
|
|
74
|
+
version: str,
|
|
75
|
+
) -> None:
|
|
54
76
|
"""PhEval Runner Command Line Interface
|
|
55
|
-
|
|
56
77
|
Args:
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
input_dir (Path): The input directory (relative path: e.g exomiser-13.11)
|
|
79
|
+
testdata_dir (Path): The input directory (relative path: e.g ./data
|
|
59
80
|
runner (str): Runner implementation (e.g exomiser-13.11)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
config (
|
|
81
|
+
tmp_dir (Path): The path of the temporary directory (optional)
|
|
82
|
+
output_dir (Path): The path of the output directory
|
|
83
|
+
config (Path): The path of the configuration file (optional e.g., config.yaml)
|
|
84
|
+
version (str): The version of the tool implementation
|
|
63
85
|
"""
|
|
64
86
|
runner_class = get_implementation_resolver().lookup(runner)
|
|
65
|
-
runner_instance = runner_class(
|
|
87
|
+
runner_instance = runner_class(input_dir, testdata_dir, tmp_dir, output_dir, config, version)
|
|
88
|
+
runner_instance.build_output_directory_structure()
|
|
66
89
|
runner_instance.prepare()
|
|
67
90
|
runner_instance.run()
|
|
68
91
|
runner_instance.post_process()
|
|
92
|
+
run_metadata = runner_instance.construct_meta_data()
|
|
93
|
+
write_metadata(output_dir, run_metadata)
|
pheval/cli_pheval_utils.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"""PhEval utils Command Line Interface"""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
3
5
|
import click
|
|
4
6
|
|
|
7
|
+
from pheval.prepare.create_noisy_phenopackets import scramble_phenopackets
|
|
8
|
+
from pheval.prepare.create_spiked_vcf import spike_vcfs
|
|
9
|
+
from pheval.prepare.custom_exceptions import InputError, MutuallyExclusiveOptionError
|
|
10
|
+
from pheval.prepare.update_phenopacket import update_phenopackets
|
|
11
|
+
from pheval.utils.semsim_utils import percentage_diff, semsim_heatmap_plot
|
|
12
|
+
|
|
5
13
|
|
|
6
14
|
@click.command()
|
|
7
15
|
@click.option(
|
|
@@ -11,19 +19,228 @@ import click
|
|
|
11
19
|
metavar="FILE",
|
|
12
20
|
help="Path to the semantic similarity profile to be scrambled.",
|
|
13
21
|
)
|
|
14
|
-
def scramble_semsim():
|
|
22
|
+
def scramble_semsim(input: Path):
|
|
15
23
|
"""scramble_semsim"""
|
|
16
24
|
print("running pheval_utils::scramble_semsim command")
|
|
17
25
|
|
|
18
26
|
|
|
19
|
-
@click.command()
|
|
27
|
+
@click.command("scramble-phenopackets")
|
|
20
28
|
@click.option(
|
|
21
|
-
"--
|
|
22
|
-
"-
|
|
29
|
+
"--phenopacket-path",
|
|
30
|
+
"-p",
|
|
31
|
+
metavar="PATH",
|
|
32
|
+
help="Path to phenopacket.",
|
|
33
|
+
type=Path,
|
|
34
|
+
cls=MutuallyExclusiveOptionError,
|
|
35
|
+
mutually_exclusive=["phenopacket_dir"],
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--phenopacket-dir",
|
|
39
|
+
"-P",
|
|
40
|
+
metavar="PATH",
|
|
41
|
+
help="Path to phenopackets directory.",
|
|
42
|
+
type=Path,
|
|
43
|
+
cls=MutuallyExclusiveOptionError,
|
|
44
|
+
mutually_exclusive=["phenopacket_path"],
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--scramble-factor",
|
|
48
|
+
"-s",
|
|
49
|
+
metavar=float,
|
|
50
|
+
required=True,
|
|
51
|
+
default=0.5,
|
|
52
|
+
show_default=True,
|
|
53
|
+
help="Scramble factor for randomising phenopacket phenotypic profiles.",
|
|
54
|
+
type=float,
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--output-dir",
|
|
58
|
+
"-O",
|
|
59
|
+
metavar="PATH",
|
|
60
|
+
required=True,
|
|
61
|
+
help="Path for creation of output directory",
|
|
62
|
+
default="noisy_phenopackets",
|
|
63
|
+
type=Path,
|
|
64
|
+
)
|
|
65
|
+
def scramble_phenopackets_command(
|
|
66
|
+
phenopacket_path: Path,
|
|
67
|
+
phenopacket_dir: Path,
|
|
68
|
+
scramble_factor: float,
|
|
69
|
+
output_dir: Path,
|
|
70
|
+
):
|
|
71
|
+
"""Generate noisy phenopackets from existing ones."""
|
|
72
|
+
if phenopacket_path is None and phenopacket_dir is None:
|
|
73
|
+
raise InputError("Either a phenopacket or phenopacket directory must be specified")
|
|
74
|
+
else:
|
|
75
|
+
scramble_phenopackets(output_dir, phenopacket_path, phenopacket_dir, scramble_factor)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.command("semsim-comparison")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--semsim-left",
|
|
81
|
+
"-L",
|
|
82
|
+
required=True,
|
|
83
|
+
metavar="FILE",
|
|
84
|
+
help="Path to the first semantic similarity profile.",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--semsim-right",
|
|
88
|
+
"-R",
|
|
23
89
|
required=True,
|
|
24
90
|
metavar="FILE",
|
|
25
|
-
help="Path to the
|
|
91
|
+
help="Path to the second semantic similarity profile.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--score-column",
|
|
95
|
+
"-s",
|
|
96
|
+
required=True,
|
|
97
|
+
type=click.Choice(
|
|
98
|
+
["jaccard_similarity", "dice_similarity", "phenodigm_score"], case_sensitive=False
|
|
99
|
+
),
|
|
100
|
+
help="Score column that will be used in comparison",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--analysis",
|
|
104
|
+
"-a",
|
|
105
|
+
required=True,
|
|
106
|
+
type=click.Choice(["heatmap", "percentage_diff"], case_sensitive=False),
|
|
107
|
+
help="""There are two types of analysis:
|
|
108
|
+
heatmap - Generates a heatmap plot that shows the differences between the semantic similarity profiles using the
|
|
109
|
+
score column for this purpose. Defaults to "heatmap".
|
|
110
|
+
percentage_diff - Calculates the score column percentage difference between the semantic similarity profiles""",
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--output",
|
|
114
|
+
"-o",
|
|
115
|
+
metavar="FILE",
|
|
116
|
+
default="percentage_diff.semsim.tsv",
|
|
117
|
+
help="Output path for the difference tsv. Defaults to percentage_diff.semsim.tsv",
|
|
118
|
+
)
|
|
119
|
+
def semsim_comparison(
|
|
120
|
+
semsim_left: Path,
|
|
121
|
+
semsim_right: Path,
|
|
122
|
+
score_column: str,
|
|
123
|
+
analysis: str,
|
|
124
|
+
output: Path = "percentage_diff.semsim.tsv",
|
|
125
|
+
):
|
|
126
|
+
"""Compares two semantic similarity profiles
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
semsim-left (Path): File path of the first semantic similarity profile
|
|
130
|
+
semsim-right (Path): File path of the second semantic similarity profile
|
|
131
|
+
output (Path): Output path for the difference tsv. Defaults to "percentage_diff.semsim.tsv".
|
|
132
|
+
score_column (str): Score column that will be computed (e.g. jaccard_similarity)
|
|
133
|
+
analysis (str): There are two types of analysis:
|
|
134
|
+
heatmap - Generates a heatmap plot that shows the differences between the semantic similarity profiles using the
|
|
135
|
+
score column for this purpose. Defaults to "heatmap".
|
|
136
|
+
percentage_diff - Calculates the score column percentage difference between the semantic similarity profiles.
|
|
137
|
+
"""
|
|
138
|
+
if analysis == "heatmap":
|
|
139
|
+
return semsim_heatmap_plot(semsim_left, semsim_right, score_column)
|
|
140
|
+
if analysis == "percentage_diff":
|
|
141
|
+
percentage_diff(semsim_left, semsim_right, score_column, output)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@click.command("update-phenopackets")
|
|
145
|
+
@click.option(
|
|
146
|
+
"--phenopacket-path",
|
|
147
|
+
"-p",
|
|
148
|
+
metavar="PATH",
|
|
149
|
+
help="Path to phenopacket.",
|
|
150
|
+
type=Path,
|
|
151
|
+
cls=MutuallyExclusiveOptionError,
|
|
152
|
+
mutually_exclusive=["phenopacket_dir"],
|
|
153
|
+
)
|
|
154
|
+
@click.option(
|
|
155
|
+
"--phenopacket-dir",
|
|
156
|
+
"-P",
|
|
157
|
+
metavar="PATH",
|
|
158
|
+
help="Path to phenopacket directory for updating.",
|
|
159
|
+
type=Path,
|
|
160
|
+
cls=MutuallyExclusiveOptionError,
|
|
161
|
+
mutually_exclusive=["phenopacket_path"],
|
|
162
|
+
)
|
|
163
|
+
@click.option(
|
|
164
|
+
"--output-dir",
|
|
165
|
+
"-o",
|
|
166
|
+
metavar="PATH",
|
|
167
|
+
required=True,
|
|
168
|
+
help="Path to write phenopacket.",
|
|
169
|
+
type=Path,
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--gene-identifier",
|
|
173
|
+
"-g",
|
|
174
|
+
required=False,
|
|
175
|
+
default="ensembl_id",
|
|
176
|
+
show_default=True,
|
|
177
|
+
help="Gene identifier to add to phenopacket",
|
|
178
|
+
type=click.Choice(["ensembl_id", "entrez_id", "hgnc_id"]),
|
|
179
|
+
)
|
|
180
|
+
def update_phenopackets_command(
|
|
181
|
+
phenopacket_path: Path, phenopacket_dir: Path, output_dir: Path, gene_identifier: str
|
|
182
|
+
):
|
|
183
|
+
"""Update gene symbols and identifiers for phenopackets."""
|
|
184
|
+
if phenopacket_path is None and phenopacket_dir is None:
|
|
185
|
+
raise InputError("Either a phenopacket or phenopacket directory must be specified")
|
|
186
|
+
update_phenopackets(gene_identifier, phenopacket_path, phenopacket_dir, output_dir)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@click.command("create-spiked-vcfs")
|
|
190
|
+
@click.option(
|
|
191
|
+
"--phenopacket-path",
|
|
192
|
+
"-p",
|
|
193
|
+
metavar="PATH",
|
|
194
|
+
help="Path to phenopacket.",
|
|
195
|
+
type=Path,
|
|
196
|
+
cls=MutuallyExclusiveOptionError,
|
|
197
|
+
mutually_exclusive=["phenopacket_dir"],
|
|
198
|
+
)
|
|
199
|
+
@click.option(
|
|
200
|
+
"--phenopacket-dir",
|
|
201
|
+
"-P",
|
|
202
|
+
metavar="PATH",
|
|
203
|
+
help="Path to phenopacket directory for updating.",
|
|
204
|
+
type=Path,
|
|
205
|
+
cls=MutuallyExclusiveOptionError,
|
|
206
|
+
mutually_exclusive=["phenopacket_path"],
|
|
207
|
+
)
|
|
208
|
+
@click.option(
|
|
209
|
+
"--template-vcf-path",
|
|
210
|
+
"-t",
|
|
211
|
+
cls=MutuallyExclusiveOptionError,
|
|
212
|
+
metavar="PATH",
|
|
213
|
+
required=False,
|
|
214
|
+
help="Template VCF file",
|
|
215
|
+
mutually_exclusive=["vcf_dir"],
|
|
216
|
+
type=Path,
|
|
217
|
+
)
|
|
218
|
+
@click.option(
|
|
219
|
+
"--vcf-dir",
|
|
220
|
+
"-v",
|
|
221
|
+
cls=MutuallyExclusiveOptionError,
|
|
222
|
+
metavar="PATH",
|
|
223
|
+
help="Directory containing template VCF files",
|
|
224
|
+
mutually_exclusive=["template_vcf"],
|
|
225
|
+
type=Path,
|
|
226
|
+
)
|
|
227
|
+
@click.option(
|
|
228
|
+
"--output-dir",
|
|
229
|
+
"-O",
|
|
230
|
+
metavar="PATH",
|
|
231
|
+
required=True,
|
|
232
|
+
help="Path for creation of output directory",
|
|
233
|
+
default="vcf",
|
|
234
|
+
type=Path,
|
|
26
235
|
)
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
236
|
+
def create_spiked_vcfs_command(
|
|
237
|
+
phenopacket_path: Path,
|
|
238
|
+
phenopacket_dir: Path,
|
|
239
|
+
output_dir: Path,
|
|
240
|
+
template_vcf_path: Path = None,
|
|
241
|
+
vcf_dir: Path = None,
|
|
242
|
+
):
|
|
243
|
+
"""Spikes variants into a template VCF file for a directory of phenopackets."""
|
|
244
|
+
if phenopacket_path is None and phenopacket_dir is None:
|
|
245
|
+
raise InputError("Either a phenopacket or phenopacket directory must be specified")
|
|
246
|
+
spike_vcfs(output_dir, phenopacket_path, phenopacket_dir, template_vcf_path, vcf_dir)
|
pheval/config_parser.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from serde import serde
|
|
7
|
+
from serde.yaml import from_yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@serde
|
|
11
|
+
@dataclass
|
|
12
|
+
class InputDirConfig:
|
|
13
|
+
"""
|
|
14
|
+
Class for defining the fields within the input directory config.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
tool (str): Name of the tool implementation (e.g. exomiser/phen2gene)
|
|
18
|
+
tool_version (str): Version of the tool implementation
|
|
19
|
+
phenotype_only (bool): Whether the tool is run with HPO terms only (True) or with variant data (False)
|
|
20
|
+
tool_specific_configuration_options (Any): Tool specific configurations
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
tool: str
|
|
26
|
+
tool_version: str
|
|
27
|
+
phenotype_only: bool
|
|
28
|
+
tool_specific_configuration_options: Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_input_dir_config(input_dir: Path) -> InputDirConfig:
|
|
32
|
+
"""Reads the config file."""
|
|
33
|
+
with open(Path(input_dir).joinpath("config.yaml"), "r") as config_file:
|
|
34
|
+
config = yaml.safe_load(config_file)
|
|
35
|
+
config_file.close()
|
|
36
|
+
return from_yaml(InputDirConfig, yaml.dump(config))
|
pheval/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PHEVAL_RESULTS_DIRECTORY_SUFFIX = "_results"
|
|
@@ -13,9 +13,7 @@ def get_implementation_resolver() -> ClassResolver[PhEvalRunner]:
|
|
|
13
13
|
Returns:
|
|
14
14
|
ClassResolver[PhEvalRunner]: _description_
|
|
15
15
|
"""
|
|
16
|
-
implementation_resolver: ClassResolver[
|
|
17
|
-
PhEvalRunner
|
|
18
|
-
] = ClassResolver.from_subclasses(
|
|
16
|
+
implementation_resolver: ClassResolver[PhEvalRunner] = ClassResolver.from_subclasses(
|
|
19
17
|
PhEvalRunner,
|
|
20
18
|
suffix="Implementation",
|
|
21
19
|
)
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate_end_pos(variant_start: int, variant_ref: str) -> int:
|
|
10
|
+
"""Calculate the end position for a variant."""
|
|
11
|
+
return variant_start + len(variant_ref) - 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PhEvalGeneResult:
|
|
16
|
+
"""Minimal data required from tool-specific output for gene prioritisation."""
|
|
17
|
+
|
|
18
|
+
gene_symbol: str
|
|
19
|
+
gene_identifier: str
|
|
20
|
+
score: float
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RankedPhEvalGeneResult:
|
|
25
|
+
"""PhEval gene result with corresponding rank."""
|
|
26
|
+
|
|
27
|
+
pheval_gene_result: PhEvalGeneResult
|
|
28
|
+
rank: int
|
|
29
|
+
|
|
30
|
+
def as_dict(self):
|
|
31
|
+
"""Return PhEval gene result as dictionary."""
|
|
32
|
+
return {
|
|
33
|
+
"gene_symbol": self.pheval_gene_result.gene_symbol,
|
|
34
|
+
"gene_identifier": self.pheval_gene_result.gene_identifier,
|
|
35
|
+
"score": self.pheval_gene_result.score,
|
|
36
|
+
"rank": self.rank,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PhEvalVariantResult:
|
|
42
|
+
"""Minimal data required from tool-specific output for variant prioritisation."""
|
|
43
|
+
|
|
44
|
+
chromosome: str
|
|
45
|
+
start: int
|
|
46
|
+
end: int
|
|
47
|
+
ref: str
|
|
48
|
+
alt: str
|
|
49
|
+
score: float
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class RankedPhEvalVariantResult:
|
|
54
|
+
"""PhEval variant result with corresponding rank."""
|
|
55
|
+
|
|
56
|
+
pheval_variant_result: PhEvalVariantResult
|
|
57
|
+
rank: int
|
|
58
|
+
|
|
59
|
+
def as_dict(self):
|
|
60
|
+
"""Return PhEval variant result as dictionary."""
|
|
61
|
+
return {
|
|
62
|
+
"chromosome": self.pheval_variant_result.chromosome,
|
|
63
|
+
"start": self.pheval_variant_result.start,
|
|
64
|
+
"end": self.pheval_variant_result.end,
|
|
65
|
+
"ref": self.pheval_variant_result.ref,
|
|
66
|
+
"alt": self.pheval_variant_result.alt,
|
|
67
|
+
"score": self.pheval_variant_result.score,
|
|
68
|
+
"rank": self.rank,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SortOrder(Enum):
|
|
73
|
+
ASCENDING = 1
|
|
74
|
+
DESCENDING = 2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ResultSorter:
|
|
78
|
+
def __init__(
|
|
79
|
+
self, pheval_results: [PhEvalGeneResult] or [PhEvalVariantResult], sort_order: SortOrder
|
|
80
|
+
):
|
|
81
|
+
self.pheval_results = pheval_results
|
|
82
|
+
self.sort_order = sort_order
|
|
83
|
+
|
|
84
|
+
def _sort_by_decreasing_score(self) -> [PhEvalGeneResult] or [PhEvalVariantResult]:
|
|
85
|
+
"""Sort results in descending order."""
|
|
86
|
+
return sorted(self.pheval_results, key=operator.attrgetter("score"), reverse=True)
|
|
87
|
+
|
|
88
|
+
def _sort_by_increasing_score(self) -> [PhEvalGeneResult] or [PhEvalVariantResult]:
|
|
89
|
+
"""Sort results in ascending order."""
|
|
90
|
+
return sorted(self.pheval_results, key=operator.attrgetter("score"), reverse=False)
|
|
91
|
+
|
|
92
|
+
def sort_pheval_results(self) -> [PhEvalGeneResult] or [PhEvalVariantResult]:
|
|
93
|
+
"""Sort results with best score first."""
|
|
94
|
+
return (
|
|
95
|
+
self._sort_by_increasing_score()
|
|
96
|
+
if self.sort_order == SortOrder.ASCENDING
|
|
97
|
+
else self._sort_by_decreasing_score()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ScoreRanker:
|
|
102
|
+
rank: int = 0
|
|
103
|
+
current_score: float = float("inf")
|
|
104
|
+
count: int = 0
|
|
105
|
+
|
|
106
|
+
def __init__(self, sort_order: SortOrder):
|
|
107
|
+
self.sort_order = sort_order
|
|
108
|
+
|
|
109
|
+
def _check_rank_order(self, round_score: float) -> None:
|
|
110
|
+
"""Check the results are correctly ordered."""
|
|
111
|
+
if self.sort_order == SortOrder.ASCENDING and round_score < self.current_score != float(
|
|
112
|
+
"inf"
|
|
113
|
+
):
|
|
114
|
+
raise ValueError("Results are not correctly sorted!")
|
|
115
|
+
elif self.sort_order == SortOrder.DESCENDING and round_score > self.current_score != float(
|
|
116
|
+
"inf"
|
|
117
|
+
):
|
|
118
|
+
raise ValueError("Results are not correctly sorted!")
|
|
119
|
+
|
|
120
|
+
def rank_scores(self, round_score: float) -> int:
|
|
121
|
+
"""Add ranks to a result, equal scores are given the same rank e.g., 1,1,3."""
|
|
122
|
+
self._check_rank_order(round_score)
|
|
123
|
+
self.count += 1
|
|
124
|
+
if self.current_score == round_score:
|
|
125
|
+
return self.rank
|
|
126
|
+
self.current_score = round_score
|
|
127
|
+
self.rank = self.count
|
|
128
|
+
return self.rank
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _rank_pheval_result(
|
|
132
|
+
pheval_result: [PhEvalGeneResult] or [PhEvalVariantResult], sort_order: SortOrder
|
|
133
|
+
) -> [RankedPhEvalGeneResult] or [RankedPhEvalVariantResult]:
|
|
134
|
+
"""Ranks either a PhEval gene or variant result post-processed from a tool specific output.
|
|
135
|
+
Deals with ex aequo scores"""
|
|
136
|
+
score_ranker = ScoreRanker(sort_order)
|
|
137
|
+
ranked_result = []
|
|
138
|
+
for result in pheval_result:
|
|
139
|
+
ranked_result.append(
|
|
140
|
+
RankedPhEvalGeneResult(
|
|
141
|
+
pheval_gene_result=result, rank=score_ranker.rank_scores(result.score)
|
|
142
|
+
)
|
|
143
|
+
) if type(result) == PhEvalGeneResult else ranked_result.append(
|
|
144
|
+
RankedPhEvalVariantResult(
|
|
145
|
+
pheval_variant_result=result, rank=score_ranker.rank_scores(result.score)
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return ranked_result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _return_sort_order(sort_order_str: str) -> SortOrder:
|
|
152
|
+
"""Return the SortOrder Enum from string derived from config."""
|
|
153
|
+
try:
|
|
154
|
+
return SortOrder[sort_order_str.upper()]
|
|
155
|
+
except KeyError:
|
|
156
|
+
raise ValueError("Incompatible ordering method specified.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _create_pheval_result(
|
|
160
|
+
pheval_result: [PhEvalGeneResult] or [PhEvalVariantResult], sort_order_str: str
|
|
161
|
+
) -> [RankedPhEvalGeneResult] or [RankedPhEvalVariantResult]:
|
|
162
|
+
"""Create PhEval gene/variant result with corresponding ranks."""
|
|
163
|
+
sort_order = _return_sort_order(sort_order_str)
|
|
164
|
+
sorted_pheval_result = ResultSorter(pheval_result, sort_order).sort_pheval_results()
|
|
165
|
+
return _rank_pheval_result(sorted_pheval_result, sort_order)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write_pheval_gene_result(
|
|
169
|
+
ranked_pheval_result: [RankedPhEvalGeneResult], output_dir: Path, tool_result_path: Path
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Write ranked PhEval gene result to tsv."""
|
|
172
|
+
ranked_result = pd.DataFrame([x.as_dict() for x in ranked_pheval_result])
|
|
173
|
+
pheval_gene_output = ranked_result.loc[:, ["rank", "score", "gene_symbol", "gene_identifier"]]
|
|
174
|
+
pheval_gene_output.to_csv(
|
|
175
|
+
output_dir.joinpath(
|
|
176
|
+
"pheval_gene_results/" + tool_result_path.stem + "-pheval_gene_result.tsv"
|
|
177
|
+
),
|
|
178
|
+
sep="\t",
|
|
179
|
+
index=False,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _write_pheval_variant_result(
|
|
184
|
+
ranked_pheval_result: [RankedPhEvalVariantResult], output_dir: Path, tool_result_path: Path
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Write ranked PhEval variant result to tsv."""
|
|
187
|
+
ranked_result = pd.DataFrame([x.as_dict() for x in ranked_pheval_result])
|
|
188
|
+
pheval_variant_output = ranked_result.loc[
|
|
189
|
+
:, ["rank", "score", "chromosome", "start", "end", "ref", "alt"]
|
|
190
|
+
]
|
|
191
|
+
pheval_variant_output.to_csv(
|
|
192
|
+
output_dir.joinpath(
|
|
193
|
+
"pheval_variant_results/" + tool_result_path.stem + "-pheval_variant_result.tsv"
|
|
194
|
+
),
|
|
195
|
+
sep="\t",
|
|
196
|
+
index=False,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def generate_pheval_result(
|
|
201
|
+
pheval_result: [PhEvalGeneResult] or [PhEvalVariantResult],
|
|
202
|
+
sort_order_str: str,
|
|
203
|
+
output_dir: Path,
|
|
204
|
+
tool_result_path: Path,
|
|
205
|
+
):
|
|
206
|
+
"""Generate either a PhEval variant or PhEval gene tsv result."""
|
|
207
|
+
ranked_pheval_result = _create_pheval_result(pheval_result, sort_order_str)
|
|
208
|
+
_write_pheval_variant_result(ranked_pheval_result, output_dir, tool_result_path) if all(
|
|
209
|
+
isinstance(result, RankedPhEvalVariantResult) for result in ranked_pheval_result
|
|
210
|
+
) else _write_pheval_gene_result(ranked_pheval_result, output_dir, tool_result_path)
|
|
File without changes
|