RSS
菜单
全部文章快讯开发科技深度热点

第 58 章 案例十一:物流配送分析

内容摘要

本篇是案例系列的第十一篇(也是本批教程的收官篇),用一份模拟的同城配送订单表(5000 笔)走完物流运营分析全流程:配送时长分布探索 → 准时率口径与城市/网点对比 → 路线效率(距离 vs 时长、时效利用率)→ 异常件类型分布与异常率排行 → 延迟件根因初探(分组统计 + 相关系数)→ 区域与网点 Top/Bottom 榜。前置知识:[第 13 章](13-分组聚合:groupby.md)的 groupby、[第 14 章](14-时间序列分析.md)的时间差、[第 15 章](15-数据可视化.md)的直方图与散点图。学完后你能独立产出一份"给站点运营看的配送质量日报"。

📖 本篇简介:本篇是案例系列的第十一篇(也是本批教程的收官篇),用一份模拟的同城配送订单表(5000 笔)走完物流运营分析全流程:配送时长分布探索 → 准时率口径与城市/网点对比 → 路线效率(距离 vs 时长、时效利用率)→ 异常件类型分布与异常率排行 → 延迟件根因初探(分组统计 + 相关系数)→ 区域与网点 Top/Bottom 榜。前置知识:第 13 章的 groupby、第 14 章的时间差、第 15 章的直方图与散点图。学完后你能独立产出一份"给站点运营看的配送质量日报"。
物流配送分析是 pandas 在 O2O / 零售行业最典型的落地场景。它的特殊性在于:"准时"是一个有明确业务口径的二分类问题——实际用时有没有超过承诺时效,直接决定用户投诉率。本篇会带你把"距离、天气、时段、网点"这四个因素和延迟的关系量化出来。
⚠️ 数据说明:本篇全部数据均为 np.random 模拟生成(固定种子 20251007),模拟数据,结论仅供方法演示,不代表任何真实快递公司。

目录

1. 案例背景与目标

2. 数据准备:配送单表(模拟)

3. 数据清洗与字段衍生

4. 配送时长分布

5. 准时率:城市与网点对比

6. 路线效率:距离 vs 时长与时效利用率

7. 异常件分析:类型分布与异常率排行

8. 延迟件根因初探

9. 区域与网点排行(Top / Bottom)

10. 可视化报告

11. 结论与行动建议

12. 常见坑小结

13. 扩展练习


1. 案例背景与目标

背景:某同城即时配送平台在 6 个城市运营 13 个网点,30 天内产生 5000 笔配送订单。运营团队想知道:

1. 快不快:配送时长的分布长什么样?中位数多久?

2. 准不准:承诺时效内送达的比例(准时率)是多少?哪个城市/网点拖后腿?

3. 异常多不多:延迟、破损、取消、其他四类异常各占多少?

4. 为什么慢:延迟到底是距离远、天气差、还是高峰拥堵造成的?

分析目标:从配送单表出发,算出上述指标,输出网点调度和异常监控的改进建议。

先明确"准时"的口径:
- 实际用时 = 送达时间 − 接单时间(不包含用户下单后等接单的时间);
- 准时 = 实际用时 ≤ 承诺时效;
- 这个口径和"下单到送达总时长"不同——后者包含接单等待,会高估配送环节的责任。

2. 数据准备:配送单表(模拟)

构造 5000 笔订单,字段覆盖城市、网点、三个时间戳、承诺时效、距离、金额、天气、状态与异常原因。

import pandas as pd
import numpy as np

np.random.seed(20251007)   # 固定种子,可复现
N = 5000

# 2.1 城市与网点(每城 2~3 个网点)
cities = ["北京", "上海", "广州", "深圳", "杭州", "成都"]
outlets = {
    "北京": ["北京-朝阳站", "北京-海淀站", "北京-通州站"],
    "上海": ["上海-浦东站", "上海-静安站"],
    "广州": ["广州-天河站", "广州-越秀站", "广州-番禺站"],
    "深圳": ["深圳-南山站", "深圳-福田站"],
    "杭州": ["杭州-西湖站", "杭州-滨江站"],
    "成都": ["成都-高新站", "成都-锦江站"],
}
city_w = [0.20, 0.18, 0.16, 0.16, 0.15, 0.15]

# 2.2 下单时间:30 天,早晚高峰时段更密
order_dates = pd.date_range("2025-04-01", periods=30, freq="D")
base_day = np.random.choice(order_dates, size=N)
hour_w = np.array([1,1,1,1,1,2,4,7,9,6,4,4,4,4,4,5,6,8,9,7,5,3,2,1])
hours = np.random.choice(np.arange(24), size=N, p=hour_w/hour_w.sum())
order_time = pd.to_datetime(base_day) + pd.to_timedelta(hours, unit="h") \
             + pd.to_timedelta(np.random.randint(0,60,N), unit="m")

# 2.3 城市、网点、距离、金额
city_arr = np.random.choice(cities, size=N, p=city_w)
outlet_arr = np.array([np.random.choice(outlets[c]) for c in city_arr])
distance = np.random.lognormal(mean=2.0, sigma=0.45, size=N).clip(1, 40)
order_amt = np.random.lognormal(mean=3.5, sigma=0.5, size=N).round(2)

# 2.4 天气(影响配送速度)
weather = np.random.choice(["晴","阴","小雨","大雨","雪"], size=N, p=[0.55,0.22,0.15,0.06,0.02])
w_factor = pd.Series(weather).map({"晴":1.0,"阴":1.05,"小雨":1.15,"大雨":1.4,"雪":1.55}).values

# 2.5 承诺时效(小时):1.2h 基础 + 每公里 0.075h,整体偏紧,让约 15% 超时
promise = np.clip(1.2 + distance*0.075 + np.random.normal(0,0.08,N), 0.8, None)

# 2.6 实际用时(接单->送达,小时):距离 + 固定装卸 + 高峰 + 天气 + 慢网点
peak = ((hours>=8)&(hours<=10)) | ((hours>=17)&(hours<=19))
slow = {"北京-通州站":1.25, "广州-番禺站":1.20, "深圳-福田站":1.10}
o_factor = np.array([slow.get(o, 1.0) for o in outlet_arr])
actual = (distance*0.085 + np.random.normal(0.35,0.15,N) + peak.astype(float)*0.25)
actual = np.clip(actual * w_factor * o_factor, 0.2, None)

# 2.7 接单/送达时间戳
accept_time = order_time + pd.to_timedelta(np.random.uniform(5,25,N)/60.0, unit="h")
deliver_time = accept_time + pd.to_timedelta(actual, unit="h")

# 2.8 状态与异常原因(延迟优先级最高)
on_time = actual <= promise
delay_flag = ~on_time
damage = np.random.random(N) < 0.015
cancel = np.random.random(N) < 0.010
other = np.random.random(N) < 0.008
reason = np.where(delay_flag,"延迟",
         np.where(damage,"破损",
         np.where(cancel,"取消",
         np.where(other,"其他","正常"))))

df = pd.DataFrame({
    "单号":[f"D{i:06d}" for i in range(N)],
    "城市":city_arr, "网点":outlet_arr,
    "下单时间":order_time, "接单时间":accept_time, "送达时间":deliver_time,
    "承诺时效":promise.round(2), "距离":distance.round(2),
    "订单金额":order_amt, "天气":weather,
    "状态":np.where(reason=="正常","正常","异常"),
    "异常原因":reason, "实际用时":actual.round(3),
})
df["小时"] = df["下单时间"].dt.hour
print("配送单表形状:", df.shape)
print(df.head(2).to_string(index=False))
配送单表形状: (5000, 14)
     单号 城市     网点                下单时间                          接单时间                          送达时间  承诺时效    距离  订单金额 天气 状态 异常原因  实际用时  小时
D000000 北京 北京-海淀站 2025-04-24 16:15:00 2025-04-24 16:30:49.485132763 2025-04-24 17:46:21.350894473  1.85  8.79 17.79  晴 正常   正常 1.259  16
D000001 成都 成都-高新站 2025-04-21 08:01:00 2025-04-21 08:23:25.527553734 2025-04-21 09:58:02.559302107  2.00 10.61 56.44  小雨 正常   正常 1.577   8
字段说明:状态是粗粒度(正常/异常),异常原因是细粒度(延迟/破损/取消/其他/正常)。一笔单只要超时就算"延迟",哪怕它没破损没取消。

3. 数据清洗与字段衍生

# 3.1 三个时间戳必须是 datetime,否则后面减法报错
print("时间列 dtype:", df["下单时间"].dtype, df["送达时间"].dtype)

# 3.2 校验:实际用时是否 == 送达-接单(防止埋点错)
calc = (df["送达时间"] - df["接单时间"]).dt.total_seconds()/3600
print("埋点用时与重算用时最大偏差:", (calc - df["实际用时"]).abs().max().round(4))

# 3.3 准时标记(核心口径)
df["是否准时"] = (df["实际用时"] <= df["承诺时效"]).astype(int)

# 3.4 检查缺失与异常
print("缺失值合计:", df.isna().sum().sum())
print("距离<=0 的行:", (df["距离"]<=0).sum())
print("实际用时<=0 的行:", (df["实际用时"]<=0).sum())
时间列 dtype: datetime64[ns] datetime64[ns]
埋点用时与重算用时最大偏差: 0.0
缺失值合计: 0
距离<=0 的行: 0
实际用时<=0 的行: 0
⚠️ 常见坑:时间列如果读进来是字符串,送达-接单 会报错或得到字符串相减。先 pd.to_datetime() 转类型,再做时间差。时间差用 .dt.total_seconds()/3600 转小时,不要直接取 .seconds(它只取当天以内的秒数,跨天会错)。

4. 配送时长分布

先看"实际用时"这一列的整体分布——均值、中位数、分位数。

t = df["实际用时"]
dist = pd.DataFrame({
    "指标": ["均值","中位数","P25","P75","P90","P95","最大值"],
    "小时": [t.mean(), t.median(), t.quantile(.25), t.quantile(.75),
            t.quantile(.90), t.quantile(.95), t.max()],
}).round(3)
print(dist.to_string(index=False))
 指标    小时
 均值 1.274
中位数 1.206
  P25 0.953
  P75 1.513
  P90 1.869
  P95 2.138
最大值 4.337
怎么读:中位数 1.21h < 均值 1.27h,说明分布右偏——大多数单子 1 小时多送到,但有少数长尾慢单(P95 2.14h、最大 4.34h)把均值拉高了。这种"长尾"正是物流优化要抓的对象:把最慢的那 5% 压下来,整体体验提升最明显。
⚠️ 常见坑:物流数据几乎总是右偏(长尾)。只看均值会被极端慢单误导,中位数和 P90/P95 才是更真实的体验指标。对外汇报建议同时给中位数和 P95。

5. 准时率:城市与网点对比

# 5.1 整体准时率
overall_ot = df["是否准时"].mean()
print(f"整体准时率: {overall_ot:.2%}")

# 5.2 按城市
city_ot = df.groupby("城市").agg(
    单量=("单号","count"),
    准时率=("是否准时","mean"),
    平均用时=("实际用时","mean"),
).sort_values("准时率")
print("\n各城市准时率(升序):")
print(city_ot.round(3).to_string())
整体准时率: 95.32%

各城市准时率(升序):
      单量  准时率  平均用时
城市
广州   772  0.927  1.328
北京  1039  0.929  1.309
深圳   795  0.950  1.297
杭州   777  0.967  1.244
上海   923  0.972  1.229
成都   694  0.983  1.230

城市准时率对比表:

| 城市 | 单量 | 准时率 | 平均用时(h) | 评价 |

|------|------|------|------|------|

| 广州 | 772 | 92.7% | 1.33 | 🔴 垫底 |

| 北京 | 1039 | 92.9% | 1.31 | 🔴 垫底 |

| 深圳 | 795 | 95.0% | 1.30 | 🟡 中等 |

| 杭州 | 777 | 96.7% | 1.24 | 🟢 良好 |

| 上海 | 923 | 97.2% | 1.23 | 🟢 良好 |

| 成都 | 694 | 98.3% | 1.23 | 🟢 最优 |

结论:广州、北京准时率明显低于其他城市(93% vs 97%+),且平均用时更长——这两个城市是优化重点。
# 5.3 按网点(城市+网点),找最差和最好
outlet_ot = df.groupby(["城市","网点"]).agg(
    单量=("单号","count"),
    准时率=("是否准时","mean"),
    平均用时=("实际用时","mean"),
).sort_values("准时率")
print("最差 5 个网点:")
print(outlet_ot.head(5).round(3).to_string())
print("\n最好 5 个网点:")
print(outlet_ot.tail(5).round(3).to_string())
最差 5 个网点:
            单量  准时率  平均用时
城市 网点
广州 广州-番禺站  230  0.826  1.514
北京 北京-通州站  324  0.830  1.507
深圳 深圳-福田站  393  0.921  1.363
杭州 杭州-西湖站  364  0.962  1.242
北京 北京-海淀站  369  0.967  1.229

最好 5 个网点:
            单量  准时率  平均用时
城市 网点
上海 上海-静安站  454  0.974  1.245
深圳 深圳-南山站  402  0.978  1.232
成都 成都-高新站  332  0.979  1.265
北京 北京-朝阳站  346  0.980  1.208
成都 成都-锦江站  362  0.986  1.198
结论:广州-番禺站(82.6%)和北京-通州站(83.0%)是两个明显拖后腿的网点,平均用时 1.5h+,比最好的成都-锦江站(98.6%、1.20h)差出一大截。城市层面的差距,其实是被个别差网点拉低的。

6. 路线效率:距离 vs 时长与时效利用率

# 6.1 距离与实际用时的关系
corr_dist = df["距离"].corr(df["实际用时"])
print(f"距离 vs 实际用时 Pearson 相关系数: {corr_dist:.3f}")

# 6.2 时效利用率 = 承诺时效 / 实际用时(按题目指定口径)
#   >1:实际用时比承诺窗口短,提前送达(效率高)
#   <1:实际用时超过承诺窗口,超时(迟到)
df["时效利用率"] = df["承诺时效"] / df["实际用时"]
print(f"时效利用率 均值: {df['时效利用率'].mean():.3f}  中位数: {df['时效利用率'].median():.3f}")
print(f"超时(时效利用率<1)占比: {(df['时效利用率']<1).mean():.2%}")
距离 vs 实际用时 Pearson 相关系数: 0.802
时效利用率 均值: 1.555  中位数: 1.469
超时(时效利用率<1)占比: 4.68%
怎么读:
- 距离和实际用时相关系数 0.80,强正相关——距离确实是用时的主因;
- 时效利用率中位数 1.47,意味着"典型订单用了承诺时间的 1/1.47 ≈ 68%"就送到了,留有余量;
- 但仍有 4.68% 的单子时效利用率 < 1(超时),这些就是投诉高发单。
⚠️ 常见坑:题目指定的时效利用率是 承诺时效/实际用时(>1 为好)。如果反过来定义成 实际用时/承诺时效,结论方向就反了。写指标时务必把"大于 1 是好是坏"在标题里写清楚,否则报表读者会误读。

7. 异常件分析:类型分布与异常率排行

# 7.1 异常类型分布
reason_dist = df["异常原因"].value_counts()
reason_rate = (reason_dist / len(df) * 100).round(2)
tab = pd.DataFrame({"单数": reason_dist, "占比%": reason_rate})
print(tab.to_string())
print(f"\n异常件总数: {(df['状态']=='异常').sum()}, 异常率: {(df['状态']=='异常').mean():.2%}")
        单数    占比%
异常原因
正常    4607  92.14
延迟     235   4.70
破损      68   1.36
取消      54   1.08
其他      36   0.72

异常件总数: 393, 异常率: 7.86%

异常类型分布表:

| 异常类型 | 单数 | 占全部订单 | 占异常件 |

|------|------|------|------|

| 延迟 | 235 | 4.70% | 59.8% |

| 破损 | 68 | 1.36% | 17.3% |

| 取消 | 54 | 1.08% | 13.7% |

| 其他 | 36 | 0.72% | 9.2% |

结论:异常件 393 单(异常率 7.86%),其中延迟一类就占了异常件的近 60%——抓异常主要就是抓延迟。
# 7.2 异常率按城市排序
city_abn = df.groupby("城市").apply(
    lambda g: pd.Series({
        "单量": len(g),
        "异常率": (g["状态"]=="异常").mean(),
    }), include_groups=False
).sort_values("异常率", ascending=False)
print(city_abn.round(3).to_string())
      单量   异常率
城市
广州  772.0  0.101
北京 1039.0  0.099
深圳  795.0  0.078
杭州  777.0  0.068
上海  923.0  0.062
成都  694.0  0.058
结论:广州(10.1%)、北京(9.9%)异常率最高,和准时率垫底的城市完全重合——异常主要就是延迟,两件事互相印证。
💡 pandas 3.0 提示:groupby.apply 在 3.0 默认会抛出关于"组列是否包含"的警告,上面加了 include_groups=False 显式关掉。等价写法也可以用 groupby("城市").agg(异常率=("状态", lambda s:(s=="异常").mean()), 单量=("单号","count")),更简洁、性能更好。

8. 延迟件根因初探

把延迟件和正常件放在一起对比,看延迟到底和什么有关。

delay = df[df["异常原因"]=="延迟"]
normal = df[df["状态"]=="正常"]
print(f"平均距离: 延迟件 {delay['距离'].mean():.2f} km vs 正常件 {normal['距离'].mean():.2f} km")
print(f"平均用时: 延迟件 {delay['实际用时'].mean():.2f} h vs 正常件 {normal['实际用时'].mean():.2f} h")
print("\n延迟件天气分布(%):")
print((delay["天气"].value_counts(normalize=True)*100).round(1).to_string())

# 分时段准时率
print("\n分时段准时率:")
print(df.groupby("小时")["是否准时"].mean().round(3).to_string())
平均距离: 延迟件 11.13 km vs 正常件 8.04 km
平均用时: 延迟件 2.24 h vs 正常件 1.23 h

延迟件天气分布(%):
天气
大雨    41.7
小雨    19.6
雪     18.7
阴     10.6
晴      9.4

分时段准时率:
小时
0     0.981
8     0.907
9     0.920
10    0.903
17    0.941
18    0.922
19    0.932
20    0.988

延迟件 vs 正常件特征表:

| 特征 | 延迟件 | 正常件 | 差异 |

|------|------|------|------|

| 平均距离 | 11.13 km | 8.04 km | 远 39% |

| 平均用时 | 2.24 h | 1.23 h | 慢 82% |

| 坏天气占比(雨+雪) | 80.0% | — | 显著偏高 |

# 相关系数:量化各因素与延迟的关系
print("=== 相关系数 ===")
print(f"实际用时 ~ 距离:      {df['实际用时'].corr(df['距离']):.3f}")
print(f"是否准时 ~ 距离:      {df['是否准时'].corr(df['距离']):.3f}")
print(f"实际用时 ~ 下单小时:  {df['实际用时'].corr(df['小时']):.3f}")
=== 相关系数 ===
实际用时 ~ 距离:      0.802
是否准时 ~ 距离:     -0.168
实际用时 ~ 下单小时:  0.022
根因结论:
1. 距离是用时的最强解释变量(相关 0.80),距离越远越容易超时(准时率~距离 −0.17);
2. 坏天气是延迟的重要触发器——延迟件里 80% 发生在雨/雪天,而天气在全量里只占 ~23%;
3. 高峰时段(早 8~10 点准时率 90~92%、晚 17~19 点 92~94%)明显比深夜(98%+)差,但"下单小时"和用时的线性相关只有 0.02(因为高峰效应是脉冲式的,线性相关捕捉不到,要看分组对比)。
⚠️ 常见坑:线性相关系数低(0.02)不代表"时段不重要"——高峰效应是非线性脉冲,相关系数只能抓线性关系。遇到这种情况,要用分组均值(分时段准时率)而不是相关系数下结论。

9. 区域与网点排行(Top / Bottom)

把最差网点和最好网点放在一张表里,给运营一个直接的整改清单。

rank = df.groupby(["城市","网点"]).agg(
    单量=("单号","count"),
    准时率=("是否准时","mean"),
    平均用时=("实际用时","mean"),
    异常率=("状态", lambda s:(s=="异常").mean()),
).sort_values("准时率")

top_bottom = pd.concat([
    rank.head(3).assign(梯队="🔴 待整改"),
    rank.tail(3).assign(梯队="🟢 标杆"),
])
print(top_bottom.round(3).to_string())
                单量  准时率  平均用时   异常率    梯队
城市 网点
广州 广州-番禺站  230  0.826  1.514  0.200  🔴 待整改
北京 北京-通州站  324  0.830  1.507  0.191  🔴 待整改
深圳 深圳-福田站  393  0.921  1.363  0.107  🔴 待整改
成都 成都-高新站  332  0.979  1.265  0.066  🟢 标杆
北京 北京-朝阳站  346  0.980  1.208  0.046  🟢 标杆
成都 成都-锦江站  362  0.986  1.198  0.039  🟢 标杆
一张表说清:同样是送单,成都-锦江站准时率 98.6%、异常率 3.9%;广州-番禺站准时率只有 82.6%、异常率高达 20.0%。两个站差出 16 个百分点——这就是调度优化的空间。

10. 可视化报告

import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
plt.rcParams["axes.unicode_minus"] = False

fig, axes = plt.subplots(2, 2, figsize=(14, 9))
# 1) 配送时长直方图
axes[0,0].hist(df["实际用时"], bins=50, color="#2E86AB")
axes[0,0].set_title("配送时长分布(小时)")
# 2) 距离 vs 实际用时散点
axes[0,1].scatter(df["距离"], df["实际用时"], s=5, alpha=0.3, color="#F18F01")
axes[0,1].set_xlabel("距离(km)"); axes[0,1].set_ylabel("实际用时(h)")
axes[0,1].set_title("距离 vs 实际用时")
# 3) 异常类型分布
reason_dist.drop("正常").plot(kind="bar", ax=axes[1,0], color="#C0392B")
axes[1,0].set_title("异常类型分布")
# 4) 各城市准时率
city_ot["准时率"].plot(kind="barh", ax=axes[1,1], color="#27AE60")
axes[1,1].set_title("各城市准时率")
plt.tight_layout()
plt.savefig("ch58_report.png", dpi=150)   # 实际使用时存到临时目录
plt.show()
图上能看到:时长直方图明显右偏;距离-用时散点呈带状正相关;异常里延迟占绝对大头;城市准时率广州、北京明显落后。

11. 结论与行动建议

=== 结论(模拟数据,结论仅供方法演示) ===
1. 配送时长中位数 1.21h、均值 1.27h、P95 2.14h,右偏,长尾慢单是优化重点。
2. 整体准时率 95.32%;广州(92.7%)、北京(92.9%)垫底,被广州-番禺站(82.6%)、
   北京-通州站(83.0%)两个差网点拉低。
3. 异常率 7.86%(393 单),其中延迟占异常件近 60%,是异常治理主战场。
4. 延迟根因:距离远(11.1 vs 8.0 km)、坏天气(雨雪占 80%)、高峰时段(8-10 点准时率<91%)。
5. 距离与实际用时强相关(0.80),是路线效率的第一解释变量。

=== 行动建议(面向运营) ===
1. 网点调度:重点整改广州-番禺站、北京-通州站——增配骑手、重划配送范围,
   目标把准时率从 ~83% 拉回 95%;把成都-锦江站的排班/路线做法沉淀成 SOP 推广。
2. 异常监控:把"延迟率"按城市/网点做成日报看板,异常率周环比 >1pct 自动预警;
   破损/取消占比小,单独做专项即可。
3. 高峰与天气预案:早高峰(8-10)、晚高峰(17-19)提前 30 分钟加派运力;
   雨雪天对长距离单提前延长承诺时效或加收缓冲,降低超时投诉。
4. 长距离单:对 >10km 的订单单独评估时效承诺是否合理,必要时拆分或转次日达。

12. 常见坑小结

| 坑 | 错误做法 | 正确做法 |

|------|------|------|

| 时间差 | 直接 .seconds | .dt.total_seconds()/3600,跨天也对 |

| 时长分布 | 只看均值 | 看中位数 + P90/P95,右偏长尾要看尾 |

| 准时口径 | 用下单到送达总时长 | 用送达−接单(剔除接单等待) |

| 时效利用率 | 方向搞反 | 题目指定 承诺/实际,>1 为好,写清方向 |

| 时段效应 | 靠相关系数判断 | 相关抓不到脉冲,用分时段分组均值 |

| groupby.apply | 不管组列警告 | pandas 3.0 用 include_groups=False 或改 agg |


13. 扩展练习

1. 给"接单等待时长"(接单时间−下单时间)也做一个分布分析,看它对总时效投诉的贡献有多大。

2. 用 pd.cut 把距离分成 [0,5,10,20,40] 四档,算每档的准时率和平均用时,验证"距离越远越慢"。

3. 给天气做独热编码,用 df.groupby('天气')['是否准时'].mean() 量化每种天气的准时率损失。

4. 挑一个最差网点(广州-番禺站),拆解它的延迟到底来自距离、天气还是时段,和标杆网点对比。

5. 计算每个网点的"P95 配送时长"而非均值,用 P95 重新排一次网点榜,看排名是否变化。

6. 用 pivot_table 做"城市 × 时段"的准时率透视表,找出最差的交叉单元格。


下一篇预告:总目录 —— pandas 从入门到精通系列至此收官,回到总目录回顾全部章节与学习路径。
📌 再次提示:本篇全部为模拟数据,所有指标数字仅用于演示分析方法,真实业务请用自有数据重跑。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 58 章 案例十一:物流配送分析》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。