Pandas的使用(下)

在这一章,我们接着介绍Pandas的基本使用方法。基于上一章介绍的Pandas的基本操作,我们尝试进行一些实际应用中经常会用到的数据处理步骤,例如缺失值检测和处理、数据格式的转换、数据集的合并和分组计算等。这一章使用的例子都是最简单的情况,在实际应用中我们可能会遇到更加复杂的需求,会要求我们能够更加灵活地运用这些基本数据处理技术,将困难的问题拆解为标准的情况进行处理。

1 数据清洗和准备

首先仍然是导入我们需要用到的Pandas和NumPy:

import pandas as pd
import numpy as np

1.1 处理缺失数据

1.1.1 判断缺失数据

Pandas将缺失值统称为NA(not available),包括NaN(not a number)和None。回顾一下,NaN是我们在NumPy中见到的概念,float类型,表达的是"虽然在概念上是数据,但不是数字”的概念,例如我们除以0的时候就会得到一个NaN;而None是Python原生的基础类型之一,表示空。

我们可以用isnull来判断是否是缺失数据:

obj = pd.Series([0, '1', None, np.nan])
print(obj.isnull())

输出为:

0    False
1    False
2     True
3     True
dtype: bool

1.1.2 滤除缺失数据

Pandas提供了dropna函数来过滤掉那些缺失的数据。对于一个Series,dropna返回一个仅含非空数据和索引值的Series:

obj = pd.Series([0, '1', None, np.nan])
print(obj.dropna())
print()
print(obj)  # 返回了一个新的对象,obj本身没有变化

输出为:

0    0
1    1
dtype: object

0       0
1       1
2    None
3     NaN
dtype: object

而对于DataFrame对象,dropna默认丢弃任何含有缺失值的行,我们也可以通过axis=1指明丢弃任何含有缺失值的列:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.dropna())
print()
print(df.dropna(axis=1))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0

Empty DataFrame
Columns: []
Index: [0, 1, 2, 3]

如果只想丢弃全部为NA的行(或者列),我们可以指明参数how='all':

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.dropna(how='all'))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
3  NaN  6.5  3.0

另外我们也可以用thresh参数来指明最多删除几行:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.dropna())
print()
print(df.dropna(thresh=1))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
3  NaN  6.5  3.0

1.1.3 填充缺失数据

我们可以调用fillna函数来填充缺失数据,例如将所有缺失值填充为一个常数:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna(0))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

如果我们通过一个字典进行填充,就可以实现对不同的列填充不同的值:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna({0: 0, 1: 1, 2: 2}))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  1.0  2.0
2  0.0  1.0  2.0
3  0.0  6.5  3.0

如果我们不想返回新的对象,而是“原地”修改df,可以指明参数inplace=True:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
df.fillna(0, inplace=True)
print(df)  # df被修改了

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

我们也可以通过method='ffill'来指明我们用上面一行的值来进行填充:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna(method='ffill'))  # 我们会看到有一些值被“复制”了多次,我们可以通过limit来加一个限制
print()
print(df.fillna(method='ffill', limit=1))

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  6.5  3.0
2  1.0  6.5  3.0
3  1.0  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  6.5  3.0
2  1.0  NaN  NaN
3  NaN  6.5  3.0

另外一种常见情况是我们想要用其余数据的平均值来填充缺失值。对于Series而言,我们可以这样操作:

obj = pd.Series([1., np.nan, 2., np.nan])
print(obj.fillna(obj.mean()))

输出为:

0    1.0
1    1.5
2    2.0
3    1.5
dtype: float64

对于DataFrame,我们可以通过某一列的平均值来填充这一列的缺失值:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna(df.mean()))  # 默认情况按列的平均值填充

输出为:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna(df.mean()))  # 默认情况按列的平均值填充
df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.fillna(df.mean()))  # 默认情况按列的平均值填充
     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  6.5  3.0
2  1.0  6.5  3.0
3  1.0  6.5  3.0

1.2 数据转换

1.2.1 移除重复数据

有时候由于数据收集过程中的问题,我们会遇到重复数据的情况。我们可以用duplicated来检查各行是之前已经出现过的重复行:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
print(df.duplicated())

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

要移除这些重复的数据,可以用drop_duplicates,它会返回一个新的DataFrame:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
print(df.drop_duplicates())

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4

duplicated和drop_duplicates都默认会判断全部列,只有当全部列的值都是一样的,才认为是重复的。不过我们可以指定部分列进行重复项判断:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
print(df.drop_duplicates(['k1']))  # 例如这里我们只依据k1这一列

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

    k1  k2
0  one   1
1  two   1

另外,duplicated和drop_duplicates默认保留的是第一个出现的值组合。传入keep='last'则保留最后一个:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
print(df.drop_duplicates(keep='last'))

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
6  two   4

1.2.2 利用函数或映射进行数据转换

有些时候我们希望根据数组、Series或DataFrame列中的值来实现转换工作,这个时候就需要用到map函数,并给它一个函数或含有映射关系的字典型对象作为我们转换的规则:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
map_to_k3 = {'one': 1, 'two': 2}  # 定义一个字典作为映射的规则,one->1, two->2
df['k3'] = df['k1'].map(map_to_k3)  # 对df['k1']这个Series应用我们的映射规则,并得到一个新的Series作为我们的k3这一列
print(df)

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

    k1  k2  k3
0  one   1   1
1  two   1   2
2  one   2   1
3  two   3   2
4  one   3   1
5  two   4   2
6  two   4   2

我们也可以定义一个lambda函数来实现相同的功能:

df = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                   'k2': [1, 1, 2, 3, 3, 4, 4]})
print(df)
print()
map_to_k3 = {'one': 1, 'two': 2}  # 定义一个字典作为映射的规则,one->1, two->2
map_func = lambda x:map_to_k3[x]  # map_func的作用就是字典中按键取值的操作
df['k3'] = df['k1'].map(map_func)  # 对df['k1']这个Series应用我们的映射规则,并得到一个新的Series作为我们的k3这一列
print(df)

输出为:

    k1  k2
0  one   1
1  two   1
2  one   2
3  two   3
4  one   3
5  two   4
6  two   4

    k1  k2  k3
0  one   1   1
1  two   1   2
2  one   2   1
3  two   3   2
4  one   3   1
5  two   4   2
6  two   4   2

1.2.3 替换值

有些时候我们想要把Series或者DataFrame中的某个值换成另一个值,一个特例是我们之前fillna将NA值替换成某个具体数值的操作。更通用和灵活的方法是使用replace函数:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.replace(np.nan, 0))  # 我们要把np.nan换成0

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  1.0  6.5  3.0
1  1.0  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

如果我们希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.replace([np.nan, 1.0], 0))  # 我们要把np.nan和1.0都换成0

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0  0.0  6.5  3.0
1  0.0  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

如果要让每个值有不同的替换值,可以传递一个替换列表:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.replace([np.nan, 1.0], [0, -1]))  # 我们要把np.nan换成0,把1.0换成-1.0

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0 -1.0  6.5  3.0
1 -1.0  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

传入的参数也可以是字典:

df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
print(df)
print()
print(df.replace({np.nan:0, 1.0:-0.1}))  # 我们要把np.nan换成0,把1.0换成-1.0

输出为:

     0    1    2
0  1.0  6.5  3.0
1  1.0  NaN  NaN
2  NaN  NaN  NaN
3  NaN  6.5  3.0

     0    1    2
0 -0.1  6.5  3.0
1 -0.1  0.0  0.0
2  0.0  0.0  0.0
3  0.0  6.5  3.0

1.2.4 重命名轴索引

我们前面提过了Series或DataFrame的索引都可以通过reindex函数直接进行重新索引。而我们也可以跟Series中的值一样,通过函数或映射进行转换,从而得到一个新的索引:

df = pd.DataFrame(np.arange(9).reshape((3, 3)),
                     index=['a', 'c', 'd'],
                     columns=['Ohio', 'Texas', 'California'])
print(df)
map_func = lambda x:x.upper()  # 例如我们要把索引值都改成大写,直接调用字符串的upper函数
df.index = df.index.map(map_func)  # 直接对DataFrame的索引对象应用map_func
print(df)

输出为:

   Ohio  Texas  California
a     0      1           2
c     3      4           5
d     6      7           8
   Ohio  Texas  California
A     0      1           2
C     3      4           5
D     6      7           8

1.2.5 离散化和面元划分

连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,我们希望将它们划分为不同的年龄组:“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,我们需要使用pandas的cut函数:

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)  # 传入数据和我们用来分隔的区间
print(cats)

输出为:

[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

Pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。我们可以查看分隔之后的结果:

print(cats.codes)  # 查看每个数据点究竟属于哪个区间
print()
print(cats.categories)  # 查看所有区间
print()
print(pd.value_counts(cats))  # 用pandas的value_counts函数统计每一个区间中的元素个数

输出为:

[0 0 1 1 0 1 2 2 3 3 3 2]

IntervalIndex([(19.999, 22.75], (22.75, 29.0], (29.0, 38.0], (38.0, 61.0]],
              closed='right',
              dtype='interval[float64]')

(38.0, 61.0]       3
(29.0, 38.0]       3
(22.75, 29.0]      3
(19.999, 22.75]    3
dtype: int64

跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins, right=False)
print(cats.categories)

输出为:

IntervalIndex([[18, 25), [25, 35), [35, 60), [60, 100)],
              closed='left',
              dtype='interval[int64]')

qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
cats = pd.qcut(ages, 4)  # 我们要把ages大致分成4组,使得4组包含的元素个数差不多
print(cats.categories)  # qcut会自动计算区间如何划分
print()
print(pd.value_counts(cats))

输出为:

IntervalIndex([(19.999, 22.75], (22.75, 29.0], (29.0, 38.0], (38.0, 61.0]],
              closed='right',
              dtype='interval[float64]')

(38.0, 61.0]       3
(29.0, 38.0]       3
(22.75, 29.0]      3
(19.999, 22.75]    3
dtype: int64

1.2.6 检测和过滤异常值

我们可以通过一些条件来检测和过滤掉一些异常值,例如特别大或者特别小的值:

df = pd.DataFrame([[1, 3, 2, 4], [1, 1, 1, 1], [-1, -2, -4, -4]])
print(df)
print()
print(df[2][np.abs(df[2]) > 3])  # 找到某列中绝对值大小超过3的值
print()
print(df[(np.abs(df) > 3).any(1)])  # 用any函数,只要有一列绝对值大小超过3,就把这一行找出来

输出为:

   0  1  2  3
0  1  3  2  4
1  1  1  1  1
2 -1 -2 -4 -4

2   -4
Name: 2, dtype: int64

   0  1  2  3
0  1  3  2  4
2 -1 -2 -4 -4

找到这些值之后,我们可以用赋值的方式将它们限制在一个范围之内:

df[np.abs(df) > 3] = np.sign(df) * 3  # sign会给出这个数的正负号
print(df)

输出为:

   0  1  2  3
0  1  3  2  3
1  1  1  1  1
2 -1 -2 -3 -3

1.2.7 排列和随机采样

要对Series或者DataFrame中的数据进行重新排列,我们可以使用np.random.permutation函数,生成一个表示新顺序的整数数组,然后利用iloc的索引操作,或者是take函数:

df = pd.DataFrame([[1, 3, 2, 4], [1, 1, 1, 1], [-1, -2, -4, -4]])
print(df)
print()
sampler = np.random.permutation(3)
print(df.iloc[sampler])
print()
print(df.take(sampler))

输出为:

   0  1  2  3
0  1  3  2  4
1  1  1  1  1
2 -1 -2 -4 -4

   0  1  2  3
0  1  3  2  4
1  1  1  1  1
2 -1 -2 -4 -4

   0  1  2  3
0  1  3  2  4
1  1  1  1  1
2 -1 -2 -4 -4

我们也可以使用sample函数进行采样。指明replace=True可以允许重复选择,即有放回的采样。

df = pd.DataFrame([[1, 3, 2, 4], [1, 1, 1, 1], [-1, -2, -4, -4]])
print(df)
print()
print(df.sample(3))
print()
print(df.sample(10, replace=True))
# print(df.sample(10))  # 如果不指明replace=True,并尝试采样的数量超过数据总量时会报错

输出为:

   0  1  2  3
0  1  3  2  4
1  1  1  1  1
2 -1 -2 -4 -4

   0  1  2  3
1  1  1  1  1
0  1  3  2  4
2 -1 -2 -4 -4

   0  1  2  3
0  1  3  2  4
0  1  3  2  4
2 -1 -2 -4 -4
2 -1 -2 -4 -4
0  1  3  2  4
1  1  1  1  1
0  1  3  2  4
2 -1 -2 -4 -4
1  1  1  1  1
1  1  1  1  1

2 层次化索引

层次化索引(hierarchical indexing)是Pandas的一项重要功能,它使我们能在一个轴上拥有多个(两个以上)索引级别。

data = pd.Series(np.random.randn(9),
                 index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
                        [1, 2, 3, 1, 3, 1, 2, 2, 3]])  # 传入一个列表的列表,作为两层的索引
print(data)
print()
print(data.index)

输出为:

a  1   -1.648029
   2    0.420233
   3    1.034082
b  1    0.758560
   3   -1.871431
c  1    0.107052
   2    1.128419
d  2    0.601011
   3    0.984184
dtype: float64

MultiIndex(levels=[['a', 'b', 'c', 'd'], [1, 2, 3]],
           codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 0, 2, 0, 1, 1, 2]])

我们在选取了外层的数据之后,还可以在内层继续选取,或者直接按内层索引进行选取:

print(data['b'][3])  # 外层索引用'b',内层索引用3
print()
print(data.loc[:, 2])  # 直接选取内层索引为2的数据

输出为:

-1.2163750584353654

a   -0.709735
c    0.993300
d   -0.152503
dtype: float64

通过unstack函数,我们可以把这种两层索引的Series重新安排到一个DataFrame中,而stack刚好是相反的操作:

print(data.unstack())
print()
print(data.unstack().stack())

输出为:

          1         2         3
a  0.548756 -0.709735  2.586256
b  0.107833       NaN -1.216375
c -0.195742  0.993300       NaN
d       NaN -0.152503 -0.217671

a  1    0.548756
   2   -0.709735
   3    2.586256
b  1    0.107833
   3   -1.216375
c  1   -0.195742
   2    0.993300
d  2   -0.152503
   3   -0.217671
dtype: float64

对于一个DataFrame,每条轴都可以有分层索引:

frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
                     index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                     columns=[['Ohio', 'Ohio', 'Colorado'],
                              ['Green', 'Red', 'Green']])
print(frame)

输出为:

     Ohio     Colorado
    Green Red    Green
a 1     0   1        2
  2     3   4        5
b 1     6   7        8
  2     9  10       11

每层索引都可以指明名字:

frame.index.names = ['key1', 'key2']
frame.columns.names = ['state', 'color']
print(frame)

输出为:

state      Ohio     Colorado
color     Green Red    Green
key1 key2                   
a    1        0   1        2
     2        3   4        5
b    1        6   7        8
     2        9  10       11

3 合并数据集

Pandas提供了merge,join,concat等函数将不同的数据集组合成一个数据集。

3.1 concat

concat可以沿着一条轴将多个对象堆叠到一起。默认情况是按列推叠,相同的列的数据会被推到一起:

df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                    'B': ['B0', 'B1', 'B2', 'B3'],
                    'C': ['C0', 'C1', 'C2', 'C3'],
                    'D': ['D0', 'D1', 'D2', 'D3']},
                   index=[0, 1, 2, 3])

df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
                    'B': ['B4', 'B5', 'B6', 'B7'],
                    'C': ['C4', 'C5', 'C6', 'C7'],
                    'D': ['D4', 'D5', 'D6', 'D7']},
                   index=[4, 5, 6, 7])

df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
                    'B': ['B8', 'B9', 'B10', 'B11'],
                    'C': ['C8', 'C9', 'C10', 'C11'],
                    'D': ['D8', 'D9', 'D10', 'D11']},
                   index=[8, 9, 10, 11])
frames = [df1, df2, df3]
result = pd.concat(frames)
print(result)

输出为:

      A    B    C    D
0    A0   B0   C0   D0
1    A1   B1   C1   D1
2    A2   B2   C2   D2
3    A3   B3   C3   D3
4    A4   B4   C4   D4
5    A5   B5   C5   D5
6    A6   B6   C6   D6
7    A7   B7   C7   D7
8    A8   B8   C8   D8
9    A9   B9   C9   D9
10  A10  B10  C10  D10
11  A11  B11  C11  D11

图片说明

我们可以通过keys参数指明外层的索引:

result = pd.concat(frames, keys=['x', 'y', 'z'])
print(result)

输出为:

        A    B    C    D
x 0    A0   B0   C0   D0
  1    A1   B1   C1   D1
  2    A2   B2   C2   D2
  3    A3   B3   C3   D3
y 4    A4   B4   C4   D4
  5    A5   B5   C5   D5
  6    A6   B6   C6   D6
  7    A7   B7   C7   D7
z 8    A8   B8   C8   D8
  9    A9   B9   C9   D9
  10  A10  B10  C10  D10
  11  A11  B11  C11  D11

图片说明

或者我们可以通过传入一个字典的形式实现上面一样的功能:

pieces = {'x': df1, 'y': df2, 'z': df3}
result = pd.concat(pieces)
print(result)

输出为:

        A    B    C    D
x 0    A0   B0   C0   D0
  1    A1   B1   C1   D1
  2    A2   B2   C2   D2
  3    A3   B3   C3   D3
y 4    A4   B4   C4   D4
  5    A5   B5   C5   D5
  6    A6   B6   C6   D6
  7    A7   B7   C7   D7
z 8    A8   B8   C8   D8
  9    A9   B9   C9   D9
  10  A10  B10  C10  D10
  11  A11  B11  C11  D11

这样我们可以比较方便地取回对应原来数据集的部分:

print(result.loc['y'])

输出为:

    A   B   C   D
4  A4  B4  C4  D4
5  A5  B5  C5  D5
6  A6  B6  C6  D6
7  A7  B7  C7  D7

我们也可以通过axis指明推叠方向:

df4 = pd.DataFrame({'B': ['B2', 'B3', 'B6', 'B7'],
                    'D': ['D2', 'D3', 'D6', 'D7'],
                    'F': ['F2', 'F3', 'F6', 'F7']},
                   index=[2, 3, 6, 7])
result = pd.concat([df1, df4], axis=1)  # 指明按行进行推叠
print(result)

输出为:

     A    B    C    D    B    D    F
0   A0   B0   C0   D0  NaN  NaN  NaN
1   A1   B1   C1   D1  NaN  NaN  NaN
2   A2   B2   C2   D2   B2   D2   F2
3   A3   B3   C3   D3   B3   D3   F3
6  NaN  NaN  NaN  NaN   B6   D6   F6
7  NaN  NaN  NaN  NaN   B7   D7   F7

图片说明

在上面的默认情况下,join的方式被指定为outer(并集),也就是说只要df1或者df4中有的行索引都会被保留下来,我们也可以指明join='inner'(交集)来仅仅保留df1和df4中都有的行索引:

result = pd.concat([df1, df4], axis=1, join='inner')
print(result)

输出为:

    A   B   C   D   B   D   F
2  A2  B2  C2  D2  B2  D2  F2
3  A3  B3  C3  D3  B3  D3  F3

图片说明

或者我们指明使用df1的行索引:

result = pd.concat([df1, df4], axis=1, join_axes=[df1.index])
print(result)

输出为:

    A   B   C   D    B    D    F
0  A0  B0  C0  D0  NaN  NaN  NaN
1  A1  B1  C1  D1  NaN  NaN  NaN
2  A2  B2  C2  D2   B2   D2   F2
3  A3  B3  C3  D3   B3   D3   F3

图片说明

append函数的功能和concat相同,但是append是应用在某一个Series或DataFrame上的:

result = df1.append(df2)
print(result)

输出为:

    A   B   C   D
0  A0  B0  C0  D0
1  A1  B1  C1  D1
2  A2  B2  C2  D2
3  A3  B3  C3  D3
4  A4  B4  C4  D4
5  A5  B5  C5  D5
6  A6  B6  C6  D6
7  A7  B7  C7  D7

图片说明

在应用append时,两个DataFrame的列不一定要完全相同:

result = df1.append(df4)
print(result)

输出为:

     A   B    C   D    F
0   A0  B0   C0  D0  NaN
1   A1  B1   C1  D1  NaN
2   A2  B2   C2  D2  NaN
3   A3  B3   C3  D3  NaN
2  NaN  B2  NaN  D2   F2
3  NaN  B3  NaN  D3   F3
6  NaN  B6  NaN  D6   F6
7  NaN  B7  NaN  D7   F7

图片说明

我们也可以在append中使用列表,同时推叠多个DataFrame:

result = df1.append([df2, df3])
print(result)

输出为:

      A    B    C    D
0    A0   B0   C0   D0
1    A1   B1   C1   D1
2    A2   B2   C2   D2
3    A3   B3   C3   D3
4    A4   B4   C4   D4
5    A5   B5   C5   D5
6    A6   B6   C6   D6
7    A7   B7   C7   D7
8    A8   B8   C8   D8
9    A9   B9   C9   D9
10  A10  B10  C10  D10
11  A11  B11  C11  D11

图片说明

如果我们不想保留原来的DataFrame的行索引,我们可以在concat函数或append函数中指明ignore_index=True:

result = pd.concat([df1, df4], ignore_index=True)
print(result)

输出为:

     A   B    C   D    F
0   A0  B0   C0  D0  NaN
1   A1  B1   C1  D1  NaN
2   A2  B2   C2  D2  NaN
3   A3  B3   C3  D3  NaN
4  NaN  B2  NaN  D2   F2
5  NaN  B3  NaN  D3   F3
6  NaN  B6  NaN  D6   F6
7  NaN  B7  NaN  D7   F7

图片说明

我们也可以拼接一个DataFrame和一个Series,在拼接的过程中,Series会被自动转成DataFrame:

s1 = pd.Series(['X0', 'X1', 'X2', 'X3'], name='X')
result = pd.concat([df1, s1], axis=1)
print(result)

输出为:

    A   B   C   D   X
0  A0  B0  C0  D0  X0
1  A1  B1  C1  D1  X1
2  A2  B2  C2  D2  X2
3  A3  B3  C3  D3  X3

图片说明

如果Series没有指明name,那么列名会被自动计数填充:

s2 = pd.Series(['_0', '_1', '_2', '_3'])
result = pd.concat([df1, s2, s2, s2], axis=1)
print(result)

输出为:

    A   B   C   D   0   1   2
0  A0  B0  C0  D0  _0  _0  _0
1  A1  B1  C1  D1  _1  _1  _1
2  A2  B2  C2  D2  _2  _2  _2
3  A3  B3  C3  D3  _3  _3  _3

图片说明

如果指明参数ignore_index=True,那么所有的列名都会被重新编号:

result = pd.concat([df1, s1], axis=1, ignore_index=True)
print(result)

输出为:

    0   1   2   3   4
0  A0  B0  C0  D0  X0
1  A1  B1  C1  D1  X1
2  A2  B2  C2  D2  X2
3  A3  B3  C3  D3  X3

图片说明

我们也可以通过append函数将一个Series作为新的一行加到一个DataFrame中,在这种情况下我们一般会指明ignore_index=True:

s2 = pd.Series(['X0', 'X1', 'X2', 'X3'], index=['A', 'B', 'C', 'D'])
result = df1.append(s2, ignore_index=True)
print(result)

输出为:

    A   B   C   D
0  A0  B0  C0  D0
1  A1  B1  C1  D1
2  A2  B2  C2  D2
3  A3  B3  C3  D3
4  X0  X1  X2  X3

图片说明

3.2 merge

merge可以根据一个或多个键将不同DataFrame中的行连接起来,来看具体的例子:

left = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                     'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3']})
right = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                      'C': ['C0', 'C1', 'C2', 'C3'],
                      'D': ['D0', 'D1', 'D2', 'D3']})
result = pd.merge(left, right, on='key')  # 参数on指明了我们要进行连接的依据
print(result)

输出为:

  key   A   B   C   D
0  K0  A0  B0  C0  D0
1  K1  A1  B1  C1  D1
2  K2  A2  B2  C2  D2
3  K3  A3  B3  C3  D3

图片说明

在上面的例子中,left和right的具有相同key的行会被连接成result中的一行。我们也可以在参数on中指明多列作为连接的依据,默认情况下会采取how='inner',即取交集的方式:

left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],
                     'key2': ['K0', 'K1', 'K0', 'K1'],
                     'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3']})
right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],
                      'key2': ['K0', 'K0', 'K0', 'K0'],
                      'C': ['C0', 'C1', 'C2', 'C3'],
                      'D': ['D0', 'D1', 'D2', 'D3']})
result = pd.merge(left, right, on=['key1', 'key2'])
print(result)

输出为:

  key1 key2   A   B   C   D
0   K0   K0  A0  B0  C0  D0
1   K1   K0  A2  B2  C1  D1
2   K1   K0  A2  B2  C2  D2

图片说明

我们也可以修改控制连接方式的参数how为outer(并集),left(用第一个DataFrame中的数值),right(用第二个DataFrame中的数值):

result = pd.merge(left, right, how='left', on=['key1', 'key2'])
print(result)

输出为:

  key1 key2   A   B    C    D
0   K0   K0  A0  B0   C0   D0
1   K0   K1  A1  B1  NaN  NaN
2   K1   K0  A2  B2   C1   D1
3   K1   K0  A2  B2   C2   D2
4   K2   K1  A3  B3  NaN  NaN

图片说明

result = pd.merge(left, right, how='right', on=['key1', 'key2'])
print(result)

输出为:

  key1 key2    A    B   C   D
0   K0   K0   A0   B0  C0  D0
1   K1   K0   A2   B2  C1  D1
2   K1   K0   A2   B2  C2  D2
3   K2   K0  NaN  NaN  C3  D3

图片说明

result = pd.merge(left, right, how='outer', on=['key1', 'key2'])
print(result)

输出为:

  key1 key2    A    B    C    D
0   K0   K0   A0   B0   C0   D0
1   K0   K1   A1   B1  NaN  NaN
2   K1   K0   A2   B2   C1   D1
3   K1   K0   A2   B2   C2   D2
4   K2   K1   A3   B3  NaN  NaN
5   K2   K0  NaN  NaN   C3   D3

图片说明

如果left和right这两个DataFrame除了用来连接的列之外,还有相同的列的时候,pandas会自动地在result中把这些列加以区分:

left = pd.DataFrame({'A' : [1,2], 'B' : [2, 2]})
right = pd.DataFrame({'A' : [4,5,6], 'B': [2,2,2]})
result = pd.merge(left, right, on='B', how='outer')  # left和right的列都是A和B,但是在连接时只使用B作为依据
print(result)

输出为:

   A_x  B  A_y
0    1  2    4
1    1  2    5
2    1  2    6
3    2  2    4
4    2  2    5
5    2  2    6

图片说明

如果我们不想采用这种默认的区分方式,也可以自己指明后缀suffixes:

left = pd.DataFrame({'A' : [1,2], 'B' : [2, 2]})
right = pd.DataFrame({'A' : [4,5,6], 'B': [2,2,2]})
result = pd.merge(left, right, on='B', how='outer', suffixes=['_left', '_right'])
print(result)

输出为:

   A_left  B  A_right
0       1  2        4
1       1  2        5
2       1  2        6
3       2  2        4
4       2  2        5
5       2  2        6

3.3 join

join可以按照行索引,将两个DataFrame连接成一个,有点类似merge,但是默认的连接方式是how='left':

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                    index=['K0', 'K1', 'K2'])
right = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K3'])
result = left.join(right)  # 用left去join right
print(result)
print()
# 用merge其实也能实现相同的功能,不过比较繁琐,不指明参数on,而是指明left_index=True, right_index=True来说明我们要按照行索引进行连接
result = pd.merge(left, right, left_index=True, right_index=True, how='left')
print(result)

输出为:

     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C2   D2

     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C2   D2

图片说明

在join中,我们也可以指明连接方式为inner或outer:

result = left.join(right, how='inner')
print(result)

输出为:

     A   B   C   D
K0  A0  B0  C0  D0
K2  A2  B2  C2  D2

图片说明

result = left.join(right, how='outer')
print(result)

输出为:

      A    B    C    D
K0   A0   B0   C0   D0
K1   A1   B1  NaN  NaN
K2   A2   B2   C2   D2
K3  NaN  NaN   C3   D3

图片说明

当left和right出现重复的列时,join会要求我们指明后缀,来区分来自不同DataFrame中的相同的列:

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                    index=['K0', 'K1', 'K2'])
right = pd.DataFrame({'A': ['C0', 'C2', 'C3'],
                      'B': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K3'])
# result = left.join(right)  # 直接join会报错
result = left.join(right, lsuffix='_left', rsuffix='_right')  # 指明后缀
print(result)

输出为:

   A_left B_left A_right B_right
K0     A0     B0      C0      D0
K1     A1     B1     NaN     NaN
K2     A2     B2      C2      D2

4 分组计算

4.1 GroupBy机制

pandas利用GroupBy机制来实现分组计算,具体的过程包括“split-apply-combine”(拆分-应用-合并):

  • pandas对象(包括Series和DataFrame)按照某个键被拆分(split)为多组,例如按照行索引或列索引;
  • 然后,我们可以将一个函数应用(apply)到各个分组并产生一个新值;
  • 最后,所有这些函数的结果会被合并(combine)到最终的结果对象中。

整个流程可以用下图说明:
图片说明

我们来看具体的例子。对于一个DataFrame,我们想要按照key1分组,然后计算data1列的平均值:

df = pd.DataFrame({'key1': ['a', 'a', 'b', 'b', 'a'],
                   'key2': ['one', 'two', 'one', 'two', 'one'],
                   'data1': np.random.randn(5),
                   'data2': np.random.randn(5)})
print(df)
print()
grouped = df['data1'].groupby(df['key1'])  # 对data1按照key1进行分组
print(grouped)  # 得到一个GroupBy对象,此时只是中间数据,还没有进行任何计算
print()
print(grouped.mean())  # 然后我们调用平均值函数,就可以得到分组平均值

输出为:

  key1 key2     data1     data2
0    a  one -0.644855 -1.171283
1    a  two  0.557939 -2.207411
2    b  one  1.008353  1.138036
3    b  two  1.132750  0.189582
4    a  one  0.223394 -0.318009

<pandas.core.groupby.generic.SeriesGroupBy object at 0x0000026FE4313D68>

key1
a    0.045493
b    1.070552
Name: data1, dtype: float64

如果我们通过两个键进行分组,就会得到一个层次化索引的Series:

means = df['data1'].groupby([df['key1'], df['key2']]).mean()  # 通过key1和key2进行分组
print(means)

输出为:

key1  key2
a     one    -0.210730
      two     0.557939
b     one     1.008353
      two     1.132750
Name: data1, dtype: float64

4.2 对分组进行迭代

GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成):

print(df)  # 用之前定义好的DataFrame
print()
for name, group in df.groupby('key1'):
    print(name)
    print(group)

输出为:

  key1 key2     data1     data2
0    a  one -0.644855 -1.171283
1    a  two  0.557939 -2.207411
2    b  one  1.008353  1.138036
3    b  two  1.132750  0.189582
4    a  one  0.223394 -0.318009

a
  key1 key2     data1     data2
0    a  one -0.644855 -1.171283
1    a  two  0.557939 -2.207411
4    a  one  0.223394 -0.318009
b
  key1 key2     data1     data2
2    b  one  1.008353  1.138036
3    b  two  1.132750  0.189582

我们也可以把上面的结果做成一个字典:

pieces = dict(list(df.groupby('key1')))
print(pieces['b'])

输出为:

  key1 key2     data1     data2
2    b  one  1.008353  1.138036
3    b  two  1.132750  0.189582

对于多重键的情况,元组的第一个元素将会是由键值组成的元组:

for (k1, k2), group in df.groupby(['key1', 'key2']):
    print((k1, k2))
    print(group)

输出为:

('a', 'one')
  key1 key2     data1     data2
0    a  one -0.644855 -1.171283
4    a  one  0.223394 -0.318009
('a', 'two')
  key1 key2     data1     data2
1    a  two  0.557939 -2.207411
('b', 'one')
  key1 key2     data1     data2
2    b  one  1.008353  1.138036
('b', 'two')
  key1 key2    data1     data2
3    b  two  1.13275  0.189582

4.3 选取一列或列的子集

对于由DataFrame产生的GroupBy对象,如果用一个(单个字符串)或一组(字符串数组)列名对其进行索引,就能实现选取部分列进行聚合的目的。

print(df.groupby('key1')['data1'])  # 得到一个数据为Series的GroupBy对象
print()
print(df.groupby('key1')[['data2']])  # 得到一个数据为DataFrame的GroupBy对象
print()
print(df.groupby('key1')[['data2']].mean())

输出为:

<pandas.core.groupby.generic.SeriesGroupBy object at 0x0000026FE43F18D0>

<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000026FE43357F0>

         data2
key1          
a    -1.232234
b     0.663809

4.4 apply

除了上面看到的mean、sum等内置的函数,我们也可以定义自己的函数,然后用apply函数进行调用:

def f(x):
    return np.random.choice(x, 1)  # 从一列中的每组数据中随机选一个作为结果

print(df.groupby('key1')['data1'].apply(f))  # 这里函数f接受的是一维的输入,因此我们取了data1这一列作为输入

输出为:

key1
a    [0.22339406178719257]
b     [1.0083533433635783]
Name: data1, dtype: object

如果我们要应用的函数f还有参数,我们也可以在apply函数中指明相对应的参数,它们会传递到函数f中:

def f(x, n=1):
    return np.random.choice(x, n)  # 从一列中的每组数据中随机选n个作为结果

print(df.groupby('key1')['data1'].apply(f, n=2))  # 我们可以在apply中指明n=2,它会被自动地应用到函数f中

输出为:

key1
a    [0.5579390947161788, 0.22339406178719257]
b     [1.1327497534971687, 1.1327497534971687]
Name: data1, dtype: object

回顾之前我们提到的fillna的操作,我们也可以采用apply函数,实现按分组填充平均值的操作:

df = pd.DataFrame({'key1': ['a', 'a', 'b', 'b', 'a'],
                   'key2': ['one', 'two', 'one', 'two', 'one'],
                   'data1': [1, 2, 3, 4, 5],
                   'data2': [6, 7, 8, 9, 10]})
df.loc[1, 'data1'] = np.nan  # 我们人为添加一些缺失值作为演示
df.loc[3, 'data2'] = np.nan
print(df)
print()
print(df.fillna(df.mean()))  # 直接调用fillna会填充全列的平均值

输出为:

  key1 key2  data1  data2
0    a  one    1.0    6.0
1    a  two    NaN    7.0
2    b  one    3.0    8.0
3    b  two    4.0    NaN
4    a  one    5.0   10.0

  key1 key2  data1  data2
0    a  one   1.00   6.00
1    a  two   3.25   7.00
2    b  one   3.00   8.00
3    b  two   4.00   7.75
4    a  one   5.00  10.00

我们可以用分组平均值去填充NA:

fill_mean = lambda g: g.fillna(g.mean())  # 相当于我们把填充平均值的操作在每一个分组内的DataFrame上进行操作
print(df.groupby('key1').apply(fill_mean))  # 按照key1进行分组再填充
print()
print(df.groupby('key2').apply(fill_mean))  # 换了分组的依据,填充的值也变得不一样

输出为:

       key1 key2  data1  data2
key1                          
a    0    a  one    1.0    6.0
     1    a  two    3.0    7.0
     4    a  one    5.0   10.0
b    2    b  one    3.0    8.0
     3    b  two    4.0    8.0

       key1 key2  data1  data2
key2                          
one  0    a  one    1.0    6.0
     2    b  one    3.0    8.0
     4    a  one    5.0   10.0
two  1    a  two    4.0    7.0
     3    b  two    4.0    7.0

小结

这一章介绍了Pandas在实际问题中的常见数据处理技术,这些技术构成了大型数据处理问题的基础,需要大家熟练掌握并且在以后的问题中灵活运用。

全部评论

相关推荐

牛客583549203号:腾讯还好,况且实习而已,实习生流动性很大,属于正常现象,记得和HR委婉解释
点赞 评论 收藏
分享
04-11 21:31
四川大学 Java
野猪不是猪🐗:(ja)va学弟这招太狠了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务