Pandas
Pandas建立在NumPy基础上,处理二维数据更加得心应手。
Series和DataFrame
Series和DataFrame是Pandas中的两种核心数据结构,大部分Pandas的功能都围绕着两种这两种数据结构进行。
Series是值得序列,可以理解为一维数组,只有一个列和索引。索引可以定制,当不指定时默认使用整数索引,而且索引可以被命名:
In [1]: import pandas as pd
In [2]: import numpy as np
In [4]: from pandas import Series, DataFrame
In [5]: s1 = Series([1, 2, 3, 4, 5])
In [6]: s1
Out[6]:
0 1
1 2
2 3
3 4
4 5
dtype: int64
In [7]: s2 = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e' ])
In [8]: s2
Out[8]:
a 1
b 2
c 3
d 4
e 5
dtype: int64
In [9]: s2.index.name = 'index'
In [10]: s2.index
Out[10]: Index(['a', 'b', 'c', 'd', 'e'], dtype='object', name='index')
DataFrame类似于二维数组,有行和列之分,除了像Series一样,多个行有索引之外,每个列上面还可以有标签label,索引和标签本身都可以被命名:
In [14]: df = DataFrame(np.random.randn(4, 4), index=['a', 'b', 'c', 'd'], columns=['A', 'B', 'C', 'D'])
In [15]: df
Out[15]:
A B C D
a 0.311412 0.063509 1.195284 -0.921551
b 0.906860 -1.183861 -0.002180 -2.067180
c -0.121993 0.494496 0.870654 0.852347
d -0.461892 0.784124 0.069672 -0.813653
In [16]: df.index
Out[16]: Index(['a', 'b', 'c', 'd'], dtype='object')
In [17]: df.columns
Out[17]: Index(['A', 'B', 'C', 'D'], dtype='object')
上面得代码,通过指定索引和标签(columns参数)创建了一个DataFrame实例。可以通过df.index和df.columns分别访问索引和标签。
选择
对于Series数据来说,主要通过其索引来进行选择:
In [18]: s2 = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e'])
In [19]: s2
Out[19]:
a 1
b 2
c 3
d 4
e 5
dtype: int64
In [20]: s2[0]
Out[20]: 1
In [21]: s2['a']
Out[21]: 1
In [22]: s2[0:3]
Out[22]:
a 1
b 2
c 3
dtype: int64
In [23]: s2['a':'c']
Out[23]:
a 1
b 2
c 3
dtype: int64
对于指定了索引的Series序列来说,有两种选择元素的方式,一种是整数索引,(说明整数索引一直默认存在),第二种方式是通过指定字符索引进行。整数索引和字符索引分别调用了s2.iloc和s2.loc索引,其中iloc代表整数索引:
In [24]: s2.iloc[0:3]
Out[24]:
a 1
b 2
c 3
dtype: int64
In [25]: s2.loc['a':'c']
Out[25]:
a 1
b 2
c 3
dtype: int64
对于DateFrame数据,由于有行列之分,可以有多种选择数据的方式,可以通过索引和标签来选择,对于标签(列)选择:
In [27]: df.A
Out[27]:
a 0.311412
b 0.906860
c -0.121993
d -0.461892
Name: A, dtype: float64
In [29]: df['A']
Out[29]:
a 0.311412
b 0.906860
c -0.121993
d -0.461892
Name: A, dtype: float64
对于标签(列),可以通过df.A的属性方式,或者通过df['A']来访问。如果要选择多列,可以通过df.columns来获取所有列标签,然后选择部分标签来选择df的多列数据。
In [30]: df[df.columns[0:2]]
Out[30]:
A B
a 0.311412 0.063509
b 0.906860 -1.183861
c -0.121993 0.494496
d -0.461892 0.784124
可以用loc和iloc属性来选择某一行
In [33]: df
Out[33]:
A B C D
a 0.311412 0.063509 1.195284 -0.921551
b 0.906860 -1.183861 -0.002180 -2.067180
c -0.121993 0.494496 0.870654 0.852347
d -0.461892 0.784124 0.069672 -0.813653
In [34]: df.loc['a']
Out[34]:
A 0.311412
B 0.063509
C 1.195284
D -0.921551
Name: a, dtype: float64
In [35]: df.loc['a':'b']
Out[35]:
A B C D
a 0.311412 0.063509 1.195284 -0.921551
b 0.906860 -1.183861 -0.002180 -2.067180
In [36]: df.iloc[0]
Out[36]:
A 0.311412
B 0.063509
C 1.195284
D -0.921551
Name: a, dtype: float64
In [37]: df.iloc[0:2]
Out[37]:
A B C D
a 0.311412 0.063509 1.195284 -0.921551
b 0.906860 -1.183861 -0.002180 -2.067180
有了行选择方式就有了更多方法来选择列:
In [38]: df.loc[:,['B', 'C', 'D']]
Out[38]:
B C D
a 0.063509 1.195284 -0.921551
b -1.183861 -0.002180 -2.067180
c 0.494496 0.870654 0.852347
d 0.784124 0.069672 -0.813653
df.loc支持二维选择,可以同时选择行和列,:符号代表选择所有的行。可以同时选择具体的某一行或者某一列:
In [39]: df
Out[39]:
A B C D
a 0.311412 0.063509 1.195284 -0.921551
b 0.906860 -1.183861 -0.002180 -2.067180
c -0.121993 0.494496 0.870654 0.852347
d -0.461892 0.784124 0.069672 -0.813653
In [40]: df.loc['a', 'A']
Out[40]: 0.31141203738121559
In [41]: df.loc['b':'c', 'B':'C']
Out[41]:
B C
b -1.183861 -0.002180
c 0.494496 0.870654
缺失值和数据自动对齐
Pandas中最重要的一个功能是,它可以对不同索引的对象进行算术运算。比如对两个Series数据进行相加时,如果存在不用的索引,则结果时两个索引的并集:
In [42]: s1 = Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
In [43]: s1
Out[43]:
a 1
b 2
c 3
d 4
dtype: int64
In [44]: s2 = Series([2, 3, 4, 5], index=['b', 'c', 'd', 'e'])
In [45]: s2
Out[45]:
b 2
c 3
d 4
e 5
dtype: int64
In [46]: s1 + s2
Out[46]:
a NaN
b 4.0
c 6.0
d 8.0
e NaN
dtype: float64
以上代码中创建了s1和s2两个序列,具有相同的索引['b', 'c', 'd'],所以在相加时,相同索引的值会相加,但不重叠的索引会引入NaN值,也就是缺失值。缺失值会在运算中传播,所以最终结果也是NaN值。
根据相同索引进行自动计算,就是自动对齐功能。同样的规则也适用于DataFrame:
In [49]: df1 = DataFrame(np.arange(9).reshape(3, 3), columns=list('ABC'), index=list('abc'
...: ))
In [50]: df2 = DataFrame(np.arange(12).reshape(3, 4), columns=list('ABDE'), index=list('bc
...: d'))
In [51]: df1
Out[51]:
A B C
a 0 1 2
b 3 4 5
c 6 7 8
In [52]: df2
Out[52]:
A B D E
b 0 1 2 3
c 4 5 6 7
d 8 9 10 11
In [53]: df1 + df2
Out[53]:
A B C D E
a NaN NaN NaN NaN NaN
b 3.0 5.0 NaN NaN NaN
c 10.0 12.0 NaN NaN NaN
d NaN NaN NaN NaN NaN
在计算时可以指定使用的值来填充NaN值,然后带入计算过程:
In [54]: df1.add(df2, fill_value=0)
Out[54]:
A B C D E
a 0.0 1.0 2.0 NaN NaN
b 3.0 5.0 5.0 2.0 3.0
c 10.0 12.0 8.0 6.0 7.0
d 8.0 9.0 NaN 10.0 11.0
指定了使用0来填充NaN值,然后带入计算过程,注意这里填充的不是最终的计算结果。可以看到依然有元素为NoN值,是因为这个位置的元素在df1和df2中都未定义。
常用运算
Series和DataFrame的常用运算方式和NumPy中的差不多。在Pandas中还有一种比较常见的操作是将函数应用到每行或者每列上面,DataFrame的apply方法可以实现此功能,比如想统计每行和每列的极差(最大值和最小值之差):
In [55]: df1 = DataFrame(np.arange(9).reshape(3, 3), columns=list('ABC'), index=list('abc'
...: ))
In [56]: df1
Out[56]:
A B C
a 0 1 2
b 3 4 5
c 6 7 8
In [57]: f = lambda x: x.max() - x.min()
In [58]: df1.apply(f)
Out[58]:
A 6
B 6
C 6
dtype: int64
In [59]: df1.apply(f, axis=1)
Out[59]:
a 2
b 2
c 2
dtype: int64
以上代码中,定义了f匿名函数,通过df.apply(f)应用f,统计出每列的极差,接着通过传入 axis=1 参数,统计出每行的极差。
如果想把函数应用到每一个元素上,DateFrame数据可以使用df.applymap方法,Series可以使用s.map方法:
In [60]: df1.applymap(lambda x: x+1)
Out[60]:
A B C
a 1 2 3
b 4 5 6
c 7 8 9
以上代码中,使所有元素都自增加1,其结果和 df1+1的运算结果相同。
常用统计
类似于NumPy, Series和DataFrame也有各种统计方法,比如求平均值,方差,和等方法,同时通过describe方法可以得到当前数据的一些常用的统计信息:
In [61]: df1 = DataFrame(np.arange(9).reshape(3,3), columns=list('ABC'), index=list('abc')
...: )
In [62]: df1
Out[62]:
A B C
a 0 1 2
b 3 4 5
c 6 7 8
In [63]: df1.sum()
Out[63]:
A 9
B 12
C 15
dtype: int64
In [64]: df1.mean()
Out[64]:
A 3.0
B 4.0
C 5.0
dtype: float64
In [65]: df1.sum(axis=1)
Out[65]:
a 3
b 12
c 21
dtype: int64
In [66]: df1.describe()
Out[66]:
A B C
count 3.0 3.0 3.0
mean 3.0 4.0 5.0
std 3.0 3.0 3.0
min 0.0 1.0 2.0
25% 1.5 2.5 3.5
50% 3.0 4.0 5.0
75% 4.5 5.5 6.5
max 6.0 7.0 8.0
对于这些统计函数,我们可以指定统计的纬度,和NumPy多维数组的统计函数十分相似。默认按列统计,也可以通过参数axis=1指定按行统计。
describe方法显示了一些常用的统计信息:
- 'conut' 元素值得数量:
- 'mean' 平均值:
- 'std' 标准差:
- 'min' 最小值:
- '25%' 下四分位数:
- '50%' 中位数:
- '75%' 上四分位数:
- 'max' 最大值:
数据合并和分组
有时候需要合并两个DataFrame数据,合并数据得方式有两种,一种简单得进行拼接,另一种是根据列名类像数据库查询一样进行合并。这两种方法分别通过pandas.concat和pandas.merge方法实现:
In [67]: df1 = DataFrame(np.random.randn(3, 3))
In [69]: df2 = DataFrame(np.random.randn(3, 3), index=[5, 6, 7])
In [70]: df1
Out[70]:
0 1 2
0 -0.975353 0.516803 -0.784033
1 0.243648 -0.671816 -1.232789
2 0.433984 1.175839 -2.421708
In [71]: df2
Out[71]:
0 1 2
5 -1.261180 0.043069 0.301943
6 0.747142 -0.113258 -0.700924
7 -0.687073 0.834874 0.790432
In [72]: pd.concat([df1, df2])
Out[72]:
0 1 2
0 -0.975353 0.516803 -0.784033
1 0.243648 -0.671816 -1.232789
2 0.433984 1.175839 -2.421708
5 -1.261180 0.043069 0.301943
6 0.747142 -0.113258 -0.700924
7 -0.687073 0.834874 0.790432
有的时候,有多个数据集,这些数据集有相同的列,就可以按照这个列进行合并操作,类似于数据库中的join操作:
In [73]: df1 = DataFrame({'user_id': [5348, 13], 'course': [12, 45], 'minutes': [9, 36]})
In [74]: df2 = DataFrame({'course': [12, 45], 'name': ['Linux基础入门', '数据分析']})
In [75]: df1
Out[75]:
course minutes user_id
0 12 9 5348
1 45 36 13
In [76]: df2
Out[76]:
course name
0 12 Linux基础入门
1 45 数据分析
In [77]: pd.merge(df1, df2)
Out[77]:
course minutes user_id name
0 12 9 5348 Linux基础入门
1 45 36 13 数据分析
可以看到当通过字段创建DataFrame数据集时,键变成了列名。df1和df2有共同的列course,当进行merge操作的时候Pandas会自动安装这列进行合并。
在Pandas中,也支持类似数据库查询语句GROUP BY的功能,也就是按照某列进行分组,然后在分组上进行一些计算操作,有如下数据集,计算其中user_id为5348的用户的学习时间:
In [79]: df = DataFrame({'user_id': [5348, 13, 5348], 'course': [12, 45, 23], 'minutes': [
...: 9, 36, 45]})
In [80]: df
Out[80]:
course minutes user_id
0 12 9 5348
1 45 36 13
2 23 45 5348
一种办法时筛选出所有user_id为5348的行,然后进行求和统计:
In [81]: df[df['user_id'] == 5348]
Out[81]:
course minutes user_id
0 12 9 5348
2 23 45 5348
In [82]: df[df['user_id'] == 5348]['minutes']
Out[82]:
0 9
2 45
Name: minutes, dtype: int64
In [83]: df[df['user_id'] == 5348]['minutes'].sum()
Out[83]: 54
也可以使用类似于数据库的 GROUP BY功能进行计算:
In [84]: df[['user_id', 'minutes']]
Out[84]:
user_id minutes
0 5348 9
1 13 36
2 5348 45
In [85]: df[['user_id', 'minutes']].groupby('user_id').sum()
Out[85]:
minutes
user_id
13 36
5348 54
相对于第一种方式,通过groupby方法在user_id上进行分组并求和,相对于前一种方式,分组求和更加灵活一些。
时间序列处理
要分析的数据中,很多数据都带有时间戳,用Pandas根据时间戳进行计算很方便。
创建一个简单的时间序列:
from datetime import datetime
In [88]: dates = [datetime(2018, 1, 1), datetime(2018, 1, 2), datetime(2018, 1, 3), dateti
...: me(2018, 1, 4)]
In [89]: ts = Series(np.random.randn(4), index=dates)
In [90]: ts
Out[90]:
2018-01-01 0.885089
2018-01-02 0.114941
2018-01-03 -0.831710
2018-01-04 0.382641
dtype: float64
In [91]: ts.index
Out[91]: DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04'], dtype='datetime64[ns]', freq=None)
In [92]: ts.index[0]
Out[92]: Timestamp('2018-01-01 00:00:00')
通过datetime时间类型生成了一堆时间,然后基于此创建了一个时间序列ts,ts的索引类型为DatetimeIndex类型。生成ts以后我们就有了多种发放选择元素了,只需要传入一个能被Pandas识别的日期字符串就可以:
In [92]: ts.index[0]
Out[92]: Timestamp('2018-01-01 00:00:00')
In [93]: ts[ts.index[0]]
Out[93]: 0.8850886421792693
In [94]: ts['2018-01-01']
Out[94]: 0.8850886421792693
In [95]: ts['2018/01/01']
Out[95]: 0.8850886421792693
In [96]: ts['1/1/2018']
Out[96]: 0.8850886421792693
In [97]: ts[datetime(2018, 1, 1)]
Out[97]: 0.8850886421792693
在Pandas中生成日期范围也非常灵活,主要通过pandas.date_range函数完成,该函数主要有以下几个参数:
-
start: 指定了日期范围的起始时间;
-
end: 指定了日期范围的结束时间;
-
periods: 指定了间隔范围,如果只是指定了start和end日期的其中一个,则需要该参数;
-
freq: 指定了日期频率,比如D代表每天,H代表每小时,M代表月,这些频率字符前也可以指定一个整数,代表具体多少天,多少小时,比如5D代表5天。MS代表每月第一天,BM代表每月最后一个工作日,或者时频率组合字符串,1h30min代表1小时30分钟。
In [98]: pd.date_range('2018-1-1', '2019', freq='M')
Out[98]:
DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31', '2018-04-30',
'2018-05-31', '2018-06-30', '2018-07-31', '2018-08-31',
'2018-09-30', '2018-10-31', '2018-11-30', '2018-12-31'],
dtype='datetime64[ns]', freq='M')
In [99]: pd.date_range('2018-1-1', '2018-12-1', freq='MS')
Out[99]:
DatetimeIndex(['2018-01-01', '2018-02-01', '2018-03-01', '2018-04-01',
'2018-05-01', '2018-06-01', '2018-07-01', '2018-08-01',
'2018-09-01', '2018-10-01', '2018-11-01', '2018-12-01'],
dtype='datetime64[ns]', freq='MS')
有时候时间序列是按每小时显示统计的,但是我们想将统计频率转换成按天来统计,可以使用时间序列的resample方法,该方法非常强大,不仅仅支持高频率的数据聚合到低频率(降采样),也支持低频率转化到高频率统计(升采样):
In [100]: dates = pd.date_range('2018-1-1', '2018-1-2 23:00:00', freq='H')
In [101]: ts = Series(np.arange(len(dates)), index=dates)
In [102]: ts.size
Out[102]: 48
In [103]: ts.head(5)
Out[103]:
2018-01-01 00:00:00 0
2018-01-01 01:00:00 1
2018-01-01 02:00:00 2
2018-01-01 03:00:00 3
2018-01-01 04:00:00 4
Freq: H, dtype: int32
In [104]: ts.tail(5)
Out[104]:
2018-01-02 19:00:00 43
2018-01-02 20:00:00 44
2018-01-02 21:00:00 45
2018-01-02 22:00:00 46
2018-01-02 23:00:00 47
Freq: H, dtype: int32
以上代码,创建了以每小时为频率的时间序列,该数据集有48个元素,使用ts.head(5)方法显示前5个元素,使用ts.tail(5)显示后5个元素。
把以上数据转换为按天数据统计:
In [107]: ts.resample('D').sum()
Out[107]:
2018-01-01 276
2018-01-02 852
Freq: D, dtype: int32
也可以按天的所有数据的平均数进行统计:
In [108]: ts.resample('D').mean()
Out[108]:
2018-01-01 11.5
2018-01-02 35.5
Freq: D, dtype: float64
当把低频率的数据转换成高频率的数据时,默认情况下Pandas会引入NoN值,因为没有办法从低频率的数据计算出高频率的数据,但可以通过fill_method参数指定插值方式:
In [109]: ts.resample('D').mean().resample('H').mean()
Out[109]:
2018-01-01 00:00:00 11.5
2018-01-01 01:00:00 NaN
2018-01-01 02:00:00 NaN
....
2018-01-01 21:00:00 NaN
2018-01-01 22:00:00 NaN
2018-01-01 23:00:00 NaN
2018-01-02 00:00:00 35.5
Freq: H, dtype: float64
以上代码,我们先将ts转换为按天统计,接着又转换成按小时平均值统计,Pandas引入NaN值,但当使用ffill(用前面的值代替NaN值)就不会又NaN值出现:
In [110]: ts.resample('D').mean().resample('H').ffill()
Out[110]:
2018-01-01 00:00:00 11.5
2018-01-01 01:00:00 11.5
....
2018-01-01 23:00:00 11.5
2018-01-02 00:00:00 35.5
Freq: H, dtype: float64
Pandas总结
Pandas虽然构建于NumPy之上,但提供了更灵活强大的功能:
- Series 和 DataFrame数据集的创建;
- 数据集的数据项选择方式;
- 数据集的自动对齐规则;
- 数据集的合并方式;
- 缺失值的处理;
- 时间序列的处理;