diff --git a/task3/07_sensitivity.py b/task3/07_sensitivity.py
index cb524db..a828a93 100644
--- a/task3/07_sensitivity.py
+++ b/task3/07_sensitivity.py
@@ -36,7 +36,6 @@ from cycler import cycler
# 设置绘图风格(低饱和度 Morandi 色系)
MORANDI = {
- "bg": "#F5F2ED",
"grid": "#D7D1C7",
"text": "#3F3F3F",
"muted_blue": "#8FA3A7",
@@ -46,17 +45,20 @@ MORANDI = {
"warm_gray": "#8C857A",
}
+FIG_BG = "#FFFFFF"
+FIG_GRID = "#DADDE1"
+
plt.rcParams.update(
{
- "figure.facecolor": MORANDI["bg"],
- "axes.facecolor": MORANDI["bg"],
- "savefig.facecolor": MORANDI["bg"],
- "axes.edgecolor": MORANDI["grid"],
+ "figure.facecolor": FIG_BG,
+ "axes.facecolor": FIG_BG,
+ "savefig.facecolor": FIG_BG,
+ "axes.edgecolor": FIG_GRID,
"axes.labelcolor": MORANDI["text"],
"xtick.color": MORANDI["text"],
"ytick.color": MORANDI["text"],
"text.color": MORANDI["text"],
- "grid.color": MORANDI["grid"],
+ "grid.color": FIG_GRID,
"grid.alpha": 0.55,
"axes.grid": True,
"axes.prop_cycle": cycler(
@@ -70,8 +72,8 @@ plt.rcParams.update(
),
"legend.frameon": True,
"legend.framealpha": 0.9,
- "legend.facecolor": MORANDI["bg"],
- "legend.edgecolor": MORANDI["grid"],
+ "legend.facecolor": FIG_BG,
+ "legend.edgecolor": FIG_GRID,
"font.sans-serif": ["Arial Unicode MS", "SimHei", "DejaVu Sans"],
"axes.unicode_minus": False,
}
@@ -436,7 +438,7 @@ for i, param in enumerate(params_list):
ax.grid(True, alpha=0.55)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
-plt.savefig(OUTPUT_FIG, dpi=300)
+plt.savefig(OUTPUT_FIG, dpi=300, facecolor=FIG_BG)
print(f"图表已保存至: {OUTPUT_FIG}")
# ============================================
diff --git a/task3/07_sensitivity.xlsx b/task3/07_sensitivity.xlsx
index ca8aa1f..1181224 100644
Binary files a/task3/07_sensitivity.xlsx and b/task3/07_sensitivity.xlsx differ
diff --git a/task3/08_visualize.py b/task3/08_visualize.py
index 6a44d58..cf2fa7e 100644
--- a/task3/08_visualize.py
+++ b/task3/08_visualize.py
@@ -1,10 +1,10 @@
"""
-Task 3 - Step 8: 可视化(Fig.1/2/4/5)
+Task 3 - Step 8: 可视化(Fig.1/2/4/5/6)
=====================================
输入:
- 01_distance.xlsx (sites: 经纬度、mu/sigma)
- - 02_pairing.xlsx (selected_pairs: 34对配对连线)
+ - 02_pairing.xlsx (selected_pairs: 配对连线)
- 03_allocation.xlsx (allocation: q*, E_total)
- 05_calendar.xlsx (calendar: 365天×2槽位排程)
- 06_evaluate.xlsx (pair_risk: 缺口概率分布)
@@ -12,6 +12,7 @@ Task 3 - Step 8: 可视化(Fig.1/2/4/5)
输出:
- figures/fig1_pairing_map.png
- figures/fig2_allocation_scatter.png
+ - figures/fig6_fairness_satisfaction.png
- figures/fig4_calendar_heatmap.png
- figures/fig5_risk_distribution.png
"""
@@ -47,7 +48,6 @@ BASE_DIR = Path(__file__).resolve().parent
MORANDI = {
- "bg": "#F5F2ED",
"grid": "#D7D1C7",
"text": "#3F3F3F",
"muted_blue": "#8FA3A7",
@@ -59,19 +59,22 @@ MORANDI = {
"terracotta": "#B07A6A",
}
+FIG_BG = "#FFFFFF"
+FIG_GRID = "#DADDE1"
+
def _apply_morandi_style() -> None:
plt.rcParams.update(
{
- "figure.facecolor": MORANDI["bg"],
- "axes.facecolor": MORANDI["bg"],
- "savefig.facecolor": MORANDI["bg"],
- "axes.edgecolor": MORANDI["grid"],
+ "figure.facecolor": FIG_BG,
+ "axes.facecolor": FIG_BG,
+ "savefig.facecolor": FIG_BG,
+ "axes.edgecolor": FIG_GRID,
"axes.labelcolor": MORANDI["text"],
"xtick.color": MORANDI["text"],
"ytick.color": MORANDI["text"],
"text.color": MORANDI["text"],
- "grid.color": MORANDI["grid"],
+ "grid.color": FIG_GRID,
"grid.alpha": 0.55,
"axes.grid": True,
"axes.titleweight": "semibold",
@@ -79,8 +82,8 @@ def _apply_morandi_style() -> None:
"axes.labelsize": 10,
"legend.frameon": True,
"legend.framealpha": 0.9,
- "legend.facecolor": MORANDI["bg"],
- "legend.edgecolor": MORANDI["grid"],
+ "legend.facecolor": FIG_BG,
+ "legend.edgecolor": FIG_GRID,
"axes.prop_cycle": cycler(
"color",
[
@@ -188,7 +191,7 @@ def fig1_pairing_map(paths: Paths) -> Path:
zorder=3,
)
- ax.set_title("Fig.1 Pairing map (sites + selected 34 links)")
+ ax.set_title(f"Fig.1 Pairing map (sites + selected n={len(pairs)} links)")
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
ax.grid(True, alpha=0.55)
@@ -196,7 +199,7 @@ def fig1_pairing_map(paths: Paths) -> Path:
out = paths.figures_dir / "fig1_pairing_map.png"
fig.tight_layout()
- fig.savefig(out)
+ fig.savefig(out, facecolor=FIG_BG)
plt.close(fig)
return out
@@ -287,11 +290,11 @@ def fig2_allocation_scatter(paths: Paths) -> Path:
ax.grid(True, alpha=0.55)
axes[0].set_ylabel("$q^*/Q$")
- fig.suptitle("Fig.2 Allocation strategy scatter (34 pairs)", y=1.02)
+ fig.suptitle(f"Fig.2 Allocation strategy scatter (n={len(df)} pairs)", y=1.02)
fig.tight_layout()
out = paths.figures_dir / "fig2_allocation_scatter.png"
- fig.savefig(out, bbox_inches="tight")
+ fig.savefig(out, bbox_inches="tight", facecolor=FIG_BG)
plt.close(fig)
return out
@@ -336,7 +339,7 @@ def fig4_calendar_heatmap(paths: Paths) -> Path:
cmap = mcolors.LinearSegmentedColormap.from_list(
"morandi",
[
- MORANDI["bg"],
+ FIG_BG,
"#E7E1D7",
MORANDI["sage"],
MORANDI["muted_blue"],
@@ -358,7 +361,7 @@ def fig4_calendar_heatmap(paths: Paths) -> Path:
fig.tight_layout()
out = paths.figures_dir / "fig4_calendar_heatmap.png"
- fig.savefig(out, bbox_inches="tight")
+ fig.savefig(out, bbox_inches="tight", facecolor=FIG_BG)
plt.close(fig)
return out
@@ -373,7 +376,7 @@ def fig5_risk_distribution(paths: Paths) -> Path:
fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), dpi=200)
ax = axes[0]
- ax.hist(p, bins=10, color=MORANDI["sage"], alpha=0.9, edgecolor=MORANDI["bg"])
+ ax.hist(p, bins=10, color=MORANDI["sage"], alpha=0.9, edgecolor=FIG_BG)
ax.axvline(
float(np.mean(p)),
color=MORANDI["terracotta"],
@@ -396,7 +399,7 @@ def fig5_risk_distribution(paths: Paths) -> Path:
linewidth=1.6,
color=MORANDI["muted_blue_dark"],
markerfacecolor=MORANDI["muted_blue"],
- markeredgecolor=MORANDI["bg"],
+ markeredgecolor=FIG_BG,
markeredgewidth=0.6,
)
ax.set_title("Sorted by risk (descending)")
@@ -404,10 +407,61 @@ def fig5_risk_distribution(paths: Paths) -> Path:
ax.set_ylabel("Shortfall probability")
ax.grid(True, alpha=0.55)
- fig.suptitle("Fig.5 Risk distribution across 34 pairs", y=1.02)
+ fig.suptitle(f"Fig.5 Risk distribution across n={len(p)} pairs", y=1.02)
fig.tight_layout()
out = paths.figures_dir / "fig5_risk_distribution.png"
- fig.savefig(out, bbox_inches="tight")
+ fig.savefig(out, bbox_inches="tight", facecolor=FIG_BG)
+ plt.close(fig)
+ return out
+
+
+def fig6_fairness_satisfaction(paths: Paths) -> Path:
+ _apply_morandi_style()
+
+ df = pd.read_excel(paths.evaluate_xlsx, sheet_name="site_satisfaction").copy()
+ r = df["satisfaction_rate_r"].to_numpy(dtype=float)
+ r = r[np.isfinite(r)]
+ r_sorted = np.sort(r)
+
+ # Lorenz curve (site share vs satisfaction share)
+ n = len(r_sorted)
+ if n == 0 or np.sum(r_sorted) <= 0:
+ x = np.array([0.0, 1.0])
+ y = np.array([0.0, 1.0])
+ gini = 0.0
+ else:
+ cum = np.cumsum(r_sorted)
+ x = np.concatenate([[0.0], np.arange(1, n + 1) / n])
+ y = np.concatenate([[0.0], cum / cum[-1]])
+ # Gini via Lorenz area
+ gini = 1.0 - 2.0 * float(np.trapezoid(y, x))
+
+ fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), dpi=200)
+
+ ax = axes[0]
+ ax.hist(r, bins=12, color=MORANDI["muted_blue"], alpha=0.9, edgecolor=FIG_BG)
+ ax.axvline(float(np.mean(r)), color=MORANDI["terracotta"], linewidth=2.2, label=f"mean={np.mean(r):.2f}")
+ ax.set_title("Satisfaction rate distribution")
+ ax.set_xlabel("Satisfaction rate $r_i$")
+ ax.set_ylabel("Count")
+ ax.grid(True, alpha=0.55)
+ ax.legend(loc="best", frameon=True)
+
+ ax = axes[1]
+ ax.plot(x, y, color=MORANDI["sage"], linewidth=2.2, label="Lorenz curve")
+ ax.plot([0, 1], [0, 1], color=MORANDI["warm_gray"], linestyle="--", linewidth=1.4, label="Equality line")
+ ax.set_title(f"Lorenz curve (Gini={gini:.3f})")
+ ax.set_xlabel("Cumulative share of sites")
+ ax.set_ylabel("Cumulative share of satisfaction")
+ ax.set_xlim(0, 1)
+ ax.set_ylim(0, 1)
+ ax.grid(True, alpha=0.55)
+ ax.legend(loc="lower right", frameon=True)
+
+ fig.suptitle(f"Fig.6 Fairness diagnostics (n={len(r)} sites)", y=1.02)
+ fig.tight_layout()
+ out = paths.figures_dir / "fig6_fairness_satisfaction.png"
+ fig.savefig(out, bbox_inches="tight", facecolor=FIG_BG)
plt.close(fig)
return out
@@ -434,6 +488,8 @@ def main() -> None:
print(f"Saved: {out1_js.relative_to(BASE_DIR)}")
out2 = fig2_allocation_scatter(paths)
print(f"Saved: {out2.relative_to(BASE_DIR)}")
+ out6 = fig6_fairness_satisfaction(paths)
+ print(f"Saved: {out6.relative_to(BASE_DIR)}")
out4 = fig4_calendar_heatmap(paths)
print(f"Saved: {out4.relative_to(BASE_DIR)}")
out5 = fig5_risk_distribution(paths)
diff --git a/task3/README.md b/task3/README.md
index ef47a4f..8323b94 100644
--- a/task3/README.md
+++ b/task3/README.md
@@ -79,6 +79,7 @@ flowchart TB
P3[Fig.3 敏感性曲线 ✅]
P4[Fig.4 日历热力图 ✅]
P5[Fig.5 风险分布图 ✅]
+ P6[Fig.6 公平性诊断 ✅]
end
CORE --> VALIDATE
@@ -89,7 +90,7 @@ flowchart TB
subgraph OUTPUT["输出文件"]
F1[01_distance.xlsx
距离矩阵]
- F2[02_pairing.xlsx
34对配对]
+ F2[02_pairing.xlsx
24对配对]
F3[03_allocation.xlsx
最优分配]
F4[04_reschedule.xlsx
访问次数]
F5[05_calendar.xlsx
365天排程]
@@ -127,7 +128,7 @@ flowchart TB
│ │ ▼ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ 01_distance.py │ ──▶ │ 02_pairing.py │ ──▶ │ 03_allocation.py │ │ │
-│ │ │ 距离矩阵70×70 │ │ 34对配对选择 │ │ 最优分配q* │ │ │
+│ │ │ 距离矩阵70×70 │ │ 24对配对选择 │ │ 最优分配q* │ │ │
│ │ └──────────────────┘ └──────────────────┘ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
@@ -150,7 +151,7 @@ flowchart TB
│ │ │ │ ├──────────────┤ │ │ ✓ 风险分布图 │ │ 05_calendar.xlsx │ │
│ │ 结论: │ │ │距离阈值 │ │ │ │ │ 06_evaluate.xlsx │ │
│ │ E1↑9.8% │ │ │ 10-100 mi │ │ │ 图表清单: │ │ 07_sensitivity.xlsx│ │
-│ │ E2↑6.2% │ │ ├──────────────┤ │ │ Fig.1-5 ✅ │ │ │ │
+│ │ E2↑6.2% │ │ ├──────────────┤ │ │ Fig.1-6 ✅ │ │ │ │
│ │ RS=9.5% │ │ │容量上限 │ │ │ │ │ │ │
│ │ R1=1.2% │ │ │ 350-550 │ │ │ │ │ │ │
│ │ │ │ ├──────────────┤ │ │ │ │ │ │
@@ -291,7 +292,7 @@ $$\mu_i + \mu_j \leq Q + \delta = 450$$
**为什么需要价值函数?**
-满足约束的配对可能有上千个(实际1568对),需要选择最优的子集。
+满足约束的配对可能有上千个(本次数据下实际为 361 对),需要选择最优的子集。
**价值函数设计**:
@@ -311,12 +312,12 @@ $$V_{ij} = \underbrace{\alpha \cdot \frac{\mu_i + \mu_j}{Q}}_{\text{容量利用
- 整数规划:精确但复杂度高,收益有限
- 约束"每站点最多配对一次"使贪心算法非常有效
-### 3.3 实际运行结果
+### 3.3 实际运行结果(以 `02_pairing.xlsx` 为准)
**配对筛选**:
-- 候选配对数:1568对(满足3个约束)
-- 最终选择:**34对**(覆盖68个站点,97%)
-- 未配对站点:2个
+- 候选配对数:361对(满足距离/容量/CV等约束)
+- 最终选择:**24对**(覆盖48个站点)
+- 未配对站点:22个
**Top 10 高价值配对展示**:
@@ -427,16 +428,16 @@ $$\mu_i - k\sigma_i \leq q \leq Q - \mu_j + k\sigma_j$$
取 $k = 1$(约84%保护水平)。
-**实际效果**:34对配对中,无一触及边界约束——说明最优解本身已经是鲁棒的。
+**实际效果**:24对配对中,无一触及边界约束——说明在当前参数下闭式最优解具有良好的“自鲁棒性”。
### 4.5 实际分配结果
| 统计量 | 值 |
|--------|-----|
-| $q^*$ 范围 | [23.9, 315.6] |
-| 分配比例范围 | [6.0%, 78.9%] |
-| 平均分配比例 | 50.6% |
-| 平均效率 | 94.2% |
+| $q^*$ 范围 | [47.3, 369.6] |
+| 分配比例范围 | [11.8%, 92.4%] |
+| 平均分配比例 | 61.0% |
+| 平均效率 | 98.9% |
**Top 5 配对的最优分配方案 ($q^*$ Strategies)**:
@@ -480,7 +481,7 @@ $$k_{ij} = \lfloor \min(k_i, k_j) / 2 \rfloor$$
题目说"sending the same truck to visit two sites on **some of the trips**"——双站点是一次"行程"。
**重分配逻辑**:
-1. 计算释放的槽位:$\Delta N = \sum k_{ij} = 142$
+1. 计算释放的槽位:$\Delta N = \sum k_{ij} = 70$
2. 按需求比例分配给所有站点
3. 使用Hamilton方法取整
@@ -488,16 +489,20 @@ $$k_{ij} = \lfloor \min(k_i, k_j) / 2 \rfloor$$
| 指标 | 值 |
|------|-----|
-| 配对数 | 34 |
-| 双站点访问次数 | 142 |
-| 释放槽位 | 142 |
-| 最终单站点访问 | 588 |
+| 配对数 | 24 |
+| 双站点访问次数 | 70 |
+| 释放槽位 | 70 |
+| 最终单站点访问 | 660 |
| 最终总事件 | 730(符合约束)|
---
## 6. 效果评估
+Task 3 的评估必须同时回答两类问题:
+1) **Effectiveness(有效性)**:在总事件数固定(730)的情况下,是否显著提高总服务量/有效服务量?
+2) **Fairness(公平性)**:改善有效性的同时,是否让某些站点“被系统性牺牲”(满足率分布显著变差)?
+
### 6.1 指标定义与逻辑
**E1':期望总服务量**
@@ -529,7 +534,20 @@ $$r_i = \frac{k_i^{single} \cdot \mu_i + k_{ij} \cdot E[S_i]}{\tilde{\mu}_i}$$
$$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
-### 6.2 实际结果对比分析
+### 6.2 如何用指标刻画 Effectiveness & Fairness
+
+**Effectiveness(有效性)** 用两套口径避免“只看总量”的误判:
+- `E1/E1'`:总服务量(只反映覆盖规模,越大越好)
+- `E2/E2'`:质量加权服务量(用 $q(\\mu)=\\min(1,250/\\mu)$ 近似刻画“人均分得量”下降带来的效用折扣,越大越好)
+
+**Fairness(公平性)** 用站点级满足率分布刻画“是否均衡”:
+- `r_i`:站点 $i$ 的满足率(用服务机会与“截断修正后的真实需求”归一化,越大代表该站点相对更被保障)
+- `F1/F1'`:$\\{r_i\\}$ 的 Gini 系数(越小越均衡)
+- `F2/F2'`:最低满足率(worst-case 保障,下界越高越好)
+
+为了避免仅用一个数字“掩盖结构性不公平”,我们在 `06_evaluate.xlsx` 的 `site_satisfaction` 输出了每个站点的 `r_i`,并在 Fig.6 中用“直方图 + Lorenz 曲线”展示分布形态。
+
+### 6.3 实际结果对比分析(Task 3 vs Task 1)
下表对比了 Task 1(传统单站点模式)与 Task 3(双站点优化模式)的关键性能指标。数据表明,新模型在各项核心指标上均取得了显著突破。
@@ -537,6 +555,7 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
- **E2 (质量加权)**:提升 **6.3%**。虽然增幅略低于 E1(因为双站点模式下每户平均分得量可能略减,导致质量因子下降),但整体质量效益依然显著为正。
- **R1 (缺口风险)**:虽然从 0 增加到 1.2%,但这一数值远低于行业通常接受的 5% 风险阈值,说明模型成功用微小的风险代价换取了巨大的效率提升。
- **RS (资源节省)**:**9.6%** 的资源节省率意味着 FBST 可以用同样的卡车和志愿者资源,多服务近一成的社区,或者在维持现有服务水平下减少 9.6% 的运营成本。
+- **公平性(F1/F2)**:最低满足率 `F2` 不变(仍为 2.0),说明“底线保障”未被破坏;Gini `F1` 略有上升(0.314→0.322),表示在换取效率提升的同时,引入了轻微的不均衡,需要用配对准入约束/合并比例进一步调参。
| 指标 | Task 1 | Task 3 | 变化 | 变化% |
|---|---|---|---|---|
@@ -548,11 +567,20 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
| **RS (资源节省)** | 0% | 9.6% | +9.6% | 新增 |
**核心发现**:
-1. 通过双站点模式,释放9.5%的访问槽位
-2. 总服务量提升9.8%
-3. 公平性几乎不变(Gini仅增加2.4%)
+1. 通过双站点模式,释放约 9.6% 的访问槽位(70/730)
+2. 总服务量提升约 9.9%
+3. 公平性基本稳定(Gini 上升约 2.5%)
4. 引入极低的服务缺口风险(1.2%)
+### 6.4 Fig.6 公平性分布诊断(Satisfaction & Lorenz)
+
+
+
+**读图要点**:
+- 左图直方图:检查是否出现“长尾站点”被显著压低(大量站点集中在极低 `r_i`)。
+- 右图 Lorenz 曲线:越贴近对角线代表越公平;图内给出的 Gini 与表格中的 `F1/F1'` 对应。
+在本次结果中,公平性指标保持在与基准相近的区间,且 `F2` 不下降,说明效率提升并未以牺牲最弱站点为代价。
+
---
## 7. 敏感性分析
@@ -631,18 +659,19 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
| 每日访问事件数 | 2 | min=2, max=2 | ✅ 通过 |
| 年度总事件数 | 730 | 730 | ✅ 通过 |
| 站点覆盖 | 全覆盖 | 70/70 | ✅ 通过 |
-| $q^*$边界检查 | 在[q_lower, q_upper]内 | 34/34在边界内 | ✅ 通过 |
+| $q^*$边界检查 | 在[q_lower, q_upper]内 | 24/24在边界内 | ✅ 通过 |
### 8.2 模型有效性验证
**与Task 1对比**:
-- E1提升16.9%:释放的槽位被有效利用
-- F1几乎不变:公平性未受损
-- R1可控:17.1%的缺口风险在合理范围
+- E1 提升约 9.9%:释放的 70 个槽位被有效利用
+- E2 提升约 6.3%:在“人均分得量折扣”的口径下仍显著为正
+- F1 略有上升(0.314→0.322):公平性轻微变差,但幅度很小;且 F2(最低满足率)不下降
+- R1 可控(约 1.18%):用于换取效率提升的风险代价较低
**物理合理性**:
-- 高价值配对的需求和接近容量(平均413)
-- 最优分配比例接近50%(平均50.6%)
+- 配对总需求均值约 295(`02_pairing.xlsx` 汇总),说明整体以“互补填满”而非“硬凑满载”为主
+- 最优分配比例均值约 61%(`03_allocation.xlsx`),反映模型会根据两站点波动性进行风险缓冲
- 低需求配对的双站点次数较少(合理)
---
@@ -657,11 +686,13 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$
**图表说明**:
该图展示了纽约州南部六县 70 个食品分发站点的地理分布及最终的配对拓扑结构。
-- **红色节点 (Paired Sites)**:表示被模型选中参与“双站点同车访问”模式的 68 个站点。
-- **灰色节点 (Unpaired Sites)**:表示因距离过远、需求过大或波动性过高,不适合配对而保留为“单站点访问”模式的 2 个站点。
-- **橙色连线 (Links)**:连接了 34 对被选中的最优配对,连线的长短直观反映了站点间的物理距离。
+- **红色节点 (Paired Sites)**:表示被模型选中参与“双站点同车访问”模式的 48 个站点。
+- **灰色节点 (Unpaired Sites)**:表示因距离过远、需求过大或波动性过高,不适合配对而保留为“单站点访问”模式的 22 个站点。
+- **橙色连线 (Links)**:连接了 24 对被选中的最优配对,连线的长短直观反映了站点间的物理距离。
- **节点大小**:正比于该站点的平均需求量 ($\mu$),直观展示了需求的地理分布密度。
+**交互版地图**:打开 `task3/fig1_carto.html`(同目录需有 `task3/fig1_points.js`),可点击站点/连线查看站点参数与距离信息。
+
**分析结论**:
配对结果显示出显著的**“地理邻近性”**特征,大多数连线短且互不交叉,表明模型中的距离惩罚项 ($\beta$) 有效地限制了长途无效行驶。同时,可以观察到**“核心-边缘”**的配对模式(大点连小点),这有助于平衡单次行程的总负载,避免因两站点均为高需求而频繁导致容量溢出。
@@ -717,7 +748,7 @@ $$R_1 = P(S_i / D_i < 0.8 \text{ 或 } S_j / D_j < 0.8)$$

**图表说明**:
-该图详细量化了 34 对配在最优分配策略下的剩余风险。
+该图详细量化了 24 对配在最优分配策略下的剩余风险。
- **左图 (直方图)**:展示了风险概率 ($P(\text{Shortfall})$) 的频数分布。可以看出绝大多数配对的风险落在 [0, 0.02] 的极低区间。
- **右图 (排序曲线)**:将所有配对按风险值从高到低排序。
@@ -774,7 +805,7 @@ task3/
├── 01_distance.py ✅ 距离矩阵计算
│ └── 01_distance.xlsx (70×70矩阵)
├── 02_pairing.py ✅ 配对筛选与选择
-│ └── 02_pairing.xlsx (34对配对)
+│ └── 02_pairing.xlsx (24对配对)
├── 03_allocation.py ✅ 最优分配计算
│ └── 03_allocation.xlsx (q*值)
├── 04_reschedule.py ✅ 访问次数重分配
@@ -785,7 +816,7 @@ task3/
│ └── 06_evaluate.xlsx (指标对比)
├── 07_sensitivity.py ✅ 敏感性分析
│ └── 07_sensitivity.xlsx (4参数)
-├── 08_visualize.py ✅ 可视化(Fig.1/2/4/5)
+├── 08_visualize.py ✅ 可视化(Fig.1/2/4/5/6)
│ └── figures/ (5张图)
└── README.md ✅ 本文档
```
diff --git a/task3/__pycache__/07_sensitivity.cpython-313.pyc b/task3/__pycache__/07_sensitivity.cpython-313.pyc
index d7ab62d..245ef33 100644
Binary files a/task3/__pycache__/07_sensitivity.cpython-313.pyc and b/task3/__pycache__/07_sensitivity.cpython-313.pyc differ
diff --git a/task3/__pycache__/08_visualize.cpython-313.pyc b/task3/__pycache__/08_visualize.cpython-313.pyc
index e1e59f0..42d6b59 100644
Binary files a/task3/__pycache__/08_visualize.cpython-313.pyc and b/task3/__pycache__/08_visualize.cpython-313.pyc differ
diff --git a/task3/fig1_carto.html b/task3/fig1_carto.html
index e462d4a..b4fd802 100644
--- a/task3/fig1_carto.html
+++ b/task3/fig1_carto.html
@@ -1,5 +1,5 @@
-
+