拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python和Pandas的图书馆借阅数据分析全流程实践

基于Python和Pandas的图书馆借阅数据分析全流程实践

简介:数据分析是当今信息化社会的基础能力,而数据清洗与指标建模则是分析项目中决定成败的关键环节。面对动辄数万行的业务流水,如何用Python高效地完成数据预处理、聚合统计和可视化呈现,已是图书馆数字化运营中的高频需求。本文从借阅数据的字段类型设计出发,讲解Pandas在数据清洗、派生字段、去重规则中的核心用法,并延伸至趋势分析、读者分层及Apriori关联规则挖掘等进阶场景,帮助读者建立从原始数据到业务洞察的完整链路。无论是Python入门者还是需要汇报馆藏利用的管理员,都能从中获得可复用的工程实践思路。

1. 「基于python的图书馆借阅数据分析设计与实现」到底在做什么

「基于python的图书馆借阅数据分析设计与实现」这个标题,说的是一类非常典型的课设/毕设选题:手里有一份从图书馆管理系统导出的借阅流水,要用 Python 把它变成可解释、可复现的分析结论。它解决的不是爬数据,而是把原始数据清洗干净、把指标算对、把结果落到图表和报表里。这个题适合两类人:一类是 Python 入门后想完成第一个完整项目的学生,另一类是需要在季度例会上汇报馆藏利用情况的图书馆管理员。我见过太多人把精力花在美化图表上,最后在数据口径上翻车——同一份数据,有人算出月借阅量 5000,有人算出 8000,查来查去只是去重规则不同。这类项目的真正难点,其实在动手前就得把指标口径定死,并在设计阶段把数据模型想清楚。

2. 想清楚再写代码:借阅分析的指标体系与数据模型

2.1 先回答三个问题:给谁看、看什么、看到什么程度

我接到这类需求,第一件事不是打开 CSV 看数据,而是问“这份分析给谁看”。给馆领导看的,核心是总量趋势和到馆人次;给采编部门看的,核心是分类借阅占比和热门图书清单;给读者服务部门看的,核心是复借率和读者活跃分层。同一张借阅流水,能产出的指标很多,但真正会被用到的就那么几个。先定指标,再写代码,能省掉后面一半的返工。

以下是我在这个项目里最常用的一套指标体系。注意,指标不是越多越好,而是每个指标都要能对应一个业务动作:总量下降要考虑开放时间调整,某分类占比连续三个月上升要考虑采购倾斜,复借率低要考虑荐书和曝光位。

指标计算公式业务用途
总借阅量有效借阅记录数衡量整体流通规模
月均借阅量总借阅量 / 统计月数判断淡旺季和增长趋势
分类借阅占比某分类借阅量 / 总借阅量指导采编和馆藏优化
热门图书 TopN按“书名+作者”分组计数榜单展示、采购复本参考
读者借阅分层按人均借阅量分桶营销活动对象圈选
复借率有过2次及以上借阅的读者数 / 活跃读者数评估读者粘性
平均借阅时长归还日期 - 借出日期 的均值判断借阅周期和续借政策

2.2 数据模型怎么设计:一张宽表还是三张规范表

常见做法是直接从图书馆管理后台导出一张借阅流水表,字段一般包含借阅证号、图书条码、书名、作者、分类号、借出日期、应还日期、实际归还日期。在课设和数据量不大的场景下,我通常把读者信息和图书信息通过 JOIN 冗余成一张宽表再分析,而不是建三张表做外键关联。原因很简单:Pandas 处理单张宽表的内存和代码量都更低,出报表时也不用反复连表。

字段设计上有一个容易被新手忽略的点:借阅证号和图书条码必须按字符串读入,否则会被解析成浮点数甚至科学计数法。我自己在做字段表时会强制写清楚每个字段的类型,避免后续清洗时踩坑。

字段名类型示例备注
user_idstr2023000123借阅证号,必须按字符串读
book_idstrTP312-1234图书条码
book_namestr深入理解计算机系统书名可能有空格和版本后缀
authorstrBryant用于和书名联合去重
categorystr文学 / I247.5分类号与类名混存的常见坑
borrow_datedatetime2024-03-12借出日期
due_datedatetime2024-04-11应还日期
return_datedatetime2024-04-10未归还时为空

2.3 技术选型:为什么是 Pandas 而不是 Excel

这类分析用 Excel 也能做,但我一般坚持用 Python 的原因有三条。第一,借阅流水一年通常有几万到几十万行,Excel 的筛选和数据透视表在这种量级下会明显卡顿。第二,Excel 操作不可复现——你在菜单里点了十几步得到一张透视图,换一个人过来完全不知道你怎么算的;而 Python 脚本跑一遍就是一遍,口径写在代码里,谁都能核对。第三,Python 的结果可以无缝对接后面的可视化阶段,甚至做成一个网页报表系统。

环境准备不需要太多,我的常用依赖就四个:

pip install pandas matplotlib mlxtend openpyxl

pandas 负责数据清洗和聚合,matplotlib 负责出静态图,mlxtend 只在做关联规则推荐时用到,openpyxl 用来把结果写回 Excel。如果你是想做成带交互图表的网页版,常见做法是加一个 Flask,前端用 ECharts 渲染 JSON 数据,这一点后面的分析章节会提到接口怎么设计。和“企业客户信息数据统计分析系统”这类同型题目一样,借阅分析的难点从来不是算法,而是数据口径。

3. 用 Pandas 把借阅流水整理成可分析的数据:清洗脚本与字段派生

3.1 读入数据并做一次体检

数据清洗是整个项目里最枯燥但最值钱的部分。我拿到 CSV 后的第一个动作是读进来看看形状、字段类型和空值率,而不是急着算总量。下面的脚本里有两个关键参数需要说明:parse_dates 会把日期列直接转成 datetime 类型,后续做月份聚合才能按时间排序;dtype 强制指定证号和条码为字符串,避免 0 开头丢位和科学计数法。

import pandas as pd df = pd.read_csv( 'borrow_log.csv', encoding='utf-8-sig', # Excel 导出的 CSV 常带 BOM,用这个编码不会出现 \ufeff dtype={'user_id': str, 'book_id': str, 'isbn': str}, # 一律按字符串读 parse_dates=['borrow_date', 'due_date', 'return_date'] ) print(df.shape) print(df.dtypes) print(df.isna().sum()) # 看每一列空值数量 print(df.head())

读入后我会先看三件事:总行数是多少,哪些列有空值,日期列有没有被正确解析。很多新手在这一步直接跳过体检,结果后面 groupby 出来的月份是乱的,或者 user_id 变成了浮点数——这些都是可以在这个阶段一眼发现的问题。如果发现日期列是 object 类型,就说明 parse_dates 没起作用,要回去检查 CSV 里的日期格式是不是标准的 YYYY-MM-DD。

3.2 四条清洗规则:空值、重复、日期、分类

清洗规则因人而异,但我有一套固定的顺序:先删关键字段空值,再去重,再统一分类,最后做日期合法性过滤。顺序不能乱,比如先做日期过滤再做去重,可能会让本该被去重掉的坏数据存活下来。

# 1. 借阅证号或图书条码为空,无法分析,直接删 df = df.dropna(subset=['user_id', 'book_id']) # 2. 同一个人同一天借同一本书,只保留一条 df = df.drop_duplicates(subset=['user_id', 'book_id', 'borrow_date']) # 3. 分类空值补一个“未分类”,分类号 I247.5 等统一映射到大类 df['category'] = df['category'].fillna('未分类').astype(str).str.strip() df.loc[df['category'].str.startswith('I'), 'category'] = '文学' # 4. 借出日期晚于应还日期的记录属于录入错误,直接剔除 df = df[df['borrow_date'] <= df['due_date']] # 5. 重置索引,避免过滤后索引空洞带来的隐性问题 df = df.reset_index(drop=True)

这里最容易被误解的是去重。drop_duplicates 的 subset 是三个字段联合判定,也就是说同一个人同一天借了同一本书两次才被合并;如果只是同样的书在不同日期借,不会被误删。分类映射这里要特别留意,不同学校的导出格式不一样,有的给的是分类号,有的给的是分类名,还有的是两类混存。我的习惯是把分类号代码段单独拉出来检查一遍再写映射,不要靠猜。

3.3 派生字段:借阅时长、月份、星期几

清洗完之后,原始字段还不能直接支撑分析。借阅时长是最常用的派生字段,它是由 return_date 减 borrow_date 算出来的;月份字段用于趋势分析;星期几字段用于判断工作日和周末的借阅差异。注意,未归还的记录在 return_date 上是空值,直接相减会得到 NaT,必须先用今天日期填充。这一步也会暴露上一轮清洗没处理干净的异常数据。

import pandas as pd # 未归还的记录按今天算,用于判断是否超期 df['return_date'] = df['return_date'].fillna(pd.Timestamp('today')) # 借阅时长:单位是天 df['duration'] = (df['return_date'] - df['borrow_date']).dt.days # 月份字段,Period 类型可以直接用于 groupby 和排序 df['borrow_month'] = df['borrow_date'].dt.to_period('M') # 星期几:0=周一,6=周日 df['weekday'] = df['borrow_date'].dt.dayofweek # 是否超期:返回日期晚于应还日期 df['overdue'] = df['return_date'] > df['due_date'] print(df[['book_name', 'borrow_date', 'return_date', 'duration', 'borrow_month', 'weekday']].head())

duration 算出来后,我建议立刻做一次描述性统计。如果 min 是负数,说明还有归还日期早于借出日期的坏记录;如果 max 超过 365,说明有长期未还的书或者录入异常。这两种情况都别急着改数据,先看清楚量级——只有几条就手动剔除,有几十条以上就要回去查导出口径。

3.4 清洗结果的验证与落盘

清洗不是跑完就结束,要有一道验证工序。我的做法是用 assert 把关键约束写死,一旦数据不符合预期脚本直接报错,而不是带着脏数据往下跑。这样以后拿到新一批数据时,只要重跑脚本就知道能不能用。

assert df['duration'].min() >= 0, '存在负借阅时长' assert df['duration'].median() < 60, '借阅时长中位数异常' assert df['user_id'].str.len().between(6, 20).all(), '借阅证号长度异常' df.to_csv('borrow_clean.csv', index=False, encoding='utf-8-sig') print(df.shape)

验证结束的清洗数据我一般会单独存一份 borrow_clean.csv,后面所有分析和可视化都从这份文件读入。这样做的好处是,如果后续分析脚本写错了,只需要改分析脚本重跑,而不需要重新清洗一遍原始数据。清洗阶段的另一个好处是:原始文件始终保留,任何时候发现清洗规则错了,都有后悔药可吃。

4. 三类核心分析:借阅趋势、热门书目与读者分层

4.1 借阅趋势分析:按月聚合的三行核心代码

趋势分析的目标是看借阅量随时间的走势。最标准的做法是按自然月聚合。这里有个细节:一定要先把 borrow_date 转成月份类型的字段再 groupby,而不是直接对日期字符串做切片。用字符串切片虽然也能得到“2024-03”这种结果,但排序时会按字典序排,导致 2024-10 排在 2024-03 前面这种低级错误。

import pandas as pd df = pd.read_csv('borrow_clean.csv', parse_dates=['borrow_date', 'return_date']) df['borrow_month'] = df['borrow_date'].dt.to_period('M') # 按月份统计借阅量 monthly = df.groupby('borrow_month').size().reset_index(name='borrow_cnt') # 计算环比变化率:本月相对上月的增减幅度 monthly['mom'] = monthly['borrow_cnt'].pct_change() print(monthly)

pct_change 算出来的是百分比的小数形式,比如 0.12 表示环比上涨 12%。如果看到某个月 mom 出现断崖式负值,不要先怀疑读者流失,先查一下那个月有没有闭馆、寒假或系统升级停用的情况。趋势分析的结果要结合馆历去看,不能脱离背景解释数据,这是这个项目里最容易出笑话的地方。

4.2 热门书目分析:按“书名+作者”聚合而不是只按书名

热门榜单看起来简单,实际非常容易翻车。如果只按书名分组,不同作者的同名书会被算成一本,同一个人的同一本书的不同版本也会被拆散到多行里,导致榜单失真。更稳妥的做法是把书名校验和统一后,再和作者联合分组。具体实现如下:

# 书名做一次清洗:去空格、去版本标注 df['book_name'] = df['book_name'].astype(str).str.strip().str.replace(r'[((].*?[))]$', '', regex=True) # 按“书名 + 作者”联合统计借阅次数 top_books = ( df.groupby(['book_name', 'author']) .size() .reset_index(name='borrow_cnt') .sort_values('borrow_cnt', ascending=False) .head(15) ) print(top_books)

正则替换那一行是为了去掉书名末尾的版本信息,比如“某某书(第2版)”,让同一本书的不同版本能聚到一起。如果你的系统导出的书名已经很规整,可以省掉这一步。榜单出来之后,我一般会再按分类看一眼分布——如果 Top15 全集中在文学类,说明榜单展示的只是“大众偏好”,而不能直接指导采购,还需要配合分类占比分析来看长尾情况。

4.3 读者画像:把读者按借阅量分成四类

读者画像的目标是给读者分层。我用得最多的是按总借阅量分桶:0 本是沉睡读者,1 到 5 本是低频,6 到 20 本是活跃,20 本以上是高频。分桶边界不是拍脑袋定的,一般会先看一次分位数,让活跃读者的占比在 20% 到 30% 之间,这样后续做精准运营时圈选的对象才有数量基础。

# 每个读者的借阅总量 user_stats = df.groupby('user_id').size().rename('total_borrow') # 分桶:左开右闭 bins = pd.cut( user_stats, bins=[0, 1, 6, 21, float('inf')], labels=['沉睡', '低频', '活跃', '高频'], right=True ) # 统计各分桶人数和占比 user_level = user_stats.to_frame().assign(level=bins) level_summary = ( user_level.groupby('level', observed=True) .size() .reset_index(name='user_cnt') ) level_summary['ratio'] = level_summary['user_cnt'] / level_summary['user_cnt'].sum() print(level_summary)

这段代码里值得解释的是 pd.cut 的参数。bins 数组定义了四个区间边界,labels 对应每个区间的名字,right=True 表示区间右端闭合,也就是 1 本会落入“低频”而不是“沉睡”。如果你的数据分布特别偏,可能五成读者都是沉睡状态,那就要调整边界,比如把 0 本沉睡、1 到 3 本低频、4 到 10 本活跃、10 本以上高频。边界跟着业务走,没有标准答案。

4.4 把分析结果画成图:Matplotlib 出图脚本

分析表格做出来还不够,课设或者汇报场景下都要有图。我在静态图阶段统一用 Matplotlib 出图,核心是先把中文字体配置好,否则图里全是方框。配置字体之后,再把趋势图和榜单图拼在一个画布上输出,方便直接放进报告。

import matplotlib.pyplot as plt # 中文字体配置,Windows 一般有 SimHei,Mac/Linux 视环境而定 plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题 fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 左图:月度借阅趋势 monthly['borrow_month'] = monthly['borrow_month'].astype(str) axes[0].plot(monthly['borrow_month'], monthly['borrow_cnt'], marker='o') axes[0].set_title('月度借阅趋势') axes[0].tick_params(axis='x', rotation=45) # 右图:热门图书 Top15 axes[1].barh(top_books['book_name'].head(10)[::-1], top_books['borrow_cnt'].head(10)[::-1]) axes[1].set_title('热门图书 Top10') fig.tight_layout() fig.savefig('borrow_report.png', dpi=300, bbox_inches='tight')

savefig 的两个参数值得说清楚:dpi=300 是保证打印到纸质报告里不模糊;bbox_inches='tight' 会自动收缩画布边界,避免标题或坐标轴标签被裁掉。如果后面要做成交互式网页报表,常见做法是把 monthly、top_books、level_summary 三个 DataFrame 转成 JSON,放到 Flask 接口里返回,前端 ECharts 直接消费,分析逻辑和这里的完全一样。换句话说,先出静态图验证口径,再做网页版,能省掉大量调试时间。

5. 图书馆借阅数据分析的 5 个高频踩坑点与排查思路

5.1 中文乱码:图里全是方框或问号

现象:Matplotlib 画出来的图里,标题和坐标轴文字全是方框,英文和数字正常。

原因:Matplotlib 默认字体不支持中文,系统里也没有它默认查找的中文字体,所以中文字符渲染不出来。

解决:在绘图脚本开头显式指定中文字体。Windows 上最省事的是 SimHei,Linux 或 Mac 上如果没有这个字体,先安装或者改用系统中已有的中文字体名。

plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False

如果换了好几个字体名都没效果,就用下面这行命令先看系统里到底有哪些可用字体,再决定指定哪个名:

import matplotlib.font_manager as fm fonts = [f.name for f in fm.fontManager.ttflist] print(sorted(set(fonts)))

5.2 借阅证号读进来变了:0 丢失、变成科学计数法

现象:user_id 显示成 1.23457e+14,或者开头是 0 的证号变成了纯数字,0 没了。和读者表关联时匹配不上。

原因:CSV 里的证号被 pandas 自动识别成了数值类型。比如 000123 会被读成 123,超过 15 位还会变成浮点数科学计数法。

解决:在 read_csv 时用 dtype 参数强制指定字符串,这是唯一的根治方法。如果数据已经读进来了,可以用 astype(str) 补救,但只能恢复形状,恢复不了已经丢失的前导 0。

df = pd.read_csv('borrow_clean.csv', dtype={'user_id': str, 'book_id': str})

更隐蔽的场景是合并读者信息表时,身份证号、手机号这类长数字字段也会被同样的机制破坏。我的原则是:只要字段不是用来做算术的,一律在读入阶段按字符串处理,不然后面返工的代价远大于省下的那点内存。

5.3 去重规则错了:热门榜单被低估

现象:同一本书的借阅次数明显低于系统后台查到的流通次数,榜单排名和实际感受不符。

原因:去重时用了 user_id + book_id 两个字段,而同一个读者对同一本书的续借、到期再借会被当成“重复记录”删掉。另一个常见错误是只按 user_id + book_id + borrow_date 去重时没有先看续借在系统里怎么表示——有些系统续借会新产生一条 borrow_date 相同的记录。

解决:先把数据按借出日期排序,再按“证号+书号+借出日期”去重;同时确认系统里续借是否复用原借出日期。如果系统里续借是修改 due_date 而不新增行,就不存在这个问题。做热门榜时我特别建议先和图书馆管理后台的流通统计对一次总数,对不上就说明清洗规则和系统口径不一致。

5.4 借阅时长出现负数或几百天

现象:duration 的最小值是负数,或者中位数高达 300 多天,A 书平均借了 10 个月。

原因:负数来自身份证录入时还书日期早于借出日期,一般是个别手工录入错误。几百天则有两种情况:长期未还的书被 fillna 成今天导致计算出了超长时长,或者系统把预约日期误写成了借出日期。

解决:清洗阶段先过滤 borrow_date <= return_date 的记录;未归还的书单独打标,不参与平均借阅时长的计算,只参与超期分析;对 duration 超过 365 天的记录做一次抽样复查,如果数量很少直接剔除,超过 1% 就要回去查导出口径。

df = df[df['borrow_date'] <= df['return_date']] df['duration'] = (df['return_date'] - df['borrow_date']).dt.days df_borrowed = df[df['return_date'] <= pd.Timestamp('today')]

5.5 关联规则脚本内存爆炸或结果为空

现象:构建 one-hot 矩阵时内存占用飙到几个 GB,或者跑 Apriori 时 min_support 设为 0.02 却一条规则都出不来。

原因:图书馆藏书通常有几万甚至十几万个品种,每个品种作为一个 item 做 one-hot 编码,矩阵列数爆炸;同时大量书可能一年只被借了一次,support 天然极低,0.02 的阈值对它们来说遥不可及。

解决:不直接用“书名”当 item,而是先过滤掉借阅次数低于 5 次的冷门书;再把“同一个读者在同一次借阅周期内借的书”作为一个篮子事务,而不是把全部借阅历史拉平。这样事务数和 item 数都大幅下降,min_support 从 0.01 甚至 0.005 开始调才合理。这个问题的详细实现我放在下一章,属于进阶玩法。

6. 进阶验证:用关联规则找出“一起被借”的书单

6.1 把借阅流水转成购物篮事务

前面做的都是统计型分析,能回答“什么书最火”,回答不了“什么书和什么书容易被一起借”。常见做法是把借阅记录按“读者 + 一次借阅周期”组合成购物篮,借出日期近似看成同一次到馆,篮子里放这次借的所有书目。实现时先把低频书目过滤掉,再用 get_dummies 转成 one-hot 矩阵。

from mlxtend.frequent_patterns import apriori, association_rules # 过滤借阅次数少于 5 次的冷门书 book_cnt = df['book_name'].value_counts() hot_books = book_cnt[book_cnt >= 5].index df_hot = df[df['book_name'].isin(hot_books)] # 篮子:同一读者 + 同一天借出的书作为一个事务 basket = df_hot.groupby(['user_id', 'borrow_date'])['book_name'].apply(list) # 转成 one-hot 事务矩阵 encoded = basket.apply(lambda x: ','.join(x)).str.get_dummies(',') print(encoded.shape)

6.2 跑 Apriori 并验证规则质量

Apriori 的两个核心参数是 min_support 和 metric 阈值。support 控制频繁项集的下限,lift 才真正衡量“一起借”是否超出独立概率。如果两本热门书 lift 接近 1,说明它们只是各自都热门,并没有组合关系;lift 大于 1.5 的组合才有上推荐位的价值。

# 频繁项集 frequent = apriori(encoded, min_support=0.01, use_colnames=True) # 规则生成,按提升度排序 rules = association_rules(frequent, metric='lift', min_threshold=1.0) rules = rules.sort_values('lift', ascending=False) # 只看置信度不低于 30% 的规则 strong_rules = rules[rules['confidence'] >= 0.3] print(strong_rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']].head(20))

验证规则时我会抽样回原数据确认,比如规则显示“借了 A 的人同时借 B”,就人工翻几条实际的借阅流水,看看是不是来自同一个读者、同一次借阅。这一步能挡住一半以上的“假规则”。做过一次完整的关联规则分析之后,我的习惯是把它沉淀成一个独立脚本,以后每个月新数据进来,清洗、统计、关联规则三步一次跑通。数据分析和系统实现一样,能自动化的事就别靠手工。希望这个方案能帮你把借阅数据真正用起来,也希望你踩过的坑比我少。

本文还有配套的精品资源,点击获取

返回列表