# Python实现个人支出统计与可视化工具:从记录到图表的全流程


背景介绍

日常支出管理是理财的基础,但手动记录和统计既繁琐又容易出错。开发一个轻量化的支出统计工具,不仅能帮助我们高效管理支出,还能通过实践巩固文件操作、数据统计、可视化的编程技能。本文将带你从需求分析到代码实现,打造一个支持「记录支出、统计报表、可视化图表」的个人支出管理工具。

思路分析

该工具的核心需求可拆解为三个模块:
1. 支出记录:通过命令行收集「日期、类别、金额、备注」,持久化到CSV文件(简单易读,适合本地存储)。
2. 统计报表:按「类别」或「时间段」分组,计算支出总额和笔数,控制台输出直观结果。
3. 可视化图表:用Matplotlib生成「类别占比饼图」或「月度支出柱状图」,将数据转化为更直观的图形。

技术选型上,Python的csv库处理文件,datetime处理日期,matplotlib实现可视化,命令行交互通过input完成,无需复杂框架,适合新手实践。

代码实现

下面是完整的代码实现,包含详细注释:

import csv
import os
from datetime import datetime
import matplotlib.pyplot as plt

# 支出数据文件路径(本地CSV)
EXPENSES_FILE = 'expenses.csv'

def add_expense():
    """添加支出记录到CSV文件"""
    # 输入日期并验证格式
    date = input("日期(格式:YYYY-MM-DD):")
    try:
        datetime.strptime(date, '%Y-%m-%d')
    except ValueError:
        print("日期格式错误,请使用YYYY-MM-DD格式(例如:2023-10-01)!")
        return

    # 输入类别并验证(限制为餐饮/交通/购物/娱乐)
    category = input("支出类别(餐饮/交通/购物/娱乐):")
    valid_categories = ['餐饮', '交通', '购物', '娱乐']
    if category not in valid_categories:
        print(f"支出类别必须为{valid_categories}之一!")
        return

    # 输入金额并验证为数字
    try:
        amount = float(input("金额(数字,例如:10.5):"))
    except ValueError:
        print("金额必须是有效的数字!")
        return

    # 输入备注(可选,直接回车则为空)
    remark = input("备注(可选,直接回车则为空):") or ""

    # 写入CSV文件(追加模式,自动创建表头)
    fieldnames = ['日期', '类别', '金额', '备注']
    file_exists = os.path.exists(EXPENSES_FILE)
    with open(EXPENSES_FILE, 'a', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        if not file_exists:
            writer.writeheader()  # 文件不存在时写入表头
        writer.writerow({
            '日期': date,
            '类别': category,
            '金额': amount,
            '备注': remark
        })
    print(f"支出记录已成功添加到 {EXPENSES_FILE} 文件中。")

def report_expense():
    """生成支出统计报表(控制台输出+可视化图表)"""
    report_type = input("请选择报表类型(category:按类别统计 / time:按时间统计):")
    start_date_str = input("请输入统计起始日期(YYYY-MM-DD):")
    end_date_str = input("请输入统计结束日期(YYYY-MM-DD):")

    # 验证日期格式
    try:
        start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
        end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
    except ValueError:
        print("日期格式错误,请使用YYYY-MM-DD格式!")
        return

    # 读取支出数据(筛选日期范围内的记录)
    expenses = []
    if os.path.exists(EXPENSES_FILE):
        with open(EXPENSES_FILE, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                row_date = datetime.strptime(row['日期'], '%Y-%m-%d')
                if start_date <= row_date <= end_date:
                    expenses.append({
                        '日期': row_date,
                        '类别': row['类别'],
                        '金额': float(row['金额']),
                        '备注': row['备注']
                    })
    else:
        print(f"错误:{EXPENSES_FILE} 文件不存在,无支出记录!")
        return

    if not expenses:
        print("该时间段内无支出记录!")
        return

    if report_type == 'category':
        # 按类别统计:金额总和、笔数
        category_stats = {}
        total_amount = 0.0
        total_count = 0
        for exp in expenses:
            category = exp['类别']
            amount = exp['金额']
            if category not in category_stats:
                category_stats[category] = {'amount': 0.0, 'count': 0}
            category_stats[category]['amount'] += amount
            category_stats[category]['count'] += 1
            total_amount += amount
            total_count += 1

        # 输出统计报表
        print(f"\n===== 支出类别统计({start_date_str} ~ {end_date_str}) ======")
        for cat, stats in category_stats.items():
            print(f"{cat}:{stats['amount']:.2f}元(共{stats['count']}笔)")
        print(f"总计:{total_amount:.2f}元(共{total_count}笔)")

        # 生成类别占比饼图
        categories = list(category_stats.keys())
        amounts = [stats['amount'] for stats in category_stats.values()]
        generate_pie_chart(categories, amounts, start_date_str, end_date_str, 'category')

    elif report_type == 'time':
        # 按月份统计(年月格式:YYYY-MM)
        month_stats = {}
        for exp in expenses:
            month_key = exp['日期'].strftime('%Y-%m')  # 提取年月作为分组键
            if month_key not in month_stats:
                month_stats[month_key] = {'amount': 0.0, 'count': 0}
            month_stats[month_key]['amount'] += exp['金额']
            month_stats[month_key]['count'] += 1

        # 输出统计报表(按月份排序)
        print(f"\n===== 支出时间统计({start_date_str} ~ {end_date_str}) ======")
        total_amount = 0.0
        total_count = 0
        for month, stats in sorted(month_stats.items()):
            print(f"{month}:{stats['amount']:.2f}元(共{stats['count']}笔)")
            total_amount += stats['amount']
            total_count += stats['count']
        print(f"总计:{total_amount:.2f}元(共{total_count}笔)")

    else:
        print("报表类型错误,可选:category(按类别) / time(按时间)")

def generate_pie_chart(categories, amounts, start_date, end_date, chart_type):
    """生成饼图(类别占比或其他类型)"""
    plt.figure(figsize=(8, 6))
    plt.pie(
        amounts, 
        labels=categories, 
        autopct='%1.1f%%',  # 显示百分比(保留1位小数)
        startangle=140,     # 饼图起始角度(避免标签重叠)
        textprops={'fontsize': 10}
    )
    plt.axis('equal')  # 保证饼图为正圆形
    title = f'支出{chart_type}占比({start_date} ~ {end_date})'
    plt.title(title, fontsize=12)
    plt.tight_layout()  # 调整布局,避免标签被截断
    # 生成带时间戳的文件名(避免重复)
    filename = f"{chart_type}_pie_{start_date.replace('-','')}_{end_date.replace('-','')}.png"
    plt.savefig(filename)
    print(f"图表已生成:{filename}")
    plt.close()  # 关闭图表,释放内存

def generate_bar_chart(months, amounts, start_date, end_date):
    """生成月度支出柱状图"""
    plt.figure(figsize=(10, 6))
    x = range(len(months))
    plt.bar(x, amounts, width=0.6, color='#1f77b4')
    # 设置x轴标签(月份),旋转45度避免重叠
    plt.xticks(x, months, rotation=45, ha='right', fontsize=9)
    plt.xlabel('月份', fontsize=10)
    plt.ylabel('支出金额(元)', fontsize=10)
    title = f'月度支出统计({start_date} ~ {end_date})'
    plt.title(title, fontsize=12)
    plt.tight_layout()
    # 生成带时间戳的文件名
    filename = f"monthly_bar_{start_date.replace('-','')}_{end_date.replace('-','')}.png"
    plt.savefig(filename)
    print(f"图表已生成:{filename}")
    plt.close()

def generate_chart():
    """生成可视化图表(月度柱状图或类别饼图)"""
    chart_type = input("请选择图表类型(monthly:月度柱状图 / category:类别饼图):")
    start_date_str = input("请输入统计起始日期(YYYY-MM-DD):")
    end_date_str = input("请输入统计结束日期(YYYY-MM-DD):")

    # 验证日期格式
    try:
        start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
        end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
    except ValueError:
        print("日期格式错误,请使用YYYY-MM-DD格式!")
        return

    # 读取支出数据
    expenses = []
    if os.path.exists(EXPENSES_FILE):
        with open(EXPENSES_FILE, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                row_date = datetime.strptime(row['日期'], '%Y-%m-%d')
                if start_date <= row_date <= end_date:
                    expenses.append({
                        '日期': row_date,
                        '类别': row['类别'],
                        '金额': float(row['金额']),
                        '备注': row['备注']
                    })
    else:
        print(f"错误:{EXPENSES_FILE} 文件不存在,无支出记录!")
        return

    if not expenses:
        print("该时间段内无支出记录!")
        return

    if chart_type == 'monthly':
        # 按月统计金额
        month_stats = {}
        for exp in expenses:
            month_key = exp['日期'].strftime('%Y-%m')
            if month_key not in month_stats:
                month_stats[month_key] = 0.0
            month_stats[month_key] += exp['金额']
        # 按月份排序
        months = sorted(month_stats.keys())
        amounts = [month_stats[month] for month in months]
        generate_bar_chart(months, amounts, start_date_str, end_date_str)

    elif chart_type == 'category':
        # 按类别统计金额
        category_stats = {}
        for exp in expenses:
            category = exp['类别']
            if category not in category_stats:
                category_stats[category] = 0.0
            category_stats[category] += exp['金额']
        categories = list(category_stats.keys())
        amounts = list(category_stats.values())
        generate_pie_chart(categories, amounts, start_date_str, end_date_str, 'category')

    else:
        print("图表类型错误,可选:monthly(月度柱状图) / category(类别饼图)")

def main():
    """主函数:命令行交互入口"""
    print("欢迎使用个人支出统计与可视化工具!")
    while True:
        choice = input("\n请选择操作(add:添加记录 / report:统计报表 / chart:生成图表 / exit:退出):")
        if choice == 'add':
            add_expense()
        elif choice == 'report':
            report_expense()
        elif choice == 'chart':
            generate_chart()
        elif choice == 'exit':
            print("感谢使用,祝您支出合理,生活愉快!")
            break
        else:
            print("无效操作,请重新选择!")

if __name__ == "__main__":
    main()

关键逻辑解析

  1. 文件持久化:使用csv.DictWriter以追加模式写入数据,自动处理表头(文件不存在时创建),确保数据格式统一。
  2. 数据统计
    • 按类别统计时,用字典category_stats累加金额和笔数,时间复杂度O(n)。
    • 按时间统计时,提取「年月」作为分组键(如2023-10),确保数据按月份有序输出。
  3. 可视化细节
    • 饼图通过autopct='%1.1f%%'显示百分比,startangle调整起始角度避免标签重叠。
    • 柱状图通过rotation=45旋转x轴标签,tight_layout()避免图表元素被截断。

总结

本工具通过「文件操作+数据统计+可视化」的组合,实现了从支出记录到图表分析的全流程。核心难点在于日期格式验证(确保输入合法性)、数据分组统计(字典高效累加)和Matplotlib图表美化(布局、标签优化)。

通过这个项目,你不仅能掌握Python基础库的使用,还能理解「数据采集→处理→可视化」的完整链路。若需扩展,可增加「支出预算预警」「多用户支持」等功能,进一步提升工具实用性。

赶快运行代码,开始记录你的支出,看看钱都花去哪儿了吧!