alethiotx.artemis.chembl

1from .query import molecules, infer_nct_year
2
3__all__ = ['molecules', 'infer_nct_year']
def molecules(version: str = '36', top_n_activities: int = 1):
 85def molecules(version: str = '36', top_n_activities: int = 1):
 86    """
 87    Query ChEMBL database for bioactive drug molecules with clinical trial data and therapeutic indications.
 88    
 89    This function retrieves comprehensive drug-target-indication relationships from ChEMBL, automatically
 90    normalizing all molecular forms (salts, formulations, etc.) to their parent compounds. It integrates
 91    clinical trial phases, MeSH disease classifications, drug mechanisms, and target information to create
 92    a unified dataset for drug target prioritization and discovery.
 93    
 94    **Data Processing Workflow:**
 95    
 96    1. **Parent Normalization**: All child molecules (salts, prodrugs, formulations) are mapped to their
 97       parent compound using ChEMBL's molecule hierarchy
 98    2. **Indication Aggregation**: Drug indications from both parent and all child molecules are combined
 99    3. **Target Assignment**: Molecular targets are identified using a three-tier hierarchy:
100       
101    - Primary: Parent molecule's ``DRUG_MECHANISM`` table entries (known mechanisms)
102    - Secondary: Child molecule mechanisms (inherited when parent lacks mechanisms)
103    - Tertiary: Top N most-studied targets from ``ACTIVITIES`` table (bioassay data)
104    
105    4. **Clinical Trial Mapping**: Links molecules to ClinicalTrials.gov identifiers with phase information
106    5. **Year Inference**: Estimates trial registration year from NCT identifiers
107    
108    **Key Features:**
109    
110    - Only includes molecules with clinical trial references (ClinicalTrials.gov)
111    - Filters to human targets only (``Homo sapiens``)
112    - One molecule-indication-target per row (exploded format for multi-trial drugs)
113    - Mechanisms apply to all indications of a molecule (not indication-specific)
114    
115    :param version: ChEMBL database version to query. Version 36 covers data through 2024.
116                    See https://www.ebi.ac.uk/chembl/ for available versions.
117    :type version: str, optional
118    :param top_n_activities: For molecules without documented mechanisms (no ``DRUG_MECHANISM`` entries),
119                             include the top N most-studied targets from bioassay data. Set to 0 to
120                             exclude activity-based targets entirely. Defaults to 1 (most-studied target only).
121    :type top_n_activities: int, optional
122    
123    :return: DataFrame with one row per parent-molecule-indication-target combination. 
124    
125    **Columns:**
126    
127    **Molecule Information:**
128    
129    - ``chembl_id`` (str): ChEMBL identifier for the parent molecule (e.g., 'CHEMBL25')
130    - ``pref_name`` (str): Preferred drug name (e.g., 'ASPIRIN')
131    - ``parent_molregno`` (int): Internal ChEMBL registry number for parent molecule
132    
133    **Indication Information:**
134    
135    - ``mesh_heading`` (str): MeSH disease term (e.g., 'Lung Neoplasms')
136    - ``mesh_id`` (str): MeSH unique identifier (e.g., 'D008175')
137    - ``phase`` (int): Maximum clinical trial phase for this indication (0-4, where 4=approved)
138    - ``reference_type`` (str): Always 'ClinicalTrials' (pre-filtered)
139    - ``clinical_trial_id`` (str): ClinicalTrials.gov NCT identifier (e.g., 'NCT00123456')
140    - ``trial_year`` (int, nullable): Inferred trial registration year via :func:`infer_nct_year`
141    
142    **Target Information:**
143    
144    - ``target_chembl_id`` (str): ChEMBL target identifier (e.g., 'CHEMBL240')
145    - ``target_organism`` (str): Always 'Homo sapiens' (pre-filtered)
146    - ``target_type`` (str): Target classification (e.g., 'SINGLE PROTEIN', 'PROTEIN COMPLEX')
147    - ``target_uniprot_id`` (str, nullable): UniProt accession (e.g., 'P35354')
148    - ``target_gene_name`` (str, nullable): HGNC gene symbol (e.g., 'EGFR')
149    - ``mechanism_of_action`` (str, nullable): Mechanism description (NULL for activity-derived targets)
150    - ``action_type`` (str, nullable): Drug action type (e.g., 'INHIBITOR', 'AGONIST'; NULL for activities)
151    - ``target_source`` (str): Data provenance - one of:
152        
153        - ``DRUG_MECHANISM``: From parent's mechanism table (highest confidence)
154        - ``DRUG_MECHANISM_CHILD``: Inherited from child molecule's mechanism
155        - ``ACTIVITIES``: Derived from bioassay activity data (lower confidence)
156    
157    :rtype: pandas.DataFrame
158    
159    **Examples**
160    
161    Basic usage - retrieve all molecules from ChEMBL v36::
162    
163    >>> from alethiotx.artemis.chembl import molecules
164    >>> df = molecules(version='36', top_n_activities=1)
165    >>> print(f"{len(df)} records, {df['chembl_id'].nunique()} unique molecules")
166    >>> print(df[['chembl_id', 'pref_name', 'mesh_heading', 'target_gene_name']].head())
167    
168    Filter to specific disease and approved drugs only::
169    
170    >>> df = molecules(version='36')
171    >>> lung_cancer = df[df['mesh_heading'] == 'Lung Neoplasms']
172    >>> approved = lung_cancer[lung_cancer['phase'] == 4]
173    >>> print(approved[['pref_name', 'target_gene_name']].drop_duplicates())
174    
175    Exclude activity-based targets (mechanism data only)::
176    
177    >>> df = molecules(version='36', top_n_activities=0)
178    >>> print(f"Mechanisms only: {df['target_source'].value_counts()}")
179    
180    Analyze recent trials (last 6 years)::
181    
182    >>> from datetime import datetime
183    >>> df = molecules(version='36')
184    >>> current_year = datetime.now().year
185    >>> recent = df[df['trial_year'] >= current_year - 6]
186    >>> print(f"Recent trials: {len(recent)} records")
187    
188    .. note::
189        **Parent-Child Normalization**: All molecular forms (salts like 'aspirin sodium',
190        formulations like 'aspirin tablet') are normalized to their parent compound ('aspirin').
191        This ensures consistent target mapping and prevents double-counting.
192    
193    .. note::
194        **Mechanism-Indication Independence**: A molecule's targets are the same across all its
195        indications. For example, if aspirin targets COX1/COX2, these targets apply whether the
196        indication is 'Pain' or 'Cardiovascular Disease'. This reflects biological reality - a
197        drug's mechanism doesn't change based on what it's prescribed for.
198    
199    .. note::
200        **Clinical Trial ID Explosion**: When a molecule has multiple comma-separated trial IDs
201        (e.g., 'NCT001,NCT002'), they are exploded into separate rows. This enables per-trial
202        analysis and proper trial counting.
203    
204    .. warning::
205        **Data Volume**: ChEMBL v36 contains hundreds of thousands of molecule-target relationships.
206        Full queries may take several minutes and return large DataFrames (>100K rows). Consider
207        filtering by version, phase, or disease after loading to reduce memory usage.
208    
209    .. warning::
210        **Activity-Based Targets**: Targets from the ``ACTIVITIES`` table (``target_source='ACTIVITIES'``)
211        have lower confidence than mechanism-based targets. They represent bioassay activity but may
212        not reflect clinical mechanisms. Set ``top_n_activities=0`` to exclude these if you need
213        high-confidence mechanisms only.
214    
215    .. warning::
216        **Requires chembl-downloader**: This function requires the ``chembl-downloader`` package
217        to be installed. Install via: ``pip install chembl-downloader``
218    
219    .. seealso::
220        - :func:`infer_nct_year`: Used internally to estimate trial registration years
221        - ChEMBL Documentation: https://chembl.gitbook.io/chembl-interface-documentation/
222        - ClinicalTrials.gov: https://clinicaltrials.gov/
223
224    """
225    
226    print("Step 1: Getting all parent molecules with their children's indications...")
227    # Get all molecules (parent and children) with clinical trial indications
228    # Map everything to parent_molregno
229    sql_all_indications = """
230    SELECT DISTINCT
231        COALESCE(MH.parent_molregno, MD.molregno) AS parent_molregno,
232        MD_parent.chembl_id AS parent_chembl_id,
233        MD_parent.pref_name AS parent_pref_name,
234        DI.mesh_heading,
235        DI.mesh_id,
236        DI.max_phase_for_ind AS phase,
237        IREF.ref_type AS reference_type,
238        IREF.ref_id AS clinical_trial_id
239    FROM MOLECULE_DICTIONARY MD
240    INNER JOIN DRUG_INDICATION DI ON MD.molregno = DI.molregno
241    INNER JOIN INDICATION_REFS IREF ON DI.drugind_id = IREF.drugind_id
242    LEFT JOIN MOLECULE_HIERARCHY MH ON MD.molregno = MH.molregno
243    INNER JOIN MOLECULE_DICTIONARY MD_parent ON COALESCE(MH.parent_molregno, MD.molregno) = MD_parent.molregno
244    WHERE IREF.ref_id IS NOT NULL 
245        AND IREF.ref_type = 'ClinicalTrials'
246    """
247    df_indications = chembl_downloader.query(sql_all_indications, version=version)
248    print(f"  Found {len(df_indications)} indication records for {df_indications['parent_molregno'].nunique()} parent molecules")
249    
250    print("\nStep 2: Getting mechanisms for parent molecules...")
251    # Get mechanisms from parent molecules directly
252    sql_parent_mechanisms = """
253    SELECT DISTINCT
254        MD.molregno AS parent_molregno,
255        TD.chembl_id AS target_chembl_id,
256        TD.organism AS target_organism,
257        TD.target_type,
258        CS_TARGET.accession AS target_uniprot_id,
259        COMP_SYN.component_synonym AS target_gene_name,
260        DM.mechanism_of_action,
261        DM.action_type,
262        'DRUG_MECHANISM' AS target_source
263    FROM MOLECULE_DICTIONARY MD
264    INNER JOIN DRUG_MECHANISM DM ON MD.molregno = DM.molregno
265    INNER JOIN TARGET_DICTIONARY TD ON DM.tid = TD.tid
266    LEFT JOIN TARGET_COMPONENTS TC ON TD.tid = TC.tid
267    LEFT JOIN COMPONENT_SEQUENCES CS_TARGET ON TC.component_id = CS_TARGET.component_id
268    LEFT JOIN COMPONENT_SYNONYMS COMP_SYN ON CS_TARGET.component_id = COMP_SYN.component_id AND COMP_SYN.syn_type = 'GENE_SYMBOL'
269    WHERE TD.organism = 'Homo sapiens'
270    """
271    df_parent_mech = chembl_downloader.query(sql_parent_mechanisms, version=version)
272    print(f"  Found {len(df_parent_mech)} mechanism records for {df_parent_mech['parent_molregno'].nunique()} parent molecules")
273    
274    print("\nStep 3: Getting mechanisms from children molecules for parents without mechanisms...")
275    # Get mechanisms from children for parents that lack mechanisms
276    sql_child_mechanisms = """
277    SELECT DISTINCT
278        MH.parent_molregno,
279        TD.chembl_id AS target_chembl_id,
280        TD.organism AS target_organism,
281        TD.target_type,
282        CS_TARGET.accession AS target_uniprot_id,
283        COMP_SYN.component_synonym AS target_gene_name,
284        DM.mechanism_of_action,
285        DM.action_type,
286        'DRUG_MECHANISM_CHILD' AS target_source
287    FROM MOLECULE_HIERARCHY MH
288    INNER JOIN DRUG_MECHANISM DM ON MH.molregno = DM.molregno
289    INNER JOIN TARGET_DICTIONARY TD ON DM.tid = TD.tid
290    LEFT JOIN TARGET_COMPONENTS TC ON TD.tid = TC.tid
291    LEFT JOIN COMPONENT_SEQUENCES CS_TARGET ON TC.component_id = CS_TARGET.component_id
292    LEFT JOIN COMPONENT_SYNONYMS COMP_SYN ON CS_TARGET.component_id = COMP_SYN.component_id AND COMP_SYN.syn_type = 'GENE_SYMBOL'
293    WHERE TD.organism = 'Homo sapiens'
294        AND NOT EXISTS (
295            SELECT 1 FROM DRUG_MECHANISM DM2 WHERE DM2.molregno = MH.parent_molregno
296        )
297    """
298    df_child_mech = chembl_downloader.query(sql_child_mechanisms, version=version)
299    print(f"  Found {len(df_child_mech)} child mechanism records for {df_child_mech['parent_molregno'].nunique()} parent molecules")
300    
301    # Combine mechanisms
302    df_mechanisms = pd.concat([df_parent_mech, df_child_mech], ignore_index=True)
303    
304    print("\nStep 4: Getting activities for parents without any mechanisms...")
305    # Find parents without mechanisms
306    parents_with_mech = set(df_mechanisms['parent_molregno'].unique())
307    all_parents = set(df_indications['parent_molregno'].unique())
308    parents_without_mech = all_parents - parents_with_mech
309    
310    print(f"  Parents needing activities: {len(parents_without_mech)}")
311    
312    if len(parents_without_mech) > 0:
313        molregno_list = ','.join(map(str, parents_without_mech))
314        
315        sql_activities = f"""
316        WITH RankedActivities AS (
317            SELECT 
318                COALESCE(MH.parent_molregno, ACT.molregno) AS parent_molregno,
319                TD.chembl_id AS target_chembl_id,
320                TD.organism AS target_organism,
321                TD.target_type,
322                CS_TARGET.accession AS target_uniprot_id,
323                COMP_SYN.component_synonym AS target_gene_name,
324                COUNT(ACT.activity_id) as activity_count,
325                ROW_NUMBER() OVER (PARTITION BY COALESCE(MH.parent_molregno, ACT.molregno) ORDER BY COUNT(ACT.activity_id) DESC) as rn
326            FROM ACTIVITIES ACT
327            LEFT JOIN MOLECULE_HIERARCHY MH ON ACT.molregno = MH.molregno
328            INNER JOIN ASSAYS A ON ACT.assay_id = A.assay_id
329            INNER JOIN TARGET_DICTIONARY TD ON A.tid = TD.tid
330            LEFT JOIN TARGET_COMPONENTS TC ON TD.tid = TC.tid
331            LEFT JOIN COMPONENT_SEQUENCES CS_TARGET ON TC.component_id = CS_TARGET.component_id
332            LEFT JOIN COMPONENT_SYNONYMS COMP_SYN ON CS_TARGET.component_id = COMP_SYN.component_id AND COMP_SYN.syn_type = 'GENE_SYMBOL'
333            WHERE COALESCE(MH.parent_molregno, ACT.molregno) IN ({molregno_list})
334                AND TD.organism = 'Homo sapiens'
335            GROUP BY COALESCE(MH.parent_molregno, ACT.molregno), TD.chembl_id, TD.organism, TD.target_type, CS_TARGET.accession, COMP_SYN.component_synonym
336        )
337        SELECT 
338            parent_molregno,
339            target_chembl_id,
340            target_organism,
341            target_type,
342            target_uniprot_id,
343            target_gene_name,
344            NULL AS mechanism_of_action,
345            NULL AS action_type,
346            'ACTIVITIES' AS target_source
347        FROM RankedActivities
348        WHERE rn <= {top_n_activities}
349        """
350        
351        df_activities = chembl_downloader.query(sql_activities, version=version)
352        print(f"  Found {len(df_activities)} activity-based targets for {df_activities['parent_molregno'].nunique()} parent molecules")
353        
354        # Add to mechanisms
355        df_mechanisms = pd.concat([df_mechanisms, df_activities], ignore_index=True)
356    
357    print("\nStep 5: Combining indications with mechanisms...")
358    # Cross join indications with mechanisms at parent level (each indication gets all targets)
359    df_combined = df_indications.merge(
360        df_mechanisms,
361        on='parent_molregno',
362        how='left'
363    )
364    
365    # Rename columns for final output
366    df_combined = df_combined.rename(columns={
367        'parent_chembl_id': 'chembl_id',
368        'parent_pref_name': 'pref_name'
369    })
370    
371    print(f"  Total combined records: {len(df_combined)}")
372    print(f"  Total unique parent molecules: {df_combined['chembl_id'].nunique()}")
373    
374    # Explode clinical_trial_id column if it contains multiple comma-separated IDs
375    if 'clinical_trial_id' in df_combined.columns and df_combined['clinical_trial_id'].notna().any():
376        df_combined['clinical_trial_id'] = df_combined['clinical_trial_id'].apply(
377            lambda x: x.split(',') if isinstance(x, str) and ',' in x else [x] if x else []
378        )
379        df_combined = df_combined.explode('clinical_trial_id')
380        df_combined['clinical_trial_id'] = df_combined['clinical_trial_id'].apply(lambda x: x.strip() if isinstance(x, str) else x)
381        
382        # Add inferred year column as integer
383        df_combined['trial_year'] = df_combined['clinical_trial_id'].apply(infer_nct_year)
384        df_combined['trial_year'] = df_combined['trial_year'].astype('Int64')  # Use nullable integer type
385
386    return df_combined

Query ChEMBL database for bioactive drug molecules with clinical trial data and therapeutic indications.

This function retrieves comprehensive drug-target-indication relationships from ChEMBL, automatically normalizing all molecular forms (salts, formulations, etc.) to their parent compounds. It integrates clinical trial phases, MeSH disease classifications, drug mechanisms, and target information to create a unified dataset for drug target prioritization and discovery.

Data Processing Workflow:

  1. Parent Normalization: All child molecules (salts, prodrugs, formulations) are mapped to their parent compound using ChEMBL's molecule hierarchy
  2. Indication Aggregation: Drug indications from both parent and all child molecules are combined
  3. Target Assignment: Molecular targets are identified using a three-tier hierarchy:
  • Primary: Parent molecule's DRUG_MECHANISM table entries (known mechanisms)
  • Secondary: Child molecule mechanisms (inherited when parent lacks mechanisms)
  • Tertiary: Top N most-studied targets from ACTIVITIES table (bioassay data)
  1. Clinical Trial Mapping: Links molecules to ClinicalTrials.gov identifiers with phase information
  2. Year Inference: Estimates trial registration year from NCT identifiers

Key Features:

  • Only includes molecules with clinical trial references (ClinicalTrials.gov)
  • Filters to human targets only (Homo sapiens)
  • One molecule-indication-target per row (exploded format for multi-trial drugs)
  • Mechanisms apply to all indications of a molecule (not indication-specific)
Parameters
  • version: ChEMBL database version to query. Version 36 covers data through 2024. See https://www.ebi.ac.uk/chembl/ for available versions.
  • top_n_activities: For molecules without documented mechanisms (no DRUG_MECHANISM entries), include the top N most-studied targets from bioassay data. Set to 0 to exclude activity-based targets entirely. Defaults to 1 (most-studied target only).
Returns

DataFrame with one row per parent-molecule-indication-target combination.

Columns:

Molecule Information:

  • chembl_id (str): ChEMBL identifier for the parent molecule (e.g., 'CHEMBL25')
  • pref_name (str): Preferred drug name (e.g., 'ASPIRIN')
  • parent_molregno (int): Internal ChEMBL registry number for parent molecule

Indication Information:

  • mesh_heading (str): MeSH disease term (e.g., 'Lung Neoplasms')
  • mesh_id (str): MeSH unique identifier (e.g., 'D008175')
  • phase (int): Maximum clinical trial phase for this indication (0-4, where 4=approved)
  • reference_type (str): Always 'ClinicalTrials' (pre-filtered)
  • clinical_trial_id (str): ClinicalTrials.gov NCT identifier (e.g., 'NCT00123456')
  • trial_year (int, nullable): Inferred trial registration year via infer_nct_year()

Target Information:

  • target_chembl_id (str): ChEMBL target identifier (e.g., 'CHEMBL240')
  • target_organism (str): Always 'Homo sapiens' (pre-filtered)
  • target_type (str): Target classification (e.g., 'SINGLE PROTEIN', 'PROTEIN COMPLEX')
  • target_uniprot_id (str, nullable): UniProt accession (e.g., 'P35354')
  • target_gene_name (str, nullable): HGNC gene symbol (e.g., 'EGFR')
  • mechanism_of_action (str, nullable): Mechanism description (NULL for activity-derived targets)
  • action_type (str, nullable): Drug action type (e.g., 'INHIBITOR', 'AGONIST'; NULL for activities)
  • target_source (str): Data provenance - one of:

    • DRUG_MECHANISM: From parent's mechanism table (highest confidence)
    • DRUG_MECHANISM_CHILD: Inherited from child molecule's mechanism
    • ACTIVITIES: Derived from bioassay activity data (lower confidence)

Examples

Basic usage - retrieve all molecules from ChEMBL v36::

>>> from alethiotx.artemis.chembl import molecules
>>> df = molecules(version='36', top_n_activities=1)
>>> print(f"{len(df)} records, {df['chembl_id'].nunique()} unique molecules")
>>> print(df[['chembl_id', 'pref_name', 'mesh_heading', 'target_gene_name']].head())

Filter to specific disease and approved drugs only::

>>> df = molecules(version='36')
>>> lung_cancer = df[df['mesh_heading'] == 'Lung Neoplasms']
>>> approved = lung_cancer[lung_cancer['phase'] == 4]
>>> print(approved[['pref_name', 'target_gene_name']].drop_duplicates())

Exclude activity-based targets (mechanism data only)::

>>> df = molecules(version='36', top_n_activities=0)
>>> print(f"Mechanisms only: {df['target_source'].value_counts()}")

Analyze recent trials (last 6 years)::

>>> from datetime import datetime
>>> df = molecules(version='36')
>>> current_year = datetime.now().year
>>> recent = df[df['trial_year'] >= current_year - 6]
>>> print(f"Recent trials: {len(recent)} records")

Parent-Child Normalization: All molecular forms (salts like 'aspirin sodium', formulations like 'aspirin tablet') are normalized to their parent compound ('aspirin'). This ensures consistent target mapping and prevents double-counting.

Mechanism-Indication Independence: A molecule's targets are the same across all its indications. For example, if aspirin targets COX1/COX2, these targets apply whether the indication is 'Pain' or 'Cardiovascular Disease'. This reflects biological reality - a drug's mechanism doesn't change based on what it's prescribed for.

Clinical Trial ID Explosion: When a molecule has multiple comma-separated trial IDs (e.g., 'NCT001,NCT002'), they are exploded into separate rows. This enables per-trial analysis and proper trial counting.

Data Volume: ChEMBL v36 contains hundreds of thousands of molecule-target relationships. Full queries may take several minutes and return large DataFrames (>100K rows). Consider filtering by version, phase, or disease after loading to reduce memory usage.

Activity-Based Targets: Targets from the ACTIVITIES table (target_source='ACTIVITIES') have lower confidence than mechanism-based targets. They represent bioassay activity but may not reflect clinical mechanisms. Set top_n_activities=0 to exclude these if you need high-confidence mechanisms only.

Requires chembl-downloader: This function requires the chembl-downloader package to be installed. Install via: pip install chembl-downloader

seealso:

def infer_nct_year(nct_id):
 6def infer_nct_year(nct_id):
 7    """
 8    Infer the approximate registration year from a ClinicalTrials.gov NCT identifier.
 9    
10    NCT IDs follow the format ``NCT########``, where the 8-digit numeric portion is assigned
11    sequentially and increases over time. This function uses empirically-observed NCT ID
12    allocation patterns to estimate when a trial was registered, which is useful for temporal
13    filtering and analysis when exact registration dates are not available.
14
15    **NCT ID Allocation Ranges:**
16    
17    - ``NCT00000000`` - ``NCT00999999``: ~1999-2004
18    - ``NCT01000000`` - ``NCT01999999``: ~2005-2011
19    - ``NCT02000000`` - ``NCT02999999``: ~2012-2015
20    - ``NCT03000000`` - ``NCT03999999``: ~2016-2018
21    - ``NCT04000000`` - ``NCT04999999``: ~2019-2021
22    - ``NCT05000000`` - ``NCT05999999``: ~2022-2023
23    - ``NCT06000000``+: ~2024+
24
25    :param nct_id: A ClinicalTrials.gov identifier (e.g., ``NCT00500000``)
26    :type nct_id: str
27    :return: Estimated year of trial registration as an integer, or ``None`` if the NCT ID is invalid/malformed
28    :rtype: int or None
29
30    **Examples**
31    
32    >>> infer_nct_year("NCT00500000")
33    2002
34    >>> infer_nct_year("NCT03000000")
35    2016
36    >>> infer_nct_year("NCT06123456")
37    2024
38    >>> infer_nct_year("invalid")
39    None
40    >>> infer_nct_year("NCT123")  # Too short
41    None
42    
43    **Use Cases:**
44    
45    - Filtering clinical trials data by approximate time period
46    - Temporal analysis of drug development trends
47    - Quick year estimation when full trial metadata is unavailable
48    
49    .. note::
50        This function provides an **approximation** based on historical NCT ID allocation
51        patterns. Individual trials may vary by ±1-2 years from the estimated value.
52        For precise temporal analysis, obtain the actual registration date from the
53        ClinicalTrials.gov API or database.
54    
55    .. warning::
56        Returns ``None`` for invalid inputs including non-string types, IDs without the
57        "NCT" prefix, or IDs that don't contain exactly 8 digits after "NCT".
58    """
59    if not isinstance(nct_id, str) or not nct_id.startswith('NCT'):
60        return None
61    
62    # Extract the numeric part
63    match = re.search(r'NCT(\d{8})', nct_id)
64    if not match:
65        return None
66    
67    num = int(match.group(1))
68    
69    # Approximate year ranges based on NCT ID ranges
70    if num < 1000000:
71        return 1999 + (num // 200000)  # ~1999-2004
72    elif num < 2000000:
73        return 2005 + ((num - 1000000) // 140000)  # ~2005-2011
74    elif num < 3000000:
75        return 2012 + ((num - 2000000) // 250000)  # ~2012-2015
76    elif num < 4000000:
77        return 2016 + ((num - 3000000) // 330000)  # ~2016-2018
78    elif num < 5000000:
79        return 2019 + ((num - 4000000) // 350000)  # ~2019-2021
80    elif num < 6000000:
81        return 2022 + ((num - 5000000) // 500000)  # ~2022-2023
82    else:
83        return 2024 + ((num - 6000000) // 500000)  # ~2024+

Infer the approximate registration year from a ClinicalTrials.gov NCT identifier.

NCT IDs follow the format NCT########, where the 8-digit numeric portion is assigned sequentially and increases over time. This function uses empirically-observed NCT ID allocation patterns to estimate when a trial was registered, which is useful for temporal filtering and analysis when exact registration dates are not available.

NCT ID Allocation Ranges:

  • NCT00000000 - NCT00999999: ~1999-2004
  • NCT01000000 - NCT01999999: ~2005-2011
  • NCT02000000 - NCT02999999: ~2012-2015
  • NCT03000000 - NCT03999999: ~2016-2018
  • NCT04000000 - NCT04999999: ~2019-2021
  • NCT05000000 - NCT05999999: ~2022-2023
  • NCT06000000+: ~2024+
Parameters
  • nct_id: A ClinicalTrials.gov identifier (e.g., NCT00500000)
Returns

Estimated year of trial registration as an integer, or None if the NCT ID is invalid/malformed

Examples

>>> infer_nct_year("NCT00500000")
2002
>>> infer_nct_year("NCT03000000")
2016
>>> infer_nct_year("NCT06123456")
2024
>>> infer_nct_year("invalid")
None
>>> infer_nct_year("NCT123")  # Too short
None

Use Cases:

  • Filtering clinical trials data by approximate time period
  • Temporal analysis of drug development trends
  • Quick year estimation when full trial metadata is unavailable

This function provides an approximation based on historical NCT ID allocation patterns. Individual trials may vary by ±1-2 years from the estimated value. For precise temporal analysis, obtain the actual registration date from the ClinicalTrials.gov API or database.

Returns None for invalid inputs including non-string types, IDs without the "NCT" prefix, or IDs that don't contain exactly 8 digits after "NCT".