袋熊的树洞

日拱一卒,功不唐捐

0%

介绍

有些机器学习算法,比如最小二乘支持向量机 (Least Squares Support Vector Machine, LSSVM),需要计算核矩阵 (Kernel Matrix)。核矩阵的定义如下

$$
\Omega = K(X, Y) = \begin{bmatrix}
K(x_1, y_1) & K(x_1, y_2) & \cdots & K(x_1, y_n) \
K(x_2, y_1) & K(x_2, y_2) & \cdots & K(x_2, y_n) \
\vdots & \vdots & \ddots & \vdots \
K(x_m, y_1) & K(x_n, y_2) & \cdots & K(x_m, y_n)
\end{bmatrix}
$$

其中$K(\cdot, \cdot)$为核函数,常见的有线性核以及RBF核,矩阵$X \in R^{m \times d}$和$Y \in R^{n \times d}$为数据矩阵,每一行代表一个样本,定义为

$$
\begin{aligned}
X & = [x_1 , x_2 , \cdots , x_m]^T \
Y & = [y_1 , y_2 , \cdots , y_n]^T
\end{aligned}
$$

核矩阵的规模为$m \times n$,可以看出核矩阵的规模是由数据个数决定的,如果按照定义一个个生成核矩阵每一个元素,可想而知生成效率会极其低下,因此考虑用其他方式快速生成核矩阵。

快速生成核矩阵

本文只考虑核函数为RBF核的情况,其他情况有相类似的推导。RBF核又称高斯核,其定义为

$$
K(x, y) = e^{-\frac{\Vert x - y \Vert^2}{2\sigma^2}}
$$

其中$\sigma > 0$为高斯核带宽,$\Vert\cdot\Vert$为$L_2$范数。此时核矩阵$\Omega$为

$$
\Omega=K(X, Y) = \begin{bmatrix}
e^{-\frac{\Vert x_1 - y_1\Vert^2}{2\sigma^2}} & e^{-\frac{\Vert x_1 - y_2 \Vert^2}{2\sigma^2}} & \cdots & e^{-\frac{\Vert x_1 - y_n \Vert^2}{2\sigma^2}} \
e^{-\frac{\Vert x_2 - y_1 \Vert^2}{2\sigma^2}} & e^{-\frac{\Vert x_2 - y_2 \Vert^2}{2\sigma^2}} & \cdots & e^{-\frac{\Vert x_2 - y_n \Vert^2}{2\sigma^2}} \
\vdots & \vdots & \ddots & \vdots \
e^{-\frac{\Vert x_m - y_1 \Vert^2}{2\sigma^2}} & e^{-\frac{\Vert x_m - y_2 \Vert^2}{2\sigma^2}} & \cdots & e^{-\frac{\Vert x_m - y_n \Vert^2}{2\sigma^2}}
\end{bmatrix}
$$

将自然指数运算提取到矩阵外面,则核矩阵$\Omega$表示为$\Omega=e^{-\frac{D}{2\sigma^2}}$,这里矩阵$D$为

$$
D = \begin{bmatrix}
\Vert x_1 - y_1 \Vert^2 & \Vert x_1 - y_2 \Vert^2 & \cdots & \Vert x_1 - y_n \Vert^2 \
\Vert x_2 - y_1 \Vert^2 & \Vert x_2 - y_2 \Vert^2 & \cdots & \Vert x_2 - y_n \Vert^2 \
\vdots & \vdots & \ddots & \vdots \
\Vert x_m - y_1 \Vert^2 & \Vert x_m - y_2 \Vert^2 & \cdots & \Vert x_m - y_n \Vert^2
\end{bmatrix}
$$

将矩阵$D$中每一个元素展开,则可得

$$
\begin{aligned}
\Vert x_i - y_j \Vert^2 & = (x_i - y_j)^T(x_i - y_j) \
& = x_i^T x_i - 2 x_i^T y_j + y_j^T y_j \
& = \Vert x_i \Vert^2 - 2 x_i^T y_j + \Vert y_j \Vert^2
\end{aligned}
$$

从而矩阵$D$可以分解为三个矩阵之和

$$
D = A - 2B + C
$$

其中矩阵$A$定义为

$$
A = \begin{bmatrix}
\Vert x_1 \Vert^2 & \Vert x_1 \Vert^2 & \cdots & \Vert x_1 \Vert^2 \
\Vert x_2 \Vert^2 & \Vert x_2 \Vert^2 & \cdots & \Vert x_2 \Vert^2 \
\vdots & \vdots & \ddots & \vdots \
\Vert x_m \Vert^2 & \Vert x_m \Vert^2 & \cdots & \Vert x_m \Vert^2
\end{bmatrix}_{m \times n}
$$

这里定义一个向量$\alpha$,其第$i$个元素为数据矩阵$X$第$i$行所有元素平方和,即

$$
\alpha = \begin{bmatrix}
\Vert x_1 \Vert^2 & \Vert x_2 \Vert^2 & \cdots & \Vert x_m \Vert^2
\end{bmatrix}^T
$$

则矩阵$A$可以表示为

$$
\begin{aligned}
A & = \alpha 1^T \
& = \begin{bmatrix}
\Vert x_1 \Vert^2 \
\Vert x_2 \Vert^2 \
\vdots \
\Vert x_m \Vert^2
\end{bmatrix} \begin{bmatrix}
1 & 1 & \cdots & 1
\end{bmatrix}
\end{aligned}
$$

矩阵$B$定义为

$$
B = \begin{bmatrix}
x_1^T y_1 & x_1^T y_2 & \cdots & x_1^T y_n \
x_2^T y_1 & x_2^T y_2 & \cdots & x_2^T y_n \
\vdots & \vdots & \ddots & \vdots \
x_m^T y_1 & x_m^T y_2 & \cdots & x_m^T y_n
\end{bmatrix}_{m \times n}
$$

很容易可以知道矩阵$B$由数据矩阵$X$和$Y$相乘而得

$$
B = XY^T
$$

矩阵$C$定义为

$$
C = \begin{bmatrix}
\Vert y_1 \Vert^2 & \Vert y_2 \Vert^2 & \cdots & \Vert y_n \Vert^2 \
\Vert y_1 \Vert^2 & \Vert y_2 \Vert^2 & \cdots & \Vert y_n \Vert^2 \
\vdots & \vdots & \ddots & \vdots \
\Vert y_1 \Vert^2 & \Vert y_2 \Vert^2 & \cdots & \Vert y_n \Vert^2
\end{bmatrix}_{m \times n}
$$

类似地,这里定义一个向量$\beta$,其第$i$个元素为数据矩阵$Y$第$i$行所有元素平方和,即

$$
\beta = \begin{bmatrix}
\Vert y_1 \Vert^2 & \Vert y_2 \Vert & \cdots & \Vert y_n \Vert^2
\end{bmatrix}^T
$$

则矩阵$C$可以表示为

$$
\begin{aligned}
C & = 1 \beta^T \
& = \begin{bmatrix}
1 \
1 \
\vdots \
1
\end{bmatrix} \begin{bmatrix}
\Vert y_1 \Vert^2 & \Vert y_2 \Vert & \cdots & \Vert y_n \Vert^2
\end{bmatrix}
\end{aligned}
$$

综上可以得到

$$
D = \alpha 1^T - 2 XY^T + 1 \beta^T
$$

实现

原始生成方法

按照核矩阵定义,一个个生成矩阵每一个元素,实现代码如下

1
2
3
4
5
6
7
8
9
10
11
def method1(X1, X2, sigma=1.0):
num_samples1, dims = X1.shape
num_samples2, dims = X2.shape
mat = np.empty((num_samples1, num_samples2))

for i in range(num_samples1):
for j in range(num_samples2):
vec = X1[i, :] - X2[j, :]
mat[i, j] = np.exp(-0.5 / sigma**2 * np.dot(vec, vec))

return mat

快速生成方法1

根据前面讨论,将核矩阵分解为一系列矩阵向量乘积,实现代码如下

1
2
3
4
5
6
7
8
9
10
11
def method2(X1, X2, sigma=1.0):
X1_sum = np.sum(X1**2, axis=1)
X2_sum = np.sum(X2**2, axis=1)
mat1 = np.dot(np.reshape(X1_sum, (X1_sum.shape[0], 1)),
np.ones((1, X2_sum.shape[0])))
mat2 = np.dot(np.ones((X1_sum.shape[0], 1)),
np.reshape(X2_sum, (1, X2_sum.shape[0])))
mat = mat1 + mat2 - 2 * np.dot(X1, X2.T)
mat = np.exp(-0.5 / sigma**2 * mat)

return mat

快速生成方法2

利用Numpy的广播机制,进一步提升核矩阵生成速度,实现代码如下

1
2
3
4
5
def method3(X1, X2, sigma=1.0):
mat = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * np.dot(X1, X2.T)
mat = np.exp(-0.5 / sigma**2 * mat)

return mat

实验

实验代码在:https://gist.github.com/luowanqian/d2f655a95f77dfe088fd626f72d78966

这里做一个简单的实验,比对各种方法的效率。实验的数据矩阵$X$大小为$1000 \times 100$,数据矩阵$Y$大小为$5000 \times 100$,实验结果如下

方法 生成时间
原始生成 25.1 s
快速生成1 132 ms
快速生成2 106 ms

由结果可以看出,快速生成方法速度提升是极大的。

问题

使用Tmux时经常会遇到的一个问题,那就是系统重启时,Session会被清除,每次打开电脑都要重启建一个Session,然后创建一堆Window以及Pane,这极大地降低了Tmux使用效率,因此需要想一个办法能够保存Tmux的Session。

解决方案

该问题的解决方案是安装一个叫Tmux Resurrect的插件。Tmux要安装插件,可以通过Tmux Plugin Manager这个插件进行安装,该插件相当于一个插件管理系统,可以快速地安装、更新以及删除插件。

Tmux Plugin Manager

安装Tmux Plugin Manager插件可以参考该插件的GitHub:tmux-plugins/tpm。安装很简单,首先clone插件到本地

1
$ git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

然后修改.tmux.conf,在文件最底部添加以下内容

1
2
3
4
5
6
# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'

# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'

重新加载配置文件

1
2
# type this in terminal if tmux is already running
$ tmux source ~/.tmux.conf

然后就可以在Tmux中使用快捷prefix + I(注意这里的I是大写)安装配置文件.tmux.conf中定义的插件了。

补充:

如果Tmux安装了gpakosz/.tmux,在文件.tmux.conf.local中配置好Tmux Plugin Manager后,在Tmux中使用prefix + I安装插件会没有效果,相关的讨论见“run ‘~/.tmux/plugins/tpm/tpm’” has no effect in .tmux.conf.local #61,具体的解决方案是用另一种写法写set -g @plugin,即将

1
2
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'

改写为

1
2
3
4
set -g @tpm_plugins '          \
tmux-plugins/tpm \
tmux-plugins/tmux-sensible
'

解决方案来源于:Help, tpm not working!

Tmux Resurrect

安装好Tmux Plugin Manager后,就可以安装Tmux Resurrect插件(tmux-plugins/tmux-resurrect)了。在Tmux配置文件.tmux.conf中添加

1
set -g @plugin 'tmux-plugins/tmux-resurrect'

然后使用快捷键prefix + I就可以安装插件了。

保存和恢复

安装完Tmux Plugin Manager,就可以使用下面快捷键保存和恢复Session了。

  • prefix + Ctrl-s - save
  • prefix + Ctrl-r - restore

平常重启电脑后,首先在终端打开Tmux,然后使用prefix + Ctrl-r就可以恢复保存的Session了,如果有多个Session可以使用prefix + s来选择Session。

参考

  1. 保存和恢复 Tmux 会话

介绍

看本文之前建议看文章”Typed Memoryviews“,该文章主要介绍的是Memoryviews,使用Memoryviews可以直接访问Numpy数组的memory buffer,直接操作Numpy数组的数据,同时是连接C数组与Numpy数组之间的桥梁。相关代码在:CythonDemos/Demo1

BLAS

BLAS,全称Basic Linear Algebra Subprograms,即基础线性代数子程序库,里面有大量已经编写好的关于线性代数运算的程序。BLAS库的API分三大类,分别为Level 1Level 2以及Level 3

  • Level 1: 函数处理单一向量的线性运算以及两个向量的二元运算
  • Level 2: 函数处理矩阵与向量的运算,同时也包含线性方程组求解
  • Level 3: 函数处理矩阵与矩阵之间的运算

API的全部介绍可以参考官方文档:blasqr.pdf

Cython调用BLAS函数

Cython要调用BLAS函数,主要是通过BLAS库的C接口,接口的具体定义可以参考现有BLAS库的 cblas.h,里面函数的命名和 blasqr.pdf 中函数命名是一一对应的,只不过函数前多了cblas_前缀。这里演示如何调用函数cblas_dgemv,在头文件 cblas.h 中函数声明为

1
void cblas_dgemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE trans, OPENBLAS_CONST blasint m, OPENBLAS_CONST blasint n, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *a, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double beta, double *y, OPENBLAS_CONST blasint incy);

去除一些宏以及替换定义的数据类型,简单表示就是

1
void cblas_dgemv(enum CBLAS_ORDER order, enum CBLAS_TRANSPOSE trans, int m, int n, double alpha, double *a, int lda, double *x, int incx, double beta, double *y, int incy)

这个函数主要是实现的功能计算矩阵与向量相乘,在 blasqr.pdf 中对应的是Level2xGEMV函数

$$
y = \alpha Ax + \beta y
$$

在Cython调用BLAS函数首先要使用cdef extern引入头文件,然后声明函数

1
2
3
4
5
6
7
8
9
10
11
12
13
cdef extern from 'cblas.h':
enum CBLAS_ORDER:
CblasRowMajor=101
CblasColMajor=102
enum CBLAS_TRANSPOSE:
CblasNoTrans=111
CblasTrans=112
CblasConjTrans=113
CblasConjNoTrans=114
void dgemv "cblas_dgemv"(CBLAS_ORDER Order, CBLAS_TRANSPOSE TransA,
int M, int N, double alpha, double *A, int lda,
double *X, int incX, double beta,
double *Y, int incY) nogil

关于enumCblasRowMajor这些标识符的取值,是按照 cblas.h 中定义来取值。函数声明后就可以编写一个wrapper函数来调用BLAS函数,之所以要写一个wrapper函数,是因为声明的BLAS函数只能在C中进行调用,我们还要使得其能够在Python中进行调用,wrapper函数定义如下:

1
2
3
4
5
6
7
8
cpdef blas_dgemv(double[:, ::1] A, double[::1] x, double[::1] y,
int M, int N, double alpha, double beta):
cdef double *A_ptr = &A[0, 0]
cdef double *x_ptr = &x[0]
cdef double *y_ptr = &y[0]

dgemv(CblasRowMajor, CblasNoTrans, M, N,
alpha, A_ptr, N, x_ptr, 1, beta, y_ptr, 1)

通过cpdef可以使得定义的函数可以被Python调用。这里说一下如何将MemoryViews转化成C的指针,对于向量x可以通过对第一个元素进行取址来进行转换&x[0],对于矩阵A则是对其第一行第一列的元素进行取址操作&A[0, 0]

由于BLAS库中将矩阵数据看成是一个一维数组,所以在调用BLAS函数时需要指定矩阵是Row Major还是Column Major,即参数CBLAS_ORDER Order,这里设定是Row Major,因此函数blas_dgemv中参数A限定是Row Major,即声明为double[:, ::1]

整个pyx文件的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cdef extern from 'cblas.h':
enum CBLAS_ORDER:
CblasRowMajor=101
CblasColMajor=102
enum CBLAS_TRANSPOSE:
CblasNoTrans=111
CblasTrans=112
CblasConjTrans=113
CblasConjNoTrans=114
void dgemv "cblas_dgemv"(CBLAS_ORDER Order, CBLAS_TRANSPOSE TransA,
int M, int N, double alpha, double *A, int lda,
double *X, int incX, double beta,
double *Y, int incY) nogil

cpdef blas_dgemv(double[:, ::1] A, double[::1] x, double[::1] y,
int M, int N, double alpha, double beta):
cdef double *A_ptr = &A[0, 0]
cdef double *x_ptr = &x[0]
cdef double *y_ptr = &y[0]

dgemv(CblasRowMajor, CblasNoTrans, M, N,
alpha, A_ptr, N, x_ptr, 1, beta, y_ptr, 1)

剩下就是将pyx进行编译,然后就可以在Python中调用blas_dgemv函数了。

参考

  1. Calling BLAS from Cython

1. Pandas备忘录系列文章

  1. Pandas备忘录

2. 介绍

Note: 如果没有特别说明,pd指的是pandasnp指的是numpy

1
2
import pandas as pd
import numpy as np

3. 查找

3.1. contains函数

主要是用Series.str.contains这个函数,如果参数regex设为True,则查找字符串解析为正则表达式,设为False则用常规查找方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> s1 = pd.Series(['Mouse', 'dog', 'house and parrot', '23', 'frog'])
>>> s1
0 Mouse
1 dog
2 house and parrot
3 23
4 frog
dtype: object
>>> s1.str.contains('og', regex=False)
0 False
1 True
2 False
3 False
4 True
dtype: bool
>>> s1[s1.str.contains('og', regex=False)]
1 dog
4 frog
dtype: object

4. DataFrame增加行

4.1. append函数

可以使用append函数给DataFrame增加行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> df = pd.DataFrame([[1, 2], [3, 4]], columns=list('AB'))
>>> df
A B
0 1 2
1 3 4
>>> df2 = pd.DataFrame([[5, 6]], columns=list('AB'))
>>> df2
A B
0 5 6
>>> df.append(df2, ignore_index=True)
A B
0 1 2
1 3 4
2 5 6

4.2. 使用特定索引名称增加行

使用DataFrame的loc函数可以指定索引名称增加行,前提是得知道列的排列顺序,因此在创建DataFrame时要用columns参数指定列的排列顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> employees = pd.DataFrame(
... data={'Name': ['John Doe', 'William Spark'],
... 'Age': [23, 24]},
... index=['Emp001', 'Emp002'],
... columns=['Name', 'Age'])
>>> employees
Name Age
Emp001 John Doe 23
Emp002 William Spark 24
>>> employees.loc['Emp003'] = ['Sunny', 45]
>>> employees
Name Age
Emp001 John Doe 23
Emp002 William Spark 24
Emp003 Sunny 45

Weighted random sampling with replacement,中文翻译为带权重的有放回地采样,简单来说就是每一个样本都有一个权重,然后根据样本权重大小有放回地采样数据,样本的权重越大,样本被选择的概率也就越大。该问题换种方式描述就是,给定一个权重数组 $w$,大小为 $N$,然后根据权重大小,在下标数组 ${0, 1, \ldots, N-1}$ 中进行有放回地采样,下标 $i$ 被选中的概率为 $w[i]/\text{sum}(w)$。

这个采样方法一个应用是在Boosting算法中,Boosting算法每次训练完模型时,根据模型分类的情况改变数据样本的权重,然后用带权重的样本训练下一个模型,如果模型的训练不支持带权重的样本,则进行带权重的样本采样。

本文主要将博文 WEIGHTED RANDOM SAMPLING WITH REPLACEMENT WITH DYNAMIC WEIGHTS 内容进行解析 (文章拷贝:Copy)。算法实现代码在:adefazio/sampler

Rejection Sampling

这个思想很简单,给定一个权重数组 $w \in R^N$,权重最大值为 $w_\max$,然后在下标数组 ${ 0, 1, \ldots, N-1 }$ 中随机采样得到下标 $i$,接下来就是判断该下标是否被accept,首先根据分布 $X \sim U(0, 1)$ 获得一个随机数 $x$,然后判断 $w_\max * x \le w[i]$,如果符合该条件,则accept该下标 $i$,否则就reject该下标。

算法实现的代码如下:

1
2
3
4
5
6
7
8
9
10
w = [1,4,2,5] # Some data
w_max = max(w)
n = len(w)

while True:
idx = random.randrange(n)
u = w_max*random.random()
if u <= w[idx]:
break
print idx

算法思想可以用下面图表示:

橘色区域代表accept区域,灰色区域代表reject区域。这个算法有个缺点就是当数据不平衡时,性能会很低,因为如果小部分数据权重很大时,$w_\max$ 会变的很大,数据权重小的下标很难被accept,而且权重小的占大部分,这导致算法会经过很多次循环。

Level Rejection Sampling

这个是在Rejection Sampling之前对下标进行分层 (level),根据权重 $w[i]$ 的大小将下标 $i$ 分到不同层中,第 $k$ 层权重的范围为 $[2^k, 2^{k+1}]$,由于每一层内的权重的差距不会偏大,对该层进行Rejection Sampling效率会较高。

该方法的思想如下图所示:

如图所示,总共分了5层,每一层存储的是数组下标。下标分层后,在进行采样时,首先随机选择某一层,然后进行Rejection Sampling获得最终想要的下标。

模拟实验

贴出一段实验代码:

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
import numpy as np
from sampler import Sampler
import matplotlib.pyplot as plt


if __name__ == '__main__':
weights = np.array([1, 1, 3, 5, 2], dtype='d')
num_entries = len(weights)
normalized_weights = weights / np.sum(weights)

weight_sampler = Sampler(num_entries, max_value=100, min_value=1)
for i in range(num_entries):
weight_sampler.add(i, weights[i])

num_samples = 10000
distro = np.zeros(num_entries)

for i in range(num_samples):
idx = weight_sampler.sample()
distro[idx] += 1

normalized_distro = distro / np.sum(distro)

print('Sample distribution: {}'.format(distro))
print('Weights distribution: {}'.format(weights))
print('Normalized sample distribution: {}'.format(normalized_distro))
print('Normalized weights distribution: {}'.format(normalized_weights))

# plot frequency
plt.bar(np.arange(num_entries),
normalized_weights, width=0.35,
label='Normalized Weights Distribution')
plt.bar(np.arange(num_entries)+0.35,
normalized_distro, width=0.35,
label='Normalized Sample Distribution')
plt.legend()
plt.show()

输出为:

1
2
3
4
Sample distribution: [ 843.  824. 2513. 4182. 1638.]
Weights distribution: [1. 1. 3. 5. 2.]
Normalized sample distribution: [0.0843 0.0824 0.2513 0.4182 0.1638]
Normalized weights distribution: [0.08333333 0.08333333 0.25 0.41666667 0.16666667]

得到的结果如下图所示:

图中横坐标代表数组下标,蓝色柱代表各个下标的权重大小 (归一化),橘色柱代表经过大量采样时,各个下标被采中的比例。从图中可以看出,采样结果基本符合权重分布规律,权重越大,被采中的概率也就越大。

在使用h5py这类库读取HDF5里面的数据时,数据的存储方式为row-major order,而一些机器学习算法的实现在使用数据时需要的存储方式为column-major order,此时需要对数据进行转换。如果数据量很小,转换是没有问题的,如果数据量巨大,进行转换时,由于需要多一倍内存来存放转换的数据,因此可能导致内存溢出。本篇博文主要是重新实现h5py库中的 read_direct 函数,使得该函数在读取HDF5文件时,可以使得读取到内存的数据是以column-major order方式存储。

相关实现代码在: HDF5 gist

思路

基本思路就是一行行地读取HDF5文件里面的数据,然后存储到内存的数组中,内存的数组是以column-major order方式存储数据。这里假设HDF5文件里面的数据存储是连续的,没有进行分块,复杂的情况等以后再实现,这里只是应对最简单的情况。

实现

实现主要是 Python + C 形式,Python部分主要提供一个函数接口用于调用,C部分主要是使用HDF5 C API对HDF5数据进行读取,两者之间的相互调用使用的是Cython。

C

这部分需要使用HDF5库提供的C API对HDF5文件数据进行读取。主要使用的API是 H5Sselect_hyperslab,这个函数可以设定读取存储空间的哪一部分数据或者将数据存在存储空间的哪一部分,这里的存储空间是一个抽象概念,既可以表示HDF5文件里面内容,也可以表示内存申请的存储空间,这里我们分别对HDF5文件内容和内存申请的空间分别进行设定。实现代码如下:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* 从H5文件中读取矩阵数据,以column order存储数据
*
* filename: H5文件名
* dataset_name: 数据集名
* buffer: 数据内存存储处,大小为 num_rows * num_cols
* num_rows: 待读取的矩阵的行数
* num_cols: 待读取的矩阵的列数
* offset_x: 读取的数据在H5文件中的矩阵的x方向上的偏移 (第几行)
* offset_y: 读取的数据在H5文件中的矩阵的y方向上的偏移 (第几列)
*
*/
int read_direct_f(char *file_name, char *dataset_name, double *buffer,
int num_rows, int num_cols, int offset_x, int offset_y)
{
int i;

hid_t file_id, dataset_id;
hid_t dataspace_id, memspace_id;
herr_t status;

hsize_t dims_mem[1];

hsize_t offset[2];
hsize_t count[2];
hsize_t stride[2];
hsize_t block[2];
hsize_t offset_mem[1];
hsize_t count_mem[1];
hsize_t stride_mem[1];
hsize_t block_mem[1];

// open the hdf5 file and the dataset
file_id = H5Fopen(file_name, H5F_ACC_RDONLY, H5P_DEFAULT);
dataset_id = H5Dopen2(file_id, dataset_name, H5P_DEFAULT);

// get file dataspace
dataspace_id = H5Dget_space(dataset_id);

// create memory space
dims_mem[0] = num_rows; // avoid numeric overflow
dims_mem[0] = dims_mem[0] * num_cols;
memspace_id = H5Screate_simple(1, dims_mem, NULL);

// specify size and shape of subset to read
offset[0] = offset_x;
offset[1] = offset_y;
count[0] = 1;
count[1] = num_cols;
stride[0] = 1;
stride[1] = 1;
block[0] = 1;
block[1] = 1;
offset_mem[0] = 0;
count_mem[0] = num_cols;
stride_mem[0] = num_rows;
block_mem[0] = 1;

for (i=0; i<num_rows; i++) {
offset[0] = offset_x + i;
offset_mem[0] = i;

// select subset from dataspace
status = H5Sselect_hyperslab(dataspace_id, H5S_SELECT_SET,
offset, stride, count, block);
if (status < 0) {
H5Eprint2(H5E_DEFAULT, NULL);
return -1;
}

status = H5Sselect_hyperslab(memspace_id, H5S_SELECT_SET,
offset_mem, stride_mem,
count_mem, block_mem);
if (status < 0) {
H5Eprint2(H5E_DEFAULT, NULL);
return -1;
}

// read data
status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE,
memspace_id, dataspace_id,
H5P_DEFAULT, buffer);
if (status < 0) {
H5Eprint2(H5E_DEFAULT, NULL);
return -1;
}
}

// close the resources
H5Sclose(memspace_id);
H5Sclose(dataspace_id);
H5Dclose(dataset_id);
H5Fclose(file_id);

return 0;
}

基本实现思路就是每次就只读取HDF5文件中矩阵的一行,然后写入到内存空间中,每次读取都要用函数 H5Sselect_hyperslab 函数设定读取区域,同样,每次写入都要使用函数 H5Sselect_hyperslab 设定写入区域。这个实现思想参考了HDF5官方教程 Reading From or Writing To a Subset of a Dataset,教程中仅仅设定了文件的读取区域,没有设定写入区域。

Python

这部分使用Cython来实现Python中调用前面所写的C语言代码,这部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def read_direct_fortran(str file_name, str dataset_name,
double[::1, :] data, int offset_x, int offset_y):
cdef int num_rows
cdef int num_cols
cdef bytes file_name_bytes = bytes(file_name, encoding='utf-8')
cdef bytes dataset_name_bytes = bytes(dataset_name, encoding='utf-8')
cdef int ret;

num_rows = data.shape[0]
num_cols = data.shape[1]

ret = read_direct_f(file_name_bytes, dataset_name_bytes, &data[0, 0],
num_rows, num_cols, offset_x, offset_y)

问题描述

Given a 32-bit signed integer, reverse digits of an integer.

Example 1:

1
2
Input: 123
Output: 321

Example 2:

1
2
Input: -123
Output: -321

Example 3:

1
2
Input: 120
Output: 21

Note:

Assume we are dealing with an environment which could only store integers within the 32-bit signed integer range: $[−2^{31}, 2^{31} − 1]$. For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.

Related Topics: Math

原问题: 7. Reverse Integer

中文翻译版: 7. 反转整数

解决方案

解决思路就是:利用求模得到数字的每一位,求位的顺序要从低位到高位,然后根据获得的数位构造出逆数。这里有个困难就在求解过程中会发生数字溢出,一个解决方案就是用更多字节的整数类型来表示逆数,题目中使用的整数int是四个字节,此时可以用八字节的long long代替,然后判断数是否在32位整数表示的范围即可。代码如下:

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
#include <iostream>
using namespace std;

class Solution {
public:
int reverse(int x) {
int mod;
long long num;
bool postive;

num = 0;
postive = x >=0 ? true : false;
while (x) {
mod = x % 10;
x = x / 10;
num = num * 10 + mod;
if ((postive && num > 0x7FFFFFFF)
|| (!postive && num < (signed int)0x80000000))
return 0;
}

return num;
}
};

int main()
{
int x = 120;

Solution solu;
cout << "Revert: " << solu.reverse(x) << endl;

return 0;
}

介绍

斯特林公式 (Stirling’s approximation) 是一条用来取n的阶乘的近似值的数学公式。常见的形式为

$$
n! \approx \sqrt{2 \pi n} (\frac{n}{e})^n
$$

对n的阶乘取自然对数,可以得到

$$
\ln{(n!)} \approx \frac{1}{2} \ln{(2 \pi)} + (n + \frac{1}{2}) \ln{(n)} - n
$$

一个会经常遇到的问题,那就是计算 $n! / a_n$,单独计算 $n!$ 需要很大的计算量,如果将问题转换为

$$
\frac{n!}{a_n} = e^{\ln{(n!)} - \ln{(a_n)}}
$$

$\ln{(n!)}$ 可以用前面介绍的近似公式进行求解,此时计算量就会小很多。

例子

这里举一个例子来感受下这个近似公式是如何缩减大量计算量。一个经典的概率问题-Birthday Problem

Birthday Prolem

Calculate the probability p that at least two people in a group of k people will have the same birthday, that is, will have been born on the same day of the same month but not necessarily in the same year.

可以很快地想到

至少两个人的生日相同的概率 = 1 - 所有人的生日不相同的概率

则计算公式为

$$
p = 1 - \frac{P_{365, k}}{365^k}
$$

其中 $P_{365, k}$ 为排列公式,等于 $365! / (365 - k)!$,则概率 $p$ 为

$$
p = 1 - \frac{365!}{(365 - k)!365^k}
$$

如果按照这个式子一个个计算里面的每一项时,需要很大的计算量,尤其是 $365!$,一般计算器估计算不出来。此时,我们将式子用指数,对数进行改写,可写为

$$
p = 1 - e^{\ln{(365)!} - \ln{(365-k)!} - k\ln{(365)}}
$$

其中 $\ln{(365)!}$ 和 $\ln{(365-k)!}$ 可以用Stirling公式进行近似计算,可以得到

$$
\begin{align}
& \ln{(365)!} - \ln{(365-k)!} - k \ln{(365)} \
\approx ; & \left[\frac{1}{2} \ln{(2 \pi)} + (365 + \frac{1}{2}) \ln{(365)} - 365 \right] \
& - \left[ \frac{1}{2} \ln{(2 \pi)} + (365 - k + \frac{1}{2}) \ln{(365-k)} - 365 + k \right] \
& - k \ln{(365)} \
\approx ; & (365 + \frac{1}{2}) \ln{(365)} - (365 - k + \frac{1}{2}) \ln{(365-k)} - k - k \ln {(365)}
\end{align}
$$

可见,阶乘和指数部分变成一堆对数的乘积以及求和,这样计算量就小了很多。这里贴一下用Python实现的概率计算程序

1
2
3
4
5
6
7
8
9
10
11
12
import math
import sys


if __name__ == '__main__':
k = int(sys.argv[1])
sum = ((365 + 0.5) * math.log(365) -
(365 - k + 0.5) * math.log(365 - k) -
k - k * math.log(365))
prob = 1.0 - math.exp(sum)
print('Probability (k = {}) is {}'.format(k, prob))

用程序计算了一些值,得到下面列表

k prob k prob
5 0.027 30 0.706
10 0.117 40 0.891
15 0.253 50 0.970
20 0.411 60 0.994
25 0.569 70 0.999

从表格中可以看到,$k = 70$ 时,概率很接近 $1$ 了,原以为至少需要100多人才可能达到0.999这个概率值,可是现在看来是不需要达到100人就可以了。这让我想起了在班里,有人的生日和我是同一天,整个班的人数大概是50人,根据表格,可以得到概率为0.970,由此可见班里同学生日和我同一天的概率还是挺高的。

介绍

本文主要是记录一些 Matplotlib 的使用方法以及注意事项。

Note: 如果没有特别说明,plt 代表 matplotlib.pyplotnp 代表 numpy

1
2
import numpy as np
import matplotlib.pyplot as plt

本文代码主要在 Jupyter notebook 中执行,画图之前使用了一下 magic 方法

1
%matplotlib notebook

创建多个子图

一个常用的应用就是在一个 figure 里面画多个图,此时需要 add_subplot 方法或者 subplots 方法。

使用 add_subplot

1
2
3
4
5
6
7
fig = plt.figure()

ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 2)

ax1.plot(np.random.randn(50).cumsum(), 'k--')
ax2.hist(np.random.randn(100), bins=20, color='k', alpha=0.3)

使用 subplots

1
2
3
4
5
6
7
fig, axes = plt.subplots(1, 2)

ax1 = axes[0]
ax2 = axes[1]

ax1.plot(np.random.randn(50).cumsum(), 'k--')
ax2.hist(np.random.randn(100), bins=20, color='k', alpha=0.3)

保存图像到文件中

可以使用 plt.savefig 将图像保存到文件中,也可以使用 figure 对象的属性方法 savefig

1
2
3
4
5
6
data = np.arange(10)
fig, ax = plt.subplots(1, 1)
ax.plot(data)

plt.savefig('1.png')
fig.savefig('2.png')

特定图像

Histogram

直方图 (Histogram) 使用的是 hist 函数进行绘制,bins 选项可以设置bin的个数。

1
2
3
4
5
6
7
8
mu, sigma = 100, 15
num_bins = 100
x = mu + sigma * np.random.randn(1000000)

n, bins, patches = plt.hist(x, num_bins, density=True)

plt.title('Histogram')
plt.grid(True)

1. Pandas备忘录系列文章

  1. Pandas备忘录2

2. 介绍

本文主要是记录一些Pandas的使用方法以及注意事项

Note: 如果没有特别说明,pd 指的是 pandasnp 指的是 numpy

1
2
import pandas as pd
import numpy as np

3. 构建DataFrame

构建一个DataFrame方法有很多,这里介绍一部分常用的方法,即

  • From dict of ndarrays / lists
  • From a list of dicts

3.1. From dict of ndarrays / lists

输入数据为一个字典,字典的key为列名,字典的value为一个Numpy的数组或者一个list,存储DataFrame一列的值

1
2
3
4
5
6
7
>>> data = {'one': [1, 2, 3],
... 'two': [3, 2, 1]}
>>> pd.DataFrame(data)
one two
0 1 3
1 2 2
2 3 1

3.2. From a list of dicts

输入数据为一个list,每个元素为一个字典,存储DataFrame一行的值,字典的key为列名,value为一个DataFrame中一个元素。

1
2
3
4
5
6
7
8
>>> data = [{'one': 1, 'two': 3},
... {'one': 2, 'two': 2},
... {'one': 3, 'two': 1}]
>>> pd.DataFrame(data)
one two
0 1 3
1 2 2
2 3 1

4. Series索引

Series索引类似Numpy的数组索引,除了可以使用 integer 作为索引值,还可以使用 index 作为索引值

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
>>> obj = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
>>> obj
a 0.0
b 1.0
c 2.0
d 3.0
dtype: float64
>>> obj['b']
1.0
>>> obj[1]
1.0
>>> obj[:2]
a 0.0
b 1.0
dtype: float64
>>> obj[obj < 2]
a 0.0
b 1.0
dtype: float64

# 如果使用 label 索引,则索引区间为闭区间
>>> obj['b':'c']
b 1.0
c 2.0
dtype: float64

5. DataFrame索引

先贴张DataFrame索引方法的表格,摘录自《Python for Data Analysis》。

Type Notes
df[val] Select single column or sequence of columns from the DataFrame; special case conveniences: boolean array (filter rows), slice (slice rows), or boolean DataFrame (set values bases on some criterion)
df.loc[val] Selects single row or subset of rows from the DataFrame by label
df.loc[:, val] Selects single column or subset of columns by label
df.loc[val1, val2] Select both rows and columns by label
df.iloc[where] Selects single row of subsets of rows from the DataFrame by integer position
df.iloc[:, where] Selects single column or subset of columns by integer position
df.iloc[where_i, where_j] Select both rows and columns by integer position
df.at[label_i, label_j] Select a single scalar value by row and column label
df.iat[i, j] Select a single scalar value by row and column position (integers)

先创建一个数据

1
2
3
4
5
6
7
8
9
>>> data = pd.DataFrame(np.arange(16).reshape((4, 4)),
... index=['Ohio', 'Colorado', 'Utah', 'New York'],
... columns=['one', 'two', 'three', 'four'])
>>> data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15

5.1. df[]语法

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
# 利用单个label选择单列
>>> data['two']
Ohio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int64


# 利用多个label选择多列,可以改变列顺序
>>> data[['three', 'one']]
three one
Ohio 2 0
Colorado 6 4
Utah 10 8
New York 14 12


# 利用boolean数组选择多行
>>> bools = np.array([False, True, False, True])
>>> bools
array([False, True, False, True])
>>> data[bools]
one two three four
Colorado 4 5 6 7
New York 12 13 14 15


# 利用切片(slice)选择多行,类似Numpy的语法
>>> data[:2]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7


# 利用boolean DataFrame选择数据
>>> data < 5
one two three four
Ohio True True True True
Colorado True False False False
Utah False False False False
New York False False False False
>>> data[data < 5] = 0
>>> data
one two three four
Ohio 0 0 0 0
Colorado 0 5 6 7
Utah 8 9 10 11
New York 12 13 14 15

5.2. df.loc语法

df.loc[] 索引值为 axis labels

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
# 使用 df.loc[val] 选择行
>>> data.loc['Utah']
one 8
two 9
three 10
four 11
Name: Utah, dtype: int64
>>> data.loc[['Utah', 'Ohio']]
one two three four
Utah 8 9 10 11
Ohio 0 1 2 3
# 如果使用 label 索引,则索引区间为闭区间
>>> data.loc[:'Utah']
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11


# 使用 df.loc[:, val] 选择列
>>> data.loc[:, 'one']
Ohio 0
Colorado 4
Utah 8
New York 12
Name: one, dtype: int64
>>> data.loc[:, ['one', 'two']]
one two
Ohio 0 1
Colorado 4 5
Utah 8 9
New York 12 13
# 如果使用 label 索引,则索引区间为闭区间
>>> data.loc[:, :'two']
one two
Ohio 0 1
Colorado 4 5
Utah 8 9
New York 12 13


# 使用 df.loc[val1, val2] 选择多行多列
>>> data.loc[['Colorado', 'Ohio'], ['two', 'three']]
two three
Colorado 5 6
Ohio 1 2

5.3. df.iloc语法

df.iloc[] 索引值为 integers

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
# 使用 df.iloc[where] 选择行
>>> data.iloc[2]
one 8
two 9
three 10
four 11
Name: Utah, dtype: int64
>>> data.iloc[[2,1]]
one two three four
Utah 8 9 10 11
Colorado 4 5 6 7
>>> data.iloc[:2]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7


# 使用 df.iloc[:, where] 选择列
>>> data.iloc[:, 1]
Ohio 1
Colorado 5
Utah 9
New York 13
Name: two, dtype: int64
>>> data.iloc[:, [2, 0]]
three one
Ohio 2 0
Colorado 6 4
Utah 10 8
New York 14 12
>>> data.iloc[:, :2]
one two
Ohio 0 1
Colorado 4 5
Utah 8 9
New York 12 13


# 使用 df.iloc[where_i, where_j] 选择多行多列
>>> data.iloc[2, :2]
one 8
two 9
Name: Utah, dtype: int64
>>> data.iloc[:2, :2]
one two
Ohio 0 1
Colorado 4 5

5.4. Reset Index

可以使用DataFrame的reset_index()函数将index变为DataFrame的一列,原index替换为递增的整数索引

1
2
3
4
5
6
>>> data.reset_index()
index one two three four
0 Ohio 0 1 2 3
1 Colorado 4 5 6 7
2 Utah 8 9 10 11
3 New York 12 13 14 15

这个函数还有一个用处就是可以给每一行编一个号,如果index已为递增的整数索引,再调用reset_index()函数时,就会多出一列,内容为递增整数,相当于给每一行编个号。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> data2 = data.reset_index()
>>> data2
index one two three four
0 Ohio 0 1 2 3
1 Colorado 4 5 6 7
2 Utah 8 9 10 11
3 New York 12 13 14 15
>>> data2.reset_index()
level_0 index one two three four
0 0 Ohio 0 1 2 3
1 1 Colorado 4 5 6 7
2 2 Utah 8 9 10 11
3 3 New York 12 13 14 15

level_0可以用于数据编号。

6. Series数据映射

已知数据

1
2
3
4
5
6
7
8
9
>>> data = pd.DataFrame({'Age': [22, 38, 26, 35, 35],
... 'Sex': ['male', 'female', 'female', 'female', 'male']})
>>> data
Age Sex
0 22 male
1 38 female
2 26 female
3 35 female
4 35 male

Sex 那一列的数据取值有两种,分别为 femalemale,此时想要作一个数据映射

1
2
female -> 0
male -> 1

可以使用 map() 函数,输入映射参数 (映射用一个字典表示)

1
2
3
4
5
6
7
8
9
>>> sex_to_num = {'female': 0, 'male': 1}
>>> data['Sex'] = data['Sex'].map(sex_to_num)
>>> data
Age Sex
0 22 1
1 38 0
2 26 0
3 35 0
4 35 1

7. 缺失值

Note: 本节所述的 NA 代表

1
from numpy import nan as NA

7.1. 缺失值分析

首先分析数据缺失值情况

  1. Series

对于 Series,可以使用 isnull() 函数判断 Series 中的数据是否为缺失值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> data = pd.Series([1, NA, 3.5, NA, 7])
>>> data
0 1.0
1 NaN
2 3.5
3 NaN
4 7.0
dtype: float64
>>> data.isnull()
0 False
1 True
2 False
3 True
4 False
dtype: bool

要判断有多少个缺失值,可以使用 sum() 判断 isnull() 返回的 Boolean 数组中有多少个 True。

1
2
>>> data.isnull().sum()
2
  1. DataFrame

对于 DataFrame,同样可以使用 isnull() 函数判断数据是否为缺失值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> data = pd.DataFrame([[1., 6.5, 3.], [1, NA, NA],
... [NA, NA, NA], [NA, 6.5, 3.]])
>>> data
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
>>> data.isnull()
0 1 2
0 False False False
1 False True True
2 True True True
3 True False False

如果要知道每一列的缺失值是多少,可以使用 info() 函数,或者将 isnull()sum() 配合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 3 columns):
0 2 non-null float64
1 2 non-null float64
2 2 non-null float64
dtypes: float64(3)
memory usage: 176.0 bytes
>>> data.isnull().sum()
0 2
1 2
2 2
dtype: int64

如果要知道缺失值总数,可以作两次 sum()

1
2
>>> data.isnull().sum().sum()
6

7.2. 缺失值补全

处理缺失值的一个方法就是使用插值或者自设的值补全缺失值。首先构造一个有缺失值的 DataFrame

1
2
3
4
5
6
7
8
9
10
11
12
>>> df = pd.DataFrame(np.random.randn(7, 3))
>>> df.iloc[:4, 1] = NA
>>> df.iloc[:2, 2] = NA
>>> df
0 1 2
0 1.309480 NaN NaN
1 -0.542253 NaN NaN
2 -1.129503 NaN -0.396921
3 1.172466 NaN -1.114420
4 0.893893 0.572520 0.648532
5 0.293066 1.875362 -0.426759
6 -0.389516 0.379184 0.759198
  1. 使用自设的值补全缺失值

这里主要使用函数 fillna(),传入一个值,则缺失值就被设为该值

1
2
3
4
5
6
7
8
9
10
# 缺失值使用0补全
>>> df.fillna(0)
0 1 2
0 1.309480 0.000000 0.000000
1 -0.542253 0.000000 0.000000
2 -1.129503 0.000000 -0.396921
3 1.172466 0.000000 -1.114420
4 0.893893 0.572520 0.648532
5 0.293066 1.875362 -0.426759
6 -0.389516 0.379184 0.759198

如果传入的是一个字典,则可以对每一列的缺失值设定不同的补全值

1
2
3
4
5
6
7
8
9
10
# 第二列缺失值用0.5补全,第三列缺失值用0补全
>>> df.fillna({1: 0.5, 2: 0})
0 1 2
0 1.309480 0.500000 0.000000
1 -0.542253 0.500000 0.000000
2 -1.129503 0.500000 -0.396921
3 1.172466 0.500000 -1.114420
4 0.893893 0.572520 0.648532
5 0.293066 1.875362 -0.426759
6 -0.389516 0.379184 0.759198
  1. 插值补全缺失值

这里主要使用函数 fillna()method 参数,例子中数据由于某列第一个元素有缺失,所以方法选择 backfill。

1
2
3
4
5
6
7
8
9
>>> df.fillna(method='backfill')
0 1 2
0 1.309480 0.572520 -0.396921
1 -0.542253 0.572520 -0.396921
2 -1.129503 0.572520 -0.396921
3 1.172466 0.572520 -1.114420
4 0.893893 0.572520 0.648532
5 0.293066 1.875362 -0.426759
6 -0.389516 0.379184 0.759198

7.3. 缺失值删除

  1. 删除有缺失值的行

DataFrame的dropna函数默认是删除有缺失值的行

1
2
3
4
5
6
7
8
9
10
11
>>> df = pd.DataFrame({"name": ['Alfred', 'Batman', 'Catwoman'],
... "toy": [np.nan, 'Batmobile', 'Bullwhip'],
... "born": [pd.NaT, pd.Timestamp("1940-04-25"), pd.NaT]})
>>> df
name toy born
0 Alfred NaN NaT
1 Batman Batmobile 1940-04-25
2 Catwoman Bullwhip NaT
>>> df.dropna()
name toy born
1 Batman Batmobile 1940-04-25
  1. 删除有缺失值的列

设定dropna函数的参数axis为1可以删除有缺失值的列

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> df = pd.DataFrame({"name": ['Alfred', 'Batman', 'Catwoman'],
... "toy": [np.nan, 'Batmobile', 'Bullwhip'],
... "born": [pd.NaT, pd.Timestamp("1940-04-25"), pd.NaT]})
>>> df
name toy born
0 Alfred NaN NaT
1 Batman Batmobile 1940-04-25
2 Catwoman Bullwhip NaT
>>> df.dropna(axis=1)
name
0 Alfred
1 Batman
2 Catwoman

8. 重复值

已知DataFrame

1
2
3
4
5
6
7
8
9
10
11
>>> data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
... 'k2': [1, 1, 2, 3, 3, 4, 4]})
>>> data
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4

8.1. 删除重复值

可以使用函数drop_duplicates()删除重复值

1
2
3
4
5
6
7
8
>>> data.drop_duplicates()
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4

函数drop_duplicates()默认考虑的是全部列,也可以设定某些列来判断是否重复

1
2
3
4
>>> data.drop_duplicates(['k1'])
k1 k2
0 one 1
1 two 1

9. 排序

已知DataFrame

1
2
3
4
5
6
7
>>> df = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})
>>> df
a b
0 0 4
1 1 7
2 0 -3
3 1 2

9.1. 根据某些列进行排序

排序使用函数sort_values(),如果要根据某些列进行排序,可以设定by=参数

1
2
3
4
5
6
7
8
9
10
11
12
>>> df.sort_values(by='a')
a b
0 0 4
2 0 -3
1 1 7
3 1 2
>>> df.sort_values(by=['a', 'b'])
a b
2 0 -3
0 0 4
3 1 2
1 1 7

10. GroupBy

已知DataFrame

1
2
3
4
5
6
7
8
9
10
11
>>> df = pd.DataFrame({'key1':['a','a','b','b','a'],
... 'key2':['one','two','one','two','one'],
... 'data1':np.random.randn(5),
... 'data2':np.random.randn(5)})
>>> df
data1 data2 key1 key2
0 2.462027 0.054159 a one
1 0.283423 -0.658160 a two
2 -0.969307 -0.407126 b one
3 -0.636756 1.925338 b two
4 -0.408266 1.833710 a one

10.1. 查看分组名

DataFrame分了组后,想知道每个分组的名字,可以写为

1
2
3
4
>>> df.groupby('key1').groups
{'a': Int64Index([0, 1, 4], dtype='int64'), 'b': Int64Index([2, 3], dtype='int64')}
>>> df.groupby('key1').groups.keys()
dict_keys(['a', 'b'])

10.2. 分组计算和以及平均值

如果想要根据列key1的值分组计算data1的和,可以写为

1
2
3
4
>>> df['data1'].groupby(df['key1']).sum().reset_index()
key1 data1
0 a 2.337185
1 b -1.606063

或者

1
2
3
4
>>> df.filter(['data1', 'key1']).groupby('key1', as_index=False).sum()
key1 data1
0 a 2.337185
1 b -1.606063

这个as_index=False使得key1的值不作为index。计算平均值只需要将sum()换成mean()即可。

11. Merge

11.1. 笛卡尔乘积

两个集合$X$和$Y$的笛卡尔乘积(Cartesian product),表示为$X \times Y$,是指第一个对象是$X$的成员而第二个对象是$Y$的所有可能有序对的其中一个成员。举个例子,假设集合$A = { a,b }$,集合$B = { 0, 1, 2 }$,则两个集合的笛卡尔积为 ${ (a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2) }$。使用Pandas的merge函数可以实现两个DataFrame的笛卡尔积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> df1 = pd.DataFrame({'A': ['a', 'b', 'c']})
>>> df1
A
0 a
1 b
2 c
>>> df2 = pd.DataFrame({'B': [0, 1, 2]})
>>> df2
B
0 0
1 1
2 2
>>> pd.merge(df1.assign(foo=0), df2.assign(foo=0), on=['foo']).drop(columns=['foo'])
A B
0 a 0
1 a 1
2 a 2
3 b 0
4 b 1
5 b 2
6 c 0
7 c 1
8 c 2

主要是实现思想是增加一列foo,值设为0,然后使用merge函数进行合并。

12. 读取数据

12.1. 读取csv文件

读取csv数据文件需要用到 read_csv() 函数