医療CDSS開発パターン は何をしますか?
EMRワークフローに統合する臨床意思決定支援システム(CDSS)を構築するためのパターン。CDSSモジュールは患者安全上極めて重要であり、偽陰性は許容されません。
使用するタイミング
- 薬物相互作用チェックの実装
- 投与量検証エンジンの構築
- 臨床スコアリングシステム(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: `文書化されたアレルギーとの交差反応: ${allergy}`,
recommendation: 'アレルギー専門医の相談なしに処方しないこと' });
}
}
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: '検証ルールがありません', suggestedRange: null, factors: [] };
const factors: string[] = [];
// 安全:ルールが体重を必要とするが体重がない場合はブロック(通過させない)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `${drug}には体重が必要です(mg/kg薬剤)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `投与量が${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: `${patientAge}歳に対する年齢調整最大値を超えています`,
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: `eGFR ${renalFunction}に対する腎機能調整最大値を超えています`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// 絶対最大値
if (dose > rules.absoluteMax) {
return { valid: false, message: `絶対最大値 ${rules.absoluteMax}${rules.unit}を超えています`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: '範囲内',
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のテスト(偽陰性は絶対に許容しない)
describe('CDSS — 患者安全', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`${drugA} + ${drugB} (${severity})を検出する`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`${drugB} + ${drugA} (逆方向)を検出する`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('体重がない場合、mg/kg薬剤をブロックする', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('不正な薬剤データを適切に処理する', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
合格基準:100%。1件の見逃し相互作用も患者安全上のインシデントです。
アンチパターン
- CDSSチェックをオプションまたはスキップ可能にし、文書化された理由を求めない
- 相互作用チェックをトースト通知として実装する
- 薬剤または臨床データに
any型を使用する - 保守可能なデータ構造の代わりに相互作用ペアをハードコーディングする
- CDSSエンジンでエラーを黙ってキャッチする(障害は明確に通知する必要がある)
- 体重が利用できない場合に体重ベースの検証をスキップする(通過させず、ブロックする必要がある)
例
例1:薬物相互作用チェック
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: '出血リスク増加', recommendation: '併用を避ける' }]
例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: '絶対最大値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: '緊急の臨床評価が必要。ICUを検討。' }