pa-decision-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrior 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
自动化方法选择
| Condition | Approach | Rationale |
|---|---|---|
| Clear-cut policy rules (step therapy, age, lab threshold) | Rules engine | Auditable, deterministic, regulatory-safe |
| Ambiguous cases with soft criteria | ML classifier + SHAP | Handles nuance; SHAP provides explainability |
| < 1000 historical decisions available | Rules engine only | Insufficient data for reliable ML training |
| Multi-payer deployment | Separate model per payer | Policies differ; cross-payer models fail |
| Input is X12 278 (EDI) | | Pipe-delimited, segment-based |
| Input is FHIR PAS Bundle | | JSON, resource-typed entries |
| 条件 | 方法 | 理由 |
|---|---|---|
| 明确、固定的政策规则(阶梯治疗、年龄、实验室指标阈值) | 规则引擎 | 可审计、确定性、符合监管要求 |
| 含软性标准的模糊案例 | ML 分类器 + SHAP | 可处理细微差异;SHAP 提供可解释性 |
| 可用的历史决策少于 1000 条 | 仅使用规则引擎 | 数据不足以可靠地训练 ML 模型 |
| 多付款方部署 | 每个付款方使用独立模型 | 政策存在差异;跨付款方模型会失效 |
| 输入为 X12 278(EDI) | | 管道符分隔、基于 segment |
| 输入为 FHIR PAS Bundle | | 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 reqX12 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 req1.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 reqpython
"""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 req2. Clinical Feature Extraction
2. 临床特征提取
2.1 Feature Set for PA Adjudication
2.1 用于 PA adjudication 的特征集
| Feature | Source | Type | Description |
|---|---|---|---|
| Diagnosis codes | int | ICD-10 code length (3=category, 4-7=specific) |
| Service code (NDC/HCPCS) | categorical | Therapeutic class (e.g., biologic, opioid) |
| Claims history | int | Number of prior drugs tried in same class |
| Claims + formulary | bool | All required prior steps documented |
| Claims history | int | Gap since last related treatment |
| Lab results | bool | Key lab (e.g., HbA1c) meets threshold |
| Provider data | categorical | Specialty of ordering provider |
| Claim | categorical | Office, outpatient, inpatient, home |
| PA history | int | Count of prior denials for same service |
| Supporting info | float | Completeness score (0-1) based on required docs |
| 特征 | 来源 | 类型 | 描述 |
|---|---|---|---|
| 诊断代码 | int | ICD-10 代码长度(3=类别,4-7=具体) |
| 服务代码(NDC/HCPCS) | categorical | 治疗类别(例如生物制剂、阿片类药物) |
| 理赔历史 | int | 同一类别中已尝试的既往药物数量 |
| 理赔记录 + 药品目录 | bool | 所有必需的既往治疗步骤均已记录 |
| 理赔历史 | int | 距上次相关治疗的间隔天数 |
| 实验室结果 | bool | 关键实验室指标(例如 HbA1c)达到阈值 |
| 提供方数据 | categorical | 申请医生的专业类别 |
| Claim | categorical | 诊所、门诊、住院、居家 |
| PA 历史 | int | 同一服务既往被拒绝的次数 |
| 支持信息 | 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 analysespython
"""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 analyses5.2 Denial Reason Code Reference
5.2 拒付原因代码参考
| Reason Code | Description | Remediation |
|---|---|---|
| DENY-STEP | Step therapy not completed | Document prior treatments with dates |
| DENY-DX | Non-specific diagnosis | Use highest-specificity ICD-10 code |
| DENY-LAB | Lab criteria not met | Resubmit with current lab results |
| DENY-MN | Medical necessity not established | Submit letter of medical necessity |
| DENY-EXP | Experimental/investigational | Cite peer-reviewed evidence |
| PEND-001 | Incomplete documentation | Submit 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 超参数
| Parameter | Default | Range |
|---|---|---|
| 300 | 100–1000 |
| 6 | 3–10 |
| 0.05 | 0.01–0.3 |
| 0.8 | 0.5–1.0 |
| 0.8 | 0.5–1.0 |
| 1.0 | Set to neg/pos ratio for imbalanced data |
| 参数 | 默认值 | 范围 |
|---|---|---|
| 300 | 100–1000 |
| 6 | 3–10 |
| 0.05 | 0.01–0.3 |
| 0.8 | 0.5–1.0 |
| 0.8 | 0.5–1.0 |
| 1.0 | 针对不平衡数据设置为 neg/pos 比率 |
6.2 Documentation Completeness Scoring
6.2 文档完整性评分
| Document Type | Weight | Required For |
|---|---|---|
| Clinical notes | 0.30 | All PA requests |
| Lab results | 0.25 | Drug PAs with lab criteria |
| Treatment history | 0.25 | Step therapy drugs |
| Diagnosis confirmation | 0.10 | All PA requests |
| Specialist referral | 0.10 | Specialty 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: Setto 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
scale_pos_weight -
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 分类器 正确: 将设置为 neg/pos 比率,或使用 SMOTE 平衡训练集 原因: PA 数据集中通常有 70–80% 的申请获得批准;如果不进行处理,模型会学会批准所有申请
scale_pos_weight -
错误: 纳入由 PA 请求时尚不可用的信息派生出的特征 正确: 确保所有特征仅使用提交时或提交前可用的数据(使用理赔历史,而非未来结果) 原因: 泄漏未来数据会虚高模型在训练期间的表现,却会在生产环境中完全失效
-
错误: 部署基于一个付款方决策训练的模型来审核另一个付款方的请求 正确: 针对每个付款方分别训练和验证模型,或者在拥有足够付款方专属训练数据时将付款方身份作为特征 原因: 每个付款方都有独立的临床政策;使用付款方 A 的标准训练的模型会对付款方 B 作出错误决策
-
错误: 不经临床验证便直接接受 SHAP 解释 正确: 验证排名靠前的 SHAP 特征是否符合付款方已发布政策中的已知临床标准 原因: 训练数据中的伪相关可能产生看似合理但没有临床意义的解释
-
错误: 通过自由文本匹配硬编码拒付原因字符串 正确: 使用标准化原因代码(CARC/RARC),并将其映射到结构化 enum 原因: 自由文本匹配十分脆弱——措辞的细微变化就会破坏逻辑并导致静默失败
-
错误: 使用 ML 分类器完全替代规则引擎 正确: 使用 ML 增强确定性的政策规则;将明确案例交给规则处理,将模糊案例交给 ML 处理 原因: 可审计且可解释的决策需要确定性规则来满足监管合规要求;仅使用 ML 无法通过审计辩护