前言

在之前写个人年度总结的时候,提到了自己最近半年养成了一个新的习惯——在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字段写上大类的名称,如:奶茶、晚餐、早餐,在备注处写上写上具体的奶茶种类。这样会使得后续的分析更加可靠。

错误的记录类型

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

Last modification:January 13th, 2021 at 10:09 pm