简介:这份面向汽车行业管理者与供应链从业者的PPT资料,围绕供应链管理在汽车产业中的核心地位展开,适合企业战略规划、组织架构调整及供应链数字化转型等场景的学习与内部培训参考。压缩包内仅含1个pptx文件,体积约851KB,以幻灯片形式系统梳理了行业变革动因、组织与全球供应链协同、关键绩效指标设定、信息技术应用、最佳实践与基准比较,以及客户、供应商与第三方协作角色等模块,并配有目录导航与阶段演进脉络,便于按主题快速定位。目前已有84人学习下载。读者可借助其中的供应链成熟度自评框架、成本与风险管理思路以及绩效衡量方法,用于梳理自身供应链流程、识别瓶颈并制定改进方向,尤其适合希望从战略层面理解汽车供应链运作、推动跨部门协同的中高级管理人员参考。
1. 从一份汽车供应链管理咨询 PPT 说起:它其实是个数据工程问题
「某咨询汽车行业供应链管理.pptx」这个文件名本身就透露了很多信息:它不是一份一次写完就归档的报告,而是一份会被反复刷新、反复交付的模板化产物。里面通常是几十页——产能爬坡、订单交付率、库存周转、单一供应商风险、物流成本拆解。反直觉的地方在于,这类 PPT 的返工大半不是排版问题,而是口径对不上:同一页里销量用的是批发口径,交付率用的是终端零售口径,翻到第三页就被客户当场问住。把它当一个数据工程问题来做更划算——先把 BOM、MRP、OTD 的口径钉死,再用 Python 算指标,最后让脚本把数字灌进母版。这套做法适合每周或每月要交一版供应链分析、又不想手工贴图对数字的人。
2. 汽车供应链数据的口径与取数:BOM、MRP、OTD 三类主数据的对齐
2.1 汽车供应链的四层物料层级与 BOM 展开
汽车供应链的层级比大多数制造业深一层。整车厂(OEM)往下是 Tier1 总成供应商,再往下 Tier2 零件、Tier3 原材料,一份「整车交付延迟」的结论,往往要沿这条链往下追三层才能定位到真实原因。做分析前先把维度表理清,比急着写 SQL 更重要。
| 层级 | 典型主键 | 常见来源表 | 容易踩的坑 |
|---|---|---|---|
| 整车/车型 | model_code | dim_model | 年款与车型混用,跨年款聚合失真 |
| 系统/模块 | module_id | dim_module | 一个模块跨多车型,映射表缺行 |
| 总成 | assembly_pn | bom_header | 没过滤 BOM 生效日期 |
| 零件 | part_no | bom_item | 替代料没做等价合并 |
BOM 展开最容易出事的点是生效日期。汽车行业的工程变更(ECN)频繁,同一颗零件上半年和下半年的用量可能不同,如果不按订单交付日期去选版本,算出来的物料需求会整体偏移。
-- 按交付日期选择生效中的 BOM 版本,避免用到未来版本或已废止版本 WITH bom_eff AS ( SELECT parent_pn, part_no, qty_per, scrap_rate FROM bom_item WHERE plant_code = :plant -- 工厂维度必须带,多工厂共线时不能省 AND :deliver_date >= eff_from AND :deliver_date < COALESCE(eff_to, DATE '9999-12-31') ) SELECT b.parent_pn, b.part_no, SUM(o.qty * b.qty_per * (1 + b.scrap_rate)) AS gross_require_qty FROM fact_order o JOIN bom_eff b ON b.parent_pn = o.assembly_pn WHERE o.order_date BETWEEN :d0 AND :d1 GROUP BY 1, 2;参数说明::plant用来隔离工厂;:deliver_date是选 BOM 版本的基准,一般取承诺交付日而不是下单日;scrap_rate是报废率,计划口径和财务口径对报废的处理方式不同,做需求测算时要显式带上,否则缺口会被系统性低估。
提示:替代料(alternate part)如果不做等价合并,同一个物理约束会被拆成两行,库存看板会显示「两边都不够、两边都有余」。
2.2 用 SQL 把 OTD 口径算出来
OTD(On-Time Delivery)是这类 PPT 里出现频率最高的指标,也是争议最多的。争议来源通常不是公式,而是「On-Time」对标的是承诺日(promised date)还是原计划日(original plan date)。汽车行业客户计划频繁滚动,承诺日会被改,原计划日不会。建议两个都算,页面上并排展示。
SELECT o.order_id, o.model_code, o.promised_date, a.actual_date, DATEDIFF('day', o.promised_date, a.actual_date) AS delay_days, CASE WHEN a.actual_date <= o.promised_date THEN 1 ELSE 0 END AS otd_flag FROM fact_order o LEFT JOIN fact_delivery a ON a.order_id = o.order_id AND a.deliver_type = 'FIRST_SHIP' -- 只取首次发运,退货/返修不算 WHERE o.order_status <> 'CANCELLED' AND o.promised_date BETWEEN :d0 AND :d1;:d0和:d1建议按承诺日切窗口而不是按下单日,这样每个月的 OTD 是「本月应交付的订单里按时交付了多少」,跟客户对账时口径一致。deliver_type这个过滤条件别省,否则补发、换货会把延迟天数算成负数,OTD 会虚高。
2.3 取数时最容易错的三处
第一是时间基准混用。下单日、承诺日、实际发运日、客户签收日,四个日期至少对应四套说法,交付分析里必须写明用的是哪一套。
第二是退货与返修。汽车行业的三包、召回、返修单如果混进正常发运,交付量会虚增,且延迟天数全为负。
第三是替代料与共用件。共用件被多个总成引用时,做缺口测算要用「总需求 - 可用库存」而不是逐个总成单独算,否则会重复扣减库存。
注意:宽表落地时把口径字段一起写进去,比如
otd_basis = 'PROMISED'、window_type = 'PROMISED_MONTH',半年后别人接手才不会重新猜一遍。
2.4 落成宽表的字段约定
中间层建议直接落成一张按「零件 × 供应商 × 月份」聚合的宽表,PPT 里所有图表都从它出,这样数字对不上时只需要查一个地方。
| 字段 | 含义 | 用途 |
|---|---|---|
| part_no | 零件号 | 主键之一 |
| supplier_code / supplier_tier | 供应商与层级 | 风险页分组 |
| month_key | 月份 | 时间轴 |
| otd_rate | 该月 OTD | 交付页 |
| delay_p90 | 延迟天数 P90 | 交付页尾部风险 |
| inv_qty / avg_daily_usage | 库存与日均用量 | 周转页 |
| dos | 库存天数 | 周转页 |
| risk_score | 缺料风险分 | 风险页 |
3. 用 Python 算汽车供应链核心指标:OTD、库存周转、缺料风险的关键参数
3.1 交付履约:OTD 与延迟天数的分位数
平均数在交付分析里基本没用。一个供应商 OTD 85%,可能是 85% 的订单都准时,也可能是 60% 准时、另外 40% 全是超期一个月的大单被平均掉了。用分位数看尾部。
import pandas as pd df = pd.read_parquet("dws_sc_delivery.parquet") # 只保留首次发运,且延迟天数为正负各留,负数代表提前 df = df[df["deliver_type"] == "FIRST_SHIP"] otd = (df.groupby(["supplier_code", "month_key"]) .agg( orders=("order_id", "nunique"), otd_rate=("otd_flag", "mean"), delay_p50=("delay_days", lambda s: s.quantile(0.50)), delay_p90=("delay_days", lambda s: s.quantile(0.90)), delay_max=("delay_days", "max"), ) .reset_index()) # 订单量太少的组合不参与排名,避免 3 单全准时排到第一 otd = otd[otd["orders"] >= 20] print(otd.sort_values("delay_p90", ascending=False).head(10))orders >= 20这个门槛是经验值,样本太小时 OTD 的置信区间宽到没法用。delay_p90用来驱动「尾部风险」那一页,比平均值更有说服力:客户真正难受的是那 10% 的超长延迟,不是整体均值。
3.2 库存周转与 DOS:分母口径决定你被质疑的次数
库存周转率 = 出库成本 / 平均库存成本。分子分母的期间必须对齐,且平均库存要用期初期末平均而不是期末数。汽车行业月内波动大,直接用期末库存会把周转算得忽高忽低。
# 日均用量取近 3 个完整月,排除当月未结束导致的低估 COMPLETE_MONTHS = 3 usage = (df_usage[df_usage["is_complete_month"]] .groupby("part_no")["usage_qty"] .sum() / (COMPLETE_MONTHS * 30)) inv = df_inv.groupby("part_no")["inv_qty"].mean() dos = (inv / usage).rename("dos").reset_index() # 用量为 0 的呆滞件单独分桶,不要让它把周转率拉到无穷大 slow_moving = dos[dos["dos"].isna() | (dos["dos"] > 180)]is_complete_month这个标记位建议在数据层就打好,别在分析层用「当前月」去猜,脚本在下个月 1 号跑出来的结果会完全不同。DOS 超过 180 天的零件单独出一页呆滞清单,这在汽车行业往往比周转率本身更受关注。
3.3 缺料风险打分:把波动、集中度和提前期放进一个可解释模型
缺料风险页最忌讳的是给出一个没有解释力的「风险指数」。建议用三个可解释因子线性加权,权重写在配置里,客户质疑时能当场改。
import numpy as np def minmax(s): return (s - s.min()) / (s.max() - s.min() + 1e-9) W = {"cv": 0.4, "concentration": 0.35, "lt": 0.25} # 权重显式写死 feat = base.copy() feat["cv"] = minmax(feat["demand_std"] / (feat["demand_mean"] + 1e-9)) # 需求波动 feat["concentration"] = minmax(1 / feat["qualified_supplier_cnt"]) # 供应商集中度 feat["lt"] = minmax(feat["lead_time_days"]) # 采购提前期 feat["risk_score"] = (W["cv"] * feat["cv"] + W["concentration"] * feat["concentration"] + W["lt"] * feat["lt"]) * 100 # 有安全库存覆盖的降权,避免把所有长周期件都标红 feat["risk_score_adj"] = feat["risk_score"] * np.where(feat["dos"] >= 45, 0.6, 1.0)qualified_supplier_cnt只统计通过认证的合格供应商,把「名义上有三家、实际只有一家能供」的情况算准。dos >= 45的降权系数 0.6 是经验值,含义是库存天数超过 45 天的零件即使波动大,短期也不会断线。
3.4 必须显式写进配置的阈值
| 参数 | 建议初值 | 影响 |
|---|---|---|
| 最小订单样本量 | 20 | 低于此值不参与 OTD 排名 |
| 完整月窗口 | 3 | 日均用量的计算区间 |
| DOS 呆滞阈值 | 180 天 | 呆滞清单的切分线 |
| 安全库存覆盖天数 | 45 天 | 风险分降权触发线 |
| 风险分权重 | 0.4 / 0.35 / 0.25 | 波动、集中度、提前期 |
# sc_config.yml —— 每次交付只改这一份,脚本和 PPT 都读它 otd: min_orders: 20 window: promised_month inventory: complete_months: 3 slow_moving_days: 180 risk: min_dos_for_discount: 45 discount: 0.6 weights: {cv: 0.4, concentration: 0.35, lt: 0.25}把阈值集中在一份 YAML 里,好处是版本对比时只看一个文件,也能顺手写进 PPT 的脚注页。
4. 生成汽车行业供应链管理.pptx:python-pptx 的母版、版式与图表落地
4.1 为什么不要从零画形状
用add_textbox一个个摆位置,前二十页能跑通,到客户改一次版式就全崩。正确做法是让设计师先做一份母版,把页眉、页码、结论条、图表区都做成版式(slide layout)里的占位符(placeholder),脚本只负责往占位符里灌内容。这样客户改字体、换配色,脚本一行不用动。
| 版式名 | 用途 | 占位符约定 |
|---|---|---|
| 封面 | 报告首页 | idx 0 标题,idx 1 副标题 |
| 单图表+结论条 | 交付、周转页 | idx 0 标题,idx 1 正文,图表锚点在第 3 个 |
| 两栏对比 | 供应商对比 | idx 0 标题,idx 1 / idx 2 两栏正文 |
| 表格页 | 参数表、清单 | idx 0 标题,idx 1 表格占位 |
| 附录 | 口径说明 | idx 0 标题,idx 1 正文 |
4.2 读取版式并批量灌入内容
from pptx import Presentation from pptx.util import Pt, Cm TPL = "template/汽车行业供应链管理_母版.pptx" prs = Presentation(TPL) LAYOUT_CHART = 1 # 按母版里的实际顺序调,不要硬编码名字 def add_page(prs, title, bullets, layout_idx=LAYOUT_CHART): slide = prs.slides.add_slide(prs.slide_layouts[layout_idx]) # 占位符索引在不同母版里不固定,按 idx 取比按 name 稳 ph = {p.placeholder_format.idx: p for p in slide.placeholders} ph[0].text = title tf = ph[1].text_frame tf.clear() # 不清空会残留母版里的示例文字 for i, line in enumerate(bullets): para = tf.paragraphs[0] if i == 0 else tf.add_paragraph() para.text = line para.level = 0 para.font.size = Pt(14) return slide add_page(prs, "Q3 交付履约复盘", ["整体 OTD 84.2%", "尾部延迟 P90 为 11 天"]) prs.save("out/汽车行业供应链管理.pptx")tf.clear()是最容易被忘的一行,不清空时母版里的示例文字会跟着一起输出,翻页看到「此处填写结论」很尴尬。Pt(14)直接显式设字号,不依赖母版默认值,否则不同机器上渲染出来的行高会变,长文本容易溢出文本框。
4.3 图表:原生图表和贴图各司其职
PPT 里的图分两类。需要客户当场改数据、改口径的,用 pptx 原生图表;纯展示的排名条形图、甘特式趋势图,用 matplotlib 出高 DPI 图再插入,样式可控性更高。
from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE from pptx.util import Cm data = CategoryChartData() data.categories = top10["supplier_code"].tolist() data.add_series("延迟天数 P90", tuple(top10["delay_p90"])) gf = slide.shapes.add_chart( XL_CHART_TYPE.BAR_CLUSTERED, Cm(1.5), Cm(4.0), Cm(22.0), Cm(9.0), data ) chart = gf.chart chart.has_legend = False # 单系列不需要图例,省地方 chart.value_axis.has_major_gridlines = FalseBAR_CLUSTERED在供应商排名场景比COLUMN_CLUSTERED好读,因为供应商编码一般是长字符串,横向排布不用旋转标签。图表锚点用Cm而不是Inches,中文字体下Cm换算更接近设计师给的稿。
4.4 几个必踩的坑
中文字体:如果母版用的是系统中文字体,而生成环境(比如容器)里没装,脚本跑出来会静默回退成默认字体,字宽变化导致换行位置全变。生成镜像里把字体文件装上。
图片 DPI:matplotlib 默认 100 DPI,投到会议室大屏上会糊。保存时显式设dpi=200,并控制单页图片体积,整套 PPT 控制在 30MB 以内,方便邮件发送。
文件句柄:Presentation对象在保存前一直持有文件,在同一进程里连续生成多版本时,先save再重新加载母版,不要复用同一个对象改来改去,容易出现版式错乱。
提示:生成脚本的输出目录固定为
out/,并把文件名写成固定值,别带时间戳。客户每次拿到的文件名一致,版本管理交给 git,而不是靠文件名区分。
5. 让 .pptx 每次刷新都可信:一致性校验与版本对比的实用技巧
5.1 生成后自动跑的三项校验
一份自动生成的报告最容易在三个地方翻车:占位符没填、数字和上页对不上、页码乱。这三项都能在保存后自动查。
from pptx import Presentation def audit(path, expect_pages=None): prs = Presentation(path) problems = [] for i, slide in enumerate(prs.slides, 1): for ph in slide.placeholders: txt = ph.text_frame.text.strip() if ph.has_text_frame else "" if not txt: problems.append(f"第 {i} 页占位符 {ph.placeholder_format.idx} 为空") if "点击此处" in txt or "示例" in txt: problems.append(f"第 {i} 页残留母版提示文字: {txt[:20]}") if expect_pages and len(prs.slides) != expect_pages: problems.append(f"页数不符: 实际 {len(prs.slides)},预期 {expect_pages}") return problems for p in audit("out/汽车行业供应链管理.pptx", expect_pages=42): print(p)has_text_frame的判断不能省,图表占位符没有 text_frame,直接取.text会抛异常。expect_pages作为参数传入而不是写死,章节增删时手动改一次,比事后翻页找问题快。
5.2 用反读文本做口径回归
比校验空占位符更值钱的,是跨版本对比。把上一版 PPT 的文本全部读出来存成基线,这一版生成后再读一次,做 diff,就能看到哪些结论句发生了变化。指标口径改动导致数字整体跳变时,这个 diff 是唯一能在交付前发现问题的办法。
def dump_text(path): prs = Presentation(path) lines = [] for i, slide in enumerate(prs.slides, 1): for shape in slide.shapes: if shape.has_text_frame and shape.text_frame.text.strip(): lines.append(f"[{i}] {shape.text_frame.text.strip()}") return lines base = dump_text("baseline/汽车行业供应链管理.pptx") curr = dump_text("out/汽车行业供应链管理.pptx") import difflib for line in difflib.unified_diff(base, curr, lineterm="", n=0): print(line)表头的[页码]前缀是为了让 diff 结果能直接定位到第几页,否则改了三十页之后只见文字不见位置。基线版本放仓库里,每次正式交付前更新一次,日常迭代只跑对账不做覆盖。
注意:
unified_diff的n=0只输出变化行本身,行数多的时候比默认上下文更好扫。真要看上下文时再调大。
一个更省事的做法是把校验和回归串成一条命令,生成完直接跑,屏幕上只打印非空行——没输出就说明这一版可以直接发。把 baseline 目录留在仓库里,每次交付前跑一次,比在会议室里被问到再翻页强。
本文还有配套的精品资源,点击获取