| name | sensitivity-analysis |
| description | 灵敏度分析:参数扰动、单因素/多因素分析、Monte Carlo 模拟、龙卷风图/蛛网图。触发词: 灵敏度分析、参数敏感性、sensitivity analysis、Monte Carlo、鲁棒性测试、参数扰动。 |
| argument-hint | ["model-file-or-parameter-list"] |
| allowed-tools | Bash(*), Read, Write, Edit, Grep, Glob, Agent, mcp__codex__codex, mcp__codex__codex-reply |
灵敏度分析
执行描述: $ARGUMENTS
Constants
- OUTPUT_REPORT =
SENSITIVITY_ANALYSIS_REPORT.md — 主输出文件,供 paper-write、model-review 使用。
- FIGURE_DIR =
figures/sensitivity/ — 灵敏度图表存放目录。
- ARTIFACT_DIR =
artifacts/ — 中间结构化文件存放目录。
- DEFAULT_PERTURBATION_RANGE =
0.20 — 默认参数扰动幅度为基准值的 +/-20%。
- DEFAULT_PERTURBATION_STEPS =
11 — 单因素分析默认取 11 个等距点(含基准值)。
- MAX_PARAMETERS =
15 — 超过 15 个参数时先做初筛(Morris 方法或相关性排序)。
- MONTE_CARLO_SAMPLES =
10000 — Monte Carlo 模拟默认采样次数。
- MONTE_CARLO_MIN_SAMPLES =
1000 — 采样次数不得低于 1000。
- CONFIDENCE_LEVEL =
0.95 — 置信区间默认 95%。
- SIGNIFICANCE_THRESHOLD =
0.05 — 灵敏度指数超过 0.05 才视为"敏感参数"。
- REVIEWER_MODEL =
gpt-5.4 — Codex MCP 交叉验证模型。
- REASONING_EFFORT =
high — 灵敏度分析验证使用高推理强度。
- MAX_COMPUTE_MINUTES =
30 — 单次灵敏度分析的计算时间上限。
Workflow
Phase 1: 定位模型与参数
Input: $ARGUMENTS、已有模型文件和求解结果。
Output: artifacts/sensitivity_scope.json(参数列表与基准值)。
- 如果
$ARGUMENTS 是文件路径,读取模型代码或参数文件。
- 如果无具体参数,扫描以下文件获取模型信息:
MODEL_REPORT.md、SOLVE_PLAN.md、FINAL_PROPOSAL.md
scripts/、src/ 目录中的模型实现代码
results/ 目录中的求解结果
- 从模型代码中提取所有可调参数:
- 模型参数(权重、系数、阈值)
- 超参数(步长、迭代次数、正则化参数)
- 假设参数(折现率、增长率、概率分布参数)
- 边界条件和约束常数
- 为每个参数记录:名称、当前基准值、物理含义、合理取值范围、数据类型。
- 如果参数过多(>
MAX_PARAMETERS),使用 Morris 筛选法或专家判断先缩减范围。
- 识别目标函数(要分析灵敏度的输出量)。
- 确认模型可以在修改参数后重新求解(函数化封装)。
import json
from pathlib import Path
import re
def extract_parameters_from_code(code_path: str) -> list:
"""Extract tunable parameters from Python model code."""
code = Path(code_path).read_text(encoding="utf-8")
params = []
patterns = [
r"(\w+)\s*=\s*([\d.eE+-]+)\s*#\s*(.+)",
r"(\w+)\s*=\s*([\d.eE+-]+)\s*$",
r"params\[.(\w+).\]\s*=\s*([\d.eE+-]+)",
]
for pattern in patterns:
for match in re.finditer(pattern, code, re.MULTILINE):
groups = match.groups()
name = groups[0]
value = float(groups[1])
desc = groups[2] if len(groups) > 2 else ""
params.append({
"name": name,
"baseline": value,
"description": desc.strip(),
"range_low": value * 0.8,
"range_high": value * 1.2,
})
return params
Path("artifacts").mkdir(exist_ok=True)
Phase 2: 单因素灵敏度分析 (OAT — One-At-a-Time)
Input: 参数列表、基准值、模型求解函数。
Output: artifacts/oat_results.json、龙卷风图。
- 对每个参数,固定其他参数为基准值,只变动当前参数。
- 在
[基准值 * (1 - PERTURBATION_RANGE), 基准值 * (1 + PERTURBATION_RANGE)] 范围内取 PERTURBATION_STEPS 个等距点。
- 对每个点运行模型求解,记录目标函数值。
- 计算灵敏度指数:
S_i = (max(Y) - min(Y)) / Y_baseline。
- 按灵敏度指数降序排列参数。
- 灵敏度指数 >
SIGNIFICANCE_THRESHOLD 的参数标记为"敏感参数"。
- 对每个参数绘制"参数值 vs 目标函数值"曲线(蛛网图数据)。
- 生成龙卷风图(Tornado Chart):横轴为目标函数变化幅度,纵轴为各参数。
- 龙卷风图按影响大小排序,最敏感的参数在最上方。
- 如果某参数的影响明显非线性(非单调),在报告中特别标注。
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def one_at_a_time(model_func, params: list, perturbation: float = 0.20, steps: int = 11):
"""Run OAT sensitivity analysis."""
baseline_values = {p["name"]: p["baseline"] for p in params}
y_baseline = model_func(**baseline_values)
results = {}
for p in params:
name = p["name"]
low = p["baseline"] * (1 - perturbation)
high = p["baseline"] * (1 + perturbation)
test_values = np.linspace(low, high, steps)
y_values = []
for v in test_values:
trial = baseline_values.copy()
trial[name] = v
y_values.append(model_func(**trial))
y_arr = np.array(y_values)
sensitivity_index = (y_arr.max() - y_arr.min()) / abs(y_baseline) if y_baseline != 0 else float("inf")
results[name] = {
"test_values": test_values.tolist(),
"y_values": [float(y) for y in y_arr],
"sensitivity_index": round(float(sensitivity_index), 6),
"y_baseline": float(y_baseline),
"monotonic": bool(np.all(np.diff(y_arr) >= 0) or np.all(np.diff(y_arr) <= 0)),
}
return results
def plot_tornado(oat_results: dict, fig_path: str = "figures/sensitivity/tornado.png"):
"""Generate tornado chart from OAT results."""
sorted_params = sorted(oat_results.items(), key=lambda x: x[1]["sensitivity_index"])
names = [k for k, _ in sorted_params]
low_deltas = []
high_deltas = []
baseline = list(oat_results.values())[0]["y_baseline"]
for name, res in sorted_params:
y_vals = res["y_values"]
low_deltas.append(min(y_vals) - baseline)
high_deltas.append(max(y_vals) - baseline)
fig, ax = plt.subplots(figsize=(10, max(4, len(names) * 0.5)))
y_pos = range(len(names))
ax.barh(y_pos, high_deltas, left=0, color="#2196F3", alpha=0.8, label="Increase")
ax.barh(y_pos, low_deltas, left=0, color="#FF5722", alpha=0.8, label="Decrease")
ax.set_yticks(y_pos)
ax.set_yticklabels(names)
ax.axvline(x=0, color="black", linewidth=0.8)
ax.set_xlabel("Change in Objective Function")
ax.set_title("Tornado Chart — Parameter Sensitivity")
ax.legend()
fig.tight_layout()
Path(fig_path).parent.mkdir(parents=True, exist_ok=True)
fig.savefig(fig_path, dpi=150)
plt.close(fig)
Phase 3: 多因素灵敏度分析 (可选)
Input: Phase 2 识别出的敏感参数、模型求解函数。
Output: artifacts/multi_factor_results.json、交互效应热力图。
- 仅对 Phase 2 中灵敏度指数排名前 3-5 的参数做多因素分析。
- 使用拉丁超立方采样 (LHS) 或 Sobol 序列生成参数组合。
- 计算 Sobol 全局灵敏度指数(一阶 S1、全阶 ST)。
- S1 衡量单参数的独立贡献,ST 衡量包含交互效应的总贡献。
- 如果 ST - S1 显著大于 0,说明该参数与其他参数存在交互效应。
- 生成交互效应热力图,展示参数对之间的二阶灵敏度指数。
- 如果计算量过大(预估 >
MAX_COMPUTE_MINUTES),降低采样数或减少参数。
- 对高度交互的参数对,在报告中建议后续建模关注其联合效应。
- 如果所有参数的交互效应都很小,可以简化报告只展示一阶结果。
- 此阶段为可选,如果时间紧迫可跳过并在报告中说明。
from scipy.stats import qmc
def latin_hypercube_sample(params: list, n_samples: int = 1000) -> np.ndarray:
"""Generate LHS samples for multi-factor analysis."""
n_params = len(params)
sampler = qmc.LatinHypercube(d=n_params)
samples_unit = sampler.random(n=n_samples)
lower = np.array([p["range_low"] for p in params])
upper = np.array([p["range_high"] for p in params])
samples = qmc.scale(samples_unit, lower, upper)
return samples
def compute_sobol_indices(model_func, params: list, n_samples: int = 1024):
"""Compute first-order and total Sobol indices using Saltelli scheme."""
from SALib.sample import saltelli
from SALib.analyze import sobol as sobol_analyze
problem = {
"num_vars": len(params),
"names": [p["name"] for p in params],
"bounds": [[p["range_low"], p["range_high"]] for p in params],
}
X = saltelli.sample(problem, n_samples)
Y = np.array([model_func(**dict(zip(problem["names"], x))) for x in X])
Si = sobol_analyze.analyze(problem, Y)
return {
"S1": dict(zip(problem["names"], Si["S1"].tolist())),
"ST": dict(zip(problem["names"], Si["ST"].tolist())),
"S2": Si.get("S2", None),
}
Phase 4: Monte Carlo 模拟
Input: 参数分布假设、模型求解函数。
Output: artifacts/monte_carlo_results.json、目标函数分布图。
- 为每个敏感参数假设概率分布(正态、均匀、三角、对数正态等)。
- 分布参数基于:数据拟合结果、物理约束、专家经验、文献参考。
- 使用
MONTE_CARLO_SAMPLES 次随机采样。
- 对每次采样运行模型求解,收集目标函数值序列。
- 统计输出分布:均值、标准差、中位数、分位数、置信区间。
- 计算
CONFIDENCE_LEVEL 置信区间的上下界。
- 绘制目标函数的直方图+KDE 曲线。
- 绘制累积分布函数 (CDF) 曲线。
- 如果有多个目标函数,分别做 Monte Carlo 分析。
- 评估结果的鲁棒性:置信区间宽度占均值的比例。
def monte_carlo_simulation(
model_func,
param_distributions: dict,
n_samples: int = 10000,
confidence: float = 0.95,
) -> dict:
"""
Run Monte Carlo simulation.
param_distributions: {"param_name": {"dist": "normal", "mean": 5.0, "std": 0.5}, ...}
"""
samples = {}
for name, dist_info in param_distributions.items():
dist_type = dist_info["dist"]
if dist_type == "normal":
samples[name] = np.random.normal(dist_info["mean"], dist_info["std"], n_samples)
elif dist_type == "uniform":
samples[name] = np.random.uniform(dist_info["low"], dist_info["high"], n_samples)
elif dist_type == "triangular":
samples[name] = np.random.triangular(dist_info["left"], dist_info["mode"], dist_info["right"], n_samples)
elif dist_type == "lognormal":
samples[name] = np.random.lognormal(dist_info["mean"], dist_info["sigma"], n_samples)
results = []
for i in range(n_samples):
kwargs = {name: float(samples[name][i]) for name in samples}
results.append(model_func(**kwargs))
results = np.array(results)
alpha = 1 - confidence
ci_low = np.percentile(results, 100 * alpha / 2)
ci_high = np.percentile(results, 100 * (1 - alpha / 2))
return {
"n_samples": n_samples,
"mean": float(np.mean(results)),
"std": float(np.std(results)),
"median": float(np.median(results)),
"ci_low": float(ci_low),
"ci_high": float(ci_high),
"ci_level": confidence,
"min": float(np.min(results)),
"max": float(np.max(results)),
"cv": float(np.std(results) / np.mean(results)) if np.mean(results) != 0 else float("inf"),
}
def plot_mc_distribution(results: np.ndarray, fig_path: str = "figures/sensitivity/mc_distribution.png"):
"""Plot Monte Carlo result distribution."""
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].hist(results, bins=50, density=True, alpha=0.7, color="#2196F3", edgecolor="white")
from scipy.stats import gaussian_kde
kde = gaussian_kde(results)
x_range = np.linspace(results.min(), results.max(), 200)
axes[0].plot(x_range, kde(x_range), color="#FF5722", linewidth=2)
axes[0].set_title("Monte Carlo Distribution (PDF)")
axes[0].set_xlabel("Objective Function Value")
axes[0].set_ylabel("Density")
sorted_results = np.sort(results)
cdf = np.arange(1, len(sorted_results) + 1) / len(sorted_results)
axes[1].plot(sorted_results, cdf, color="#4CAF50", linewidth=2)
axes[1].set_title("Cumulative Distribution (CDF)")
axes[1].set_xlabel("Objective Function Value")
axes[1].set_ylabel("Cumulative Probability")
axes[1].axhline(y=0.025, color="red", linestyle="--", alpha=0.5, label="2.5%/97.5%")
axes[1].axhline(y=0.975, color="red", linestyle="--", alpha=0.5)
axes[1].legend()
fig.tight_layout()
Path(fig_path).parent.mkdir(parents=True, exist_ok=True)
fig.savefig(fig_path, dpi=150)
plt.close(fig)
Phase 5: 使用 Codex MCP 交叉验证灵敏度分析
Input: OAT 结果、Monte Carlo 结果、模型上下文。
Output: 修订建议。
- 使用
gpt-5.4 与 high 推理强度做独立审查。
- 提示词必须包含参数列表、灵敏度指数排名、Monte Carlo 统计量。
- 要求外部模型检查:
- 敏感参数的排序是否合理
- 扰动范围是否过大或过小
- 参数分布假设是否合理
- 结果的鲁棒性判断是否恰当
- 保存
threadId,后续可追问具体参数的灵敏度解释。
mcp__codex__codex:
model: gpt-5.4
config: {"model_reasoning_effort": "high"}
prompt: |
你是数学建模竞赛灵敏度分析专家,请审查以下分析结果。
模型概述:
[粘贴模型描述和目标函数]
参数灵敏度排名 (OAT):
[粘贴龙卷风图数据表]
Monte Carlo 结果:
[粘贴统计摘要:均值、标准差、置信区间]
请逐项回答:
1. 敏感参数排序是否符合物理/数学直觉?有无反常排序需要解释?
2. 参数扰动范围(+/-20%)是否合适?哪些参数应该用更大或更小的范围?
3. Monte Carlo 中的分布假设是否合理?有无更好的分布选择?
4. 置信区间宽度是否可接受?模型结论是否足够鲁棒?
5. 对论文写作有什么建议?哪些图表最能说明问题?
Phase 6: Output
将最终结果写入 SENSITIVITY_ANALYSIS_REPORT.md。
- 报告开头写 3-5 句总体摘要,交代分析的参数数量、最敏感参数、模型鲁棒性结论。
- 主表必须覆盖:参数名、基准值、扰动范围、灵敏度指数、排名、是否敏感。
- 追加"龙卷风图"和"蛛网图"。
- 追加"Monte Carlo 统计表":均值、标准差、置信区间、变异系数。
- 追加"Monte Carlo 分布图"和"CDF 图"。
- 如果做了多因素分析,追加"Sobol 指数表"和"交互效应热力图"。
- 追加 "Open Questions" 区块,暴露灵敏度分析中的不确定性。
- 用简短段落总结模型鲁棒性结论和对论文的建议。
Output files:
| 路径 | 必需 | 内容 | 说明 |
|---|
SENSITIVITY_ANALYSIS_REPORT.md | Yes | 主报告 | 提供给 paper-write 和 model-review |
figures/sensitivity/tornado.png | Yes | 龙卷风图 | 可直接用于论文 |
figures/sensitivity/spider.png | Recommended | 蛛网图 | 展示参数-输出关系 |
figures/sensitivity/mc_distribution.png | Recommended | MC 分布图 | 展示结果不确定性 |
figures/sensitivity/mc_cdf.png | Recommended | MC 累积分布 | 展示置信区间 |
figures/sensitivity/sobol_heatmap.png | Optional | Sobol 交互热力图 | 多因素分析时生成 |
artifacts/sensitivity_scope.json | Recommended | 参数范围定义 | 机器可读 |
artifacts/oat_results.json | Recommended | OAT 分析结果 | 支持复查 |
artifacts/monte_carlo_results.json | Recommended | MC 模拟结果 | 支持复查 |
artifacts/multi_factor_results.json | Optional | 多因素分析结果 | Sobol 指数 |
Key Rules
- 大文件处理: If the Write tool fails due to file size, immediately retry using Bash (
cat << 'EOF' > file) to write in chunks. Do NOT ask the user for permission — just do it silently.
- 灵敏度分析必须在模型有基准解之后进行,不要在模型未调通时做。
- 扰动范围必须有物理意义,不能机械地用 +/-20%(如概率参数不能超过 1)。
- 龙卷风图是数模论文中灵敏度分析的标准图表,必须生成。
- Monte Carlo 采样次数不得低于
MONTE_CARLO_MIN_SAMPLES (1000)。
- 参数分布假设必须有依据(数据拟合、物理约束或文献),不能随意假设正态分布。
- 如果模型单次求解时间 > 1 秒,Monte Carlo 采样数需要酌情减少并说明原因。
- 多因素分析是加分项,时间紧迫时可跳过但必须在报告中说明。
- 所有灵敏度结论必须与物理/数学直觉对照,反直觉结果需要特别解释。
- 图表必须高质量(dpi >= 150),标签清晰,可直接用于论文。
- 报告中必须保留
Open Questions,暴露灵敏度分析的局限性。
Composing with Other Skills
model-creator
-> solve-plan
-> run-solver
-> analyze-results
-> sensitivity-analysis
-> model-validation
-> paper-write
sensitivity-analysis
-> paper-figure (提供灵敏度图表)
-> paper-write (提供灵敏度分析章节素材)
-> model-review (评委常问灵敏度)