本案例为《机器学习实践》课程第四章分类模型配套案例。数据来自手机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)
In [1]:
## 载入必要库
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
In [2]:
# 忽略警告
import warnings
warnings.filterwarnings("ignore")

数据读取

In [3]:
# 导入数据
edm = pd.read_csv('./input/xAPI-Edu-Data.csv')
edm.head()
Out[3]:
gender NationalITy PlaceofBirth StageID GradeID SectionID Topic Semester Relation raisedhands VisITedResources AnnouncementsView Discussion ParentAnsweringSurvey ParentschoolSatisfaction StudentAbsenceDays Class
0 M KW KuwaIT lowerlevel G-04 A IT F Father 15 16 2 20 Yes Good Under-7 M
1 M KW KuwaIT lowerlevel G-04 A IT F Father 20 20 3 25 Yes Good Under-7 M
2 M KW KuwaIT lowerlevel G-04 A IT F Father 10 7 0 30 No Bad Above-7 L
3 M KW KuwaIT lowerlevel G-04 A IT F Father 30 25 5 35 No Bad Above-7 L
4 M KW KuwaIT lowerlevel G-04 A IT F Father 40 50 12 50 No Bad Above-7 M

探索性数据分析

首先,我们来查看一下数据集的基本信息:

In [4]:
## 数据集基本信息
edm.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 480 entries, 0 to 479
Data columns (total 17 columns):
gender                      480 non-null object
NationalITy                 480 non-null object
PlaceofBirth                480 non-null object
StageID                     480 non-null object
GradeID                     480 non-null object
SectionID                   480 non-null object
Topic                       480 non-null object
Semester                    480 non-null object
Relation                    480 non-null object
raisedhands                 480 non-null int64
VisITedResources            480 non-null int64
AnnouncementsView           480 non-null int64
Discussion                  480 non-null int64
ParentAnsweringSurvey       480 non-null object
ParentschoolSatisfaction    480 non-null object
StudentAbsenceDays          480 non-null object
Class                       480 non-null object
dtypes: int64(4), object(13)
memory usage: 63.8+ KB

可以看到数据中不存在缺失值,但列名中有些字母大小写不统一,为方便我们统一改为首字母大写,其余小写:

In [5]:
# 修改列名
edm.rename(index=str, columns={'gender':'Gender', 'NationalITy':'Nationality',
                               'raisedhands':'RaisedHands', 'VisITedResources':'VisitedResources'},
                               inplace=True)

接下来我们通过可视化来进一步挖掘数据中包含的信息,首先来看3个成绩等级的数量分布情况:

In [6]:
## 绘制条形图
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()

可以看到虽然成绩中等的学生要比其余两个成绩等级的学生多一些,但数据集不存在类别分布极端不平衡的情况。

继续查看学生的国籍分布情况:

In [7]:
## 绘制条形图
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个国家,大多数学生来自科威特或约旦。

下面再来看看两个不同学期间,学生成绩等级的数量分布差异:

In [8]:
## 绘制条形图
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')好一些。 在第二学期,成绩中等的学生人数保持不变,但是成绩差的学生人数较少,而成绩好的学生人数较多。

接着来看看不同性别之间,学生成绩等级的数量分布差异:

In [9]:
## 绘制条形图
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()

可以看到学生中男生较多,并且较女生而言,低分成绩的人较多,高分成绩的人较少。

再来看看访问在线教学资源次数的多少会不会影响学生的成绩等级:

In [10]:
## 绘制条形图
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)的女性几乎都访问了很多在线资源。

从数据集的基本信息中可以看到,有些特征的类型是字符型,需要在建模前做一些预处理。

数据预处理

首先对字符型特征进行数值编码:

In [11]:
## 选出需要进行数值编码的特征
str_columns = edm.dtypes[edm.dtypes == 'object'].index

## 数值编码
for col in str_columns:
    edm[col] = LabelEncoder().fit_transform(edm[col])

为方便后续建立模型,需要对除去目标特征之外的无序分类特征进行独热编码:

In [12]:
## 独热编码
edm_new = pd.get_dummies(edm, columns=str_columns.drop(['Class', 'StageID']))
In [13]:
## 查看独热编码后的维度
edm_new.shape
Out[13]:
(480, 71)

最后,将目标与数据分离,准备建立模型:

In [14]:
## 分离目标
X_new = edm_new.drop(['Class'], axis=1)
y_new = edm_new['Class']

建立模型并评估

首先将数据集划分为训练集和测试集,比例为4:3:

In [15]:
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-近邻、逻辑回归和支持向量机模型,并输出测试集的分类正确率和分类报告:

In [16]:
## 建模并评估
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-近邻建模效果:

             precision    recall  f1-score   support

          H       0.74      0.71      0.72        35
          L       0.75      0.75      0.75        32
          M       0.70      0.72      0.71        53

avg / total       0.73      0.72      0.73       120

分类正确率:0.725



逻辑回归建模效果:

             precision    recall  f1-score   support

          H       0.68      0.54      0.60        35
          L       0.78      0.78      0.78        32
          M       0.62      0.70      0.65        53

avg / total       0.68      0.68      0.67       120

分类正确率:0.675



线性支持向量机建模效果:

             precision    recall  f1-score   support

          H       0.00      0.00      0.00        35
          L       0.80      0.75      0.77        32
          M       0.52      0.89      0.66        53

avg / total       0.44      0.59      0.50       120

分类正确率:0.5916666666666667



支持向量机建模效果:

             precision    recall  f1-score   support

          H       0.71      0.34      0.46        35
          L       0.80      0.38      0.51        32
          M       0.52      0.87      0.65        53

avg / total       0.65      0.58      0.56       120

分类正确率:0.5833333333333334



Out[16]:
model accuracy score
0 K-近邻 0.725000
1 逻辑回归 0.675000
2 线性支持向量机 0.591667
3 支持向量机 0.583333

从上表看到,K-近邻模型的预测效果较另外三种模型要好。

接下来我们建立决策树和三种朴素贝叶斯模型:

In [17]:
## 建模并评估
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
决策树建模效果:

             precision    recall  f1-score   support

          H       0.63      0.63      0.63        35
          L       0.82      0.72      0.77        32
          M       0.61      0.66      0.64        53

avg / total       0.67      0.67      0.67       120

分类正确率:0.6666666666666666



高斯贝叶斯建模效果:

             precision    recall  f1-score   support

          H       0.57      0.80      0.67        35
          L       0.57      0.88      0.69        32
          M       0.59      0.25      0.35        53

avg / total       0.58      0.57      0.53       120

分类正确率:0.575



伯努利贝叶斯建模效果:

             precision    recall  f1-score   support

          H       0.64      0.66      0.65        35
          L       0.71      0.75      0.73        32
          M       0.64      0.60      0.62        53

avg / total       0.66      0.66      0.66       120

分类正确率:0.6583333333333333



多项式贝叶斯建模效果:

             precision    recall  f1-score   support

          H       0.59      0.66      0.62        35
          L       0.72      0.66      0.69        32
          M       0.58      0.57      0.57        53

avg / total       0.62      0.62      0.62       120

分类正确率:0.6166666666666667



Out[17]:
model accuracy score
0 决策树 0.666667
1 高斯贝叶斯 0.575000
2 伯努利贝叶斯 0.658333
3 多项式贝叶斯 0.616667

上表可以看到决策树、伯努利朴素贝叶斯模型效果较好。

参数调优

我们对两次建模效果比较好的三种模型:K-近邻、决策树、逻辑回归模型(伯努利朴素贝叶斯模型不作调整)进行调参,看看能否进一步提升模型的效果:

逻辑回归

首先对LogisticRegression类中的参数Cpenaltyclass_weight进行调整:

In [18]:
# 创建一个包含不同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
Out[18]:
[(0.2, 'l2', 'balanced'),
 (0.2, 'l2', None),
 (0.2, 'l1', 'balanced'),
 (0.2, 'l1', None),
 (0.4, 'l2', 'balanced'),
 (0.4, 'l2', None),
 (0.4, 'l1', 'balanced'),
 (0.4, 'l1', None),
 (0.6, 'l2', 'balanced'),
 (0.6, 'l2', None),
 (0.6, 'l1', 'balanced'),
 (0.6, 'l1', None),
 (0.8, 'l2', 'balanced'),
 (0.8, 'l2', None),
 (0.8, 'l1', 'balanced'),
 (0.8, 'l1', None),
 (1, 'l2', 'balanced'),
 (1, 'l2', None),
 (1, 'l1', 'balanced'),
 (1, 'l1', None),
 (1.2, 'l2', 'balanced'),
 (1.2, 'l2', None),
 (1.2, 'l1', 'balanced'),
 (1.2, 'l1', None),
 (1.4, 'l2', 'balanced'),
 (1.4, 'l2', None),
 (1.4, 'l1', 'balanced'),
 (1.4, 'l1', None),
 (1.6, 'l2', 'balanced'),
 (1.6, 'l2', None),
 (1.6, 'l1', 'balanced'),
 (1.6, 'l1', None),
 (1.8, 'l2', 'balanced'),
 (1.8, 'l2', None),
 (1.8, 'l1', 'balanced'),
 (1.8, 'l1', None)]
In [20]:
## 参数调优
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)
Out[20]:
parameter_list accuracy
0 (1, l1, balanced) 0.708333
1 (1.2, l1, balanced) 0.708333
2 (1.4, l1, balanced) 0.708333
3 (1.8, l1, balanced) 0.700000
4 (1, l1, None) 0.700000

当采用L1正则化项,正则化强度为1,且平衡样本分类权重时,模型效果提升最为明显。

K-近邻

接着我们对KNeighborsClassifier类中的参数n_neighborsweights进行调整:

In [21]:
# 创建一个包含不同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
Out[21]:
[(1, 'uniform'),
 (1, 'distance'),
 (2, 'uniform'),
 (2, 'distance'),
 (3, 'uniform'),
 (3, 'distance'),
 (4, 'uniform'),
 (4, 'distance'),
 (5, 'uniform'),
 (5, 'distance'),
 (6, 'uniform'),
 (6, 'distance'),
 (7, 'uniform'),
 (7, 'distance'),
 (8, 'uniform'),
 (8, 'distance')]
In [22]:
## 参数调优
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)
Out[22]:
parameter_list accuracy
0 (5, uniform) 0.725000
1 (5, distance) 0.725000
2 (6, uniform) 0.691667
3 (6, distance) 0.691667
4 (7, uniform) 0.683333

可以看到当近邻样本个数为5,K-近邻与加权K-近邻模型效果一致,且与之前持平。

决策树

最后我们对DecisionTreeClassifier类中的参数criterionmax_depthclass_weight进行调整:

In [23]:
# 创建一个包含不同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
Out[23]:
[('gini', 1, 'balanced'),
 ('gini', 1, None),
 ('gini', 2, 'balanced'),
 ('gini', 2, None),
 ('gini', 3, 'balanced'),
 ('gini', 3, None),
 ('gini', 4, 'balanced'),
 ('gini', 4, None),
 ('gini', 5, 'balanced'),
 ('gini', 5, None),
 ('gini', 6, 'balanced'),
 ('gini', 6, None),
 ('gini', 7, 'balanced'),
 ('gini', 7, None),
 ('gini', 8, 'balanced'),
 ('gini', 8, None),
 ('gini', None, 'balanced'),
 ('gini', None, None),
 ('entropy', 1, 'balanced'),
 ('entropy', 1, None),
 ('entropy', 2, 'balanced'),
 ('entropy', 2, None),
 ('entropy', 3, 'balanced'),
 ('entropy', 3, None),
 ('entropy', 4, 'balanced'),
 ('entropy', 4, None),
 ('entropy', 5, 'balanced'),
 ('entropy', 5, None),
 ('entropy', 6, 'balanced'),
 ('entropy', 6, None),
 ('entropy', 7, 'balanced'),
 ('entropy', 7, None),
 ('entropy', 8, 'balanced'),
 ('entropy', 8, None),
 ('entropy', None, 'balanced'),
 ('entropy', None, None)]
In [25]:
## 参数调优
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)
Out[25]:
parameter_list accuracy
0 (gini, 6, None) 0.741667
1 (gini, 4, balanced) 0.733333
2 (gini, 5, None) 0.733333
3 (gini, 5, balanced) 0.733333
4 (gini, 4, None) 0.733333

当采用Gini系数计算不纯度,最大树深度设为6且不平衡样本分类权重时,模型效果提升最明显, 正确率达到了74%。

决策树模型特征重要性排序

我们再次利用选出的最佳参数建立模型,并输出特征重要性:

In [26]:
## 训练模型
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
Out[26]:
StudentAbsenceDays_0    0.364314
VisitedResources        0.205334
RaisedHands             0.103485
Relation_1              0.083464
AnnouncementsView       0.066993
dtype: float64

可以看到学生缺勤的天数StudentAbsenceDays、访问在线课程次数VisitedResources、举手次数RaisedHands、监护人Relation和检查新公告的次数AnnouncementsView这5个特征最为重要。至此,我们通过探索性数据分析、数据预处理、模型选择和参数调优,最终建立了决策树模型来预测学生的成绩等级。