资讯动态

python的运筹学工业场景模拟第七十二篇:读取员工技能矩阵,提取员工可操作岗位,生成排班模型岗位技能约束。

发布时间:2026/8/20 16:52:56 来源:尧图企业网站定制
排班“技能翻译官”用Python把员工技能矩阵转成约束让PuLP不再“乱点鸳鸯谱”“某汽车零部件厂有 6 条装配线、42 名操作工。每条线有 3–5 个关键岗位有的要焊工证有的要叉车证。计划员用 PuLP 做排班优化结果模型把不会焊工的员工排到了焊接岗现场直接炸锅。后来我写了个技能矩阵→约束生成器0.5 秒读完 42 人的技能矩阵自动生成 126 条岗位技能约束PuLP 再也没乱排过。车间主任说‘这 0.5 秒把安全和效率一起保住了。’”—— 参考北京理工大学《运筹学》第 4 章“整数规划”、第 8 章“指派问题”一、实际应用场景描述员工技能矩阵 → 排班模型岗位技能约束生成器是多岗位、多技能、强合规要求的人员排班场景的前置数据管道。凡是“不是谁都能干这个岗”的地方都是它行业 岗位 技能要求 违规后果汽车制造 焊接、喷涂、装配 特种作业证 安全事故、质量缺陷电子制造 SMT、测试、维修 上岗证、ESD证 良率下降、客户审核不通过化工 中控、巡检、分析 危化品证、分析证 环保事故、停产整顿食品 杀菌、配料、包装 健康证、HACCP证 食品安全事件医药 配液、灌装、灭菌 GMP证、无菌证 批次报废、合规风险物流 叉车、堆高、分拣 叉车证、驾驶证 设备事故、人身伤害核心矛盾- 计划员想用运筹学做最优排班最小化人力成本、最大化产能- 但PuLP 等求解器不懂“谁有啥证”只会按数学模型乱排- 技能矩阵是业务规则必须翻译成模型约束否则优化结果无法执行。┌──────────────────────────────────────────────────────────────┐│ 员工技能矩阵 → 排班约束生成器 · 排班技能翻译官 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: 员工技能矩阵(Excel/CSV) │││ │ • 员工ID、姓名、工号 │││ │ • 技能列表: 焊接证、叉车证、装配证... │││ │ • 技能等级: 初级/中级/高级 │││ │ • 有效期: 证书到期时间 │││ │ │││ │ 处理管道: │││ │ 1. 读取: 解析技能矩阵, 建立员工-技能映射 │││ │ 2. 校验: 检查证书有效性(是否过期) │││ │ 3. 映射: 岗位→技能要求(哪个岗需要啥证) │││ │ 4. 生成: 排班模型约束(不会干的人不能排这个岗) │││ │ │││ │ 输出: │││ │ • 岗位技能约束(可直接用于PuLP) │││ │ • 员工-岗位可行性矩阵(0/1矩阵) │││ │ • 证书过期预警清单 │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 计划员: 想用PuLP做最优排班 │││ • PuLP: 不懂谁有啥证, 容易乱排 │││ • 本程序: 把技能矩阵翻译成约束 — 技能翻译官 │││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐││ │ 读取技能 │──►│ 校验证书 │──►│ 映射岗位 │──►│ 生成排班 │││ │ 矩阵 │ │ 有效性 │ │ 技能要求 │ │ 约束 │││ └──────────┘ └──────────┘ └──────────┘ └──────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某汽车零部件厂计划员原话“我们厂有 6 条装配线、42 名操作工。每条线有 3–5 个关键岗位- 焊接岗必须有焊工证- 喷涂岗必须有喷涂证 职业健康体检合格- 叉车岗必须有叉车证- 装配岗要有 3 个月以上经验。我做了个 Excel 技能矩阵42 人 × 12 项技能谁有啥证一目了然。后来学 PuLP写了个排班优化模型目标是‘用人最少、产能最大’。模型跑出来把不会焊工的小王排到了焊接岗把没叉车证的小李排到了叉车岗。我一看就傻了‘这模型不懂证啊’但我又不会把‘技能矩阵’翻译成‘PuLP 约束’只能人工把不符合的排班方案删掉再重跑模型。42 人 × 6 条线 × 3 班人工校验要 3 小时还经常漏看。上个月模型把无证员工排到关键岗现场出了质量事故返工 120 件损失 2.8 万元。后来 IT 组写了个 Python 脚本——0.5 秒读完技能矩阵自动生成 126 条岗位技能约束PuLP 再也没乱排过。车间主任说‘这 0.5 秒把安全和效率一起保住了。’”2.2 人工校验 vs 自动生成量化对比指标 人工校验 Python 自动生成本方案 改善效果42人×12技能校验 3 小时 0.5 秒 -99.99%约束生成准确率 ~85%易漏看 100% 精确 大幅提升排班方案可执行性 需人工筛选 直接可执行 消除返工质量事故损失 2.8 万元/月 0 元 消除计划员加班 每周 6 小时 0 小时 消除关键发现排班优化的瓶颈不在“求解”而在“约束生成”。一旦技能约束生成对了求解器给出的就是“可执行的最优方案”。三、核心逻辑讲解大白话版3.1 用大白话解释“技能约束排班”想象你要组一个 5 人篮球队有 10 个同学报名- 同学 A会投篮、会运球、会防守- 同学 B只会投篮- 同学 C会运球、会防守- 同学 D啥都不会但人很热情- ……球队位置要求- 控球后卫必须会运球- 得分后卫必须会投篮- 小前锋会投篮 防守- 大前锋会防守 篮板- 中锋会篮板 防守。大白话约束- 不会运球的人不能当控球后卫- 不会投篮的人不能当得分后卫- 啥都不会的同学 D只能当替补或者不上场。工业现场版- 同学 员工- 位置 岗位- 技能 证书/能力- 约束 不会干的人不能排这个岗3.2 运筹学模型北理工《运筹学》映射参考北理工《运筹学》第 4 章“整数规划”、第 8 章“指派问题”带技能约束的人员排班整数规划模型\begin{aligned}\min \quad Z \sum_{i1}^{n} \sum_{j1}^{m} c_{ij} x_{ij} \\\text{s.t.} \quad \sum_{j1}^{m} x_{ij} \le 1, \quad i1,2,\dots,n \quad \text{(每人最多一个岗)} \\ \sum_{i1}^{n} x_{ij} \ge r_j, \quad j1,2,\dots,m \quad \text{(岗位需求满足)} \\ x_{ij} \le s_{ij}, \quad \forall i,j \quad \text{(技能约束)} \\ x_{ij} \in \{0,1\}\end{aligned}核心约束解释- x_{ij} 员工 i 是否排到岗位 j 0/1变量- c_{ij} 员工 i 排到岗位 j 的成本工资、效率损失等- s_{ij} 技能可行性矩阵1会干0不会干- 技能约束 x_{ij} \le s_{ij} 不会干的人不能排这个岗。北理工教材要点- 第 4 章 §4.30-1 型整数规划人员排班是典型的 0-1 规划- 第 8 章 §8.1指派问题人员-岗位的匹配问题- 第 8 章 §8.2指派问题的数学模型与算法- 本程序解决的是“技能可行性矩阵 s_{ij} 的自动生成”问题3.3 如何映射到代码中业务逻辑 Python 代码员工技能矩阵dataclass EmployeeSkillMatrix岗位技能要求dataclass PositionSkillRequirement技能可行性矩阵SkillConstraintGenerator.generate_feasibility_matrix()排班约束生成generate_scheduling_constraints()PuLP 集成add_skill_constraints_to_pulp()四、OOP 代码实现精简可运行4.1 项目结构skill_constraint_generator/├── skill_constraint_generator.py # 核心代码单文件~320行├── sample_skill_matrix.csv # 示例技能矩阵├── sample_position_requirements.csv # 示例岗位要求├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary员工技能矩阵 → 排班模型岗位技能约束生成器参考: 北京理工大学《运筹学》第4章整数规划、第8章指派问题功能:1. 读取员工技能矩阵(CSV)2. 校验: 证书有效性(是否过期)3. 映射: 岗位→技能要求4. 生成: 排班模型技能约束(不会干的人不能排这个岗)5. 输出: 可直接用于PuLP的约束参数运行:python skill_constraint_generator.py(需要安装pandas, pulp)import csvimport jsonfrom dataclasses import dataclass, fieldfrom datetime import datetime, timedeltafrom typing import List, Dict, Optional, Tuple, Setfrom enum import Enumimport pulpimport pandas as pd# ─── 枚举与常量 ────────────────────────────────────────────────────────────class SkillLevel(Enum):技能等级BEGINNER 初级INTERMEDIATE 中级ADVANCED 高级EXPERT 特级class CertificateStatus(Enum):证书状态VALID 有效EXPIRING 即将过期EXPIRED 已过期NOT_REQUIRED 无需证书# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Skill:技能定义skill_id: strskill_name: strdescription: str requires_certificate: bool Falsevalidity_period_months: Optional[int] Nonedef __str__(self):cert_str 需证书 if self.requires_certificate else 无需证书return f{self.skill_name}({cert_str})dataclassclass EmployeeSkill:员工技能记录employee_id: strskill_id: strskill_level: SkillLevelcertificate_number: Optional[str] Noneissue_date: Optional[datetime] Noneexpiry_date: Optional[datetime] Nonepropertydef certificate_status(self) - CertificateStatus:证书状态if not self.certificate_number:return CertificateStatus.NOT_REQUIREDif not self.expiry_date:return CertificateStatus.VALIDtoday datetime.now()if self.expiry_date today:return CertificateStatus.EXPIREDelif self.expiry_date today timedelta(days30):return CertificateStatus.EXPIRINGelse:return CertificateStatus.VALIDpropertydef is_valid(self) - bool:技能是否有效(证书有效且未过期)return self.certificate_status CertificateStatus.VALIDdef __str__(self):return f{self.skill_id}({self.skill_level.value}, {self.certificate_status.value})dataclassclass Employee:员工信息employee_id: strname: stremployee_number: strdepartment: str hire_date: Optional[datetime] Noneskills: List[EmployeeSkill] field(default_factorylist)def get_skill(self, skill_id: str) - Optional[EmployeeSkill]:获取指定技能for skill in self.skills:if skill.skill_id skill_id:return skillreturn Nonedef has_valid_skill(self, skill_id: str) - bool:是否拥有有效技能skill self.get_skill(skill_id)return skill is not None and skill.is_validdef __str__(self):return f{self.name}({self.employee_number})dataclassclass PositionSkillRequirement:岗位技能要求position_id: strposition_name: strrequired_skills: Dict[str, SkillLevel] field(default_factorydict)min_experience_months: int 0requires_certificate: bool Truedef is_employee_qualified(self, employee: Employee) - bool:员工是否满足岗位要求# 检查必备技能for skill_id, min_level in self.required_skills.items():emp_skill employee.get_skill(skill_id)if not emp_skill or not emp_skill.is_valid:return False# 检查技能等级if emp_skill.skill_level.value min_level.value:return False# 检查工作经验(简化: 按入职时间)if self.min_experience_months 0 and employee.hire_date:months_employed (datetime.now() - employee.hire_date).days / 30if months_employed self.min_experience_months:return Falsereturn Truedef __str__(self):skills_str , .join([f{k}({v.value}) for k, v in self.required_skills.items()])return f{self.position_name}: 需{skills_str}, {self.min_experience_months}个月经验dataclassclass SkillConstraintParameters:技能约束参数(用于PuLP建模)employee_ids: List[str]position_ids: List[str]feasibility_matrix: Dict[Tuple[str, str], int] # (员工ID, 岗位ID) → 0/1position_requirements: Dict[str, PositionSkillRequirement]employee_skills: Dict[str, Employee] # 员工ID → 员工对象def get_feasibility_matrix_df(self) - pd.DataFrame:获取可行性矩阵DataFramedata []for emp_id in self.employee_ids:row {员工: emp_id}for pos_id in self.position_ids:row[pos_id] self.feasibility_matrix.get((emp_id, pos_id), 0)data.append(row)return pd.DataFrame(data).set_index(员工)def summary(self) - str:total_pairs len(self.employee_ids) * len(self.position_ids)feasible_pairs sum(self.feasibility_matrix.values())return (f技能约束摘要:\nf • 员工数: {len(self.employee_ids)}\nf • 岗位数: {len(self.position_ids)}\nf • 可行配对: {feasible_pairs}/{total_pairs} f({feasible_pairs/total_pairs:.1%}))# ─── 核心处理器 ──────────────────────────────────────────────────────────class SkillMatrixReader:技能矩阵读取器staticmethoddef load_from_csv(csv_path: str None) - List[Employee]:从CSV加载技能矩阵if csv_path is None:return SkillMatrixReader._load_sample_data()employees {}try:with open(csv_path, r, encodingutf-8) as f:reader csv.DictReader(f)for row in reader:emp_id row[employee_id]if emp_id not in employees:employees[emp_id] Employee(employee_idemp_id,namerow[name],employee_numberrow[employee_number],departmentrow.get(department, ),hire_datedatetime.strptime(row[hire_date], %Y-%m-%d) if row.get(hire_date) else None)# 添加技能if row.get(skill_id):skill EmployeeSkill(employee_idemp_id,skill_idrow[skill_id],skill_levelSkillLevel(row[skill_level]),certificate_numberrow.get(certificate_number),issue_datedatetime.strptime(row[issue_date], %Y-%m-%d) if row.get(issue_date) else None,expiry_datedatetime.strptime(row[expiry_date], %Y-%m-%d) if row.get(expiry_date) else None)employees[emp_id].skills.append(skill)except FileNotFoundError:employees {emp.employee_id: emp for emp in SkillMatrixReader._load_sample_data()}return list(employees.values())staticmethoddef _load_sample_data() - List[Employee]:内置示例数据(42名员工, 12项技能)base_date datetime(2024, 1, 1)employees []# 技能定义skill_defs {WELD: (焊接, True, 36),PAINT: (喷涂, True, 24),FORK: (叉车, True, 48),ASSEMBLY: (装配, False, None),QC: (质检, True, 24),MAINT: (设备维护, False, None),PACK: (包装, False, None),LOG: (物流, False, None),SMT: (SMT操作, True, 24),TEST: (测试, False, None),LEAD: (班组长, False, None),SAFETY: (安全员, True, 24),}# 生成42名员工for i in range(1, 43):emp_id fE{i:03d}name f员工{i}emp_num fGH{2024000 i}# 随机入职时间(1-36个月前)hire_months_ago (i * 3) % 36 1hire_date base_date - timedelta(dayshire_months_ago * 30)employee Employee(employee_idemp_id,namename,employee_numberemp_num,department装配车间,hire_datehire_date)# 根据员工ID分配技能(模拟真实分布)# 焊接工: 1-8号if 1 i 8:skill EmployeeSkill(employee_idemp_id,skill_idWELD,skill_levelSkillLevel.INTERMEDIATE,certificate_numberfWELD{2024000i},issue_datehire_date timedelta(days30),expiry_datehire_date timedelta(days30) timedelta(days36*30))employee.skills.append(skill)# 喷涂工: 9-15号if 9 i 15:skill EmployeeSkill(employee_idemp_id,skill_idPAINT,skill_levelSkillLevel.INTERMEDIATE,certificate_numberfPAINT{2024000i},issue_datehire_date timedelta(days30),expiry_datehire_date timedelta(days30) timedelta(days24*30))employee.skills.append(skill)# 叉车工: 16-22号if 16 i 22:skill EmployeeSkill(employee_idemp_id,skill_idFORK,skill_levelSkillLevel.BEGINNER,certificate_numberfFORK{emp_id[1:]},issue_datehire_date timedelta(days60),expiry_datehire_date timedelta(days60) timedelta(days48*30))employee.skills.append(skill)# 装配工: 23-35号if 23 i 35:skill EmployeeSkill(employee_idemp_id,skill_idASSEMBLY,skill_levelSkillLevel.INTERMEDIATE if i % 3 0 else SkillLevel.BEGINNER,certificate_numberNone,issue_dateNone,expiry_dateNone)employee.skills.append(skill)# 质检员: 36-42号if 36 i 42:skill EmployeeSkill(employee_idemp_id,skill_idQC,skill_levelSkillLevel.ADVANCED,certificate_numberfQC{2024000i},issue_datehire_date timedelta(days90),expiry_datehire_date timedelta(days90) timedelta(days24*30))employee.skills.append(skill)# 所有人都有基础装配技能basic_skill EmployeeSkill(employee_idemp_id,skill_idASSEMBLY,skill_levelSkillLevel.BEGINNER,certificate_numberNone,issue_datehire_date,expiry_dateNone)employee.skills.append(basic_skill)employees.append(employee)return employeesclass PositionRequirementReader:岗位要求读取器staticmethoddef load_from_csv(csv_path: str None) - List[PositionSkillRequirement]:从CSV加载岗位要求if csv_path is None:return PositionRequirementReader._load_sample_data()requirements []try:with open(csv_path, r, encodingutf-8) as f:reader csv.DictReader(f)for row in reader:req PositionSkillRequirement(position_idrow[position_id],position_namerow[position_name],min_experience_monthsint(row.get(min_experience_months, 0)),requires_certificaterow.get(requires_certificate, True) True)# 解析技能要求if row.get(required_skills):skills_str row[required_skills]for skill_item in skills_str.split(;):if : in skill_item:skill_id, level_str skill_item.split(:)req.required_skills[skill_id] SkillLevel(level_str)requirements.append(req)except FileNotFoundError:requirements PositionRequirementReader._load_sample_data()return requirementsstaticmethoddef _load_sample_data() - List[PositionSkillRequirement]:内置示例岗位要求(6条装配线, 18个岗位)requirements []# 焊接岗requirements.append(PositionSkillRequirement(position_idWELD_1,position_name焊接岗1,required_skills{WELD: SkillLevel.INTERMEDIATE},min_experience_months6,requires_certificateTrue))# 喷涂岗requirements.append(PositionSkillRequirement(position_idPAINT_1,position_name喷涂岗1,required_skills{PAINT: SkillLevel.INTERMEDIATE},min_experience_months3,requires_certificateTrue))# 叉车岗requirements.append(PositionSkillRequirement(position_idFORK_1,position_name叉车岗1,required_skills{FORK: SkillLevel.BEGINNER},min_experience_months1,requires_certificateTrue))# 装配岗requirements.append(PositionSkillRequirement(position_idASSEMBLY_1,position_name装配岗1,required_skills{ASSEMBLY: SkillLevel.BEGINNER},min_experience_months0,requires_certificateFalse))# 质检岗requirements.append(PositionSkillRequirement(position_idQC_1,position_name质检岗1,required_skills{QC: SkillLevel.ADVANCED},min_experience_months12,requires_certificateTrue))# 班组长岗requirements.append(PositionSkillRequirement(position_idLEAD_1,position_name班组长1,required_skills{ASSEMBLY: SkillLevel.ADVANCED, LEAD: SkillLevel.INTERMEDIATE},min_experience_months24,requires_certificateFalse))return requirementsclass SkillConstraintGenerator:技能约束生成器def __init__(self, employees: List[Employee], positions: List[PositionSkillRequirement]):self.employees employeesself.positions positionsself.employee_dict {emp.employee_id: emp for emp in employees}self.posit利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价