483 lines
15 KiB
Markdown
483 lines
15 KiB
Markdown
|
## 深入浅出pandas-4
|
|||
|
|
|||
|
### 数据透视
|
|||
|
|
|||
|
经过前面的学习,我们已经将数据准备就绪而且变成了我们想要的样子,接下来就是最为重要的数据透视阶段了。当我们拿到一大堆数据的时候,如何从数据中迅速的解读出有价值的信息,把繁杂的数据变成容易解读的统计图表并再此基础上产生业务洞察,这就是数据分析要解决的核心问题。
|
|||
|
|
|||
|
#### 获取描述性统计信息
|
|||
|
|
|||
|
首先,我们可以获取数据的描述性统计信息,通过描述性统计信息,我们可以了解数据的集中趋势和离散趋势。
|
|||
|
|
|||
|
例如,我们有如下所示的学生成绩表。
|
|||
|
|
|||
|
```Python
|
|||
|
scores = np.random.randint(50, 101, (5, 3))
|
|||
|
names = ('关羽', '张飞', '赵云', '马超', '黄忠')
|
|||
|
courses = ('语文', '数学', '英语')
|
|||
|
df = pd.DataFrame(data=scores, columns=courses, index=names)
|
|||
|
df
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 数学 英语
|
|||
|
关羽 96 72 73
|
|||
|
张飞 72 70 97
|
|||
|
赵云 74 51 79
|
|||
|
马超 100 54 54
|
|||
|
黄忠 89 100 88
|
|||
|
```
|
|||
|
|
|||
|
我们可以通过`DataFrame`对象的方法`mean`、`max`、`min`、`std`、`var`等方法分别获取每个学生或每门课程的平均分、最高分、最低分、标准差、方差等信息,也可以直接通过`describe`方法直接获取描述性统计信息,代码如下所示。
|
|||
|
|
|||
|
计算每门课程成绩的平均分。
|
|||
|
|
|||
|
```Python
|
|||
|
df.mean()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 86.2
|
|||
|
数学 69.4
|
|||
|
英语 78.2
|
|||
|
dtype: float64
|
|||
|
```
|
|||
|
|
|||
|
计算每个学生成绩的平均分。
|
|||
|
|
|||
|
```Python
|
|||
|
df.mean(axis=1)
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
关羽 80.333333
|
|||
|
张飞 79.666667
|
|||
|
赵云 68.000000
|
|||
|
马超 69.333333
|
|||
|
黄忠 92.333333
|
|||
|
dtype: float64
|
|||
|
```
|
|||
|
|
|||
|
计算每门课程成绩的方差。
|
|||
|
|
|||
|
```Python
|
|||
|
df.var()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 161.2
|
|||
|
数学 379.8
|
|||
|
英语 265.7
|
|||
|
dtype: float64
|
|||
|
```
|
|||
|
|
|||
|
> **说明**:通过方差可以看出,数学成绩波动最大,两极分化可能更严重。
|
|||
|
|
|||
|
获取每门课程的描述性统计信息。
|
|||
|
|
|||
|
```Python
|
|||
|
df.describe()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 数学 英语
|
|||
|
count 5.000000 5.000000 5.000000
|
|||
|
mean 86.200000 69.400000 78.200000
|
|||
|
std 12.696456 19.488458 16.300307
|
|||
|
min 72.000000 51.000000 54.000000
|
|||
|
25% 74.000000 54.000000 73.000000
|
|||
|
50% 89.000000 70.000000 79.000000
|
|||
|
75% 96.000000 72.000000 88.000000
|
|||
|
max 100.000000 100.000000 97.000000
|
|||
|
```
|
|||
|
|
|||
|
#### 排序和取头部值
|
|||
|
|
|||
|
如果需要对数据进行排序,可以使用`DataFrame`对象的`sort_values`方法,该方法的`by`参数可以指定根据哪个列或哪些列进行排序,而`ascending`参数可以指定升序或是降序。例如,下面的代码展示了如何将学生表按语文成绩排降序。
|
|||
|
|
|||
|
```Python
|
|||
|
df.sort_values(by='语文', ascending=False)
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 数学 英语
|
|||
|
马超 100 54 54
|
|||
|
关羽 96 72 73
|
|||
|
黄忠 89 100 88
|
|||
|
赵云 74 51 79
|
|||
|
张飞 72 70 97
|
|||
|
```
|
|||
|
|
|||
|
如果`DataFrame`数据量很大,排序将是一个非常耗费时间的操作。有的时候我们只需要获得排前N名或后N名的数据,这个时候其实没有必要对整个数据进行排序,而是直接利用堆结构找出Top-N的数据。`DataFrame`的`nlargest`和`nsmallest`方法就提供对Top-N操作的支持,代码如下所示。
|
|||
|
|
|||
|
找出语文成绩前3名的学生信息。
|
|||
|
|
|||
|
```Python
|
|||
|
df.nlargest(3, '语文')
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 数学 英语
|
|||
|
马超 100 54 54
|
|||
|
关羽 96 72 73
|
|||
|
黄忠 89 100 88
|
|||
|
```
|
|||
|
|
|||
|
找出数学成绩最低的3名学生的信息。
|
|||
|
|
|||
|
```Python
|
|||
|
df.nsmallest(3, '数学')
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
语文 数学 英语
|
|||
|
赵云 74 51 79
|
|||
|
马超 100 54 54
|
|||
|
张飞 72 70 97
|
|||
|
```
|
|||
|
|
|||
|
#### 分组聚合
|
|||
|
|
|||
|
我们先从之前使用过的 Excel 文件中读取2020年销售数据,然后再为大家演示如何进行分组聚合操作。
|
|||
|
|
|||
|
```Python
|
|||
|
df = pd.read_excel('data/2020年销售数据.xlsx')
|
|||
|
df.head()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量
|
|||
|
0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83
|
|||
|
1 2020-01-01 上海 抖音 205635-402 八匹马 219 29
|
|||
|
2 2020-01-01 上海 天猫 205654-021 八匹马 169 85
|
|||
|
3 2020-01-01 上海 天猫 205654-519 八匹马 169 14
|
|||
|
4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61
|
|||
|
```
|
|||
|
|
|||
|
如果我们要统计每个销售区域的销售总额,可以先通过“售价”和“销售数量”计算出销售额,为`DataFrame`添加一个列,代码如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
df['销售额'] = df['售价'] * df['销售数量']
|
|||
|
df.head()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量 销售额
|
|||
|
0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83 8217
|
|||
|
1 2020-01-01 上海 抖音 205635-402 八匹马 219 29 6351
|
|||
|
2 2020-01-01 上海 天猫 205654-021 八匹马 169 85 14365
|
|||
|
3 2020-01-01 上海 天猫 205654-519 八匹马 169 14 2366
|
|||
|
4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61 15189
|
|||
|
```
|
|||
|
|
|||
|
然后再根据“销售区域”列对数据进行分组,这里我们使用的是`DataFrame`对象的`groupby`方法。分组之后,我们取“销售额”这个列在分组内进行求和处理,代码和结果如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby('销售区域').销售额.sum()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售区域
|
|||
|
上海 11610489
|
|||
|
北京 12477717
|
|||
|
安徽 895463
|
|||
|
广东 1617949
|
|||
|
江苏 2304380
|
|||
|
浙江 687862
|
|||
|
福建 10178227
|
|||
|
Name: 销售额, dtype: int64
|
|||
|
```
|
|||
|
|
|||
|
如果我们要统计每个月的销售总额,我们可以将“销售日期”作为groupby`方法的参数,当然这里需要先将“销售日期”处理成月,代码和结果如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby(df['销售日期'].dt.month).销售额.sum()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售日期
|
|||
|
1 5409855
|
|||
|
2 4608455
|
|||
|
3 4164972
|
|||
|
4 3996770
|
|||
|
5 3239005
|
|||
|
6 2817936
|
|||
|
7 3501304
|
|||
|
8 2948189
|
|||
|
9 2632960
|
|||
|
10 2375385
|
|||
|
11 2385283
|
|||
|
12 1691973
|
|||
|
Name: 销售额, dtype: int64
|
|||
|
```
|
|||
|
|
|||
|
接下来我们将难度升级,统计每个销售区域每个月的销售总额,这又该如何处理呢?事实上,`groupby`方法的第一个参数可以是一个列表,列表中可以指定多个分组的依据,大家看看下面的代码和输出结果就明白了。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby(['销售区域', df['销售日期'].dt.month]).销售额.sum()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售区域 销售日期
|
|||
|
上海 1 1679125
|
|||
|
2 1689527
|
|||
|
3 1061193
|
|||
|
4 1082187
|
|||
|
5 841199
|
|||
|
6 785404
|
|||
|
7 863906
|
|||
|
8 734937
|
|||
|
9 1107693
|
|||
|
10 412108
|
|||
|
11 825169
|
|||
|
12 528041
|
|||
|
北京 1 1878234
|
|||
|
2 1807787
|
|||
|
3 1360666
|
|||
|
4 1205989
|
|||
|
5 807300
|
|||
|
6 1216432
|
|||
|
7 1219083
|
|||
|
8 645727
|
|||
|
9 390077
|
|||
|
10 671608
|
|||
|
11 678668
|
|||
|
12 596146
|
|||
|
安徽 4 341308
|
|||
|
5 554155
|
|||
|
广东 3 388180
|
|||
|
8 469390
|
|||
|
9 365191
|
|||
|
11 395188
|
|||
|
江苏 4 537079
|
|||
|
7 841032
|
|||
|
10 710962
|
|||
|
12 215307
|
|||
|
浙江 3 248354
|
|||
|
8 439508
|
|||
|
福建 1 1852496
|
|||
|
2 1111141
|
|||
|
3 1106579
|
|||
|
4 830207
|
|||
|
5 1036351
|
|||
|
6 816100
|
|||
|
7 577283
|
|||
|
8 658627
|
|||
|
9 769999
|
|||
|
10 580707
|
|||
|
11 486258
|
|||
|
12 352479
|
|||
|
Name: 销售额, dtype: int64
|
|||
|
```
|
|||
|
|
|||
|
如果希望统计出每个区域的销售总额以及每个区域单笔金额的最高和最低,我们可以在`DataFrame`或`Series`对象上使用`agg`方法并指定多个聚合函数,代码和结果如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby('销售区域').销售额.agg(['sum', 'max', 'min'])
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
sum max min
|
|||
|
销售区域
|
|||
|
上海 11610489 116303 948
|
|||
|
北京 12477717 133411 690
|
|||
|
安徽 895463 68502 1683
|
|||
|
广东 1617949 120807 990
|
|||
|
江苏 2304380 114312 1089
|
|||
|
浙江 687862 90909 3927
|
|||
|
福建 10178227 87527 897
|
|||
|
```
|
|||
|
|
|||
|
如果希望自定义聚合后的列的名字,可以使用如下所示的方法。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby('销售区域').销售额.agg(销售总额='sum', 单笔最高='max', 单笔最低='min')
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售总额 单笔最高 单笔最低
|
|||
|
销售区域
|
|||
|
上海 11610489 116303 948
|
|||
|
北京 12477717 133411 690
|
|||
|
安徽 895463 68502 1683
|
|||
|
广东 1617949 120807 990
|
|||
|
江苏 2304380 114312 1089
|
|||
|
浙江 687862 90909 3927
|
|||
|
福建 10178227 87527 897
|
|||
|
```
|
|||
|
|
|||
|
如果需要对多个列使用不同的聚合函数,例如“统计每个销售区域销售额的总和以及销售数量的最低值和最高值”,我们可以按照下面的方式来操作。
|
|||
|
|
|||
|
```Python
|
|||
|
df.groupby('销售区域')[['销售额', '销售数量']].agg({
|
|||
|
'销售额': 'sum', '销售数量': ['max', 'min']
|
|||
|
})
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售额 销售数量
|
|||
|
sum max min
|
|||
|
销售区域
|
|||
|
上海 11610489 100 10
|
|||
|
北京 12477717 100 10
|
|||
|
安徽 895463 98 16
|
|||
|
广东 1617949 98 10
|
|||
|
江苏 2304380 100 11
|
|||
|
浙江 687862 95 20
|
|||
|
福建 10178227 100 10
|
|||
|
```
|
|||
|
|
|||
|
#### 透视表和交叉表
|
|||
|
|
|||
|
上面的例子中,“统计每个销售区域每个月的销售总额”会产生一个看起来很长的结果,在实际工作中我们通常把那些行很多列很少的表成为“窄表”,如果我们不想得到这样的一个“窄表”,可以使用`DataFrame`的`pivot_table`方法或者是`pivot_table`函数来生成透视表。透视表的本质就是对数据进行分组聚合操作,**根据 A 列对 B 列进行统计**,如果大家有使用 Excel 的经验,相信对透视表这个概念一定不会陌生。例如,我们要“统计每个销售区域的销售总额”,那么“销售区域”就是我们的 A 列,而“销售额”就是我们的 B 列,在`pivot_table`函数中分别对应`index`和`values`参数,这两个参数都可以是单个列或者多个列。
|
|||
|
|
|||
|
```Python
|
|||
|
pd.pivot_table(df, index='销售区域', values='销售额', aggfunc='sum')
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
```
|
|||
|
销售额
|
|||
|
销售区域
|
|||
|
上海 11610489
|
|||
|
北京 12477717
|
|||
|
安徽 895463
|
|||
|
广东 1617949
|
|||
|
江苏 2304380
|
|||
|
浙江 687862
|
|||
|
福建 10178227
|
|||
|
```
|
|||
|
|
|||
|
> **注意**:上面的结果操作跟之前用`groupby`的方式得到的结果有一些区别,`groupby`操作后,如果对单个列进行聚合,得到的结果是一个`Series`对象,而上面的结果是一个`DataFrame` 对象。
|
|||
|
|
|||
|
如果要统计每个销售区域每个月的销售总额,也可以使用`pivot_table`函数,代码如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
df['月份'] = df['销售日期'].dt.month
|
|||
|
pd.pivot_table(df, index=['销售区域', '月份'], values='销售额', aggfunc='sum')
|
|||
|
```
|
|||
|
|
|||
|
上面的操作结果是一个`DataFrame`,但也是一个长长的“窄表”,如果希望做成一个行比较少列比较多的“宽表”,可以将`index`参数中的列放到`columns`参数中,代码如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
pd.pivot_table(df, index='销售区域', columns='月份', values='销售额', aggfunc='sum', fill_value=0)
|
|||
|
```
|
|||
|
|
|||
|
> **说明**:`pivot_table`函数的`fill_value=0`会将空值处理为`0`。
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
<img src="res/pivot_table_1.png" style="zoom:50%;">
|
|||
|
|
|||
|
使用`pivot_table`函数时,还可以通过添加`margins`和`margins_name`参数对分组聚合的结果做一个汇总,具体的操作和效果如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
pd.pivot_table(df, index='销售区域', columns='月份', values='销售额', aggfunc='sum', fill_value=0, margins=True, margins_name='总计')
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
<img src="res/pivot_table_2.png" style="zoom:50%;">
|
|||
|
|
|||
|
交叉表就是一种特殊的透视表,它不需要先构造一个`DataFrame`对象,而是直接通过数组或`Series`对象指定两个或多个因素进行运算得到统计结果。例如,我们要统计每个销售区域的销售总额,也可以按照如下所示的方式来完成,我们先准备三组数据。
|
|||
|
|
|||
|
```Python
|
|||
|
sales_area, sales_month, sales_amount = df['销售区域'], df['月份'], df['销售额']
|
|||
|
```
|
|||
|
|
|||
|
使用`crosstab`函数生成交叉表。
|
|||
|
|
|||
|
```Python
|
|||
|
pd.crosstab(index=sales_area, columns=sales_month, values=sales_amount, aggfunc='sum').fillna(0).astype('i8')
|
|||
|
```
|
|||
|
|
|||
|
> **说明**:上面的代码使用了`DataFrame`对象的`fillna`方法将空值处理为0,再使用`astype`方法将数据类型处理成整数。
|
|||
|
|
|||
|
### 数据呈现
|
|||
|
|
|||
|
一图胜千言,我们对数据进行透视的结果,最终要通过图表的方式呈现出来,因为图表具有极强的表现力,能够让我们迅速的解读数据中隐藏的价值。和`Series`一样,`DataFrame`对象提供了`plot`方法来支持绘图,底层仍然是通过`matplotlib`库实现图表的渲染。关于`matplotlib`的内容,我们在下一个章节进行详细的探讨,这里我们只简单的讲解`plot`方法的用法。
|
|||
|
|
|||
|
例如,我们想通过一张柱状图来比较“每个销售区域的销售总额”,可以直接在透视表上使用`plot`方法生成柱状图。我们先导入`matplotlib.pyplot`模块,通过修改绘图的参数使其支持中文显示。
|
|||
|
|
|||
|
```Python
|
|||
|
import matplotlib.pyplot as plt
|
|||
|
|
|||
|
plt.rcParams['font.sans-serif'] = 'FZJKai-Z03S'
|
|||
|
```
|
|||
|
|
|||
|
> **说明**:上面的`FZJKai-Z03S`是我电脑上已经安装的一种支持中文的字体的名称,字体的名称可以通过查看用户主目录下`.matplotlib`文件夹下名为`fontlist-v330.json`的文件来获得,而这个文件在执行上面的命令后就会生成。
|
|||
|
|
|||
|
使用魔法指令配置生成矢量图。
|
|||
|
|
|||
|
```Python
|
|||
|
%config InlineBackend.figure_format = 'svg'
|
|||
|
```
|
|||
|
|
|||
|
绘制“每个销售区域销售总额”的柱状图。
|
|||
|
|
|||
|
```Python
|
|||
|
temp = pd.pivot_table(df, index='销售区域', values='销售额', aggfunc='sum')
|
|||
|
temp.plot(figsize=(8, 4), kind='bar')
|
|||
|
plt.xticks(rotation=0)
|
|||
|
plt.show()
|
|||
|
```
|
|||
|
|
|||
|
> **说明**:上面的第3行代码会将横轴刻度上的文字旋转到0度,第4行代码会显示图像。
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
<img src="res/sales_bar_graph.png" style="zoom:45%;">
|
|||
|
|
|||
|
如果要绘制饼图,可以修改`plot`方法的`kind`参数为`pie`,然后使用定制饼图的参数对图表加以定制,代码如下所示。
|
|||
|
|
|||
|
```Python
|
|||
|
temp.sort_values(by='销售额', ascending=False).plot(
|
|||
|
figsize=(6, 6),
|
|||
|
kind='pie',
|
|||
|
y='销售额',
|
|||
|
ylabel='',
|
|||
|
autopct='%.2f%%',
|
|||
|
pctdistance=0.8,
|
|||
|
wedgeprops=dict(linewidth=1, width=0.35),
|
|||
|
legend=False
|
|||
|
)
|
|||
|
plt.show()
|
|||
|
```
|
|||
|
|
|||
|
输出:
|
|||
|
|
|||
|
<img src="res/sales_pie_graph.png" style="zoom:35%;">
|
|||
|
|