pa-decision-automation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Prior Authorization Decision Automation Pipeline

事前授权决策自动化 Pipeline

Overview

概述

Provide deterministic code snippets and pipeline recipes for automating prior authorization (PA) workflows: parsing inbound requests, extracting clinical features, applying rules-based adjudication, training ML classifiers, and analyzing denial patterns.
提供确定性代码片段和 Pipeline 配方,用于自动化事前授权(PA)工作流:解析传入请求、提取临床特征、应用基于规则的 adjudication、训练 ML 分类器并分析拒付模式。

Usage

使用场景

  • Building or debugging X12 278 or FHIR PAS bundle parsers for PA intake
  • Training ML classifiers on historical PA decisions or generating SHAP explanations
  • Analyzing denial patterns to identify systemic documentation or policy gaps
  • 构建或调试用于 PA 接收的 X12 278 或 FHIR PAS Bundle 解析器
  • 在历史 PA 决策上训练 ML 分类器,或生成 SHAP 解释
  • 分析拒付模式,以发现系统性的文档或政策缺口

Core Concepts

核心概念

Automation Approach Selection

自动化方法选择

ConditionApproachRationale
Clear-cut policy rules (step therapy, age, lab threshold)Rules engineAuditable, deterministic, regulatory-safe
Ambiguous cases with soft criteriaML classifier + SHAPHandles nuance; SHAP provides explainability
< 1000 historical decisions availableRules engine onlyInsufficient data for reliable ML training
Multi-payer deploymentSeparate model per payerPolicies differ; cross-payer models fail
Input is X12 278 (EDI)
parse_278()
→ segment iteration
Pipe-delimited, segment-based
Input is FHIR PAS Bundle
parse_pas_bundle()
→ resource extraction
JSON, resource-typed entries
条件方法理由
明确、固定的政策规则(阶梯治疗、年龄、实验室指标阈值)规则引擎可审计、确定性、符合监管要求
含软性标准的模糊案例ML 分类器 + SHAP可处理细微差异;SHAP 提供可解释性
可用的历史决策少于 1000 条仅使用规则引擎数据不足以可靠地训练 ML 模型
多付款方部署每个付款方使用独立模型政策存在差异;跨付款方模型会失效
输入为 X12 278(EDI)
parse_278()
→ segment 迭代
管道符分隔、基于 segment
输入为 FHIR PAS Bundle
parse_pas_bundle()
→ resource 提取
JSON、包含带类型的 resource 条目

Response Format

响应格式

  • Lead with the command or code the user needs — explain after
  • Structure as: confirm inputs → working code → key parameters explained → gotchas
  • One complete working example per task; do not show every alternative
  • Keep code comments minimal and functional (what, not why-it-exists)
  • Target: 50-100 lines of code with brief surrounding explanation
  • 首先给出用户所需的命令或代码——说明放在后面
  • 按以下结构组织:确认输入 → 可运行代码 → 关键参数说明 → 注意事项
  • 每项任务提供一个完整可运行示例;不要展示每一种备选方案
  • 将代码注释控制在最少且实用的程度(说明做什么,而非为何存在)
  • 目标:50–100 行代码,并附简短说明

1. PA Request Parsing

1. PA 请求解析

1.1 Parse X12 278 Transaction

1.1 解析 X12 278 事务

The X12 278 Health Care Services Review transaction carries PA requests and responses.
python
"""Parse X12 278 prior authorization request into structured dict."""
import re
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class PA278Request:
    member_id: str = ""
    provider_npi: str = ""
    diagnosis_codes: list[str] = field(default_factory=list)
    procedure_codes: list[str] = field(default_factory=list)
    service_date: str = ""
    quantity: int = 0
    place_of_service: str = ""


def parse_278(raw: str) -> PA278Request:
    """Parse X12 278 segments into a PA278Request."""
    req = PA278Request()
    segments = raw.replace("\n", "").split("~")
    for seg in segments:
        elements = seg.strip().split("*")
        seg_id = elements[0] if elements else ""
        if seg_id == "NM1" and len(elements) > 9:
            qualifier = elements[1]
            if qualifier == "IL":  # insured/member
                req.member_id = elements[9] if len(elements) > 9 else ""
            elif qualifier == "1P":  # provider
                req.provider_npi = elements[9] if len(elements) > 9 else ""
        elif seg_id == "HI":
            for el in elements[1:]:
                parts = el.split(":")
                if len(parts) >= 2:
                    code_qualifier, code = parts[0], parts[1]
                    if code_qualifier in ("ABK", "ABF"):  # ICD-10
                        req.diagnosis_codes.append(code)
        elif seg_id == "SV1" and len(elements) > 1:
            svc_parts = elements[1].split(":")
            if len(svc_parts) >= 2:
                req.procedure_codes.append(svc_parts[1])
        elif seg_id == "DTP" and len(elements) > 3:
            if elements[1] == "472":  # service date
                req.service_date = elements[3]
    return req
X12 278 医疗保健服务审查事务用于承载 PA 请求和响应。
python
"""Parse X12 278 prior authorization request into structured dict."""
import re
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class PA278Request:
    member_id: str = ""
    provider_npi: str = ""
    diagnosis_codes: list[str] = field(default_factory=list)
    procedure_codes: list[str] = field(default_factory=list)
    service_date: str = ""
    quantity: int = 0
    place_of_service: str = ""


def parse_278(raw: str) -> PA278Request:
    """Parse X12 278 segments into a PA278Request."""
    req = PA278Request()
    segments = raw.replace("\n", "").split("~")
    for seg in segments:
        elements = seg.strip().split("*")
        seg_id = elements[0] if elements else ""
        if seg_id == "NM1" and len(elements) > 9:
            qualifier = elements[1]
            if qualifier == "IL":  # insured/member
                req.member_id = elements[9] if len(elements) > 9 else ""
            elif qualifier == "1P":  # provider
                req.provider_npi = elements[9] if len(elements) > 9 else ""
        elif seg_id == "HI":
            for el in elements[1:]:
                parts = el.split(":")
                if len(parts) >= 2:
                    code_qualifier, code = parts[0], parts[1]
                    if code_qualifier in ("ABK", "ABF"):  # ICD-10
                        req.diagnosis_codes.append(code)
        elif seg_id == "SV1" and len(elements) > 1:
            svc_parts = elements[1].split(":")
            if len(svc_parts) >= 2:
                req.procedure_codes.append(svc_parts[1])
        elif seg_id == "DTP" and len(elements) > 3:
            if elements[1] == "472":  # service date
                req.service_date = elements[3]
    return req

1.2 Parse FHIR Da Vinci PAS Bundle

1.2 解析 FHIR Da Vinci PAS Bundle

python
"""Extract PA fields from a FHIR Da Vinci PAS Bundle."""
import json
from dataclasses import dataclass, field


@dataclass
class PASRequest:
    member_id: str = ""
    provider_npi: str = ""
    diagnosis_codes: list[str] = field(default_factory=list)
    service_codes: list[str] = field(default_factory=list)
    supporting_info_types: list[str] = field(default_factory=list)


def parse_pas_bundle(bundle: dict) -> PASRequest:
    """Extract PA data from a FHIR PAS transaction Bundle."""
    req = PASRequest()
    resources = {
        entry["resource"]["resourceType"]: entry["resource"]
        for entry in bundle.get("entry", [])
        if "resource" in entry
    }
    # Patient / member
    patient = resources.get("Patient", {})
    for ident in patient.get("identifier", []):
        if ident.get("type", {}).get("coding", [{}])[0].get("code") == "MB":
            req.member_id = ident.get("value", "")
            break
    # Practitioner / provider NPI
    practitioner = resources.get("Practitioner", {})
    for ident in practitioner.get("identifier", []):
        if ident.get("system", "").endswith("/npi"):
            req.provider_npi = ident.get("value", "")
            break
    # Claim resource — core of the PA request
    claim = resources.get("Claim", {})
    for dx in claim.get("diagnosis", []):
        coding = dx.get("diagnosisCodeableConcept", {}).get("coding", [])
        for c in coding:
            req.diagnosis_codes.append(c.get("code", ""))
    for item in claim.get("item", []):
        svc_coding = item.get("productOrService", {}).get("coding", [])
        for c in svc_coding:
            req.service_codes.append(c.get("code", ""))
    for info in claim.get("supportingInfo", []):
        cat_coding = info.get("category", {}).get("coding", [])
        for c in cat_coding:
            req.supporting_info_types.append(c.get("code", ""))
    return req
python
"""Extract PA fields from a FHIR Da Vinci PAS Bundle."""
import json
from dataclasses import dataclass, field


@dataclass
class PASRequest:
    member_id: str = ""
    provider_npi: str = ""
    diagnosis_codes: list[str] = field(default_factory=list)
    service_codes: list[str] = field(default_factory=list)
    supporting_info_types: list[str] = field(default_factory=list)


def parse_pas_bundle(bundle: dict) -> PASRequest:
    """Extract PA data from a FHIR PAS transaction Bundle."""
    req = PASRequest()
    resources = {
        entry["resource"]["resourceType"]: entry["resource"]
        for entry in bundle.get("entry", [])
        if "resource" in entry
    }
    # Patient / member
    patient = resources.get("Patient", {})
    for ident in patient.get("identifier", []):
        if ident.get("type", {}).get("coding", [{}])[0].get("code") == "MB":
            req.member_id = ident.get("value", "")
            break
    # Practitioner / provider NPI
    practitioner = resources.get("Practitioner", {})
    for ident in practitioner.get("identifier", []):
        if ident.get("system", "").endswith("/npi"):
            req.provider_npi = ident.get("value", "")
            break
    # Claim resource — core of the PA request
    claim = resources.get("Claim", {})
    for dx in claim.get("diagnosis", []):
        coding = dx.get("diagnosisCodeableConcept", {}).get("coding", [])
        for c in coding:
            req.diagnosis_codes.append(c.get("code", ""))
    for item in claim.get("item", []):
        svc_coding = item.get("productOrService", {}).get("coding", [])
        for c in svc_coding:
            req.service_codes.append(c.get("code", ""))
    for info in claim.get("supportingInfo", []):
        cat_coding = info.get("category", {}).get("coding", [])
        for c in cat_coding:
            req.supporting_info_types.append(c.get("code", ""))
    return req

2. Clinical Feature Extraction

2. 临床特征提取

2.1 Feature Set for PA Adjudication

2.1 用于 PA adjudication 的特征集

FeatureSourceTypeDescription
dx_specificity
Diagnosis codesintICD-10 code length (3=category, 4-7=specific)
drug_class
Service code (NDC/HCPCS)categoricalTherapeutic class (e.g., biologic, opioid)
prior_treatment_count
Claims historyintNumber of prior drugs tried in same class
step_therapy_complete
Claims + formularyboolAll required prior steps documented
days_since_last_treatment
Claims historyintGap since last related treatment
lab_value_in_range
Lab resultsboolKey lab (e.g., HbA1c) meets threshold
provider_specialty
Provider datacategoricalSpecialty of ordering provider
place_of_service
ClaimcategoricalOffice, outpatient, inpatient, home
prior_pa_denials
PA historyintCount of prior denials for same service
documentation_score
Supporting infofloatCompleteness score (0-1) based on required docs
特征来源类型描述
dx_specificity
诊断代码intICD-10 代码长度(3=类别,4-7=具体)
drug_class
服务代码(NDC/HCPCS)categorical治疗类别(例如生物制剂、阿片类药物)
prior_treatment_count
理赔历史int同一类别中已尝试的既往药物数量
step_therapy_complete
理赔记录 + 药品目录bool所有必需的既往治疗步骤均已记录
days_since_last_treatment
理赔历史int距上次相关治疗的间隔天数
lab_value_in_range
实验室结果bool关键实验室指标(例如 HbA1c)达到阈值
provider_specialty
提供方数据categorical申请医生的专业类别
place_of_service
Claimcategorical诊所、门诊、住院、居家
prior_pa_denials
PA 历史int同一服务既往被拒绝的次数
documentation_score
支持信息float根据必需文档计算的完整度评分(0-1)

2.2 Feature Extraction Code

2.2 特征提取代码

python
"""Extract clinical features from parsed PA request + claims history."""
import pandas as pd
from datetime import datetime


def extract_features(
    pa_request: dict,
    claims_history: pd.DataFrame,
    formulary: pd.DataFrame,
    lab_results: pd.DataFrame,
) -> dict:
    """Build feature vector for PA adjudication model.

    Args:
        pa_request: Parsed PA request (from parse_278 or parse_pas_bundle).
        claims_history: Member's prior claims with columns:
            [member_id, service_date, drug_class, ndc, diagnosis_code].
        formulary: Formulary table with columns:
            [ndc, tier, step_therapy_required, prior_drugs_required].
        lab_results: Lab results with columns:
            [member_id, test_code, result_value, result_date].
    """
    member_id = pa_request.get("member_id", "")
    dx_codes = pa_request.get("diagnosis_codes", [])
    svc_codes = pa_request.get("service_codes", [])

    # Diagnosis specificity: max ICD-10 code length
    dx_specificity = max((len(c.replace(".", "")) for c in dx_codes), default=3)

    # Prior treatment count in same drug class
    requested_drug = svc_codes[0] if svc_codes else ""
    drug_info = formulary[formulary["ndc"] == requested_drug]
    drug_class = drug_info["drug_class"].iloc[0] if len(drug_info) > 0 else "unknown"
    member_claims = claims_history[claims_history["member_id"] == member_id]
    prior_treatments = member_claims[member_claims["drug_class"] == drug_class]
    prior_treatment_count = prior_treatments["ndc"].nunique()

    # Step therapy completion
    required_steps = int(drug_info["prior_drugs_required"].iloc[0]) if len(drug_info) > 0 else 0
    step_therapy_complete = prior_treatment_count >= required_steps

    # Days since last treatment in class
    if len(prior_treatments) > 0:
        last_date = pd.to_datetime(prior_treatments["service_date"]).max()
        days_since = (datetime.now() - last_date).days
    else:
        days_since = -1  # no prior treatment

    # Lab value check (example: HbA1c for diabetes drugs)
    member_labs = lab_results[lab_results["member_id"] == member_id]
    recent_lab = member_labs.sort_values("result_date", ascending=False).head(1)
    lab_in_range = bool(recent_lab["result_value"].iloc[0] >= 7.0) if len(recent_lab) > 0 else False

    # Documentation completeness score
    supporting_info = pa_request.get("supporting_info_types", [])
    required_docs = {"clinical-note", "lab-result", "treatment-history", "diagnosis"}
    doc_score = len(set(supporting_info) & required_docs) / len(required_docs)

    return {
        "dx_specificity": dx_specificity,
        "drug_class": drug_class,
        "prior_treatment_count": prior_treatment_count,
        "step_therapy_complete": step_therapy_complete,
        "days_since_last_treatment": days_since,
        "lab_value_in_range": lab_in_range,
        "documentation_score": doc_score,
    }
python
"""Extract clinical features from parsed PA request + claims history."""
import pandas as pd
from datetime import datetime


def extract_features(
    pa_request: dict,
    claims_history: pd.DataFrame,
    formulary: pd.DataFrame,
    lab_results: pd.DataFrame,
) -> dict:
    """Build feature vector for PA adjudication model.

    Args:
        pa_request: Parsed PA request (from parse_278 or parse_pas_bundle).
        claims_history: Member's prior claims with columns:
            [member_id, service_date, drug_class, ndc, diagnosis_code].
        formulary: Formulary table with columns:
            [ndc, tier, step_therapy_required, prior_drugs_required].
        lab_results: Lab results with columns:
            [member_id, test_code, result_value, result_date].
    """
    member_id = pa_request.get("member_id", "")
    dx_codes = pa_request.get("diagnosis_codes", [])
    svc_codes = pa_request.get("service_codes", [])

    # Diagnosis specificity: max ICD-10 code length
    dx_specificity = max((len(c.replace(".", "")) for c in dx_codes), default=3)

    # Prior treatment count in same drug class
    requested_drug = svc_codes[0] if svc_codes else ""
    drug_info = formulary[formulary["ndc"] == requested_drug]
    drug_class = drug_info["drug_class"].iloc[0] if len(drug_info) > 0 else "unknown"
    member_claims = claims_history[claims_history["member_id"] == member_id]
    prior_treatments = member_claims[member_claims["drug_class"] == drug_class]
    prior_treatment_count = prior_treatments["ndc"].nunique()

    # Step therapy completion
    required_steps = int(drug_info["prior_drugs_required"].iloc[0]) if len(drug_info) > 0 else 0
    step_therapy_complete = prior_treatment_count >= required_steps

    # Days since last treatment in class
    if len(prior_treatments) > 0:
        last_date = pd.to_datetime(prior_treatments["service_date"]).max()
        days_since = (datetime.now() - last_date).days
    else:
        days_since = -1  # no prior treatment

    # Lab value check (example: HbA1c for diabetes drugs)
    member_labs = lab_results[lab_results["member_id"] == member_id]
    recent_lab = member_labs.sort_values("result_date", ascending=False).head(1)
    lab_in_range = bool(recent_lab["result_value"].iloc[0] >= 7.0) if len(recent_lab) > 0 else False

    # Documentation completeness score
    supporting_info = pa_request.get("supporting_info_types", [])
    required_docs = {"clinical-note", "lab-result", "treatment-history", "diagnosis"}
    doc_score = len(set(supporting_info) & required_docs) / len(required_docs)

    return {
        "dx_specificity": dx_specificity,
        "drug_class": drug_class,
        "prior_treatment_count": prior_treatment_count,
        "step_therapy_complete": step_therapy_complete,
        "days_since_last_treatment": days_since,
        "lab_value_in_range": lab_in_range,
        "documentation_score": doc_score,
    }

3. Rules-Based Adjudication Engine

3. 基于规则的 adjudication 引擎

python
"""Rules-based PA adjudication engine."""
from dataclasses import dataclass
from enum import Enum


class Decision(Enum):
    APPROVE = "approve"
    DENY = "deny"
    PEND = "pend"


@dataclass
class AdjudicationResult:
    decision: Decision
    reason_code: str
    reason_text: str


def adjudicate(features: dict, policy: dict) -> AdjudicationResult:
    """Apply rules-based adjudication logic.

    Args:
        features: Feature dict from extract_features().
        policy: Policy config with keys:
            - require_step_therapy (bool)
            - min_dx_specificity (int)
            - min_documentation_score (float)
            - require_lab_in_range (bool)
    """
    # Rule 1: Documentation completeness
    if features["documentation_score"] < policy.get("min_documentation_score", 0.75):
        return AdjudicationResult(
            Decision.PEND, "PEND-001", "Insufficient documentation; request additional clinical records"
        )
    # Rule 2: Diagnosis specificity
    if features["dx_specificity"] < policy.get("min_dx_specificity", 4):
        return AdjudicationResult(
            Decision.DENY, "DENY-DX", "Diagnosis code lacks required specificity"
        )
    # Rule 3: Step therapy
    if policy.get("require_step_therapy", True) and not features["step_therapy_complete"]:
        return AdjudicationResult(
            Decision.DENY, "DENY-STEP", "Step therapy requirements not met"
        )
    # Rule 4: Lab value threshold
    if policy.get("require_lab_in_range", False) and not features["lab_value_in_range"]:
        return AdjudicationResult(
            Decision.DENY, "DENY-LAB", "Required lab value not within criteria range"
        )
    # All rules passed
    return AdjudicationResult(Decision.APPROVE, "APPROVE-001", "All clinical criteria met")
python
"""Rules-based PA adjudication engine."""
from dataclasses import dataclass
from enum import Enum


class Decision(Enum):
    APPROVE = "approve"
    DENY = "deny"
    PEND = "pend"


@dataclass
class AdjudicationResult:
    decision: Decision
    reason_code: str
    reason_text: str


def adjudicate(features: dict, policy: dict) -> AdjudicationResult:
    """Apply rules-based adjudication logic.

    Args:
        features: Feature dict from extract_features().
        policy: Policy config with keys:
            - require_step_therapy (bool)
            - min_dx_specificity (int)
            - min_documentation_score (float)
            - require_lab_in_range (bool)
    """
    # Rule 1: Documentation completeness
    if features["documentation_score"] < policy.get("min_documentation_score", 0.75):
        return AdjudicationResult(
            Decision.PEND, "PEND-001", "Insufficient documentation; request additional clinical records"
        )
    # Rule 2: Diagnosis specificity
    if features["dx_specificity"] < policy.get("min_dx_specificity", 4):
        return AdjudicationResult(
            Decision.DENY, "DENY-DX", "Diagnosis code lacks required specificity"
        )
    # Rule 3: Step therapy
    if policy.get("require_step_therapy", True) and not features["step_therapy_complete"]:
        return AdjudicationResult(
            Decision.DENY, "DENY-STEP", "Step therapy requirements not met"
        )
    # Rule 4: Lab value threshold
    if policy.get("require_lab_in_range", False) and not features["lab_value_in_range"]:
        return AdjudicationResult(
            Decision.DENY, "DENY-LAB", "Required lab value not within criteria range"
        )
    # All rules passed
    return AdjudicationResult(Decision.APPROVE, "APPROVE-001", "All clinical criteria met")

4. ML Classifier for PA Decisions

4. 用于 PA 决策的 ML 分类器

4.1 Training Pipeline

4.1 训练 Pipeline

python
"""Train a gradient-boosted classifier on historical PA decisions."""
import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import classification_report, roc_auc_score
import xgboost as xgb


def train_pa_classifier(
    data: pd.DataFrame, target_col: str = "decision",
    feature_cols: list[str] | None = None, n_folds: int = 5,
) -> tuple[xgb.XGBClassifier, pd.DataFrame]:
    """Train XGBoost on historical PA decisions (1=approved, 0=denied)."""
    if feature_cols is None:
        feature_cols = [c for c in data.columns if c != target_col]
    X, y = data[feature_cols].copy(), data[target_col].copy()
    cat_cols = X.select_dtypes(include=["object", "category"]).columns.tolist()
    X[cat_cols] = X[cat_cols].astype("category")
    model = xgb.XGBClassifier(
        n_estimators=300, max_depth=6, learning_rate=0.05,
        subsample=0.8, colsample_bytree=0.8, enable_categorical=True,
        eval_metric="logloss", random_state=42,
    )
    cv_results = []
    for fold, (ti, vi) in enumerate(StratifiedKFold(n_folds, shuffle=True, random_state=42).split(X, y)):
        model.fit(X.iloc[ti], y.iloc[ti], eval_set=[(X.iloc[vi], y.iloc[vi])], verbose=False)
        y_prob = model.predict_proba(X.iloc[vi])[:, 1]
        cv_results.append({"fold": fold, "auc": roc_auc_score(y.iloc[vi], y_prob)})
    model.fit(X, y, verbose=False)
    return model, pd.DataFrame(cv_results)
python
"""Train a gradient-boosted classifier on historical PA decisions."""
import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import classification_report, roc_auc_score
import xgboost as xgb


def train_pa_classifier(
    data: pd.DataFrame, target_col: str = "decision",
    feature_cols: list[str] | None = None, n_folds: int = 5,
) -> tuple[xgb.XGBClassifier, pd.DataFrame]:
    """Train XGBoost on historical PA decisions (1=approved, 0=denied)."""
    if feature_cols is None:
        feature_cols = [c for c in data.columns if c != target_col]
    X, y = data[feature_cols].copy(), data[target_col].copy()
    cat_cols = X.select_dtypes(include=["object", "category"]).columns.tolist()
    X[cat_cols] = X[cat_cols].astype("category")
    model = xgb.XGBClassifier(
        n_estimators=300, max_depth=6, learning_rate=0.05,
        subsample=0.8, colsample_bytree=0.8, enable_categorical=True,
        eval_metric="logloss", random_state=42,
    )
    cv_results = []
    for fold, (ti, vi) in enumerate(StratifiedKFold(n_folds, shuffle=True, random_state=42).split(X, y)):
        model.fit(X.iloc[ti], y.iloc[ti], eval_set=[(X.iloc[vi], y.iloc[vi])], verbose=False)
        y_prob = model.predict_proba(X.iloc[vi])[:, 1]
        cv_results.append({"fold": fold, "auc": roc_auc_score(y.iloc[vi], y_prob)})
    model.fit(X, y, verbose=False)
    return model, pd.DataFrame(cv_results)

4.2 SHAP Explainability

4.2 SHAP 可解释性

python
"""Generate SHAP explanations for PA decisions."""
import shap


def explain_pa_decision(model, X, instance_idx: int = 0) -> dict:
    """Generate SHAP values for a single PA decision."""
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X)
    explanation = dict(sorted(
        zip(X.columns, shap_values[instance_idx]),
        key=lambda x: abs(x[1]), reverse=True,
    ))
    return {
        "base_value": float(explainer.expected_value),
        "prediction": float(model.predict_proba(X.iloc[[instance_idx]])[:, 1][0]),
        "feature_contributions": explanation,
    }
python
"""Generate SHAP explanations for PA decisions."""
import shap


def explain_pa_decision(model, X, instance_idx: int = 0) -> dict:
    """Generate SHAP values for a single PA decision."""
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X)
    explanation = dict(sorted(
        zip(X.columns, shap_values[instance_idx]),
        key=lambda x: abs(x[1]), reverse=True,
    ))
    return {
        "base_value": float(explainer.expected_value),
        "prediction": float(model.predict_proba(X.iloc[[instance_idx]])[:, 1][0]),
        "feature_contributions": explanation,
    }

5. Denial Reason Analysis

5. 拒付原因分析

5.1 Denial Pattern Analysis

5.1 拒付模式分析

python
"""Analyze PA denial patterns to identify systemic issues."""
import pandas as pd


def analyze_denials(pa_decisions: pd.DataFrame, group_cols: list[str] | None = None) -> dict[str, pd.DataFrame]:
    """Analyze denial patterns across dimensions.

    Args:
        pa_decisions: DataFrame [pa_id, member_id, provider_npi, drug_class, denial_reason, decision, decision_date].
        group_cols: Columns to group by. Defaults to [denial_reason, drug_class, provider_npi].
    """
    denied = pa_decisions[pa_decisions["decision"] == "denied"].copy()
    if group_cols is None:
        group_cols = ["denial_reason", "drug_class", "provider_npi"]
    analyses = {}
    for col in group_cols:
        if col not in denied.columns:
            continue
        g = denied.groupby(col).agg(denial_count=("pa_id", "count"), unique_members=("member_id", "nunique")).sort_values("denial_count", ascending=False).reset_index()
        g["pct_of_denials"] = (g["denial_count"] / len(denied) * 100).round(1)
        analyses[col] = g
    if "denial_reason" in denied.columns:
        doc_gaps = denied[denied["denial_reason"].str.contains("documentation|insufficient", case=False, na=False)]
        analyses["documentation_gaps"] = doc_gaps.groupby("drug_class").agg(gap_count=("pa_id", "count")).sort_values("gap_count", ascending=False).reset_index()
    return analyses
python
"""Analyze PA denial patterns to identify systemic issues."""
import pandas as pd


def analyze_denials(pa_decisions: pd.DataFrame, group_cols: list[str] | None = None) -> dict[str, pd.DataFrame]:
    """Analyze denial patterns across dimensions.

    Args:
        pa_decisions: DataFrame [pa_id, member_id, provider_npi, drug_class, denial_reason, decision, decision_date].
        group_cols: Columns to group by. Defaults to [denial_reason, drug_class, provider_npi].
    """
    denied = pa_decisions[pa_decisions["decision"] == "denied"].copy()
    if group_cols is None:
        group_cols = ["denial_reason", "drug_class", "provider_npi"]
    analyses = {}
    for col in group_cols:
        if col not in denied.columns:
            continue
        g = denied.groupby(col).agg(denial_count=("pa_id", "count"), unique_members=("member_id", "nunique")).sort_values("denial_count", ascending=False).reset_index()
        g["pct_of_denials"] = (g["denial_count"] / len(denied) * 100).round(1)
        analyses[col] = g
    if "denial_reason" in denied.columns:
        doc_gaps = denied[denied["denial_reason"].str.contains("documentation|insufficient", case=False, na=False)]
        analyses["documentation_gaps"] = doc_gaps.groupby("drug_class").agg(gap_count=("pa_id", "count")).sort_values("gap_count", ascending=False).reset_index()
    return analyses

5.2 Denial Reason Code Reference

5.2 拒付原因代码参考

Reason CodeDescriptionRemediation
DENY-STEPStep therapy not completedDocument prior treatments with dates
DENY-DXNon-specific diagnosisUse highest-specificity ICD-10 code
DENY-LABLab criteria not metResubmit with current lab results
DENY-MNMedical necessity not establishedSubmit letter of medical necessity
DENY-EXPExperimental/investigationalCite peer-reviewed evidence
PEND-001Incomplete documentationSubmit clinical notes, labs, history
原因代码描述补救措施
DENY-STEP未完成阶梯治疗记录既往治疗及其日期
DENY-DX诊断代码不够具体使用具体程度最高的 ICD-10 代码
DENY-LAB未满足实验室指标标准使用当前实验室结果重新提交
DENY-MN尚未确立医疗必要性提交医疗必要性说明函
DENY-EXP实验性或研究性项目引用同行评议证据
PEND-001文档不完整提交临床记录、实验室结果和治疗史

6. Parameter Reference

6. 参数参考

6.1 XGBoost Hyperparameters for PA Classification

6.1 用于 PA 分类的 XGBoost 超参数

ParameterDefaultRange
n_estimators
300100–1000
max_depth
63–10
learning_rate
0.050.01–0.3
subsample
0.80.5–1.0
colsample_bytree
0.80.5–1.0
scale_pos_weight
1.0Set to neg/pos ratio for imbalanced data
参数默认值范围
n_estimators
300100–1000
max_depth
63–10
learning_rate
0.050.01–0.3
subsample
0.80.5–1.0
colsample_bytree
0.80.5–1.0
scale_pos_weight
1.0针对不平衡数据设置为 neg/pos 比率

6.2 Documentation Completeness Scoring

6.2 文档完整性评分

Document TypeWeightRequired For
Clinical notes0.30All PA requests
Lab results0.25Drug PAs with lab criteria
Treatment history0.25Step therapy drugs
Diagnosis confirmation0.10All PA requests
Specialist referral0.10Specialty drugs
文档类型权重适用场景
临床记录0.30所有 PA 请求
实验室结果0.25包含实验室指标标准的药物 PA
治疗史0.25阶梯治疗药物
诊断确认0.10所有 PA 请求
专科转诊0.10专科药物

7. Common Mistakes

7. 常见错误

  • Wrong: Training a PA classifier on imbalanced data without correction Right: Set
    scale_pos_weight
    to the neg/pos ratio, or apply SMOTE to balance the training set Why: PA datasets are often 70–80% approvals; without correction, the model learns to approve everything
  • Wrong: Including features derived from information not available at the time of the PA request Right: Ensure all features use only data available before or at the moment of submission (claims history, not future outcomes) Why: Leaking future data inflates model performance in training but fails completely in production
  • Wrong: Deploying a model trained on one payer's decisions to adjudicate another payer's requests Right: Train and validate separate models per payer, or include payer identity as a feature with sufficient per-payer training data Why: Each payer has independent clinical policies; a model trained on Payer A's criteria will make wrong decisions for Payer B
  • Wrong: Accepting SHAP explanations at face value without clinical validation Right: Verify that top SHAP features align with known clinical criteria from the payer's published policy Why: Spurious correlations in training data can produce plausible-looking but clinically meaningless explanations
  • Wrong: Hardcoding denial reason strings with free-text matching Right: Use standardized reason codes (CARC/RARC) and map them to structured enums Why: Free-text matching is brittle — minor wording changes break the logic and cause silent failures
  • Wrong: Replacing the rules engine entirely with an ML classifier Right: Use ML to augment deterministic policy rules; route clear-cut cases through rules and ambiguous cases through ML Why: Auditable, explainable decisions require deterministic rules for regulatory compliance; ML alone is not audit-defensible
  • 错误: 在未对不平衡数据进行处理的情况下训练 PA 分类器 正确: 将
    scale_pos_weight
    设置为 neg/pos 比率,或使用 SMOTE 平衡训练集 原因: PA 数据集中通常有 70–80% 的申请获得批准;如果不进行处理,模型会学会批准所有申请
  • 错误: 纳入由 PA 请求时尚不可用的信息派生出的特征 正确: 确保所有特征仅使用提交时或提交前可用的数据(使用理赔历史,而非未来结果) 原因: 泄漏未来数据会虚高模型在训练期间的表现,却会在生产环境中完全失效
  • 错误: 部署基于一个付款方决策训练的模型来审核另一个付款方的请求 正确: 针对每个付款方分别训练和验证模型,或者在拥有足够付款方专属训练数据时将付款方身份作为特征 原因: 每个付款方都有独立的临床政策;使用付款方 A 的标准训练的模型会对付款方 B 作出错误决策
  • 错误: 不经临床验证便直接接受 SHAP 解释 正确: 验证排名靠前的 SHAP 特征是否符合付款方已发布政策中的已知临床标准 原因: 训练数据中的伪相关可能产生看似合理但没有临床意义的解释
  • 错误: 通过自由文本匹配硬编码拒付原因字符串 正确: 使用标准化原因代码(CARC/RARC),并将其映射到结构化 enum 原因: 自由文本匹配十分脆弱——措辞的细微变化就会破坏逻辑并导致静默失败
  • 错误: 使用 ML 分类器完全替代规则引擎 正确: 使用 ML 增强确定性的政策规则;将明确案例交给规则处理,将模糊案例交给 ML 处理 原因: 可审计且可解释的决策需要确定性规则来满足监管合规要求;仅使用 ML 无法通过审计辩护