update task3

This commit is contained in:
2026-01-20 01:55:46 +08:00
parent d49a10a042
commit 101e8b7e8e
14 changed files with 176 additions and 78 deletions

View File

@@ -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}")
# ============================================

Binary file not shown.

View File

@@ -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)

View File

@@ -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<br/>距离矩阵]
F2[02_pairing.xlsx<br/>34对配对]
F2[02_pairing.xlsx<br/>24对配对]
F3[03_allocation.xlsx<br/>最优分配]
F4[04_reschedule.xlsx<br/>访问次数]
F5[05_calendar.xlsx<br/>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 ✅ 本文档
```

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -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(
`<div style="font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">\n` +
`<div style="font-weight:700;margin-bottom:6px;">${p.site_id}. ${p.site_name}</div>` +
`<div><b>mu</b>: ${p.mu.toFixed(1)}</div>` +
`<div><b>sigma</b>: ${p.sigma.toFixed(1)}</div>` +
`<div><b>k</b>: ${p.k}</div>` +
`<div><b>paired</b>: ${p.is_paired ? "yes" : "no"}</div>` +
`<div><b>Mean (μ)</b>: ${p.mu.toFixed(1)}</div>` +
`<div><b>Std (σ)</b>: ${p.sigma.toFixed(1)}</div>` +
`<div><b>Annual visits (k)</b>: ${p.k}</div>` +
`<div><b>Paired</b>: ${p.is_paired ? "Yes" : "No"}</div>` +
`</div>`
);
siteLayer.addLayer(marker);
@@ -146,7 +154,7 @@
`<div style="font-weight:700;margin-bottom:6px;">Pair</div>` +
`<div>${e.site_i_id}. ${e.site_i_name}</div>` +
`<div>${e.site_j_id}. ${e.site_j_name}</div>` +
`<div><b>distance</b>: ${e.distance.toFixed(2)} mi</div>` +
`<div><b>Distance</b>: ${e.distance.toFixed(2)} mi</div>` +
`</div>`
);
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 =
`<div class="title">Task 3 Fig.1 配对地图(交互)</div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.paired}"></span>已配对站点</span><span>${pairedCount}</span></div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.unpaired}"></span>未配对站点</span><span>${points.length - pairedCount}</span></div>` +
`<div class="muted">点大小: mu线性缩放</div>` +
`<div class="title">Task 3 Fig.1 Pairing Map (Interactive)</div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.paired}"></span>Paired sites</span><span>${pairedCount}</span></div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.unpaired}"></span>Unpaired sites</span><span>${points.length - pairedCount}</span></div>` +
`<div class="muted">Marker size: mean demand μ (linear scale)</div>` +
`<div class="line"></div>` +
`<div class="muted">连线: 34条配对点击查看距离</div>`;
`<div class="muted">Links: ${linkCount} selected pairs (click for distance)</div>`;
return div;
};
legend.addTo(map);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 KiB

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
task3/~$03_allocation.xlsx Normal file

Binary file not shown.