《利用Python進(jìn)行數(shù)據(jù)分析·第2版》第4章 NumPy基礎(chǔ):數(shù)組和矢量計(jì)算

本文是跟隨學(xué)習(xí),原文鏈接在《利用Python進(jìn)行數(shù)據(jù)分析·第2版》第1章 準(zhǔn)備工作
為避免原鏈接失效,特轉(zhuǎn)載一份保存。中間可能會根據(jù)學(xué)習(xí)情況略有改動。
第1章 準(zhǔn)備工作
第2章 Python語法基礎(chǔ),IPython和Jupyter
第3章 Python的數(shù)據(jù)結(jié)構(gòu)、函數(shù)和文件
第4章 NumPy基礎(chǔ):數(shù)組和矢量計(jì)算
第5章 pandas入門
第6章 數(shù)據(jù)加載、存儲與文件格式
第7章 數(shù)據(jù)清洗和準(zhǔn)備
第8章 數(shù)據(jù)規(guī)整:聚合、合并和重塑
第9章 繪圖和可視化
第10章 數(shù)據(jù)聚合與分組運(yùn)算
第11章 時(shí)間序列
第12章 pandas高級應(yīng)用
第13章 Python建模庫介紹
第14章 數(shù)據(jù)分析案例
附錄A NumPy高級應(yīng)用
附錄B 更多關(guān)于IPython的內(nèi)容(完)

NumPy(Numerical Python的簡稱)是Python數(shù)值計(jì)算最重要的基礎(chǔ)包。大多數(shù)提供科學(xué)計(jì)算的包都是用NumPy的數(shù)組作為構(gòu)建基礎(chǔ)。

NumPy的部分功能如下:

  • ndarray,一個(gè)具有矢量算術(shù)運(yùn)算和復(fù)雜廣播能力的快速且節(jié)省空間的多維數(shù)組。
  • 用于對整組數(shù)據(jù)進(jìn)行快速運(yùn)算的標(biāo)準(zhǔn)數(shù)學(xué)函數(shù)(無需編寫循環(huán))。
  • 用于讀寫磁盤數(shù)據(jù)的工具以及用于操作內(nèi)存映射文件的工具。
  • 線性代數(shù)、隨機(jī)數(shù)生成以及傅里葉變換功能。
  • 用于集成由C、C++、Fortran等語言編寫的代碼的A C API。

由于NumPy提供了一個(gè)簡單易用的C API,因此很容易將數(shù)據(jù)傳遞給由低級語言編寫的外部庫,外部庫也能以NumPy數(shù)組的形式將數(shù)據(jù)返回給Python。這個(gè)功能使Python成為一種包裝C/C++/Fortran歷史代碼庫的選擇,并使被包裝庫擁有一個(gè)動態(tài)的、易用的接口。

NumPy本身并沒有提供多么高級的數(shù)據(jù)分析功能,理解NumPy數(shù)組以及面向數(shù)組的計(jì)算將有助于你更加高效地使用諸如pandas之類的工具。因?yàn)镹umPy是一個(gè)很大的題目,我會在附錄A中介紹更多NumPy高級功能,比如廣播。

對于大部分?jǐn)?shù)據(jù)分析應(yīng)用而言,我最關(guān)注的功能主要集中在:

  • 用于數(shù)據(jù)整理和清理、子集構(gòu)造和過濾、轉(zhuǎn)換等快速的矢量化數(shù)組運(yùn)算。
  • 常用的數(shù)組算法,如排序、唯一化、集合運(yùn)算等。
  • 高效的描述統(tǒng)計(jì)和數(shù)據(jù)聚合/摘要運(yùn)算。
  • 用于異構(gòu)數(shù)據(jù)集的合并/連接運(yùn)算的數(shù)據(jù)對齊和關(guān)系型數(shù)據(jù)運(yùn)算。
  • 將條件邏輯表述為數(shù)組表達(dá)式(而不是帶有if-elif-else分支的循環(huán))。
  • 數(shù)據(jù)的分組運(yùn)算(聚合、轉(zhuǎn)換、函數(shù)應(yīng)用等)。。

雖然NumPy提供了通用的數(shù)值數(shù)據(jù)處理的計(jì)算基礎(chǔ),但大多數(shù)讀者可能還是想將pandas作為統(tǒng)計(jì)和分析工作的基礎(chǔ),尤其是處理表格數(shù)據(jù)時(shí)。pandas還提供了一些NumPy所沒有的領(lǐng)域特定的功能,如時(shí)間序列處理等。

筆記:Python的面向數(shù)組計(jì)算可以追溯到1995年,Jim Hugunin創(chuàng)建了Numeric庫。接下來的10年,許多科學(xué)編程社區(qū)紛紛開始使用Python的數(shù)組編程,但是進(jìn)入21世紀(jì),庫的生態(tài)系統(tǒng)變得碎片化了。2005年,Travis Oliphant從Numeric和Numarray項(xiàng)目整了出了NumPy項(xiàng)目,進(jìn)而所有社區(qū)都集合到了這個(gè)框架下。

NumPy之于數(shù)值計(jì)算特別重要的原因之一,是因?yàn)樗梢愿咝幚泶髷?shù)組的數(shù)據(jù)。這是因?yàn)椋?/p>

  • NumPy是在一個(gè)連續(xù)的內(nèi)存塊中存儲數(shù)據(jù),獨(dú)立于其他Python內(nèi)置對象。NumPy的C語言編寫的算法庫可以操作內(nèi)存,而不必進(jìn)行類型檢查或其它前期工作。比起Python的內(nèi)置序列,NumPy數(shù)組使用的內(nèi)存更少。
  • NumPy可以在整個(gè)數(shù)組上執(zhí)行復(fù)雜的計(jì)算,而不需要Python的for循環(huán)。

要搞明白具體的性能差距,考察一個(gè)包含一百萬整數(shù)的數(shù)組,和一個(gè)等價(jià)的Python列表:

In [7]: import numpy as np

In [8]: my_arr = np.arange(1000000)

In [9]: my_list = list(range(1000000))

各個(gè)序列分別乘以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倍(甚至更快),并且使用的內(nèi)存更少。

4.1 NumPy的ndarray:一種多維數(shù)組對象

NumPy最重要的一個(gè)特點(diǎn)就是其N維數(shù)組對象(即ndarray),該對象是一個(gè)快速而靈活的大數(shù)據(jù)集容器。你可以利用這種數(shù)組對整塊數(shù)據(jù)執(zhí)行一些數(shù)學(xué)運(yùn)算,其語法跟標(biāo)量元素之間的運(yùn)算一樣。

要明白Python是如何利用與標(biāo)量值類似的語法進(jìn)行批次計(jì)算,我先引入NumPy,然后生成一個(gè)包含隨機(jī)數(shù)據(jù)的小數(shù)組:

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

然后進(jìn)行數(shù)學(xué)運(yùn)算:

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

第一個(gè)例子中,所有的元素都乘以10。第二個(gè)例子中,每個(gè)元素都與自身相加。

筆記:在本章及全書中,我會使用標(biāo)準(zhǔn)的NumPy慣用法import numpy as np。你當(dāng)然也可以在代碼中使用from numpy import *,但不建議這么做。numpy的命名空間很大,包含許多函數(shù),其中一些的名字與Python的內(nèi)置函數(shù)重名(比如min和max)。

ndarray是一個(gè)通用的同構(gòu)數(shù)據(jù)多維容器,也就是說,其中的所有元素必須是相同類型的。每個(gè)數(shù)組都有一個(gè)shape(一個(gè)表示各維度大小的元組)和一個(gè)dtype(一個(gè)用于說明數(shù)組數(shù)據(jù)類型的對象):

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

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

本章將會介紹NumPy數(shù)組的基本用法,這對于本書后面各章的理解基本夠用。雖然大多數(shù)數(shù)據(jù)分析工作不需要深入理解NumPy,但是精通面向數(shù)組的編程和思維方式是成為Python科學(xué)計(jì)算牛人的一大關(guān)鍵步驟。

筆記:當(dāng)你在本書中看到“數(shù)組”、“NumPy數(shù)組”、"ndarray"時(shí),基本上都指的是同一樣?xùn)|西,即ndarray對象。

創(chuàng)建ndarray

創(chuàng)建數(shù)組最簡單的辦法就是使用array函數(shù)。它接受一切序列型的對象(包括其他數(shù)組),然后產(chǎn)生一個(gè)新的含有傳入數(shù)據(jù)的NumPy數(shù)組。以一個(gè)列表的轉(zhuǎn)換為例:

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

嵌套序列(比如由一組等長列表組成的列表)將會被轉(zhuǎn)換為一個(gè)多維數(shù)組:

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

因?yàn)閐ata2是列表的列表,NumPy數(shù)組arr2的兩個(gè)維度的shape是從data2引入的。可以用屬性ndim和shape驗(yàn)證:

In [25]: arr2.ndim
Out[25]: 2

In [26]: arr2.shape
Out[26]: (2, 4)

除非特別說明(稍后將會詳細(xì)介紹),np.array會嘗試為新建的這個(gè)數(shù)組推斷出一個(gè)較為合適的數(shù)據(jù)類型。數(shù)據(jù)類型保存在一個(gè)特殊的dtype對象中。比如說,在上面的兩個(gè)例子中,我們有:

In [27]: arr1.dtype
Out[27]: dtype('float64')

In [28]: arr2.dtype
Out[28]: dtype('int64')

除np.array之外,還有一些函數(shù)也可以新建數(shù)組。比如,zeros和ones分別可以創(chuàng)建指定長度或形狀的全0或全1數(shù)組。empty可以創(chuàng)建一個(gè)沒有任何具體值的數(shù)組。要用這些方法創(chuàng)建多維數(shù)組,只需傳入一個(gè)表示形狀的元組即可:

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

注意:認(rèn)為np.empty會返回全0數(shù)組的想法是不安全的。很多情況下(如前所示),它返回的都是一些未初始化的垃圾值。

arange是Python內(nèi)置函數(shù)range的數(shù)組版:

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列出了一些數(shù)組創(chuàng)建函數(shù)。由于NumPy關(guān)注的是數(shù)值計(jì)算,因此,如果沒有特別指定,數(shù)據(jù)類型基本都是float64(浮點(diǎn)數(shù))。

image

ndarray的數(shù)據(jù)類型

dtype(數(shù)據(jù)類型)是一個(gè)特殊的對象,它含有ndarray將一塊內(nèi)存解釋為特定數(shù)據(jù)類型所需的信息:

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靈活交互其它系統(tǒng)的源泉之一。多數(shù)情況下,它們直接映射到相應(yīng)的機(jī)器表示,這使得“讀寫磁盤上的二進(jìn)制數(shù)據(jù)流”以及“集成低級語言代碼(如C、Fortran)”等工作變得更加簡單。數(shù)值型dtype的命名方式相同:一個(gè)類型名(如float或int),后面跟一個(gè)用于表示各元素位長的數(shù)字。標(biāo)準(zhǔn)的雙精度浮點(diǎn)值(即Python中的float對象)需要占用8字節(jié)(即64位)。因此,該類型在NumPy中就記作float64。表4-2列出了NumPy所支持的全部數(shù)據(jù)類型。

筆記:記不住這些NumPy的dtype也沒關(guān)系,新手更是如此。通常只需要知道你所處理的數(shù)據(jù)的大致類型是浮點(diǎn)數(shù)、復(fù)數(shù)、整數(shù)、布爾值、字符串,還是普通的Python對象即可。當(dāng)你需要控制數(shù)據(jù)在內(nèi)存和磁盤中的存儲方式時(shí)(尤其是對大數(shù)據(jù)集),那就得了解如何控制存儲類型。

image
image

你可以通過ndarray的astype方法明確地將一個(gè)數(shù)組從一個(gè)dtype轉(zhuǎn)換成另一個(gè)dtype:

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

在本例中,整數(shù)被轉(zhuǎn)換成了浮點(diǎn)數(shù)。如果將浮點(diǎn)數(shù)轉(zhuǎn)換成整數(shù),則小數(shù)部分將會被截取刪除:

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)

如果某字符串?dāng)?shù)組表示的全是數(shù)字,也可以用astype將其轉(zhuǎn)換為數(shù)值形式:

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_類型時(shí),一定要小心,因?yàn)镹umPy的字符串?dāng)?shù)據(jù)是大小固定的,發(fā)生截取時(shí),不會發(fā)出警告。pandas提供了更多非數(shù)值數(shù)據(jù)的便利的處理方法。

如果轉(zhuǎn)換過程因?yàn)槟撤N原因而失敗了(比如某個(gè)不能被轉(zhuǎn)換為float64的字符串),就會引發(fā)一個(gè)ValueError。這里,我比較懶,寫的是float而不是np.float64;NumPy很聰明,它會將Python類型映射到等價(jià)的dtype上。

數(shù)組的dtype還有另一個(gè)屬性:

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_uint32
Out[50]: 
array([         0, 1075314688,          0, 1075707904,          0,
       1075838976,          0, 1072693248], dtype=uint32)

筆記:調(diào)用astype總會創(chuàng)建一個(gè)新的數(shù)組(一個(gè)數(shù)據(jù)的備份),即使新的dtype與舊的dtype相同。

NumPy數(shù)組的運(yùn)算

數(shù)組很重要,因?yàn)樗鼓悴挥镁帉懷h(huán)即可對數(shù)據(jù)執(zhí)行批量運(yùn)算。NumPy用戶稱其為矢量化(vectorization)。大小相等的數(shù)組之間的任何算術(shù)運(yùn)算都會將運(yùn)算應(yīng)用到元素級:

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

數(shù)組與標(biāo)量的算術(shù)運(yùn)算會將標(biāo)量值傳播到各個(gè)元素:

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

大小相同的數(shù)組之間的比較會生成布爾值數(shù)組:

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)

不同大小的數(shù)組之間的運(yùn)算叫做廣播(broadcasting),將在附錄A中對其進(jìn)行詳細(xì)討論。本書的內(nèi)容不需要對廣播機(jī)制有多深的理解。

基本的索引和切片

NumPy數(shù)組的索引是一個(gè)內(nèi)容豐富的主題,因?yàn)檫x取數(shù)據(jù)子集或單個(gè)元素的方式有很多。一維數(shù)組很簡單。從表面上看,它們跟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])

如上所示,當(dāng)你將一個(gè)標(biāo)量值賦值給一個(gè)切片時(shí)(如arr[5:8]=12),該值會自動傳播(也就說后面將會講到的“廣播”)到整個(gè)選區(qū)。跟列表最重要的區(qū)別在于,數(shù)組切片是原始數(shù)組的視圖。這意味著數(shù)據(jù)不會被復(fù)制,視圖上的任何修改都會直接反映到源數(shù)組上。

作為例子,先創(chuàng)建一個(gè)arr的切片:

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

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

現(xiàn)在,當(dāng)我修改arr_slice中的值,變動也會體現(xiàn)在原始數(shù)組arr中:

In [68]: arr_slice[1] = 12345

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

切片[ : ]會給數(shù)組中的所有值賦值:

In [70]: arr_slice[:] = 64

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

如果你剛開始接觸NumPy,可能會對此感到驚訝(尤其是當(dāng)你曾經(jīng)用過其他熱衷于復(fù)制數(shù)組數(shù)據(jù)的編程語言)。由于NumPy的設(shè)計(jì)目的是處理大數(shù)據(jù),所以你可以想象一下,假如NumPy堅(jiān)持要將數(shù)據(jù)復(fù)制來復(fù)制去的話會產(chǎn)生何等的性能和內(nèi)存問題。

注意:如果你想要得到的是ndarray切片的一份副本而非視圖,就需要明確地進(jìn)行復(fù)制操作,例如arr[5:8].copy()

對于高維度數(shù)組,能做的事情更多。在一個(gè)二維數(shù)組中,各索引位置上的元素不再是標(biāo)量而是一維數(shù)組:

In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

In [73]: arr2d[2]
Out[73]: array([7, 8, 9])

因此,可以對各個(gè)元素進(jìn)行遞歸訪問,但這樣需要做的事情有點(diǎn)多。你可以傳入一個(gè)以逗號隔開的索引列表來選取單個(gè)元素。也就是說,下面兩種方式是等價(jià)的:

In [74]: arr2d[0][2]
Out[74]: 3

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

圖4-1說明了二維數(shù)組的索引方式。軸0作為行,軸1作為列。

image

在多維數(shù)組中,如果省略了后面的索引,則返回對象會是一個(gè)維度低一點(diǎn)的ndarray(它含有高一級維度上的所有數(shù)據(jù))。因此,在2×2×3數(shù)組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]是一個(gè)2×3數(shù)組:

In [78]: arr3d[0]
Out[78]: 
array([[1, 2, 3],
       [4, 5, 6]])

標(biāo)量值和數(shù)組都可以被賦值給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)開頭的那些值(以一維數(shù)組的形式返回):

In [84]: arr3d[1, 0]
Out[84]: array([7, 8, 9])

雖然是用兩步進(jìn)行索引的,表達(dá)式是相同的:

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

注意,在上面所有這些選取數(shù)組子集的例子中,返回的數(shù)組都是視圖。

切片索引

ndarray的切片語法跟Python列表這樣的一維對象差不多:

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

對于之前的二維數(shù)組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軸(即第一個(gè)軸)切片的。也就是說,切片是沿著一個(gè)軸向選取元素的。表達(dá)式arr2d[:2]可以被認(rèn)為是“選取arr2d的前兩行”。

你可以一次傳入多個(gè)切片,就像傳入多個(gè)索引那樣:

In [92]: arr2d[:2, 1:]
Out[92]: 
array([[2, 3],
       [5, 6]])

像這樣進(jìn)行切片時(shí),只能得到相同維數(shù)的數(shù)組視圖。通過將整數(shù)索引和切片混合,可以得到低維度的切片。

例如,我可以選取第二行的前兩列:

In [93]: arr2d[1, :2]
Out[93]: array([4, 5])

相似的,還可以選擇第三列的前兩行:

In [94]: arr2d[:2, 2]
Out[94]: array([3, 6])

圖4-2對此進(jìn)行了說明。注意,“只有冒號”表示選取整個(gè)軸,因此你可以像下面這樣只對高維軸進(jìn)行切片:

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

image

自然,對切片表達(dá)式的賦值操作也會被擴(kuò)散到整個(gè)選區(qū):

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

In [97]: arr2d
Out[97]: 
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])

布爾型索引

<meta charset="utf-8">

來看這樣一個(gè)例子,假設(shè)我們有一個(gè)用于存儲數(shù)據(jù)的數(shù)組以及一個(gè)存儲姓名的數(shù)組(含有重復(fù)項(xiàng))。在這里,我將使用numpy.random中的randn函數(shù)生成一些正態(tài)分布的隨機(jī)數(shù)據(jù):

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

假設(shè)每個(gè)名字都對應(yīng)data數(shù)組中的一行,而我們想要選出對應(yīng)于名字"Bob"的所有行。跟算術(shù)運(yùn)算一樣,數(shù)組的比較運(yùn)算(如==)也是矢量化的。因此,對names和字符串"Bob"的比較運(yùn)算將會產(chǎn)生一個(gè)布爾型數(shù)組:

In [102]: names == 'Bob'
Out[102]: array([ True, False, False,  True, False, False, False], dtype=bool)

這個(gè)布爾型數(shù)組可用于數(shù)組索引:

In [103]: data[names == 'Bob']
Out[103]: 
array([[ 0.0929,  0.2817,  0.769 ,  1.2464],
       [ 1.669 , -0.4386, -0.5397,  0.477 ]])

布爾型數(shù)組的長度必須跟被索引的軸長度一致。此外,還可以將布爾型數(shù)組跟切片、整數(shù)(或整數(shù)序列,稍后將對此進(jìn)行詳細(xì)講解)混合使用:

In [103]: data[names == 'Bob']
Out[103]: 
array([[ 0.0929,  0.2817,  0.769 ,  1.2464],
       [ 1.669 , -0.4386, -0.5397,  0.477 ]])

注意:如果布爾型數(shù)組的長度不對,布爾型選擇就會出錯,因此一定要小心。

下面的例子,我選取了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"以外的其他值,既可以使用不等于符號(!=),也可以通過~對條件進(jìn)行否定:

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

~操作符用來反轉(zhuǎn)條件很好用:

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

選取這三個(gè)名字中的兩個(gè)需要組合應(yīng)用多個(gè)布爾條件,使用&(和)、|(或)之類的布爾算術(shù)運(yùn)算符即可:

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

通過布爾型索引選取數(shù)組中的數(shù)據(jù),將總是創(chuàng)建數(shù)據(jù)的副本,即使返回一模一樣的數(shù)組也是如此。

注意:Python關(guān)鍵字and和or在布爾型數(shù)組中無效。要使用&與|。

通過布爾型數(shù)組設(shè)置值是一種經(jīng)常用到的手段。為了將data中的所有負(fù)值都設(shè)置為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.    ]])

通過一維布爾數(shù)組設(shè)置整行或列的值也很簡單:

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

后面會看到,這類二維數(shù)據(jù)的操作也可以用pandas方便的來做。

花式索引

花式索引(Fancy indexing)是一個(gè)NumPy術(shù)語,它指的是利用整數(shù)數(shù)組進(jìn)行索引。假設(shè)我們有一個(gè)8×4數(shù)組:

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

為了以特定順序選取行子集,只需傳入一個(gè)用于指定順序的整數(shù)列表或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.]])

這段代碼確實(shí)達(dá)到我們的要求了!使用負(fù)數(shù)索引將會從末尾開始選取行:

In [121]: arr[[-3, -5, -7]]
Out[121]: 
array([[ 5.,  5.,  5.,  5.],
       [ 3.,  3.,  3.,  3.],
       [ 1.,  1.,  1.,  1.]])

一次傳入多個(gè)索引數(shù)組會有一點(diǎn)特別。它返回的是一個(gè)一維數(shù)組,其中的元素對應(yīng)各個(gè)索引元組:

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中會詳細(xì)介紹reshape方法。

最終選出的是元素(1,0)、(5,3)、(7,1)和(2,2)。無論數(shù)組是多少維的,花式索引總是一維的。

這個(gè)花式索引的行為可能會跟某些用戶的預(yù)期不一樣(包括我在內(nèi)),選取矩陣的行列子集應(yīng)該是矩形區(qū)域的形式才對。下面是得到該結(jié)果的一個(gè)辦法:

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

記住,花式索引跟切片不一樣,它總是將數(shù)據(jù)復(fù)制到新數(shù)組中。

數(shù)組轉(zhuǎn)置和軸對換

轉(zhuǎn)置是重塑的一種特殊形式,它返回的是源數(shù)據(jù)的視圖(不會進(jìn)行任何復(fù)制操作)。數(shù)組不僅有transpose方法,還有一個(gè)特殊的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]])

在進(jìn)行矩陣計(jì)算時(shí),經(jīng)常需要用到該操作,比如利用np.dot計(jì)算矩陣內(nèi)積:

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

對于高維數(shù)組,transpose需要得到一個(gè)由軸編號組成的元組才能對這些軸進(jìn)行轉(zhuǎn)置(比較費(fèi)腦子):

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

這里,第一個(gè)軸被換成了第二個(gè),第二個(gè)軸被換成了第一個(gè),最后一個(gè)軸不變。

簡單的轉(zhuǎn)置可以使用.T,它其實(shí)就是進(jìn)行軸對換而已。ndarray還有一個(gè)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也是返回源數(shù)據(jù)的視圖(不會進(jìn)行任何復(fù)制操作)。

4.2 通用函數(shù):快速的元素級數(shù)組函數(shù)

通用函數(shù)(即ufunc)是一種對ndarray中的數(shù)據(jù)執(zhí)行元素級運(yùn)算的函數(shù)。你可以將其看做簡單函數(shù)(接受一個(gè)或多個(gè)標(biāo)量值,并產(chǎn)生一個(gè)或多個(gè)標(biāo)量值)的矢量化包裝器。

許多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個(gè)數(shù)組(因此也叫二元(binary)ufunc),并返回一個(gè)結(jié)果數(shù)組:

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計(jì)算了x和y中元素級別最大的元素。

雖然并不常見,但有些ufunc的確可以返回多個(gè)數(shù)組。modf就是一個(gè)例子,它是Python內(nèi)置函數(shù)divmod的矢量化版本,它會返回浮點(diǎn)數(shù)數(shù)組的小數(shù)和整數(shù)部分:

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可以接受一個(gè)out可選參數(shù),這樣就能在數(shù)組原地進(jìn)行操作:

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分別列出了一些一元和二元ufunc。

image
image
image
image
image

4.3 利用數(shù)組進(jìn)行數(shù)據(jù)處理

NumPy數(shù)組使你可以將許多種數(shù)據(jù)處理任務(wù)表述為簡潔的數(shù)組表達(dá)式(否則需要編寫循環(huán))。用數(shù)組表達(dá)式代替循環(huán)的做法,通常被稱為矢量化。一般來說,矢量化數(shù)組運(yùn)算要比等價(jià)的純Python方式快上一兩個(gè)數(shù)量級(甚至更多),尤其是各種數(shù)值計(jì)算。在后面內(nèi)容中(見附錄A)我將介紹廣播,這是一種針對矢量化計(jì)算的強(qiáng)大手段。

作為簡單的例子,假設(shè)我們想要在一組值(網(wǎng)格型)上計(jì)算函數(shù)sqrt(x^2+y^2)。np.meshgrid函數(shù)接受兩個(gè)一維數(shù)組,并產(chǎn)生兩個(gè)二維矩陣(對應(yīng)于兩個(gè)數(shù)組中所有的(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]])

現(xiàn)在,對該函數(shù)的求值運(yùn)算就好辦了,把這兩個(gè)數(shù)組當(dāng)做兩個(gè)浮點(diǎn)數(shù)那樣編寫表達(dá)式即可:

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章的先導(dǎo),我用matplotlib創(chuàng)建了這個(gè)二維數(shù)組的可視化:

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函數(shù)創(chuàng)建的。

image

將條件邏輯表述為數(shù)組運(yùn)算

numpy.where函數(shù)是三元表達(dá)式x if condition else y的矢量化版本。假設(shè)我們有一個(gè)布爾數(shù)組和兩個(gè)值數(shù)組:

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

假設(shè)我們想要根據(jù)cond中的值選取xarr和yarr的值:當(dāng)cond中的值為True時(shí),選取xarr的值,否則從yarr中選取。列表推導(dǎo)式的寫法應(yīng)該如下所示:

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]

這有幾個(gè)問題。第一,它對大數(shù)組的處理速度不是很快(因?yàn)樗泄ぷ鞫际怯杉働ython完成的)。第二,無法用于多維數(shù)組。若使用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的第二個(gè)和第三個(gè)參數(shù)不必是數(shù)組,它們都可以是標(biāo)量值。在數(shù)據(jù)分析工作中,where通常用于根據(jù)另一個(gè)數(shù)組而產(chǎn)生一個(gè)新的數(shù)組。假設(shè)有一個(gè)由隨機(jī)數(shù)據(jù)組成的矩陣,你希望將所有正值替換為2,將所有負(fù)值替換為-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,可以將標(biāo)量和數(shù)組結(jié)合起來。例如,我可用常數(shù)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.    ]])

傳遞給where的數(shù)組大小可以不相等,甚至可以是標(biāo)量值。

數(shù)學(xué)和統(tǒng)計(jì)方法

可以通過數(shù)組上的一組數(shù)學(xué)函數(shù)對整個(gè)數(shù)組或某個(gè)軸向的數(shù)據(jù)進(jìn)行統(tǒng)計(jì)計(jì)算。sum、mean以及標(biāo)準(zhǔn)差std等聚合計(jì)算(aggregation,通常叫做約簡(reduction))既可以當(dāng)做數(shù)組的實(shí)例方法調(diào)用,也可以當(dāng)做頂級NumPy函數(shù)使用。

這里,我生成了一些正態(tài)分布隨機(jī)數(shù)據(jù),然后做了聚類統(tǒng)計(jì):

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這類的函數(shù)可以接受一個(gè)axis選項(xiàng)參數(shù),用于計(jì)算該軸向上的統(tǒng)計(jì)值,最終結(jié)果是一個(gè)少一維的數(shù)組:

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)是“計(jì)算行的平均值”,arr.sum(0)是“計(jì)算每列的和”。

其他如cumsum和cumprod之類的方法則不聚合,而是產(chǎn)生一個(gè)由中間結(jié)果組成的數(shù)組:

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

在多維數(shù)組中,累加函數(shù)(如cumsum)返回的是同樣大小的數(shù)組,但是會根據(jù)每個(gè)低維的切片沿著標(biāo)記軸計(jì)算部分聚類:

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列出了全部的基本數(shù)組統(tǒng)計(jì)方法。后續(xù)章節(jié)中有很多例子都會用到這些方法。

image
image

用于布爾型數(shù)組的方法

在上面這些方法中,布爾值會被強(qiáng)制轉(zhuǎn)換為1(True)和0(False)。因此,sum經(jīng)常被用來對布爾型數(shù)組中的True值計(jì)數(shù):

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

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

另外還有兩個(gè)方法any和all,它們對布爾型數(shù)組非常有用。any用于測試數(shù)組中是否存在一個(gè)或多個(gè)True,而all則檢查數(shù)組中所有值是否都是True:

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

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

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

這兩個(gè)方法也能用于非布爾型數(shù)組,所有非0元素將會被當(dāng)做True。

排序

跟Python內(nèi)置的列表類型一樣,NumPy數(shù)組也可以通過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  ])

多維數(shù)組可以在任何一個(gè)軸向上進(jìn)行排序,只需將軸編號傳給sort即可:

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返回的是數(shù)組的已排序副本,而就地排序則會修改數(shù)組本身。計(jì)算數(shù)組分位數(shù)最簡單的辦法是對其進(jìn)行排序,然后選取特定位置的值:

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

更多關(guān)于NumPy排序方法以及諸如間接排序之類的高級技術(shù),請參閱附錄A。在pandas中還可以找到一些其他跟排序有關(guān)的數(shù)據(jù)操作(比如根據(jù)一列或多列對表格型數(shù)據(jù)進(jìn)行排序)。

唯一化以及其它的集合邏輯

NumPy提供了一些針對一維ndarray的基本集合運(yùn)算。最常用的可能要數(shù)np.unique了,它用于找出數(shù)組中的唯一值并返回已排序的結(jié)果:

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等價(jià)的純Python代碼來對比一下:

In [210]: sorted(set(names))
Out[210]: ['Bob', 'Joe', 'Will']

另一個(gè)函數(shù)np.in1d用于測試一個(gè)數(shù)組中的值在另一個(gè)數(shù)組中的成員資格,返回一個(gè)布爾型數(shù)組:

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中的集合函數(shù)請參見表4-6。

image

4.4 用于數(shù)組的文件輸入輸出

NumPy能夠讀寫磁盤上的文本數(shù)據(jù)或二進(jìn)制數(shù)據(jù)。這一小節(jié)只討論NumPy的內(nèi)置二進(jìn)制格式,因?yàn)楦嗟挠脩魰褂胮andas或其它工具加載文本或表格數(shù)據(jù)(見第6章)。

np.save和np.load是讀寫磁盤數(shù)組數(shù)據(jù)的兩個(gè)主要函數(shù)。默認(rèn)情況下,數(shù)組是以未壓縮的原始二進(jìn)制格式保存在擴(kuò)展名為.npy的文件中的:

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

In [214]: np.save('some_array', arr)

如果文件路徑末尾沒有擴(kuò)展名.npy,則該擴(kuò)展名會被自動加上。然后就可以通過np.load讀取磁盤上的數(shù)組:

In [215]: np.load('some_array.npy')
Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

通過np.savez可以將多個(gè)數(shù)組保存到一個(gè)未壓縮文件中,將數(shù)組以關(guān)鍵字參數(shù)的形式傳入即可:

In [216]: np.savez('array_archive.npz', a=arr, b=arr)

加載.npz文件時(shí),你會得到一個(gè)類似字典的對象,該對象會對各個(gè)數(shù)組進(jìn)行延遲加載:

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

如果要將數(shù)據(jù)壓縮,可以使用numpy.savez_compressed:

In [219]: np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)

4.5 線性代數(shù)

線性代數(shù)(如矩陣乘法、矩陣分解、行列式以及其他方陣數(shù)學(xué)等)是任何數(shù)組庫的重要組成部分。不像某些語言(如MATLAB),通過*對兩個(gè)二維數(shù)組相乘得到的是一個(gè)元素級的積,而不是一個(gè)矩陣點(diǎn)積。因此,NumPy提供了一個(gè)用于矩陣乘法的dot函數(shù)(既是一個(gè)數(shù)組方法也是numpy命名空間中的一個(gè)函數(shù)):

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)等價(jià)于np.dot(x, y):

In [228]: np.dot(x, y)
Out[228]: 
array([[  28.,   64.],
       [  67.,  181.]])

一個(gè)二維數(shù)組跟一個(gè)大小合適的一維數(shù)組的矩陣點(diǎn)積運(yùn)算之后將會得到一個(gè)一維數(shù)組:

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

@符(類似Python 3.5)也可以用作中綴運(yùn)算符,進(jìn)行矩陣乘法:

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

numpy.linalg中有一組標(biāo)準(zhǔn)的矩陣分解運(yùn)算以及諸如求逆和行列式之類的東西。它們跟MATLAB和R等語言所使用的是相同的行業(yè)標(biāo)準(zhǔn)線性代數(shù)庫,如BLAS、LAPACK、Intel MKL(Math Kernel Library,可能有,取決于你的NumPy版本)等:

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

表達(dá)式X.T.dot(X)計(jì)算X和它的轉(zhuǎn)置X.T的點(diǎn)積。

表4-7中列出了一些最常用的線性代數(shù)函數(shù)。

image

4.6 偽隨機(jī)數(shù)生成

numpy.random模塊對Python內(nèi)置的random進(jìn)行了補(bǔ)充,增加了一些用于高效生成多種概率分布的樣本值的函數(shù)。例如,你可以用normal來得到一個(gè)標(biāo)準(zhǔn)正態(tài)分布的4×4樣本數(shù)組:

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內(nèi)置的random模塊則只能一次生成一個(gè)樣本值。從下面的測試結(jié)果中可以看出,如果需要產(chǎn)生大量樣本值,numpy.random快了不止一個(gè)數(shù)量級:

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)

我們說這些都是偽隨機(jī)數(shù),是因?yàn)樗鼈兌际峭ㄟ^算法基于隨機(jī)數(shù)生成器種子,在確定性的條件下生成的。你可以用NumPy的np.random.seed更改隨機(jī)數(shù)生成種子:

In [244]: np.random.seed(1234)

numpy.random的數(shù)據(jù)生成函數(shù)使用了全局的隨機(jī)種子。要避免全局狀態(tài),你可以使用numpy.random.RandomState,創(chuàng)建一個(gè)與其它隔離的隨機(jī)數(shù)生成器:

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中的部分函數(shù)。在下一節(jié)中,我將給出一些利用這些函數(shù)一次性生成大量樣本值的范例。

image
image

4.7 示例:隨機(jī)漫步

我們通過模擬隨機(jī)漫步來說明如何運(yùn)用數(shù)組運(yùn)算。先來看一個(gè)簡單的隨機(jī)漫步的例子:從0開始,步長1和-1出現(xiàn)的概率相等。

下面是一個(gè)通過內(nèi)置的random模塊以純Python的方式實(shí)現(xiàn)1000步的隨機(jī)漫步:

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是根據(jù)前100個(gè)隨機(jī)漫步值生成的折線圖:

In [249]: plt.plot(walk[:100])

image

不難看出,這其實(shí)就是隨機(jī)漫步中各步的累計(jì)和,可以用一個(gè)數(shù)組運(yùn)算來實(shí)現(xiàn)。因此,我用np.random模塊一次性隨機(jī)產(chǎn)生1000個(gè)“擲硬幣”結(jié)果(即兩個(gè)數(shù)中任選一個(gè)),將其分別設(shè)置為1或-1,然后計(jì)算累計(jì)和:

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

有了這些數(shù)據(jù)之后,我們就可以沿著漫步路徑做一些統(tǒng)計(jì)工作了,比如求取最大值和最小值:

In [255]: walk.min()
Out[255]: -3

In [256]: walk.max()
Out[256]: 31

現(xiàn)在來看一個(gè)復(fù)雜點(diǎn)的統(tǒng)計(jì)任務(wù)——首次穿越時(shí)間,即隨機(jī)漫步過程中第一次到達(dá)某個(gè)特定值的時(shí)間。假設(shè)我們想要知道本次隨機(jī)漫步需要多久才能距離初始0點(diǎn)至少10步遠(yuǎn)(任一方向均可)。np.abs(walk)>=10可以得到一個(gè)布爾型數(shù)組,它表示的是距離是否達(dá)到或超過10,而我們想要知道的是第一個(gè)10或-10的索引。可以用argmax來解決這個(gè)問題,它返回的是該布爾型數(shù)組第一個(gè)最大值的索引(True就是最大值):

In [257]: (np.abs(walk) >= 10).argmax()
Out[257]: 37

注意,這里使用argmax并不是很高效,因?yàn)樗鼰o論如何都會對數(shù)組進(jìn)行完全掃描。在本例中,只要發(fā)現(xiàn)了一個(gè)True,那我們就知道它是個(gè)最大值了。

一次模擬多個(gè)隨機(jī)漫步

如果你希望模擬多個(gè)隨機(jī)漫步過程(比如5000個(gè)),只需對上面的代碼做一點(diǎn)點(diǎn)修改即可生成所有的隨機(jī)漫步過程。只要給numpy.random的函數(shù)傳入一個(gè)二元元組就可以產(chǎn)生一個(gè)二維數(shù)組,然后我們就可以一次性計(jì)算5000個(gè)隨機(jī)漫步過程(一行一個(gè))的累計(jì)和了:

In [258]: nwalks = 5000

In [259]: nsteps = 1000

In [260]: draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1

In [261]: steps = np.where(draws > 0, 1, -1)

In [262]: walks = steps.cumsum(1)

In [263]: walks
Out[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]])

現(xiàn)在,我們來計(jì)算所有隨機(jī)漫步過程的最大值和最小值:

In [264]: walks.max()
Out[264]: 138

In [265]: walks.min()
Out[265]: -133

得到這些數(shù)據(jù)之后,我們來計(jì)算30或-30的最小穿越時(shí)間。這里稍微復(fù)雜些,因?yàn)椴皇?000個(gè)過程都到達(dá)了30。我們可以用any方法來對此進(jìn)行檢查:

In [266]: hits30 = (np.abs(walks) >= 30).any(1)

In [267]: hits30
Out[267]: array([False,  True, False, ..., False,  True, False], dtype=bool)

In [268]: hits30.sum() # Number that hit 30 or -30
Out[268]: 3410

然后我們利用這個(gè)布爾型數(shù)組選出那些穿越了30(絕對值)的隨機(jī)漫步(行),并調(diào)用argmax在軸1上獲取穿越時(shí)間:

In [269]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)

In [270]: crossing_times.mean()
Out[270]: 498.88973607038122

請嘗試用其他分布方式得到漫步數(shù)據(jù)。只需使用不同的隨機(jī)數(shù)生成函數(shù)即可,如normal用于生成指定均值和標(biāo)準(zhǔn)差的正態(tài)分布數(shù)據(jù):

In [271]: steps = np.random.normal(loc=0, scale=0.25,
   .....:                          size=(nwalks, nsteps))

4.8 結(jié)論

雖然本書剩下的章節(jié)大部分是用pandas規(guī)整數(shù)據(jù),我們還是會用到相似的基于數(shù)組的計(jì)算。在附錄A中,我們會深入挖掘NumPy的特點(diǎn),進(jìn)一步學(xué)習(xí)數(shù)組的技巧。

作者:SeanCheney
鏈接:http://www.lxweimin.com/p/a380222a3292
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評論 2 375