在完成了一些数据科学的课程后,感到有必要将这些技能应用到一些项目中。为此,分析并构建了一个机器学习模型,该模型基于来自伊朗一家电信公司的数据集,每行代表一年期间的一位客户。从Kaggle获取了这个数据集。在这篇文章中,将分享在处理这些数据时的经验。将了解到从数据库检索数据、处理不平衡数据、数据可视化以及构建机器学习模型的过程。
在开始分析之前,需要导入一些库。以下是所需的库列表。
# 导入所需的库
import pandas as pd
import sqlite3
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, ConfusionMatrixDisplay
from imblearn.over_sampling import SMOTE
以上代码展示了需要的库,包括数据处理、可视化和机器学习模型构建所需的库。
从Kaggle获取数据后,将这些数据推送到数据库中,以便在进行这个项目时练习一些SQL。可以在GitHub仓库中找到数据库和数据的CSV文件。在这里,使用SQLite数据库以简化操作。在Python中,已经有一个名为sqlite3的库用于读取SQLite数据库。
# 连接数据库并查看表的数量
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
print(cursor.fetchall())
运行上述代码后,将看到输出结果,显示数据库中只有一个表。这很合理,因为只向数据库推送了一个表。现在让看看customer_churn表的结构。为此,需要运行以下代码。
# 查看表的结构
cursor.execute("PRAGMA table_info(customer_churn);")
print(cursor.fetchall())
如果运行此代码,输出将如下所示。上述表的所有列的含义如下所述:
从结果中,可以说:
仅用一行代码就了解到了这么多信息,这难道不令人惊叹吗?现在,让看看表中的一些值。运行以下代码。
# 查看表中的一些值
pd.read_sql("SELECT * FROM customer_churn LIMIT 10", conn)
由于有很多列,无法显示所有列。但将简要描述所有列及其值,以更好地理解数据。请参见下图。
从上图中,已经猜到了,尽管所有列都是数值型的,但存在一些编码的分类列。编码的分类列包括Complaints、Charge Amount、Age Group、Tariff Plan、Status和Churn。Churn列是这里的目标列。
现在,是时候将这个数据集读取为pandas dataframe了。但不取所有列。
在年龄上有两个列。因此,只取Age Group,因为在将数据输入机器学习模型之前,必须对那一列进行编码,这里已经完成了。忽略了call_failure、subscription_length、charge_amount和tariff_plan,因为它们对于确定客户流失不太有用。
# 读取数据并进行预处理
tel_data = pd.read_sql("SELECT * FROM customer_churn", conn)
tel_data = tel_data.drop(['call_failure', 'subscription_length', 'charge_amount', 'tariff_plan'], axis=1)
现在数据已经准备好进行一些严肃的分析了。
现在,让看看数值列的汇总统计。
# 查看数值列的汇总统计
tel_data[["seconds_of_use", "frequency_of_use", "frequency_of_sms", "customer_value"]].describe()
以上代码将给出以下输出:
从上述统计数据中,可以很容易地注意到所有数值列的最小值都是0。它们可能是新客户,或者这些列中的缺失值以0的形式存在。哪一个是真的🤨?让找出答案。
# 检查0值是否表示新客户
assert tel_data[tel_data['seconds_of_use'] == 0].shape[0] == tel_data[tel_data['frequency_of_use'] == 0].shape[0]
如果运行上述代码,没有得到任何输出。这可能发生,因为这两个列——seconds_of_use和frequency_of_use——在数据中具有相同的重要性。现在必须找出frequency_of_sms列在这两个列为0的地方是否也是0?让看看🔍。
# 检查0值的分布
tel_data[tel_data['seconds_of_use'] == 0]['frequency_of_sms'].hist(bins=50, log=True)
plt.show()
tel_data['frequency_of_sms'].hist(bins=50, log=True)
plt.show()
如果运行上述代码,将看到以下两个图表。可以很容易地注意到上述两个图表之间的差异。左侧图表绘制在数据集的秒数使用和使用频率列包含值为零的部分。另一个图表绘制在整个数据集上。第一个图表中SMS的最大值是25,但在另一个图表中,这个最大值超过500!所以现在得出结论,这些列中的零值不是缺失值,它们表示它们是新客户。否则,在左侧图表中看不到任何条形。
现在,可能注意到在绘制时使用了对数刻度的y轴。为什么呢?因为frequency_of_sms列中的值之间存在很大的差异。如果一列中的值差异很大,在绘制这些值时不使用对数刻度,那么从图表中得出一些结论是非常困难的。所以当遇到这样的问题时,必须使用对数刻度。
现在,这个数据集包含8列。它们都重要吗?让通过找到相关性矩阵来看看。
# 查看相关性矩阵
tel_data.corr()
输出结果如下所示的图像。
从相关性矩阵中,可以很容易地注意到frequency_of_use和seconds_of_use之间有很高的正相关性。所以可以把它们中的任何一个放入数据中。
# 删除相关性高的列
tel_data_selected = tel_data.drop('frequency_of_use', axis=1)
处理不平衡数据。
在这里,想要正确预测流失的客户。让看看数据中每个类别有多少行。
# 查看每个类别的行数
tel_data_selected['Churn'].value_counts()
输出结果如下。嗯,只有15%的数据与流失客户相关,84%的数据与非流失客户相关。这是一个很大的差异。必须对少数类别进行过采样。为此,使用SMOTE(合成少数过采样技术),它使用最近邻的特征创建合成数据。这种技术在imblearn Python库中可用。
还有一件事。不想在假数据上测试机器学习模型!所以只在训练数据上使用这种技术。为此,使用scikit-learn中的train_test_split来分割数据。
# 分割数据并进行过采样
X = tel_data_selected.drop('Churn', axis=1)
y = tel_data_selected['Churn']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
smote = SMOTE(random_state=42)
X_train, y_train = smote.fit_resample(X_train, y_train)
现在数据已经平衡了。在训练模型之前还有一件事要做——缩放数值数据。seconds_of_use、frequency_of_sms和customer_value列是数值列。其他列是编码的分类列。所以只需要缩放这3列。为了选择特定的列进行缩放,使用scikit-learn中的ColumnTransformer和StandardScaler进行缩放。
# 缩放数值数据
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), ['seconds_of_use', 'frequency_of_sms', 'customer_value'])
],
remainder='passthrough'
)
现在是训练模型的时候了。在这里,使用LogisticRegression进行分类。
# 训练模型
model = LogisticRegression()
model.fit(preprocessor.fit_transform(X_train), y_train)
在这里没有使模型过于复杂,以便初学者可以轻松理解。为Logistic Regression使用了所有默认的超参数。
现在需要检查这个默认模型是否在预测流失客户方面表现良好。由于只对特定类别感兴趣,所以使用召回率作为评估指标。召回率计算模型正确识别的实际阳性的百分比(真阳性)。在这里,预测流失的客户,这些是正类别。让看看得到了什么。
# 评估模型
y_pred = model.predict(preprocessor.transform(X_test))
print(classification_report(y_test, y_pred))
从上述分类报告中,可以看到模型在预测客户流失方面给出了不错的分数,这很好!
现在,让绘制混淆矩阵。
# 绘制混淆矩阵
ConfusionMatrixDisplay.from_predictions(y_test, y_pred)
plt.show()
从99个流失客户样本中,正确检测到90个样本,9个被误分类。这个结果在多次运行中可能会改变。
如果不进行重采样就传递数据会怎样?让看看。
# 不进行重采样的模型评估
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
现在得到了一个糟糕的召回分数。混淆矩阵呢?让看看。
从混淆矩阵中,可以看到这个模型在检测流失客户方面表现不佳。