☰
Python源码实战:pandas清洗账单+数据可视化分析个人消费方向
2026/9/26 22:07:39 网站建设 项目流程

简介:针对日常记账数据自动化分析与可视化需求,这份Python源码示例适合想用编程管理个人消费的用户,也适合数据分析入门者跟着动手实践。压缩包共3个文件,包含Python脚本、Excel原始账本和程序使用说明,整体仅20KB,结构精简便于对照学习,已有191人学习下载。示例围绕Pandas数据清洗与分类汇总、NumPy数值计算以及matplotlib/seaborn图表绘制展开,演示如何从表格中提取消费类别、统计占比并生成柱状图或饼图,同时引入自动化处理思路,让记账分析不再依赖手工操作。通过运行脚本,读者能掌握读取Excel、按食品/交通/娱乐等维度归类汇总、输出可视化结果的完整流程,还可将方法迁移至电商订单、外卖记录等场景,或结合网络爬虫自动抓取消费数据,提升个人财务数据管理效率。

1. 为什么一份Python源码就能把记账数据变成消费方向结论

你的账单里塞满了“拼多多付款”“美团外卖”“滴滴出行”这类流水备注,Excel里翻一年也看不出钱到底流向哪里。但如果你把这些日常记账数据交给pandas做一轮清理和聚合,再用matplotlib画几张图,消费方向其实可以在一屏之内讲清楚。标题里这份“数据分析和可视化分析日常记账数据总结个人消费方向”的Python源码示例zip,做的就是这件事:它把“记账数据→清洗→聚合→可视化分析→消费方向结论”这条链路串成可复用的代码,你只需喂给它一张导出的账单CSV,就能得到每个月钱花在哪些大类、占比如何变化。这篇文章按我实际跑这套方案的经验拆开来讲:数据怎么整理、消费方向怎么算、图表怎么画,以及最容易翻车的几个点。

2. 记账数据整理:从账单CSV到可分析的pandas表

2.1 账单数据源与统一字段设计

支付宝、微信、银行卡导出的账单,表头完全不同。支付宝导出的是“交易时间、交易金额、交易分类、商品说明”,微信导出的是“交易时间、交易类型、交易备注、商户单号”,Excel手工记账更是千奇百怪。如果每个数据源都写一套读取逻辑,这套Python源码示例就没法复用到下个月。所以第一步是先把所有账单统一成四个核心字段:日期、金额、分类、备注。

常见做法是我在源码包里放一个load_bill()函数,专门处理CSV文件里最常见的干扰项:BOM头、空行、表头偏移、编码问题。支付宝账单默认是UTF-8带BOM,用Excel另存的CSV可能是GBK,直接pd.read_csv会读出一堆乱码。我这里先按UTF-8-sig读取,如果发现列名不对,再回退到GBK。

import pandas as pd def load_bill(path): # 支付宝/csv账单一般带BOM头,Excel导出的可能是GBK编码 # 先尝试UTF-8-sig,失败后用GBK读取 try: df = pd.read_csv(path, encoding='utf-8-sig') except UnicodeDecodeError: df = pd.read_csv(path, encoding='gbk') # 统一列名:后续分析只依赖日期、金额、分类、备注四个字段 df.rename(columns={ '交易时间': '日期', '交易金额': '金额', '交易分类': '分类', '商品说明': '备注', '备注': '备注' }, inplace=True) # 只保留需要的列,防止无关字段干扰内存 cols = [c for c in ['日期', '金额', '分类', '备注'] if c in df.columns] df = df[cols] df['日期'] = pd.to_datetime(df['日期'], errors='coerce') return df

这个函数的逻辑很直白:先解决编码,再解决列名映射,最后把日期转成pandas的datetime类型。重点说一下errors='coerce',遇到解析不了的时间字符串会变成NaT而不是直接抛异常,后面清洗时可以统一丢弃。参数方面,如果你用微信账单,列名映射要换成“交易时间、交易金额、交易类型、交易备注”,我在源码里是单独留了一个BILL_CONFIG字典,而不是写死。

2.2 清洗脏数据:过滤退款、空值和不需要的交易类型

很多人下载了支付宝账单直接跑聚合,发现总支出比实际多出一大截。原因通常是账单里有“退款”和“资金互转”“信用卡还款”这类根本不算消费的记录。退款是负金额,但它对应的原交易已经在支出里记过,如果不处理,相当于同一笔钱扣了两次。资金互转则是把钱从余额转到银行卡,并不是消费。

我习惯把它和金额清洗放在同一个函数里,这样源码示例从load_bill()拿到原始数据后,只需再调clean_bill()就得到干净表。

def clean_bill(df): # 丢弃日期或金额为空的数据 df = df.dropna(subset=['日期', '金额']) # 退款记录:备注中出现“退款/交易关闭”时,金额保持为负,但单独标记 refund_mask = df['备注'].astype(str).str.contains('退款|交易关闭', na=False) df['is_refund'] = refund_mask # 过滤掉不算消费的类别,这些项目会使消费方向分析偏掉 exclude_cats = ['资金互转', '信用卡还款', '余额宝', '理财'] df = df[~df['分类'].isin(exclude_cats)] # 金额列可能是字符串,比如“¥12.50”或“1,200.00” # 先去掉人民币符号和千分位逗号,再转float df['金额'] = (df['金额'].astype(str) .str.replace('¥', '', regex=False) .str.replace(' ', '', regex=False) .str.replace(',', '', regex=True) .astype(float)) return df

这里有一个处理退款的关键点:我没有直接删掉退款记录,而是加上is_refund标记。因为在月度汇总时,退款应该从当月支出里减掉,而不是当做一个负支出直接累加。更稳妥的做法是:先把退款按原交易去重,再在月度总额中扣减。虽然百分比小,但金额大时会让最后结论偏差几十上百元。金额清洗时我用了regex=True去逗号,因为千分位分隔符可能出现在金额里,但¥是固定字符所以用regex=False,效率更高。

2.3 消费类别归一化:从商户名到真正的消费方向

账单自带的“分类”字段通常不统一。支付宝可能给“餐饮美食”“交通出行”“购物娱乐”,微信则是一堆“商户消费”“转账”等笼统标签。如果不做归一化,做出来的饼图会有一堆“其他”,根本看不出消费方向。所以我在源码里维护一个CATEGORY_KEYWORDS关键词映射表,把常见商户或备注字样归成五到六个大类:餐饮、交通、购物、居住、娱乐、其他。

CATEGORY_KEYWORDS = { '餐饮': ['美团', '饿了么', '餐厅', '星巴克', '麦当劳', '瑞幸', '外卖'], '交通': ['滴滴', '地铁', '公交', '加油', '停车', '高铁', '机票'], '购物': ['淘宝', '京东', '拼多多', '天猫', '唯品会', '超市', '便利店'], '居住': ['房租', '水费', '电费', '燃气', '物业', '宽带'], '娱乐': ['电影', '游戏', 'bilibili', '爱奇艺', '腾讯视频', 'KTV'], } def map_category(row): # 如果账单自带的分类已经是我们目标大类,直接用 if row['分类'] in CATEGORY_KEYWORDS: return row['分类'] # 否则在备注里做关键词匹配,这是最常见的兜底方案 note = str(row['备注']) for cat, keywords in CATEGORY_KEYWORDS.items(): for kw in keywords: if kw in note: return cat return '其他' df['方向'] = df.apply(map_category, axis=1)

这段代码的要点是apply(map_category, axis=1)逐行判断,开销可以接受,因为个人记账数据一年也就几千行。关键词顺序会影响结果:比如“电影院”同时命中“购物”?不会,因为影院我只放在娱乐里,而“超市”只在购物里。但像“盒马”这种既有餐饮又有购物的商户,我建议单独处理,否则就会变成“餐饮”和“购物”边界模糊。这种边界情况不用追求完美,只要保证同一个连锁品牌都落到同一个方向,结论就可比。

提高准确性还有一个办法:如果账单自带二级分类(如“餐饮美食-火锅”),可以用正则提取二级分类的首个词,再二级映射到大类。但个人记账源码示例里,维护关键词映射表是最容易理解和修改的方式。

3. 用pandas算消费方向:聚合、透视与环比

3.1 月度支出趋势:按自然月重采样

有了干净数据,第一个要算的通常是“每个月总共花了多少钱”。这不仅告诉你支出是涨是跌,也是后续所有占比分析的分母。需要注意:记账数据里的金额支出是负数,收入是正数,如果直接用df['金额'].sum(),会把工资和退款一起算进去,那看的就是“结余”而不是“消费方向”。所以我先把支出过滤出来,再对金额取绝对值。

# 支出是负值,先取绝对值,方便后续画图与求和 expense = df[(df['金额'] < 0) & (~df['is_refund'])].copy() expense['支出'] = expense['金额'].abs() # 以日期为索引,按自然月重采样求和 monthly_total = ( expense.set_index('日期')['支出'] .resample('ME') .sum() .fillna(0) )

resample是pandas里时间重采样最常用的方法。ME表示月末频率,pandas 2.0以上版本推荐用'ME',更早的版本要写'M',否则会收到警告。如果你有几个月没有消费记录,重采样会得到0,这对画图是好事,折线图不会断掉。如果希望看到的是“自然周”而不是“自然月”,把'ME'换成'W'即可,但我个人不太建议按周,因为房租、账单这类固定支出会周期性抖动。

3.2 消费方向占比:用透视表做多维聚合

光看总支出还不够,重点是搞清楚钱花在了哪个方向。我用两种方式做聚合:一种是把整个时间范围内的支出按“方向”求和,看总盘子里的占比;另一种是通过pivot_table生成“月份 × 方向”的矩阵,这样既能看方向变化,也能看占比演变。

# 按消费方向汇总 cat_total = expense.groupby('方向')['支出'].sum().sort_values(ascending=False) cat_ratio = (cat_total / cat_total.sum() * 100).round(1) # 用透视表生成“月份 × 方向”的矩阵,行是月份,列是方向 monthly_cat = expense.pivot_table( values='支出', index=expense['日期'].dt.to_period('M'), columns='方向', aggfunc='sum', fill_value=0 ) # 把当月各方向金额转成百分比,方便以后比较 monthly_cat_pct = monthly_cat.div(monthly_cat.sum(axis=1), axis=0) * 100

这个pivot_table有四个参数容易写错。第一个values必须是数值列;index我用了to_period('M')把日期降成月周期,避免每个月出现日期维度;columns填方向;最关键的aggfunc,默认是mean,如果你不写,出来的结果是每个月每个方向的平均支出而不是总支出,那就完全偏离目标了。所以aggfunc='sum'必须显式写出来。fill_value=0是为了让没有交易的月份显示0而不是NaN,后面做div时不会到处冒警告。

拿到monthly_cat_pct后,消费方向已经很直观:比如3月餐饮占比45%,4月餐饮占比52%,5月降到38%。要总结个人消费方向,我会再按总金额排序,取前三个类别作为“主力消费方向”,另一个指标是看各类别占比是否连续三个月上升或下降。

3.3 环比变化:找出消费方向里的“异常移动”

前两步告诉了你“哪些方向花得多”,但没告诉你“哪个方向变化最快”。比如某个月“购物”占比突然从20%跳到35%,但总额没怎么涨,说明这个人在那个月买了很多大件。要识别这种变化,我会在源码里加一个diff()计算占比环比变化:

# 计算每个方向占比相对于上个月的增减 change = monthly_cat_pct.diff().tail(3) # 找出变化率绝对值最大的方向,标记为“需要关注的消费方向” notice = change.abs().max(axis=1).idxmax() print(f"最近三个月变化最明显的方向是:{notice}")

这段代码的思路是:用diff()拿每个月所有方向占比的差分,取最后三个月;再按行取绝对值最大值的索引,得到变化最剧烈的方向。pct_change()其实也能用,但它算的是相对变化率,如果上月占比接近0,变化率会爆炸到几百甚至几千,反而不适合占比分析。我的经验是,占比变化看diff,金额变化看pct_change,两者结合起来才有完整判断。

这里还要提醒一点:monthly_cat_pct是按方向占比算的,如果某个月有其他类别金额为0,diff()会把0和正常值比较,这反而会影响判断。所以我通常会先剔除掉占比持续低于5%的方向,再求环比,避免“其他”这种兜底类别干扰视线。

4. 可视化分析:把统计数据画成看得懂的图

4.1 用matplotlib生成消费方向饼图和月度趋势折线图

统计数字算完之后,最终落到图上才能和人沟通。源码示例里最常用的是matplotlib的subplots,一张饼图展示总消费方向结构,一张折线图展示月度走势。画图前必须处理中文字体,否则图上的类别名会变成一坨方块,这是新手最容易踩的坑。

import matplotlib.pyplot as plt # 设置中文字体,Windows用SimHei,macOS用PingFang SC,Linux用Noto Sans CJK plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'Noto Sans CJK SC'] plt.rcParams['axes.unicode_minus'] = False fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 饼图展示消费方向占比,保留一位小数 axes[0].pie( cat_ratio.values, labels=cat_ratio.index, autopct='%.1f%%', startangle=90, counterclock=False ) axes[0].set_title('消费方向占比') # 折线图展示月度总支出,带圆点标记 axes[1].plot( monthly_total.index.astype(str), monthly_total.values, marker='o', linewidth=2 ) axes[1].set_title('月度总支出走势') axes[1].tick_params(axis='x', rotation=45) plt.tight_layout() plt.savefig('consumption_analysis.png', dpi=200)

饼图的autopct='%.1f%%'控制百分比标签格式,counterclock=False让饼图按逆时针排列,否则各个扇区顺序会和你印象中的排序颠倒。折线图的x轴我强制转成字符串,这样每个月都显示,不会因为时间索引而压缩间距。savefig的dpi=200是为了放大到屏幕上还清晰,实际输出到PPT可以用300。

4.2 用pyecharts做交互式HTML看板

如果你想在手机或浏览器上查看,或者需要给朋友/同事发一个不需要Python环境就能打开的分析报告,我会在源码示例里换用pyecharts生成HTML文件。pyecharts 2.x的API和0.5.x差别很大,这里以2.x为例:

from pyecharts.charts import Pie, Bar from pyecharts import options as opts # 饼图:展示近一年消费方向 pie = ( Pie() .add( series_name="消费方向", data_pair=[list(x) for x in zip(cat_ratio.index, cat_ratio.values)], radius=["30%", "75%"] ) .set_global_opts(title_opts=opts.TitleOpts(title="个人消费方向汇总")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}%")) ) # 柱状图:展示月度总支出 bar = ( Bar() .add_xaxis(monthly_total.index.astype(str).tolist()) .add_yaxis("总支出", monthly_total.values.tolist()) .set_global_opts(title_opts=opts.TitleOpts(title="月度总支出")) ) pie.render("消费方向.html") bar.render("月度趋势.html")

data_pair需要的是一个列表,每个元素是(类别, 数值)元组,所以用zip转换。radius=["30%", "75%"]是环形饼图常用设置,让中间留白方便看底图。formatter="{b}: {c}%"里的{b}是类别名,{c}是数值,这里因为我们已经把cat_ratio存成百分数,所以直接显示。如果你不想生成两个HTML,也可以把Pie和Bar放到一个Grid里,但源码示例里分开更简单,方便单独分享。

4.3 图表选型:饼图不是万能的,堆叠面积图更适合看变化

饼图直观,但有一个明显缺陷:当方向超过五个时,小扇区很难辨认,尤其是“交通”“居住”这类占比相似的时候。我的做法是,总消费结构用饼图,但时间维度上的变化改用堆叠面积图或堆叠柱状图。堆叠面积图能同时看到每个月各类别的占比变化和总支出水位,比单独看饼图更接近“消费方向演变”这个主题。

图表类型适合看的结论缺点
饼图全年各类别占比大小只展示静态结果,难以对比月份
普通柱状图月度或类别间的金额大小比较丢失内部结构
堆叠面积图/柱状图各类别占比随时间的变化类别过多时有堆积交叉,需合理配色
折线图单一指标的趋势(如总支出、餐饮类金额)不包含占比结构信息

在源码示例里,我实际会同时输出三张图:饼图用于总结主导方向,折线图用于看月度总支出趋势,堆叠柱状图用于看每个方向的金额变化。堆叠图在pandas中可以用monthly_cat.div(monthly_cat.sum(axis=1), axis=0)得到占比后在plot.bar(stacked=True)直接画,但要注意把上节生成的monthly_cat_pct传入,避免用绝对金额。颜色方面,不要给每个类别都上高饱和度颜色,否则堆积后视觉上很混乱。

5. 常见问题与排查:跑源码示例时最容易踩的5个坑

5.1 中文乱码和图片里的方块字

现象:matplotlib画出的饼图、柱状图里,所有中文标签变成一个个方框,英文和大数字正常。

原因:matplotlib默认字体是DejaVu Sans,不支持中文字形。用plt.rcParams['font.sans-serif']设置了字体但系统里没装对应字体,或者设置的字体顺序不对,也会失效。

解决:先确认你系统里有什么中文字体。Windows一般有SimHei或Microsoft YaHei,macOS用PingFang SC,Linux要装Noto Sans CJK并刷新字体缓存。然后在代码里用font_manager直接指定字体文件路径,效果更稳:

import matplotlib.pyplot as plt from matplotlib import font_manager # Windows常见路径:C:/Windows/Fonts/simhei.ttf font_path = "C:/Windows/Fonts/simhei.ttf" font_manager.fontManager.addfont(font_path) plt.rcParams['font.family'] = font_manager.FontProperties(fname=font_path).get_name()

用addfont添加后,font.family会直接识别出新字体,避免依赖系统字体列表。这个坑在远程Linux服务器跑源码时尤其明显,别只在本地跑通就以为没问题。

5.2 pandas版本不同导致resample频率报错

现象:代码里写resample('M')运行正常,但收到FutureWarning: 'M' is deprecated and will be removed in a future version.;或者在pandas 2.2上写'M'直接报错。

原因:pandas 2.0以后,M代表的是“Month end”,被统一改成'ME',但一部分旧接口还能兼容。

解决:统一用'ME'和'MS'。另外,如果你在源码示例里会拿到不同人的电脑上跑,建议在读取数据后打印pd.__version__,并在注释里写明版本要求。具体到代码,resample('ME')在pandas 1.x也可能报错,所以我的源码里会加一个兼容分支:

try: monthly_total = expense.set_index('日期')['支出'].resample('ME').sum() except ValueError: monthly_total = expense.set_index('日期')['支出'].resample('M').sum()

这个try/except看起来笨但是实用,毕竟源码zip是发给不同环境用的。

5.3 金额列是字符串,包含“¥”和“,”导致求和报错

现象:expense['支出'].sum()报TypeError: unsupported operand type(s) for +: 'float' and 'str',或者求和结果明显不对。

原因:导出的账单里,金额列经常是文本类型,包含人民币符号、千分位逗号,比如1,234.50或¥12.30。直接astype(float)会把1,234.50解析失败。

解决:在clean_bill阶段强制清洗金额,用str.replace去掉所有逗号和货币符号,再做类型转换。如果你的账单里金额是1,234.50且逗号是千分位,可以用.str.replace(',', '');如果是英文逗号作为小数点,那就要反过来处理,先看地区再决定。我的源码里保留了一个CURRENCY_SYMBOL变量,方便改。

5.4 解压zip后ImportError: No module named 'pyecharts'

现象:运行到from pyecharts.charts import Pie时直接报错;或者运行pip install pyecharts后报错说只找到了pyecharts 0.5.x,API对不上。

原因:源码示例里pyecharts代码是按照2.x语法写的,但环境里没安装,或者安装了旧版0.5.x。pyecharts 0.5和2.x的导入路径完全不同,0.5是from pyecharts import Pie,2.x是from pyecharts.charts import Pie。

解决:先指定版本安装:

pip install "pyecharts>=2.0"

然后检查版本:python -c "import pyecharts; print(pyecharts.__version__)"。如果版本低于2.0,卸载重装。另一种情况是pip install安装到了全局环境,但你的Python项目里用了虚拟环境,需要先激活虚拟环境再装。这类依赖问题在源码zip里很常见,我的做法是随包带一个requirements.txt,至少包含pandas、matplotlib、pyecharts三行,并在说明里写清安装命令。

5.5 月度总支出虚高,原因是没过滤退款和资金互转

现象:算出来的月度总支出比支付宝“月账单”里的支出高出一两百元,甚至更多;饼图里“其他”占比突然变大。

原因:账单里包含“退款”“交易关闭”等记录,对应金额是负的但小于原交易;另外“余额宝提现到银行卡”“信用卡还款”这类资金互转也被算进支出,它们本不该出现在消费方向里。

解决:洗数据阶段加两个过滤:备注里含“退款”的记录单独标记并在聚合时排除;分类属于“资金互转”“信用卡还款”“理财”的整行过滤掉。如果退款金额需要冲减当月支出,不要直接加支出,而是建立一个退款表先按原交易去重,再在月度堆叠图上体现。代码上,我在expense = df[(df['金额'] < 0) & (~df['is_refund'])]这一行里直接排除了退款,但保留is_refund标记让你可以单独统计退款对账。

6. 进阶技巧:用双账本对比和小样本校验验证整个分析结论

消费方向分析跑通之后,很多人会担心一个问题:这份结论可信吗?我最常用的验证方法是“双账本对比”。找一个你信任的外部记录,比如支付宝自带的年度账单、微信支付月账单,或者你自己手工记账的App,把我们的聚合结果和它做逐月对比。

# 随机抽取2%的原始明细,人工核对类别映射是否正确 sample = expense[['日期', '备注', '方向']].sample(frac=0.02, random_state=42) print(sample.to_string()) # 和外部账本对比的偏差计算 external_bill = {"2025-01": 3456.0, "2025-02": 3890.5} # 来自支付宝月账单 actual = expense.set_index('日期')['支出'].resample('ME').sum().astype(float) for month, ext_amount in external_bill.items(): diff = actual.get(month, 0) - ext_amount print(f"{month} 偏差: {diff:.2f}")

只要偏差率控制在2%以内,说明方向聚合基本可信。如果超过,优先检查三个方面:退款是否漏过滤、资金互转是否混入、关键词映射是否把某个商户分错类。我习惯每月跑一次之后,把sample打印结果直接存档,这也是给自己留一份“后悔药”。

再往后可以加一个简单的小技巧:对关键词映射表做config化,把它单独放到category_map.json里,每次有新商户出现时不用改主程序,只改JSON就行。这个源码示例的下一步扩展方向就是它,从固定规则到可配置,再到用聚类自动分组,是个人记账数据分析最常见的演进路线。

我自己现在每月10号固定跑一遍这套流程,输出三张图留着,月底再和主账户余额对一下。只要偏差在50元以内,我就信任这个消费方向结论;超过,就回头查退款和漏记。希望我的这些经验,能帮你在自己的记账数据上少走几步弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询