A Python command-line tool that screens clinical trial participants against a study protocol's inclusion and exclusion criteria, and generates an eligibility report for each one.
This project was built as a first exercise in applying core programming concepts — classes, modules, and separation of concerns — to a realistic, structured problem: automating patient eligibility screening for a clinical trial.
- Background Information
- Inclusion Criteria
- Exclusion Criteria
- Project Structure
- How It Works
- Getting Started
- Usage Example
- Design Decisions
- Limitations & Future Improvements
A Phase II, Open-Label Study Evaluating the Safety and Efficacy of an Investigational Immunotherapy in Patients with Advanced Non-Small Cell Lung Cancer (NSCLC)
Lung cancer remains one of the leading causes of cancer-related mortality worldwide, with non-small cell lung cancer (NSCLC) representing the majority of diagnosed cases. Despite advances in targeted therapies and immune checkpoint inhibitors, many patients with advanced disease eventually develop resistance or experience disease progression, highlighting the need for new therapeutic approaches.
This Phase II clinical trial aims to evaluate the safety, tolerability, and preliminary efficacy of an investigational immunotherapy agent in adult patients with advanced or metastatic NSCLC who have progressed after standard-of-care treatment. The study will assess whether the investigational therapy can provide meaningful clinical benefit, measured through endpoints such as objective response rate (ORR), progression-free survival (PFS), and safety outcomes.
Patient selection is based on predefined eligibility criteria to ensure that enrolled participants represent the intended study population while maintaining patient safety. These criteria include demographic characteristics, cancer diagnosis and stage, previous treatments, biomarker expression, performance status, and adequate organ function.
The development of automated patient screening tools can support clinical research teams by improving the identification of potentially eligible participants, reducing manual screening workload, and accelerating patient recruitment.
- Phase: II
- Study ID: NSCLC-001
- Therapeutic area: Oncology
- Study type: Interventional clinical trial
- Condition: Advanced Non-Small Cell Lung Cancer (NSCLC)
- Population: Adult patients with metastatic disease
- Intervention: Investigational immunotherapy agent
- Primary objective: Evaluate preliminary efficacy and safety
- Estimated Enrollment: 120 participants
- Study duration: 24 months
- Age >= 18 years
- Ability to provide informed consent
- Histologically confirmed diagnosis of NSCLC
- Stage IV or unresectable locally advanced disease
- At least one measurable lesion according to RECIST 1.1
- Disease progression after at least one previous systemic therapy
- Previous treatment with platinum-based chemotherapy required
- PD-L1 expression ≥ 50% (tumor proportion score)
- ECOG Performance Status 0–1
- Life expectancy ≥ 12 weeks
- Neutrophils ≥ 1,500/mm³
- Platelets ≥ 100,000/mm³
- AST/ALT ≤ 2.5 × ULN
- Total bilirubin ≤ 1.5 mg/dL
- Creatinine clearance ≥ 50 mL/min
- Active autoimmune disease requiring systemic immunosuppressive therapy
- Untreated or symptomatic brain metastases
- Previous treatment with the same investigational agent
- Active uncontrolled infection
- History of another malignancy within the previous 3 years
- Pregnancy or breastfeeding
ct-screening-tool/
├── main.py # Entry point: collects participant data and managed the flow
├── participant.py # Participant class - stores data for one participant
├── protocol.py # Study protocol - inclusion/exclusion criteria as constants
├── eligibility.py # Decision logic - checks a Participant against protocol.py
├── validators.py # Reusable input validation functions (text, int, float, choice)
├── reports.py # Formats and prints individual and summary eligibility reports
└── README.md
Each file has a single, well-defined responsibility:
| File | Responsibility |
|---|---|
participant.py |
Holds data for one participant. No logic. |
protocol.py |
Holds the trial's eligibility criteria as named constants. No logic. |
validators.py |
Repeatedly prompts the user until a valid value is entered. No knowledge of the trial's specific criteria. |
eligibility.py |
Pure decision logic: takes a Participant, compares it against protocol.py, returns whether they're eligible and why (if not). |
reports.py |
Takes the result from eligibility.py and formats it for display. No decision-making. |
main.py |
Orchestrates everything: calls the input functions, then eligibility.py, then reports.py. |
main.py
│
├── collects participant data (via validators.py)
│ ↓
├── creates a Participant object (participant.py)
│ ↓
├── passes it to eligibility.is_eligible() (eligibility.py)
│ │
│ └── compares Participant's attributes against protocol.py's criteria
│ ↓
└── passes the result to reports.py, which prints the final report
Because eligibility.py never touches input() or print(), it can be tested independently of the console — for example, feeding it hand-crafted Participant objects and checking the output, without needing to type anything.
- Python 3.8 or later
- No external dependencies — the project only uses the Python standard library
git clone https://github.com/tomasgeraldes/Clinical-Trial-Screening-Tool.git
cd ct_screening_toolpython main.pyOn startup, you'll see a menu:
1 - Introduce new participant
2 - Load sample participants
3 - Finish and generate reports
Option 2 loads 5 pre-built sample participants (1 eligible, 4 non-eligible for different reasons) — a quick way to see the tool in action without manually entering data.
Loading the sample participants (option 2) produces output like this:
============================================================
ELIGIBILITY REPORT - John Smith (ID: P001)
============================================================
Age: 65 | Gender: M | Stage: IV
Result: ELEGIBLE
============================================================
============================================================
ELIGIBILITY REPORT - Anna Lee (ID: P002)
============================================================
Age: 16 | Gender: F | Stage: IV
Result: NOT ELEGIBLE
Criteria failed:
- Age below the minimum required (18 years old).
- PD-L1 expression insufficient (minimum: 50%).
============================================================
============================================================
ELIGIBILITY REPORT - Hudson Brown (ID: P003)
============================================================
Age: 58 | Gender: M | Stage: ULA
Result: NOT ELEGIBLE
Criteria failed:
- Active autoimmune disease requiring systemic immunosuppression therapy (exclusion criterion)
============================================================
...followed by a final summary of how many participants were eligible overall.
- Separation of data, logic, and presentation.
participant.pyandprotocol.pyonly hold data;eligibility.pyonly makes decisions;reports.pyonly formats output. This means, for example, that the eligibility rules can be unit-tested without any console interaction, and the output format can be changed (e.g., to write to a file) without touching the decision logic. - Protocol as configuration, not hardcoded logic. All eligibility thresholds live in
protocol.pyas plain constants. If the trial's criteria change, only that file needs editing —eligibility.pydoesn't need to change. - A single renal function cutoff, not gender-specific. Reference ranges for creatinine clearance differ by sex and age in the general population, but clinical trial eligibility typically uses a single safety cutoff, since the value entered is generally already derived from a formula that accounts for age, sex, and weight. This was a deliberate choice, not an oversight.
- Full failure reporting, not fail-fast.
check_inclusion()andcheck_exclusion()collect all failed criteria rather than stopping at the first one, so the final report shows a participant's complete picture — closer to how a real screening report would be used.
- Command-line only. All interaction happens via the terminal. A natural next step would be a web-based interface (e.g., with Streamlit) built on top of the same
eligibility.pyandprotocol.py, without changing the decision logic. - No persistence. Participant data and reports exist only for the duration of a single run; nothing is saved to disk. Exporting reports to CSV or a text file would be a straightforward addition.
- No automated tests yet. Since
eligibility.pyis pure logic with no I/O, it's a good candidate for unit tests (e.g., withpytest) — a planned next step. - Single-protocol design. The tool currently supports one hardcoded protocol (
protocol.py). Supporting multiple trials would require loading protocol definitions dynamically (e.g., from JSON or YAML files) instead of a single Python module.