数据可以是重复数据、未观测数据和异常数据(离群值),可以有不存在的地址、错误的电话号码、区号,不准确的地理坐标、错误的日期,不正确的标签、大小写字母混乱、尾随空格以及许多其它更小的问题。数据工程师的工作就是清理数据,这样才能建立一个统计或者学习的机器学习的模型
检查重复数据、未观测数据和异常数据(离群值)
重复数据
重复数据是在数据集中出现在不同行,但是仔细检查后是相同的观测数据。
如果你的数据是由某种ID形式来区分每条记录(或者例如将记录和特定的用户关联)的,那么最初看上去重复的可能不是重复数据;有时系统失败产生了错误的ID。在这种情况下,需要检查同一个ID是否真的是一个重复数据或者你需要一个新的ID的系统。
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = spark.createDataFrame([
(1, 144.5, 5.9, 33, 'M'),
(2, 167.2, 5.4, 45, 'M'),
(3, 124.1, 5.2, 23, 'F'),
(4, 144.5, 5.9, 33, 'M'),
(5, 133.1, 5.7, 54, 'F'),
(3, 124.1, 5.2, 23, 'F'),
(5, 129.2, 5.3, 42, 'M')
], ['id', 'weight', 'height', 'age', 'gender'])
- 有两行ID等于3并且完全相同。
- ID为1和4的两行是一样的————唯一不同的是他们的ID,因此我们完全可以假定它们是同一个人的数据
- 有两行的ID等于5,但似乎是一个记录问题,因为它们看上去不是同一个人的数据
检查是否有重复数据:比较完整数据集和运行.distinct() 方法后的数据集的数量:
print('Count of rows: {0}'.format(df.count()))
print('Count of distinct rows: {0}'.format(df.distinct().count()))
Count of rows: 7
Count of distinct rows: 6
使用.dropDuplicates(…)方法将这些重复的行移除:
df = df.dropDuplicates()
df.show()
+---+------+------+---+------+
| id|weight|height|age|gender|
+---+------+------+---+------+
| 5| 129.2| 5.3| 42| M|
| 1| 144.5| 5.9| 33| M|
| 5| 133.1| 5.7| 54| F|
| 4| 144.5| 5.9| 33| M|
| 2| 167.2| 5.4| 45| M|
| 3| 124.1| 5.2| 23| F|
+---+------+------+---+------+
删除了一行ID为3的行。现在检查数据中是否有任何和ID无关的重复数据。
print('Count of ids: {0}'.format(df.count()))
print('Count of distinct ids: {0}'.format(
df.select([c for c in df.columns if c != 'id']).distinct().count()
))
Count of ids: 6
Count of distinct ids: 5
df = df.dropDuplicates(subset=[c for c in df.columns if c != 'id'])
df.show()
+---+------+------+---+------+
| id|weight|height|age|gender|
+---+------+------+---+------+
| 1| 144.5| 5.9| 33| M|
| 2| 167.2| 5.4| 45| M|
| 3| 124.1| 5.2| 23| F|
| 5| 129.2| 5.3| 42| M|
| 5| 133.1| 5.7| 54| F|
+---+------+------+---+------+
检查是否有重复的ID。计算ID的总数和ID的唯一个数,可以使用.agg(…)
# 导入方法
import pyspark.sql.functions as fn
df.agg(
fn.count('id').alias('count'),
fn.countDistinct('id').alias('distinct')
).show()
+-----+--------+
|count|distinct|
+-----+--------+
| 5| 4|
+-----+--------+
分别使用.count(…)和.countDistinct(…)计算DataFrame的行数和id的唯一数。.alias(…)
df.withColumn('new_id', fn.monotonically_increasing_id()).show()
+---+------+------+---+------+-------------+
| id|weight|height|age|gender| new_id|
+---+------+------+---+------+-------------+
| 1| 144.5| 5.9| 33| M| 171798691840|
| 2| 167.2| 5.4| 45| M| 592705486848|
| 3| 124.1| 5.2| 23| F|1236950581248|
| 5| 129.2| 5.3| 42| M|1365799600128|
| 5| 133.1| 5.7| 54| F|1511828488192|
+---+------+------+---+------+-------------+
fn.monotonically_increasing_id()方法给每一条记录提供了一个唯一并且递增的ID。
未观测数据
空白数据集:这种缺失值原因很多:系统故障,人为失误,数据模式更改
处理缺失值最简单的方法是发现任何缺失值时都要移除所有观测数据。
根据数据集中缺失值的分布,移除可能会对数据集的可能性造成严重的影响。
另一个解决缺失值观测数据是在None的位置填充一些值。根据数据类型,可以选择以下几种:
- 如果数据是离散布尔型:可以通过添加第三个类型Missing,将其转换为一个分类变量
- 如果数据正在处理顺迅雷或则数值类数据,可以填充平均数、中位数、众数或者一些其它预定义的值(根据数据分布形状而定
df_miss = spark.createDataFrame([
(1, 143.5, 5.6, 28, 'M', 100000),
(2, 167.2, 5.4, 45, 'M', None),
(3, None, 5.2, None, None, None),
(4, 144.5, 5.9, 33, 'M', None),
(5, 133.2, 5.7, 54, 'F', None),
(6, 124.1, 5.2, None, 'F', None),
(7, 129.2, 5.3, 42, 'M', 76000),
], ['id', 'wight', 'height', 'age', 'gender', 'income'])
处理缺失值的类别
-
ID为3的行只有一条有用的信息——高度(height)
-
ID为6的行只有一个缺失值——年龄(age)
分析这些列,可以发现以下内容: -
income列的发部分值都是缺失,因为它是会透漏非常私人的事情
-
weight列和gender列都各有一个缺失值
-
age列有两个缺失值
# 查看每行的缺失数据
df_miss.rdd.map(
lambda row: (row['id'], sum([c == None for c in row]))).collect()
[(1, 0), (2, 1), (3, 4), (4, 1), (5, 1), (6, 2), (7, 0)]
查看缺失的数据,可以决定是否要一起移除观测数据或者填充观测数据:
df_miss.where('id == 3').show()
+---+-----+------+----+------+------+
| id|wight|height| age|gender|income|
+---+-----+------+----+------+------+
| 3| null| 5.2|null| null| null|
+---+-----+------+----+------+------+
检查每一列中缺失的观测数据的百分比:
df_miss.agg(*[
(1 - (fn.count(c) / fn.count('*'))).alias( c + '_missing')
for c in df_miss.columns
]).show()
+----------+------------------+--------------+------------------+------------------+------------------+
|id_missing| wight_missing|height_missing| age_missing| gender_missing| income_missing|
+----------+------------------+--------------+------------------+------------------+------------------+
| 0.0|0.1428571428571429| 0.0|0.2857142857142857|0.1428571428571429|0.7142857142857143|
+----------+------------------+--------------+------------------+------------------+------------------+
income列缺失数据为72%,所以移除income列
df_miss_no_income = df_miss.select([
c for c in df_miss.columns if c != 'income'
])
'weight’列和’age‘中有足够的观测数据来计算平均值并且填充缺失值的地方。
如果决定移除观测数据,可以使用.dropna(…)方法。还可利用thresh参数,该参数为每一行缺失观测数据的数量指定一个阈值,限定要移除的行。
如果是一个具有几十个或这几百个特征的数据集,并且只想移除这些超出某个缺失值阈值的行:
df_miss_no_income.dropna(thresh=3).show()
+---+-----+------+----+------+
| id|wight|height| age|gender|
+---+-----+------+----+------+
| 1|143.5| 5.6| 28| M|
| 2|167.2| 5.4| 45| M|
| 4|144.5| 5.9| 33| M|
| 5|133.2| 5.7| 54| F|
| 6|124.1| 5.2|null| F|
| 7|129.2| 5.3| 42| M|
+---+-----+------+----+------+
另一方面,要填充观测数据,可以使用.fillna(…)方法。该方法能填充单个整型(integer)(长整型long也可以)、浮点型(float)或者字符串(string);整个数据集中所有缺失的值都将用该值来填充。
如果哟啊填充一个平均数、中间值或者其它计算值,需要先计算出这个值,创建一个带值的代码字典,在传递给.fillna方法。
means = df_miss_no_income.agg(
*[
fn.mean(c).alias(c)
for c in df_miss_no_income.columns
if c != 'gender'
]).toPandas().to_dict('records')[0]
means['gender'] = 'missing'
df_miss_no_income.fillna(means).show()
+---+------------------+------+---+-------+
| id| wight|height|age| gender|
+---+------------------+------+---+-------+
| 1| 143.5| 5.6| 28| M|
| 2| 167.2| 5.4| 45| M|
| 3|140.28333333333333| 5.2| 40|missing|
| 4| 144.5| 5.9| 33| M|
| 5| 133.2| 5.7| 54| F|
| 6| 124.1| 5.2| 40| F|
| 7| 129.2| 5.3| 42| M|
+---+------------------+------+---+-------+
离群值
异常数据(离群值)指的是那些与样本其余部分的分布显著偏离的观测数据。如果值大致在Q1-1.5IQR和Q3+1.5IQR范围内,IQR指四分位范围,你可以认为没有离群值;IQR定义为上分位(upper-quartile)和下分位(lower-quartile)之差,也就是分别为75个百分位(Q3)和第25个百分位(Q1)。
df_outliers = spark.createDataFrame([
(1, 143.5, 5.3, 28),
(2, 154.2, 5.5, 45),
(3, 342.3, 5.1, 99),
(4, 144.5, 5.5, 33),
(5, 133.2, 5.4, 54),
(6, 124.1, 5.1, 21),
(7, 129.2, 5.3, 42),
],['id', 'weight', 'height', 'age'])
cols = ['weight', 'height', 'age']
bounds = {}
for col in cols:
quantiles = df_outliers.approxQuantile(
col, [0.25, 0.75], 0.05
)
IQR = quantiles[1] - quantiles[0]
bounds[col] = [
quantiles[0] - 1.5 * IQR,
quantiles[1] + 1.5 * IQR
]
outliers = df_outliers.select(*['id'] + [
(
(df_outliers[c] < bounds[c][0]) |
(df_outliers[c] > bounds[c][1])
).alias(c + '_o') for c in cols
])
outliers.show()
+---+--------+--------+-----+
| id|weight_o|height_o|age_o|
+---+--------+--------+-----+
| 1| false| false|false|
| 2| false| false|false|
| 3| true| false| true|
| 4| false| false|false|
| 5| false| false|false|
| 6| false| false|false|
| 7| false| false|false|
+---+--------+--------+-----+
bounds
{'weight': [91.69999999999999, 191.7],
'height': [4.499999999999999, 6.1000000000000005],
'age': [-11.0, 93.0]}
df_outliers = df_outliers.join(outliers, on='id')
df_outliers.filter('weight_o').select('id', 'weight').show()
df_outliers.filter('age_o').select('id', 'age').show()