Python 数据科学入门#

Python 中提供了十分完善的数据科学生态,在本节中,我们将简单介绍 Python 中使用最为广泛的数据处理与分析包 NumPy 以及 pandas。其中 NumPy 主要用于数据计算,pandas 主要用于数据的筛选和拼接等数据处理操作。

在开始本节教程之前,推荐先阅读:Python 编程语言入门

NumPy 入门教程#

NumPy 是 Python 中数据科学计算的一个基础包。它提供了多维数组对象以及用于数组快速运算的各种方法,例如数学运算、形状操作、基本统计分析、随机模拟等等。NumPy 几乎是整个 Python 数据科学生态的基础与核心,被广泛应用于包括机器学习在内的各个领域。

如果你想更全面的了解 NumPy 的使用方法以及实现原理等内容,可以参考:NumPy documentation

什么是数组#

NumPy 的核心为 ndarray 对象,它是一个 n 维数组,其中有序地存储着具有相同数据类型的数据,我们接下来将简称之为“数组”。

NumPy 的数组和 Python 中的 list 对象有一定的相似之处,但是有如下不同点:

  • 数组的大小是固定的,无法动态扩增。

  • 数组中的元素必须具有相同的数据类型。

  • 数组支持更多的高级数学运算和其他操作。

以上的特点使得数组操作的执行效率更高、灵活性更强、代码更为简洁。

导入 NumPy#

和导入其他第三方模块一样,我们使用 import 导入 NumPy。

import numpy as np

创建数组#

NumPy 提供了许多方式来创建数组,接下来我们将讲解最为常用的几种。

从 list 创建数组#

使用 np.array() 方法可以将一个 list 类型的对象转为数组,这也是最常用的数组创建方法。

a = np.array([1, 2, 3, 4])
print(a)  # 一维数组
[1 2 3 4]

小技巧

NumPy 数组与 R 中的 vector() 类似。即 a = np.array([1, 2, 3, 4]) 类似于 R 中 a = c(1, 2, 3, 4)

b = np.array([[5, 6], [7, 8]])
print(b)  # 二维数组
[[5 6]
 [7 8]]

备注

二维数组即矩阵。

创建指定范围的数组#

使用 np.arange() 或者 np.linspace() 可以创建指定范围的数组,在构建群体模型模拟数据时十分常用。可以通过以下几个例子观察两个方法之间的差异,你可以阅读它们的文档字符串获取更多信息。

# arange 指定的是步长,且为前闭后开区间

a = np.arange(0, 30, 5)
print(a)
[ 0  5 10 15 20 25]
b = np.arange(0, 2, 0.3)
print(b)
[0.  0.3 0.6 0.9 1.2 1.5 1.8]
# linspace 指定的是个数

c = np.linspace(0, 30, 5)
print(c)
[ 0.   7.5 15.  22.5 30. ]

小技巧

如果你熟悉 R 语言的话,np.arange() 与 R 中的 seq() 有一定的相似之处。

创建具有相同数据的数组#

使用 np.zeros()np.ones()np.full() 可以快速创建指定形状的具有相同数据的数组。

# 创建一个长度为 5,数组值均为 0 的数组

a = np.zeros(5)
print(a)
[0. 0. 0. 0. 0.]
# 创建一个长度为 10,数组值均为 1 的数组

b = np.ones(10)
print(b)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
# 创建一个大小为 3*3,数组值均为 3.14 的二维数组

c = np.full((3, 3), 3.14)
print(c)
[[3.14 3.14 3.14]
 [3.14 3.14 3.14]
 [3.14 3.14 3.14]]

小技巧

同样的,如果你熟悉 R 语言的话,这些函数与 R 中的 rep() 类似。

创建随机数数组#

使用 np.random 下的函数可以快速创建含有随机数的数组。

# 设置随机种

np.random.seed(1234)
# 创建一个由在 [0~1) 内均匀分布的随机数组成的长度为 5 的数组

a = np.random.random(5)
print(a)
[0.19151945 0.62210877 0.43772774 0.78535858 0.77997581]
# 创建一个由符合标准正态分布的随机数组成的长度为 10 的数组

b = np.random.normal(size=10)
print(b)

## 创建一个符合均值为 10,标准差为 3 的正态分布的长度为 10 的数组

c = np.random.normal(loc=10, scale=3, size=10)
print(c)
[-0.94029827 -0.95658428 -0.33060682  0.87412791  2.00254961  0.01086208
 -0.86924706  1.4249841   0.1458091   2.89409095]
[ 9.08721945 12.58498301  7.93021999 10.56249211 11.81292623  9.45095733
  6.6204926  14.97661852  8.01867576 13.12325791]
# 创建一个由在 [0~100) 内均匀分布的随机整数组成的长度为 10 的数组

d = np.random.randint(0, 100, 10)
print(d)
[71 60 46 28 81 87 13 96 12 69]

小技巧

这些函数也和 R 中的 runif()rnorm() 有共通之处。

数组的运算#

基本算术与逻辑运算#

在 NumPy 中,支持对数组的基本算术运算和逻辑运算,例如加减乘除、大于小于逻辑比较等。这些运算是逐元素的,可以观察下方的例子:

a = np.array([1, 2, 3, 4])
print("a + 2:", a + 2)
print("a * 1.5:", a * 1.5)
print("a < 3:", a < 3)
a + 2: [3 4 5 6]
a * 1.5: [1.5 3.  4.5 6. ]
a < 3: [ True  True False False]

除了与数字进行运算外,数组和数组之间也可以进行运算,参考下方的例子:

a = np.array([1, 2, 3, 4])
b = np.full(4, 4)
print("a + b:", a + b)
print("a * b:", a * b)
a + b: [5 6 7 8]
a * b: [ 4  8 12 16]

思考一个问题:如果上述例子中的 ab 不等长的话,对两者进行运算会发生什么?

a = np.array([1, 2, 3, 4])
b = np.array([4])
print("a + b:", a + b)
a + b: [5 6 7 8]

观察上面的例子,我们可以发现即使两个数组不等长也可以执行运算——数组 a 的每个元素都被加上了 4。

这一运算能实现的原因是因为 NumPy 会对较短的数组进行广播(broadcast),其原理可以参考 图 546

../_images/pre-numpy-broadcast.png

图 546 广播机制原理示意图#

数学函数#

NumPy 也支持对数组进行一些常用的数学函数运算,例如 sin、cos 等。这些函数被称为 “universal functions”(ufunc)。这些函数也是逐元素操作数组的。

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

print("a:", a)
print("sin(a):", np.sin(a))
print("cos(a):", np.cos(a))
print("a^0.5:", np.sqrt(a))
print("3^a:", np.power(3, a))
print("e^a:", np.exp(a))
print("ln(a):", np.log(a))
print("log10(a):", np.log10(a))
a: [1 2 3 4]
sin(a): [ 0.84147098  0.90929743  0.14112001 -0.7568025 ]
cos(a): [ 0.54030231 -0.41614684 -0.9899925  -0.65364362]
a^0.5: [1.         1.41421356 1.73205081 2.        ]
3^a: [ 3  9 27 81]
e^a: [ 2.71828183  7.3890561  20.08553692 54.59815003]
ln(a): [0.         0.69314718 1.09861229 1.38629436]
log10(a): [0.         0.30103    0.47712125 0.60205999]

备注

和手动撰写循环语句进行计算相比,这些函数的运行效率更高。

统计分析#

NumPy 也支持对数组内的元素进行一些基础的统计分析,例如求极值、求均值等。

a = np.arange(0, 11)

print("a:", a)
print("求和:", np.sum(a))
print("均值:", np.mean(a))
print("标准差:", np.std(a))
print("方差:", np.var(a))
print("最大值:", np.max(a))
print("最小值:", np.min(a))
print("中位数:", np.median(a))
print("第 90 分位数:", np.percentile(a, 90))
a: [ 0  1  2  3  4  5  6  7  8  9 10]
求和: 55
均值: 5.0
标准差: 3.1622776601683795
方差: 10.0
最大值: 10
最小值: 0
中位数: 5.0
第 90 分位数: 9.0

小技巧

这些统计分析功能也以数组的类方法的形式实现了。例如需要计算数组 a 的均值,你可以直接使用 a.mean(),其结果和 np.mean(a) 是一致的。

数组的取值#

和 Python 原生的 list 对象类似,NumPy 数组也支持使用索引值取值。

a = np.arange(0, 10)

print("a:", a)
print("a[1]:", a[1])
print("a[-2]:", a[-2])
print("a[5:]:", a[5:])
print("a[:3]:", a[:3])
print("a[0:4]:", a[0:3])
a: [0 1 2 3 4 5 6 7 8 9]
a[1]: 1
a[-2]: 8
a[5:]: [5 6 7 8 9]
a[:3]: [0 1 2]
a[0:4]: [0 1 2]

除了使用单个整数作为索引值进行取值,也可以使用整数列表来对数组进行取值:

a = np.arange(0, 10)

print("a:", a)
print("索引值为 0, 2 与 5 的元素:", a[[0, 2, 5]])
a: [0 1 2 3 4 5 6 7 8 9]
索引值为 0, 2 与 5 的元素: [0 2 5]

除此之外,我们也可以使用逻辑表达式(本质上是传入了一个布尔值数组)来进行取值,这样的操作在数据筛选时十分常用。

a = np.arange(0, 10)

print("a:", a)
print("小于 5 的元素是:", a[a < 5])
print("是偶数的元素是:", a[a % 2 == 0])
a: [0 1 2 3 4 5 6 7 8 9]
小于 5 的元素是: [0 1 2 3 4]
是偶数的元素是: [0 2 4 6 8]

数组的拼接和切分#

数组的拼接#

在 NumPy 可以灵活地对数组进行拼接,这些操作可以有助于我们快速构建大型数据。

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

c = np.concatenate([a, b])  # 拼接数组
print(c)
[1 2 3 4 5 6]
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

c = np.vstack([a, b])  # 纵向拼接一维数组
print(c)
[[1 2 3]
 [4 5 6]]
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

c = np.hstack([a, b])  # 横向拼接一维数组
print(c)
[1 2 3 4 5 6]

在二维数组中,np.vstack()np.hstack() 的差异更为明显:

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

c = np.vstack([a, b])  # 纵向拼接二维数组
print(c)
[[1 2]
 [3 4]
 [5 6]
 [7 8]]
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

c = np.hstack([a, b])  # 横向拼接二维数组
print(c)
[[1 2 5 6]
 [3 4 7 8]]

小技巧

这些拼接方法和 R 中的 cbind()rbind() 也有一定的相同点。

数组的切分#

与拼接操作类似,我们可以使用 np.split()np.vsplit()np.hsplit() 来切分数组。

需要注意的是,使用这些函数时需要传入欲切分的个数或切分点的位置。他们的差异可以观察下方的例子:

a = np.arange(0, 10)

# 若传入一个整数,则其将被视为欲切分的个数
b = np.split(a, 5)  # 切分为五个数组
print(b)

# 若传入一个一维数组或列表,则其将被视为切分点的位置
c = np.split(a, [5])  # 在索引值为 5 的位置切分数组
print(c)
[array([0, 1]), array([2, 3]), array([4, 5]), array([6, 7]), array([8, 9])]
[array([0, 1, 2, 3, 4]), array([5, 6, 7, 8, 9])]

以下是一些二维数组使用 np.vsplit()np.hsplit() 的例子:

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

b = np.vsplit(a, [1])  # 纵向切分
print(b)
[array([[1, 2, 3, 4]]), array([[ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])]
a = np.array([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]])

b = np.hsplit(a, [2, 4])  # 横向切分
print(b)
[array([[1, 2],
       [7, 8]]), array([[ 3,  4],
       [ 9, 10]]), array([[ 5,  6],
       [11, 12]])]

polars 入门教程#

与 NumPy 类似,polars 也是 Python 数据科学生态中的核心之一。其提供的数据结构可以使我们更灵活地完成数据导入导出、数据清理、缺失值处理、数据分析等工作。

在阅读下文前,推荐先阅读本节中的 NumPy 入门教程

若你想更深入地了解 polars,可以参考:polars documentation

导入 polars#

类似于导入 NumPy,我们使用 import 导入 polars。推荐同时导入 NumPy 以便于进行后续数据集的创建操作。

import numpy as np
import polars as pl

Series 和 DataFrame#

polars 的基础数据结构有两种,分别为 DataFrameSeries

DataFrame 与我们在 Excel 中常用的表格类似:它是一个二维数据集,由行与列组成,且同时拥有列名与行名。在下文中,我们将简称 DataFrame 对象为“数据集”。

df = pl.DataFrame(
    {
        "Name": [
            "Marlin",
            "Dory",
            "Nemo",
        ],
        "Length": [20, 35, 18],
        "Width": [6.5, 9.8, 5.8],
    }
)
df
shape: (3, 3)
NameLengthWidth
stri64f64
"Marlin"206.5
"Dory"359.8
"Nemo"185.8

DataFrame 的每一列都是一个 Series。可以将 Series 理解为一个带标签的一维列表。

df["Name"]  # 可以使用 [] 取出一列
shape: (3,)
Name
str
"Marlin"
"Dory"
"Nemo"
print("polars DataFrame 的每一列的类型是:")

type(df["Name"])
polars DataFrame 的每一列的类型是:
polars.series.series.Series

小技巧

DataFrame 与 R 语言中的 data.frame 类似。此外,下文中的许多函数也有在 R 语言中对应或相似的版本。

创建数据集#

我们可以用 listndarray 或者 dict 对象来创建一个数据集:

df1 = pl.DataFrame([[28.5, 30, 31.1, 26.8], ["Female", "Male", "Female", "Male"]], schema=["Score", "Sex"])
df1
shape: (4, 2)
ScoreSex
f64str
28.5"Female"
30.0"Male"
31.1"Female"
26.8"Male"
df2 = pl.DataFrame(np.array([[1, 2, 3], [4, 5, 6]]))
df2
shape: (2, 3)
column_0column_1column_2
i32i32i32
123
456
df3 = pl.DataFrame(
    {"ID": [1, 2, 3, 4], "Age": pl.Series([20, 19, 24, 36]), "Score": np.array([96, 97, 88, 90]), "Country": "China"}
)
df3
shape: (4, 4)
IDAgeScoreCountry
i64i64i32str
12096"China"
21997"China"
32488"China"
43690"China"

数据集检视#

首先,我们可以直接使用 print() 来查看完整的数据集:

df = pl.DataFrame(
    {
        "ID": [1, 2, 3, 4, 5, 6, 7, 8],
        "Age": [20, 19, 24, 36, 18, 21, 24, 25],
        "Score": [96, 97, 88, 90, 76, 89, 91, 85],
    }
)
df
shape: (8, 3)
IDAgeScore
i64i64i64
12096
21997
32488
43690
51876
62189
72491
82585

在实际场景中,药物数据集一般都较为庞大,此时我们可以使用 DataFrame.head() 或者 DataFrame.tail() 来检视数据集开头或末尾的几行:

df.head(5)
shape: (5, 3)
IDAgeScore
i64i64i64
12096
21997
32488
43690
51876
df.tail(3)
shape: (3, 3)
IDAgeScore
i64i64i64
62189
72491
82585

对于大型数据集,使用 DataFrame.describe() 方法获取数据集的描述性统计摘要也是十分实用的:

df.describe()
shape: (9, 4)
statisticIDAgeScore
strf64f64f64
"count"8.08.08.0
"null_count"0.00.00.0
"mean"4.523.37589.0
"std"2.449495.7055746.590036
"min"1.018.076.0
"25%"3.020.088.0
"50%"5.024.090.0
"75%"6.024.091.0
"max"8.036.097.0

可以使用 DataFrame.columns 属性来查看数据集的列名:

df.columns
['ID', 'Age', 'Score']

数据集排序#

使用 DataFrame.sort 就可以根据数据的大小对数据集进行排序:

df = pl.DataFrame(
    {
        "ID": [1, 2, 3, 4],
        "Age": [20, 19, 24, 36],
        "Score": [96, 97, 88, 90],
        "Name": ["Marlin", "Dory", "Nemo", "Bruce"],
    }
)
df
shape: (4, 4)
IDAgeScoreName
i64i64i64str
12096"Marlin"
21997"Dory"
32488"Nemo"
43690"Bruce"
df.sort("Name")
shape: (4, 4)
IDAgeScoreName
i64i64i64str
43690"Bruce"
21997"Dory"
12096"Marlin"
32488"Nemo"
df.sort("Name", descending=True)  # 递减排序
shape: (4, 4)
IDAgeScoreName
i64i64i64str
32488"Nemo"
12096"Marlin"
21997"Dory"
43690"Bruce"
df.sort(by=["Score"])  # 以 "Score" 列数据的大小递增排序
shape: (4, 4)
IDAgeScoreName
i64i64i64str
32488"Nemo"
43690"Bruce"
12096"Marlin"
21997"Dory"

缺失值处理#

在处理药物数据时,时常会出现缺失值,polars 也提供了丰富的检查缺失值(DataFrame.is_null())、删除缺失值(DataFrame.drop_nulls())、替换缺失值(DataFrame.fill_null())的方法。

df = pl.DataFrame({"Time": [0, 2, 4, 8, 12, 16], "Conc": [0.0, 0.9, 2.4, None, 0.7, None]})
df
shape: (6, 2)
TimeConc
i64f64
00.0
20.9
42.4
8null
120.7
16null
df.select(pl.col("Conc").is_null().alias("Conc_is_missing"))  # 判断数据是否为缺失值
shape: (6, 1)
Conc_is_missing
bool
false
false
false
true
false
true
df.drop_nulls()  # 丢弃含缺失值的行
shape: (4, 2)
TimeConc
i64f64
00.0
20.9
42.4
120.7
lloq = 0.05


df.fill_null(lloq / 2)  # 替换缺失值
shape: (6, 2)
TimeConc
f64f64
0.00.0
2.00.9
4.02.4
8.00.025
12.00.7
16.00.025

数据集的运算#

与 NumPy 数组一样,polars 的数据集也支持基础的算数运算、逻辑运算和基本统计分析功能。

基本算术与逻辑运算#

df = pl.DataFrame({"A": [0, 1, 2], "B": [3, 4, 5], "C": [6, 7, 8]})
df
shape: (3, 3)
ABC
i64i64i64
036
147
258
df + 2
shape: (3, 3)
ABC
i64i64i64
258
369
4710
df * 4
shape: (3, 3)
ABC
i64i64i64
01224
41628
82032
df < 5
shape: (3, 3)
ABC
boolboolbool
truetruefalse
truetruefalse
truefalsefalse

NumPy 中的 数学函数ufunc)也可以作用于数据集:

np.sin(df)
array([[ 0.        ,  0.14112001, -0.2794155 ],
       [ 0.84147098, -0.7568025 ,  0.6569866 ],
       [ 0.90929743, -0.95892427,  0.98935825]])
np.exp(df)
array([[1.00000000e+00, 2.00855369e+01, 4.03428793e+02],
       [2.71828183e+00, 5.45981500e+01, 1.09663316e+03],
       [7.38905610e+00, 1.48413159e+02, 2.98095799e+03]])

数据集与数据集之间也可以进行运算:

df1 = pl.DataFrame({"A": [0, 1, 2], "B": [3, 4, 5], "C": [6, 7, 8]})
df1
shape: (3, 3)
ABC
i64i64i64
036
147
258
df2 = pl.DataFrame({"A": [9, 10, 11], "B": [12, 13, 14], "C": [6, 7, 8]})
df2
shape: (3, 3)
ABC
i64i64i64
9126
10137
11148
df1 + df2
shape: (3, 3)
ABC
i64i64i64
91512
111714
131916

统计分析#

pandas 数据集也支持 NumPy 中的 统计分析函数

df = pl.DataFrame({"A": [0, 1, 2, 3], "B": [4, 5, 6, 7], "C": [8, 9, 10, 11]})
df
shape: (4, 3)
ABC
i64i64i64
048
159
2610
3711
print("各列所有数据总和:")
df.sum()
各列所有数据总和:
shape: (1, 3)
ABC
i64i64i64
62238
print("各列数据均值:")
df.mean()
各列数据均值:
shape: (1, 3)
ABC
f64f64f64
1.55.59.5

数据集的取值#

一般来说,我们可以使用 [] 完成对数据集的取值操作。

使用 [列名] 可以取出某列,使用 [行号:行号] 可以取出某些行:

df = pl.DataFrame({"ID": [1, 1, 1, 2, 2, 2], "Time": [0, 2, 4, 0, 2, 4], "Conc": [0.0, 1.4, 6.7, 0.05, 1.2, 7.8]})
df
shape: (6, 3)
IDTimeConc
i64i64f64
100.0
121.4
146.7
200.05
221.2
247.8
print("浓度列数据:")
df["Conc"]
浓度列数据:
shape: (6,)
Conc
f64
0.0
1.4
6.7
0.05
1.2
7.8
print("第2-3行数据:")
df[1:3]
第2-3行数据:
shape: (2, 3)
IDTimeConc
i64i64f64
121.4
146.7

不过对于复杂的筛选场景,简单的 [] 取值似乎有些捉襟见肘。这时候我们就需要使用 polars 强大的 select/filter/col/selector 的表达式组合来达成我们的筛选目的。

列选择 select#

对于 polars 数据集,我们可以使用 select 函数来选择列并对列的值进行变换。

import polars.selectors as selectors
df = pl.DataFrame(
    {
        "Province": ["California", "Texas", "New York", "Florida", "Illinois"],
        "Area": [423967, 695662, 141297, 170312, 149995],
        "Population": [38332521, 26448193, 19651127, 19552860, 12882135],
    },
)
df
shape: (5, 3)
ProvinceAreaPopulation
stri64i64
"California"42396738332521
"Texas"69566226448193
"New York"14129719651127
"Florida"17031219552860
"Illinois"14999512882135
# 全选
df.select(pl.all())
shape: (5, 3)
ProvinceAreaPopulation
stri64i64
"California"42396738332521
"Texas"69566226448193
"New York"14129719651127
"Florida"17031219552860
"Illinois"14999512882135
# 数值列
df.select(selectors.numeric())
shape: (5, 2)
AreaPopulation
i64i64
42396738332521
69566226448193
14129719651127
17031219552860
14999512882135
# 以 "P" 开头的列
df.select(selectors.starts_with("P"))
shape: (5, 2)
ProvincePopulation
stri64
"California"38332521
"Texas"26448193
"New York"19651127
"Florida"19552860
"Illinois"12882135

行选择 filter#

选择行的行为和选择列类似,polars 提供了 filter 该方法来实现这个功能。

# 选择人口数量大于 2e7 的数据
df.filter(pl.col("Population") > 2e7)
shape: (2, 3)
ProvinceAreaPopulation
stri64i64
"California"42396738332521
"Texas"69566226448193
# 选择人均面积大于 0.01 的数据
df.filter((pl.col("Area") / pl.col("Population")) > 0.01)
shape: (3, 3)
ProvinceAreaPopulation
stri64i64
"California"42396738332521
"Texas"69566226448193
"Illinois"14999512882135

CSV 文件的导入和导出#

使用 pl.read_csv()pl.write_csv() 可以实现本地 CSV 文件的导入与导出:

import tempfile
import pathlib

save_to = pathlib.Path(tempfile.gettempdir()) / "example.csv"
df = pl.DataFrame(
    {"Name": ["Bob", "Jake", "Lisa", "Sue"], "Department": ["Accounting", "Engineering", "Engineering", "HR"]}
)

df.write_csv(save_to)
pl.read_csv(save_to, infer_schema_length=None)
shape: (4, 2)
NameDepartment
strstr
"Bob""Accounting"
"Jake""Engineering"
"Lisa""Engineering"
"Sue""HR"