简介:这是一份面向办公管理场景的日常工作排班记录与可视化展示工具,资源以 React + TypeScript 前端项目形式呈现,适合需要快速搭建排班或任务追踪界面的开发者、产品经理及行政管理人员参考。包内共 32 个文件,以 tsx 组件、ts 逻辑、json 配置、css 样式及 html 入口等为主,压缩包整体约 57KB,目录结构清晰,可帮助读者了解基于组件化的日常排班/计划管理前端的组织方式。资源目前已获得 27 人学习下载。通过该资源,读者可以借鉴多视图页面(如今日、计划、历史、统计)的数据展示思路,学习如何运用进度环、甘特图、日期选择器等交互组件实现排班与任务可视化,并参考状态管理、工具函数、类型定义等基础架构设计,便于在此基础上扩展成完整的考勤、工时或人员调度应用。
1. 排班记录表及可视化展示:先想清楚月底要怎么用这张表
月底对着几百行排班记录表核工时,发现有人加班算了三天还在漏——这种翻车基本都出在同一件事:排班记录表只被当成“记事的表”,没被当成“数据源”。这个标题要解决的就是把它升级成结构化记录,再配一层可视化展示,让每天的班次一眼能查,月底的数字一算就出。适合门店值班、客服班次、运维值守这类固定排班场景,也适合每月还在手工拼图表的排班负责人。
我见过太多排班表第一天就在给自己埋雷:日期填成文本、班次写“早/晚/中”、人员没有唯一编号、换班信息直接盖在备注栏里。等到做月度汇总,几十号人的数据全靠肉眼筛,工作量大不说,结果还没法复核。把这张表当成数据库来设计,是后面一切统计和图表的起点。文章按我实际跑通过的路径展开:先定表结构,再清洗统计,再出图,最后讲几个必踩的坑。
2. 排班表的数据模型:字段、班次编码和日期维度怎么定才不返工
排班表最忌讳一上来就画“好看”的表格。先想清楚一个月后你要统计哪些数字:每个人上了几个班、各是什么班次、总工时多少、哪天缺人、节假日排了谁。这些诉求直接决定了表里必须有哪些字段、哪些字段要单独拆开。
2.1 字段设计的底线:人员、日期、班次、岗位缺一不可
我一般的做法是让排班主表保持“一列一个事实”的宽表结构,每行只记录一条排班事实。核心字段如下:
| 字段名 | 类型 | 示例 | 说明 |
|---|---|---|---|
| 日期 | 日期,非文本 | 2024-06-01 | 排班发生日 |
| 人员ID | 文本/数字 | E001 | 人员唯一标识,与姓名分离 |
| 姓名 | 文本 | 张三 | 仅用于展示 |
| 班次 | 编码 | A / B / C / 休 | 见班次编码表 |
| 岗位 | 文本 | 前台 / 收银 | 同一天同一人可排不同岗位 |
| 工时 | 数值 | 8 | 该班次折算小时数 |
| 备注 | 文本 | 换班:张三替李四 | 只记录,不参与统计 |
人员ID和姓名必须分开。很多人觉得多此一举,但重名和离职复用会让“按姓名统计”直接串线。ID用“E+三位数字”这种纯文本格式,避免读进脚本后被自动转成小数。
班次和岗位分开的原因是:同一天同一个排班可能同时包含“班次”和“岗位”两个业务维度。比如同样是晚班,一个人在前台,另一个人在收银。把岗位塞进备注,等于放弃用数据筛选岗位分布的能力。备注栏永远只放“人读的”信息,不放“机器要算的”信息。
2.2 班次编码与工时换算:把中文班次变成机器能算的数字
中文班次“早班/晚班/夜班”在统计时是灾难,因为同一个叫法在不同门店含义可能完全不同。我会在排班表旁边放一个固定编码表,这个编码表本身就是数据字典,脚本读取时直接引用。
| 编码 | 含义 | 起止时间 | 工时 |
|---|---|---|---|
| A | 早班 | 08:00-16:00 | 8 |
| B | 晚班 | 16:00-24:00 | 8 |
| C | 夜班 | 00:00-08:00 | 8 |
| D | 跨夜班 | 22:00-次日06:00 | 8 |
| E | 休 | – | 0 |
工时列放在排班记录表里而不是让脚本现算,是因为排班可能中途调整:某天临时加班加了2小时,或早班提前一小时下班。如果工时只存在编码表里,这些调整就无处安放。所以工时列要允许被覆盖,但编码表给出默认值。
跨夜班D的处理是工时统计里最容易出错的地方。从22点到次日6点,表面上看结束时间比开始时间小,直接相减就是负数。我会在清洗时增加一列“结束日期”,当结束时间小于开始时间时,结束日期=日期+1天。这样无论用Excel还是用脚本统计,工时都是正的。
2.3 日期与节假日维度:跨月统计和排班模板的元数据
排班记录表里的“日期”只是一个事实,但统计时经常要按“第几周”“是否节假日”“属于哪个月”来切分。与其在每次统计时临时判断,不如单独维护一张日期维度表,把排班主表和它用日期关联起来。
日期维度表的常见做法是一个辅助sheet或CSV,每行一个日期,字段包括:日期、年、月、周序号、星期、是否工作日、是否法定节假日。这张表一次生成,全年复用。统计节假日加班时直接关联它筛出“节假日=是”的记录,不用每年去查“几号放几天假,哪天调休”。
维护这张表的最大好处是口径统一。比如“周末”的定义在各人眼里不一样,有人觉得周五晚上就算周末,有人只看周六周日。日期维度表统一了“周末”的判定后,所有报表就都长得一样了。
3. 用 pandas 把排班记录变成统计报表:清洗、透视与导出
表结构定好之后,剩下的活交给脚本。pandas 是处理这种二维表最顺手的工具,下面三个脚本基本覆盖了“读取-清洗-统计-导出”的完整链路。代码里每一步都拆开了,方便你在自己的文件上改。
3.1 读取 Excel 并统一类型:第一个脚本要解决的问题
拿到排班Excel后不能直接用,先做类型统一和脏数据清理。最常见的脏数据包括:日期列是文本、姓名前后带空格、班次大小写混用、空行夹在数据中间。
import pandas as pd # 读取排班记录表,人员ID按文本读,防止变成科学计数法 df = pd.read_excel("排班记录表.xlsx", sheet_name="排班明细", dtype={"人员ID": str}) # 日期列统一转成 datetime 类型 df["日期"] = pd.to_datetime(df["日期"], errors="coerce") # 姓名去空格,"张三" 和 " 张三" 才不会变成两个人 df["姓名"] = df["姓名"].str.strip() # 班次统一转大写再去空格,防止手写 "a" 和 "A" 不一致 df["班次"] = df["班次"].str.upper().str.strip() # 删除日期、ID、班次全为空的行 df = df.dropna(subset=["日期", "人员ID", "班次"])逻辑说明:errors="coerce"让无法解析的日期变成缺失值而不是直接报错,方便后面检查脏数据。班次列统一成大写后,Excel里手误输入的“a”“A”会被当成同一种班次。dropna的subset指定只按三列判断,避免备注为空就把整行删掉。
参数说明:dtype={"人员ID": str}是专门处理Excel读ID时变成“E001”→“1”这种问题的,写成字符类型后原样保留。如果你的排班表里ID本来就是数字编号,可以去掉这个参数。
3.2 工时、出勤天数与班次分布:统计口径比代码更重要
统计口径要先定清楚,否则代码写得再对,数字也叫不准。“出勤天数”我按“人+日期去重”算,因为同一个人同一天可能跨两个班次;“工时合计”直接对工时列求和;“班次分布”则统计每个人在各班次上的天数。
# 出勤天数:同一人同一天多段排班只算一次出勤 attend_days = (df[df["班次"] != "休"] .groupby(["人员ID", "姓名"])["日期"] .nunique() .reset_index(name="出勤天数")) # 工时合计:先转成数值,再按人员汇总 df["工时"] = pd.to_numeric(df["工时"], errors="coerce") hours = (df.groupby(["人员ID", "姓名"])["工时"] .sum() .reset_index(name="工时合计")) # 班次分布:全表统计每种班次出现的次数 shift_dist = df["班次"].value_counts().reset_index() shift_dist.columns = ["班次", "天数"]逻辑说明:nunique()统计不重复日期数,核心是利用“人+日期”组合做去重。先过滤掉“休”再统计,是为了避免“休班”被当成出勤。pd.to_numeric把工时列转成数值,如果表里有“8h”这种文本会被转成缺失值,这正是需要提前清洗的信号。
参数说明:value_counts()默认按出现次数降序排列,适合直接看哪种班次最多。如果要多维统计(比如“按人员按班次”),这里不够,需要用到下一节的透视表。
3.3 透视表与 Excel 模板联动:按人按班次出月度汇总
前两段脚本产出的是“清单式统计”,报表要的是“矩阵式汇总”:行是人,列是班次,格子是次数。pivot_table 就是干这个的,配合 ExcelWriter 可以一次输出多个 sheet。
# 生成人员×班次的天数矩阵 pivot = pd.pivot_table( df, index=["人员ID", "姓名"], columns="班次", values="日期", aggfunc="count", fill_value=0, ) # 多 sheet 导出:一个文件里带明细、汇总和透视 with pd.ExcelWriter("排班统计_月度.xlsx") as writer: df.to_excel(writer, sheet_name="排班明细", index=False) hours.to_excel(writer, sheet_name="工时汇总", index=False) pivot.to_excel(writer, sheet_name="班次透视")逻辑说明:aggfunc="count"对“日期”列计数,每一行的每一条排班记录都被算进对应班次的格子里。fill_value=0把“这个人在这个班次没有排班”的格子填成0,避免图表输出时出现空块。多 sheet 导出让报表和明细装在一个文件里,别人核对数字时不用来回翻文件。
参数说明:index决定透视表的行维度,这里用了人员ID和姓名;columns决定列维度,是班次编码。如果你还想按“岗位”看分布,把columns换成“岗位”即可,脚本不用动。
4. 可视化展示:从 Excel 图表到日历热力图和甘特图
统计数字到位后,可视化展示才谈得上。展示的目的不是“画得好看”,而是回答三个问题:某天谁在岗、某人上了多少班、有没有明显超负荷的排班。选错了展示方式,再好看的图也解决不了这三个问题。
4.1 展示选型:原生图表、Python 图表与 BI 工具怎么选
我的判断标准是数据量和自动化程度,而不是“什么工具流行用什么”。
| 方案 | 适用场景 | 优点 | 短板 |
|---|---|---|---|
| Excel原生图表 | 数据量小、临时汇报 | 上手快,改数方便 | 每次要手动刷新,无法复用 |
| Python matplotlib/plotly | 月度固定报表、要自动化 | 一次写脚本长期复用,可批量出图 | 需要一点代码能力 |
| BI工具(如在线多维表格) | 多人协作、实时看板 | 数据实时更新,看板自助查询 | 排班数据结构迁移成本高 |
如果排班表还停留在Excel手工阶段,直接上BI工具是杀鸡用牛刀。我一般先用手工阶段把“要展示什么”想清楚,再固化到Python脚本里。等排班数据能自动生成了,再考虑把可视化展示搬进在线多维表格,让值班负责人自己筛。
4.2 日历热力图:一眼看出一个月的班次构成
热力图适合展示“人员在横轴、日期在纵轴、颜色表示班次”的矩阵,排班表天然适合。用 matplotlib 手绘一个网格,比套用现成库更可控。
import matplotlib.pyplot as plt import matplotlib.patches as mpatches # 把数据转成 人员×日期 的矩阵,取每天第一条排班 table = df.pivot_table(index="姓名", columns="日期", values="班次", aggfunc="first") # 班次到颜色的映射,空值(没排班)显示白色 color_map = {"A": "#4CAF50", "B": "#FF9800", "C": "#2196F3", "D": "#9C27B0", "休": "#ECEFF1"} fig, ax = plt.subplots(figsize=(16, max(2, len(table) * 0.5))) for i, (_, row) in enumerate(table.iterrows()): for j, val in enumerate(row): ax.add_patch(plt.Rectangle( (j, i), 1, 1, color=color_map.get(val, "#FFFFFF"))) # 在格子里直接写班次编码 ax.text(j + 0.35, i + 0.42, str(val), fontsize=8) ax.set_yticks(range(len(table))) ax.set_yticklabels(table.index) ax.set_xticks(range(len(table.columns))) ax.set_xticklabels(table.columns.strftime("%m-%d"), rotation=90) ax.set_xlim(0, len(table.columns)) ax.set_ylim(0, len(table)) ax.invert_yaxis() # 让第一个人在顶部 plt.show()逻辑说明:pivot_table的aggfunc="first"取每个人每天的第一次排班,因为正常排班一天最多一个主要班次。color_map.get(val, "#FFFFFF")处理缺失值,没排班的日子直接留白。ax.text把班次编码写进格子,让颜色和文字双保险,色弱的人也能读。
参数说明:figsize按人数动态计算高度,人少时不高不扁,人多时自动加长。invert_yaxis()把纵轴倒过来,和Excel表格的习惯一致,第一个人在顶部。字体大小如果出现重叠,可以调小fontsize或加大figsize的高度系数。
4.3 甘特图:把班次起止时间画成时间线
热力图回答“哪天排了谁”,甘特图回答“这个班次从几点到几点”。排班表的班次只有编码,没有起止时间,画甘特图前要先从编码表里把起止时间查出来。
# 班次时间字典:两个值分别是开始小时和持续小时 shift_time = { "A": (8, 8), # 08:00 开始,8小时 "B": (16, 8), "C": (0, 8), # 00:00 开始,跨到白天 "D": (22, 8), # 22:00 开始,跨到次日 } # 取某个人一周的数据,按日期排序 one_person = (df[df["姓名"] == "张三"] .sort_values("日期") .tail(7)) fig, ax = plt.subplots(figsize=(10, 3)) for _, row in one_person.iterrows(): start_h, dur = shift_time[row["班次"]] label = row["日期"].strftime("%m-%d") + " " + row["班次"] ax.barh(label, dur, left=start_h, height=0.6) ax.set_xlabel("小时") ax.set_xlim(0, 24) ax.invert_yaxis() plt.show()逻辑说明:barh的left参数决定班次起点,width参数决定持续时长。跨夜班D从22点开始画到次日6点,在时间轴上会跨过24点,图表自然显示为“从右端伸出去”,配合 x 轴 0-24 就能直观看出跨越。tail(7)取最近7天,适合周报场景。
参数说明:xlim(0, 24)固定时间轴范围,避免不同人的图比例不一致。如果同一人一天有多个班次,行会变成两条横条,这时把height调小或按班次分组着色,读起来更清楚。
4.4 轻量看板:用 HTML 把排班总览发到群里
图表文件每次都要打开来看,不如直接生成一个 HTML 看板,放在共享目录或工作群里,手机浏览器就能打开。用 pandas 的 iterrows 拼表格行是最省事的做法。
html_rows = "".join( "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>".format( r["日期"].strftime("%m-%d"), r["姓名"], r["班次"], r["工时"] ) for _, r in df.iterrows() ) html = f"""<html> <head><meta charset="utf-8"><title>本周排班看板</title></head> <body><table border="1" cellpadding="6"> <tr><th>日期</th><th>姓名</th><th>班次</th><th>工时</th></tr> {html_rows} </table></body></html>""" with open("排班看板.html", "w", encoding="utf-8") as f: f.write(html)逻辑说明:这里把清洗后的 DataFrame 直接渲染成 HTML 表格,一行一个排班记录。encoding="utf-8"是中文 HTML 能正常显示的前提,漏了这个打开就是乱码。这个看板可以和前面的热力图、甘特图一起作为附件输出,也可以把图片路径写进 HTML 里直接内嵌展示。
5. 排班表实操避坑:五个高频翻车点与排查方法
这部分内容来自我实际调过的问题。每一条都是先看到奇怪的结果,再反推原因,最后把规则写进脚本。看完能帮你少走至少一个月的弯路。
5.1 日期格式“看起来对,算起来错”
现象:透视表按日期分组时,顺序变成 01、02、03……10、11,或者同一个日期出现多行;工时统计月份时漏掉部分记录。
原因:Excel 里日期列有两种存储形式——真正的日期类型,和“看起来是日期”的文本。文本日期无法被 pandas 直接识别,分组时被当成字符串处理,顺序全乱。
解决:读取后统一执行df["日期"] = pd.to_datetime(df["日期"], errors="coerce"),转换后检查isnull()的行。出现异常值回 Excel 里看那几格的单元格格式,把文本型日期批量刷成日期格式。
5.2 跨午夜班次的工时算少了半小时
现象:夜班或跨夜班的工时统计出来只有负值,或者明明上了 8 小时,汇总后经常少算半天。
原因:把“22:00-06:00”直接用结束时间减开始时间,得到负 16 小时。如果没有结束日期判断,脚本会把这个班次当成无效数据丢弃或计数为 0。
解决:在清洗阶段增加“结束日期”字段,结束时间小于开始时间时结束日期加 1 天。按编码表给工时兜底,统计时不直接用时间差,而是以工时列为主,避免负值污染汇总。
5.3 人员重名与离职复用导致历史统计串线
现象:两个人的月度工时突然对不上,仔细查发现统计把同名的两人合并了;或者离职人员复岗后,历史数据和新数据混在一起。
原因:排班表里没有人员唯一 ID,统计时用姓名做主键。同名的人在 groupby 时被合并,离职复岗的人因为没有独立 ID,新旧记录无法区分。
解决:先建人员 ID 映射表,每个 ID 对应一个唯一姓名,排班明细里只存 ID 不存姓名;统计完成后再用映射表关联出姓名用于展示。这个改动要在排班模板里强制生效,不能靠填表人自觉。
5.4 透视后的空值与“无排班”被当成正常出勤
现象:热力图里出现大片空白或全白格子,工时统计多算出一批不存在的出勤。
原因:排班表里“没排班”存在两种表达:单元格空白和写了“休”。空白在 pivot_table 里默认变成 NaN,而 NaN 在统计时不排除,可能被某些函数自动填充为 0 或保留。
解决:统一规定“休”必须写“E”,空白只允许出现在“今天还没排班”的临时状态下。透视表加fill_value=0,统计前先执行df["班次"].fillna("E", inplace=True),把空值先归一到“休”,再算工时和出勤。
5.5 模板版本失控引起汇总口径不一致
现象:上个月脚本还能跑,这个月突然报“找不到列”;或者上个月叫“班次”,这个月有人把列改名成“排班”,统计全错。
原因:排班 Excel 是手工维护的模板,每个人改表时可能动列名、加 sheet、插空列。脚本按死列名读取,模板一变就崩。
解决:在脚本开头加一次列名校验,用assert或条件判断检查必需列是否齐全,缺列直接把错误打在日志里并停住,而不是带病执行完再出错的汇总。把这个校验和前面的清洗脚本固定成一个入口函数,每次跑数都走同一套标准。
6. 再进一步:排班自动生成与周报自动产出的落地技巧
能跑通统计和可视化之后,下一步是把“排班生成”也交给脚本。手工排一张几十人的周表,光平衡休息天数和班次轮换就要很久,高频固定场景完全可以用规则自动生成初稿,人工只做微调。
6.1 用轮转规则自动生成初始排班
固定排班最常见的需求是:一班人按 A/B/C 顺序轮转,每人轮满一轮后休息一天,晚班和夜班保持基本均衡。这个规则可以写成差位轮转:第 i 天第 j 个人的班次编码 = 轮转表[(i + j) % 轮转长度],这样同一天不会所有人都上同一个班次。
import pandas as pd # 人员列表:前4人参与三班轮转,最后1人固定夜班 full_time = ["张三", "李四", "王五", "赵六"] night_shift = ["钱七"] shift_cycle = ["A", "B", "C"] # 三天一轮 dates = pd.date_range("2024-06-01", periods=30, freq="D") rows = [] for i, day in enumerate(dates): for j, name in enumerate(full_time): # 差位轮转:同一天4个人的班次互不相同且错开 shift = shift_cycle[(i + j) % len(shift_cycle)] rows.append([day.date(), name, shift, 8]) for day in dates: rows.append([day.date(), "钱七", "C", 8]) df_new = pd.DataFrame(rows, columns=["日期", "姓名", "班次", "工时"]) df_new.to_excel("排班生成_月度.xlsx", index=False)逻辑说明:(i + j) % 3让同一天里不同人落在不同班次上,第二天整体向后移一位,实现“A→B→C”循环且每天覆盖所有班次。固定夜班的人员单独追加,不参与轮转,保证夜班永远有人。生成的初稿再交给排班负责人调整换班和休假。
参数说明:date_range("2024-06-01", periods=30, freq="D")生成 6 月整月日期,换成任意月份只需改开始日期和 period 天数。shift_cycle的顺序可以按实际业务调整,比如需要“早早晚晚休”的节奏,把循环改成["A", "A", "B", "B", "E"]即可。
6.2 把可视化脚本挂进计划任务,每周五自动出图
固定流程可以做成每周任务:周五一早自动跑清洗、统计、出图、写报表,并把结果输出到共享目录。这样周会前所有人拿到的都是同一份排班总览,不用等某个人手动跑脚本。
import logging from pathlib import Path # 日志记录:脚本有没有跑成功,卡在哪一步 logging.basicConfig( filename="排班脚本.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) output_dir = Path("排班报表") output_dir.mkdir(exist_ok=True) try: df = load_and_clean("排班记录表.xlsx") generate_heatmap(df, output_dir / "月度排班热力图.png") generate_gantt(df, output_dir / "某人_周甘特.png") generate_html(df, output_dir / "排班看板.html") logging.info("排班报表生成成功,共 %d 条记录", len(df)) except Exception as e: logging.error("报表生成失败:%s", e) raise逻辑说明:try/except把异常捕获进日志而不是让任务计划程序默默吞掉。output_dir.mkdir(exist_ok=True)保证目录不存在时自动创建,避免“找不到路径”这种低级报错。把load_and_clean和三个生成函数拆开,任何一个环节挂了,日志里都能看到具体是哪一步。
我最早那版排班表只有姓名没有 ID,三个月后汇总时才发现重名污染了整个工时表,从那以后我把“人员必须带 ID、列名不许改”这两条规则写死进了模板和脚本校验里。排班记录表和可视化展示这套东西,做到“填表有约束、统计能复核、图表自动出”就算及格了。希望帮到你。
本文还有配套的精品资源,点击获取