基于分类算法的学习失败预警(上)
文章目录
- 前言
- 1.数据预处理
- 1.1数据探索
- 1.2数值化处理
- 1.3空值填充
- 1.4添加标签
- 1.5特征筛选
- 1.6数据集类别平衡
- 1.7划分数据集
- 1.8训练集类平衡
- 1.9标准化
- 2.模型选择
- 2.1建立模型
- 2.2模型调参
- 2.2.1遍历调参
- 2.2.2网格搜索
- 结语
前言
本次案例通过对现有数据分析,采用如下图所示的学习失败风险预测流程,从整体上分为生成模型和使用模型两个层次:
- 生成模型:将原始数据经过特征工程进行统计加工生成新的样本特征,对所有特征进行验证和清洗,剔除缺失值较多的特征。在分类算法中,不平衡的样本对算法影响较大(上次实验已用实验证明,使用不平衡的训练集进行训练,容易出现过拟合,导致模型实际性能较差),所以在模型训练前先对样本进行平衡,通过子采样的方法使两类标签的样本基本相同,同时将数据集划分成训练集和测试集,用于模型训练和验证,经过多轮训练,验证过后最终确认风险预测模型。
- 使用模型:模型的输入数据也需要经过和训练阶段相同的处理流程,即原始数据经过特征工程和数据清洗之后,将最终选择的特征输入至模型中,经过预测之后的结果作为测试样本的风险值。、
1.数据预处理
1.1数据探索
首先,拿到数据第一件事就是数据探索,旨在理清样本数量、数据类型等信息。这里我们借用panda
库进行操作,将用户相关特征读取进来转化为DataFrame
对象。具体操作如下:
import pandas as pd
df=pd.read_csv("uwide.csv")
df.info()
运行结果为:
通过结果可以看出,该数据集共计2006条数据,共计52列(特征数),部分列含有缺失值,后续可能需要某种算法进行填充。同时52个特征中有29个特征的特征值类型为float、13个特征的特征值类型为int型数据,10个特征的特征值是object(未知),该变量共计占815.1+KB内存存储。
其次,我们也可以通过如下代码,统计数据的分布情况。
df.describe()
运行结果:
其中count
字段数据凡是小于2006的都含有缺失值,只是程度不同,后续需要将缺失程度较大的特征剔除。
1.2数值化处理
通过整体观察阶段,我们发现有10个特征的特征值是object(未知),因此需要将object类型字段处理为数字类型变量(这里根据后续特征需要,只对SEX字段进行了数值化处理,其他的object类型字段均被剔除比如USERID等)
代码如下:
factor=pd.factorize(df['SEX'])
df.SEX=factor[0]
definitions=factor[1]
print(df.SEX.head())
print(definitions)
运行结果:
通过结果发现,0指代了‘女’,1指代了‘男’,达到了数字化的效果。
1.3空值填充
接着,我们需要对空值进行处理,首先,查看哪些列具有空值:
null_columns=df.columns[df.isnull().any()]
df[null_columns].isnull().sum()
运行结果:
通过结果发现,不同列出现了不同程度的缺失,共计2006条数据,有的缺失值都达到了1778个,对于缺失值严重的,我们后续要对该特征进行去除。
对于列含有缺失值的,我们需要进行统计并填充,这里选用了较简单的方式,直接填充0,填充完成后,我们再次统计含有空值的列,具体代码如下:
df=df.fillna(0)
df.columns[df.isnull().any()]
通过结果可以看到,目前和我们预计的效果一致,无缺失值了
1.4添加标签
因为该问题是一个二分类问题,但数据集中并没有相应的标签,因此我们需要添加一列表示标签(label)。
这里标签的值可以通过TOTALSCORE字段的值是否大于60进行计算判断。因为本实验的目的是预测出学业失败的,所以我们将失败的表示为1(即低于60分的),然后我们统计特征数。具体代码如下:
import numpy as np
df["SState"]=np.where(df["TOTALSCORE"]>60,0,1)
cols=df.columns.tolist()
cols,len(cols)
运行结果:
通过结果发现,此时特征数由52->53,多出的特征为新加的label。
1.5特征筛选
我们将数据质量较差的字段移除,同时去掉一些无意义的变量(主观判断),因为这类字段无助于分类算法的改进,故将此类字段全部进行过滤,过程如下:
df=df[[
# 'USERID',
'BROWSER_COUNT',
'COURSE_COUNT',
#'COURSE_LAST_ACCESS',
'COURSE_SUM_VIEW',
'COURSE_AVG_SCORE',
'EXAM_AH_SCORE',
'EXAM_WRITEN_SCORE',
'EXAM_MIDDLE_SCORE',
'EXAM_LAB',
'EXAM_PROGRESS',
'EXAM_GROUP_SCORE',
'EXAM_FACE_SCORE',
'EXAM_ONLINE_SCORE',
#'NODEBB_LAST_POST',
'NODEBB_CHANNEL_COUNT',
'NODEBB_TOPIC_COUNT',
'COURSE_SUM_VIDEO_LEN',
'SEX',
# 'MAJORID',
# 'STATUS',
# 'GRADE',
# 'CLASSID',
'EXAM_HOMEWORK',
'EXAM_LABSCORE',
'EXAM_OTHERSCORE',
'NODEBB_PARTICIPATIONRATE',
'COURSE_WORKTIME',
'COURSE_WORKACCURACYRATE',
'COURSE_WORKCOMPLETERATE',
'NODEBB_POSTSCOUNT',
'NODEBB_NORMALBBSPOSTSCOUONT',
'NODEBB_REALBBSARCHIVECOUNT',
'NORMALBBSARCHIVECOUNT',
'COURSE_WORKCOUNT',
# 'STUNO',
# 'ID',
# 'STUID',
# 'COURSEOPENID',
# 'HOMEWORKSCORE',
# 'WRITTENASSIGNMENTSCORE',
# 'MIDDLEASSIGNMENTSCORE',
# 'LABSCORE',
# 'OTHERSCORE',
# 'TOTALSCORE',
# 'STUDYTERM',
# 'COURSEID',
# 'EXAMNUM',
# 'PROCESS',
# 'GROUPSTUDYSCORE',
# 'FACESTUDYSCORE',
# 'ONLINESTUDYSCORE',
'SState']]
len(df.columns.tolist()),df.columns.tolist()
运行结果为:
通过结果可以看出,列数由53->29
这里我们也可以查看前几行数据
通过结果可以看到,每行数据确实变成了28(特征)+1(标签)=29
1.6数据集类别平衡
我们可以通过如下代码,观察不同类别的数量。
df.SState.value_counts()
通过结果发现,类别存在不平衡现象,因此我们可以先做一个初步的平衡,直接下采样多的类别,具体代码如下:
df_major=df[df['SState']==0]
df_minor=df[df['SState']==1]
df_major_downsample=df_major.sample(8*len(df_minor))
df_downsample=pd.concat([df_major_downsample,df_minor])
df_downsample.SState.value_counts()
运行结果:
当然此处你也可以使用resample()函数
。这里之所以平衡数据集,为了确保训练 / 测试集的类别分布合理,避免评估偏差,此处将数据集类别比例调整到8:1
.
1.7划分数据集
因为本次数据只有一个文件即整个数据集,因此需要对数据集进行划分,此处选择8:2的比例进行划分,你也可以选择其他的比例,具体代码如下:
from sklearn.model_selection import train_test_split
x=df_downsample.iloc[:,:-1].values
y=df_downsample.iloc[:,-1].values
x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.2,random_state=1)
x_train.shape,x_test.shape
运行结果:
通过结果可以发现,训练集样本数量为1476,测试集样本数量为369。
1.8训练集类平衡
此时我们再进行统计训练集的类别比例,发现类不平衡现象。(注:因为之前代码取的是DataFrame的value值,此时x_train,x_test,y_train,y_test
都是numpy.ndarray
类型数据,所以没有直接用value_counts()
函数)
为了避免模型对类别的偏好,因此进行类平衡。这里我们采用SMOTE算法生成新的样本
SMOTE(Synthetic Minority Over-sampling Technique)是一种用于解决数据不平衡问题的过采样算法,主要通过生成少数类的合成样本来平衡数据集。
from imblearn.over_sampling import SMOTE
smote=SMOTE(random_state=1)
x_smote,y_smote=smote.fit_resample(x_train,y_train)
np.sum(y_smote==1),np.sum(y_smote==0)
运行结果:
用生成的样本覆盖之前的样本。
x_train=x_smote
y_train=y_smote
1.9标准化
由于特征中值差别较大,较分散,会影响模型的训练效果,因此采用变换将特征值映射到某一取值范围中,并记录变换方法方便进行还原。变换方式如下:
代码如下:
from sklearn.preprocessing import StandardScaler
scale=StandardScaler()
x_train=scale.fit_transform(x_train)
x_test=scale.transform(x_test)
备注:这里需要对测试集进行同样的处理,而不是使用fit_transform()
方法,因为该方法会重新计算均值和方差,使测试集使用的均值和方差不是训练集的,导致变换方式不一致,后续会出现泛化性能较低的结果。
2.模型选择
根据上次案例的经验——机器学习实战案例——保险产品推荐(下),集成学习的效果比单个决策树效果更好,所以解决此类问题推荐使用随机森林或adaboosting。这里以随机森林为例。
2.1建立模型
因为随机森林可以直接通过RandomForestClassifier()
函数直接调用,通过fit()
函数实现训练。训练完成后通过一个指标进行衡量模型的好坏,因为我们希望能够将找到所有具有学习失败风险的学生,并及时给出预警。所以此处选择查全率(召回率)。具体代码如下:
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import recall_score
clf=RandomForestClassifier(random_state=1)
clf.fit(x_train,y_train)
y_pred=clf.predict(x_test)
print(recall_score(y_test,y_pred))
运行结果:
此处我们发现模型性能并不是很优异,因此我们进行调参,进一步找到更好模型。
2.2模型调参
2.2.1遍历调参
这里我们按照上一篇博客的方式进行调参:机器学习实战案例——保险产品推荐(下)
具体代码如下:
params={
'criterion':['gini','entropy'],
'n_estimators':[50,100,150,200],
'max_depth':[5,10,15,20],
'min_samples_split':range(2,10,2),
'max_features':[5,10,20]
}
results=pd.DataFrame(columns=['criterion','n_estimators','max_depth','min_samples_split','max_features','recall'])
for criterion in params['criterion']:
for n_estimators in params['n_estimators']:
for max_depth in params['max_depth']:
for min_samples_split in params['min_samples_split']:
for max_features in params['max_features']:
clf=RandomForestClassifier(criterion=criterion,n_estimators=n_estimators,max_depth=max_depth,min_samples_split=min_samples_split,max_features=max_features,random_state=1)
clf.fit(x_train,y_train)
y_pred=clf.predict(x_test)
recall=recall_score(y_test,y_pred)
row=pd.DataFrame({'criterion':criterion,'n_estimators':n_estimators,'max_depth':max_depth,'min_samples_split':min_samples_split,'max_features':max_features,'recall':recall},index=[0])
results=pd.concat([results,row],ignore_index=True)
# results.sort_values(by='recall',ascending=False)
results
运行结果:
这个运行时间可能有点长,这个警告可以不用管暂时。通过结果发现最优参数为前两个,即entropy,50,20,2,5,0.800000
和entropy,50,20,4,5,0.800000
2.2.2网格搜索
这里我们介绍另一种方式进行选择参数——网格搜索。
网格搜索(Grid Search)是机器学习中用于超参数调优的经典方法,通过系统性地遍历指定参数的所有可能组合,找到最优参数配置。
代码如下:
from sklearn.metrics import make_scorer
from sklearn.model_selection import GridSearchCV
classifier=RandomForestClassifier(oob_score=True,random_state=1)
refit_score=make_scorer(recall_score)
grid_search=GridSearchCV(classifier,param_grid=params,scoring=refit_score,n_jobs=-1)
grid_search.fit(x_train,y_train)
print(grid_search.best_params_)
print(grid_search.best_score_)
这里加了n_jobs=-1
不然默认使用单线程、会运行很长时间,譬如
此处结果为:{'criterion': 'entropy', 'max_depth': 20, 'max_features': 5, 'min_samples_split': 4, 'n_estimators': 200}
召回率更是达到了0.98,貌似比之前的结果好了不止一星半点儿。真是如此吗?显然不是,这里都没有用到测试集,所以这里的评分基于训练集的,因此我们查看它真实的召回率(即在测试集上的)代码如下:
y_pred=grid_search.predict(x_test)
print(recall_score(y_test,y_pred))
这时我们再查看之前我们逐个遍历的结果,结果保持一致(因为random_state=1
设置的一致)
结语
这里我们抛出一个疑问,到底选择训练集上效果好的模型,还是测试集上效果好的模型呢?如何判断它是否过拟合了呢?(注:其实我怕回答不恰当,容我去问问,哈哈)
未完待续······