资讯动态

Python+Pandas:图书馆借阅数据分析与可视化实战

发布时间:2026/10/18 9:08:01 来源:尧图企业网站定制
简介这是一份面向图书馆信息化从业者、数据分析学习者及Python初学者的完整项目资料围绕2014至2017年图书借还数据展开主题词提取、热门书分析、排名变化与帕累托分析等典型任务适合用于课程设计、毕业设计或实际业务场景的数据挖掘实践。资料包共16个文件压缩后约46.45MB内含7个xlsx原始数据表、4个py分析脚本、3个txt辅助语料及readme说明文件类型划分清晰便于按数据处理流程快速上手。目前已有2275人学习下载。读者可以借助现成脚本直接复用分析逻辑结合读者信息、图书目录和借还记录完成从数据清洗到可视化展示的完整流程背景图中还提供了可用于报告的掩码素材具有较强的参考与扩展价值。1. 从 Excel 流水到借阅画像这个标题在解决什么图书馆管理系统每天导出的借阅记录通常是一张大而全的流水表借书证号、书名、ISBN、借书日期、还书日期、续借次数动辄几万行。馆员月底用它做统计最常用的操作是把 Excel 透视表拖来拖去得到“本月借了 X 册”这种总数。但领导追问“哪类书该加副本”“哪些读者是真活跃”时透视表就很难回答了。基于 Python 的图书馆借阅数据分析设计与实现要做的事就是把这张流水表变成可决策的信息读者画像、分类热度、月度趋势、馆藏建议全部用脚本跑出来。这套方案适合两类人一类是高校或公共图书馆里被报表缠住的馆员想用 Python 替代手工透视另一类是刚接触数据分析的开发者需要一个字段清晰、业务含义明确、又不会涉及复杂隐私的练手项目。借阅数据的好处是没有坑人的缺失率也没有复杂的时间序列但它的脏数据问题很典型编码混乱、日期格式不统一、学号丢前导零、还书日期早于借书日期。做过一遍后面处理其他业务数据会有很强的迁移感。2. 借阅数据的清洗与字段设计先让数据能统计2.1 图书管理系统能导出什么字段清单与口径确认常见做法是图书管理系统支持按时间段导出借阅流水大多能导成 Excel 或 CSV。拿到手的第一件事不是写代码而是确认字段语义。我一般会把字段清单和口径先整理成一张表发给熟悉系统的老师确认这张表就是后面分析的“合同”。字段名示例值口径说明分析用途借书证号2023010112学生学号或读者证号可能带前导零读者去重与画像读者类型本科生 / 研究生 / 教师判断读者群体分层统计所在学院计算机学院关联读者归属学院维度分析书名Python编程从入门到实践显示用不适合直接聚合热门书排行ISBN978-7-115-42846-6同一本书可能有多个版次书目去重分类号TP311.56中图法分类号决定学科热度分类占比借书日期2024-03-01借出时间月度趋势还书日期2024-03-15未还可能为空借阅周期续借次数1续借只延长应还时间不是新借活跃度修正这个环节最容易忽略的是“续借次数”的口径。有的系统把续借算成一条新记录有的系统只在原记录上累加次数。不确认清楚后面算借阅量就会翻倍。另一个需要确认的是已还记录和未还记录是否在同一张表里。未还记录的还书日期为空计算借阅周期时必须排除。2.2 没有真实数据时用模拟数据把流程跑通真实借阅记录涉及读者隐私直接贴出来不合适。我习惯先用模拟数据生成一份结构相同的假数据把清洗和分析流程跑通再换成真实导出的 Excel 就能无缝使用真实数据只在本机运行不外传。下面这个脚本生成一张流水表包含一万多条借阅记录。import pandas as pd import numpy as np from datetime import datetime, timedelta np.random.seed(42) n_reader, n_book 200, 600 n_record 12000 # 读者表200个读者含学院与读者类型 readers pd.DataFrame({ reader_id: [f{20230000 i:08d} for i in range(n_reader)], # 学号补零到8位 reader_type: np.random.choice([本科生, 研究生, 教师], n_reader, p[0.7, 0.2, 0.1]), dept: np.random.choice([计算机, 机械, 经管, 外语, 法学], n_reader), }) # 图书表600本书含分类号和上架日期 books pd.DataFrame({ book_id: range(n_book), category: np.random.choice([计算机, 文学, 历史, 经济, 艺术, 哲学], n_book), shelf_date: [datetime(2022, 1, 1) timedelta(daysint(np.random.exponential(300))) for _ in range(n_book)], }) # 借阅流水随机选读者和书 rows [] for _ in range(n_record): rid readers.sample(1).iloc[0] bid books.sample(1).iloc[0] borrow datetime(2022, 1, 1) timedelta(daysint(np.random.randint(0, 1095))) # 20%概率未还80%概率已还 return_date borrow timedelta(daysint(np.random.randint(3, 60))) if np.random.rand() 0.2 else pd.NaT rows.append({ reader_id: rid[reader_id], reader_type: rid[reader_type], dept: rid[dept], book_id: bid[book_id], category: bid[category], shelf_date: bid[shelf_date], borrow_date: borrow, return_date: return_date, renew_count: int(np.random.choice([0, 0, 1, 2])), # 大多数不续借 }) df pd.DataFrame(rows) df.to_excel(lending_record.xlsx, indexFalse) df.to_csv(lending_record.csv, indexFalse, encodingutf-8-sig)这段代码生成了三张表读者表、图书表、借阅流水表。生成的时候就把三个常见脏数据问题埋进去了学号用20230000 i格式化成了 8 位字符串保证前导零不丢return_date有 20% 的概率是空值模拟未还记录renew_count用np.random.choice控制大部分是 0避免续借次数分布太极端。参数说明np.random.seed(42)让随机结果可复现n_reader200, n_book600只是方便演示真实场景根据系统导出规模调整p[0.7, 0.2, 0.1]控制读者类型比例可以按学校实际情况修改。这里特意用timedelta(daysint(np.random.exponential(300)))生成上架日期是为了让图书的“在架时长”有差异后面算借阅密度时用得上。2.3 清洗三板斧类型修正、时间解析、去重用pd.read_excel直接读取导出的流水第一件事是把所有列都按字符串读进来再做类型转换。这一步至关重要Excel 文件里的借书证号、ISBN 经常被 Excel 自动转成科学计数法直接读成 int 后精度会丢失。import pandas as pd # dtypestr 防止学号、ISBN 被自动转成数字 df pd.read_excel(lending_record.xlsx, dtypestr) # 统一日期格式把 2024/1/1 这类斜杠格式转成短横线再解析 for col in [borrow_date, return_date]: df[col] df[col].str.replace(/, -, regexFalse) df[col] pd.to_datetime(df[col], errorscoerce) # 学号补零有些系统导出后丢了前导零 df[reader_id] df[reader_id].str.zfill(8) # 关键字段去重同一人同一天借同一本书视为重复流水 dup_before len(df) df df.drop_duplicates(subset[reader_id, book_id, borrow_date]) print(f去重: {dup_before} - {len(df)}) # 剔除还书日期早于借书日期的错误记录 err df[df[return_date] df[borrow_date]] print(f异常记录: {len(err)}) df df[~(df[return_date] df[borrow_date])]清洗逻辑有三处值得说明。第一dtypestr是读取 Excel 最保险的做法日期列后面单独用pd.to_datetime解析避免 Excel 把日期序列化成数字后解析失败。第二errorscoerce让无法解析的时间变成NaT后面用isna()统计缺失而不是让程序直接报错中断。第三去重按“读者书借书日”三个字段因为正常业务里同一个人同一天不会重复借同一本书两遍出现重复基本是系统重试或手动补录导致的。参数说明str.zfill(8)按 8 位补零如果你的借书证号是 10 位就改成zfill(10)drop_duplicates的subset可以根据实际字段调整比如要保留续借记录就把renew_count加进去。最后一步过滤错误记录时我选择直接剔除而不是填充因为还书日期早于借书日期属于录入错误没有可靠修复依据但要把剔除数量打印出来方便后续发现系统长期存在录入问题时反馈给管理员。2.4 清洗结果校验不急着分析先看数据基本面清洗完不要急着画图先用几行代码看基本面总记录数、读者数、时间范围、未还比例。这些数字能暴露第一个版本清洗逻辑遗漏的问题。print(流水总量:, len(df)) print(唯一读者:, df[reader_id].nunique()) print(唯一图书:, df[book_id].nunique()) print(时间范围:, df[borrow_date].min(), -, df[borrow_date].max()) print(未还记录数:, df[return_date].isna().sum()) # 按年看每月借阅量确认趋势没有突变 monthly df.groupby(df[borrow_date].dt.to_period(M)).size() print(monthly.head())这里nunique()是判断去重是否过度的关键指标。如果去重前唯一读者 200 人去重后变成 150 人说明drop_duplicates的subset选得太宽了把合法借阅也删了。return_date.isna().sum()看未还记录占比正常在 10%30% 之间如果超过 50%要回去确认导出时是否只导出了未还记录。to_period(M)是 pandas 里处理月度聚合很顺手的方法它能把时间戳归一到月份周期比手动提取year和month两列再groupby更直接。上面这段校验脚本不用保存它更像是清洗逻辑的“冒烟测试”每次改完清洗步骤都要跑一遍确认数字在合理区间再进入下一章。3. 用 Pandas 算借阅指标月度趋势、分类热度与读者分层3.1 指标设计先定口径再写代码分析之前先想清楚要输出什么结论。借阅分析标题很大真正能落到馆藏和服务的核心指标只有五个借阅量趋势、分类热度、读者活跃度、借阅周期、借阅密度。每个指标的业务含义和计算口径要先定死在表格里否则后面来回改代码很痛苦。指标名计算口径业务含义月度借阅量按 borrow_date 归月统计记录数整体需求走势分类借阅占比按 category 分类统计次数 / 总量馆藏配置是否合理读者活跃度单人借阅次数分 0 / 1-3 / 4-9 / 10 档核心读者群识别借阅周期已还记录的 return_date - borrow_date判断借期设置是否合理借阅密度借阅次数 / (统计截止日 - 上架日期) 月数新书热门度的公平比较这里特别提一下借阅密度。直接用总借阅次数给图书排名对 2024 年 1 月上架的新书不公平它在架 2 个月被借 20 次显然比 2022 年在架 24 个月被借 30 次更热门。借阅密度的口径是“单位在架时间内的被借次数”这样新书和老书才能放在同一张榜上比较。3.2 月度趋势与分类热度一个 groupby 加一个 resample月度趋势用上一章校验时见过的to_period(M)来聚合。分类热度则要看两种形态一种是绝对值的分类排行适合回答“哪类书借得最多”另一种是分类占比随时间的变化适合回答“某类书的热度是在上升还是下降”。import pandas as pd # 按月统计借阅量 df[month] df[borrow_date].dt.to_period(M) monthly_trend df.groupby(month).size().reset_index(namecount) monthly_trend[month] monthly_trend[month].astype(str) # 分类借阅绝对值 cat_rank df.groupby(category).size().sort_values(ascendingFalse) print(cat_rank) # 分类占比随月度变化行是月份列是分类单元格是借阅次数 cat_month df.pivot_table(indexmonth, columnscategory, valuesbook_id, aggfunccount, fill_value0) cat_month_pct cat_month.div(cat_month.sum(axis1), axis0) # 转成占比pivot_table生成的是透视表结构行是月份、列是分类。第一次做的时候容易直接拿这个表去看绝对值但月份之间的借阅总量本身在波动直接比绝对值会误判。div(cat_month.sum(axis1), axis0)把每一行除以当月总量转成占比后再看分类的变化趋势才靠谱。参数说明aggfunccount是对book_id计数也可以换成aggfuncsum去统计某个数值列fill_value0把没有借阅的月份补成 0避免透视表出现空值导致后续计算报错。分类字段如果用的是中图法分类号而不是中文分类名建议先做一张分类映射表把TP311.56映射成“计算机”否则表和图都会很难读。3.3 读者分层活跃度不是简单数次数读者活跃度分层核心动作是按借书证号分组计算借阅次数再分段。但这里有两个口径容易混续借次数不能算作新的借阅未还的记录也算一次有效借阅因为它说明书已经离开馆藏了。# 读者维度聚合 reader_stat df.groupby(reader_id).agg( borrow_times(book_id, count), renew_total(renew_count, sum), distinct_cat(category, nunique), last_borrow(borrow_date, max), ).reset_index() # 科学计数法的学号还原成字符串并补零 reader_stat[reader_id] reader_stat[reader_id].astype(str).str.zfill(8) # 活跃度分档 bins [0, 1, 4, 10, float(inf)] labels [沉默, 低频, 活跃, 核心] reader_stat[level] pd.cut(reader_stat[borrow_times], binsbins, labelslabels, rightTrue) print(reader_stat[level].value_counts())这里pd.cut的边界要注意bins[0, 1, 4, 10, inf]表示借 1 本算低频4 本以下是活跃10 本及以上是核心读者。rightTrue表示区间是左开右闭即(1,4]落在“活跃”。这个分档阈值不是固定的小馆可以改成 0/2/6/15指标设计阶段就应该和馆员确认。distinct_cat是“读者涉猎的学科数”这个指标常被忽略但很有用一个核心读者如果只借同一类书说明他是深度需求如果一个普通读者借了四个不同分类说明他有探索性阅读需求荐购时可以重点参考。last_borrow最后借书日期配合当前日期可以算“沉睡读者”这在读者激活工作里很实用。3.4 借阅周期与借阅密度剔除未还后的正确算法借阅周期只能用已还记录计算因为未还记录的还书日期是空的。直接用两列时间相减之前必须先剔除异常记录——真实系统里还书日期早于借书日期的现象并不少见多半是人工录入时选错日期。returned df[df[return_date].notna()].copy() returned[loan_days] (returned[return_date] - returned[borrow_date]).dt.days # 去掉小于1天的异常比如当天借当天还 returned returned[returned[loan_days] 1] print(returned[loan_days].describe()) # 逾期判断假设默认借期30天逾期标记 returned[overdue] returned[loan_days] 30 print(逾期率:, round(returned[overdue].mean(), 4)) # 借阅密度按book_id聚合 shelf_stats df.groupby(book_id).agg( borrow_times(book_id, count), shelf_date(shelf_date, first), ).reset_index() shelf_stats[shelf_months] ( pd.Timestamp(2024-12-31) - pd.to_datetime(shelf_stats[shelf_date]) ).dt.days / 30 shelf_stats[density] shelf_stats[borrow_times] / shelf_stats[shelf_months].clip(lower0.5) print(shelf_stats.sort_values(density, ascendingFalse).head(10))loan_days用.dt.days把时间差转成整数天数这是为了后面分箱和比较方便。逾期阈值 30 天是默认值不同图书馆的借期不一样本科生可能是 30 天研究生 60 天教师在 90 天以上。如果读者类型混在一起需要先按读者类型分组各组用自己的借期阈值。借阅密度这里用了一个clip(lower0.5)作用是把在架月数不足 0.5 个月的书的计算分母撑到 0.5避免刚上架一两周的书密度值飙到几十倍干扰排行榜。这个处理是经验值如果你的数据上新书特别多可以把下界改成 1。pd.Timestamp(2024-12-31)是统计截止日实际操作中应该用真实运行日期的前一天保证每月重跑时结果口径一致。4. 可视化输出把报表变成一张能拍板的图4.1 中文字体与基础画布先解决乱码再谈美观借阅分析的图表比指标本身更复杂的地方在于中文。matplotlib 默认字体不包含中文字符不加设置画出来的图全是方块。这一步是每台机器都要做的“环境准备”我把三个必要的设置写在一个块里每次新环境先跑它。import matplotlib.pyplot as plt import matplotlib.font_manager as fm # 查找系统已安装的中文字体找不到就设置成 sans-serif 防止直接报错 zh_font None for f in fm.fontManager.ttflist: if SimHei in f.name or Microsoft YaHei in f.name or Noto Sans CJK in f.name: zh_font f.name break plt.rcParams[font.sans-serif] [zh_font] if zh_font else [sans-serif] plt.rcParams[axes.unicode_minus] False # 解决负号显示成方块的问题 # 统一定义图尺寸4:3 的宽高比更适合借阅趋势 fig, ax plt.subplots(figsize(10, 6))这段设置的逻辑是遍历已安装字体找常见中文字体找到就用找不到就把字体回退到默认。axes.unicode_minus必须设置为False否则坐标轴上的负号会被渲染成方块。如果你在 Linux 服务器上跑脚本没有 SimHei可以执行系统包管理器安装 Noto Sans CJK然后重新运行这段查找逻辑。4.2 三张核心图趋势折线、分类条形、读者金字塔三张图对应三个决策场景看整体走势、看结构、看人群。第一张图是月度折线适合回答“借阅量在涨还是跌”第二张是分类条形图适合回答“哪类书最受欢迎”第三张是读者活跃分档的水平条形图适合回答“核心读者有多少”。# 图1月度借阅趋势 fig, ax plt.subplots(figsize(10, 6)) ax.plot(monthly_trend[month], monthly_trend[count], markero, linewidth2) ax.set_title(月度借阅量趋势) ax.set_xlabel(月份) ax.set_ylabel(借阅次数) plt.xticks(rotation45) plt.tight_layout() plt.savefig(output_monthly_trend.png, dpi150) plt.close() # 图2分类借阅Top fig, ax plt.subplots(figsize(10, 6)) cat_rank.head(8).plot(kindbarh, axax, color#4C72B0) ax.set_title(分类借阅量Top8) ax.set_xlabel(借阅次数) plt.tight_layout() plt.savefig(output_category_rank.png, dpi150) plt.close() # 图3核心读者占比 fig, ax plt.subplots(figsize(10, 5)) level_order [沉默, 低频, 活跃, 核心] level_counts reader_stat[level].value_counts().reindex(level_order) ax.barh(level_order, level_counts.values, color[#999999, #5DA5DA, #F4A261, #E76F51]) for i, v in enumerate(level_counts.values): ax.text(v 5, i, str(v), vacenter) ax.set_title(读者活跃度分层) ax.set_xlabel(人数) plt.tight_layout() plt.savefig(output_reader_level.png, dpi150) plt.close()三张图都用了plt.savefig(dpi150)输出 PNG 而不是plt.show()因为实际使用场景里脚本通常是放在服务器或办公电脑上定时跑的结果要打包进报告发给不同的人看没有人守在屏幕前。图2我用barh横向条形图分类名是中文横向排布比纵向更易读不至于让“计算机科学与技术”这类长分类名倾斜折叠。color#4C72B0选的是色盲友好的配色这个细节在正式报告中会有用。图3从value_counts()拿到的结果进行reindex(level_order)排序保证条形图从上到下是“沉默、低频、活跃、核心”而不是按借阅人数自动重排否则图的叙事性会打折扣。ax.text(v 5, i, str(v), vacenter)在条形末端标注人数这是我认为最省事且清晰的做法不需要额外的图例。4.3 把图表拼成一份可发的 HTML 报告单张图发给别人对方容易丢失上下文。更常见的做法是把图拼进一份带简单文字结论的 HTML 报告。不需要现成的报表框架用一个基础模板就够了。html f!DOCTYPE html htmlheadmeta charsetutf-8 title月度借阅分析简报/title style body {{ font-family: Microsoft YaHei, sans-serif; margin: 40px auto; max-width: 900px; }} img {{ max-width: 100%; margin: 20px 0; }} h2 {{ border-left: 4px solid #4C72B0; padding-left: 12px; }} /style/headbody h2一、月度借阅量趋势/h2 p{monthly_trend[count].sum()} 条借阅记录月均 {round(monthly_trend[count].mean(), 1)} 次。/p img srcoutput_monthly_trend.png h2二、分类借阅结构/h2 p借阅量最高的是{cat_rank.index[0]}。/p img srcoutput_category_rank.png h2三、读者活跃分层/h2 p核心读者 {level_counts[核心]} 人贡献了 {round(reader_stat[reader_stat[level]核心][borrow_times].sum() / len(df) * 100, 1)}% 的借阅量。/p img srcoutput_reader_level.png /body/html with open(report.html, w, encodingutf-8) as f: f.write(html)HTML 里的三行结论文字不是用来装饰的它们是分析结果的核心交付物。馆领导看报告时通常只读第一段文字图表是给想深入看的人准备的。所以每段结论必须是一个可量化的判断句比如“核心读者贡献了 43.6% 的借阅量”而不是“读者活跃度分布见下图”。这个模板的扩展空间很大想加学院排行就复制一个h2区块替换图片和文字想发邮件用 Python 的smtplib把这个 HTML 作为正文发出即可。我通常最后还会把report.html改名加上日期比如report_2024_W24.html方便追溯每次跑了什么数据。5. 避坑借阅分析里我踩过的五个数据坑5.1 时间字段混入斜杠格式与 Excel 序列号现象pd.to_datetime解析借书日期时有一部分行报错变成NaT检查原始 Excel 发现同一列里既有2024/1/1也有2024-01-01还有几个显示为45292的数字。原因图书管理系统导出时按不同字段模板拼接了日期斜杠和短横线混在一起而个别日期在 Excel 里被识别成了真正的日期类型导出后以序列号存储。errorscoerce会把它们统一转成NaT但不解决根本问题。解决读取时先强制dtypestr把所有值都当文本读入然后统一替换斜杠为短横线再判断哪些值是全数字的序列号用pd.to_datetime(1899-12-30) pd.to_timedelta(int(x), unitD)转回日期。我在清洗脚本里加了一步“格式归一化”代码如下import pandas as pd from datetime import datetime, timedelta df pd.read_excel(lending_record.xlsx, dtypestr) def parse_mixed_date(s): s str(s).replace(/, -) if s.isdigit(): # Excel序列号基准日期是1899-12-30 return datetime(1899, 12, 30) timedelta(daysint(s)) try: return pd.to_datetime(s) except Exception: return pd.NaT for col in [borrow_date, return_date]: df[col] df[col].apply(parse_mixed_date)说明Excel 序列号基准是 1899-12-30含一个历史 bug 偏移这个基准值不要自己凭记忆写我用datetime(1899, 12, 30)是长期验证过的结果。解析时优先尝试常规格式全数字再走序列号分支。5.2 学号前导零丢失后groupby出现“假读者”现象读者统计里出现大量只借过一次的“新读者”姓名列还都是空的后来核查发现借书证号2023000123被 Excel 存成了202300123少了一位零。groupby(reader_id)把这些错位学号当成两个不同的人导致读者总数虚高。原因Excel 中超过 11 位的数字会自动转成科学计数法但 8 位学号不会。真正的原因往往是图书管理系统导出的文本字段里学号本来就是字符串如果中间经过一次 Excel 打开再另存文本被转成了数字。2023000123这种以 0 结尾的数字转成数字后末尾的 0 被丢弃。解决读取时dtypestr只能防未转换的情况对已经丢失的 0 无能为力。更可靠的办法是和读者表做一次匹配用reader_id.astype(str).str.zfill(8)统一补零后再和读者主表merge合并后匹配不上的再人工核查。我一般把补零逻辑写在清洗函数里而不是数据读取阶段方便复用。5.3 同一本书多个 ISBN 导致分类排行失真现象分类热度榜上某本经典教材因为出了第 3 版、第 4 版、第 5 版被拆成了三个条目每个条目都在榜单 10 名开外但把三个版次合并后实际是第一。原因图书管理系统里 ISBN 是唯一键每换一次版次就生成一条新书目记录。分类号相同但书名相近直接用book_id聚合无法识别“同一本书的不同版次”。解决用分类号 规范化书名做合并键。规范化的做法是把书名里的空格、版本号、“:”副标题分隔符去掉再和分类号拼接。代码里我会在生成指标前先加一个“书目归一化”步骤import re df[title_norm] df[book_title].apply( lambda x: re.sub(r[:\s()第\d版], , str(x)) ) df[book_group] df[category] _ df[title_norm]说明re.sub的正则里第\d版匹配“第3版”这样的版次后缀替换成空字符串。这个规则要谨慎如果一本书的书名本身就含“第X卷”或“第二版”这类有实际分类意义的字样会影响归并效果。所以我通常先跑一版打印出book_group重复数最多的前 20 条人眼确认后再放进指标计算。5.4 还书日期早于借书日期的脏记录现象计算借阅周期时loan_days出现负数甚至出现 -300 多天的极端值把平均借阅周期拉到一个完全不可信的值。原因系统录入还书日期时默认带出了当前日期如果补录的是几年前的借书记录还书日期反而早于借书日期也有读者在自助借还机上操作时日期没有同步导致错误。解决清洗阶段直接剔除并计数同时在日报里保留剔除数量。不要尝试用“取绝对值”或“把借书日期改成还书日期”这种自以为聪明的修法因为这样会掩盖系统问题。如果剔除比例超过总记录的 1%就需要反馈给系统管理员检查自助借还机的日期同步配置了。5.5 续借次数被当成新借阅现象某个月的借阅量突然暴涨追踪发现是系统升级后把读者续借操作也写入了一条新的借阅流水导致同一个人同一本书在同一借期内出现了两条记录。原因不同版本的系统对续借的存储方式不同旧版在renew_count字段累加新版直接插一条新流水并把原记录的还书日期置空。清洗去重步骤没有覆盖这种“同人同书同时段”的模式。解决去重时把时间段考虑进去。先按reader_id book_id分组再判断同一组内两条记录的借书日期差是否小于 30 天且存在重叠把重叠的后一条标记为续借并删除。我把处理逻辑写成独立函数仅在数据来源系统升级时启用平时保持关闭状态以免误删真实借阅记录。def merge_renew_duplicates(df, gap_days30): df df.sort_values([reader_id, book_id, borrow_date]) df[prev_return] df.groupby([reader_id, book_id])[return_date].shift() mask (df[borrow_date] - df[prev_return]).dt.days gap_days return df[~mask.fillna(False)]说明shift()取同一组上一行的还书日期如果当前借书日期距离上一行的还书日期在 30 天内视为续借产生的重复记录。gap_days是可调参数默认 30 天要结合贵馆的借期设置来定。这个方法也有误删风险如果读者真在还书后 20 天又借了同一本书会被误判。所以我要求这个函数运行时必须打印删除量每次维护时人工确认。6. 收尾成组件让每个月的分析只需一条命令6.1 把清洗、指标、图表封装成带参数的模块前面的脚本都是平铺直叙的顺序代码换一个月的数据、换一个学院就要手动改多处变量。第六个月我就受够了决定把整套逻辑封装成一个带参数的入口文件。这样每月 1 号跑数据时只需改一个日期区间。import argparse import pandas as pd import matplotlib.pyplot as plt def clean_data(df): df df.copy() df[borrow_date] pd.to_datetime(df[borrow_date], errorscoerce) df[return_date] pd.to_datetime(df[return_date], errorscoerce) df[reader_id] df[reader_id].astype(str).str.zfill(8) df df.drop_duplicates(subset[reader_id, book_id, borrow_date]) return df[~(df[return_date] df[borrow_date])] def compute_metrics(df, top_n10): df[month] df[borrow_date].dt.to_period(M) monthly df.groupby(month).size().reset_index(namecount) cat df.groupby(category).size().sort_values(ascendingFalse).head(top_n) reader df.groupby(reader_id)[book_id].count() return monthly, cat, reader def plot_outputs(monthly, cat, reader): # 具体绘图代码见第四章这里省略重复部分 pass def main(): parser argparse.ArgumentParser(description借阅分析脚本) parser.add_argument(--file, requiredTrue, help导出的Excel文件路径) parser.add_argument(--start, default2024-01-01, help统计开始日期) parser.add_argument(--end, default2024-12-31, help统计结束日期) parser.add_argument(--topn, typeint, default10, help分类榜条数) parser.add_argument(--overdue-days, typeint, default30, help逾期天数阈值) args parser.parse_args() df pd.read_excel(args.file, dtypestr) df df[(df[borrow_date] args.start) (df[borrow_date] args.end)] df clean_data(df) monthly, cat, reader compute_metrics(df, args.topn) print(月度趋势:\n, monthly) print(分类Top:\n, cat) print(读者人均借阅:, round(reader.mean(), 2)) if __name__ __main__: main()命令行的参数里--file是必填参数指向每月新导出的 Excel--start和--end默认值覆盖一整年但月度运行时可以传最近一个月--topn控制分类榜条数避免排行榜太长失去焦点--overdue-days默认 30 天每个馆可以按自己的借期规则覆盖。参数设计的原则是必须改的参数用requiredTrue不常改的参数给合理默认值同时保留覆盖能力。封装之后每月运行脚本的命令只有一条python analyze.py --file lending_2025_01.xlsx --start 2025-01-01 --end 2025-01-31如果要在 Windows 的定时任务或 Linux 的 cron 里跑还可以加一行输出重定向python analyze.py ... report_2025_01.txt日志和数据分离。6.2 验证输出靠“三个一”一眼、一数、一对比模块化以后代码本身是干净的但没人保证新月份的数据不出幺蛾子。我的习惯是每次跑完用“三个一”来验证输出可信度再把这个结果发出去。# 自动对比相邻两个月的总量变化 import sys report { borrow_total: len(df), active_readers: df[reader_id].nunique(), silent_rate: 1 - df[reader_id].nunique() / reader_total, } # 手工记录上一期的总量 prev_total 12800 # 上次跑完脚本记住的值 if abs(report[borrow_total] - prev_total) / prev_total 0.3: print(警告借阅总量环比变化超过30%检查数据导出是否有误)不需要太复杂的验证机制“和上一期数据对比变化率”是我这些年养成的习惯。变化率超过 30% 就停下来确认而不是直接相信新数据。图书馆借阅量受寒暑假、考试周影响有周期性波动所以我会把上一期选在去年同期而不是上个月避免把暑假自然回落当成数据异常。6.3 更进一步从统计到推荐和馆藏优化指标稳定跑通后这个项目的天花板不在统计而在两个进阶方向。第一个方向是读者推荐按“读者所在学院 历史借阅分类占比”计算偏好向量给读者推荐本学院同学借得多的书比单纯按全馆榜单推荐精准得多。第二个方向是馆藏优化把每年的借阅密度按分类汇总密度长期偏低的分类可以压缩复本量密度高但馆藏量少的分类是下一轮采购的重点。# 按学院分类看偏好用于推荐候选 dept_pref df.groupby([dept, category]).size().reset_index(namecount) dept_pref[rank] dept_pref.groupby(dept)[count].rank(methoddense, ascendingFalse) top_dept_books dept_pref[dept_pref[rank] 5]我在这类项目上的习惯是先把统计报表稳定跑三个月让馆员确认数字可信再动推荐和采购决策功能否则两件事叠在一起出了问题很难判断是数据问题还是推荐算法问题。希望这个从清洗到封装的完整链路能帮你把借阅数据真正用起来每月的分析不再靠手动透视表。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑