ctgov
Paginating ClinicalTrials.gov API v2 client with free-text endpoint normalization.
Zero dependencies. Pure standard library. No API key.
pip install ctgov
Why
Two things the registry makes harder than they should be.
1. You cannot get more than 1,000 studies
The API caps a response at 1,000 and hands back a nextPageToken. Existing wrappers stop
there, so "how do I pull more than 1000 studies?" is the question that keeps getting asked
and never answered.
ctgov follows the token for you. Your result set is bounded by your query, not by the page size.
from ctgov import Client
client = Client()
studies = client.studies("AREA[Phase]PHASE3", max_studies=5000) # pages automatically
print(len(studies)) # 5000
for study in client.iter_studies("cancer"): # streams; one page in memory at a time
...
2. Endpoints are prose, so you cannot compare across trials
Straight from the API, an outcome measure looks like this:
| raw endpoint | raw timeframe |
|---|---|
Serum Markers (HbA1c) to Follow the Evolution of Liver Damage |
6 months |
Primary Safety: Freedom From Index Procedure Related Major Complications |
30 days |
Cmax of midazolam |
0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12 hours post-dose |
Anxiety Rating |
Baseline |
You cannot group, filter, or compare on that. ctgov structures it:
| domain | measure type | canonical | horizon_days | direction |
|---|---|---|---|---|
| biomarker_laboratory | change_from_baseline | hba1c_change | 182.64 | lower_is_better |
| safety | proportion | — | 30.0 | higher_is_better |
| pharmacokinetics | pk_parameter | pk_exposure | 0.5 | unspecified |
| patient_reported | score_scale | anxiety_score | 0.0 | lower_is_better |
Query to structured rows in one call
from ctgov import endpoints
rows = [r for r in endpoints("AREA[Phase]PHASE3", max_studies=200)
if r["normalization_confidence"] >= 0.7]
for r in rows[:3]:
print(r["endpoint_canonical"], r["horizon_days"], r["condition_primary"])
Each row carries the study context (phase, condition, enrollment, sponsor, allocation, masking), the normalized endpoint fields, and the untouched raw text, so any row can be audited rather than taken on trust.
Straight into pandas:
import pandas as pd
df = pd.DataFrame(endpoints("AREA[Condition]diabetes", max_studies=500))
df.groupby("endpoint_canonical")["horizon_days"].median().sort_values()
Counting without downloading
client.count("AREA[Phase]PHASE3", filters={"advanced": "AREA[HasResults]true"}) # 14748
What you get per endpoint
| field | meaning |
|---|---|
endpoint_raw |
untouched registry text |
endpoint_domain |
safety, efficacy, patient_reported, pharmacokinetics, biomarker_laboratory, clinical_assessment, behavioral_functional, healthcare_utilization, immunogenicity, feasibility_implementation, diagnostic_accuracy, unclassified |
endpoint_measure_type |
change_from_baseline, proportion, count, score_scale, time_to_event, pk_parameter, concentration, diagnostic_accuracy, unclassified |
endpoint_canonical |
recognised concept: overall_survival, progression_free_survival, objective_response_rate, adverse_event_incidence, hba1c_change, quality_of_life, seroconversion, … |
endpoint_direction |
higher_is_better / lower_is_better / ambiguous / unspecified |
horizon_days |
free-text timeframe parsed to a number |
timepoint_count, is_longitudinal, baseline_anchored |
schedule shape |
normalization_confidence |
0–1. Filter on >= 0.7 for high-precision work. |
Honesty about coverage
Normalization is deterministic and rule-based — no model inference, so it is reproducible,
auditable, and free to run. It does not classify everything, and it says so rather than
guessing: across the full registry, ~67% of endpoints get a domain, ~87% a measure type,
~61% a parsed time horizon. The rest is a genuine long tail of disease-specific measures
("Swollen Joint Count", "Bowel Filling Properties: Distension/Distal Ileum") and is labelled
unclassified with raw text retained.
normalization_confidence exists so you can pick your own precision/recall tradeoff.
Being a good citizen
NLM asks for no more than 20 requests/second per IP. ctgov defaults to roughly 3/s and
retries with exponential backoff. Please set a contactable user agent:
Client(user_agent="my-project (me@example.com)")
Relationship to pytrials
pytrials is a good, focused API wrapper. ctgov is not
a replacement for it so much as a different scope: pagination past the 1,000 cap, and a
normalization layer over the free-text outcome fields. If you only need to pull a few hundred
studies as a DataFrame, pytrials is a smaller dependency and will serve you fine.
Licence and attribution
MIT. Source data is ClinicalTrials.gov (U.S. National Library of Medicine), public domain.
This product uses publicly available data from the U.S. National Library of Medicine (NLM), National Institutes of Health, Department of Health and Human Services; NLM is not responsible for the product and does not endorse or recommend this or any other product.
Metadata
Release files for ctgov 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ctgov-0.1.0.tar.gz | 13.9 kB | Without provenance | Details for ctgov-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ctgov-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for ctgov-0.1.0-py3-none-any.whl |
Total release size: 28.3 kB
Release files / ctgov-0.1.0.tar.gz
| Download URL | ctgov-0.1.0.tar.gz |
|---|---|
| Size | 13.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d0db529c5035e04e0d6740f874135c3eeb9a7df5ed810cd39018300311e8da29
|
|
BLAKE2b-256 checksum How to use checksums |
a9450abcfb754282a5166aed3831b19ad5757fdc7e743f9be3e614fcc6be0b3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / ctgov-0.1.0-py3-none-any.whl
| Download URL | ctgov-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0975cf6077ce686ba6ab8a283205da5ecdcf094254aaf6610e555f59d75b5404
|
|
BLAKE2b-256 checksum How to use checksums |
9df97f1239a3d4671cf353b941e0941b2843ada98246de274dd278e83f830b0d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for ctgov 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.