清风的技术小屋

不为所动, 做更专业的自己

0%

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

介绍

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

Note: 如果没有特别说明,np 指的是 numpy,代表导入的 numpy

1
import numpy as np

PS: 网上找到一份Numpy的CheatSheet,内容不错,感兴趣的可以去下载:Numpy CheatSheet

转换ndarray数据类型

如果想转换ndarray的数据类型,可以使用ndarray的 astype 方法

1
2
3
4
5
6
>>> arr = np.array([1, 2, 3, 4, 5])
>>> arr.dtype
dtype('int64')
>>> float_arr = arr.astype(np.float64)
>>> float_arr.dtype
dtype('float64')

Note: 调用 astype 方法会生成新的数组,因此需要赋值到一个变量上。

切片不会生成新的数组

对数组进行切片后,返回的数组并不是原始数组的拷贝,只是一个对原始数组的引用,如果对切片后的数组进行数据修改,原始数组相应的位置数据会被修改。

1
2
3
4
5
6
7
8
9
10
11
12
>>> arr = np.arange(10)
>>> arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> new_arr = arr[5:8]
>>> new_arr
array([5, 6, 7])

>>> new_arr[:] = 12
>>> new_arr
array([12, 12, 12])
>>> arr
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])

如果想要切片后的数组是原始数组的拷贝,需要调用 copy() 方法。

1
>>> new_arr = arr[5:8].copy()

Axis编号

调用一些Numpy函数时会遇到设置 axis 参数,该参数可以设为 01 ,对于这两个值表示的意义,可以参考下图

也就是说axis 0 代表行方向,axis 1 代表列方向。例如,我们使用 mean() 计算矩阵的平均值时,A.mean(axis=0) 代表每个平均值是沿着行方向计算可得,A.mean(axis=1) 代表每个平均值是沿着列方向计算可得。

1
2
3
4
5
6
7
8
>>> A = np.random.randn(2, 3)
>>> A
array([[-0.40797393, 0.24059956, -1.57582642],
[ 0.31626161, -0.07033558, 0.58346107]])
>>> A.mean(axis=0)
array([-0.04585616, 0.08513199, -0.49618267])
>>> A.mean(axis=1)
array([-0.58106693, 0.27646237])

A.mean(axis=0) 大小为矩阵A列的个数,A.mean(axis=1)大小为矩阵A行的个数。

统计Boolean数组中True的个数

Boolean值通常可以转换成 0 (False) 或 1 (True),因此可以使用 sum() 函数统计Boolean数组中 True 的个数

1
2
3
4
5
>>> bools = np.array([False, True, True, False, False])
>>> bools.sum()
2
>>> np.sum(bools)
2

既然可以知道数组中 True 的个数,自然也可以知道 True 所占的比例,此时可以用 mean() 函数进行计算

1
2
3
4
>>> bools.mean()
0.4
>>> np.mean(bools)
0.4

环境

OS: OS X 10.11.6

Shell: bash and zsh

Other: iTerm2

快捷键列表

快捷键 功能
Option + Left or Right 光标按单词前移或后移
Control + F or B 光标向前或向后移动一个字符
Control + D 删除当前光标的字符
Control + H 删除光标之前的字符
Control + W 删除光标之前的单词
Command + A 移动光标到行首
Command + E 移动光标到行尾巴
Control + U 清除整行内容
Control + K 删除光标之后的内容
Control + C Kill掉当前运行的程序
Control + Z 暂停当前运行程序,要恢复运行使用 fg process_name

快捷键详细信息

Option + Left or Right

如果使用iTerm2作为终端模拟器时,该快捷键不是所预想的功能,此时需要进行设置,这里贴出[4]提供的教程

Step 1: 在 Profiles-Keys 下设置 Left ⌥ key to act as Esc+

Step 2: 添加一个快捷键 ⌥ ←,快捷键设置内容为

  • Keyboard Shortcut: ⌥ ←
  • Action: Send Escape Sequence
  • Esc+: b

Step 3: 添加一个快捷键 ⌥ →,快捷键设置内容为

  • Keyboard Shortcut: ⌥ →
  • Action: Send Escape Sequence
  • Esc+: f

参考

  1. iTerm2 快捷键大全
  2. The Best Keyboard Shortcuts for Bash (aka the Linux and macOS Terminal)
  3. 20 Terminal shortcuts developers need to know
  4. Use ⌥ ← and ⌥→ to jump forwards / backwards words in iTerm 2, on OS X

1. 判断文件或目录是否存在

创建一个目录和一个文件

1
2
3
4
5
$ mkdir dir1 && touch file1.txt
$ ls -l
total 0
drwxr-xr-x 2 luowanqian wheel 68 5 2 23:21 dir1
-rw-r--r-- 1 luowanqian wheel 0 5 2 23:21 file1.txt

使用 os.path.exists() 可以判断文件或目录是否存在,但是不能判断是该路径是一个文件还是目录,要进一步判断,需要使用 os.path.isfile(),如果该路径是一个文件,则返回 True,否则返回 False,当然,也可以直接使用 os.path.isfile() 判断文件是否存在。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import os
>>> os.path.exists('dir1')
True
>>> os.path.exists('file1.txt')
True
>>> os.path.isfile('dir1')
False
>>> os.path.isfile('file1.txt')
True

>>> os.path.exists('dir2')
False
>>> os.path.exists('file2.txt')
False
>>> os.path.isfile('dir2')
False
>>> os.path.isfile('file2.txt')
False

目录 dir2 和文件 file2.txt 均不存在,所以函数 os.path.exists()os.path.isfile() 均返回 False。

2. ParameterGrid

机器学习算法最常见的调参方法是网格搜索,需要将多组参数进行组合,Scikit-learn提供了一个类 ParametGrid 可以生成所有的参数组合,这里提取其关键代码单独写成一个生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from itertools import product


def parameters_grid(parameter_map):
items = sorted(parameter_map.items())
if not items:
yield {}
else:
keys, values = zip(*items)
for v in product(*values):
params = dict(zip(keys, v))
yield params


if __name__ == '__main__':
parameter_map = {'a': [1, 2], 'b': [True, False]}
for params in parameters_grid(parameter_map):
print(params)

代码运行结果

1
2
3
4
{'a': 1, 'b': True}
{'a': 1, 'b': False}
{'a': 2, 'b': True}
{'a': 2, 'b': False}

3. 批量下载图片

这里使用requests库批量下载图片,为了加快下载速度,还实现了多线程下载,同时为了避免一次下载失败,脚本支持自动重试下载。没有处理具体的异常,只是捕获异常后输出异常信息。

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
import os
import requests
from retrying import retry
from io import BytesIO
from PIL import Image
import progressbar
import concurrent.futures as concurrent


@retry(stop_max_attempt_number=10)
def download(image_url, image_path):
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))
img.save(image_path)


if __name__ == '__main__':
base_url = 'http://www.baidu.com'
num_images = 3
suffix = '.jpg'
image_dir = 'images'

num_workers = 4

if not os.path.isdir(image_dir):
os.mkdir(image_dir)

with concurrent.ThreadPoolExecutor(max_workers=num_workers) as executor:
image_urls = []
image_paths = []
for image_id in range(num_images):
url = base_url + '/' + str(image_id + 1) + suffix
file_path = os.path.join(image_dir, str(image_id + 1) + suffix)
image_urls.append(url)
image_paths.append(file_path)

tasks = {
executor.submit(download, url, file_path):
(url, file_path) for url, file_path in zip(image_urls, image_paths)
}

i = 0
total = len(image_urls)
pbar = progressbar.ProgressBar(max_value=total).start()
for task in concurrent.as_completed(tasks):
url, file_path = tasks[task]
try:
task.result()
i = i + 1
pbar.update(i)
except Exception as exc:
print('{} generated an exception: {}'.format(url, exc))
pbar.finish()

4. 遍历文件夹中所有文件

首先目录结构如下:

1
2
3
4
5
6
7
$ tree test
test
├── 1.txt
├── 2.txt
└── test2
├── 3.txt
└── 4.txt

使用os.walk()遍历test目录,代码如下:

1
2
3
4
5
6
root_dir = '/tmp/test'
for root, dirs, files in os.walk(root_dir, topdown=True):
for name in files:
print(os.path.join(root, name))
for name in dirs:
print(os.path.join(root, name))

得到结果如下:

1
2
3
4
5
/tmp/test/1.txt
/tmp/test/2.txt
/tmp/test/test2
/tmp/test/test2/3.txt
/tmp/test/test2/4.txt

如果设置topdown=False,得到结果如下:

1
2
3
4
5
/tmp/test/test2/3.txt
/tmp/test/test2/4.txt
/tmp/test/1.txt
/tmp/test/2.txt
/tmp/test/test2

5. 计算函数运行时间

这里使用装饰器来计算函数运行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time
from functools import wraps


def timethis(func):
"""
Decorator that reports the execution time
"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)

return result

return wrapper


@timethis
def countdown(n):
while n > 0:
n -= 1

6. 判断对象是否可迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def isiterable(obj):
try:
obj = iter(obj)
except:
return False
else:
return True


if __name__ == "__main__":
a = [1, 2]
b = 3
print(f"'a' is {'iterable' if isiterable(a) else 'not iterable'}")
print(f"'b' is {'iterable' if isiterable(b) else 'not iterable'}")

执行结果

1
2
'a' is iterable
'b' is not iterable

7. 判断操作系统类型

可以使用 sys.platform 判断当前是什么操作系统。常见的操作系统,其返回值如下

System platform value
AIX 'aix'
Linux 'linux'
Windows 'win32'
Windows/Cygwin 'cygwin'
macOS 'darwin'

官方推荐使用 startswith() 判断系统类型 (见 sys.platform),这里贴一段测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys


def identify_platform():
platform = sys.platform
if platform.startswith("freebsd"):
return "freebsd"
elif platform.startswith("linux"):
return "linux"
elif platform.startswith("aix"):
return "aix"
elif platform.startswith("win"):
return "windows"
elif platform.startswith("darwin"):
return "macos"
else:
return "unknown"


if __name__ == "__main__":
print(f"Platform: {identify_platform()}")