mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
del
This commit is contained in:
254
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/45丨数据清洗:如何使用SQL对数据进行清洗?.md
Normal file
254
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/45丨数据清洗:如何使用SQL对数据进行清洗?.md
Normal file
@@ -0,0 +1,254 @@
|
||||
<audio id="audio" title="45丨数据清洗:如何使用SQL对数据进行清洗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/66/7949c73f26a0f4f565edf0a223f47166.mp3"></audio>
|
||||
|
||||
SQL可以帮我们进行数据处理,总的来说可以分成OLTP和OLAP两种方式。
|
||||
|
||||
OLTP称之为联机事务处理,我们之前讲解的对数据进行增删改查,SQL查询优化,事务处理等就属于OLTP的范畴。它对实时性要求高,需要将用户的数据有效地存储到数据库中,同时有时候针对互联网应用的需求,我们还需要设置数据库的主从架构保证数据库的高并发和高可用性。
|
||||
|
||||
OLAP称之为联机分析处理,它是对已经存储在数据库中的数据进行分析,帮我们得出报表,指导业务。它对数据的实时性要求不高,但数据量往往很大,存储在数据库(数据仓库)中的数据可能还存在数据质量的问题,比如数据重复、数据中有缺失值,或者单位不统一等,因此在进行数据分析之前,首要任务就是对收集的数据进行清洗,从而保证数据质量。
|
||||
|
||||
对于数据分析工作来说,好的数据质量才是至关重要的,它决定了后期数据分析和挖掘的结果上限。数据挖掘模型选择得再好,也只能最大化地将数据特征挖掘出来。
|
||||
|
||||
高质量的数据清洗,才有高质量的数据。今天我们就来看下,如何用SQL对数据进行清洗。
|
||||
|
||||
1. 想要进行数据清洗有怎样的准则呢?
|
||||
1. 如何使用SQL对数据进行清洗?
|
||||
1. 如何对清洗之后的数据进行可视化?
|
||||
|
||||
## 数据清洗的准则
|
||||
|
||||
我在《数据分析实战45讲》里专门讲到过数据清洗的原则,这里为了方便你理解,我用一个数据集实例讲一遍。
|
||||
|
||||
一般而言,数据集或多或少地会存在数据质量问题。这里我们使用泰坦尼克号乘客生存预测数据集,你可以从[GitHub](https://www.kaggle.com/c/titanic/data)上下载这个数据集。
|
||||
|
||||
数据集格式为csv,一共有两种文件:train.csv是训练数据集,包含特征信息和存活与否的标签;test.csv是测试数据集,只包含特征信息。
|
||||
|
||||
数据集中包括了以下字段,具体的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/e0/e717facd7cd53e7e2cfb714937347fe0.png" alt=""><br>
|
||||
训练集给出了891名乘客幸存与否的结果,以及相关的乘客信息。通过训练集,我们可以对数据进行建模形成一个分类器,从而对测试集中的乘客生存情况进行预测。不过今天我们并不讲解数据分析的模型,而是来看下在数据分析之前,如何对数据进行清洗。
|
||||
|
||||
首先,我们可以通过Navicat将CSV文件导入到MySQL数据库中,然后浏览下数据集中的前几行,可以发现数据中存在缺失值的情况还是很明显的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/77/54b1d91f186945bcbb7f3a5df3575e77.png" alt=""><br>
|
||||
数据存在数据缺失值是非常常见的情况,此外我们还需要考虑数据集中某个字段是否存在单位标识不统一,数值是否合法,以及数据是否唯一等情况。要考虑的情况非常多,这里我将数据清洗中需要考虑的规则总结为4个关键点,统一起来称之为“完全合一”准则,你可以[点这里](https://time.geekbang.org/column/article/76307)看一下。
|
||||
|
||||
“完全合一”是个通用的准则,针对具体的数据集存在的问题,我们还需要对症下药,采取适合的解决办法,甚至为了后续分析方便,有时我们还需要将字符类型的字段替换成数值类型,比如我们想做一个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数据表中:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/b5/5ddd7c23f942cc2f90ed9621cb751eb5.png" alt="">
|
||||
|
||||
### 检查完整性
|
||||
|
||||
在完整性这里,我们需要重点检查字段数值是否存在空值,在此之前,我们需要先统计每个字段空值的个数。在SQL中,我们可以分别统计每个字段的空值个数,比如针对Age字段进行空值个数的统计,使用下面的命令即可:
|
||||
|
||||
```
|
||||
SELECT COUNT(*) as num FROM titanic_train WHERE Age IS NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为177。
|
||||
|
||||
当然我们也可以同时对多个字段的非空值进行统计:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/bb/42d6d85c533707c29bda5a7cf95ad6bb.png" alt=""><br>
|
||||
不过这种方式适用于字段个数较少的情况,如果一个数据表存在几十个,甚至更多的字段,那么采用这种方式既麻烦又容易出错。这时我们可以采用存储过程的方式,用程序来进行字段的空值检查,代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `check_column_null_num`(IN schema_name VARCHAR(100), IN table_name2 VARCHAR(100))
|
||||
BEGIN
|
||||
-- 数据表schema_name中的列名称
|
||||
DECLARE temp_column VARCHAR(100);
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标来操作每一个COLUMN_NAME
|
||||
DECLARE cursor_column CURSOR FOR
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE table_schema = schema_name AND table_name = table_name2;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
-- 打开游标
|
||||
OPEN cursor_column;
|
||||
read_loop:LOOP
|
||||
FETCH cursor_column INTO temp_column;
|
||||
-- 判断游标的循环是否结束
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
-- 这里需要设置具体的SQL语句temp_query
|
||||
SET @temp_query=CONCAT('SELECT COUNT(*) as ', temp_column, '_null_num FROM ', table_name2, ' WHERE ', temp_column, ' IS NULL');
|
||||
-- 执行SQL语句
|
||||
PREPARE stmt FROM @temp_query;
|
||||
EXECUTE stmt;
|
||||
END LOOP;
|
||||
-- 关闭游标
|
||||
CLOSE cursor_column;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
我来说下这个存储过程的作用,首先我定义了两个输入的参数schema_name和table_name2,用来接收想要检查的数据库的名称以及数据表名。
|
||||
|
||||
然后使用游标来操作读取出来的column_name,赋值给变量temp_column。对于列名,我们需要检查它是否为空,但是这个列名在MySQL中是动态的,我们无法使用@temp_column 来表示列名,对其进行判断,在这里我们需要使用SQL拼接的方式,这里我设置了@temp_query表示想要进行查询的SQL语句,然后设置COUNT(*)的别名为动态别名,也就是temp_column加上_null_num,同样在WHERE条件判断中,我们使用temp_column进行动态列名的输出,以此来判断这个列数值是否为空。
|
||||
|
||||
然后我们执行这个SQL语句,提取相应的结果。
|
||||
|
||||
```
|
||||
call check_column_null_num('wucai', 'titanic_train');
|
||||
|
||||
```
|
||||
|
||||
运行结果如下:
|
||||
|
||||
```
|
||||
Age_null_num:177
|
||||
Cabin_null_num:687
|
||||
Embarked_null_num:2
|
||||
Fare_null_num:0
|
||||
Name_null_num:0
|
||||
Parch_null_num:0
|
||||
PassengerId_null_num:0
|
||||
Pclass_null_num:0
|
||||
Sex_null_num:0
|
||||
SibSp_null_num:0
|
||||
Survived_null_num:0
|
||||
Ticket_null_num:0
|
||||
|
||||
```
|
||||
|
||||
为了浏览方便我调整了运行结果的格式,你能看到在titanic_train数据表中,有3个字段是存在空值的,其中Cabin空值数最多为687个,Age字段空值个数177个,Embarked空值个数2个。
|
||||
|
||||
既然存在空值的情况,我们就需要对它进行处理。针对缺失值,我们有3种处理方式。
|
||||
|
||||
1. 删除:删除数据缺失的记录;
|
||||
1. 均值:使用当前列的均值;
|
||||
1. 高频:使用当前列出现频率最高的数据。
|
||||
|
||||
对于Age字段,这里我们采用均值的方式进行填充,但如果直接使用SQL语句可能会存在问题,比如下面这样。
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET age = (SELECT AVG(age) FROM titanic_train) WHERE age IS NULL
|
||||
|
||||
```
|
||||
|
||||
这时会报错:
|
||||
|
||||
```
|
||||
1093 - You can't specify target table 'titanic_train' for update in FROM clause
|
||||
|
||||
```
|
||||
|
||||
也就是说同一条SQL语句不能先查询出来部分内容,再同时对当前表做修改。
|
||||
|
||||
这种情况下,最简单的方式就是复制一个临时表titanic_train2,数据和titanic_train完全一样,然后再执行下面这条语句:
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET age = (SELECT ROUND(AVG(age),1) FROM titanic_train2) WHERE age IS NULL
|
||||
|
||||
```
|
||||
|
||||
这里使用了ROUND函数,对age平均值AVG(age)进行四舍五入,只保留小数点后一位。
|
||||
|
||||
针对Cabin这个字段,我们了解到这个字段代表用户的船舱位置,我们先来看下Cabin字段的数值分布情况:
|
||||
|
||||
```
|
||||
SELECT COUNT(cabin), COUNT(DISTINCT(cabin)) FROM titanic_train
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/13/379348df1bdd00235646df78ebc54f13.png" alt=""><br>
|
||||
从结果中能看出Cabin字段的数值分布很广,而且根据常识,我们也可以知道船舱位置每个人的差异会很大,这里既不能删除掉记录航,又不能采用均值或者高频的方式填充空值,实际上这些空值即无法填充,也无法对后续分析结果产生影响,因此我们可以不处理这些空值,保留即可。
|
||||
|
||||
然后我们来看下Embarked字段,这里有2个空值,我们可以采用该字段中高频值作为填充,首先我们先了解字段的分布情况使用:
|
||||
|
||||
```
|
||||
SELECT COUNT(*), embarked FROM titanic_train GROUP BY embarked
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/cc/0efcdf6248e65e482502dbe313c6efcc.png" alt=""><br>
|
||||
我们可以直接用S来对缺失值进行填充:
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET embarked = 'S' WHERE embarked IS NULL
|
||||
|
||||
```
|
||||
|
||||
至此,对于titanic_train这张数据表中的缺失值我们就处理完了。
|
||||
|
||||
### 检查全面性
|
||||
|
||||
在这个过程中,我们需要观察每一列的数值情况,同时查看每个字段的类型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/77/46d0be3acf6bf3526284cf4e56202277.png" alt=""><br>
|
||||
因为数据是直接从CSV文件中导进来的,所以每个字段默认都是VARCHAR(255)类型,但很明显PassengerID、Survived、Pclass和Sibsp应该设置为INT类型,Age和Fare应该设置为DECIMAL类型,这样更方便后续的操作。使用下面的SQL命令即可:
|
||||
|
||||
```
|
||||
ALTER TABLE titanic_train CHANGE PassengerId PassengerId INT(11) NOT NULL PRIMARY KEY;
|
||||
ALTER TABLE titanic_train CHANGE Survived Survived INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Pclass Pclass INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Sibsp Sibsp INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Age Age DECIMAL(5,2) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Fare Fare DECIMAL(7,4) NOT NULL;
|
||||
|
||||
```
|
||||
|
||||
然后我们将其余的字段(除了Cabin)都进行NOT NULL,这样在后续进行数据插入或其他操作的时候,即使发现数据异常,也可以对字段进行约束规范。
|
||||
|
||||
在全面性这个检查阶段里,除了字段类型定义需要修改以外,我们没有发现其他问题。
|
||||
|
||||
**然后我们来检查下合法性及唯一性。**合法性就是要检查数据内容、大小等是否合法,这里不存在数据合法性问题。
|
||||
|
||||
针对数据是否存在重复的情况,我们刚才对PassengerId 字段类型进行更新的时候设置为了主键,并没有发现异常,证明数据是没有重复的。
|
||||
|
||||
## 对清洗之后的数据进行可视化
|
||||
|
||||
我们之前讲到过如何通过Excel来导入MySQL中的数据,以及如何使用Excel来进行数据透视表和数据透视图的呈现。
|
||||
|
||||
这里我们使用MySQL For Excel插件来进行操作,在操作之前有两个工具需要安装。
|
||||
|
||||
首先是mysql-for-excel,点击[这里](https://dev.mysql.com/downloads/windows/excel/)进行下载;然后是mysql-connector-odbc,点击[这里](https://dev.mysql.com/downloads/connector/odbc/)进行下载。
|
||||
|
||||
安装好之后,我们新建一个空的excel文件,打开这个文件,在数据选项中可以找到“MySQL for Excel”按钮,点击进入,然后输入密码连接MySQL数据库。
|
||||
|
||||
然后选择我们的数据库以及数据表名称,在下面可以找到Import MySQL Data按钮,选中后将数据表导入到Excel文件中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/f0/69aa90c22d9ecf107271e1dee38675f0.png" alt=""><br>
|
||||
在“插入”选项中找到“数据透视图”,这里我们选中Survived、Sex和Embarked字段,然后将Survive字段放到图例(系列)栏中,将Sex字段放到求和值栏中,可以看到呈现出如下的数据透视表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/74/03b27cf20f5e3dc313014c1018121174.png" alt=""><br>
|
||||
从这个透视表中你可以清晰地了解到用户生存情况(Survived)与Embarked字段的关系,当然你也可以通过数据透视图进行其他字段之间关系的探索。
|
||||
|
||||
为了让你能更好地理解操作的过程,我录制了一段操作视频。
|
||||
|
||||
<video poster="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/snapshots/5df1fee85a4c4c1a9211393dc35ea39e-00003.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/1e1fd347-16d4e5518af-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/1fa5200c814341e19c6d602700f2ff7f-d97a045b4b0d8266650e6248565634b0-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/1fa5200c814341e19c6d602700f2ff7f-fdf87ccb8238a9b89d3ce3d8f46875f9-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
## 总结
|
||||
|
||||
在数据清洗过程中,你能看到通过SQL来进行数据概览的查询还是很方便的,但是使用SQL做数据清洗,会有些繁琐,这时你可以采用存储过程对数据进行逐一处理,当然你也可以使用后端语言,比如使用Python来做具体的数据清洗。
|
||||
|
||||
在进行数据探索的过程中,我们可能也会使用到数据可视化,如果不采用Python进行可视化,你也可以选择使用Excel自带的数据透视图来进行可视化的呈现,它会让你对数据有个更直观的认识。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/0e/d8361b069fe64e5e826ba58965ba000e.png" alt=""><br>
|
||||
今天讲解的数据清洗的实例比较简单,实际上数据清洗是个反复的过程,有时候我们需要几天时间才能把数据完整清洗好。你在工作中,会使用哪些工具进行数据清洗呢?
|
||||
|
||||
另外,数据缺失问题在数据清洗中非常常见,我今天列举了三种填充数据缺失的方式,分别是删除、均值和高频的方式。实际上缺失值的处理方式不局限于这三种,你可以思考下,如果数据量非常大,某个字段的取值分布也很广,那么对这个字段中的缺失值该采用哪种方式来进行数据填充呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
187
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/46丨数据集成:如何对各种数据库进行集成和转换?.md
Normal file
187
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/46丨数据集成:如何对各种数据库进行集成和转换?.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<audio id="audio" title="46丨数据集成:如何对各种数据库进行集成和转换?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/fe/95d30b17540e66799bdb3379f9a205fe.mp3"></audio>
|
||||
|
||||
我们的数据可能分散在不同的数据源中,如果想要对这些数据分析,就需要先对这些数据进行集成。同时因为不同的来源,这些数据可能会存在各种问题,比如这些数据源采用了不同的DBMS,数据之间存在冗余的情况,比如某一条数据在不同的数据源中都有记录,那么在数据集成中我们只保留其中的一条就可以了。除此以外,这些不同的数据源还可能字段标识不统一,再或者我们需要将数据转换成我们想要的格式要求进行输出。
|
||||
|
||||
数据集成是数据分析之前非常重要的工作,它将不同来源、不同规范以及不同质量的数据进行统一收集和整理,为后续数据分析提供统一的数据源。
|
||||
|
||||
好了,关于这部分内容,今天我们一起来学习下:
|
||||
|
||||
1. 我们将数据从OLTP系统中转换加载到OLAP数据仓库中,这中间重要的步骤就是ETL。那什么是ETL呢?
|
||||
1. 认识Kettle工具。在Kettle中有两个重要的脚本,分别是Transformation转换和Job作业,它们分别代表什么?
|
||||
1. 完成两个实例项目。通过使用Kettle完成MySQL数据表的数据同步,以及根据我们的需求将银行客户转账的记录导出到目标文件中。
|
||||
|
||||
## 什么是ETL
|
||||
|
||||
在使用数据的时候,根据需求,我们可以分成OLTP和OLAP两种场景。OLTP更注重数据的实时性,而OLAP更注重数据的分析能力,对时效性要求不高。在这个过程中,我们的数据源来自于OLTP系统,而最终得到的数据仓库则应用在OLAP系统中,中间的转换过程就是ETL,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/42/7d8ee4be2192b2c87d3e997e184c4542.jpg" alt=""><br>
|
||||
ETL是英文Extract、Transform和Load的缩写,也就是将数据从不同的数据源进行抽取,然后通过交互转换,最终加载到目的地的过程。
|
||||
|
||||
在Extract数据抽取这个过程中,需要做大量的工作,我们需要了解企业分散在不同地方的数据源都采用了哪种DBMS,还需要了解这些数据源存放的数据结构等,是结构化数据,还是非结构化数据。在抽取中,我们也可以采用全量抽取和增量抽取两种方式。相比于全量抽取,增量抽取使用得更为广泛,它可以帮我们动态捕捉数据源的数据变化,并进行同步更新。
|
||||
|
||||
在Transform数据转换的过程中,我们可以使用一些数据转换的组件,比如说数据字段的映射、数据清洗、数据验证和数据过滤等,这些模块可以像是在流水线上进行作业一样,帮我们完成各种数据转换的需求,从而将不同质量,不同规范的数据进行统一。
|
||||
|
||||
在Load数据加载的过程中,我们可以将转换之后的数据加载到目的地,如果目标是RDBMS,我们可以直接通过SQL进行加载,或者使用批量加载的方式进行加载。
|
||||
|
||||
## 认识Kettle工具
|
||||
|
||||
Kettle可以帮助我们完成ETL工作,它的设计师希望它能像水壶一样,可以从将不同的数据通过Kettle水壶按照指定的格式流出来。
|
||||
|
||||
相比于其他商业软件来说,Kettle是Java开发的免费开源工具,可以运行在多个操作系统中。因此在使用之前,你需要安装Java运行环境(JRE)。Kettle的[下载地址](https://community.hitachivantara.com/docs/DOC-1009855)在这里。
|
||||
|
||||
在Kettle中有3个重要的组件:
|
||||
|
||||
1. Spoon(勺子),它是一个图形界面,帮我们启动作业和转换设计。
|
||||
1. Pan(锅),通过命令行方式完成转换执行(Transformation)。
|
||||
1. Kitchen(厨房),通过命令行方式完成作业执行(Job)。
|
||||
|
||||
通过Spoon,我们可以采用可视化的方式对Kettle中的两种脚本进行操作,这两种脚本分别是Transformation(转换)和 Job(作业)。
|
||||
|
||||
Transformation(转换)对应的是.ktr文件,它相当于一个容器,对数据操作进行了定义。数据操作就是数据从输入到输出的一个过程,Tranformation可以帮我们完成数据的基础转换。
|
||||
|
||||
Job(作业)对应的是.kjb文件,Job帮我们完成整个工作流的控制。相比于Transformation来说,它是个更大的容器,负责将Transformation组织起来完成某项作业。
|
||||
|
||||
你可以把Transformation理解成比Job粒度更小的容器。在通常的工作中,我们会把任务分解成为不同的Job,然后再把Job分解成多个Transformation。
|
||||
|
||||
## Kettle使用实例
|
||||
|
||||
我们刚才对Kettle有了大致的了解,Kettle工具包含的内容非常多,下面我们通过两个实例更深入地了解一下。
|
||||
|
||||
### 实例1:将test1数据库中的heros数据表同步到test2数据库中
|
||||
|
||||
**数据准备:**
|
||||
|
||||
首先我们在MySQL中创建好test1和test2两个数据库,在test1中存储了我们之前已有的heros数据表(包括表结构和表数据),然后在test2数据库中,同样保存一个heros数据表,注意test2数据库中只需要有heros表结构即可。数据同步是需要我们使用Kettle工具来完成的。
|
||||
|
||||
你可以[点击这里](https://github.com/cystanford/SQL-Kettle/blob/master/heros.sql)下载heros数据表结构及数据。
|
||||
|
||||
下面我们来使用Kettle来完成这个工作,只需要3步即可。
|
||||
|
||||
**第一步,创建表输入组件,并对test1数据库中的heros数据表进行查询。**
|
||||
|
||||
在Kettle左侧的面板中找到“核心对象”,输入“表输入”找到表输入组件,然后拖拽到中间的工作区。
|
||||
|
||||
双击表输入,选择Wizard来对数据库连接进行配置。这里我们可以创建一个数据库连接test1,然后选择MySQL。然后输入我们的服务器主机地址以及数据库名称,最后输入MySQL的用户名和密码完成数据库连接的创建工作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/44/92b0c458667f6c3db6a7c49409a25844.png" alt=""><br>
|
||||
创建好连接之后,我们来对heros数据表进行查询,输入SQL语句:SELECT * FROM heros,你也可以通过获取SQL查询语句的功能自动选择想要查询的字段。
|
||||
|
||||
然后点击确定完成表输入组件的创建。
|
||||
|
||||
**第二步,创建插入/更新组件,配置test2数据库中的heros数据表的更新字段。**
|
||||
|
||||
如果我们想要对目标表进行插入或者更新,这里需要使用“插入/更新”组件。我们在Kettle左侧的面板中找到这个组件,然后将它拖拽到中间的工作区。
|
||||
|
||||
在配置“插入/更新”组件之前,我们先创建一个从“表输入”到“插入/更新”的连接,这里可以将鼠标移动到“表输入”控件上,然后按住Shift键用鼠标从“表输入”拉一个箭头到“插入更新”。这样我们就可以在“插入/更新”组件中看到表输入的数据了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/af/2392155c3e0a5aa8cafb872e5ab14eaf.png" alt=""><br>
|
||||
然后我们对“插入/更新”组件进行配置,双击该组件,这里同样需要先创建数据库连接test2,来完成对数据库test2的连接,原理与创建数据库连接test1一样。
|
||||
|
||||
接着,我们选择目标表,这里点击浏览按钮,在test2连接中找到我们的数据表heros选中并确定。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/eb/e97761f5be696453a897f88803723beb.png" alt=""><br>
|
||||
然后我们可以在下面指定查询关键字,这里指定表字段为id,比较符为(=),数据流里的字段1为id,这样我们就可以通过id来进行查询关联。
|
||||
|
||||
然后对于目的表中缺失的数据,我们需要对相应的字段进行更新(插入),这里可以直接通过“获取和更新字段”来完成全部更新字段的自动获取。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/f7/c669c77dbd993e5d10c0ba96a5f288f7.png" alt=""><br>
|
||||
然后点击“确定”完成“插入/更新”组件的创建。
|
||||
|
||||
**第三步,点击启动,开始执行转换。**
|
||||
|
||||
这时Kettle就会自动根据数据流中的组件顺序来完成相应的转换,我们可以在MySQL中的test2数据库中看到更新的heros数据表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/d4/7362b4c7c51026d99ce089c92889edd4.png" alt=""><br>
|
||||
我将转换保存为test1.ktr 上传到了[GitHub](https://github.com/cystanford/SQL-Kettle)上,你可以下载一下。
|
||||
|
||||
## 实例2:导入用户交易流水
|
||||
|
||||
刚才我们完成了一个简单的Kettle使用实例,现在我们来做一个稍微复杂一些的数据转换。
|
||||
|
||||
首先准备数据库。在数据库创建account和trade两张表。其中account表为客户表,字段含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/ca/cb64830c0f832242f4f3ed3420a791ca.png" alt=""><br>
|
||||
trade表为客户交易表,字段含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f1/68/f11cdfc9e83d124c9a07555f76862268.png" alt=""><br>
|
||||
你可以[点击这里](https://github.com/cystanford/SQL-Kettle)下载account和trade数据表结构及数据下载。
|
||||
|
||||
现在我们希望将客户的交易流水导入到txt文件中,输出内容包括4个字段account_id1、account_id2、amount和value。其中value为新增的字段,表示转账的类型,当转账对象account_id2为个人账户,则输出“对私客户发生的交易”,为公司账户时则输出“对公客户发生的交易”。
|
||||
|
||||
实际上我们在模拟从多个数据源中导出我们想要的数据,针对这个例子,我们想要输出的数据内容为打款账户,收款账户,转账金额,以及交易类型。
|
||||
|
||||
下面我们来看下如何使用Kettle来完成这个工作。
|
||||
|
||||
**第一步,创建表输入组件,并对数据库中的trade数据表进行查询。**
|
||||
|
||||
这里我们创建数据库连接,然后在SQL查询中输入:SELECT * FROM trade,相当于对trade进行全量获取。
|
||||
|
||||
**第二步,创建数据库查询,对account表进行查询。**
|
||||
|
||||
在Kettle左侧面板找到“数据库查询”控件,拖拽到中间的工作区,命名为“account表查询”,然后将“表输入”控件和“account表查询”之间进行连接。这样我们就可以得到“表输入”控件中的数据流。
|
||||
|
||||
然后我们对“account表查询”控件进行配置,这里我们需要连接上account数据表,然后对account数据表中的account_id与trade数据表中的account_id2进行关联,查询返回值设置为customer_type。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/2c/4574b9897615b7d67b19c0f415f1992c.png" alt=""><br>
|
||||
这样我们可以通过account数据表得到trade.account_id2所对应的customer_type。也就是收款账户的账户类型(个人/公司)。
|
||||
|
||||
**第三步,创建过滤记录,对不同的customer_type进行类型修改。**
|
||||
|
||||
我们需要根据收款账户的类型,来进行交易类型的判断,因此这里我们可以根据前面得到的customer_type来进行记录的过滤。
|
||||
|
||||
这里在Kettle左侧的面板选择“过滤记录”,拖拽到中间的工作区,然后创建从“account表查询”到“过滤记录”的连接。
|
||||
|
||||
在“过滤记录”中,我们设置判断条件为customer_type = 1。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/47/a5682bae98f0adb29507efc4d68ebd47.png" alt=""><br>
|
||||
然后在从Kettle左侧面板中拖拽两个“JavaScript代码”控件到工作区,分别将步骤名称设置为“对公类型修改”和“对私类型修改”
|
||||
|
||||
然后在对应的Script脚本处,设置变量customer_type_cn。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/cc/ef78804fa6698712cdb286f028a2f6cc.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/61/d2f6be25c63aceb1aea9f04dc94eb361.png" alt=""><br>
|
||||
然后我们在Kettle左侧面板拖拽两个“增加常量”控件,到中间的工作区,分别命名为“增加对公常量”和“增加对私常量”。然后将刚才的设置的两个“Javascript代码”控件与“增加常量”控件进行连接。
|
||||
|
||||
在增加常量控件中,我们可以设置输出的常量:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/1a/bd820ef009d740854034771d69d0211a.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/3a/b5c69f9cf550c02a43bce694f056443a.png" alt=""><br>
|
||||
这样我们就可以把得到的常量在后面结果中进行输出。
|
||||
|
||||
**第四步,创建文本文件输出,将数据表导入到文件中。**
|
||||
|
||||
刚才我们已经设置了从trade数据表中进行查询,然后通过account表关联查询得到customer_type,然后再根据customer_type来判断输出的常量,是对公客户发生的交易,还是对私客户发生的交易。这里如果我们想要导入到一个文本文件中,可以从Kettle左侧面板中拖拽相应的控件到中间的工作区。然后将刚才设置的两个控件“增加对公常量”和“增加对私常量”设置连接到“文本文件输出”控件中。
|
||||
|
||||
然后在“文本文件输出”控件中,找到字段选项,设置我们想要导入的字段,你可以通过“获取字段”来帮你辅助完成这个操作。这里我们选择了account_id1、account_id2、amount以及刚才配置的常量value。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/8b/b91d81cba3663ed5d5a7049da3cb6e8b.png" alt=""><br>
|
||||
然后我们点击确定就完成了所有配置工作,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/b5/1ffac59cd97629aaf8734a45dad3f8b5.png" alt=""><br>
|
||||
当我们开启转换后,整个数据流就会从表输入开始自动完成转换和判断操作。将其导入到我们的文本文件中,导出的结果如下:
|
||||
|
||||
```
|
||||
account_id1;account_id2;amout;value
|
||||
322202020312335;622202020312337;200.0;对私客户发生的交易
|
||||
322202020312335;322202020312336;100.0;对公客户发生的交易
|
||||
622202020312336;322202020312337;300.0;对公客户发生的交易
|
||||
622202020312337;322202020312335;400.0;对公客户发生的交易
|
||||
|
||||
```
|
||||
|
||||
我将上述过程的转换保存为test2.ktr 上传到了[GitHub](https://github.com/cystanford/SQL-Kettle)上,你可以下载看一下。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了数据集成的作用,以及ETL的原理。在实际工作中,因为数据源可能是不同的DBMS,因此我们往往会使用第三方工具来帮我们完成数据集成的工作,Kettle作为免费开源的工作,在ETL工作中被经常使用到。它支持多种RDBMS和非关系型数据库,比如MySQL、Oracle、SQLServer、DB2、PostgreSQL、MongoDB等。不仅如此,Kettle易于配置和使用,通过可视化界面我们可以设置好想要进行转换的数据源,并且还可以通过JOB作业进行定时,这样就可以按照每周每日等频率进行数据集成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/c4/4b004a7fa2adea85131654e7766937c4.png" alt=""><br>
|
||||
通过今天的两个Kettle实例,相信你对Kettle使用有一定的了解,你之前都用过哪些ETL工具,不妨说说你的经历?
|
||||
|
||||
第二个实例中,我们将交易类型分成了“对公客户发生的交易”以及“对私客户发生的交易”。如果我们的需求是分成4种交易类型,包括“公对公交易”、“公对私交易”、“私对公交易”以及“私对私交易”,那么该如何使用Kettle完成这个转换呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
121
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/47丨如何利用SQL对零售数据进行分析?.md
Normal file
121
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/47丨如何利用SQL对零售数据进行分析?.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<audio id="audio" title="47丨如何利用SQL对零售数据进行分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4a/f5/4a89ff436f212733f9a69beeaa8e0ef5.mp3"></audio>
|
||||
|
||||
我们通过OLTP系统实时捕捉到了用户的数据,还需要在OLAP系统中对它们进行分析。之前我们讲解了如何对数据进行清洗,以及如何对分散在不同地方的数据进行集成,今天我们来看下如何使用SQL分析这些数据。
|
||||
|
||||
关于这部分内容,今天我们一起来学习下:
|
||||
|
||||
1. 使用SQL进行数据分析都有哪几种姿势?
|
||||
1. 如何通过关联规则挖掘零售数据中的频繁项集?
|
||||
1. 如何使用SQL+Python完成零售数据的关联分析?
|
||||
|
||||
## 使用SQL进行数据分析的5种姿势
|
||||
|
||||
在DBMS中,有些数据库管理系统很好地集成了BI工具,可以方便我们对收集的数据进行商业分析。
|
||||
|
||||
SQL Server提供了BI分析工具,我们可以通过使用SQL Server中的Analysis Services完成数据挖掘任务。SQL Server内置了多种数据挖掘算法,比如常用的EM、K-Means聚类算法、决策树、朴素贝叶斯和逻辑回归等分类算法,以及神经网络等模型。我们还可以对这些算法模型进行可视化效果呈现,帮我们优化和评估算法模型的好坏。
|
||||
|
||||
PostgreSQL是免费开源的对象-关系数据库(ORDBMS),它的稳定性非常强,功能强大,在OLTP和OLAP系统上表现都非常出色。同时在机器学习上,配合Madlib项目可以让PostgreSQL如虎添翼。Madlib包括了多种机器学习算法,比如分类、聚类、文本分析、回归分析、关联规则挖掘和验证分析等功能。这样我们可以通过使用SQL,在PostgreSQL中使用各种机器学习算法模型,帮我们进行数据挖掘和分析。
|
||||
|
||||
2018年Google将机器学习(Machine Learning)工具集成到了BigQuery中,发布了BigQuery ML,这样开发者就可以在大型的结构化或半结构化的数据集上构建和使用机器学习模型。通过BigQuery控制台,开发者可以像使用SQL语句一样来完成机器学习模型的训练和预测。
|
||||
|
||||
SQLFlow是蚂蚁金服于2019年开源的机器学习工具,我们通过使用SQL就可以完成机器学习算法的调用,你可以将SQLFlow理解为机器学习的翻译器。我们在SELECT之后加上TRAIN从句就可以完成机器学习模型的训练,在SELECT语句之后加上PREDICT就可以使用模型来进行预测。这些算法模型既包括了传统的机器学习模型,也包括了基于Tensorflow、PyTorch等框架的深度学习模型。
|
||||
|
||||
从下图中你也能看出SQLFlow的使用过程,首先我们可以通过Jupyter notebook来完成SQL语句的交互。SQLFlow支持了多种SQL引擎,包括MySQL、Oracle、Hive、SparkSQL和Flink等,这样我们就可以通过SQL语句从这些DBMS中抽取数据,然后选择想要进行的机器学习算法(包括传统机器学习和深度学习模型)进行训练和预测。不过这个工具刚刚上线,工具、文档、社区还有很多需要完善的地方。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/fd/e50038152a1b4e7a9940919be9634dfd.jpg" alt=""><br>
|
||||
最后一个方法是SQL+Python,也是我们今天要讲解的内容。刚才介绍的工具可以说既是SQL查询数据的入口,也是数据分析、机器学习的入口。不过这些模块耦合度高,也可能存在使用的问题。一方面工具会很大,比如在安装SQLFlow的时候,采用Docker方式(下图为使用Docker安装sqlflow的过程)进行安装,整体需要下载的文件会超过2G。同时,在进行机器学习算法调参、优化的时候也存在灵活度差的情况。因此最直接的方式,还是将SQL与机器学习模块分开,采用SQL读取数据,然后通过Python来进行机器学习的处理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/c8/38864b57d8d65728439b730d57d841c8.png" alt="">
|
||||
|
||||
## 案例:挖掘零售数据中的频繁项集与关联规则
|
||||
|
||||
刚才我们讲解了如何通过SQL来完成数据分析(机器学习)的5种姿势,下面我们还需要通过一个案例来进行具体的讲解。
|
||||
|
||||
我们要分析的是购物篮问题,采用的技术为关联分析。它可以帮我们在大量的数据集中找到商品之间的关联关系,从而挖掘出经常被人们购买的商品组合,一个经典的例子就是“啤酒和尿布”的例子。
|
||||
|
||||
今天我们的数据集来自于一个面包店的21293笔订单,字段包括了Date(日期)、Time(时间)、Transaction(交易ID)以及Item(商品名称)。其中交易ID的范围是[1,9684],在这中间也有一些交易ID是空缺的,同一笔交易中存在商品重复的情况。除此以外,有些交易是没有商品的,也就是对应的Item为NONE。具体的数据集你可以从[GitHub](https://github.com/cystanford/SQLApriori)上下载。
|
||||
|
||||
我们采用的关联分析算法是Apriori算法,它帮我们查找频繁项集,首先我们需要先明白什么是频繁项集。
|
||||
|
||||
频繁项集就是支持度大于等于最小支持度阈值的项集,小于这个最小值支持度的项目就是非频繁项集,而大于等于最小支持度的项集就是频繁项集。支持度是个百分比,指的是某个商品组合出现的次数与总次数之间的比例。支持度越高,代表这个组合出现的频率越大。
|
||||
|
||||
我们来看个例子理解一下,下面是5笔用户的订单,以及每笔订单购买的商品:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/38/58d7791f7b1fe08f810e9e630b03bf38.png" alt=""><br>
|
||||
在这个例子中,“牛奶”出现了4次,那么这5笔订单中“牛奶”的支持度就是4/5=0.8。同样“牛奶+面包”出现了3次,那么这5笔订单中“牛奶+面包”的支持度就是3/5=0.6。
|
||||
|
||||
同时,我们还需要理解一个概念叫做“置信度”,它表示的是当你购买了商品A,会有多大的概率购买商品B,在这个例子中,置信度(牛奶→啤酒)=2/4=0.5,代表如果你购买了牛奶,会有50%的概率会购买啤酒;置信度(啤酒→牛奶)=2/3=0.67,代表如果你购买了啤酒,有67%的概率会购买牛奶。
|
||||
|
||||
所以说置信度是个条件概念,指的是在A发生的情况下,B发生的概率是多少。
|
||||
|
||||
我们在计算关联关系的时候,往往需要规定最小支持度和最小置信度,这样才可以寻找大于等于最小支持度的频繁项集,以及在频繁项集的基础上,大于等于最小置信度的关联规则。
|
||||
|
||||
## 使用SQL+Python完成零售数据的关联分析
|
||||
|
||||
针对上面的零售数据关联分析的案例,我们可以使用工具自带的关联规则进行分析,比如使用SQL Server Analysis Services的多维数据分析,或者是在Madlib、BigQuery ML、SQLFlow工具中都可以找到相应的关联规则,通过写SQL的方式就可以完成关联规则的调用。
|
||||
|
||||
除此以外,我们还可以直接使用SQL完成数据的查询,然后通过Python的机器学习工具包完成关联分析。下面我们通过之前讲解的SQLAlchemy来完成SQL查询,使用efficient_apriori工具包的Apriori算法。整个工程一共包括3个部分。
|
||||
|
||||
第一个部分为数据加载,首先我们通过sql.create_engine创建SQL连接,然后从bread_basket数据表中读取全部的数据加载到data中。这里需要配置你的MySQL账户名和密码
|
||||
|
||||
第二步为数据预处理,因为数据中存在无效的数据,比如item为NONE的情况,同时Item的大小写格式不统一,因此我们需要先将Item字段都转换为小写的形式,然后去掉Item字段中数值为none的项。在数据预处理中,我们还需要得到一个transactions数组,里面包括了每笔订单的信息,其中每笔订单是以集合的形式进行存储的,这样相同的订单中item就不存在重复的情况,同时也可以使用Apriori工具包直接进行计算。
|
||||
|
||||
最后一步,使用Apriori工具包进行关联分析,这里我们设定了参数min_support=0.02,min_confidence=0.5,也就是最小支持度为0.02,最小置信度为0.5。根据条件找出transactions中的频繁项集itemsets和关联规则rules。
|
||||
|
||||
具体的代码如下:
|
||||
|
||||
```
|
||||
from efficient_apriori import apriori
|
||||
import sqlalchemy as sql
|
||||
import pandas as pd
|
||||
# 数据加载
|
||||
engine = sql.create_engine('mysql+mysqlconnector://root:passwd@localhost/wucai')
|
||||
query = 'SELECT * FROM bread_basket'
|
||||
data = pd.read_sql_query(query, engine)
|
||||
# 统一小写
|
||||
data['Item'] = data['Item'].str.lower()
|
||||
# 去掉none项
|
||||
data = data.drop(data[data.Item == 'none'].index)
|
||||
|
||||
# 得到一维数组orders_series,并且将Transaction作为index, value为Item取值
|
||||
orders_series = data.set_index('Transaction')['Item']
|
||||
# 将数据集进行格式转换
|
||||
transactions = []
|
||||
temp_index = 0
|
||||
for i, v in orders_series.items():
|
||||
if i != temp_index:
|
||||
temp_set = set()
|
||||
temp_index = i
|
||||
temp_set.add(v)
|
||||
transactions.append(temp_set)
|
||||
else:
|
||||
temp_set.add(v)
|
||||
# 挖掘频繁项集和频繁规则
|
||||
itemsets, rules = apriori(transactions, min_support=0.02, min_confidence=0.5)
|
||||
print('频繁项集:', itemsets)
|
||||
print('关联规则:', rules)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
频繁项集: {1: {('alfajores',): 344, ('bread',): 3096, ('brownie',): 379, ('cake',): 983, ('coffee',): 4528, ('cookies',): 515, ('farm house',): 371, ('hot chocolate',): 552, ('juice',): 365, ('medialuna',): 585, ('muffin',): 364, ('pastry',): 815, ('sandwich',): 680, ('scandinavian',): 275, ('scone',): 327, ('soup',): 326, ('tea',): 1350, ('toast',): 318, ('truffles',): 192}, 2: {('bread', 'cake'): 221, ('bread', 'coffee'): 852, ('bread', 'pastry'): 276, ('bread', 'tea'): 266, ('cake', 'coffee'): 518, ('cake', 'tea'): 225, ('coffee', 'cookies'): 267, ('coffee', 'hot chocolate'): 280, ('coffee', 'juice'): 195, ('coffee', 'medialuna'): 333, ('coffee', 'pastry'): 450, ('coffee', 'sandwich'): 362, ('coffee', 'tea'): 472, ('coffee', 'toast'): 224}}
|
||||
关联规则: [{cake} -> {coffee}, {cookies} -> {coffee}, {hot chocolate} -> {coffee}, {juice} -> {coffee}, {medialuna} -> {coffee}, {pastry} -> {coffee}, {sandwich} -> {coffee}, {toast} -> {coffee}]
|
||||
|
||||
```
|
||||
|
||||
从结果中你能看到购物篮组合中,商品个数为1的频繁项集有19种,分别为面包、蛋糕、咖啡等。商品个数为2的频繁项集有14种,包括(面包,蛋糕),(面包,咖啡)等。其中关联规则有8种,包括了购买蛋糕的人也会购买咖啡,购买曲奇的同时也会购买咖啡等。
|
||||
|
||||
## 总结
|
||||
|
||||
通过SQL完成机器学习往往还是需要使用到Python,因为数据分析是Python的擅长。通过今天的学习你应该能体会到采用SQL工具作为数据查询和分析的入口是一种数据全栈的思路,对于开发人员来说降低了数据分析的技术门槛。
|
||||
|
||||
如果你想要对机器学习或者数据分析算法有更深入的理解,也可以参考我的《数据分析实战45讲》专栏,相信在当今的数据时代,我们的业务增长会越来越依靠于SQL引擎+AI引擎。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/63/886fc87f717463557457ef8e23218b63.png" alt=""><br>
|
||||
我在文章中举了一个购物篮分析的例子,如下图所示,其中(牛奶、面包、尿布)的支持度是多少呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/e6/a1767ae691f2c18d02f8009a687ba1e6.png" alt=""><br>
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user