Welcome to pydata's documentation!

第三章:内建数据结构,函数和文件

这章讨论贯穿全书内嵌入python语言的能力。 虽然像pandas和numpy等附加库为大规模数据集增加了高级计算函数,但它们是被设计和python内建数据处理工具一起使用。

我们将从python重度使用的数据结构开始:元组、列表、字典和集合。 然后我们将讨论创建你自己重复使用的python函数。 最后,我们将关注python文件对象技术和与本地硬盘交互。

3.1 数据结构和序列

python的数据结构是简单但强大的。掌握它们的使用是成为一个熟练的python程序员的一个关键部分。

元组

一个元组是一个固定长度,不可变序列的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等许多其它语言相同,Python序列索引从0开始:

In [8]: tup[0]
Out[8]: 's'

虽然存储在元组中的对象本身可能是可变的,但是一旦创建了元组,就无法修改每个位置(slot)中存储的对象:

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

如果一个元组中对象是可变的,如list,你可以原位修改它们:

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')

用整数乘以元组,和list一样,有连接多个相同元组的效果:

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

另一种常见用法是从函数返回多个值。 我将在稍后详细介绍这一点(I’ll cover this in more detail later)。

Python语言最近获得一些先进的元组解包以帮助某些情景下我们可能想取出元组开头的部分元素。 这使用特殊语法*rest,它也用于函数签名捕获任意(arbitrarily)长的位置参数列表:

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
元组方法

由于元组的大小和内容无法修改,因此实例的方法很少。 一个特别有用的(在列表中也可用)是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']

列表和元组在语义上(semantically)是相似的(虽然元组不能被修改),在许多函数中可被替换(interchangeably)使用。

列表函数经常被作为一种具化迭代器或生成表达式方式在数据处理中使用:

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到列表长度之间,两端值也都包括(inclusive)。

注意:插入比追加计算昂贵,因为涉及的子列必须在内部移动以为新元素腾出位置。 如果你需要在一个序列的开头和结尾处插入元素,为此,你可以研究collection.deque,一个双端列队。

insert的对立操作是pop,在list的特定索引移除和返回一个元素:

In [49]: b_list.pop(2)
Out[49]: 'peekaboo'

In [50]: b_list
Out[50]: ['foo', 'red', 'baz', 'dwarf']

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

关键字not可用于否定(negate in):

In [56]: 'dwarf' not in b_list
Out[56]: False

检查一个list是否包含某个值比相同的操作在字典和集合更慢,因为Python对列表值做线性扫描,而它可以在恒定时间内检查其它数据结构(基于哈希表)。

连接和组合列表

与元组相似,把两个列表加在一起使用+连接它们:

In [57]: [4, None, 'foo'] + [7, 8, (2, 3)]
Out[57]: [4, None, 'foo', 7, 8, (2, 3)]

如果你已经定义了一个list,你可以使用extend方法追加多个元素到list:

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)]

注意通过加号连接列表是一个相对昂贵的操作,因为一个新列表必须被创建并且对象要被复制过去。 使用extent追加元素到一个已存在的列表中,尤其当你构建一个大的list时,通常更好。因此:

everything = []
for chunk in list_of_lists:
        everything.extend(chunk)
排序

你可以通过调用list的sort方法原位(in-plance)排序它(没有创建一个新list):

In [61]: a = [7, 2, 5, 1, 3]

In [62]: a.sort()

In [63]: a
Out[63]: [1, 2, 3, 5, 7]

sort有几个选项偶尔会派上用场(come in handy). 一种是能够传递一个二级排序键--产生一个值来排序对象的函数。 例如,我们可以通过字符串长度来排序一个字符串容器:

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函数,可以产生一个通用序列排好序的副本。

注意: a.sort()返回值为None,因为它是原位修改a的,所以不能a = a.sort()

切片

你可以通过切片记号(notation)选择大多数序列类型对象的部分,由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]

因为在start索引的元素被包含,stop索引的不被包含,所以元素数量是stop-start。

start或stop都可以被省略(omit),在这种情况下,默认分别从序列的开头和序列结尾:

In [77]: seq[:5]
Out[77]: [7, 2, 3, 6, 3]

In [78]: seq[3:]
Out[78]: [6, 3, 5, 6, 0, 1]

负索引(negative indices)将序列相对于末尾切片:

In [79]: seq[-4:]
Out[79]: [5, 6, 0, 1]

In [80]: seq[-6:-2]
Out[80]: [6, 3, 5, 6]

切片语义需要一些习惯,特别是,如果你来自R或MATLAB。 图3-1对正负整数切片是一个有用的指导。 在图中,索引显示在格子边缘,帮助显示使用正负整数索引切片选择的开始和结束位置。

第二个冒号后可以使用一个步长,用以每隔几个取元素:

In [81]: seq[::2]
Out[81]: [7, 3, 3, 6, 1]

一种机智的使用是使步长为-1,可以反向一个list或tuple:

In [82]: seq[::-1]
Out[82]: [1, 0, 6, 5, 3, 6, 3, 2, 7]
_images/Figure_3-1_Illustration_of_Python_slicing_conventions.bmp

内置序列函数

Python有几个(a handful of)有用的序列函数,您应该熟悉并随时使用它们。

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函数从任何序列的元素返回一个新的排好序的list:

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同时(simultaneously)迭代多个序列:

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

给一个"zipped"序列,zip可以被聪明的应用于"unzip"序列。 另一种方式实现这个是通过转换一行list到一列list。语法看起来有点神奇:

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是一个生成器(稍后将对此进行更详细的讨论),所以在具体化之前它不会创建一个反向序列(例如用list或for循环)。

字典

dict可能是python中最重要的内置数据结构。 一个更常见的名字是哈希表或关联数组。 它是一个可变大小的键-值对容器,键和值都是python的对象。创建字典的一种方式是使用花括号(curly braces),并用冒号分隔键值:

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方法原位修改字典,所以任何被传递去更新的数据,已存在的键它原来的值将丢弃。

从序列创建字典

经常有想把两个序列逐元素配对成字典的情况。 第一次尝试,你可能写的代码像下面这样:

mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = value

由于(since)dict本质上(essentially)是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 comprehensions,另一种构建字典的优雅方式。

默认值

下面逻辑很常见:

if key in some_dict:
        value = some_dict[key]
else:
        value = default_value

字典方法get和pop能返回一个默认值,所以上面if-else语句块可以如下简写:

value = some_dict.get(key, default_value)

如果key不存在get默认返回None,pop将抛出一个异常。 对于赋值,一种常见的情况是dict中的值是其他集合,如列表。 例如,你可以想象用单词的第一个字母对单词列表进行分类,形成一个列表字典:

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字典方法精确用于此目的。前面的(preceding)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)
合法的字典键类型

尽管字典值可以是任何字典类型,但键通常是不可变对象,如标量(scalar)类型(int, float, string)、元组(所有在元组中的对象也要是不可变的)。 技术术语叫可哈希能力(hashability)。 你可以用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'

如果要使用一个list作为键,可将其转为tuple,只要它的元素也能够被哈希:

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}

set支持集合的数学运算,如并集、交集(intersection,)、差、和对称差(symmetric difference)。考虑下面两个示例集合:

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}

常用集合方法见表3-1.

_images/Table_3-1_python_set_operations.png

所有逻辑集合操作都具有就地对应(in-place counterparts),使得你可以用结果替换操作符左侧的集合内容。 对一个很大的集合来说,这可能是更有效率的:

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)}

你也可检查一个集合是否是另一个集合的子集或超集:

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

列表、字典和集合推导(comprehensions)

列表推导是最受喜欢的python特性之一。 它允许你简洁地从容器过滤元素生成一个新列表,在一个简洁表达式中转换通过过滤器的元素,它的基本形式是:

[expr for val in collection if condition]

这与下面的for循环等效:

result = []
for val in collection:
        if condition:
                result.append(expr)

过滤条件可以省略,仅留下表达式。 例如,给定一个字符串列表,我们过滤字符串长度小于等于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']

集合和字典推导是天然的扩展,在惯用地(idiomatically)相似方式产生集合和字典。 字典推导像这个:

dict_comp = {key-expr : value-expr for value in collection if condition}

集合推导除了用花括号代替方括号外,与列表推导看起来很像:

set_comp = {expr for value in collection if condition}

像列表推导一样,集合和字典推导主要(mostly)是便利,但是它们同样(similarly)可以使代码更容易编写和阅读。 考虑来自前面的字符串列表。 假设我们想要一个包含容器中字符串长度的集合,我们可以很方便地使用集合推导来计算:

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}

作为一个简单的字典推导例子,我们可以创建一个字符串到它们在列表中位置的查阅表:

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'的全部名字的单个列表。 我们当然可以用一个简单的循环实现它:

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)

你实际上可以将整个操作包装(wrap)在单个嵌套列表解析中,像这样:

In [162]: result = [name for names in all_data for name in names if name.count('e') >= 2]

In [163]: result
Out[163]: ['Steven']

首先,嵌套列表推导有点难以理解(a bit hard to wrap your head around)。 列表推导的for部分按照嵌套顺序排列(arrange),任何过滤条件像之前一样放在末尾。 这里是另一个“扁平化(flantten)”整型元组列表到一个简单的整型列表中的例子:

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)

你可以有任意多水平嵌套,但是如果你有超过2或3层嵌套,你可能要开始疑问,站在代码可读性角度这是否有意义。 展示从一个列表推导里面嵌套列表推导,对区分语法而言是重要的,也是完全有效的(?):

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中初级的、最重要的代码组织和重用方式。 根据经验(as a rule of thumb),如果你有预料要重复相同或相似的代码,写一个可重复使用的函数可能值得。 通过给代码块取一个名字,函数也可以增加你的代码可读性。

函数使用def关键字声明,从return关键字处返回:

def my_function(x, y, z=1.5):
        if z > 1:
                return z * (x + y)
        else:
                return z / (x + y)

有多个return语句没有问题。 如果到达函数的结尾处还没有遇到(encounter)返回语句,python自动返回None。

每个函数可以有位置参数和关键字参数。 关键字参数常用来指定默认值或作为可选参数。 在前面的(preceding, adj)函数中,x和y是位置参数,z是关键参数。 这意味着函数可以用以下这些方式中任一种调用:

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)

在某些情况下这可以帮助增加代码可读性。

名字空间、作用域和局部函数

函数在两种不同的作用域内可访问变量:全局和局部。 在python中,变量作用域更具描述力的名字叫名字空间。 任何在函数内被赋值的变量默认是局部名字空间。 局部名字空间在函数被调用时创建,立即由函数参数充填(populate). 在函数结束时,局部名字空间被释放(在本章范围(purview)外有一些例外)。 考虑下面函数:

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关键字。全局变量在储存系统有些状态时使用比较典型。 如果你发现自己使用的比较多,你可能要考虑使用面向对象的编程(使用类)。

返回多个值

当我使用过Java和C++后,第一次使用python编程时,我最喜爱的特性之一就是能够用一个简单的语法使函数返回多个值。这里有一个例子:

def f():
        a = 5
        b = 6
        c = 7
        return a, b, c

a, b, c = f()

在数据分析和其它的科学应用中,你经常发现自己干这件事。 函数在这里实际上返回的是一个对象,叫元组,被解包到结果变量中去了。 在之前的例子中,我们可以这样做:

return_value = f()

在这种情况,return_value将是一个有三个返回变量的三元元组。 像以前,一个可能具有吸引力的代替方案是使用字典返回多个值:

def f():
        a = 5
        b = 6
        c = 7
        return {'a' : a, 'b' : b, 'c' : c}

这种替代方案是有用的,取决于你想尝试做什么。

函数是对象

因为python函数是对象,所以在其他语言中难以表达的结构在Python中是比较容易表达的。 假如我们正在做数据清洗工作,需要对下列字符串列表进行一些列的变换:

In [171]: states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
.....:  'south carolina##', 'West virginia?']

任何处理过用户提交的调查数据都会见到这些凌乱的结果。 需要做很多事情来使这个字符串列表统一为分析做准备:移除空格,删除标点(punctuation)符号,并标准化适当的大小写。 做这件事的一种方式是使用内置的字符串方法以及(along with)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

结果像这样:

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函数现在也更具可重用性和通用性(generic)。

你可以将函数作为其它函数的参数,如内置的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 * 2 for x in ints],但是这里我们可以简洁地(succinctly)传递一份定制的操作给apply_to_list函数。 另一个例子,假设你想分类一个字符串容器,通过每个字符串中可区分的字母:

In [177]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

这里我们传递一个lambda函数给list的sort方法:

In [178]: strings.sort(key=lambda x: len(set(list(x))))

In [179]: strings
Out[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']

提醒: 匿名函数被叫做lambda函数的一个原因是,不像用def定义(declared)的函数,函数对象本身没有一个明确的__name__属性。

Currying:部分(partical)参数应用

Currying是计算机科学的行话(以数学家Haskell Curry命名(named after)),意思是通过部分参数应用从已存在的函数中得到(derive)新函数。 举例,假设我们有一个不重要的函数,两个数相加:

def add_numbers(x, y):
        return x + y

使用这个函数,我们得到一个变量的新函数,add_five, 加5到这个参数上:

add_five = lambda y: add_numbers(5, y)

add_number函数的第二个参数被叫做currying。这儿没有什么有趣的东西,我们实际做的就是定义一个新函数,调用已存在的函数。 内置functools模型的partial函数可以简化这一过程:

from functools import partial
add_five = partial(add_numbers, 5)

生成器

有一致的方式迭代如list中的objects或文件中的lines等序列,是一种重要的Python特性。 这是通过(by means of)迭代器协议(iterator protocol)实现的,迭代器协议是使对象可迭代的(iterable)通用(generic)方法。 例如,迭代dict会产生dict键:

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循环这样的上下文中被使用到,迭代器是yield到Python解释器中的任何对象。 许多参数是list或类似list的对象的方法也接受任何迭代器对象作为参数。这包括内建的方法如min、max和sum,以及类型构造函数如list、tuple:

In [184]: list(dict_iterator)
Out[184]: ['a', 'b', 'c']

生成器是构造迭代器对象的一种简洁方式。正常函数执行和返回单个结果每次,生成器以懒加载方式返回多个结果的一个序列,在返回一个结果后暂停知道下一个结果被请求。 为创建生成器,在函数中使用yield关键字代替return:

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
生成器表达式

另一种甚至更简洁的方式产生生成器是使用生成器表达式。 这种生成器类似于(analogue)列表、字典和集合推导。 创建生成器,括起列表推导使用圆括号(parentheses)代替方括号(breckets):

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用任何序列和函数作为参数,通过参数中函数返回值分组序列中连续的(consecutive)元素。这里有一个例子:

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']

表3-2是其它itertools函数清单,这些函数时常让我觉得很有用。 更多关于这些有用的内置实用模块,你要去查看Python官方的文档。

_images/Table_3-2_Some_useful_itertools_functions.png

错误和异常处理

仔细处理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

在except中的代码仅在float(x)抛出一个异常情况下执行:

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'

您可能只想处理(suppress)ValueError,因为TypeError(输入不是字符串或数值)可能表示程序中存在合法(legitimate)错误。 写异常类型在except后面来做这个:

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总是要关闭。 类似地,你也可以有代码块仅仅在try:代码块执行成功情况下执行,使用else:

f = open(path, 'w')
try:
        write_to_file(f)
except:
        print('Failed')
else:
        print('Succeeded')
finally:
        f.close()
IPython中异常

当你通过%运行一个脚本或执行任何语句抛出异常,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解释器(不提供附加内容)是一个很大优势。 你可以使用%xmodoe魔术方法控制显示的内容数量,从Plain(和标准Python解释器一样)到Verbose(其中内联函数参数值等)。你将在在后面章节看到,你也可以在错误发生后进入堆栈(使用%debug或%pdb魔法)交互式分析(post-mortem)调试。

3.3 文件和操作系统

这本书大多数地方使用高层次工具如pandas.readcsv来从磁盘读数据文件到Python数据结构。 尽管如此,理解基本的关于Python中文件如何工作是很重要的。 幸运的是这很简单,这也是为什么Python在文本和文件处理中如此受欢迎的原因之一。

打开文件读写,使用内置的open函数,用相对路径或绝对路径:

In [207]: path = 'examples/segismundo.txt'

In [208]: f = open(path)

默认文件以只读模式打开。 然后我们就可以像一个列表一样对待文件句柄,像下面这样迭代文本行:

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创建文件时,完成后显式关闭文件是重要的。关闭文件可以释放它的资源返还给操作系统:

In [211]: f.close()

一种清理打开文件的简单方式是使用with语句:

In [212]: with open(path) as f:
.....:          lines = [x.rstrip() for x in f]

在离开with代码块时将自动关闭文件f。

如果我们敲f = open(path, 'w'), 一个新文件在examples/segismundo.txt将被创建(小心!)。重写在这个地方的任何文件。也有'x'文件模式,创建一个可写文件但是如果文件已存在将创建失败。 见表3-3 所有合法读写模式清单。

对于可读文件,一些尝试用的方法是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方法使文件句柄的位置按读取的字节数递增(advance, 有前进之意)。 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()
_images/Table_3-3_Python_file_modes.png

写文本到文件中可以使用文件的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']

表3-4有许多最常用的文件方法。

_images/Table_3-4_Important_Python_file_methods_or_attributes_1.png _images/Table_3-4_Important_Python_file_methods_or_attributes_2.png

文件的字节和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从文件读足够的字节来解码这么多字符。如果我们用'rb'模式打开文件,read请求确定数量的字节:

In [232]: with open(path, 'rb') as f:
.....:          data = f.read(10)
In [233]: data
Out[233]: b'Sue\xc3\xb1a el '

取决于文本编码,你可以解析字节到字符串对象,但仅仅是在每个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编码的字符中间字节,随后的(subsequent)读将产生一个错误:

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官方文档。

3.4 总结

现在有了Python环境和语言的一些基础知识,是时候继续前进,学习NumPy和Python面向数组的计算。

第四章:numpy基础:数组和向量化计算

NumPy是Numerical Python的缩写(short for),是Python中数值计算最重要的基础包之一。 大多数提供科学计算功能的包使用NumPy的数组对象作为数据交换的通用语言。

有几样你将在NumPy中发现:

ndarray,一个高效的多维数组,提供快速的面向数组的算法操作和灵活的broadcasting能力。

在整个数组数据上不需要写循环就可以快速操作的数学函数。

读写数组数据到磁盘的工具,和内存映射文件工作。

线性代数、随机数生成和傅里叶变换等能力。

连接NumPy到用C、C++或FORTRAN写的库的C API。

因为NumPy提供易于使用的C API,很容易传递数据到用更低语言写的外部库和从外部库返回数据到Python的NumPy数组中。 此功能使Python成为包装(wrap)旧(legacy)C / C ++ / Fortran代码库并为其提供动态和易于使用的接口。

NumPy本身不提供建模或科学计算功能,理解NumPy数组和面向数组的计算将帮助你使用面向数组语义十分有效的工具,如pandas。 因为NumPy是一个很大的主题,我将更深入讨论许多如broadcasting等先进的NumPy特性(见附录A)。

对于大多数数据分析应用,我们将关注主要功能领域是:

快速向量化数组操作,用于数据整理和清理、子集和过滤、转换和任何其他类型的计算

常见的数组算法,如排序,唯一和集合操作

高效的描述性统计和聚合(aggregating)/汇总数据

数据对齐和关系数据操作对于合并和连接到一个异构(heterogeneous)数据集中

将条件逻辑表示为数组表达式而不是使用if-elif-else的循环分支机构

分组数据操作(聚合,转换,功能应用)

虽然Numpy提供关于通用数值数据处理的计算功能,但大量用户想要使用pandas作为基础对于许多统计和分析,尤其关于表格数据。 pandas也提供一些特殊领域,如时间序列操作,NumPy中没有呈现的。

注: Python中面向数组的计算根源可追溯到1995年,在Jim Hugunin创建了数值计算库时。 后面的10年,大量科学计算程序社区开始在Python中做数组编程,但是库的生态系统在2000年初支离破碎(fragmented)。 在2005年,Travis Oliphant通过当时的Numeric和Numarray项目打造(forge)NumPy项目,将社区整合到单个数组计算框架中。

在Python数值计算,NumPy如此重要的原因之一是因为它被设计成对大型数组数据高效。有以下一点原因:

NumPy内部储存数据在连续的内存块,独立于其它内建Python对象。用C语言实现的NumPy算法库能够操纵内存无须类型检查或其它开销(overhead)。

NumPy在整个数组上执行复杂计算无须Python for循环。

为了给你性能差异的直观概念,考虑百万整型的Numpy数据以及等效的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实现(counterparts)快10到100倍(或者更多)并使用明显更少的内存。

4.1 NumPy ndarray:一个多维数组对象

NumPy最关键的特点之一是它的N维数组对象,或者叫ndarray,在Python中是一个快速、灵活的大数据集容器。 数组使你能够使用相似的语法在整个数据块上执行数学操作,与在标量元素上有相同效果的操作。

为了让您了解NumPy如何使用类似语法对于内置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 *在你的代码中,避免一直写np.,但是我不建议这样的习惯。 numpy名字空间是很大的,包含许多名字和内置Python函数冲突的函数(像min和max)。

ndarray是用于同类(homogeneous)数据的通用多维容器; 这是说,所有的元素必须是相同类型。每个数组有一个shape。一个表示每个维度大小的元组,dtype,描述数组数据类型的对象:

In [17]: data.shape
Out[17]: (2, 3)

In [18]: data.dtype
Out[18]: dtype('float64')

这章讲给你介绍基本的NumPy数组使用,会大量出现于本书的剩余部分。 虽然不必要深入理解NumPy的许多数据分析应用程序,但是精通(proficient)面向数组的编程和思考是成为科学计算Python专家(guru)的关键一步。

注: 不管在文中你是见到array、NumPy array还是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。 我们可以通过检查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的数组,使用给定的长度或shape。empty创建没有初始化值到具体值的数组。 为了用这些方法创建更高维度的数组,传一个元组给shape:

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])

见表4-1 标准数组创建函数短列表。 由于NumPy关注于数值计算,数据类型如果未指定,在许多情况下是float64(浮点)。

_images/Table_4-1_Array_creation_functions.png

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,随后是一个表明每个元素占多少位的数字。标准双精度浮点数占8个字节或64位。 因此,这种类型在NumPy中被命名为float64。见表4-2 NumPy支持的数据类型全面清单。

注:不要担心记忆NumPy数据类型,尤其是你现在还是一个新手时。 仅仅有必要关心你正在处理的通用数据种类,是浮点数。复数、整型、布尔型还是一般的Python对象。 当你需要更多地控制在磁盘和内存中数据储存方式时,特别是大型数据集,最好知道你控制的储存类型。

_images/Table_4-2_NumPy_data_types.png

你能显式转换或铸造(cast)一个数组从一种dtype到另一种,使用ndarray的adtype方法:

In [37]: arr = np.array([1, 2, 3, 4, 5])

In [38]: arr.dtype
Out[38]: dtype('int64')

In [39]: float_arr = arr.astype(np.float64)

In [40]: float_arr.dtype
Out[40]: dtype('float64')

在这个例子中,整型被转换成浮点型。 如果我转换一些浮点数字到一个整型dtype,小数部分将被丢弃:

In [41]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])

In [42]: arr
Out[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_type,因为string是固定大小,可能丢弃输入而没有警告。 pandas对非数字数据有更直观的开箱即用(out-of-the-box)行为。

如果由于一些原因(如字符串不能别转换成float64)转换失败,ValueError将抛出。 这儿我有点懒,写了float代替np.float64;NumPy将Python类型别名为其自己的等效数据类型。 你也可以使用另一种数组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.])

有速记(shorthand)类型代码字符串,你可以指定一个dtype:

In [49]: empty_uint32 = np.empty(8, dtype='u4')

In [50]: empty_uint32
Out[50]:
array([ 0, 1075314688, 0, 1075707904, 0,
1075838976, 0, 1072693248], dtype=uint32)

注: 调用astype一直会创建一个新数组(数据的一份拷贝),即使新dtype与老dtype相同。

使用NumPy数组进行算术运算(arithmetic)

数组是重要的因为它不需要使用for循环就可以表达数据的批量操作。 NumPy使用者称此为向量化。 任何在相同尺寸数组之间的算术操作都将操作应用到每个元素上:

In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])

In [52]: arr
Out[52]:
array([[ 1., 2., 3.],
[ 4., 5., 6.]])

In [53]: arr * arr
Out[53]:
array([[ 1., 4., 9.],
[ 16., 25., 36.]])

In [54]: arr - arr
Out[54]:
array([[ 0., 0., 0.],
[ 0., 0., 0.]])

与标量的算术操作将操作扩散(propagate)到数组中每个元素:

In [55]: 1 / arr
Out[55]:
array([[ 1. , 0.5 , 0.3333],
[ 0.25 , 0.2 , 0.1667]])

In [56]: arr ** 0.5
Out[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]: arr2
Out[58]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])

In [59]: arr2 > arr
Out[59]:
array([[False, True, False],
[ True, False, True]], dtype=bool)

在不同大小数组之间操作称之为广播(broadcasting),将在附录A中详细讨论。本书的大部分不需要深入理解广播。

基本索引和切片

NumPy数组索引是一个丰富的主题,因为有许多方法可以从你的数据中选择子集或单个元素。 一维数组很简单,表面上看与Python列表操作相似:

In [60]: arr = np.arange(10)

In [61]: arr
Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [62]: arr[5]
Out[62]: 5

In [63]: arr[5:8]
Out[63]: array([5, 6, 7])

In [64]: arr[5:8] = 12

In [65]: arr
Out[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])

如你看到的,你可以给切片赋一个标量值,像在arr[5:8] = 12,值被扩散到整个选中区域。 与Python列表一个重要的区别是数组切片是原始数组视图。 这意味着数据没有拷贝,任何对视图的修改将反映到源数组中。

给一个这样的例子,我首先创建arr的一个切片:

In [66]: arr_slice = arr[5:8]

In [67]: arr_slice
Out[67]: array([12, 12, 12])

现在当我改变arr_slice值,变化(mutation)反映到原始数组arr中:

In [68]: arr_slice[1] = 12345

In [69]: arr
Out[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8,
9])

"光秃秃"的切片将给一个数组中所有元素赋值:

In [70]: arr_slice[:] = 64

In [71]: arr
Out[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]: 3

In [75]: arr2d[0, 2]
Out[75]: 3

见图4-1二维数组索引说明。 我发现将轴0想成"行"轴1想成"列"是有帮助的。

_images/Figure_4-1_Indexing_elements_in_a_NumPy_array.png

在多维数组中,如果你省略后面的索引,返回的对象将是包含沿更高维全部数据的更低维ndarray。 所以在2x2x3数组arr3d:

In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

In [77]: arr3d
Out[77]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])

arr3d[0]是2x3数组::

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)开始的所有值,形成一个1维数组:

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])

注意上面全部例子里,数组被选中的子集返回数组都是视图。

使用切片进行索引

一维对象像Python列表,ndarray使用相似的语法切片:

In [88]: arr
Out[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]: arr2d
Out[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])

见图4-2的说明。 注意逗号本身意味着对全部轴操作,所以你可以仅仅在更高维轴通过这样切片:

In [95]: arr2d[:, :1]
Out[95]:
array([[1],
[4],
[7]])

当然,赋值给一个切片表达式等于赋值给整个选中内容:

In [96]: arr2d[:2, 1:] = 0

In [97]: arr2d
Out[97]:
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
_images/Figure_4-2_Two-dimensional_array_slicing.png

布尔索引

让我们考虑一个例子,我们有一些数据在数组中,数组中有重复的名字。 这里我将使用numpy.random中randn函数来生成一些随机正态(normally)分布数据:

In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])

In [99]: data = np.random.randn(7, 4)

In [100]: names
Out[100]:
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='<U4')

In [101]: data
Out[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 ]])

布尔数组必须与它索引数组的轴相同长度。 你甚至可以混合和用索引或整型(或整型序列;稍后介绍)匹配布尔数组。

注意: 如果布尔数组长度不正确,布尔选择不会失败,所以建议小心使用这个特性。

在这些例子中,我选择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]: mask
Out[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不能使用在布尔数组中,使用&和|代替。

使用布尔数组设置值是显而易见的(common-sense, 尝试的)。 要设置data中所有负值为0,我们仅要做:

In [113]: data[data < 0] = 0

In [114]: data
Out[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'] = 7

In [116]: data
Out[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)

Fancy indexing 是NumPy采用的术语来描述使用整型数组索引。 假设我们有一个8x4的数组:

In [117]: arr = np.empty((8, 4))

In [118]: for i in range(8):
.....: arr[i] = i

In [119]: arr
Out[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.]])

为了选出特定顺序行的子集,你可以简化传一个列表或整型的adarray指明想要的顺序:

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]: arr
Out[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)。 无论数组有多少维度(这里只有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]: arr
Out[127]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])

In [128]: arr.T
Out[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]: arr
Out[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]])

对于更高维度的数组,转置将接受轴编号的元组来置换轴(for extra mind bending,这句话没理解):

In [132]: arr = np.arange(16).reshape((2, 2, 4))

In [133]: arr
Out[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]]])

注解

将0轴和1轴交换意思是新的arr[0][1][0]是原来的arr[1][0][0], 新的arr[0][1][1]是原来的arr[1][0][1]。 转置操作有点抽象。

这儿,轴被重排,第二个轴变成第一个轴,第一个轴变成第二个,最后一个轴没有改变。

.T简单转置是交换轴的特殊情况。ndarray有swapaxes方法,它接受一对轴号并切换指示的轴以重新排列数据:

In [135]: arr
Out[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 通用函数:快速逐元素(element-wise)数组函数

通用函数或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])

这些被称为一元ufunc。其他的,例如add或maximum,取两个数组(因此成为二元ufuncs)并返回一个数组作为结果:

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.maximun在x和y之间逐元素计算最大的元素。

虽然(while)不常见,但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接受一个可选的输出参数允许进行数组原位操作:

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])

In [153]: np.sqrt(arr, arr)
Out[153]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])

In [154]: arr
Out[154]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])

见表4-3和4-4 可用ufuncs清单

Table 4-3. Unary ufuncs

_images/Table_4-3__Unary_ufuncs.png _images/Table_4-4_Binary_universal_functions_1.png _images/Table_4-4_Binary_universal_functions_2.png

4.3 面向数组的数组编程

使用NumPy数组使你能够用简洁的数组表达式表达许多数据处理任务而不要写循环。 用数组表达式替换显示循环的实践一般称为向量化。 一般,向量化操作时常比纯Python等效实现快一两个(或更多)数量级,在任何种类的数值计算上影响更大。 在附录A中,我阐述了广播(broadcasting),一种对于向量化计算强大的方法。

举个简单的例子,假设我们希望通过规则的网格值求函数sqrt(x^2 + y^2)值。 np.meshgrid函数接受2个一维数组并产生2个二维矩阵,对应于两个数组中的所有(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>

见表4-3,这里我使用matplotlib中imshow函数从一个函数值的二维数组创建图像。

_images/Figure_4-3_Plot_of_function_evaluated_on_grid.png

将条件逻辑表示为数组运算

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])

假设相应位置的值在条件中为True时我们从xarr取值,否则从yarr取值。 列表推导做这件事可能像这样:

In [168]: result = [(x if c else y)
.....: for x, y, c in zip(xarr, yarr, cond)]

In [169]: result
Out[169]: [1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]

这有多个问题。 首先,大于大型数组这并不快(因为所有工作都是在解释的Python代码中完成的)。 第二,对于多维数组无法工作。用np.where你可以简洁地写这个:

In [170]: result = np.where(cond, xarr, yarr)

In [171]: result
Out[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]: arr
Out[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 > 0
Out[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 2
Out[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. ]])

传递给np.where的数组可以不仅仅是大小相等的数组或标量。

数学和统计方法

一组数学函数,用于计算有关整个数组或关于整个数组沿轴数据的统计信息,可作为数组类的方法访问。 你可以通过调用数组实例方法或使用顶层NumPy函数来使用聚合(aggregation)(通常称为缩减),如sum,mean和std(标准差)。

这儿我随机生成一些标准正态分布数据和计算一些聚合统计信息:

In [177]: arr = np.random.randn(5, 4)

In [178]: arr
Out[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.19607051119998253

In [180]: np.mean(arr)
Out[180]: 0.19607051119998253

In [181]: arr.sum()
Out[181]: 3.9214102239996507

像mean和sum函数有一个可选轴参数,计算沿给定轴统计信息,结果保存在一个更低维度数组中:

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不进行聚合,代替产生一个中间(intermediate)结果的数组:

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]: arr
Out[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]])

注解

沿某个轴进行操作不是切片意义(垂直轴)上对该轴数据进行操作,而是坐标轴意义(平行轴)上对该轴每个数据进行操作。

见表4-5 完全的清单。我们将在后面章节看到许多实际使用这些方法的例子。

_images/Table_4-5_Basic_array_statistical_methods.png

布尔数组方法

在前面的方法中,布尔值被强制为1(True)和0(False)。 因此,sum通常用作计算布尔数组中True值的方法(as a means of):

In [190]: arr = np.random.randn(100)

In [191]: (arr > 0).sum() # Number of positive values
Out[191]: 42

有两个额外的方法,any和all,对布尔数组特别有用。 any测试在数组中是否有一个或多个True,all检查是否每个值都是True:

In [192]: bools = np.array([False, False, True, False])

In [193]: bools.any()
Out[193]: True

In [194]: bools.all()
Out[194]: False

这些方法对非布尔型数组也起作用,非0元素视为True。

排序

像Python内置列表类型,NumPy数组可以使用sort方法原位排序:

In [195]: arr = np.random.randn(6)

In [196]: arr
Out[196]: array([ 0.6095, -0.4938, 1.24 , -0.1357, 1.43 , -0.8469])

In [197]: arr.sort()

In [198]: arr
Out[198]: array([-0.8469, -0.4938, -0.1357, 0.6095, 1.24 , 1.43 ])

你可以原位排序多维数组中的每一维值,通过传递轴编号沿轴排序:

In [199]: arr = np.random.randn(5, 3)

In [200]: arr
Out[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]: arr
Out[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% quantile
Out[205]: -1.5311513550102103

有关使用NumPy的排序方法和更高级技术如间接排序的更多详细信息,见附录A. 几个其它与排序相关(例如通过一列或多列排序数据表)的数据处理操作也能够在pandas中找到。

Unique和其它集合逻辑

NumPy对于一维数组有一些基本的集合操作。 经常使用的一个是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))    # 是对np.unique的很好诠释
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)

表4-6是NumPy集合函数清单

_images/Table_4-6_Array_set_operations.png

4.4 带数组的文件输入输出

NumPy能够以文本或二进制格式从磁盘保存和加载数据。 在这个部分我仅仅讨论NumPy内置二进制格式,因为大多数用户更喜欢选择pandas和其它工具加载文本或表格数据(更多内容见第六章)。

np.save和np.load是有效保存和加载磁盘上数组数据的两个主力(workhorse)函数。 数组默认以未压缩的原始二进制格式文件后缀为.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)

当加载一个.npy文件,返回一个类似于dict对象来懒加载单个数组:

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 线性代数

线性代数像矩阵乘法、分解(decompositions)、行列式(determinants)和其它方阵(square matrix)数学运算是任何数组库的重要组成部分。 不像一些其它语言如MATLAB,两个二维数组用*相乘是逐元素乘积(element-wise product)而不是矩阵点积(dot product)。 因此,对于矩阵乘法有一个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]: x
Out[225]:
array([[ 1., 2., 3.],
[ 4., 5., 6.]])

In [226]: y
Out[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.]])

二维数组和适当大小的一维阵列之间的矩阵乘积(product)产生一维数组:

In [229]: np.dot(x, np.ones(3))
Out[229]: array([ 6., 15.])

@符号(从Python 3.5开始(as of Python 3.5))也可以作为执行矩阵的中缀(**infix**)运算符乘法(multiplication)::

In [230]: x @ np.ones(3)
Out[230]: array([ 6., 15.])

numpy.linalg有一个标准矩阵分解、求逆和行列式集合。 这些是通过相同的行业标准引擎下实现的线性代数库,这个库也用于其他语言如MATLAB和R。这样的库如BLAS,LAPACK或可能(取决于您的NumPy版本)专有的英特尔MKL(数学核心库):

In [231]: from numpy.linalg import inv, qr

In [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]: r
Out[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-7 一些最常用的线性代数函数清单。

_images/Table_4-7_Commonly_used_numpy_linalg_functions_1.png _images/Table_4-7_Commonly_used_numpy_linalg_functions_2.png

4.6 伪随机(pseudorandom)数生成

numpy.rondm模块对于高效从各种概率分布生成整个数组扩充(supplements)了Python内置random函数。 例如,你可以使用normal从标准正态分布采样得到一个4*4数组:

In [238]: samples = np.random.normal(size=(4, 4))

In [239]: samples
Out[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模块仅仅每次采样一个值。 从这个基准测试(benchmark)中可以看出,numpy.random在生成非常大的样本时要快一个数量级(an order of magnitude):

In [240]: from random import normalvariate

In [241]: N = 1000000

In [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)

我们说这是伪随机数是因为它们通过基于随机数生成器种子的一个确定性(deterministic)行为的算法。 你能使用np.random.seed改变NumPy的随机数生成器种子:

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])

见表4-8 numpy.random部分可用函数清单。在下一节中我将举例说明利用(leverage)这些函数一次生成大量样本的能力。

_images/Table_4-8._Partial_list_of_numpy.random_functions.png

4.7 示例:随机游走

随机游走仿真提供一个说明性使用(utilize)数组操作应用。 让我们考虑一个简单的随机游走,从0开始,步长为1和-1,等概率发生。

这儿有一个纯Python方式实现的一个简单随机游走,1000个步长,使用内建的random模块:

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])
_images/Figure_4-4._A_simple_random_walk.png

你可能观察到游走是简单地随机步长累加并且可以使用一个数组表达式产生。因此,我使用np.random模块一次画1000个抛硬币,结果置为1和-1,并且计算累(cumulative)加值:

In [251]: nsteps = 1000

In [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]: -3

In [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被观察到我们就已经知道它是最大值,无须再对后面值进行扫描。

4.8 总结