在之前写个人年度总结的时候,提到了自己最近半年养成了一个新的习惯——在 Notion 上进行进行各方面的记录,其中就包括了记录每日所做的事情以及账单,这一年做过的事情后面我也会进行数据分析,这篇文章我们来分析一下我过去这半年(从 6 月至今)的账单。
在这里再安利一下 Notion 这个软件,我以前用过很多个个人效率提升类的软件,但是只有 Notion 是我坚持用下来的,因为他赋予用户的权限实在是太高了,你可以随心所欲地定制你的 WorkSpace,像设计数据库一样定义外键,定义各种视图,在 Notion 里你几乎可以定义出自己想要的任何功能,只有你想得到,没有它做不到。而且在今年,Notion 官方公布了新的会员体系,免费会员也可以拥有无限数量的 block 了,毕竟内容是笔记软件的核心,当 block 数量没有限制并且承诺永久免费之后,个人用户才可以放心没后顾之忧地使用 Notion 来记录任何的资料。仅这一点就让 Notion 免费用户的实用性飙升了好几个档次。
我在 Notion 中定义了一个用于记录每天消费记录的表,通过这个表我可以每天进行记账,十分方便。
同样的,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 的数据。
可以看到记录次数最多的是一日四餐(早餐、午餐、晚餐以及夜宵),前 10 全部被吃的包揽了,值得注意的是我有记录的早餐次数很少,甚至比夜宵次数还少,立个 flag,希望新的一年里能够多吃早餐,少吃夜宵。
同理,我们再使用remark_counts.head(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)
不看不知道,今年手机前后坏了三次,修手机就花了 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
这里 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 月消费占大头的几件是看病、购置蓝牙耳机、衬衫以及购买一些知识付费课程。但是其中也发现了 T8 蓝牙耳机的数据,或者蓝牙耳机后来因为质量问题退货了,所以 10 月的消费应该虚高了 100,理应删掉。
通过对数据进行分析,得到了挺多有意思的结果,这些东西可能是反直觉的,拿餐饮支出的比例来说,我以为我的餐饮支出可能达到了 70%左右的地步,但其实只占到所有支出的 50%左右;还有就是,仅是去医院看一次小病,就占了全年消费的 5%,我的身体一直以来还算健康,要是差些的话,想都不敢想。
分析的时候也发现了一些问题,就是在记录的时候Name
字段和标签项我有时候划分的不少特别清楚,举个例子(如下图),我可能会在Name
字段的地方写上具体的名称,但其实正确的做法应该是在Name
字段写上大类的名称,如:奶茶、晚餐、早餐,在备注处写上写上具体的奶茶种类。这样会使得后续的分析更加可靠。
第一次发这类型的文章,不知道大家喜不喜欢,如果反馈不错的话,过两天把自己最近一年所作任务的数据分析结果整理一下发出来。