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

|
||||
|
||||
**读图要点**:
|
||||
- 左图直方图:检查是否出现“长尾站点”被显著压低(大量站点集中在极低 `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 ✅ 本文档
|
||||
```
|
||||
|
||||
@@ -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 });
|
||||
@@ -115,10 +123,10 @@
|
||||
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);
|
||||
|
||||
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 755 KiB After Width: | Height: | Size: 726 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 85 KiB |
BIN
task3/figures/fig6_fairness_satisfaction.png
Normal file
|
After Width: | Height: | Size: 112 KiB |