本案例为《机器学习实践》课程第四章分类模型配套案例。数据来自手机APP"Kalboard 360"的学习管理系统(LMS)。Kalboard 360旨在利用尖端技术来提升学校K-12教育的教育水平。数据集由480个学生记录和16个特征组成。这些特征分为三大类:
(1)性别和国籍等人口统计特征。
(2)学历背景特征,如教育阶段,年级和隶属教室。
(3)行为特征,如上课举手,访问资源,家长回答问卷调查,学校满意度等。
该数据集的收集来自两个学期:第一学期收集了245个学生记录,第二学期收集了235个学生记录。最后学生依据其总成绩被分为三类: 低:0-69、中:70-89、高:90-100。我们的任务是根据收集的数据预测学生的成绩等级。
数据字段及说明:
特征 | 说明 |
---|---|
gender | 学生性别( 'Male' or 'Female’) |
NationalITy | 学生国籍 |
PlaceofBirth | 学生的出生地 |
StageID | 受教育水平(‘lowerlevel’,’MiddleSchool’,’HighSchool’) |
GradeID | 年级(‘G-01’, ‘G-02’, ‘G-03’, ‘G-04’, ‘G-05’, ‘G-06’, ‘G-07’, ‘G-08’, ‘G-09’, ‘G-10’, ‘G-11’, ‘G-12 ‘) |
SectionID | 隶属的教室(’A’,’B’,’C’) |
Topic | 课程名 |
Semester | 学校的学期(’ First’,’ Second’) |
Relation | 监护学生的家长(’mom’,’father’) |
raisedhands | 学生在教室中举手次数(0-100) |
VisITedResources | 学生访问在线课程次数(0-100) |
AnnouncementsView | 学生检查新公告的次数(0-100) |
Discussion | 学生参加讨论组的次数(0-100) |
ParentAnsweringSurvey | 家长是否回答了学校提供的调查问卷(’Yes’,’No’) |
ParentschoolSatisfaction | 家长对学校的满意度(’Yes’,’No’) |
StudentAbsenceDays | 每个学生的缺勤天数('above-7', 'under-7') |
Class | 根据学生的总成绩分为三个等级(低分:0-69,中等分数:70-89,高分:90-100) |
## 载入必要库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import BernoulliNB
from sklearn.naive_bayes import GaussianNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
# 忽略警告
import warnings
warnings.filterwarnings("ignore")
# 导入数据
edm = pd.read_csv('./input/xAPI-Edu-Data.csv')
edm.head()
首先,我们来查看一下数据集的基本信息:
## 数据集基本信息
edm.info()
可以看到数据中不存在缺失值,但列名中有些字母大小写不统一,为方便我们统一改为首字母大写,其余小写:
# 修改列名
edm.rename(index=str, columns={'gender':'Gender', 'NationalITy':'Nationality',
'raisedhands':'RaisedHands', 'VisITedResources':'VisitedResources'},
inplace=True)
接下来我们通过可视化来进一步挖掘数据中包含的信息,首先来看3个成绩等级的数量分布情况:
## 绘制条形图
plt.figure(figsize=(8, 6))
counts = sns.countplot(x='Class', data=edm, palette='coolwarm')
counts.set(xlabel='Class', ylabel='Count', title='Occurences per class')
plt.show()
可以看到虽然成绩中等的学生要比其余两个成绩等级的学生多一些,但数据集不存在类别分布极端不平衡的情况。
继续查看学生的国籍分布情况:
## 绘制条形图
plt.figure(figsize=(8, 6))
nat = sns.countplot(x='Nationality', data=edm, palette='coolwarm')
nat.set(xlabel='Nationality', ylabel='Count', title='Nationality Representation')
plt.setp(nat.get_xticklabels(), rotation=60)
plt.show()
数据集中的学生分别来自14个国家,大多数学生来自科威特或约旦。
下面再来看看两个不同学期间,学生成绩等级的数量分布差异:
## 绘制条形图
plt.figure(figsize=(8, 6))
sem = sns.countplot(x='Class', hue='Semester', order=['L', 'M', 'H'], data=edm, palette='coolwarm')
sem.set(xlabel='Class', ylabel='Count', title='Semester comparison')
plt.show()
学生在第二学期('S')的表现比第一学期('F')好一些。 在第二学期,成绩中等的学生人数保持不变,但是成绩差的学生人数较少,而成绩好的学生人数较多。
接着来看看不同性别之间,学生成绩等级的数量分布差异:
## 绘制条形图
plt.figure(figsize=(8, 6))
plot = sns.countplot(x='Class', hue='Gender', data=edm, order=['L', 'M', 'H'], palette='coolwarm')
plot.set(xlabel='Class', ylabel='Count', title='Gender comparison')
plt.show()
可以看到学生中男生较多,并且较女生而言,低分成绩的人较多,高分成绩的人较少。
再来看看访问在线教学资源次数的多少会不会影响学生的成绩等级:
## 绘制条形图
plt.figure(figsize=(8, 6))
plot = sns.swarmplot(x='Class', y='VisitedResources', hue='Gender', order=['L', 'M', 'H'],
data=edm, palette='coolwarm')
plot.set(xlabel='Class', ylabel='Count', title='Gender comparison on visited resources')
plt.show()
上图显示获得低分(L)的学生比获得中等分数(M)或高分(H)的学生访问的资源少的多。此外,获得高分(H)的女性几乎都访问了很多在线资源。
从数据集的基本信息中可以看到,有些特征的类型是字符型,需要在建模前做一些预处理。
首先对字符型特征进行数值编码:
## 选出需要进行数值编码的特征
str_columns = edm.dtypes[edm.dtypes == 'object'].index
## 数值编码
for col in str_columns:
edm[col] = LabelEncoder().fit_transform(edm[col])
为方便后续建立模型,需要对除去目标特征之外的无序分类特征进行独热编码:
## 独热编码
edm_new = pd.get_dummies(edm, columns=str_columns.drop(['Class', 'StageID']))
## 查看独热编码后的维度
edm_new.shape
最后,将目标与数据分离,准备建立模型:
## 分离目标
X_new = edm_new.drop(['Class'], axis=1)
y_new = edm_new['Class']
首先将数据集划分为训练集和测试集,比例为4:3:
X_train,X_test, y_train, y_test = train_test_split(X_new, y_new,
test_size=.25,
random_state=10,
stratify=y_new)
我们首先建立K-近邻、逻辑回归和支持向量机模型,并输出测试集的分类正确率和分类报告:
## 建模并评估
keys = []
scores = []
models = {'K-近邻': KNeighborsClassifier(),
'逻辑回归': LogisticRegression(),
'线性支持向量机': LinearSVC(),
'支持向量机': SVC()}
for k,v in models.items():
mod = v
mod.fit(X_train, y_train)
pred = mod.predict(X_test)
print(str(k) + '建模效果:' + '\n')
print(classification_report(y_test, pred, target_names=['H', 'L', 'M']))
acc = accuracy_score(y_test, pred)
print('分类正确率:'+ str(acc))
print('\n' + '\n')
keys.append(k)
scores.append(acc)
table = pd.DataFrame({'model':keys, 'accuracy score':scores})
table
从上表看到,K-近邻模型的预测效果较另外三种模型要好。
接下来我们建立决策树和三种朴素贝叶斯模型:
## 建模并评估
keys = []
scores = []
models = {'决策树': DecisionTreeClassifier(),
'高斯贝叶斯': GaussianNB(),
'伯努利贝叶斯': BernoulliNB(),
'多项式贝叶斯': MultinomialNB()}
for k,v in models.items():
mod = v
mod.fit(X_train, y_train)
pred = mod.predict(X_test)
print(str(k) + '建模效果:' + '\n')
print(classification_report(y_test, pred, target_names=['H', 'L', 'M']))
acc = accuracy_score(y_test, pred)
print('分类正确率:'+ str(acc))
print('\n' + '\n')
keys.append(k)
scores.append(acc)
table = pd.DataFrame({'model':keys, 'accuracy score':scores})
table
上表可以看到决策树、伯努利朴素贝叶斯模型效果较好。
# 创建一个包含不同C取值的列表
C_grid = [0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8]
# 创建一个包含不同penalty取值的列表
penalty_grid = ["l2", "l1"]
# 创建一个包含不同class_weight取值的列表
class_weight_grid = ['balanced', None]
# 组合成元组列表
parameters=[(C_, penalty_, class_weight_) for C_ in C_grid for penalty_ in penalty_grid for class_weight_ in class_weight_grid]
parameters
## 参数调优
result_accuracy={}
for parameter in parameters:
result_accuracy[parameter] = LogisticRegression(random_state=0,
penalty=parameter[1],
C=parameter[0],
class_weight=parameter[2]).fit(X_train, y_train).score(X_test, y_test)
df = pd.DataFrame(list(result_accuracy.items()),
columns=['parameter_list', 'accuracy']).sort_values(by='accuracy', ascending=False)[:5]
df.reset_index(drop=True)
当采用L1正则化项,正则化强度为1,且平衡样本分类权重时,模型效果提升最为明显。
接着我们对KNeighborsClassifier
类中的参数n_neighbors
和weights
进行调整:
# 创建一个包含不同n_neighbors取值的列表
k_grid = [1, 2, 3, 4, 5, 6, 7, 8]
# 创建一个包含不同weights取值的列表
weights_grid = ["uniform", "distance"]
# 组合成元组列表
parameters = [(k_, weights_) for k_ in k_grid for weights_ in weights_grid]
parameters
## 参数调优
result_accuracy={}
for parameter in parameters:
result_accuracy[parameter] = KNeighborsClassifier(n_neighbors=parameter[0],
weights=parameter[1]).fit(X_train, y_train).score(X_test, y_test)
df = pd.DataFrame(list(result_accuracy.items()),
columns=['parameter_list', 'accuracy']).sort_values(by='accuracy', ascending=False)[:5]
df.reset_index(drop=True)
可以看到当近邻样本个数为5,K-近邻与加权K-近邻模型效果一致,且与之前持平。
最后我们对DecisionTreeClassifier
类中的参数criterion
、max_depth
和class_weight
进行调整:
# 创建一个包含不同criterion取值的列表
criterion_grid = ['gini', 'entropy']
# 创建一个包含不同max_depth取值的列表
depth_grid = [1, 2, 3, 4, 5, 6, 7, 8, None]
# 创建一个包含不同class_weight取值的列表
class_weight_grid = ['balanced', None]
# 组合成元组列表
parameters = [(criterion_, depth_, weights_) for criterion_ in criterion_grid for depth_ in depth_grid for weights_ in class_weight_grid]
parameters
## 参数调优
result_accuracy={}
for parameter in parameters:
result_accuracy[parameter] = DecisionTreeClassifier(criterion=parameter[0],
max_depth=parameter[1],
class_weight=parameter[2]).fit(X_train, y_train).score(X_test, y_test)
df = pd.DataFrame(list(result_accuracy.items()),
columns=['parameter_list', 'accuracy']).sort_values(by='accuracy', ascending=False)[:5]
df.reset_index(drop=True)
当采用Gini系数计算不纯度,最大树深度设为6且不平衡样本分类权重时,模型效果提升最明显, 正确率达到了74%。
我们再次利用选出的最佳参数建立模型,并输出特征重要性:
## 训练模型
model_DF = DecisionTreeClassifier(max_depth=5, class_weight='balanced')
model_DF.fit(X_train, y_train)
## 特征重要性
fea_imp = pd.Series(model_DF.feature_importances_, index=X_train.columns).sort_values(ascending=False)[:5]
fea_imp
可以看到学生缺勤的天数StudentAbsenceDays
、访问在线课程次数VisitedResources
、举手次数RaisedHands
、监护人Relation
和检查新公告的次数AnnouncementsView
这5个特征最为重要。至此,我们通过探索性数据分析、数据预处理、模型选择和参数调优,最终建立了决策树模型来预测学生的成绩等级。