ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Python的图书馆借阅数据分析:从清洗到可视化实战

基于Python的图书馆借阅数据分析:从清洗到可视化实战

简介:基于python的图书馆借阅数据分析设计与实现,是一份面向数据分析初学者、图书馆专业学生和信息化从业者的实战资源,可用于课程设计、毕业设计或数据分析项目练手。它围绕2014至2017年图书借还记录、读者信息、图书目录等数据,通过主题词提取、帕累托分析、热门书分析、排名变化四个Python脚本,并配以图书分类法简表、停用词表等文本资源,完整演示了从数据清洗到分析结果展示的流程。压缩包共16个文件,包含7个xlsx数据文件、4个py脚本、3个txt说明文档、1个readme和1张说明图,整体仅46.45MB,目录结构清晰,readme可帮助快速了解各文件用途。该资料已有2275人学习,适合想用真实业务数据提升Python数据分析与挖掘能力的读者,代码与文档配合到位,便于理解每一步分析思路,直接运行即可复现结果。

1. 基于 Python 的图书馆借阅数据分析:源码包拆解与实战复现路径

图书馆借阅数据本质上是长期积累的用户行为数据,它包含读者信息、借还记录、图书目录三张核心表。基于 Python 做借阅数据分析,不是跑一两个统计函数出几张图就完事,而是要把“谁在借什么书、借阅量如何变化、热门书排名怎么迁移、主题词分布反映什么学科热点”这套完整链路跑通。这份资源包正好覆盖了借书名 TOP20 排名变化、帕累托分析、热门书分析和主题词提取四个模块,适合正在做图书相关毕设、需要完成部门借阅报告的数据从业者。它解决的不是“会不会 pandas”的问题,而是“拿到真实图书馆数据后按什么顺序清洗、分组、透视、出结论”的问题。下面我按实际拆解的顺序,把每个脚本的设计意图和落地参数逐一讲透。

2. 借阅数据结构和预处理:四张 Excel 表先要解决三个脏数据问题

整个分析链路的前提是数据能对齐。包里给了 2014 到 2017 四年的“图书借还”表,外加读者信息、图书目录、中图法简表和停用词表。很多人上来直接 pd.read_excel 然后就开始 groupby,结果透视出来的数字对不上——这不是运气问题,是没做前置校验。

2.1 四张表的关系:主键、外键和粒度

先明确表之间的关系:图书目录表以“图书ID”为主键;读者信息表以“读者证号”为主键;借还表是明细流水,每一行是一次借阅行为,包含图书ID、读者证号、借书日期、续借次数等字段。粒度上,借还表是“一本书被借一次”一行,不是按天汇总,也不是按读者汇总。

读进来之后第一件事是统一字段类型。

import pandas as pd # 读取四张核心表 borrow_2014 = pd.read_excel('图书借还2014.xlsx') borrow_2015 = pd.read_excel('图书借还2015.xlsx') borrow_2016 = pd.read_excel('图书借还2016.xlsx') borrow_2017 = pd.read_excel('图书借还2017.xlsx') reader_df = pd.read_excel('读者信息.xlsx') book_df = pd.read_excel('图书目录.xlsx') # 合并四年借阅数据,加一个年份字段方便后续按年对比 borrow_2014['年份'] = 2014 borrow_2015['年份'] = 2015 borrow_2016['年份'] = 2016 borrow_2017['年份'] = 2017 borrow_df = pd.concat( [borrow_2014, borrow_2015, borrow_2016, borrow_2017], ignore_index=True ) print(borrow_df.shape) print(borrow_df.dtypes)

逻辑说明:concat 纵向堆叠四张表之前,先手动加“年份”列,否则合并后无法区分一条记录来自哪一年。ignore_index=True 是让合并后的行索引重新从 0 编号,避免出现重复索引导致后续 groupby 结果诡异。

参数说明:read_excel 在读真实图书馆导出的表时,经常遇到表头不在第一行的情况。如果发现列名变成了“Unnamed: 0”,用header=1或skiprows指定真正的表头行,比事后改名省事。

2.2 脏数据问题一:读者证号数字和字符串混存

图书馆业务系统导出的读者证号,有的表里是纯数字,有的表里是文本,还有的带着前导零。如果不统一,两个表 merge 的时候同一读者会被拆成两条记录。

# 统一读者证号为字符串,并去掉两端空白 for col in ['读者证号', '证号']: if col in borrow_df.columns: borrow_df[col] = borrow_df[col].astype(str).str.strip() if col in reader_df.columns: reader_df[col] = reader_df[col].astype(str).str.strip() # 校验:看看证号是否有空值 print(borrow_df['读者证号'].isna().sum()) print(reader_df['证号'].isna().sum())

逻辑说明:astype(str) 会把数字 1001 转成字符串 "1001",但如果是浮点数 1001.0 会转成 "1001.0",所以更稳妥的做法是先去掉 ".0" 后缀。strip() 是去掉业务系统里常见的不可见空格。

参数说明:如果你发现证号字段变成 "1001" 和 "1001.0" 两种形态,用str.replace(r'\.0$', '', regex=True)清洗。这一步不做,后面所有 merge 都会少数据。

2.3 脏数据问题二:书名字段里的版本、册次、标点噪声

同一本书在借还记录里可能写成《三体》、《三体(全集)》、《三体 1》等。做热门书排名时,这些会被当成三本书分别统计,热榜就被稀释了。

import re def clean_title(title): """清理书名中的版本、册次和标点噪声""" if not isinstance(title, str): return title # 去掉括号内容,如(全集)、(上中下册) title = re.sub(r'[((][^))]*[))]', '', title) # 去掉“第X册/部/卷”这类册次描述 title = re.sub(r'第[0-9一二三四五六七八九十]+[册部卷]', '', title) # 去掉书名号 title = title.replace('《', '').replace('》', '') return title.strip() borrow_df['题名_清洗'] = borrow_df['题名'].apply(clean_title)

逻辑说明:清洗规则要克制。去掉括号内容能合并“全集/套装”噪声,但有些书名本身带括号且是正式书名的一部分(比如《经济(季刊)》),这一条规则会误伤。取舍上,热门书分析用清洗后的字段,单本明细展示用原始字段。

参数说明:正则[((][^))]*[))]匹配中文和英文括号内的内容。如果你的数据里有英文括号嵌套,先跑一轮看清洗前后的唯一值数量变化,确认没有大面积误伤再定稿。

2.4 脏数据问题三:借书日期跨年错乱和空值

借还表里偶发“还书日期早于借书日期”这种物理不可能的数据,以及部分日期缺失。处理策略不是删除,而是打标记。

# 统一日期列 borrow_df['借书日期'] = pd.to_datetime(borrow_df['借书日期'], errors='coerce') borrow_df['还书日期'] = pd.to_datetime(borrow_df['还书日期'], errors='coerce') # 标记异常:还书日期比借书日期更早 mask = borrow_df['还书日期'] < borrow_df['借书日期'] print(f"异常借还记录数:{mask.sum()}") # 过滤掉借书日期为空的记录,保留还书日期异常但借书日期正常的记录 borrow_df = borrow_df[borrow_df['借书日期'].notna()].copy()

逻辑说明:errors='coerce' 会把无法解析的日期变成 NaT,然后过滤掉借书日期的空值。还书日期异常记录建议单独存一份,不直接删除——有些系统在续借时会把还书日期重写,看起来像负借阅时长。

参数说明:dateutil 解析中式日期 "2015/1/2" 和 "2015-01-02" 都能处理,但如果遇到纯数字格式 "20150102",pd.to_datetime 需要加format='%Y%m%d'指定。

3. 热门书分析和帕累托图:从借阅明细到采购决策的落地路径

预处理做完,进入第一个核心分析模块。资源包里“热门书分析.py”和“生成帕累托.py”两个脚本,解决的是同一个问题的两个侧面:热门书告诉你是哪几本在撑场面,帕累托告诉你这种“撑场面”到底有多集中。

3.1 热门书 TOP20:聚合键怎么选、并列排名怎么处理

热门书分析的第一步是按图书聚合借阅次数。这里有个细节:按“图书ID”聚合还是按“题名”聚合?我的建议是优先用“图书ID”聚合,因为同一本书在书目表里 ID 唯一,题名可能有细微差异。

# 热门书 TOP20:按图书ID聚合,附加题名信息 book_hot = ( borrow_df .groupby(['图书ID']) .agg( 借阅次数=('图书ID', 'count'), 题名=('题名_清洗', 'first') ) .reset_index() .sort_values('借阅次数', ascending=False) ) book_hot_top20 = book_hot.head(20).copy() book_hot_top20['排名'] = range(1, 21) print(book_hot_top20[['排名', '题名', '借阅次数']])

逻辑说明:groupby('图书ID') 后 agg 里同时传两个 aggfunc:count 统计借阅次数,first 取该 ID 下第一条题名作为代表。sorted 后 head(20) 截取前 20。

参数说明:如果你希望并列排名,比如两本书并列第五,用rank(method='min')生成排名列,而不是 range(1, 21)。业务上并列排名更常见,因为真实借阅数据里次数相同的概率不低。

3.2 帕累托分析:头部 20% 图书到底扛了多少借阅量

帕累托图的核心是算累计占比。这里的关键不是图,而是“前多少本书贡献了 80% 的借阅量”。

# 帕累托分析:排序后的累计贡献 book_hot['累计借阅次数'] = book_hot['借阅次数'].cumsum() book_hot['累计占比'] = book_hot['累计借阅次数'] / book_hot['借阅次数'].sum() * 100 # 找达到 80% 的临界位置 reach_80 = book_hot[book_hot['累计占比'] >= 80] print(f"达到80%累计借阅量需要 {reach_80.index[0] + 1} 本书") print(f"图书总数: {len(book_hot)}") print(f"头部{reach_80.index[0] + 1}本的借阅量占比: " f"{book_hot['累计占比'].iloc[reach_80.index[0]]:.2f}%")

逻辑说明:cumsum 依次累加,累计占比第一次突破 80 的行索引加一就是“覆盖八成借阅量所需的最少图书数”。index[0] 取的是第一个满足条件的索引位置。

参数说明:一本图书馆藏几万册时,这个数字通常在几百到一千之间。如果前 50 本就贡献了 80% 借阅量,说明馆藏极度头部集中——采购复本量需要加大,冷门书要考虑剔旧。

3.3 帕累托图的 Matplotlib 绘制参数

脚本最终要出图。帕累托图是柱状图加折线图的组合:柱状图是每本书的借阅次数,折线图是累计占比。

import matplotlib.pyplot as plt # 只画前 50 本,后面的长尾太长没有可视化意义 plot_data = book_hot.head(50) fig, ax1 = plt.subplots(figsize=(12, 6)) ax1.bar(plot_data['题名'][:20], plot_data['借阅次数'][:20], color='#4C72B0') ax1.set_xlabel('图书') ax1.set_ylabel('借阅次数') ax1.set_xticklabels(plot_data['题名'][:20], rotation=45, ha='right') # 次坐标轴画累计占比折线 ax2 = ax1.twinx() ax2.plot(plot_data['累计占比'][:20], color='#C44E52', marker='o') ax2.set_ylabel('累计占比 (%)') ax2.set_ylim(0, 100) plt.title('图书馆借阅帕累托分析(前20本)') plt.tight_layout() plt.savefig('pareto_top20.png', dpi=150)

逻辑说明:twinx() 创建共享 x 轴的第二个 y 轴,让柱状图和折线图量纲不同也能叠在一起。取前 20 本书画柱状图是因为前 20 的借阅次数差异大,画前 50 会让小数值被压成一条线。

参数说明:dpi=150 是输出分辨率,用在报告里够清晰,用在投影屏幕上建议提到 200。color 参数可以换成图书馆统一的 VI 色,避免默认红蓝配色和学校报告模板冲突。

4. 借阅排名变化分析:跨年度对比与消减趋势的量化算法

只看单年热榜容易判断失误——某本书去年排第一,今年可能掉出前十。尤其高校图书馆每年采购经费、专业设置都在变,书的热度迁移能反映学科建设方向的调整。资源包里的“排名变化”脚本,就是做跨 2014 到 2017 四年的借阅榜对比,量化哪本书上升了、哪本掉队了。

4.1 多年度对比的透视表设计:排名矩阵

核心思路是把四年排名压成一张宽表:每一行是一本书,每一列是一个年份,单元格的值是这本书当年的热榜排名。这样横向看一本书的迁移轨迹,纵向看某年的热榜构成。

# 先按年分组计算借阅次数和当年的内部排名 borrow_df['年度排名'] = ( borrow_df .groupby('年份')['图书ID'] .transform('count') ) # 按年度排序并生成排名 book_rank = ( borrow_df .groupby(['年份', '图书ID', '题名_清洗']) .agg(借阅次数=('图书ID', 'count')) .reset_index() ) book_rank['当年排名'] = ( book_rank .groupby('年份')['借阅次数'] .rank(method='dense', ascending=False) ) # 透视成宽表 rank_pivot = book_rank.pivot_table( index='题名_清洗', columns='年份', values='当年排名', aggfunc='min' ) print(rank_pivot.head(20))

逻辑说明:rank 部分放在了 groupby 之后而不是 transform,因为需要先聚合出每本书在每年的总借阅次数,再算排名。pivot_table 的 aggfunc='min' 处理同一本书在同一年出现多行的情况。

参数说明:rank 的 method='dense' 表示并列名次不占位,两本书并列第一时,下一本还是第二而不是第三。如果你要严格排名,用 method='min' 或 'first'。

4.2 消减趋势判定:连续两年在榜但名次下滑的书

排名变化不只是看差值,还要识别“持续在榜但逐步下滑”的书——这种比某一年的暴跌更值得关注。

# 筛选连续四年都在 TOP20 榜单的书 mask_2014 = rank_pivot[2014] <= 20 mask_2015 = rank_pivot[2015] <= 20 mask_2016 = rank_pivot[2016] <= 20 mask_2017 = rank_pivot[2017] <= 20 continuous = rank_pivot[mask_2014 & mask_2015 & mask_2016 & mask_2017] # 计算四年排名斜率(用线性拟合的系数近似) from scipy.stats import linregress def trend_slope(row): years = [2014, 2015, 2016, 2017] ranks = row.values # 排除 NaN 后做最小二乘拟合 valid = [(y, r) for y, r in zip(years, ranks) if not pd.isna(r)] if len(valid) < 2: return 0 slope, _, _, _, _ = linregress([y for y, _ in valid], [r for _, r in valid]) return slope return 0

等一下,上面 return 0 的缩进写错了,改一下。

def trend_slope(row): years = [2014, 2015, 2016, 2017] ranks = row.values # 排除 NaN 后做最小二乘拟合 valid = [(y, r) for y, r in zip(years, ranks) if not pd.isna(r)] if len(valid) < 2: return 0 slope, _, _, _, _ = linregress([y for y, _ in valid], [r for _, r in valid]) return slope continuous['排名斜率'] = continuous.apply(trend_slope, axis=1) # 斜率为正代表排名数值变大,即排名下滑 print(continuous[continuous['排名斜率'] > 1].sort_values('排名斜率', ascending=False).head(10))

逻辑说明:linregress 用最小二乘拟合“年份-排名”的线性关系,斜率大于 1 意味着排名每年大约下滑 1 名以上。排名数值是离散整数,线性拟合会损失一些精度,但用于排序筛选足够。

参数说明:scipy.stats.linregress 返回五个值,取第一个即斜率。如果没装 scipy,可以用 numpy 的 polyfit 替换,效果一致。

4.3 排名变化的热图和标注:把变化趋势可视化

趋势表是给分析人员看的,热图是给决策者看的。热图的 y 轴是书名,x 轴是年份,颜色越深代表排名越靠前。

import seaborn as sns # 取四年都上榜的排名数据画热图 plot_rank = continuous[['排名斜率']].merge( rank_pivot, left_index=True, right_index=True ).head(15) plt.figure(figsize=(10, 8)) sns.heatmap( plot_rank[[2014, 2015, 2016, 2017]], annot=True, fmt='g', cmap='YlOrRd', cbar_kws={'label': '排名'}, linewidths=0.5 ) plt.title('2014-2017 连续上榜图书排名变化热图') plt.tight_layout() plt.savefig('rank_heatmap.png', dpi=150)

逻辑说明:annot=True 在每个方格中写入排名数值,fmt='g' 去除小数位。颜色深和浅的跳变点在视觉上非常直观——要么是断崖式下滑,要么是新书强势冲榜。

参数说明:cmap 选 'YlOrRd' 是黄红渐变,排名 1 对应黄色。如果想让第一名最显眼,用cmap='rocket_r'(深色代表第一)。

5. 主题词提取与文本挖掘:从书名到学科热点的分析闭环

借阅次数只能告诉你哪本书火,没法告诉你哪类知识火。图书馆报告常用的做法是提取书名主题词,再映射到中图法分类号,量化“计算机类借阅上涨 X%”、“文学类占比下降 Y%”。资源包里的“提取主题词.py”处理的就是这个环节。

5.1 主题词提取:jieba 分词与停用词过滤的参数设置

主题词提取不是简单的分词,要去掉停用词、只保留有区分度的实词。包里的 stopwords.txt 是领域定制停用词表,比通用表多了“上册”“下册”“大全”“指南”这类图书馆场景词。

import jieba from collections import Counter # 加载停用词表 with open('stopwords.txt', encoding='utf-8') as f: stopwords = set(line.strip() for line in f if line.strip()) def extract_top_keywords(title_series, top_n=30): """ 从书名Series提取主题词并统计频次 """ word_counter = Counter() for title in title_series: if not isinstance(title, str): continue # 精确模式分词 words = jieba.lcut(title) for w in words: w = w.strip() # 过滤停用词、单字、纯数字 if ( w and w not in stopwords and len(w) > 1 and not w.isdigit() ): word_counter[w] += 1 return word_counter.most_common(top_n) title_keywords = extract_top_keywords(borrow_df['题名_清洗'], top_n=30) for word, count in title_keywords[:20]: print(word, count)

逻辑说明:jieba.lcut 返回分词后的列表;len(w) > 1 过滤掉“学”“书”这类单字;not w.isdigit() 过滤掉书名里的年份和卷号数字。词频统计放在最后一个循环里,避免中途重复遍历。

参数说明:jieba 默认词库里“Python”可能被切成“Python”,也可以切成“Py”“thon”——如果切分结果不理想,手动 add_word 加固领域词。

5.2 中图法分类映射:从 22 个大类到热门主题词画像

中文图书分类法把馆藏拆成 22 个大类(A 马克思主义、B 哲学、C 社会科学总论……T 工业技术),书的“中图分类号”在图书目录表里。把借阅流水的图书ID join 上分类号,就能按类目聚合。

# 借阅流水关联中图分类号 borrow_with_cat = borrow_df.merge( book_df[['图书ID', '中图分类号']], on='图书ID', how='left' ) # 取分类号的首字母作为一级类目 borrow_with_cat['一级类目'] = borrow_with_cat['中图分类号'].str[0] # 按年份+类目统计借阅量 cat_yearly = ( borrow_with_cat .groupby(['年份', '一级类目']) .agg(借阅次数=('图书ID', 'count')) .reset_index() ) # 透视成宽表方便比较各年占比 cat_pivot = cat_yearly.pivot_table( index='年份', columns='一级类目', values='借阅次数', aggfunc='sum' ).fillna(0) # 计算各类目年度占比 cat_ratio = cat_pivot.div(cat_pivot.sum(axis=1), axis=0) * 100 print(cat_ratio.round(2))

逻辑说明:merge 时 how='left' 保借阅流水全部记录,没有匹配到分类号的书会留空。str[0] 取分类号首位字母,如果你想精确到二级类目,用 str[:3]。

参数说明:有的图书馆编目时分类号带辅助符号,比如“TP312.5”,str[0] 取的是 T;空分类号的书算入“未知类目”——建议单独统计占比,不要直接填充为某个字母,否则会污染类目聚合结果。

5.3 学科热点的年度趋势:主题词与头部类目的交叉验证

把主题词频和中图法类目趋势交叉起来看,能有“软件工程主题词频上涨,T 类借阅增长 15%”这种交叉验证结论。写代码时注意:主题词从书名提取,类目从分类号聚合,两者是互补视角。

# 按年度提取 TOP10 主题词,观察主题词迁移 for year in [2014, 2015, 2016, 2017]: year_data = borrow_df[borrow_df['年份'] == year] year_top = extract_top_keywords(year_data['题名_清洗'], top_n=10) words = [w for w, c in year_top[:5]] print(f"{year}: {', '.join(words)}") # 输出:2014和2017的热词对比通常是年度报告的核心数据

逻辑说明:主题词的年度对比建议输出到 CSV 而不是直接打印——后续可以做词频涨幅排序,找出“新晋热词”和“退热词”。所谓新晋热词是指上一年的 TOP20 里没有、今年冲进 TOP10 的词。

参数说明:top_n 参数控制每年前 N 个词。如果图书馆馆藏偏向理工类,建议把停用词表里加上“研究、分析、方法、基于、实现”这些论文常用噪声词。

6. 借阅分析报告的避坑与常见问题排查

从数据清洗到主题词提取,每个环节都有几个高频雷区。这里按“现象-原因-解决方案”记录实际拆解中踩过的坑。

6.1 重复借阅记录导致借阅次数虚高

现象:同一本书同一个读者在同一分钟借了两次,热门书排名里这本书虚高一位。

原因:系统在读者续借时生成了一条新记录,而不是在原记录上更新续借次数;或者读者还书后立刻再借同一本书,时间差只有几秒。

解决:按“图书ID + 读者证号 + 借书日期”三个字段去重。注意不要去掉“一小时内同一读者重借同一本书”这种数据,可能是真实的有针对性借阅。

6.2 年份字段混入日期格式导致 concat 后年份列变成 datetime

现象:合并四年数据后,年份列出现 2015-01-01 而不是 2015。

原因:某一张表的 Excel 单元格格式被设成了日期,导致 pandas 读出的是 datetime 而不是 int。

解决:读入后统一处理年份列:如果是 datetime,用 dt.year 提取年份;如果是 int,保持原样。

# 年份列兜底:如果是 datetime 类型,提取年份 def coerce_year(series): if pd.api.types.is_datetime64_any_dtype(series): return series.dt.year return series for year_df in [borrow_2014, borrow_2015, borrow_2016, borrow_2017]: year_df['年份'] = coerce_year(year_df['年份'])

6.3 中图分类号缺失导致类目分析出现巨大“未知类”

现象:类目分析里“未知”占比达到 30%,无法解释其他类目的趋势。

原因:图书目录表和借还表关联时,部分索书号为空,或者分类号字段存在中文描述和字母代码混用。

解决:对空值单独标记,不参与热词类目分析;分类号别直接用 str[0],先 strip 再加非法字符校验(取值必须是 A-Z 开头)。

6.4 笛卡尔积爆炸:merge 时忘了去掉重复的图书ID

现象:借阅数据从几万行变成几百万行,groupby 结果明显错误。

原因:图书目录表里同一本书出现多行(不同副本、不同馆藏地点),直接 merge 时每一行都匹配上了。

解决:先 drop_duplicates 再 merge。

book_df_dedup = book_df.drop_duplicates(subset=['图书ID']) borrow_with_cat = borrow_df.merge( book_df_dedup[['图书ID', '中图分类号']], on='图书ID', how='left' )

7. 资源包复现的扩展用法:把静态分析变成动态模板

分析做完,代码是死的,数据是新的。这份资源包的价值在于换成新数据也能跑通。最后给出三个扩展用法,帮你把一次性分析变成可持续的月度报告模板。

7.1 参数化年度对比:函数化重构分析脚本

把四年对比写成函数,传任意年份列表就能跑,不用每次改代码。

def yearly_borrow_report(borrow_full, years): """生成指定年份集合的借阅对比报表核心字段""" summary_rows = [] for y in years: y_data = borrow_full[borrow_full['年份'] == y] top_book = ( y_data.groupby('题名_清洗')['图书ID'] .count() .sort_values(ascending=False) .head(1) ) summary_rows.append({ '年份': y, '总借阅次数': len(y_data), '活跃读者数': y_data['读者证号'].nunique(), '榜首图书': top_book.index[0], '榜首借阅次数': int(top_book.iloc[0]), }) return pd.DataFrame(summary_rows) # 示例调用 report = yearly_borrow_report(borrow_df, [2014, 2015, 2016, 2017]) print(report)

逻辑说明:nunique 是去重计数,和 count 的区别是关键——总借阅次数可以重复,活跃读者数不能重复。

7.2 排行榜断崖检测:用排名变化斜率自动标注异常书

把第四章的斜率逻辑换成分类判断,自动输出预警列表。

def flag_rank_anomalies(rank_matrix, drop_threshold=5): """ 标记排名大幅下滑的书 drop_threshold: 连续两年排名下滑超过该值则预警 """ flags = [] for book in rank_matrix.index: ranks = rank_matrix.loc[book].dropna() if len(ranks) >= 3: diff = ranks.diff().dropna() if (diff > drop_threshold).any(): flags.append({ '书名': book, '最大单年下滑': int(diff.max()), '最近排名': int(ranks.iloc[-1]), }) return pd.DataFrame(flags) # 输出下滑预警列表 anomaly_df = flag_rank_anomalies(rank_pivot) print(anomaly_df.sort_values('最大单年下滑', ascending=False).head())

逻辑说明:diff() 计算行内相邻年份的排名差,值为正代表排名数字变大即下滑。超过阈值的年份被标记为异常。

7.3 生成一页纸摘要:把多维指标压进一个 Excel

最终交付不是一堆图,而是一个单一入口的报表文件。把总览、TOP10、类目趋势、排名变化放进不同 sheet,打开第一个 sheet 就能看到核心结论。

# 写出多 sheet 报表 with pd.ExcelWriter('某高校图书馆借阅分析月报.xlsx', engine='openpyxl') as writer: report.to_excel(writer, sheet_name='总览', index=False) book_hot_top20.to_excel(writer, sheet_name='热门书TOP20', index=False) cat_ratio.round(2).to_excel(writer, sheet_name='类目占比') rank_pivot.head(30).to_excel(writer, sheet_name='排名变化')

逻辑说明:ExcelWriter 是写多 sheet 的标准姿势。注意第一个 sheet 必须是总览,而不是数据明细——决策者打开文件看到的第一个东西决定ta对这个报告的整体印象。

参数说明:engine='openpyxl' 是 xlsx 格式最稳定的选择;如果遇到数字被存成文本,补一步sheet.to_excel(..., float_format='%.2f')统一数字格式。

作为拆过多个图书数据项目的人,最大的体会是这类分析没有“一次跑通”的运气,只有“反复验证”的习惯。每次拿到新的月度数据,我都强制自己先跑一遍去重率、空值率、同比波动三个 sanity check,再谈分析和图表——这个习惯帮我躲过了好几次排名虚高和类目缺失的坑。这份资源包的脚本拿过去可以直接改路径运行,但真正值钱的是里面清洗逻辑和分类映射的取舍。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进