将不同的数据源进行合并,这既包括将两个不同的数据集非常简单地拼接在一起,也包括用数据库那样的连接(join)和合并(merge)操作处理有重叠字段的数据集。
先来用 pd.concat 函数演示一个 Series 与 DataFrame 的简单合并操作。简单起见,定义一个能够创建 DataFrame 某种形式的函数:
def make_df(cols,ind):
'''一个简单的 DataFrame '''
data = {c:[str(c) + str(i) for i in ind]
for c in cols}
return pd.DataFrame(data,ind)
# DataFrame 示例
print(make_df('ABC',range(3)))
'''
A B C
0 A0 B0 C0
1 A1 B1 C1
2 A2 B2 C2
'''
合并 Series 和 DataFrame 与合并 NumPy 数组基本相同,可以使用 np.concatenate 函数即可完成:
x = [1,2,3]
y = [4,5,6]
z = [7,8,9]
print(np.concatenate([x,y,z])) # [1 2 3 4 5 6 7 8 9]
第一个参数是需要合并的数组列表或元组。还有一个 axis 参数可以设置合并的坐标轴方向:
x = [[1,2],
[3,4]]
print(np.concatenate([x,x],axis=1))
'''
[[1 2 1 2]
[3 4 3 4]]
'''
Pandas 有一个 pd.concat() 函数与 np.concatenate 语法类似,但是配置参数更多,功能也更强大:
# Pandas 的函数签名
# print(pd.concat(objs,axis=0,join='outer',join_axes=None,ignore_index=False,
# keys=None,levels=None,names=None,verify_integrity=False,
# copy=True))
pd.concat() 可以简单地合并一维的 Series 或 DataFrame 对象,与 np.concatenate() 合并数组一样:
ser1 = pd.Series(['A','B','C'],index=[1,2,3])
ser2 = pd.Series(['D','E','F'],index=[4,5,6])
print(pd.concat([ser1,ser2]))
'''
1 A
2 B
3 C
4 D
5 E
6 F
dtype: object
'''
它也可以用来合并高维数据,例如下面的 DataFrame:
df1 = make_df('AB',[1,2])
df2 = make_df('AB',[3,4])
print(df1);print(df2);print(pd.concat([df1,df2]))
'''
A B
1 A1 B1
2 A2 B2
A B
3 A3 B3
4 A4 B4
A B
1 A1 B1
2 A2 B2
3 A3 B3
4 A4 B4
'''
默认情况下,DataFrame 的合并都是逐行进行的(默认设置是 axis = 0)。与 np.concatennate() 一样,pd.concat 也可以设置合并坐标轴,例如:
df3 = make_df('AB',[0,1])
df4 = make_df('CD',[0,1])
print(df3);print(df4);print(pd.concat([df3,df4],axis=1))
'''
A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1
'''
索引重复
np.concatenate 与 pd0.concat 最主要的差异之一就是 Pandas 在合并时会保留索引,即使索引是重复的!例如:
x = make_df('AB',[0,1])
y = make_df('AB',[2,3])
y.index = x.index # 复制索引
print(x);
'''
A B
0 A0 B0
1 A1 B1
'''
print(y);
'''
A B
0 A2 B2
1 A3 B3
'''
print(pd.concat([x,y]))
'''
A B
0 A0 B0
1 A1 B1
0 A2 B2
1 A3 B3
'''
你会发现结果中的索引是重复的。虽然 DataFrame 允许这么做,但结果并不是我们想要的。pd.concat() 提供了一些解决这个问题的方法。
(1) 捕捉索引重复的错误。如果你想要检测 pd.concat() 合并的结果中是否出现了重复的索引,可以设置 vertify_integrity 参数。将参数设置为 True,合并时若是有索引重复就会触发异常。
try:
pd.concat([x,y],verify_integrity=True)
except ValueError as e:
print("ValueError:",e) # ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype='int64')
(2) 忽略索引。有时索引无关紧要,那么合并时就可以忽略它们,可以通过设置 ignore_index 参数来实现。如果将参设置为 True,那么合并时将会创建一个新的整数索引。
print(x);
'''
A B
0 A0 B0
1 A1 B1
'''
print(y);
'''
A B
0 A2 B2
1 A3 B3
'''
print(pd.concat([x,y],ignore_index=True))
'''
A B
0 A0 B0
1 A1 B1
2 A2 B2
3 A3 B3
'''
(3) 增加多级索引。另一种处理索引重复的方法是通过 keys 参数为数据源设置多级索引标签,这样结果数据就会带上多级索引:
print(x);
'''
A B
0 A0 B0
1 A1 B1
'''
print(y);
'''
A B
0 A2 B2
1 A3 B3
'''
print(pd.concat([x,y],keys=['x','y']))
'''
A B
x 0 A0 B0
1 A1 B1
y 0 A2 B2
1 A3 B3
'''
类似 join 的合并
前面的示例都有一个共同特点,那就是合并的 DataFrame 都是同样的列名。而在实际工作中,需要合并的数据往往带有不同的列名,而 pd.concat 提供了一些选项来解决这个问题。例如下面的两个 DataFrame,它们的列名部分相同,却又不完全相同:
df5 = make_df('ABC',[1,2])
df6 = make_df('BCD',[3,4])
print(df5);
'''
A B C
1 A1 B1 C1
2 A2 B2 C2
'''
print(df6);
'''
B C D
3 B3 C3 D3
4 B4 C4 D4
'''
print(pd.concat([df5,df6],sort=False))
'''
A B C D
1 A1 B1 C1 NaN
2 A2 B2 C2 NaN
3 NaN B3 C3 D3
4 NaN B4 C4 D4
'''
默认情况下,某个位置上缺失的数据会用 NaN 表示。如果不想这样,可以用 join 和 join_axes 参数设置合并方式。默认的合并方式是所有输入列进行并集合并(join = ‘outer’),当然也可以用 join = ‘inner’ 实现对输入列的交集合并:
print(pd.concat([df5,df6],join='inner'))
'''
B C
1 B1 C1
2 B2 C2
3 B3 C3
4 B4 C4
'''
另一种合并方式是直接确定结果使用的列名,设置 join_axes 参数,里面是索引对象构成的列表(是列表的列表)。如下面的示例,将结果的列名设置为第一个输入的列名:
print(pd.concat([df5,df6],join_axes=[df5.columns]))
'''
A B C
1 A1 B1 C1
2 A2 B2 C2
3 NaN B3 C3
4 NaN B4 C4
'''
append() 方法
因为直接进行数组合并的需求非常普遍,所以 Series 和 DataFrame 对象都支持 append 方法,让你通过最少的代码实现合并的功能。例如,使用 df1.append(df2),效果与 pd.concat([df1,df2]) 一样:
print(df1.append(df2))
'''
A B
1 A1 B1
2 A2 B2
3 A3 B3
4 A4 B4
'''
Pandas 的基本特性之一就是高性能的内存式数据连接(join)与合并(merge)操作。
pd.merge() 实现的功能基于关系代数的一部分。关系代数是处理关系型数据库的通用理论,绝大部分数据库的可用操作都以此为理论基础。
pd.merge() 函数实现了三种数据连接的类型:一对一、一对多和多对多。这三种数据连接类型都通过 pd.merge() 接口进行调用,根据不同的数据连接需求进行不同的操作。
一对一
df1 = pd.DataFrame({'employee':['Bob','Jake','Lisa','Sue'],
'group':['Accounting','Engineering','Engineering','HR']})
df2 = pd.DataFrame({'employee':['Lisa','Bob','Jake','Sue'],
'hire_date':[2004,2008,2012,2014]})
print(df1);
'''
employee group
0 Bob Accounting
1 Jake Engineering
2 Lisa Engineering
3 Sue HR
'''
print(df2)
'''
employee hire_date
0 Lisa 2004
1 Bob 2008
2 Jake 2012
3 Sue 2014
'''
若想将这两个 DataFrame 合并成一个 DataFrame,可以用 pd.merge() 实现:
df3 = pd.merge(df1,df2)
print(df3)
'''
employee group hire_date
0 Bob Accounting 2008
1 Jake Engineering 2012
2 Lisa Engineering 2004
3 Sue HR 2014
'''
pd.merge() 方法会发现两个 DataFrame 都有 “employee” 列,并会自动以这列作为键进行连接。两个输入的合并结果是一个新的 DataFrame。需要注意的是,共同列的位置可以是不一致的。例如在这个例子中,虽然 df1 和 df2 中 “employee” 列的位置是不一样的,但是 pd.merge 函数会正确处理这个问题。另外还需注意的是,pd.merge() 会默认丢掉原来的行索引,不过也可以自定义。
多对一
多对一指的是,在需要连接的两个列中,有一列的值有重复。通过多对一连接获得的结果 DataFrame 将会保留重复值。
df4 = pd.DataFrame({'group':['Accounting','Engineering','HR'],
'supervisor':['Carly','Guido','Steve']})
print(df3);
'''
employee group hire_date
0 Bob Accounting 2008
1 Jake Engineering 2012
2 Lisa Engineering 2004
3 Sue HR 2014
'''
print(df4);
'''
group supervisor
0 Accounting Carly
1 Engineering Guido
2 HR Steve
'''
print(pd.merge(df3,df4))
'''
employee group hire_date supervisor
0 Bob Accounting 2008 Carly
1 Jake Engineering 2012 Guido
2 Lisa Engineering 2004 Guido
3 Sue HR 2014 Steve
'''
在结果 DataFrame 中多了一个 “supervisor” 列,里面有些值会因为输入数据的对应关系而有所重复。
多对多
多对多连接是个有点复杂的概念,不过也可以理解。如果左右两个输入的共同列都包含重复值,那么合并的结果就是一种多对多连接。下面的例子,里面有一个 DataFrame 显示不同岗位人员的一种或多种能力。
通过多对多连接,就可以知道每位员工所具备的能力:
df5 = pd.DataFrame({'group':['Accounting','Accounting',
'Engineering','Engineering','HR','HR'],
'skills':['math','spreadsheets','coding','linux',
'spreadsheets','organizaion']})
print(df1);
'''
employee group
0 Bob Accounting
1 Jake Engineering
2 Lisa Engineering
3 Sue HR
'''
print(df5);
'''
group skills
0 Accounting math
1 Accounting spreadsheets
2 Engineering coding
3 Engineering linux
4 HR spreadsheets
5 HR organizaion
'''
print(pd.merge(df1,df5))
'''
employee group skills
0 Bob Accounting math
1 Bob Accounting spreadsheets
2 Jake Engineering coding
3 Jake Engineering linux
4 Lisa Engineering coding
5 Lisa Engineering linux
6 Sue HR spreadsheets
7 Sue HR organizaion
'''
参数 on 的用法
最简单的方法就是直接将参数 on 设置为一个列名字符串或者一个包含多列名称的列表:
print(df6);
'''
B C D
3 B3 C3 D3
4 B4 C4 D4
'''
print(df2);
'''
employee hire_date
0 Lisa 2004
1 Bob 2008
2 Jake 2012
3 Sue 2014
'''
print(pd.merge(df1,df2,on='employee'))
'''
employee group hire_date
0 Bob Accounting 2008
1 Jake Engineering 2012
2 Lisa Engineering 2004
3 Sue HR 2014
'''
这个参数只能在两个 DataFrame 有共同列名的时候才可以使用。
left_on 与 right_on 参数
有时你也需要合并两个列名不同的数据集,例如前面的员工信息表中有一个字段不是 “employee” 而是 “name”,就可以使用 left_on 和 right_on 参数来指定列名:
df3 = pd.DataFrame({'name':['Bob','Jake','Lisa','Sue'],
'salary':[70000,80000,120000,90000]})
print(df1);
'''
employee group
0 Bob Accounting
1 Jake Engineering
2 Lisa Engineering
3 Sue HR
'''
print(df3);
'''
name salary
0 Bob 70000
1 Jake 80000
2 Lisa 120000
3 Sue 90000
'''
print(pd.merge(df1,df3,left_on='employee',right_on='name'))
'''
employee group name salary
0 Bob Accounting Bob 70000
1 Jake Engineering Jake 80000
2 Lisa Engineering Lisa 120000
3 Sue HR Sue 90000
'''
获取的结果中会有一个多余的列,可以通过 DataFrame 的 drop() 方法将这列去掉:
print(pd.merge(df1,df3,left_on='employee',right_on='name').drop('name',axis=1))
'''
employee group salary
0 Bob Accounting 70000
1 Jake Engineering 80000
2 Lisa Engineering 120000
3 Sue HR 90000
'''
left_index 和 right_index 参数
除了合并列之外,你还可能需要合并索引。
df1a = df1.set_index('employee')
df2a = df2.set_index('employee')
print(df1a);
'''
group
employee
Bob Accounting
Jake Engineering
Lisa Engineering
Sue HR
'''
print(df2a)
'''
hire_date
employee
Lisa 2004
Bob 2008
Jake 2012
Sue 2014
'''
你可以通过设置 pd.merge() 中的 left_index 和 / 或 right_index 参数将索引设置为键来实现合并:
print(pd.merge(df1a,df2a,left_index=True,right_index=True))
'''
group hire_date
employee
Bob Accounting 2008
Jake Engineering 2012
Lisa Engineering 2004
Sue HR 2014
'''
为了方便考虑,DataFrame 实现了 join() 方法,它可以按照索引进行数据合并:
print(df1a.join(df2a))
'''
employee
Bob Accounting 2008
Jake Engineering 2012
Lisa Engineering 2004
Sue HR 2014
'''
如果考虑将索引与列混合使用,那么可以通过结合 left_index 和 right_on,或者结合 left_on 与 right_index 来实现。
前面的数据连接我们总结出一个重要的条件:集合操作规则。当一个值出现在一列,却没有出现在另一列时,就需要考虑集合操作规则了。例如:
df6 = pd.DataFrame({'name':['Peter','Paul','Mary'],
'food':['fish','beans','bread']},
columns=['name','food'])
df7 = pd.DataFrame({'name':['Mary','Joseph'],
'drink':['wine','beer']},
columns=['name','drink'])
print(df6);
'''
name food
0 Peter fish
1 Paul beans
2 Mary bread
'''
print(df7);
'''
name drink
0 Mary wine
1 Joseph beer
'''
print(pd.merge(df6,df7))
'''
name food drink
0 Mary bread wine
'''
我们合并两个数据集,在 “name” 列中只有一个共同的值:Mary。默认情况下,结果中只会包含两个输入集合的交集,这种连接方式被称为内连接(inner join)。我们可以用 how 参数设置连接方式,默认值为 ‘inner’:
print(pd.merge(df6,df7,how='inner'))
'''
name food drink
0 Mary bread wine
'''
how 支持的数据连接方式还有 ‘outer’、‘left’、‘right’。外连接返回的是两个输入列的并集,所有缺失值都用 NaN 填充:
print(pd.merge(df6,df7,how='outer'))
'''
name food drink
0 Peter fish NaN
1 Paul beans NaN
2 Mary bread wine
3 Joseph NaN beer
'''
左连接和右连接返回的结果分别只包含左列和右列,如下:
print(pd.merge(df6,df7,how='left'))
'''
name food drink
0 Peter fish NaN
1 Paul beans NaN
2 Mary bread wine
'''
现在输出的行中只包含左边输入列的值。
最后,你可能会遇到两个输入 DataFrame 有重名的情况。例如:
df8 = pd.DataFrame({'name':['Bob','Jake','Lisa','Sue'],
'rank':[1,2,3,4]})
df9 = pd.DataFrame({'name':['Bob','Jake','Lisa','Sue'],
'rank':[3,1,4,2]})
print(df8);
'''
name rank
0 Bob 1
1 Jake 2
2 Lisa 3
3 Sue 4
'''
print(df9);
'''
name rank
0 Bob 3
1 Jake 1
2 Lisa 4
3 Sue 2
'''
print(pd.merge(df8,df9,on='name'))
'''
name rank_x rank_y
0 Bob 1 3
1 Jake 2 1
2 Lisa 3 4
3 Sue 4 2
'''
由于输出结果中有两个重复的列名,因此 pd.merge() 函数会自动为它们增加后缀 _x 和 _y,当然也可以通过 suffixes 参数自定义后缀名:
print(pd.merge(df8,df9,on='name',suffixes=["_L","_R"]))
'''
name rank_L rank_R
0 Bob 1 3
1 Jake 2 1
2 Lisa 3 4
3 Sue 4 2
'''
在对较大的数据进行分析时,一项基本的工作就是有效的数据累计(summarization):计算累计(aggregation)指标,如:sum()、mean()、median()、min() 和 max(),其中每一个指标都呈现了大数据集的特性。
通过 Seaborn 用一份行星数据来进行演示,其中包含天文学家观测到的围绕恒星运转的行星数据。行星数据可以直接通过 Seaborn 下载:
import seaborn as sns
plants = sns.load_dataset('planets')
print(plants.shape) # (1035, 6)
print(plants.head())
'''
method number orbital_period mass distance year
0 Radial Velocity 1 269.300 7.10 77.40 2006
1 Radial Velocity 1 874.774 2.21 56.95 2008
2 Radial Velocity 1 763.000 2.60 19.84 2011
3 Radial Velocity 1 326.030 19.40 110.62 2007
4 Radial Velocity 1 516.220 10.50 119.47 2009
'''
与一维 NumPy 数组相同,Pandas 的 Series 的累计函数也会返回一个统计值:
rng = np.random.RandomState(42)
ser = pd.Series(rng.rand(5))
print(ser)
'''
0 0.374540
1 0.950714
2 0.731994
3 0.598658
4 0.156019
dtype: float64
'''
print(ser.sum()) # 2.811925491708157
print(ser.mean()) # 0.5623850983416314
DataFrame 的累计函数默认对每列进行统计:
df = pd.DataFrame({'A':rng.rand(5),
'B':rng.rand(5)})
print(df)
'''
A B
0 0.155995 0.020584
1 0.058084 0.969910
2 0.866176 0.832443
3 0.601115 0.212339
4 0.708073 0.181825
'''
print(df.mean())
'''
A 0.477888
B 0.443420
dtype: float64
'''
设置 axis 参数,你就可以对每一行进行统计了:
print(df.mean(axis='columns'))
'''
0 0.088290
1 0.513997
2 0.849309
3 0.406727
4 0.444949
dtype: float64
'''
Pandas 的 Series 和 DataFrame 支持所有前面介绍的常用累计函数。另外,还有一个非常方便的 describe() 方法可以计算每一列的若干常用统计值。例如:
print(plants.dropna().describe())
'''
number orbital_period ... distance year
count 498.00000 498.000000 ... 498.000000 498.000000
mean 1.73494 835.778671 ... 52.068213 2007.377510
std 1.17572 1469.128259 ... 46.596041 4.167284
min 1.00000 1.328300 ... 1.350000 1989.000000
25% 1.00000 38.272250 ... 24.497500 2005.000000
50% 1.00000 357.000000 ... 39.940000 2009.000000
75% 2.00000 999.600000 ... 59.332500 2011.000000
max 6.00000 17337.500000 ... 354.000000 2014.000000
[8 rows x 5 columns]
'''
Pandas 内置的一些累计方法:
指标 | 描述 |
---|---|
count() | 计数项 |
first()、last() | 第一项与最后一项 |
mean() 、median() | 均值与中位数 |
min()、max() | 最小值与最大值 |
std()、var() | 标准差与方差 |
mad() | 均值绝对偏差 |
prod() | 所有项乘积 |
sum() | 所有项求和 |
虽然我们可以用前面介绍的一系列的掩码、累计与合并操作来实现,但是意识到中间分割过程不需要显示地暴露出来这一点十分重要。而且 GroupBy 只需要一行代码,就可以计算每组的和、均值、计数、最小值以及其他累计值。GroupBy 的用处就是将这些步骤抽象:用户不需要知道在底层如何计算,只要把操作看成一个整体就够了。
df = pd.DataFrame({'key':['A','B','C','A','B','C'],
'data':range(6)},columns=['key','data'])
print(df)
'''
key data
0 A 0
1 B 1
2 C 2
3 A 3
4 B 4
5 C 5
'''
我们可以用 DataFrame 的 groupby() 方法进行大多数常见的分割—应用—组合操作,将需要分组的列名传进入即可:
print(df.groupby('key'))
#
需要注意的是,这里的返回值不是一个 DataFrame 对象,而是一个 DataFrameGroupBy 对象。这个对象的魔力在于,你可以将它看作是一种特殊形式的 DataFrame,里面隐藏着若干组数据,但是在没有应用累计函数之前不会计算。
为了得到这种结果,可以对 DataFrameGroupBy 对象应用累计函数,它会完成相应的应用 / 组合步骤并生成结果:
print(df.groupby('key').sum())
'''
key
A 3
B 5
C 7
'''
GroupBy 对象是一种非常灵活的抽象类型。你可以把它看成是 DataFrame 的集合,在底层解决所有难题。
GroupBy 中最重要的可能就是 aggregate、filter、transform 和 apply(累计、过滤、转换、应用)了。先来介绍一下基本操作:
按列取值。GroupBy 对象与 DataFrame 一样,也支持按列取值,并返回一个修改过的 GroupBy 对象,例如:
print(plants.groupby('method'))
#
print(plants.groupby('method')['orbital_period'])
#
这里从原来的 DataFrame 中取某个列名作为一个 Series 组。与 GroupBy 对象一样,直到我们运行累计函数,才会开始计算:
print(plants.groupby('method')['orbital_period'].median())
'''
method
Astrometry 631.180000
Eclipse Timing Variations 4343.500000
Imaging 27500.000000
Microlensing 3300.000000
Orbital Brightness Modulation 0.342887
Pulsar Timing 66.541900
Pulsation Timing Variations 1170.000000
Radial Velocity 360.200000
Transit 5.714932
Transit Timing Variations 57.011000
Name: orbital_period, dtype: float64
'''
这样就可以获得不同方法下所有行星公转周期(按天计算)的中位数。
按组迭代。GroupBy 对象支持直接按组迭代,返回的每一组都是 Series 或 DataFrame:
for (method,group) in plants.groupby('method'):
print("{0:30s} shape={1}".format(method,group.shape))
'''
Astrometry shape=(2, 6)
Eclipse Timing Variations shape=(9, 6)
Imaging shape=(38, 6)
Microlensing shape=(23, 6)
Orbital Brightness Modulation shape=(3, 6)
Pulsar Timing shape=(5, 6)
Pulsation Timing Variations shape=(1, 6)
Radial Velocity shape=(553, 6)
Transit shape=(397, 6)
Transit Timing Variations shape=(4, 6)
'''
调用方法。借助 Python 类的魔力(@classmethod),可以让任何不由 GroupBy 对象直接实现的方法直接应用到每一组,无论是 DataFrame 还是 Series 对象都同样适用。例如,你可以用 DataFrame 的 describe() 方法进行累计,对每一组数据进行描述性统计:
print(plants.groupby('method')['year'].describe().unstack())
'''
method
count Astrometry 2.000000
Eclipse Timing Variations 9.000000
Imaging 38.000000
Microlensing 23.000000
Orbital Brightness Modulation 3.000000
Pulsar Timing 5.000000
Pulsation Timing Variations 1.000000
Radial Velocity 553.000000
Transit 397.000000
Transit Timing Variations 4.000000
mean Astrometry 2011.500000
...
Length: 80, dtype: float64
'''
为了方便后面的演示,使用下面的这个 DataFrame:
rng = np.random.RandomState(0)
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
'data1': range(6),
'data2': rng.randint(0, 10, 6)},
columns=['key', 'data1', 'data2'])
print(df)
'''
key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9
'''
累计。前面的函数都是一些比较简单的累计函数。但是 aggregate() 其实可以支持更复杂的操作,比如字符串、函数或者函数列表,并且能一次性计算所有累计值。例如:
print(df.groupby('key').aggregate(['min',np.median,max]))
'''
data1 data2
min median max min median max
key
A 0 1.5 3 3 4.0 5
B 1 2.5 4 0 3.5 7
C 2 3.5 5 3 6.0 9
'''
另外一种方法就是通过 Python 字典指定不同列需要累计的函数:
print(df.groupby('key').aggregate({'data1':'min',
'data2':'max'}))
'''
data1 data2
key
A 0 5
B 1 7
C 2 9
'''
过滤。过滤操作可以让你按照分组的属性丢弃若干数据。例如,我们可能只需要保留标准差超过某个阈值的组:
def filter_func(x):
return x['data2'].std() > 4
print(df);
'''
key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9
'''
print(df.groupby('key').std());
'''
data1 data2
key
A 2.12132 1.414214
B 2.12132 4.949747
C 2.12132 4.242641
'''
print(df.groupby('key').filter(filter_func))
'''
key data1 data2
1 B 1 0
2 C 2 3
4 B 4 7
5 C 5 9
'''
转换。累计操作返回的是对组内全量数据缩减过的结果,而转换操作会返回一个新的全量数据。数据经过转换之后,其形状与原来的输入数据是一样的。常见的例子是将每一组的样本数据减去各组的均值,实现数据标准化:
print(df.groupby('key').transform(lambda x:x - x.mean()))
'''
data1 data2
0 -1.5 1.0
1 -1.5 -3.5
2 -1.5 -3.0
3 1.5 -1.0
4 1.5 3.5
5 1.5 3.0
'''
apply() 方法。apply() 方法让你可以在每组上应用任意方法。这个函数输入一个 DataFrame,返回一个 Pandas 对象(DtaFrame 或 Series)或一个标量(scalar,单个数值)。组合操作会适应返回结果类型。
下面的例子就是用 apply() 方法将第一列数据以第二列的和作为基数进行标准化:
def norm_by_data2(x):
# x 是一个分组数据的 DataFrame
x['data1'] /= x['data2'].sum()
return x
print(df);
'''
key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9
'''print(df.groupby('key').apply(norm_by_data2))
'''
key data1 data2
0 A 0.000000 5
1 B 0.142857 0
2 C 0.166667 3
3 A 0.375000 3
4 B 0.571429 7
5 C 0.416667 9
'''
GroupBy 里的 apply() 方法非常灵活,唯一注意的是它总是输入分组数据的 DataFrame,返回 Pandas 对象或标量。
前面的例子一直在用列名分割 DataFrame。下面介绍更多的方法。
将列表、数组、Series 或索引作为分组键。分组键可以是长度与 DataFrame 匹配的任意 Series 或列表,例如:
L = [0,1,0,1,2,0]
print(df);
'''
key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9
'''print(df.groupby(L).sum())
'''
data1 data2
0 7 17
1 4 3
2 4 7
'''
因此,还有一种比前面直接用列名更啰嗦的表示方法 df.groupby(‘key’):
print(df.groupby(df['key']).sum())
'''
data1 data2
key
A 3 8
B 5 7
C 7 12
'''
用字典或 Series 将索引映射到分组名称。另一种方法是提供一个字典,将索引映射到分组键:
df2 = df.set_index('key')
mapping = {'A':'vowel','B':'consonant','C':'consonant'}
print(df2);
'''
data1 data2
key
A 0 5
B 1 0
C 2 3
A 3 3
B 4 7
C 5 9
'''
print(df2.groupby(mapping).sum())
'''
data1 data2
consonant 12 19
vowel 3 8
'''
任意 Python 函数。与前面的字典类似,你可以将任意 Python 函数传入 groupby,函数映射到索引,然后新的分组输出:
print(df2.groupby(str.lower).mean())
'''
data1 data2
a 1.5 4.0
b 2.5 3.5
c 3.5 6.0
'''
多个有效键构成的列表。此外,任意之前有效的键可以组合起来进行分组,从而返回一个多级索引的分组结果:
print(df2.groupby([str.lower,mapping]).mean())
'''
data1 data2
a vowel 1.5 4.0
b consonant 2.5 3.5
c consonant 3.5 6.0
'''
获取不同方法和不同年份发现的行星数量:
decade = 10 * (plants['year'] // 10)
decade = decade.astype(str) + 's'
decade.name = 'decade'
print(plants.groupby(['method',decade])['number'].sum().unstack().fillna(0))
'''
decade 1980s 1990s 2000s 2010s
method
Astrometry 0.0 0.0 0.0 2.0
Eclipse Timing Variations 0.0 0.0 5.0 10.0
Imaging 0.0 0.0 29.0 21.0
Microlensing 0.0 0.0 12.0 15.0
Orbital Brightness Modulation 0.0 0.0 0.0 5.0
Pulsar Timing 0.0 9.0 1.0 1.0
Pulsation Timing Variations 0.0 0.0 1.0 0.0
Radial Velocity 1.0 52.0 475.0 424.0
Transit 0.0 0.0 64.0 712.0
Transit Timing Variations 0.0 0.0 0.0 9.0
'''
透视表是将每一列数据作为输入,输出将数据不断细分成多个维度累计信息的二维数据表。数据透视表更像是一种多维的 GroupBy 累计操作。
采用泰坦尼克号的乘客信息数据库来演示:
titanic = sns.load_dataset('titanic')
print(titanic.head())
'''
survived pclass sex age ... deck embark_town alive alone
0 0 3 male 22.0 ... NaN Southampton no False
1 1 1 female 38.0 ... C Cherbourg yes False
2 1 3 female 26.0 ... NaN Southampton yes True
3 1 1 female 35.0 ... C Southampton yes False
4 0 3 male 35.0 ... NaN Southampton no True
[5 rows x 15 columns]
'''
在研究这些数据之前,先将它们按照性别、最终生还状态或其他组合属性进行分组。你可以用 GroupBy 来实现,例如这样统计不同性别乘客的生还率:
print(titanic.groupby('sex')[['survived']].mean())
'''
survived
sex
female 0.742038
male 0.188908
'''
如果要进一步探索:同时观察不同性别与船舱等级的生还情况。根据 GroupBy,我们也许能够实现想要的结果:将船舱等级(‘class’)与性别(‘sex’)分组,然后选择生还状态(‘survived’)列,应用均值(‘mean’)累计函数,再将各组结果组合,最后通过行索引转列索引操作将行索引转换成列索引,形成二维数组。
print(titanic.groupby(['sex','class'])['survived'].aggregate('mean').unstack())
'''
class First Second Third
sex
female 0.968085 0.921053 0.500000
male 0.368852 0.157407 0.135447
'''
用 DataFrame 的 pivot_table 实现的效果等同于上面的管道命令:
print(titanic.pivot_table('survived',index='sex',columns='class'))
'''
class First Second Third
sex
female 0.968085 0.921053 0.500000
male 0.368852 0.157407 0.135447
'''
与 GroupBy 类似,数据透视表中的分组也可以通过各种参数指定多个等级。例如,我们可能想把年龄(‘age’)也加进去作为第三个维度,这就可以通过 pd.cut 函数将年龄进行分段:
age = pd.cut(titanic['age'],[0,18,80])
print(titanic.pivot_table('survived',['sex',age],'class'))
'''
class First Second Third
sex age
female (0, 18] 0.909091 1.000000 0.511628
(18, 80] 0.972973 0.900000 0.423729
male (0, 18] 0.800000 0.600000 0.215686
(18, 80] 0.375000 0.071429 0.133663
'''
对某一列也可以使用同样的策略——让我们用 pd.cut 将船票价格按照计数项等分为两份,加入数据透视表看看:
fare = pd.qcut(titanic['fare'],2)
print(titanic.pivot_table('survived',['sex',age],[fare,'class']))
'''
fare (-0.001, 14.454] ... (14.454, 512.329]
class First Second ... Second Third
sex age ...
female (0, 18] NaN 1.000000 ... 1.000000 0.318182
(18, 80] NaN 0.880000 ... 0.914286 0.391304
male (0, 18] NaN 0.000000 ... 0.818182 0.178571
(18, 80] 0.0 0.098039 ... 0.030303 0.192308
'''
DataFrame 的 pivot_table 的完整签名如下:
def pivot_table(data, values=None, index=None, columns=None,
aggfunc='mean', fill_value=None, margins=False,
dropna=True, margins_name='All')
fill_value 和 dropna 用来处理缺失值。aggfunc 参数用于设置累计函数类型,默认值为均值(mean)。与 GroupBy 的用法一样,累计函数可以用一些常见的字符串(‘sum’、‘mean’、‘count’、‘min’、‘max’ 等)表示,也可以用标准的累计函数(np.sum()、min()、sum())表示。另外还可以通过字典为不同的列指定不同的累计函数:
print(titanic.pivot_table(index='sex',columns='class',
aggfunc={'survived':sum,'fare':'mean'}))
'''
fare survived
class First Second Third First Second Third
sex
female 106.125798 21.970121 16.118810 91 70 72
male 67.226127 19.741782 12.661633 45 17 47
'''
当需要计算每一组的总数时,可以通过 margins 参数来设置:
print(titanic.pivot_table('survived',index='sex',columns='class',margins=True))
'''
class First Second Third All
sex
female 0.968085 0.921053 0.500000 0.742038
male 0.368852 0.157407 0.135447 0.188908
All 0.629630 0.472826 0.242363 0.383838
'''
前面已经介绍过如何使用 NumPy 和 Pandas 进行一般的运算操作,因此我们也能简便快速地对多个数组元素执行同样的操作,例如:
x = np.array([2,3,5,7,11,13])
print(x * 2) # [ 4 6 10 14 22 26]
向量化操作简化了纯数值的数组操作语法——我们不需要再担心数组的长度或维度,只需要关心需要的操作。然而,由于 NumPy 并没有为字符串数组提供简单的接口,因此需要通过繁琐的 for 循环来解决问题:
data = ['peter','Paul','MARY','gUIDO']
print([s.capitalize() for s in data]) # ['Peter', 'Paul', 'Mary', 'Guido']
虽然这么做对于某些数据可能是有效的,但是假如数据中出现了缺失值,那么这样做就会引起异常,例如:
data = ['peter','Paul',None,'MARY','gUIDO']
print([s.capitalize() for s in data])
'''
AttributeError: 'NoneType' object has no attribute 'capitalize'
'''
Pandas 为包含字符串的 Series 和 Index 对象提供的 str 属性堪称两全其美的方法,它既可以满足向量化字符串的需求,又可以正确地处理缺失值。例如,我们用前面的数据 data 创建一个 Pandas 的 Series:
names = pd.Series(data)
print(names)
'''
0 peter
1 Paul
2 None
3 MARY
4 gUIDO
dtype: object
'''
现在就可以直接调用转换大写方法 capitalize() 将所有的字符串变成大写形式,缺失值会被跳过:
print(names.str.capitalize())
'''
0 Peter
1 Paul
2 None
3 Mary
4 Guido
dtype: object
'''
这一节的示例将采用一些人名来演示:
monte = pd.Series(['Graham Chapman','John Cleese','Terry Gilliam',
'Eric Idle','Terry Jones','Michael Palin'])
几乎所有的 Python 内置的字符串方法都被复制到 Pandas 的向量化字符串方法中。如下表:
len() | lower() | translate() | islower() |
---|---|---|---|
ljust() | upper() | startswith() | isupper() |
rjust() | find() | endwith() | isnumeric() |
center() | rfind() | isalnum() | isdecimal() |
zfill() | index() | isalpha() | split() |
strip() | rindex() | isdigit() | rsplit() |
rstrip() | capitalize() | isspace() | partition() |
lstrip() | swapcase() | istitle() | rpartition() |
需要注意的是,这些方法的返回值不同,例如 lower() 方法返回一个字符串 Series:
print(monte.str.lower())
'''
0 graham chapman
1 john cleese
2 terry gilliam
3 eric idle
4 terry jones
5 michael palin
dtype: object
'''
但也有些方法返回数值:
print(monte.str.len())
'''
0 14
1 11
2 13
3 9
4 11
5 13
dtype: int64
'''
有些方法返回布尔值:
print(monte.str.startwith('T'))
'''
0 False
1 False
2 True
3 False
4 True
5 False
dtype: bool
'''
有些方法返回列表或其他复合值:
print(monte.str.split())
'''
0 [Graham, Chapman]
1 [John, Cleese]
2 [Terry, Gilliam]
3 [Eric, Idle]
4 [Terry, Jones]
5 [Michael, Palin]
dtype: object
'''
Pandas 向量化字符串方法与 Python 标准库的 re 模块函数的对应关系:
方法 | 描述 |
---|---|
match() | 对每个元素调用 re.match(),返回布尔类型值 |
extract() | 对每个元素调用 re.match(),返回匹配的字符串组(groups) |
findall() | 对每个元素调用 re.findall() |
replace() | 用正则表达式替换字符串 |
contains() | 对每个元素调用 re.search(),返回布尔类型值 |
count() | 等价于 str.split(),支持正则表达式 |
rsplit() | 等价于 str.rsplit(),支持正则表达式 |
通过这些方法,你就可以实现各种有趣的操作了。例如,可以提取元素前面的连续字母作为每个人的名字(first name):
print(monte.str.extract('([A-Za-z]+)'))
'''
0
0 Graham
1 John
2 Terry
3 Eric
4 Terry
5 Michael
'''
我们还可以实现更复杂的操作,例如找出所有开头与结尾都是辅音字母的名字——这可以用正则表达式中的开始符号(^)与结尾符号($)来实现:
print(monte.str.findall(r'^[^ARIOU].*[^aeiou]$'))
'''
0 [Graham Chapman]
1 []
2 [Terry Gilliam]
3 []
4 [Terry Jones]
5 [Michael Palin]
dtype: object
'''
方法 | 描述 |
---|---|
get() | 获取元素索引位置上的值,索引从 0 开始 |
slice() | 对元素进行切片取值 |
slice_replace() | 对元素进行切片替换 |
cat() | 连接字符串 |
repeat() | 重复元素 |
normalize() | 将字符串转换为 Unicode 规范形式 |
pad() | 在字符串的左边、右边或两边增加空格 |
wrap() | 将字符串按照指定的宽度换行 |
join() | 用分隔符连接 Series 的每个元素 |
get_dummies() | 按照分隔符提取每个元素的 dummy 变量,转换成独热(one-hot)编码的 DataFrame |
本节介绍的日期与时间数据主要包含三类:
Python 基本的日期与时间功能都在标准库的 datatime 模块中。如果和第三方库 dateutil 模块搭配使用,可以快速实现许多处理日期与时间的功能。例如,你可以用 dateutil 类型创建一个日期:
from datetime import datetime
print(datetime(year=2015,month=7,day=4)) # 2015-07-04 00:00:00
或者使用 dateutil 模块对各种字符串格式的日期进行正确解析:
from dateutil import parser
date = parser.parse('4th of July,2015')
print(date) # 2015-07-04 00:00:00
一旦有了 datetime 对象,就可以进行许多操作了,例如打印这一天是星期几:
print(date.strftime('%A')) # Saturday
NumPy 团队为 NumPy 增加了自己的时间序列类型。datetime64 类型将日期编码为 64 位整数,这样可以让日期数组非常紧凑(节省内存)。datetime64 需要在设置日期时确定具体输入的类型:
date = np.array('2015-07-04',dtype=np.datetime64)
print(date) # 2015-07-04
但只要有了这个日期格式,就可以进行快速的向量化运算:
print(date + np.arange(12))
'''
['2015-07-04' '2015-07-05' '2015-07-06' '2015-07-07' '2015-07-08'
'2015-07-09' '2015-07-10' '2015-07-11' '2015-07-12' '2015-07-13'
'2015-07-14' '2015-07-15']
'''
datetime64 与 timedelta64 对象的一个共同特点是,它们都是在基本时间单位的基础上建立的。由于 datetime64 对象是 64 位精度,所以可编码的时间范围可以是基本单元的 264 倍。也就是说,datetime64 在时间精度与最大时间跨度之间达到一种平衡。
比如你想要一个时间纳秒(ns)级的时间精度,那么你可以将时间编码到 0~264 纳秒或 600 年之内,NumPy 会自动判断输入时间需要使用的时间单位。例如,下面是以天为单位的日期:
print(np.datetime64('2015-07-04')) # 2015-07-04
而这是一个以分钟为单位的日期:
print(np.datetime64('2015-07-04 12:00')) # 2015-07-04T12:00
需要注意的是,时区将自动设置为执行代码的操作系统的时区。你可以通过各种格式的代码设置基本时间单位。例如,将时间单位设置为纳秒:
print(np.datetime64('2015-07-04 12:59:59.50','ns')) # 2015-07-04T12:59:59.500000000
Pandas 所有关于日期与时间处理方法全部都是通过 Timestamp 对象实现的,它利用 numpy.datetime64 的有效存储和向量化接口将 datetime 和 dateutil 的易用性有机结合起来。Pandas 通过一组 Timestamp 对象就可以创建一个可以作为 Series 或 DataFrame 索引的 DatatimeIndex。
例如,可以用 Pandas 的方式演示前面介绍的日期与时间功能。我们还可以灵活处理不同格式的日期与时间字符串,获取某一天是星期几:
date = pd.to_datetime('4th of July,2016')
print(date) # 2016-07-04 00:00:00
print(date.strftime('%A')) # Monday
另外,也可以直接进行 NumPy 类型的向量化运算:
print(date + pd.to_timedelta(np.arange(12),'D'))
'''
DatetimeIndex(['2016-07-04', '2016-07-05', '2016-07-06', '2016-07-07',
'2016-07-08', '2016-07-09', '2016-07-10', '2016-07-11',
'2016-07-12', '2016-07-13', '2016-07-14', '2016-07-15'],
dtype='datetime64[ns]', freq=None)
'''
Pandas 时间序列工具非常适合用来处理带时间戳的索引数据。例如,我们可以通过一个时间索引数据创建一个 Series 对象:
index = pd.DataFrame(['2014-07-04','2014-08-04',
'2015-07-04','2015-08-04'])
data = pd.Series([0,1,2,3],index=index)
print(data)
'''
(2014-07-04,) 0
(2014-08-04,) 1
(2015-07-04,) 2
(2015-08-04,) 3
dtype: int64
'''
本节介绍 Pandas 用来处理时间序列的基础数据类型:
最基础的日期/时间对象是 Timestamp 和 DatatimeIndex。这两种对象可以直接使用,最常用的方法是 pd.to_datetime() 函数,它可以解析许多日期与时间格式。对 pd.to_datetime() 传递一个日期会返回一个 Timestamp 类型,传递一个时间序列会返回一个 DatatimeIndex 类型:
dates = pd.to_datetime([datetime(2015,7,3),'4th of July,2015',
'2015-Jul-6','07-07-2015','20150708'])
print(dates)
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='datetime64[ns]', freq=None)
'''
任何 DatatimeIndex 类型都可以通过 to_period() 方法和一个频率代码转换成 PeriodIndex 类型。下面用 ‘D’ 将数据转换成单日的时间序列:
print(dates.to_period('D'))
'''
PeriodIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='period[D]', freq='D')
'''
当用一个日期减去另一个日期时,返回的结果是 TimedeltaIndex 类型:
print(dates - dates[0])
'''
TimedeltaIndex(['0 days', '1 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq=None)
'''
有规律的时间序列:pd.date_range()
为了能够更简便地创建一个有规律的时间序列,Pandas 提供了一些方法:pd.date_range() 可以处理时间戳、pd.period_range() 可以处理周期、pd.timedelta_range() 可以处理时间间隔。pd.date_range() 通过开始日期、结束日期和频率代码(可选的)创建一个有规律的日期序列,默认的频率是天:
print(pd.date_range('2015-07-03','2015-07-10'))
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
'''
此外,日期范围不一定非是开始时间与结束时间,也可以是开始时间与周期数 periods:
print(pd.date_range('2015-07-03',periods=8))
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
'''
你可以通过 freq 参数改变时间间隔,默认是 D。例如,可以创建一个按小时变化的时间戳:
print(pd.date_range('2015-07-03',periods=8,freq='H'))
'''
DatetimeIndex(['2015-07-03 00:00:00', '2015-07-03 01:00:00',
'2015-07-03 02:00:00', '2015-07-03 03:00:00',
'2015-07-03 04:00:00', '2015-07-03 05:00:00',
'2015-07-03 06:00:00', '2015-07-03 07:00:00'],
dtype='datetime64[ns]', freq='H')
'''
如果要创建一个有规律的周期或时间间隔序列,有类似的函数 pd.period_range() 和 pd.timedelta_range()。下面是一个以月为周期的示例:
print(pd.period_range('2015-07',periods=8,freq='M'))
'''
PeriodIndex(['2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12',
'2016-01', '2016-02'],
dtype='period[M]', freq='M')
'''
以及一个以小时递增的序列:
print(pd.timedelta_range(0,periods=10,freq='H'))
'''
TimedeltaIndex(['00:00:00', '01:00:00', '02:00:00', '03:00:00', '04:00:00',
'05:00:00', '06:00:00', '07:00:00', '08:00:00', '09:00:00'],
dtype='timedelta64[ns]', freq='H')
'''
Pandas 时间序列工具的基础是时间频率或偏移量(offset)代码。就像之前见过的 D(day)和 H(hour)代码,我们可以用这些代码设置任意需要的时间间隔。下表是主要的频率代码:
代码 | 描述 | 代码 | 描述 |
---|---|---|---|
D | 天 | B | 天(仅含工作日) |
W | 周 | ||
M | 月末 | BW | 月末(仅含工作日) |
Q | 季末 | BQ | 季末(仅含工作日) |
A | 年末 | BA | 年末(仅含工作日) |
H | 小时 | BH | 小时(工作时间) |
T | 分钟 | ||
S | 秒 | ||
L | 毫秒 | ||
U | 微秒 | ||
N | 纳秒 |
带开始索引的频率代码:
代码 | 频率 |
---|---|
MS | 月初 |
BMS | 月初(仅含工作日) |
QS | 季初 |
BQS | 季初(仅含工作日) |
AS | 年初 |
BAS | 年初(仅含工作日) |
另外,你还可以在频率代码后面加三位月份缩写字母来改变季、年频率的开始时间:
同理,也可以在后面加上三位星期缩写字母来改变一周的开始时间:
在此基础上,还可以将频率组合起来创建的新的周期。例如,可以用小时(H)和分钟(T)的组合来实现 2 小时 30 分钟:
print(pd.timedelta_range(0,periods=9,freq='2H30T'))
'''
TimedeltaIndex(['00:00:00', '02:30:00', '05:00:00', '07:30:00', '10:00:00',
'12:30:00', '15:00:00', '17:30:00', '20:00:00'],
dtype='timedelta64[ns]', freq='150T')
'''
所有这些频率代码都对应 Pandas 时间序列的偏移量。例如,可以用下面的方法直接创建一个工作日偏移序列:
from pandas.tseries.offsets import BDay
print(pd.date_range('2015-07-01',periods=5,freq=BDay()))
'''
DatetimeIndex(['2015-07-01', '2015-07-02', '2015-07-03', '2015-07-06',
'2015-07-07'],
dtype='datetime64[ns]', freq='B')
'''