Numpy练习_数据清洗
Numpy 的一切都是和数据打交道,那么在大数据时代,数据其实是不完整,不完美的。比如下面这张图里面, 你会发现,这份数据显然有些不完整的地方,city有数据缺失,duration 虽然是时间上的数据,但是时间单位不统一,时间格式不统一, 这都给后面我们让机器使用这份数据增加难度。
这次课程我会展示如何用 Numpy 处理 Numpy 比较擅长的数据清理。但是在数据清理上,还有一个更好用的 Pandas 库,我们会在后期的教学当中,一步步深入到。
脏数据
如果你真有做过数据分析,当你拿到一份数据的时候,是十分期望这份数据没什么问题的,但是事与愿违,通常这份数据都多多少少有些问题。 常见的脏数据种类有:
- 数据值缺失
- 数据值异常大或小
- 格式错误
- 非独立数据错误
一般来说,你遇到的数都是需要从外部存储读取出来的,但是为了简化这节的内容,我们直接用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计算的。而只有dtype
为int
,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:])
总结
好了,数据清洗的工作已经告一段落啦。你会发现,即使是这么小规模的数据,我们也能体验到多种不同情况的数据清洗工作:
- 数据值缺失
- 数据值异常大或小
- 格式错误
- 非独立数据错误
有了这次体验,我相信你在自己的数据上也能更清楚如何操作了。