某教育平台线上课程用户行为数据分析报告

目录

项目背景

分析思路

1.链路分析

2.指标拆解

探索数据(EDA)

数据处理

用户活跃度分析

1.区域维度

2.时间维度

用户流失分析

流失预警模型

1.流失用户定义

2.特征工程

3.特征筛选

4.数据建模 

完整代码下载


项目背景

此数据集来自于2020年泰迪杯个人技能赛,为某线上教育平台真实数据。此次数据分析的基本目的有两个:

1.探索该线上教育平台的用户行为特征

2.发现业务增长点或业务问题,并用数据建模或其他方式提供相应支持

分析思路

首先我们进行第一项,也就是用户行为特征分析。首先整理一下分析思路。

1.链路分析

分析数据我们不能随便拍脑袋凭感觉来,对于一项业务,我们先按照个人的认知进行链路分析。下图是个人对于线上教育平台业务流程的简单梳理(当然实际业务比这复杂的多)。这里的用户注册,活跃,支付,流失等一系列行为以及指标就是我们需要分析的。

某教育平台线上课程用户行为数据分析报告_第1张图片

2.指标拆解

每项业务都有一个北极星指标,显然的,线上教育的北极星指标就是总收入。我们可以把该指标进行拆解,指标拆解的方式有很多种,这里用最常见的方式来拆。那么我们得到

总收入=总支付人数*人均支付金额

总支付人数=总活跃人数*支付率

总活跃人数=总注册人数*活跃率

总注册人数=总获取人数*注册率

探索数据(EDA)

在梳理过分析思路之后,我们大概知道需要计算哪些指标了。接下来我们对数据进行探索,大体查看我们可以获取哪些信息,可以计算哪些指标,可以从哪些维度去分析。

读取数据,并简单查看数据信息。

import pandas as pd
df_login=pd.read_csv('jiaopei/login.csv',encoding='gbk')
df_login.head()

某教育平台线上课程用户行为数据分析报告_第2张图片

df_user=pd.read_csv('jiaopei/users.csv',encoding='gbk')
df_user.head()

某教育平台线上课程用户行为数据分析报告_第3张图片

df_study=pd.read_csv('jiaopei/study_information.csv',encoding='gbk')
df_study.head()

 某教育平台线上课程用户行为数据分析报告_第4张图片

 可以看到我们可用的有三张表,分别是用户登录行为记录表,用户个人信息维度表,用户和参与课程的对照表。可以看到这里提供了用户注册时间,登录时间,选课时间,登录地址等信息。所以我们初步确定我们可以计算的KPI有总金额,活跃人数(DAU, MAU),用户平均每月营收(ARPU)等。我们可以分析的维度有地区维度和时间维度。

数据处理

首先查看每个表的空值情况,比较简单,这里不放代码,直接说结果,缺失值并不多,只有用户信息表里的school字段存在大量空缺,这里先不做处理,等后面需要建模时再考虑新增一个是否填写学校信息的字段。

为了方便分析各省份的数据,我们先对登录表里的登录地址拆分到省市。这里我们调用腾讯地图API解析地址数据。并且手工处理了异常地址信息,拆出province和city两个字段。

某教育平台线上课程用户行为数据分析报告_第5张图片

用户活跃度分析

1.区域维度

我们根据前文指标拆解里所拆解出来的几个指标,分析不同省份这些指标的情况。本来应该是要看一下每个省份的历史总支付金额的情况,但发现用户学习表和用户登录表有些出路,也就是有些用户在某些月份有学习行为,却没有登录记录,比较奇怪。由于我们无法得知数据来源的更详细信息,所以这里为了避免误解,暂不对总的收入进行分析。

首先我们看总活跃用户数量。可以看到广东省的历史活跃用户数最多且遥遥领先,多达8981人,之后是湖北省,有3049名历史活跃用户数。这两个省份的活跃用户数均明显高于平均水平1864。

某教育平台线上课程用户行为数据分析报告_第6张图片

  而在活跃用户平均每月支付金额方面,湖北用户的支付金额是最高的,高达4299元,当然这里比较写实的反映了疫情初期,湖北用户通过线上学习比较频繁的特点。而除湖北之外,福建省的用户也有较高的平均支付金额。而前面所看到的活跃用户数遥遥领先的广东省,人均支付金额并不突出,只有1837元,甚至未能达到平均值的2202元,因此广东地区的总体营收仍有较大上升空间。

某教育平台线上课程用户行为数据分析报告_第7张图片

而各省份的付费用户率,看上去挺有意思。经济欠发达的中西部地区,付费用户率却很高,而经济发达的北京和上海,付费用户率却垫底。由此我们大概可以知道该教育平台主打的应该是K12基础教育,而非职场人士的技能教育。

某教育平台线上课程用户行为数据分析报告_第8张图片

某教育平台线上课程用户行为数据分析报告_第9张图片

2.时间维度

对于时间维度,我们按照常见的时间维度拆解方式进行分析,也就是分析月度,周一至周日,一天24小时不同时段的活跃用户数量。

 从每月的总活跃人数MAU来看,该教育平台起始于2018年9月。仅从2019年的数据来看,3,4月份和9,10月份的MAU比其他月份会多出将近一倍,也就是这几个月份属于用户线上学习的旺季,推广活动重点考虑放在3,4,9,10月份进行。而从2020年2月开始,MAU迎来爆发式增长,这显然是因为疫情的缘故。由于数据的截止时间只到2020年6月,所以我们不对疫情后的情况进行进一步分析。

某教育平台线上课程用户行为数据分析报告_第10张图片

再看看ARPU的情况,结合前面的MAU情况,我们大概可以得知,2019年3月应该是做了推广活动,拉进了大量新客户,所以MAU暴涨。但这些新客户还未进行支付,因此拉低了ARPU。但业务员相当给力,次月就大幅提升了ARPU。而随着业务的成熟,在2019年10月这个旺季,ARPU达到了最高点2922元。

某教育平台线上课程用户行为数据分析报告_第11张图片

周一至周日的日活,可以看到周一的日均日活人数是最多的,平均705人,越往后,基本上每天的日活都会下降一些。

某教育平台线上课程用户行为数据分析报告_第12张图片

 通过24小时每个时段的平均活跃人数,可以看到早上10点左右的平均活跃人数最多,另外在下午3点左右和晚上8点左右也有个小高峰,这说明我们前面的判断基本是正确的,该平台主打的是K12教育。

某教育平台线上课程用户行为数据分析报告_第13张图片

用户流失分析

分析完活跃度,我们来看看用户流失的情况。对于企业来说,业务成熟之后,开发一个新用户的成本,要远高于维护一个老客户的成本。所以分析用户流失是非常有必要的。这里我们按照最常见的方法计算流失率,也就是以上个月的MAU为基数,用本月的MAU减去本月新增用户,得到本月活跃老用户,再用上月MAU减去本月活跃老用户数,就是流失的老用户,再除以上月MAU,就是本月的流失率。

某教育平台线上课程用户行为数据分析报告_第14张图片

 可以看到平台创立初期,每月的流失率较高,从2019年7月开始,流失率开始维持在60%左右,2020年3月受疫情影响,用户粘性大幅提升,流失率仅为24.9%。 但在疫情初期过后,用户流失率又开始上升。60%的月流失率是什么概念呢?相当于每个月的拉新,加上召回长时间未上线的老客户,需要达到上个月的60%,才能维持MAU这个关键KPI不滑落。这显然会浪费大量推广和召回的成本,所以我们考虑搭建用户流失预警模型,挖掘出那些大概率流失的客户,提前为业务人员做出预警,尽快采取措施挽留客户。

流失预警模型

1.流失用户定义

对于流失预警模型,我们首先要定义什么样的用户算流失用户。前文用的是最传统的方法,也就是上个月的用户如果在次月未登录,就算流失,这可以认为是用户30天未登录就视为流失。我们需要探究这样的划分是否合理。下图是最近一次登录距离最新时间的天数间隔的用户人数,可以看到,上次登录日期大于40天的用户又很明显的上升,也就是说,用户超过40天未登录,之后就可能不会再登录了,也就是流失用户。

显然,比起30天,我们更应该把40天定为一个阈值,超过40天未登录的用户视为流失用户。

某教育平台线上课程用户行为数据分析报告_第15张图片

 然后根据数据的特点,我们取4月份日活比较大的一天作为样本,取该天活跃用户过往60天的行为特征,并用之后40天是否有过登录行为,判断其是否流失。

2.特征工程

这里我们取4月30号的数据作为训练集。取其过往30天,60天以及历史特征,显然的,我们很容易凭直觉认为登录次数,选课数量,学习进度,支付金额这些是重要特征,所以我们将这些特征提取出来,并提取对应的sum,max,min等进行特征衍生。

from dateutil.relativedelta import relativedelta
startdate = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')+relativedelta(days=-60)
enddate = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')
churndate = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')+relativedelta(days=40)

df_train_p60=df_login_user[(df_login_user['login_date']>=startdate) & (df_login_user['login_date']<=enddate)]
# df_train.head()
df_train=df_login_user[(df_login_user['login_date']==enddate)]
df_train.head()

df_train_target_data=df_login_user[(df_login_user['login_date']>enddate) & (df_login_user['login_date']<=churndate)]
# 用之后40天是否有登录行为标记用户是否流失
df_train['churn']=df_train['user_id'].map(lambda x:1 if x in df_train_target_data['user_id'].unique() else 0)

# 取出我们需要的字段
df_train=df_train[['user_id',  'province_x',  'number_of_classes_join', 'number_of_classes_out',
       'learn_time', 'school','churn']].rename(columns={'province_x':'province'})

# 如果用户出现多个登录地址,用出现次数最多的登录省份作为用户的省份
dftemp=df_train.groupby(['user_id','province'])['churn'].count().reset_index()
dftemp2=dftemp.groupby(by='user_id', as_index=False)['churn'].max()
dftemp2=dftemp2.merge(dftemp,on=['user_id','churn'],how='left')
user_place_map={}
for i in np.array(dftemp2).tolist():
    user_place_map[i[0]]=[i[2]]
    
df_train['province']=df_train['user_id'].map(lambda x:user_place_map[x][0])
# df_train2['city']=df_train2['user_id'].map(lambda x:user_place_map[x][1])
df_train=df_train.drop_duplicates()

df_train=df_train.drop_duplicates()
startdate1 = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')+relativedelta(days=-30)
startdate2 = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')+relativedelta(days=-60)
enddate = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')
churndate = datetime.datetime.strptime('2020-04-30', '%Y-%m-%d')+relativedelta(days=40)
# df_study_temp=df_study[(df_study['course_join_date']>=startdate) & (df_study['course_join_date'])=startdate1)]

temp_dict=df_study_temp.groupby(['user_id'])['course_id'].agg(['nunique']).reset_index().rename(columns={'nunique':'course_cnt'})
temp_dict.index = temp_dict['user_id'].values
temp_dict = temp_dict['course_cnt'].to_dict()
df_train2['course_cnt_p30']=df_train2['user_id'].map(temp_dict)

g = df_study_temp.groupby('user_id', as_index=False)
feat = g['price'].agg({
    '{}_max_p30'.format('price'): 'max', '{}_min_p30'.format('price'): 'min',
    '{}_sum_p30'.format('price'): 'sum','{}_mean_p30'.format('price'): 'mean',
})
df_train2 = df_train2.merge(feat, on='user_id', how='left')
# print (feat[feat['user_id']=='用户4'])

# 过往30天登录次数
df_login['login_date']=df_login['login_date'].map(lambda x:datetime.datetime.strptime(str(x)[:10], '%Y-%m-%d'))

df_study_temp=df_login[(df_login['login_date']=startdate1)]
temp_dict=df_study_temp.groupby(['user_id'])['login_date'].agg(['nunique']).reset_index().rename(columns={'nunique':'login_cnt'})
temp_dict.index = temp_dict['user_id'].values
temp_dict = temp_dict['login_cnt'].to_dict()
df_train2['login_cnt_p30']=df_train2['user_id'].map(temp_dict)

# 2020 4月30过往60天购买的参加的所有课程,所有总付费金额,最大付费金额,最小付费金额,平均进度,最大进度,最小进度
df_study_temp=df_study[(df_study['course_join_date']=startdate2)]

temp_dict=df_study_temp.groupby(['user_id'])['course_id'].agg(['nunique']).reset_index().rename(columns={'nunique':'course_cnt'})
temp_dict.index = temp_dict['user_id'].values
temp_dict = temp_dict['course_cnt'].to_dict()
df_train2['course_cnt_p60']=df_train2['user_id'].map(temp_dict)

g = df_study_temp.groupby('user_id', as_index=False)
feat = g['price'].agg({
    '{}_max_p60'.format('price'): 'max', '{}_min_p60'.format('price'): 'min',
    '{}_sum_p60'.format('price'): 'sum','{}_mean_p60'.format('price'): 'mean',
})
df_train2 = df_train2.merge(feat, on='user_id', how='left')

# 过往60天登录次数

df_study_temp=df_login[(df_login['login_date']=startdate2)]
temp_dict=df_study_temp.groupby(['user_id'])['login_date'].agg(['nunique']).reset_index().rename(columns={'nunique':'login_cnt'})
temp_dict.index = temp_dict['user_id'].values
temp_dict = temp_dict['login_cnt'].to_dict()
df_train2['login_cnt_p60']=df_train2['user_id'].map(temp_dict)

# 历史登录次数

df_study_temp=df_login[(df_login['login_date']

对于school字段,鉴于有大量缺失,我们将其改为二分类特征,有院校信息的为1,否则为0。

# 对于school字段,鉴于有大量缺失,我们将其改为二分类特征,有院校信息的为1,否则为0
df_train2['school']=df_train2['school'].fillna(0,inplace=True)
df_train2['school']=df_train2['school'].map(lambda x:1 if x!=0 else 0)

df_train2.head()

查看衍生出来的特征是否有空值,经检查是有的,这里我们直接用0填充空值,因为显然衍生出来的这些特征如果为空,那就代表没有相应行为。

df_train2.isnull().sum()
for fea in df_train.columns:
    df_train2[fea].fillna(0,inplace = True)

 这里的省份特征基数较高,不适合进行one-hot编码,所以采取计数编码。

# 对高基数分类变量进行编码

for data in [df_train2]:
    for f in ['province']:
        data[f+'_cnts'] = data.groupby([f])['province'].transform('count')
        del data[f]

3.特征筛选

我们通过查看相关系数热力图,查找高度相关的特征。

import matplotlib.pyplot as plt
import seaborn as sns
def correlation_heatmap(df):
    _ , ax = plt.subplots(figsize =(14, 12))
    colormap = sns.diverging_palette(220, 10, as_cmap = True)
    
    _ = sns.heatmap(
        df.corr(), 
        cmap = colormap,
        square=True, 
        cbar_kws={'shrink':.9 }, 
        ax=ax,
        annot=True, 
        linewidths=0.1,vmax=1.0, linecolor='white',
        annot_kws={'fontsize':12 }
    )
    
    plt.title('Pearson Correlation of Features', y=1.05, size=15)
    
numerical_fea = list(df_train2.select_dtypes(exclude=['object']).columns)
category_fea = list(filter(lambda x: x not in numerical_fea,list(df_train2.columns)))

correlation_heatmap(df_train2[numerical_fea])

 可以看到过往30天和过往60天的支付行为有较高相关性,但是鉴于我们这里的数据量较小以及特征数量并不算多,所以我们这里仍然保留这些特征。

某教育平台线上课程用户行为数据分析报告_第16张图片

4.数据建模 

完成特征工程和特征筛选后,就可以进行数据建模了,这里我们将通过交叉验证的方式,比较"SVC","DecisionTree","AdaBoost","RandomForest","ExtraTrees","GradientBoosting","MultipleLayerPerceptron","KNeighboors","LogisticRegression","LinearDiscriminantAnalysis集中模型的拟合结果。

from sklearn.model_selection import GridSearchCV, cross_val_score, StratifiedKFold, learning_curve
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, ExtraTreesClassifier, VotingClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.svm import SVC
kfold = StratifiedKFold(n_splits=5)
# Modeling step Test differents algorithms 
random_state = 2
classifiers = []
classifiers.append(SVC(random_state=random_state))
classifiers.append(DecisionTreeClassifier(random_state=random_state))
classifiers.append(AdaBoostClassifier(DecisionTreeClassifier(random_state=random_state),random_state=random_state,learning_rate=0.1))
classifiers.append(RandomForestClassifier(random_state=random_state))
classifiers.append(ExtraTreesClassifier(random_state=random_state))
classifiers.append(GradientBoostingClassifier(random_state=random_state))
classifiers.append(MLPClassifier(random_state=random_state))
classifiers.append(KNeighborsClassifier())
classifiers.append(LogisticRegression(random_state = random_state))
classifiers.append(LinearDiscriminantAnalysis())

df_train3=df_train2.copy()
Y = df_train3["churn"]
X = df_train3.drop(labels = ["user_id","churn"],axis = 1)

cv_results = []
for classifier in classifiers :
    cv_results.append(cross_val_score(classifier, X, y = Y, scoring = "accuracy", cv = kfold, n_jobs=4))

cv_means = []
cv_std = []
for cv_result in cv_results:
    cv_means.append(cv_result.mean())
    cv_std.append(cv_result.std())

cv_res = pd.DataFrame({"CrossValMeans":cv_means,"CrossValerrors": cv_std,"Algorithm":["SVC","DecisionTree","AdaBoost",
"RandomForest","ExtraTrees","GradientBoosting","MultipleLayerPerceptron","KNeighboors","LogisticRegression","LinearDiscriminantAnalysis"]})

g = sns.barplot("CrossValMeans","Algorithm",data = cv_res, palette="Set3",orient = "h",**{'xerr':cv_std})
g.set_xlabel("Mean Accuracy")
g = g.set_title("Cross validation scores")

 可以看到,随机森林(RandomForest)模型的交叉验证平均得分最高,所以我们采用随机森林进行建模。

某教育平台线上课程用户行为数据分析报告_第17张图片

 后续如果模型能够落地,应该是搭建一个评分卡模型,也就是对用户的流失概率做预测。在业务使用方面,业务员需要先自定义一个预流失期,判断哪些用户是预流失用户,然后通过评分卡模型判断用户流失概率。

比如业务员定义20天未登录用户属于预流失用户,那么比如说4月30号,业务员需要判断4月10号最后一次登录的用户,有哪些是大概率流失的,那么我们可以通过上面的模型预测这些用户流失的概率,业务员根据用户流失的概率,在成本有限的情况下,更精准地对高流失概率的用户进行挽回。

完整代码下载

链接:https://pan.baidu.com/s/1O9HQOOM35donqltUbC8LSQ 
提取码:jnpw

你可能感兴趣的:(数据分析,数据挖掘)