Formel-Q质量能力评价PDF结构化与EK值评分引擎实现 📅 发布时间:2026/9/17 12:04:48 👁 浏览次数: 简介这份PDF是大众汽车集团《Formel Q 质量能力》第9版中文版面向汽车供应链中的供应商、分供方质量管理人员及采购、审核岗位从业者用于解答如何满足大众对过程质量与零件质量的约定要求。文档说明Formel Q是询价与报价流程的组成部分适用于大众集团各品牌并梳理了从1991年第一版到2022年12月第九版的完整修订脉络涵盖目的、潜在供应商审核、质量能力评价与定级、目标协议、后续行动及远程/混合审核规定等模块同时明确供应商需将要求逐级落实至自身供应链并接受定期审核版权归大众汽车集团所有、以德语版为最终解释依据。资源为1个PDF文件压缩包约1.32MB已有1107人学习下载适合需要对照条款核对审核准备、理解评价逻辑与供应链质量责任的读者快速查阅。1. 一份 Formel-Q 质量能力评价表为什么值得先拆成数据再做系统供应商审核报告回来那天最容易被质疑的不是审核员现场看到了什么而是汇总表上那个 EK 值算错了一位小数。Formel-Q 质量能力评价是供应链质量里被引用得最多、也最容易被手工处理坏的一类文档它把评价对象、评价要素、逐条提问和 0/4/6/8/10 的打分档位压在一张表里最后用一个百分比决定供应商落在 A、B 还是 C。PDF 拿到手很多团队第一反应是照着版式把表复刻一遍顺序其实反了——先把它拆成可校验的结构化数据再谈系统。这套思路适合三类人做供应商质量系统或 SRM/QMS 平台的开发需要把评价结果接进报表和提醒做审核数字化的质量工程师想让复评周期自动跑起来以及被总分够了等级却掉档折腾过的项目负责人。整条链路只有四段——提取、归一化、算分与降级、结果校验每一段都能单独复现和验证。2. Formel-Q 质量能力评价表的字段结构从评价对象到打分档位手工处理出错的根因通常是没把文档结构和数据模型分开看。表格里人眼看到的是一整块版面程序需要的是四个互相独立、可以分别校验的层级。2.1 评价表的四层结构封面信息、评价要素、提问条目、单条评分第一层是评价批次也就是封面供应商代码、被评价的工厂或产品范围、审核类型初次、复评、特殊审核、审核日期、审核员、以及手上这份文档自身的版本标识。第二层是评价要素常见做法是沿产品生命周期切分比如项目管理、产品与过程开发的策划、产品与过程开发的实现、供应商管理、批量生产、顾客关怀与服务具体条目名称和编号以你手上那份文档为准。第三层是提问条目每条有一个可检索的编号和一句判定描述。第四层才是评分本身分值、是否不适用、备注与证据引用。层级典型字段数据特征抽取难点评价批次供应商、工厂、产品范围、审核类型、日期、审核员每份文档一行分布在封面多行散排文本里需要按关键词锚定评价要素要素编号、要素名称每份文档 58 行与提问行共用同一列容易混入提问条目提问编号、判定描述每份文档几十到上百行跨页续表行高不固定单条评分分值、n.a. 标记、备注每行一格空格、全角字符、n.a. 写法不统一把四层拆开之后一个很实际的好处是校验点变多了提问条数可以跟文档目录对账满分基数可以跟要素条数对账等级可以跟人工抽查的历史报告对账。整块版面一起处理时错在哪一层根本看不出来。2.2 打分档位与 EK 值0/4/6/8/10 是怎么折算成百分比的Formel-Q 质量能力评价常见的打分档位是五档0、4、6、8、10。0 表示完全不满足10 表示完全满足中间三档对应不同程度的符合情况通常还配合是否有证据是否可复现是否覆盖全部相关区域这类判断维度。手工评分时最关键的一点是不适用n.a.的条目要从分子和分母里同时剔除只从分子扣分会让 EK 值被人为压低只从分母剔除会让它虚高。换算逻辑很直白EK 值等于所有有效条目的实际得分之和除以有效条目数乘以 10再乘 100。之所以强调有效条目是因为剔除规则一旦写错后面所有等级判断都是错的。把档位和剔除规则写成常量表比散落在脚本里的 if-else 可靠得多# 打分档位与满分基数改档位只需要改这里 SCORE_LEVELS (0, 4, 6, 8, 10) MAX_PER_ITEM 10 # 视为不适用的原始文本写法抽取阶段统一映射到这里 NA_TOKENS {n.a., na, n/a, -, —, 不适用, 空}参数说明SCORE_LEVELS只用于合法性校验任何不在集合内的分值都应当抛异常而不是静默丢弃MAX_PER_ITEM是满分基数如果后续文档改成 0/3/6/8/10 这种档位只改这一个常量就能让整条计算链路跟着变NA_TOKENS负责把格式差异收敛掉抽取层不做收敛算分层就要写一堆特判。2.3 A/B/C 等级与降级规则为什么总分够了等级还是掉档EK 值算出来之后走一张阈值表≥90% 判 A80%89.9% 判 B低于 80% 判 C。这三档在很多采购体系里直接对应不同的后续动作比如 A 类可以维持正常配额B 类需要提交改进计划并在下一周期复评C 类往往触发限制新增业务或者暂停报价资格。阈值本身是可以在项目配置里调整的别写死在代码里。真正容易踩的坑在降级规则。文档里通常还有一组规则作用是即便总分到线也要往下调一档某个关键要素出现 0 分、某几条严重不符合被标记、不适用条目占比过高导致样本不足都会触发降级。这类规则在不同版本里措辞和数量都不一样硬编码进算分函数是维护灾难。常见做法是把每条规则写成一个判定函数 一个降级档数用列表注册判定函数只读上下文对象# 降级规则注册表新增版本只需要往列表里加一条 DEGRADE_RULES [ # (规则名, 判定函数, 降级档数) (关键要素出现 0 分, lambda ctx: any( it[score] 0 and it[element_no] in ctx[key_elements] for it in ctx[items]), 1), (n.a. 占比超过 15%, lambda ctx: ctx[na_ratio] 0.15, 1), (存在标记为严重的不符合, lambda ctx: ctx[has_major_nc], 1), ]参数说明ctx至少要包含items条目列表、key_elements关键要素编号集合、na_ratio不适用占比、has_major_nc是否有严重不符合。判定函数必须是纯函数不查库、不改状态这样规则才能单独写单测。降级结果要连同命中的规则名一起落库否则复评时没人说得清上次为什么是 B。3. 用 Python 把 Formel-Q 质量能力 PDF 抽成结构化数据结构清楚之后抽取就从复刻版式变成了填四张表。这一步的目标不是 100% 自动而是把需要人工确认的范围缩到最小。3.1 pdfplumber 还是 camelot先看表格有没有边框线选型只看一个特征提问表有没有完整边框线。有线的情况下pdfplumber用lines策略就能切得非常干净没线、靠留白对齐的表camelot的flavorstream往往更稳但需要调edge_tol和row_tol。两者的输出都是二维列表后面的清洗逻辑可以共用。方案适用版式主要参数代价pdfplumber lines有完整边框线的提问表snap_tolerance、join_tolerance无线表识别率骤降pdfplumber text半结构化、列宽稳定的表text_x_tolerance多列贴太近会串行camelot stream无边框、靠留白对齐edge_tol、row_tol、columns依赖外部依赖较多固定模板映射版式完全固定的年度表坐标区间版式一改全废工程上更实际的组合是先用pdfplumber的lines策略跑一遍把抽到的条目数与人工数一遍的结果对账缺口部分再针对那几页用text策略或人工补录。不要一上来就追求全自动先把覆盖率做到 95% 以上剩下的 5% 用复核队列兜住。3.2 抽取封面信息与提问表的最小可用代码提问行的识别靠编号正则别靠行号。表格里混着表头、合并单元格残留和跨页重复的列标题用行号固定这种假设迟早会崩import re import pdfplumber # 提问编号纯数字 3.1、纯数字 3或字母前缀 EU1 / P6.2 ITEM_NO re.compile(r^(?:\d(?:\.\d)?|[A-Z]{1,3}\d(?:\.\d)?)$) def parse_score(raw: str): 返回 0/4/6/8/10、na或 None 表示抽取失败需人工复核 if raw is None: return None s raw.strip().lower().replace( , ) if s in (n.a., na, n/a, -, —, 不适用): return na m re.search(r(10|[0468]), s) return int(m.group(1)) if m else None def extract_items(pdf_path: str): items [] with pdfplumber.open(pdf_path) as pdf: for pno, page in enumerate(pdf.pages, start1): tables page.extract_tables({ vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, # 相邻线条合并容差版式紧时调小 }) for tbl in tables: for row in tbl: cells [(c or ).replace(\n, ).strip() for c in row] if not cells or not ITEM_NO.match(cells[0]): continue # 跳过表头与空行 items.append({ page: pno, no: cells[0], question: cells[1] if len(cells) 1 else , score: parse_score(cells[-2] if len(cells) 2 else None), remark: cells[-1] if len(cells) 2 else , }) return items参数说明snap_tolerance控制多近的两条线会被当成一条版式紧凑时调小到 2线条细碎时调到 5ITEM_NO用锚点匹配开头而不是全文搜索避免把正文里的编号误判成条目parse_score返回None而不是默认 0这个区别很重要——0 是合法的完全不满足None是脚本没看懂两者混在一起会把 EK 值直接拉低一个档次。跑完之后先统计score is None的条数这个数就是你的人工复核工作量。3.3 入库表结构评价批次、要素、提问三张表落库时不要把所有字段塞进一张宽表三张表对应三个层级复核和追溯都会轻松很多CREATE TABLE fq_audit ( -- 第一层评价批次 audit_id BIGSERIAL PRIMARY KEY, supplier_code VARCHAR(32) NOT NULL, plant_code VARCHAR(32), product_scope TEXT, audit_type VARCHAR(16), -- 初次 / 复评 / 特殊审核 audit_date DATE, auditor VARCHAR(64), doc_version VARCHAR(32), -- 手上那份文档的版本标识 created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE fq_element ( -- 第二层评价要素 element_id BIGSERIAL PRIMARY KEY, audit_id BIGINT REFERENCES fq_audit(audit_id) ON DELETE CASCADE, element_no VARCHAR(16), element_name TEXT, is_key BOOLEAN DEFAULT FALSE -- 是否属于降级规则里的关键要素 ); CREATE TABLE fq_item ( -- 第三、四层提问与评分 item_id BIGSERIAL PRIMARY KEY, element_id BIGINT REFERENCES fq_element(element_id) ON DELETE CASCADE, item_no VARCHAR(16), question TEXT, score SMALLINT, -- 0/4/6/8/10NULL 表示未评分 is_na BOOLEAN DEFAULT FALSE, remark TEXT, source_page INT -- 便于回原 PDF 复核 ); CREATE INDEX idx_fq_item_element ON fq_item(element_id);参数说明score用SMALLINT而不是枚举类型是为了让历史数据在档位调整时不用改表结构合法性交给应用层校验is_na单独一列而不是把score存成 -1是因为算分时要按它剔除分子分母用标志位比用魔法值可读source_page看着可有可无但复核时这条到底是哪页抽出来的这个问题每周都会有人问早存早省事。4. 评分引擎EK 值、等级与降级规则的可配置实现抽取解决数据从哪来算分解决数据怎么用。这一段要保证的是同一个输入永远得到同一个输出且每一步都能单独验证。4.1 分值归一化非法值、未评分和不适用的三种处理进入计算之前先把条目分成三类。第一类是正常评分分值必须在{0,4,6,8,10}里第二类是is_naTrue计入总数但不计入得分和满分第三类是score is None且is_naFalse属于未完成评分。第三类必须拦住不能让它悄悄进计算VALID_SCORES {0, 4, 6, 8, 10} def normalize(items): scored, na, pending [], [], [] for it in items: if it[is_na]: na.append(it) elif it[score] is None: pending.append(it) elif it[score] in VALID_SCORES: scored.append(it) else: raise ValueError(f非法分值 {it[score]}条目 {it[item_no]}) return scored, na, pending参数说明raise而不是continue是有意为之非法分值说明抽取环节或者人工录入出了问题静默跳过会让 EK 值看起来正常但实际不对pending列表要回传到前端或复核队列通常做法是在有未评分条目时直接禁止出报告而不是先出一个带星号的临时结果。4.2 计算 EK 与基础等级得分计算用Decimal而不是浮点避免 89.95 这种边界值因为二进制表示误差掉到 B 档from decimal import Decimal, ROUND_HALF_UP def compute_ek(scored): if not scored: return Decimal(0.0), 0, 0 got sum(it[score] for it in scored) full len(scored) * 10 ek (Decimal(got) / Decimal(full) * 100).quantize(Decimal(0.1), ROUND_HALF_UP) return ek, got, full def base_grade(ek: Decimal) - str: if ek Decimal(90.0): return A if ek Decimal(80.0): return B return C参数说明ROUND_HALF_UP让 89.95 进位成 90.0与人工按计算器得到的结果一致ROUND_HALF_EVEN会产生让人反复来问的偏差阈值写成Decimal而不是整数 90是为了和ek的类型对齐混合比较在边界上会出意外返回got和full两个中间值是为了在报告里能显示实得 X 分 / 满分 Y 分只给百分比在争议场景下说服力不够。4.3 降级规则引擎把版本差异关进配置里降级规则是最不能硬编码的部分。做法是把每条规则写成独立的判定函数用上下文对象传入所有依赖命中后按档数在 A→B→C 的序列上往下走ORDER [A, B, C] # C 已是最低档降无可降 DEGRADE_RULES [ (关键要素出现 0 分, lambda c: any( i[score] 0 and i[element_no] in c[key_elements] for i in c[scored]), 1), (n.a. 占比超过 15%, lambda c: c[na_ratio] Decimal(0.15), 1), (存在标记为严重的不符合, lambda c: c[has_major_nc], 1), ] def apply_degradation(grade: str, ctx: dict): idx, hits ORDER.index(grade), [] for name, cond, step in DEGRADE_RULES: if cond(ctx): hits.append(name) idx min(idx step, len(ORDER) - 1) return ORDER[idx], hits参数说明min(idx step, len(ORDER) - 1)保证多条规则同时命中时不会越界多条规则是否叠加降级取决于文档规定改成idx 1只降一次也很容易hits必须落库到评价主表复评时上次为什么是 B就是查这一列na_ratio用Decimal计算跟阈值保持同一类型。场景基础等级命中规则最终等级EK 92.0无异常A无AEK 91.5量产要素一条 0 分A关键要素出现 0 分BEK 83.2n.a. 占比 18%Bn.a. 占比超过 15%CEK 77.0无异常C无C5. 上线前的结果自检与复评调度三条对账规则和一个看板技巧算分逻辑写完不等于能用真正让它立住的是三组对账。第一条是条数对账抽取到的提问条目数必须等于文档目录或要素汇总里写的条目数差额非零就说明有续表没抽到通常出在跨页那一两页上回头用text策略单独补。第二条是满分对账len(scored) * 10应当等于文档里标注的满分值这条能同时抓出条目漏抽和n.a. 判定错误两类问题。第三条是等级对账拿三到五份历史人工报告跑一遍引擎等级必须一致不一致的先看是分值抽取错还是降级规则理解错前者改代码后者改配置。复评提醒按审核类型给不同周期落成一条查询就够了-- 找出已进入复评窗口的供应商周期按审核类型区分 SELECT a.supplier_code, a.audit_date, MAX(r.final_grade) AS last_grade, a.audit_date CASE a.audit_type WHEN 初次 THEN INTERVAL 180 days WHEN 复评 THEN INTERVAL 365 days ELSE INTERVAL 90 days END AS next_due FROM fq_audit a JOIN fq_result r USING (audit_id) GROUP BY a.supplier_code, a.audit_date, a.audit_type HAVING a.audit_date CASE a.audit_type WHEN 初次 THEN INTERVAL 180 days WHEN 复评 THEN INTERVAL 365 days ELSE INTERVAL 90 days END now() INTERVAL 30 days;查询里的三段周期是项目配置项不是文档规定值具体按采购体系的要求改。把结果挂到调度器上每天跑一次命中就推送到质量工程师的待办里比在群里发提醒靠谱。最后一个技巧是别只盯总分看要素得分分布。同一批供应商按要素分组求平均短板会立刻显形import pandas as pd df pd.read_sql( SELECT e.element_name, i.score FROM fq_item i JOIN fq_element e USING (element_id) WHERE i.is_na FALSE AND i.score IS NOT NULL AND e.audit_id IN (SELECT audit_id FROM fq_audit WHERE supplier_code ANY(%(suppliers)s)) , conn, params{suppliers: supplier_list}) pivot (df.groupby(element_name)[score] .agg([mean, count]) .assign(meanlambda d: d[mean].round(2)) .sort_values(mean)) print(pivot)按mean升序排列排在最前面的两三个要素就是这个供应商群体反复栽跟头的地方。常见的结论是批量生产要素分数还行但产品与过程开发的策划长期偏低——这种结构性问题靠看总分永远发现不了因为高分的要素会把低分的盖住。把这条查询做成周报里的固定一节比在评审会上讨论整体表现还不错要有效得多。本文还有配套的精品资源点击获取