Numpy练习_数据清洗

​ Numpy 的一切都是和数据打交道,那么在大数据时代,数据其实是不完整,不完美的。比如下面这张图里面, 你会发现,这份数据显然有些不完整的地方,city有数据缺失,duration 虽然是时间上的数据,但是时间单位不统一,时间格式不统一, 这都给后面我们让机器使用这份数据增加难度。

​ 这次课程我会展示如何用 Numpy 处理 Numpy 比较擅长的数据清理。但是在数据清理上,还有一个更好用的 Pandas 库,我们会在后期的教学当中,一步步深入到。

脏数据

​ 如果你真有做过数据分析,当你拿到一份数据的时候,是十分期望这份数据没什么问题的,但是事与愿违,通常这份数据都多多少少有些问题。 常见的脏数据种类有:

  1. 数据值缺失
  2. 数据值异常大或小
  3. 格式错误
  4. 非独立数据错误

​ 一般来说,你遇到的数都是需要从外部存储读取出来的,但是为了简化这节的内容,我们直接用Python的List来做一份数据。想要学习使用Numpy读取保存数据的话,可以看下一篇文章。

​ 自造数据

raw_data = [
["Name", "StudentID", "Age", "AttendClass", "Score"],
["小明", 20131, 10, 1, 67],
["小花", 20132, 11, 1, 88],
["小菜", 20133, None, 1, "98"],
["小七", 20134, 8, 1, 110],
["花菜", 20134, 98, 0, None],
["刘欣", 20136, 12, 0, 12]
]
print(raw_data)

​ 这是一份学生上课的考试的数据,里面记录了学生姓名,ID,年龄,是否上过课(1-上过,0-没上),课程成绩信息。 比这复杂的数据还有很多,但是这份数据中的脏数据类型基本上都涵盖了我上面讲的那些。

​ 首先,我们要做的就是把这份 Python list 数据转化成 Numpy 数据。其实用 Python 列表也能做数据清洗工作, 但为什么要用 Numpy 来做呢?这不得不提到 Numpy 面对大数据时,它得天独厚的计算速度优势了。同样一份 1GB 的数据,Python List 处理起来可能要 1 小时, Numpy 优化后,速度可以成倍增长。

​ 首先,我们尝试直接转一下 Python list 到 Numpy Array,看看会不会有问题。

import numpy as np
data = np.array(raw_data)
data

​ 运行结果

array([['Name', 'StudentID', 'Age', 'AttendClass', 'Score'],
       ['小明', 20131, 10, 1, 67],
       ['小花', 20132, 11, 1, 88],
       ['小菜', 20133, None, 1, '98'],
       ['小七', 20134, 8, 1, 110],
       ['花菜', 20134, 98, 0, None],
       ['刘欣', 20136, 12, 0, 12]], dtype=object)

​ 你会发现,这时的 array 输出的结果,结尾处有一个标识。dtype=object,这是什么意思呢?(剧透一下:这种 dtype 会对后续数据处理带来很多麻烦) 我们对比一下如果是纯数据的格式,numpy 的 dtype 是什么样。

test1 = np.array([1,2,3])
test2 = np.array([1.1,2.3,3.4])
test3 = np.array([1,2,3], dtype=np.float)
print("test1.dtype", test1.dtype)
print("test2.dtype", test2.dtype)
print("test3.dtype", test3.dtype)

print("test2 > 2 ", test2 > 2)
print("data > 2", data > 2) # 这里会报错
#TypeError: '>' not supported between instances of 'str' and 'int'

​ 我们可以看到不同的array创建方式,可能有不同的dtype,而之前的data.dtype=object,说明Python list直接转换的 data是无法参与诸多Numpy计算的。而只有dtypeint,float这样的数值格式,才能参与运算。

数据预处理

​ 那怎么办呢?我们怎么才能把那个又有 string,又有 None,还有数值的数据转换为 Numpy 能运算的数据呢? 方法很简单,我们筛选过滤一下。

data_process = []
for i in range(len(raw_data)):
    if i == 0:
        continue    # 不要首行字符串
    # 去掉首列名字
    data_process.append(raw_data[i][1:])
data = np.array(data_process, dtype=np.float)
print("data.dtype", data.dtype)
print(data) 

​ 运行结果

[[2.0131e+04 1.0000e+01 1.0000e+00 6.7000e+01]
 [2.0132e+04 1.1000e+01 1.0000e+00 8.8000e+01]
 [2.0133e+04        nan 1.0000e+00 9.8000e+01]
 [2.0134e+04 8.0000e+00 1.0000e+00 1.1000e+02]
 [2.0134e+04 9.8000e+01 0.0000e+00        nan]
 [2.0136e+04 1.2000e+01 0.0000e+00 1.2000e+01]]

​ 有同学可能注意到了,这份数据没有 float,应该是全部是整数 int,为什么我在创建 array 的时候,选的 dtype=np.float 呢? 那是因为 dtype=np.int 会报错呀,不信你试试。因为这份数据中存在 None,而只有 np.float 能转换 None,所以不是我不想,而是人家不让。

另外有趣的一个点是,我们在原始数据中有一个字符串的 98,这个字符串也被转换成数字了,可见 numpy 还自动帮我们处理了一部份能被转换的数字。

好了,现在总算是把这个数据的预处理做好了,接下来就要来清洗数据啦。

清洗数据

​ 怎么清洗脏数据,取决于你要拿这个数据做什么,很明显,我们想要看看有没有什么数据是不合逻辑的。比如我发现学号有重复,可能是在输入学生信息的时候手误输错了。

sid = data[:, 0]
unique, counts = np.unique(sid, return_counts=True)
print(counts)
#[1 1 1 2 1]

np.unique() 功能为用来查找数据中独一无二的数据的,return_counts 让我能清楚看到是数据的重复数量。 比如有一个数据重复出现了 2 次。我们来看看是哪个数据出现了多次。

print(unique[counts > 1])
#[20134.]

​ 然后综合判断,我们的数据中少了一个 20135,可能就是把某个同学的学号输错了,我们将错误的同学修改过来。

data[4, 0] = 20135
print(data)

​ 学号应该就没有大问题了。我们来看看第二列 Age 年龄。一眼可以发现,存在两个问题,有一个同学没有年龄。我们看看能不能用平均年龄补全这个缺失的信息。

is_nan = np.isnan(data[:,1])
print("is_nan:", is_nan)
nan_idx = np.argwhere(is_nan)

# 计算有数据的平均年龄,用 ~ 符号可以 True/False 对调
mean_age = data[~np.isnan(data[:,1]), 1].mean()
print("有数据的平均年龄:", mean_age)

呀!为啥平均年龄有 27.8 岁,不正常啊!是的,在你处理自己的数据的时候,也会遇到不太正常的情况。 是哪里出问题了呢?原来仔细看,我们居然有一个 98 岁的学生。 这不太正常啊,所以我觉得我也得把这个 98 岁的当异常数据看待。(在你的项目中,你也要综合判断哪些是异常数据)。我也没什么好办法,就用除开 98 和 nan 的数据, 计算其它数的平均值代替他们吧。

# ~ 表示 True/False 对调,& 就是逐个做 Python and 的运算
normal_age_mask = ~np.isnan(data[:,1]) & (data[:,1] < 20)
print("normal_age_mask:", normal_age_mask)

normal_age_mean = data[normal_age_mask, 1].mean()
print("normal_age_mean:", normal_age_mean)

data[~normal_age_mask, 1] = normal_age_mean
print("ages:", data[:, 1])

​ 到这一步之后,我们要看看上课和成绩的数据需不需要清洗。按理来说,如果没有上课(0),课程的成绩应该是不存在的(nan);上课了(1)才会有成绩。 所以我们观察下面两组数据:

print(data[-3:, 2:])

​ 你会发现,倒数第二行的数据还算是 ok 的,因为没上课,就没成绩,但是倒数第一行,没上课,怎么还有成绩?还有倒数第三行,成绩居然超出了满分 100 分。这些情况都是我们要处理的情况。

# 没上课的转成 nan
data[data[:,2] == 0, 3] = np.nan

# 超过 100 分和低于 0 分的都处理一下
data[:, 3] = np.clip(data[:, 3], 0, 100)

print(data[:, 2:])

总结

好了,数据清洗的工作已经告一段落啦。你会发现,即使是这么小规模的数据,我们也能体验到多种不同情况的数据清洗工作:

  1. 数据值缺失
  2. 数据值异常大或小
  3. 格式错误
  4. 非独立数据错误

有了这次体验,我相信你在自己的数据上也能更清楚如何操作了。