-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcustomer_churn.py
More file actions
300 lines (204 loc) · 10.2 KB
/
Copy pathcustomer_churn.py
File metadata and controls
300 lines (204 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# Databricks notebook source
from pyspark.sql.types import StructType, StructField, LongType, StringType, DoubleType
custfile = 'dbfs:///FileStore/tables/internet_service_churn.csv'
custschema = StructType([StructField('id', LongType(), False),
StructField('is_tv_subscriber', LongType(), True),
StructField('is_movie_package_subscriber', LongType(), True),
StructField('subscription_age', DoubleType(), True),
StructField('bill_avg', LongType(), True),
StructField('remaining_contract', DoubleType(), True),
StructField('service_failure_count', LongType(), True),
StructField('download_avg', DoubleType(), True),
StructField('upload_avg', DoubleType(), True),
StructField('download_over_limit', LongType(), True),
StructField('churn', LongType(), True)])
custdf = spark.read.format('csv').option('header', True).schema(custschema).load(custfile)
custdf.show()
# COMMAND ----------
# We dony need the ID so we will drop it
custdf = custdf.drop("id")
#Rename Churn with label
custdf = custdf.withColumnRenamed("churn","label")
#Separating the columns in categorical and continuous
cat_cols = ['is_tv_subscriber','is_movie_package_subscriber']
con_cols = ['subscription_age','bill_avg','remaining_contract','service_failure_count','download_avg','upload_avg','download_over_limit']
target_col = ['label']
print("The categorial cols are : ", cat_cols)
print("The continuous cols are : ", con_cols)
print("The target variable is : ", target_col)
# COMMAND ----------
from pyspark.sql.functions import isnull
from pyspark.sql.functions import *
#Check missing data
total_missing = custdf.select([count(when(col(c).isNull(),c)).alias(c) for c in custdf.columns])
display(total_missing)
# COMMAND ----------
#Check the data for null download average
custdf.filter(col("download_avg").isNull()).sample(withReplacement=False, fraction=0.20).show()
custdf.filter(col("upload_avg").isNull()).sample(withReplacement=False, fraction=0.20).show()
print('Total Number of rows:',custdf.count())
percentmiss = (381/custdf.count())*100
print('Percent of Missing value for download and upload:', percentmiss)
# COMMAND ----------
# MAGIC %md
# MAGIC According to the data information on remaining_contract,it is described as how many year remaining for customer contract. If null, that means customer hasnt have a contract. The customer who has a contract time have to use their service until contract end. if they canceled their service before contract time end they pay a penalty fare.
# MAGIC
# MAGIC Which mean null data are customer doesnt have any contract, and don't have the obligation to pay penalty in case they cancelled the service.
# MAGIC In this case we can replace the null data with 0 instead.
# MAGIC
# MAGIC Since the missing value for download and upload average are within the same rows, and adds up only 0.5% of the whole data, I'm just going to remove the missing rows.
# COMMAND ----------
#Fill Null value with 0 for remaining contract
custdf = custdf.na.fill(value=0,subset=['remaining_contract'])
#Remove the missing null rows for upload and download average
custdf = custdf.na.drop(subset=['download_avg','upload_avg'])
total_missing = custdf.select([count(when(col(c).isNull(),c)).alias(c) for c in custdf.columns])
display(total_missing)
# COMMAND ----------
# Describe continous column data
custdf[con_cols].describe().show()
# COMMAND ----------
# MAGIC %md
# MAGIC
# MAGIC Theres a minus value for subscription age which is unusual, since its a value that describe how long a customer has used the service.
# MAGIC There's a very huge gap in bills between customer, while the average is only 19, the highest one is 406, same with download_avg.
# COMMAND ----------
#Check the distribution using databricks Visualization
display(custdf[con_cols])
# COMMAND ----------
# Check for outliers in subscription age
custdf.orderBy(asc("subscription_age")).limit(5).show()
# Remove the minus value from subscription age
custdf = custdf[custdf["subscription_age"] >= 0]
# COMMAND ----------
# Check for outliers billing average
custdf.orderBy(desc("bill_avg")).limit(20).show()
# COMMAND ----------
# Check for outliers download average
custdf.orderBy(desc("download_avg")).limit(20).show()
# COMMAND ----------
# There are customer churn which has values 0 for all columns
# Probably Data of past churned customer
# Drop this data as it doesnt reflect our current data
import pyspark.sql.functions as F
condition = (custdf.is_tv_subscriber == 0) & (custdf.is_movie_package_subscriber == 0) & (custdf.subscription_age == 0) & (custdf.bill_avg == 0) & (custdf.remaining_contract == 0) & (custdf.service_failure_count == 0) & (custdf.download_avg == 0.0) & (custdf.upload_avg == 0.0) & (custdf.download_over_limit == 0)
custdf = custdf.filter(~condition)
# COMMAND ----------
#Correlation Analysis
from pyspark.ml.stat import Correlation
from pyspark.ml.feature import VectorAssembler
# convert to vector column first
vector_col = "corr_features"
assembler = VectorAssembler(inputCols=custdf.columns, outputCol=vector_col)
df_vector = assembler.transform(custdf).select(vector_col)
# get correlation matrix
matrix = Correlation.corr(df_vector, vector_col)
cor_np = matrix.collect()[0][matrix.columns[0]].toArray()
cor_np
# COMMAND ----------
# MAGIC %md
# MAGIC
# MAGIC Remaining time seems to has the most negative correlation towards customer churn, It seems customers are not willing to stop the service if they have to pay the penalty.
# COMMAND ----------
# Split Data
# Split the data into 70% training and 30% testing
traindf, testdf = custdf.randomSplit(weights=[0.8,0.2], seed=200)
# COMMAND ----------
from pyspark.ml.linalg import Vectors
from pyspark.ml.feature import StringIndexer, Bucketizer, Binarizer
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.feature import StandardScaler
#Bucketizer for Subscription Age
agesplits = [0,2,4,6,8,float("inf")]
SubsAgeBucketizer = Bucketizer(splits=agesplits, inputCol="subscription_age", outputCol="SubsAgeBucket")
# Train and Fit
trainingData = SubsAgeBucketizer.transform(traindf)
#Create Feature
feature = VectorAssembler(inputCols=['is_tv_subscriber','is_movie_package_subscriber','SubsAgeBucket','bill_avg','remaining_contract','service_failure_count','download_avg','upload_avg','download_over_limit'], outputCol = 'features')
trainingData = feature.transform(trainingData)
#Standard Scaler
scaler = StandardScaler(inputCol="features", outputCol="scaledFeatures",
withStd=True, withMean=False)
# Standard Scaler the features
scalerModel = scaler.fit(trainingData)
trainingData = scalerModel.transform(trainingData)
# Set Regression Model
# Logistic Regression
lr = LogisticRegression(maxIter = 10, regParam = 0.01, featuresCol = 'scaledFeatures')
trainingData.show()
# COMMAND ----------
from pyspark.ml import Pipeline
stages = [SubsAgeBucketizer, feature, scaler, lr]
p = Pipeline(stages=stages)
pModel = p.fit(traindf)
# Compare the model with original training data
check = pModel.transform(traindf).select('probability','label','rawPrediction','prediction')
#See the result on test data
pred = pModel.transform(testdf).select('probability','label','rawPrediction','prediction')
# Evaluate
evaluator = BinaryClassificationEvaluator(rawPredictionCol="rawPrediction")
print("Evaluation with test data result:", evaluator.evaluate(pred))
print(" \n\nPrediction for the test data")
pred.show()
# COMMAND ----------
#Test using Decision Tree
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
dt = DecisionTreeClassifier(featuresCol="features")
stages = [SubsAgeBucketizer, feature, dt]
p = Pipeline(stages=stages)
model = p.fit(traindf)
predictions = model.transform(testdf)
predictions.select("prediction", "label", "features").show()
# Select (prediction, true label) and compute test error
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Evaluation with test data result:", accuracy)
print("Test Error = %g " % (1.0 - accuracy))
treeModel = model.stages[2]
print(treeModel)
print('Feature Importance:',treeModel.featureImportances)
# COMMAND ----------
#Test with Random Forest
from pyspark.ml.classification import RandomForestClassifier
# Train a RandomForest model.
rf = RandomForestClassifier(labelCol="label", featuresCol="features", numTrees=9)
stages = [SubsAgeBucketizer, feature, rf]
p = Pipeline(stages=stages)
model = p.fit(traindf)
# Make predictions.
predictions = model.transform(testdf)
#predictions.select("prediction", "label", "features").show(5)
# Select (prediction, true label) and compute test error
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Evaluation with test data result:", accuracy)
print("Test Error = %g" % (1.0 - accuracy))
rfModel = model.stages[2]
print(rfModel) # summary
print('Feature Importance:',rfModel.featureImportances)
# COMMAND ----------
from pyspark.ml.classification import GBTClassifier
# Train a GBT model.
gbt = GBTClassifier(labelCol="label", featuresCol="features", maxIter=10)
stages = [SubsAgeBucketizer, feature, gbt]
p = Pipeline(stages=stages)
model = p.fit(traindf)
# Make predictions.
predictions = model.transform(testdf)
#predictions.select("prediction", "label", "features").show(5)
# Select example rows to display.
predictions.select("prediction", "label", "features").show(5)
# Select (prediction, true label) and compute test error
evaluator = MulticlassClassificationEvaluator(
labelCol="label", predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Evaluation with test data result:", accuracy)
print("Test Error = %g" % (1.0 - accuracy))
gbtModel = model.stages[2]
print(gbtModel)
print('Feature Importance:',gbtModel.featureImportances)