学习书籍: 《利用Python进行数据分析·第2版》
第02章 Python语法基础
2.3 Python语法基础
组织代码方式
Python使用空白字符(tab和空格)来组织代码,而不是通过括号()
例子:排序算法的for循环
for x in array:
if x < pivot:
less.append(x)
else:
greater.append(x)
Python的语句不需要用分号结尾。但是,分号却可以用来给同在一行的语句切分
a = 5; b = 6; c = 7
万物皆对象
Python语言的一个重要特性就是它的对象模型的一致性。每个数字、字符串、数据结构、函数、类、模块等等,都是在Python解释器的自有“盒子”内,它被认为是Python对象。每个对象都有类型(例如,字符串或函数)和内部数据。在实际中,这可以让语言非常灵活,因为函数也可以被当做对象使用。
注释标记为#
例子:
results = []
for line in file_handle:
# keep the empty lines for now
# if len(line) == 0:
# continue
results.append(line.replace('foo', 'bar'))
变量与参数传递
创建一个整数列表:
In [8]: a = [1, 2, 3]
假设将a赋值给一个新变量b:
In [9]: b = a
在Python中,a和b实际上是同一个对象,即原有列表[1, 2, 3]。你可以在a中添加一个元素,然后检查b:
In [10]: a.append(4)
In [11]: b
Out[11]: [1, 2, 3, 4]
python是强类型化语言
python是强类型化语言,意味着每个对象都有明确的类型(或类)(str型不能与int型相加),默许转换只会发生在特定的情况下(float型能与int型相加)
例子1:str型不能与int型相加
In [16]: '5' + 5
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-16-f9dbf5f0b234> in <module>()
----> 1 '5' + 5
TypeError: must be str, not int
例子2:loat型能与int型相加
In [17]: a = 4.5
In [18]: b = 2
# String formatting, to be visited later
In [19]: print('a is {0}, b is {1}'.format(type(a), type(b)))
a is <class 'float'>, b is <class 'int'>
In [20]: a / b
Out[20]: 2.25
isinstance函数 : 检查对象是某个类型的实例,可以用类型元组,检查对象的类型是否在元组中
Python的对象通常都有属性(其它存储在对象内部的Python对象)和方法(对象的附属函数可以访问对象的内部数据)
可迭代对象
Iterable object(可迭代对象)
可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。
数组是可迭代的。但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。
如果从技术上讲,对象不是数组,而是表示某物的集合(列表,集合),for..of 是一个能够遍历它的很好的语法
iter函数 : 判断对象是否是可迭代的
可定义isiterable函数
def isiterable(obj):
try:
iter(obj)
return True
except TypeError: # not iterable
return False
isiterable函数 :返回字符串以及大多数Python集合类型为True
In [29]: isiterable('a string')
Out[29]: True
In [30]: isiterable([1, 2, 3])
Out[30]: True
In [31]: isiterable(5)
Out[31]: False
引入模块
在Python中,模块就是一个有.py扩展名、包含Python代码的文件。假设有以下模块:
# some_module.py
PI = 3.14159
def f(x):
return x + 2
def g(a, b):
return a + b
如果想从同目录下的另一个文件访问some_module.py中定义的变量和函数,可以:
import some_module
result = some_module.f(5)
pi = some_module.PI
或
from some_module import f, g, PI
result = g(5, PI)
使用as关键词,你可以给引入起不同的变量名:
import some_module as sm
from some_module import PI as pi, g as gf
r1 = sm.f(pi)
r2 = gf(6, pi)
二元运算符和比较运算符
要判断两个引用是否指向同一个对象,可以使用**is方法。is not方法可以判断两个对象是不同的(因为list**总是创建一个新的Python列表(即复制),我们可以断定c是不同于a的。使用is比较与==运算符不同,如下):
In [35]: a = [1, 2, 3]
In [36]: b = a
In [37]: c = list(a)
In [38]: a is b
Out[38]: True
In [39]: a is not c
Out[39]: True
In [40]: a == c
Out[40]: True
可变与不可变对象
Python中的大多数对象(列表、字典、NumPy数组,和用户定义的类型(类))都是可变对象),即这些对象或包含的值可以被修改,而字符串和元组为不可变对象
字符串
#单引号或双引号来写字符串
a ='one way of writing a string'
b ="another way"
#对于有换行符的字符串,可以使用三引号,’’’或”””写字符串
c ="""
This is a longer string that
spans multiple lines
"""
Python的字符串是不可变的,不能修改字符串
str函数:可以被转化为字符串
In[61]: a =5.6
In[62]: s = str(a)
In[63]:print(s)
5.6
字符串是一个序列的Unicode字符,因此可以像其它序列,比如列表和元组一样处理:
In[64]: s ='python'
In[65]: list(s)
Out[65]:['p','y','t','h','o','n']
In[66]: s[:3]
Out[66]:'pyt'
语法s[:3]被称作切片,适用于许多Python序列。
反斜杠是转义字符
意思是它备用来表示特殊字符,比如换行符\n或Unicode字符。要写一个包含反斜杠的字符串,需要进行转义:
In[67]: s ='12\\34'
In[68]:print(s)
12\34
可以在字符串前面加一个r(代表raw),表明字符就是它自身
In[71]: a ='this is the first half '
In[72]: b ='and this is the second half'
In[73]: a + b
Out[73]:'this is the first half and this is the second half'
字符串对象有format方法,可以替换格式化的参数为字符串,产生一个新的字符串:
In[74]: template ='{0:.2f} {1:s} are worth US${2:d}'
#{0:.2f}表示格式化第一个参数为带有两位小数的浮点数。
#{1:s}表示格式化第二个参数为字符串。
#{2:d}表示格式化第三个参数为一个整数。
要替换参数为这些格式化的参数,我们传递format方法一个序列:
In[75]: template.format(4.5560,'Argentine Pesos',1)
Out[75]:'4.56 Argentine Pesos are worth US$1'
字节和Unicode
在Python 3及以上版本中,Unicode是一级的字符串类型,这样可以更一致的处理ASCII和Non-ASCII文本。在老的Python版本中,字符串都是字节,不使用Unicode编码。
相关函数:encode decode
布尔值
Python中的布尔值有两个,True和False
类型转换
str、bool、int和float也是函数,可以用来转换类型:
In[91]: s ='3.14159'
In[92]: fval = float(s)
In[93]: type(fval)
Out[93]: float
In[94]: int(fval)
Out[94]:3
In[95]: bool(fval)
Out[95]:True
In[96]: bool(0)
Out[96]:False
None
None是Python的空值类型。如果一个函数没有明确的返回值,就会默认返回None:
In[97]: a =None
In[98]: a isNone
Out[98]:True
In[99]: b =5
In[100]: b isnotNone
Out[100]:True
None也常常作为函数的默认参数
def add_and_maybe_multiply(a, b, c=None):
result = a + b
if c isnotNone:
result = result * c
return result
另外,None不仅是一个保留字,还是唯一的NoneType的实例:
In[101]: type(None)
Out[101]:NoneType
日期和时间
Python内建的datetime模块提供了datetime、date和time类型。datetime类型结合了date和time,是最常使用的:
Python内建的datetime模块提供了datetime、date和time类型。datetime类型结合了date和time,是最常使用的:
In[102]:from datetime import datetime, date, time
In[103]: dt = datetime(2011,10,29,20,30,21)
In[104]: dt.day
Out[104]:29
In[105]: dt.minute
Out[105]:30
根据datetime实例,你可以用date和time提取出各自的对象:
In[106]: dt.date()
Out[106]: datetime.date(2011,10,29)
In[107]: dt.time()
Out[107]: datetime.time(20,30,21)
strftime方法可以将datetime格式化为字符串:
In[108]: dt.strftime('%m/%d/%Y %H:%M')
Out[108]:'10/29/2011 20:30'
strptime可以将字符串转换成datetime对象:
In[109]: datetime.strptime('20091031','%Y%m%d')
Out[109]: datetime.datetime(2009,10,31,0,0)
聚类或对时间序列进行分组,替换datetimes的time字段有时会很有用。例如,用0替换分和秒:
In[110]: dt.replace(minute=0, second=0)
Out[110]: datetime.datetime(2011,10,29,20,0)
因为datetime.datetime是不可变类型,上面的方法会产生新的对象。
两个datetime对象的差会产生一个datetime.timedelta类型:
In[111]: dt2 = datetime(2011,11,15,22,30)
In[112]: delta = dt2 - dt
In[113]: delta
Out[113]: datetime.timedelta(17,7179)
In[114]: type(delta)
Out[114]: datetime.timedelta
结果timedelta(17, 7179)指明了timedelta将17天、7179秒的编码方式。
将timedelta添加到datetime,会产生一个新的偏移datetime:
In[115]: dt
Out[115]: datetime.datetime(2011,10,29,20,30,21)
In[116]: dt + delta
Out[116]: datetime.datetime(2011,11,15,22,30)
控制流
Python有若干内建的关键字进行条件逻辑、循环和其它控制流操作。
if、elif和else
if
if是最广为人知的控制流语句。它检查一个条件,如果为True,就执行后面的语句:
if x <0:
print('It's negative')
if后面可以跟一个或多个elif,所有条件都是False时,还可以添加一个else:
if x <0:
print('It's negative')
elif x == 0:
print('Equal to zero')
elif 0 < x < 5:
print('Positive but smaller than 5')
else:
print('Positiveand larger than or equal to 5')
如果某个条件为True,后面的elif就不会被执行。当使用and和or时,复合条件语句是从左到右执行:
In[117]: a =5; b =7
In[118]: c =8; d =4
In[119]:if a < b or c > d:
.....:print('Made it')
Made it
在这个例子中,c > d不会被执行,因为第一个比较是True:
也可以把比较式串在一起:
In[120]:4>3>2>1
Out[120]:True
for循环
for循环是在一个集合(列表或元组)中进行迭代,或者就是一个迭代器。for循环的标准语法是:
for value in collection:
# do something with value
你可以用continue使for循环提前,跳过剩下的部分。看下面这个例子,将一个列表中的整数相加,跳过None:
sequence =[1,2,None,4,None,5]
total =0
for value in sequence:
if value isNone:
continue
total += value
可以用break跳出for循环。下面的代码将各元素相加,直到遇到5:
sequence =[1,2,0,4,6,5,2,1]
total_until_5 =0
for value in sequence:
if value ==5:
break
total_until_5 += value
break只中断for循环的最内层,其余的for循环仍会运行:
In[121]:for i in range(4):
.....:for j in range(4):
.....:if j > i:
.....:break
.....:print((i, j))
.....:
(0,0)
(1,0)
(1,1)
(2,0)
(2,1)
(2,2)
(3,0)
(3,1)
(3,2)
(3,3)
如果集合或迭代器中的元素序列(元组或列表),可以用for循环将其方便地拆分成变量:
for a, b, c in iterator:
# do something
While循环
while循环指定了条件和代码,当条件为False或用break退出循环,代码才会退出:
x =256
total =0
while x >0:
if total >500:
break
total += x
x = x //2
pass非操作语句
pass是Python中的非操作语句。代码块不需要任何动作时可以使用(作为未执行代码的占位符);因为Python需要使用空白字符划定代码块,所以需要pass:
if x <0:
print('negative!')
elif x ==0:
# TODO: put something smart here
pass
else:
print('positive!')
range函数
range函数返回一个迭代器,它产生一个均匀分布的整数序列:
In[122]: range(10)
Out[122]: range(0,10)
In[123]: list(range(10))
Out[123]:[0,1,2,3,4,5,6,7,8,9]
range的三个参数是(起点,终点,步进):
In[124]: list(range(0,20,2))
Out[124]:[0,2,4,6,8,10,12,14,16,18]
In[125]: list(range(5,0,-1))
Out[125]:[5,4,3,2,1]
可以看到,range产生的整数不包括终点。range的常见用法是用序号迭代序列:
seq =[1,2,3,4]
for i in range(len(seq)):
val = seq[i]
可以使用list来存储range在其他数据结构中生成的所有整数,默认的迭代器形式通常是你想要的。下面的代码对0到99999中3或5的倍数求和:
sum =0
for i in range(100000):
# % is the modulo operator
if i %3==0or i %5==0:
sum += i
虽然range可以产生任意大的数,但任意时刻耗用的内存却很小。
三元表达式
三元表达式
Python中的三元表达式可以将if-else语句放到一行里。语法如下:
value = true-expr if condition else false-expr
true-expr或false-expr可以是任何Python代码。它和下面的代码效果相同:
if condition:
value = true-expr
else:
value = false-expr
下面是一个更具体的例子:
In[126]: x =5
In[127]:'Non-negative'if x >=0else'Negative'
Out[127]:'Non-negative'
和if-else一样,只有一个表达式会被执行。因此,三元表达式中的if和else可以包含大量的计算,但只有True的分支会被执行。因此,三元表达式中的if和else可以包含大量的计算,但只有True的分支会被执行。
虽然使用三元表达式可以压缩代码,但会降低代码可读性。
第03章 Python的数据结构、函数和文件
3.1 数据结构和序列
元组
元组是一个固定长度,不可改变的Python序列对象。创建元组的最简单方式,是用逗号分隔一列值:
In [1]: tup = 4, 5, 6
In [2]: tup
Out[2]: (4, 5, 6)
当用复杂的表达式定义元组,最好将值放到圆括号内,如下所示:
In [3]: nested_tup = (4, 5, 6), (7, 8)
In [4]: nested_tup
Out[4]: ((4, 5, 6), (7, 8))
用tuple可以将任意序列或迭代器转换成元组:
In [5]: tuple([4, 0, 2])
Out[5]: (4, 0, 2)
In [6]: tup = tuple('string')
In [7]: tup
Out[7]: ('s', 't', 'r', 'i', 'n', 'g')
可以用方括号访问元组中的元素。和C、C++、JAVA等语言一样,序列是从0开始的:
In [8]: tup[0]
Out[8]: 's'
元组中存储的对象可能是可变对象。一旦创建了元组,元组中的对象就不能修改了:
In [9]: tup = tuple(['foo', [1, 2], True])
In [10]: tup[2] = False
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-c7308343b841> in <module>()
----> 1 tup[2] = False
TypeError: 'tuple' object does not support item assignment
如果元组中的某个对象是可变的,比如列表,可以在原位进行修改:
In [11]: tup[1].append(3)
In [12]: tup
Out[12]: ('foo', [1, 2, 3], True)
可以用加号运算符将元组串联起来:
In [13]: (4, None, 'foo') + (6, 0) + ('bar',)
Out[13]: (4, None, 'foo', 6, 0, 'bar')
元组乘以一个整数,像列表一样,会将几个元组的复制串联起来:
In [14]: ('foo', 'bar') * 4
Out[14]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')
对象本身并没有被复制,只是引用了它。
拆分元组
如果你想将元组赋值给类似元组的变量,Python会试图拆分等号右边的值:
In [15]: tup = (4, 5, 6)
In [16]: a, b, c = tup
In [17]: b
Out[17]: 5
即使含有元组的元组也会被拆分:
In [18]: tup = 4, 5, (6, 7)
In [19]: a, b, (c, d) = tup
In [20]: d
Out[20]: 7
使用这个功能,你可以很容易地替换变量的名字,其它语言可能是这样:
tmp = a
a = b
b = tmp
但是在Python中,替换可以这样做:
In [21]: a, b = 1, 2
In [22]: a
Out[22]: 1
In [23]: b
Out[23]: 2
In [24]: b, a = a, b
In [25]: a
Out[25]: 2
In [26]: b
Out[26]: 1
变量拆分常用来迭代元组或列表序列:
In [27]: seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [28]: for a, b, c in seq:
....: print('a={0}, b={1}, c={2}'.format(a, b, c))
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9
format的用法参考python格式化输出之format用法
format基本用法参考
>>> print('{} {}'.format('hello','world')) # 不带字段
hello world
>>> print('{0} {1}'.format('hello','world')) # 带数字编号
hello world
>>> print('{0} {1} {0}'.format('hello','world')) # 打乱顺序
hello world hello
>>> print('{1} {1} {0}'.format('hello','world'))
world world hello
>>> print('{a} {tom} {a}'.format(tom='hello',a='world')) # 带关键字
world hello world
另一个常见用法是从函数返回多个值。后面会详解。
Python最近新增了更多高级的元组拆分功能,允许从元组的开头“摘取”几个元素。它使用了特殊的语法*rest,这也用在函数签名中以抓取任意长度列表的位置参数:
In [29]: values = 1, 2, 3, 4, 5
In [30]: a, b, *rest = values
In [31]: a, b
Out[31]: (1, 2)
In [32]: rest
Out[32]: [3, 4, 5]
rest的部分是想要舍弃的部分,rest的名字不重要。作为惯用写法,许多Python程序员会将不需要的变量使用下划线:
In [33]: a, b, *_ = values
tuple方法
因为元组的大小和内容不能修改,它的实例方法都很轻量。其中一个很有用的就是count(也适用于列表),它可以统计某个值得出现频率:
In [34]: a = (1, 2, 2, 2, 3, 4, 2)
In [35]: a.count(2)
Out[35]: 4
列表
与元组对比(括号定义,不能修改),列表的长度可变、内容可以被修改。你可以用方括号定义,或用list函数:
In [36]: a_list = [2, 3, 7, None]
In [37]: tup = ('foo', 'bar', 'baz')
In [38]: b_list = list(tup)
In [39]: b_list
Out[39]: ['foo', 'bar', 'baz']
In [40]: b_list[1] = 'peekaboo'
In [41]: b_list
Out[41]: ['foo', 'peekaboo', 'baz']
列表和元组的语义接近,在许多函数中可以交叉使用。
list函数常用来在数据处理中实体化迭代器或生成器:
In [42]: gen = range(10)
In [43]: gen
Out[43]: range(0, 10)
In [44]: list(gen)
Out[44]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
添加和删除元素
可以用append在列表末尾添加元素:
In [45]: b_list.append('dwarf')
In [46]: b_list
Out[46]: ['foo', 'peekaboo', 'baz', 'dwarf']
insert可以在特定的位置插入元素:
In [47]: b_list.insert(1, 'red')
In [48]: b_list
Out[48]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']
插入的序号必须在0和列表长度之间。
警告:与append相比,insert耗费的计算量大,因为对后续元素的引用必须在内部迁移,以便为新元素提供空间。如果要在序列的头部和尾部插入元素,你可能需要使用collections.deque,一个双尾部队列。
insert的逆运算是pop,它移除并返回指定位置的元素:
In [49]: b_list.pop(2)
Out[49]: 'peekaboo'
In [50]: b_list
Out[50]: ['foo', 'red', 'baz', 'dwarf']
可以用remove去除某个值,remove会先寻找第一个值并除去:
In [51]: b_list.append('foo')
In [52]: b_list
Out[52]: ['foo', 'red', 'baz', 'dwarf', 'foo']
In [53]: b_list.remove('foo')
In [54]: b_list
Out[54]: ['red', 'baz', 'dwarf', 'foo']
如果不考虑性能,使用append和remove,可以把Python的列表当做完美的“多重集”数据结构。
用in可以检查列表是否包含某个值:
In [55]: 'dwarf' in b_list
Out[55]: True
否定in可以再加一个not:
In [56]: 'dwarf' not in b_list
Out[56]: False
在列表中检查是否存在某个值远比字典和集合速度慢,因为Python是线性搜索列表中的值,但在字典和集合中,在同样的时间内还可以检查其它项(基于哈希表)。
串联和组合列表
与元组类似,可以用加号将两个列表串联起来:
In [57]: [4, None, 'foo'] + [7, 8, (2, 3)]
Out[57]: [4, None, 'foo', 7, 8, (2, 3)]
如果已经定义了一个列表,用extend方法可以追加多个元素:
In [58]: x = [4, None, 'foo']
In [59]: x.extend([7, 8, (2, 3)])
In [60]: x
Out[60]: [4, None, 'foo', 7, 8, (2, 3)]
通过加法将列表串联的计算量较大,因为要新建一个列表,并且要复制对象。用extend追加元素,尤其是到一个大列表中,更为可取。因此:
everything = []
for chunk in list_of_lists:
everything.extend(chunk)
要比串联方法快:
everything = []
for chunk in list_of_lists:
everything = everything + chunk
排序
你可以用sort函数将一个列表原地排序(不创建新的对象):
In [61]: a = [7, 2, 5, 1, 3]
In [62]: a.sort()
In [63]: a
Out[63]: [1, 2, 3, 5, 7]
sort有一些选项,有时会很好用。其中之一是二级排序key,可以用这个key进行排序。例如,我们可以按长度对字符串进行排序:
In [64]: b = ['saw', 'small', 'He', 'foxes', 'six']
In [65]: b.sort(key=len)
In [66]: b
Out[66]: ['He', 'saw', 'six', 'small', 'foxes']
稍后,我们会学习sorted函数,它可以产生一个排好序的序列副本。
二分搜索和维护已排序的列表
bisect模块支持二分查找,和向已排序的列表插入值。bisect.bisect可以找到插入值后仍保证排序的位置,bisect.insort是向这个位置插入值:
In [67]: import bisect
In [68]: c = [1, 2, 2, 2, 3, 4, 7]
In [69]: bisect.bisect(c, 2)
Out[69]: 4
In [70]: bisect.bisect(c, 5)
Out[70]: 6
In [71]: bisect.insort(c, 6)
In [72]: c
Out[72]: [1, 2, 2, 2, 3, 4, 6, 7]
注意:bisect模块不会检查列表是否已排好序,进行检查的话会耗费大量计算。因此,对未排序的列表使用bisect不会产生错误,但结果不一定正确。
切片
用切边可以选取大多数序列类型的一部分,切片的基本形式是在方括号中使用start:stop:
In [73]: seq = [7, 2, 3, 7, 5, 6, 0, 1]
In [74]: seq[1:5]
Out[74]: [2, 3, 7, 5]
切片也可以被序列赋值:
In [75]: seq[3:4] = [6, 3]
In [76]: seq
Out[76]: [7, 2, 3, 6, 3, 5, 6, 0, 1]
切片的起始元素是包括的,不包含结束元素。因此,结果中包含的元素个数是stop - start。
start或stop都可以被省略,省略之后,分别默认序列的开头和结尾:
In [77]: seq[:5]
Out[77]: [7, 2, 3, 6, 3]
In [78]: seq[3:]
Out[78]: [6, 3, 5, 6, 0, 1]
负数表明从后向前切片:
In [79]: seq[-4:]
Out[79]: [5, 6, 0, 1]
In [80]: seq[-6:-2]
Out[80]: [6, 3, 5, 6]
需要一段时间来熟悉使用切片,尤其是当你之前学的是R或MATLAB。下图展示了正整数和负整数的切片。在图中,指数标示在边缘以表明切片是在哪里开始哪里结束的。
在第二个冒号后面使用step,可以隔一个取一个元素:
In [81]: seq[::2]
Out[81]: [7, 3, 3, 6, 1]
一个聪明的方法是使用-1,它可以将列表或元组颠倒过来:
In [82]: seq[::-1]
Out[82]: [1, 0, 6, 5, 3, 6, 3, 2, 7]
序列函数
Python有一些有用的序列函数。
enumerate函数
迭代一个序列时,你可能想跟踪当前项的序号。手动的方法可能是下面这样:
i = 0
for value in collection:
# do something with value
i += 1
因为这么做很常见,Python内建了一个enumerate函数,可以返回(i, value)元组序列:
for i, value in enumerate(collection):
# do something with value
当你索引数据时,使用enumerate的一个好方法是计算序列(唯一的)dict映射到位置的值:
In [83]: some_list = ['foo', 'bar', 'baz']
In [84]: mapping = {}
In [85]: for i, v in enumerate(some_list):
....: mapping[v] = i
In [86]: mapping
Out[86]: {'bar': 1, 'baz': 2, 'foo': 0}
sorted函数
sorted函数可以从任意序列的元素返回一个新的排好序的列表:
In [87]: sorted([7, 1, 2, 6, 0, 3, 2])
Out[87]: [0, 1, 2, 2, 3, 6, 7]
In [88]: sorted('horse race')
Out[88]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']
sorted函数可以接受和sort相同的参数。
zip函数
zip可以将多个列表、元组或其它序列成对组合成一个元组列表:
In [89]: seq1 = ['foo', 'bar', 'baz']
In [90]: seq2 = ['one', 'two', 'three']
In [91]: zipped = zip(seq1, seq2)
In [92]: list(zipped)
Out[92]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]
zip可以处理任意多的序列,元素的个数取决于最短的序列:
In [93]: seq3 = [False, True]
In [94]: list(zip(seq1, seq2, seq3))
Out[94]: [('foo', 'one', False), ('bar', 'two', True)]
zip的常见用法之一是同时迭代多个序列,可能结合enumerate使用:
In [95]: for i, (a, b) in enumerate(zip(seq1, seq2)):
....: print('{0}: {1}, {2}'.format(i, a, b))
....:
0: foo, one
1: bar, two
2: baz, three
给出一个“被压缩的”序列,zip可以被用来解压序列。也可以当作把行的列表转换为列的列表。这个方法看起来有点神奇:
In [96]: pitchers = [('Nolan', 'Ryan'), ('Roger', 'Clemens'),
....: ('Schilling', 'Curt')]
In [97]: first_names, last_names = zip(*pitchers)
In [98]: first_names
Out[98]: ('Nolan', 'Roger', 'Schilling')
In [99]: last_names
Out[99]: ('Ryan', 'Clemens', 'Curt')
reversed函数
reversed可以从后向前迭代一个序列:
In [100]: list(reversed(range(10)))
Out[100]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
要记住reversed是一个生成器(后面详细介绍),只有实体化(即列表或for循环)之后才能创建翻转的序列。
字典
字典可能是Python最为重要的数据结构。它更为常见的名字是哈希映射或关联数组。它是键值对的大小可变集合,键和值都是Python对象。创建字典的方法之一是使用尖括号,用冒号分隔键和值:
In [101]: empty_dict = {}
In [102]: d1 = {'a' : 'some value', 'b' : [1, 2, 3, 4]}
In [103]: d1
Out[103]: {'a': 'some value', 'b': [1, 2, 3, 4]}
你可以像访问列表或元组中的元素一样,访问、插入或设定字典中的元素:
In [104]: d1[7] = 'an integer'
In [105]: d1
Out[105]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
In [106]: d1['b']
Out[106]: [1, 2, 3, 4]
你可以用检查列表和元组是否包含某个值的方法,检查字典中是否包含某个键:
In [107]: 'b' in d1
Out[107]: True
可以用del关键字或pop方法(返回值的同时删除键)删除值:
In [108]: d1[5] = 'some value'
In [109]: d1
Out[109]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value'}
In [110]: d1['dummy'] = 'another value'
In [111]: d1
Out[111]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value',
'dummy': 'another value'}
In [112]: del d1[5]
In [113]: d1
Out[113]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
'dummy': 'another value'}
In [114]: ret = d1.pop('dummy')
In [115]: ret
Out[115]: 'another value'
In [116]: d1
Out[116]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
keys和values是字典的键和值的迭代器方法。虽然键值对没有顺序,这两个方法可以用相同的顺序输出键和值:
In [117]: list(d1.keys())
Out[117]: ['a', 'b', 7]
In [118]: list(d1.values())
Out[118]: ['some value', [1, 2, 3, 4], 'an integer']
用update方法可以将一个字典与另一个融合:
In [119]: d1.update({'b' : 'foo', 'c' : 12})
In [120]: d1
Out[120]: {'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12}
update方法是原地改变字典,因此任何传递给update的键的旧的值都会被舍弃。
用序列创建字典
常常,你可能想将两个序列配对组合成字典。下面是一种写法:
mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = value
因为字典本质上是2元元组的集合,dict可以接受2元元组的列表:
In [121]: mapping = dict(zip(range(5), reversed(range(5))))
In [122]: mapping
Out[122]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
dict() 函数
函数输入输出实例:
#输入:
dict0 = dict() # 传一个空字典
print('dict0:', dict0)
dict1 = dict({'three': 3, 'four': 4}) # 传一个字典
print('dict1:', dict1)
dict2 = dict(five=5, six=6) # 传关键字
print('dict2:', dict2)
dict3 = dict([('seven', 7), ('eight', 8)]) # 传一个包含一个或多个元祖的列表
print('dict3:', dict3)
dict5 = dict(zip(['eleven', 'twelve'], [11, 12])) # 传一个zip()函数
print('dict5:', dict5)
#输出:
dict0: {}
dict1: {'four': 4, 'three': 3}
dict2: {'five': 5, 'six': 6}
dict3: {'seven': 7, 'eight': 8}
dict5: {'twelve': 12, 'eleven': 11}
后面会谈到dict comprehensions,另一种构建字典的优雅方式。
默认值
下面的逻辑很常见:
if key in some_dict:
value = some_dict[key]
else:
value = default_value
因此,dict的方法get和pop可以取默认值进行返回,上面的if-else语句可以简写成下面:
value = some_dict.get(key, default_value)
get默认会返回None,如果不存在键,pop会抛出一个例外。关于设定值,常见的情况是在字典的值是属于其它集合,如列表。例如,你可以通过首字母,将一个列表中的单词分类:
In [123]: words = ['apple', 'bat', 'bar', 'atom', 'book']
In [124]: by_letter = {}
In [125]: for word in words:
.....: letter = word[0]
.....: if letter not in by_letter:
.....: by_letter[letter] = [word]
.....: else:
.....: by_letter[letter].append(word)
.....:
In [126]: by_letter
Out[126]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}
setdefault方法就正是用于单词分类。前面的for循环可以改写为:
for word in words:
letter = word[0]
by_letter.setdefault(letter, []).append(word)
collections模块有一个很有用的类,defaultdict,它可以进一步简化上面。传递类型或函数以生成每个位置的默认值:
from collections import defaultdict
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
如何使用defaultdict
defaultdict接受一个工厂函数作为参数,如下来构造:
dict =defaultdict( factory_function)
这个factory_function可以是list、set、str等等,作用是当key不存在时,返回的是工厂函数的默认值,比如list对应[ ],str对应的是空字符串,set对应set( ),int对应0,如下举例:
from collections import defaultdict
dict1 = defaultdict(int)
dict2 = defaultdict(set)
dict3 = defaultdict(str)
dict4 = defaultdict(list)
dict1[2] ='two'
print(dict1[1])
print(dict2[1])
print(dict3[1])
print(dict4[1])
#输出为
0
set()
[]
有效的键类型
字典的值可以是任意Python对象,而键通常是不可变的标量类型(整数、浮点型、字符串)或元组(元组中的对象必须是不可变的),这被称为“可哈希性。可以用hash函数可检测一个对象是否是可哈希的(可被用作字典的键):
In [127]: hash('string')
Out[127]: 5023931463650008331
In [128]: hash((1, 2, (2, 3)))
Out[128]: 1097636502276347782
In [129]: hash((1, 2, [2, 3])) # fails because lists are mutable
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-129-800cd14ba8be> in <module>()
----> 1 hash((1, 2, [2, 3])) # fails because lists are mutable
TypeError: unhashable type: 'list'
要用列表当做键,一种方法是将列表转化为元组,只要内部元素可以被哈希,它也就可以被哈希:
In [130]: d = {}
In [131]: d[tuple([1, 2, 3])] = 5
In [132]: d
Out[132]: {(1, 2, 3): 5}
集合
集合是无序的不可重复的元素的集合。你可以把它当做字典,但是只有键没有值。可以用两种方式创建集合:通过set函数或使用尖括号set语句:
In [133]: set([2, 2, 2, 1, 3, 3])
Out[133]: {1, 2, 3}
In [134]: {2, 2, 2, 1, 3, 3}
Out[134]: {1, 2, 3}
集合支持合并、交集、差分和对称差等数学集合运算。考虑两个示例集合:
In [135]: a = {1, 2, 3, 4, 5}
In [136]: b = {3, 4, 5, 6, 7, 8}
合并是取两个集合中不重复的元素。可以用union方法,或者|运算符:
In [137]: a.union(b)
Out[137]: {1, 2, 3, 4, 5, 6, 7, 8}
In [138]: a | b
Out[138]: {1, 2, 3, 4, 5, 6, 7, 8}
交集的元素包含在两个集合中。可以用intersection或&运算符:
In [139]: a.intersection(b)
Out[139]: {3, 4, 5}
In [140]: a & b
Out[140]: {3, 4, 5}
。
所有逻辑集合操作都有另外的原地实现方法,可以直接用结果替代集合的内容。对于大的集合,这么做效率更高:
In [141]: c = a.copy()
In [142]: c |= b
In [143]: c
Out[143]: {1, 2, 3, 4, 5, 6, 7, 8}
In [144]: d = a.copy()
In [145]: d &= b
In [146]: d
Out[146]: {3, 4, 5}
与字典类似,集合元素通常都是不可变的。要获得类似列表的元素,必须转换成元组:
In [147]: my_data = [1, 2, 3, 4]
In [148]: my_set = {tuple(my_data)}
In [149]: my_set
Out[149]: {(1, 2, 3, 4)}
你使用issubset还可以检测一个集合是否是另一个集合的子集或父集:
In [150]: a_set = {1, 2, 3, 4, 5}
In [151]: {1, 2, 3}.issubset(a_set)
Out[151]: True
In [152]: a_set.issuperset({1, 2, 3})
Out[152]: True
集合的内容相同时,集合才对等:
In [153]: {1, 2, 3} == {3, 2, 1}
Out[153]: True
列表、集合和字典推导式
列表推导式是Python最受喜爱的特性之一。它允许用户方便的从一个集合过滤元素,形成列表,在传递参数的过程中还可以修改元素。形式如下:
[expr for val in collection if condition]
它等同于下面的for循环;
result = []
for val in collection:
if condition:
result.append(expr)
filter条件可以被忽略,只留下表达式就行。例如,给定一个字符串列表,我们可以过滤出长度在2及以下的字符串,并将其转换成大写:
In [154]: strings = ['a', 'as', 'bat', 'car', 'dove', 'python']
In [155]: [x.upper() for x in strings if len(x) > 2]
Out[155]: ['BAT', 'CAR', 'DOVE', 'PYTHON']
用相似的方法,还可以推导集合和字典。字典的推导式如下所示:
dict_comp = {key-expr : value-expr for value in collection if condition}
集合的推导式与列表很像,只不过用的是尖括号:
set_comp = {expr for value in collection if condition}
与列表推导式类似,集合与字典的推导也很方便,而且使代码的读写都很容易。来看前面的字符串列表。假如我们只想要字符串的长度,用集合推导式的方法非常方便:
In [156]: unique_lengths = {len(x) for x in strings}
In [157]: unique_lengths
Out[157]: {1, 2, 3, 4, 6}
map函数可以进一步简化:
In [158]: set(map(len, strings))
Out[158]: {1, 2, 3, 4, 6}
map()是一个 Python 内建函数,它允许你不需要使用循环就可以编写简洁的代码,具体用法参考Python Map 函数的使用](https://zhuanlan.zhihu.com/p/205466485)),基本用法为:
map(function, iterable, ...)
#function` - 针对每一个迭代调用的函数
#iterable` - 支持迭代的一个或者多个对象。在 Python 中大部分内建对象,例如 lists, dictionaries, 和 tuples 都是可迭代的
作为一个字典推导式的例子,我们可以创建一个字符串的查找映射表以确定它在列表中的位置:
In [159]: loc_mapping = {val : index for index, val in enumerate(strings)}
In [160]: loc_mapping
Out[160]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}
嵌套列表推导式
假设我们有一个包含列表的列表,包含了一些英文名和西班牙名:
In [161]: all_data = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
.....: ['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]
你可能是从一些文件得到的这些名字,然后想按照语言进行分类。现在假设我们想用一个列表包含所有的名字,这些名字中包含两个或更多的e。可以用for循环来做:
names_of_interest = []
for names in all_data:
enough_es = [name for name in names if name.count('e') >= 2]
names_of_interest.extend(enough_es)
可以用嵌套列表推导式的方法,将这些写在一起,如下所示:
In [162]: result = [name for names in all_data for name in names
.....: if name.count('e') >= 2]
In [163]: result
Out[163]: ['Steven']
嵌套列表推导式看起来有些复杂。列表推导式的for部分是根据嵌套的顺序,过滤条件还是放在最后。下面是另一个例子,我们将一个整数元组的列表扁平化成了一个整数列表:
In [164]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [165]: flattened = [x for tup in some_tuples for x in tup]
In [166]: flattened
Out[166]: [1, 2, 3, 4, 5, 6, 7, 8, 9]
记住,for表达式的顺序是与嵌套for循环的顺序一样(而不是列表推导式的顺序):
flattened = []
for tup in some_tuples:
for x in tup:
flattened.append(x)
你可以有任意多级别的嵌套,但是如果你有两三个以上的嵌套,你就应该考虑下代码可读性的问题了。分辨列表推导式的列表推导式中的语法也是很重要的:
In [167]: [[x for x in tup] for tup in some_tuples]
Out[167]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
这段代码产生了一个列表的列表,而不是扁平化的只包含元素的列表。
3.2 函数
函数是Python中最主要也是最重要的代码组织和复用手段。作为最重要的原则,如果你要重复使用相同或非常类似的代码,就需要写一个函数。通过给函数起一个名字,还可以提高代码的可读性。
函数使用def
关键字声明,用return
关键字返回值:
def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)
同时拥有多条return
语句也是可以的。如果到达函数末尾时没有遇到任何一条return
语句,则返回None
。
函数可以有一些位置参数(positional
)和一些关键字参数(keyword
)。关键字参数通常用于指定默认值或可选参数。在上面的函数中,x和y是位置参数,而z则是关键字参数。也就是说,该函数可以下面这3种方式进行调用:
my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)
函数参数的主要限制在于:关键字参数必须位于位置参数(如果有的话)之后。你可以任何顺序指定关键字参数。也就是说,你不用死记硬背函数参数的顺序,只要记得它们的名字就可以了。
#笔记:也可以用"关键字传递位置参数"。前面的例子,也可以写为:
my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)
#这种写法可以提高可读性。
命名空间、作用域,和局部函数
函数可以访问两种不同作用域中的变量:全局(global)和局部(local)。Python有一种更科学的用于描述变量作用域的名称,即命名空间(namespace)。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁(会有一些例外的情况,具体请参见后面介绍闭包的那一节)。看看下面这个函数:
def func():
a = []
for i in range(5):
a.append(i)
调用func()
之后,首先会创建出空列表a,然后添加5个元素,最后a会在该函数退出的时候被销毁。假如我们像下面这样定义a:
a = []
def func():
for i in range(5):
a.append(i)
虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行:
In [168]: a = None
In [169]: def bind_a_variable():
.....: global a
.....: a = []
.....: bind_a_variable()
.....:
In [170]: print(a)
[]
注意:我常常建议人们不要频繁使用global关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能就说明得要来点儿面向对象编程了(即使用类)。
返回多个值
在我第一次用Python编程时(之前已经习惯了Java和C++),最喜欢的一个功能是:函数可以返回多个值。下面是一个简单的例子:
def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()
在数据分析和其他科学计算应用中,你会发现自己常常这么干。该函数其实只返回了一个对象,也就是一个元组,最后该元组会被拆包到各个结果变量中。在上面的例子中,我们还可以这样写:
return_value = f()
这里的return_value将会是一个含有3个返回值的三元元组。此外,还有一种非常具有吸引力的多值返回方式——返回字典:
def f():
a = 5
b = 6
c = 7
return {'a' : a, 'b' : b, 'c' : c}
取决于工作内容,第二种方法可能很有用。
函数也是对象
由于Python函数都是对象(2分钟让你明白什么是面向对象编程 - 知乎 (zhihu.com)),因此,在其他语言中较难表达的一些设计思想在Python中就要简单很多了。假设我们有下面这样一个字符串数组,希望对其进行一些数据清理工作并执行一堆转换:
In [171]: states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
.....: 'south carolina##', 'West virginia?']
不管是谁,只要处理过由用户提交的调查数据,就能明白这种乱七八糟的数据是怎么一回事。为了得到一组能用于分析工作的格式统一的字符串,需要做很多事情:去除空白符、删除各种标点符号、正确的大写格式等。做法之一是使用内建的字符串方法和正则表达式re模块(可参考正则表达式re模块 ):
import re
def clean_strings(strings):
result = []
for value in strings:
value = value.strip()
value = re.sub('[!#?]', '', value)
value = value.title()#
result.append(value)
return result
其中,strip
方法的功能为移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。其语法与实例为(可参考Python strip()方法)):
#语法
str.strip([chars]);
#实例
str = "00000003210Runoob01230000000";
print str.strip( '0' ); # 去除首尾字符 0
str2 = " Runoob "; # 去除首尾空格
print str2.strip();
#输出:
3210Runoob0123
Runoob
title
方法返回”标题化”的字符串,就是说所有单词的首个字母转化为大写,其余字母均为小写,用法为:
str.title();
结果如下所示:
In [173]: clean_strings(states)
Out[173]:
['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']
其实还有另外一种不错的办法:将需要在一组给定字符串上执行的所有运算做成一个列表,再将列表中的运算一个一个执行:
def remove_punctuation(value):
return re.sub('[!#?]', '', value)
clean_ops = [str.strip, remove_punctuation, str.title]
def clean_strings(strings, ops):
result = []
for value in strings:
for function in ops:
value = function(value)
result.append(value)
return result
然后我们就有了:
In [175]: clean_strings(states, clean_ops)
Out[175]:
['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']
这种多函数模式使你能在很高的层次上轻松修改字符串的转换方式。此时的clean_strings也更具可复用性!
还可以将函数用作其他函数的参数,比如内置的map
函数,它用于在一组数据上应用一个函数:
In [176]: for x in map(remove_punctuation, states):
.....: print(x)
Alabama
Georgia
Georgia
georgia
FlOrIda
south carolina
West virginia
匿名(lambda)函数
Python支持一种被称为匿名的、或lambda
函数。它仅由单条语句组成,该语句的结果就是返回值。它是通过lambda
关键字定义的,这个关键字没有别的含义,仅仅是说“我们正在声明的是一个匿名函数”。
def short_function(x):
return x * 2
equiv_anon = lambda x: x * 2
本书其余部分一般将其称为lambda函数。它们在数据分析工作中非常方便,因为你会发现很多数据转换函数都以函数作为参数的。直接传入lambda函数比编写完整函数声明要少输入很多字(也更清晰),甚至比将lambda
函数赋值给一个变量还要少输入很多字。看看下面这个简单得有些傻的例子:
def apply_to_list(some_list, f):
return [f(x) for x in some_list]
ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2)
虽然你可以直接编写[x *2for x in ints]
,但是这里我们可以非常轻松地传入一个自定义运算给apply_to_list函数。
再来看另外一个例子。假设有一组字符串,你想要根据各字符串不同字母的数量对其进行排序:
In [177]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']
这里,我们可以传入一个lambda函数到列表的sort方法:
In [178]: strings.sort(key=lambda x: len(set(list(x)))) #字符串中的不同的字符数目
In [179]: strings
Out[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']
#笔记:lambda函数之所以会被称为匿名函数,与def声明的函数不同,原因之一就是这种函数对象本身是没有提供名称name属性。
柯里化:部分参数应用
柯里化(currying)是一个有趣的计算机科学术语,它指的是通过“部分参数应用”(partial argument application)从现有函数派生出新函数的技术。例如,假设我们有一个执行两数相加的简单函数:
def add_numbers(x, y):
return x + y
通过这个函数,我们可以派生出一个新的只有一个参数的函数——add_five,它用于对其参数加5:
add_five = lambda y: add_numbers(5, y)
add_numbers的第二个参数称为“柯里化的”(curried)。这里没什么特别花哨的东西,因为我们其实就只是定义了一个可以调用现有函数的新函数而已。内置的functools
模块可以用partial函数将此过程简化:
from functools import partial
add_five = partial(add_numbers, 5)
生成器
能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)是Python的一个重要特点。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的,一个原生的使对象可迭代的方法。比如说,对字典进行迭代可以得到其所有的键:
In [180]: some_dict = {'a': 1, 'b': 2, 'c': 3}
In [181]: for key in some_dict:
.....: print(key)
a
b
c
当你编写for key in some_dict
时,Python解释器首先会尝试从some_dict创建一个迭代器:
In [182]: dict_iterator = iter(some_dict)
In [183]: dict_iterator
Out[183]: <dict_keyiterator at 0x7fbbd5a9f908>
迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的方法也都可以接受任何可迭代对象。比如min、max、sum
等内置方法以及list、tuple
等类型构造器:
In [184]: list(dict_iterator)
Out[184]: ['a', 'b', 'c']
生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return
替换为yeild
即可:
def squares(n=10):
print('Generating squares from 1 to {0}'.format(n ** 2))
for i in range(1, n + 1):
yield i ** 2
调用该生成器时,没有任何代码会被立即执行:
In [186]: gen = squares()
In [187]: gen
Out[187]: <generator object squares at 0x7fbbd5ab4570>
直到你从该生成器中请求元素时,它才会开始执行其代码:
In [188]: for x in gen:
.....: print(x, end=' ')
Generating squares from 1 to 100
1 4 9 16 25 36 49 64 81 100
生成器表达式
另一种更简洁的构造生成器的方法是使用生成器表达式(generator expression)。这是一种类似于列表、字典、集合推导式的生成器。其创建方式为,把列表推导式两端的方括号改成圆括号:
In [189]: gen = (x ** 2 for x in range(100))
In [190]: gen
Out[190]: <generator object <genexpr> at 0x7fbbd5ab29e8>
它跟下面这个冗长得多的生成器是完全等价的:
def _make_gen():
for x in range(100):
yield x ** 2
gen = _make_gen()
生成器表达式也可以取代列表推导式,作为函数参数:
In [191]: sum(x ** 2 for x in range(100))
Out[191]: 328350
In [192]: dict((i, i **2) for i in range(5))
Out[192]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
itertools
模块
标准库itertools
模块中有一组用于许多常见数据算法的生成器。例如,groupby
可以接受任何序列和一个函数。它根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:
In [193]: import itertools
In [194]: first_letter = lambda x: x[0]
In [195]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']
In [196]: for letter, names in itertools.groupby(names, first_letter):
.....: print(letter, list(names)) # names is a generator
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']
下表中列出了一些我经常用到的itertools
函数。建议参阅Python官方文档,进一步学习。
错误和异常处理
优雅地处理Python的错误和异常是构建健壮程序的重要部分。在数据分析中,许多函数函数只用于部分输入。例如,Python的float
函数可以将字符串转换成浮点数,但输入有误时,有ValueError错误:
In [197]: float('1.2345')
Out[197]: 1.2345
In [198]: float('something')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-198-439904410854> in <module>()
----> 1 float('something')
ValueError: could not convert string to float: 'something'
假如想优雅地处理float
的错误,让它返回输入值。我们可以写一个函数,在try/except中调用float:
def attempt_float(x):
try:
return float(x)
except:
return x
当float(x)
抛出异常时,才会执行except
的部分:
In [200]: attempt_float('1.2345')
Out[200]: 1.2345
In [201]: attempt_float('something')
Out[201]: 'something'
你可能注意到float抛出的异常不仅是ValueError
:
In [202]: float((1, 2))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-202-842079ebb635> in <module>()
----> 1 float((1, 2))
TypeError: float() argument must be a string or a number, not 'tuple'
你可能只想处理ValueError,TypeError
错误(输入不是字符串或数值)可能是合理的bug。可以写一个异常类型:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
然后有:
In [204]: attempt_float((1, 2))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-204-9bdfd730cead> in <module>()
----> 1 attempt_float((1, 2))
<ipython-input-203-3e06b8379b6b> in attempt_float(x)
1 def attempt_float(x):
2 try:
----> 3 return float(x)
4 except ValueError:
5 return x
TypeError: float() argument must be a string or a number, not 'tuple'
可以用元组包含多个异常:
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x
某些情况下,你可能不想抑制异常,你想无论try部分的代码是否成功,都执行一段代码。可以使用finally:
f = open(path, 'w')
try:
write_to_file(f)
finally:
f.close()
这里,文件处理f总会被关闭。相似的,你可以用else让只在try部分成功的情况下,才执行代码:
f = open(path, 'w')
try:
write_to_file(f)
except:
print('Failed')
else:
print('Succeeded')
finally:
f.close()
IPython的异常
如果是在%run一个脚本或一条语句时抛出异常,IPython默认会打印完整的调用栈(traceback),在栈的每个点都会有几行上下文:
In [10]: %run examples/ipython_bug.py
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()
13 throws_an_exception()
14
---> 15 calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
11 def calling_things():
12 works_fine()
---> 13 throws_an_exception()
14
15 calling_things()
/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
7 a = 5
8 b = 6
----> 9 assert(a + b == 10)
10
11 def calling_things():
AssertionError:
自身就带有文本是相对于Python标准解释器的极大优点。你可以用魔术命令%xmode,从Plain(与Python标准解释器相同)到Verbose(带有函数的参数值)控制文本显示的数量。后面可以看到,发生错误之后,(用%debug或%pdb magics)可以进入stack进行事后调试。
3.3 文件和操作系统
本书的代码示例大多使用诸如pandas.read_csv之类的高级工具将磁盘上的数据文件读入Python数据结构。但我们还是需要了解一些有关Python文件处理方面的基础知识。好在它本来就很简单,这也是Python在文本和文件处理方面的如此流行的原因之一。
为了打开一个文件以便读写,可以使用内置的open
函数以及一个相对或绝对的文件路径:
In [207]: path = 'examples/segismundo.txt'
In [208]: f = open(path)
默认情况下,文件是以只读模式(’r’)
打开的。然后,我们就可以像处理列表那样来处理这个文件句柄f了,比如对行进行迭代:
for line in f:
pass
从文件中取出的行都带有完整的行结束符(EOL),因此你常常会看到下面这样的代码(得到一组没有EOL的行):
In [209]: lines = [x.rstrip() for x in open(path)]
In [210]: lines
Out[210]:
['Sueña el rico en su riqueza,',
'que más cuidados le ofrece;',
'',
'sueña el pobre que padece',
'su miseria y su pobreza;',
'',
'sueña el que a medrar empieza,',
'sueña el que afana y pretende,',
'sueña el que agravia y ofende,',
'',
'y en el mundo, en conclusión,',
'todos sueñan lo que son,',
'aunque ninguno lo entiende.',
'']
如果使用open
创建文件对象,一定要用close
关闭它。关闭文件可以返回操作系统资源:
In [211]: f.close()
用with
语句可以可以更容易地清理打开的文件:
In [212]: with open(path) as f:
.....: lines = [x.rstrip() for x in f]
这样可以在退出代码块时,自动关闭文件。
rstrip()
函数: 删除 string 字符串末尾的指定字符(默认为空格),语法与实例如下:(可参考rstrip()
))
#用法
str.rstrip([chars])
#实例
str = " this is string example....wow!!! ";
print str.rstrip();
str = "88888888this is string example....wow!!!8888888";
print str.rstrip('8');
#输出
this is string example....wow!!!
88888888this is string example....wow!!!
如果输入f =open(path,’w’)
,就会有一个新文件被创建在examples/segismundo.txt,并覆盖掉该位置原来的任何数据。另外有一个x文件模式,它可以创建可写的文件,但是如果文件路径存在,就无法创建。下表列出了所有的读/写模式。
对于可读文件,一些常用的方法是read、seek和tell
。read
会从文件返回字符。字符的内容是由文件的编码决定的(如UTF-8),如果是二进制模式打开的就是原始字节:
In [213]: f = open(path)
In [214]: f.read(10)
Out[214]: 'Sueña el r'
In [215]: f2 = open(path, 'rb') # Binary mode
In [216]: f2.read(10)
Out[216]: b'Sue\xc3\xb1a el '
read
模式会将文件句柄的位置提前,提前的数量是读取的字节数。tell
可以给出当前的位置:
In [217]: f.tell()
Out[217]: 11
In [218]: f2.tell()
Out[218]: 10
尽管我们从文件读取了10个字符,位置却是11,这是因为用默认的编码用了这么多字节才解码了这10个字符。你可以用sys模块检查默认的编码:
In [219]: import sys
In [220]: sys.getdefaultencoding()
Out[220]: 'utf-8'
seek
将文件位置更改为文件中的指定字节:
In [221]: f.seek(3)
Out[221]: 3
In [222]: f.read(1)
Out[222]: 'ñ'
最后,关闭文件:
In [223]: f.close()
In [224]: f2.close()
向文件写入,可以使用文件的write或writelines方法。例如,我们可以创建一个无空行版的prof_mod.py:
In [225]: with open('tmp.txt', 'w') as handle:
.....: handle.writelines(x for x in open(path) if len(x) > 1)
In [226]: with open('tmp.txt') as f:
.....: lines = f.readlines()
In [227]: lines
Out[227]:
['Sueña el rico en su riqueza,\n',
'que más cuidados le ofrece;\n',
'sueña el pobre que padece\n',
'su miseria y su pobreza;\n',
'sueña el que a medrar empieza,\n',
'sueña el que afana y pretende,\n',
'sueña el que agravia y ofende,\n',
'y en el mundo, en conclusión,\n',
'todos sueñan lo que son,\n',
'aunque ninguno lo entiende.\n']
下表列出了一些最常用的文件方法。
文件的字节和Unicode
Python文件的默认操作是“文本模式”,也就是说,你需要处理Python的字符串(即Unicode)。它与“二进制模式”相对,文件模式加一个b。我们来看上一节的文件(UTF-8编码、包含非ASCII字符):
In [230]: with open(path) as f:
.....: chars = f.read(10)
In [231]: chars
Out[231]: 'Sueña el r'
UTF-8是长度可变的Unicode编码,所以当我从文件请求一定数量的字符时,Python会从文件读取足够多(可能少至10或多至40字节)的字节进行解码。如果以“rb”模式打开文件,则读取确切的请求字节数:
In [232]: with open(path, 'rb') as f:
.....: data = f.read(10)
In [233]: data
Out[233]: b'Sue\xc3\xb1a el '
取决于文本的编码,你可以将字节解码为str对象,但只有当每个编码的Unicode字符都完全成形时才能这么做:
In [234]: data.decode('utf8')
Out[234]: 'Sueña el '
In [235]: data[:4].decode('utf8')
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-235-300e0af10bb7> in <module>()
----> 1 data[:4].decode('utf8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 3: unexpecte
d end of data
文本模式结合了open
的编码选项,提供了一种更方便的方法将Unicode转换为另一种编码:
In [236]: sink_path = 'sink.txt'
In [237]: with open(path) as source:
.....: with open(sink_path, 'xt', encoding='iso-8859-1') as sink:
.....: sink.write(source.read())
In [238]: with open(sink_path, encoding='iso-8859-1') as f:
.....: print(f.read(10))
Sueña el r
注意,不要在二进制模式中使用seek
。如果文件位置位于定义Unicode字符的字节的中间位置,读取后面会产生错误:
In [240]: f = open(path)
In [241]: f.read(5)
Out[241]: 'Sueña'
In [242]: f.seek(4)
Out[242]: 4
In [243]: f.read(1)
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-243-7841103e33f5> in <module>()
----> 1 f.read(1)
/miniconda/envs/book-env/lib/python3.6/codecs.py in decode(self, input, final)
319 # decode input (taking the buffer into account)
320 data = self.buffer + input
--> 321 (result, consumed) = self._buffer_decode(data, self.errors, final
)
322 # keep undecoded input until the next call
323 self.buffer = data[consumed:]
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb1 in position 0: invalid s
tart byte
In [244]: f.close()
如果你经常要对非ASCII字符文本进行数据分析,通晓Python的Unicode功能是非常重要的。更多内容,参阅Python官方文档。
第 04 章 NumPy 基础:数组和矢量计算
NumPy(Numerical Python的简称)是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。
NumPy的部分功能如下:
- ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。
- 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。
- 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
- 线性代数、随机数生成以及傅里叶变换功能。
- 用于集成由C、C++、Fortran等语言编写的代码的A C API。
由于NumPy提供了一个简单易用的C API,因此很容易将数据传递给由低级语言编写的外部库,外部库也能以NumPy数组的形式将数据返回给Python。这个功能使Python成为一种包装C/C++/Fortran历史代码库的选择,并使被包装库拥有一个动态的、易用的接口。
NumPy本身并没有提供多么高级的数据分析功能,理解NumPy数组以及面向数组的计算将有助于你更加高效地使用诸如pandas之类的工具。因为NumPy是一个很大的题目,我会在附录A中介绍更多NumPy高级功能,比如广播。
对于大部分数据分析应用而言,我最关注的功能主要集中在:
- 用于数据整理和清理、子集构造和过滤、转换等快速的矢量化数组运算。
- 常用的数组算法,如排序、唯一化、集合运算等。
- 高效的描述统计和数据聚合/摘要运算。
- 用于异构数据集的合并/连接运算的数据对齐和关系型数据运算。
- 将条件逻辑表述为数组表达式(而不是带有if-elif-else分支的循环)。
- 数据的分组运算(聚合、转换、函数应用等)
虽然NumPy提供了通用的数值数据处理的计算基础,但大多数读者可能还是想将pandas作为统计和分析工作的基础,尤其是处理表格数据时。pandas还提供了一些NumPy所没有的领域特定的功能,如时间序列处理等。
- 笔记:Python的面向数组计算可以追溯到1995年,Jim Hugunin创建了Numeric库。接下来的10年,许多科学编程社区纷纷开始使用Python的数组编程,但是进入21世纪,库的生态系统变得碎片化了。2005年,Travis Oliphant从Numeric和Numarray项目整了出了NumPy项目,进而所有社区都集合到了这个框架下。
NumPy之于数值计算特别重要的原因之一,是因为它可以高效处理大数组的数据。这是因为:
- NumPy是在一个连续的内存块中存储数据,独立于其他Python内置对象。NumPy的C语言编写的算法库可以操作内存,而不必进行类型检查或其它前期工作。比起Python的内置序列,NumPy数组使用的内存更少。
- NumPy可以在整个数组上执行复杂的计算,而不需要Python的for循环。
要搞明白具体的性能差距,考察一个包含一百万整数的数组,和一个等价的Python列表:
In [7]: import numpy as np
In [8]: my_arr = np.arange(1000000)
In [9]: my_list = list(range(1000000))
各个序列分别乘以2:
In [10]: %time for _ in range(10): my_arr2 = my_arr * 2
CPU times: user 20 ms, sys: 50 ms, total: 70 ms
Wall time: 72.4 ms
In [11]: %time for _ in range(10): my_list2 = [x * 2 for x in my_list]
CPU times: user 760 ms, sys: 290 ms, total: 1.05 s
Wall time: 1.05 s
基于NumPy的算法要比纯Python快10到100倍(甚至更快),并且使用的内存更少。
4.1 NumPy的ndarray:一种多维数组对象
NumPy
最重要的一个特点就是其N维数组对象(即ndarray
),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整块数据执行一些数学运算,其语法跟标量元素之间的运算一样。
要明白Python是如何利用与标量值类似的语法进行批次计算,我先引入NumPy
,然后生成一个包含随机数据的小数组:
In [12]: import numpy as np
# Generate some random data
In [13]: data = np.random.randn(2, 3)
In [14]: data
Out[14]:
array([[-0.2047, 0.4789, -0.5194],
[-0.5557, 1.9658, 1.3934]])
然后进行数学运算:
In [15]: data * 10
Out[15]:
array([[ -2.0471, 4.7894, -5.1944],
[ -5.5573, 19.6578, 13.9341]])
In [16]: data + data
Out[16]:
array([[-0.4094, 0.9579, -1.0389],
[-1.1115, 3.9316, 2.7868]])
第一个例子中,所有的元素都乘以10。第二个例子中,每个元素都与自身相加。
笔记:在本章及全书中,我会使用标准的NumPy惯用法
import numpy as np
。你当然也可以在代码中使用from numpy import *
,但不建议这么做。numpy
的命名空间很大,包含许多函数,其中一些的名字与Python的内置函数重名(比如min和max)。
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):
In [17]: data.shape
Out[17]: (2, 3)
In [18]: data.dtype
Out[18]: dtype('float64')
本章将会介绍NumPy
数组的基本用法,这对于本书后面各章的理解基本够用。虽然大多数数据分析工作不需要深入理解NumPy
,但是精通面向数组的编程和思维方式是成为Python科学计算牛人的一大关键步骤。
笔记:当你在本书中看到“数组”、“
NumPy
数组”、”ndarray
”时,基本上都指的是同一样东西,即ndarray
对象。
创建ndarray
创建数组最简单的办法就是使用array
函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy
数组。以一个列表的转换为例:
In [19]: data1 = [6, 7.5, 8, 0, 1]
In [20]: arr1 = np.array(data1)
In [21]: arr1
Out[21]: array([ 6. , 7.5, 8. , 0. , 1. ])
嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:
In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
In [23]: arr2 = np.array(data2)
In [24]: arr2
Out[24]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
因为data2是列表的列表,NumPy
数组arr2的两个维度的shape是从data2引入的。可以用属性ndim和shape验证:
In [25]: arr2.ndim
Out[25]: 2
In [26]: arr2.shape
Out[26]: (2, 4)
除非特别说明(稍后将会详细介绍),np.array
会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype
对象中。比如说,在上面的两个例子中,我们有:
In [27]: arr1.dtype
Out[27]: dtype('float64')
In [28]: arr2.dtype
Out[28]: dtype('int64')
除np.array
之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
In [29]: np.zeros(10)
Out[29]: array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [30]: np.zeros((3, 6))
Out[30]:
array([[ 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0.]])
In [31]: np.empty((2, 3, 2))
Out[31]:
array([[[ 0., 0.],
[ 0., 0.],
[ 0., 0.]],
[[ 0., 0.],
[ 0., 0.],
[ 0., 0.]]])
注意:认为np.empty会返回全0数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
arange
是Python内置函数range
的数组版:
In [32]: np.arange(15)Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
下表列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。
ndarray
的数据类型
dtype(数据类型)是一个特殊的对象,它含有ndarray
将一块内存解释为特定数据类型所需的信息:
In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64) In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32) In [35]: arr1.dtype Out[35]: dtype('float64') In [36]: arr2.dtype Out[36]: dtype('int32')
dtype是NumPy
灵活交互其它系统的源泉之一。多数情况下,它们直接映射到相应的机器表示,这使得“读写磁盘上的二进制数据流”以及“集成低级语言代码(如C、Fortran)”等工作变得更加简单。数值型dtype的命名方式相同:一个类型名(如float或int),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即Python中的float对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64。下表列出了NumPy所支持的全部数据类型。
笔记:记不住这些NumPy的dtype也没关系,新手更是如此。通常只需要知道你所处理的数据的大致类型是浮点数、复数、整数、布尔值、字符串,还是普通的Python对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。
你可以通过ndarray
的astype方法明确地将一个数组从一个dtype转换成另一个dtype:
In [37]: arr = np.array([1, 2, 3, 4, 5])In [38]: arr.dtypeOut[38]: dtype('int64')In [39]: float_arr = arr.astype(np.float64)In [40]: float_arr.dtypeOut[40]: dtype('float64')
在本例中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截取删除:
In [41]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])In [42]: arrOut[42]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])In [43]: arr.astype(np.int32)Out[43]: array([ 3, -1, -2, 0, 12, 10], dtype=int32)
如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:
In [44]: numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)In [45]: numeric_strings.astype(float)Out[45]: array([ 1.25, -9.6 , 42. ])
注意:使用
numpy.string
_类型时,一定要小心,因为NumPy
的字符串数据是大小固定的,发生截取时,不会发出警告。pandas
提供了更多非数值数据的便利的处理方法。
如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串),就会引发一个ValueError。这里,我比较懒,写的是float而不是np.float64;NumPy很聪明,它会将Python类型映射到等价的dtype上。
数组的dtype还有另一个属性:
In [46]: int_array = np.arange(10)In [47]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)In [48]: int_array.astype(calibers.dtype)Out[48]: array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
你还可以用简洁的类型代码来表示dtype:
In [49]: empty_uint32 = np.empty(8, dtype='u4')In [50]: empty_uint32Out[50]: array([ 0, 1075314688, 0, 1075707904, 0, 1075838976, 0, 1072693248], dtype=uint32)
笔记:调用astype总会创建一个新的数组(一个数据的备份),即使新的dtype与旧的dtype相同。
NumPy
数组的运算
数组很重要,因为它使你不用编写循环即可对数据执行批量运算。NumPy用户称其为矢量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级:
In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])In [52]: arrOut[52]: array([[ 1., 2., 3.], [ 4., 5., 6.]])In [53]: arr * arrOut[53]: array([[ 1., 4., 9.], [ 16., 25., 36.]])In [54]: arr - arrOut[54]: array([[ 0., 0., 0.], [ 0., 0., 0.]])
数组与标量的算术运算会将标量值传播到各个元素:
In [55]: 1 / arrOut[55]: array([[ 1. , 0.5 , 0.3333], [ 0.25 , 0.2 , 0.1667]])In [56]: arr ** 0.5Out[56]: array([[ 1. , 1.4142, 1.7321], [ 2. , 2.2361, 2.4495]])
大小相同的数组之间的比较会生成布尔值数组:
In [57]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])In [58]: arr2Out[58]: array([[ 0., 4., 1.], [ 7., 2., 12.]])In [59]: arr2 > arrOut[59]:array([[False, True, False], [ True, False, True]], dtype=bool)
不同大小的数组之间的运算叫做广播(broadcasting),将在附录A中对其进行详细讨论。本书的内容不需要对广播机制有多深的理解。
基本的索引和切片
NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:
In [60]: arr = np.arange(10)In [61]: arrOut[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])In [62]: arr[5]Out[62]: 5In [63]: arr[5:8]Out[63]: array([5, 6, 7])In [64]: arr[5:8] = 12In [65]: arrOut[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示,当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。
作为例子,先创建一个arr的切片:
In [66]: arr_slice = arr[5:8]In [67]: arr_sliceOut[67]: array([12, 12, 12])
现在,当我修稿arr_slice中的值,变动也会体现在原始数组arr中:
In [68]: arr_slice[1] = 12345In [69]: arrOut[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
切片[ : ]会给数组中的所有值赋值:
In [70]: arr_slice[:] = 64In [71]: arrOut[71]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
如果你刚开始接触NumPy,可能会对此感到惊讶(尤其是当你曾经用过其他热衷于复制数组数据的编程语言)。由于NumPy的设计目的是处理大数据,所以你可以想象一下,假如NumPy坚持要将数据复制来复制去的话会产生何等的性能和内存问题。
注意:如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如
arr[5:8].copy()
。
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])In [73]: arr2d[2]Out[73]: array([7, 8, 9])
因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
In [74]: arr2d[0][2]Out[74]: 3In [75]: arr2d[0, 2]Out[75]: 3
下图说明了二维数组的索引方式。轴0作为行,轴1作为列。
在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray
(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:
In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])In [77]: arr3dOut[77]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
arr3d[0]是一个2×3数组:
In [78]: arr3d[0] Out[78]: array([[1, 2, 3], [4, 5, 6]])
标量值和数组都可以被赋值给arr3d[0]:
In [79]: old_values = arr3d[0].copy() In [80]: arr3d[0] = 42 In [81]: arr3d Out[81]: array([[[42, 42, 42], [42, 42, 42]], [[ 7, 8, 9], [10, 11, 12]]]) In [82]: arr3d[0] = old_values In [83]: arr3d Out[83]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):
In [84]: arr3d[1, 0] Out[84]: array([7, 8, 9])
虽然是用两步进行索引的,表达式是相同的:
In [85]: x = arr3d[1] In [86]: x Out[86]: array([[ 7, 8, 9], [10, 11, 12]]) In [87]: x[0] Out[87]: array([7, 8, 9])
注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。
切片索引
ndarray的切片语法跟Python列表这样的一维对象差不多:
In [88]: arrOut[88]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])In [89]: arr[1:6]Out[89]: array([ 1, 2, 3, 4, 64])
对于之前的二维数组arr2d,其切片方式稍显不同:
In [90]: arr2dOut[90]: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])In [91]: arr2d[:2]Out[91]: array([[1, 2, 3], [4, 5, 6]])
可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。
你可以一次传入多个切片,就像传入多个索引那样:
In [92]: arr2d[:2, 1:]Out[92]: array([[2, 3], [5, 6]])
像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。
例如,我可以选取第二行的前两列:
In [93]: arr2d[1, :2]Out[93]: array([4, 5])
相似的,还可以选择第三列的前两行:
In [94]: arr2d[:2, 2]Out[94]: array([3, 6])
下图对此进行了说明。注意,“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:
In [95]: arr2d[:, :1]Out[95]: array([[1], [4], [7]])
自然,对切片表达式的赋值操作也会被扩散到整个选区:
In [96]: arr2d[:2, 1:] = 0In [97]: arr2dOut[97]: array([[1, 0, 0], [4, 0, 0], [7, 8, 9]])
布尔型索引
来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:
In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])In [99]: data = np.random.randn(7, 4)In [100]: namesOut[100]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],dtype='<U4')In [101]: dataOut[101]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 1.669 , -0.4386, -0.5397, 0.477 ], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
假设每个名字都对应data数组中的一行,而我们想要选出对应于名字”Bob”的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串”Bob”的比较运算将会产生一个布尔型数组:
In [102]: names == 'Bob'Out[102]: array([ True, False, False, True, False, False, False], dtype=bool)
这个布尔型数组可用于数组索引:
In [103]: data[names == 'Bob']Out[103]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.669 , -0.4386, -0.5397, 0.477 ]])
布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:
In [103]: data[names == 'Bob']Out[103]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.669 , -0.4386, -0.5397, 0.477 ]])
注意:如果布尔型数组的长度不对,布尔型选择就会出错,因此一定要小心。
下面的例子,我选取了names == 'Bob'
的行,并索引了列:
In [104]: data[names == 'Bob', 2:]Out[104]: array([[ 0.769 , 1.2464], [-0.5397, 0.477 ]])In [105]: data[names == 'Bob', 3]Out[105]: array([ 1.2464, 0.477 ])
要选择除”Bob”以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:
In [106]: names != 'Bob'Out[106]: array([False, True, True, False, True, True, True], dtype=bool)In [107]: data[~(names == 'Bob')]Out[107]:array([[ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
~操作符用来反转条件很好用:
In [108]: cond = names == 'Bob'In [109]: data[~cond]Out[109]: array([[ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:
In [110]: mask = (names == 'Bob') | (names == 'Will')In [111]: maskOut[111]: array([ True, False, True, True, True, False, False], dtype=bool)In [112]: data[mask]Out[112]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 1.669 , -0.4386, -0.5397, 0.477 ], [ 3.2489, -1.0212, -0.5771, 0.1241]])
通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。
注意:Python关键字and和or在布尔型数组中无效。要使用&与|。
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:
In [113]: data[data < 0] = 0In [114]: dataOut[114]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.0072, 0. , 0.275 , 0.2289], [ 1.3529, 0.8864, 0. , 0. ], [ 1.669 , 0. , 0. , 0.477 ], [ 3.2489, 0. , 0. , 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [ 0. , 0. , 0. , 0. ]])
通过一维布尔数组设置整行或列的值也很简单:
In [115]: data[names != 'Joe'] = 7In [116]: dataOut[116]: array([[ 7. , 7. , 7. , 7. ], [ 1.0072, 0. , 0.275 , 0.2289], [ 7. , 7. , 7. , 7. ], [ 7. , 7. , 7. , 7. ], [ 7. , 7. , 7. , 7. ], [ 0.3026, 0.5238, 0.0009, 1.3438], [ 0. , 0. , 0. , 0. ]])
后面会看到,这类二维数据的操作也可以用pandas方便的来做。
花式索引
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:
In [117]: arr = np.empty((8, 4))In [118]: for i in range(8):.....: arr[i] = iIn [119]: arrOut[119]: array([[ 0., 0., 0., 0.], [ 1., 1., 1., 1.], [ 2., 2., 2., 2.], [ 3., 3., 3., 3.], [ 4., 4., 4., 4.], [ 5., 5., 5., 5.], [ 6., 6., 6., 6.], [ 7., 7., 7., 7.]])
为了以特定顺序选取行子集,只需传入一个用于指定顺序的整数列表或ndarray
即可:
In [120]: arr[[4, 3, 0, 6]]Out[120]: array([[ 4., 4., 4., 4.], [ 3., 3., 3., 3.], [ 0., 0., 0., 0.], [ 6., 6., 6., 6.]])
这段代码确实达到我们的要求了!使用负数索引将会从末尾开始选取行:
In [121]: arr[[-3, -5, -7]]Out[121]: array([[ 5., 5., 5., 5.], [ 3., 3., 3., 3.], [ 1., 1., 1., 1.]])
一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:
In [122]: arr = np.arange(32).reshape((8, 4))In [123]: arrOut[123]: array([[ 0, 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]])In [124]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]Out[124]: array([ 4, 23, 29, 10])
附录A中会详细介绍reshape方法。
最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。
这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:
In [125]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]Out[125]: array([[ 4, 7, 5, 6], [20, 23, 21, 22], [28, 31, 29, 30], [ 8, 11, 9, 10]])
记住,花式索引跟切片不一样,它总是将数据复制到新数组中。
数组转置和轴对换
转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:
In [126]: arr = np.arange(15).reshape((3, 5))In [127]: arrOut[127]: array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]])In [128]: arr.TOut[128]: array([[ 0, 5, 10], [ 1, 6, 11], [ 2, 7, 12], [ 3, 8, 13], [ 4, 9, 14]])
在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:
In [129]: arr = np.random.randn(6, 3)In [130]: arrOut[130]: array([[-0.8608, 0.5601, -1.2659], [ 0.1198, -1.0635, 0.3329], [-2.3594, -0.1995, -1.542 ], [-0.9707, -1.307 , 0.2863], [ 0.378 , -0.7539, 0.3313], [ 1.3497, 0.0699, 0.2467]])In [131]: np.dot(arr.T, arr)Out[131]:array([[ 9.2291, 0.9394, 4.948 ], [ 0.9394, 3.7662, -1.3622], [ 4.948 , -1.3622, 4.3437]])
对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子):
In [132]: arr = np.arange(16).reshape((2, 2, 4))In [133]: arrOut[133]: array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7]], [[ 8, 9, 10, 11], [12, 13, 14, 15]]])In [134]: arr.transpose((1, 0, 2))Out[134]: array([[[ 0, 1, 2, 3], [ 8, 9, 10, 11]], [[ 4, 5, 6, 7], [12, 13, 14, 15]]])
这里,第一个轴被换成了第二个,第二个轴被换成了第一个,最后一个轴不变。
简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:
In [135]: arrOut[135]: array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7]], [[ 8, 9, 10, 11], [12, 13, 14, 15]]])In [136]: arr.swapaxes(1, 2)Out[136]: array([[[ 0, 4], [ 1, 5], [ 2, 6], [ 3, 7]], [[ 8, 12], [ 9, 13], [10, 14], [11, 15]]])
swapaxes也是返回源数据的视图(不会进行任何复制操作)。
4.2 通用函数:快速的元素级数组函数
通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看做简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。
许多ufunc都是简单的元素级变体,如sqrt
和exp
:
In [137]: arr = np.arange(10)
In [138]: arr
Out[138]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [139]: np.sqrt(arr)
Out[139]:
array([ 0. , 1. , 1.4142, 1.7321, 2. , 2.2361, 2.4495,
2.6458, 2.8284, 3. ])
In [140]: np.exp(arr)
Out[140]:
array([ 1. , 2.7183, 7.3891, 20.0855, 54.5982,
148.4132, 403.4288, 1096.6332, 2980.958 , 8103.0839])
这些都是一元(unary)ufunc。另外一些(如add
或maximum
)接受2个数组(因此也叫二元(binary)ufunc),并返回一个结果数组:
In [141]: x = np.random.randn(8)
In [142]: y = np.random.randn(8)
In [143]: x
Out[143]:
array([-0.0119, 1.0048, 1.3272, -0.9193, -1.5491, 0.0222, 0.7584,
-0.6605])
In [144]: y
Out[144]:
array([ 0.8626, -0.01 , 0.05 , 0.6702, 0.853 , -0.9559, -0.0235,
-2.3042])
In [145]: np.maximum(x, y)
Out[145]:
array([ 0.8626, 1.0048, 1.3272, 0.6702, 0.853 , 0.0222, 0.7584,
-0.6605])
这里,numpy.maximum
计算了x和y中元素级别最大的元素。
虽然并不常见,但有些ufunc的确可以返回多个数组。modf
就是一个例子,它是Python内置函数divmod
(返回小数)的矢量化版本,它会返回浮点数数组的小数和整数部分:
In [146]: arr = np.random.randn(7) * 5
In [147]: arr
Out[147]: array([-3.2623, -6.0915, -6.663 , 5.3731, 3.6182, 3.45 , 5.0077])
In [148]: remainder, whole_part = np.modf(arr)
In [149]: remainder
Out[149]: array([-0.2623, -0.0915, -0.663 , 0.3731,
0.6182, 0.45 , 0.0077])
In [150]: whole_part
Out[150]: array([-3., -6., -6., 5., 3., 3., 5.])
Ufuncs可以接受一个out可选参数,这样就能在数组原地进行操作:
In [151]: arr
Out[151]: array([-3.2623, -6.0915, -6.663 , 5.3731, 3.6182, 3.45 , 5.0077])
In [152]: np.sqrt(arr)
Out[152]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])#影响对象arr
In [153]: np.sqrt(arr, arr)
Out[153]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])#不影响对象arr
In [154]: arr
Out[154]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])
下表分别列出了一些一元和二元ufunc。
4.3 利用数组进行数据处理
NumPy数组使你可以将许多种数据处理任务表述为简洁的数组表达式(否则需要编写循环)。用数组表达式代替循环的做法,通常被称为矢量化。一般来说,矢量化数组运算要比等价的纯Python方式快上一两个数量级(甚至更多),尤其是各种数值计算。在后面内容中(见附录A)我将介绍广播,这是一种针对矢量化计算的强大手段。
作为简单的例子,假设我们想要在一组值(网格型)上计算函数sqrt(x^2+y^2)。np.meshgrid
函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的(x,y)对):
In [155]: points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
In [156]: xs, ys = np.meshgrid(points, points)
In [157]: ys
Out[157]:
array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
[-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
[-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
...,
[ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
[ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
[ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])
现在,对该函数的求值运算就好办了,把这两个数组当做两个浮点数那样编写表达式即可:
In [158]: z = np.sqrt(xs ** 2 + ys ** 2)
In [159]: z
Out[159]:
array([[ 7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ],
[ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
[ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
...,
[ 7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
[ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
[ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])
作为第9章的先导,我用matplotlib
创建了这个二维数组的可视化:
In [160]: import matplotlib.pyplot as plt
In [161]: plt.imshow(z, cmap=plt.cm.gray); plt.colorbar()
Out[161]: <matplotlib.colorbar.Colorbar at 0x7f715e3fa630>
In [162]: plt.title("Image plot of $\sqrt{x^2 + y^2}$ for a grid of values")
Out[162]: <matplotlib.text.Text at 0x7f715d2de748>
下图是用matplotlib
的imshow
函数创建的。
将条件逻辑表述为数组运算
numpy.where
函数是三元表达式x if condition else y
的矢量化版本。假设我们有一个布尔数组和两个值数组:
In [165]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [166]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [167]: cond = np.array([True, False, True, True, False])
假设我们想要根据cond中的值选取xarr和yarr的值:当cond中的值为True时,选取xarr的值,否则从yarr中选取。列表推导式的写法应该如下所示:
In [168]: result = [(x if c else y) .....: for x, y, c in zip(xarr, yarr, cond)]In [169]: resultOut[169]: [1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]
这有几个问题。第一,它对大数组的处理速度不是很快(因为所有工作都是由纯Python完成的)。第二,无法用于多维数组。若使用np.where
,则可以将该功能写得非常简洁:
In [170]: result = np.where(cond, xarr, yarr)In [171]: resultOut[171]: array([ 1.1, 2.2, 1.3, 1.4, 2.5])
np.where
的第二个和第三个参数不必是数组,它们都可以是标量值。在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组。假设有一个由随机数据组成的矩阵,你希望将所有正值替换为2,将所有负值替换为-2。若利用np.where,则会非常简单:
In [172]: arr = np.random.randn(4, 4)In [173]: arrOut[173]: array([[-0.5031, -0.6223, -0.9212, -0.7262], [ 0.2229, 0.0513, -1.1577, 0.8167], [ 0.4336, 1.0107, 1.8249, -0.9975], [ 0.8506, -0.1316, 0.9124, 0.1882]])In [174]: arr > 0Out[174]: array([[False, False, False, False], [ True, True, False, True], [ True, True, True, False], [ True, False, True, True]], dtype=bool)In [175]: np.where(arr > 0, 2, -2)Out[175]: array([[-2, -2, -2, -2], [ 2, 2, -2, 2], [ 2, 2, 2, -2], [ 2, -2, 2, 2]])
使用np.where
,可以将标量和数组结合起来。例如,我可用常数2替换arr中所有正的值:
In [176]: np.where(arr > 0, 2, arr) # set only positive values to 2Out[176]: array([[-0.5031, -0.6223, -0.9212, -0.7262], [ 2. , 2. , -1.1577, 2. ], [ 2. , 2. , 2. , -0.9975], [ 2. , -0.1316, 2. , 2. ]])
传递给where的数组大小可以不相等,甚至可以是标量值。
数学和统计方法
可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。sum、mean
以及标准差std
等聚合计算(aggregation,通常叫做约简(reduction))既可以当做数组的实例方法调用,也可以当做顶级NumPy函数使用。
这里,我生成了一些正态分布随机数据,然后做了聚类统计:
In [177]: arr = np.random.randn(5, 4)In [178]: arrOut[178]: array([[ 2.1695, -0.1149, 2.0037, 0.0296], [ 0.7953, 0.1181, -0.7485, 0.585 ], [ 0.1527, -1.5657, -0.5625, -0.0327], [-0.929 , -0.4826, -0.0363, 1.0954], [ 0.9809, -0.5895, 1.5817, -0.5287]])In [179]: arr.mean()Out[179]: 0.19607051119998253In [180]: np.mean(arr)Out[180]: 0.19607051119998253In [181]: arr.sum()Out[181]: 3.9214102239996507
mean和sum这类的函数可以接受一个axis选项参数,用于计算该轴向上的统计值,最终结果是一个少一维的数组:
In [182]: arr.mean(axis=1)Out[182]: array([ 1.022 , 0.1875, -0.502 , -0.0881, 0.3611])In [183]: arr.sum(axis=0)Out[183]: array([ 3.1693, -2.6345, 2.2381, 1.1486])
这里,arr.mean(1)
是“计算行的平均值”,arr.sum(0)
是“计算每列的和”。
其他如cumsum和cumprod之类的方法则不聚合,而是产生一个由中间结果组成的数组:
In [184]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])In [185]: arr.cumsum()Out[185]: array([ 0, 1, 3, 6, 10, 15, 21, 28])
在多维数组中,累加函数(如cumsum)返回的是同样大小的数组,但是会根据每个低维的切片沿着标记轴计算部分聚类:
In [186]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])In [187]: arrOut[187]: array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])In [188]: arr.cumsum(axis=0)Out[188]: array([[ 0, 1, 2], [ 3, 5, 7], [ 9, 12, 15]])In [189]: arr.cumprod(axis=1)Out[189]: array([[ 0, 0, 0], [ 3, 12, 60], [ 6, 42, 336]])
下表列出了全部的基本数组统计方法。后续章节中有很多例子都会用到这些方法。
用于布尔型数组的方法
在上面这些方法中,布尔值会被强制转换为1(True)和0(False)。因此,sum经常被用来对布尔型数组中的True值计数:
In [190]: arr = np.random.randn(100)In [191]: (arr > 0).sum() # Number of positive valuesOut[191]: 42
另外还有两个方法any
和all
,它们对布尔型数组非常有用。any
用于测试数组中是否存在一个或多个True
,而all
则检查数组中所有值是否都是True
:
In [192]: bools = np.array([False, False, True, False])In [193]: bools.any()Out[193]: TrueIn [194]: bools.all()Out[194]: False
这两个方法也能用于非布尔型数组,所有非0元素将会被当做True。
排序
跟Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序:
In [195]: arr = np.random.randn(6)In [196]: arrOut[196]: array([ 0.6095, -0.4938, 1.24 , -0.1357, 1.43 , -0.8469])In [197]: arr.sort()In [198]: arrOut[198]: array([-0.8469, -0.4938, -0.1357, 0.6095, 1.24 , 1.43 ])
多维数组可以在任何一个轴向上进行排序,只需将轴编号传给sort即可:
In [199]: arr = np.random.randn(5, 3)In [200]: arrOut[200]: array([[ 0.6033, 1.2636, -0.2555], [-0.4457, 0.4684, -0.9616], [-1.8245, 0.6254, 1.0229], [ 1.1074, 0.0909, -0.3501], [ 0.218 , -0.8948, -1.7415]])In [201]: arr.sort(1)In [202]: arrOut[202]: array([[-0.2555, 0.6033, 1.2636], [-0.9616, -0.4457, 0.4684], [-1.8245, 0.6254, 1.0229], [-0.3501, 0.0909, 1.1074], [-1.7415, -0.8948, 0.218 ]])
顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。计算数组分位数最简单的办法是对其进行排序,然后选取特定位置的值:
In [203]: large_arr = np.random.randn(1000)In [204]: large_arr.sort()In [205]: large_arr[int(0.05 * len(large_arr))] # 5% quantileOut[205]: -1.5311513550102103
更多关于NumPy排序方法以及诸如间接排序之类的高级技术,请参阅附录A。在pandas中还可以找到一些其他跟排序有关的数据操作(比如根据一列或多列对表格型数据进行排序)。
唯一化以及其它的集合逻辑
NumPy提供了一些针对一维ndarray的基本集合运算。最常用的可能要数np.unique
了,它用于找出数组中的唯一值并返回已排序的结果:
In [206]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])In [207]: np.unique(names)Out[207]: array(['Bob', 'Joe', 'Will'], dtype='<U4')In [208]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])In [209]: np.unique(ints)Out[209]: array([1, 2, 3, 4])
拿跟np.unique等价的纯Python代码来对比一下:
In [210]: sorted(set(names))Out[210]: ['Bob', 'Joe', 'Will']
另一个函数np.in1d
用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:
In [211]: values = np.array([6, 0, 0, 3, 2, 5, 6])In [212]: np.in1d(values, [2, 3, 6])Out[212]: array([ True, False, False, True, True, False, True], dtype=bool)
NumPy中的集合函数请参见下表。
4.4 用于数组的文件输入输出
NumPy
能够读写磁盘上的文本数据或二进制数据。这一小节只讨论NumPy
的内置二进制格式,因为更多的用户会使用pandas或其它工具加载文本或表格数据(见第6章)。
np.save
和np.load
是读写磁盘数组数据的两个主要函数。默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为.npy的文件中的:
In [213]: arr = np.arange(10)In [214]: np.save('some_array', arr)
如果文件路径末尾没有扩展名.npy,则该扩展名会被自动加上。然后就可以通过np.load读取磁盘上的数组:
In [215]: np.load('some_array.npy')Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
通过np.savez可以将多个数组保存到一个未压缩文件中,将数组以关键字参数的形式传入即可:
In [216]: np.savez('array_archive.npz', a=arr, b=arr)
加载.npz文件时,你会得到一个类似字典的对象,该对象会对各个数组进行延迟加载:
In [217]: arch = np.load('array_archive.npz')In [218]: arch['b']Out[218]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
如果要将数据压缩,可以使用numpy.savez_compressed:
In [219]: np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)
4.5 线性代数
线性代数(如矩阵乘法、矩阵分解、行列式以及其他方阵数学等)是任何数组库的重要组成部分。不像某些语言(如MATLAB),通过*对两个二维数组相乘得到的是一个元素级的积,而不是一个矩阵点积。因此,NumPy提供了一个用于矩阵乘法的dot函数(既是一个数组方法也是numpy命名空间中的一个函数):
In [223]: x = np.array([[1., 2., 3.], [4., 5., 6.]])In [224]: y = np.array([[6., 23.], [-1, 7], [8, 9]])In [225]: xOut[225]: array([[ 1., 2., 3.], [ 4., 5., 6.]])In [226]: yOut[226]: array([[ 6., 23.], [ -1., 7.], [ 8., 9.]])In [227]: x.dot(y)Out[227]: array([[ 28., 64.], [ 67., 181.]])
x.dot(y)等价于np.dot(x, y):
In [228]: np.dot(x, y)Out[228]: array([[ 28., 64.], [ 67., 181.]])
一个二维数组跟一个大小合适的一维数组的矩阵点积运算之后将会得到一个一维数组:
In [229]: np.dot(x, np.ones(3))Out[229]: array([ 6., 15.])
@
符(类似Python 3.5)也可以用作中缀运算符,进行矩阵乘法:
In [230]: x @ np.ones(3)Out[230]: array([ 6., 15.])
numpy.linalg中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西。它们跟MATLAB和R等语言所使用的是相同的行业标准线性代数库,如BLAS、LAPACK、Intel MKL(Math Kernel Library,可能有,取决于你的NumPy版本)等:
In [231]: from numpy.linalg import inv, qrIn [232]: X = np.random.randn(5, 5)In [233]: mat = X.T.dot(X)In [234]: inv(mat)Out[234]: array([[ 933.1189, 871.8258, -1417.6902, -1460.4005, 1782.1391], [ 871.8258, 815.3929, -1325.9965, -1365.9242, 1666.9347], [-1417.6902, -1325.9965, 2158.4424, 2222.0191, -2711.6822], [-1460.4005, -1365.9242, 2222.0191, 2289.0575, -2793.422 ], [ 1782.1391, 1666.9347, -2711.6822, -2793.422 , 3409.5128]])In [235]: mat.dot(inv(mat))Out[235]: array([[ 1., 0., -0., -0., -0.], [-0., 1., 0., 0., 0.], [ 0., 0., 1., 0., 0.], [-0., 0., 0., 1., -0.], [-0., 0., 0., 0., 1.]])In [236]: q, r = qr(mat)In [237]: rOut[237]: array([[-1.6914, 4.38 , 0.1757, 0.4075, -0.7838], [ 0. , -2.6436, 0.1939, -3.072 , -1.0702], [ 0. , 0. , -0.8138, 1.5414, 0.6155], [ 0. , 0. , 0. , -2.6445, -2.1669], [ 0. , 0. , 0. , 0. , 0.0002]])
表达式X.T.dot(X)计算X和它的转置X.T的点积。
下表中列出了一些最常用的线性代数函数。
4.6 伪随机数生成
numpy.random
模块对Python内置的random进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数。例如,你可以用normal来得到一个标准正态分布的4×4样本数组:
In [238]: samples = np.random.normal(size=(4, 4))In [239]: samplesOut[239]: array([[ 0.5732, 0.1933, 0.4429, 1.2796], [ 0.575 , 0.4339, -0.7658, -1.237 ], [-0.5367, 1.8545, -0.92 , -0.1082], [ 0.1525, 0.9435, -1.0953, -0.144 ]])
而Python内置的random模块则只能一次生成一个样本值。从下面的测试结果中可以看出,如果需要产生大量样本值,numpy.random
快了不止一个数量级:
In [240]: from random import normalvariateIn [241]: N = 1000000In [242]: %timeit samples = [normalvariate(0, 1) for _ in range(N)]1.77 s +- 126 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)In [243]: %timeit np.random.normal(size=N)61.7 ms +- 1.32 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
我们说这些都是伪随机数,是因为它们都是通过算法基于随机数生成器种子,在确定性的条件下生成的。你可以用NumPy的np.random.seed更改随机数生成种子:
In [244]: np.random.seed(1234)
numpy.random的数据生成函数使用了全局的随机种子。要避免全局状态,你可以使用numpy.random.RandomState,创建一个与其它隔离的随机数生成器:
In [245]: rng = np.random.RandomState(1234)In [246]: rng.randn(10)Out[246]: array([ 0.4714, -1.191 , 1.4327, -0.3127, -0.7206, 0.8872, 0.8596, -0.6365, 0.0157, -2.2427])
下表列出了numpy.random
中的部分函数。在下一节中,我将给出一些利用这些函数一次性生成大量样本值的范例。
4.7 示例:随机漫步
我们通过模拟随机漫步来说明如何运用数组运算。先来看一个简单的随机漫步的例子:从0开始,步长1和-1出现的概率相等。
下面是一个通过内置的random
模块以纯Python的方式实现1000步的随机漫步:
In [247]: import random .....: position = 0 .....: walk = [position] .....: steps = 1000 .....: for i in range(steps): .....: step = 1 if random.randint(0, 1) else -1 .....: position += step .....: walk.append(position) .....:
图4-4是根据前100个随机漫步值生成的折线图:
In [249]: plt.plot(walk[:100])
不难看出,这其实就是随机漫步中各步的累计和,可以用一个数组运算来实现。因此,我用np.random
模块一次性随机产生1000个“掷硬币”结果(即两个数中任选一个),将其分别设置为1或-1,然后计算累计和:
In [251]: nsteps = 1000In [252]: draws = np.random.randint(0, 2, size=nsteps)In [253]: steps = np.where(draws > 0, 1, -1)In [254]: walk = steps.cumsum()
有了这些数据之后,我们就可以沿着漫步路径做一些统计工作了,比如求取最大值和最小值:
In [255]: walk.min()Out[255]: -3In [256]: walk.max()Out[256]: 31
现在来看一个复杂点的统计任务——首次穿越时间,即随机漫步过程中第一次到达某个特定值的时间。假设我们想要知道本次随机漫步需要多久才能距离初始0点至少10步远(任一方向均可)。np.abs(walk)>=10可以得到一个布尔型数组,它表示的是距离是否达到或超过10,而我们想要知道的是第一个10或-10的索引。可以用argmax来解决这个问题,它返回的是该布尔型数组第一个最大值的索引(True就是最大值):
In [257]: (np.abs(walk) >= 10).argmax()Out[257]: 37
注意,这里使用argmax并不是很高效,因为它无论如何都会对数组进行完全扫描。在本例中,只要发现了一个True,那我们就知道它是个最大值了。
一次模拟多个随机漫步
如果你希望模拟多个随机漫步过程(比如5000个),只需对上面的代码做一点点修改即可生成所有的随机漫步过程。只要给numpy.random
的函数传入一个二元元组就可以产生一个二维数组,然后我们就可以一次性计算5000个随机漫步过程(一行一个)的累计和了:
In [258]: nwalks = 5000In [259]: nsteps = 1000In [260]: draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1In [261]: steps = np.where(draws > 0, 1, -1)In [262]: walks = steps.cumsum(1)In [263]: walksOut[263]: array([[ 1, 0, 1, ..., 8, 7, 8], [ 1, 0, -1, ..., 34, 33, 32], [ 1, 0, -1, ..., 4, 5, 4], ..., [ 1, 2, 1, ..., 24, 25, 26], [ 1, 2, 3, ..., 14, 13, 14], [ -1, -2, -3, ..., -24, -23, -22]])
现在,我们来计算所有随机漫步过程的最大值和最小值:
In [264]: walks.max()Out[264]: 138In [265]: walks.min()Out[265]: -133
得到这些数据之后,我们来计算30或-30的最小穿越时间。这里稍微复杂些,因为不是5000个过程都到达了30。我们可以用any方法来对此进行检查:
In [266]: hits30 = (np.abs(walks) >= 30).any(1)In [267]: hits30Out[267]: array([False, True, False, ..., False, True, False], dtype=bool)In [268]: hits30.sum() # Number that hit 30 or -30Out[268]: 3410
然后我们利用这个布尔型数组选出那些穿越了30(绝对值)的随机漫步(行),并调用argmax在轴1上获取穿越时间:
In [269]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)In [270]: crossing_times.mean()Out[270]: 498.88973607038122
请尝试用其他分布方式得到漫步数据。只需使用不同的随机数生成函数即可,如normal用于生成指定均值和标准差的正态分布数据:
In [271]: steps = np.random.normal(loc=0, scale=0.25, .....: size=(nwalks, nsteps))
第 05 章 pandas 入门
pandas是本书后续内容的首选库。它含有使数据清洗和分析工作变得更快更简单的数据结构和操作工具。pandas经常和其它工具一同使用,如数值计算工具NumPy和SciPy,分析库statsmodels和scikit-learn,和数据可视化库matplotlib。pandas是基于NumPy数组构建的,特别是基于数组的函数和不使用for循环的数据处理。
虽然pandas采用了大量的NumPy编码风格,但二者最大的不同是pandas是专门为处理表格和混杂数据设计的。而NumPy更适合处理统一的数值数组数据。
自从2010年pandas开源以来,pandas逐渐成长为一个非常大的库,应用于许多真实案例。开发者社区已经有了800个独立的贡献者,他们在解决日常数据问题的同时为这个项目提供贡献。
在本书后续部分中,我将使用下面这样的pandas引入约定:
In [1]: import pandas as pd
因此,只要你在代码中看到pd.,就得想到这是pandas。因为Series和DataFrame用的次数非常多,所以将其引入本地命名空间中会更方便:
In [2]: from pandas import Series, DataFrame
5.1 pandas的数据结构介绍
要使用pandas
,你首先就得熟悉它的两个主要数据结构:Series
和DataFrame
。虽然它们并不能解决所有问题,但它们为大多数应用提供了一种可靠的、易于使用的基础。
Series
Series是一种类似于一维数组的对象,它由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据即可产生最简单的Series:
In [11]: obj = pd.Series([4, 7, -5, 3])
In [12]: obj
Out[12]:
0 4
1 7
2 -5
3 3
dtype: int64
Series
的字符串表现形式为:索引在左边,值在右边。由于我们没有为数据指定索引,于是会自动创建一个0到N-1(N为数据的长度)的整数型索引。你可以通过Series
的values
和index
属性获取其数组表示形式和索引对象:
In [13]: obj.values
Out[13]: array([ 4, 7, -5, 3])
In [14]: obj.index # like range(4)
Out[14]: RangeIndex(start=0, stop=4, step=1)
通常,我们希望所创建的Series带有一个可以对各个数据点进行标记的索引:
In [15]: obj2 = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
In [16]: obj2
Out[16]:
d 4
b 7
a -5
c 3
dtype: int64
In [17]: obj2.index
Out[17]: Index(['d', 'b', 'a', 'c'], dtype='object')
与普通NumPy
数组相比,你可以通过索引的方式选取Series中的单个或一组值:
In [18]: obj2['a']
Out[18]: -5
In [19]: obj2['d'] = 6
In [20]: obj2[['c', 'a', 'd']]
Out[20]:
c 3
a -5
d 6
dtype: int64
[‘c’, ‘a’, ‘d’]是索引列表,即使它包含的是字符串而不是整数。
使用NumPy
函数或类似NumPy
的运算(如根据布尔型数组进行过滤、标量乘法、应用数学函数等)都会保留索引值的链接:
In [21]: obj2[obj2 > 0]
Out[21]:
d 6
b 7
c 3
dtype: int64
In [22]: obj2 * 2
Out[22]:
d 12
b 14
a -10
c 6
dtype: int64
In [23]: np.exp(obj2)
Out[23]:
d 403.428793
b 1096.633158
a 0.006738
c 20.085537
dtype: float64
还可以将Series
看成是一个定长的有序字典,因为它是索引值到数据值的一个映射。它可以用在许多原本需要字典参数的函数中:
In [24]: 'b' in obj2
Out[24]: True
In [25]: 'e' in obj2
Out[25]: False
如果数据被存放在一个Python字典中,也可以直接通过这个字典来创建Series:
In [26]: sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}In [27]: obj3 = pd.Series(sdata)In [28]: obj3Out[28]: Ohio 35000Oregon 16000Texas 71000Utah 5000dtype: int64
如果只传入一个字典,则结果Series中的索引就是原字典的键(有序排列)。你可以传入排好序的字典的键以改变顺序:
In [29]: states = ['California', 'Ohio', 'Oregon', 'Texas']In [30]: obj4 = pd.Series(sdata, index=states)In [31]: obj4Out[31]: California NaNOhio 35000.0Oregon 16000.0Texas 71000.0dtype: float64
在这个例子中,sdata中跟states索引相匹配的那3个值会被找出来并放到相应的位置上,但由于”California”所对应的sdata值找不到,所以其结果就为NaN(即“非数字”(not a number),在pandas中,它用于表示缺失或NA值)。因为‘Utah’不在states中,它被从结果中除去。
我将使用缺失(missing)或NA表示缺失数据。pandas的isnull和notnull函数可用于检测缺失数据:
In [32]: pd.isnull(obj4)Out[32]: California TrueOhio FalseOregon FalseTexas Falsedtype: boolIn [33]: pd.notnull(obj4)Out[33]: California FalseOhio TrueOregon TrueTexas Truedtype: bool
Series也有类似的实例方法:
In [34]: obj4.isnull()Out[34]: California TrueOhio FalseOregon FalseTexas Falsedtype: bool
我将在第7章详细讲解如何处理缺失数据。
对于许多应用而言,Series
最重要的一个功能是,它会根据运算的索引标签自动对齐数据:
In [35]: obj3Out[35]: Ohio 35000Oregon 16000Texas 71000Utah 5000dtype: int64In [36]: obj4Out[36]: California NaNOhio 35000.0Oregon 16000.0Texas 71000.0dtype: float64In [37]: obj3 + obj4Out[37]: California NaNOhio 70000.0Oregon 32000.0Texas 142000.0Utah NaNdtype: float64
数据对齐功能将在后面详细讲解。如果你使用过数据库,你可以认为是类似join的操作。
Series对象本身及其索引都有一个name
属性,该属性跟pandas
其他的关键功能关系非常密切:
In [38]: obj4.name = 'population'In [39]: obj4.index.name = 'state'In [40]: obj4Out[40]: stateCalifornia NaNOhio 35000.0Oregon 16000.0Texas 71000.0Name: population, dtype: float64
Series的索引可以通过赋值的方式就地修改:
In [41]: objOut[41]: 0 41 72 -53 3dtype: int64In [42]: obj.index = ['Bob', 'Steve', 'Jeff', 'Ryan']In [43]: objOut[43]: Bob 4Steve 7Jeff -5Ryan 3dtype: int64
DataFrame
DataFrame是一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame既有行索引也有列索引,它可以被看做由Series组成的字典(共用同一个索引)。DataFrame中的数据是以一个或多个二维块存放的(而不是列表、字典或别的一维数据结构)。有关DataFrame内部的技术细节远远超出了本书所讨论的范围。
- 笔记:虽然DataFrame是以二维结构保存数据的,但你仍然可以轻松地将其表示为更高维度的数据(层次化索引的表格型结构,这是pandas中许多高级数据处理功能的关键要素,我们会在第8章讨论这个问题)。
建DataFrame的办法有很多,最常用的一种是直接传入一个由等长列表或NumPy数组组成的字典:
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'], 'year': [2000, 2001, 2002, 2001, 2002, 2003], 'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}frame = pd.DataFrame(data)
结果DataFrame会自动加上索引(跟Series
一样),且全部列会被有序排列:
In [45]: frameOut[45]: pop state year0 1.5 Ohio 20001 1.7 Ohio 20012 3.6 Ohio 20023 2.4 Nevada 20014 2.9 Nevada 20025 3.2 Nevada 2003
如果你使用的是Jupyter notebook,pandas DataFrame对象会以对浏览器友好的HTML表格的方式呈现。
对于特别大的DataFrame
,head
方法会选取前五行:
In [46]: frame.head()Out[46]: pop state year0 1.5 Ohio 20001 1.7 Ohio 20012 3.6 Ohio 20023 2.4 Nevada 20014 2.9 Nevada 2002
如果指定了列序列,则DataFrame
的列就会按照指定顺序进行排列:
In [47]: pd.DataFrame(data, columns=['year', 'state', 'pop'])Out[47]: year state pop0 2000 Ohio 1.51 2001 Ohio 1.72 2002 Ohio 3.63 2001 Nevada 2.44 2002 Nevada 2.95 2003 Nevada 3.2
如果传入的列在数据中找不到,就会在结果中产生缺失值:
In [48]: frame2 = pd.DataFrame(data, columns=['year', 'state', 'pop', 'debt'], ....: index=['one', 'two', 'three', 'four', ....: 'five', 'six'])In [49]: frame2Out[49]: year state pop debtone 2000 Ohio 1.5 NaNtwo 2001 Ohio 1.7 NaNthree 2002 Ohio 3.6 NaNfour 2001 Nevada 2.4 NaNfive 2002 Nevada 2.9 NaNsix 2003 Nevada 3.2 NaNIn [50]: frame2.columnsOut[50]: Index(['year', 'state', 'pop', 'debt'], dtype='object')
通过类似字典标记的方式或属性的方式,可以将DataFrame
的列获取为一个Series
:
In [51]: frame2['state']Out[51]: one Ohiotwo Ohiothree Ohiofour Nevadafive Nevadasix NevadaName: state, dtype: objectIn [52]: frame2.yearOut[52]: one 2000two 2001three 2002four 2001five 2002six 2003Name: year, dtype: int64
笔记:IPython提供了类似属性的访问(即frame2.year)和tab补全。
frame2[column]适用于任何列的名,但是frame2.column只有在列名是一个合理的Python变量名时才适用。
注意,返回的Series
拥有原DataFrame
相同的索引,且其name
属性也已经被相应地设置好了。
行也可以通过位置或名称的方式进行获取,比如用loc
属性(稍后将对此进行详细讲解):
In [53]: frame2.loc['three']Out[53]: year 2002state Ohiopop 3.6debt NaNName: three, dtype: object
列可以通过赋值的方式进行修改。例如,我们可以给那个空的”debt”列赋上一个标量值或一组值:
In [54]: frame2['debt'] = 16.5In [55]: frame2Out[55]: year state pop debtone 2000 Ohio 1.5 16.5two 2001 Ohio 1.7 16.5three 2002 Ohio 3.6 16.5four 2001 Nevada 2.4 16.5five 2002 Nevada 2.9 16.5six 2003 Nevada 3.2 16.5In [56]: frame2['debt'] = np.arange(6.)In [57]: frame2Out[57]: year state pop debtone 2000 Ohio 1.5 0.0two 2001 Ohio 1.7 1.0three 2002 Ohio 3.6 2.0four 2001 Nevada 2.4 3.0five 2002 Nevada 2.9 4.0six 2003 Nevada 3.2 5.0
将列表或数组赋值给某个列时,其长度必须跟DataFrame的长度相匹配。如果赋值的是一个Series,就会精确匹配DataFrame的索引,所有的空位都将被填上缺失值:
In [58]: val = pd.Series([-1.2, -1.5, -1.7], index=['two', 'four', 'five'])In [59]: frame2['debt'] = valIn [60]: frame2Out[60]: year state pop debtone 2000 Ohio 1.5 NaNtwo 2001 Ohio 1.7 -1.2three 2002 Ohio 3.6 NaNfour 2001 Nevada 2.4 -1.5five 2002 Nevada 2.9 -1.7six 2003 Nevada 3.2 NaN
为不存在的列赋值会创建出一个新列。关键字del用于删除列。
作为del的例子,我先添加一个新的布尔值的列,state是否为’Ohio’:
In [61]: frame2['eastern'] = frame2.state == 'Ohio'In [62]: frame2Out[62]: year state pop debt easternone 2000 Ohio 1.5 NaN Truetwo 2001 Ohio 1.7 -1.2 Truethree 2002 Ohio 3.6 NaN Truefour 2001 Nevada 2.4 -1.5 Falsefive 2002 Nevada 2.9 -1.7 Falsesix 2003 Nevada 3.2 NaN False#注意:不能用frame2.eastern创建新的列。
del方法可以用来删除这列:
In [63]: del frame2['eastern']In [64]: frame2.columnsOut[64]: Index(['year', 'state', 'pop', 'debt'], dtype='object')#注意:通过索引方式返回的列只是相应数据的视图而已,并不是副本。因此,对返回的Series所做的任何就地修改全都会反映到源DataFrame上。通过Series的copy方法即可指定复制列。
另一种常见的数据形式是嵌套字典:
In [65]: pop = {'Nevada': {2001: 2.4, 2002: 2.9},....: 'Ohio': {2000: 1.5, 2001: 1.7, 2002: 3.6}}
如果嵌套字典传给DataFrame
,pandas
就会被解释为:外层字典的键作为列,内层键则作为行索引:
In [66]: frame3 = pd.DataFrame(pop)In [67]: frame3Out[67]: Nevada Ohio2000 NaN 1.52001 2.4 1.72002 2.9 3.6
你也可以使用类似NumPy
数组的方法,对DataFrame
进行转置(交换行和列):
In [68]: frame3.TOut[68]: 2000 2001 2002Nevada NaN 2.4 2.9Ohio 1.5 1.7 3.6
内层字典的键会被合并、排序以形成最终的索引。如果明确指定了索引,则不会这样:
In [69]: pd.DataFrame(pop, index=[2001, 2002, 2003])Out[69]: Nevada Ohio2001 2.4 1.72002 2.9 3.62003 NaN NaN
由Series组成的字典差不多也是一样的用法:
In [70]: pdata = {'Ohio': frame3['Ohio'][:-1],....: 'Nevada': frame3['Nevada'][:2]}In [71]: pd.DataFrame(pdata)Out[71]: Nevada Ohio2000 NaN 1.52001 2.4 1.7
下表列出了DataFrame
构造函数所能接受的各种数据。
如果设置了DataFrame的index和columns的name属性,则这些信息也会被显示出来:
In [72]: frame3.index.name = 'year'; frame3.columns.name = 'state'In [73]: frame3Out[73]: state Nevada Ohioyear2000 NaN 1.52001 2.4 1.72002 2.9 3.6
跟Series
一样,values
属性也会以二维ndarray
的形式返回DataFrame
中的数据:
In [74]: frame3.valuesOut[74]: array([[ nan, 1.5], [ 2.4, 1.7], [ 2.9, 3.6]])
如果DataFrame各列的数据类型不同,则值数组的dtype就会选用能兼容所有列的数据类型:
In [75]: frame2.valuesOut[75]:array([[2000, 'Ohio', 1.5, nan], [2001, 'Ohio', 1.7, -1.2], [2002, 'Ohio', 3.6, nan], [2001, 'Nevada', 2.4, -1.5], [2002, 'Nevada', 2.9, -1.7], [2003, 'Nevada', 3.2, nan]], dtype=object)
索引对象
pandas的索引对象负责管理轴标签和其他元数据(比如轴名称等)。构建Series或DataFrame时,所用到的任何数组或其他序列的标签都会被转换成一个Index:
In [76]: obj = pd.Series(range(3), index=['a', 'b', 'c'])In [77]: index = obj.indexIn [78]: indexOut[78]: Index(['a', 'b', 'c'], dtype='object')In [79]: index[1:]Out[79]: Index(['b', 'c'], dtype='object')
Index对象是不可变的,因此用户不能对其进行修改:
index[1] = 'd' # TypeError
不可变可以使Index对象在多个数据结构之间安全共享:
In [80]: labels = pd.Index(np.arange(3))In [81]: labelsOut[81]: Int64Index([0, 1, 2], dtype='int64')In [82]: obj2 = pd.Series([1.5, -2.5, 0], index=labels)In [83]: obj2Out[83]: 0 1.51 -2.52 0.0dtype: float64In [84]: obj2.index is labelsOut[84]: True#注意:虽然用户不需要经常使用Index的功能,但是因为一些操作会生成包含被索引化的数据,理解它们的工作原理是很重要的。
除了类似于数组,Index的功能也类似一个固定大小的集合:
In [85]: frame3Out[85]: state Nevada Ohioyear 2000 NaN 1.52001 2.4 1.72002 2.9 3.6In [86]: frame3.columnsOut[86]: Index(['Nevada', 'Ohio'], dtype='object', name='state')In [87]: 'Ohio' in frame3.columnsOut[87]: TrueIn [88]: 2003 in frame3.indexOut[88]: False
与python的集合不同,pandas的Index可以包含重复的标签:
In [89]: dup_labels = pd.Index(['foo', 'foo', 'bar', 'bar'])In [90]: dup_labelsOut[90]: Index(['foo', 'foo', 'bar', 'bar'], dtype='object')
选择重复的标签,会显示所有的结果。
每个索引都有一些方法和属性,它们可用于设置逻辑并回答有关该索引所包含的数据的常见问题。下表列出了这些函数。
5.2 基本功能
本节中,我将介绍操作Series和DataFrame中的数据的基本手段。后续章节将更加深入地挖掘pandas在数据分析和处理方面的功能。本书不是pandas库的详尽文档,主要关注的是最重要的功能,那些不大常用的内容(也就是那些更深奥的内容)就交给你自己去摸索吧。
重新索引
pandas对象的一个重要方法是reindex,其作用是创建一个新对象,它的数据符合新的索引。看下面的例子:
In [91]: obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=['d', 'b', 'a', 'c'])In [92]: objOut[92]: d 4.5b 7.2a -5.3c 3.6dtype: float64
用该Series的reindex
将会根据新索引进行重排。如果某个索引值当前不存在,就引入缺失值:
In [93]: obj2 = obj.reindex(['a', 'b', 'c', 'd', 'e'])In [94]: obj2Out[94]: a -5.3b 7.2c 3.6d 4.5e NaNdtype: float64
对于时间序列这样的有序数据,重新索引时可能需要做一些插值处理。method
选项即可达到此目的,例如,使用ffill可以实现前向值填充:
In [95]: obj3 = pd.Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])In [96]: obj3Out[96]: 0 blue2 purple4 yellowdtype: objectIn [97]: obj3.reindex(range(6), method='ffill')Out[97]: 0 blue1 blue2 purple3 purple4 yellow5 yellowdtype: object
借助DataFrame,reindex可以修改(行)索引和列。只传递一个序列时,会重新索引结果的行:
In [98]: frame = pd.DataFrame(np.arange(9).reshape((3, 3)), ....: index=['a', 'c', 'd'], ....: columns=['Ohio', 'Texas', 'California'])In [99]: frameOut[99]: Ohio Texas Californiaa 0 1 2c 3 4 5d 6 7 8In [100]: frame2 = frame.reindex(['a', 'b', 'c', 'd'])In [101]: frame2Out[101]: Ohio Texas Californiaa 0.0 1.0 2.0b NaN NaN NaNc 3.0 4.0 5.0d 6.0 7.0 8.0
列可以用columns关键字重新索引:
In [102]: states = ['Texas', 'Utah', 'California']In [103]: frame.reindex(columns=states)Out[103]: Texas Utah Californiaa 1 NaN 2c 4 NaN 5d 7 NaN 8
丢弃指定轴上的项
丢弃某条轴上的一个或多个项很简单,只要有一个索引数组或列表即可。由于需要执行一些数据整理和集合逻辑,所以drop方法返回的是一个在指定轴上删除了指定值的新对象:
In [105]: obj = pd.Series(np.arange(5.), index=['a', 'b', 'c', 'd', 'e'])In [106]: objOut[106]: a 0.0b 1.0c 2.0d 3.0e 4.0dtype: float64In [107]: new_obj = obj.drop('c')In [108]: new_objOut[108]: a 0.0b 1.0d 3.0e 4.0dtype: float64In [109]: obj.drop(['d', 'c'])Out[109]: a 0.0b 1.0e 4.0dtype: float64
对于DataFrame
,可以删除任意轴上的索引值。为了演示,先新建一个DataFrame例子:
In [110]: data = pd.DataFrame(np.arange(16).reshape((4, 4)), .....: index=['Ohio', 'Colorado', 'Utah', 'New York'], .....: columns=['one', 'two', 'three', 'four'])In [111]: dataOut[111]: one two three fourOhio 0 1 2 3Colorado 4 5 6 7Utah 8 9 10 11New York 12 13 14 15
用标签序列调用drop会从行标签(axis 0)删除值:
In [112]: data.drop(['Colorado', 'Ohio'])Out[112]: one two three fourUtah 8 9 10 11New York 12 13 14 15
通过传递axis=1
或axis=’columns’
可以删除列的值:
In [113]: data.drop('two', axis=1)Out[113]: one three fourOhio 0 2 3Colorado 4 6 7Utah 8 10 11New York 12 14 15In [114]: data.drop(['two', 'four'], axis='columns')Out[114]: one threeOhio 0 2Colorado 4 6Utah 8 10New York 12 14
许多函数,如drop,会修改Series或DataFrame的大小或形状,可以就地修改对象,不会返回新的对象:
In [115]: obj.drop('c', inplace=True)In [116]: objOut[116]: a 0.0b 1.0d 3.0e 4.0dtype: float64
小心使用inplace,它会销毁所有被删除的数据。
索引、选取和过滤
Series索引(obj[…])的工作方式类似于NumPy
数组的索引,只不过Series
的索引值不只是整数。下面是几个例子:
In [117]: obj = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])In [118]: objOut[118]: a 0.0b 1.0c 2.0d 3.0dtype: float64In [119]: obj['b']Out[119]: 1.0In [120]: obj[1]Out[120]: 1.0In [121]: obj[2:4]Out[121]: c 2.0d 3.0dtype: float64In [122]: obj[['b', 'a', 'd']]Out[122]:b 1.0a 0.0d 3.0dtype: float64In [123]: obj[[1, 3]]Out[123]: b 1.0d 3.0dtype: float64In [124]: obj[obj < 2]Out[124]: a 0.0b 1.0dtype: float64
利用标签的切片运算与普通的Python切片运算不同,其末端是包含的:
In [125]: obj['b':'c']Out[125]:b 1.0c 2.0dtype: float64
用切片可以对Series
的相应部分进行设置:
In [126]: obj['b':'c'] = 5In [127]: objOut[127]: a 0.0b 5.0c 5.0d 3.0dtype: float64
用一个值或序列对DataFrame进行索引其实就是获取一个或多个列:
In [128]: data = pd.DataFrame(np.arange(16).reshape((4, 4)), .....: index=['Ohio', 'Colorado', 'Utah', 'New York'], .....: columns=['one', 'two', 'three', 'four'])In [129]: dataOut[129]: one two three fourOhio 0 1 2 3Colorado 4 5 6 7Utah 8 9 10 11New York 12 13 14 15In [130]: data['two']Out[130]: Ohio 1Colorado 5Utah 9New York 13Name: two, dtype: int64In [131]: data[['three', 'one']]Out[131]: three oneOhio 2 0Colorado 6 4Utah 10 8New York 14 12
这种索引方式有几个特殊的情况。首先通过切片或布尔型数组选取数据:
In [132]: data[:2]Out[132]: one two three fourOhio 0 1 2 3Colorado 4 5 6 7In [133]: data[data['three'] > 5]Out[133]: one two three fourColorado 4 5 6 7Utah 8 9 10 11New York 12 13 14 15
选取行的语法data[:2]十分方便。向[ ]传递单一的元素或列表,就可选择列。
另一种用法是通过布尔型DataFrame(比如下面这个由标量比较运算得出的)进行索引:
In [134]: data < 5Out[134]: one two three fourOhio True True True TrueColorado True False False FalseUtah False False False FalseNew York False False False FalseIn [135]: data[data < 5] = 0In [136]: dataOut[136]: one two three fourOhio 0 0 0 0Colorado 0 5 6 7Utah 8 9 10 11New York 12 13 14 15
这使得DataFrame的语法与NumPy二维数组的语法很像。
用loc和iloc进行选取
对于DataFrame的行的标签索引,我引入了特殊的标签运算符loc和iloc。它们可以让你用类似NumPy的标记,使用轴标签(loc)或整数索引(iloc),从DataFrame选择行和列的子集。
作为一个初步示例,让我们通过标签选择一行和多列:
In [137]: data.loc['Colorado', ['two', 'three']]Out[137]: two 5three 6Name: Colorado, dtype: int64
然后用iloc和整数进行选取:
In [138]: data.iloc[2, [3, 0, 1]]Out[138]: four 11one 8two 9Name: Utah, dtype: int64In [139]: data.iloc[2]Out[139]: one 8two 9three 10four 11Name: Utah, dtype: int64In [140]: data.iloc[[1, 2], [3, 0, 1]]Out[140]: four one twoColorado 7 0 5Utah 11 8 9
这两个索引函数也适用于一个标签或多个标签的切片:
In [141]: data.loc[:'Utah', 'two']Out[141]: Ohio 0Colorado 5Utah 9Name: two, dtype: int64In [142]: data.iloc[:, :3][data.three > 5]Out[142]: one two threeColorado 0 5 6Utah 8 9 10New York 12 13 14
所以,在pandas中,有多个方法可以选取和重新组合数据。对于DataFrame,表5-4进行了总结。后面会看到,还有更多的方法进行层级化索引。
笔记:在一开始设计pandas时,我觉得用frame[:, col]选取列过于繁琐(也容易出错),因为列的选择是非常常见的操作。我做了些取舍,将花式索引的功能(标签和整数)放到了ix运算符中。在实践中,这会导致许多边缘情况,数据的轴标签是整数,所以pandas团队决定创造loc和iloc运算符分别处理严格基于标签和整数的索引。
ix运算符仍然可用,但并不推荐。
整数索引
处理整数索引的pandas对象常常难住新手,因为它与Python内置的列表和元组的索引语法不同。例如,你可能不认为下面的代码会出错:
ser = pd.Series(np.arange(3.))serser[-1]
这里,pandas可以勉强进行整数索引,但是会导致小bug。我们有包含0,1,2的索引,但是引入用户想要的东西(基于标签或位置的索引)很难:
In [144]: serOut[144]: 0 0.01 1.02 2.0dtype: float64
另外,对于非整数索引,不会产生歧义:
In [145]: ser2 = pd.Series(np.arange(3.), index=['a', 'b', 'c'])In [146]: ser2[-1]Out[146]: 2.0
为了进行统一,如果轴索引含有整数,数据选取总会使用标签。为了更准确,请使用loc(标签)或iloc(整数):
In [147]: ser[:1]Out[147]: 0 0.0dtype: float64In [148]: ser.loc[:1]Out[148]: 0 0.01 1.0dtype: float64In [149]: ser.iloc[:1]Out[149]: 0 0.0dtype: float64
算术运算和数据对齐
pandas
最重要的一个功能是,它可以对不同索引的对象进行算术运算。在将对象相加时,如果存在不同的索引对,则结果的索引就是该索引对的并集。对于有数据库经验的用户,这就像在索引标签上进行自动外连接。看一个简单的例子:
In [150]: s1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=['a', 'c', 'd', 'e'])In [151]: s2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1], .....: index=['a', 'c', 'e', 'f', 'g'])In [152]: s1Out[152]: a 7.3c -2.5d 3.4e 1.5dtype: float64In [153]: s2Out[153]: a -2.1c 3.6e -1.5f 4.0g 3.1dtype: float64
将它们相加就会产生:
In [154]: s1 + s2Out[154]: a 5.2c 1.1d NaNe 0.0f NaNg NaNdtype: float64
自动的数据对齐操作在不重叠的索引处引入了NA值。缺失值会在算术运算过程中传播。
对于DataFrame,对齐操作会同时发生在行和列上:
In [155]: df1 = pd.DataFrame(np.arange(9.).reshape((3, 3)), columns=list('bcd'), .....: index=['Ohio', 'Texas', 'Colorado'])In [156]: df2 = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list('bde'), .....: index=['Utah', 'Ohio', 'Texas', 'Oregon'])In [157]: df1Out[157]: b c dOhio 0.0 1.0 2.0Texas 3.0 4.0 5.0Colorado 6.0 7.0 8.0In [158]: df2Out[158]: b d eUtah 0.0 1.0 2.0Ohio 3.0 4.0 5.0Texas 6.0 7.0 8.0Oregon 9.0 10.0 11.0
把它们相加后将会返回一个新的DataFrame,其索引和列为原来那两个DataFrame的并集:
In [159]: df1 + df2Out[159]: b c d eColorado NaN NaN NaN NaNOhio 3.0 NaN 6.0 NaNOregon NaN NaN NaN NaNTexas 9.0 NaN 12.0 NaNUtah NaN NaN NaN NaN
因为’c’和’e’列均不在两个DataFrame对象中,在结果中以缺省值呈现。行也是同样。
如果DataFrame对象相加,没有共用的列或行标签,结果都会是空:
In [160]: df1 = pd.DataFrame({'A': [1, 2]})In [161]: df2 = pd.DataFrame({'B': [3, 4]})In [162]: df1Out[162]: A0 11 2In [163]: df2Out[163]: B0 31 4In [164]: df1 - df2Out[164]: A B0 NaN NaN1 NaN NaN
在算术方法中填充值
在对不同索引的对象进行算术运算时,你可能希望当一个对象中某个轴标签在另一个对象中找不到时填充一个特殊值(比如0):
In [165]: df1 = pd.DataFrame(np.arange(12.).reshape((3, 4)), .....: columns=list('abcd'))In [166]: df2 = pd.DataFrame(np.arange(20.).reshape((4, 5)), .....: columns=list('abcde'))In [167]: df2.loc[1, 'b'] = np.nanIn [168]: df1Out[168]: a b c d0 0.0 1.0 2.0 3.01 4.0 5.0 6.0 7.02 8.0 9.0 10.0 11.0In [169]: df2Out[169]: a b c d e0 0.0 1.0 2.0 3.0 4.01 5.0 NaN 7.0 8.0 9.02 10.0 11.0 12.0 13.0 14.03 15.0 16.0 17.0 18.0 19.0
将它们相加时,没有重叠的位置就会产生NA值:
In [170]: df1 + df2Out[170]: a b c d e0 0.0 2.0 4.0 6.0 NaN1 9.0 NaN 13.0 15.0 NaN2 18.0 20.0 22.0 24.0 NaN3 NaN NaN NaN NaN NaN
使用df1的add方法,传入df2以及一个fill_value参数:
In [171]: df1.add(df2, fill_value=0)Out[171]: a b c d e0 0.0 2.0 4.0 6.0 4.01 9.0 5.0 13.0 15.0 9.02 18.0 20.0 22.0 24.0 14.03 15.0 16.0 17.0 18.0 19.0
下表列出了Series和DataFrame的算术方法。它们每个都有一个副本,以字母r开头,它会翻转参数。因此这两个语句是等价的:
In [172]: 1 / df1Out[172]: a b c d0 inf 1.000000 0.500000 0.3333331 0.250000 0.200000 0.166667 0.1428572 0.125000 0.111111 0.100000 0.090909In [173]: df1.rdiv(1)Out[173]: a b c d0 inf 1.000000 0.500000 0.3333331 0.250000 0.200000 0.166667 0.1428572 0.125000 0.111111 0.100000 0.090909
与此类似,在对Series或DataFrame重新索引时,也可以指定一个填充值:
In [174]: df1.reindex(columns=df2.columns, fill_value=0)Out[174]: a b c d e0 0.0 1.0 2.0 3.0 01 4.0 5.0 6.0 7.0 02 8.0 9.0 10.0 11.0 0
DataFrame和Series之间的运算
跟不同维度的NumPy数组一样,DataFrame
和Series
之间算术运算也是有明确规定的。先来看一个具有启发性的例子,计算一个二维数组与其某行之间的差:
In [175]: arr = np.arange(12.).reshape((3, 4))In [176]: arrOut[176]: array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.]])In [177]: arr[0]Out[177]: array([ 0., 1., 2., 3.])In [178]: arr - arr[0]Out[178]: array([[ 0., 0., 0., 0.], [ 4., 4., 4., 4.], [ 8., 8., 8., 8.]])
当我们从arr减去arr[0],每一行都会执行这个操作。这就叫做广播(broadcasting),附录A将对此进行详细讲解。DataFrame和Series之间的运算差不多也是如此:
In [179]: frame = pd.DataFrame(np.arange(12.).reshape((4, 3)), .....: columns=list('bde'), .....: index=['Utah', 'Ohio', 'Texas', 'Oregon'])In [180]: series = frame.iloc[0]In [181]: frameOut[181]: b d eUtah 0.0 1.0 2.0Ohio 3.0 4.0 5.0Texas 6.0 7.0 8.0Oregon 9.0 10.0 11.0In [182]: seriesOut[182]: b 0.0d 1.0e 2.0Name: Utah, dtype: float64
默认情况下,DataFrame和Series之间的算术运算会将Series的索引匹配到DataFrame的列,然后沿着行一直向下广播:
In [183]: frame - seriesOut[183]: b d eUtah 0.0 0.0 0.0Ohio 3.0 3.0 3.0Texas 6.0 6.0 6.0Oregon 9.0 9.0 9.0
如果某个索引值在DataFrame
的列或Series
的索引中找不到,则参与运算的两个对象就会被重新索引以形成并集:
In [184]: series2 = pd.Series(range(3), index=['b', 'e', 'f'])In [185]: frame + series2Out[185]: b d e fUtah 0.0 NaN 3.0 NaNOhio 3.0 NaN 6.0 NaNTexas 6.0 NaN 9.0 NaNOregon 9.0 NaN 12.0 NaN
如果你希望匹配行且在列上广播,则必须使用算术运算方法。例如:
In [186]: series3 = frame['d']In [187]: frameOut[187]: b d eUtah 0.0 1.0 2.0Ohio 3.0 4.0 5.0Texas 6.0 7.0 8.0Oregon 9.0 10.0 11.0In [188]: series3Out[188]: Utah 1.0Ohio 4.0Texas 7.0Oregon 10.0Name: d, dtype: float64In [189]: frame.sub(series3, axis='index')Out[189]: b d eUtah -1.0 0.0 1.0Ohio -1.0 0.0 1.0Texas -1.0 0.0 1.0Oregon -1.0 0.0 1.0
传入的轴号就是希望匹配的轴。在本例中,我们的目的是匹配DataFrame的行索引(axis=’index’ or axis=0)并进行广播。
函数应用和映射
NumPy的ufuncs(元素级数组方法)也可用于操作pandas对象:
In [190]: frame = pd.DataFrame(np.random.randn(4, 3), columns=list('bde'), .....: index=['Utah', 'Ohio', 'Texas', 'Oregon'])In [191]: frameOut[191]: b d eUtah -0.204708 0.478943 -0.519439Ohio -0.555730 1.965781 1.393406Texas 0.092908 0.281746 0.769023Oregon 1.246435 1.007189 -1.296221In [192]: np.abs(frame)Out[192]: b d eUtah 0.204708 0.478943 0.519439Ohio 0.555730 1.965781 1.393406Texas 0.092908 0.281746 0.769023Oregon 1.246435 1.007189 1.296221
另一个常见的操作是,将函数应用到由各列或行所形成的一维数组上。DataFrame的apply方法即可实现此功能:
In [193]: f = lambda x: x.max() - x.min()In [194]: frame.apply(f)Out[194]: b 1.802165d 1.684034e 2.689627dtype: float64
这里的函数f,计算了一个Series
的最大值和最小值的差,在frame
的每列都执行了一次。结果是一个Series,使用frame的列作为索引。
如果传递axis=’columns’到apply,这个函数会在每行执行:
In [195]: frame.apply(f, axis='columns')Out[195]:Utah 0.998382Ohio 2.521511Texas 0.676115Oregon 2.542656dtype: float64
许多最为常见的数组统计功能都被实现成DataFrame
的方法(如sum
和mean
),因此无需使用apply方法。
传递到apply的函数不是必须返回一个标量,还可以返回由多个值组成的Series
:
In [196]: def f(x): .....: return pd.Series([x.min(), x.max()], index=['min', 'max'])In [197]: frame.apply(f)Out[197]: b d emin -0.555730 0.281746 -1.296221max 1.246435 1.965781 1.393406
元素级的Python函数也是可以用的。假如你想得到frame中各个浮点值的格式化字符串,使用applymap即可:
In [198]: format = lambda x: '%.2f' % xIn [199]: frame.applymap(format)Out[199]: b d eUtah -0.20 0.48 -0.52Ohio -0.56 1.97 1.39Texas 0.09 0.28 0.77Oregon 1.25 1.01 -1.30
之所以叫做applymap,是因为Series有一个用于应用元素级函数的map方法:
In [200]: frame['e'].map(format)Out[200]: Utah -0.52Ohio 1.39Texas 0.77Oregon -1.30Name: e, dtype: object
排序和排名
根据条件对数据集排序(sorting
)也是一种重要的内置运算。要对行或列索引进行排序(按字典顺序),可使用sort_index方法,它将返回一个已排序的新对象:
In [201]: obj = pd.Series(range(4), index=['d', 'a', 'b', 'c'])In [202]: obj.sort_index()Out[202]:a 1b 2c 3d 0dtype: int64
对于DataFrame,则可以根据任意一个轴上的索引进行排序:
In [203]: frame = pd.DataFrame(np.arange(8).reshape((2, 4)), .....: index=['three', 'one'], .....: columns=['d', 'a', 'b', 'c'])In [204]: frame.sort_index()Out[204]: d a b cone 4 5 6 7three 0 1 2 3In [205]: frame.sort_index(axis=1)Out[205]: a b c dthree 1 2 3 0one 5 6 7 4
数据默认是按升序排序的,但也可以降序排序:
In [206]: frame.sort_index(axis=1, ascending=False)Out[206]: d c b athree 0 3 2 1one 4 7 6 5
若要按值对Series进行排序,可使用其sort_values方法:
In [207]: obj = pd.Series([4, 7, -3, 2])In [208]: obj.sort_values()Out[208]: 2 -33 20 41 7dtype: int64
在排序时,任何缺失值默认都会被放到Series的末尾:
In [209]: obj = pd.Series([4, np.nan, 7, np.nan, -3, 2])In [210]: obj.sort_values()Out[210]: 4 -3.05 2.00 4.02 7.01 NaN3 NaNdtype: float64
当排序一个DataFrame
时,你可能希望根据一个或多个列中的值进行排序。将一个或多个列的名字传递给sort_values的by选项即可达到该目的:
In [211]: frame = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})In [212]: frameOut[212]: a b0 0 41 1 72 0 -33 1 2In [213]: frame.sort_values(by='b')Out[213]: a b2 0 -33 1 20 0 41 1 7
要根据多个列进行排序,传入名称的列表即可:
In [214]: frame.sort_values(by=['a', 'b'])Out[214]: a b2 0 -30 0 43 1 21 1 7
排名会从1开始一直到数组中有效数据的数量。接下来介绍Series
和DataFrame
的rank方法。默认情况下,rank是通过“为各组分配一个平均排名”的方式破坏平级关系的:
In [215]: obj = pd.Series([7, -5, 7, 4, 2, 0, 4])In [216]: obj.rank()Out[216]: 0 6.51 1.02 6.53 4.54 3.05 2.06 4.5dtype: float64
也可以根据值在原数据中出现的顺序给出排名:
In [217]: obj.rank(method='first')Out[217]: 0 6.01 1.02 7.03 4.04 3.05 2.06 5.0dtype: float64
这里,条目0和2没有使用平均排名6.5,它们被设成了6和7,因为数据中标签0位于标签2的前面。
你也可以按降序进行排名:
# Assign tie values the maximum rank in the groupIn [218]: obj.rank(ascending=False, method='max')Out[218]: 0 2.01 7.02 2.03 4.04 5.05 6.06 4.0dtype: float64
下表列出了所有用于破坏平级关系的。DataFrame可以在行或列上计算排名:
In [219]: frame = pd.DataFrame({'b': [4.3, 7, -3, 2], 'a': [0, 1, 0, 1], .....: 'c': [-2, 5, 8, -2.5]})In [220]: frameOut[220]: a b c0 0 4.3 -2.01 1 7.0 5.02 0 -3.0 8.03 1 2.0 -2.5In [221]: frame.rank(axis='columns')Out[221]: a b c0 2.0 3.0 1.01 1.0 3.0 2.02 2.0 1.0 3.03 2.0 3.0 1.0
带有重复标签的轴索引
直到目前为止,我所介绍的所有范例都有着唯一的轴标签(索引值)。虽然许多pandas函数(如reindex)都要求标签唯一,但这并不是强制性的。我们来看看下面这个简单的带有重复索引值的Series:
In [222]: obj = pd.Series(range(5), index=['a', 'a', 'b', 'b', 'c'])In [223]: objOut[223]: a 0a 1b 2b 3c 4dtype: int64
索引的is_unique属性可以告诉你它的值是否是唯一的:
In [224]: obj.index.is_uniqueOut[224]: False
对于带有重复值的索引,数据选取的行为将会有些不同。如果某个索引对应多个值,则返回一个Series;而对应单个值的,则返回一个标量值:
In [225]: obj['a']Out[225]: a 0a 1dtype: int64In [226]: obj['c']Out[226]: 4
这样会使代码变复杂,因为索引的输出类型会根据标签是否有重复发生变化。
对DataFrame的行进行索引时也是如此:
In [227]: df = pd.DataFrame(np.random.randn(4, 3), index=['a', 'a', 'b', 'b'])In [228]: dfOut[228]: 0 1 2a 0.274992 0.228913 1.352917a 0.886429 -2.001637 -0.371843b 1.669025 -0.438570 -0.539741b 0.476985 3.248944 -1.021228In [229]: df.loc['b']Out[229]: 0 1 2b 1.669025 -0.438570 -0.539741b 0.476985 3.248944 -1.021228
5.3 汇总和计算描述统计
pandas
对象拥有一组常用的数学和统计方法。它们大部分都属于约简和汇总统计,用于从Series中提取单个值(如sum或mean)或从DataFrame的行或列中提取一个Series。跟对应的NumPy数组方法相比,它们都是基于没有缺失数据的假设而构建的。看一个简单的DataFrame:
In [230]: df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5], .....: [np.nan, np.nan], [0.75, -1.3]], .....: index=['a', 'b', 'c', 'd'], .....: columns=['one', 'two'])In [231]: dfOut[231]: one twoa 1.40 NaNb 7.10 -4.5c NaN NaNd 0.75 -1.3
调用DataFrame的sum方法将会返回一个含有列的和的Series:
In [232]: df.sum()Out[232]: one 9.25two -5.80dtype: float64
传入axis=’columns’或axis=1将会按行进行求和运算:
In [233]: df.sum(axis=1)Out[233]:a 1.40b 2.60c NaNd -0.55
NA值会自动被排除,除非整个切片(这里指的是行或列)都是NA。通过skipna选项可以禁用该功能:
In [234]: df.mean(axis='columns', skipna=False)Out[234]: a NaNb 1.300c NaNd -0.275dtype: float64
下表列出了这些约简方法的常用选项。
有些方法(如idxmin和idxmax)返回的是间接统计(比如达到最小值或最大值的索引):
In [235]: df.idxmax()Out[235]: one btwo ddtype: object
另一些方法则是累计型的:
In [236]: df.cumsum()Out[236]: one twoa 1.40 NaNb 8.50 -4.5c NaN NaNd 9.25 -5.8
还有一种方法,它既不是约简型也不是累计型。describe就是一个例子,它用于一次性产生多个汇总统计:
In [237]: df.describe()Out[237]: one twocount 3.000000 2.000000mean 3.083333 -2.900000std 3.493685 2.262742min 0.750000 -4.50000025% 1.075000 -3.70000050% 1.400000 -2.90000075% 4.250000 -2.100000max 7.100000 -1.300000
对于非数值型数据,describe
会产生另外一种汇总统计:
In [238]: obj = pd.Series(['a', 'a', 'b', 'c'] * 4)In [239]: obj.describe()Out[239]: count 16unique 3top afreq 8dtype: object
下表列出了所有与描述统计相关的方法。
相关系数与协方差
有些汇总统计(如相关系数和协方差)是通过参数对计算出来的。我们来看几个DataFrame,它们的数据来自Yahoo!Finance的股票价格和成交量,使用的是pandas-datareader包(可以用conda或pip安装):
conda install pandas-datareader
我使用pandas_datareader模块下载了一些股票数据:
import pandas_datareader.data as weball_data = {ticker: web.get_data_yahoo(ticker) for ticker in ['AAPL', 'IBM', 'MSFT', 'GOOG']}price = pd.DataFrame({ticker: data['Adj Close'] for ticker, data in all_data.items()})volume = pd.DataFrame({ticker: data['Volume'] for ticker, data in all_data.items()})
注意:此时Yahoo! Finance已经不存在了,因为2017年Yahoo!被Verizon收购了。参阅pandas-datareader文档,可以学习最新的功能。
现在计算价格的百分数变化,时间序列的操作会在第11章介绍:
In [242]: returns = price.pct_change()In [243]: returns.tail()Out[243]: AAPL GOOG IBM MSFTDate 2016-10-17 -0.000680 0.001837 0.002072 -0.0034832016-10-18 -0.000681 0.019616 -0.026168 0.0076902016-10-19 -0.002979 0.007846 0.003583 -0.0022552016-10-20 -0.000512 -0.005652 0.001719 -0.0048672016-10-21 -0.003930 0.003011 -0.012474 0.042096
Series的corr方法用于计算两个Series中重叠的、非NA的、按索引对齐的值的相关系数。与此类似,cov用于计算协方差:
In [244]: returns['MSFT'].corr(returns['IBM'])Out[244]: 0.49976361144151144In [245]: returns['MSFT'].cov(returns['IBM'])Out[245]: 8.8706554797035462e-05
因为MSTF是一个合理的Python属性,我们还可以用更简洁的语法选择列:
In [246]: returns.MSFT.corr(returns.IBM)Out[246]: 0.49976361144151144
另一方面,DataFrame的corr和cov方法将以DataFrame的形式分别返回完整的相关系数或协方差矩阵:
In [247]: returns.corr()Out[247]: AAPL GOOG IBM MSFTAAPL 1.000000 0.407919 0.386817 0.389695GOOG 0.407919 1.000000 0.405099 0.465919IBM 0.386817 0.405099 1.000000 0.499764MSFT 0.389695 0.465919 0.499764 1.000000In [248]: returns.cov()Out[248]: AAPL GOOG IBM MSFTAAPL 0.000277 0.000107 0.000078 0.000095GOOG 0.000107 0.000251 0.000078 0.000108IBM 0.000078 0.000078 0.000146 0.000089MSFT 0.000095 0.000108 0.000089 0.000215
利用DataFrame的corrwith方法,你可以计算其列或行跟另一个Series或DataFrame之间的相关系数。传入一个Series将会返回一个相关系数值Series(针对各列进行计算):
In [249]: returns.corrwith(returns.IBM)Out[249]: AAPL 0.386817GOOG 0.405099IBM 1.000000MSFT 0.499764dtype: float64
传入一个DataFrame
则会计算按列名配对的相关系数。这里,我计算百分比变化与成交量的相关系数:
In [250]: returns.corrwith(volume)Out[250]: AAPL -0.075565GOOG -0.007067IBM -0.204849MSFT -0.092950dtype: float64
传入axis=’columns’即可按行进行计算。无论如何,在计算相关系数之前,所有的数据项都会按标签对齐。
唯一值、值计数以及成员资格
还有一类方法可以从一维Series的值中抽取信息。看下面的例子:
In [251]: obj = pd.Series(['c', 'a', 'd', 'a', 'a', 'b', 'b', 'c', 'c'])
第一个函数是unique,它可以得到Series中的唯一值数组:
In [252]: uniques = obj.unique()In [253]: uniquesOut[253]: array(['c', 'a', 'd', 'b'], dtype=object)
返回的唯一值是未排序的,如果需要的话,可以对结果再次进行排序(uniques.sort())。相似的,value_counts用于计算一个Series中各值出现的频率:
In [254]: obj.value_counts()Out[254]: c 3a 3b 2d 1dtype: int64
为了便于查看,结果Series是按值频率降序排列的。value_counts还是一个顶级pandas方法,可用于任何数组或序列:
In [255]: pd.value_counts(obj.values, sort=False)Out[255]: a 3b 2c 3d 1dtype: int64
isin用于判断矢量化集合的成员资格,可用于过滤Series中或DataFrame列中数据的子集:
In [256]: objOut[256]: 0 c1 a2 d3 a4 a5 b6 b7 c8 cdtype: objectIn [257]: mask = obj.isin(['b', 'c'])In [258]: maskOut[258]: 0 True1 False2 False3 False4 False5 True6 True7 True8 Truedtype: boolIn [259]: obj[mask]Out[259]: 0 c5 b6 b7 c8 cdtype: object
与isin类似的是Index.get_indexer方法,它可以给你一个索引数组,从可能包含重复值的数组到另一个不同值的数组:
In [260]: to_match = pd.Series(['c', 'a', 'b', 'b', 'c', 'a'])In [261]: unique_vals = pd.Series(['c', 'b', 'a'])In [262]: pd.Index(unique_vals).get_indexer(to_match)Out[262]: array([0, 2, 1, 1, 0, 2])
表给出了这几个方法的一些参考信息。
有时,你可能希望得到DataFrame中多个相关列的一张柱状图。例如:
In [263]: data = pd.DataFrame({'Qu1': [1, 3, 4, 3, 4], .....: 'Qu2': [2, 3, 1, 2, 3], .....: 'Qu3': [1, 5, 2, 4, 4]})In [264]: dataOut[264]: Qu1 Qu2 Qu30 1 2 11 3 3 52 4 1 23 3 2 44 4 3 4
将pandas.value_counts传给该DataFrame的apply函数,就会出现:
In [265]: result = data.apply(pd.value_counts).fillna(0)In [266]: resultOut[266]: Qu1 Qu2 Qu31 1.0 1.0 1.02 0.0 2.0 1.03 2.0 2.0 0.04 2.0 0.0 2.05 0.0 0.0 1.0
这里,结果中的行标签是所有列的唯一值。后面的频率值是每个列中这些值的相应计数。