资讯动态

基于Python的图书馆借阅数据分析与协同过滤推荐系统实践

发布时间:2026/9/17 16:41:23 来源:尧图企业网站定制
简介一份面向专科与本科毕业生的原创毕业论文主题为基于Python的图书馆借阅数据分析设计与实现内容兼具数据挖掘、爬虫技术与Django Web开发。全文已做降重处理适合作为学术参考或毕业设计蓝本。论文在绪论中明确研究背景、目的与意义梳理国内外现状随后介绍Python、Pandas、NumPy、Matplotlib等数据分析工具及Django框架再依次涵盖数据抓取与清洗、存储管理、预处理、可视化分析和统计挖掘完整展示系统设计与实现脉络。资源包仅含1个docx文档大小35KB全文排版规范、目录清晰内容为西南财经大学学士论文的完整结构。目前已有473人学习下载。对需要掌握Python数据分析落地流程、了解图书馆场景业务分析的读者而言这篇论文既提供了理论框架也给出可复用的方法路径是一份扎实的参考资料。1. 借阅数据分析从“查统计”到“建模”的 Python 转型逻辑大部分图书馆管理系统自带的统计报表只能回答“哪本书借得多”回答不了“为什么多”“接下来谁会借哪本”“哪些书该下架”。这套基于 Python 的借阅数据分析论文项目本质是把图书馆的流通记录当作一张可挖掘的行为数据表借阅时间、读者身份、图书分类、续借次数全部变成特征再交给 Pandas 做聚合、用 Matplotlib 出图、最后用 Django 把分析结果变成可交互的界面。对正在做毕设的学生来说它是一条完整的数据分析流水线复现样本对已经在用 Python 做业务的工程师来说看点是这个场景里“数据清洗的脏程度远大于建模难度”这一现实问题。下文按我在本地复现这个项目时的推进顺序把数据获取、特征计算、可视化和系统集成四段讲透每段都给出可直接运行的代码和参数说明。2. 借阅数据获取与清洗建好分析的地基2.1 数据来源管理系统导出与爬虫补充采集图书馆借阅数据的常规来源有三个集成管理系统ILS的借阅流水表、读者信息表、图书书目表。ILS 一般支持按批次导出 CSV 或 Excel核心字段包括借书证号、图书条码、书名、中图法分类号、借出日期、应还日期、实还日期、续借次数。这套论文项目里数据源被设计成“管理系统导出为主、爬虫补充为辅”的组合爬虫只负责补全公开的书目元数据比如封面链接、出版社、主题标签这一步能让后续特征工程拿到更细的类别维度。import pandas as pd # 读取系统导出的借阅流水dtype 指定字符串防止证号丢前导零 circulation pd.read_csv( circulation_2024.csv, dtype{reader_id: str, barcode: str}, parse_dates[borrow_date, due_date, return_date], ) print(circulation.shape) print(circulation.isna().mean())逻辑说明parse_dates让 Pandas 在读取阶段就把三个日期字段解析为datetime64类型后续做时间差计算时不会因为字符串而报错dtype里指定读者证号为字符串是因为很多图书馆系统的证号是“A0001”这类混合格式直接按数字读取会丢掉字母。isna().mean()输出每个字段的缺失率用于判断哪些列需要重点清洗。爬虫补充部分常用requests拉取书目公开接口配合BeautifulSoup解析返回的 HTML 或 JSON。抓取时注意三点设置合理的请求间隔、声明 User-Agent、只采集允许公开访问的数据。补全后的书目信息通过 ISBN 与借阅流水做关联形成“读者—图书—主题”三层可分析结构。2.2 清洗规则重复、缺失、异常值处理借阅数据里最常见的坑是重复记录。同一本书在同一天被同一位读者多次借出往往是因为系统重试写入或归还后重新借出造成的重复行如果不处理聚合统计时会被重复计数。清洗时要先按“读者 ID 图书条码 借出日期”三列判断完全重复的行并删除之后再处理缺失值归还日期为空表示图书仍在借出状态这类记录不能直接丢弃而是要在计算借阅时长时用当前日期补齐。异常值方面借阅时长为负数归还日期早于借出日期和借阅日期早于馆藏创建日期都是典型的脏数据。# 完全重复去重keepfirst 保留第一条 circulation circulation.drop_duplicates( subset[reader_id, barcode, borrow_date] ) # 计算借阅时长未归还的记录用数据日期补齐 circulation[return_date] circulation[return_date].fillna( pd.Timestamp(2024-12-31) ) circulation[borrow_days] ( circulation[return_date] - circulation[borrow_date] ).dt.days # 过滤异常值时长必须落在 [0, 365] 区间 circulation circulation[ (circulation[borrow_days] 0) (circulation[borrow_days] 365) ]逻辑说明drop_duplicates的subset参数决定按哪些列判断重复这里必须限定三列而不是整个 DataFrame 相同否则两条借阅时间不同但证号和书相同的合法记录会被误删。fillna补齐未归还日期后dt.days把时间差转为整数天数这一步之后再过滤负值和超长借期能避免后续统计口径失真。参数边界设为 365 天是因为图书馆一般不允许超过一年不归还的普通借阅超长记录应该单独看而不是跟着正常数据一起算均值。2.3 存储与管理SQLite 分层表结构清洗完成的数据建议落到 SQLite理由有三个零配置文件、支持 SQL 聚合、便于 Django 直接读取。表结构分三层reader、book、borrow借阅流水表只存外键 ID不冗余存书名和读者姓名这样在做系统对接时只需要更新维度表不需要重刷流水表。CREATE TABLE reader ( reader_id TEXT PRIMARY KEY, gender TEXT, department TEXT, grade TEXT ); CREATE TABLE book ( barcode TEXT PRIMARY KEY, title TEXT, category TEXT, press TEXT, isbn TEXT ); CREATE TABLE borrow ( id INTEGER PRIMARY KEY AUTOINCREMENT, reader_id TEXT, barcode TEXT, borrow_date TEXT, return_date TEXT, renew_count INTEGER, FOREIGN KEY(reader_id) REFERENCES reader(reader_id), FOREIGN KEY(barcode) REFERENCES book(barcode) );表结构说明主键全部用业务自然键证号、条码而不是自增代理键这样 DBA 从旧系统迁移数据时不用做 ID 映射borrow表保留自增主键作为流水线的唯一标识位方便增量同步时记录上次拉取的位置。外键约束保证不会出现“借阅记录关联不到书”的孤儿数据一旦出现说明源头系统有脏数据或者清洗时联表丢了字段。3. Pandas 特征工程与借阅行为指标计算3.1 读者维度的借阅频次与周期特征数据清洗只是开始论文里真正有分析价值的环节是从流水表中提取行为特征。这里把借阅行为拆成两个粒度读者维度和图书维度。读者维度关注“谁在借、借得多不多、周期稳不稳定”典型特征包括累计借阅次数、平均借阅周期、最大同时借阅数、活跃时长跨度。这些特征用groupby加agg一次性算完。reader_features ( circulation.groupby(reader_id) .agg( borrow_count(id, count), avg_borrow_days(borrow_days, mean), renew_total(renew_count, sum), first_borrow(borrow_date, min), last_borrow(borrow_date, max), ) .reset_index() ) reader_features[active_span] ( reader_features[last_borrow] - reader_features[first_borrow] ).dt.days # 月均借阅量活跃跨度太小会导致数值异常放大 reader_features[monthly_avg] ( reader_features[borrow_count] / (reader_features[active_span].clip(lower30) / 30) ).round(2)逻辑说明agg里用字典映射把多个聚合函数一次性作用到不同列比多次groupby后合并要高一个量级renew_total是续借次数总和反映读者对长周期图书的依赖程度。active_span用最小值和最大值做差得到活跃天数再通过clip(lower30)防止活跃期不足一个月的读者被算出虚高的月均借阅量这一细节决定了后续做读者分群时不会出现极端离群点。3.2 图书热度与类别偏好的交叉分析图书维度要回答的是“馆藏结构合不合理”。把借阅流水按中图法分类号聚合再和馆藏总量做占比对比能一眼看出哪些类别属于“高借阅低馆藏”这才是采购优化的真正依据。另一个重要指标是动态借阅率即某本书被借出过的天数占它在馆总天数的比例。book_stats ( circulation.groupby([barcode, category]) .agg( borrow_count(id, count), distinct_readers(reader_id, nunique), avg_days(borrow_days, mean), ) .reset_index() ) # 借阅率 平均借出天数 / 365反映图书流通效率 book_stats[circulation_rate] ( book_stats[avg_days] / 365 ).clip(upper1).round(3) # 读者集中度不同读者数 / 借阅次数值越低说明复借现象越明显 book_stats[reader_concentration] ( book_stats[distinct_readers] / book_stats[borrow_count] ).round(3)关键指标说明nunique统计去重后的读者数它和count的比值就是读者集中度值接近 0.2 时说明一本书长期被少数读者反复借阅这种书适合采购复本而不是扩充新书circulation_rate被clip(upper1)限制在 0 到 1 之间因为借出天数不可能超过总天数。这两个比率比单纯的借阅次数更能反映馆藏的真实利用效率。3.3 从统计指标到可解释结论特征算完后一定要落到可解释的业务结论否则就是给自己造了一堆列名。这套论文项目里我用了一个四象限表来判断图书类别冷热横轴是借阅量、纵轴是读者数位于“高借阅高读者”象限的类别继续保量“高借阅低读者”象限属于集中型需求要增加复本“低借阅高读者”说明书有人问津但推广不足双低类别直接进入淘汰评估流程。整个判断逻辑可以用一个 2×2 分位表固化到代码里。象限借阅量分位读者数分位处理建议活跃区P75 以上P75 以上保持馆藏量增加复本集中区P75 以上P25 以下分析读者画像补充专业需求冷门区P25 以下P75 以上检查排架位置与推广渠道沉默区P25 以下P25 以下列入调整候选清单分位计算用pd.qcut输出标签再把象限规则做一次条件映射最后输出结果直接作为采购决策依据的输入层。注意这里的分位是相对值如果当年采购量整体偏低P75 对应的绝对值可能并不高因此阈值要按年度重新计算而不是写死数字。4. 可视化与推荐模型的工程实现4.1 借阅趋势与类别构成的可视化可视化部分承担的是“让非技术人员也能读结论”的职责。月度借阅趋势用折线图展示峰值月份类别构成用横向条形图展示借阅量 Top 10 分类读者分群用散点图体现活跃度分布。Matplotlib负责底层绘制Seaborn提供更简洁的样式接口。import matplotlib.pyplot as plt import seaborn as sns # 按月份聚合借阅量绘制趋势线 monthly ( circulation.set_index(borrow_date) .resample(M) .size() .reset_index(namecount) ) fig, ax plt.subplots(figsize(10, 4)) sns.lineplot(datamonthly, xborrow_date, ycount, markero, axax) ax.set_title(Monthly Borrowing Trend) ax.set_xlabel() ax.set_ylabel(Borrow Count) plt.tight_layout() plt.savefig(monthly_trend.png, dpi150)绘图参数说明resample(M)是 Pandas 的时间重采样方法M 表示按自然月聚合size()统计每个月的事件数reset_index把时间索引还原为普通列markero在折线上标出数据点当月度数据点密度低时避免图形变成纯线条。dpi150控制输出图片的清晰度插入论文时 150 足够清晰导出演示大屏可以提到 200 以上。tight_layout()会自动调整子图间距避免坐标轴标签被裁掉。4.2 基于借阅矩阵的协同过滤推荐推荐模块没有直接上深度学习而是用“基于物品的协同过滤”来生成推荐结果原因有三个计算量小、结果可解释、无需读者画像字段。先把借阅记录转换为“读者—图书”二值矩阵行是读者 ID列是图书条码值为是否借阅过。相似度计算采用余弦相似度选出目标读者借阅过的图书中最相似的 N 本书作为候选推荐。from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 构造读者-图书矩阵 matrix ( circulation.groupby([reader_id, barcode]) .size() .unstack(fill_value0) ) # 计算图书间的余弦相似度矩阵 book_sim cosine_similarity(matrix.T) book_sim_df pd.DataFrame(book_sim, indexmatrix.columns, columnsmatrix.columns) def recommend_for_reader(reader_id, top_k5): borrowed set(matrix.loc[reader_id][matrix.loc[reader_id] 0].index) scores {} for book in borrowed: # 与当前借阅书相似的候选图书 sim_series book_sim_df[book].drop(indexborrowed) for candidate, sim in sim_series.items(): scores[candidate] scores.get(candidate, 0) sim return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k]逻辑说明unstack把长表变成宽表行是读者、列是书空缺位置填 0这是做矩阵运算的标准姿势cosine_similarity接受行为样本、列为特征的矩阵所以要先转置matrix.T让列变成“样本”即图书drop(indexborrowed)把读者已经借过的书从候选集里移除避免推荐重复内容。最终分数是目标读者所有已借图书对候选书相似度的累加分数越高说明候选书与读者历史上偏好的整体重合度越高。参数top_k默认取 5推荐位较少时可以调整到 10但精确率会随 k 值增大而下降。4.3 推荐效果评估与参数折中推荐模型要用离线实验验证。把借阅记录按时间切分前 80% 做训练集后 20% 做测试集。用训练集生成推荐列表检查测试集中读者实际借的书有没有出现在推荐候选里以此计算命中率和覆盖率。命中率评估的是“猜得准不准”覆盖率评估的是“推荐范围够不够广”这两个指标在图书馆场景下往往互相制约。# 命中率验证测试期实际借阅记录出现在推荐列表中的比例 hit_count 0 test_count 0 for reader, group in test_data.groupby(reader_id): test_books set(group[barcode]) recommendations [book for book, _ in recommend_for_reader(reader, top_k10)] hit_count len(test_books set(recommendations)) test_count len(test_books) hit_rate hit_count / test_count print(fHit Rate{10}: {hit_rate:.3f})评估说明这里的top_k10给到 10 个推荐位是为了给受测读者更多的匹配机会test_books set(recommendations)取交集统计命中的图书数再除以测试期的借阅总数。这个口径是“推荐是否覆盖了用户的实际行为”如果命中率低于 0.15需要回查训练集切分是否泄漏或者相似度阈值是否设得太宽松。覆盖率则用推荐列表中出现的图书数量除以馆藏图书总数来计算覆盖率低于 20% 时说明推荐集中在少部分热门书上冷门书永远没有露出机会这在图书馆场景是不可接受的。5. Django 系统集成与借阅数据 API 的调试要点5.1 分析结果如何输出为可交互界面论文项目最后落地的形态是一个 Django 应用把前面算好的特征表和分析结论固定成接口。我的建议是用三层结构组织models.py只放清洗后的原始数据模型services.py放特征计算逻辑views.py只负责把计算结果序列化成 JSON 响应。不要把聚合逻辑写在views里否则每次页面打开都会触发全表扫描接口响应时间会从毫秒级退化成秒级。# services.py 中预计算月度借阅统计并缓存 from django.core.cache import cache MONTHLY_CACHE_KEY library_borrow_monthly_stats def get_monthly_stats(): result cache.get(MONTHLY_CACHE_KEY) if result is not None: return result stats ( Borrow.objects .annotate(monthTruncMonth(borrow_date)) .values(month) .annotate(countCount(id)) .order_by(month) ) data list(stats) cache.set(MONTHLY_CACHE_KEY, data, 60 * 30) return data代码说明TruncMonth是 Django ORM 的时间截断函数把borrow_date归一到当月第一天再分组Count(id)统计组内记录数。cache.set的第三个参数是过期时间这道题里设为 1800 秒也就是半小时因为借阅数据每天才会增量更新一次半小时刷新一次完全够用。缓存命中时直接返回列表避免每次请求都打数据库。5.2 接口层参数设计与时区陷阱Django 接口设计时要主动暴露分析参数而不是写死。例如月度趋势接口接受year参数热门图书接口接受limit和category参数读者画像接口接受reader_id参数。参数通过查询字符串传入视图里用request.GET.get()获取再传给 services 层。有一个高频排错点Django 默认时区是 UTC如果把借阅时间存成DateTimeField在TruncMonth分组时会出现“跨日记录被归到前一个月”的时区偏差。# 查看当前 Django 项目时区设置 python manage.py shell -c from django.conf import settings; print(settings.TIME_ZONE) # 修正settings.py 中设置为本地时区 # TIME_ZONE Asia/Shanghai # USE_TZ True参数说明USE_TZ True表示启用时区支持数据库里存 UTC 时间展示时按本地时区转换TIME_ZONE设置为Asia/Shanghai后Django 在模板渲染和 ORM 查询时都会自动做时区换算。如果USE_TZ是FalseDjango 会按TIME_ZONE直接存储本地时间此时TruncMonth按本地时间分组但配置了USE_TZ False的项目迁移到新环境时容易因为服务器时区和数据库时区不一致产生时间偏移所以更推荐保留USE_TZ True只在查询时用TruncMonth配合__date表达式做转换。5.3 三个值得检查的细节第一个是缓存未命中时的雪崩效应。如果多个分析接口共用同一个过期时间缓存同时到期时所有请求会同时打到数据库解决方法是给不同接口的缓存 key 加随机偏移量。第二个是借阅数据增量同步的幂等处理设计一个sync_log表记录上次同步的流水主键 ID下次同步时只拉取大于该 ID 的记录重跑时不删历史数据而是按主键做 upsert。第三个是数据库索引不可省略Borrow表的borrow_date和reader_id必须分别加索引否则TruncMonth分组和groupby(reader_id)在数据量超过十万条时会明显变慢这就是为什么有些分析系统换一台机器就奇慢无比而自家库里却很快的原因。验证索引是否生效用 Django ORM 的.explain()方法查看查询计划确认走了索引再继续这一步能省下后面大半年的排查时间。本文还有配套的精品资源点击获取

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

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

免费获取报价