From c160fff9b323c61cf4aeb3569c797f8e9a87c6b6 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 17 Oct 2023 13:38:29 +0100 Subject: [PATCH 01/47] feat: add eQTL ingestion to the list of steps in DAG --- src/airflow/dags/dag_preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/airflow/dags/dag_preprocess.py b/src/airflow/dags/dag_preprocess.py index 345586080..1a52a495a 100644 --- a/src/airflow/dags/dag_preprocess.py +++ b/src/airflow/dags/dag_preprocess.py @@ -8,7 +8,7 @@ CLUSTER_NAME = "otg-preprocess" -ALL_STEPS = ["finngen", "ld_index", "variant_annotation"] +ALL_STEPS = ["finngen", "eqtl_catalogue", "ld_index", "variant_annotation"] with DAG( From a9d961af1e872d127e66d7eeb84050cbc3ead835 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:11:54 +0000 Subject: [PATCH 02/47] docs: update running instructions --- docs/development/contributing.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/development/contributing.md b/docs/development/contributing.md index f016f8646..af9b69af6 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -29,7 +29,7 @@ All pipelines in this repository are intended to be run in Google Dataproc. Runn In order to run the code: -1. Manually edit your local `workflow/dag.yaml` file and comment out the steps you do not want to run. +1. Manually edit your local `src/airflow/dags/*` file and comment out the steps you do not want to run. 2. Manually edit your local `pyproject.toml` file and modify the version of the code. - This must be different from the version used by any other people working on the repository to avoid any deployment conflicts, so it's a good idea to use your name, for example: `1.2.3+jdoe`. @@ -37,17 +37,15 @@ In order to run the code: - Note that the version must comply with [PEP440 conventions](https://peps.python.org/pep-0440/#normalization), otherwise Poetry will not allow it to be deployed. - Do not use underscores or hyphens in your version name. When building the WHL file, they will be automatically converted to dots, which means the file name will no longer match the version and the build will fail. Use dots instead. -3. Run `make build`. +3. Manually edit your local `src/airflow/dags/common_airflow.py` and set `OTG_VERSION` to the same version as you did in the previous step. + +4. Run `make build`. - This will create a bundle containing the neccessary code, configuration and dependencies to run the ETL pipeline, and then upload this bundle to Google Cloud. - A version specific subpath is used, so uploading the code will not affect any branches but your own. - If there was already a code bundle uploaded with the same version number, it will be replaced. -4. Submit the Dataproc job with `poetry run python workflow/workflow_template.py` - - You will need to specify additional parameters, some are mandatory and some are optional. Run with `--help` to see usage. - - The script will provision the cluster and submit the job. - - The cluster will take a few minutes to get provisioned and running, during which the script will not output anything, this is normal. - - Once submitted, you can monitor the progress of your job on this page: https://console.cloud.google.com/dataproc/jobs?project=open-targets-genetics-dev. - - On completion (whether successful or a failure), the cluster will be automatically removed, so you don't have to worry about shutting it down to avoid incurring charges. +5. Open Airflow UI and run the DAG. + ## Contributing checklist When making changes, and especially when implementing a new module or feature, it's essential to ensure that all relevant sections of the code base are modified. From 369051621844b01b9d8e2d3e3e24f65d6466a24c Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:15:45 +0000 Subject: [PATCH 03/47] docs: update contributing checklist --- docs/development/contributing.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/development/contributing.md b/docs/development/contributing.md index af9b69af6..d666995fc 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -55,19 +55,20 @@ When making changes, and especially when implementing a new module or feature, i - [ ] Update the documentation and check it with `make build-documentation`. This will start a local server to browse it (URL will be printed, usually `http://127.0.0.1:8000/`) For more details on each of these steps, see the sections below. + ### Documentation * If during development you had a question which wasn't covered in the documentation, and someone explained it to you, add it to the documentation. The same applies if you encountered any instructions in the documentation which were obsolete or incorrect. * Documentation autogeneration expressions start with `:::`. They will automatically generate sections of the documentation based on class and method docstrings. Be sure to update them for: - + Dataset definitions in `docs/reference/dataset` (example: `docs/reference/dataset/study_index/study_index_finngen.md`) - + Step definition in `docs/reference/step` (example: `docs/reference/step/finngen.md`) + + Dataset definitions in `docs/python_api/datasource/STEP` (example: `docs/python_api/datasource/finngen/study_index/study_index.md`) + + Step definition in `docs/python_api/step/STEP.md` (example: `docs/python_api/step/finngen.md`) ### Configuration * Input and output paths in `config/datasets/gcp.yaml` * Step configuration in `config/step/STEP.yaml` (example: `config/step/finngen.yaml`) ### Classes -* Dataset class in `src/org/dataset/` (example: `src/otg/dataset/study_index.py` → `StudyIndexFinnGen`) -* Step main running class in `src/org/STEP.py` (example: `src/org/finngen.py`) +* Dataset class in `src/otg/datasource/STEP` (example: `src/otg/datasource/finngen/study_index.py` → `FinnGenStudyIndex`) +* Step main running class in `src/otg/STEP.py` (example: `src/otg/finngen.py`) ### Tests * Test study fixture in `tests/conftest.py` (example: `mock_study_index_finngen` in that module) From 49b8c0e3a305fc09d40fc023811b7b2d270ceba3 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:19:13 +0000 Subject: [PATCH 04/47] docs: add automatically generated docs --- docs/development/contributing.md | 2 +- docs/python_api/datasource/eqtl_catalogue/study_index.md | 4 ++++ docs/python_api/datasource/eqtl_catalogue/summary_stats.md | 4 ++++ docs/python_api/step/eqtl_catalogue.md | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/python_api/datasource/eqtl_catalogue/study_index.md create mode 100644 docs/python_api/datasource/eqtl_catalogue/summary_stats.md create mode 100644 docs/python_api/step/eqtl_catalogue.md diff --git a/docs/development/contributing.md b/docs/development/contributing.md index d666995fc..3b37d18e8 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -59,7 +59,7 @@ For more details on each of these steps, see the sections below. ### Documentation * If during development you had a question which wasn't covered in the documentation, and someone explained it to you, add it to the documentation. The same applies if you encountered any instructions in the documentation which were obsolete or incorrect. * Documentation autogeneration expressions start with `:::`. They will automatically generate sections of the documentation based on class and method docstrings. Be sure to update them for: - + Dataset definitions in `docs/python_api/datasource/STEP` (example: `docs/python_api/datasource/finngen/study_index/study_index.md`) + + Dataset definitions in `docs/python_api/datasource/STEP` (example: `docs/python_api/datasource/finngen/study_index.md`) + Step definition in `docs/python_api/step/STEP.md` (example: `docs/python_api/step/finngen.md`) ### Configuration diff --git a/docs/python_api/datasource/eqtl_catalogue/study_index.md b/docs/python_api/datasource/eqtl_catalogue/study_index.md new file mode 100644 index 000000000..c1e675200 --- /dev/null +++ b/docs/python_api/datasource/eqtl_catalogue/study_index.md @@ -0,0 +1,4 @@ +--- +title: Study Index +--- +::: otg.datasource.eqtl_catalogue.study_index.EqtlCatalogueStudyIndex diff --git a/docs/python_api/datasource/eqtl_catalogue/summary_stats.md b/docs/python_api/datasource/eqtl_catalogue/summary_stats.md new file mode 100644 index 000000000..62f2b4be2 --- /dev/null +++ b/docs/python_api/datasource/eqtl_catalogue/summary_stats.md @@ -0,0 +1,4 @@ +--- +title: Summary Stats +--- +::: otg.datasource.eqtl_catalogue.summary_stats.EqtlCatalogueSummaryStats diff --git a/docs/python_api/step/eqtl_catalogue.md b/docs/python_api/step/eqtl_catalogue.md new file mode 100644 index 000000000..06cb578c1 --- /dev/null +++ b/docs/python_api/step/eqtl_catalogue.md @@ -0,0 +1,4 @@ +--- +title: eQTL Catalogue +--- +::: otg.finngen.EqtlCatalogueStep From f7e7e3df90783114e02c1901884e75a8b2257ce6 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:25:38 +0000 Subject: [PATCH 05/47] chore: add configuration --- config/datasets/gcp.yaml | 3 +++ config/step/eqtl_catalogue.yaml | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 config/step/eqtl_catalogue.yaml diff --git a/config/datasets/gcp.yaml b/config/datasets/gcp.yaml index 829696e83..c9c57063d 100644 --- a/config/datasets/gcp.yaml +++ b/config/datasets/gcp.yaml @@ -19,6 +19,7 @@ finngen_phenotype_table_url: https://r9.finngen.fi/api/phenos ukbiobank_manifest: gs://genetics-portal-input/ukb_phenotypes/neale2_saige_study_manifest.190430.tsv l2g_gold_standard_curation: ${datasets.inputs}/l2g/gold_standard/curation.json gene_interactions: ${datasets.inputs}/l2g/interaction # 23.09 data +eqtl_catalogue_paths_imported: https://raw.githubusercontent.com/eQTL-Catalogue/eQTL-Catalogue-resources/master/tabix/tabix_ftp_paths_imported.tsv # Output datasets gene_index: ${datasets.outputs}/gene_index @@ -36,6 +37,8 @@ finngen_summary_stats: ${datasets.outputs}/finngen_summary_stats ukbiobank_study_index: ${datasets.outputs}/ukbiobank_study_index l2g_model: ${datasets.outputs}/l2g_model l2g_predictions: ${datasets.outputs}/l2g_predictions +eqtl_catalogue_study_index_out: ${datasets.outputs}/preprocess/eqtl_catalogue/study_index +eqtl_catalogue_summary_stats_out: ${datasets.outputs}/preprocess/eqtl_catalogue/summary_stats # Constants finngen_release_prefix: FINNGEN_R9 diff --git a/config/step/eqtl_catalogue.yaml b/config/step/eqtl_catalogue.yaml new file mode 100644 index 000000000..04a958993 --- /dev/null +++ b/config/step/eqtl_catalogue.yaml @@ -0,0 +1,4 @@ +_target_: otg.eqtl_catalogue.EqtlCatalogueStep +eqtl_catalogue_paths_imported: ${datasets.eqtl_catalogue_paths_imported} +eqtl_catalogue_study_index_out: ${datasets.eqtl_catalogue_study_index_out} +eqtl_catalogue_summary_stats_out: ${datasets.eqtl_catalogue_summary_stats_out} From 9e312e6fe2e41f2c835c7c97eabe980be22d1d66 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:57:14 +0000 Subject: [PATCH 06/47] chore: unify FinnGen config with eQTL Catalogue --- config/datasets/gcp.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/datasets/gcp.yaml b/config/datasets/gcp.yaml index c9c57063d..7d8c2a737 100644 --- a/config/datasets/gcp.yaml +++ b/config/datasets/gcp.yaml @@ -15,10 +15,10 @@ catalog_associations: ${datasets.inputs}/v2d/gwas_catalog_v1.0.2-associations_e1 catalog_studies: ${datasets.inputs}/v2d/gwas-catalog-v1.0.3-studies-r2023-09-11.tsv catalog_ancestries: ${datasets.inputs}/v2d/gwas-catalog-v1.0.3-ancestries-r2023-09-11.tsv catalog_sumstats_lut: ${datasets.inputs}/v2d/harmonised_list-r2023-09-11.txt -finngen_phenotype_table_url: https://r9.finngen.fi/api/phenos ukbiobank_manifest: gs://genetics-portal-input/ukb_phenotypes/neale2_saige_study_manifest.190430.tsv l2g_gold_standard_curation: ${datasets.inputs}/l2g/gold_standard/curation.json gene_interactions: ${datasets.inputs}/l2g/interaction # 23.09 data +finngen_phenotype_table_url: https://r9.finngen.fi/api/phenos eqtl_catalogue_paths_imported: https://raw.githubusercontent.com/eQTL-Catalogue/eQTL-Catalogue-resources/master/tabix/tabix_ftp_paths_imported.tsv # Output datasets @@ -32,11 +32,11 @@ v2g: ${datasets.outputs}/v2g ld_index: ${datasets.outputs}/ld_index catalog_study_index: ${datasets.outputs}/catalog_study_index catalog_study_locus: ${datasets.study_locus}/catalog_study_locus -finngen_study_index: ${datasets.outputs}/finngen_study_index -finngen_summary_stats: ${datasets.outputs}/finngen_summary_stats ukbiobank_study_index: ${datasets.outputs}/ukbiobank_study_index l2g_model: ${datasets.outputs}/l2g_model l2g_predictions: ${datasets.outputs}/l2g_predictions +finngen_study_index: ${datasets.outputs}/finngen/study_index +finngen_summary_stats: ${datasets.outputs}/finngen/summary_stats eqtl_catalogue_study_index_out: ${datasets.outputs}/preprocess/eqtl_catalogue/study_index eqtl_catalogue_summary_stats_out: ${datasets.outputs}/preprocess/eqtl_catalogue/summary_stats From eeacd0dc9e56bbe7ba7ddf4312180f96f72a788e Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 13:57:34 +0000 Subject: [PATCH 07/47] feat: eQTL Catalogue main ingestion script --- src/otg/eqtl_catalogue.py | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/otg/eqtl_catalogue.py diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py new file mode 100644 index 000000000..f529c06b3 --- /dev/null +++ b/src/otg/eqtl_catalogue.py @@ -0,0 +1,58 @@ +"""Step to run eQTL Catalogue study table ingestion.""" + +from __future__ import annotations + +from dataclasses import dataclass +from urllib.request import urlopen + +from omegaconf import MISSING + +from otg.common.session import Session +from otg.datasource.eqtl_catalogue.study_index import EqtlStudyIndex +from otg.datasource.eqtl_catalogue.summary_stats import EqtlSummaryStats + + +@dataclass +class EqtlStep: + """eQTL Catalogue ingestion step. + + Attributes: + session (Session): Session object. + eqtl_catalogue_paths_imported (str): eQTL Catalogue input files for the harmonised and imported data. + eqtl_catalogue_study_index_out (str): Output path for the eQTL Catalogue study index dataset. + eqtl_catalogue_summary_stats_out (str): Output path for the eQTL Catalogue summary stats statistics. + """ + + session: Session = Session() + + eqtl_catalogue_paths_imported: str = MISSING + eqtl_catalogue_study_index_out: str = MISSING + eqtl_catalogue_summary_stats_out: str = MISSING + + def __post_init__(self: EqtlStep) -> None: + """Run step.""" + # Fetch study index. + tsv_data = urlopen(self.eqtl_catalogue_paths_imported).read().decode("utf-8") + rdd = self.session.spark.sparkContext.parallelize([tsv_data]) + df = self.session.spark.read.option("delimiter", "\t").csv(rdd) + # Process study index. + study_index = EqtlStudyIndex.from_source(df) + # Write study index. + study_index.df.write.mode(self.session.write_mode).parquet( + self.eqtl_catalogue_study_index_out + ) + + # Fetch summary stats. + input_filenames = [row.summarystatsLocation for row in study_index.collect()] + summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( + input_filenames, header=True + ) + # Process summary stats. + summary_stats_df = EqtlSummaryStats.from_source(summary_stats_df).df + # Write summary stats. + ( + summary_stats_df.sortWithinPartitions("position") + .write.partitionBy("studyId", "chromosome") + .mode(self.session.write_mode) + .parquet(self.eqtl_catalogue_summary_stats_out) + ) From f3f5c3a8967e8f575247ea91bce14070fd6b8737 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 14:51:17 +0000 Subject: [PATCH 08/47] feat: implement study index ingestion --- .../datasource/eqtl_catalogue/study_index.py | 83 +++++++++++++++++++ src/otg/eqtl_catalogue.py | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/otg/datasource/eqtl_catalogue/study_index.py diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py new file mode 100644 index 000000000..03c87a196 --- /dev/null +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -0,0 +1,83 @@ +"""Study Index for Finngen data source.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pyspark.sql.functions as f + +from otg.dataset.study_index import StudyIndex + +if TYPE_CHECKING: + from pyspark.sql import DataFrame + + +class EqtlStudyIndex(StudyIndex): + """Study index dataset from eQTL Catalogue.""" + + @classmethod + def from_source( + cls: type[EqtlStudyIndex], + eqtl_studies: DataFrame, + ) -> EqtlStudyIndex: + """Ingest study level metadata from eQTL Catalogue.""" + return EqtlStudyIndex( + _df=eqtl_studies.select( + # Constant values. + f.lit("EQTL_CATALOGUE").alias("projectId"), + f.lit("eqtl").alias("studyType"), + f.lit(True).alias("hasSumstats"), + # Sample information. + f.lit(838).alias("nSamples"), + f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), + f.array( + f.struct( + f.lit(715).cast("long").alias("sampleSize"), + f.lit("European American").alias("ancestry"), + ), + f.struct( + f.lit(103).cast("long").alias("sampleSize"), + f.lit("African American").alias("ancestry"), + ), + f.struct( + f.lit(12).cast("long").alias("sampleSize"), + f.lit("Asian American").alias("ancestry"), + ), + f.struct( + f.lit(16).cast("long").alias("sampleSize"), + f.lit("Hispanic or Latino").alias("ancestry"), + ), + ).alias("discoverySamples"), + # Publication information. + f.lit("32913098").alias("pubmedId"), + f.lit( + "The GTEx Consortium atlas of genetic regulatory effects across human tissues" + ).alias("publicationTitle"), + f.lit("GTEx Consortium").alias("publicationFirstAuthor"), + f.lit("publicationDate").alias("2020-09-11"), + f.lit("Science").alias("publicationJournal"), + # Study ID, example: "GTEx_V8_Adipose_Subcutaneous". + f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias( + "studyId" + ), + # Human readable tissue label, example: "Adipose - Subcutaneous". + f.col("tissue_label").alias("traitFromSource"), + # Ontology identifier for the tissue, for example: "UBERON:0001157". + f.array( + f.regexp_replace( + f.regexp_replace( + f.col("tissue_ontology_id"), + "UBER_", + "UBERON_", + ), + "_", + ":", + ) + ).alias("traitFromSourceMappedIds"), + # Summary statistics location. + f.col("ftp_path").alias("summarystatsLocation"), + ).withColumn( + "ldPopulationStructure", + cls.aggregate_and_map_ancestries(f.col("discoverySamples")), + ), + _schema=cls.get_schema(), + ) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index f529c06b3..b73810f2e 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -20,7 +20,7 @@ class EqtlStep: session (Session): Session object. eqtl_catalogue_paths_imported (str): eQTL Catalogue input files for the harmonised and imported data. eqtl_catalogue_study_index_out (str): Output path for the eQTL Catalogue study index dataset. - eqtl_catalogue_summary_stats_out (str): Output path for the eQTL Catalogue summary stats statistics. + eqtl_catalogue_summary_stats_out (str): Output path for the eQTL Catalogue summary stats. """ session: Session = Session() From d75b1a2226d801aed72c7a764b84df9c883c96a0 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 5 Nov 2023 15:15:42 +0000 Subject: [PATCH 09/47] feat: implement summary stats ingestion --- .../eqtl_catalogue/summary_stats.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/otg/datasource/eqtl_catalogue/summary_stats.py diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py new file mode 100644 index 000000000..31487d7cc --- /dev/null +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -0,0 +1,71 @@ +"""Summary statistics ingestion for Eqtl.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import pyspark.sql.functions as f +import pyspark.sql.types as t + +from otg.common.utils import calculate_confidence_interval, parse_pvalue +from otg.dataset.summary_statistics import SummaryStatistics + +if TYPE_CHECKING: + from pyspark.sql import DataFrame + + +@dataclass +class EqtlSummaryStats(SummaryStatistics): + """Summary statistics dataset for eQTL Catalogue.""" + + @classmethod + def from_source( + cls: type[EqtlSummaryStats], + summary_stats_df: DataFrame, + ) -> EqtlSummaryStats: + """Ingests all summary statst for all eQTL Catalogue studies.""" + processed_summary_stats_df = ( + summary_stats_df + # Drop rows which don't have proper position. + .filter(f.col("posision").cast(t.IntegerType()).isNotNull()).select( + # From the full path, extracts just the filename, and converts to upper case to get the study ID. + f.upper(f.regexp_extract(f.input_file_name(), r"([^/]+)\.gz", 1)).alias( + "studyId" + ), + # Add variant information. + f.concat_ws( + "_", + f.col("chromosome"), + f.col("position"), + f.col("ref"), + f.col("alt"), + ).alias("variantId"), + f.col("chromosome"), + f.col("position").cast(t.IntegerType()), + # Parse p-value into mantissa and exponent. + *parse_pvalue(f.col("pvalue")), + # Add beta, standard error, and allele frequency information. + f.col("beta").cast("double"), + f.col("se").cast("double").alias("standardError"), + (f.col("ac") / f.col("an")) + .cast("float") + .alias("effectAlleleFrequencyFromSource"), + ) + # Calculating the confidence intervals. + .select( + "*", + *calculate_confidence_interval( + f.col("pValueMantissa"), + f.col("pValueExponent"), + f.col("beta"), + f.col("standardError"), + ), + ) + ) + + # Initializing summary statistics object: + return cls( + _df=processed_summary_stats_df, + _schema=cls.get_schema(), + ) From 6ba36b42870e60d71e0e1a4a98729369d139ea5e Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 14:33:27 +0000 Subject: [PATCH 10/47] refactor: update eQTL study index import --- .../datasource/eqtl_catalogue/study_index.py | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 03c87a196..3b9ba67dc 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -1,4 +1,4 @@ -"""Study Index for Finngen data source.""" +"""Study Index for eQTL Catalogue data source.""" from __future__ import annotations from typing import TYPE_CHECKING @@ -22,10 +22,32 @@ def from_source( """Ingest study level metadata from eQTL Catalogue.""" return EqtlStudyIndex( _df=eqtl_studies.select( - # Constant values. - f.lit("EQTL_CATALOGUE").alias("projectId"), + # Project ID, example: "GTEx_V8". + f.col("study").alias("projectId"), + # Partial study ID, example: "GTEx_V8_Adipose_Subcutaneous". This ID will be converted to final only + # when summary statistics are parsed, because it must also include a gene ID. + f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias( + "studyId" + ), + # Summary stats location. + f.col("ftp_path").alias("summarystatsLocation"), + # Constant value fields. f.lit("eqtl").alias("studyType"), f.lit(True).alias("hasSumstats"), + # Human readable tissue label, example: "Adipose - Subcutaneous". + f.col("tissue_label").alias("traitFromSource"), + # Ontology identifier for the tissue, for example: "UBERON:0001157". + f.array( + f.regexp_replace( + f.regexp_replace( + f.col("tissue_ontology_id"), + "UBER_", + "UBERON_", + ), + "_", + ":", + ) + ).alias("traitFromSourceMappedIds"), # Sample information. f.lit(838).alias("nSamples"), f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), @@ -55,26 +77,6 @@ def from_source( f.lit("GTEx Consortium").alias("publicationFirstAuthor"), f.lit("publicationDate").alias("2020-09-11"), f.lit("Science").alias("publicationJournal"), - # Study ID, example: "GTEx_V8_Adipose_Subcutaneous". - f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias( - "studyId" - ), - # Human readable tissue label, example: "Adipose - Subcutaneous". - f.col("tissue_label").alias("traitFromSource"), - # Ontology identifier for the tissue, for example: "UBERON:0001157". - f.array( - f.regexp_replace( - f.regexp_replace( - f.col("tissue_ontology_id"), - "UBER_", - "UBERON_", - ), - "_", - ":", - ) - ).alias("traitFromSourceMappedIds"), - # Summary statistics location. - f.col("ftp_path").alias("summarystatsLocation"), ).withColumn( "ldPopulationStructure", cls.aggregate_and_map_ancestries(f.col("discoverySamples")), From be5efad1826249aabbb0cda0830a61bc9103549f Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 14:41:14 +0000 Subject: [PATCH 11/47] refactor: reorganise study index ingestion for readability --- .../datasource/eqtl_catalogue/study_index.py | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 3b9ba67dc..71969bce0 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -14,6 +14,76 @@ class EqtlStudyIndex(StudyIndex): """Study index dataset from eQTL Catalogue.""" + _study_attributes = ( + # Project ID, example: "GTEx_V8". + f.col("study").alias("projectId"), + # Partial study ID, example: "GTEx_V8_Adipose_Subcutaneous". This ID will be converted to final only when + # summary statistics are parsed, because it must also include a gene ID. + f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias("studyId"), + # Summary stats location. + f.col("ftp_path").alias("summarystatsLocation"), + # Constant value fields. + f.lit(True).alias("hasSumstats"), + f.lit("eqtl").alias("studyType"), + ) + + _tissue_attributes = ( + # Human readable tissue label, example: "Adipose - Subcutaneous". + f.col("tissue_label").alias("traitFromSource"), + # Ontology identifier for the tissue, for example: "UBERON:0001157". + f.array( + f.regexp_replace( + f.regexp_replace( + f.col("tissue_ontology_id"), + "UBER_", + "UBERON_", + ), + "_", + ":", + ) + ).alias("traitFromSourceMappedIds"), + ) + + _sample_attributes = ( + f.lit(838).alias("nSamples"), + f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), + f.array( + f.struct( + f.lit(715).cast("long").alias("sampleSize"), + f.lit("European American").alias("ancestry"), + ), + f.struct( + f.lit(103).cast("long").alias("sampleSize"), + f.lit("African American").alias("ancestry"), + ), + f.struct( + f.lit(12).cast("long").alias("sampleSize"), + f.lit("Asian American").alias("ancestry"), + ), + f.struct( + f.lit(16).cast("long").alias("sampleSize"), + f.lit("Hispanic or Latino").alias("ancestry"), + ), + ).alias("discoverySamples"), + ) + + _publication_attributes = ( + f.lit("32913098").alias("pubmedId"), + f.lit( + "The GTEx Consortium atlas of genetic regulatory effects across human tissues" + ).alias("publicationTitle"), + f.lit("GTEx Consortium").alias("publicationFirstAuthor"), + f.lit("publicationDate").alias("2020-09-11"), + f.lit("Science").alias("publicationJournal"), + ) + + _all_attributes = ( + _study_attributes + + _tissue_attributes + + _sample_attributes + + _publication_attributes + ) + @classmethod def from_source( cls: type[EqtlStudyIndex], @@ -21,63 +91,7 @@ def from_source( ) -> EqtlStudyIndex: """Ingest study level metadata from eQTL Catalogue.""" return EqtlStudyIndex( - _df=eqtl_studies.select( - # Project ID, example: "GTEx_V8". - f.col("study").alias("projectId"), - # Partial study ID, example: "GTEx_V8_Adipose_Subcutaneous". This ID will be converted to final only - # when summary statistics are parsed, because it must also include a gene ID. - f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias( - "studyId" - ), - # Summary stats location. - f.col("ftp_path").alias("summarystatsLocation"), - # Constant value fields. - f.lit("eqtl").alias("studyType"), - f.lit(True).alias("hasSumstats"), - # Human readable tissue label, example: "Adipose - Subcutaneous". - f.col("tissue_label").alias("traitFromSource"), - # Ontology identifier for the tissue, for example: "UBERON:0001157". - f.array( - f.regexp_replace( - f.regexp_replace( - f.col("tissue_ontology_id"), - "UBER_", - "UBERON_", - ), - "_", - ":", - ) - ).alias("traitFromSourceMappedIds"), - # Sample information. - f.lit(838).alias("nSamples"), - f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), - f.array( - f.struct( - f.lit(715).cast("long").alias("sampleSize"), - f.lit("European American").alias("ancestry"), - ), - f.struct( - f.lit(103).cast("long").alias("sampleSize"), - f.lit("African American").alias("ancestry"), - ), - f.struct( - f.lit(12).cast("long").alias("sampleSize"), - f.lit("Asian American").alias("ancestry"), - ), - f.struct( - f.lit(16).cast("long").alias("sampleSize"), - f.lit("Hispanic or Latino").alias("ancestry"), - ), - ).alias("discoverySamples"), - # Publication information. - f.lit("32913098").alias("pubmedId"), - f.lit( - "The GTEx Consortium atlas of genetic regulatory effects across human tissues" - ).alias("publicationTitle"), - f.lit("GTEx Consortium").alias("publicationFirstAuthor"), - f.lit("publicationDate").alias("2020-09-11"), - f.lit("Science").alias("publicationJournal"), - ).withColumn( + _df=eqtl_studies.select(*cls._all_attributes).withColumn( "ldPopulationStructure", cls.aggregate_and_map_ancestries(f.col("discoverySamples")), ), From 59a5391566f5adc94d55c4779b684b1be060d2dd Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 14:45:07 +0000 Subject: [PATCH 12/47] style: docstring for eQTL Catalogue summary stats ingestion --- src/otg/datasource/eqtl_catalogue/summary_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index 31487d7cc..b99fb2001 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -1,4 +1,4 @@ -"""Summary statistics ingestion for Eqtl.""" +"""Summary statistics ingestion for eQTL Catalogue.""" from __future__ import annotations From 071e375768894473a52a136a323e8e585d44ab0c Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 15:10:02 +0000 Subject: [PATCH 13/47] feat: construct study ID based on all appropriate columns --- .../eqtl_catalogue/summary_stats.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index b99fb2001..ad426e3b9 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -19,6 +19,25 @@ class EqtlSummaryStats(SummaryStatistics): """Summary statistics dataset for eQTL Catalogue.""" + # The following regular expresions are used to construct a full study ID. + # Example of a URI which is used for parsing: + # "ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz". + + # Regular expession to extract project ID from URI. Example: "GTEx_V8". + _project_id = f.regexp_extract( + f.input_file_name(), + r"ftp://ftp\.ebi\.ac\.uk/pub/databases/spot/eQTL/imported/([^/]+)/.*", + 1, + ) + # Regular expression to extract QTL group from URI. Example: "Adipose_Subcutaneous". + _qtl_group = f.regexp_extract(f.input_file_name(), r"([^/]+)\.tsv\.gz", 1) + # Extracting gene ID from the column. Example: "ENSG00000225630". + _gene_id = f.col("gene_id") + + # We can now construct the full study ID based on all fields. + # Example: "GTEx_V8_Adipose_Subcutaneous_ENSG00000225630". + _study_id = f.concat(_project_id, f.lit("_"), _qtl_group, f.lit("_"), _gene_id) + @classmethod def from_source( cls: type[EqtlSummaryStats], @@ -29,10 +48,8 @@ def from_source( summary_stats_df # Drop rows which don't have proper position. .filter(f.col("posision").cast(t.IntegerType()).isNotNull()).select( - # From the full path, extracts just the filename, and converts to upper case to get the study ID. - f.upper(f.regexp_extract(f.input_file_name(), r"([^/]+)\.gz", 1)).alias( - "studyId" - ), + # Construct study ID from the appropriate columns. + cls._study_id.alias("studyId"), # Add variant information. f.concat_ws( "_", @@ -48,9 +65,7 @@ def from_source( # Add beta, standard error, and allele frequency information. f.col("beta").cast("double"), f.col("se").cast("double").alias("standardError"), - (f.col("ac") / f.col("an")) - .cast("float") - .alias("effectAlleleFrequencyFromSource"), + f.col("maf").cast("float").alias("effectAlleleFrequencyFromSource"), ) # Calculating the confidence intervals. .select( From 24e9be3b1ffff801a2ff317a9d33146cfce618e1 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 15:43:41 +0000 Subject: [PATCH 14/47] feat: map partial and full study IDs --- src/otg/eqtl_catalogue.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index b73810f2e..6c620aafd 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from urllib.request import urlopen +import pyspark.sql.functions as f from omegaconf import MISSING from otg.common.session import Session @@ -35,20 +36,43 @@ def __post_init__(self: EqtlStep) -> None: tsv_data = urlopen(self.eqtl_catalogue_paths_imported).read().decode("utf-8") rdd = self.session.spark.sparkContext.parallelize([tsv_data]) df = self.session.spark.read.option("delimiter", "\t").csv(rdd) - # Process study index. - study_index = EqtlStudyIndex.from_source(df) - # Write study index. - study_index.df.write.mode(self.session.write_mode).parquet( - self.eqtl_catalogue_study_index_out - ) + # Process partial study index. At this point, it is not complete because we don't have the gene IDs, which we + # will only get once the summary stats are ingested. + study_index_df = EqtlStudyIndex.from_source(df).df # Fetch summary stats. - input_filenames = [row.summarystatsLocation for row in study_index.collect()] + input_filenames = [row.summarystatsLocation for row in study_index_df.collect()] summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( input_filenames, header=True ) # Process summary stats. summary_stats_df = EqtlSummaryStats.from_source(summary_stats_df).df + + # Explode study index based on the list of genes. While the original list contains one entry per tissue, what we + # consider as a single study is one mini-GWAS for an expression of a _particular gene_ in a particular study. + # At this stage we have a study index with partial study IDs like "PROJECT_QTLGROUP", and a summary statistics + # object with full study IDs like "PROJECT_QTLGROUP_GENEID", so we need to perform a merge and explosion to + # obtain our final study index. + partial_to_full_study_id = ( + summary_stats_df.select(f.col("studyId")) + .distinct() + .select( + f.col("studyId").alias("fullStudyId"), # PROJECT_QTLGROUP_GENEID + f.regexp_extract(f.col("studyId"), r"(.*)_[\_]+", 1).alias( + "studyId" + ), # PROJECT_QTLGROUP + ) + .groupBy("studyId") + .agg(f.collect_list("fullStudyId").alias("fullStudyIdList")) + ) + print(partial_to_full_study_id) + + # f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1).alias("geneId"), # GENEID + + # Write study index. + study_index_df.write.mode(self.session.write_mode).parquet( + self.eqtl_catalogue_study_index_out + ) # Write summary stats. ( summary_stats_df.sortWithinPartitions("position") From 53a4532b7ceb5445b841011be8f538edd4a273e8 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 15:48:49 +0000 Subject: [PATCH 15/47] feat: join dataframes to add the full study ID information --- src/otg/eqtl_catalogue.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index 6c620aafd..bbdfbf27c 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -65,7 +65,15 @@ def __post_init__(self: EqtlStep) -> None: .groupBy("studyId") .agg(f.collect_list("fullStudyId").alias("fullStudyIdList")) ) - print(partial_to_full_study_id) + study_index_df = ( + study_index_df.join(partial_to_full_study_id, "studyId", "inner") + .select( + "*", + f.explode("fullStudyIdList").alias("fullStudyId"), + ) + .drop("fullStudyIdList") + ) + print(study_index_df) # f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1).alias("geneId"), # GENEID From fb8f6f83889734e3eaccee4780ecda90385d6ac9 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 15:51:34 +0000 Subject: [PATCH 16/47] feat: populate geneId column --- src/otg/eqtl_catalogue.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index bbdfbf27c..ff42b85b3 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -67,15 +67,11 @@ def __post_init__(self: EqtlStep) -> None: ) study_index_df = ( study_index_df.join(partial_to_full_study_id, "studyId", "inner") - .select( - "*", - f.explode("fullStudyIdList").alias("fullStudyId"), - ) + .withColumn(f.explode("fullStudyIdList").alias("fullStudyId")) .drop("fullStudyIdList") + .withColumn("geneId", f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1)) + .drop("fullStudyId") ) - print(study_index_df) - - # f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1).alias("geneId"), # GENEID # Write study index. study_index_df.write.mode(self.session.write_mode).parquet( From c95adf147e6ac889a8e375bbbfb729057638a623 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:01:27 +0000 Subject: [PATCH 17/47] refactor: move gene ID joining into the study index class --- .../datasource/eqtl_catalogue/study_index.py | 34 +++++++++++++++++++ src/otg/eqtl_catalogue.py | 30 +++------------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 71969bce0..550ee2fd6 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -97,3 +97,37 @@ def from_source( ), _schema=cls.get_schema(), ) + + @classmethod + def add_gene_id_column( + cls: type[EqtlStudyIndex], + study_index_df: DataFrame, + summary_stats_df: DataFrame, + ) -> EqtlStudyIndex: + """Add a geneId column to the study index and explode. + + While the original list contains one entry per tissue, what we consider as a single study is one mini-GWAS for + an expression of a _particular gene_ in a particular study. At this stage we have a study index with partial + study IDs like "PROJECT_QTLGROUP", and a summary statistics object with full study IDs like + "PROJECT_QTLGROUP_GENEID", so we need to perform a merge and explosion to obtain our final study index. + """ + partial_to_full_study_id = ( + summary_stats_df.select(f.col("studyId")) + .distinct() + .select( + f.col("studyId").alias("fullStudyId"), # PROJECT_QTLGROUP_GENEID + f.regexp_extract(f.col("studyId"), r"(.*)_[\_]+", 1).alias( + "studyId" + ), # PROJECT_QTLGROUP + ) + .groupBy("studyId") + .agg(f.collect_list("fullStudyId").alias("fullStudyIdList")) + ) + study_index_df = ( + study_index_df.join(partial_to_full_study_id, "studyId", "inner") + .withColumn("fullStudyId", f.explode("fullStudyIdList")) + .drop("fullStudyIdList") + .withColumn("geneId", f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1)) + .drop("fullStudyId") + ) + return EqtlStudyIndex(_df=study_index_df, _schema=cls.get_schema()) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index ff42b85b3..9cdfc682e 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from urllib.request import urlopen -import pyspark.sql.functions as f from omegaconf import MISSING from otg.common.session import Session @@ -48,30 +47,11 @@ def __post_init__(self: EqtlStep) -> None: # Process summary stats. summary_stats_df = EqtlSummaryStats.from_source(summary_stats_df).df - # Explode study index based on the list of genes. While the original list contains one entry per tissue, what we - # consider as a single study is one mini-GWAS for an expression of a _particular gene_ in a particular study. - # At this stage we have a study index with partial study IDs like "PROJECT_QTLGROUP", and a summary statistics - # object with full study IDs like "PROJECT_QTLGROUP_GENEID", so we need to perform a merge and explosion to - # obtain our final study index. - partial_to_full_study_id = ( - summary_stats_df.select(f.col("studyId")) - .distinct() - .select( - f.col("studyId").alias("fullStudyId"), # PROJECT_QTLGROUP_GENEID - f.regexp_extract(f.col("studyId"), r"(.*)_[\_]+", 1).alias( - "studyId" - ), # PROJECT_QTLGROUP - ) - .groupBy("studyId") - .agg(f.collect_list("fullStudyId").alias("fullStudyIdList")) - ) - study_index_df = ( - study_index_df.join(partial_to_full_study_id, "studyId", "inner") - .withColumn(f.explode("fullStudyIdList").alias("fullStudyId")) - .drop("fullStudyIdList") - .withColumn("geneId", f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1)) - .drop("fullStudyId") - ) + # Add geneId column to the study index. + study_index_df = EqtlStudyIndex.add_gene_id_column( + study_index_df, + summary_stats_df, + ).df # Write study index. study_index_df.write.mode(self.session.write_mode).parquet( From 6a900dd17932f52dcb285b2bb6f3845ce01895af Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:02:02 +0000 Subject: [PATCH 18/47] fix: do not partition by chromosome for QTL studies --- src/otg/eqtl_catalogue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index 9cdfc682e..e2acadff1 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -60,7 +60,7 @@ def __post_init__(self: EqtlStep) -> None: # Write summary stats. ( summary_stats_df.sortWithinPartitions("position") - .write.partitionBy("studyId", "chromosome") + .write.partitionBy("studyId") .mode(self.session.write_mode) .parquet(self.eqtl_catalogue_summary_stats_out) ) From ee031da091aaebe49babd851ca7d39d5c270e42a Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:10:17 +0000 Subject: [PATCH 19/47] fix: update class names --- src/otg/datasource/eqtl_catalogue/study_index.py | 14 +++++++------- src/otg/datasource/eqtl_catalogue/summary_stats.py | 6 +++--- src/otg/eqtl_catalogue.py | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 550ee2fd6..86d3770ec 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -11,7 +11,7 @@ from pyspark.sql import DataFrame -class EqtlStudyIndex(StudyIndex): +class EqtlCatalogueStudyIndex(StudyIndex): """Study index dataset from eQTL Catalogue.""" _study_attributes = ( @@ -86,11 +86,11 @@ class EqtlStudyIndex(StudyIndex): @classmethod def from_source( - cls: type[EqtlStudyIndex], + cls: type[EqtlCatalogueStudyIndex], eqtl_studies: DataFrame, - ) -> EqtlStudyIndex: + ) -> EqtlCatalogueStudyIndex: """Ingest study level metadata from eQTL Catalogue.""" - return EqtlStudyIndex( + return EqtlCatalogueStudyIndex( _df=eqtl_studies.select(*cls._all_attributes).withColumn( "ldPopulationStructure", cls.aggregate_and_map_ancestries(f.col("discoverySamples")), @@ -100,10 +100,10 @@ def from_source( @classmethod def add_gene_id_column( - cls: type[EqtlStudyIndex], + cls: type[EqtlCatalogueStudyIndex], study_index_df: DataFrame, summary_stats_df: DataFrame, - ) -> EqtlStudyIndex: + ) -> EqtlCatalogueStudyIndex: """Add a geneId column to the study index and explode. While the original list contains one entry per tissue, what we consider as a single study is one mini-GWAS for @@ -130,4 +130,4 @@ def add_gene_id_column( .withColumn("geneId", f.regexp_extract(f.col("studyId"), r".*_([\_]+)", 1)) .drop("fullStudyId") ) - return EqtlStudyIndex(_df=study_index_df, _schema=cls.get_schema()) + return EqtlCatalogueStudyIndex(_df=study_index_df, _schema=cls.get_schema()) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index ad426e3b9..ab45449ef 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -16,7 +16,7 @@ @dataclass -class EqtlSummaryStats(SummaryStatistics): +class EqtlCatalogueSummaryStats(SummaryStatistics): """Summary statistics dataset for eQTL Catalogue.""" # The following regular expresions are used to construct a full study ID. @@ -40,9 +40,9 @@ class EqtlSummaryStats(SummaryStatistics): @classmethod def from_source( - cls: type[EqtlSummaryStats], + cls: type[EqtlCatalogueSummaryStats], summary_stats_df: DataFrame, - ) -> EqtlSummaryStats: + ) -> EqtlCatalogueSummaryStats: """Ingests all summary statst for all eQTL Catalogue studies.""" processed_summary_stats_df = ( summary_stats_df diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index e2acadff1..740a1c5b5 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -8,8 +8,8 @@ from omegaconf import MISSING from otg.common.session import Session -from otg.datasource.eqtl_catalogue.study_index import EqtlStudyIndex -from otg.datasource.eqtl_catalogue.summary_stats import EqtlSummaryStats +from otg.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex +from otg.datasource.eqtl_catalogue.summary_stats import EqtlCatalogueSummaryStats @dataclass @@ -37,7 +37,7 @@ def __post_init__(self: EqtlStep) -> None: df = self.session.spark.read.option("delimiter", "\t").csv(rdd) # Process partial study index. At this point, it is not complete because we don't have the gene IDs, which we # will only get once the summary stats are ingested. - study_index_df = EqtlStudyIndex.from_source(df).df + study_index_df = EqtlCatalogueStudyIndex.from_source(df).df # Fetch summary stats. input_filenames = [row.summarystatsLocation for row in study_index_df.collect()] @@ -45,10 +45,10 @@ def __post_init__(self: EqtlStep) -> None: input_filenames, header=True ) # Process summary stats. - summary_stats_df = EqtlSummaryStats.from_source(summary_stats_df).df + summary_stats_df = EqtlCatalogueSummaryStats.from_source(summary_stats_df).df # Add geneId column to the study index. - study_index_df = EqtlStudyIndex.add_gene_id_column( + study_index_df = EqtlCatalogueStudyIndex.add_gene_id_column( study_index_df, summary_stats_df, ).df From 72e8e50637d7a17240703d01470f44ef56c6bc97 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:21:54 +0000 Subject: [PATCH 20/47] chore: add __init__.py for eQTL Catalogue --- src/otg/datasource/eqtl_catalogue/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/otg/datasource/eqtl_catalogue/__init__.py diff --git a/src/otg/datasource/eqtl_catalogue/__init__.py b/src/otg/datasource/eqtl_catalogue/__init__.py new file mode 100644 index 000000000..9632698b0 --- /dev/null +++ b/src/otg/datasource/eqtl_catalogue/__init__.py @@ -0,0 +1,3 @@ +"""eQTL Catalogue datasource classes.""" + +from __future__ import annotations From b1cb0481986db068b349445ef1e139278464d2ab Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:29:32 +0000 Subject: [PATCH 21/47] fix: eqtl_catalogue path in docs --- docs/python_api/step/eqtl_catalogue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python_api/step/eqtl_catalogue.md b/docs/python_api/step/eqtl_catalogue.md index 06cb578c1..9d96f3486 100644 --- a/docs/python_api/step/eqtl_catalogue.md +++ b/docs/python_api/step/eqtl_catalogue.md @@ -1,4 +1,4 @@ --- title: eQTL Catalogue --- -::: otg.finngen.EqtlCatalogueStep +::: otg.eqtl_catalogue.EqtlCatalogueStep From 8243af88b69d5d40be564f91c5e34f69b5291130 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 7 Nov 2023 16:37:39 +0000 Subject: [PATCH 22/47] fix: name of EqtlCatalogueStep class --- src/otg/eqtl_catalogue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index 740a1c5b5..6502977da 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -13,7 +13,7 @@ @dataclass -class EqtlStep: +class EqtlCatalogueStep: """eQTL Catalogue ingestion step. Attributes: @@ -29,7 +29,7 @@ class EqtlStep: eqtl_catalogue_study_index_out: str = MISSING eqtl_catalogue_summary_stats_out: str = MISSING - def __post_init__(self: EqtlStep) -> None: + def __post_init__(self: EqtlCatalogueStep) -> None: """Run step.""" # Fetch study index. tsv_data = urlopen(self.eqtl_catalogue_paths_imported).read().decode("utf-8") From f3b87b3d452f6af31dff7593fd1b7962332ef871 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 15:46:06 +0000 Subject: [PATCH 23/47] chore: replace attributes with static methods for eQTL study index --- .../datasource/eqtl_catalogue/study_index.py | 156 ++++++++++-------- 1 file changed, 87 insertions(+), 69 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 86d3770ec..a2f0027ff 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -1,7 +1,7 @@ """Study Index for eQTL Catalogue data source.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List import pyspark.sql.functions as f @@ -9,89 +9,100 @@ if TYPE_CHECKING: from pyspark.sql import DataFrame + from pyspark.sql.column import Column class EqtlCatalogueStudyIndex(StudyIndex): """Study index dataset from eQTL Catalogue.""" - _study_attributes = ( - # Project ID, example: "GTEx_V8". - f.col("study").alias("projectId"), - # Partial study ID, example: "GTEx_V8_Adipose_Subcutaneous". This ID will be converted to final only when - # summary statistics are parsed, because it must also include a gene ID. - f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias("studyId"), - # Summary stats location. - f.col("ftp_path").alias("summarystatsLocation"), - # Constant value fields. - f.lit(True).alias("hasSumstats"), - f.lit("eqtl").alias("studyType"), - ) + @staticmethod + def _all_attributes() -> List[Column]: + """A helper function to return all study index attribute expressions. - _tissue_attributes = ( - # Human readable tissue label, example: "Adipose - Subcutaneous". - f.col("tissue_label").alias("traitFromSource"), - # Ontology identifier for the tissue, for example: "UBERON:0001157". - f.array( - f.regexp_replace( + Returns: + List[Column]: all study index attribute expressions. + """ + study_attributes = [ + # Project ID, example: "GTEx_V8". + f.col("study").alias("projectId"), + # Partial study ID, example: "GTEx_V8_Adipose_Subcutaneous". This ID will be converted to final only when + # summary statistics are parsed, because it must also include a gene ID. + f.concat(f.col("study"), f.lit("_"), f.col("qtl_group")).alias("studyId"), + # Summary stats location. + f.col("ftp_path").alias("summarystatsLocation"), + # Constant value fields. + f.lit(True).alias("hasSumstats"), + f.lit("eqtl").alias("studyType"), + ] + tissue_attributes = [ + # Human readable tissue label, example: "Adipose - Subcutaneous". + f.col("tissue_label").alias("traitFromSource"), + # Ontology identifier for the tissue, for example: "UBERON:0001157". + f.array( f.regexp_replace( - f.col("tissue_ontology_id"), - "UBER_", - "UBERON_", + f.regexp_replace( + f.col("tissue_ontology_id"), + "UBER_", + "UBERON_", + ), + "_", + ":", + ) + ).alias("traitFromSourceMappedIds"), + ] + sample_attributes = [ + f.lit(838).alias("nSamples"), + f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), + f.array( + f.struct( + f.lit(715).cast("long").alias("sampleSize"), + f.lit("European American").alias("ancestry"), ), - "_", - ":", - ) - ).alias("traitFromSourceMappedIds"), - ) - - _sample_attributes = ( - f.lit(838).alias("nSamples"), - f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), - f.array( - f.struct( - f.lit(715).cast("long").alias("sampleSize"), - f.lit("European American").alias("ancestry"), - ), - f.struct( - f.lit(103).cast("long").alias("sampleSize"), - f.lit("African American").alias("ancestry"), - ), - f.struct( - f.lit(12).cast("long").alias("sampleSize"), - f.lit("Asian American").alias("ancestry"), - ), - f.struct( - f.lit(16).cast("long").alias("sampleSize"), - f.lit("Hispanic or Latino").alias("ancestry"), - ), - ).alias("discoverySamples"), - ) - - _publication_attributes = ( - f.lit("32913098").alias("pubmedId"), - f.lit( - "The GTEx Consortium atlas of genetic regulatory effects across human tissues" - ).alias("publicationTitle"), - f.lit("GTEx Consortium").alias("publicationFirstAuthor"), - f.lit("publicationDate").alias("2020-09-11"), - f.lit("Science").alias("publicationJournal"), - ) - - _all_attributes = ( - _study_attributes - + _tissue_attributes - + _sample_attributes - + _publication_attributes - ) + f.struct( + f.lit(103).cast("long").alias("sampleSize"), + f.lit("African American").alias("ancestry"), + ), + f.struct( + f.lit(12).cast("long").alias("sampleSize"), + f.lit("Asian American").alias("ancestry"), + ), + f.struct( + f.lit(16).cast("long").alias("sampleSize"), + f.lit("Hispanic or Latino").alias("ancestry"), + ), + ).alias("discoverySamples"), + ] + publication_attributes = [ + f.lit("32913098").alias("pubmedId"), + f.lit( + "The GTEx Consortium atlas of genetic regulatory effects across human tissues" + ).alias("publicationTitle"), + f.lit("GTEx Consortium").alias("publicationFirstAuthor"), + f.lit("publicationDate").alias("2020-09-11"), + f.lit("Science").alias("publicationJournal"), + ] + return ( + study_attributes + + tissue_attributes + + sample_attributes + + publication_attributes + ) @classmethod def from_source( cls: type[EqtlCatalogueStudyIndex], eqtl_studies: DataFrame, ) -> EqtlCatalogueStudyIndex: - """Ingest study level metadata from eQTL Catalogue.""" + """Ingest study level metadata from eQTL Catalogue. + + Args: + eqtl_studies (DataFrame): ingested but unprocessed eQTL Catalogue studies. + + Returns: + EqtlCatalogueStudyIndex: preliminary processed study index for eQTL Catalogue studies. + """ return EqtlCatalogueStudyIndex( - _df=eqtl_studies.select(*cls._all_attributes).withColumn( + _df=eqtl_studies.select(*cls._all_attributes()).withColumn( "ldPopulationStructure", cls.aggregate_and_map_ancestries(f.col("discoverySamples")), ), @@ -110,6 +121,13 @@ def add_gene_id_column( an expression of a _particular gene_ in a particular study. At this stage we have a study index with partial study IDs like "PROJECT_QTLGROUP", and a summary statistics object with full study IDs like "PROJECT_QTLGROUP_GENEID", so we need to perform a merge and explosion to obtain our final study index. + + Args: + study_index_df (DataFrame): preliminary study index for eQTL Catalogue studies. + summary_stats_df (DataFrame): summary statistics dataframe for eQTL Catalogue data. + + Returns: + EqtlCatalogueStudyIndex: final study index for eQTL Catalogue studies. """ partial_to_full_study_id = ( summary_stats_df.select(f.col("studyId")) From 01878b87c05db7f89a0ff70414819c3d7099e106 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 15:48:31 +0000 Subject: [PATCH 24/47] chore: replace attributes with static methods for eQTL summary stats --- .../eqtl_catalogue/summary_stats.py | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index ab45449ef..89f06540d 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -13,43 +13,57 @@ if TYPE_CHECKING: from pyspark.sql import DataFrame + from pyspark.sql.column import Column @dataclass class EqtlCatalogueSummaryStats(SummaryStatistics): """Summary statistics dataset for eQTL Catalogue.""" - # The following regular expresions are used to construct a full study ID. - # Example of a URI which is used for parsing: - # "ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz". + @staticmethod + def _full_study_id_regexp() -> Column: + """Constructs a full study ID from the URI. - # Regular expession to extract project ID from URI. Example: "GTEx_V8". - _project_id = f.regexp_extract( - f.input_file_name(), - r"ftp://ftp\.ebi\.ac\.uk/pub/databases/spot/eQTL/imported/([^/]+)/.*", - 1, - ) - # Regular expression to extract QTL group from URI. Example: "Adipose_Subcutaneous". - _qtl_group = f.regexp_extract(f.input_file_name(), r"([^/]+)\.tsv\.gz", 1) - # Extracting gene ID from the column. Example: "ENSG00000225630". - _gene_id = f.col("gene_id") + Returns: + Column: expression to extract a full study ID from the URI. + """ + # Example of a URI which is used for parsing: + # "ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz". - # We can now construct the full study ID based on all fields. - # Example: "GTEx_V8_Adipose_Subcutaneous_ENSG00000225630". - _study_id = f.concat(_project_id, f.lit("_"), _qtl_group, f.lit("_"), _gene_id) + # Regular expession to extract project ID from URI. Example: "GTEx_V8". + _project_id = f.regexp_extract( + f.input_file_name(), + r"ftp://ftp\.ebi\.ac\.uk/pub/databases/spot/eQTL/imported/([^/]+)/.*", + 1, + ) + # Regular expression to extract QTL group from URI. Example: "Adipose_Subcutaneous". + _qtl_group = f.regexp_extract(f.input_file_name(), r"([^/]+)\.tsv\.gz", 1) + # Extracting gene ID from the column. Example: "ENSG00000225630". + _gene_id = f.col("gene_id") + + # We can now construct the full study ID based on all fields. + # Example: "GTEx_V8_Adipose_Subcutaneous_ENSG00000225630". + return f.concat(_project_id, f.lit("_"), _qtl_group, f.lit("_"), _gene_id) @classmethod def from_source( cls: type[EqtlCatalogueSummaryStats], summary_stats_df: DataFrame, ) -> EqtlCatalogueSummaryStats: - """Ingests all summary statst for all eQTL Catalogue studies.""" + """Ingests all summary stats for all eQTL Catalogue studies. + + Args: + summary_stats_df (DataFrame): an ingested but unprocessed summary statistics dataframe from eQTL Catalogue. + + Returns: + EqtlCatalogueSummaryStats: a processed summary statistics dataframe for eQTL Catalogue. + """ processed_summary_stats_df = ( summary_stats_df # Drop rows which don't have proper position. .filter(f.col("posision").cast(t.IntegerType()).isNotNull()).select( # Construct study ID from the appropriate columns. - cls._study_id.alias("studyId"), + cls._full_study_id_regexp().alias("studyId"), # Add variant information. f.concat_ws( "_", From eca511cd09c68946e06f6eed2e4458ea0a527275 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:00:24 +0000 Subject: [PATCH 25/47] fix: do not initialise session in the main class --- src/otg/eqtl_catalogue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index 6502977da..f76c20e4d 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -23,7 +23,7 @@ class EqtlCatalogueStep: eqtl_catalogue_summary_stats_out (str): Output path for the eQTL Catalogue summary stats. """ - session: Session = Session() + session: Session = MISSING eqtl_catalogue_paths_imported: str = MISSING eqtl_catalogue_study_index_out: str = MISSING From e27b0650fe9bbf2016e9cb4793afc2a8c603a65d Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:11:06 +0000 Subject: [PATCH 26/47] test: add conftests for eQTL Catalogue --- tests/conftest.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index b2d8be3da..f91a4d7d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,8 @@ from otg.dataset.v2g import V2G from otg.dataset.variant_annotation import VariantAnnotation from otg.dataset.variant_index import VariantIndex +from otg.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex +from otg.datasource.eqtl_catalogue.summary_stats import EqtlCatalogueSummaryStats from otg.datasource.finngen.study_index import FinnGenStudyIndex from otg.datasource.finngen.summary_stats import FinnGenSummaryStats from otg.datasource.gwas_catalog.associations import GWASCatalogAssociations @@ -163,6 +165,24 @@ def mock_summary_stats_finngen(spark: SparkSession) -> FinnGenSummaryStats: ) +@pytest.fixture() +def mock_study_index_eqtl_catalogue(spark: SparkSession) -> EqtlCatalogueStudyIndex: + """Mock EqtlCatalogueStudyIndex dataset.""" + return EqtlCatalogueStudyIndex( + _df=mock_study_index_data(spark), + _schema=StudyIndex.get_schema(), + ) + + +@pytest.fixture() +def mock_summary_stats_eqtl_catalogue(spark: SparkSession) -> EqtlCatalogueSummaryStats: + """Mock EqtlCatalogueSummaryStats dataset.""" + return EqtlCatalogueSummaryStats( + _df=mock_summary_statistics_data(spark), + _schema=SummaryStatistics.get_schema(), + ) + + @pytest.fixture() def mock_study_index_ukbiobank(spark: SparkSession) -> UKBiobankStudyIndex: """Mock StudyIndexUKBiobank dataset.""" From 5b3e85f429e92c7b6917496bbe402fa148086459 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:17:21 +0000 Subject: [PATCH 27/47] fix: include header when reading the study index --- src/otg/eqtl_catalogue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index f76c20e4d..997229172 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -34,7 +34,7 @@ def __post_init__(self: EqtlCatalogueStep) -> None: # Fetch study index. tsv_data = urlopen(self.eqtl_catalogue_paths_imported).read().decode("utf-8") rdd = self.session.spark.sparkContext.parallelize([tsv_data]) - df = self.session.spark.read.option("delimiter", "\t").csv(rdd) + df = self.session.spark.read.option("delimiter", "\t").csv(rdd, header=True) # Process partial study index. At this point, it is not complete because we don't have the gene IDs, which we # will only get once the summary stats are ingested. study_index_df = EqtlCatalogueStudyIndex.from_source(df).df From 571c4dc530cf8a52c645934a70f4c68db196f2f9 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:21:29 +0000 Subject: [PATCH 28/47] fix: populating publicationDate --- src/otg/datasource/eqtl_catalogue/study_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index a2f0027ff..19c2ae380 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -78,7 +78,7 @@ def _all_attributes() -> List[Column]: "The GTEx Consortium atlas of genetic regulatory effects across human tissues" ).alias("publicationTitle"), f.lit("GTEx Consortium").alias("publicationFirstAuthor"), - f.lit("publicationDate").alias("2020-09-11"), + f.lit("2020-09-11").alias("publicationDate"), f.lit("Science").alias("publicationJournal"), ] return ( From 83af9eb81998b2b2a5fca2950e1f1f0c155efdb9 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:27:37 +0000 Subject: [PATCH 29/47] fix: cast nSamples as long --- src/otg/datasource/eqtl_catalogue/study_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/datasource/eqtl_catalogue/study_index.py b/src/otg/datasource/eqtl_catalogue/study_index.py index 19c2ae380..5ae5c3bfd 100644 --- a/src/otg/datasource/eqtl_catalogue/study_index.py +++ b/src/otg/datasource/eqtl_catalogue/study_index.py @@ -51,7 +51,7 @@ def _all_attributes() -> List[Column]: ).alias("traitFromSourceMappedIds"), ] sample_attributes = [ - f.lit(838).alias("nSamples"), + f.lit(838).cast("long").alias("nSamples"), f.lit("838 (281 females and 557 males)").alias("initialSampleSize"), f.array( f.struct( From 2c0c1d0c12634bba55711e7471e09a577bb0b8b0 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:46:29 +0000 Subject: [PATCH 30/47] fix: manually specify schema for eQTL Catalogue summary stats --- src/otg/eqtl_catalogue.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index 997229172..de9f60bb0 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -6,6 +6,7 @@ from urllib.request import urlopen from omegaconf import MISSING +from pyspark.sql.types import DoubleType, LongType, StringType, StructField, StructType from otg.common.session import Session from otg.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex @@ -29,6 +30,31 @@ class EqtlCatalogueStep: eqtl_catalogue_study_index_out: str = MISSING eqtl_catalogue_summary_stats_out: str = MISSING + # Schema needs to be specified manually here, because otherwise Spark emits the following error: + # "pyspark.sql.utils.AnalysisException: Unable to infer schema for CSV. It must be specified manually." + _summary_stats_schema = StructType( + [ + StructField("variant", StringType(), True), + StructField("r2", StringType(), True), + StructField("pvalue", DoubleType(), True), + StructField("molecular_trait_object_id", StringType(), True), + StructField("molecular_trait_id", StringType(), True), + StructField("maf", DoubleType(), True), + StructField("gene_id", StringType(), True), + StructField("median_tpm", DoubleType(), True), + StructField("beta", DoubleType(), True), + StructField("se", DoubleType(), True), + StructField("an", LongType(), True), + StructField("ac", LongType(), True), + StructField("chromosome", StringType(), True), + StructField("position", LongType(), True), + StructField("ref", StringType(), True), + StructField("alt", StringType(), True), + StructField("type", StringType(), True), + StructField("rsid", StringType(), True), + ] + ) + def __post_init__(self: EqtlCatalogueStep) -> None: """Run step.""" # Fetch study index. @@ -42,7 +68,7 @@ def __post_init__(self: EqtlCatalogueStep) -> None: # Fetch summary stats. input_filenames = [row.summarystatsLocation for row in study_index_df.collect()] summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( - input_filenames, header=True + input_filenames, header=True, schema=self._summary_stats_schema ) # Process summary stats. summary_stats_df = EqtlCatalogueSummaryStats.from_source(summary_stats_df).df From 4462110af8001212598b602a0c5d686885d2eb28 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:46:52 +0000 Subject: [PATCH 31/47] fix: typo in position field name --- src/otg/datasource/eqtl_catalogue/summary_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index 89f06540d..bedfdc7d1 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -61,7 +61,7 @@ def from_source( processed_summary_stats_df = ( summary_stats_df # Drop rows which don't have proper position. - .filter(f.col("posision").cast(t.IntegerType()).isNotNull()).select( + .filter(f.col("position").cast(t.IntegerType()).isNotNull()).select( # Construct study ID from the appropriate columns. cls._full_study_id_regexp().alias("studyId"), # Add variant information. From 4c87a9efc159093886300143de71256372000fbf Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 17:59:26 +0000 Subject: [PATCH 32/47] test: add sample eQTL Catalogue studies --- tests/conftest.py | 13 +++++++++++++ .../data_samples/eqtl_catalogue_studies_sample.tsv | 11 +++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/data_samples/eqtl_catalogue_studies_sample.tsv diff --git a/tests/conftest.py b/tests/conftest.py index f91a4d7d8..296923aa9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -539,6 +539,19 @@ def sample_finngen_summary_stats(spark: SparkSession) -> DataFrame: ) +@pytest.fixture() +def sample_eqtl_catalogue_studies(spark: SparkSession) -> DataFrame: + """Sample eQTL Catalogue studies.""" + # For reference, the sample file was generated with the following command: + # curl https://raw.githubusercontent.com/eQTL-Catalogue/eQTL-Catalogue-resources/master/tabix/tabix_ftp_paths_imported.tsv | head -n11 > tests/data_samples/eqtl_catalogue_studies_sample.tsv + with open( + "tests/data_samples/eqtl_catalogue_studies_sample.json" + ) as eqtl_catalogue: + tsv = eqtl_catalogue.read() + rdd = spark.sparkContext.parallelize([tsv]) + return spark.read.csv(rdd, sep="\t", header=True) + + @pytest.fixture() def sample_ukbiobank_studies(spark: SparkSession) -> DataFrame: """Sample UKBiobank manifest.""" diff --git a/tests/data_samples/eqtl_catalogue_studies_sample.tsv b/tests/data_samples/eqtl_catalogue_studies_sample.tsv new file mode 100644 index 000000000..f756ecd6e --- /dev/null +++ b/tests/data_samples/eqtl_catalogue_studies_sample.tsv @@ -0,0 +1,11 @@ +study qtl_group tissue_ontology_id tissue_ontology_term tissue_label condition_label quant_method ftp_path +GTEx_V8 Adipose_Subcutaneous UBER_0002190 subcutaneous adipose tissue Adipose - Subcutaneous naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz +GTEx_V8 Adipose_Visceral_Omentum UBER_0010414 omental fat pad Adipose - Visceral (Omentum) naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Visceral_Omentum.tsv.gz +GTEx_V8 Adrenal_Gland UBER_0002369 adrenal gland Adrenal Gland naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adrenal_Gland.tsv.gz +GTEx_V8 Artery_Aorta UBER_0001496 ascending aorta Artery - Aorta naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Artery_Aorta.tsv.gz +GTEx_V8 Artery_Coronary UBER_0001621 coronary artery Artery - Coronary naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Artery_Coronary.tsv.gz +GTEx_V8 Artery_Tibial UBER_0007610 tibial artery Artery - Tibial naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Artery_Tibial.tsv.gz +GTEx_V8 Brain_Amygdala UBER_0001876 amygdala Brain - Amygdala naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Brain_Amygdala.tsv.gz +GTEx_V8 Brain_Anterior_cingulate_cortex_BA24 UBER_0009835 anterior cingulate cortex Brain - Anterior cingulate cortex (BA24) naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Brain_Anterior_cingulate_cortex_BA24.tsv.gz +GTEx_V8 Brain_Caudate_basal_ganglia UBER_0001873 caudate nucleus Brain - Caudate (basal ganglia) naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Brain_Caudate_basal_ganglia.tsv.gz +GTEx_V8 Brain_Cerebellar_Hemisphere UBER_0002037 cerebellum Brain - Cerebellar Hemisphere naive ge ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Brain_Cerebellar_Hemisphere.tsv.gz From 3b00bb917632c7ef7f496867ad4b7c33787557b1 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 18:04:57 +0000 Subject: [PATCH 33/47] test: add sample eQTL Catalogue summary stats --- .../datasource/eqtl_catalogue/summary_stats.py | 2 +- tests/conftest.py | 13 +++++++++++++ .../GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz | Bin 0 -> 491 bytes 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index bedfdc7d1..9ebd5359b 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -33,7 +33,7 @@ def _full_study_id_regexp() -> Column: # Regular expession to extract project ID from URI. Example: "GTEx_V8". _project_id = f.regexp_extract( f.input_file_name(), - r"ftp://ftp\.ebi\.ac\.uk/pub/databases/spot/eQTL/imported/([^/]+)/.*", + r"imported/([^/]+)/.*", 1, ) # Regular expression to extract QTL group from URI. Example: "Adipose_Subcutaneous". diff --git a/tests/conftest.py b/tests/conftest.py index 296923aa9..619a77d96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -552,6 +552,19 @@ def sample_eqtl_catalogue_studies(spark: SparkSession) -> DataFrame: return spark.read.csv(rdd, sep="\t", header=True) +@pytest.fixture() +def sample_eqtl_catalogue_summary_stats(spark: SparkSession) -> DataFrame: + """Sample eQTL Catalogue summary stats.""" + # For reference, the sample file was generated with the following commands: + # mkdir -p tests/data_samples/imported/GTEx_V8/ge + # curl ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz | gzip -cd | head -n11 | gzip -c > tests/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz + # It's important for the test file to be named in exactly this way, because eQTL Catalogue study ID is populated based on input file name. + return spark.read.option("delimiter", "\t").csv( + "tests/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz", + header=True, + ) + + @pytest.fixture() def sample_ukbiobank_studies(spark: SparkSession) -> DataFrame: """Sample UKBiobank manifest.""" diff --git a/tests/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz b/tests/data_samples/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..74c0844a4ad146b41483bcd07d65d85a8700a520 GIT binary patch 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 Date: Sun, 12 Nov 2023 18:08:48 +0000 Subject: [PATCH 34/47] test: add test for eQTL Catalogue study index --- .../test_eqtl_catalogue_study_index.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/datasource/eqtl_catalogue/test_eqtl_catalogue_study_index.py diff --git a/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_study_index.py b/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_study_index.py new file mode 100644 index 000000000..cc97914bf --- /dev/null +++ b/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_study_index.py @@ -0,0 +1,20 @@ +"""Tests for study index dataset from eQTL Catalogue.""" + +from __future__ import annotations + +from pyspark.sql import DataFrame + +from otg.dataset.study_index import StudyIndex +from otg.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex + + +def test_eqtl_catalogue_study_index_from_source( + sample_eqtl_catalogue_studies: DataFrame, +) -> None: + """Test study index from source.""" + assert isinstance( + EqtlCatalogueStudyIndex.from_source( + sample_eqtl_catalogue_studies, + ), + StudyIndex, + ) From 7bcf1c8989bd55c8724b6548579f3a6be09f9e47 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 18:09:43 +0000 Subject: [PATCH 35/47] test: add test for eQTL Catalogue summary stats --- .../test_eqtl_catalogue_summary_stats.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/datasource/eqtl_catalogue/test_eqtl_catalogue_summary_stats.py diff --git a/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_summary_stats.py b/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_summary_stats.py new file mode 100644 index 000000000..26132f986 --- /dev/null +++ b/tests/datasource/eqtl_catalogue/test_eqtl_catalogue_summary_stats.py @@ -0,0 +1,18 @@ +"""Tests for study index dataset from eQTL Catalogue.""" + +from __future__ import annotations + +from pyspark.sql import DataFrame + +from otg.dataset.summary_statistics import SummaryStatistics +from otg.datasource.eqtl_catalogue.summary_stats import EqtlCatalogueSummaryStats + + +def test_eqtl_catalogue_summary_stats_from_source( + sample_eqtl_catalogue_summary_stats: DataFrame, +) -> None: + """Test summary statistics from source.""" + assert isinstance( + EqtlCatalogueSummaryStats.from_source(sample_eqtl_catalogue_summary_stats), + SummaryStatistics, + ) From f741006964f623f9bf2c67ae74ce59514e2f8049 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 18:18:31 +0000 Subject: [PATCH 36/47] chore: partition output data by chromosome --- src/otg/eqtl_catalogue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index de9f60bb0..ac3489849 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -86,7 +86,7 @@ def __post_init__(self: EqtlCatalogueStep) -> None: # Write summary stats. ( summary_stats_df.sortWithinPartitions("position") - .write.partitionBy("studyId") + .write.partitionBy("chromosome") .mode(self.session.write_mode) .parquet(self.eqtl_catalogue_summary_stats_out) ) From d467f797f8b7e31aca60219ce11983c5a5ce4f6f Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 19:16:23 +0000 Subject: [PATCH 37/47] chore: read input data from Google Storage --- config/datasets/gcp.yaml | 2 +- .../eqtl_catalogue/summary_stats.py | 2 +- src/otg/eqtl_catalogue.py | 35 +++---------------- 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/config/datasets/gcp.yaml b/config/datasets/gcp.yaml index 7d8c2a737..96c745596 100644 --- a/config/datasets/gcp.yaml +++ b/config/datasets/gcp.yaml @@ -19,7 +19,7 @@ ukbiobank_manifest: gs://genetics-portal-input/ukb_phenotypes/neale2_saige_study l2g_gold_standard_curation: ${datasets.inputs}/l2g/gold_standard/curation.json gene_interactions: ${datasets.inputs}/l2g/interaction # 23.09 data finngen_phenotype_table_url: https://r9.finngen.fi/api/phenos -eqtl_catalogue_paths_imported: https://raw.githubusercontent.com/eQTL-Catalogue/eQTL-Catalogue-resources/master/tabix/tabix_ftp_paths_imported.tsv +eqtl_catalogue_paths_imported: ${datasets.inputs}/preprocess/eqtl_catalogue/tabix_ftp_paths_imported.tsv # Output datasets gene_index: ${datasets.outputs}/gene_index diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index 9ebd5359b..599f93018 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -28,7 +28,7 @@ def _full_study_id_regexp() -> Column: Column: expression to extract a full study ID from the URI. """ # Example of a URI which is used for parsing: - # "ftp://ftp.ebi.ac.uk/pub/databases/spot/eQTL/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz". + # "gs://genetics_etl_python_playground/input/preprocess/eqtl_catalogue/imported/GTEx_V8/ge/Adipose_Subcutaneous.tsv.gz". # Regular expession to extract project ID from URI. Example: "GTEx_V8". _project_id = f.regexp_extract( diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index ac3489849..a5c4e606f 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -3,10 +3,8 @@ from __future__ import annotations from dataclasses import dataclass -from urllib.request import urlopen from omegaconf import MISSING -from pyspark.sql.types import DoubleType, LongType, StringType, StructField, StructType from otg.common.session import Session from otg.datasource.eqtl_catalogue.study_index import EqtlCatalogueStudyIndex @@ -30,37 +28,12 @@ class EqtlCatalogueStep: eqtl_catalogue_study_index_out: str = MISSING eqtl_catalogue_summary_stats_out: str = MISSING - # Schema needs to be specified manually here, because otherwise Spark emits the following error: - # "pyspark.sql.utils.AnalysisException: Unable to infer schema for CSV. It must be specified manually." - _summary_stats_schema = StructType( - [ - StructField("variant", StringType(), True), - StructField("r2", StringType(), True), - StructField("pvalue", DoubleType(), True), - StructField("molecular_trait_object_id", StringType(), True), - StructField("molecular_trait_id", StringType(), True), - StructField("maf", DoubleType(), True), - StructField("gene_id", StringType(), True), - StructField("median_tpm", DoubleType(), True), - StructField("beta", DoubleType(), True), - StructField("se", DoubleType(), True), - StructField("an", LongType(), True), - StructField("ac", LongType(), True), - StructField("chromosome", StringType(), True), - StructField("position", LongType(), True), - StructField("ref", StringType(), True), - StructField("alt", StringType(), True), - StructField("type", StringType(), True), - StructField("rsid", StringType(), True), - ] - ) - def __post_init__(self: EqtlCatalogueStep) -> None: """Run step.""" # Fetch study index. - tsv_data = urlopen(self.eqtl_catalogue_paths_imported).read().decode("utf-8") - rdd = self.session.spark.sparkContext.parallelize([tsv_data]) - df = self.session.spark.read.option("delimiter", "\t").csv(rdd, header=True) + df = self.session.spark.read.option("delimiter", "\t").csv( + self.eqtl_catalogue_paths_imported, header=True + ) # Process partial study index. At this point, it is not complete because we don't have the gene IDs, which we # will only get once the summary stats are ingested. study_index_df = EqtlCatalogueStudyIndex.from_source(df).df @@ -68,7 +41,7 @@ def __post_init__(self: EqtlCatalogueStep) -> None: # Fetch summary stats. input_filenames = [row.summarystatsLocation for row in study_index_df.collect()] summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( - input_filenames, header=True, schema=self._summary_stats_schema + input_filenames, header=True ) # Process summary stats. summary_stats_df = EqtlCatalogueSummaryStats.from_source(summary_stats_df).df From e58a959c482febab8da1a77f4164c669223656b3 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Sun, 12 Nov 2023 19:19:09 +0000 Subject: [PATCH 38/47] fix: studies sample filename --- tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 619a77d96..0d59302d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -544,9 +544,7 @@ def sample_eqtl_catalogue_studies(spark: SparkSession) -> DataFrame: """Sample eQTL Catalogue studies.""" # For reference, the sample file was generated with the following command: # curl https://raw.githubusercontent.com/eQTL-Catalogue/eQTL-Catalogue-resources/master/tabix/tabix_ftp_paths_imported.tsv | head -n11 > tests/data_samples/eqtl_catalogue_studies_sample.tsv - with open( - "tests/data_samples/eqtl_catalogue_studies_sample.json" - ) as eqtl_catalogue: + with open("tests/data_samples/eqtl_catalogue_studies_sample.tsv") as eqtl_catalogue: tsv = eqtl_catalogue.read() rdd = spark.sparkContext.parallelize([tsv]) return spark.read.csv(rdd, sep="\t", header=True) From f81f617ca6391aa5d6656f06e8c77bdd6e2f2ac7 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Mon, 13 Nov 2023 13:02:24 +0000 Subject: [PATCH 39/47] chore: repartition data before processing --- src/otg/eqtl_catalogue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/otg/eqtl_catalogue.py b/src/otg/eqtl_catalogue.py index a5c4e606f..c57f95ed3 100644 --- a/src/otg/eqtl_catalogue.py +++ b/src/otg/eqtl_catalogue.py @@ -40,8 +40,10 @@ def __post_init__(self: EqtlCatalogueStep) -> None: # Fetch summary stats. input_filenames = [row.summarystatsLocation for row in study_index_df.collect()] - summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( - input_filenames, header=True + summary_stats_df = ( + self.session.spark.read.option("delimiter", "\t") + .csv(input_filenames, header=True) + .repartition(1280) ) # Process summary stats. summary_stats_df = EqtlCatalogueSummaryStats.from_source(summary_stats_df).df From 75dd3d6e5b0688e0737b550d7b9311c39b6e45c9 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 14 Nov 2023 22:53:44 +0000 Subject: [PATCH 40/47] revert: repartition call --- src/otg/finngen.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/otg/finngen.py b/src/otg/finngen.py index 2f6a3d1f7..44e41cdfc 100644 --- a/src/otg/finngen.py +++ b/src/otg/finngen.py @@ -54,10 +54,8 @@ def __post_init__(self: FinnGenStep) -> None: # Fetch summary stats. input_filenames = [row.summarystatsLocation for row in study_index.df.collect()] - summary_stats_df = ( - self.session.spark.read.option("delimiter", "\t") - .csv(input_filenames, header=True) - .repartition(len(input_filenames) * 16) + summary_stats_df = self.session.spark.read.option("delimiter", "\t").csv( + input_filenames, header=True ) # Process summary stats. summary_stats_df = FinnGenSummaryStats.from_source(summary_stats_df).df From 13de75fd239e2b445928cbbf8ffa32d692f8bbdc Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Tue, 21 Nov 2023 13:44:11 +0000 Subject: [PATCH 41/47] chore: remove beta value interval calculation --- .../eqtl_catalogue/summary_stats.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/otg/datasource/eqtl_catalogue/summary_stats.py b/src/otg/datasource/eqtl_catalogue/summary_stats.py index 599f93018..0b11b6759 100644 --- a/src/otg/datasource/eqtl_catalogue/summary_stats.py +++ b/src/otg/datasource/eqtl_catalogue/summary_stats.py @@ -8,7 +8,7 @@ import pyspark.sql.functions as f import pyspark.sql.types as t -from otg.common.utils import calculate_confidence_interval, parse_pvalue +from otg.common.utils import parse_pvalue from otg.dataset.summary_statistics import SummaryStatistics if TYPE_CHECKING: @@ -59,9 +59,7 @@ def from_source( EqtlCatalogueSummaryStats: a processed summary statistics dataframe for eQTL Catalogue. """ processed_summary_stats_df = ( - summary_stats_df - # Drop rows which don't have proper position. - .filter(f.col("position").cast(t.IntegerType()).isNotNull()).select( + summary_stats_df.select( # Construct study ID from the appropriate columns. cls._full_study_id_regexp().alias("studyId"), # Add variant information. @@ -81,19 +79,14 @@ def from_source( f.col("se").cast("double").alias("standardError"), f.col("maf").cast("float").alias("effectAlleleFrequencyFromSource"), ) - # Calculating the confidence intervals. - .select( - "*", - *calculate_confidence_interval( - f.col("pValueMantissa"), - f.col("pValueExponent"), - f.col("beta"), - f.col("standardError"), - ), + # Drop rows which don't have proper position or beta value. + .filter( + f.col("position").cast(t.IntegerType()).isNotNull() + & (f.col("beta") != 0) ) ) - # Initializing summary statistics object: + # Initialise a summary statistics object. return cls( _df=processed_summary_stats_df, _schema=cls.get_schema(), From 0db58a7d27939dc355383e81a8cdc892b01dfad0 Mon Sep 17 00:00:00 2001 From: Daniel Suveges Date: Tue, 21 Nov 2023 15:48:50 +0000 Subject: [PATCH 42/47] refactor: removing odds ratio, and confidence intervals from the schema --- src/otg/assets/schemas/study_locus.json | 30 ---------- .../datasource/gwas_catalog/associations.py | 58 ------------------- tests/conftest.py | 5 -- 3 files changed, 93 deletions(-) diff --git a/src/otg/assets/schemas/study_locus.json b/src/otg/assets/schemas/study_locus.json index 8b987e630..97f9da3bf 100644 --- a/src/otg/assets/schemas/study_locus.json +++ b/src/otg/assets/schemas/study_locus.json @@ -36,36 +36,6 @@ "nullable": true, "type": "double" }, - { - "metadata": {}, - "name": "oddsRatio", - "nullable": true, - "type": "double" - }, - { - "metadata": {}, - "name": "oddsRatioConfidenceIntervalLower", - "nullable": true, - "type": "double" - }, - { - "metadata": {}, - "name": "oddsRatioConfidenceIntervalUpper", - "nullable": true, - "type": "double" - }, - { - "metadata": {}, - "name": "betaConfidenceIntervalLower", - "nullable": true, - "type": "double" - }, - { - "metadata": {}, - "name": "betaConfidenceIntervalUpper", - "nullable": true, - "type": "double" - }, { "metadata": {}, "name": "pValueMantissa", diff --git a/src/otg/datasource/gwas_catalog/associations.py b/src/otg/datasource/gwas_catalog/associations.py index 1229d17ec..642e656c2 100644 --- a/src/otg/datasource/gwas_catalog/associations.py +++ b/src/otg/datasource/gwas_catalog/associations.py @@ -1042,64 +1042,6 @@ def from_source( f.col("OR or BETA"), f.col("95% CI (TEXT)"), ).alias("beta"), - # odds ratio of the association - GWASCatalogAssociations._harmonise_odds_ratio( - GWASCatalogAssociations._normalise_risk_allele( - f.col("STRONGEST SNP-RISK ALLELE") - ), - f.col("referenceAllele"), - f.col("alternateAllele"), - f.col("OR or BETA"), - f.col("95% CI (TEXT)"), - ).alias("oddsRatio"), - # CI lower of the beta value - GWASCatalogAssociations._harmonise_beta_ci( - GWASCatalogAssociations._normalise_risk_allele( - f.col("STRONGEST SNP-RISK ALLELE") - ), - f.col("referenceAllele"), - f.col("alternateAllele"), - f.col("OR or BETA"), - f.col("95% CI (TEXT)"), - f.col("P-VALUE"), - "lower", - ).alias("betaConfidenceIntervalLower"), - # CI upper for the beta value - GWASCatalogAssociations._harmonise_beta_ci( - GWASCatalogAssociations._normalise_risk_allele( - f.col("STRONGEST SNP-RISK ALLELE") - ), - f.col("referenceAllele"), - f.col("alternateAllele"), - f.col("OR or BETA"), - f.col("95% CI (TEXT)"), - f.col("P-VALUE"), - "upper", - ).alias("betaConfidenceIntervalUpper"), - # CI lower of the odds ratio value - GWASCatalogAssociations._harmonise_odds_ratio_ci( - GWASCatalogAssociations._normalise_risk_allele( - f.col("STRONGEST SNP-RISK ALLELE") - ), - f.col("referenceAllele"), - f.col("alternateAllele"), - f.col("OR or BETA"), - f.col("95% CI (TEXT)"), - f.col("P-VALUE"), - "lower", - ).alias("oddsRatioConfidenceIntervalLower"), - # CI upper of the odds ratio value - GWASCatalogAssociations._harmonise_odds_ratio_ci( - GWASCatalogAssociations._normalise_risk_allele( - f.col("STRONGEST SNP-RISK ALLELE") - ), - f.col("referenceAllele"), - f.col("alternateAllele"), - f.col("OR or BETA"), - f.col("95% CI (TEXT)"), - f.col("P-VALUE"), - "upper", - ).alias("oddsRatioConfidenceIntervalUpper"), # p-value of the association, string: split into exponent and mantissa. *GWASCatalogAssociations._parse_pvalue(f.col("P-VALUE")), # Capturing phenotype granularity at the association level diff --git a/tests/conftest.py b/tests/conftest.py index 6ed3572f4..5c7a3434f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -202,11 +202,6 @@ def mock_study_locus_data(spark: SparkSession) -> DataFrame: .withColumnSpec("chromosome", percentNulls=0.1) .withColumnSpec("position", percentNulls=0.1) .withColumnSpec("beta", percentNulls=0.1) - .withColumnSpec("oddsRatio", percentNulls=0.1) - .withColumnSpec("oddsRatioConfidenceIntervalLower", percentNulls=0.1) - .withColumnSpec("oddsRatioConfidenceIntervalUpper", percentNulls=0.1) - .withColumnSpec("betaConfidenceIntervalLower", percentNulls=0.1) - .withColumnSpec("betaConfidenceIntervalUpper", percentNulls=0.1) .withColumnSpec("effectAlleleFrequencyFromSource", percentNulls=0.1) .withColumnSpec("standardError", percentNulls=0.1) .withColumnSpec("subStudyDescription", percentNulls=0.1) From 2eee6ca6a660034c9f3c674db1354a2f6ee85117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20L=C3=B3pez?= Date: Tue, 21 Nov 2023 23:16:37 +0000 Subject: [PATCH 43/47] fix: multiply standard error by zscore in `calculate_confidence_interval` --- src/otg/common/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/otg/common/utils.py b/src/otg/common/utils.py index 608038eec..d05810670 100644 --- a/src/otg/common/utils.py +++ b/src/otg/common/utils.py @@ -86,9 +86,9 @@ def calculate_confidence_interval( +---------------+---------------+----+--------------+---------------------------+---------------------------+ |pvalue_mantissa|pvalue_exponent|beta|standard_error|betaConfidenceIntervalLower|betaConfidenceIntervalUpper| +---------------+---------------+----+--------------+---------------------------+---------------------------+ - | 2.5| -10| 0.5| 0.2| 0.3| 0.7| - | 3.0| -5| 1.0| null| 0.7603910153486024| 1.2396089846513976| - | 1.5| -8|-0.2| 0.1| -0.30000000000000004| -0.1| + | 2.5| -10| 0.5| 0.2| 0.10799999999999998| 0.892| + | 3.0| -5| 1.0| null| 0.5303663900832607| 1.4696336099167393| + | 1.5| -8|-0.2| 0.1| -0.396| -0.00400000000000...| +---------------+---------------+----+--------------+---------------------------+---------------------------+ """ @@ -104,8 +104,13 @@ def calculate_confidence_interval( ).otherwise(standard_error) # Calculate upper and lower confidence interval: - ci_lower = (beta - standard_error).alias("betaConfidenceIntervalLower") - ci_upper = (beta + standard_error).alias("betaConfidenceIntervalUpper") + z_score_095 = 1.96 + ci_lower = (beta - z_score_095 * standard_error).alias( + "betaConfidenceIntervalLower" + ) + ci_upper = (beta + z_score_095 * standard_error).alias( + "betaConfidenceIntervalUpper" + ) return (ci_lower, ci_upper) From 4635b99a6d91bc09ae38456f62b53389e2b7be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20L=C3=B3pez?= Date: Wed, 22 Nov 2023 09:10:43 +0000 Subject: [PATCH 44/47] chore: remove reference to confidence intervals --- tests/conftest.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e1a73bffd..f796913ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -410,11 +410,6 @@ def mock_summary_statistics_data(spark: SparkSession) -> DataFrame: # Making sure p-values are below 1: ).build() - # Because some of the columns are not strictly speaking required, they are dropped now: - data_spec = data_spec.drop( - "betaConfidenceIntervalLower", "betaConfidenceIntervalUpper" - ) - return data_spec From 23e3dc6d365a03b1b3d1bdca30673f524debe393 Mon Sep 17 00:00:00 2001 From: Kirill Tsukanov Date: Wed, 22 Nov 2023 09:38:49 +0000 Subject: [PATCH 45/47] fix: local SSD initialisation --- .pre-commit-config.yaml | 2 +- src/airflow/dags/common_airflow.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a83aff3ad..12aaa1513 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: - id: black - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v9.9.0 + rev: v9.10.0 hooks: - id: commitlint additional_dependencies: ["@commitlint/config-conventional"] diff --git a/src/airflow/dags/common_airflow.py b/src/airflow/dags/common_airflow.py index e62a95d5a..ea7855e0e 100644 --- a/src/airflow/dags/common_airflow.py +++ b/src/airflow/dags/common_airflow.py @@ -106,7 +106,9 @@ def create_cluster( # Create a disk config section if it does not exist. cluster_config[worker_section].setdefault("disk_config", dict()) # Specify the number of local SSDs. - cluster_config[worker_section]["num_local_ssds"] = num_local_ssds + cluster_config[worker_section]["disk_config"][ + "num_local_ssds" + ] = num_local_ssds # Return the cluster creation operator. return DataprocCreateClusterOperator( From 8f92ea999c59f92cc803f6ab3bd9ed31dad5fe6c Mon Sep 17 00:00:00 2001 From: David Ochoa Date: Fri, 24 Nov 2023 14:41:55 +0000 Subject: [PATCH 46/47] feat: gitignore .venv file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 38083e8fd..07c8bef82 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ docs/assets/schemas/ src/airflow/logs/* !src/airflow/logs/.gitkeep site/ +.env From 024d0848b0ddb65635d2d75bb57565dec9e43fbd Mon Sep 17 00:00:00 2001 From: Daniel Suveges Date: Sun, 26 Nov 2023 19:33:23 +0000 Subject: [PATCH 47/47] feat: ingestion supported for both new and old format of the harmonized GWAS Catalog Summary stats. (#274) * feat: converting gwas catalog ingestion to the new format * feat: generalizing GWAS catalog sumstas ingestion both format supported * fix: sample file must follow the convention of the real files * fix: woopsie, the sample file is not added * test: adding trst for old gwas format * feat: updating gwas summary stats ingestion step to the new way of getting data * fix: casting types to make schema explicit --- src/otg/common/spark_helpers.py | 32 +++++ .../gwas_catalog/summary_statistics.py | 119 ++++++++++++++---- src/otg/gwas_catalog_sumstat_preprocess.py | 11 +- tests/conftest.py | 10 -- .../gwas_summary_stats_sample.tsv.gz | Bin 53190 -> 0 bytes .../new_format_GCST90293086.h.tsv.gz | Bin 0 -> 39703 bytes .../old_format_GCST006090.h.tsv.gz | Bin 0 -> 46248 bytes .../test_gwas_catalog_summary_statistics.py | 114 +++++++++++++++-- 8 files changed, 230 insertions(+), 56 deletions(-) delete mode 100644 tests/data_samples/gwas_summary_stats_sample.tsv.gz create mode 100644 tests/data_samples/new_format_GCST90293086.h.tsv.gz create mode 100644 tests/data_samples/old_format_GCST006090.h.tsv.gz diff --git a/src/otg/common/spark_helpers.py b/src/otg/common/spark_helpers.py index 90fd8afb2..bd22d91c9 100644 --- a/src/otg/common/spark_helpers.py +++ b/src/otg/common/spark_helpers.py @@ -250,6 +250,38 @@ def normalise_column( ) +def neglog_pvalue_to_mantissa_and_exponent(p_value: Column) -> tuple[Column, Column]: + """Computing p-value mantissa and exponent based on the negative 10 based logarithm of the p-value. + + Args: + p_value (Column): Neg-log p-value (string) + + Returns: + tuple[Column, Column]: mantissa and exponent of the p-value + + Examples: + >>> ( + ... spark.createDataFrame([(4.56, 'a'),(2109.23, 'b')], ['negLogPv', 'label']) + ... .select('negLogPv',*neglog_pvalue_to_mantissa_and_exponent(f.col('negLogPv'))) + ... .show() + ... ) + +--------+------------------+--------------+ + |negLogPv| pValueMantissa|pValueExponent| + +--------+------------------+--------------+ + | 4.56| 3.63078054770101| -5| + | 2109.23|1.6982436524618154| -2110| + +--------+------------------+--------------+ + + """ + exponent: Column = f.ceil(p_value) + mantissa: Column = f.pow(f.lit(10), (p_value - exponent + f.lit(1))) + + return ( + mantissa.cast(t.DoubleType()).alias("pValueMantissa"), + (-1 * exponent).cast(t.IntegerType()).alias("pValueExponent"), + ) + + def calculate_neglog_pvalue( p_value_mantissa: Column, p_value_exponent: Column ) -> Column: diff --git a/src/otg/datasource/gwas_catalog/summary_statistics.py b/src/otg/datasource/gwas_catalog/summary_statistics.py index ccb97a77e..141cc7785 100644 --- a/src/otg/datasource/gwas_catalog/summary_statistics.py +++ b/src/otg/datasource/gwas_catalog/summary_statistics.py @@ -8,11 +8,12 @@ import pyspark.sql.functions as f import pyspark.sql.types as t +from otg.common.spark_helpers import neglog_pvalue_to_mantissa_and_exponent from otg.common.utils import convert_odds_ratio_to_beta, parse_pvalue from otg.dataset.summary_statistics import SummaryStatistics if TYPE_CHECKING: - from pyspark.sql import DataFrame + from pyspark.sql import SparkSession @dataclass @@ -22,53 +23,120 @@ class GWASCatalogSummaryStatistics(SummaryStatistics): @classmethod def from_gwas_harmonized_summary_stats( cls: type[GWASCatalogSummaryStatistics], - sumstats_df: DataFrame, - study_id: str, + spark: SparkSession, + sumstats_file: str, ) -> GWASCatalogSummaryStatistics: """Create summary statistics object from summary statistics flatfile, harmonized by the GWAS Catalog. + Things got slightly complicated given the GWAS Catalog harmonization pipelines changed recently so we had to accomodate to + both formats. + Args: - sumstats_df (DataFrame): Harmonized dataset read as a spark dataframe from GWAS Catalog. - study_id (str): GWAS Catalog study accession. + spark (SparkSession): spark session + sumstats_file (str): list of GWAS Catalog summary stat files, with study ids in them. Returns: GWASCatalogSummaryStatistics: Summary statistics object. """ + sumstats_df = spark.read.csv(sumstats_file, sep="\t", header=True).withColumn( + "studyId", + f.upper( + f.regexp_extract(f.input_file_name(), r"(GCST\d+)\.h\.tsv\.gz$", 1) + ), + ) + + # Parsing variant id fields: + chromosome = ( + f.col("hm_chrom") + if "hm_chrom" in sumstats_df.columns + else f.col("chromosome") + ).cast(t.StringType()) + position = ( + f.col("hm_pos") + if "hm_pos" in sumstats_df.columns + else f.col("base_pair_location") + ).cast(t.IntegerType()) + ref_allele = ( + f.col("hm_other_allele") + if "hm_other_allele" in sumstats_df.columns + else f.col("other_allele") + ) + alt_allele = ( + f.col("hm_effect_allele") + if "hm_effect_allele" in sumstats_df.columns + else f.col("effect_allele") + ) + + # Parsing p-value (get a tuple with mantissa and exponent): + p_value_expression = ( + parse_pvalue(f.col("p_value")) + if "p_value" in sumstats_df.columns + else neglog_pvalue_to_mantissa_and_exponent(f.col("neg_log_10_p_value")) + ) + # The effect allele frequency is an optional column, we have to test if it is there: - allele_frequency_expression = ( - f.col("hm_effect_allele_frequency").cast(t.FloatType()) - if "hm_effect_allele_frequency" in sumstats_df.columns + allele_frequency = ( + f.col("effect_allele_frequency") + if "effect_allele_frequency" in sumstats_df.columns else f.lit(None) - ) + ).cast(t.FloatType()) # Do we have sample size? This expression captures 99.7% of sample size columns. - sample_size_expression = ( - f.col("n").cast(t.IntegerType()) - if "n" in sumstats_df.columns - else f.lit(None).cast(t.IntegerType()) + sample_size = (f.col("n") if "n" in sumstats_df.columns else f.lit(None)).cast( + t.IntegerType() ) + # Depending on the input, we might have beta, but the column might not be there at all also old format calls differently: + beta_expression = ( + f.col("hm_beta") + if "hm_beta" in sumstats_df.columns + else f.col("beta") + if "beta" in sumstats_df.columns + # If no column, create one: + else f.lit(None) + ).cast(t.DoubleType()) + + # We might have odds ratio or hazard ratio, wich are basically the same: + odds_ratio_expression = ( + f.col("hm_odds_ratio") + if "hm_odds_ratio" in sumstats_df.columns + else f.col("odds_ratio") + if "odds_ratio" in sumstats_df.columns + else f.col("hazard_ratio") + if "hazard_ratio" in sumstats_df.columns + # If no column, create one: + else f.lit(None) + ).cast(t.DoubleType()) + + # Standard error is mandatory but called differently in the new format: + standard_error = f.col("standard_error").cast(t.DoubleType()) + # Processing columns of interest: processed_sumstats_df = ( sumstats_df # Dropping rows which doesn't have proper position: .select( - # Adding study identifier: - f.lit(study_id).cast(t.StringType()).alias("studyId"), + "studyId", # Adding variant identifier: - f.col("hm_variant_id").alias("variantId"), - f.col("hm_chrom").alias("chromosome"), - f.col("hm_pos").cast(t.IntegerType()).alias("position"), + f.concat_ws( + "_", + chromosome, + position, + ref_allele, + alt_allele, + ).alias("variantId"), + chromosome.alias("chromosome"), + position.alias("position"), # Parsing p-value mantissa and exponent: - *parse_pvalue(f.col("p_value")), + *p_value_expression, # Converting/calculating effect and confidence interval: *convert_odds_ratio_to_beta( - f.col("hm_beta").cast(t.DoubleType()), - f.col("hm_odds_ratio").cast(t.DoubleType()), - f.col("standard_error").cast(t.DoubleType()), + beta_expression, + odds_ratio_expression, + standard_error, ), - allele_frequency_expression.alias("effectAlleleFrequencyFromSource"), - sample_size_expression.alias("sampleSize"), + allele_frequency.alias("effectAlleleFrequencyFromSource"), + sample_size.alias("sampleSize"), ) .filter( # Dropping associations where no harmonized position is available: @@ -78,7 +146,8 @@ def from_gwas_harmonized_summary_stats( (f.col("beta") != 0) ) .orderBy(f.col("chromosome"), f.col("position")) - .repartition(400) + # median study size is 200Mb, max is 2.6Gb + .repartition(20) ) # Initializing summary statistics object: diff --git a/src/otg/gwas_catalog_sumstat_preprocess.py b/src/otg/gwas_catalog_sumstat_preprocess.py index 590552ed5..6a315f3cb 100644 --- a/src/otg/gwas_catalog_sumstat_preprocess.py +++ b/src/otg/gwas_catalog_sumstat_preprocess.py @@ -17,31 +17,24 @@ class GWASCatalogSumstatsPreprocessStep: session (Session): Session object. raw_sumstats_path (str): Input raw GWAS Catalog summary statistics path. out_sumstats_path (str): Output GWAS Catalog summary statistics path. - study_id (str): GWAS Catalog study identifier. """ session: Session = MISSING raw_sumstats_path: str = MISSING out_sumstats_path: str = MISSING - study_id: str = MISSING def __post_init__(self: GWASCatalogSumstatsPreprocessStep) -> None: """Run step.""" # Extract self.session.logger.info(self.raw_sumstats_path) self.session.logger.info(self.out_sumstats_path) - self.session.logger.info(self.study_id) - # Reading dataset: - raw_dataset = self.session.spark.read.csv( - self.raw_sumstats_path, header=True, sep="\t" - ) self.session.logger.info( - f"Number of single point associations: {raw_dataset.count()}" + f"Ingesting summary stats from: {self.raw_sumstats_path}" ) # Processing dataset: GWASCatalogSummaryStatistics.from_gwas_harmonized_summary_stats( - raw_dataset, self.study_id + self.session.spark, self.raw_sumstats_path ).df.write.mode(self.session.write_mode).parquet(self.out_sumstats_path) self.session.logger.info("Processing dataset successfully completed.") diff --git a/tests/conftest.py b/tests/conftest.py index f796913ae..8ad6a7083 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -465,16 +465,6 @@ def sample_gwas_catalog_ancestries_lut(spark: SparkSession) -> DataFrame: ) -@pytest.fixture() -def sample_gwas_catalog_harmonised_sumstats(spark: SparkSession) -> DataFrame: - """Sample GWAS harmonised sumstats sample data.""" - return spark.read.csv( - "tests/data_samples/gwas_summary_stats_sample.tsv.gz", - sep="\t", - header=True, - ) - - @pytest.fixture() def sample_gwas_catalog_harmonised_sumstats_list(spark: SparkSession) -> DataFrame: """Sample GWAS harmonised sumstats sample data.""" diff --git a/tests/data_samples/gwas_summary_stats_sample.tsv.gz b/tests/data_samples/gwas_summary_stats_sample.tsv.gz deleted file mode 100644 index 9a9c5f735f53bef21f711f55e8d492aae2bcb165..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53190 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 z 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 T^<(wZVJ80%GH@u2to#B1+A`rt diff --git a/tests/data_samples/new_format_GCST90293086.h.tsv.gz b/tests/data_samples/new_format_GCST90293086.h.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..a15d475df93ca47621b949350de1b4e1ec10fba3 GIT binary patch literal 39703 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 literal 0 HcmV?d00001 diff --git a/tests/data_samples/old_format_GCST006090.h.tsv.gz b/tests/data_samples/old_format_GCST006090.h.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..76a207ccff9137696b536388fe0cff90915d9e57 GIT binary patch literal 46248 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 None: - """Test GWASCatalogSummaryStatistics creation with mock data.""" - assert isinstance( - GWASCatalogSummaryStatistics.from_gwas_harmonized_summary_stats( - sample_gwas_catalog_harmonised_sumstats, "GCST000000" - ), - GWASCatalogSummaryStatistics, + from pyspark.sql import SparkSession + from pytest import FixtureRequest + + +class TestGWASCatalogSummaryStatistics: + """Test suite for GWAS Catalog summary stats ingestion.""" + + @pytest.fixture(scope="class") + def gwas_catalog_summary_statistics__new_format( + self: TestGWASCatalogSummaryStatistics, + spark: SparkSession, + ) -> GWASCatalogSummaryStatistics: + """Test GWASCatalogSummaryStatistics creation with mock data.""" + return GWASCatalogSummaryStatistics.from_gwas_harmonized_summary_stats( + spark, "tests/data_samples/new_format_GCST90293086.h.tsv.gz" + ) + + @pytest.fixture(scope="class") + def gwas_catalog_summary_statistics__old_format( + self: TestGWASCatalogSummaryStatistics, + spark: SparkSession, + ) -> GWASCatalogSummaryStatistics: + """Test GWASCatalogSummaryStatistics creation with mock data.""" + return GWASCatalogSummaryStatistics.from_gwas_harmonized_summary_stats( + spark, "tests/data_samples/old_format_GCST006090.h.tsv.gz" + ) + + @pytest.fixture(scope="class") + def test_dataset_instance( + self: TestGWASCatalogSummaryStatistics, request: FixtureRequest + ) -> GWASCatalogSummaryStatistics: + """Meta fixture to return the value of any requested fixture.""" + return request.getfixturevalue(request.param) + + @pytest.mark.parametrize( + "test_dataset_instance", + [ + "gwas_catalog_summary_statistics__old_format", + "gwas_catalog_summary_statistics__new_format", + ], + indirect=True, + ) + def test_return_type( + self: TestGWASCatalogSummaryStatistics, + test_dataset_instance: SummaryStatistics, + ) -> None: + """Testing return type.""" + assert isinstance(test_dataset_instance, SummaryStatistics) + + @pytest.mark.parametrize( + "test_dataset_instance", + [ + "gwas_catalog_summary_statistics__old_format", + "gwas_catalog_summary_statistics__new_format", + ], + indirect=True, + ) + def test_p_value_parsed_correctly( + self: TestGWASCatalogSummaryStatistics, + test_dataset_instance: SummaryStatistics, + ) -> None: + """Testing parsed p-value.""" + assert ( + test_dataset_instance.df.filter(f.col("pValueMantissa").isNotNull()).count() + > 1 + ) + + @pytest.mark.parametrize( + "test_dataset_instance", + [ + "gwas_catalog_summary_statistics__old_format", + "gwas_catalog_summary_statistics__new_format", + ], + indirect=True, + ) + def test_effect_parsed_correctly( + self: TestGWASCatalogSummaryStatistics, + test_dataset_instance: SummaryStatistics, + ) -> None: + """Testing properly parsed effect.""" + assert test_dataset_instance.df.filter(f.col("beta").isNotNull()).count() > 1 + + @pytest.mark.parametrize( + "test_dataset_instance", + [ + "gwas_catalog_summary_statistics__old_format", + "gwas_catalog_summary_statistics__new_format", + ], + indirect=True, ) + def test_study_id( + self: TestGWASCatalogSummaryStatistics, + test_dataset_instance: SummaryStatistics, + ) -> None: + """Testing properly parsed effect.""" + assert ( + test_dataset_instance.df.filter(f.col("studyId").startswith("GCST")).count() + == test_dataset_instance.df.count() + )