From f79c789ae2ef0fa8f55e9ca2a78088db9dd14512 Mon Sep 17 00:00:00 2001 From: Daniel Suveges Date: Sun, 30 Jun 2024 21:13:04 +0100 Subject: [PATCH] feat(datasets): adding new variant index model (#641) * feat(variant annotation): new variant annotation schema + logic to extract from VEP * fix: typehints in function * refactor(variant annotation): migrating methods to the new schema * chore: pre-commit auto fixes [...] * refactor(variant index): sorting out new variant index dataset * chore: pre-commit auto fixes [...] * feature(vep): adding predictors to vep transcript object * fix(schema): fixing schema missing fields * fix(schema): fixing schema missing fields * fix(schema): fixing schema missing fields * fix(schema): fixing schema missing fields * chore: pre-commit auto fixes [...] * fix(annotation): array union under condition * fix: merging dbxref objects * feat(variants): updating variants to make more robust * feat: migrating methods to new variant index * adjusting variant index methods * some updates * rename v2g to variant to gene * chore: pre-commit auto fixes [...] * adding test * chore: pre-commit auto fixes [...] * fix(precommit): json file needed to rename to jsonl * fix(precommit): removing steps depending on old data model * fix(coftest): fixing variant index mock generation * fix: typo in package import * fix: sorting out conftest * refactor(gwas ingest): Updating GnomAD handling * refactor(gnomad): variant annotation removed, changed to variant index, steps updated * refactor: shuffling around gnomad logic * fix: references in tests * refactor: sorting out gnomad variant dag * refactor: cleaning configs and tests * docs(vep): adding datasource description * test(vep): adding more test to the vep parser * test(vep): tests are now running * fix: removing version suffix from pyproject and airflow config * fix: reverting DAGs - removing temporary modifications I added for testing * fix: addressing reviewer comments * refactor: fiddling with variant index annotation logic * chore: addressing comments * fix: variant cross-ref snake case * fix: correcting join strategy --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .vscode/settings.json | 5 +- config/datasets/ot_gcp.yaml | 10 +- config/step/ot_variant_annotation.yaml | 19 - config/step/ot_variant_index.yaml | 4 +- config/step/ot_variant_to_gene.yaml | 3 +- docs/assets/imgs/ensembl_logo.png | Bin 0 -> 91672 bytes .../python_api/datasets/variant_annotation.md | 9 - docs/python_api/datasources/_datasources.md | 3 +- .../datasources/ensembl/_ensembl.md | 10 + .../variant_effect_predictor_parser.md | 5 + docs/python_api/steps/ld_index.md | 4 +- .../steps/variant_annotation_step.md | 4 +- docs/python_api/steps/variant_to_gene_step.md | 2 +- poetry.lock | 1 + src/airflow/dags/gnomad_preprocess.py | 6 +- src/gentropy/assets/data/so_mappings.json | 43 + .../assets/schemas/variant_annotation.json | 215 ----- .../assets/schemas/variant_index.json | 280 ++++--- .../assets/schemas/vep_json_output.json | 531 ++++++++++++ src/gentropy/config.py | 18 +- src/gentropy/dataset/variant_index.py | 297 ++++++- src/gentropy/datasource/ensembl/__init__.py | 3 + src/gentropy/datasource/ensembl/vep_parser.py | 784 ++++++++++++++++++ src/gentropy/datasource/gnomad/variants.py | 96 +-- .../datasource/gwas_catalog/associations.py | 26 +- src/gentropy/datasource/ukbiobank/__init__.py | 2 +- .../{ld_index.py => gnomad_ingestion.py} | 60 +- src/gentropy/gwas_catalog_ingestion.py | 13 +- src/gentropy/gwas_catalog_study_inclusion.py | 21 +- src/gentropy/variant_annotation.py | 66 -- src/gentropy/variant_index.py | 43 +- src/gentropy/{v2g.py => variant_to_gene.py} | 30 +- tests/gentropy/conftest.py | 94 ++- tests/gentropy/data_samples/finucane_PIPs.npy | Bin 40128 -> 0 bytes .../GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz | Bin 491 -> 0 bytes .../data_samples/vep_consequences_sample.tsv | 45 - tests/gentropy/data_samples/vep_sample.jsonl | 2 + .../dataset/test_variant_annotation.py | 30 - tests/gentropy/dataset/test_variant_index.py | 23 +- .../datasource/ensembl/test_vep_variants.py | 145 ++++ .../test_gwas_catalog_associations.py | 14 +- tests/gentropy/test_schemas.py | 3 + 42 files changed, 2239 insertions(+), 730 deletions(-) delete mode 100644 config/step/ot_variant_annotation.yaml create mode 100644 docs/assets/imgs/ensembl_logo.png delete mode 100644 docs/python_api/datasets/variant_annotation.md create mode 100644 docs/python_api/datasources/ensembl/_ensembl.md create mode 100644 docs/python_api/datasources/ensembl/variant_effect_predictor_parser.md create mode 100644 src/gentropy/assets/data/so_mappings.json delete mode 100644 src/gentropy/assets/schemas/variant_annotation.json create mode 100644 src/gentropy/assets/schemas/vep_json_output.json create mode 100644 src/gentropy/datasource/ensembl/__init__.py create mode 100644 src/gentropy/datasource/ensembl/vep_parser.py rename src/gentropy/{ld_index.py => gnomad_ingestion.py} (54%) delete mode 100644 src/gentropy/variant_annotation.py rename src/gentropy/{v2g.py => variant_to_gene.py} (81%) delete mode 100644 tests/gentropy/data_samples/finucane_PIPs.npy delete mode 100644 tests/gentropy/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz delete mode 100644 tests/gentropy/data_samples/vep_consequences_sample.tsv create mode 100644 tests/gentropy/data_samples/vep_sample.jsonl delete mode 100644 tests/gentropy/dataset/test_variant_annotation.py create mode 100644 tests/gentropy/datasource/ensembl/test_vep_variants.py diff --git a/.vscode/settings.json b/.vscode/settings.json index aaafda97f..ecc456889 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,8 @@ "python.testing.pytestEnabled": true, "mypy-type-checker.severity": { "error": "Information" - } + }, + "yaml.extension.recommendations": false, + "workbench.remoteIndicator.showExtensionRecommendations": false, + "extensions.ignoreRecommendations": true } diff --git a/config/datasets/ot_gcp.yaml b/config/datasets/ot_gcp.yaml index c8f67d418..c61863b86 100644 --- a/config/datasets/ot_gcp.yaml +++ b/config/datasets/ot_gcp.yaml @@ -37,13 +37,13 @@ gnomad_public_bucket: gs://gcp-public-data--gnomad/release/ ld_matrix_template: ${datasets.gnomad_public_bucket}/2.1.1/ld/gnomad.genomes.r2.1.1.{POP}.common.adj.ld.bm ld_index_raw_template: ${datasets.gnomad_public_bucket}/2.1.1/ld/gnomad.genomes.r2.1.1.{POP}.common.ld.variant_indices.ht liftover_ht_path: ${datasets.gnomad_public_bucket}/2.1.1/liftover_grch38/ht/genomes/gnomad.genomes.r2.1.1.sites.liftover_grch38.ht -# variant_annotation +# GnomAD variant set: gnomad_genomes_path: ${datasets.gnomad_public_bucket}4.0/ht/genomes/gnomad.genomes.v4.0.sites.ht/ # Others chain_38_37: gs://hail-common/references/grch38_to_grch37.over.chain.gz chain_37_38: ${datasets.static_assets}/grch37_to_grch38.over.chain -vep_consequences: ${datasets.static_assets}/vep_consequences.tsv +vep_consequences: ${datasets.static_assets}/variant_consequence_to_score.tsv anderson: ${datasets.static_assets}/andersson2014/enhancer_tss_associations.bed javierre: ${datasets.static_assets}/javierre_2016_preprocessed jung: ${datasets.static_assets}/jung2019_pchic_tableS3.csv @@ -55,7 +55,7 @@ finngen_finemapping_results_path: ${datasets.inputs}/Finngen_susie_finemapping_r finngen_finemapping_summaries_path: ${datasets.inputs}/Finngen_susie_finemapping_r10/Finngen_susie_credset_summary_r10.tsv # Dev output datasets -variant_annotation: ${datasets.outputs}/variant_annotation +gnomad_variants: ${datasets.outputs}/gnomad_variants study_locus: ${datasets.outputs}/study_locus summary_statistics: ${datasets.outputs}/summary_statistics study_locus_overlap: ${datasets.outputs}/study_locus_overlap @@ -68,6 +68,8 @@ catalog_study_locus: ${datasets.study_locus}/catalog_study_locus from_sumstats_study_locus: ${datasets.study_locus}/from_sumstats from_sumstats_pics: ${datasets.credible_set}/from_sumstats +vep_output_path: gs://genetics_etl_python_playground/vep/full_variant_index_vcf + # ETL output datasets: l2g_gold_standard_curation: ${datasets.release_folder}/locus_to_gene_gold_standard.json l2g_model: ${datasets.release_folder}/locus_to_gene_model/classifier.skops @@ -78,4 +80,4 @@ study_index: ${datasets.release_folder}/study_index variant_index: ${datasets.release_folder}/variant_index credible_set: ${datasets.release_folder}/credible_set gene_index: ${datasets.release_folder}/gene_index -v2g: ${datasets.release_folder}/variant_to_gene +variant_to_gene: ${datasets.release_folder}/variant_to_gene diff --git a/config/step/ot_variant_annotation.yaml b/config/step/ot_variant_annotation.yaml deleted file mode 100644 index 55a9503ce..000000000 --- a/config/step/ot_variant_annotation.yaml +++ /dev/null @@ -1,19 +0,0 @@ -defaults: - - variant_annotation - -variant_annotation_path: ${datasets.variant_annotation} -gnomad_genomes_path: ${datasets.gnomad_genomes_path} -chain_38_37: ${datasets.chain_38_37} -gnomad_variant_populations: - - afr # African-American - - amr # American Admixed/Latino - - ami # Amish ancestry - - asj # Ashkenazi Jewish - - eas # East Asian - - fin # Finnish - - nfe # Non-Finnish European - - mid # Middle Eastern - - sas # South Asian - - remaining # Other -# The version will of the gnomad will be inferred from ld_matrix_template and appended to the ld_index_out. -use_version_from_input: true diff --git a/config/step/ot_variant_index.yaml b/config/step/ot_variant_index.yaml index 3834196b2..00b6b1602 100644 --- a/config/step/ot_variant_index.yaml +++ b/config/step/ot_variant_index.yaml @@ -1,6 +1,6 @@ defaults: - variant_index -variant_annotation_path: ${datasets.variant_annotation} -credible_set_path: ${datasets.credible_set} +vep_output_json_path: ${datasets.vep_output_path} +gnomad_variant_annotations_path: ${datasets.gnomad_variants} variant_index_path: ${datasets.variant_index} diff --git a/config/step/ot_variant_to_gene.yaml b/config/step/ot_variant_to_gene.yaml index 1ac6d2fbe..7187a0625 100644 --- a/config/step/ot_variant_to_gene.yaml +++ b/config/step/ot_variant_to_gene.yaml @@ -2,7 +2,6 @@ defaults: - variant_to_gene variant_index_path: ${datasets.variant_index} -variant_annotation_path: ${datasets.variant_annotation} gene_index_path: ${datasets.gene_index} vep_consequences_path: ${datasets.vep_consequences} liftover_chain_file_path: ${datasets.chain_37_38} @@ -11,4 +10,4 @@ interval_sources: javierre: ${datasets.javierre} jung: ${datasets.jung} thurman: ${datasets.thurman} -v2g_path: ${datasets.v2g} +v2g_path: ${datasets.variant_to_gene} diff --git a/docs/assets/imgs/ensembl_logo.png b/docs/assets/imgs/ensembl_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..83fd25751297e283995012325826986c6d36418e GIT binary patch literal 91672 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 + +

Ensembl

+ + +[Ensembl](https://www.ensembl.org/index.html) provides a diverse set of genetic data Gentropy takes advantage of including gene set, and variant annotations. diff --git a/docs/python_api/datasources/ensembl/variant_effect_predictor_parser.md b/docs/python_api/datasources/ensembl/variant_effect_predictor_parser.md new file mode 100644 index 000000000..c956ed3e1 --- /dev/null +++ b/docs/python_api/datasources/ensembl/variant_effect_predictor_parser.md @@ -0,0 +1,5 @@ +--- +title: Variant effector parser +--- + +::: gentropy.datasource.ensembl.vep_parser.VariantEffectPredictorParser diff --git a/docs/python_api/steps/ld_index.md b/docs/python_api/steps/ld_index.md index bf8b9b58e..d8f61a528 100644 --- a/docs/python_api/steps/ld_index.md +++ b/docs/python_api/steps/ld_index.md @@ -1,5 +1,5 @@ --- -title: ld_index +title: GnomAD Linkage data ingestion --- -::: gentropy.ld_index.LDIndexStep +::: gentropy.gnomad_ingestion.LDIndexStep diff --git a/docs/python_api/steps/variant_annotation_step.md b/docs/python_api/steps/variant_annotation_step.md index e65a071b2..2b5582df4 100644 --- a/docs/python_api/steps/variant_annotation_step.md +++ b/docs/python_api/steps/variant_annotation_step.md @@ -1,5 +1,5 @@ --- -title: variant_annotation +title: GnomAD variant data ingestion --- -::: gentropy.variant_annotation.VariantAnnotationStep +::: gentropy.gnomad_ingestion.GnomadVariantIndexStep diff --git a/docs/python_api/steps/variant_to_gene_step.md b/docs/python_api/steps/variant_to_gene_step.md index 1a3e56af8..db1c1fd20 100644 --- a/docs/python_api/steps/variant_to_gene_step.md +++ b/docs/python_api/steps/variant_to_gene_step.md @@ -2,4 +2,4 @@ title: variant_to_gene --- -::: gentropy.v2g.V2GStep +::: gentropy.variant_to_gene.V2GStep diff --git a/poetry.lock b/poetry.lock index b181572d4..432c3d4cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6881,6 +6881,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/src/airflow/dags/gnomad_preprocess.py b/src/airflow/dags/gnomad_preprocess.py index 22e7fa056..03962bec5 100644 --- a/src/airflow/dags/gnomad_preprocess.py +++ b/src/airflow/dags/gnomad_preprocess.py @@ -1,4 +1,4 @@ -"""Airflow DAG for the Preprocess part of the pipeline.""" +"""Airflow DAG for the Preprocess GnomAD datasets - LD index and GnomAD variant set.""" from __future__ import annotations @@ -11,13 +11,13 @@ ALL_STEPS = [ "ot_ld_index", - "ot_variant_annotation", + "ot_gnomad_variants", ] with DAG( dag_id=Path(__file__).stem, - description="Open Targets Genetics — Preprocess", + description="Open Targets Genetics — GnomAD Preprocess", default_args=common.shared_dag_args, **common.shared_dag_kwargs, ): diff --git a/src/gentropy/assets/data/so_mappings.json b/src/gentropy/assets/data/so_mappings.json new file mode 100644 index 000000000..8a087b6f7 --- /dev/null +++ b/src/gentropy/assets/data/so_mappings.json @@ -0,0 +1,43 @@ +{ + "transcript_ablation": "SO_0001893", + "splice_acceptor_variant": "SO_0001574", + "splice_donor_variant": "SO_0001575", + "stop_gained": "SO_0001587", + "frameshift_variant": "SO_0001589", + "stop_lost": "SO_0001578", + "start_lost": "SO_0002012", + "transcript_amplification": "SO_0001889", + "feature_elongation": "SO_0001907", + "feature_truncation": "SO_0001906", + "inframe_insertion": "SO_0001821", + "inframe_deletion": "SO_0001822", + "missense_variant": "SO_0001583", + "protein_altering_variant": "SO_0001818", + "splice_donor_5th_base_variant": "SO_0001787", + "splice_region_variant": "SO_0001630", + "splice_donor_region_variant": "SO_0002170", + "splice_polypyrimidine_tract_variant": "SO_0002169", + "incomplete_terminal_codon_variant": "SO_0001626", + "start_retained_variant": "SO_0002019", + "stop_retained_variant": "SO_0001567", + "synonymous_variant": "SO_0001819", + "coding_sequence_variant": "SO_0001580", + "mature_miRNA_variant": "SO_0001620", + "5_prime_UTR_variant": "SO_0001623", + "3_prime_UTR_variant": "SO_0001624", + "non_coding_transcript_exon_variant": "SO_0001792", + "intron_variant": "SO_0001627", + "NMD_transcript_variant": "SO_0001621", + "non_coding_transcript_variant": "SO_0001619", + "coding_transcript_variant": "SO_0001968", + "upstream_gene_variant": "SO_0001631", + "downstream_gene_variant": "SO_0001632", + "TFBS_ablation": "SO_0001895", + "TFBS_amplification": "SO_0001892", + "TF_binding_site_variant": "SO_0001782", + "regulatory_region_ablation": "SO_0001894", + "regulatory_region_amplification": "SO_0001891", + "regulatory_region_variant": "SO_0001566", + "intergenic_variant": "SO_0001628", + "sequence_variant": "SO_0001060" +} diff --git a/src/gentropy/assets/schemas/variant_annotation.json b/src/gentropy/assets/schemas/variant_annotation.json deleted file mode 100644 index ab8767389..000000000 --- a/src/gentropy/assets/schemas/variant_annotation.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "type": "struct", - "fields": [ - { - "name": "variantId", - "type": "string", - "nullable": false, - "metadata": {} - }, - { - "name": "chromosome", - "type": "string", - "nullable": false, - "metadata": {} - }, - { - "name": "position", - "type": "integer", - "nullable": false, - "metadata": {} - }, - { - "name": "referenceAllele", - "type": "string", - "nullable": false, - "metadata": {} - }, - { - "name": "alternateAllele", - "type": "string", - "nullable": false, - "metadata": {} - }, - { - "name": "chromosomeB37", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "positionB37", - "type": "integer", - "nullable": true, - "metadata": {} - }, - { - "name": "alleleType", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "rsIds", - "type": { - "type": "array", - "elementType": "string", - "containsNull": true - }, - "nullable": true, - "metadata": {} - }, - { - "name": "alleleFrequencies", - "type": { - "type": "array", - "elementType": { - "type": "struct", - "fields": [ - { - "name": "populationName", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "alleleFrequency", - "type": "double", - "nullable": true, - "metadata": {} - } - ] - }, - "containsNull": true - }, - "nullable": false, - "metadata": {} - }, - { - "name": "inSilicoPredictors", - "nullable": false, - "metadata": {}, - "type": { - "type": "struct", - "fields": [ - { - "name": "cadd", - "nullable": true, - "metadata": {}, - "type": { - "type": "struct", - "fields": [ - { - "name": "raw", - "type": "float", - "nullable": true, - "metadata": {} - }, - { - "name": "phred", - "type": "float", - "nullable": true, - "metadata": {} - } - ] - } - }, - { - "name": "revelMax", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "spliceaiDsMax", - "type": "float", - "nullable": true, - "metadata": {} - }, - { - "name": "pangolinLargestDs", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "phylop", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "siftMax", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "polyphenMax", - "type": "double", - "nullable": true, - "metadata": {} - } - ] - } - }, - { - "name": "vep", - "type": { - "type": "struct", - "fields": [ - { - "name": "mostSevereConsequence", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "transcriptConsequences", - "type": { - "type": "array", - "elementType": { - "type": "struct", - "fields": [ - { - "name": "aminoAcids", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "consequenceTerms", - "type": { - "type": "array", - "elementType": "string", - "containsNull": true - }, - "nullable": true, - "metadata": {} - }, - { - "name": "geneId", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "lof", - "type": "string", - "nullable": true, - "metadata": {} - } - ] - }, - "containsNull": true - }, - "nullable": true, - "metadata": {} - } - ] - }, - "nullable": false, - "metadata": {} - } - ] -} diff --git a/src/gentropy/assets/schemas/variant_index.json b/src/gentropy/assets/schemas/variant_index.json index c6a3702c9..d9be4e50f 100644 --- a/src/gentropy/assets/schemas/variant_index.json +++ b/src/gentropy/assets/schemas/variant_index.json @@ -1,53 +1,188 @@ { - "type": "struct", "fields": [ { + "metadata": {}, "name": "variantId", - "type": "string", "nullable": false, - "metadata": {} + "type": "string" }, { + "metadata": {}, "name": "chromosome", - "type": "string", "nullable": false, - "metadata": {} + "type": "string" }, { + "metadata": {}, "name": "position", - "type": "integer", "nullable": false, - "metadata": {} + "type": "integer" }, { + "metadata": {}, "name": "referenceAllele", - "type": "string", "nullable": false, - "metadata": {} + "type": "string" }, { + "metadata": {}, "name": "alternateAllele", - "type": "string", "nullable": false, - "metadata": {} + "type": "string" }, { - "name": "chromosomeB37", - "type": "string", - "nullable": true, - "metadata": {} + "metadata": {}, + "name": "inSilicoPredictors", + "nullable": false, + "type": { + "containsNull": true, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "method", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "assessment", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "score", + "nullable": true, + "type": "float" + }, + { + "metadata": {}, + "name": "assessmentFlag", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "targetId", + "nullable": true, + "type": "string" + } + ], + "type": "struct" + }, + "type": "array" + } + }, + { + "metadata": {}, + "name": "mostSevereConsequenceId", + "nullable": false, + "type": "string" }, { - "name": "positionB37", - "type": "integer", + "metadata": {}, + "name": "transcriptConsequences", "nullable": true, - "metadata": {} + "type": { + "containsNull": false, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "variantFunctionalConsequenceIds", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "aminoAcidChange", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "uniprotAccessions", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "isEnsemblCanonical", + "nullable": false, + "type": "boolean" + }, + { + "metadata": {}, + "name": "codons", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "distance", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "targetId", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "impact", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "lofteePrediction", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "siftPrediction", + "nullable": true, + "type": "float" + }, + { + "metadata": {}, + "name": "polyphenPrediction", + "nullable": true, + "type": "float" + }, + { + "metadata": {}, + "name": "transcriptId", + "nullable": true, + "type": "string" + } + ], + "type": "struct" + }, + "type": "array" + } }, { - "name": "alleleType", - "type": "string", - "nullable": false, - "metadata": {} + "metadata": {}, + "name": "rsIds", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } }, { "name": "alleleFrequencies", @@ -76,88 +211,31 @@ "metadata": {} }, { - "name": "inSilicoPredictors", - "nullable": false, "metadata": {}, + "name": "dbXrefs", + "nullable": false, "type": { - "type": "struct", - "fields": [ - { - "name": "cadd", - "nullable": true, - "metadata": {}, - "type": { - "type": "struct", - "fields": [ - { - "name": "raw", - "type": "float", - "nullable": true, - "metadata": {} - }, - { - "name": "phred", - "type": "float", - "nullable": true, - "metadata": {} - } - ] + "containsNull": true, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "id", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "source", + "nullable": true, + "type": "string" } - }, - { - "name": "revelMax", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "spliceaiDsMax", - "type": "float", - "nullable": true, - "metadata": {} - }, - { - "name": "pangolinLargestDs", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "phylop", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "siftMax", - "type": "double", - "nullable": true, - "metadata": {} - }, - { - "name": "polyphenMax", - "type": "double", - "nullable": true, - "metadata": {} - } - ] + ], + "type": "struct" + }, + "type": "array" } - }, - { - "name": "mostSevereConsequence", - "type": "string", - "nullable": true, - "metadata": {} - }, - { - "name": "rsIds", - "type": { - "type": "array", - "elementType": "string", - "containsNull": true - }, - "nullable": true, - "metadata": {} } - ] + ], + "type": "struct" } diff --git a/src/gentropy/assets/schemas/vep_json_output.json b/src/gentropy/assets/schemas/vep_json_output.json new file mode 100644 index 000000000..ecad3ea1e --- /dev/null +++ b/src/gentropy/assets/schemas/vep_json_output.json @@ -0,0 +1,531 @@ +{ + "fields": [ + { + "metadata": {}, + "name": "allele_string", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "assembly_name", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "intergenic_consequences", + "nullable": true, + "type": { + "containsNull": true, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "cadd_phred", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "cadd_raw", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "consequence_terms", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "impact", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "variant_allele", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "gene_id", + "nullable": true, + "type": "string" + } + ], + "type": "struct" + }, + "type": "array" + } + }, + { + "metadata": {}, + "name": "colocated_variants", + "nullable": true, + "type": { + "containsNull": true, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "allele_string", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "clin_sig", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "clin_sig_allele", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "end", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "id", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "phenotype_or_disease", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "pubmed", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "long", + "type": "array" + } + }, + { + "metadata": {}, + "name": "seq_region_name", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "start", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "strand", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "var_synonyms", + "nullable": true, + "type": { + "fields": [ + { + "metadata": {}, + "name": "ClinVar", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "LMDD", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "OIVD", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "OMIM", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "double", + "type": "array" + } + }, + { + "metadata": {}, + "name": "PharmGKB", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "PhenCode", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "UniProt", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "dbPEX", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + } + ], + "type": "struct" + } + } + ], + "type": "struct" + }, + "type": "array" + } + }, + { + "metadata": {}, + "name": "end", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "id", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "input", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "most_severe_consequence", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "seq_region_name", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "start", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "strand", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "transcript_consequences", + "nullable": true, + "type": { + "containsNull": true, + "elementType": { + "fields": [ + { + "metadata": {}, + "name": "alphamissense", + "nullable": true, + "type": { + "fields": [ + { + "metadata": {}, + "name": "am_class", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "am_pathogenicity", + "nullable": true, + "type": "double" + } + ], + "type": "struct" + } + }, + { + "metadata": {}, + "name": "amino_acids", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "cadd_phred", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "cadd_raw", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "canonical", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "cdna_end", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "cdna_start", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "cds_end", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "cds_start", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "codons", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "consequence_terms", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "distance", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "flags", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "gene_id", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "impact", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "lof", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "lof_filter", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "lof_flags", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "lof_info", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "polyphen_prediction", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "polyphen_score", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "protein_end", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "protein_start", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "sift_prediction", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "sift_score", + "nullable": true, + "type": "double" + }, + { + "metadata": {}, + "name": "strand", + "nullable": true, + "type": "long" + }, + { + "metadata": {}, + "name": "swissprot", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "transcript_id", + "nullable": true, + "type": "string" + }, + { + "metadata": {}, + "name": "trembl", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "uniparc", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "uniprot_isoform", + "nullable": true, + "type": { + "containsNull": true, + "elementType": "string", + "type": "array" + } + }, + { + "metadata": {}, + "name": "variant_allele", + "nullable": true, + "type": "string" + } + ], + "type": "struct" + }, + "type": "array" + } + } + ], + "type": "struct" +} diff --git a/src/gentropy/config.py b/src/gentropy/config.py index 4ebf4cbe8..f7ba73e98 100644 --- a/src/gentropy/config.py +++ b/src/gentropy/config.py @@ -180,7 +180,7 @@ class LDIndexConfig(StepConfig): ] ) use_version_from_input: bool = False - _target_: str = "gentropy.ld_index.LDIndexStep" + _target_: str = "gentropy.gnomad_ingestion.LDIndexStep" @dataclass @@ -302,8 +302,8 @@ class UkbPppEurConfig(StepConfig): @dataclass -class VariantAnnotationConfig(StepConfig): - """Variant annotation step configuration.""" +class GnomadVariantConfig(StepConfig): + """Gnomad variant ingestion step configuration.""" session: Any = field( default_factory=lambda: { @@ -312,7 +312,6 @@ class VariantAnnotationConfig(StepConfig): ) variant_annotation_path: str = MISSING gnomad_genomes_path: str = "gs://gcp-public-data--gnomad/release/4.0/ht/genomes/gnomad.genomes.v4.0.sites.ht/" - chain_38_37: str = "gs://hail-common/references/grch38_to_grch37.over.chain.gz" gnomad_variant_populations: list[str] = field( default_factory=lambda: [ "afr", # African-American @@ -328,16 +327,16 @@ class VariantAnnotationConfig(StepConfig): ] ) use_version_from_input: bool = False - _target_: str = "gentropy.variant_annotation.VariantAnnotationStep" + _target_: str = "gentropy.gnomad_ingestion.GnomadVariantIndexStep" @dataclass class VariantIndexConfig(StepConfig): """Variant index step configuration.""" - variant_annotation_path: str = MISSING - credible_set_path: str = MISSING + vep_output_json_path: str = MISSING variant_index_path: str = MISSING + gnomad_variant_annotations_path: str | None = None _target_: str = "gentropy.variant_index.VariantIndexStep" @@ -346,7 +345,6 @@ class VariantToGeneConfig(StepConfig): """V2G step configuration.""" variant_index_path: str = MISSING - variant_annotation_path: str = MISSING gene_index_path: str = MISSING vep_consequences_path: str = MISSING liftover_chain_file_path: str = MISSING @@ -371,7 +369,7 @@ class VariantToGeneConfig(StepConfig): ) interval_sources: Dict[str, str] = field(default_factory=dict) v2g_path: str = MISSING - _target_: str = "gentropy.v2g.V2GStep" + _target_: str = "gentropy.variant_to_gene.V2GStep" @dataclass @@ -491,8 +489,8 @@ def register_config() -> None: ) cs.store(group="step", name="pics", node=PICSConfig) + cs.store(group="step", name="variant_annotation", node=GnomadVariantConfig) cs.store(group="step", name="ukb_ppp_eur_sumstat_preprocess", node=UkbPppEurConfig) - cs.store(group="step", name="variant_annotation", node=VariantAnnotationConfig) cs.store(group="step", name="variant_index", node=VariantIndexConfig) cs.store(group="step", name="variant_to_gene", node=VariantToGeneConfig) cs.store( diff --git a/src/gentropy/dataset/variant_index.py b/src/gentropy/dataset/variant_index.py index 4bafbc288..a9e24dcab 100644 --- a/src/gentropy/dataset/variant_index.py +++ b/src/gentropy/dataset/variant_index.py @@ -1,78 +1,295 @@ -"""Variant index dataset.""" +"""Dataset definition for variant annotation.""" + from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -import pyspark.sql.functions as f +from pyspark.sql import functions as f from gentropy.common.schemas import parse_spark_schema -from gentropy.common.spark_helpers import nullify_empty_array +from gentropy.common.spark_helpers import ( + get_record_with_maximum_value, + normalise_column, +) from gentropy.dataset.dataset import Dataset -from gentropy.dataset.study_locus import StudyLocus +from gentropy.dataset.gene_index import GeneIndex +from gentropy.dataset.v2g import V2G if TYPE_CHECKING: + from pyspark.sql import Column, DataFrame from pyspark.sql.types import StructType - from gentropy.dataset.variant_annotation import VariantAnnotation - @dataclass class VariantIndex(Dataset): - """Variant index dataset. - - Variant index dataset is the result of intersecting the variant annotation dataset with the variants with V2D available information. - """ + """Dataset for representing variants and methods applied on them.""" @classmethod def get_schema(cls: type[VariantIndex]) -> StructType: - """Provides the schema for the VariantIndex dataset. + """Provides the schema for the variant index dataset. Returns: StructType: Schema for the VariantIndex dataset """ return parse_spark_schema("variant_index.json") - @classmethod - def from_variant_annotation( - cls: type[VariantIndex], - variant_annotation: VariantAnnotation, - study_locus: StudyLocus, + def add_annotation( + self: VariantIndex, annotation_source: VariantIndex ) -> VariantIndex: - """Initialise VariantIndex from pre-existing variant annotation dataset. + """Import annotation from an other variant index dataset. + + At this point the annotation can be extended with extra cross-references, + in-silico predictions and allele frequencies. Args: - variant_annotation (VariantAnnotation): Variant annotation dataset - study_locus (StudyLocus): Study locus dataset with the variants to intersect with the variant annotation dataset + annotation_source (VariantIndex): Annotation to add to the dataset + + Returns: + VariantIndex: VariantIndex dataset with the annotation added + """ + # Columns in the source dataset: + variant_index_columns = [ + # Combining cross-references: + f.array_union(f.col("dbXrefs"), f.col("annotation_dbXrefs")) + if row == "dbXrefs" + # Combining in silico predictors: + else f.array_union( + f.col("inSilicoPredictors"), f.col("annotation_inSilicoPredictors") + ) + if row == "inSilicoPredictors" + # Combining allele frequencies: + else f.array_union( + f.col("alleleFrequencies"), f.col("annotation_alleleFrequencies") + ) + if row == "alleleFrequencies" + # Carrying over all other columns: + else row + for row in self.df.columns + ] + + # Rename columns in the annotation source to avoid conflicts: + annotation = annotation_source.df.select( + *[ + f.col(col).alias(f"annotation_{col}") if col != "variantId" else col + for col in annotation_source.df.columns + ] + ) + + # Join the annotation to the dataset: + return VariantIndex( + _df=( + annotation.join( + f.broadcast(self.df), on="variantId", how="right" + ).select(*variant_index_columns) + ), + _schema=self.schema, + ) + + def max_maf(self: VariantIndex) -> Column: + """Maximum minor allele frequency accross all populations assuming all variants biallelic. Returns: - VariantIndex: Variant index dataset + Column: Maximum minor allele frequency accross all populations. + + Raises: + ValueError: Allele frequencies are not present in the dataset. """ - unchanged_cols = [ + if "alleleFrequencies" not in self.df.columns: + raise ValueError("Allele frequencies are not present in the dataset.") + + return f.array_max( + f.transform( + self.df.alleleFrequencies, + lambda af: f.when( + af.alleleFrequency > 0.5, 1 - af.alleleFrequency + ).otherwise(af.alleleFrequency), + ) + ) + + def filter_by_variant(self: VariantIndex, df: DataFrame) -> VariantIndex: + """Filter variant annotation dataset by a variant dataframe. + + Args: + df (DataFrame): A dataframe of variants + + Returns: + VariantIndex: A filtered variant annotation dataset + """ + join_columns = ["variantId", "chromosome"] + + assert all( + col in df.columns for col in join_columns + ), "The variant dataframe must contain the columns 'variantId' and 'chromosome'." + + return VariantIndex( + _df=self._df.join( + f.broadcast(df.select(*join_columns).distinct()), + on=join_columns, + how="inner", + ), + _schema=self.schema, + ) + + def get_transcript_consequence_df( + self: VariantIndex, gene_index: GeneIndex | None = None + ) -> DataFrame: + """Dataframe of exploded transcript consequences. + + Optionally the trancript consequences can be reduced to the universe of a gene index. + + Args: + gene_index (GeneIndex | None): A gene index. Defaults to None. + + Returns: + DataFrame: A dataframe exploded by transcript consequences with the columns variantId, chromosome, transcriptConsequence + """ + # exploding the array removes records without VEP annotation + transript_consequences = self.df.withColumn( + "transcriptConsequence", f.explode("transcriptConsequences") + ).select( "variantId", "chromosome", "position", - "referenceAllele", - "alternateAllele", - "chromosomeB37", - "positionB37", - "alleleType", - "alleleFrequencies", - "inSilicoPredictors", - ] - va_slimmed = variant_annotation.filter_by_variant_df( - study_locus.unique_variants_in_locus() + "transcriptConsequence", + f.col("transcriptConsequence.targetId").alias("geneId"), ) - return cls( + if gene_index: + transript_consequences = transript_consequences.join( + f.broadcast(gene_index.df), + on=["chromosome", "geneId"], + ) + return transript_consequences + + def get_distance_to_tss( + self: VariantIndex, + gene_index: GeneIndex, + max_distance: int = 500_000, + ) -> V2G: + """Extracts variant to gene assignments for variants falling within a window of a gene's TSS. + + Args: + gene_index (GeneIndex): A gene index to filter by. + max_distance (int): The maximum distance from the TSS to consider. Defaults to 500_000. + + Returns: + V2G: variant to gene assignments with their distance to the TSS + """ + return V2G( + _df=( + self.df.alias("variant") + .join( + f.broadcast(gene_index.locations_lut()).alias("gene"), + on=[ + f.col("variant.chromosome") == f.col("gene.chromosome"), + f.abs(f.col("variant.position") - f.col("gene.tss")) + <= max_distance, + ], + how="inner", + ) + .withColumn( + "distance", f.abs(f.col("variant.position") - f.col("gene.tss")) + ) + .withColumn( + "inverse_distance", + max_distance - f.col("distance"), + ) + .transform(lambda df: normalise_column(df, "inverse_distance", "score")) + .select( + "variantId", + f.col("variant.chromosome").alias("chromosome"), + "distance", + "geneId", + "score", + f.lit("distance").alias("datatypeId"), + f.lit("canonical_tss").alias("datasourceId"), + ) + ), + _schema=V2G.get_schema(), + ) + + def get_plof_v2g(self: VariantIndex, gene_index: GeneIndex) -> V2G: + """Creates a dataset with variant to gene assignments with a flag indicating if the variant is predicted to be a loss-of-function variant by the LOFTEE algorithm. + + Optionally the trancript consequences can be reduced to the universe of a gene index. + + Args: + gene_index (GeneIndex): A gene index to filter by. + + Returns: + V2G: variant to gene assignments from the LOFTEE algorithm + """ + return V2G( _df=( - va_slimmed.df.select( - *unchanged_cols, - f.col("vep.mostSevereConsequence").alias("mostSevereConsequence"), - # filters/rsid are arrays that can be empty, in this case we convert them to null - nullify_empty_array(f.col("rsIds")).alias("rsIds"), + self.get_transcript_consequence_df(gene_index) + .filter(f.col("transcriptConsequence.lofteePrediction").isNotNull()) + .withColumn( + "isHighQualityPlof", + f.when( + f.col("transcriptConsequence.lofteePrediction") == "HC", True + ).when( + f.col("transcriptConsequence.lofteePrediction") == "LC", False + ), + ) + .withColumn( + "score", + f.when(f.col("isHighQualityPlof"), 1.0).when( + ~f.col("isHighQualityPlof"), 0 + ), + ) + .select( + "variantId", + "chromosome", + "geneId", + "isHighQualityPlof", + f.col("score"), + f.lit("vep").alias("datatypeId"), + f.lit("loftee").alias("datasourceId"), + ) + ), + _schema=V2G.get_schema(), + ) + + def get_most_severe_transcript_consequence( + self: VariantIndex, + vep_consequences: DataFrame, + gene_index: GeneIndex, + ) -> V2G: + """Creates a dataset with variant to gene assignments based on VEP's predicted consequence of the transcript. + + Optionally the trancript consequences can be reduced to the universe of a gene index. + + Args: + vep_consequences (DataFrame): A dataframe of VEP consequences + gene_index (GeneIndex): A gene index to filter by. Defaults to None. + + Returns: + V2G: High and medium severity variant to gene assignments + """ + return V2G( + _df=self.get_transcript_consequence_df(gene_index) + .select( + "variantId", + "chromosome", + f.col("transcriptConsequence.targetId").alias("geneId"), + f.explode( + "transcriptConsequence.variantFunctionalConsequenceIds" + ).alias("variantFunctionalConsequenceId"), + f.lit("vep").alias("datatypeId"), + f.lit("variantConsequence").alias("datasourceId"), + ) + .join( + f.broadcast(vep_consequences), + on="variantFunctionalConsequenceId", + how="inner", + ) + .drop("label") + .filter(f.col("score") != 0) + # A variant can have multiple predicted consequences on a transcript, the most severe one is selected + .transform( + lambda df: get_record_with_maximum_value( + df, ["variantId", "geneId"], "score" ) - .repartition(400, "chromosome") - .sortWithinPartitions("chromosome", "position") ), - _schema=cls.get_schema(), + _schema=V2G.get_schema(), ) diff --git a/src/gentropy/datasource/ensembl/__init__.py b/src/gentropy/datasource/ensembl/__init__.py new file mode 100644 index 000000000..e20ce9a95 --- /dev/null +++ b/src/gentropy/datasource/ensembl/__init__.py @@ -0,0 +1,3 @@ +"""Ensembl's Variant Effect Predictor datasource.""" + +from __future__ import annotations diff --git a/src/gentropy/datasource/ensembl/vep_parser.py b/src/gentropy/datasource/ensembl/vep_parser.py new file mode 100644 index 000000000..fcade35b1 --- /dev/null +++ b/src/gentropy/datasource/ensembl/vep_parser.py @@ -0,0 +1,784 @@ +"""Generating variant index based on Esembl's Variant Effect Predictor output.""" + +from __future__ import annotations + +import importlib.resources as pkg_resources +import json +from itertools import chain +from typing import TYPE_CHECKING, Dict, List + +from pyspark.sql import SparkSession +from pyspark.sql import functions as f +from pyspark.sql import types as t + +from gentropy.assets import data +from gentropy.common.schemas import parse_spark_schema +from gentropy.common.spark_helpers import ( + enforce_schema, + order_array_of_structs_by_field, +) +from gentropy.dataset.variant_index import VariantIndex + +if TYPE_CHECKING: + from pyspark.sql import Column, DataFrame + + +class VariantEffectPredictorParser: + """Collection of methods to parse VEP output in json format.""" + + # Schema description of the dbXref object: + DBXREF_SCHEMA = t.ArrayType( + t.StructType( + [ + t.StructField("id", t.StringType(), True), + t.StructField("source", t.StringType(), True), + ] + ) + ) + + # Schema description of the in silico predictor object: + IN_SILICO_PREDICTOR_SCHEMA = t.StructType( + [ + t.StructField("method", t.StringType(), True), + t.StructField("assessment", t.StringType(), True), + t.StructField("score", t.FloatType(), True), + t.StructField("assessmentFlag", t.StringType(), True), + t.StructField("targetId", t.StringType(), True), + ] + ) + + # Schema for the allele frequency column: + ALLELE_FREQUENCY_SCHEMA = t.ArrayType( + t.StructType( + [ + t.StructField("populationName", t.StringType(), True), + t.StructField("alleleFrequency", t.DoubleType(), True), + ] + ), + False, + ) + + @staticmethod + def get_schema() -> t.StructType: + """Return the schema of the VEP output. + + Returns: + t.StructType: VEP output schema. + + Examples: + >>> type(VariantEffectPredictorParser.get_schema()) + + """ + return parse_spark_schema("vep_json_output.json") + + @classmethod + def extract_variant_index_from_vep( + cls: type[VariantEffectPredictorParser], + spark: SparkSession, + vep_output_path: str | list[str], + **kwargs: bool | float | int | str | None, + ) -> VariantIndex: + """Extract variant index from VEP output. + + Args: + spark (SparkSession): Spark session. + vep_output_path (str | list[str]): Path to the VEP output. + **kwargs (bool | float | int | str | None): Additional arguments to pass to spark.read.json. + + Returns: + VariantIndex: Variant index dataset. + + Raises: + ValueError: Failed reading file. + ValueError: The dataset is empty. + """ + # To speed things up and simplify the json structure, read data following an expected schema: + vep_schema = cls.get_schema() + + try: + vep_data = spark.read.json(vep_output_path, schema=vep_schema, **kwargs) + except ValueError as e: + raise ValueError(f"Failed reading file: {vep_output_path}.") from e + + if vep_data.isEmpty(): + raise ValueError(f"The dataset is empty: {vep_output_path}") + + # Convert to VariantAnnotation dataset: + return VariantIndex( + _df=VariantEffectPredictorParser.process_vep_output(vep_data), + _schema=VariantIndex.get_schema(), + ) + + @staticmethod + def _extract_ensembl_xrefs(colocated_variants: Column) -> Column: + """Extract rs identifiers and build cross reference to Ensembl's variant page. + + Args: + colocated_variants (Column): Colocated variants field from VEP output. + + Returns: + Column: List of dbXrefs for rs identifiers. + """ + return VariantEffectPredictorParser._generate_dbxrefs( + VariantEffectPredictorParser._colocated_variants_to_rsids( + colocated_variants + ), + "ensembl_variation", + ) + + @enforce_schema(DBXREF_SCHEMA) + @staticmethod + def _generate_dbxrefs(ids: Column, source: str) -> Column: + """Convert a list of variant identifiers to dbXrefs given the source label. + + Identifiers are cast to strings, then Null values are filtered out of the id list. + + Args: + ids (Column): List of variant identifiers. + source (str): Source label for the dbXrefs. + + Returns: + Column: List of dbXrefs. + + Examples: + >>> ( + ... spark.createDataFrame([('rs12',),(None,)], ['test_id']) + ... .select(VariantEffectPredictorParser._generate_dbxrefs(f.array(f.col('test_id')), "ensemblVariation").alias('col')) + ... .show(truncate=False) + ... ) + +--------------------------+ + |col | + +--------------------------+ + |[{rs12, ensemblVariation}]| + |[] | + +--------------------------+ + + >>> ( + ... spark.createDataFrame([('rs12',),(None,)], ['test_id']) + ... .select(VariantEffectPredictorParser._generate_dbxrefs(f.array(f.col('test_id')), "ensemblVariation").alias('col')) + ... .first().col[0].asDict() + ... ) + {'id': 'rs12', 'source': 'ensemblVariation'} + """ + ids = f.filter(ids, lambda id: id.isNotNull()) + xref_column = f.transform( + ids, + lambda id: f.struct( + id.cast(t.StringType()).alias("id"), f.lit(source).alias("source") + ), + ) + + return f.when(xref_column.isNull(), f.array()).otherwise(xref_column) + + @staticmethod + def _colocated_variants_to_rsids(colocated_variants: Column) -> Column: + """Extract rs identifiers from the colocated variants VEP field. + + Args: + colocated_variants (Column): Colocated variants field from VEP output. + + Returns: + Column: List of rs identifiers. + + Examples: + >>> data = [('s1', 'rs1'),('s1', 'rs2'),('s1', 'rs3'),('s2', None),] + >>> ( + ... spark.createDataFrame(data, ['s','v']) + ... .groupBy('s') + ... .agg(f.collect_list(f.struct(f.col('v').alias('id'))).alias('cv')) + ... .select(VariantEffectPredictorParser._colocated_variants_to_rsids(f.col('cv')).alias('rsIds'),) + ... .show(truncate=False) + ... ) + +---------------+ + |rsIds | + +---------------+ + |[rs1, rs2, rs3]| + |[null] | + +---------------+ + + """ + return f.when( + colocated_variants.isNotNull(), + f.transform( + colocated_variants, lambda variant: variant.getItem("id").alias("id") + ), + ).alias("rsIds") + + @staticmethod + def _extract_omim_xrefs(colocated_variants: Column) -> Column: + """Build xdbRefs for OMIM identifiers. + + OMIM identifiers are extracted from the colocated variants field, casted to strings and formatted as dbXrefs. + + Args: + colocated_variants (Column): Colocated variants field from VEP output. + + Returns: + Column: List of dbXrefs for OMIM identifiers. + + Examples: + >>> data = [('234234.32', 'rs1', 's1',),(None, 'rs1', 's1',),] + >>> ( + ... spark.createDataFrame(data, ['id', 'rsid', 's']) + ... .groupBy('s') + ... .agg(f.collect_list(f.struct(f.struct(f.array(f.col('id')).alias('OMIM')).alias('var_synonyms'),f.col('rsid').alias('id'),),).alias('cv'),).select(VariantEffectPredictorParser._extract_omim_xrefs(f.col('cv')).alias('dbXrefs')) + ... .show(truncate=False) + ... ) + +-------------------+ + |dbXrefs | + +-------------------+ + |[{234234#32, omim}]| + +-------------------+ + + """ + variants_w_omim_ref = f.filter( + colocated_variants, + lambda variant: variant.getItem("var_synonyms").getItem("OMIM").isNotNull(), + ) + + omim_var_ids = f.transform( + variants_w_omim_ref, + lambda var: f.transform( + var.getItem("var_synonyms").getItem("OMIM"), + lambda var: f.regexp_replace(var.cast(t.StringType()), r"\.", r"#"), + ), + ) + + return VariantEffectPredictorParser._generate_dbxrefs( + f.flatten(omim_var_ids), "omim" + ) + + @staticmethod + def _extract_clinvar_xrefs(colocated_variants: Column) -> Column: + """Build xdbRefs for VCV ClinVar identifiers. + + ClinVar identifiers are extracted from the colocated variants field. + VCV-identifiers are filtered out to generate cross-references. + + Args: + colocated_variants (Column): Colocated variants field from VEP output. + + Returns: + Column: List of dbXrefs for VCV ClinVar identifiers. + + Examples: + >>> data = [('VCV2323,RCV3423', 'rs1', 's1',),(None, 'rs1', 's1',),] + >>> ( + ... spark.createDataFrame(data, ['id', 'rsid', 's']) + ... .groupBy('s') + ... .agg(f.collect_list(f.struct(f.struct(f.split(f.col('id'),',').alias('ClinVar')).alias('var_synonyms'),f.col('rsid').alias('id'),),).alias('cv'),).select(VariantEffectPredictorParser._extract_clinvar_xrefs(f.col('cv')).alias('dbXrefs')) + ... .show(truncate=False) + ... ) + +--------------------+ + |dbXrefs | + +--------------------+ + |[{VCV2323, clinvar}]| + +--------------------+ + + """ + variants_w_clinvar_ref = f.filter( + colocated_variants, + lambda variant: variant.getItem("var_synonyms") + .getItem("ClinVar") + .isNotNull(), + ) + + clin_var_ids = f.transform( + variants_w_clinvar_ref, + lambda var: f.filter( + var.getItem("var_synonyms").getItem("ClinVar"), + lambda x: x.startswith("VCV"), + ), + ) + + return VariantEffectPredictorParser._generate_dbxrefs( + f.flatten(clin_var_ids), "clinvar" + ) + + @staticmethod + def _get_most_severe_transcript( + transcript_column_name: str, score_field_name: str + ) -> Column: + """Return transcript with the highest in silico predicted score. + + This method assumes the higher the score, the more severe the consequence of the variant is. + Hence, by selecting the transcript with the highest score, we are selecting the most severe consequence. + + Args: + transcript_column_name (str): Name of the column containing the list of transcripts. + score_field_name (str): Name of the field containing the severity score. + + Returns: + Column: Most severe transcript. + + Examples: + >>> data = [("v1", 0.2, 'transcript1'),("v1", None, 'transcript2'),("v1", 0.6, 'transcript3'),("v1", 0.4, 'transcript4'),] + >>> ( + ... spark.createDataFrame(data, ['v', 'score', 'transcriptId']) + ... .groupBy('v') + ... .agg(f.collect_list(f.struct(f.col('score'),f.col('transcriptId'))).alias('transcripts')) + ... .select(VariantEffectPredictorParser._get_most_severe_transcript('transcripts', 'score').alias('most_severe_transcript')) + ... .show(truncate=False) + ... ) + +----------------------+ + |most_severe_transcript| + +----------------------+ + |{0.6, transcript3} | + +----------------------+ + + """ + assert isinstance( + transcript_column_name, str + ), "transcript_column_name must be a string and not a column." + # Order transcripts by severity score: + ordered_transcripts = order_array_of_structs_by_field( + transcript_column_name, score_field_name + ) + + # Drop transcripts with no severity score and return the most severe one: + return f.filter( + ordered_transcripts, + lambda transcript: transcript.getItem(score_field_name).isNotNull(), + )[0] + + @enforce_schema(IN_SILICO_PREDICTOR_SCHEMA) + @staticmethod + def _get_max_alpha_missense(transcripts: Column) -> Column: + """Return the most severe alpha missense prediction from all transcripts. + + This function assumes one variant can fall onto only one gene with alpha-sense prediction available on the canonical transcript. + + Args: + transcripts (Column): List of transcripts. + + Returns: + Column: Most severe alpha missense prediction. + + Examples: + >>> data = [('v1', 0.4, 'assessment 1'), ('v1', None, None), ('v1', None, None),('v2', None, None),] + >>> ( + ... spark.createDataFrame(data, ['v', 'a', 'b']) + ... .groupBy('v') + ... .agg(f.collect_list(f.struct(f.struct( + ... f.col('a').alias('am_pathogenicity'), + ... f.col('b').alias('am_class')).alias('alphamissense'), + ... f.lit('gene1').alias('gene_id'))).alias('transcripts') + ... ) + ... .select(VariantEffectPredictorParser._get_max_alpha_missense(f.col('transcripts')).alias('am')) + ... .show(truncate=False) + ... ) + +----------------------------------------------------+ + |am | + +----------------------------------------------------+ + |{max alpha missense, assessment 1, 0.4, null, gene1}| + |{max alpha missense, null, null, null, gene1} | + +----------------------------------------------------+ + + """ + return f.transform( + # Extract transcripts with alpha missense values: + f.filter( + transcripts, + lambda transcript: transcript.getItem("alphamissense").isNotNull(), + ), + # Extract alpha missense values: + lambda transcript: f.struct( + transcript.getItem("alphamissense") + .getItem("am_pathogenicity") + .cast(t.FloatType()) + .alias("score"), + transcript.getItem("alphamissense") + .getItem("am_class") + .alias("assessment"), + f.lit("max alpha missense").alias("method"), + transcript.getItem("gene_id").alias("targetId"), + ), + )[0] + + @enforce_schema(IN_SILICO_PREDICTOR_SCHEMA) + @staticmethod + def _vep_in_silico_prediction_extractor( + transcript_column_name: str, + method_name: str, + score_column_name: str | None = None, + assessment_column_name: str | None = None, + assessment_flag_column_name: str | None = None, + ) -> Column: + """Extract in silico prediction from VEP output. + + Args: + transcript_column_name (str): Name of the column containing the list of transcripts. + method_name (str): Name of the in silico predictor. + score_column_name (str | None): Name of the column containing the score. + assessment_column_name (str | None): Name of the column containing the assessment. + assessment_flag_column_name (str | None): Name of the column containing the assessment flag. + + Returns: + Column: In silico predictor. + """ + # Get highest score: + most_severe_transcript: Column = ( + # Getting the most severe transcript: + VariantEffectPredictorParser._get_most_severe_transcript( + transcript_column_name, score_column_name + ) + if score_column_name is not None + # If we don't have score, just pick one of the transcript where assessment is available: + else f.filter( + f.col(transcript_column_name), + lambda transcript: transcript.getItem( + assessment_column_name + ).isNotNull(), + ) + ) + + return f.when( + most_severe_transcript.isNotNull(), + f.struct( + # Adding method name: + f.lit(method_name).cast(t.StringType()).alias("method"), + # Adding assessment: + f.lit(None).cast(t.StringType()).alias("assessment") + if assessment_column_name is None + else most_severe_transcript.getField(assessment_column_name).alias( + "assessment" + ), + # Adding score: + f.lit(None).cast(t.FloatType()).alias("score") + if score_column_name is None + else most_severe_transcript.getField(score_column_name) + .cast(t.FloatType()) + .alias("score"), + # Adding assessment flag: + f.lit(None).cast(t.StringType()).alias("assessmentFlag") + if assessment_flag_column_name is None + else most_severe_transcript.getField(assessment_flag_column_name) + .cast(t.FloatType()) + .alias("assessmentFlag"), + # Adding target id if present: + most_severe_transcript.getItem("gene_id").alias("targetId"), + ), + ) + + @staticmethod + def _parser_amino_acid_change(amino_acids: Column, protein_end: Column) -> Column: + """Convert VEP amino acid change information to one letter code aa substitution code. + + The logic assumes that the amino acid change is given in the format "from/to" and the protein end is given also. + If any of the information is missing, the amino acid change will be set to None. + + Args: + amino_acids (Column): Amino acid change information. + protein_end (Column): Protein end information. + + Returns: + Column: Amino acid change in one letter code. + + Examples: + >>> data = [('A/B', 1),('A/B', None),(None, 1),] + >>> ( + ... spark.createDataFrame(data, ['amino_acids', 'protein_end']) + ... .select(VariantEffectPredictorParser._parser_amino_acid_change(f.col('amino_acids'), f.col('protein_end')).alias('amino_acid_change')) + ... .show() + ... ) + +-----------------+ + |amino_acid_change| + +-----------------+ + | A1B| + | null| + | null| + +-----------------+ + + """ + return f.when( + amino_acids.isNotNull() & protein_end.isNotNull(), + f.concat( + f.split(amino_acids, r"\/")[0], + protein_end, + f.split(amino_acids, r"\/")[1], + ), + ).otherwise(f.lit(None)) + + @staticmethod + def _collect_uniprot_accessions(trembl: Column, swissprot: Column) -> Column: + """Flatten arrays containing Uniprot accessions. + + Args: + trembl (Column): List of TrEMBL protein accessions. + swissprot (Column): List of SwissProt protein accessions. + + Returns: + Column: List of unique Uniprot accessions extracted from swissprot and trembl arrays, splitted from version numbers. + + Examples: + >>> data = [('v1', ["sp_1"], ["tr_1"]), ('v1', None, None), ('v1', None, ["tr_2"]),] + >>> ( + ... spark.createDataFrame(data, ['v', 'sp', 'tr']) + ... .select(VariantEffectPredictorParser._collect_uniprot_accessions(f.col('sp'), f.col('tr')).alias('proteinIds')) + ... .show() + ... ) + +------------+ + | proteinIds| + +------------+ + |[sp_1, tr_1]| + | []| + | [tr_2]| + +------------+ + + """ + # Dropping Null values and flattening the arrays: + return f.filter( + f.array_distinct( + f.transform( + f.flatten( + f.filter( + f.array(trembl, swissprot), + lambda x: x.isNotNull(), + ) + ), + lambda x: f.split(x, r"\.")[0], + ) + ), + lambda x: x.isNotNull(), + ) + + @staticmethod + def _consequence_to_sequence_ontology( + col: Column, so_dict: Dict[str, str] + ) -> Column: + """Convert VEP consequence terms to sequence ontology identifiers. + + Missing consequence label will be converted to None, unmapped consequences will be mapped as None. + + Args: + col (Column): Column containing VEP consequence terms. + so_dict (Dict[str, str]): Dictionary mapping VEP consequence terms to sequence ontology identifiers. + + Returns: + Column: Column containing sequence ontology identifiers. + + Examples: + >>> data = [('consequence_1',),('unmapped_consequence',),(None,)] + >>> m = {'consequence_1': 'SO:000000'} + >>> ( + ... spark.createDataFrame(data, ['label']) + ... .select('label',VariantEffectPredictorParser._consequence_to_sequence_ontology(f.col('label'),m).alias('id')) + ... .show() + ... ) + +--------------------+---------+ + | label| id| + +--------------------+---------+ + | consequence_1|SO:000000| + |unmapped_consequence| null| + | null| null| + +--------------------+---------+ + + """ + map_expr = f.create_map(*[f.lit(x) for x in chain(*so_dict.items())]) + + return map_expr[col].alias("ancestry") + + @staticmethod + def _parse_variant_location_id(vep_input_field: Column) -> List[Column]: + r"""Parse variant identifier, chromosome, position, reference allele and alternate allele from VEP input field. + + Args: + vep_input_field (Column): Column containing variant vcf string used as VEP input. + + Returns: + List[Column]: List of columns containing chromosome, position, reference allele and alternate allele. + """ + variant_fields = f.split(vep_input_field, r"\t") + return [ + f.concat_ws( + "_", + f.array( + variant_fields[0], + variant_fields[1], + variant_fields[3], + variant_fields[4], + ), + ).alias("variantId"), + variant_fields[0].cast(t.StringType()).alias("chromosome"), + variant_fields[1].cast(t.IntegerType()).alias("position"), + variant_fields[3].cast(t.StringType()).alias("referenceAllele"), + variant_fields[4].cast(t.StringType()).alias("alternateAllele"), + ] + + @classmethod + def process_vep_output(cls, vep_output: DataFrame) -> DataFrame: + """Process and format a VEP output in JSON format. + + Args: + vep_output (DataFrame): raw VEP output, read as spark DataFrame. + + Returns: + DataFrame: processed data in the right shape. + """ + # Reading consequence to sequence ontology map: + sequence_ontology_map = json.loads( + pkg_resources.read_text(data, "so_mappings.json", encoding="utf-8") + ) + # Processing VEP output: + return ( + vep_output + # Dropping non-canonical transcript consequences: # TODO: parametrize this. + .withColumn( + "transcript_consequences", + f.filter( + f.col("transcript_consequences"), + lambda consequence: consequence.getItem("canonical") == 1, + ), + ) + .select( + # Parse id and chr:pos:alt:ref: + *cls._parse_variant_location_id(f.col("input")), + # Extracting corss_references from colocated variants: + cls._extract_ensembl_xrefs(f.col("colocated_variants")).alias( + "ensembl_xrefs" + ), + cls._extract_omim_xrefs(f.col("colocated_variants")).alias( + "omim_xrefs" + ), + cls._extract_clinvar_xrefs(f.col("colocated_variants")).alias( + "clinvar_xrefs" + ), + # Extracting in silico predictors + f.when( + # The following in-silico predictors are only available for variants with transcript consequences: + f.col("transcript_consequences").isNotNull(), + f.filter( + f.array( + # Extract CADD scores: + cls._vep_in_silico_prediction_extractor( + transcript_column_name="transcript_consequences", + method_name="phred scaled CADD", + score_column_name="cadd_phred", + ), + # Extract polyphen scores: + cls._vep_in_silico_prediction_extractor( + transcript_column_name="transcript_consequences", + method_name="polyphen", + score_column_name="polyphen_score", + assessment_column_name="polyphen_prediction", + ), + # Extract sift scores: + cls._vep_in_silico_prediction_extractor( + transcript_column_name="transcript_consequences", + method_name="sift", + score_column_name="sift_score", + assessment_column_name="sift_prediction", + ), + # Extract loftee scores: + cls._vep_in_silico_prediction_extractor( + method_name="loftee", + transcript_column_name="transcript_consequences", + score_column_name="lof", + assessment_column_name="lof", + assessment_flag_column_name="lof_filter", + ), + # Extract max alpha missense: + cls._get_max_alpha_missense( + f.col("transcript_consequences") + ), + ), + lambda predictor: predictor.isNotNull(), + ), + ) + .otherwise( + # Extract CADD scores from intergenic object: + f.array( + cls._vep_in_silico_prediction_extractor( + transcript_column_name="intergenic_consequences", + method_name="phred scaled CADD", + score_column_name="cadd_phred", + ), + ) + ) + .alias("inSilicoPredictors"), + # Convert consequence to SO: + cls._consequence_to_sequence_ontology( + f.col("most_severe_consequence"), sequence_ontology_map + ).alias("mostSevereConsequenceId"), + # Collect transcript consequence: + f.when( + f.col("transcript_consequences").isNotNull(), + f.transform( + f.col("transcript_consequences"), + lambda transcript: f.struct( + # Convert consequence terms to SO identifier: + f.transform( + transcript.consequence_terms, + lambda y: cls._consequence_to_sequence_ontology( + y, sequence_ontology_map + ), + ).alias("variantFunctionalConsequenceIds"), + # Format amino acid change: + cls._parser_amino_acid_change( + transcript.amino_acids, transcript.protein_end + ).alias("aminoAcidChange"), + # Extract and clean uniprot identifiers: + cls._collect_uniprot_accessions( + transcript.swissprot, + transcript.trembl, + ).alias("uniprotAccessions"), + # Add canonical flag: + f.when(transcript.canonical == 1, f.lit(True)) + .otherwise(f.lit(False)) + .alias("isEnsemblCanonical"), + # Extract other fields as is: + transcript.codons.alias("codons"), + transcript.distance.alias("distance"), + transcript.gene_id.alias("targetId"), + transcript.impact.alias("impact"), + transcript.transcript_id.alias("transcriptId"), + transcript.lof.cast(t.StringType()).alias( + "lofteePrediction" + ), + transcript.lof.cast(t.FloatType()).alias("siftPrediction"), + transcript.lof.cast(t.FloatType()).alias( + "polyphenPrediction" + ), + ), + ), + ).alias("transcriptConsequences"), + # Extracting rsids: + cls._colocated_variants_to_rsids(f.col("colocated_variants")).alias( + "rsIds" + ), + # Adding empty array for allele frequencies - now this piece of data is not coming form the VEP data: + f.array().cast(cls.ALLELE_FREQUENCY_SCHEMA).alias("alleleFrequencies"), + ) + # Adding protvar xref for missense variants: # TODO: making and extendable list of consequences + .withColumn( + "protvar_xrefs", + f.when( + f.size( + f.filter( + f.col("transcriptConsequences"), + lambda x: f.array_contains( + x.variantFunctionalConsequenceIds, "SO_0001583" + ), + ) + ) + > 0, + cls._generate_dbxrefs(f.array(f.col("variantId")), "protvar"), + ), + ) + .withColumn( + "dbXrefs", + f.flatten( + f.filter( + f.array( + f.col("ensembl_xrefs"), + f.col("omim_xrefs"), + f.col("clinvar_xrefs"), + f.col("protvar_xrefs"), + ), + lambda x: x.isNotNull(), + ) + ), + ) + # Dropping intermediate xref columns: + .drop(*["ensembl_xrefs", "omim_xrefs", "clinvar_xrefs", "protvar_xrefs"]) + ) diff --git a/src/gentropy/datasource/gnomad/variants.py b/src/gentropy/datasource/gnomad/variants.py index fdc67a7cb..98e7013f5 100644 --- a/src/gentropy/datasource/gnomad/variants.py +++ b/src/gentropy/datasource/gnomad/variants.py @@ -7,8 +7,8 @@ import hail as hl from gentropy.common.types import VariantPopulation -from gentropy.config import VariantAnnotationConfig -from gentropy.dataset.variant_annotation import VariantAnnotation +from gentropy.config import GnomadVariantConfig +from gentropy.dataset.variant_index import VariantIndex if TYPE_CHECKING: pass @@ -19,26 +19,23 @@ class GnomADVariants: def __init__( self, - gnomad_genomes_path: str = VariantAnnotationConfig().gnomad_genomes_path, - chain_38_37: str = VariantAnnotationConfig().chain_38_37, + gnomad_genomes_path: str = GnomadVariantConfig().gnomad_genomes_path, gnomad_variant_populations: list[ VariantPopulation | str - ] = VariantAnnotationConfig().gnomad_variant_populations, + ] = GnomadVariantConfig().gnomad_variant_populations, ): """Initialize. Args: gnomad_genomes_path (str): Path to gnomAD genomes hail table. - chain_38_37 (str): Path to GRCh38 to GRCh37 chain file. gnomad_variant_populations (list[VariantPopulation | str]): List of populations to include. - All defaults are stored in VariantAnnotationConfig. + All defaults are stored in GnomadVariantConfig. """ self.gnomad_genomes_path = gnomad_genomes_path - self.chain_38_37 = chain_38_37 self.gnomad_variant_populations = gnomad_variant_populations - def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: + def as_variant_index(self: GnomADVariants) -> VariantIndex: """Generate variant annotation dataset from gnomAD. Some relevant modifications to the original dataset are: @@ -48,7 +45,7 @@ def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: 3. Field names are converted to camel case to follow the convention. Returns: - VariantAnnotation: Variant annotation dataset + VariantIndex: GnomaAD variants dataset. """ # Load variants dataset ht = hl.read_table( @@ -56,19 +53,14 @@ def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: _load_refs=False, ) - # Liftover - grch37 = hl.get_reference("GRCh37") - grch38 = hl.get_reference("GRCh38") - grch38.add_liftover(self.chain_38_37, grch37) - # Drop non biallelic variants ht = ht.filter(ht.alleles.length() == 2) - # Liftover - ht = ht.annotate(locus_GRCh37=hl.liftover(ht.locus, "GRCh37")) + # Select relevant fields and nested records to create class - return VariantAnnotation( + return VariantIndex( _df=( ht.select( + # Extract mandatory fields: variantId=hl.str("_").join( [ ht.locus.contig.replace("chr", ""), @@ -79,12 +71,9 @@ def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: ), chromosome=ht.locus.contig.replace("chr", ""), position=ht.locus.position, - chromosomeB37=ht.locus_GRCh37.contig.replace("chr", ""), - positionB37=ht.locus_GRCh37.position, referenceAllele=ht.alleles[0], alternateAllele=ht.alleles[1], - rsIds=ht.rsid, - alleleType=ht.allele_info.allele_type, + # Extract allele frequencies from populations of interest: alleleFrequencies=hl.set( [f"{pop}_adj" for pop in self.gnomad_variant_populations] ).map( @@ -93,33 +82,46 @@ def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: alleleFrequency=ht.freq[ht.globals.freq_index_dict[p]].AF, ) ), - vep=hl.struct( - mostSevereConsequence=ht.vep.most_severe_consequence, - transcriptConsequences=hl.map( - lambda x: hl.struct( - aminoAcids=x.amino_acids, - consequenceTerms=x.consequence_terms, - geneId=x.gene_id, - lof=x.lof, + # Extract most severe consequence: + mostSevereConsequence=ht.vep.most_severe_consequence, + # Extract in silico predictors: + inSilicoPredictors=hl.array( + [ + hl.struct( + method=hl.str("spliceai"), + assessment=hl.missing(hl.tstr), + score=hl.expr.functions.float32( + ht.in_silico_predictors.spliceai_ds_max + ), + assessmentFlag=hl.missing(hl.tstr), + targetId=hl.missing(hl.tstr), ), - # Only keeping canonical transcripts - ht.vep.transcript_consequences.filter( - lambda x: (x.canonical == 1) - & (x.gene_symbol_source == "HGNC") + hl.struct( + method=hl.str("pangolin"), + assessment=hl.missing(hl.tstr), + score=hl.expr.functions.float32( + ht.in_silico_predictors.pangolin_largest_ds + ), + assessmentFlag=hl.missing(hl.tstr), + targetId=hl.missing(hl.tstr), ), - ), + ] ), - inSilicoPredictors=hl.struct( - cadd=hl.struct( - phred=ht.in_silico_predictors.cadd.phred, - raw=ht.in_silico_predictors.cadd.raw_score, - ), - revelMax=ht.in_silico_predictors.revel_max, - spliceaiDsMax=ht.in_silico_predictors.spliceai_ds_max, - pangolinLargestDs=ht.in_silico_predictors.pangolin_largest_ds, - phylop=ht.in_silico_predictors.phylop, - siftMax=ht.in_silico_predictors.sift_max, - polyphenMax=ht.in_silico_predictors.polyphen_max, + # Extract cross references to GnomAD: + dbXrefs=hl.array( + [ + hl.struct( + id=hl.str("-").join( + [ + ht.locus.contig.replace("chr", ""), + hl.str(ht.locus.position), + ht.alleles[0], + ht.alleles[1], + ] + ), + source=hl.str("gnomad"), + ) + ] ), ) .key_by("chromosome", "position") @@ -127,5 +129,5 @@ def as_variant_annotation(self: GnomADVariants) -> VariantAnnotation: .select_globals() .to_spark(flatten=False) ), - _schema=VariantAnnotation.get_schema(), + _schema=VariantIndex.get_schema(), ) diff --git a/src/gentropy/datasource/gwas_catalog/associations.py b/src/gentropy/datasource/gwas_catalog/associations.py index d6763cb35..b4e9a2d45 100644 --- a/src/gentropy/datasource/gwas_catalog/associations.py +++ b/src/gentropy/datasource/gwas_catalog/associations.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: from pyspark.sql import Column, DataFrame - from gentropy.dataset.variant_annotation import VariantAnnotation + from gentropy.dataset.variant_index import VariantIndex @dataclass @@ -197,14 +197,14 @@ def _collect_rsids( return f.array_distinct(f.array(snp_id, snp_id_current, risk_allele)) @staticmethod - def _map_to_variant_annotation_variants( - gwas_associations: DataFrame, variant_annotation: VariantAnnotation + def _map_variants_to_variant_index( + gwas_associations: DataFrame, variant_index: VariantIndex ) -> DataFrame: """Add variant metadata in associations. Args: - gwas_associations (DataFrame): raw GWAS Catalog associations - variant_annotation (VariantAnnotation): variant annotation dataset + gwas_associations (DataFrame): raw GWAS Catalog associations. + variant_index (VariantIndex): GnomaAD variants dataset with allele frequencies. Returns: DataFrame: GWAS Catalog associations data including `variantId`, `referenceAllele`, @@ -228,7 +228,7 @@ def _map_to_variant_annotation_variants( ) # Subset of variant annotation required for GWAS Catalog annotations: - va_subset = variant_annotation.df.select( + va_subset = variant_index.df.select( "variantId", "chromosome", # Calculate the position in Ensembl coordinates for indels: @@ -241,7 +241,7 @@ def _map_to_variant_annotation_variants( "referenceAllele", "alternateAllele", "alleleFrequencies", - variant_annotation.max_maf().alias("maxMaf"), + variant_index.max_maf().alias("maxMaf"), ).join( f.broadcast( gwas_associations_subset.select( @@ -1035,7 +1035,7 @@ def _qc_palindromic_alleles( def from_source( cls: type[GWASCatalogCuratedAssociationsParser], gwas_associations: DataFrame, - variant_annotation: VariantAnnotation, + variant_index: VariantIndex, pvalue_threshold: float = WindowBasedClumpingStepConfig.gwas_significance, ) -> StudyLocusGWASCatalog: """Read GWASCatalog associations. @@ -1044,9 +1044,9 @@ def from_source( applies some pre-defined filters on the data: Args: - gwas_associations (DataFrame): GWAS Catalog raw associations dataset - variant_annotation (VariantAnnotation): Variant annotation dataset - pvalue_threshold (float): P-value threshold for flagging associations + gwas_associations (DataFrame): GWAS Catalog raw associations dataset. + variant_index (VariantIndex): Variant index dataset with available allele frequencies. + pvalue_threshold (float): P-value threshold for flagging associations. Returns: StudyLocusGWASCatalog: GWASCatalogAssociations dataset @@ -1060,8 +1060,8 @@ def from_source( .transform( # Map/harmonise variants to variant annotation dataset: # This function adds columns: variantId, referenceAllele, alternateAllele, chromosome, position - lambda df: GWASCatalogCuratedAssociationsParser._map_to_variant_annotation_variants( - df, variant_annotation + lambda df: GWASCatalogCuratedAssociationsParser._map_variants_to_variant_index( + df, variant_index ) ) .withColumn( diff --git a/src/gentropy/datasource/ukbiobank/__init__.py b/src/gentropy/datasource/ukbiobank/__init__.py index 544779b18..910603703 100644 --- a/src/gentropy/datasource/ukbiobank/__init__.py +++ b/src/gentropy/datasource/ukbiobank/__init__.py @@ -1,3 +1,3 @@ -"""GWAS Catalog Data Source.""" +"""UK biobank.""" from __future__ import annotations diff --git a/src/gentropy/ld_index.py b/src/gentropy/gnomad_ingestion.py similarity index 54% rename from src/gentropy/ld_index.py rename to src/gentropy/gnomad_ingestion.py index 0cc00cf14..07b7fd58b 100644 --- a/src/gentropy/ld_index.py +++ b/src/gentropy/gnomad_ingestion.py @@ -1,14 +1,15 @@ -"""Step to dump a filtered version of a LD matrix (block matrix) as Parquet files.""" +"""Step to dump a filtered version of a LD matrix (block matrix) and GnomAD variants.""" from __future__ import annotations import hail as hl from gentropy.common.session import Session -from gentropy.common.types import LD_Population +from gentropy.common.types import LD_Population, VariantPopulation from gentropy.common.version_engine import VersionEngine -from gentropy.config import LDIndexConfig +from gentropy.config import GnomadVariantConfig, LDIndexConfig from gentropy.datasource.gnomad.ld import GnomADLDMatrix +from gentropy.datasource.gnomad.variants import GnomADVariants class LDIndexStep: @@ -69,3 +70,56 @@ def __init__( .parquet(ld_index_out) ) session.logger.info(ld_index_out) + + +class GnomadVariantIndexStep: + """A step to generate variant index dataset from gnomad data. + + Variant annotation step produces a dataset of the type `VariantIndex` derived from gnomADs `gnomad.genomes.vX.X.X.sites.ht` Hail's table. + This dataset is used to validate variants and as a source of annotation. + """ + + def __init__( + self, + session: Session, + gnomad_variant_output: str, + gnomad_genomes_path: str = GnomadVariantConfig().gnomad_genomes_path, + gnomad_variant_populations: list[ + VariantPopulation | str + ] = GnomadVariantConfig().gnomad_variant_populations, + use_version_from_input: bool = GnomadVariantConfig().use_version_from_input, + ) -> None: + """Run Variant Annotation step. + + Args: + session (Session): Session object. + gnomad_variant_output (str): Path to resulting dataset. + gnomad_genomes_path (str): Path to gnomAD genomes hail table, e.g. `gs://gcp-public-data--gnomad/release/4.0/ht/genomes/gnomad.genomes.v4.0.sites.ht/`. + gnomad_variant_populations (list[VariantPopulation | str]): List of populations to include. + use_version_from_input (bool): Append version derived from input gnomad_genomes_path to the output gnomad_variant_output. Defaults to False. + + In case use_version_from_input is set to True, + data source version inferred from gnomad_genomes_path is appended as the last path segment to the output path. + All defaults are stored in the GnomadVariantConfig. + """ + # amend data source version to output path + if use_version_from_input: + gnomad_variant_output = VersionEngine("gnomad").amend_version( + gnomad_genomes_path, gnomad_variant_output + ) + + # Initialise hail session. + hl.init(sc=session.spark.sparkContext, log="/dev/null") + + # Parse variant info from source. + gnomad_variants = GnomADVariants( + gnomad_genomes_path=gnomad_genomes_path, + gnomad_variant_populations=gnomad_variant_populations, + ).as_variant_index() + + # Write data partitioned by chromosome and position. + ( + gnomad_variants.df.write.mode(session.write_mode).parquet( + gnomad_variant_output + ) + ) diff --git a/src/gentropy/gwas_catalog_ingestion.py b/src/gentropy/gwas_catalog_ingestion.py index 6930a2df9..725f1ca4d 100644 --- a/src/gentropy/gwas_catalog_ingestion.py +++ b/src/gentropy/gwas_catalog_ingestion.py @@ -1,8 +1,9 @@ """Step to process GWAS Catalog associations and study table.""" + from __future__ import annotations from gentropy.common.session import Session -from gentropy.dataset.variant_annotation import VariantAnnotation +from gentropy.dataset.variant_index import VariantIndex from gentropy.datasource.gwas_catalog.associations import ( GWASCatalogCuratedAssociationsParser, ) @@ -26,7 +27,7 @@ def __init__( catalog_ancestry_files: list[str], catalog_sumstats_lut: str, catalog_associations_file: str, - variant_annotation_path: str, + gnomad_variant_path: str, catalog_studies_out: str, catalog_associations_out: str, gwas_catalog_study_curation_file: str | None = None, @@ -40,14 +41,14 @@ def __init__( catalog_ancestry_files (list[str]): List of raw ancestry annotations files from GWAS Catalog. catalog_sumstats_lut (str): GWAS Catalog summary statistics lookup table. catalog_associations_file (str): Raw GWAS catalog associations file. - variant_annotation_path (str): Input variant annotation path. + gnomad_variant_path (str): Path to GnomAD variants. catalog_studies_out (str): Output GWAS catalog studies path. catalog_associations_out (str): Output GWAS catalog associations path. gwas_catalog_study_curation_file (str | None): file of the curation table. Optional. inclusion_list_path (str | None): optional inclusion list (parquet) """ # Extract - va = VariantAnnotation.from_parquet(session, variant_annotation_path) + gnomad_variants = VariantIndex.from_parquet(session, gnomad_variant_path) catalog_studies = session.spark.read.csv( list(catalog_study_files), sep="\t", header=True ) @@ -69,7 +70,9 @@ def __init__( StudyIndexGWASCatalogParser.from_source( catalog_studies, ancestry_lut, sumstats_lut ).annotate_from_study_curation(gwas_catalog_study_curation), - GWASCatalogCuratedAssociationsParser.from_source(catalog_associations, va), + GWASCatalogCuratedAssociationsParser.from_source( + catalog_associations, gnomad_variants + ), ) # if inclusion list is provided apply filter: diff --git a/src/gentropy/gwas_catalog_study_inclusion.py b/src/gentropy/gwas_catalog_study_inclusion.py index 872177601..f07f851a7 100644 --- a/src/gentropy/gwas_catalog_study_inclusion.py +++ b/src/gentropy/gwas_catalog_study_inclusion.py @@ -1,4 +1,5 @@ """Step to generate an GWAS Catalog study identifier inclusion and exclusion list.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -6,7 +7,7 @@ from pyspark.sql import functions as f from gentropy.common.session import Session -from gentropy.dataset.variant_annotation import VariantAnnotation +from gentropy.dataset.variant_index import VariantIndex from gentropy.datasource.gwas_catalog.associations import ( GWASCatalogCuratedAssociationsParser, ) @@ -84,7 +85,7 @@ def process_harmonised_list(studies: list[str], session: Session) -> DataFrame: @staticmethod def get_gwas_catalog_study_index( session: Session, - variant_annotation_path: str, + gnomad_variant_path: str, catalog_study_files: list[str], catalog_ancestry_files: list[str], harmonised_study_file: str, @@ -95,7 +96,7 @@ def get_gwas_catalog_study_index( Args: session (Session): Session object. - variant_annotation_path (str): Input variant annotation path. + gnomad_variant_path (str): Path to GnomAD variant list. catalog_study_files (list[str]): List of raw GWAS catalog studies file. catalog_ancestry_files (list[str]): List of raw ancestry annotations files from GWAS Catalog. harmonised_study_file (str): GWAS Catalog summary statistics lookup table. @@ -106,7 +107,7 @@ def get_gwas_catalog_study_index( StudyIndexGWASCatalog: Completely processed and fully annotated study index. """ # Extract - va = VariantAnnotation.from_parquet(session, variant_annotation_path) + gnomad_variants = VariantIndex.from_parquet(session, gnomad_variant_path) catalog_studies = session.spark.read.csv( list(catalog_study_files), sep="\t", header=True ) @@ -130,7 +131,9 @@ def get_gwas_catalog_study_index( ancestry_lut, sumstats_lut, ).annotate_from_study_curation(gwas_catalog_study_curation), - GWASCatalogCuratedAssociationsParser.from_source(catalog_associations, va), + GWASCatalogCuratedAssociationsParser.from_source( + catalog_associations, gnomad_variants + ), ) return study_index @@ -142,7 +145,7 @@ def __init__( catalog_ancestry_files: list[str], catalog_associations_file: str, gwas_catalog_study_curation_file: str, - variant_annotation_path: str, + gnomad_variant_path: str, harmonised_study_file: str, criteria: str, inclusion_list_path: str, @@ -151,12 +154,12 @@ def __init__( """Run step. Args: - session (Session): Session object. + session (Session): Session objecct. catalog_study_files (list[str]): List of raw GWAS catalog studies file. catalog_ancestry_files (list[str]): List of raw ancestry annotations files from GWAS Catalog. catalog_associations_file (str): Raw GWAS catalog associations file. gwas_catalog_study_curation_file (str): file of the curation table. Optional. - variant_annotation_path (str): Input variant annotation path. + gnomad_variant_path (str): Path to GnomAD variant list. harmonised_study_file (str): GWAS Catalog summary statistics lookup table. criteria (str): name of the filter set to be applied. inclusion_list_path (str): Output path for the inclusion list. @@ -165,7 +168,7 @@ def __init__( # Create study index: study_index = self.get_gwas_catalog_study_index( session, - variant_annotation_path, + gnomad_variant_path, catalog_study_files, catalog_ancestry_files, harmonised_study_file, diff --git a/src/gentropy/variant_annotation.py b/src/gentropy/variant_annotation.py deleted file mode 100644 index 355a4dfea..000000000 --- a/src/gentropy/variant_annotation.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Step to generate variant annotation dataset.""" - -from __future__ import annotations - -import hail as hl - -from gentropy.common.session import Session -from gentropy.common.types import VariantPopulation -from gentropy.common.version_engine import VersionEngine -from gentropy.config import VariantAnnotationConfig -from gentropy.datasource.gnomad.variants import GnomADVariants - - -class VariantAnnotationStep: - """Variant annotation step. - - Variant annotation step produces a dataset of the type `VariantAnnotation` derived from gnomADs `gnomad.genomes.vX.X.X.sites.ht` Hail's table. - This dataset is used to validate variants and as a source of annotation. - """ - - def __init__( - self, - session: Session, - variant_annotation_path: str, - gnomad_genomes_path: str = VariantAnnotationConfig().gnomad_genomes_path, - gnomad_variant_populations: list[ - VariantPopulation | str - ] = VariantAnnotationConfig().gnomad_variant_populations, - chain_38_37: str = VariantAnnotationConfig().chain_38_37, - use_version_from_input: bool = VariantAnnotationConfig().use_version_from_input, - ) -> None: - """Run Variant Annotation step. - - Args: - session (Session): Session object. - variant_annotation_path (str): Variant annotation dataset path. - gnomad_genomes_path (str): Path to gnomAD genomes hail table, e.g. `gs://gcp-public-data--gnomad/release/4.0/ht/genomes/gnomad.genomes.v4.0.sites.ht/`. - gnomad_variant_populations (list[VariantPopulation | str]): List of populations to include. - chain_38_37 (str): Path to GRCh38 to GRCh37 chain file for lifover. - use_version_from_input (bool): Append version derived from input gnomad_genomes_path to the output variant_annotation_path. Defaults to False. - - In case use_version_from_input is set to True, - data source version inferred from gnomad_genomes_path is appended as the last path segment to the output path. - All defaults are stored in the VariantAnnotationConfig. - """ - # amend data source version to output path - if use_version_from_input: - variant_annotation_path = VersionEngine("gnomad").amend_version( - gnomad_genomes_path, variant_annotation_path - ) - - # Initialise hail session. - hl.init(sc=session.spark.sparkContext, log="/dev/null") - # Run variant annotation. - variant_annotation = GnomADVariants( - gnomad_genomes_path=gnomad_genomes_path, - gnomad_variant_populations=gnomad_variant_populations, - chain_38_37=chain_38_37, - ).as_variant_annotation() - - # Write data partitioned by chromosome and position. - ( - variant_annotation.df.write.mode(session.write_mode).parquet( - variant_annotation_path - ) - ) diff --git a/src/gentropy/variant_index.py b/src/gentropy/variant_index.py index bdf838ce2..ba86c4602 100644 --- a/src/gentropy/variant_index.py +++ b/src/gentropy/variant_index.py @@ -1,44 +1,53 @@ -"""Step to generate variant index dataset.""" +"""Step to generate variant index dataset based on VEP output.""" + from __future__ import annotations from gentropy.common.session import Session -from gentropy.dataset.study_locus import StudyLocus -from gentropy.dataset.variant_annotation import VariantAnnotation from gentropy.dataset.variant_index import VariantIndex +from gentropy.datasource.ensembl.vep_parser import VariantEffectPredictorParser class VariantIndexStep: - """Run variant index step to only variants in study-locus sets. + """Generate variant index based on a VEP output in json format. - Using a `VariantAnnotation` dataset as a reference, this step creates and writes a dataset of the type `VariantIndex` that includes only variants that have disease-association data with a reduced set of annotations. + The variant index is a dataset that contains variant annotations extracted from VEP output. It is expected that all variants in the VEP output are present in the variant index. + There's an option to provide extra variant annotations to be added to the variant index eg. allele frequencies from GnomAD. """ def __init__( self: VariantIndexStep, session: Session, - variant_annotation_path: str, - credible_set_path: str, + vep_output_json_path: str, variant_index_path: str, + gnomad_variant_annotations_path: str | None = None, ) -> None: """Run VariantIndex step. Args: session (Session): Session object. - variant_annotation_path (str): Variant annotation dataset path. - credible_set_path (str): Credible set dataset path. - variant_index_path (str): Variant index dataset path. + vep_output_json_path (str): Variant effect predictor output path (in json format). + variant_index_path (str): Variant index dataset path to save resulting data. + gnomad_variant_annotations_path (str | None): Path to extra variant annotation dataset. """ - # Extract - va = VariantAnnotation.from_parquet(session, variant_annotation_path) - credible_set = StudyLocus.from_parquet( - session, credible_set_path, recursiveFileLookup=True + # Extract variant annotations from VEP output: + variant_index = VariantEffectPredictorParser.extract_variant_index_from_vep( + session.spark, vep_output_json_path ) - # Transform - vi = VariantIndex.from_variant_annotation(va, credible_set) + # Process variant annotations if provided: + if gnomad_variant_annotations_path: + # Read variant annotations from parquet: + annotations = VariantIndex.from_parquet( + session=session, + path=gnomad_variant_annotations_path, + recursiveFileLookup=True, + ) + + # Update file with extra annotations: + variant_index = variant_index.add_annotation(annotations) ( - vi.df.write.partitionBy("chromosome") + variant_index.df.write.partitionBy("chromosome") .mode(session.write_mode) .parquet(variant_index_path) ) diff --git a/src/gentropy/v2g.py b/src/gentropy/variant_to_gene.py similarity index 81% rename from src/gentropy/v2g.py rename to src/gentropy/variant_to_gene.py index e98348a01..cf21053d7 100644 --- a/src/gentropy/v2g.py +++ b/src/gentropy/variant_to_gene.py @@ -1,16 +1,16 @@ """Step to generate variant annotation dataset.""" + from __future__ import annotations from functools import reduce -import pyspark.sql.functions as f +from pyspark.sql import functions as f from gentropy.common.Liftover import LiftOverSpark from gentropy.common.session import Session from gentropy.dataset.gene_index import GeneIndex from gentropy.dataset.intervals import Intervals from gentropy.dataset.v2g import V2G -from gentropy.dataset.variant_annotation import VariantAnnotation from gentropy.dataset.variant_index import VariantIndex @@ -26,7 +26,6 @@ class V2GStep: Attributes: session (Session): Session object. variant_index_path (str): Input variant index path. - variant_annotation_path (str): Input variant annotation path. gene_index_path (str): Input gene index path. vep_consequences_path (str): Input VEP consequences path. liftover_chain_file_path (str): Path to GRCh37 to GRCh38 chain file. @@ -41,7 +40,6 @@ def __init__( self, session: Session, variant_index_path: str, - variant_annotation_path: str, gene_index_path: str, vep_consequences_path: str, liftover_chain_file_path: str, @@ -56,7 +54,6 @@ def __init__( Args: session (Session): Session object. variant_index_path (str): Input variant index path. - variant_annotation_path (str): Input variant annotation path. gene_index_path (str): Input gene index path. vep_consequences_path (str): Input VEP consequences path. liftover_chain_file_path (str): Path to GRCh37 to GRCh38 chain file. @@ -69,16 +66,10 @@ def __init__( # Read gene_index = GeneIndex.from_parquet(session, gene_index_path) vi = VariantIndex.from_parquet(session, variant_index_path).persist() - va = VariantAnnotation.from_parquet(session, variant_annotation_path) + # Reading VEP consequence to score table and cast the score to the right type: vep_consequences = session.spark.read.csv( vep_consequences_path, sep="\t", header=True - ).select( - f.element_at(f.split("Accession", r"/"), -1).alias( - "variantFunctionalConsequenceId" - ), - f.col("Term").alias("label"), - f.col("v2g_score").cast("double").alias("score"), - ) + ).withColumn("score", f.col("score").cast("double")) # Transform lift = LiftOverSpark( @@ -90,10 +81,7 @@ def __init__( # Filter gene index by approved biotypes to define V2G gene universe list(approved_biotypes) ) - va_slimmed = va.filter_by_variant_df( - # Variant annotation reduced to the variant index to define V2G variant universe - vi.df - ).persist() + intervals = Intervals( _df=reduce( lambda x, y: x.unionByName(y, allowMissingColumns=True), @@ -108,9 +96,11 @@ def __init__( _schema=Intervals.get_schema(), ) v2g_datasets = [ - va_slimmed.get_distance_to_tss(gene_index_filtered, max_distance), - va_slimmed.get_most_severe_vep_v2g(vep_consequences, gene_index_filtered), - va_slimmed.get_plof_v2g(gene_index_filtered), + vi.get_distance_to_tss(gene_index_filtered, max_distance), + vi.get_most_severe_transcript_consequence( + vep_consequences, gene_index_filtered + ), + vi.get_plof_v2g(gene_index_filtered), intervals.v2g(vi), ] v2g = V2G( diff --git a/tests/gentropy/conftest.py b/tests/gentropy/conftest.py index 62b873355..c35188466 100644 --- a/tests/gentropy/conftest.py +++ b/tests/gentropy/conftest.py @@ -23,7 +23,6 @@ from gentropy.dataset.study_locus_overlap import StudyLocusOverlap from gentropy.dataset.summary_statistics import SummaryStatistics from gentropy.dataset.v2g import V2G -from gentropy.dataset.variant_annotation import VariantAnnotation from gentropy.dataset.variant_index import VariantIndex from gentropy.datasource.eqtl_catalogue.finemapping import EqtlCatalogueFinemapping from gentropy.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex @@ -276,45 +275,6 @@ def mock_v2g(spark: SparkSession) -> V2G: return V2G(_df=data_spec.build(), _schema=v2g_schema) -@pytest.fixture() -def mock_variant_annotation(spark: SparkSession) -> VariantAnnotation: - """Mock variant annotation.""" - va_schema = VariantAnnotation.get_schema() - - data_spec = ( - dg.DataGenerator( - spark, - rows=400, - partitions=4, - randomSeedMethod="hash_fieldname", - ) - .withSchema(va_schema) - .withColumnSpec("alleleType", percentNulls=0.1) - .withColumnSpec("chromosomeB37", percentNulls=0.1) - .withColumnSpec("positionB37", percentNulls=0.1) - # Nested column handling workaround - # https://github.com/databrickslabs/dbldatagen/issues/135 - # It's a workaround for nested column handling in dbldatagen. - .withColumnSpec( - "alleleFrequencies", - expr='array(named_struct("alleleFrequency", rand(), "populationName", cast(rand() as string)))', - percentNulls=0.1, - ) - .withColumnSpec("rsIds", expr="array(cast(rand() AS string))", percentNulls=0.1) - .withColumnSpec( - "vep", - expr='named_struct("mostSevereConsequence", cast(rand() as string), "transcriptConsequences", array(named_struct("aminoAcids", cast(rand() as string), "consequenceTerms", array(cast(rand() as string)), "geneId", cast(rand() as string), "lof", cast(rand() as string))))', - percentNulls=0.1, - ) - .withColumnSpec( - "inSilicoPredictors", - expr='named_struct("cadd", named_struct("phred", cast(rand() as float), "raw_score", cast(rand() as float)), "revelMax", cast(rand() as double), "spliceaiDsMax", cast(rand() as float), "pangolinLargestDs", cast(rand() as double), "phylop", cast(rand() as double), "polyphenMax", cast(rand() as double), "siftMax", cast(rand() as double))', - percentNulls=0.1, - ) - ) - return VariantAnnotation(_df=data_spec.build(), _schema=va_schema) - - @pytest.fixture() def mock_variant_index(spark: SparkSession) -> VariantIndex: """Mock variant index.""" @@ -328,23 +288,65 @@ def mock_variant_index(spark: SparkSession) -> VariantIndex: randomSeedMethod="hash_fieldname", ) .withSchema(vi_schema) - .withColumnSpec("chromosomeB37", percentNulls=0.1) - .withColumnSpec("positionB37", percentNulls=0.1) - .withColumnSpec("mostSevereConsequence", percentNulls=0.1) + .withColumnSpec("mostSevereConsequenceId", percentNulls=0.1) # Nested column handling workaround # https://github.com/databrickslabs/dbldatagen/issues/135 # It's a workaround for nested column handling in dbldatagen. + .withColumnSpec( + "inSilicoPredictors", + expr=""" + array( + named_struct( + "method", cast(rand() as string), + "assessment", cast(rand() as string), + "score", rand(), + "assessmentFlag", cast(rand() as string), + "targetId", cast(rand() as string) + ) + ) + """, + percentNulls=0.1, + ) .withColumnSpec( "alleleFrequencies", expr='array(named_struct("alleleFrequency", rand(), "populationName", cast(rand() as string)))', percentNulls=0.1, ) + .withColumnSpec("rsIds", expr="array(cast(rand() AS string))", percentNulls=0.1) .withColumnSpec( - "inSilicoPredictors", - expr='named_struct("cadd", named_struct("phred", cast(rand() as float), "raw_score", cast(rand() as float)), "revelMax", cast(rand() as double), "spliceaiDsMax", cast(rand() as float), "pangolinLargestDs", cast(rand() as double), "phylop", cast(rand() as double), "polyphenMax", cast(rand() as double), "siftMax", cast(rand() as double))', + "transcriptConsequences", + expr=""" + array( + named_struct( + "variantFunctionalConsequenceIds", array(cast(rand() as string)), + "aminoAcidChange", cast(rand() as string), + "uniprotAccessions", array(cast(rand() as string)), + "isEnsemblCanonical", cast(rand() as boolean), + "codons", cast(rand() as string), + "distance", cast(rand() as long), + "targetId", cast(rand() as string), + "impact", cast(rand() as string), + "lofteePrediction", cast(rand() as string), + "siftPrediction", rand(), + "polyphenPrediction", rand(), + "transcriptId", cast(rand() as string) + ) + ) + """, + percentNulls=0.1, + ) + .withColumnSpec( + "dbXrefs", + expr=""" + array( + named_struct( + "id", cast(rand() as string), + "source", cast(rand() as string) + ) + ) + """, percentNulls=0.1, ) - .withColumnSpec("rsIds", expr="array(cast(rand() AS string))", percentNulls=0.1) ) return VariantIndex(_df=data_spec.build(), _schema=vi_schema) diff --git a/tests/gentropy/data_samples/finucane_PIPs.npy b/tests/gentropy/data_samples/finucane_PIPs.npy deleted file mode 100644 index 2a93a5c713b018031fcce92b70614a8b8ef1790b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40128 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 Gci{hy&#Ywt diff --git a/tests/gentropy/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz b/tests/gentropy/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz deleted file mode 100644 index 74c0844a4ad146b41483bcd07d65d85a8700a520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 491 zcmV(6u;>h`#B z%2M+N-_s?H^K{I=s)y?tUenOPEkSdE<^YEueRu9|-8sRvyR}2xU7$~I(3}Pseq9sv zxAyoY@1mMS73*F71p6nz3Q;f$3q0>%c31|pdclRxTS;9+T{zKGC4unIpuAUOK+a!A zqM$(J87Kh3SRQup1TXt<8B5h6h8Sp_&ifc2DpD{Fvd}ARe;w6m9N{Zg!ZA9Zm&l^! z$#y7X7XMJo%+wZojcRhJij=9KFtKL3!lXfEJ5z9^j|`d6yj$qiE`B-%AQWtN{00izCs%I<003u4 None: - """Test gene index creation with mock gene index.""" - assert isinstance(mock_variant_annotation, VariantAnnotation) - - -def test_get_plof_v2g( - mock_variant_annotation: VariantAnnotation, mock_gene_index: GeneIndex -) -> None: - """Test get_plof_v2g with mock variant annotation.""" - assert isinstance(mock_variant_annotation.get_plof_v2g(mock_gene_index), V2G) - - -def test_get_distance_to_tss( - mock_variant_annotation: VariantAnnotation, mock_gene_index: GeneIndex -) -> None: - """Test get_distance_to_tss with mock variant annotation.""" - assert isinstance(mock_variant_annotation.get_distance_to_tss(mock_gene_index), V2G) diff --git a/tests/gentropy/dataset/test_variant_index.py b/tests/gentropy/dataset/test_variant_index.py index a41db3031..4d97ef536 100644 --- a/tests/gentropy/dataset/test_variant_index.py +++ b/tests/gentropy/dataset/test_variant_index.py @@ -4,11 +4,12 @@ from typing import TYPE_CHECKING +from gentropy.dataset.gene_index import GeneIndex +from gentropy.dataset.v2g import V2G from gentropy.dataset.variant_index import VariantIndex if TYPE_CHECKING: - from gentropy.dataset.study_locus import StudyLocus - from gentropy.dataset.variant_annotation import VariantAnnotation + pass def test_variant_index_creation(mock_variant_index: VariantIndex) -> None: @@ -16,11 +17,15 @@ def test_variant_index_creation(mock_variant_index: VariantIndex) -> None: assert isinstance(mock_variant_index, VariantIndex) -def test_from_variant_annotation( - mock_variant_annotation: VariantAnnotation, mock_study_locus: StudyLocus +def test_get_plof_v2g( + mock_variant_index: VariantIndex, mock_gene_index: GeneIndex ) -> None: - """Test variant index creation from variant annotation.""" - variant_index = VariantIndex.from_variant_annotation( - mock_variant_annotation, mock_study_locus - ) - assert isinstance(variant_index, VariantIndex) + """Test get_plof_v2g with mock variant annotation.""" + assert isinstance(mock_variant_index.get_plof_v2g(mock_gene_index), V2G) + + +def test_get_distance_to_tss( + mock_variant_index: VariantIndex, mock_gene_index: GeneIndex +) -> None: + """Test get_distance_to_tss with mock variant annotation.""" + assert isinstance(mock_variant_index.get_distance_to_tss(mock_gene_index), V2G) diff --git a/tests/gentropy/datasource/ensembl/test_vep_variants.py b/tests/gentropy/datasource/ensembl/test_vep_variants.py new file mode 100644 index 000000000..1401f1b6c --- /dev/null +++ b/tests/gentropy/datasource/ensembl/test_vep_variants.py @@ -0,0 +1,145 @@ +"""Testing VEP parsing and variant index extraction.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from gentropy.dataset.variant_index import VariantIndex +from gentropy.datasource.ensembl.vep_parser import VariantEffectPredictorParser +from pyspark.sql import DataFrame +from pyspark.sql import functions as f + +if TYPE_CHECKING: + from pyspark.sql import SparkSession + + +class TestVEPParserInSilicoExtractor: + """Testing the _vep_in_silico_prediction_extractor method of the VEP parser class. + + These tests assumes that the _get_most_severe_transcript() method works correctly, as it's not tested. + + The test cases try to cover the following scenarios: + - Transcripts with no assessments. + - Transcripts without assessments flag. + - Transcripts with no score. + - Testing cases, where no score column is provided. + """ + + # Data prototype: + SAMPLE_DATA = [ + # Complete dataset: + ("v1", "deleterious", 0.1, "gene1", "flag"), + # No assessment: + ("v2", None, 0.1, "gene1", "flag"), + # No flag: + ("v3", "deleterious", 0.1, "gene1", None), + # No score: + ("v4", "deleterious", None, "gene1", "flag"), + ] + + SAMPLE_COLUMNS = ["variantId", "assessment", "score", "gene_id", "flag"] + + @pytest.fixture(autouse=True) + def _setup(self: TestVEPParserInSilicoExtractor, spark: SparkSession) -> None: + """Setup fixture.""" + parsed_df = ( + spark.createDataFrame(self.SAMPLE_DATA, self.SAMPLE_COLUMNS) + .groupBy("variantId") + .agg( + f.collect_list( + f.struct( + f.col("assessment").alias("assessment"), + f.col("score").alias("score"), + f.col("flag").alias("flag"), + f.col("gene_id").alias("gene_id"), + ) + ).alias("transcripts") + ) + .select( + "variantId", + VariantEffectPredictorParser._vep_in_silico_prediction_extractor( + "transcripts", "method_name", "score", "assessment", "flag" + ).alias("in_silico_predictions"), + ) + ).persist() + + self.df = parsed_df + + def test_in_silico_output_missing_value( + self: TestVEPParserInSilicoExtractor, + ) -> None: + """Test if the in silico output count is correct.""" + variant_with_missing_score = [ + x[0] for x in filter(lambda x: x[2] is None, self.SAMPLE_DATA) + ] + # Assert that the correct variants return null: + assert ( + [ + x["variantId"] + for x in self.df.filter( + f.col("in_silico_predictions").isNull() + ).collect() + ] + == variant_with_missing_score + ), "Not the right variants got nullified in-silico predictor object." + + +class TestVEPParser: + """Testing VEP parser class. + + Some of the input data: + - 6_151445307_C_T - complicated variant with numerous annotations. + - 2_140699625_G_GT - simple variant with no annotations whatsoever. + """ + + SAMPLE_VEP_DATA_PATH = "tests/gentropy/data_samples/vep_sample.jsonl" + + @pytest.fixture(autouse=True) + def _setup(self: TestVEPParser, spark: SparkSession) -> None: + """Setup fixture.""" + self.raw_vep_output = spark.read.json( + self.SAMPLE_VEP_DATA_PATH, + schema=VariantEffectPredictorParser.get_schema(), + ) + self.processed_vep_output = VariantEffectPredictorParser.process_vep_output( + self.raw_vep_output + ) + + def test_extract_variant_index_from_vep( + self: TestVEPParser, spark: SparkSession + ) -> None: + """Test if the variant index can be extracted from the VEP output.""" + variant_index = VariantEffectPredictorParser.extract_variant_index_from_vep( + spark, self.SAMPLE_VEP_DATA_PATH + ) + + assert isinstance( + variant_index, VariantIndex + ), "VariantIndex object not created." + + def test_process(self: TestVEPParser) -> None: + """Test process method.""" + df = VariantEffectPredictorParser.process_vep_output(self.raw_vep_output) + assert isinstance(df, DataFrame), "Processed VEP output is not a DataFrame." + assert df.count() > 0, "No variant data in processed VEP dataframe." + + def test_conversion(self: TestVEPParser) -> None: + """Test if processed data can be converted into a VariantIndex object.""" + variant_index = VariantIndex( + _df=self.processed_vep_output, + _schema=VariantIndex.get_schema(), + ) + + assert isinstance( + variant_index, VariantIndex + ), "VariantIndex object not created." + + def test_variant_count(self: TestVEPParser) -> None: + """Test if the number of variants is correct. + + It is expected that all rows from the parsed VEP output are present in the processed VEP output. + """ + assert ( + self.raw_vep_output.count() == self.processed_vep_output.count() + ), f"Incorrect number of variants in processed VEP output: expected {self.raw_vep_output.count()}, got {self.processed_vep_output.count()}." diff --git a/tests/gentropy/datasource/gwas_catalog/test_gwas_catalog_associations.py b/tests/gentropy/datasource/gwas_catalog/test_gwas_catalog_associations.py index de70e8b63..e7067e3d9 100644 --- a/tests/gentropy/datasource/gwas_catalog/test_gwas_catalog_associations.py +++ b/tests/gentropy/datasource/gwas_catalog/test_gwas_catalog_associations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from gentropy.dataset.variant_annotation import VariantAnnotation +from gentropy.dataset.variant_index import VariantIndex from gentropy.datasource.gwas_catalog.associations import ( GWASCatalogCuratedAssociationsParser, StudyLocusGWASCatalog, @@ -50,29 +50,29 @@ def test_qc_ambiguous_study( def test_study_locus_gwas_catalog_from_source( - mock_variant_annotation: VariantAnnotation, + mock_variant_index: VariantIndex, sample_gwas_catalog_associations: DataFrame, ) -> None: """Test study locus from gwas catalog mock data.""" assert isinstance( GWASCatalogCuratedAssociationsParser.from_source( - sample_gwas_catalog_associations, mock_variant_annotation + sample_gwas_catalog_associations, mock_variant_index ), StudyLocusGWASCatalog, ) -def test__map_to_variant_annotation_variants( +def test_map_variants_to_variant_index( sample_gwas_catalog_associations: DataFrame, - mock_variant_annotation: VariantAnnotation, + mock_variant_index: VariantIndex, ) -> None: """Test mapping to variant annotation variants.""" assert isinstance( - GWASCatalogCuratedAssociationsParser._map_to_variant_annotation_variants( + GWASCatalogCuratedAssociationsParser._map_variants_to_variant_index( sample_gwas_catalog_associations.withColumn( "studyLocusId", f.monotonically_increasing_id().cast(LongType()) ), - mock_variant_annotation, + mock_variant_index, ), DataFrame, ) diff --git a/tests/gentropy/test_schemas.py b/tests/gentropy/test_schemas.py index 417eb4657..630abd0ab 100644 --- a/tests/gentropy/test_schemas.py +++ b/tests/gentropy/test_schemas.py @@ -57,6 +57,9 @@ def test_schema_columns_camelcase(schema_json: str) -> None: Args: schema_json (str): schema filename """ + if schema_json == "vep_json_output.json": + pytest.skip("VEP schema is exempt from camelCase check.") + core_schema = json.loads(Path(SCHEMA_DIR, schema_json).read_text(encoding="utf-8")) schema = StructType.fromJson(core_schema) # Use a regular expression to check if the identifier is in camelCase