Spark机器学习进阶实战
上QQ阅读APP看书,第一时间看更新

2.2 数据分析流程

数据分析可以帮助我们从数据中发现有用信息,找出有建设性的结论,并基于分析结论辅助决策。如图2-2所示,数据分析流程主要包括业务调研、明确目标、数据准备、特征处理、模型训练与评估、输出结论6个关键环节。

图2-2 数据分析流程

数据分析能力并非一朝一夕养成的,需要长期扎根业务进行积累,需要长期根据数据分析流程一步一个脚印地分析问题,培养自己对数据的敏感度,从而养成用数据分析、用数据说话的习惯。当你可以基于一些数据,根据自己的经验做出初步的判断和预测,你就基本拥有数据思维了。

接下来对数据分析流程进行针对性的讲解。

2.2.1 业务调研

数据分析的常见困难是不知道最值得调研的问题是什么、无法获取大量可靠的数据、复杂问题难以拆解、难以追查问题背后的真实原因、难以提前预判方案是否可行。

调研主要分为3个阶段。

1)调研准备阶段,这个阶段要进行调研计划编写和确认、调研背景资料的准备,这个阶段的工作质量将对能否顺利开展调研工作起到关键保障作用。

2)调研实施阶段,根据调研计划完成各项调研工作,倾听客户的痛点、难点、堵点,思考解决痛点、攻克难点、疏通堵点的思路和方法。

3)调研总结阶段,根据现场调研结果,总结并提出合理数据分析解决方案,找出问题的真正原因,趁热打铁,把后续工作落实到一定程度,此时调研工作才能算结束。

调研是数据分析的起点,也是指引后续分析的灯塔。很多时候,调研清楚了,数据采集到了,结论就很明显了。

2.2.2 明确目标

明确目标是指通过调研结果定义问题、拆解问题,根据拆解的问题进一步量化指标,具体思路如下。

1)问题定义,对调研出来的问题进行定义,找出性价比最高的问题。

2)拆解问题,一个问题并非仅由一个原因引起,可能是很多因素互相影响的结果,这个时候要找出所有可能的因素,对问题进行拆解。比如网页搜索体验不好,原因可能是数据来源问题、相关性问题、Query解析问题、版本不稳定等。

3)量化指标,就是将指标进行量化,转化为数值型或者有序型指标。将指标进行量化,如版本稳定性这样的影响因子,可以通过崩溃日志和错误日志计算出每个版本的崩溃率、出错率等指标来表达。有时受限于我们拥有的数据,并不是列举的所有留存影响因素都能被处理成量化的指标,因此就需要通过技术手段获得这些数据。

表2-3 本地测试参数和值

2.2.3 数据准备

明确目标之后,进一步需要确定数据源,准备数据、统一数据标准,并对数据进行预处理。在这个阶段,需要把各个字表关联起来,形成一张数据宽表。

❑ 数据源,确定数据来源,并有效地获取数据。

❑ 数据标准,统一定义数据指标的含义,对于数据标准的明确,需要结合数据分析调研的需求以及具体业务场景,定义清晰的数据标准对后面的数据ETL以及建模、分析具有重要意义。

❑ 数据预处理,也叫数据ETL,用来描述对原始数据抽取、清洗转换和加载的过程。ETL按照统一的规则集成并提高数据的价值,是将数据从数据源向目标数据仓库(DW)转化的过程。

(1)数据抽取

数据的抽取是从各个不同的数据源抽取数据并存储到操作数据存储(Operational Data Store, ODS)中的过程,在抽取的过程中需要选择不同的抽取方法,尽量提高ETL的运行效率。一般认为,与存放DW的数据库系统相同的数据源直接建立链接,与DW数据库系统不同的数据源采取工具导出、工具导入的方法,对于文件类型的数据源先利用业务人员导入临时数据库中转抽取,对于数据量大的系统采取增量抽取。

(2)数据清洗转换

数据清洗转换包括数据清洗和数据转换两个过程。

数据清洗是指对空数据、缺失数据进行补缺操作,对非法数据进行替换,保证数据的正确性。

数据转换是指对数据进行整合、拆分和变换,数据整合是指通过多表关联,将不同类型数据之间可能存在潜在关联关系的多条数据进行合并,通过数据的整合,丰富数据维度,有利于发现更多有价值的信息。数据拆分是指按一定规则对数据进行拆分,将一条数据拆分为多条。数据变换是指对数据进行行列互换、排序、修改序号、去除重复记录变换操作。

(3)数据加载

数据加载将清洗转换后的数据加载到数据仓库中,数据加载有多种方式,主要包括:时间戳方式、日志表方式、全表对比方式、全表删除再插入方式等,其中时间戳方式、日志表方式、全表对比方式属于增量加载,全表删除再插入方式属于全量加载,实践中建议增量加载。

在实际应用中,单机处理海量数据的ETL变得越来越困难,Spark能够较好地支持海量数据的ETL工作,Spark的应用程序编程接口(Application Programming Interface, API)简单易用,处理大数据时效率很高,并且较好地支持了对各种主流数据库的访问,DataFrame提供了详细的数据结构信息,使得Spark SQL可以方便地了解数据的组成、结构和数据类型。

2.2.4 特征处理

数据预处理对数据进行了初步的抽取和清洗,更进一步,可以从数据中提取有用的特征用于机器学习建模,接下来介绍数据特征处理的方法。

在数据分析中,我们把数据对象称作样本,数据对象拥有一些基本特性,叫作特征或维度。例如,对于一个学生信息的样本数据,每一个样本对应一个学生,而数据集中学生的ID、年级、成绩等则是学生的特征。

1.特征向量化

除了基本的统计分析之外,机器学习模型要求输入特征向量,原始特征需要转化为特征向量,才能用于机器学习模型的训练,下面介绍各类特征向量化的方法。

常用特征包括数值特征、类别特征、文本特征、统计特征等。

1)数值特征:数值类型的特征,如年龄、温度等,一般可以直接作为特征向量的维度使用,它可以取无穷多的值。

2)类别特征:具有相同特性的特征,如一幅图片的颜色(黑色、棕色、蓝色等)就属于类别特征,类别特征有可穷举的值。类别特征不能直接使用,一般对类别特征进行编号,将其转化为数值特征。

3)文本特征:从文本内容中提取出来的特征,如电影评论,文本特征也不能直接使用,需要进行分词、编码等处理,接下来会具体介绍文本特征的处理方法。

4)统计特征:从原始数据中使用统计方法得到的高级特征,常用的统计特征包括平均值、中位数、求和、最大值、最小值等。统计特征比原始特征包含更多的信息,通常使用统计特征可以得到更好的模型训练效果,统计特征和数值特征一样可以直接作为特征向量的维度使用。

5)其他特征:还有一些特征不属于上述特征的范畴,如音频、视频特征,地理位置特征等。这些特征需要使用特殊的处理方法,如图像需要转化为SIFT特征,音频需要转化为MFCC特征等。

实践中,类别特征的可取值比数值特征要少得多,在进行统计分析时更容易处理,所以我们有时需要通过分段,把数值特征转化为类别特征以便于分析建模,例如我们可以把连续的身高特征分成150cm以下、150cm~180cm、180cm以上三个类别。

2.文本特征处理

文本特征是一类常见的特征,相比类别特征和数值特征,它的处理要复杂得多,一般对文本特征的处理,需要经过分词、去停用词、词稀疏编码等步骤,MLlib为这些处理步骤提供了相应的方法。

(1)分词

MLlib提供Tokenization方法对文本进行分词,RegexTokenizer基于正则表达式匹配提供了更高级的分词。默认用多个空格(\s+)作为分隔符,可以通过参数pattern指定分隔符,分词的样例代码如下:

        import org.apache.spark.ml.feature.{RegexTokenizer, Tokenizer}
        val sentenceDataFrame = spark.createDataFrame(Seq(
          (0, "Hi I heard about Spark"),
          (1, "I wish Java could use case classes"),
          (2, "Logistic, regression, models, are, neat")
        )).toDF("label", "sentence")

        val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
        val regexTokenizer = new RegexTokenizer()
          .setInputCol("sentence")
          .setOutputCol("words")
          .setPattern("\\W") // alternatively .setPattern("\\w+")
            .setGaps(false)

        val tokenized = tokenizer.transform(sentenceDataFrame)
        tokenized.select("words", "label").take(3).foreach(println)

        val regexTokenized = regexTokenizer.transform(sentenceDataFrame)
        regexTokenized.select("words", "label").take(3).foreach(println)

(2)去停用词

停用词是那些需要从输入数据中排除掉的词,这些词出现频繁,却并没有携带太多有意义的信息。MLlib提供StopWordsRemover方法实现这一功能。停用词表通过stopWords参数来指定。可以通过调用loadDefaultStopWords(language:string)调用默认的停用词表,默认词表提供Jenglish、french、germon、danish等几种语言的停用词,但对于中文停用词需要自己提供。代码示例如下:

        import org.apache.spark.ml.feature.StopWordsRemover

        val remover = new StopWordsRemover()
          .setInputCol("raw")
          .setOutputCol("filtered")

        val dataSet = spark.createDataFrame(Seq(
          (0, Seq("I", "saw", "the", "red", "baloon")),
          (1, Seq("Mary", "had", "a", "little", "lamb"))
        )).toDF("id", "raw")

        remover.transform(dataSet).show()

(3)词稀疏编码

分词和去停用词之后把一篇文章变成了一个词的集合,现在需要把这个集合用数值来表示,我们使用MLlib提供的StringIndexer方法来实现这一需求。StringIndexer给每个词按照出现频率安排一个编号索引,索引的范围是[0, vocab_size), vocab_size为词表的大小,示例代码如下:

        import org.apache.spark.ml.feature.StringIndexer

        val df = spark.createDataFrame(
          Seq((0, "a"), (1, "b"), (2, "c"), (3, "a"), (4, "a"), (5, "c"))
        ).toDF("id", "category")

        val indexer = new StringIndexer()
          .setInputCol("category")
          .setOutputCol("categoryIndex")

        val indexed = indexer.fit(df).transform(df)
        indexed.show()

此外,MLlib还为文本处理提供了Ngram、TF/IDF、word2vec等高级方法,可以在实践中查看相关资料。

3.特征预处理

在前面的章节中我们介绍了对各类特征进行处理的方法,在使用生成的特征进行训练之前,对特征进行预处理有助于优化模型训练效果,提升模型训练速度。MLlib提供了丰富的特征预处理方法,下面介绍3种最常用的特征预处理方法。

(1)特征归一化

特征归一化是用来统一特征范围的方法,它对特征进行标准化,将特征值的大小映射到一个固定的范围内,从而避免特征量级差距过大影响模型训练的情形,此外特征归一化还能加速训练的收敛。

MLlib提供3种归一化方法:StandardScaler、MinMaxScaler和MaxAbsScaler。Standard-Scaler对所有数据减去均值除以标准差,处理后的数据均值变为0,标准差变为1; MinMax-Scaler将每个特征调整到一个特定的范围(通常是[0,1]),在转化过程中可能把0转化为非0的值,因此可能会破坏数据的稀疏性;MaxAbsScaler转换将每个特征调整到[-1,1]的范围,它通过每个特征内的最大绝对值来划分,不会破坏数据的稀疏性。

其中,StandardScaler是使用最广泛的归一化方法,使用StandardScaler方法进行特征归一化的示例代码如下。

        import org.apache.spark.SparkContext._
        import org.apache.spark.mllib.feature.StandardScaler
        import org.apache.spark.mllib.linalg.Vectors
        import org.apache.spark.mllib.util.MLUtils
        // Spark程序data文件夹下的测试数据
        val data = MLUtils.loadLibSVMFile(sc, "data/MLlib/sample_libsvm_data.txt")
        val scaler1 = new StandardScaler().fit(data.map(x => x.features))
        val scaler2 = new StandardScaler(withMean = true, withStd = true).fit(data.map(x
    => x.features))
        // scaler3是与scaler2相同的模型,并且会产生相同的转换
        val scaler3 = new StandardScalerModel(scaler2.std, scaler2.mean)
        // data1是单位方差
        val data1 = data.map(x => (x.label, scaler1.transform(x.features)))
        //如果不将这些特征转换成密度向量,那么零均值转换就会增加。稀疏向量例外
        // data2将是单位方差和零均值
        val  data2  =  data.map(x  =>  (x.label,  scaler2.transform(Vectors.dense(x.features.
    toArray))))

(2)正则化

正则化是指计算某个特征向量的p-范数(p=0,范数是指向量中非零元素的个数;p=1,范数为绝对值之和;p=2,范数是指通常意义上的模;p=无穷,范数是取向量的最大值),然后对每个元素除以p-范数,以将特征值正则化。正则化后不仅可以加快梯度下降求最优解的速度,还能提高模型精度。

MLlib提供Normalizer方法实现特征正则化,示例代码如下:

        import org.apache.spark.SparkContext._
        import org.apache.spark.MLlib.feature.Normalizer
        import org.apache.spark.MLlib.linalg.Vectors
        import org.apache.spark.MLlib.util.MLUtils
        // Spark程序data文件夹下的测试数据
        val data = MLUtils.loadLibSVMFile(sc, "data/MLlib/sample_libsvm_data.txt")
        // 默认情况下,p=2,计算2阶范数
        val normalizer1 = new Normalizer()
        // p正无穷范数
        val normalizer2 = new Normalizer(p = Double.PositiveInfinity)
        // data1中的每个样本将使用L2范数进行标准化
        val data1 = data.map(x => (x.label, normalizer1.transform(x.features)))
        // data2中的每个样本将使用无穷范数进行标准化
        val data2 = data.map(x => (x.label, normalizer2.transform(x.features)))

(3)二值化

二值化是一个将数值特征转换为二值特征的处理过程,根据一个阈值将数值特征分为两类,值大于阈值的特征二值化为1,否则二值化为0。二值化能够大大减少特征的复杂度,提高训练效率,但在二值化的过程中损失了一些信息,这可能会影响训练的效果,二值化的代码示例如下:

        import org.apache.spark.ml.feature.Binarizer

        val data = Array((0, 0.1), (1, 0.8), (2, 0.2))
        val dataFrame = spark.createDataFrame(data).toDF("label", "feature")

        val binarizer: Binarizer = new Binarizer()
          .setInputCol("feature")
          .setOutputCol("binarized_feature")
          .setThreshold(0.5)

        val binarizedDataFrame = binarizer.transform(dataFrame)
        val binarizedFeatures = binarizedDataFrame.select("binarized_feature")
        binarizedFeatures.collect().foreach(println)

若训练数据的指标维数太高,则容易造成模型过拟合,且相互独立的特征维数越高,在测试集上达到相同的效果表现所需要的训练样本的数目就越大。此外,指标数量过多,训练、测试以及存储的压力也都会增大,可运用主成分分析等手段,以Spark为工具对数据集进行计算降维。

2.2.5 模型训练与评估

模型构建是数据分析工作的核心阶段,主要包括如下几点。

(1)准备数据集

使用机器学习构建模型的时候,需要将数据集切分为训练数据(Train Data)和测试数据(Test Data)。训练数据用于构建模型,但是有时候在模型构建过程中需要验证模型,辅助模型构建,此时会将训练数据分出一部分作为验证数据(Validation Data)。测试数据用于检测模型,评估模型的准确率。

Spark提供了将数据集切分为训练集和测试集的函数,默认数据集的70%作为训练数据,30%作为测试数据。

(2)选择适当的建模技术

构建模型是数据分析的关键,选择合适的建模技术直接影响数据分析的结果,这里提供一些建模技术的选择经验和思考,希望能够引起共鸣。

❑ 汇总统计。

解决问题:输入法的月留存率是多少?我们的用户人均装有多少个有效App?

建模方法:加和、计数、均值、标准差、中位数、众数、四分位数、最大值、最小值等。

❑ 对比分析。

解决问题:某公司年利润2.2亿,同比增长8%。内容的某两种推送方式的效果有显著差异吗?

建模方法:卡方检验、方差分析等。

❑ 趋势分析。

解决问题:合肥2017年10月新房均价涨到11000/平方米,现在要不要购买呢?今年的业绩不错,预测一下明年的KPI。

建模方法:回归等。

❑ 分布分析。

解决问题:“小飞读报”的用户年龄结构是怎样的?这款巧克力代可可脂、糖分、添加剂占比情况如何?

建模方法:统计分布等。

❑ 因子分析。

解决问题:最近用户留存率下降比较厉害,找出影响用户留存的因子,并加以解决。

建模方法:多元线性回归模型等。

❑ 聚类分析

解决问题:使用GPS数据看一下城市用户的生活模式,研究一下这款游戏App的主要用户群体,形成有针对性的营销方案。

建模方法:分层聚类分析、LDA主题模型等。

(3)建立模型

选择适当的建模技术,使用Spark技术针对训练数据进行训练,迭代训练,观察效果,并生成训练模型。

(4)模型评估

模型评估主要是将测试结果转化为混淆矩阵,表2-1展示了测试结果的4种情况。使用准确率、召回率等进行评估。

表2-1 数据测试结果的4种情况

说明:

❑ TP(True Positive,真正):被模型预测为正的正样本。

❑ FP(False Positive,假正):被模型预测为正的负样本。

❑ FN(False Negative,假负):被模型预测为负的正样本。

❑ TN(True Negative,真负):被模型预测为负的负样本。

模型评估的测试结果如图2-3所示,实心点代表正类、空心点代表负类,用椭圆标识被选中的元素。

图2-3 模型评估混淆矩阵图

准确率就是找得对,也叫作查准率,是针对预测结果而言的,它表示预测为正的样本中有多少是真正的正样本。

准确率(Precision)P:P=TP/(TP+FP)

召回率就是找得全,也叫作查全率,是针对原来的样本而言的,它表示样本中的正例有多少被预测正确了。

召回率(Recall Rate)R:R=TP/(TP+FN)

其他模型评估的相关方法,在具体章节中详细叙述。

2.2.6 输出结论

对于数据分析结论,首先需要设计一个好的分析框架,若它结构清晰、主次分明,则可以让读者正确理解报告内容。其次需要图文并茂,选择合适的图表类型和呈现,能够让读者一目了然,数据更加生动活泼,进而提高视觉冲击力,有助于阅读者更形象、直观地看清楚问题和结论,从而产生思考。

好的数据分析报告不局限于对数据进行简单的概括性总结,需要根据客观数据事实推论出明确的结论,并给出行动建议。衡量数据分析的结论包括及格、良好、优秀三个阶段。下面以某公司某产品处于市场领先地位而需要针对次位的竞争对手近期的发展进行数据分析为例来说明,如表2-2所示。

表2-2 某公司竞争对手近期的发展情况分析