This project implements a data pipeline to process information about drugs, PubMed publications, and clinical trials. It identifies mentions of specific drugs within publication titles and generates a JSON output representing a graph of these relationships, linking drugs to the journals and dates of their mentions.
This project serves as a technical assessment.
- Project Objective
- Project Structure
- Input Data Sources
- Output
- Setup and Installation
- Running the Pipeline
- Data Quality Considerations & Handling
- Future Considerations & Improvements
The primary goal is to build a robust data pipeline that:
- Ingests data from multiple sources:
drugs.csv,pubmed.csv,pubmed.json, andclinical_trials.csv. - Cleans and standardizes the ingested data.
- Identifies mentions of drugs (from
drugs.csv) within the titles of publications (from PubMed and Clinical Trials). - Constructs a graph, represented as a JSON object, linking drugs to the journals that mentioned them, including the date and details of each mention.
data/drugs.csv: Contains drug names and ATC codes. (Fields:atccode,drug)data/pubmed.csv: PubMed article information. (Fields:id,title,date,journal)data/pubmed.json: PubMed article information in JSON format. (Fields:id,title,date,journal)data/clinical_trials.csv: Clinical trial information. (Fields:id,scientific_title,date,journal)
The pipeline generates a single JSON file:
output/drug_mentions_graph.json: This file contains the graph structure. The top-level keys are normalized drug names. Each drug maps to an object where keys are normalized journal names. Each journal name maps to a list of mention objects, each detailing a specific publication:{ "normalized_drug_name_1": { "normalized_journal_name_a": [ { "date": "YYYY-MM-DD", // or "UNKNOWN_DATE" or original if unparseable "publication_title": "Original Publication Title...", "source_type": "pubmed_csv", // or pubmed_json, clinical_trials_csv "source_id": "original_publication_id_or_placeholder" } ], // ... other journals for drug_1 }, // ... other drugs }
-
Clone the repository (if applicable):
git clone <repository_url> cd MedicalResearchStudy
-
Create and activate a Python virtual environment: It is highly recommended to use a virtual environment to manage project dependencies.
# Ensure you are in the MedicalResearchStudy project root python -m venv venvActivate the environment:
- On macOS/Linux:
source venv/bin/activate - On Windows:
.\venv\Scripts\activate
- On macOS/Linux:
-
Install dependencies: With the virtual environment activated, install the required packages:
pip install -r requirements.txt
With the virtual environment activated and dependencies installed, run the pipeline from the project root directory (MedicalResearchStudy/):
python -m src.drug_graph_generator.mainThe script will:
- Load data from the
data/directory. - Process the data to find drug mentions.
- Generate the
drug_mentions_graph.jsonfile in theoutput/directory (this directory will be created if it doesn't exist). - Print progress messages to the console.
Below is an example output for the pipeline (along with the adhoc queries).
This pipeline incorporates several strategies to handle inconsistencies and potential issues in the input data:
- Problem: Input dates appear in various formats (e.g.,
DD/MM/YYYY,MM/DD/YYYY,YYYY-MM-DD, "Month Day, Year"). The ambiguity betweenDD/MMandMM/DD(e.g., "03/04/2025") is a key challenge. - Approach:
- The
utils.parse_datefunction attempts a series ofstrptimeformats. - Priority:
DD/MM/YYYY(European/Standard) is attempted beforeMM/DD/YYYY(American) for ambiguous slash-separated dates, as it's considered more broadly standard in many contexts. Users should be aware of this assumption. - Other common formats (
YYYY-MM-DD, textual month names) are also handled. - Commas are stripped from date strings before parsing.
- If a date cannot be parsed into
YYYY-MM-DD, the original (comma-stripped) string or a placeholder (UNKNOWN_DATE) is used.
- The
- Relevant Code:
src/drug_graph_generator/utils.py(parse_datefunction).
- Problem: Fields like
id,title, orjournalcan be missing or empty. - Approach:
- IDs: If an
idis missing or empty, a unique placeholder ID is generated (e.g.,pubmed_json_item_0_no_id) to ensure each record can be uniquely referenced internally. Your note "In our case, for the null ID cases we have, we'll just keep them" is interpreted as: we don't discard the record, but we do need an internal way to track it, hence the placeholder. The originalidvalue (even if empty) is not directly used if it's problematic for keys. - Titles: Publications with empty titles after normalization are filtered out before graph building, as drug matching cannot occur. Original titles (even if empty) are stored in the
original_titlefield. - Journals: If a journal name is missing or empty, a placeholder (e.g.,
unknown_journal) is used for consistent grouping in the output graph.
- IDs: If an
- Relevant Code:
src/drug_graph_generator/data_loader.py(within_create_publication_entryand specific loader classes).
- Problem: The
pubmed.jsonfile example includes a trailing comma, which is invalid in strict JSON and causes errors with Python's standardjson.load(). - Approach: The
json5library is used to parse input JSON files (pubmed.json).json5is more lenient and handles common non-standard JSON features like trailing commas. The output JSON generated by the pipeline is still strict standard JSON usingjson.dump(). - Relevant Code:
src/drug_graph_generator/data_loader.py(inPubMedJSONLoader),import json5.
- Problem: Drug names, titles, and journal names may appear with different casings or leading/trailing whitespace.
- Approach: The
utils.normalize_textfunction converts text to lowercase and strips whitespace. This is applied to:- Drug names (for storage and matching).
- Publication titles (for searching/matching drugs).
- Journal names (for use as consistent keys in the output graph). The original, unnormalized publication titles are preserved for readability in the final JSON output.
- Relevant Code:
src/drug_graph_generator/utils.py(normalize_text), used throughoutdata_loader.py.
- Problem: Input files might use non-ASCII characters (e.g.,
ñinclinical_trials.csvas\xc3\xb1) or include a Byte Order Mark (BOM). - Approach:
- Files are generally opened with
encoding='utf-8'. - For CSV files potentially containing a BOM (like
clinical_trials.csv),encoding='utf-8-sig'is used to correctly handle it. - A second, regex-based transformation is made thereafter to ensure no trailing badly encoded characters remain.
- Output JSON is written with
ensure_ascii=Falseto preserve non-ASCII characters correctly.
- Files are generally opened with
- Relevant Code: File opening statements in
data_loader.py,json.dumpinmain.py.
- Problem: Simple substring matching (e.g.,
"ace" in "face cream") can lead to false positives. - Approach: Drug names and publication titles are normalized (lowercased). The
processing.build_drug_mention_graphfunction uses regular expressions (re.searchwith\bword boundaries:r'\b' + re.escape(drug_name) + r'\b') to match whole drug words within the normalized titles.re.escapeis used to handle any special regex characters within drug names themselves. - Relevant Code:
src/drug_graph_generator/processing.py(build_drug_mention_graph).
The current pipeline processes data in memory and would not scale to terabytes of data or millions of files. To handle such volumes, significant architectural changes would be necessary:
-
Iterative Processing & Orchestration: For continuously arriving data or extremely large historical datasets, an iterative or streaming approach would be preferable over full re-computation. The overall pipeline would be orchestrated by a tool like Apache Airflow, managing the execution of distributed jobs (e.g., Spark applications) rather than a single Python script. Utility functions for normalization and parsing would be adapted for use within the distributed framework (e.g., as UDFs).
-
Distributed Processing & Storage: The core modification would involve adopting a distributed processing framework like Apache Spark or Dask. Data ingestion would change from loading entire files into Python lists to distributed reading (e.g.,
spark.read.csv()). Input data and intermediate results would be stored in scalable systems like cloud object storage (S3, GCS, Azure Blob) or HDFS, ideally using analytics-optimized columnar formats like Apache Parquet. -
Algorithmic Optimization: Most of the functions we're using use a fairly naive approach and do not optimize for time and space complexity. If we were to have large datasets, we would need to seek those marginal gains.
This project includes SQL queries designed for analyzing sales data from the TRANSACTIONS and PRODUCT_NOMENCLATURE tables, as specified in Part II of the technical test. These queries are written for Google BigQuery and can be found in the queries/ directory.
-
queries/daily_revenue.sql:- Objective: Calculates the total daily sales revenue (
ventes) for the year 2019. - Key Considerations & Logic:
- Two approaches are presented: one showing sales only for dates with transactions, and a preferred second option that ensures all dates in 2019 are displayed.
- The second option achieves this by generating a complete 2019 date series and
LEFT JOIN-ing it with aggregated daily sales.COALESCEis then used to present0revenue for dates without sales, providing a comprehensive daily view. - Filtering is applied for the year 2019 using
EXTRACT(YEAR FROM date).
- Expected Output Columns (for the comprehensive option):
date,ventes
- Objective: Calculates the total daily sales revenue (
-
queries/sale_types_per_user.sql:- Objective: Determines the total sales for "MEUBLE" (furniture) and "DECO" (decoration) product types per client for the year 2019.
- Key Considerations & Logic:
- Utilizes a
LEFT JOINbetweenTRANSACTIONSandPRODUCT_NOMENCLATUREto link sales to product types. - Employs conditional aggregation (
SUM(CASE WHEN ... END)) to pivot sales intoventes_meubleandventes_decocolumns. - Critically,
COALESCE(..., 0)is applied to these sums. This ensures that clients with sales in 2019 but none for a specific product type (e.g., no "MEUBLE" sales) will show0for that category, rather thanNULL. This approach enhances data presentation and is robust for scenarios where one mightLEFT JOINfrom aclientsdimension, ensuring all clients are represented even if they have no relevant transactions. - Filters for transactions within 2019 and groups results by
client_id.
- Utilizes a
- Expected Output Columns:
client_id,ventes_meuble,ventes_deco
Assumptions for SQL Queries:
- Tables are named
transactionsandproduct_nomenclature(or fully qualified paths). - Relevant columns (
date,prod_price,prod_qty,product_type) exist with appropriate data types.
Below are example outputs for the SQL queries (I used the Test Document sample data, modifying the year to 2019 to comply with the filter).




