OpenMedica by IntelMedica.ai
Back to Skills
Mental Health Caution High Evidence

PHQ-9 Depression Screening Tool

by Open Medical Skills Community

Description

Validated Patient Health Questionnaire-9 (PHQ-9) for depression screening and severity assessment. Automatic scoring and interpretation with suicide risk flagging.

Quick Install

Run in Manus
View Source

Installation

npx skills add Open-Medica/open-medical-skills --skill phq9-depression-screening

Skill Files

Python
#!/usr/bin/env python3
"""
PHQ-9 Depression Screening Tool
=================================
Validated Patient Health Questionnaire-9 (PHQ-9) for depression screening,
severity assessment, and treatment monitoring with automatic suicide risk
flagging (Item 9).

Clinical Purpose:
    The PHQ-9 is the most widely used depression screening instrument in
    primary care. It serves dual purposes: (1) establishing a probable
    diagnosis of major depressive disorder using DSM criteria, and (2)
    grading depression severity for treatment selection and monitoring.
    Item 9 screens for suicidal ideation and requires immediate attention.

References:
    - Kroenke K, Spitzer RL, Williams JBW. "The PHQ-9: Validity of a Brief
      Depression Severity Measure." J Gen Intern Med. 2001;16(9):606-613.
    - Lowe B, et al. "Monitoring Depression Treatment Outcomes with the
      PHQ-9." Med Care. 2004;42(12):1194-1201.
    - Manea L, et al. "Optimal Cut-off Score for Diagnosing Depression with
      the PHQ-9: A Meta-Analysis." CMAJ. 2012;184(3):E191-E196.
    - USPSTF. Depression Screening in Adults. JAMA. 2016;315(4):380-387.

DISCLAIMER: The PHQ-9 is a screening and monitoring tool. A positive screen
requires clinical confirmation. Item 9 (suicidal ideation) MUST be followed
up with a comprehensive suicide risk assessment. If a patient endorses
suicidal thoughts, ensure immediate safety evaluation.
"""

import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional


# PHQ-9 Question Items (Kroenke et al., 2001)
# Maps directly to DSM-5 criteria for Major Depressive Episode
PHQ9_ITEMS = [
    {
        "number": 1,
        "text": "Little interest or pleasure in doing things",
        "dsm_criterion": "Anhedonia (Criterion A2)",
    },
    {
        "number": 2,
        "text": "Feeling down, depressed, or hopeless",
        "dsm_criterion": "Depressed mood (Criterion A1)",
    },
    {
        "number": 3,
        "text": "Trouble falling or staying asleep, or sleeping too much",
        "dsm_criterion": "Sleep disturbance (Criterion A4)",
    },
    {
        "number": 4,
        "text": "Feeling tired or having little energy",
        "dsm_criterion": "Fatigue (Criterion A6)",
    },
    {
        "number": 5,
        "text": "Poor appetite or overeating",
        "dsm_criterion": "Appetite/weight change (Criterion A3)",
    },
    {
        "number": 6,
        "text": "Feeling bad about yourself - or that you are a failure or "
                "have let yourself or your family down",
        "dsm_criterion": "Worthlessness/guilt (Criterion A7)",
    },
    {
        "number": 7,
        "text": "Trouble concentrating on things, such as reading the newspaper "
                "or watching television",
        "dsm_criterion": "Diminished concentration (Criterion A8)",
    },
    {
        "number": 8,
        "text": "Moving or speaking so slowly that other people could have noticed? "
                "Or the opposite - being so fidgety or restless that you have been "
                "moving around a lot more than usual",
        "dsm_criterion": "Psychomotor agitation/retardation (Criterion A5)",
    },
    {
        "number": 9,
        "text": "Thoughts that you would be better off dead, or of hurting yourself "
                "in some way",
        "dsm_criterion": "Suicidal ideation (Criterion A9)",
        "safety_critical": True,
    },
]

RESPONSE_OPTIONS = {
    0: "Not at all",
    1: "Several days",
    2: "More than half the days",
    3: "Nearly every day",
}

# Severity thresholds (Kroenke et al., 2001)
SEVERITY_THRESHOLDS = [
    (0, 4, "none_minimal", "None to minimal depression"),
    (5, 9, "mild", "Mild depression"),
    (10, 14, "moderate", "Moderate depression"),
    (15, 19, "moderately_severe", "Moderately severe depression"),
    (20, 27, "severe", "Severe depression"),
]

# PHQ-2 screening subset (items 1 and 2)
PHQ2_THRESHOLD = 3  # Score >= 3 warrants full PHQ-9

# Functional impairment question
FUNCTIONAL_QUESTION = (
    "If you checked off any problems, how difficult have these problems "
    "made it for you to do your work, take care of things at home, or "
    "get along with other people?"
)


@dataclass
class PHQ9Result:
    responses: list
    total_score: int = 0
    severity: str = ""
    severity_label: str = ""
    item9_score: int = 0
    suicide_risk_flag: bool = False
    phq2_score: int = 0
    phq2_positive: bool = False
    meets_dsm_criteria: bool = False
    functional_impairment: Optional[int] = None
    recommendations: list = field(default_factory=list)
    safety_actions: list = field(default_factory=list)
    date_administered: str = ""

    def __post_init__(self):
        if not self.date_administered:
            self.date_administered = datetime.now().strftime("%Y-%m-%d")


def validate_responses(responses: list) -> tuple:
    """Validate PHQ-9 responses."""
    if not isinstance(responses, list):
        return False, "Responses must be a list of 9 integers (0-3)"
    if len(responses) != 9:
        return False, f"Expected 9 responses, got {len(responses)}"
    for i, r in enumerate(responses):
        if not isinstance(r, int) or r < 0 or r > 3:
            return False, f"Response {i+1} must be an integer 0-3, got {r}"
    return True, ""


def check_dsm_criteria(responses: list) -> dict:
    """
    Check if PHQ-9 responses suggest major depressive disorder per DSM-5.

    MDD requires:
    - >= 5 symptoms present "more than half the days" (score >= 2)
    - At least one symptom is depressed mood (item 2) or anhedonia (item 1)
    - Symptoms cause functional impairment
    """
    symptoms_present = sum(1 for r in responses if r >= 2)
    has_core_symptom = responses[0] >= 2 or responses[1] >= 2

    meets_criteria = symptoms_present >= 5 and has_core_symptom

    return {
        "symptoms_at_threshold": symptoms_present,
        "required_symptoms": 5,
        "has_core_symptom": has_core_symptom,
        "core_symptoms": {
            "anhedonia_item1": responses[0] >= 2,
            "depressed_mood_item2": responses[1] >= 2,
        },
        "probable_mdd": meets_criteria,
        "note": ("PHQ-9 suggests probable MDD. Clinical interview required to "
                 "confirm diagnosis, assess duration, and rule out other causes."
                 if meets_criteria else
                 "Does not meet PHQ-9 threshold for probable MDD."),
    }


def score_phq9(responses: list, functional_impairment: int = None) -> PHQ9Result:
    """
    Score the PHQ-9 questionnaire.

    Args:
        responses: List of 9 integers (0-3)
        functional_impairment: Difficulty score (0-3)

    Returns:
        PHQ9Result with score, severity, and clinical recommendations
    """
    is_valid, error = validate_responses(responses)
    if not is_valid:
        raise ValueError(error)

    total = sum(responses)
    phq2_score = responses[0] + responses[1]
    item9_score = responses[8]

    # Determine severity
    severity = ""
    severity_label = ""
    for low, high, sev, label in SEVERITY_THRESHOLDS:
        if low <= total <= high:
            severity = sev
            severity_label = label
            break

    # DSM criteria check
    dsm_check = check_dsm_criteria(responses)

    result = PHQ9Result(
        responses=responses,
        total_score=total,
        severity=severity,
        severity_label=severity_label,
        item9_score=item9_score,
        suicide_risk_flag=item9_score > 0,
        phq2_score=phq2_score,
        phq2_positive=phq2_score >= PHQ2_THRESHOLD,
        meets_dsm_criteria=dsm_check["probable_mdd"],
        functional_impairment=functional_impairment,
    )

    # Safety actions for Item 9
    if item9_score > 0:
        result.safety_actions = _generate_safety_actions(item9_score)

    result.recommendations = _generate_recommendations(result)
    return result


def _generate_safety_actions(item9_score: int) -> list:
    """
    Generate immediate safety actions for any endorsement of Item 9.

    ANY positive response on Item 9 requires follow-up, regardless of
    the overall PHQ-9 score.
    """
    actions = [
        {
            "priority": "IMMEDIATE",
            "action": "Conduct suicide risk assessment",
            "details": [
                "Ask directly about suicidal thoughts, plan, intent, and means",
                "Use structured tool: Columbia Suicide Severity Rating Scale (C-SSRS)",
                "Assess protective factors (social support, reasons for living)",
                "Determine level of risk (low, moderate, high, imminent)",
            ],
        },
        {
            "priority": "IMMEDIATE",
            "action": "Ensure patient safety",
            "details": [
                "Do NOT leave a high-risk patient alone",
                "Remove access to lethal means if possible",
                "Contact behavioral health or psychiatry for consultation",
                "If imminent risk: emergency psychiatric evaluation",
            ],
        },
        {
            "priority": "DOCUMENT",
            "action": "Document and communicate",
            "details": [
                "Document the positive screen and follow-up assessment",
                "Communicate risk level to treatment team",
                "Develop or update safety plan with patient",
                "Provide crisis resources: 988 Suicide & Crisis Lifeline",
            ],
        },
    ]

    if item9_score >= 2:
        actions.insert(0, {
            "priority": "CRITICAL",
            "action": "ELEVATED SUICIDE RISK - Patient endorses suicidal thoughts "
                      "more than half the days or nearly every day",
            "details": [
                "Immediate in-person safety evaluation required",
                "Consider inpatient psychiatric admission",
                "Notify attending physician immediately",
                "1:1 observation until safety assessment completed",
            ],
        })

    return actions


def _generate_recommendations(result: PHQ9Result) -> list:
    """Generate treatment recommendations based on PHQ-9 severity."""
    recs = []

    if result.severity == "none_minimal":
        recs.append({
            "category": "monitoring",
            "recommendation": "No treatment indicated for depression at this time",
            "details": "Rescreen as clinically indicated or at routine visits. "
                       "USPSTF recommends screening all adults for depression.",
        })

    elif result.severity == "mild":
        recs.append({
            "category": "watchful_waiting",
            "recommendation": "Watchful waiting with active follow-up",
            "details": "Repeat PHQ-9 in 2-4 weeks. If persistent, consider active treatment.",
        })
        recs.append({
            "category": "non_pharmacological",
            "recommendation": "Psychoeducation and lifestyle interventions",
            "details": "Regular physical exercise (150 min/week), sleep hygiene, "
                       "social engagement, stress management. Consider guided self-help (CBT-based).",
        })

    elif result.severity == "moderate":
        recs.append({
            "category": "psychotherapy",
            "recommendation": "Psychotherapy (first-line for moderate depression)",
            "details": "CBT or interpersonal therapy (IPT). 12-16 sessions. "
                       "Evidence supports psychotherapy as first-line for moderate depression "
                       "(APA Practice Guidelines 2010).",
        })
        recs.append({
            "category": "pharmacotherapy",
            "recommendation": "Consider antidepressant if psychotherapy unavailable or patient preference",
            "details": "First-line: SSRI (sertraline 50 mg, escitalopram 10 mg). "
                       "Allow 4-6 weeks for adequate trial. Monitor for side effects "
                       "and worsening in first 1-2 weeks.",
        })
        recs.append({
            "category": "monitoring",
            "recommendation": "Repeat PHQ-9 every 2-4 weeks during treatment",
            "details": "A decrease of >= 5 points indicates treatment response. "
                       "Target score < 5 (remission).",
        })

    elif result.severity == "moderately_severe":
        recs.append({
            "category": "combined_treatment",
            "recommendation": "Combined psychotherapy and pharmacotherapy recommended",
            "details": "Combined treatment is more effective than either alone for "
                       "moderately severe depression (Cuijpers et al., 2014).",
        })
        recs.append({
            "category": "pharmacotherapy",
            "recommendation": "Initiate SSRI/SNRI with close follow-up",
            "details": "First-line: sertraline 50-200 mg, escitalopram 10-20 mg, "
                       "or duloxetine 60-120 mg. Follow up in 1-2 weeks, then every 2-4 weeks. "
                       "Assess for suicidality at each visit (especially ages 18-24).",
        })
        recs.append({
            "category": "safety",
            "recommendation": "Assess suicide risk at every visit",
            "details": "Moderately severe depression increases suicide risk. "
                       "Use C-SSRS or Ask Suicide-Screening Questions (ASQ) at each contact.",
        })

    elif result.severity == "severe":
        recs.append({
            "category": "urgent_treatment",
            "recommendation": "URGENT: Initiate treatment immediately",
            "details": "Severe depression is associated with significant functional "
                       "impairment and elevated suicide risk. Do not delay treatment.",
        })
        recs.append({
            "category": "pharmacotherapy",
            "recommendation": "Start antidepressant medication promptly",
            "details": "SSRI/SNRI first-line. Consider mirtazapine for insomnia/appetite loss. "
                       "Bupropion if fatigue/concentration predominant. "
                       "Follow up within 1 week of initiation.",
        })
        recs.append({
            "category": "psychotherapy",
            "recommendation": "Concurrent psychotherapy when patient can engage",
            "details": "CBT or behavioral activation. May need to stabilize with "
                       "medication before patient can fully participate in therapy.",
        })
        recs.append({
            "category": "referral",
            "recommendation": "Psychiatric consultation recommended",
            "details": "For severe depression, consider referral to psychiatry "
                       "for medication management, especially if suicidal ideation present, "
                       "psychotic features, or treatment resistance.",
        })
        recs.append({
            "category": "safety",
            "recommendation": "Comprehensive suicide risk assessment required",
            "details": "Severe depression significantly elevates suicide risk. "
                       "Assess at every contact. Consider inpatient evaluation if imminent risk.",
        })

    # Comorbidity screening
    if result.total_score >= 10:
        recs.append({
            "category": "comorbidity",
            "recommendation": "Screen for common comorbidities",
            "details": "Screen for: anxiety (GAD-7), substance use (AUDIT-C, DAST-10), "
                       "PTSD (PC-PTSD-5), bipolar disorder (MDQ). "
                       "Assess thyroid function, vitamin D, B12 if not recently checked.",
        })

    return recs


def score_phq2(item1: int, item2: int) -> dict:
    """
    Score the PHQ-2 (ultra-brief depression screen).

    Sensitivity 83%, specificity 92% for MDD at cutoff >= 3.
    """
    total = item1 + item2
    return {
        "instrument": "PHQ-2",
        "score": total,
        "positive_screen": total >= PHQ2_THRESHOLD,
        "recommendation": (
            "Positive screen. Administer full PHQ-9."
            if total >= PHQ2_THRESHOLD
            else "Negative screen. Rescreen per clinical protocol."
        ),
        "reference": "Kroenke K, et al. Med Care. 2003;41(11):1284-1292.",
    }


def track_treatment_response(scores: list) -> dict:
    """
    Track PHQ-9 scores longitudinally for treatment monitoring.

    Response: >= 50% reduction from baseline
    Remission: PHQ-9 < 5
    Clinically meaningful change: >= 5-point decrease
    """
    if len(scores) < 2:
        return {"error": "At least 2 scores needed for tracking"}

    baseline = scores[0]
    current = scores[-1]
    change = current - baseline
    pct_change = ((current - baseline) / max(baseline, 1)) * 100

    response = pct_change <= -50 or change <= -5
    remission = current < 5

    # Assess trajectory
    trajectory = []
    for i in range(1, len(scores)):
        diff = scores[i] - scores[i-1]
        if diff <= -5:
            trajectory.append("significant_improvement")
        elif diff < -2:
            trajectory.append("mild_improvement")
        elif diff > 5:
            trajectory.append("significant_worsening")
        elif diff > 2:
            trajectory.append("mild_worsening")
        else:
            trajectory.append("stable")

    # Treatment recommendations based on trajectory
    if remission:
        tx_rec = ("Remission achieved. Continue current treatment for 4-9 months "
                  "to prevent relapse (maintenance phase). Gradually taper under "
                  "medical supervision.")
    elif response:
        tx_rec = ("Treatment response achieved but not yet in remission. "
                  "Optimize current treatment: increase dose, add psychotherapy, "
                  "or augment medication.")
    elif len(scores) >= 3 and all(t in ["stable", "mild_worsening", "significant_worsening"]
                                   for t in trajectory[-2:]):
        tx_rec = ("Insufficient response after adequate trial. Consider: "
                  "dose optimization, switching medication, augmentation strategy, "
                  "or re-evaluation of diagnosis.")
    else:
        tx_rec = "Continue current treatment and reassess in 2-4 weeks."

    return {
        "baseline_score": baseline,
        "current_score": current,
        "absolute_change": change,
        "percent_change": round(pct_change, 1),
        "treatment_response": response,
        "remission": remission,
        "trajectory": trajectory,
        "treatment_recommendation": tx_rec,
        "total_assessments": len(scores),
        "weeks_in_treatment": (len(scores) - 1) * 2,  # Assuming Q2 week assessments
    }


def get_phq9_questionnaire() -> dict:
    """Return the full PHQ-9 questionnaire."""
    return {
        "instrument": "PHQ-9",
        "reference": "Kroenke K, et al. J Gen Intern Med. 2001;16(9):606-613.",
        "instructions": (
            "Over the LAST 2 WEEKS, how often have you been bothered by "
            "any of the following problems? Rate each item from 0 to 3."
        ),
        "response_options": RESPONSE_OPTIONS,
        "items": PHQ9_ITEMS,
        "functional_question": {
            "text": FUNCTIONAL_QUESTION,
            "options": {
                0: "Not difficult at all",
                1: "Somewhat difficult",
                2: "Very difficult",
                3: "Extremely difficult",
            },
        },
        "scoring": {
            "range": "0-27",
            "thresholds": {t[2]: f"{t[0]}-{t[1]}" for t in SEVERITY_THRESHOLDS},
            "screening_cutoff": "Score >= 10 (sensitivity 88%, specificity 88% for MDD)",
        },
        "safety_note": "Item 9 screens for suicidal ideation. ANY positive response "
                       "requires immediate follow-up safety assessment.",
    }


def run_phq9(action: str, **kwargs) -> str:
    """
    Main entry point for the PHQ-9 Depression Screening Tool.

    Actions:
        questionnaire: Get the PHQ-9 questionnaire
        score: Score a completed PHQ-9 (responses: list of 9 ints)
        phq2: Score PHQ-2 brief screen (item1, item2)
        track: Track longitudinal scores (scores: list)
    """
    if action == "questionnaire":
        result = get_phq9_questionnaire()
    elif action == "score":
        try:
            phq9_result = score_phq9(
                kwargs.get("responses", []),
                kwargs.get("functional_impairment"),
            )
            result = {
                "total_score": phq9_result.total_score,
                "severity": phq9_result.severity,
                "severity_label": phq9_result.severity_label,
                "screening_positive": phq9_result.total_score >= 10,
                "meets_dsm_criteria": phq9_result.meets_dsm_criteria,
                "phq2_score": phq9_result.phq2_score,
                "item9_suicidal_ideation": {
                    "score": phq9_result.item9_score,
                    "endorsed": phq9_result.suicide_risk_flag,
                    "response_label": RESPONSE_OPTIONS.get(phq9_result.item9_score, ""),
                },
                "safety_actions": phq9_result.safety_actions,
                "recommendations": phq9_result.recommendations,
                "date": phq9_result.date_administered,
                "item_responses": {
                    PHQ9_ITEMS[i]["text"][:60]: {
                        "score": r,
                        "label": RESPONSE_OPTIONS[r],
                        "dsm_criterion": PHQ9_ITEMS[i]["dsm_criterion"],
                    }
                    for i, r in enumerate(phq9_result.responses)
                },
            }
        except ValueError as e:
            result = {"error": str(e)}
    elif action == "phq2":
        result = score_phq2(kwargs.get("item1", 0), kwargs.get("item2", 0))
    elif action == "track":
        result = track_treatment_response(kwargs.get("scores", []))
    else:
        result = {
            "error": f"Unknown action: {action}",
            "available_actions": ["questionnaire", "score", "phq2", "track"],
        }

    return json.dumps(result, indent=2)


if __name__ == "__main__":
    # Moderately severe depression with suicidal ideation
    print("=== PHQ-9 Scoring (Moderately Severe with Item 9 Positive) ===")
    print(run_phq9("score", responses=[3, 3, 2, 2, 2, 3, 2, 1, 1],
                    functional_impairment=2))

Version

1.0.0

License

MIT

Status

Published

Reviewer

Pending Review

Date Added

2026-03-02

Specialties

Psychiatry Primary Care

Tags

depression screening phq-9 mental-health