ヘルスケアCDSS開発パターン
EMRワークフローに統合するクリニカルデシジョンサポートシステム(CDSS)を構築するためのパターン。CDSSモジュールは患者の安全に直結します — false negative(見逃し)は絶対に許されません。
使用するタイミング
- 薬剤相互作用チェックの実装
- 投与量検証エンジンの構築
- 臨床スコアリングシステム(NEWS2、qSOFA、APACHE、GCS)の実装
- 異常な臨床値に対するアラートシステムの設計
- 安全性チェック付きの投薬オーダー入力の構築
- 臨床コンテキストを考慮した検査結果解釈の構築
仕組み
CDSSエンジンは副作用のない純粋関数ライブラリです。臨床データを入力し、アラートを出力します。これにより完全にテスト可能になります。
3つの主要モジュール:
checkInteractions(newDrug, currentMeds, allergies)— 新規薬剤を現在の投薬および既知のアレルギーと照合します。重症度順にソートされたInteractionAlert[]を返します。DrugInteractionPairデータモデルを使用します。validateDose(drug, dose, route, weight, age, renalFunction)— 処方された投与量を体重ベース、年齢調整、腎機能調整のルールに対して検証します。DoseValidationResultを返します。calculateNEWS2(vitals)—NEWS2InputからNational Early Warning Score 2を計算します。合計スコア、リスクレベル、エスカレーションガイダンスを含むNEWS2Resultを返します。
EMR UI
↓ (ユーザーがデータを入力)
CDSS Engine (純粋関数、副作用なし)
├── 薬剤相互作用チェッカー
├── 投与量バリデーター
├── 臨床スコアリング (NEWS2, qSOFA, など)
└── アラート分類器
↓ (アラートを返す)
EMR UI (インラインでアラートを表示、重大な場合はブロック)
薬剤相互作用チェック
interface DrugInteractionPair {
drugA: string; // 一般名
drugB: string; // 一般名
severity: 'critical' | 'major' | 'minor';
mechanism: string;
clinicalEffect: string;
recommendation: string;
}
function checkInteractions(
newDrug: string,
currentMedications: string[],
allergyList: string[]
): InteractionAlert[] {
if (!newDrug) return [];
const alerts: InteractionAlert[] = [];
for (const current of currentMedications) {
const interaction = findInteraction(newDrug, current);
if (interaction) {
alerts.push({ severity: interaction.severity, pair: [newDrug, current],
message: interaction.clinicalEffect, recommendation: interaction.recommendation });
}
}
for (const allergy of allergyList) {
if (isCrossReactive(newDrug, allergy)) {
alerts.push({ severity: 'critical', pair: [newDrug, allergy],
message: `Cross-reactivity with documented allergy: ${allergy}`,
recommendation: 'Do not prescribe without allergy consultation' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
相互作用ペアは双方向である必要があります:薬剤Aが薬剤Bと相互作用する場合、薬剤Bも薬剤Aと相互作用します。
投与量検証
interface DoseValidationResult {
valid: boolean;
message: string;
suggestedRange: { min: number; max: number; unit: string } | null;
factors: string[];
}
function validateDose(
drug: string,
dose: number,
route: 'oral' | 'iv' | 'im' | 'sc' | 'topical',
patientWeight?: number,
patientAge?: number,
renalFunction?: number
): DoseValidationResult {
const rules = getDoseRules(drug, route);
if (!rules) return { valid: true, message: 'No validation rules available', suggestedRange: null, factors: [] };
const factors: string[] = [];
// SAFETY: ルールが体重を必要とするが体重がない場合はBLOCK(通過させない)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `Weight required for ${drug} (mg/kg drug)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `Dose exceeds max for ${patientWeight}kg`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// 年齢ベースの調整(ルールが年齢区分を定義し、年齢が提供されている場合)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `Exceeds age-adjusted max for ${patientAge}yr`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// 腎機能調整(ルールがeGFR区分を定義し、eGFRが提供されている場合)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `Exceeds renal-adjusted max for eGFR ${renalFunction}`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// 絶対最大値
if (dose > rules.absoluteMax) {
return { valid: false, message: `Exceeds absolute max ${rules.absoluteMax}${rules.unit}`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: 'Within range',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
臨床スコアリング:NEWS2
interface NEWS2Input {
respiratoryRate: number; oxygenSaturation: number; supplementalOxygen: boolean;
temperature: number; systolicBP: number; heartRate: number;
consciousness: 'alert' | 'voice' | 'pain' | 'unresponsive';
}
interface NEWS2Result {
total: number; // 0-20
risk: 'low' | 'low-medium' | 'medium' | 'high';
components: Record<string, number>;
escalation: string;
}
スコアリングテーブルは、Royal College of Physiciansの仕様と完全に一致している必要があります。
アラートの重大度とUI動作
| 重大度 | UI動作 | 臨床医に必要なアクション |
|---|---|---|
| Critical | アクションをブロック。非表示不可のモーダル。赤色。 | 続行するには上書き理由を文書化する必要あり |
| Major | インラインの警告バナー。橙色。 | 続行する前に承認が必要 |
| Minor | インラインの情報メモ。黄色。 | 認識のみ、アクション不要 |
Criticalアラートは、自動で消去されたり、トースト通知として実装されてはいけません。上書き理由は監査証跡に保存される必要があります。
CDSSのテスト(False Negativeに対するゼロトレランス)
describe('CDSS — Patient Safety', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`detects ${drugA} + ${drugB} (${severity})`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`detects ${drugB} + ${drugA} (reverse)`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('blocks mg/kg drug when weight is missing', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('handles malformed drug data gracefully', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
合格基準:100%。1回の見逃し相互作用が患者安全インシデントになります。
アンチパターン
- CDSSチェックをオプションにしたり、文書化された理由なしにスキップ可能にすること
- 相互作用チェックをトースト通知として実装すること
- 薬剤データや臨床データに
any型を使用すること - 保守可能なデータ構造を使用せずに相互作用ペアをハードコーディングすること
- CDSSエンジンでエラーを黙ってキャッチすること(障害は大きく表面化させる必要があります)
- 体重が利用できない場合に体重ベースの検証をスキップすること(通過させず、ブロックする必要があります)
例
例1:薬剤相互作用チェック
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: 'Increased bleeding risk', recommendation: 'Avoid combination' }]
例2:投与量検証
const ok = validateDose('paracetamol', 1000, 'oral', 70, 45);
// { valid: true, suggestedRange: { min: 500, max: 4000, unit: 'mg' } }
const bad = validateDose('paracetamol', 5000, 'oral', 70, 45);
// { valid: false, message: 'Exceeds absolute max 4000mg' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
例3:NEWS2スコアリング
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: 'Urgent clinical review. Consider ICU.' }