前言

在之前写个人年度总结的时候,提到了自己最近半年养成了一个新的习惯——在 Notion 上进行进行各方面的记录,其中就包括了记录每日所做的事情以及账单,这一年做过的事情后面我也会进行数据分析,这篇文章我们来分析一下我过去这半年(从 6 月至今)的账单。

在这里再安利一下 Notion 这个软件,我以前用过很多个个人效率提升类的软件,但是只有 Notion 是我坚持用下来的,因为他赋予用户的权限实在是太高了,你可以随心所欲地定制你的 WorkSpace,像设计数据库一样定义外键,定义各种视图,在 Notion 里你几乎可以定义出自己想要的任何功能,只有你想得到,没有它做不到。而且在今年,Notion 官方公布了新的会员体系,免费会员也可以拥有无限数量的 block 了,毕竟内容是笔记软件的核心,当 block 数量没有限制并且承诺永久免费之后,个人用户才可以放心没后顾之忧地使用 Notion 来记录任何的资料。仅这一点就让 Notion 免费用户的实用性飙升了好几个档次。

我在 Notion 中定义了一个用于记录每天消费记录的表,通过这个表我可以每天进行记账,十分方便。

Bill界面

添加界面

同样的,Notion 也提供了十分便捷的导出功能,可以将整个表中的数据进行导出,这也是后续分析的基础。

预处理

首先读入 Notion 中导出来的账单数据

import pandas as pd
df = pd.read_csv('./Tasks.csv')
df

读进来的账单数据

可以看到从 6 月以来,我记录了 525 条消费记录,但是很明显可以发现其中有一些空值(NaN),所以在正式开始处理之前,我们需要对这些空值所在的行进行删除,可以使用 pandas 的dropna函数完成。其次,我们想分析的是 2020 年的数据,其中还有一些数据是 2021 年的消费记录,我们同样想去除掉这些记录。

# 去除空值
df = df.dropna(subset=['购买日期'])
# 筛选2020年的数据
timeline = '2021-1-1'
df = df[df['购买日期']<timeline]
df

处理后的数据

可以看到我们得到了我们想要分析的数据,接下来我们只需要对这些进行分析即可。

主要消费内容

Name 和备注两列数据描绘了消费内容的详细特征,我们可以对这两列的数据进行一下分析。首先提取一下这两列,看下出现次数最多的是什么,可以使用 pandas 的value_counts函数。

# 提取Name属性和备注属性
name_counts = pd.DataFrame(df['Name'].value_counts())
remark_counts = pd.DataFrame(df['备注'].value_counts())
# 导出为excel文件
name_counts.to_excel('name_counts.xlsx')
remark_counts.to_excel('remark_counts.xlsx')

使用name_counts.head(10)可以查看记录次数前 10 的数据。

name列出现次数前10

可以看到记录次数最多的是一日四餐(早餐、午餐、晚餐以及夜宵),前 10 全部被吃的包揽了,值得注意的是我有记录的早餐次数很少,甚至比夜宵次数还少,立个 flag,希望新的一年里能够多吃早餐,少吃夜宵。

同理,我们再使用remark_counts.head(10)在备注列中出现次数前 10 的数据。

备注列出现次数前10

在这部分出现次数前 10 的关键词同样都是和吃的相关的。我回忆了一下,食堂一楼和西区一楼应该是同一个地方,也就都是西区食堂一楼,返校的适合学校不给出去,所以吃得多的是西区一楼,现在已经比较少去了,因为感觉一楼的饭菜实在有点油腻,而且有点贵。另外几个也是没有意外,今年下半年吃的最多的确实就这几家店:商业街的杨国福、鸡公煲,还有几家炒粉摊。

下面进行进一步的分析,尝试进行可视化,首先对 Dataframe 结构进行一定的改写,将name_counts列名修改为如下格式,方便后续操作。

修改后的格式

尝试绘制出饼状图

plt.pie(name_counts['times'],labels=name_counts['name'],autopct='%1.1f%%',shadow=False,startangle=150)
plt.show()

绘制出的饼状图

我的天,我画出来的是一坨屎吧,下面要对这个图进行一些简单的优化。首先是从数据层面上,有绝大部分数据的标签都是只记录了一遍,所以可以将这些数据归类到“其他”类别里面,这样画出来的图不会挤到一起。

#读取数据
name_counts = pd.read_excel('./name_counts.xlsx')
# 记录只出现一次的记录
other_times = name_counts[name_counts['times']<5]
# 筛选大于5的记录
name_counts = name_counts[name_counts['times']>=5]
# 构造一个字典对象
append_dict = {
    'name':'其他',
    'times':len(other_times)
}
# 合并
name_counts = name_counts.append(append_dict,ignore_index=True)

# 重新绘制一遍
plt.pie(name_counts['times'],labels=name_counts['name'],autopct='%1.1f%%',shadow=False,startangle=150)
plt.show()

初步优化后的饼状图

感觉还是有点丑,再优化亿下下。

from matplotlib import font_manager as fm
from  matplotlib import cm

labels = name_counts['name']
sizes = name_counts['times']
explode = (0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01)  # "explode" ,设置突出显示

fig, axes = plt.subplots(figsize=(10,5),ncols=2) # 设置绘图区域大小
ax1, ax2 = axes.ravel()

colors = cm.Paired(np.arange(len(sizes))/len(sizes)) # colormaps: Paired, autumn, rainbow, gray,spring,Darks
patches, texts, autotexts = ax1.pie(sizes, labels=labels, autopct='%1.0f%%',explode=explode,
        shadow=False, startangle=170, colors=colors, labeldistance=1.2,pctdistance=1.03, radius=0.4)
# labeldistance: 控制labels显示的位置
# pctdistance: 控制百分比显示的位置
# radius: 控制切片突出的距离

ax1.axis('equal')  

# 重新设置字体大小
proptease = fm.FontProperties()
proptease.set_size('small')
# font size include: ‘xx-small’,x-small’,'small’,'medium’,‘large’,‘x-large’,‘xx-large’ or number, e.g. '12'
plt.setp(autotexts, fontproperties=proptease)
plt.setp(texts, fontproperties=proptease)

ax1.set_title('Hsinyan is really a foodie', loc='center')

# ax2 只显示图例(legend)
ax2.axis('off')
ax2.legend(patches, labels, loc='center left')

plt.tight_layout()
# plt.savefig("pie_shape_ufo.png", bbox_inches='tight')
plt.savefig('bill_counts.jpg')
plt.show()

优化后的饼状图

恩,这样子就舒服多了。

通过饼状图可以直观看出绝大部分的消费都是吃上面的,而且还看出来对于我来说,一日三餐应该是午餐、晚餐以及夜宵。我最喜欢的饮品是益禾堂的翠峰茉莉、烤奶以及蜜雪冰城的蜜桃四季春,希望下次大家请我喝的时候挑着这些来点(误)。

恩格尔系数

通过上面的数据分析可能带来一个错觉,就是我绝大部分的钱都花在吃的上面了,在我进行接下来的分析之前我也是这么觉得的,但是接下来的结果还是让我吓了一跳的,让我们看看接下来会发生些什么。

恩格尔系数法是国际上常用的一种测定贫困线的方法,是指居民家庭中食物支出占消费总支出的比重 ,它随家庭收入的增加而下降,即恩格尔系数越大就越贫困。我经常开玩笑说我的恩格尔系数 90%,我们来计算下我的恩格尔系数实际是多少。

# 提取餐饮部分
food = df[df['标签']=='餐饮']
food_cost = food['价格'].sum()
# 所有消费
all_cost = df['价格'].sum()
# 恩格尔系数
Engel_Coefficient = food_cost / all_cost
Engel_Coefficient
# 0.47478217254557287

通过计算,我的恩格尔系数为 0.47 左右,按照恩格尔系数的标准来看,属于刚好踏入小康标准,说实话这个令我有点惊讶,因为我觉得我绝大部分的钱都花在吃的上面了,但是是什么占据了我其他消费的大头呢,请往下看。

消费大头

首先分析下最近半年消费大头分别是什么

# 按照价格倒序排列
df_sorted_by_cost = df.sort_values(by='价格',ascending=False)
# 查看前10
df_sorted_by_cost.head(10)

年度消费前10

不看不知道,今年手机前后坏了三次,修手机就花了 800 块钱。此外,今年返校后可能因为工作量较大而且休息不够,再加上睡姿不太好等种种原因,左胸口一直不舒服,于是去医院检查,查出了肋骨软骨炎,虽然是一个不大不小的病,但还是花了 500 块钱,这应该也导致我当月花费是年度最高的。不得不说保持身体健康挺重要的,有一个健康的体魄胜过多赚那几千块钱。

各分类消费情况

我在 Notion 里,将消费划分为了餐饮、服务、人情、交通、购物、旅游以及医疗这七个种类,接下来看看各个分类下的消费情况分别是怎样的。

# 统计各个分类下消费情况
temp_dict = {}
for label in label_list:
    temp = df[df['标签']==label]
    cost = temp['价格'].sum()
    temp_dict[label] = cost

# 字典对象转为pd.DataFrame格式
label_cost = pd.DataFrame(temp_dict,index=[0])
# 行转列
label_cost = label_cost.stack().reset_index()
#重命名列名
label_cost.columns = ['temp','label','cost']
# 只要大于100块钱的部分
label_cost = label_cost[label_cost['cost']>100]

label_cost

各分类的花费

类似地,可以画出不同分类下的消费数据饼状图,代码与上面类似,我们这里选择另外一种色彩渐变风格。

from matplotlib import font_manager as fm
from  matplotlib import cm

labels = label_cost['label']
sizes = label_cost['cost']
explode = (0.01,0.01,0.01,0.01,0.01,0.01,0.01)  # "explode" ,设置突出显示

fig, axes = plt.subplots(figsize=(10,5),ncols=2) # 设置绘图区域大小
ax1, ax2 = axes.ravel()

colors = cm.rainbow(np.arange(len(sizes))/len(sizes)) # colormaps: Paired, autumn, rainbow, gray,spring,Darks
patches, texts, autotexts = ax1.pie(sizes, labels=labels, autopct='%1.0f%%',explode=explode,
        shadow=False, startangle=170, colors=colors, labeldistance=1.2,pctdistance=1.03, radius=0.4)
# labeldistance: 控制labels显示的位置
# pctdistance: 控制百分比显示的位置
# radius: 控制切片突出的距离

ax1.axis('equal')  

# 重新设置字体大小
proptease = fm.FontProperties()
proptease.set_size('medium')
# font size include: ‘xx-small’,x-small’,'small’,'medium’,‘large’,‘x-large’,‘xx-large’ or number, e.g. '12'
plt.setp(autotexts, fontproperties=proptease)
plt.setp(texts, fontproperties=proptease)

ax1.set_title("Hsinyan的年度账单", loc='left')

# ax2 只显示图例(legend)
ax2.axis('off')
ax2.legend(patches, labels, loc='center left')

plt.tight_layout()
# plt.savefig("pie_shape_ufo.png", bbox_inches='tight')
plt.savefig('label_counts.jpg')
plt.show()

各分类下的消费情况

非常的 amazing 啊!

月度消费情况

先前我提到我十月去了一趟医院检查,花了 500 块钱,当月花费应该是今年最高的,那么是不是这样的呢,画个图就知道了。

# 分割时间列表
timeline_list = [
    '2020/07/01',
    '2020/08/01',
    '2020/09/01',
    '2020/10/01',
    '2020/11/01',
    '2020/12/01',
    '2021/01/01'
]
# 定义初始变量
temp_df = df
last_timeline = '2020/06/01'
# 定义空字典
month_dict = {}

for timeline in timeline_list:
    month_df = temp_df[temp_df['购买日期']>last_timeline]
    month_df = month_df[month_df['购买日期']<timeline]
    cost = month_df['价格'].sum()
    month_dict[last_timeline] = cost
    last_timeline = timeline
    
# 转为dataframe格式
month_data = pd.DataFrame(month_dict,index=[0])
# 行转列
month_data = month_data.stack().reset_index()
month_data.columns = ['temp','month','cost']
month_data

6月~12月的消费情况

这里 temp 的那列实际意义,我在行转列的过程中冒出来的,所以不用管他,下面画个柱状图。

x = month_data['month']
y = month_data['cost']


def get_color(x, y):
    """对销量不同的区段标为不同的颜色"""
    color = []
    for i in range(len(x)):

        if y[i] < 1000:
            color.append("green")
        elif y[i] < 1500:
            color.append("lightseagreen")
        elif y[i] < 2000:
            color.append("gold")
        else:
            color.append("coral")

    return color


plt.bar(x,y,label="花费",color=get_color(x,y), tick_label=x)

for a,b in zip(x, y):
    plt.text(a, b+0.1, b, ha='center', va='bottom')

plt.legend(loc="upper left")
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.ylabel('每月花费')
plt.xlabel('月份')
plt.rcParams['savefig.dpi'] = 300  # 图片像素
plt.rcParams['figure.dpi'] = 300  # 分辨率
plt.rcParams['figure.figsize'] = (15.0, 8.0)  # 尺寸
plt.title("每月花费情况分析")
plt.savefig('result.png')
plt.show()

绘制后的柱状图

可以看到 6 月和 7 月因为疫情尚未返校,所以花钱的地方其实很少,9 月返校后平均每个月花费 1700~1800 左右,算是符合心里预期。而且 10 月的消费达到了 2889.61 元,确实是消费最多的一个月,下面来看看这个月我究竟都干了些什么。

# 筛选出10月消费记录
temp = df[df['购买日期']>'2020/09/31']
temp = temp[temp['购买日期']<'2020/11/01']
temp.sort_values('价格',ascending=False).head(10)

10月消费记录

可以看出 10 月消费占大头的几件是看病、购置蓝牙耳机、衬衫以及购买一些知识付费课程。但是其中也发现了 T8 蓝牙耳机的数据,或者蓝牙耳机后来因为质量问题退货了,所以 10 月的消费应该虚高了 100,理应删掉。

写在最后

通过对数据进行分析,得到了挺多有意思的结果,这些东西可能是反直觉的,拿餐饮支出的比例来说,我以为我的餐饮支出可能达到了 70%左右的地步,但其实只占到所有支出的 50%左右;还有就是,仅是去医院看一次小病,就占了全年消费的 5%,我的身体一直以来还算健康,要是差些的话,想都不敢想。

分析的时候也发现了一些问题,就是在记录的时候Name字段和标签项我有时候划分的不少特别清楚,举个例子(如下图),我可能会在Name字段的地方写上具体的名称,但其实正确的做法应该是在Name字段写上大类的名称,如:奶茶、晚餐、早餐,在备注处写上写上具体的奶茶种类。这样会使得后续的分析更加可靠。

错误的记录类型

第一次发这类型的文章,不知道大家喜不喜欢,如果反馈不错的话,过两天把自己最近一年所作任务的数据分析结果整理一下发出来。

最后修改:2022 年 01 月 25 日
如果觉得我的文章对你有用,请随意赞赏