45丨数据清洗:如何使用SQL对数据进行清洗?
下载APP
关闭
渠道合作
推荐作者
45丨数据清洗:如何使用SQL对数据进行清洗?
2019-09-23 陈旸 来自北京
《SQL必知必会》
课程介绍
讲述:陈旸
时长11:05大小12.68M
SQL 可以帮我们进行数据处理,总的来说可以分成 OLTP 和 OLAP 两种方式。
OLTP 称之为联机事务处理,我们之前讲解的对数据进行增删改查,SQL 查询优化,事务处理等就属于 OLTP 的范畴。它对实时性要求高,需要将用户的数据有效地存储到数据库中,同时有时候针对互联网应用的需求,我们还需要设置数据库的主从架构保证数据库的高并发和高可用性。
OLAP 称之为联机分析处理,它是对已经存储在数据库中的数据进行分析,帮我们得出报表,指导业务。它对数据的实时性要求不高,但数据量往往很大,存储在数据库(数据仓库)中的数据可能还存在数据质量的问题,比如数据重复、数据中有缺失值,或者单位不统一等,因此在进行数据分析之前,首要任务就是对收集的数据进行清洗,从而保证数据质量。
对于数据分析工作来说,好的数据质量才是至关重要的,它决定了后期数据分析和挖掘的结果上限。数据挖掘模型选择得再好,也只能最大化地将数据特征挖掘出来。
高质量的数据清洗,才有高质量的数据。今天我们就来看下,如何用 SQL 对数据进行清洗。
想要进行数据清洗有怎样的准则呢?
如何使用 SQL 对数据进行清洗?
如何对清洗之后的数据进行可视化?
数据清洗的准则
我在《数据分析实战 45 讲》里专门讲到过数据清洗的原则,这里为了方便你理解,我用一个数据集实例讲一遍。
数据集格式为 csv,一共有两种文件:train.csv 是训练数据集,包含特征信息和存活与否的标签;test.csv 是测试数据集,只包含特征信息。
数据集中包括了以下字段,具体的含义如下:
训练集给出了 891 名乘客幸存与否的结果,以及相关的乘客信息。通过训练集,我们可以对数据进行建模形成一个分类器,从而对测试集中的乘客生存情况进行预测。不过今天我们并不讲解数据分析的模型,而是来看下在数据分析之前,如何对数据进行清洗。
首先,我们可以通过 Navicat 将 CSV 文件导入到 MySQL 数据库中,然后浏览下数据集中的前几行,可以发现数据中存在缺失值的情况还是很明显的。
数据存在数据缺失值是非常常见的情况,此外我们还需要考虑数据集中某个字段是否存在单位标识不统一,数值是否合法,以及数据是否唯一等情况。要考虑的情况非常多,这里我将数据清洗中需要考虑的规则总结为 4 个关键点,统一起来称之为“完全合一”准则,你可以点这里看一下。
“完全合一”是个通用的准则,针对具体的数据集存在的问题,我们还需要对症下药,采取适合的解决办法,甚至为了后续分析方便,有时我们还需要将字符类型的字段替换成数值类型,比如我们想做一个 Steam 游戏用户的数据分析,统计数据存储在两张表上,一个是 user_game 数据表,记录了用户购买的各种 Steam 游戏,其中数据表中的 game_title 字段表示玩家购买的游戏名称,它们都采用英文字符的方式。另一个是 game 数据表,记录了游戏的 id、游戏名称等。因为这两张表存在关联关系,实际上在 user_game 数据表中的 game_title 对应了 game 数据表中的 name,这里我们就可以用 game 数据表中的 id 替换掉原有的 game_title。替换之后,我们在进行数据清洗和质量评估的时候也会更清晰,比如如果还存在某个 game_title 没有被替换的情况,就证明这款游戏在 game 数据表中缺少记录。
使用 SQL 对预测数据集进行清洗
了解了数据清洗的原则之后,下面我们就用 SQL 对泰坦尼克号数据集中的训练集进行数据清洗,也就是 train.csv 文件。我们先将这个文件导入到 titanic_train 数据表中:
检查完整性
在完整性这里,我们需要重点检查字段数值是否存在空值,在此之前,我们需要先统计每个字段空值的个数。在 SQL 中,我们可以分别统计每个字段的空值个数,比如针对 Age 字段进行空值个数的统计,使用下面的命令即可:
运行结果为 177。
当然我们也可以同时对多个字段的非空值进行统计:
运行结果:
不过这种方式适用于字段个数较少的情况,如果一个数据表存在几十个,甚至更多的字段,那么采用这种方式既麻烦又容易出错。这时我们可以采用存储过程的方式,用程序来进行字段的空值检查,代码如下:
我来说下这个存储过程的作用,首先我定义了两个输入的参数 schema_name 和 table_name2,用来接收想要检查的数据库的名称以及数据表名。
然后使用游标来操作读取出来的 column_name,赋值给变量 temp_column。对于列名,我们需要检查它是否为空,但是这个列名在 MySQL 中是动态的,我们无法使用 @temp_column 来表示列名,对其进行判断,在这里我们需要使用 SQL 拼接的方式,这里我设置了 @temp_query 表示想要进行查询的 SQL 语句,然后设置 COUNT(*) 的别名为动态别名,也就是 temp_column 加上 _null_num,同样在 WHERE 条件判断中,我们使用 temp_column 进行动态列名的输出,以此来判断这个列数值是否为空。
然后我们执行这个 SQL 语句,提取相应的结果。
运行结果如下:
为了浏览方便我调整了运行结果的格式,你能看到在 titanic_train 数据表中,有 3 个字段是存在空值的,其中 Cabin 空值数最多为 687 个,Age 字段空值个数 177 个,Embarked 空值个数 2 个。
既然存在空值的情况,我们就需要对它进行处理。针对缺失值,我们有 3 种处理方式。
删除:删除数据缺失的记录;
均值:使用当前列的均值;
高频:使用当前列出现频率最高的数据。
对于 Age 字段,这里我们采用均值的方式进行填充,但如果直接使用 SQL 语句可能会存在问题,比如下面这样。
这时会报错:
也就是说同一条 SQL 语句不能先查询出来部分内容,再同时对当前表做修改。
这种情况下,最简单的方式就是复制一个临时表 titanic_train2,数据和 titanic_train 完全一样,然后再执行下面这条语句:
这里使用了 ROUND 函数,对 age 平均值 AVG(age) 进行四舍五入,只保留小数点后一位。
针对 Cabin 这个字段,我们了解到这个字段代表用户的船舱位置,我们先来看下 Cabin 字段的数值分布情况:
运行结果:
从结果中能看出 Cabin 字段的数值分布很广,而且根据常识,我们也可以知道船舱位置每个人的差异会很大,这里既不能删除掉记录航,又不能采用均值或者高频的方式填充空值,实际上这些空值即无法填充,也无法对后续分析结果产生影响,因此我们可以不处理这些空值,保留即可。
然后我们来看下 Embarked 字段,这里有 2 个空值,我们可以采用该字段中高频值作为填充,首先我们先了解字段的分布情况使用:
运行结果:
我们可以直接用 S 来对缺失值进行填充:
至此,对于 titanic_train 这张数据表中的缺失值我们就处理完了。
检查全面性
在这个过程中,我们需要观察每一列的数值情况,同时查看每个字段的类型。
因为数据是直接从 CSV 文件中导进来的,所以每个字段默认都是 VARCHAR(255) 类型,但很明显 PassengerID、Survived、Pclass 和 Sibsp 应该设置为 INT 类型,Age 和 Fare 应该设置为 DECIMAL 类型,这样更方便后续的操作。使用下面的 SQL 命令即可:
然后我们将其余的字段(除了 Cabin)都进行 NOT NULL,这样在后续进行数据插入或其他操作的时候,即使发现数据异常,也可以对字段进行约束规范。
在全面性这个检查阶段里,除了字段类型定义需要修改以外,我们没有发现其他问题。
然后我们来检查下合法性及唯一性。合法性就是要检查数据内容、大小等是否合法,这里不存在数据合法性问题。
针对数据是否存在重复的情况,我们刚才对 PassengerId 字段类型进行更新的时候设置为了主键,并没有发现异常,证明数据是没有重复的。
对清洗之后的数据进行可视化
我们之前讲到过如何通过 Excel 来导入 MySQL 中的数据,以及如何使用 Excel 来进行数据透视表和数据透视图的呈现。
这里我们使用 MySQL For Excel 插件来进行操作,在操作之前有两个工具需要安装。
安装好之后,我们新建一个空的 excel 文件,打开这个文件,在数据选项中可以找到“MySQL for Excel”按钮,点击进入,然后输入密码连接 MySQL 数据库。
然后选择我们的数据库以及数据表名称,在下面可以找到 Import MySQL Data 按钮,选中后将数据表导入到 Excel 文件中。
在“插入”选项中找到“数据透视图”,这里我们选中 Survived、Sex 和 Embarked 字段,然后将 Survive 字段放到图例(系列)栏中,将 Sex 字段放到求和值栏中,可以看到呈现出如下的数据透视表:
从这个透视表中你可以清晰地了解到用户生存情况(Survived)与 Embarked 字段的关系,当然你也可以通过数据透视图进行其他字段之间关系的探索。
为了让你能更好地理解操作的过程,我录制了一段操作视频。
00:05 / 00:45
1.0x
- 2.0x
- 1.5x
- 1.25x
- 1.0x
- 0.75x
- 0.5x
总结
在数据清洗过程中,你能看到通过 SQL 来进行数据概览的查询还是很方便的,但是使用 SQL 做数据清洗,会有些繁琐,这时你可以采用存储过程对数据进行逐一处理,当然你也可以使用后端语言,比如使用 Python 来做具体的数据清洗。
在进行数据探索的过程中,我们可能也会使用到数据可视化,如果不采用 Python 进行可视化,你也可以选择使用 Excel 自带的数据透视图来进行可视化的呈现,它会让你对数据有个更直观的认识。
今天讲解的数据清洗的实例比较简单,实际上数据清洗是个反复的过程,有时候我们需要几天时间才能把数据完整清洗好。你在工作中,会使用哪些工具进行数据清洗呢?
另外,数据缺失问题在数据清洗中非常常见,我今天列举了三种填充数据缺失的方式,分别是删除、均值和高频的方式。实际上缺失值的处理方式不局限于这三种,你可以思考下,如果数据量非常大,某个字段的取值分布也很广,那么对这个字段中的缺失值该采用哪种方式来进行数据填充呢?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 4
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
44丨DBMS篇总结和答疑:用SQLite做词云
下一篇
46丨数据集成:如何对各种数据库进行集成和转换?
精选留言(16)
- JustDoDT2019-09-23# Mac只能用Python了 import pandas as pd import matplotlib.pyplot as plt # 读入清洗好的数据 df = pd.read_csv('./titanic_train.csv') # 数据透视表用到的数据 df_temp df_temp = df[['Embarked', 'Survived']] # 生成数据透视表 ## 方法1 table = pd.pivot_table(df_temp, index=['Embarked'], columns=['Survived'], aggfunc=len) table = pd.pivot_table(df_temp, index=['Embarked'], columns=['Survived'], aggfunc=len) ## 方法2 数据交叉表 table = pd.crosstab(df.Embarked, df.Survived) # 画图 table.plot(kind='bar') plt.show() ----------------分割线 上面是code------------------ talbe # 输出结果 Survived 0 1 Embarked C 75 93 Q 47 30 S 427 219展开
作者回复: Godo Job
12 - 骑行的掌柜J2019-12-19陈老师 我对这一节的操作全部用MySQL进行了一个实操 中间遇到一些问题 我也全部做了一个整理补充 放到了我的博客里面:https://blog.csdn.net/weixin_41013322/article/details/103616783 希望对后面学习的朋友有帮助 谢谢
作者回复: 感谢总结和分享
共 3 条评论9 - 海洋2019-09-23检查全面性修改字段类型时,直接使用Navicat的设计表格功能修改,更快,只不过不利于新手锻炼SQL代码能力,同时可视化这块,一般清洗后,直接导出,然后使用Python或者BI软件进行进一步分析可视化5
- ABC2019-11-30WPS同样可以使用,这种方式很方便.所需下载的文件我放到网盘了,地址: 链接: https://pan.baidu.com/s/1Wrq7VcypQiofKp70YaQLBA 提取码: 2avt 看了这一课,忽然想去买数据分析的课学习一下.
作者回复: 感谢 数据分析也很实用,包括Kaggle比赛的场景
3 - JustDoDT2019-09-23仅对某一列缺失值处理 时序数据:线性插值 频谱数据:重采样 ……
作者回复: 不错的缺失值处理方式
3 - 完美坚持2021-06-10可以用 LOAD DATA 导入,但是要注意,在默认的严格模式下,如果设置 age 列为 numeric 的类型,将会报错无法读取,原因是空字符段无法读取。 即使调整为非严格模式读取成功,Age 和 cabin 空的部分,本来该是NULL,但是在SQL读取后,分别是 0 和 空字符串,并非NULL。 先创建表格: CREATE TABLE titanic_train( passenger_id INT(3) PRIMARY KEY, survived INT(1), pcalss INT(1), name VARCHAR(255), sex VARCHAR(6), age DECIMAL(4,2), sibsp INT(1), parch INT(1), ticket VARCHAR(20), fare DECIMAL(7,4), cabin VARCHAR(5), embarked CHAR(1) ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 再导入数据: LOAD DATA INFILE '文件位置/train.csv' INTO TABLE titanic_train FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (passenger_id, survived, pcalss, name, sex, @age, sibsp, parch, ticket, fare, @cabin, embarked) SET age = NULLIF(@age,''), cabin = NULLIF(@cabin,'');展开2
- 哈662019-12-16老是想问一下收集过来的数据为什么需要清洗啊,能具体举一些使用场景嘛?
作者回复: 比如Titanic乘客生存预测这个例子,里面有些字段的数值是缺失的,比如Age字段,有些乘客的Age没有,而这个字段对于后续的预测是有影响的,这里就需要通过规则来把这个字段数值进行补齐,比如采用平均值的方式
2 - Venom2019-11-12找不到数据集的人,这里能下,我也是刚找到。https://download.csdn.net/download/qq_44851287/11142360
作者回复: 感谢Venom的分享
共 2 条评论2 - Elliot2019-10-27https://www.kaggle.com/c/titanic/data 这个属于github的吗?
作者回复: 可以从kaggle上下载,也可以从github上下载 https://github.com/cystanford/Titanic_Data
共 2 条评论2 - JustDoDT2019-09-23我的Python代码github地址 https://github.com/LearningChanging/sql_must_konw/tree/master/45-%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97%EF%BC%9A%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8SQL%E5%AF%B9%E6%95%B0%E6%8D%AE%E8%BF%9B%E8%A1%8C%E6%B8%85%E6%B4%97%EF%BC%9F
作者回复: 加油 感谢分享
2 - 完美坚持2021-06-11mport pandas as pd import matplotlib.pyplot as plt # Python导入数据 titanic_train1 = pd.read_csv('train.csv') # 大致看下数据,注意到空值可以正常读取为NaN,如果直接用MySQL客户端中LOAD DATA,会出现很多问题 titanic_train1.head(8) # 完整性 titanic_train1.isnull().sum() # 简单查看数据 titanic_train1.describe() # Age 列均值填充 titanic_train1.Age.fillna(titanic_train1.Age.mean(), inplace = True) # Cabin 列的不重复取值有多少个 titanic_train1.Cabin.nunique() # Cabin 列一共有多少个非空取值 titanic_train1.Cabin.count() # 上述两步只用一步就可以得出 titanic_train1.Cabin.describe() # 只有三种取值 titanic_train1.Embarked.describe() # 统计离散的登录港口变量,每个港口的个数统计 titanic_train1.Embarked.value_counts() # 采用高频方式填充控制 titanic_train1.Embarked.fillna('S', inplace = True) # MySQL导入清洗好的数据 # -*- coding: UTF-8 -*- import mysql.connector # 打开数据库连接 db = mysql.connector.connect( host="localhost", user="root", passwd="***********", # 写上你的数据库密码 database='XXX, auth_plugin='mysql_native_password' ) # 获取操作游标 cursor = db.cursor() # 执行SQL语句 cursor.execute("SELECT GROUP_CONCAT(column_name SEPARATOR ', ') FROM information_schema.COLUMNS WHERE table_name = 'titanic_train'") # 获取一条数据:列名 data = cursor.fetchone() print("列名: %s " % data) cursor.execute("SELECT * FROM titanic_train") # 获取一条数据 data = cursor.fetchall() titanic_train2 = pd.DataFrame(data, columns = ["passenger_id", "Survived", "pcalss", "name", "sex", "Age", "Sibsp", "parch", "ticket", "Fare", "cabin", "embarked" ]) # 数据可视化 # 数据透视表 table = pd.crosstab(titanic_train1.Embarked, titanic_train1.Survived) print(table) # 用得到的数据透视表作数据透视图 table.plot(kind='bar', ylabel = 'Frequency') plt.show() # 法二直接画图 import seaborn as sns ax = sns.countplot(x="Embarked", hue="Survived", data=titanic_train1)展开1
- 完美坚持2021-06-10LOAD DATA 导入数据 (二) 下面就来解决这个问题: 很容易想到方法一,但是觉得在数据量很大的时候,方法一还要在扫一遍表,而方法二只需要在读取扫描的时候,顺带加一个判断就行了,觉得这样的话,方法一消耗了一些不必要的资源。 **方法一**:导入数据后进行修改(利用 UPDATE) **方法二**:读取数据时进行判断 参考:【解决Mysql导入csv中空值变为0的问题:导入数据时设定格式】 https://blog.csdn.net/duckyamd/article/details/53143639 先获得所有列名,用逗号 ',' 分隔,这样方便我们之后写“读取数据时进行判断”的 LOAD DATA 的代码: SELECT GROUP_CONCAT(column_name SEPARATOR ', ') FROM information_schema.COLUMNS WHERE table_name = 'titanic_train'; +---------------------------------------------------------------------------------------------+ | GROUP_CONCAT(column_name SEPARATOR ', ') | +---------------------------------------------------------------------------------------------+ | passenger_id, survived, pcalss, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked | +---------------------------------------------------------------------------------------------+ 1 row in set (0.02 sec) 参考:GROUP_CONCAT 的官方文档说明:https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_group-concat passenger_id, survived, pcalss, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked # 删除表格数据 DELETE FROM titanic_train -- 加载数据的时候进行判断 LOAD DATA INFILE 'C:/Users/ASUS/Documents/Python Scripts/sql/train_ansi.csv' INTO TABLE titanic_train FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (passenger_id, survived, pcalss, name, sex, @age, sibsp, parch, ticket, fare, @cabin, embarked) SET age = NULLIF(@age,''), cabin = NULLIF(@cabin,''); Query OK, 891 rows affected (0.05 sec) Records: 891 Deleted: 0 Skipped: 0 Warnings: 0 可以看到相应空字符的位置是 null 而不是 0 了 NULLIF 的文档:意思就是如果这个变量是空字符就设置为null https://dev.mysql.com/doc/refman/8.0/en/flow-control-functions.html#function_nullif展开1
- 完美坚持2021-06-10LOAD DATA 导入数据(没有Navicat)(一): CREATE TABLE titanic_train( passenger_id INT(3) PRIMARY KEY, survived INT(1), pcalss INT(1), name VARCHAR(255), sex VARCHAR(6), age DECIMAL(4,2), sibsp INT(1), parch INT(1), ticket VARCHAR(20), fare DECIMAL(7,4), cabin VARCHAR(5), embarked CHAR(1) ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; ALTER TABLE titanic_train MODIFY name VARCHAR(255); ALTER TABLE titanic_train MODIFY sex VARCHAR(6); ALTER TABLE titanic_train MODIFY age INT(2) DEFAULT NULL; ALTER TABLE titanic_train MODIFY fare DECIMAL(7,4); ALTER TABLE titanic_train MODIFY cabin VARCHAR(255); SET SESSION sql_mode = ''; -- 严格模式下会报错:Incorrect integer value: '' for column '出错的column' at row 出错的行数 -- 这里将session 的 sql_mode 置为空,就不是严格模式了 -- 另外 这里Setting the SESSION variable affects only the current client. Each client can change its session sql_mode value at any time. -- 参考:SQL mode 网址:https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_strict_trans_tables LOAD DATA INFILE 'C:/Users/ASUS/Documents/Python Scripts/sql/train_ansi.csv' INTO TABLE titanic_train FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES; SHOW warnings; -- 在非严格模式下,很多严格模式下的错误会退化为 warnings,此时一定要查看 warnings,发现需要改正的问题。理论上讲,除了要忽略的问题(比如这里空字符无法导入int类型)之外,没有其他问题的时候,用严格模式来忽略这些问题;但是通常我们可能还有其它应该避免、不应该忽略的问题,最好查看一下非严格模式下的warnings. -- 官方文档和很多资料里,都是 LINES TERMINATED BY '\r\n',但是我只有用 \n 才能插入成功 -- 发现空字符串不能被正常读取为(数据库中的)NULL,cabin就算了(空字符和NULL的区别可能影响不大),但是age空字符串在非严格模式下(在严格模式下根本无法导入数据,会报错:Incorrect integer value: '' for column '出错的column' at row 出错的行数),空字符导入后就变成了0,但是那些乘客显然不是零岁啊(我的数据里面出错的是 Age 一列) 【mysql 导入csv空值_如何处理csv中的空值】https://blog.csdn.net/weixin_29969209/article/details/114795753 这篇文章也提到了这个问题,主要讲了讲可能会带来那些衍生的问题,并且讨论了Python和 mysql 数据交互时,空字符串读取的情况,但是并没有解决问题。展开1
- ClaireToffler2020-04-01LTP 称之为联机事务处理,我们之前讲解的对数据进行增删改查,SQL 查询优化,事务处理等就属于 OLTP 的范畴。它对实时性要求高,需要将用户的数据有效地存储到数据库中,同时有时候针对互联网应用的需求,我们还需要设置数据库的主从架构保证数据库的高并发和高可用性。 忽然感觉平时好像只接触了这个领域1
- 阿飛2019-12-29有没有oracle for excel1
- Wry2021-11-12老师,看了这个我对sum和count的使用有些懵,多个字段查询是否有null值的情况的时候为什么这里SELECT SUM((CASE WHEN Age IS NULL THEN 1 ELSE 0 END)) AS age_null_num, SUM((CASE WHEN Cabin IS NULL THEN 1 ELSE 0 END)) AS cabin_null_num FROM titanic_train使用sum而不用count呢?如果不用存储过程还有其他方法可以查多个字段null值情况吗?展开