该文章为连载的第三篇:学生成绩分析
某家在线教育机构拥有自己开发的教育产品VLE,该教育机构提供了他们四个学期里,开展的七门课的数据,接下来我会根据这些数据,为该教育机构做一系列的数据分析,包括用户的RFM模型、用户分群特征、用户成绩分析等等。
该教育机构部分数据库结构如下
如下这三篇文章为:用户成绩分析
做用户成绩分析,主要是把学生特征和学生使用产品习惯,与成绩结果做关联,所以会用到如下几个表:
注意assessment意为作业,但涵盖Exam的考试信息,因为在国外,作业和考试都有一个权重分值,都会占最终成绩的一部分,并不是考试占最终成绩的100%。
用户数据:学生编号,性别,学生居住区,最高学历,多重剥夺指数范围(衡量学生所在区的城市贫富等情况),年龄段,是否残疾,学生尝试该课程次数,当前课程的学分,最终成绩
行为数据:学习时长,点击次数
课程数据:课程编号,开课时间,使用产品的ip编码,作业/考试编号、日期等
import pandas as pd
import numpy as np
import datetime
import time
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus']=False
import matplotlib.pyplot as plt
import seaborn as sns
import sklearn
from sklearn.preprocessing import LabelEncoder
np.set_printoptions(suppress=True)
pd.set_option('display.float_format', lambda x: '%.4f' % x)
用df.isna().sum()的方法,查看三个表的空缺值情况
学生信息表里:
只有多重剥夺指数范围有1111条空值(总数据量32593,空缺占比3.4%)
学生成绩表里:
只有分数有173条空缺值,总数据量173912,空缺占比极低,会用该学期该门课的平均值填补。
考试或作业表:
只有考试日期有11条空缺值,是因为部分考试/作业无日期限制。
先看一下学生信息表大概长什么样子
把object类型的重要字段标签化
#性别
stu["gender"]=stu["gender"].map({"M":1,"F":2})
#最高教育程度
highest_education_dict={"No Formal quals":0,"Lower Than A Level":1,"A Level or Equivalent":2,"HE Qualification":3,"Post Graduate Qualification":4}
stu["highest_edu_label"]=stu['highest_education'].map(highest_education_dict)
#多重剥夺指数范围
#先去中间值,把范围转成数值
imd_band_dict={'0-10%':5,
'10-20':15,
'20-30%':25,
'30-40%':35,
'40-50%':45,
'50-60%':55,
'60-70%':65,
'70-80%':75,
'80-90%':85,
'90-100%':95}
stu["imd_band_label"]=stu['imd_band'].map(imd_band_dict)
#平均数填补空值
stu["imd_band_label"].fillna(stu["imd_band_label"].mean(),inplace=True)
#年龄:
age_encoder = LabelEncoder()
age_encoder.fit(stu['age_band'].value_counts().sort_index().index.tolist())
stu["age_band_label"]=age_encoder.transform(stu['age_band'])
#是否残疾
stu["disability"]=stu["disability"].map({"Y":1,"N":0})
#最终成绩
stu["final_result_label"]=stu["final_result"].map({'Withdrawn':0, 'Fail':1,'Pass':2, 'Distinction':3})
筛选所需要的数据:
stu=stu[["code_module","code_presentation","id_student","gender","num_of_prev_attempts","studied_credits","disability","highest_edu_label","imd_band_label","age_band_label","final_result_label"]]
合并考试作业表个学生成绩表
stu_ass=pd.merge(stu_ass,ass,on="id_assessment")
plt.figure(figsize=(8, 6))
#sns.set_style('white')
sns.boxplot(x="code_presentation", y="score", hue="code_module", data=stu_ass.sort_values("code_presentation"))
plt.legend(loc=0,bbox_to_anchor=(1, 0.8),title="课程",frameon=False)#图示靠右显示
plt.title("各学期各课程考试/作业分数分布")
plt.xlabel("学期")
plt.tight_layout()
plt.show
每学期每门课程均有分数低的异常值,并且较多
BBB和GGG课程的分数,在三个学期中都是大多数同学集中高分位置
CCC和DDD课程的分数集中在较低分数
看一下整体分数分布
sns.distplot(stu_ass["score"],kde=False)
plt.title("整体学生作业/考试分数分布")
plt.show()
看整体的话异常值并不多,大多数分数都分布在60-100之间。0分、40分、60分、80分、100分都有相对于附近的分数值分布突然变集中。
stu_ass["score"].value_counts().sort_index().head(30)
但确实低分每个分数都有并且很集中
用盖帽法处理异常,考虑每学期每门课的难度或许不同,分数分布情况也会不同。这里每学期每门课的的低分异常值定义为:中位数-2倍标准差,
只有
stu_ass["score_chuli"]=stu_ass["score"]
#定义一个方法
def yichang(code_pre,code_module):
score_list=stu_ass[(stu_ass["code_presentation"]==code_pre)&(stu_ass["code_module"]==code_module)]["score"]
s_mean = score_list.mean()
s_min = score_list.mean() - 2*score_list.std()
stu_ass.loc[(stu_ass["code_presentation"]==code_pre)&(stu_ass["code_module"]==code_module)&(stu_ass["score"] < s_min), 'score_chuli'] = s_min
stu_ass.loc[(stu_ass["code_presentation"]==code_pre)&(stu_ass["code_module"]==code_module)&(stu_ass["score"].isna()), 'score_chuli'] = s_mean
#每学期每门课
for m in stu_ass["code_presentation"].unique().tolist():
for n in stu_ass["code_module"].unique().tolist():
yichang(m,n)
修改后的各学期各门课程的分数分布:
加上交付时间与应交时间的间隔、和是否准时提交:
stu_ass["day_gap"]=stu_ass["date_submitted"]-stu_ass["date"]
stu_ass["submit_on_time"]=stu_ass["day_gap"].apply(lambda x:1 if x<=0 else 0)
每个用户在每学期每门课使用产品用了多少天,总的点击次数是多少
vle_df=vle.groupby(["code_presentation","code_module","id_student"]).agg({"date":pd.Series.nunique,"sum_click":"sum"}).reset_index().rename(columns={"date":"cnt_study_days"})
vle_df["avg_click"]=vle_df["sum_click"]/vle_df["cnt_study_days"]
stu_vle=pd.merge(stu,vle_df,on=["code_module","code_presentation","id_student"],how="left")
stu_vle.isna().sum()
聚合后发现有些用户并没有使用产品,导致该用户的cnt_study_days、sum_click和avg_click为nan,用fillna(0)填补
stu_ass_vle=pd.merge(stu_ass,stu_vle,on=["id_student","code_module","code_presentation"],how="left")
得到一个173912行, 24列的数据集,每一行代表一个学生在一个学期里的一门课的作业/考试情况,以及对应的该学生的特征和使用产品情况。
这里想分析学生的平时分数(作业/考试分数)和最终总分与其他特征会不会有什么相关性。这里用热力图表示。
#筛选出带标签的变量,建模需要使用
train_df=stu_ass_vle[["weight","day_gap","gender","num_of_prev_attempts","studied_credits","cnt_study_days","avg_click","highest_edu_label","imd_band_label","age_band_label","score_chuli","final_result_label"]]
corr_data=train_df.drop("id_student",axis=1).corr().round(4)
plt.figure(figsize=(12, 12))
# np.zeros_like() 返回一个零数组,其形状和类型与给定的数组相同;
mask = np.zeros_like(corr_data)
#np.triu_indices_from(mask) 返回数组上三角形的索引。
mask[np.triu_indices_from(mask)] = True
sns.heatmap(corr_data, cmap='viridis',annot=True,fmt='g',mask=mask)
#fmt='g'保持数据小数一致
这里看到很多特征两两之间都没有太多很强的相关性,最后一行的最终成绩可以看到:
每天使用产品越多的学生最终成绩可能越高。
最高学历越高的学生最终成绩可能越高。
平时成绩越好的学生最终成绩可能越高。
其余比较明显相关性的是:
交付作业/考试的间隔时间越大,这个作业/考试的权重可能越高。
重修课程的用户学分会更高。
使用产品天数也多的用户,相对日均点击产品去学习也会越高。
产品日均使用次数越高的,交作业会越早。
女性日均使用产品比男性高
我们选择使用以下几种模型进行建置,并比较模型的分类效能。
首先在将训练集划分为训练集和验证集,其中训练集用于训练模型,验证集用于验证模型效果。首先导入建模库:
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
# 预处理
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 模型评估
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score, f1_score, roc_auc_score
凭经验、以及从上面跟最终成绩跟其他变量的相关性挑出一些重要的指标,并划分特征和标签,并采用分层抽样的方式划分训练集和验证集。
train_df=train_df[["id_student","gender","num_of_prev_attempts","cnt_study_days","avg_click","highest_edu_label","imd_band_label","age_band_label","final_result_label"]]
# 划分特征和标签
X = train_df.drop(['id_student', 'final_result_label'], axis=1)
y = train_df['final_result_label']
# 划分训练集和验证集(分层抽样)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
print(X_train.shape, X_val.shape, y_train.shape, y_val.shape)
#(139129, 7) (34783, 7) (139129,) (34783,)
# 建立knn
knn = KNeighborsClassifier(n_neighbors=4, n_jobs=-1)
#ovo_knn=OneVsOneClassifier(knn)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_val)
#print('Simple KNeighborsClassifier accuracy:%.3f' % (ovo_knn.score(y_val, y_pred)))
print('Simple KNeighborsClassifier accuracy:%.3f' % (accuracy_score(y_val, y_pred)))
print('Simple KNeighborsClassifier f1_score: %.3f' % (f1_score(y_val, y_pred,average='macro')))
Simple KNeighborsClassifier accuracy:0.953
Simple KNeighborsClassifier f1_score: 0.914
lr = LogisticRegression(multi_class='multinomial')
lr.fit(X_train, y_train)
y_pred = lr.predict(X_val)
print('Simple LogisticRegression accuracy:%.3f' % (accuracy_score(y_val, y_pred)))
print('Simple LogisticRegression f1_score: %.3f' % (f1_score(y_val, y_pred,average='macro')))
Simple LogisticRegression accuracy:0.613
Simple LogisticRegression f1_score: 0.203
dtc = DecisionTreeClassifier(max_depth=10, random_state=0)
dtc.fit(X_train, y_train)
y_pred = dtc.predict(X_val)
print('Simple DecisionTreeClassifier accuracy:%.3f' % (accuracy_score(y_val, y_pred)))
print('Simple DecisionTreeClassifier f1_score: %.3f' % (f1_score(y_val, y_pred,average='macro')))
Simple DecisionTreeClassifier accuracy:0.657
Simple DecisionTreeClassifier f1_score: 0.394
rfc = RandomForestClassifier(n_estimators=100, max_depth=10, n_jobs=-1)
rfc.fit(X_train, y_train)
y_pred = rfc.predict(X_val)
print('Simple RandomForestClassifier accuracy:%.3f' % (accuracy_score(y_val, y_pred)))
print('Simple RandomForestClassifier f1_score: %.3f' % (f1_score(y_val, y_pred,average='macro')))
Simple RandomForestClassifier accuracy:0.658
Simple RandomForestClassifier f1_score: 0.350
综上,以f1-score作为评价标准的情况下,KNN算法有较好的分类效能,同时可以通过参数调整的方式来优化其他模型,通过调整预测的门槛值来增加预测效能等其他方式。