NumPy

NumPy是Python中进行科学计算的基础包,它提供了一个多维的数组对象,这比Python基本的列表数据结构强大的多,以及可以对其执行各种科学计算的方法,包括矩阵、逻辑、排序、线性代数、基本统计等等运算。

NumPy已经成为Python中处理数值数据的通用标准,几乎用于科学和工程的每个领域。NumPy的数据结构和API广泛应用于科学Python包。

安装NumPy并导入

安装:

1
$ pip install numpy

导入:

1
import numpy as np # 简写为np比较方便

n维数组

NumPy中多维数组对象由ndarray类提供,它描述了相同类型元素的集合,即数组中每个元素占用的内存大小是相同的,数据类型由dtype属性定义。数组通常是固定大小的容器,数组中的维数和项数由其形状决定,数组的形状是一个指定了每个维度的大小非负整数元组,由shape属性定义。

在NumPy中,维度称为,例如下面这个二维数组:

[[0, 0, 0], 
 [1, 1, 1]]

这个数组具有2个轴,第一个轴的长度大小为2,第二个轴的长度大小为3,形状可以用一个元组表示为:(2, 3)

创建数组

要创建ndarray数组对象,可以使用np.array()函数。

可以将一个列表或元组传递给这个函数,返回一个转换后的数组对象:

1
2
3
4
5
6
7
8
9
10
11
12
>>> np.array((1,2,3))
array([1, 2, 3])

>>> np.array([[0,0,0],[1,1,1]])
array([[0, 0, 0],
[1, 1, 1]])

>>> a = np.array([[1,2], [3,4]])
>>> a.dtype
dtype('int64')
>>> a.shape
(2, 2)

除此之外,还有很多高级函数可以用来创建数组对象,下面简单介绍两个。

指定创建一个内容是给定区间[start, stop)内均匀分布的值的一维数组:

1
2
>>> np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

np.arange()函数的区间start默认为0,步长默认为1,可以自定义:

1
2
>>> np.arange(1, 5, step=0.5)
array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])

np.linspace函数可以生成在给定区间[start, stop]内,均匀分布的若干个样本值,num默认为50:

1
2
>>> np.linspace(0, 10, num=3)
array([ 0., 5., 10.])

NumPy的数组对象中默认数据类型是浮点型(np.float64),但创建数组时可以通过dtype指定数据类型:

1
2
>>> np.linspace(0, 10, num=3, dtype=np.int64)
array([ 0, 5, 10], dtype=int64)

NumPy数据类型有一套自己的定义,与C语言的类型紧密联系,可以参考:

basics.types

索引和切片

数组对象中的元素可以通过索引和切片来访问。

正如Python列表可以通过形如list[index]、list[m:n]的方式进行索引和切片一样,ndarray也可以通过这样的方式进行索引和切片。

比如,有这样一个一维数组:

1
>>> a = np.array([1, 2, 3, 4, 5, 6])

访问数字6:

1
2
>>> a[5]
6

访问数字3及后面的数字:

1
2
>>> a[2:]
array([3, 4, 5, 6])

NumPy扩展了[]语法的能力,使得支持多维数组的索引和切片。比如有这样一个二维数组:

1
2
3
4
5
>>> b = np.array([[1,2,3], [4,5,6], [7,8,9]])
>>> b
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

把它当成一个矩阵,如果想访问第二行第三列的元素:

1
2
>>> b[1,2]
6

访问第二行所有元素:

1
2
>>> b[1,:]
array([4, 5, 6])

不止如此,NmuPy还扩展出了高级索引的能力,功能非常强大。

比如有这样一个数组:

1
2
3
>>> x = np.arange(10, 0, -1)
>>> x
array([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])

可以通过一个整数数组去索引访问x,这样可以一次性返回多个元素:

1
2
>>> x[np.array([3,4,9])]
array([7, 6, 1])

还可以通过布尔数组去索引访问x:

1
2
>>> x[x>6]
array([10, 9, 8, 7])

这是因为通过比较运算符,数组对象的逻辑运算会返回一个相同形状的布尔数组:

1
2
>>> x>6
array([ True, True, True, True, False, False, False, False, False, False])

然后,根据布尔数组中为True的元素,对应位置上的数组x的元素就会被索引到。

基本数组操作

数组对象之间可以进行加、减、乘、除等运算。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> a = np.array([1,2])
>>> b = np.array([3,4])

>>> a + b
array([4, 6])

>>> a - b
array([-2, -2])

>>> a * b
array([3, 8])

>>> a / b
array([0.33333333, 0.5 ])

数组还可以与单个数字之间进行运算,这在NumPy中被称为向量与标量的运算:

1
2
3
4
5
>>> a + 1
array([2, 3])

>>> a * 3
array([3, 6])

广播

上面数组对象之间运算,是发生在两个形状相同的数组之间的。如果想要两个不同形状的数组之间进行运算,需要用到NumPy的广播概念,实际上,上面的数组与一个数字进行运算,正是用到了广播。

NumPy认为对数组的运算操作应该发生在数组中的每一个元素上,即每一个元素都需要有另一个数组上对应位置上的元素与它进行运算,这个概念叫做广播。广播是一种允许NumPy对不同形状的数组进行操作的机制,但是,数组的维度必须兼容,即两个数组从最小(最右)的维度开始比较大小,要么相等要么其中一个为1,否则报错。

例如,下面这个形状为(2, 2)的数组a:

1
2
3
4
5
6
7
8
>>> a = np.array([[1,2], [3,4]])
>>> a * np.array([[3], [2]])
array([[3, 6],
[6, 8]])

>>> a * np.array([3,6])
array([[ 3, 12],
[ 9, 24]])

(2, 2)和(2, 1)形状的数组相乘,通过广播,相当于进行了下面这样的一个运算:

1
2
[1, 2] * [3] = [1, 2] * [3, 3] = [3, 6]
[3, 4] [2] [3, 4] [2, 2] [6, 8]

可以理解为(2, 1)的数组被横向拉伸为了(2, 2)的数组。另外,可以自己想象一下下面那个形状为(2,)的一维数组是如何广播到与(2, 2)形状的二维数组相乘的。

而这个例子就会报错:

1
2
>>> a * np.array([[3], [2], [1]])
ValueError: operands could not be broadcast together with shapes (2,2) (3,1)

这是广播的一般规则,广播还有一个简单的规则,也就是上面提到的一个数组与一个数字相乘,这是最简单的一个广播。一个数字标量可以看成一个形状为(1,)的数组,可以被拉伸到任何形状与其他数组进行运算。

数组方法

NumPy还提供了很多便利的函数对数组进行操作,下面进行一些简单的介绍:

求和

1
2
3
>>> a = np.array([1,2,3,4])
>>> a.sum()
10

不仅可以求全部元素的和,还可以按指定维度进行求和:

1
2
3
4
5
6
7
8
9
>>> a = np.array([[1,2,3], [4,5,6]])
>>> a.sum(axis=0)
array([5, 7, 9])

>>> a.sum(axis=1)
array([6, 15])

>>> a.sum()
21

排序

1
2
3
>>> a = np.array([9,8,7,6,5,4,3,2,1])
>>> np.sort(a)
array([1, 2, 3, 4, 5, 6, 7, 8, 9])

求平均值和最值

1
2
3
4
5
6
7
8
9
>>> a = np.array([[1,2,3], [4,5,6]])
>>> a.max()
6

>>> a.min()
1

>>> a.mean()
3.5

同求和一样,也可以指定维度进行运算:

1
2
3
4
5
6
7
8
>>> a.max(axis=1)
array([3, 6])

>>> a.min(axis=1)
array([1, 4])

>>> a.mean(axis=1)
array([2., 5.])

NumPy的数学

前面提到过,NumPy是Python中科学计算的基础,使用非常广泛。这是因为NumPy提供了很多适用于数组的简便方法,使得其实现数学公式的计算非常容易。比如下面这些:

均方误差公式

求方差在数学计算中非常普遍,在NumPy中,实现这个公式简单明了:

1
var = (1/n) * np.sum(np.square(y2 - y1))

简单演示一下:

1
2
3
4
5
6
7
8
9
10
>>> y1 = np.arange(6)
>>> y1
array([0, 1, 2, 3, 4, 5])

>>> y2 = y1 + np.array([-0.1, 0.2, -0.8, 1, -0.1, 0])
>>> y2
array([-0.1, 1.2, 1.2, 4. , 3.9, 5. ])

>>> (1/6) * np.sum(np.square(y2 - y1))
0.2833333333333333

其中,y1相当于函数y=xy2相当于一些有误差的样本数据,最后的值就是两者之间的方差。

Pandas

Pandas是Python的核心数据分析支持库,提供了快速、灵活、明确的数据结构,旨在简单、直观地处理关系型、标记型数据。

Pandas是基于NumPy开发的,可以与其它第三方科学计算支持库完美集成。Pandas适合处理以下类型的数据:

  • 与SQL或者Excel表类似的,含异构(不同数据类型)列的表格数据。
  • 有序或无序(非固定频率)的时间序列数据。
  • 带行列标签的矩阵数据,包括同构或异构型数据。
  • 任意其它形式的观测、统计数据集。

安装和导入Pandas

安装:

1
$ pip install pandas

导入:

1
2
import numpy as np # 一般连同numpy一起导入
import pandas as pd

数据结构

与NumPy的n维数组结构不同,Pandas的主要数据结构就是Series(一维数据)和DataFrame(二维数据),因为这两种数据结构已经足以处理金融、统计、社会科学、工程等领域内的大部分典型数据了。

Series

Series是带标签的一维数组,可以存储整数、浮点数、字符串、Python对象等类型的数据。Series的标签被称为索引(index),一个标签对应一个数据。

调用pd.Series函数即可创建Series:

1
>>> s = pd.Series(data, index=index)

data参数支持以下几种方式传入数据:

  • Python字典:
1
2
3
4
5
6
7
>>> d = {'b': 1, 'a': 0, 'c': 2}
>>> s = pd.Series(d)
>>> s
b 1
a 0
c 2
dtype: int64

如果以字典的方式传入数据,并且index参数缺省,则默认会以字典中相应的key作为数据的标签,可以通过s.index查看Series对象的标签:

1
2
>>> s.index
Index(['b', 'a', 'c'], dtype='object')

如果设置了index参数,则会把index参数当作key,去提取字典中的值:

1
2
3
4
5
6
7
8
>>> d = {'b': 1, 'a': 0, 'c': 2}
>>> s = pd.Series(d, index=['b', 'c', 'd', 'a'])
>>> s
b 1.0
c 2.0
d NaN
a 0.0
dtype: float64

由于字典中没有d这个key,所以Series中d标签对应的数据为NaN注意,Pandas中用NaN(Not a Number)表示数据缺失。

  • numpy对象:

data参数是一个numpy对象时,标签长度必须与numpy对象的长度一致:

1
2
3
4
5
6
7
8
>>> s = pd.Series(np.random.randn(5), index=['a', 'b', 'c', 'd', 'e'])
>>> s
a 0.085665
b 0.339841
c -1.257418
d 0.706560
e 1.096948
dtype: float64

如果没有指定index参数,则默认会创建一个整数索引:

1
2
3
4
5
6
7
8
9
10
>>> s = pd.Series(np.random.randn(5))
>>> s
0 -0.094684
1 0.528470
2 0.202291
3 0.919904
4 -0.309003
dtype: float64
>>> s.index
RangeIndex(start=0, stop=5, step=1)
  • 列表:
1
2
3
4
5
6
7
8
9
>>> s = pd.Series([1, 3, 5, np.nan, 6, 8])
>>> s
0 1.0
1 3.0
2 5.0
3 NaN
4 6.0
5 8.0
dtype: float64
  • 标量值:

如果是标量值的话,必须提供索引,Pandas会自动根据索引长度重复该标量值:

1
2
3
4
5
6
7
8
>>> s = pd.Series(5, index=['a', 'b', 'c', 'd', 'e'])
>>> s
a 5
b 5
c 5
d 5
e 5
dtype: int64

Series的操作与NumPy的ndarrary对象类似,支持大多数NumPy函数,还可以进行各种花式索引和切片:

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
>>> s = pd.Series(np.random.randn(5))
>>> s
0 -0.713951
1 1.830373
2 -0.481400
3 -2.423198
4 0.556725
dtype: float64
# 索引
>>> s[0]
-0.7139514490641597
# 切片
>>> s[:3]
0 -0.713951
1 1.830373
2 -0.481400
dtype: float64
# 布尔索引,返回大于中位数的数据
>>> s[s > s.median()]
1 1.830373
4 0.556725
dtype: float64
# 列表索引
>>> s[[4, 3, 1]]
4 0.556725
3 -2.423198
1 1.830373
dtype: float64

DataFrame

DataFrame是由多种类型的列构成的二维标签数据结构,类似于Excel、SQL表。DataFrame是最常用的Pandas对象。

DataFrame的行标签被称为index,列标签被称为columns

调用pd.DataFrame()函数即可创建DataFrame对象:

1
>>> d = pd.DataFrame(data, index=index, columns=columns)
  • 使用numpy二维数组创建DataFrame对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> dates = pd.date_range('20230101', periods=6)
>>> dates
DatetimeIndex(['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04',
'2023-01-05', '2023-01-06'],
dtype='datetime64[ns]', freq='D')

>>> df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
>>> df
A B C D
2023-01-01 -0.724145 0.934182 -0.606031 -0.381919
2023-01-02 -0.763000 0.906290 0.571081 -1.462929
2023-01-03 0.709972 1.604800 -0.156911 0.680411
2023-01-04 -0.214652 -0.415641 0.173664 0.181625
2023-01-05 -0.191492 1.166249 0.748061 -0.868161
2023-01-06 -0.528732 -0.870868 1.056890 -1.509378

先使用pd.date_range()函数生成一个时间序列作为DataFrame的行标签,再调用pd.DataFrame()创建DataFrame对象,并传入随机的二维数组数据和列标签参数。可以查看DataFrame对象的index和columns属性:

1
2
3
4
5
6
>>> df.index
DatetimeIndex(['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04',
'2023-01-05', '2023-01-06'],
dtype='datetime64[ns]', freq='D')
>>> df.columns
Index(['A', 'B', 'C', 'D'], dtype='object')
  • 使用字典创建DataFrame对象:
1
2
3
4
5
6
7
>>> data = {'AAA':[1,2,3], 'BBB':['a','b','c']}
>>> df = pd.DataFrame.from_dict(data)
>>> df
AAA BBB
0 1 a
1 2 b
2 3 c

Pandas默认是将字典的键设为列标签,可以通过设置orient参数将字典的键设为行标签:

1
2
3
4
5
>>> df = pd.DataFrame.from_dict(data, orient='index')
>>> df
0 1 2
AAA 1 2 3
BBB a b c

DataFrame常用操作

提取、添加、删除列

DataFram可以看成是由一个个Series对象组成的,一列就是一个Series对象。提取、添加、删除DataFrame的列与操作字典类似:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
>>> df
A B C D
2023-01-01 0.427900 -1.317100 -0.348818 -2.344505
2023-01-02 0.404621 -1.270579 2.182677 -0.334435
2023-01-03 0.943640 -1.205705 0.038978 1.774918
2023-01-04 -0.062885 0.207681 -0.756341 -0.267351
2023-01-05 1.522414 -1.371567 1.605561 0.264726
2023-01-06 -0.345359 -0.771864 0.391735 -1.446381

# 提取列
>>> df['A']
2023-01-01 0.427900
2023-01-02 0.404621
2023-01-03 0.943640
2023-01-04 -0.062885
2023-01-05 1.522414
2023-01-06 -0.345359
Freq: D, Name: A, dtype: float64
# 列的对象类型就是Series类型
>>> type(df['A'])
<class 'pandas.core.series.Series'>
# 除了字典方式,还可以通过DataFrame的属性直接提取列,前提是要列标签为字符串类型
>>> df.A
2023-01-01 0.427900
2023-01-02 0.404621
2023-01-03 0.943640
2023-01-04 -0.062885
2023-01-05 1.522414
2023-01-06 -0.345359
Freq: D, Name: A, dtype: float64

# 添加列,用A列的值加上B列的值生成E列
>>> df['E'] = df['A'] + df['B']
>>> df
A B C D E
2023-01-01 0.427900 -1.317100 -0.348818 -2.344505 -0.889200
2023-01-02 0.404621 -1.270579 2.182677 -0.334435 -0.865958
2023-01-03 0.943640 -1.205705 0.038978 1.774918 -0.262065
2023-01-04 -0.062885 0.207681 -0.756341 -0.267351 0.144796
2023-01-05 1.522414 -1.371567 1.605561 0.264726 0.150847
2023-01-06 -0.345359 -0.771864 0.391735 -1.446381 -1.117223

# 删除列
>>> del df['E']
>>> p = df.pop('D')
>>> df
A B C
2023-01-01 0.427900 -1.317100 -0.348818
2023-01-02 0.404621 -1.270579 2.182677
2023-01-03 0.943640 -1.205705 0.038978
2023-01-04 -0.062885 0.207681 -0.756341
2023-01-05 1.522414 -1.371567 1.605561
2023-01-06 -0.345359 -0.771864 0.391735
# 可以同时指定多个列标签来删除列,drop()函数返回的是删除后的结果,对原对象无副作用
>>> df.drop(columns=['B', 'C'])
A
2023-01-01 0.427900
2023-01-02 0.404621
2023-01-03 0.943640
2023-01-04 -0.062885
2023-01-05 1.522414
2023-01-06 -0.345359

索引

可以通过行或者列两个维度去对DataFrame中的数据进行索引,有多种方式可以进行。

  • 用列标签选择列

通过df['col']或者df.col进行,这在上面演示过,返回的是一个Series对象。

  • 用行标签对行进行选择和切片

通过df.loc['label']进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> df
A B C D
2023-01-01 -1.465884 -2.307865 1.422948 -0.568402
2023-01-02 0.623038 0.212868 -1.230473 1.132214
2023-01-03 1.147305 -0.790069 0.470447 2.206363
2023-01-04 0.186357 -0.255402 -1.256893 0.200379
2023-01-05 0.462334 0.326236 0.592972 -0.261374
2023-01-06 0.518177 -0.080145 -1.326405 1.150453
>>> df.loc['2023-01-01']
A -1.465884
B -2.307865
C 1.422948
D -0.568402
Name: 2023-01-01 00:00:00, dtype: float64
>>> df.loc['2023-01-01':'2023-01-03']
A B C D
2023-01-01 -1.465884 -2.307865 1.422948 -0.568402
2023-01-02 0.623038 0.212868 -1.230473 1.132214
2023-01-03 1.147305 -0.790069 0.470447 2.206363
  • 用整数位置对行进行选择和切片

通过df.iloc[loc]进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> df.iloc[0]
A -1.465884
B -2.307865
C 1.422948
D -0.568402
Name: 2023-01-01 00:00:00, dtype: float64
>>> df.iloc[0:3]
A B C D
2023-01-01 -1.465884 -2.307865 1.422948 -0.568402
2023-01-02 0.623038 0.212868 -1.230473 1.132214
2023-01-03 1.147305 -0.790069 0.470447 2.206363
# 或者
>>> df[0:3]
A B C D
2023-01-01 -1.465884 -2.307865 1.422948 -0.568402
2023-01-02 0.623038 0.212868 -1.230473 1.132214
2023-01-03 1.147305 -0.790069 0.470447 2.206363

仅选择一行返回的是一个Series对象,通过切片选择多行就返回一个DataFrame对象。

运算

DataFrame对象可以进行多种运算,包括转置和各种聚合。

  • 转置

DataFrame对象的T属性可以转置DataFrame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> df
A B C D
2023-01-01 -1.465884 -2.307865 1.422948 -0.568402
2023-01-02 0.623038 0.212868 -1.230473 1.132214
2023-01-03 1.147305 -0.790069 0.470447 2.206363
2023-01-04 0.186357 -0.255402 -1.256893 0.200379
2023-01-05 0.462334 0.326236 0.592972 -0.261374
2023-01-06 0.518177 -0.080145 -1.326405 1.150453
>>> df.T
2023-01-01 2023-01-02 2023-01-03 2023-01-04 2023-01-05 2023-01-06
A -1.465884 0.623038 1.147305 0.186357 0.462334 0.518177
B -2.307865 0.212868 -0.790069 -0.255402 0.326236 -0.080145
C 1.422948 -1.230473 0.470447 -1.256893 0.592972 -1.326405
D -0.568402 1.132214 2.206363 0.200379 -0.261374 1.150453
  • 合并

Pandas对DataFrame可以进行SQL风格的合并,即join:

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
>>> ldf = pd.DataFrame({'key': ['foo', 'yoo'], 'lval': [1, 2]})
>>> ldf
key lval
0 foo 1
1 yoo 2
>>> rdf = pd.DataFrame({'key': ['foo', 'zoo'], 'rval': [4, 5]})
>>> rdf
key rval
0 foo 4
1 zoo 5

# 等值连接
>>> pd.merge(ldf, rdf, on='key')
key lval rval
0 foo 1 4

# 左连接
>>> pd.merge(ldf, rdf, on='key', how='left')
key lval rval
0 foo 1 4.0
1 yoo 2 NaN

# 右连接
>>> pd.merge(ldf, rdf, on='key', how='right')
key lval rval
0 foo 1.0 4
1 zoo NaN 5
  • 分组

通过df.groupby()函数进行分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> df = pd.DataFrame({'A':['x','y','z','y','z','x'], 'B':np.random.randn(6), 'C':np.random.randn(6)})
>>> df
A B C
0 x -0.956121 -1.465992
1 y 0.387068 0.140719
2 z -0.711084 -1.684878
3 y -0.000911 1.797802
4 z -0.043587 0.594897
5 x -0.118352 0.136071

# 先分组,再通过sum()函数计算每组的总和
>>> df.groupby('A').sum()
B C
A
x -1.074473 -1.329921
y 0.386157 1.938521
z -0.754671 -1.089981

操作CSV

Pandas对CSV格式的数据提供了很便利的支持,SeriesDataFrame对象都有个to_csv()的方法,可以将对象的值写入到一个csv文件中:

1
2
3
4
5
6
7
8
9
>>> df
A B C
0 x 0.101737 0.133423
1 y 0.149723 -0.082645
2 z 1.973025 0.651403
3 y 0.934236 0.107589
4 z -0.653397 -1.817398
5 x -0.039525 -0.654297
>>> df.to_csv('test.csv', index_label='index')

生成的csv文件:

1
2
3
4
5
6
7
index,A,B,C
0,x,0.10173694674988926,0.1334227832707775
1,y,0.14972291881982086,-0.08264462105273167
2,z,1.973025056752442,0.6514030299492489
3,y,0.9342361944456802,0.10758879368220652
4,z,-0.6533969543321673,-1.817398232489989
5,x,-0.03952526261690382,-0.6542973020515079

反过来还可以通过pd.read_csv()函数读取一个csv文件生成DataFrame对象:

1
2
3
4
5
6
7
8
9
>>> pd.read_csv('test.csv', index_col=0)
A B C
index
0 x 0.101737 0.133423
1 y 0.149723 -0.082645
2 z 1.973025 0.651403
3 y 0.934236 0.107589
4 z -0.653397 -1.817398
5 x -0.039525 -0.654297