在个人理财场景中,清晰的收支统计与可视化分析能帮助我们更好地掌握消费习惯、优化支出结构。本文将介绍如何使用Python开发一款个人收支统计与可视化工具,整合CSV文件操作、数据统计、GUI交互与Matplotlib可视化,帮助中级开发者系统学习多领域技术。
功能拆解与技术思路
核心功能模块
- 数据管理:支持从CSV导入历史记录、手动录入单条收支(日期、类型、分类、金额)。
- 统计分析:按时间范围(周/月)筛选数据,统计总收入/支出、分类金额及占比。
- 可视化展示:用柱状图展示各分类收支金额,用饼图展示收入/支出的类别占比。
- 数据导出:将统计结果导出为CSV,将可视化图表导出为图片。
- GUI交互:通过Tkinter构建界面,实现按钮点击、文件选择等交互逻辑。
技术选型与学习点
- 文件操作:
csv模块处理CSV的导入/导出,需注意编码、数据格式转换。 - 数据处理:用字典/列表存储交易记录,按时间/类型/分类筛选、聚合,涉及日期解析(
datetime)、占比计算。 - 可视化:
Matplotlib绘制图表,并通过FigureCanvasTkAgg嵌入Tkinter界面。 - GUI设计:
Tkinter的布局管理(pack/grid/place)、组件事件绑定(按钮点击、输入验证)。
代码实现:从模块到完整应用
下面通过面向对象的方式实现工具,将逻辑封装在PersonalFinanceApp类中(继承自tk.Tk)。
1. 初始化与界面搭建
import csv
import tkinter as tk
from tkinter import filedialog, messagebox
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from datetime import datetime, timedelta
class PersonalFinanceApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("个人收支统计与可视化工具")
self.geometry("1000x800")
self.transactions = [] # 存储所有收支记录(字典列表)
self.current_stats = None # 存储统计结果
self.current_fig = None # 存储当前可视化图表
self.create_widgets() # 初始化界面组件
def create_widgets(self):
# 数据操作区:导入CSV + 手动录入
data_frame = tk.Frame(self)
data_frame.pack(fill='x', padx=10, pady=5)
tk.Button(data_frame, text="导入CSV文件", command=self.import_csv).pack(side='left', padx=5)
# 手动录入输入框
tk.Label(data_frame, text="日期:").pack(side='left', padx=5)
self.date_entry = tk.Entry(data_frame, width=12)
self.date_entry.pack(side='left', padx=5)
tk.Label(data_frame, text="类型:").pack(side='left', padx=5)
self.type_var = tk.StringVar(value='支出')
tk.OptionMenu(data_frame, self.type_var, '支出', '收入').pack(side='left', padx=5)
tk.Label(data_frame, text="分类:").pack(side='left', padx=5)
self.category_entry = tk.Entry(data_frame, width=10)
self.category_entry.pack(side='left', padx=5)
tk.Label(data_frame, text="金额:").pack(side='left', padx=5)
self.amount_entry = tk.Entry(data_frame, width=10)
self.amount_entry.pack(side='left', padx=5)
tk.Button(data_frame, text="手动录入", command=self.add_manual).pack(side='left', padx=5)
# 统计设置区:时间范围 + 统计按钮
stats_frame = tk.Frame(self)
stats_frame.pack(fill='x', padx=10, pady=5)
tk.Label(stats_frame, text="统计范围:").pack(side='left', padx=5)
self.time_var = tk.StringVar(value='周')
tk.OptionMenu(stats_frame, self.time_var, '周', '月').pack(side='left', padx=5)
tk.Button(stats_frame, text="统计分析", command=self.statistics).pack(side='left', padx=5)
# 统计结果显示区:文本 + 图表
result_frame = tk.Frame(self)
result_frame.pack(fill='both', expand=True, padx=10, pady=5)
self.text_area = tk.Text(result_frame, height=10, width=80)
self.text_area.pack(side='left', fill='both', expand=True, padx=5)
self.chart_frame = tk.Frame(result_frame)
self.chart_frame.pack(side='right', fill='both', expand=True, padx=5)
# 导出区:CSV + 图片
export_frame = tk.Frame(self)
export_frame.pack(fill='x', padx=10, pady=5)
tk.Button(export_frame, text="导出统计CSV", command=self.export_csv).pack(side='left', padx=5)
tk.Button(export_frame, text="导出图表图片", command=self.export_image).pack(side='left', padx=5)
2. 数据操作:CSV导入与手动录入
def import_csv(self):
"""从CSV导入收支记录,自动转换金额为float"""
file_path = filedialog.askopenfilename(filetypes=[("CSV Files", "*.csv")])
if not file_path:
return
try:
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
row['金额'] = float(row['金额']) # 金额转浮点
self.transactions.append(row)
messagebox.showinfo("成功", f"导入{len(self.transactions)}条记录")
except Exception as e:
messagebox.showerror("错误", f"导入失败:{str(e)}")
def add_manual(self):
"""手动录入单条收支记录,验证输入格式"""
date = self.date_entry.get().strip()
typ = self.type_var.get()
category = self.category_entry.get().strip()
amount = self.amount_entry.get().strip()
try:
datetime.strptime(date, '%Y-%m-%d') # 验证日期格式
amount = float(amount) # 验证金额为数字
if not category:
raise ValueError("分类不能为空")
# 添加记录
self.transactions.append({
'日期': date, '类型': typ, '分类': category, '金额': amount
})
self.date_entry.delete(0, 'end')
self.category_entry.delete(0, 'end')
self.amount_entry.delete(0, 'end')
messagebox.showinfo("成功", "记录添加成功")
except ValueError as e:
messagebox.showerror("输入错误", f"请检查输入:{str(e)}")
except Exception as e:
messagebox.showerror("错误", f"添加失败:{str(e)}")
3. 统计分析:时间筛选与分类聚合
def statistics(self):
"""按时间范围统计收支,计算分类金额与占比"""
time_range = self.time_var.get()
end_date = datetime.now()
# 计算时间范围(周:近7天,月:近30天)
start_date = end_date - timedelta(days=7) if time_range == '周' else end_date - timedelta(days=30)
start_str, end_str = start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')
# 筛选时间范围内的记录
filtered = [t for t in self.transactions
if start_str <= t['日期'] <= end_str]
if not filtered:
messagebox.showinfo("提示", "该时间范围内无记录")
return
# 分类统计收入/支出
income, expense = {}, {}
total_income, total_expense = 0, 0
for t in filtered:
typ, cat, amt = t['类型'], t['分类'], t['金额']
if typ == '收入':
income[cat] = income.get(cat, 0) + amt
total_income += amt
else:
expense[cat] = expense.get(cat, 0) + amt
total_expense += amt
# 计算占比(避免除以0)
income_ratio = {k: (v/total_income*100) for k, v in income.items()} if total_income else {}
expense_ratio = {k: (v/total_expense*100) for k, v in expense.items()} if total_expense else {}
# 保存统计结果
self.current_stats = {
'time_range': f"{start_str}至{end_str}",
'total_income': total_income,
'income_categories': income,
'income_ratios': income_ratio,
'total_expense': total_expense,
'expense_categories': expense,
'expense_ratios': expense_ratio
}
# 显示统计文本
self._show_stats_text()
# 生成可视化图表
self.visualize()
def _show_stats_text(self):
"""在Text组件中显示统计结果文本"""
stats = self.current_stats
self.text_area.delete(1.0, 'end')
text = f"时间范围:{stats['time_range']}\n"
text += f"总收入:{stats['total_income']}(" + " + ".join([f"{k}{v}" for k, v in stats['income_categories'].items()]) + ")\n"
text += f"总支出:{stats['total_expense']}(" + " + ".join([f"{k}{v}" for k, v in stats['expense_categories'].items()]) + ")\n"
text += "支出分类占比:" + "、".join([f"{k}{v:.1f}%({stats['expense_categories'][k]}/{stats['total_expense']})" for k, v in stats['expense_ratios'].items()]) + "\n"
text += "收入分类占比:" + "、".join([f"{k}{v:.1f}%({stats['income_categories'][k]}/{stats['total_income']})" for k, v in stats['income_ratios'].items()])
self.text_area.insert('end', text)
4. 可视化:Matplotlib与Tkinter的集成
def visualize(self):
"""生成柱状图(分类金额)+ 饼图(收入/支出占比),嵌入Tkinter"""
stats = self.current_stats
# 清空图表区域
for widget in self.chart_frame.winfo_children():
widget.destroy()
fig = Figure(figsize=(8, 6), dpi=100)
# 子图1:柱状图(所有分类)
ax1 = fig.add_subplot(2, 2, (1, 2))
income_cats = list(stats['income_categories'].keys())
expense_cats = list(stats['expense_categories'].keys())
all_cats = income_cats + expense_cats
all_amounts = list(stats['income_categories'].values()) + list(stats['expense_categories'].values())
colors = ['green']*len(income_cats) + ['red']*len(expense_cats) # 收入绿,支出红
ax1.bar(all_cats, all_amounts, color=colors)
ax1.set_title('各分类收支金额')
ax1.set_ylabel('金额(元)')
ax1.tick_params(axis='x', rotation=45) # X轴标签旋转,避免重叠
# 子图2:收入饼图
ax2 = fig.add_subplot(2, 2, 3)
if stats['total_income'] > 0:
labels = income_cats
sizes = list(stats['income_categories'].values())
ax2.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90)
ax2.set_title('收入分类占比')
# 子图3:支出饼图
ax3 = fig.add_subplot(2, 2, 4)
if stats['total_expense'] > 0:
labels = expense_cats
sizes = list(stats['expense_categories'].values())
ax3.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90)
ax3.set_title('支出分类占比')
fig.tight_layout() # 自动调整子图间距
# 嵌入Tkinter
canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill='both', expand=True)
self.current_fig = fig # 保存图表,用于导出
5. 数据导出:CSV与图片
def export_csv(self):
"""将统计结果导出为CSV,格式与示例一致"""
if not hasattr(self, 'current_stats'):
messagebox.showinfo("提示", "请先进行统计分析")
return
stats = self.current_stats
file_path = filedialog.asksaveasfilename(defaultextension='.csv', filetypes=[("CSV Files", "*.csv")])
if not file_path:
return
try:
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['统计类型', '时间范围', '金额', '分类', '占比'])
# 总收入(总计 + 分类)
writer.writerow(['总收入', stats['time_range'], stats['total_income'], '总计', '100.0%'])
for cat, amt in stats['income_categories'].items():
ratio = stats['income_ratios'][cat]
writer.writerow(['总收入', stats['time_range'], amt, cat, f"{ratio:.1f}%"])
# 总支出(总计 + 分类)
writer.writerow(['总支出', stats['time_range'], stats['total_expense'], '总计', '100.0%'])
for cat, amt in stats['expense_categories'].items():
ratio = stats['expense_ratios'][cat]
writer.writerow(['总支出', stats['time_range'], amt, cat, f"{ratio:.1f}%"])
messagebox.showinfo("成功", "CSV导出成功")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
def export_image(self):
"""将可视化图表导出为PNG图片"""
if not hasattr(self, 'current_fig'):
messagebox.showinfo("提示", "请先生成图表")
return
file_path = filedialog.asksaveasfilename(defaultextension='.png', filetypes=[("PNG Files", "*.png")])
if not file_path:
return
try:
self.current_fig.savefig(file_path)
messagebox.showinfo("成功", "图片导出成功")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
完整应用与运行
将上述代码整合后,运行以下入口代码:
if __name__ == "__main__":
app = PersonalFinanceApp()
app.mainloop()
启动程序后,可通过“导入CSV”或“手动录入”添加数据,选择时间范围后点击“统计分析”,即可在界面左侧查看文本统计结果,右侧查看可视化图表。最后通过“导出统计CSV”或“导出图表图片”保存结果。
总结与扩展
学习收获
- 文件操作:掌握
csv模块的字典读写、编码处理与异常捕获。 - 数据统计:学会按多维度(时间、类型、分类)筛选、聚合数据,计算占比。
- GUI交互:Tkinter的布局管理、组件事件绑定、输入验证逻辑。
- 可视化:Matplotlib图表的绘制、子图布局,以及与Tkinter的嵌入集成。
扩展方向
- 支持多用户数据隔离(按用户ID存储)。
- 增加收支预算功能,对比实际支出与预算。
- 优化可视化:添加交互(如悬停显示金额)、支持更多图表类型(如折线图展示趋势)。
通过本项目,开发者可系统提升文件操作、数据处理、GUI设计、可视化的综合能力,为个人或小型团队的财务管理提供实用工具。