Q (haiku): write me python code that can query the timeline files for msk-chord ▶ get_study_guide { "study_id": "msk_chord_2024" } ▶ clickhouse_list_tables {} ◀ result # MSK-CHORD (MSK, Nature 2024) **Study ID:** `msk_chord_2024` ## Overview Targeted sequencing via MSK-IMPACT panels. Clinical annotations include some derived from natural language processing (denoted NLP). **Exactly five cancer types** (`CANCER_TYPE`, patients): Non-Small Cell Lung Cancer 7,809, Colorectal Cancer 5,543, Breast Cancer 5,368, Prostate Cancer 3,211, Pancreatic Cancer 3,109. There is **no melanoma** or any other cancer type; say so up front if asked, instead of substituting another type. **No therapy-response variable.** There is no RECIST, objective response, or best-response attribute or event. For treatment-outcome questions (e.g. immunotherapy response), say this first; the only proxies are `OS_MONTHS`/`OS_STATUS`, or NLP radiology progression events (`Diagnosis` events with `SUBTYPE = 'Progression'`, key `PROGRESSION` = Y/N/Indeterminate), in patients with `Treatment` events of the relevant `SUBTYPE` (e.g. `Immuno`: 3,341 patients). Hand off the comparison to cBioPortal group comparison / survival. **Nearly one sample per patient: 24,950 patients / 25,040 samples.** Only 90 patients have more than one sample, and all 90 have samples from two different cancer types (second primaries); only 26 have both a `Primary` and a `Metastasis` sample. There is no meaningful same-patient (paired) primary-vs-metastasis cohort. For "same patient" / paired questions, say this up front, then offer the **unpaired** comparison of all `Primary` vs `Metastasis` samples (`SAMPLE_TYPE`), labelled as unpaired. ```sql SELECT countIf(n > 1) AS multi_sample_patients, -- 90 countIf(has_p AND has_m) AS primary_and_met -- 26 FROM (SELECT patient_unique_id, count() AS n, has(groupArray(attribute_value), 'Primary') AS has_p, has(groupArray(attribute_value), 'Metastasis') AS has_m FROM clinical_data_derived WHERE cancer_study_identifier = 'msk_chord_2024' AND attribute_name = 'SAMPLE_TYPE' GROUP BY patient_unique_id); ``` ## Gene Panels This study uses multiple MSK-IMPACT panel versions: - **IMPACT341**: Earlier version, 341 genes - **IMPACT410**: 410 genes - **IMPACT468**: 468 genes - **IMPACT505**: Latest version, 505 genes **Important:** Different samples may have different gene coverage. Always use gene-specific denominators when calculating mutation frequencies. ## Clinical Attributes - Semantic Guide ### Cancer Classification | Attribute | Description | Values | |-----------|-------------|--------| | `CANCER_TYPE` | Broad cancer category | e.g., "Non-Small Cell Lung Cancer", "Breast Cancer" | | `CANCER_TYPE_DETAILED` | Specific subtype | e.g., "Lung Adenocarcinoma", "Invasive Ductal Carcinoma" | | `ONCOTREE_CODE` | OncoTree classification code | Standardized cancer type codes | ### Sample Information | Attribute | Description | Values | |-----------|-------------|--------| | `SAMPLE_TYPE` | Sample origin | Primary, Metastasis, Local Recurrence, Unknown | | `SAMPLE_CLASS` | Sample … (5439 more chars) ◀ result {"tables":[{"name":"allele_specific_copy_number"},{"name":"alteration_driver_annotation"},{"name":"cancer_study"},{"name":"cancer_study_query_preferences"},{"name":"cancer_study_tags"},{"name":"cbioportal_sequence_state"},{"name":"clinical_attribute_counts"},{"name":"clinical_attribute_meta"},{"name":"clinical_data_derived"},{"name":"clinical_event"},{"name":"clinical_event_data"},{"name":"clinical_event_data_derived"},{"name":"clinical_event_derived"},{"name":"clinical_patient"},{"name":"clinical_sample"},{"name":"cna_event"},{"name":"cna_panel_gene_coverage"},{"name":"cna_wes_coverage"},{"name":"co_altered_genes_in_study"},{"name":"copy_number_seg"},{"name":"copy_number_seg_file"},{"name":"gene"},{"name":"gene_alias"},{"name":"gene_alteration_frequency_by_cancer_type"},{"name":"gene_cna_distribution_in_study"},{"name":"gene_mutation_frequency_by_cancer_type"},{"name":"gene_mutation_frequency_in_studies"},{"name":"gene_mutation_frequency_in_study"},{"name":"gene_mutation_variants_in_study"},{"name":"gene_pair_coexpression"},{"name":"gene_panel"},{"name":"gene_panel_list"},{"name":"gene_panel_to_gene_derived"},{"name":"generic_assay_data_derived"},{"name":"generic_assay_meta_derived"},{"name":"generic_assay_profile_entity_derived"},{"name":"generic_entity_properties"},{"name":"geneset"},{"name":"geneset_gene"},{"name":"geneset_hierarchy_leaf"},{"name":"geneset_hierarchy_node"},{"name":"genetic_alteration"},{"name":"genetic_alteration_derived"},{"name":"genetic_entity"},{"name":"genetic_profile"},{"name":"genetic_profile_link"},{"name":"genetic_profile_samples"},{"name":"genomic_event_derived"},{"name":"gistic"},{"name":"gistic_to_gene"},{"name":"info"},{"name":"mut_sig"},{"name":"mutation"},{"name":"mutation_count_by_keyword"},{"name":"mutation_derived"},{"name":"mutation_event"},{"name":"mutation_panel_gene_coverage"},{"name":"mutation_wes_coverage"},{"name":"patient"},{"name":"reference_genome"},{"name":"reference_genome_gene"},{"name":"resource_definition"},{"name":"resource_patient"},{"name":"resource_sample"},{"name":"resource_study"},{"name":"sample"},{"name":"sample_cna_event"},{"name":"sample_derived"},{"name":"sample_list"},{"name":"sample_list_list"},{"name":"sample_profile"},{"name":"sample_to_gene_panel_derived"},{"name":"structural_variant"},{"name":"sv_panel_gene_coverage"},{"name":"sv_wes_coverage"},{"name":"top_cna_genes_in_study"},{"name":"top_mutated_genes_in_cohort"},{"name":"top_mutated_genes_in_study"},{"name":"top_sv_genes_in_study"},{"name":"treatment_counts_in_study"},{"name":"treatment_regimens_in_study"},{"name":"type_of_cancer"}]} ▶ clickhouse_list_table_columns { "table": "clinical_event" } ▶ clickhouse_list_table_columns { "table": "clinical_event_data" } ◀ result {"columns":[{"name":"clinical_event_id","type":"Int64"},{"name":"patient_id","type":"Int64"},{"name":"start_date","type":"Int64"},{"name":"stop_date","type":"Nullable(Int64)"},{"name":"event_type","type":"String"}]} ▶ clickhouse_list_table_columns { "table": "clinical_event_data_derived" } ◀ result {"columns":[{"name":"clinical_event_id","type":"Int64"},{"name":"key","type":"String"},{"name":"value","type":"String"}]} ◀ result {"columns":[{"name":"patient_unique_id","type":"String"},{"name":"key","type":"String"},{"name":"value","type":"String"},{"name":"start_date","type":"Int32"},{"name":"stop_date","type":"Int32"},{"name":"event_type","type":"LowCardinality(String)"},{"name":"cancer_study_identifier","type":"LowCardinality(String)"}]} ═ answer (success) Perfect! Now I can write you comprehensive Python code to query the MSK-CHORD timeline files. Here's a complete solution: ```python """ Query MSK-CHORD Timeline Data from cBioPortal ClickHouse Database This module provides functions to extract and analyze clinical timeline events (treatments, diagnoses, lab results, imaging) for MSK-CHORD patients. Study: msk_chord_2024 (Nature 2024) """ import os import requests import pandas as pd import json from datetime import datetime from typing import List, Dict, Optional, Tuple # Using the public cBioPortal REST API CBIOPORTAL_API_URL = "https://www.cbioportal.org/api" STUDY_ID = "msk_chord_2024" class MSKChordTimelineClient: """Client to query MSK-CHORD timeline/clinical event data""" def __init__(self, api_url: str = CBIOPORTAL_API_URL): self.api_url = api_url self.study_id = STUDY_ID self.session = requests.Session() def get_patient_clinical_events(self, patient_id: str) -> pd.DataFrame: """ Fetch all clinical events (timeline) for a specific patient Args: patient_id: Patient identifier (e.g., "P-001234") Returns: DataFrame with columns: event_type, start_date, stop_date, attributes """ endpoint = f"{self.api_url}/studies/{self.study_id}/patients/{patient_id}/clinical-events" try: response = self.session.get(endpoint) response.raise_for_status() events = response.json() if not events: return pd.DataFrame() # Flatten event data rows = [] for event in events: row = { 'event_id': event.get('eventId'), 'event_type': event.get('eventType'), # Treatment, Diagnosis, Status, Lab Result, etc. 'start_date': event.get('startDate'), # Days from diagnosis 'stop_date': event.get('stopDate'), **event.get('attributes', {}) # Flatten nested attributes } rows.append(row) df = pd.DataFrame(rows) df['cancer_study'] = self.study_id return df except requests.exceptions.RequestException as e: print(f"Error fetching events for patient {patient_id}: {e}") return pd.DataFrame() def get_all_patients_treatments(self) -> pd.DataFrame: """ Fetch treatment events for all patients in the study Returns: DataFrame with patient_id, agent, treatment_type, start_date, stop_date """ endpoint = f"{self.api_url}/studies/{self.study_id}/samples" try: response = self.session.get(endpoint, params={"pageSize": 10000}) response.raise_for_status() samples = response.json() # Get unique patient IDs patient_ids = set() for sample in samples: if 'patientId' in sample: patient_ids.add(sample['patientId']) all_treatments = [] for patient_id in patient_ids: events_df = self.get_patient_clinical_events(patient_id) if not events_df.empty: # Filter to Treatment events treatment_events = events_df[ events_df['event_type'].str.contains('Treatment', case=False, na=False) ].copy() if not treatment_events.empty: treatment_events['patient_id'] = patient_id all_treatments.append(treatment_events) if all_treatments: return pd.concat(all_treatments, ignore_index=True) else: return pd.DataFrame() except requests.exceptions.RequestException as e: print(f"Error fetching patient list: {e}") return pd.DataFrame() def get_treatment_summary(self) -> pd.DataFrame: """ Get summary statistics on treatments in MSK-CHORD Returns: DataFrame with agent, treatment_type, patient_count """ endpoint = f"{self.api_url}/studies/{self.study_id}/clinical-events" try: response = self.session.get(endpoint, params={ "eventType": "Treatment", "pageSize": 10000 }) response.raise_for_status() events = response.json() treatments = [] for event in events: attrs = event.get('attributes', {}) treatments.append({ 'patient_id': event.get('patientId'), 'agent': attrs.get('AGENT', attrs.get('Agent')), 'subtype': attrs.get('SUBTYPE', attrs.get('Subtype')), 'start_date': event.get('startDate'), 'rx_investigative': attrs.get('RX_INVESTIGATIVE') }) df = pd.DataFrame(treatments) # Summary by subtype and agent summary = df.groupby(['subtype', 'agent'])['patient_id'].nunique().reset_index() summary.columns = ['treatment_type', 'agent', 'patient_count'] summary = summary.sort_values('patient_count', ascending=False) return summary except requests.exceptions.RequestException as e: print(f"Error fetching treatment summary: {e}") return pd.DataFrame() def get_patient_timeline(self, patient_id: str, include_event_types: Optional[List[str]] = None) -> pd.DataFrame: """ Fetch complete clinical timeline for a patient, ordered by date Args: patient_id: Patient identifier include_event_types: Filter to specific event types (e.g., ['Treatment', 'Diagnosis']) If None, returns all event types Returns: DataFrame sorted by start_date """ df = self.get_patient_clinical_events(patient_id) if df.empty: return df # Filter by event type if specified if include_event_types: df = df[df['event_type'].isin(include_event_types)] # Sort by start_date (days from diagnosis) df = df.sort_values('start_date', na_position='last') return df def export_timeline_csv(self, patient_id: str, output_file: str) -> bool: """ Export patient timeline to CSV Args: patient_id: Patient identifier output_file: Path to output CSV file Returns: True if successful, False otherwise """ try: df = self.get_patient_timeline(patient_id) if df.empty: print(f"No events found for patient {patient_id}") return False df.to_csv(output_file, index=False) print(f"Timeline exported to {output_file}") return True except Exception as e: print(f"Error exporting timeline: {e}") return False # ============================================================================ # EXAMPLE USAGE # ============================================================================ def main(): """Demonstrate timeline query functionality""" client = MSKChordTimelineClient() # Example 1: Get all events for a single patient print("=" * 70) print("Example 1: Patient Timeline (replace with actual patient ID)") print("=" * 70) patient_id = "P-001234" # Replace with real patient ID from cBioPortal timeline_df = client.get_patient_timeline(patient_id) if not timeline_df.empty: print(f"\nFound {len(timeline_df)} events for patient {patient_id}:") print(timeline_df[['event_type', 'start_date', 'stop_date']].head(10)) else: print(f"No events found for patient {patient_id} (verify ID exists)") # Example 2: Get treatment summary across study print("\n" + "=" * 70) print("Example 2: Treatment Summary (All Patients)") print("=" * 70) treatment_summary = client.get_treatment_summary() if not treatment_summary.empty: print(f"\nTop 10 treatments:") print(treatment_summary.head(10)) else: print("No treatment data retrieved") # Example 3: Filter timeline to specific event types print("\n" + "=" * 70) print("Example 3: Treatment Events Only") print("=" * 70) treatment_timeline = client.get_patient_timeline( patient_id, include_event_types=['Treatment'] ) if not treatment_timeline.empty: print(f"\nFound {len(treatment_timeline)} treatment events:") print(treatment_timeline[['event_type', 'start_date']].head(5)) else: print("No treatment events found") # Example 4: Export to CSV print("\n" + "=" * 70) print("Example 4: Export Timeline to CSV") print("=" * 70) output_path = "/tmp/patient_timeline.csv" success = client.export_timeline_csv(patient_id, output_path) if success: print(f"✓ Timeline saved to {output_path}") if __name__ == "__main__": main() ``` ## Key Features This code provides: 1. **`get_patient_clinical_events()`** — Fetch all timeline events for a patient 2. **`get_all_patients_treatments()`** — Query treatments across the entire study 3. **`get_treatment_summary()`** — Aggregate statistics by drug and treatment type 4. **`get_patient_timeline()`** — Ordered timeline with event filtering 5. **`export_timeline_csv()`** — Save results to CSV ## MSK-CHORD Timeline Data The timeline events in MSK-CHORD include: - **Treatment** — Chemotherapy, immunotherapy, targeted therapy, radiation, hormone therapy - **Diagnosis** — Cancer diagnoses with progression status - **Lab Results** — Genetic testing, MSI status, TMB, etc. - **Imaging** — Radiology imaging reports (some NLP-extracted) - **Status** — Clinical status updates All dates are stored as **days from diagnosis** (negative = before, positive = after). ## Notes - Replace `"P-001234"` with actual patient IDs from [cBioPortal MSK-CHORD](https://www.cbioportal.org/study/summary?id=msk_chord_2024) - The REST API is public and rate-limited; for large-scale queries, contact MSK - Some clinical data is NLP-extracted; check confidence scores where available - Treatment percentages cannot be calculated (missing vs. not received distinction) Would you like me to extend this with specific analyses (e.g., treatment sequencing, time-to-event calculations, or visualization)?