工分计算(gongfen-calc)
用途
把一张格式不固定的任务清单,变成一份可复核的工分事实清单:
Excel/CSV → 字段识别 → 单位归一 → 质检 → 批次评级 → 打分 → Excel 结果表 + Markdown 报告 + 中间 JSON
输出的是任务分,不是个人绩效等级。设计上宁可报"分析失败",也不静默编造一个看起来合理的分数。
门槛:只要识别到任务名称或描述列就执行分析,缺列不阻断——缺失的维度不参与计算,只在报告提示。 唯一阻断条件是「连任务名/描述都认不出来」。难度维度固定启用,从任务名(优先)或备注描述推断。
不适用:给人定绩效等级(A/B/C)、淘汰或扣罚名单。这类需求直接拒绝。
快速开始
# 1. 装依赖(Windows 必须走阿里云镜像,直连 pypi 会超时)
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
# 2. 跑(一条命令出结果,全程不询问、不中断)
python scripts/run.py 任务清单.xlsx
python scripts/run.py 任务清单.xlsx --out-dir out --baseline out/baseline.json
# 3. 只有在「明知第 N 列是什么、脚本却认错了」时才人工覆写(唯一的人工介入点)
python scripts/run.py 任务清单.xlsx --override 3=hours --override 5=money
# 4. 明知覆盖度不足(C/D 分支)仍要出工分 —— 默认不出,必须显式加 --force-points
python scripts/run.py 任务清单.xlsx --force-points
--override 列号=字段名:列号从 1 开始(对应报告「字段识别明细」的列号),字段名取值
task_name/hours/money/difficulty/risk/priority/quality/urgency/start_date/end_date/collab/reuse/parent/role/…。
覆写会写进仲裁记录留痕,被取代的同名字段列自动转入「未映射列」。低置信列默认按规则自动裁定,不会停下来问。
产出(默认与输入同目录,可用 --out-dir / --xlsx / --json / --md 指定):
任务清单.gongfen.xlsx—— 打分结果表(主交付物),4 个 sheet:工分明细:逐任务 原表行号 / 任务名 / 各维度档位(带权重)/ 原始值 / 完整度 / 分层 / 层内排名 / 小计 S / K系数 / 工分 P / 判定依据(容器父任务也列出,标"不计分")批次汇总:分支结论、总分 T、总工分、分层分布、维度权重与覆盖度、口径冻结、提升建议质检与自检:质检提示、自检结果、被打折任务、被剔除行(含行号与原因)、冻结任务、红线声明字段识别:列映射明细(列号/置信度/方式/样例)+ 未映射列 + 仲裁日志
任务清单.gongfen.md—— 给人看的报告(九节)任务清单.gongfen.json—— 复核与下游用的中间结构(解析层与打分层的唯一契约)out/baseline.json—— 首次运行自动存档,第二次起启用环比检测
C/D 分支下 Excel 的「工分 P」列留空,另给一列**「参考工分(仅供排序对照·不可用于分配)」**(= S×20×K),
表头与汇总页都写明不可用于分配;确需工分时加 --force-points 重跑,脚本会在保单里追加一条强制输出警示。
退出码:0 正常或降级 / 1 分析失败(已出可读报告)/ 2 参数错误 / 3 内部异常。
执行流程
脚本端到端出结果,模型不做二次判断。 不要自己打开 Excel 猜字段,不要覆写脚本给出的分数,不要在识别环节打断用户。
- 跑脚本(合并单元格展开、合计行剔除、单位归一、Excel 排版这些手工很难做对的事都在脚本里)。
- 读"批次结论":
A_正常/B_可排名/D_体系退化/C_仅描述/E_失败。这决定结果能不能用于分配。 - 交付
*.gongfen.xlsx(主交付物,直接给用户打开);Markdown 是同一份数据的文字版,用于贴聊天/贴文档。 - 把三件事讲给用户:分支结论与免责句、剔除了哪些行(Excel「质检与自检」第五节有行号与原文)、哪些任务标了异常(工时与日期矛盾、被偏离度打折、难度为估算值)。
- 用户追问某个数怎么来的,查
任务清单.gongfen.json或references/field_dictionary.md(别名表、正则、换算系数、质检规则都在里面)。 - 口径:改
scripts/params.py后重跑;列认错了用--override 列号=字段名重跑;不要手工改分数,也不要手工改 JSON。
Excel 结果表(scripts/excel_out.py)
只在非 E_失败分支产出(失败时只有 MD/JSON)。excel_out.py 只读 payload、不碰任何分数,
所以 Excel 与 MD/JSON 永远同源同值;重跑即覆盖同名文件。
| sheet | 内容 | 用途 |
|---|---|---|
| 工分明细 | 原表行号 / 任务ID / 任务名 / 各维度档位(表头带归一化权重)+ 原始值 / 有效维度数 / 完整度×2 / 分层 / 层内排名 / 小计 S / K偏离·K合计 / 工分 P / 判定依据 | 逐任务核对、按原表行号回溯 |
| 批次汇总 | 分支结论、总分 T、总工分、分层分布、维度权重与覆盖度表、口径冻结、补齐字段建议 | 一眼看结论 |
| 质检与自检 | 质检提示、批次评级提示、自检结果、被打折任务、被剔除行(行号+原因+原文)、冻结任务、红线声明 | 复核"哪些数据没被采信" |
| 字段识别 | 列号 / 原始列名 / 识别为 / 置信度 / 识别方式 / 样例值 + 未映射列 + 仲裁日志 | 认错列时查列号,配 --override 重跑 |
实现约束(改这个文件时必须守住):
- 列对齐:明细行逐列拼装,
len(line) != len(headers)直接抛错。容器父任务分支只补 「分层/排名/小计/K偏离/K合计」5 列(有效维度数与完整度在分支前已追加),多补就会越界。 - 容器父任务必须出现在明细里(标"不计分(防父子重复计分)"),否则用户会以为漏了任务。
P.COVERAGE_RANK/COVERAGE_NORMAL是百分制数值(45.0/60.0),格式化用:.0f%而不是:.0%。- 未映射列要跳过空白列名(真实表常有几十个空列)。
- 写入前清 Excel 非法控制字符并截断超长文本(备注列经常带换行与超长文本)。
五个输出分支(都是正常路径,不是异常)
| 分支 | 触发 | 结果能用于什么 |
|---|---|---|
A_正常 |
启用维度权重 ≥60%,n_eff ≥3 | 可排名,作分配参考 |
B_可排名 |
权重 45–60%,或单维归一权重 >35%(预警线),或无日期锚点 | 可排名,不建议直接分配 |
D_体系退化 |
n_eff <3,或难度排名与工时排名一致性 >0.85,或有效维度 ≤2(40% 硬上限数学不可满足) | 仅排序参考,禁作分配依据 |
C_仅描述 |
权重 <45%,或任务数 <3,或 >50% 任务无任何有效维度 | 仅描述统计,不出总分 |
E_失败 |
无任务名或描述列 / 表头定位失败 / 文件不可读或加密 | 不出分,回显失败三件套 |
门槛口径(已按用户要求放宽):只要识别到任务名称或描述列就执行分析, 缺列不再阻断——缺失的维度不参与计算(走降权重重分配),只在报告里提示。 唯一阻断条件是「连任务名/描述都认不出来」。「无任何可量化维度」由阻断改为降级提示 (
W_NO_QUANT_DIM),落到C_仅描述分支,仍出逐任务难度与排序,但不输出工分。
优先级 E → C → D → B → A,命中即停。分支 B/C/D 不停流程、不弹询问,只在报告首屏加固定提示句;
在 C/D 分支下不得给出任何分配建议,也不输出工分单价(MD 里工分列渲染为「—」,
Excel 里「工分 P」列留空,另给一列明标「参考工分(仅供排序对照·不可用于分配)」)。
确需工分时由人工显式加 --force-points,脚本会在保单与 Excel 汇总页追加"强制输出"警示 ——
默认不给,脚本不替人做这个决定。
红线
- 不产出"某人应得 X 分 / 应评为 X 级"。要按人汇总,只能说"该人负责任务的工分合计",并注明"不等同于个人绩效结论,用于分配前须经团队共识与人工复核"。
- 不产出淘汰/扣罚名单,也不设计这类规则。
- 不得静默改分。工时偏离度打折、疑似拆分、分布偏斜——全部只提示 + 列清单,判断权归人工。
- F 技能门槛永不进小计(即使有花名册)。职级→门槛的映射会让同级员工的所有任务获得加成,且同一任务换人做就变分,属于对特定人群的系统性偏差。
- 数据不足不得伪装成可用。权重合计 <45% 或 n_eff <3 时,必须显式写「仅排序参考」。
- 报告必须带红线声明;失败时必须回显三件套(①已识别列名 ②缺失字段 ③建议补充列)。
- 估算值不得冒充实测值。关键词推断出的难度必须标
estimated,且标注要出现在总分旁边,不能只写在方法说明里。
关键口径
| 项 | 口径 |
|---|---|
| 单任务小计 | S = Σ(level_i × W_i) / Σ(W_i有效),缺失维度从分子分母同时剔除,不填 0、不填中位 3 |
| 单任务工分 | P = S × 20 × K_偏离 × K_体量 × K_角色 × K_子任务。所有 K 只乘在工分上,不进小计 S;K_体量 默认关闭(K_VOLUME_ENABLED=False),K_角色 默认 1.0 |
| 工时偏离度 | K_偏离 = h_eff / h,h_eff = min(h,P90) + 0.3×max(0,h−P90);n≥10 启用;物理可行豁免:有起止日期且 h ≤ 自然日×8×0.7×1.5 → K_偏离 = 1。被打折任务逐条单列进报告第五节,证据含原始 h、P90、h_eff、K 值 |
| ⚠️ 分档输入 | 分位数与绝对档一律用原始工时 h,不得用 h_eff(绝对档下用它等于二次惩罚,与 K_偏离 重复打折) |
| ⚠️ 不存在 | hours_deviation_weight_compression 开关、「削 B 权重 + 回补」写法——只有上面一种打折实现 |
| 分档 | P10/30/70/90 右闭;n<5 或 P10==P90 → 绝对档 4/16/40/80h |
| 完整度 | 分母 = 本批启用维度权重和(不是固定 100),另输出 completeness_vs_100 对照。分层两刀:L1 ≥90%/L2 60%–90%/L3 <60% |
| 总分 | 只对 L1+L2 求和(INCLUDE_L3_IN_TOTAL=False);L3 不参与分配,另出一行含 L3 的参考总分 |
| 冻结 | 无任何有效维度的任务不填 0、不进任何层、不排名,单列第六节 |
| 覆盖度(批次级) | min(启用维度基础权重和, 100) / 100;≥60% 分支 A/45–60% 分支 B/<45% 分支 C |
| 40% 封顶 | 单维归一权重 >40% 触发回流;有效维度 ≤2 时数学不可满足 → 强制 D 分支且不输出工分 |
| 相关性 | Spearman,n≥10 才执行(n<10 不执行,不猜) |
| C 经济效益 | 覆盖率按「非空且非 0」计,<50% 整批关闭,走通用归一化 |
| A 困难程度 | 关键词规则表(params.A_KEYWORD_TABLE,剥离排除词 → 最长匹配去重 → 取最低档)。只要有任务名/描述就启用:命中率 <20% 只提示「建议校准词表」、不再关闭维度。匹配分两级源:① 任务名(+交付物列)优先;② 任务名无命中才用备注/描述列,依据前缀 描述: 区分。无模型参与 |
| J 复用价值 | 三档枚举 5/3/1;表里必须有交付物列且命中率 ≥20% 才启用 |
| 子任务折价 | 父任务被判独立任务时,子任务 ×0.60 防重复计分;父被判容器时父子不重复计 |
完整锚点、权重算例、自检项与参数表见 references/scoring_anchors.md。
脏数据处理
| 现象 | 处理 |
|---|---|
| 表头不在第一行 | 前 15 行打分定位,阈值 0.45,失败回显前 15 行 |
| 多 Sheet | 评分选表,分差 <1.5 报歧义让用户选,默认不合并 |
| 纵向合并任务名 | 按锚点聚合成 1 条,工时/金额求和(防成倍放大) |
| 合计/小计行 | 关键词 + 可加性自检(没有"合计"二字也能抓)双重判定 |
2人天 / 8h / 约3天 / 待定 混排 |
单位三来源:列名 > 日期锚点反算 > 数量级推断 |
¥12,000 / 1.2万 / 12k |
统一转元;列名已含"万"不重复乘 |
- 待定 N/A |
统一空值集,转 NaN 并计入缺失率 |
| 「状态」列 | 内容仲裁:枚举含 通过/驳回/返工/验收 → 归质量;含 已完成/进行中 → 归进度,不参与打分 |
| 「优先级 P0-P3」 | 只喂紧迫性,不喂风险(急 ≠ 错了多严重) |
| 多区块考核表(区块标题 + 独立表头 + 数据,重复多次) | detect_blocks 切分区块、每块独立识别表头与列后合并。闸门:第 2 个及以后的表头行正上方必须是区块标题行,否则不切(防单表被切碎) |
| 月评/计划/意见栏被当任务 | is_non_task_row 剔除:整行同值(合并标题)、单格文本、或命中 自我月评/下月计划/意见/签字 等 |
| 序号列被当成工时 | 公差为 1 的连续整数列判为 seq,不映射工时(否则 1–13 变成 8–104 小时,排名由行号决定且不报错) |
完整清单见 references/field_dictionary.md(24 项质检 + 别名表 + 解析正则)。
参数校准
所有经验值集中在 scripts/params.py,禁止散落硬编码。标注 [需校准] 的必须按团队实际校准后方可用于分配,重点四项:
HOURS_PER_MANDAY = 8.0—— 研发岗常见 6.5–7(单位统一时不影响分位数分档,影响较小)WORKDAY_FACTOR = 0.7+ 无节假日日历 → 工期高估 10–12%,影响面大HOURS_DEVIATION_FACTOR = 0.3(超 P90 部分保留比例)ABSOLUTE_BANDS/COLLAB_LEVEL_CUTS/LOOSE_*/ENTROPY_MIN
换算系数会冻结进 baseline.json,跨批次系数不一致时自动禁用环比比较。
已验证 / 已知限制
已跑通并回归验证(10 份 fixture,tests/):
| fixture | 验证点 |
|---|---|
fixture_dirty.xlsx |
表头在第 3 行、纵向合并聚合成 1 条、无「合计」字样但等值于上方之和的伪合计行(靠可加性自检抓到)、工时混排 2人天/8h/约3天/待定、金额混排 ¥12,000/1.2万、480h 与 2 天工期的矛盾行只标记不改分、真实 160h 长任务豁免打折 |
fixture_status_progress.xlsx |
「状态」列枚举为 已完成/进行中 → 判为进度列、不映射质量维(否则所有已完成任务同分、E 维退化成常数) |
fixture_container.xlsx |
容器父任务可加性自检(父 40h = 子 15+15+10 → 父不计分);父工时 >> 子之和 → 父判独立任务、子任务 ×0.6 折价 |
fixture_serial_date.xlsx |
Excel 序列号日期(46027 = 2026-01-05)反算可用工时,不被当成工时/金额 |
fixture_double_header.xlsx |
双层表头(父行纵向/横向合并 + 子行细分 → 拼成 工时投入|投入工时)+第三行「单位:人天」单位行 → 工时 ×8 |
fixture_minimal.xlsx |
3 维、n_eff<3 → D 分支,工分列降级为「—」 |
fixture_only_hours.xlsx |
仅工时 → C 分支,只出描述统计;同时作为 --force-points 回归用例(强制出工分不报错) |
fixture_gbk.csv |
GBK 编码 CSV,全角 ¥ 金额 |
fixture_no_dim.xlsx |
无可量化维度(只有任务名+负责人)→ 按放宽后的门槛不再阻断,落 C 分支只出描述统计 + 提示可补充的列(旧口径为退出码 1) |
fixture_noheader.xlsx |
表头定位失败 → 退出码 1 + 前 15 行回显 |
已实现但未做回归验证(遇到时以实际输出为准,不要向用户承诺):
.xls旧格式与加密文件(需 xlrd / msoffcrypto,默认环境未装)- 多 Sheet 歧义分支(分差 <1.5 需人工指定)、分页重复表头剔除、列级横向合并
- 「单位行」只解出工时的单位;金额的
万元单位行暂未回填到金额换算(同列同尺度,不影响相对档位) - Excel 输出不回写原文件(不改动用户原表),只另存
*.gongfen.xlsx;如需"原表+打分列"的合表形式, 用明细表的「原表行号」列自行 VLOOKUP 关联,不要在脚本里改写用户文件
依赖
Python ≥3.9(实测 3.13)|pandas / openpyxl / numpy / python-dateutil,版本见 requirements.txt。
文件结构
scripts/
params.py 全部经验参数(唯一来源)
excel_io.py 读取、编码嗅探、选表、表头定位、合并展开
field_recognize.py 别名词典、字段识别、仲裁、单位归一、层级
quality_stats.py 24 项质检、分档、覆盖度、自检、baseline
score.py 小计、分层、层内排名、总分(只读 JSON)
report.py Markdown 报告、固定句、失败回显
excel_out.py 打分结果 Excel(工分明细/批次汇总/质检与自检/字段识别)
run.py 编排入口
references/
scoring_anchors.md 评分锚点、动态权重、参数表、红线(口径来源)
field_dictionary.md 别名词典/单位表/解析规则/质检清单(模型与脚本共用)
tests/
make_fixture.py 生成「故意做脏」的测试表(6 份)
make_fixture_extra.py 生成 4 份高危场景测试表(状态列/容器父任务/序列号日期/双层表头)
run_regression.py 回归入口:跑全部 fixture + --force-points 用例,可接真实表路径
python tests/run_regression.py # 只跑 fixture
python tests/run_regression.py 你的真实表.xlsx # 额外跑一份真实表
预期:fixture_noheader.xlsx 退出码 1(故意失败),其余全部 0;末尾打印"全部符合预期"。
