首页 / 资讯中心 / 文章详情

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

基于Python和Pandas的图书馆借阅数据分析全流程实践 ★ FEATURED ARTICLE
简介数据分析是当今信息化社会的基础能力而数据清洗与指标建模则是分析项目中决定成败的关键环节。面对动辄数万行的业务流水如何用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 openpyxlpandas 负责数据清洗和聚合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, encodingutf-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(dropTrue)这里最容易被误解的是去重。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, indexFalse, encodingutf-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(nameborrow_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[(].*?[)]$, , regexTrue) # 按“书名 作者”联合统计借阅次数 top_books ( df.groupby([book_name, author]) .size() .reset_index(nameborrow_cnt) .sort_values(borrow_cnt, ascendingFalse) .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[沉睡, 低频, 活跃, 高频], rightTrue ) # 统计各分桶人数和占比 user_level user_stats.to_frame().assign(levelbins) level_summary ( user_level.groupby(level, observedTrue) .size() .reset_index(nameuser_cnt) ) level_summary[ratio] level_summary[user_cnt] / level_summary[user_cnt].sum() print(level_summary)这段代码里值得解释的是 pd.cut 的参数。bins 数组定义了四个区间边界labels 对应每个区间的名字rightTrue 表示区间右端闭合也就是 1 本会落入“低频”而不是“沉睡”。如果你的数据分布特别偏可能五成读者都是沉睡状态那就要调整边界比如把 0 本沉睡、1 到 3 本低频、4 到 10 本活跃、10 本以上高频。边界跟着业务走没有标准答案。4.4 把分析结果画成图Matplotlib 出图脚本分析表格做出来还不够课设或者汇报场景下都要有图。我在静态图阶段统一用 Matplotlib 出图核心是先把中文字体配置好否则图里全是方框。配置字体之后再把趋势图和榜单图拼在一个画布上输出方便直接放进报告。import matplotlib.pyplot as plt # 中文字体配置Windows 一般有 SimHeiMac/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], markero) axes[0].set_title(月度借阅趋势) axes[0].tick_params(axisx, rotation45) # 右图热门图书 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, dpi300, bbox_inchestight)savefig 的两个参数值得说清楚dpi300 是保证打印到纸质报告里不模糊bbox_inchestight 会自动收缩画布边界避免标题或坐标轴标签被裁掉。如果后面要做成交互式网页报表常见做法是把 monthly、top_books、level_summary 三个 DataFrame 转成 JSON放到 Flask 接口里返回前端 ECharts 直接消费分析逻辑和这里的完全一样。换句话说先出静态图验证口径再做网页版能省掉大量调试时间。5. 图书馆借阅数据分析的 5 个高频踩坑点与排查思路5.1 中文乱码图里全是方框或问号现象Matplotlib 画出来的图里标题和坐标轴文字全是方框英文和数字正常。原因Matplotlib 默认字体不支持中文系统里也没有它默认查找的中文字体所以中文字符渲染不出来。解决在绘图脚本开头显式指定中文字体。Windows 上最省事的是 SimHeiLinux 或 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.23457e14或者开头是 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_support0.01, use_colnamesTrue) # 规则生成按提升度排序 rules association_rules(frequent, metriclift, min_threshold1.0) rules rules.sort_values(lift, ascendingFalse) # 只看置信度不低于 30% 的规则 strong_rules rules[rules[confidence] 0.3] print(strong_rules[[antecedents, consequents, support, confidence, lift]].head(20))验证规则时我会抽样回原数据确认比如规则显示“借了 A 的人同时借 B”就人工翻几条实际的借阅流水看看是不是来自同一个读者、同一次借阅。这一步能挡住一半以上的“假规则”。做过一次完整的关联规则分析之后我的习惯是把它沉淀成一个独立脚本以后每个月新数据进来清洗、统计、关联规则三步一次跑通。数据分析和系统实现一样能自动化的事就别靠手工。希望这个方案能帮你把借阅数据真正用起来也希望你踩过的坑比我少。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站