利用PySpark的MLlib、ML Packages包预测客户流失

借翻译这篇文章的机会学习pySpark实现决策树分类,同时采用分层抽样的方法处理数据偏斜问题、利用k折交叉验证的方法进行了模型参数决策树深度的优化,从而获得了最佳决策树。
文章来源:

客户流失预测【Churn prediction】的研究有利于提前做出挽留动作,降低客户流失对利润增长造成的负面影响。客户流失预测在电芯、银行、保险等行业备受关注。
预测过程是基于数据驱动,通常是利用机器学习技术。在这篇文章中,我们会利用pySpark及其机器学习frameworks,关注那种客户数据,做一些基础的数据分析,并生成客户流失预警模型。

安装和运行Spark

这篇pyspark进行客户流失预测项目的github网址是notebook教程github链接
为了运行这篇notebook,我们需要安装Spark、pandas和matplotlib 包。
接下来的资源能帮你做好准备

运行之前添加环境变量和路径

"""运行之前添加环境变量和路径"""
import os
import sys

spark_path = "D:/spark-2.2.1-bin-hadoop2.7/spark-2.2.1-bin-hadoop2.7"

os.environ['SPARK_HOME'] = spark_path
os.environ['HADOOP_HOME'] = spark_path
sys.path.append(spark_path + "/bin")
sys.path.append(spark_path + "/python")
sys.path.append(spark_path + "/python/pyspark/")
sys.path.append(spark_path + "/python/lib")
sys.path.append(spark_path + "/python/lib/pyspark.zip")
sys.path.append(spark_path + "/python/lib/py4j-0.10.4-src.zip")

初始化

  • SparkContext是Spark功能的主要入口点,提供了spark的运行环境。
  • 在Spark2.0中,SparkSession作为数据集合、DataFrame的API,可用于创建 DataFrame在tables上进行SQL查询等,而在Spark1.0中, HiveContext或者SQLContext作为数据源的入口。
"""初始化pySpark"""
from pyspark import SparkContext
sc = SparkContext(appName="extract")
spark = SparkSession(sc)

导入数据

本教程中,我们将使用Orange Telecoms Churn Dataset。它包含清洗后的客户行为数据(特征)以及该客户是否流失的标签。数据可以由 BigML’s S3 bucket获取,已经被80/20比例拆分成两个文件:churn-80 churn-20 ,可以把大的文件当做训练集,进行交叉验证,小的数据集作为最终的测试集合性能评估集。尝试直接用csv读取报错,还是先下载到本地再读取。
我使用 spark.read.csv读取CSV文件并转化成Spark DataFrame格式。cache()操作是为了缓存到disk以方便读取。

"""导入数据"""
"""1、尝试直接用csv读取网页报错,还是先下载到本地再读取
2、inferSchema='true'是为了保留数据的Schema,否则所有数据都是string,但后期CV_data.select([col(c).cast("double").alias(c) for c in CV_data.columns])也是可以补救的
"""
#data = spark.read.csv('https://bml-data.s3.amazonaws.com/churn-bigml-20.csv',header=True)
CV_data  = spark.read.csv('../churn-bigml-80.csv',header=True,inferSchema='true')
final_test_data = spark.read.csv('../churn-bigml-20.csv',header=True,inferSchema='true')
CV_data.cache()
CV_data.printSchema()

为了看看这个数据集到底长什么样,我们选取CV_data前5行数据,并转成pandas的DataFrame格式显示,这样做的是因为pandasd的DataFrame比较好看。(づ。◕ᴗᴗ◕。)づ

"""显示前几行数据"""
import pandas as pd
pd.DataFrame(CV_data.take(5), columns=CV_data.columns).transpose()

概况统计

Spark DataFrames包括一些统计相关的内置函数 。比如说describe() ,pandas里也有,用来展示数值类变量的概况统计结果。

"""概况统计"""
CV_data.describe().toPandas()

相关性分析以及数据预处理

我们也可以使用 MLlib statistics package或者其他python包进行个性化的统计分析。在此,我们使用pandas包,通过生成散点图的,检验数值列之间的相关性。
由于pandas能处理的数据有限,我们随机选取部分数据(10%)来了解大概。

"""相关性分析以及数据预处理"""
numeric_features = [t[0] for t in CV_data.dtypes if t[1] == 'int' or t[1] == 'double']

sampled_data = CV_data.select(numeric_features).sample(False, 0.10).toPandas()

axs = pd.scatter_matrix(sampled_data, figsize=(12, 12));

#Rotate axis labels and remove axis ticks
n = len(sampled_data.columns)
for i in range(n):
    v = axs[i, 0]
    v.yaxis.label.set_rotation(0)
    v.yaxis.label.set_ha('right')
    v.set_yticks(())
    h = axs[n-1, i]
    h.xaxis.label.set_rotation(90)
    h.set_xticks(())

去除建模无用的字段State、Area Code
很明显有一些高度相关的字段【从图上看就是对角线一侧的方格内,数据散点组成了有向线段,斜率为正则正相关,为负即负相关】,比如Total day minutes 和Total day charge。这样的相关数据对我们的模型没有用处,提前去除其中一个字段即可。
另一方面,由于模型的要求,我们把类别型数据都转换成数字。采用的方法是自定义函数分别将 Yes/True and No/False映射成1和0。

"""去除数据并修改标称型数值为0/1"""
from pyspark.sql.types import DoubleType
from pyspark.sql.functions import UserDefinedFunction

binary_map = {'Yes':1.0, 'No':0.0, 'True':1.0, 'False':0.0}
toNum = UserDefinedFunction(lambda k: binary_map[k], DoubleType())

CV_data = CV_data.drop('State').drop('Area code') \
    .drop('Total day charge').drop('Total eve charge') \
    .drop('Total night charge').drop('Total intl charge') \
    .withColumn('Churn', toNum(CV_data['Churn'])) \
    .withColumn('International plan', toNum(CV_data['International plan'])) \
    .withColumn('Voice mail plan', toNum(CV_data['Voice mail plan'])).cache()

final_test_data = final_test_data.drop('State').drop('Area code') \
    .drop('Total day charge').drop('Total eve charge') \
    .drop('Total night charge').drop('Total intl charge') \
    .withColumn('Churn', toNum(final_test_data['Churn'])) \
    .withColumn('International plan', toNum(final_test_data['International plan'])) \
    .withColumn('Voice mail plan', toNum(final_test_data['Voice mail plan'])).cache()
pd.DataFrame(CV_data.take(5), columns=CV_data.columns).transpose()

使用Spark MLlib Package

MLlib Package提供了一些常用的机器学习分类、回归、聚类和降维、模型性能评估算法。决策树是一种常用的分类算法,我们来介绍一下:

决策树模型

决策树在数据挖掘中扮演重要角色,这种方法

  1. 易于理解、可读性强:能直接展示特征选取和样本预测模型的中间过程。
  2. 数据要求不高:决策树不仅对数据类型【离散型或者连续型】的要求不高,也不要求对数据进行标准化。
  3. 可以通过剪枝或者限制深度的方式提高预测精度,也能作为弱分类器集成为强分类器(比如随机森林)
    决策树是预测模型,将观测特征值与类别标签建立映射关系。模型采用自上而下的方式生成,数据源以Gini指数或者信息增益等统计方法为依据,被分割为子数据集数据或者因预定的停止标准而终止分支。
模型训练

MLlib分类器和回归其要求数据集以行标签+特征list的LabeledPoint行形式存储。采用自定义的labelData() 函数展示行处理方式,通过该函数将(CV_data) 转换为符合条件的数据集,然后分为训练集和测试集。为了降低模型的复杂度,我们先利用训练数据生成一个最大深度为2的浅决策树分类模型。

"""训练模型"""
from pyspark.mllib.regression import LabeledPoint
from pyspark.mllib.tree import DecisionTree

def labelData(data):
    # label: row[end], features: row[0:end-1]
    return data.map(lambda row: LabeledPoint(row[-1], row[:-1]))

training_data, testing_data = labelData(CV_data).randomSplit([0.8, 0.2])

model = DecisionTree.trainClassifier(training_data, numClasses=2, maxDepth=2,
                                     categoricalFeaturesInfo={1:2, 2:2},
                                     impurity='gini', maxBins=32)

print(model.toDebugString())
print('Feature 12:', CV_data.columns[12])
print('Feature 4: ', CV_data.columns[4])

toDebugString()函数可以打印决策树的节点和叶节点的预测结果。我们可以看到特征12(Customer service calls )和4(Total day minutes)被用做决策节点,因此我们认为这两个参数与客户流失概率有强相关性。由于决策树这种能自动判断最重要的特征的强技能,被用于特征选取也是得其所也。

模型性能评估

测试数据的流失率预测结果包括模型的预测函数和实际标签,我们将使用MLlib’s MulticlassMetrics() 进行模型评估。将预测值和标签以元组形式作为输入,然后输出诸如precision, recall, F1 score and confusion matrix的评估结果。

"""模型性能评估"""
from pyspark.mllib.evaluation import MulticlassMetrics

def getPredictionsLabels(model, test_data):
    predictions = model.predict(test_data.map(lambda r: r.features))
    return predictions.zip(test_data.map(lambda r: r.label))

def printMetrics(predictions_and_labels):
    metrics = MulticlassMetrics(predictions_and_labels)
    print('Precision of True ', metrics.precision(1))
    print('Precision of False', metrics.precision(0))
    print('Recall of True    ', metrics.recall(1))
    print('Recall of False   ', metrics.recall(0))
    print('F-1 Score         ', metrics.fMeasure())
    print('Confusion Matrix\n', metrics.confusionMatrix().toArray())

predictions_and_labels = getPredictionsLabels(model, testing_data)

printMetrics(predictions_and_labels)

整体accuracy, F-1 score结果不错,但是一个问题就是
recall率的偏斜。假流失样本recall(覆盖率)高,但是真流失样本recall(覆盖率)低,商业决策关注的核心是保留最可能流失的客户,而不是那些可能留存的客户。因此我们需要确保模型对真流失样本的准确性。
模型偏差的一个原因是样本数据的偏斜,毕竟流失样本数量远远低于留存客户量,我们看看按照是否流失分组得到的数量分布:

"""数据中是否流失样本数的对比"""
CV_data.groupby('Churn').count().toPandas()

分层采样

假流失样本数是真流失样本数的6倍,我们可以利用分层采样的方法把这两种样本转换为相同比例,为此我们选择可以实现按比例抽样的sampleBy()函数

"""分层抽样,避免数据偏斜的影响"""
stratified_CV_data = CV_data.sampleBy('Churn', fractions={0: 388./2278, 1: 1.0}).cache()
stratified_CV_data.groupby('Churn').count().toPandas()

这里我们保留所有的真流失样本,而减少留存样本的数量,接下来我们使用均匀分布的数据集建立模型,观察模型的性能。

"""针对分层抽样数据重新建立模型"""
training_data, testing_data = labelData(stratified_CV_data).randomSplit([0.8, 0.2])

model = DecisionTree.trainClassifier(training_data, numClasses=2, maxDepth=2,
                                     categoricalFeaturesInfo={1:2, 2:2},
                                     impurity='gini', maxBins=32)

predictions_and_labels = getPredictionsLabels(model, testing_data)
printMetrics(predictions_and_labels)

针对这些recall结果,我们看到分层抽样数据集有助于建立无偏模型,提供更稳健的预测结果。

采用Spark ML Package¶

ML package提供pipelining data transformers、estimators and model selectors的API。我们将利用它实现交叉检验从而选取最优深度的决策树模型。

Pipelining

ML package要求数据DataFrame以 (label: Double, features: Vector) 的形式作为输入,采用函数vectorizeData() 来变形。
接下来我们传递数据two transformers, StringIndexer() and VectorIndexer(),分别代表标签和特征变量。 将类别型特征索引化有利于提高决策树的性能。最后pipeline里包括一个经训练的预测器。

模型选取

为了确定哪个参数值产生最好的模型,我们需要一个系统的方法量化模型性能并确保结果的可靠性。模型选取通常采用交叉验证的方式进行,常用的方法是k-折交叉验证(k-fold cross validation)。其基本原理是将数据集随机拆分成k部分,将一部分被用作测试数据集,其余的作为训练集,重复k次。通过使用训练集和测试集生成模型,产生k个模型后,以平均性能得分作为整体得分。
为了模型选取,我们遍历模型参数,比较他们的交叉验证性能。性能最佳的模型参数即为所求。
ML package支持k-折交叉验证,可以偶联parameter grid builder和evaluator以建立模型选取工作流。接下来,我们将使用一个transformation/estimation pipeline来训练模型,交叉验证器将使用ParamGridBuilder来遍历设定的决策树maxDepth 参数,并用F1-score作为指标评估性能,重读3次以获得可靠结果。

"""模型选取"""
from pyspark.mllib.linalg import Vectors
from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, VectorIndexer
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.evaluation import MulticlassClassificationEvaluator

def vectorizeData(data):
    return data.map(lambda r: [r[-1], Vectors.dense(r[:-1])]).toDF(['label','features'])

vectorized_CV_data = vectorizeData(stratified_CV_data)

# Index labels, adding metadata to the label column
labelIndexer = StringIndexer(inputCol='label',
                             outputCol='indexedLabel').fit(vectorized_CV_data)

# Automatically identify categorical features and index them
featureIndexer = VectorIndexer(inputCol='features',
                               outputCol='indexedFeatures',
                               maxCategories=2).fit(vectorized_CV_data)

# Train a DecisionTree model
dTree = DecisionTreeClassifier(labelCol='indexedLabel', featuresCol='indexedFeatures')

# Chain indexers and tree in a Pipeline
pipeline = Pipeline(stages=[labelIndexer, featureIndexer, dTree])

# Search through decision tree's maxDepth parameter for best model
paramGrid = ParamGridBuilder().addGrid(dTree.maxDepth, [2,3,4,5,6,7]).build()

# Set F-1 score as evaluation metric for best model selection
evaluator = MulticlassClassificationEvaluator(labelCol='indexedLabel',
                                              predictionCol='prediction', metricName='f1')    

# Set up 3-fold cross validation
crossval = CrossValidator(estimator=pipeline,
                          estimatorParamMaps=paramGrid,
                          evaluator=evaluator,
                          numFolds=3)

CV_model = crossval.fit(vectorized_CV_data)

# Fetch best model
tree_model = CV_model.bestModel.stages[2]
print(tree_model)

我们发现小差验证性能最好的决策树模型深度是5,这意味着起始层数为2的决策树复杂程度不够,而深度超过5的过拟合从而导致测试性能较差。

预测以及模型性能评估

模型实际性能可以用final_test_data 数据集检验。我们转换数据集后,evaluator会给出预测结果的F-1 score ,然后我们也可以打印出他们的概率。后续也可以用CV_model.transform() 函数预测新的数据。

"""预测以及模型性能评估"""
vectorized_test_data = vectorizeData(final_test_data)

transformed_data = CV_model.transform(vectorized_test_data)
print evaluator.getMetricName(), 'accuracy:', evaluator.evaluate(transformed_data)

predictions = transformed_data.select('indexedLabel', 'prediction', 'probability')
predictions.toPandas().head()

客户流失概率可以用来表征客户的流失可能性,如此便能在有限的资源条件下挽回尽可能流失客户。

猜你喜欢

转载自blog.csdn.net/charie411/article/details/82877696