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) + +![Fig.6 公平性诊断(满足率分布 + Lorenz曲线)](figures/fig6_fairness_satisfaction.png) + +**读图要点**: +- 左图直方图:检查是否出现“长尾站点”被显著压低(大量站点集中在极低 `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)$$ ![Fig.5 风险分布图](figures/fig5_risk_distribution.png) **图表说明**: -该图详细量化了 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 @@ - + @@ -16,13 +16,19 @@ height: 100%; margin: 0; } + body { + background: #ffffff; + } + #map { + background: #ffffff; + } .legend { - background: rgba(255, 249, 240, 0.95); + background: rgba(255, 255, 255, 0.96); padding: 10px 12px; border-radius: 8px; box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15); font: 12px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; - border: 1px solid rgba(188, 108, 37, 0.25); + border: 1px solid rgba(0, 0, 0, 0.15); } .legend .title { font-weight: 700; @@ -64,7 +70,9 @@ const points = window.FIG1_POINTS || []; const links = window.FIG1_LINKS || []; if (!points.length) { - alert("FIG1_POINTS 为空:请先运行 task3/08_visualize.py 生成 fig1_points.js,或确认该文件与本页面同目录。"); + alert( + "Missing FIG1_POINTS: run `python task3/08_visualize.py` to generate `task3/fig1_points.js`, and ensure it is in the same directory as this HTML file." + ); } const map = L.map("map", { preferCanvas: true }); @@ -112,13 +120,13 @@ fillColor: colorForPaired(p.is_paired), fillOpacity: 0.92, }); - marker.bindPopup( + marker.bindPopup( `
\n` + `
${p.site_id}. ${p.site_name}
` + - `
mu: ${p.mu.toFixed(1)}
` + - `
sigma: ${p.sigma.toFixed(1)}
` + - `
k: ${p.k}
` + - `
paired: ${p.is_paired ? "yes" : "no"}
` + + `
Mean (μ): ${p.mu.toFixed(1)}
` + + `
Std (σ): ${p.sigma.toFixed(1)}
` + + `
Annual visits (k): ${p.k}
` + + `
Paired: ${p.is_paired ? "Yes" : "No"}
` + `
` ); siteLayer.addLayer(marker); @@ -146,7 +154,7 @@ `
Pair
` + `
${e.site_i_id}. ${e.site_i_name}
` + `
${e.site_j_id}. ${e.site_j_name}
` + - `
distance: ${e.distance.toFixed(2)} mi
` + + `
Distance: ${e.distance.toFixed(2)} mi
` + `` ); linkLayer.addLayer(line); @@ -159,14 +167,15 @@ const legend = L.control({ position: "bottomleft" }); legend.onAdd = function () { const pairedCount = points.filter((p) => p.is_paired).length; + const linkCount = links.length; const div = L.DomUtil.create("div", "legend"); div.innerHTML = - `
Task 3 Fig.1 配对地图(交互)
` + - `
已配对站点${pairedCount}
` + - `
未配对站点${points.length - pairedCount}
` + - `
点大小: mu(线性缩放)
` + + `
Task 3 Fig.1 Pairing Map (Interactive)
` + + `
Paired sites${pairedCount}
` + + `
Unpaired sites${points.length - pairedCount}
` + + `
Marker size: mean demand μ (linear scale)
` + `
` + - `
连线: 34条配对(点击查看距离)
`; + `
Links: ${linkCount} selected pairs (click for distance)
`; return div; }; legend.addTo(map); diff --git a/task3/figures/fig1_pairing_map.png b/task3/figures/fig1_pairing_map.png index 1d7d6cc..4534eed 100644 Binary files a/task3/figures/fig1_pairing_map.png and b/task3/figures/fig1_pairing_map.png differ diff --git a/task3/figures/fig2_allocation_scatter.png b/task3/figures/fig2_allocation_scatter.png index 8e6ad1b..784fe2a 100644 Binary files a/task3/figures/fig2_allocation_scatter.png and b/task3/figures/fig2_allocation_scatter.png differ diff --git a/task3/figures/fig3_sensitivity.png b/task3/figures/fig3_sensitivity.png index af03054..4ca1778 100644 Binary files a/task3/figures/fig3_sensitivity.png and b/task3/figures/fig3_sensitivity.png differ diff --git a/task3/figures/fig4_calendar_heatmap.png b/task3/figures/fig4_calendar_heatmap.png index a05d4c1..487d137 100644 Binary files a/task3/figures/fig4_calendar_heatmap.png and b/task3/figures/fig4_calendar_heatmap.png differ diff --git a/task3/figures/fig5_risk_distribution.png b/task3/figures/fig5_risk_distribution.png index 0e82eae..4a53b6a 100644 Binary files a/task3/figures/fig5_risk_distribution.png and b/task3/figures/fig5_risk_distribution.png differ diff --git a/task3/figures/fig6_fairness_satisfaction.png b/task3/figures/fig6_fairness_satisfaction.png new file mode 100644 index 0000000..6363b0b Binary files /dev/null and b/task3/figures/fig6_fairness_satisfaction.png differ diff --git a/task3/~$03_allocation.xlsx b/task3/~$03_allocation.xlsx new file mode 100644 index 0000000..5ffdaf9 Binary files /dev/null and b/task3/~$03_allocation.xlsx differ