alethiotx.artemis.clinical

1from .scores import compute, load, lookup_drug_family_representation, filter_overrepresented_families, unique, approved, all_targets
2
3__all__ = ['compute', 'load', 'lookup_drug_family_representation', 'filter_overrepresented_families', 'unique', 'approved', 'all_targets']
def compute( mesh_headings: list, chembl_version: str = '36', trials_only_last_n_years: int = None, filter_families: bool = True) -> dict:
397def compute(mesh_headings: list, chembl_version: str = '36', trials_only_last_n_years: int = None, filter_families: bool = True) -> dict:
398   """
399   Compute clinical validation scores for drug targets across multiple disease indications.
400   
401   This is the core function of the Artemis clinical scoring module. It processes ChEMBL drug-target-disease
402   relationships combined with clinical trial data to calculate comprehensive validation scores for each
403   target gene. These scores quantify the strength of clinical evidence supporting each target, enabling
404   data-driven target prioritization for drug discovery.
405   
406   **What This Function Does:**
407   
408   - Loads drug molecule, target, and clinical trial data from ChEMBL
409   - Expands diseases using MeSH hierarchies (e.g., "Lung Neoplasms" → all lung cancer subtypes)
410   - Counts clinical trials at each phase (0-4) for each drug-target pair
411   - Optionally filters out over-represented gene families to reduce bias
412   - Aggregates evidence across all drugs targeting the same gene
413   - Returns comprehensive target-level scores for each disease
414   
415   **Processing Workflow:**
416   
417   1. **Data Loading** (once for all diseases):
418      
419      - Downloads pre-computed ChEMBL molecules.csv from S3
420      - Contains drug-target-indication-trial relationships
421      - Optional temporal filtering to recent trials
422   
423   2. **Gene Family Analysis** (optional, once for all diseases):
424      
425      - Downloads HGNC gene family data
426      - Creates lookup table of drug-family representation percentages
427      - Identifies multi-target drugs (e.g., multi-kinase inhibitors)
428   
429   3. **Per-Disease Processing** (for each disease in mesh_headings):
430      
431   - **MeSH Expansion**: Retrieves all descendant disease terms
432      (e.g., "Lung Neoplasms" includes "Adenocarcinoma of Lung", "Small Cell Lung Carcinoma", etc.)
433   
434   - **Trial De-duplication**: Removes duplicate trials from:
435
436      - Same trial appearing in multiple mesh descendants
437      - Same trial with different molecular forms (salts) of the same parent drug
438   
439   - **Family Filtering** (optional): For each drug-disease combination:
440
441      - Identifies over-represented gene families (>40% of family targeted)
442      - Keeps one representative target per family (first alphabetically)
443      - Eliminates bias from multi-target drugs
444   
445   - **Trial Counting**: Counts trials per phase for each target:
446
447      - Phase 0-3: Experimental/developmental trials
448      - Phase 4: Approved drugs (post-marketing surveillance)
449   
450   - **Score Aggregation**: For each target gene:
451
452      - Sum all trial phases (0-3) across all drugs → phase_scores
453      - Check if any drug is approved (phase 4) → approved (boolean)
454      - Calculate final score: clinical_scores = phase_scores + (20 if approved else 0)
455   
456   - **Output**: DataFrame with one row per target, sorted by clinical_scores (descending)
457   
458   **Scoring System Details:**
459   
460   Each target receives a **clinical_scores** value based on:
461   
462   1. **Phase Scores** (0-3 points per trial):
463      
464   - Phase 0: 0 points (exploratory studies)
465   - Phase 1: 1 point (safety/dosing in small groups)
466   - Phase 2: 2 points (efficacy in larger groups)
467   - Phase 3: 3 points (large-scale confirmation)
468   - Phase 4: 0 points (counted separately as "approved")
469   
470   2. **Approval Bonus**: +20 points if target has any approved drug
471   
472   3. **Multi-Drug Aggregation**: If multiple drugs target the same gene, all their phase scores sum
473   
474   4. **Multi-Trial Aggregation**: Each distinct clinical trial contributes its phase score
475   
476   **Example Scoring:**
477   
478   - Target A: 2 Phase 2 trials + 1 Phase 3 trial = 2+2+3 = 7 points
479   - Target B: 1 Phase 3 trial + 1 approved drug = 3+20 = 23 points
480   - Target C: 3 drugs with Phase 2 trials + approved = (2+2+2)+20 = 26 points
481   
482   **Why Family Filtering Matters:**
483   
484   Multi-target drugs (e.g., sunitinib targeting 8 RTKs) can artificially inflate scores for
485   specific gene families. Without filtering, all 8 RTKs receive high scores from the same drug,
486   biasing results toward that family. With filtering (default), only 1 RTK representative is kept,
487   producing unbiased scores that better reflect biological diversity.
488   
489   :param mesh_headings: List of MeSH disease headings to analyze. Standard MeSH terms like
490   - 'Breast Neoplasms' (breast cancer)
491   - 'Lung Neoplasms' (lung cancer)
492   - 'Diabetes Mellitus, Type 2' (type 2 diabetes)
493   - 'Cardiovascular Diseases' (heart disease)
494   
495   Each heading is automatically expanded to include all MeSH descendant terms.
496   Use exact MeSH terminology for best results.
497   :type mesh_headings: list[str]
498
499   :param chembl_version: ChEMBL database version to use. Versions correspond to data release dates
500   - '36' (default): Data through 2024
501   - '35': Data through 2023
502   
503   See https://www.ebi.ac.uk/chembl/ for version details.
504   
505   :type chembl_version: str, optional
506
507   :param trials_only_last_n_years: Temporal filter for recent trials only. If provided
508   - Filters to trials registered in last N years
509   - Year inferred from ClinicalTrials.gov NCT IDs
510   - Useful for identifying emerging targets
511   
512   Examples:
513   - ``6``: Last 6 years (captures recent development)
514   - ``10``: Last decade
515   - ``None`` (default): All historical trials
516   :type trials_only_last_n_years: int or None, optional
517
518   :param filter_families: Whether to apply gene family filtering to reduce bias
519                           
520   - ``True`` (default): Filter over-represented families
521      (recommended for unbiased target prioritization)
522   - ``False``: Include all targets without filtering
523      (use when family representation is biologically relevant)
524   :type filter_families: bool, optional
525   :return: Dictionary mapping each MeSH heading to its results DataFrame.
526            
527   **Dictionary Structure:**
528   
529   - Keys: MeSH disease headings (str) - same as input ``mesh_headings``
530   - Values: DataFrames with target-level clinical scores
531   
532   **DataFrame Columns:**
533   
534   - ``target_gene_name`` (str): HGNC gene symbol (e.g., 'EGFR', 'TP53')
535   - ``phase_0`` (int): Count of phase 0 trials for this target
536   - ``phase_1`` (int): Count of phase 1 trials for this target
537   - ``phase_2`` (int): Count of phase 2 trials for this target
538   - ``phase_3`` (int): Count of phase 3 trials for this target
539   - ``phase_4`` (int): Count of phase 4 (approved) drugs for this target
540   - ``phase_scores`` (int): Sum of phase_1 + 2×phase_2 + 3×phase_3
541   - ``approved`` (bool): True if phase_4 > 0 (any approved drug exists)
542   - ``clinical_scores`` (int): phase_scores + (20 if approved else 0)
543   
544   DataFrames are sorted by ``clinical_scores`` descending (highest validation first).
545      
546   :rtype: dict[str, DataFrame]
547   
548   .. note::
549      **Efficiency Optimization**: ChEMBL data and gene family lookup tables are loaded once and
550      shared across all diseases. Processing 7 diseases takes ~5-10 minutes total, not per disease.
551   
552   .. note::
553      **MeSH Hierarchy Expansion**: Diseases are automatically expanded to include all subtypes.
554      For example, "Breast Neoplasms" includes "Ductal Carcinoma", "Lobular Carcinoma", etc.
555      This ensures comprehensive coverage of clinical evidence.
556   
557   .. note::
558      **Trial De-duplication**: The function carefully de-duplicates clinical trials to avoid
559      double-counting. Trials are unique by (drug, NCT ID, target, disease) to ensure accurate counts.
560   
561   .. warning::
562      **AWS S3 Access Required**: This function reads ChEMBL and MeSH data from the
563      ``alethiotx-artemis`` S3 bucket. Configure AWS credentials via environment variables
564      or ``aws configure`` before calling.
565   
566   .. warning::
567      **Memory Usage**: ChEMBL v36 molecules.csv is ~500MB-1GB in memory. Processing multiple
568      diseases simultaneously requires sufficient RAM. For memory-constrained environments,
569      process diseases in smaller batches.
570   
571   .. warning::
572      **Processing Time**: Full computation takes 5-10 minutes for 7 diseases with family filtering.
573      Set ``filter_families=False`` to reduce runtime by ~50% (but scores may be biased).
574   
575   .. warning::
576      **Family Filtering Off**: When ``filter_families=False``, results may show artificial
577      enrichment for gene families with multi-target drugs. Use this option only when
578      family-level representation is biologically meaningful for your analysis.
579   
580   **Examples**
581   
582   Basic usage - single disease::
583   
584   >>> from alethiotx.artemis.clinical import compute
585   >>> results = compute(['Lung Neoplasms'])
586   >>> lung_targets = results['Lung Neoplasms']
587   >>> print(f"Found {len(lung_targets)} lung cancer targets")
588   >>> print(lung_targets.head())
589      target_gene_name  phase_0  phase_1  phase_2  phase_3  phase_4  phase_scores  approved  clinical_scores
590   0  EGFR             0        5        15       8        3         45           True      65
591   1  ALK              0        2        8        6        2         32           True      52
592      
593   Multiple diseases simultaneously::
594   
595   >>> diseases = ['Breast Neoplasms', 'Lung Neoplasms', 'Prostatic Neoplasms']
596   >>> results = compute(diseases, chembl_version='36', filter_families=True)
597   >>> for disease, df in results.items():
598   ...     approved_count = df['approved'].sum()
599   ...     print(f"{disease}: {len(df)} total targets, {approved_count} approved")
600   
601   Recent trials only (emerging targets)::
602   
603   >>> # Focus on targets with recent clinical activity (last 6 years)
604   >>> results = compute(
605   ...     ['Lung Neoplasms'],
606   ...     trials_only_last_n_years=6,
607   ...     filter_families=True
608   ... )
609   >>> recent_lung = results['Lung Neoplasms']
610   >>> print("Emerging lung cancer targets:", recent_lung['target_gene_name'].tolist()[:10])
611   
612   Disable family filtering for comprehensive view::
613   
614   >>> # Include all targets without filtering (may show family bias)
615   >>> results = compute(['Lung Neoplasms'], filter_families=False)
616   >>> lung_all = results['Lung Neoplasms']
617   >>> print(f"All targets (no filtering): {len(lung_all)}")
618   
619   Filter to approved targets only::
620   
621   >>> results = compute(['Diabetes Mellitus, Type 2'])
622   >>> diabetes = results['Diabetes Mellitus, Type 2']
623   >>> approved_diabetes = diabetes[diabetes['approved'] == True]
624   >>> print(f"Approved diabetes targets: {len(approved_diabetes)}")
625   >>> print(approved_diabetes[['target_gene_name', 'clinical_scores']].head())
626   
627   Compare targets across diseases::
628   
629   >>> diseases = ['Breast Neoplasms', 'Lung Neoplasms', 'Prostatic Neoplasms']
630   >>> results = compute(diseases)
631   >>> target = 'EGFR'
632   >>> for disease in diseases:
633   ...     df = results[disease]
634   ...     if target in df['target_gene_name'].values:
635   ...         score = df[df['target_gene_name'] == target]['clinical_scores'].iloc[0]
636   ...         print(f"{disease}: {target} = {score} points")
637   
638   Save results to CSV files::
639   
640   >>> results = compute(['Breast Neoplasms', 'Lung Neoplasms'])
641   >>> for disease, df in results.items():
642   ...     filename = disease.replace(' ', '_').replace(',', '') + '.csv'
643   ...     df.to_csv(filename, index=False)
644   ...     print(f"Saved {filename}")
645   
646   Batch processing for all major diseases::
647   
648   >>> all_diseases = [
649   ...     'Breast Neoplasms',
650   ...     'Lung Neoplasms', 
651   ...     'Prostatic Neoplasms',
652   ...     'Skin Neoplasms',
653   ...     'Intestinal Neoplasms',
654   ...     'Diabetes Mellitus, Type 2',
655   ...     'Cardiovascular Diseases'
656   ... ]
657   >>> results = compute(all_diseases, trials_only_last_n_years=6)
658   >>> print(f"Processed {len(results)} diseases in one batch")
659   
660   .. seealso::
661      - :func:`lookup_drug_family_representation`: Creates family lookup table used when ``filter_families=True``
662      - :func:`filter_overrepresented_families`: Filters individual drug-disease combinations
663      - :func:`load`: Load pre-computed scores from S3 (faster than computing from scratch)
664      - :func:`~alethiotx.artemis.mesh.descendants`: Expands MeSH terms to include all subtypes
665      - :func:`~alethiotx.artemis.chembl.molecules`: Retrieves raw ChEMBL molecule data
666   """
667   print("="*80)
668   print("Loading ChEMBL data...")
669   print("="*80)
670   
671   # Load ChEMBL clinical trials data once for all diseases (efficiency optimization)
672   chembl = read_csv(f's3://alethiotx-artemis/data/chembl/{chembl_version}/molecules.csv')
673   
674   # Apply temporal filter if requested (e.g., only trials from last 6 years)
675   if trials_only_last_n_years is not None:
676      current_year = int(date.today().strftime("%Y"))
677      cutoff_year = current_year - trials_only_last_n_years
678      chembl = chembl[chembl['trial_year'] >= cutoff_year]
679      print(f"Filtered to trials from last {trials_only_last_n_years} years (>= {cutoff_year})")
680   
681   # Create gene family representation lookup table once (shared across all diseases)
682   # This identifies which drugs over-represent specific gene families
683   lookup_table = None
684   if filter_families:
685      print("\nCreating drug-family representation lookup table...")
686      lookup_table = lookup_drug_family_representation(chembl)
687      print(f"Lookup table created with {len(lookup_table)} drug-mesh-family combinations")
688   else:
689      print("\nFamily filtering disabled - all targets will be included")
690   
691   # Process each disease indication independently
692   results = {}
693   
694   for idx, mesh_heading in enumerate(mesh_headings, 1):
695      print("\n" + "="*80)
696      print(f"Processing disease {idx}/{len(mesh_headings)}: {mesh_heading}")
697      print("="*80)
698      
699      # Step 1: Get all MeSH descendants first
700      # Example: "Lung Neoplasms" includes "Adenocarcinoma of Lung", "Small Cell Lung Carcinoma", etc.
701      mesh_descendants_list = mesh_descendants(
702         mesh_heading,
703         s3_base='s3://alethiotx-artemis/data/mesh/',
704         file_base='d2025'
705      )
706      print(f"Including {len(mesh_descendants_list)} MeSH headings (including descendants)")
707      
708      # Filter ChEMBL data to this disease and its descendants
709      disease_data = chembl[chembl['mesh_heading'].isin(mesh_descendants_list)].copy()
710      
711      # Step 2: De-duplicate clinical trials
712      # Each row in ChEMBL represents one clinical trial. We need to avoid counting:
713      # - Same trial with different child molecules (salt forms) of the same parent
714      # - Same trial appearing in multiple mesh descendants
715      # De-duplicate by (parent_molregno, clinical_trial_id, mesh_heading) first
716      if 'clinical_trial_id' in disease_data.columns:
717         disease_data = disease_data.drop_duplicates(
718            subset=['parent_molregno', 'clinical_trial_id', 'mesh_heading', 'target_gene_name']
719         )
720      
721      # Step 3: Group by mesh_heading + parent_molregno + target to aggregate trials per mesh
722      # This intermediate step is needed for family filtering (which operates per mesh heading)
723      parent_per_mesh = disease_data.groupby(
724         ['mesh_heading', 'parent_molregno', 'target_gene_name'], 
725         as_index=False
726      ).agg(
727         max_phase=('phase', 'max'),  # Keep max phase for this parent in this mesh
728         chembl_id=('chembl_id', 'first')
729      )
730      
731      # Step 4: Identify approved (per mesh)
732      parent_per_mesh['approved'] = (parent_per_mesh['max_phase'] == 4)
733      
734      # Step 5: Calculate phase scores (per mesh)
735      parent_per_mesh['phase_scores'] = parent_per_mesh['max_phase'].apply(
736         lambda p: p if p != 4 else 0
737      )
738      
739      res = parent_per_mesh
740      
741      # Handle empty result case (no trials for this disease)
742      if len(res) == 0:
743         print(f"Warning: No data found for {mesh_heading}")
744         results[mesh_heading] = DataFrame(
745            columns=['target_gene_name', 'phase_scores', 'approved', 'clinical_scores']
746         )
747         continue
748
749      # Step 5: Filter over-represented gene families per drug across ALL mesh descendants (optional)
750      # This prevents multi-target drugs (e.g., multi-kinase inhibitors) from biasing results
751      # IMPORTANT: Filter at the parent mesh_heading level (not per descendant) to avoid
752      # keeping multiple representatives from different sub-indications
753      if filter_families:
754         unique_drugs = res['chembl_id'].drop_duplicates()
755         print(f"Filtering over-represented families for {len(unique_drugs)} drugs across all descendants...")
756         
757         filtered_results = []
758         for drug_id in unique_drugs:
759            # Extract all targets for this drug across ALL mesh descendants
760            drug_data = res[res['chembl_id'] == drug_id].copy()
761            
762            # Get unique mesh headings for this drug
763            unique_mesh = drug_data['mesh_heading'].unique()
764            
765            # Apply family filtering FOR EACH mesh heading separately
766            # This is critical because we need to filter each mesh heading's records independently
767            drug_filtered = []
768            for mesh in unique_mesh:
769               mesh_data = drug_data[drug_data['mesh_heading'] == mesh].copy()
770               
771               # Try filtering with the specific mesh first, fall back to parent if no data
772               filtered_mesh = filter_overrepresented_families(
773                  mesh_data, 
774                  drug_chembl_id=drug_id,
775                  mesh_heading=mesh,  # Use specific mesh heading
776                  lookup_table=lookup_table
777               )
778               
779               # If no filtering happened (no family data for specific mesh), try parent heading
780               if len(filtered_mesh) == len(mesh_data) and mesh != mesh_heading:
781                  filtered_mesh = filter_overrepresented_families(
782                     mesh_data, 
783                     drug_chembl_id=drug_id,
784                     mesh_heading=mesh_heading,  # Fall back to parent heading
785                     lookup_table=lookup_table
786                  )
787               
788               drug_filtered.append(filtered_mesh)
789            
790            # Combine all mesh headings for this drug
791            if len(drug_filtered) > 0:
792               filtered_results.append(pd_concat(drug_filtered, ignore_index=True))
793         
794         # Combine all filtered drug data
795         res = pd_concat(filtered_results, ignore_index=True)
796         print(f"After filtering: {len(res)} records, {res['target_gene_name'].nunique()} unique targets")
797      else:
798         print("Skipping family filtering (filter_families=False)")
799      
800      # Step 6: Use FILTERED data and go back to trial level for counting
801      # Important: Use 'res' (after family filtering) to preserve filtering results
802      # Get the filtered parent-target combinations
803      filtered_combinations = res[['parent_molregno', 'target_gene_name', 'chembl_id']].drop_duplicates()
804      
805      # Now go back to original trial data but only keep filtered combinations
806      # This preserves family filtering while counting ALL trials
807      if 'clinical_trial_id' in disease_data.columns:
808         # Merge to keep only filtered parent-target combinations
809         trials_filtered = disease_data.merge(
810            filtered_combinations,
811            on=['parent_molregno', 'target_gene_name'],
812            how='inner'
813         )
814         
815         # De-duplicate by clinical trial ID globally across mesh descendants
816         trials_global = trials_filtered.drop_duplicates(
817            subset=['parent_molregno', 'clinical_trial_id', 'target_gene_name']
818         ).copy()  # Create explicit copy to avoid SettingWithCopyWarning
819      else:
820         # Fallback: if no trial IDs, use the aggregated filtered data
821         trials_global = res.drop_duplicates(
822            subset=['parent_molregno', 'target_gene_name', 'max_phase']
823         ).copy()
824      
825      # Now aggregate all trials per target
826      trials_global['approved'] = (trials_global['phase'] == 4)
827      trials_global['phase_scores'] = trials_global['phase'].apply(
828         lambda p: p if p != 4 else 0
829      )
830      
831      print(f"After de-duplicating trials globally: {len(trials_global)} trial records, {trials_global['target_gene_name'].nunique()} unique targets")
832      
833      # Step 7: Count TRIALS per phase for each target
834      # Each clinical trial in a given phase counts towards that phase's total
835      phase_counts = trials_global.groupby('target_gene_name', as_index=False).agg(
836         phase_0=('phase', lambda x: (x == 0).sum()),
837         phase_1=('phase', lambda x: (x == 1).sum()),
838         phase_2=('phase', lambda x: (x == 2).sum()),
839         phase_3=('phase', lambda x: (x == 3).sum()),
840         phase_4=('phase', lambda x: (x == 4).sum())
841      )
842      
843      # Step 8: Aggregate ALL trial scores per target
844      # Sum all clinical trial phases (0-3 for non-approved, 0 for approved)
845      # Each trial contributes its phase score to the cumulative total
846      phase_scores_final = trials_global.groupby('target_gene_name', as_index=False).agg(
847         phase_scores=('phase_scores', 'sum')  # Sum ALL trial phase scores
848      )
849      
850      # Check if target has any approved drug (any trial in phase 4)
851      approved_final = trials_global.groupby('target_gene_name', as_index=False).agg(
852         approved=('approved', lambda x: x.any())  # True if ANY trial is phase 4
853      )
854      
855      # Merge all components
856      res_final = merge(phase_scores_final, approved_final, on='target_gene_name')
857      res_final = merge(res_final, phase_counts, on='target_gene_name')
858      
859      # Step 9: Calculate final clinical scores with approval bonus
860      # Formula: clinical_score = phase_scores + (20 if approved else 0)
861      res_final['clinical_scores'] = res_final['phase_scores'] + 20 * res_final['approved'].astype(int)
862      
863      # Reorder columns: target_gene_name, phase_0-4, phase_scores, approved, clinical_scores
864      column_order = ['target_gene_name', 'phase_0', 'phase_1', 'phase_2', 'phase_3', 'phase_4',
865                      'phase_scores', 'approved', 'clinical_scores']
866      res_final = res_final[column_order]
867      
868      # Sort by clinical score descending (highest validation first)
869      res_final.sort_values('clinical_scores', ascending=False, inplace=True)
870      res_final.reset_index(drop=True, inplace=True)
871      
872      results[mesh_heading] = res_final
873      print(f"Completed {mesh_heading}: {len(res_final)} targets")
874   
875   return results

Compute clinical validation scores for drug targets across multiple disease indications.

This is the core function of the Artemis clinical scoring module. It processes ChEMBL drug-target-disease relationships combined with clinical trial data to calculate comprehensive validation scores for each target gene. These scores quantify the strength of clinical evidence supporting each target, enabling data-driven target prioritization for drug discovery.

What This Function Does:

  • Loads drug molecule, target, and clinical trial data from ChEMBL
  • Expands diseases using MeSH hierarchies (e.g., "Lung Neoplasms" → all lung cancer subtypes)
  • Counts clinical trials at each phase (0-4) for each drug-target pair
  • Optionally filters out over-represented gene families to reduce bias
  • Aggregates evidence across all drugs targeting the same gene
  • Returns comprehensive target-level scores for each disease

Processing Workflow:

  1. Data Loading (once for all diseases):

    • Downloads pre-computed ChEMBL molecules.csv from S3
    • Contains drug-target-indication-trial relationships
    • Optional temporal filtering to recent trials
  2. Gene Family Analysis (optional, once for all diseases):

    • Downloads HGNC gene family data
    • Creates lookup table of drug-family representation percentages
    • Identifies multi-target drugs (e.g., multi-kinase inhibitors)
  3. Per-Disease Processing (for each disease in mesh_headings):

  • MeSH Expansion: Retrieves all descendant disease terms (e.g., "Lung Neoplasms" includes "Adenocarcinoma of Lung", "Small Cell Lung Carcinoma", etc.)

  • Trial De-duplication: Removes duplicate trials from:

    • Same trial appearing in multiple mesh descendants
    • Same trial with different molecular forms (salts) of the same parent drug
  • Family Filtering (optional): For each drug-disease combination:

    • Identifies over-represented gene families (>40% of family targeted)
    • Keeps one representative target per family (first alphabetically)
    • Eliminates bias from multi-target drugs
  • Trial Counting: Counts trials per phase for each target:

    • Phase 0-3: Experimental/developmental trials
    • Phase 4: Approved drugs (post-marketing surveillance)
  • Score Aggregation: For each target gene:

    • Sum all trial phases (0-3) across all drugs → phase_scores
    • Check if any drug is approved (phase 4) → approved (boolean)
    • Calculate final score: clinical_scores = phase_scores + (20 if approved else 0)
  • Output: DataFrame with one row per target, sorted by clinical_scores (descending)

Scoring System Details:

Each target receives a clinical_scores value based on:

  1. Phase Scores (0-3 points per trial):
  • Phase 0: 0 points (exploratory studies)
  • Phase 1: 1 point (safety/dosing in small groups)
  • Phase 2: 2 points (efficacy in larger groups)
  • Phase 3: 3 points (large-scale confirmation)
  • Phase 4: 0 points (counted separately as "approved")
  1. Approval Bonus: +20 points if target has any approved drug

  2. Multi-Drug Aggregation: If multiple drugs target the same gene, all their phase scores sum

  3. Multi-Trial Aggregation: Each distinct clinical trial contributes its phase score

Example Scoring:

  • Target A: 2 Phase 2 trials + 1 Phase 3 trial = 2+2+3 = 7 points
  • Target B: 1 Phase 3 trial + 1 approved drug = 3+20 = 23 points
  • Target C: 3 drugs with Phase 2 trials + approved = (2+2+2)+20 = 26 points

Why Family Filtering Matters:

Multi-target drugs (e.g., sunitinib targeting 8 RTKs) can artificially inflate scores for specific gene families. Without filtering, all 8 RTKs receive high scores from the same drug, biasing results toward that family. With filtering (default), only 1 RTK representative is kept, producing unbiased scores that better reflect biological diversity.

Parameters
  • mesh_headings: List of MeSH disease headings to analyze. Standard MeSH terms like
    • 'Breast Neoplasms' (breast cancer)
    • 'Lung Neoplasms' (lung cancer)
    • 'Diabetes Mellitus, Type 2' (type 2 diabetes)
    • 'Cardiovascular Diseases' (heart disease)

Each heading is automatically expanded to include all MeSH descendant terms. Use exact MeSH terminology for best results.

  • chembl_version: ChEMBL database version to use. Versions correspond to data release dates
    • '36' (default): Data through 2024
    • '35': Data through 2023

See https://www.ebi.ac.uk/chembl/ for version details.

  • trials_only_last_n_years: Temporal filter for recent trials only. If provided
    • Filters to trials registered in last N years
    • Year inferred from ClinicalTrials.gov NCT IDs
    • Useful for identifying emerging targets

Examples:

  • 6: Last 6 years (captures recent development)
  • 10: Last decade
  • None (default): All historical trials
  • filter_families: Whether to apply gene family filtering to reduce bias

    • True (default): Filter over-represented families (recommended for unbiased target prioritization)
    • False: Include all targets without filtering (use when family representation is biologically relevant)
Returns

Dictionary mapping each MeSH heading to its results DataFrame.

Dictionary Structure:

  • Keys: MeSH disease headings (str) - same as input mesh_headings
  • Values: DataFrames with target-level clinical scores

DataFrame Columns:

  • target_gene_name (str): HGNC gene symbol (e.g., 'EGFR', 'TP53')
  • phase_0 (int): Count of phase 0 trials for this target
  • phase_1 (int): Count of phase 1 trials for this target
  • phase_2 (int): Count of phase 2 trials for this target
  • phase_3 (int): Count of phase 3 trials for this target
  • phase_4 (int): Count of phase 4 (approved) drugs for this target
  • phase_scores (int): Sum of phase_1 + 2×phase_2 + 3×phase_3
  • approved (bool): True if phase_4 > 0 (any approved drug exists)
  • clinical_scores (int): phase_scores + (20 if approved else 0)

DataFrames are sorted by clinical_scores descending (highest validation first).

Efficiency Optimization: ChEMBL data and gene family lookup tables are loaded once and shared across all diseases. Processing 7 diseases takes ~5-10 minutes total, not per disease.

MeSH Hierarchy Expansion: Diseases are automatically expanded to include all subtypes. For example, "Breast Neoplasms" includes "Ductal Carcinoma", "Lobular Carcinoma", etc. This ensures comprehensive coverage of clinical evidence.

Trial De-duplication: The function carefully de-duplicates clinical trials to avoid double-counting. Trials are unique by (drug, NCT ID, target, disease) to ensure accurate counts.

AWS S3 Access Required: This function reads ChEMBL and MeSH data from the alethiotx-artemis S3 bucket. Configure AWS credentials via environment variables or aws configure before calling.

Memory Usage: ChEMBL v36 molecules.csv is ~500MB-1GB in memory. Processing multiple diseases simultaneously requires sufficient RAM. For memory-constrained environments, process diseases in smaller batches.

Processing Time: Full computation takes 5-10 minutes for 7 diseases with family filtering. Set filter_families=False to reduce runtime by ~50% (but scores may be biased).

Family Filtering Off: When filter_families=False, results may show artificial enrichment for gene families with multi-target drugs. Use this option only when family-level representation is biologically meaningful for your analysis.

Examples

Basic usage - single disease::

>>> from alethiotx.artemis.clinical import compute
>>> results = compute(['Lung Neoplasms'])
>>> lung_targets = results['Lung Neoplasms']
>>> print(f"Found {len(lung_targets)} lung cancer targets")
>>> print(lung_targets.head())
   target_gene_name  phase_0  phase_1  phase_2  phase_3  phase_4  phase_scores  approved  clinical_scores
0  EGFR             0        5        15       8        3         45           True      65
1  ALK              0        2        8        6        2         32           True      52

Multiple diseases simultaneously::

>>> diseases = ['Breast Neoplasms', 'Lung Neoplasms', 'Prostatic Neoplasms']
>>> results = compute(diseases, chembl_version='36', filter_families=True)
>>> for disease, df in results.items():
...     approved_count = df['approved'].sum()
...     print(f"{disease}: {len(df)} total targets, {approved_count} approved")

Recent trials only (emerging targets)::

>>> # Focus on targets with recent clinical activity (last 6 years)
>>> results = compute(
...     ['Lung Neoplasms'],
...     trials_only_last_n_years=6,
...     filter_families=True
... )
>>> recent_lung = results['Lung Neoplasms']
>>> print("Emerging lung cancer targets:", recent_lung['target_gene_name'].tolist()[:10])

Disable family filtering for comprehensive view::

>>> # Include all targets without filtering (may show family bias)
>>> results = compute(['Lung Neoplasms'], filter_families=False)
>>> lung_all = results['Lung Neoplasms']
>>> print(f"All targets (no filtering): {len(lung_all)}")

Filter to approved targets only::

>>> results = compute(['Diabetes Mellitus, Type 2'])
>>> diabetes = results['Diabetes Mellitus, Type 2']
>>> approved_diabetes = diabetes[diabetes['approved'] == True]
>>> print(f"Approved diabetes targets: {len(approved_diabetes)}")
>>> print(approved_diabetes[['target_gene_name', 'clinical_scores']].head())

Compare targets across diseases::

>>> diseases = ['Breast Neoplasms', 'Lung Neoplasms', 'Prostatic Neoplasms']
>>> results = compute(diseases)
>>> target = 'EGFR'
>>> for disease in diseases:
...     df = results[disease]
...     if target in df['target_gene_name'].values:
...         score = df[df['target_gene_name'] == target]['clinical_scores'].iloc[0]
...         print(f"{disease}: {target} = {score} points")

Save results to CSV files::

>>> results = compute(['Breast Neoplasms', 'Lung Neoplasms'])
>>> for disease, df in results.items():
...     filename = disease.replace(' ', '_').replace(',', '') + '.csv'
...     df.to_csv(filename, index=False)
...     print(f"Saved {filename}")

Batch processing for all major diseases::

>>> all_diseases = [
...     'Breast Neoplasms',
...     'Lung Neoplasms', 
...     'Prostatic Neoplasms',
...     'Skin Neoplasms',
...     'Intestinal Neoplasms',
...     'Diabetes Mellitus, Type 2',
...     'Cardiovascular Diseases'
... ]
>>> results = compute(all_diseases, trials_only_last_n_years=6)
>>> print(f"Processed {len(results)} diseases in one batch")

seealso:

def load(date='2025-12-08'):
877def load(date = '2025-12-08'):
878   """
879   Load pre-computed clinical validation scores for seven major disease types from S3 storage.
880
881   This convenience function retrieves pre-calculated clinical target scores that have been
882   computed using :func:`compute` and stored in the Alethio Therapeutics public S3 bucket.
883   Each disease has its own CSV file containing target-level clinical validation scores.
884   
885   **Disease Coverage:**
886   
887   Returns scores for these seven diseases (in order):
888   
889   1. Breast cancer (Breast Neoplasms)
890   2. Lung cancer (Lung Neoplasms)
891   3. Prostate cancer (Prostatic Neoplasms)
892   4. Melanoma (Skin Neoplasms)
893   5. Bowel/colorectal cancer (Intestinal Neoplasms)
894   6. Type 2 diabetes (Diabetes Mellitus, Type 2)
895   7. Cardiovascular disease (Cardiovascular Diseases)
896
897   :param date: Date stamp identifying which pre-computed dataset to load. Format: ``YYYY-MM-DD``.
898                Different dates represent different ChEMBL versions, temporal filters, or
899                computation parameters. Defaults to ``2025-12-08``.
900   :type date: str, optional
901   :return: Tuple of 7 DataFrames containing clinical scores for each disease. Each DataFrame has:
902            
903   **Columns:**
904   
905   - ``Target Gene`` (str): HGNC gene symbol
906   - ``phase_0`` through ``phase_4`` (int): Count of trials in each phase
907   - ``Phase Score`` (int): Sum of trial phases (0-3)
908   - ``approved`` (bool): Whether target has any approved (phase 4) drug
909   - ``Clinical Score`` (int): Total validation score (Phase Score + 20 if approved)
910   
911   **Order:** 
912   
913   (breast, lung, prostate, melanoma, bowel, diabetes, cardiovascular)
914
915   :rtype: tuple[DataFrame, DataFrame, DataFrame, DataFrame, DataFrame, DataFrame, DataFrame]
916   :raises FileNotFoundError: If CSV files don't exist at the specified S3 paths for the given date
917   :raises ValueError: If CSV files are malformed or missing required columns
918   :raises botocore.exceptions.NoCredentialsError: If AWS credentials are not configured
919
920   .. note::
921      **S3 Storage Structure**: Files are located at:
922      ``s3://alethiotx-artemis/data/clinical_scores/{date}/{disease}.csv``
923      
924      Where ``{disease}`` is: breast, lung, prostate, melanoma, bowel, diabetes, or cardiovascular
925
926   .. note::
927      **AWS Configuration**: Requires AWS credentials with read access to the ``alethiotx-artemis``
928      S3 bucket. Configure via environment variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``)
929      or AWS CLI (``aws configure``).
930
931   .. warning::
932      Verify the date exists in S3 before calling. Available dates depend on when scores were
933      computed and uploaded. Contact Alethio Therapeutics for a list of available dates.
934
935   .. warning::
936      Each DataFrame can be large (thousands of rows). Consider filtering to specific targets
937      or approved-only drugs after loading to reduce memory usage.
938
939   **Examples**
940   
941   Load latest pre-computed scores::
942   
943      >>> from alethiotx.artemis.clinical import load
944      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load(date='2025-12-08')
945      >>> print(f"Breast cancer: {len(breast)} targets")
946      >>> print(breast.head())
947   
948   Load and filter to approved targets only::
949   
950      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
951      >>> breast_approved = breast[breast['approved'] == True]
952      >>> print(f"Approved breast cancer targets: {len(breast_approved)}")
953   
954   Compare scores across diseases for a specific gene::
955   
956      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
957      >>> diseases = [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
958      >>> names = ['Breast', 'Lung', 'Prostate', 'Melanoma', 'Bowel', 'Diabetes', 'Cardio']
959      >>> target = 'EGFR'
960      >>> for name, df in zip(names, diseases):
961      ...     if target in df['Target Gene'].values:
962      ...         score = df[df['Target Gene'] == target]['Clinical Score'].iloc[0]
963      ...         print(f"{name}: {score}")
964   
965   Load older dataset version::
966   
967      >>> # Load scores computed on an earlier date
968      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load(date='2025-11-11')
969   
970   .. seealso::
971      - :func:`compute`: Function used to generate these pre-computed scores
972      - :func:`approved`: Filter loaded scores to approved targets only
973   """
974   breast = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/breast.csv")
975   lung = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/lung.csv")
976   prostate = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/prostate.csv")
977   melanoma = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/melanoma.csv")
978   bowel = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/bowel.csv")
979   diabetes = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/diabetes.csv")
980   cardiovascular = read_csv("s3://alethiotx-artemis/data/clinical_scores/" + date + "/cardiovascular.csv")
981
982   return(breast, lung, prostate, melanoma, bowel, diabetes, cardiovascular)

Load pre-computed clinical validation scores for seven major disease types from S3 storage.

This convenience function retrieves pre-calculated clinical target scores that have been computed using compute() and stored in the Alethio Therapeutics public S3 bucket. Each disease has its own CSV file containing target-level clinical validation scores.

Disease Coverage:

Returns scores for these seven diseases (in order):

  1. Breast cancer (Breast Neoplasms)
  2. Lung cancer (Lung Neoplasms)
  3. Prostate cancer (Prostatic Neoplasms)
  4. Melanoma (Skin Neoplasms)
  5. Bowel/colorectal cancer (Intestinal Neoplasms)
  6. Type 2 diabetes (Diabetes Mellitus, Type 2)
  7. Cardiovascular disease (Cardiovascular Diseases)
Parameters
  • date: Date stamp identifying which pre-computed dataset to load. Format: YYYY-MM-DD. Different dates represent different ChEMBL versions, temporal filters, or computation parameters. Defaults to 2025-12-08.
Returns

Tuple of 7 DataFrames containing clinical scores for each disease. Each DataFrame has:

Columns:

  • Target Gene (str): HGNC gene symbol
  • phase_0 through phase_4 (int): Count of trials in each phase
  • Phase Score (int): Sum of trial phases (0-3)
  • approved (bool): Whether target has any approved (phase 4) drug
  • Clinical Score (int): Total validation score (Phase Score + 20 if approved)

Order:

(breast, lung, prostate, melanoma, bowel, diabetes, cardiovascular)

Raises
  • FileNotFoundError: If CSV files don't exist at the specified S3 paths for the given date
  • ValueError: If CSV files are malformed or missing required columns
  • botocore.exceptions.NoCredentialsError: If AWS credentials are not configured

S3 Storage Structure: Files are located at: s3://alethiotx-artemis/data/clinical_scores/{date}/{disease}.csv

Where {disease} is: breast, lung, prostate, melanoma, bowel, diabetes, or cardiovascular

AWS Configuration: Requires AWS credentials with read access to the alethiotx-artemis S3 bucket. Configure via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or AWS CLI (aws configure).

Verify the date exists in S3 before calling. Available dates depend on when scores were computed and uploaded. Contact Alethio Therapeutics for a list of available dates.

Each DataFrame can be large (thousands of rows). Consider filtering to specific targets or approved-only drugs after loading to reduce memory usage.

Examples

Load latest pre-computed scores::

>>> from alethiotx.artemis.clinical import load
>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load(date='2025-12-08')
>>> print(f"Breast cancer: {len(breast)} targets")
>>> print(breast.head())

Load and filter to approved targets only::

>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> breast_approved = breast[breast['approved'] == True]
>>> print(f"Approved breast cancer targets: {len(breast_approved)}")

Compare scores across diseases for a specific gene::

>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> diseases = [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
>>> names = ['Breast', 'Lung', 'Prostate', 'Melanoma', 'Bowel', 'Diabetes', 'Cardio']
>>> target = 'EGFR'
>>> for name, df in zip(names, diseases):
...     if target in df['Target Gene'].values:
...         score = df[df['Target Gene'] == target]['Clinical Score'].iloc[0]
...         print(f"{name}: {score}")

Load older dataset version::

>>> # Load scores computed on an earlier date
>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load(date='2025-11-11')

seealso:

  • compute(): Function used to generate these pre-computed scores
  • approved(): Filter loaded scores to approved targets only
def lookup_drug_family_representation(chembl: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
  8def lookup_drug_family_representation(chembl: DataFrame) -> DataFrame:
  9   """
 10   Create a comprehensive lookup table mapping drug-disease-family combinations to gene family representation.
 11   
 12   This function analyzes drug-target relationships from ChEMBL clinical trial data to identify potential
 13   bias from drugs that target many genes within the same family. For example, multi-kinase inhibitors
 14   targeting 10 out of 15 kinases in a family would represent 67% of that family, potentially inflating
 15   the importance of kinase targets in clinical scoring.
 16   
 17   The lookup table enables downstream filtering of over-represented families to produce unbiased
 18   target prioritization scores.
 19   
 20   **Processing Workflow:**
 21   
 22   1. **Download HGNC Data**: Retrieves complete gene family definitions from HGNC
 23   2. **Extract Relationships**: Identifies all unique drug-target-disease combinations from ChEMBL
 24   3. **Family Mapping**: Maps each drug target to its HGNC gene family (if assigned)
 25   4. **Calculate Representation**: For each drug-disease-family combination, computes:
 26      
 27      - Number of family genes targeted by the drug
 28      - Total genes in the family
 29      - Percentage representation (targets/total * 100)
 30   
 31   5. **Return Lookup**: Creates sorted lookup table for efficient querying
 32   
 33   **Why This Matters:**
 34   
 35   When a drug targets multiple members of the same gene family (e.g., multi-kinase inhibitors,
 36   pan-HDAC inhibitors), each family member contributes to clinical scores independently. This
 37   artificially inflates the importance of that family in target prioritization. By quantifying
 38   family representation, we can identify and filter these cases to prevent bias.
 39   
 40   :param chembl: ChEMBL molecule data containing drug-target-indication relationships.
 41                  Must include columns: ``chembl_id``, ``target_gene_name``, ``mesh_heading``
 42   :type chembl: DataFrame
 43   :return: Lookup table with one row per drug-disease-family combination. 
 44   
 45   **Columns:**
 46      
 47      - ``drug_chembl_id`` (str): ChEMBL identifier for the drug (e.g., 'CHEMBL123')
 48      - ``mesh_heading`` (str): MeSH disease term (e.g., 'Lung Neoplasms')
 49      - ``family_id`` (int): HGNC gene family identifier
 50      - ``family_abbreviation`` (str): Short family name (e.g., 'RTK' for receptor tyrosine kinases)
 51      - ``family_name`` (str): Full descriptive family name (e.g., 'Receptor tyrosine kinases')
 52      - ``total_genes_in_family`` (int): Total number of genes in this HGNC family
 53      - ``targets_in_family`` (int): Number of family genes targeted by this drug for this disease
 54      - ``target_genes_in_family`` (str): Comma-separated list of targeted gene symbols
 55      - ``pct_represented`` (float): Percentage of family genes targeted (0.0-100.0)
 56      
 57      Sorted by: drug_chembl_id, mesh_heading, then pct_represented (descending)
 58      
 59   :rtype: DataFrame
 60   
 61   .. note::
 62      Only includes targets with HGNC family assignments. Targets without family data are
 63      excluded from the lookup table (they pass through unfiltered in downstream processing).
 64   
 65   .. note::
 66      This function downloads fresh HGNC data on each call (~10-30 seconds). For production
 67      workflows processing multiple diseases, call this function once and reuse the lookup table.
 68   
 69   .. warning::
 70      Requires internet connection to download HGNC gene family data from public repositories.
 71      Ensure network access to HGNC/Google Cloud Storage endpoints.
 72   
 73   **Examples**
 74   
 75   Basic usage::
 76   
 77      >>> from alethiotx.artemis.chembl import molecules
 78      >>> chembl_data = molecules(version='36')
 79      >>> lookup = lookup_drug_family_representation(chembl_data)
 80      >>> print(f"Created lookup with {len(lookup)} drug-disease-family combinations")
 81   
 82   Find drugs over-representing kinase families::
 83   
 84      >>> kinase_overrep = lookup[
 85      ...     (lookup['family_name'].str.contains('kinase', case=False)) &
 86      ...     (lookup['pct_represented'] > 50)
 87      ... ]
 88      >>> print(kinase_overrep[['drug_chembl_id', 'family_name', 'pct_represented']].head())
 89   
 90   Identify drugs with many targets in small families::
 91   
 92      >>> small_families = lookup[
 93      ...     (lookup['total_genes_in_family'] < 10) &
 94      ...     (lookup['targets_in_family'] >= 4)
 95      ... ]
 96      >>> for _, row in small_families.iterrows():
 97      ...     print(f"{row['drug_chembl_id']}: {row['targets_in_family']}/{row['total_genes_in_family']} "
 98      ...           f"of {row['family_name']}")
 99   
100   .. seealso::
101      - :func:`filter_overrepresented_families`: Uses this lookup table to filter biased targets
102      - :func:`compute`: Integrates this lookup for unbiased clinical score calculation
103   """
104   # Download HGNC gene family data
105   gene_has_family, family, hgnc_data = hgnc_download()
106   family_summary, gene_family_mapping = hgnc_process(gene_has_family, family, hgnc_data)
107   
108   # Extract unique drug-target-disease combinations from ChEMBL
109   # Use vectorized operations for better performance
110   valid_rows = chembl[['chembl_id', 'target_gene_name', 'mesh_heading']].copy()
111   valid_rows = valid_rows[
112      notna(valid_rows['target_gene_name']) & notna(valid_rows['mesh_heading'])
113   ]
114   valid_rows.rename(columns={'chembl_id': 'drug_chembl_id'}, inplace=True)
115   targets_df = valid_rows.drop_duplicates()
116   
117   print(f"\nTotal drug-target-disease combinations: {len(targets_df)}")
118   print(f"Unique targets: {targets_df['target_gene_name'].nunique()}")
119   print(f"Unique drugs: {targets_df['drug_chembl_id'].nunique()}")
120   
121   # Map targets to their HGNC gene families
122   targets_with_families = targets_df.merge(
123      gene_family_mapping[['symbol', 'family_id', 'abbreviation', 'name_family']],
124      left_on='target_gene_name',
125      right_on='symbol',
126      how='left'
127   )
128   
129   # Report mapping success rate
130   with_family = targets_with_families['family_id'].notna().sum()
131   without_family = targets_with_families['family_id'].isna().sum()
132   print(f"  Targets with family: {with_family}, without family: {without_family}")
133   
134   # Keep only targets with family assignments for representation calculation
135   targets_with_families = targets_with_families[
136      targets_with_families['family_id'].notna()
137   ].copy()
138   
139   # Calculate family representation metrics
140   # Group by drug-disease-family to count targets per family
141   family_counts = targets_with_families.groupby(
142      ['drug_chembl_id', 'mesh_heading', 'family_id'],
143      as_index=False
144   ).agg(
145      targets_in_family=('target_gene_name', 'nunique'),
146      target_genes_in_family=('symbol', lambda x: ', '.join(sorted(x.unique())))
147   )
148   
149   # Add family metadata (name, abbreviation, total gene count)
150   family_counts = family_counts.merge(
151      family_summary[['id', 'abbreviation', 'name', 'total_genes_in_family']],
152      left_on='family_id',
153      right_on='id',
154      how='left'
155   )
156   
157   # Calculate percentage representation: (targets in family / total genes in family) * 100
158   family_counts['pct_represented'] = (
159      100 * family_counts['targets_in_family'] / family_counts['total_genes_in_family']
160   ).round(1)
161   
162   # Select and organize final columns
163   result = family_counts[[
164      'drug_chembl_id',
165      'mesh_heading',
166      'family_id',
167      'abbreviation',
168      'name',
169      'total_genes_in_family',
170      'targets_in_family',
171      'target_genes_in_family',
172      'pct_represented'
173   ]].copy()
174   
175   # Rename for clarity
176   result.rename(columns={
177      'abbreviation': 'family_abbreviation',
178      'name': 'family_name'
179   }, inplace=True)
180   
181   # Sort by drug, disease, then highest representation first
182   result.sort_values(
183      ['drug_chembl_id', 'mesh_heading', 'pct_represented'],
184      ascending=[True, True, False],
185      inplace=True
186   )
187   
188   return result

Create a comprehensive lookup table mapping drug-disease-family combinations to gene family representation.

This function analyzes drug-target relationships from ChEMBL clinical trial data to identify potential bias from drugs that target many genes within the same family. For example, multi-kinase inhibitors targeting 10 out of 15 kinases in a family would represent 67% of that family, potentially inflating the importance of kinase targets in clinical scoring.

The lookup table enables downstream filtering of over-represented families to produce unbiased target prioritization scores.

Processing Workflow:

  1. Download HGNC Data: Retrieves complete gene family definitions from HGNC
  2. Extract Relationships: Identifies all unique drug-target-disease combinations from ChEMBL
  3. Family Mapping: Maps each drug target to its HGNC gene family (if assigned)
  4. Calculate Representation: For each drug-disease-family combination, computes:

    • Number of family genes targeted by the drug
    • Total genes in the family
    • Percentage representation (targets/total * 100)
  5. Return Lookup: Creates sorted lookup table for efficient querying

Why This Matters:

When a drug targets multiple members of the same gene family (e.g., multi-kinase inhibitors, pan-HDAC inhibitors), each family member contributes to clinical scores independently. This artificially inflates the importance of that family in target prioritization. By quantifying family representation, we can identify and filter these cases to prevent bias.

Parameters
  • chembl: ChEMBL molecule data containing drug-target-indication relationships. Must include columns: chembl_id, target_gene_name, mesh_heading
Returns

Lookup table with one row per drug-disease-family combination.

Columns:

  • drug_chembl_id (str): ChEMBL identifier for the drug (e.g., 'CHEMBL123')
  • mesh_heading (str): MeSH disease term (e.g., 'Lung Neoplasms')
  • family_id (int): HGNC gene family identifier
  • family_abbreviation (str): Short family name (e.g., 'RTK' for receptor tyrosine kinases)
  • family_name (str): Full descriptive family name (e.g., 'Receptor tyrosine kinases')
  • total_genes_in_family (int): Total number of genes in this HGNC family
  • targets_in_family (int): Number of family genes targeted by this drug for this disease
  • target_genes_in_family (str): Comma-separated list of targeted gene symbols
  • pct_represented (float): Percentage of family genes targeted (0.0-100.0)

    Sorted by: drug_chembl_id, mesh_heading, then pct_represented (descending)

Only includes targets with HGNC family assignments. Targets without family data are excluded from the lookup table (they pass through unfiltered in downstream processing).

This function downloads fresh HGNC data on each call (~10-30 seconds). For production workflows processing multiple diseases, call this function once and reuse the lookup table.

Requires internet connection to download HGNC gene family data from public repositories. Ensure network access to HGNC/Google Cloud Storage endpoints.

Examples

Basic usage::

>>> from alethiotx.artemis.chembl import molecules
>>> chembl_data = molecules(version='36')
>>> lookup = lookup_drug_family_representation(chembl_data)
>>> print(f"Created lookup with {len(lookup)} drug-disease-family combinations")

Find drugs over-representing kinase families::

>>> kinase_overrep = lookup[
...     (lookup['family_name'].str.contains('kinase', case=False)) &
...     (lookup['pct_represented'] > 50)
... ]
>>> print(kinase_overrep[['drug_chembl_id', 'family_name', 'pct_represented']].head())

Identify drugs with many targets in small families::

>>> small_families = lookup[
...     (lookup['total_genes_in_family'] < 10) &
...     (lookup['targets_in_family'] >= 4)
... ]
>>> for _, row in small_families.iterrows():
...     print(f"{row['drug_chembl_id']}: {row['targets_in_family']}/{row['total_genes_in_family']} "
...           f"of {row['family_name']}")

seealso:

def filter_overrepresented_families( targets_df: pandas.core.frame.DataFrame, drug_chembl_id: str, mesh_heading: str, lookup_table: pandas.core.frame.DataFrame, pct_threshold: float = 40.0, small_family_threshold: int = 5, min_targets_threshold: int = 3) -> pandas.core.frame.DataFrame:
190def filter_overrepresented_families(
191   targets_df: DataFrame,
192   drug_chembl_id: str,
193   mesh_heading: str,
194   lookup_table: DataFrame,
195   pct_threshold: float = 40.0,
196   small_family_threshold: int = 5,
197   min_targets_threshold: int = 3
198) -> DataFrame:
199   """
200   Filter over-represented gene families from drug targets to eliminate family-based bias in clinical scoring.
201   
202   Multi-target drugs (e.g., multi-kinase inhibitors like sunitinib) often target multiple members of the
203   same gene family. When computing clinical validation scores, each family member contributes independently,
204   artificially inflating the perceived importance of that family. This function identifies over-represented
205   families and keeps only one representative target per family to produce unbiased scores.
206   
207   **Why Filtering is Needed:**
208   
209   Consider sunitinib, which targets 8 receptor tyrosine kinases (RTKs). Without filtering, all 8 RTKs
210   contribute to clinical scores, making RTKs appear highly validated. With filtering, we keep only 1
211   representative RTK (e.g., FLT3 alphabetically), preventing the family from dominating results.
212   
213   **Filtering Logic:**
214   
215   A gene family is considered "over-represented" and filtered if **ALL** of these conditions are met:
216   
217   1. **Sufficient targets**: Drug targets > ``min_targets_threshold`` genes in the family (default: >3)
218   2. **Size-adjusted criteria** (either condition triggers filtering):
219      
220      a. **Large family with high %**: Family size ≥ ``small_family_threshold`` (default: ≥5)
221         AND representation > ``pct_threshold``% (default: >40%)
222      b. **Absolute threshold**: Drug targets ≥ 4 genes in family (regardless of family size)
223   
224   **Exemptions from Filtering:**
225   
226   - **Small families** (<5 genes by default): Never filtered to preserve biological specificity
227     (e.g., the 4-gene HDAC class IIA family)
228   - **Low target count** (≤3 targets by default): Insufficient redundancy to justify filtering
229   - **Targets without family assignments**: Pass through unfiltered
230   
231   **Selection of Representative:**
232   
233   For each over-represented family, the first target alphabetically is kept as the representative.
234   All other family members are excluded from the targets DataFrame.
235   
236   :param targets_df: DataFrame of drug targets for a specific drug-disease combination.
237                      Must contain a ``target_gene_name`` column with gene symbols.
238   :type targets_df: DataFrame
239   :param drug_chembl_id: ChEMBL identifier for the drug being analyzed (e.g., 'CHEMBL1200')
240   :type drug_chembl_id: str
241   :param mesh_heading: MeSH disease heading for this drug-disease combination (e.g., 'Lung Neoplasms')
242   :type mesh_heading: str
243   :param lookup_table: Pre-computed family representation lookup from :func:`lookup_drug_family_representation`.
244                        Used to identify which families are over-represented for this drug-disease pair.
245   :type lookup_table: DataFrame
246   :param pct_threshold: Percentage threshold for filtering large families (0-100). Families with
247                         representation above this threshold are filtered (if other criteria met).
248                         Lower values = more aggressive filtering. Defaults to 40.0.
249   :type pct_threshold: float, optional
250   :param small_family_threshold: Minimum family size for percentage-based filtering. Families smaller
251                                  than this are exempt from filtering to preserve biological specificity.
252                                  Defaults to 5.
253   :type small_family_threshold: int, optional
254   :param min_targets_threshold: Minimum number of targets in a family to trigger filtering. Families with
255                                 fewer targets lack sufficient redundancy to justify filtering. Defaults to 3.
256   :type min_targets_threshold: int, optional
257   :return: Filtered DataFrame with same columns as input. Over-represented family members (except
258            the first alphabetical representative) are removed. Returns input unchanged if no filtering
259            is needed or if drug/disease not found in lookup table.
260   :rtype: DataFrame
261   
262   .. note::
263      **Alphabetical Selection**: Using the first alphabetical target as representative ensures
264      deterministic, reproducible filtering across runs.
265   
266   .. note::
267      **No Family Assignment**: Targets that don't map to HGNC gene families in the lookup table
268      pass through unfiltered. These are typically novel targets or non-gene targets.
269   
270   .. warning::
271      If ``drug_chembl_id`` and ``mesh_heading`` combination is not found in the lookup table,
272      the function returns the input unfiltered and prints a warning. This can occur if the
273      drug has no targets with family assignments.
274   
275   .. warning::
276      **Aggressive Filtering**: Lowering ``pct_threshold`` or ``min_targets_threshold`` makes
277      filtering more aggressive but may remove biologically important targets. Default values
278      balance bias reduction with information retention.
279   
280   **Examples**
281   
282   Basic usage with default thresholds::
283   
284      >>> lookup = lookup_drug_family_representation(chembl_data)
285      >>> filtered = filter_overrepresented_families(
286      ...     targets_df=drug_targets,
287      ...     drug_chembl_id='CHEMBL1200',  # Sunitinib
288      ...     mesh_heading='Renal Cell Carcinoma',
289      ...     lookup_table=lookup
290      ... )
291      >>> print(f"Filtered {len(drug_targets) - len(filtered)} targets")
292   
293   More aggressive filtering (lower thresholds)::
294   
295      >>> filtered = filter_overrepresented_families(
296      ...     targets_df,
297      ...     drug_chembl_id='CHEMBL123',
298      ...     mesh_heading='Lung Neoplasms',
299      ...     lookup_table=lookup,
300      ...     pct_threshold=25.0,  # Filter families >25% represented
301      ...     min_targets_threshold=2  # Filter families with >2 targets
302      ... )
303   
304   Preserve small families (e.g., HDACs) from filtering::
305   
306      >>> # Don't filter families with <8 genes
307      >>> filtered = filter_overrepresented_families(
308      ...     targets_df,
309      ...     drug_chembl_id='CHEMBL123',
310      ...     mesh_heading='Lymphoma',
311      ...     lookup_table=lookup,
312      ...     small_family_threshold=8
313      ... )
314   
315   Inspect which families were filtered::
316   
317      >>> original_targets = set(drug_targets['target_gene_name'])
318      >>> filtered_targets = set(filtered['target_gene_name'])
319      >>> removed = original_targets - filtered_targets
320      >>> print(f"Removed targets: {removed}")
321   
322   .. seealso::
323      - :func:`lookup_drug_family_representation`: Creates the required lookup table
324      - :func:`compute`: Integrates this filtering into the clinical score calculation pipeline
325   """
326   # Query lookup table for this drug's family representation
327   drug_families = lookup_table[
328      (lookup_table['drug_chembl_id'] == drug_chembl_id) &
329      (lookup_table['mesh_heading'] == mesh_heading)
330   ].copy()
331   
332   # Handle case where drug has no family data
333   if len(drug_families) == 0:
334      print(f"Warning: No family data found for drug {drug_chembl_id} and mesh {mesh_heading}, skipping filtering")
335      return targets_df
336   
337   # Identify over-represented families based on thresholds
338   # Family must be: 
339   # (1) large enough (≥ small_family_threshold) AND 
340   # (2) has enough targets from this drug (> min_targets_threshold) AND
341   # (3) EITHER highly represented (> pct_threshold%) OR has many absolute targets
342   #
343   # The last condition handles two cases:
344   # - High percentage: drug targets most of a small/medium family (e.g., 80% of 10 genes)
345   # - Many targets: drug targets a subfamily within a large family (e.g., 4 NDUFAF genes 
346   #   within a 72-gene family, which is only 5.6% but still represents redundancy)
347   #
348   # NOTE: When absolute threshold is met (targets >= 4), bypass small family check
349   # to handle cases like HDAC class IIA (4 genes total, all 4 targeted = 100%)
350   overrep_families = drug_families[
351      (drug_families['targets_in_family'] > min_targets_threshold) &
352      (
353         # Case 1: Large family with high percentage representation
354         (
355            (drug_families['total_genes_in_family'] >= small_family_threshold) &
356            (drug_families['pct_represented'] > pct_threshold)
357         ) |
358         # Case 2: Absolute threshold met (bypass small family check)
359         (drug_families['targets_in_family'] >= 4)
360      )
361   ]
362   
363   if len(overrep_families) > 0:
364      # Report filtering context
365      print(f"Found {len(overrep_families)} over-represented families for drug {drug_chembl_id} in {mesh_heading} (>{pct_threshold}%)")
366      
367      # Extract and deduplicate target lists from over-represented families
368      # Use list comprehension for better performance than iterrows()
369      all_family_targets = [
370         row['target_genes_in_family'].split(', ')
371         for _, row in overrep_families.iterrows()
372      ]
373      
374      # Keep first alphabetical target from each family as representative
375      targets_to_keep = [
376         sorted(targets)[0] for targets in all_family_targets
377      ]
378      
379      # Build exclusion list (all family targets except the kept representatives)
380      targets_to_exclude = [
381         target 
382         for targets in all_family_targets 
383         for target in targets 
384         if target not in targets_to_keep
385      ]
386      
387      print(f"  Excluding {len(targets_to_exclude)} targets, keeping {len(targets_to_keep)} (first alphabetical from each family)")
388      
389      # Apply filter and return
390      return targets_df[
391         ~targets_df['target_gene_name'].isin(targets_to_exclude)
392      ].reset_index(drop=True)
393   
394   # No filtering needed - return input unchanged
395   return targets_df

Filter over-represented gene families from drug targets to eliminate family-based bias in clinical scoring.

Multi-target drugs (e.g., multi-kinase inhibitors like sunitinib) often target multiple members of the same gene family. When computing clinical validation scores, each family member contributes independently, artificially inflating the perceived importance of that family. This function identifies over-represented families and keeps only one representative target per family to produce unbiased scores.

Why Filtering is Needed:

Consider sunitinib, which targets 8 receptor tyrosine kinases (RTKs). Without filtering, all 8 RTKs contribute to clinical scores, making RTKs appear highly validated. With filtering, we keep only 1 representative RTK (e.g., FLT3 alphabetically), preventing the family from dominating results.

Filtering Logic:

A gene family is considered "over-represented" and filtered if ALL of these conditions are met:

  1. Sufficient targets: Drug targets > min_targets_threshold genes in the family (default: >3)
  2. Size-adjusted criteria (either condition triggers filtering):

    a. Large family with high %: Family size ≥ small_family_threshold (default: ≥5) AND representation > pct_threshold% (default: >40%) b. Absolute threshold: Drug targets ≥ 4 genes in family (regardless of family size)

Exemptions from Filtering:

  • Small families (<5 genes by default): Never filtered to preserve biological specificity (e.g., the 4-gene HDAC class IIA family)
  • Low target count (≤3 targets by default): Insufficient redundancy to justify filtering
  • Targets without family assignments: Pass through unfiltered

Selection of Representative:

For each over-represented family, the first target alphabetically is kept as the representative. All other family members are excluded from the targets DataFrame.

Parameters
  • targets_df: DataFrame of drug targets for a specific drug-disease combination. Must contain a target_gene_name column with gene symbols.
  • drug_chembl_id: ChEMBL identifier for the drug being analyzed (e.g., 'CHEMBL1200')
  • mesh_heading: MeSH disease heading for this drug-disease combination (e.g., 'Lung Neoplasms')
  • lookup_table: Pre-computed family representation lookup from lookup_drug_family_representation(). Used to identify which families are over-represented for this drug-disease pair.
  • pct_threshold: Percentage threshold for filtering large families (0-100). Families with representation above this threshold are filtered (if other criteria met). Lower values = more aggressive filtering. Defaults to 40.0.
  • small_family_threshold: Minimum family size for percentage-based filtering. Families smaller than this are exempt from filtering to preserve biological specificity. Defaults to 5.
  • min_targets_threshold: Minimum number of targets in a family to trigger filtering. Families with fewer targets lack sufficient redundancy to justify filtering. Defaults to 3.
Returns

Filtered DataFrame with same columns as input. Over-represented family members (except the first alphabetical representative) are removed. Returns input unchanged if no filtering is needed or if drug/disease not found in lookup table.

Alphabetical Selection: Using the first alphabetical target as representative ensures deterministic, reproducible filtering across runs.

No Family Assignment: Targets that don't map to HGNC gene families in the lookup table pass through unfiltered. These are typically novel targets or non-gene targets.

If drug_chembl_id and mesh_heading combination is not found in the lookup table, the function returns the input unfiltered and prints a warning. This can occur if the drug has no targets with family assignments.

Aggressive Filtering: Lowering pct_threshold or min_targets_threshold makes filtering more aggressive but may remove biologically important targets. Default values balance bias reduction with information retention.

Examples

Basic usage with default thresholds::

>>> lookup = lookup_drug_family_representation(chembl_data)
>>> filtered = filter_overrepresented_families(
...     targets_df=drug_targets,
...     drug_chembl_id='CHEMBL1200',  # Sunitinib
...     mesh_heading='Renal Cell Carcinoma',
...     lookup_table=lookup
... )
>>> print(f"Filtered {len(drug_targets) - len(filtered)} targets")

More aggressive filtering (lower thresholds)::

>>> filtered = filter_overrepresented_families(
...     targets_df,
...     drug_chembl_id='CHEMBL123',
...     mesh_heading='Lung Neoplasms',
...     lookup_table=lookup,
...     pct_threshold=25.0,  # Filter families >25% represented
...     min_targets_threshold=2  # Filter families with >2 targets
... )

Preserve small families (e.g., HDACs) from filtering::

>>> # Don't filter families with <8 genes
>>> filtered = filter_overrepresented_families(
...     targets_df,
...     drug_chembl_id='CHEMBL123',
...     mesh_heading='Lymphoma',
...     lookup_table=lookup,
...     small_family_threshold=8
... )

Inspect which families were filtered::

>>> original_targets = set(drug_targets['target_gene_name'])
>>> filtered_targets = set(filtered['target_gene_name'])
>>> removed = original_targets - filtered_targets
>>> print(f"Removed targets: {removed}")

seealso:

def unique(scores: list, overlap=1, common_genes=[]):
1052def unique(scores: list, overlap = 1, common_genes = []):
1053    """
1054    Remove cross-disease overlapping genes from clinical scores to create disease-specific target lists.
1055    
1056    When analyzing multiple diseases, some genes appear as targets across many indications (e.g., EGFR
1057    in lung, breast, and colorectal cancers). For disease-specific analyses or comparative studies,
1058    removing these "pan-disease" targets reveals indication-specific biology.
1059    
1060    This function identifies genes present in multiple disease DataFrames and removes them from all
1061    DataFrames, leaving only targets unique to fewer diseases.
1062    
1063    **Overlap Logic:**
1064    
1065    A gene is considered "overlapping" and removed if:
1066    
1067    - It appears in **more than** ``overlap`` diseases (not in all), OR
1068    - It's in the ``common_genes`` list (manual specification)
1069    
1070    **Important**: Genes appearing in **all** diseases are NOT removed by default (see Notes).
1071    
1072    **Use Cases:**
1073    
1074    - Identify disease-specific therapeutic targets
1075    - Remove broadly-acting targets (e.g., TP53, EGFR) for specialized analysis
1076    - Create non-overlapping target sets for comparative drug discovery
1077    - Study indication-specific versus pan-cancer biology
1078    
1079    :param scores: List of DataFrames containing clinical scores. Each must have a ``Target Gene``
1080                   column with HGNC gene symbols. Typically one DataFrame per disease.
1081    :type scores: list[DataFrame]
1082    :param overlap: Minimum number of diseases a gene must appear in to be removed (exclusive of all-disease genes).
1083                    - ``overlap=1``: Remove genes in 2+ diseases (but not all)
1084                    - ``overlap=2``: Remove genes in 3+ diseases (but not all)
1085                    - ``overlap=N``: Remove genes in (N+1)+ diseases (but not all)
1086                    Defaults to 1.
1087    :type overlap: int, optional
1088    :param common_genes: List of gene symbols to always remove regardless of overlap count.
1089                         Useful for manually excluding known pan-disease targets. Defaults to [].
1090    :type common_genes: list[str], optional
1091    :return: List of DataFrames (same length/order as input) with overlapping genes removed.
1092             Original DataFrames are **not** modified (returns filtered copies).
1093    :rtype: list[DataFrame]
1094
1095    .. note::
1096       **All-Disease Genes Preserved**: By default, genes appearing in ALL input DataFrames are
1097       NOT removed. This preserves truly universal targets. To change this behavior, modify the
1098       comparison in :func:`find_overlapping_genes` from ``<`` to ``<=``.
1099
1100    .. note::
1101       This function uses :func:`find_overlapping_genes` internally to identify overlap.
1102
1103    .. warning::
1104       This function creates copies of input DataFrames. Originals remain unchanged.
1105
1106    .. warning::
1107       Setting ``overlap`` too low (e.g., 0) removes genes present in only 1+ diseases,
1108       potentially eliminating most of your data. Use with caution.
1109
1110    **Examples**
1111    
1112    Remove genes appearing in 2+ diseases (but not all 7)::
1113    
1114       >>> from alethiotx.artemis.clinical import load, unique
1115       >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
1116       >>> scores_list = [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
1117       >>> unique_scores = unique(scores_list, overlap=1)
1118       >>> breast_unique, lung_unique, *rest = unique_scores
1119       >>> print(f"Breast: {len(breast)} -> {len(breast_unique)} unique targets")
1120    
1121    More aggressive filtering (remove genes in 3+ diseases)::
1122    
1123       >>> unique_scores = unique(scores_list, overlap=2)
1124    
1125    Manually exclude known pan-cancer targets::
1126    
1127       >>> pan_cancer_genes = ['TP53', 'EGFR', 'KRAS', 'PIK3CA']
1128       >>> unique_scores = unique(scores_list, overlap=1, common_genes=pan_cancer_genes)
1129    
1130    Compare disease-specific vs all targets::
1131    
1132       >>> all_scores = load()
1133       >>> unique_scores = unique(list(all_scores), overlap=1)
1134       >>> for i, disease in enumerate(['Breast', 'Lung', 'Prostate']):
1135       ...     original = len(all_scores[i])
1136       ...     unique_count = len(unique_scores[i])
1137       ...     removed = original - unique_count
1138       ...     print(f"{disease}: removed {removed} overlapping genes ({100*removed/original:.1f}%)")
1139    
1140    Identify what genes were removed::
1141    
1142       >>> original_breast_genes = set(breast['Target Gene'])
1143       >>> unique_breast_genes = set(unique_scores[0]['Target Gene'])
1144       >>> removed_genes = original_breast_genes - unique_breast_genes
1145       >>> print(f"Removed from breast: {sorted(removed_genes)}")
1146    
1147    .. seealso::
1148       - :func:`find_overlapping_genes`: Identifies which genes overlap across disease lists
1149       - :func:`load`: Load clinical scores for multiple diseases
1150    """
1151    genes = []
1152    for n, d in enumerate(scores):
1153        genes.append(d['Target Gene'].tolist())
1154
1155    overlapping_genes = find_overlapping_genes(genes, overlap = overlap, common_genes = common_genes)
1156    
1157    res = scores.copy()
1158
1159    for n, d in enumerate(res):
1160        res[n] = d[~d['Target Gene'].isin(overlapping_genes)]
1161    
1162    return(res)

Remove cross-disease overlapping genes from clinical scores to create disease-specific target lists.

When analyzing multiple diseases, some genes appear as targets across many indications (e.g., EGFR in lung, breast, and colorectal cancers). For disease-specific analyses or comparative studies, removing these "pan-disease" targets reveals indication-specific biology.

This function identifies genes present in multiple disease DataFrames and removes them from all DataFrames, leaving only targets unique to fewer diseases.

Overlap Logic:

A gene is considered "overlapping" and removed if:

  • It appears in more than overlap diseases (not in all), OR
  • It's in the common_genes list (manual specification)

Important: Genes appearing in all diseases are NOT removed by default (see Notes).

Use Cases:

  • Identify disease-specific therapeutic targets
  • Remove broadly-acting targets (e.g., TP53, EGFR) for specialized analysis
  • Create non-overlapping target sets for comparative drug discovery
  • Study indication-specific versus pan-cancer biology
Parameters
  • scores: List of DataFrames containing clinical scores. Each must have a Target Gene column with HGNC gene symbols. Typically one DataFrame per disease.
  • overlap: Minimum number of diseases a gene must appear in to be removed (exclusive of all-disease genes).
    • overlap=1: Remove genes in 2+ diseases (but not all)
    • overlap=2: Remove genes in 3+ diseases (but not all)
    • overlap=N: Remove genes in (N+1)+ diseases (but not all) Defaults to 1.
  • common_genes: List of gene symbols to always remove regardless of overlap count. Useful for manually excluding known pan-disease targets. Defaults to [].
Returns

List of DataFrames (same length/order as input) with overlapping genes removed. Original DataFrames are not modified (returns filtered copies).

All-Disease Genes Preserved: By default, genes appearing in ALL input DataFrames are NOT removed. This preserves truly universal targets. To change this behavior, modify the comparison in find_overlapping_genes() from < to <=.

This function uses find_overlapping_genes() internally to identify overlap.

This function creates copies of input DataFrames. Originals remain unchanged.

Setting overlap too low (e.g., 0) removes genes present in only 1+ diseases, potentially eliminating most of your data. Use with caution.

Examples

Remove genes appearing in 2+ diseases (but not all 7)::

>>> from alethiotx.artemis.clinical import load, unique
>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> scores_list = [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
>>> unique_scores = unique(scores_list, overlap=1)
>>> breast_unique, lung_unique, *rest = unique_scores
>>> print(f"Breast: {len(breast)} -> {len(breast_unique)} unique targets")

More aggressive filtering (remove genes in 3+ diseases)::

>>> unique_scores = unique(scores_list, overlap=2)

Manually exclude known pan-cancer targets::

>>> pan_cancer_genes = ['TP53', 'EGFR', 'KRAS', 'PIK3CA']
>>> unique_scores = unique(scores_list, overlap=1, common_genes=pan_cancer_genes)

Compare disease-specific vs all targets::

>>> all_scores = load()
>>> unique_scores = unique(list(all_scores), overlap=1)
>>> for i, disease in enumerate(['Breast', 'Lung', 'Prostate']):
...     original = len(all_scores[i])
...     unique_count = len(unique_scores[i])
...     removed = original - unique_count
...     print(f"{disease}: removed {removed} overlapping genes ({100*removed/original:.1f}%)")

Identify what genes were removed::

>>> original_breast_genes = set(breast['Target Gene'])
>>> unique_breast_genes = set(unique_scores[0]['Target Gene'])
>>> removed_genes = original_breast_genes - unique_breast_genes
>>> print(f"Removed from breast: {sorted(removed_genes)}")

seealso:

  • find_overlapping_genes(): Identifies which genes overlap across disease lists
  • load(): Load clinical scores for multiple diseases
def approved(scores: list):
 984def approved(scores: list):
 985   """
 986   Filter clinical score DataFrames to include only targets with approved drugs (Phase 4).
 987
 988   This utility function processes a list of clinical score DataFrames and retains only
 989   targets that have at least one approved drug (phase 4 trials). This is useful for
 990   focusing analysis on clinically validated targets with FDA/EMA-approved therapeutics.
 991   
 992   **Use Cases:**
 993   
 994   - Benchmark new targets against established approved targets
 995   - Study successful drug development patterns
 996   - Identify approved drugs for repurposing opportunities
 997   - Filter out experimental/developmental targets
 998
 999   :param scores: List of DataFrames containing clinical scores. Each DataFrame must have
1000                  an ``approved`` column (boolean) indicating whether the target has any
1001                  approved (phase 4) drugs. Typically obtained from :func:`load` or :func:`compute`.
1002   :type scores: list[DataFrame]
1003   :return: List of DataFrames (same length and order as input) with only approved targets.
1004            Original DataFrames are **not** modified (returns filtered copies).
1005   :rtype: list[DataFrame]
1006
1007   .. note::
1008      This function creates copies of the input DataFrames. Original DataFrames remain unchanged.
1009
1010   .. note::
1011      Empty DataFrames are returned for diseases with no approved targets (e.g., some rare cancers).
1012
1013   **Examples**
1014   
1015   Filter pre-computed scores to approved only::
1016   
1017      >>> from alethiotx.artemis.clinical import load, approved
1018      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
1019      >>> breast_app, lung_app, prostate_app, melanoma_app, bowel_app, diabetes_app, cardio_app = approved(
1020      ...     [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
1021      ... )
1022      >>> print(f"Approved breast cancer targets: {len(breast_app)}")
1023   
1024   Compare approved vs all targets::
1025   
1026      >>> all_scores = load()
1027      >>> approved_scores = approved(list(all_scores))
1028      >>> for i, disease in enumerate(['Breast', 'Lung', 'Prostate', 'Melanoma', 'Bowel', 'Diabetes', 'Cardio']):
1029      ...     all_count = len(all_scores[i])
1030      ...     approved_count = len(approved_scores[i])
1031      ...     pct = 100 * approved_count / all_count if all_count > 0 else 0
1032      ...     print(f"{disease}: {approved_count}/{all_count} ({pct:.1f}%) approved")
1033   
1034   Identify top approved targets::
1035   
1036      >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
1037      >>> breast_approved = approved([breast])[0]
1038      >>> top_5 = breast_approved.nlargest(5, 'Clinical Score')
1039      >>> print(top_5[['Target Gene', 'Clinical Score']])
1040   
1041   .. seealso::
1042      - :func:`load`: Load pre-computed scores (returns all targets)
1043      - :func:`compute`: Compute scores with ``approved`` column
1044   """
1045   res = scores.copy()
1046
1047   for n, d in enumerate(res):
1048      res[n] = d[d['approved'] == True]
1049
1050   return(res)

Filter clinical score DataFrames to include only targets with approved drugs (Phase 4).

This utility function processes a list of clinical score DataFrames and retains only targets that have at least one approved drug (phase 4 trials). This is useful for focusing analysis on clinically validated targets with FDA/EMA-approved therapeutics.

Use Cases:

  • Benchmark new targets against established approved targets
  • Study successful drug development patterns
  • Identify approved drugs for repurposing opportunities
  • Filter out experimental/developmental targets
Parameters
  • scores: List of DataFrames containing clinical scores. Each DataFrame must have an approved column (boolean) indicating whether the target has any approved (phase 4) drugs. Typically obtained from load() or compute().
Returns

List of DataFrames (same length and order as input) with only approved targets. Original DataFrames are not modified (returns filtered copies).

This function creates copies of the input DataFrames. Original DataFrames remain unchanged.

Empty DataFrames are returned for diseases with no approved targets (e.g., some rare cancers).

Examples

Filter pre-computed scores to approved only::

>>> from alethiotx.artemis.clinical import load, approved
>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> breast_app, lung_app, prostate_app, melanoma_app, bowel_app, diabetes_app, cardio_app = approved(
...     [breast, lung, prostate, melanoma, bowel, diabetes, cardio]
... )
>>> print(f"Approved breast cancer targets: {len(breast_app)}")

Compare approved vs all targets::

>>> all_scores = load()
>>> approved_scores = approved(list(all_scores))
>>> for i, disease in enumerate(['Breast', 'Lung', 'Prostate', 'Melanoma', 'Bowel', 'Diabetes', 'Cardio']):
...     all_count = len(all_scores[i])
...     approved_count = len(approved_scores[i])
...     pct = 100 * approved_count / all_count if all_count > 0 else 0
...     print(f"{disease}: {approved_count}/{all_count} ({pct:.1f}%) approved")

Identify top approved targets::

>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> breast_approved = approved([breast])[0]
>>> top_5 = breast_approved.nlargest(5, 'Clinical Score')
>>> print(top_5[['Target Gene', 'Clinical Score']])

seealso:

def all_targets(scores: list):
1164def all_targets(scores: list):
1165    """
1166    Extract all unique target genes from multiple clinical score DataFrames.
1167    
1168    This utility function aggregates target genes across multiple diseases to create a comprehensive
1169    list of all genes with clinical validation evidence. Useful for building negative sample sets,
1170    identifying the universe of clinically-validated targets, or creating background gene sets for
1171    statistical analysis.
1172
1173    :param scores: List of DataFrames containing clinical scores. Each must have a ``Target Gene``
1174                   column with gene symbols. Typically one DataFrame per disease from :func:`load`
1175                   or :func:`compute`.
1176    :type scores: list[DataFrame]
1177    :return: List of unique gene symbols (HGNC) found across all input DataFrames. Order is not guaranteed.
1178    :rtype: list[str]
1179
1180    .. note::
1181       Duplicates are automatically removed. If GENE1 appears in multiple diseases, it's included
1182       only once in the output.
1183
1184    .. note::
1185       The output is a plain Python list, not a pandas Series. Convert if needed:
1186       ``pd.Series(all_targets(scores))``
1187
1188    **Examples**
1189    
1190    Get all clinically-validated targets across diseases::
1191    
1192       >>> from alethiotx.artemis.clinical import load, all_targets
1193       >>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
1194       >>> all_genes = all_targets([breast, lung, prostate, melanoma, bowel, diabetes, cardio])
1195       >>> print(f"Total unique clinical targets: {len(all_genes)}")
1196    
1197    Build known target list for machine learning::
1198    
1199       >>> # Use as known_targets in cv.prepare() to exclude from negative sampling
1200       >>> from alethiotx.artemis.cv import prepare
1201       >>> all_clinical_targets = all_targets(disease_scores)
1202       >>> result = prepare(X, y, pathway_genes=pg, known_targets=all_clinical_targets)
1203    
1204    Compare target overlap between diseases::
1205    
1206       >>> breast_genes = set(breast['Target Gene'])
1207       >>> lung_genes = set(lung['Target Gene'])
1208       >>> all_genes = set(all_targets([breast, lung]))
1209       >>> print(f"Breast: {len(breast_genes)}, Lung: {len(lung_genes)}, Total unique: {len(all_genes)}")
1210       >>> print(f"Overlap: {len(breast_genes & lung_genes)} genes")
1211    
1212    Create gene set for enrichment analysis::
1213    
1214       >>> import gseapy as gp
1215       >>> all_clinical = all_targets(disease_scores)
1216       >>> enrich = gp.enrichr(gene_list=all_clinical, gene_sets='KEGG_2021_Human')
1217    
1218    .. seealso::
1219       - :func:`unique`: Remove overlapping genes across diseases (opposite operation)
1220       - :func:`load`: Load clinical scores containing target genes
1221    """
1222    all_targets = set()
1223
1224    for d in scores:
1225        all_targets.update(d['Target Gene'].tolist())
1226
1227    return(list(all_targets))

Extract all unique target genes from multiple clinical score DataFrames.

This utility function aggregates target genes across multiple diseases to create a comprehensive list of all genes with clinical validation evidence. Useful for building negative sample sets, identifying the universe of clinically-validated targets, or creating background gene sets for statistical analysis.

Parameters
  • scores: List of DataFrames containing clinical scores. Each must have a Target Gene column with gene symbols. Typically one DataFrame per disease from load() or compute().
Returns

List of unique gene symbols (HGNC) found across all input DataFrames. Order is not guaranteed.

Duplicates are automatically removed. If GENE1 appears in multiple diseases, it's included only once in the output.

The output is a plain Python list, not a pandas Series. Convert if needed: pd.Series(all_targets(scores))

Examples

Get all clinically-validated targets across diseases::

>>> from alethiotx.artemis.clinical import load, all_targets
>>> breast, lung, prostate, melanoma, bowel, diabetes, cardio = load()
>>> all_genes = all_targets([breast, lung, prostate, melanoma, bowel, diabetes, cardio])
>>> print(f"Total unique clinical targets: {len(all_genes)}")

Build known target list for machine learning::

>>> # Use as known_targets in cv.prepare() to exclude from negative sampling
>>> from alethiotx.artemis.cv import prepare
>>> all_clinical_targets = all_targets(disease_scores)
>>> result = prepare(X, y, pathway_genes=pg, known_targets=all_clinical_targets)

Compare target overlap between diseases::

>>> breast_genes = set(breast['Target Gene'])
>>> lung_genes = set(lung['Target Gene'])
>>> all_genes = set(all_targets([breast, lung]))
>>> print(f"Breast: {len(breast_genes)}, Lung: {len(lung_genes)}, Total unique: {len(all_genes)}")
>>> print(f"Overlap: {len(breast_genes & lung_genes)} genes")

Create gene set for enrichment analysis::

>>> import gseapy as gp
>>> all_clinical = all_targets(disease_scores)
>>> enrich = gp.enrichr(gene_list=all_clinical, gene_sets='KEGG_2021_Human')

seealso:

  • unique(): Remove overlapping genes across diseases (opposite operation)
  • load(): Load clinical scores containing target genes