| name | scientific-pharmacovigilance |
| description | ファーマコビジランス(医薬品安全性監視)スキル。FAERS/FDA 有害事象報告データベースを活用し、
不均衡分析(PRR/ROR/IC)、MedDRA 階層構造、時系列トレンド、人口統計層別化を実施。
市販後安全性シグナル検出と定量的評価を支援。
「有害事象を分析して」「FAERS データを解析して」「安全性シグナルを検出して」で発火。
|
Scientific Pharmacovigilance
医薬品安全性監視(ファーマコビジランス)のための解析スキル。
FDA FAERS(FDA Adverse Event Reporting System)を中心とした
市販後安全性データの統合的分析を支援する。
When to Use
- 市販後有害事象データの不均衡分析(Signal Detection)
- 特定医薬品の安全性プロファイル評価
- MedDRA 階層を用いた有害事象の体系的分類
- 時系列トレンドによるシグナル推移の追跡
- 人口統計(年齢・性別・体重)別の層別化解析
- 薬物間の安全性比較
Quick Start
ファーマコビジランス解析パイプライン
Phase 1: Data Acquisition
- FAERS / EudraVigilance / VigiBase からのデータ取得
- 製品名・成分名の正規化
- 重複報告の除去(Case ID ベース)
↓
Phase 2: MedDRA Coding & Hierarchy
- LLT → PT → HLT → HLGT → SOC 階層マッピング
- Preferred Term (PT) レベルでの集計
- SMQ (Standardised MedDRA Query) 適用
↓
Phase 3: Disproportionality Analysis
- PRR (Proportional Reporting Ratio) 算出
- ROR (Reporting Odds Ratio) 算出
- IC (Information Component, Bayesian) 算出
- EBGM (Empirical Bayes Geometric Mean) 算出
- シグナル閾値判定
↓
Phase 4: Temporal & Demographic Analysis
- Time-to-Onset 分布解析
- 四半期別報告トレンド
- 年齢・性別・体重・適応症別層別化
- Rechallenge / Dechallenge 分析
↓
Phase 5: Signal Evaluation & Reporting
- シグナル優先順位付け
- ケースナラティブ分析
- 因果関係評価(WHO-UMC / Naranjo スケール)
- 安全性シグナルレポート生成
↓
Phase 6: Risk-Benefit Assessment
- NNH (Number Needed to Harm) 推定
- リスク-ベネフィットバランス評価
- REMS / RiskMAP 提言
↓
Phase 7: Regulatory Communication
- PBRER / PSUR セクション対応
- シグナルサマリー文書生成
- 添付文書改訂案
Workflow
1. FAERS データ取得・前処理
import pandas as pd
import numpy as np
from scipy import stats
demo = pd.read_csv("faers/DEMO.txt", sep="$", dtype=str)
drug = pd.read_csv("faers/DRUG.txt", sep="$", dtype=str)
reac = pd.read_csv("faers/REAC.txt", sep="$", dtype=str)
demo = demo.sort_values("fda_dt", ascending=False).drop_duplicates(subset=["caseid"], keep="first")
merged = drug.merge(reac, on="primaryid", how="inner")
merged = merged.merge(demo[["primaryid", "age", "sex", "wt", "reporter_country"]], on="primaryid", how="left")
target_drug = "ACETAMINOPHEN"
target_df = merged[merged["drugname"].str.upper().str.contains(target_drug)]
print(f"Target drug reports: {len(target_df):,}")
2. MedDRA 階層マッピング
meddra_pt = pd.read_csv("meddra/pt.asc", sep="$", header=None,
names=["pt_code", "pt_name", "null_field",
"pt_soc_code", "null2", "null3"])
meddra_soc = pd.read_csv("meddra/soc.asc", sep="$", header=None,
names=["soc_code", "soc_name", "soc_abbrev", "null1"])
merged["pt_name_clean"] = merged["pt"].str.strip().str.upper()
soc_summary = merged.merge(meddra_pt, left_on="pt_name_clean", right_on="pt_name")
soc_counts = soc_summary.groupby("pt_soc_code").size().reset_index(name="count")
soc_counts = soc_counts.merge(meddra_soc, left_on="pt_soc_code", right_on="soc_code")
soc_counts = soc_counts.sort_values("count", ascending=False)
print("Top SOC categories:")
print(soc_counts[["soc_name", "count"]].head(10).to_string(index=False))
3. 不均衡分析(Disproportionality Analysis)
def disproportionality_analysis(drug_reactions_df, all_reactions_df, target_drug):
"""
PRR, ROR, IC (Information Component) を算出。
2x2 分割表:
Target AE Other AE
Target Drug a b
Other Drugs c d
"""
results = []
target_pts = drug_reactions_df[
drug_reactions_df["drugname"].str.upper().str.contains(target_drug)
]["pt"].value_counts()
total_target = drug_reactions_df[
drug_reactions_df["drugname"].str.upper().str.contains(target_drug)
]["primaryid"].nunique()
total_all = all_reactions_df["primaryid"].nunique()
total_other = total_all - total_target
for pt, a in target_pts.items():
b = total_target - a
c_total = all_reactions_df[all_reactions_df["pt"] == pt]["primaryid"].nunique()
c = c_total - a
d = total_other - c
if (a + b) > 0 and (c + d) > 0 and c > 0:
prr = (a / (a + b)) / (c / (c + d))
se_ln_prr = np.sqrt(1/a - 1/(a+b) + 1/c - 1/(c+d)) if a > 0 else np.inf
prr_lower = np.exp(np.log(prr) - 1.96 * se_ln_prr) if prr > 0 else
:
prr, prr_lower = ,
b > c > d > :
ror = (a * d) / (b * c)
se_ln_ror = np.sqrt(/a + /b + /c + /d) a > np.inf
ror_lower = np.exp(np.log(ror) - * se_ln_ror) ror >
:
ror, ror_lower = ,
expected = (a + b) * (a + c) / (a + b + c + d) (a + b + c + d) >
ic = np.log2((a + ) / (expected + )) expected >
chi2, p_value = ,
a + b + c + d > :
:
chi2, p_value, _, _ = stats.chi2_contingency([[a, b], [c, d]])
ValueError:
is_signal = (prr >= chi2 >= a >= )
is_signal_ror = (ror_lower > a >= )
is_signal_ic = (ic > a >= )
results.append({
: pt, : a, : b, : c, : d,
: (prr, ), : (prr_lower, ),
: (ror, ), : (ror_lower, ),
: (ic, ),
: (chi2, ), : (p_value, ),
: is_signal,
: is_signal_ror,
: is_signal_ic,
})
pd.DataFrame(results).sort_values(, ascending=)
signals = disproportionality_analysis(merged, merged, target_drug)
()
(signals[signals[]].head())
4. 時系列トレンド分析
import matplotlib.pyplot as plt
def temporal_trend_analysis(drug_df, target_drug, target_pt=None):
"""四半期別報告トレンドと Time-to-Onset 分布"""
target = drug_df[drug_df["drugname"].str.upper().str.contains(target_drug)].copy()
if target_pt:
target = target[target["pt"].str.upper().str.contains(target_pt)]
target["report_date"] = pd.to_datetime(target["fda_dt"], format="%Y%m%d", errors="coerce")
target["quarter"] = target["report_date"].dt.to_period("Q")
quarterly = target.groupby("quarter").size()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
quarterly.plot(kind="bar", ax=axes[0], color="#2196F3")
axes[0].set_title(f"Quarterly Reports: {target_drug}")
axes[0].set_ylabel("Report Count")
axes[0].tick_params(axis="x", rotation=45)
if "event_dt" in target.columns and "start_dt" in target.columns:
target[] = pd.to_datetime(target[], =, errors=)
target[] = pd.to_datetime(target[], =, errors=)
target[] = (target[] - target[]).dt.days
valid_tto = target[].dropna()
valid_tto = valid_tto[(valid_tto >= ) & (valid_tto <= )]
axes[].hist(valid_tto, bins=, color=, edgecolor=)
axes[].set_title()
axes[].set_xlabel()
axes[].set_ylabel()
plt.tight_layout()
plt.savefig(, dpi=, bbox_inches=)
plt.show()
temporal_trend_analysis(merged, target_drug)
5. 人口統計層別化分析
def demographic_stratification(drug_df, target_drug, target_pt):
"""年齢・性別・体重別の層別不均衡分析"""
target = drug_df[
(drug_df["drugname"].str.upper().str.contains(target_drug)) &
(drug_df["pt"].str.upper().str.contains(target_pt))
].copy()
target["age_num"] = pd.to_numeric(target["age"], errors="coerce")
target["age_group"] = pd.cut(target["age_num"],
bins=[0, 18, 40, 65, 85, 120],
labels=["<18", "18-40", "40-65", "65-85", "85+"])
sex_dist = target["sex"].value_counts()
target["wt_num"] = pd.to_numeric(target["wt"], errors="coerce")
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
target["age_group"].value_counts().sort_index().plot(
kind="bar", ax=axes[0], color="#4CAF50")
axes[0].set_title(f"Age Distribution: {target_drug} + {target_pt}")
axes[0].set_ylabel()
sex_dist.plot(kind=, ax=axes[], autopct=,
colors=[, , ])
axes[].set_title()
target.columns:
outcome_map = {: , : ,
: , : ,
: , : }
target[] = target[].(outcome_map)
target[].value_counts().plot(
kind=, ax=axes[], color=)
axes[].set_title()
plt.tight_layout()
plt.savefig(, dpi=, bbox_inches=)
plt.show()
target
demographic_stratification(merged, target_drug, )
6. 因果関係評価
def naranjo_assessment(case_data):
"""
Naranjo Adverse Drug Reaction Probability Scale
スコア: ≥9 = Definite, 5-8 = Probable, 1-4 = Possible, ≤0 = Doubtful
"""
questions = [
("以前に確立された有害反応か", 1, 0, 0),
("薬剤投与後に出現したか", 2, -1, 0),
("中止で改善したか (Dechallenge)", 1, 0, 0),
("再投与で再発したか (Rechallenge)", 2, -1, 0),
("他の原因が除外できるか", -1, 2, 0),
("プラセボでも出現するか", -1, 1, 0),
("血中濃度は中毒域か", 1, 0, 0),
("用量依存性があるか", 1, 0, 0),
("類似薬で既往があるか", 1, 0, 0),
("客観的所見で確認されたか", 1, 0, 0),
]
total_score = 0
assessment = []
for q, yes_score, no_score, dk_score in questions:
answer = case_data.get(q, "dk")
if answer == "yes":
score = yes_score
answer == :
score = no_score
:
score = dk_score
total_score += score
assessment.append({: q, : answer, : score})
total_score >= :
category =
total_score >= :
category =
total_score >= :
category =
:
category =
{
: total_score,
: category,
: assessment,
}
7. EBGM (Empirical Bayes Geometric Mean)
def calculate_ebgm(contingency_df, shrinkage_prior=0.5):
"""
EBGM (Multi-item Gamma Poisson Shrinker) による
ベイジアンシグナル検出。FDA OPIS で使用。
EBGM = exp(E[log(λ)|N])
EB05 = EBGM の下側 5% 信頼限界
"""
results = []
N_total = contingency_df[["a", "b", "c", "d"]].sum().sum()
for _, row in contingency_df.iterrows():
a = row["a"]
E = ((row["a"] + row["b"]) * (row["a"] + row["c"])) / N_total
if E > 0:
ebgm = (a + shrinkage_prior) / (E + shrinkage_prior)
from scipy.stats import poisson
eb05 = poisson.ppf(0.05, a + shrinkage_prior) / (E + shrinkage_prior)
else:
ebgm, eb05 = 0, 0
results.append({
"pt": row["pt"],
"observed": a,
"expected": round(E, 3),
"ebgm": round(ebgm, 3),
"eb05": round(eb05, 3),
"signal_ebgm": eb05 >= 2,
})
return pd.DataFrame(results).sort_values(, ascending=)
ebgm_results = calculate_ebgm(signals)
()
8. 安全性シグナルレポート生成
import json
def generate_pv_report(target_drug, signals_df, ebgm_df, output_dir="results"):
"""安全性シグナルレポートの統合生成"""
combined = signals_df.merge(ebgm_df[["pt", "ebgm", "eb05", "signal_ebgm"]], on="pt")
combined["consensus_signal"] = (
combined["signal_prr"] & combined["signal_ror"] & combined["signal_ebgm"]
)
report = {
"drug": target_drug,
"analysis_date": pd.Timestamp.now().isoformat(),
"total_reports": int(signals_df["a"].sum()),
"unique_pts_analyzed": len(signals_df),
"signals_prr": int(signals_df["signal_prr"].sum()),
"signals_ror": int(signals_df["signal_ror"].sum()),
"signals_ic": int(signals_df["signal_ic"].sum()),
"signals_ebgm": int(ebgm_df["signal_ebgm"].sum()),
"consensus_signals": int(combined["consensus_signal"].sum()),
"top_signals": combined[combined["consensus_signal"]].nlargest(20, ).to_dict(),
: {
: ,
: {
: ,
: ,
: ,
: ,
},
},
}
(, ) f:
json.dump(report, f, indent=, default=)
md =
md +=
md +=
md +=
md +=
md +=
md +=
md +=
md +=
md +=
md +=
md +=
md +=
s report[]:
md +=
(, ) f:
f.write(md)
report
Best Practices
- 複数手法のコンセンサス: PRR/ROR/IC/EBGM を併用し、2 手法以上で一致したシグナルを優先
- MedDRA PT レベルで集計: LLT は粒度が細かすぎ、SOC は粗すぎる
- 最小報告基準 N≥3: 少数報告の偽陽性を防ぐ
- Weber 効果に注意: 新薬発売直後は報告バイアスが大きい
- Notoriety Bias を考慮: メディア報道後に報告が急増する可能性
- 重複報告の除去: FAERS は重複が多い(約 10-15%)。Case ID ベースで最新版を保持
- 適応症の交絡: indication-reaction の混同に注意
Completeness Checklist
References
Output Files
| ファイル | 形式 | 生成タイミング |
|---|
results/pv_signal_report.json | シグナル検出結果(JSON) | 不均衡分析完了時 |
results/pv_signal_report.md | シグナルレポート(Markdown) | レポート生成時 |
figures/pv_temporal_trend.png | 時系列トレンド図 | トレンド分析時 |
figures/pv_demographics.png | 人口統計分布図 | 層別化分析時 |
利用可能ツール
ToolUniverse SMCP 経由で利用可能な外部ツール。
| カテゴリ | 主要ツール | 用途 |
|---|
| FAERS | FAERS_count_reactions_by_drug_event | 有害事象カウント |
| FAERS | FAERS_calculate_disproportionality | PRR/ROR/IC 不均衡分析 |
| FAERS | FAERS_stratify_by_demographics | 年齢・性別・国別層別化 |
| FDA | FDA_get_adverse_reactions_by_drug_name | 添付文書副作用情報 |
| DailyMed | DailyMed_search_spls | 添付文書検索 |
| DailyMed | DailyMed_parse_adverse_reactions | 副作用テーブル抽出 |
| PharmGKB | PharmGKB_get_dosing_guidelines | PGx 用量ガイドライン |
| CPIC | CPIC_get_guidelines | CPIC ガイドライン取得 |
参照スキル
| スキル | 連携 |
|---|
scientific-survival-clinical | ← 臨床安全性解析・有害事象グレーディング |
scientific-statistical-testing | ← χ² 検定・多重比較補正 |
scientific-drug-target-profiling | ← 標的プロファイル・薬理学的背景 |
scientific-admet-pharmacokinetics | ← 毒性予測・代謝経路情報 |
scientific-clinical-decision-support | → シグナル情報の臨床意思決定への反映 |
scientific-deep-research | ← 安全性文献の深層リサーチ |
scientific-clinical-trials-analytics | ← 臨床試験データベース照会 |
scientific-regulatory-science | → FDA/FAERS 規制データ統合 |
scientific-pharmacogenomics | ← PGx 代謝型別安全性評価 |