在随书源代码的CH10/Family子目录下,文件FamilyFinanceData.csv存储了经过整理的涉及3万多户中国家庭,10万个家庭成员的家庭收入及财产数据。这是一个逗号分隔值文件,使用Excel或者WPS软件打开,其内容结构如图10-P2所示。

数据来源:西南财经大学中国家庭金融调查与研究中心,中国家庭金融调查数据库,2019年。受限于版权,随书代码中提供给读者的数据并非原始数据,但保持了与原始数据类似的统计学特征。

数据及源代码下载

http://codelearn.club/download/Family.zip

版权声明

本文节选自《Python编程基础及应用》第2版,高等教育出版社,作者:陈波,刘慧君

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。本文不可以以纸质出版为目的进行改编、摘抄。

image-20240415122356863

图10-P2 家庭金融调查数据(前10行)

如图10-P2所见,文件中的每一行对应一位家庭成员。以2号家庭为例,该家庭有序号1至3共3位家庭成员,家庭年度总收入为142360元,家庭总资产为203654.5元。

计算家庭人数

相较于家庭年度总收入和家庭总资产,人均年度收入和家庭人均资产更能真实地反应居民的收入和财产状况。为了得到平均数据,我们先统计各家庭人数。

IDLE Shell

1
2
3
4
5
6
7
8
>>>import pandas as pd
>>>df = pd.read_csv("d:/PyLearn2/CH10/Family/FamilyFinanceData.csv")
>>>df.head(10) #显示前10行
家庭编号 家庭成员序号 家庭总收入 家庭总资产
0 1 1 193798.0 2452061.0
1 1 2 193798.0 2452061.0
... #有节略
9 4 1 1815.0 3110.0

首先,我们使用pd.read_csv()函数将文件读入为DataFrame。文件的路径请读者按自己计算机上的实际路径修改。df.head(10)的执行结果显示,文件被成功读入,结构与图10-P2所示一致。

IDLE Shell

1
2
3
4
5
6
7
8
9
10
11
>>>s = df.groupby("家庭编号").size()   #按家庭编号分组统计人数
>>>type(s)
<class 'pandas.core.series.Series'>
>>>s
家庭编号
1 3
... #有节略
4 1
... #有节略
34513 5
Length: 34513, dtype: int64

接下来,使用df.groupby(“家庭编号”).size()函数对DataFrame按家庭编号进行分组,并统计每组人数。执行结果显示,统计结果s是一个Series,其索引为家庭编号。其中,1号家庭有3人,4号家庭有1人。

IDLE Shell

1
2
3
4
5
6
>>>s = pd.DataFrame({"家庭编号":s.index, "家庭人数":s.values})
>>>s.head(3)
家庭编号 家庭人数
0 1 3
1 2 3
2 3 3

以结果Series的索引s.index为“家庭编号”列,值序列s.values为“家庭人数”列,创建DataFrame。

IDLE Shell

1
2
3
4
>>>df.head(2)
家庭编号 家庭成员序号 家庭总收入 家庭总资产
0 1 1 193798.0 2452061.0
1 1 2 193798.0 2452061.0

如上述结果所示,df和s都有一个名为“家庭编号”的列,两个DataFrame中的数据可按家庭编号对应。因此,我们使用pd.merge()函数将两个DataFrame按“家庭编号”合并。

IDLE Shell

1
2
3
4
5
6
7
>>>df = pd.merge(df,s,on="家庭编号")
>>>df.head(10)
家庭编号 家庭成员序号 家庭总收入 家庭总资产 家庭人数
0 1 1 193798.0 2452061.0 3
1 1 2 193798.0 2452061.0 3
... #有节略
9 4 1 1815.0 3110.0 1

合并结果显示,除原有列外,DataFrame新增家庭人数列。

计算人均年收入及人均资产

IDLE Shell

1
2
3
4
5
6
7
8
>>>df["人均年收入"] = df["家庭总收入"] / df["家庭人数"]
>>>df["人均资产"] = df["家庭总资产"] / df["家庭人数"]
>>>df.head(10)
家庭编号 ... 家庭人数 人均年收入 人均资产
0 1 3 64599.333333 8.173537e+05
1 1 3 64599.333333 8.173537e+05
... #有节略
9 4 1 1815.000000 3.110000e+03

如上所示,将家庭总收入列与家庭人数列相除,即得人均年收入列。将家庭总资产列除以家庭人数列,即得人均资产列。

IDLE Shell

1
>>>df.to_csv("d:/PyLearn2/CH10/Family/Stat.csv",index=False)

为方便后续使用,使用df.to_csv()函数将当前统计结果存入Stat.csv文件中。读者执行上述代码时,需要按自己计算机上的路径进行修改。

居民的人均年收入分析

IDLE Shell

1
2
3
4
5
6
7
8
9
10
11
>>>import pandas as pd
>>>df = pd.read_csv("d:/PyLearn2/CH10/Family/Stat.csv")
>>>avg = df["人均年收入"]
>>type(avg)
<class 'pandas.core.series.Series'>
>>avg
0 64599.333333
1 64599.333333
... #有节略
106753 5320.000000
Name: 人均年收入, Length: 106754, dtype: float64

首先,从统计结果文件Stat.csv读入DataFrame,然后取得“人均年收入”列。执行结果显示,avg是一个Series,其中存储了106754位居民的人均年收入。

IDLE Shell

1
2
3
4
>>>avg.mean()         #人均年收入均值
26698.420064939066
>>>avg.median() #人均年收入中位数
15932.916666666668

Series提供一系列的统计函数,其中,mean()计算均值,median()计算中位数。执行结果显示,居民人均年收入约为两万七千元,中位数约为一万六千元。

IDLE Shell

1
2
3
4
5
6
>>>avg.std()         #人均年收入标准差
64415.07321352001
>>>avg.max() #人均年收入最大值
3931565.5
>>>avg.min() #人均年收入最小值
-948972.25

如上所示,Series的std()、max()、min()成员函数分别计算Series的标准差,最大和最小值。金融调查数据中,有部分家庭的年度总收入为负值,这多半是由诸如股市亏损之类的经营性亏损导致的。

IDLE Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>>import matplotlib.pyplot as plt         #导入matplotlib
>>>plt.rcParams["font.family"] = "SimHei" #设置中文字体
>>>ax = avg.hist(bins=10000) #分10000个桶画直方图
>>>ax.set_title("家庭人均年收入分布") #设置标题
Text(0.5, 1.0, '家庭人均年收入分布')
>>>ax.set_xlabel("家庭人均年收入(元)") #设置x轴标签
Text(0.5, 0, '家庭人均年收入(元)')
>>>ax.set_ylabel("人数") #设置y轴标签
Text(0, 0.5, '人数')
>>>ax.set_xlim(-10000,200000) #设置x轴显示范围为-1万至20万
(-10000.0, 200000.0)
>>>ax.grid(True) #显示网格
>>>plt.subplots_adjust(0.1,0.1,0.9,0.9) #调整边距
>>>plt.show() #显示绘图结果

以matplotlib为基础,pandas也支持统计绘图。上述代码中,avg.hist(bins=10000)将人均年收入数据分成10000个区间,统计落入每个收入区间的人数,并绘制直方图。为便于观察,我们将x轴显示范围设为-1万至20万,绘图结果见图10-P3。

image-20240415123219715

图10-P3 居民家庭人均年收入分布(-1万至20万区间)

如图10-P3所示,绝大多数居民的家庭人均年收入在5万元以下,人均家庭年收入超过20万的居民凤毛麟角,这与各种社交媒体各博主自吹自擂的人均年入20万相去甚远。

IDLE Shell

1
2
3
4
5
6
7
8
9
10
11
12
>>>r = avg >= 200000     #统计家庭人均年收入达到20万的样本数量
>>>r
0 False
1 False
... #有节略
106753 False
Name: 人均年收入, Length: 106754, dtype: bool
>>>r.value_counts() #统计Series中各值的数量
人均年收入
False 105793
True 961
Name: count, dtype: int64

将avg与200000进行比较,可得一个包含多个布尔值的Series。进一步执行r.value_counts()可知,在106754个样本中,仅有961人的家庭人均年收入达到20万。

IDLE Shell

1
2
>>>r.value_counts()[True] / r.shape[0]
0.009002004608726605

将r.value_counts()统计结果中为True的人数除以调查样本总数,可知家庭人均年收入达到20万的人群比例仅为千分之九。

IDLE Shell

1
2
3
>>>r = (avg < 12000)     #统计小于12000的样本数量
>>>r.value_counts()[True]/r.shape[0] #计算人群占比
0.4051932480281769

以类似的方法也可以统计家庭人均年收入不足1000元(对应年收入12000元)的人群占比。上述执行结果显示,该比例约为40.5%。读者可根据14亿人口加上上述比例估计贫困人口数量。(此处不便列出)

躬行实践

10-x (人均家庭财产) 请读者以上述统计结果文件Stat.csv为基础,绘制人均家庭财产分布的直方图,并统计统计样本中人均家庭财产的均值和中位数。

财富基尼系数看贫富分化

按照本书第5章介绍的基尼系数计算方法,我们以前述金融调查数据的人均资产为基础,计算了财富基尼系数,并绘制了计算示意图,完整程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#assetgini.py
import pandas as pd, numpy as np

df = pd.read_csv('Stat.csv')
wealths = np.sort(df["人均资产"].to_numpy()) #按人均资产排序,并转换为numpy数组
cumWealths = np.cumsum(wealths) #对财富数据进行累积
sumWealths = cumWealths[-1] #求社会总财富
xs = np.array(range(1, len(cumWealths)+1),dtype=np.float64) / len(cumWealths)
ys = cumWealths / sumWealths #xs,ys分别为人群占比和财富占比
xs,ys = np.insert(xs,0,0),np.insert(ys,0,0) #在xs,ys数组的首位插入0
B = np.trapz(ys, x=xs) #计算洛伦兹曲线下的面积B
A = 0.5 - B #计算洛伦兹曲线上的面积A
print(f"财产基尼系数: {A/(A+B):.2f}") #输出基尼系数

import matplotlib.pyplot as plt, matplotlib.ticker as ticker
plt.rcParams["font.family"] = "SimHei"
plt.rcParams["font.size"] = 11
fig,ax = plt.subplots(figsize=(6,6))
ax.plot(xs,ys,label="洛伦兹曲线",color='blue',linestyle="--")
ax.plot(xs,xs,label="完全平等曲线",color='black')
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax.set_xlabel("人群占比")
ax.set_ylabel("财富占比")
ax.set_title("财富基尼系数的计算")
ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=1))
ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax=1))
plt.legend()
plt.fill_between(xs,y1=ys,y2=0,facecolor='blue',alpha=0.2)
plt.fill_between(xs,y1=ys,y2=xs,facecolor='green',alpha=0.2)
plt.text(0.5,0.3,"A",fontsize=20)
plt.text(0.8,0.1,"B",fontsize=20)
plt.show()

相关计算方法的细节请读者回顾第5章。此处仅就第11行代码加以说明。np.trapz(ys,x=xs)用于积分计算洛伦兹曲线下方B区域的面积,其中,xs为自变量值序列,ys则为函数值序列。查询numpy文档可知,在trapz()函数内部,也是使用复合梯形规则进行面积积分的。

image-20240415123651241

图10-P4 按家庭人均财产计算财富基尼系数

上述财富基尼系数的计算结果比较意外,不便展示,请读者自行计算。数据和程序在本文开始部分有下载链接。

以重庆大学计算机学院硕士研究生的毕业薪资为例,作者进行了不靠谱的小样本调查:随机问了几个刚毕业的学生。学生毕业后首年收入平均在25万以上,50万到100万也不鲜见。根据上述统计结果,只有千分之九的居民人均家庭年收入在20万以上。这些毕业生,不论男女,都是赚钱好手,他们5年赚的钱,可能就是其他人一生的总和。学计算机的男生、女生,值得拥有

数据及源代码下载

http://codelearn.club/download/Family.zip

1