目录

项目背景

分析思路

1.链路分析

2.指标拆解

探索数据(EDA)

数据处理

用户活跃度分析

1.区域维度

2.时间维度

用户流失分析

流失预警模型

1.流失用户定义

2.特征工程

3.特征筛选

4.数据建模

完整代码下载


项目背景

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

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

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

分析思路

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

1.链路分析

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

2.指标拆解

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

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

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

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

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

探索数据(EDA)

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

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

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

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

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

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

数据处理

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

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

用户活跃度分析

1.区域维度

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

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

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

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

2.时间维度

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

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

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

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

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

用户流失分析

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

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

流失预警模型

1.流失用户定义

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

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

然后根据数据的特点,我们取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'])<enddate]
# 2020 4月30之前购买的参加的所有课程,所有总付费金额,最大付费金额,最小付费金额,平均进度,最大进度,最小进度
df_study['course_join_date']=df_study['course_join_date'].map(lambda x:datetime.datetime.strptime(str(x)[:10], '%Y-%m-%d'))
df_study_temp=df_study[(df_study['course_join_date']<enddate)]
df_train2=df_train.copy()
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']=df_train2['user_id'].map(temp_dict)g = df_study_temp.groupby('user_id', as_index=False)
feat = g['price'].agg({'{}_max'.format('price'): 'max', '{}_min'.format('price'): 'min','{}_sum'.format('price'): 'sum','{}_mean'.format('price'): 'mean',
})
df_train2 = df_train2.merge(feat, on='user_id', how='left')# 2020 4月30过往30天购买的参加的所有课程,所有总付费金额,最大付费金额,最小付费金额,平均进度,最大进度,最小进度
df_study_temp=df_study[(df_study['course_join_date']<enddate) & (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']<enddate) & (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']<enddate) & (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']<enddate) & (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']<enddate)]
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']=df_train2['user_id'].map(temp_dict)df_train2.head()

对于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天的支付行为有较高相关性,但是鉴于我们这里的数据量较小以及特征数量并不算多,所以我们这里仍然保留这些特征。

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)模型的交叉验证平均得分最高,所以我们采用随机森林进行建模。

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

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

完整代码下载

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

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

  1. 教育平台线上课程用户行为分析

    教育平台线上课程用户行为分析 一. 分析的背景和目的 因为新冠疫情的影响,越来越多的教育平台开启了线上课程.线上课程相较于传统的线下课程,不论时间还是地点都更加的灵活,人们开始更加倾向于选择线上学习. ...

  2. 教育平台线上课程数据分析

    教育平台的线上课程数据分析 一.项目背景 在线教育一般指基于互联网的线上学习行为,与传统的线下教育机构.培训班.学校相比,在线教育在时间和空间上有很多优势. 近年来,随着互联网与通信技术的高速发展,各 ...

  3. 教育平台的线上课程智能推荐策略

    题目来自:http://www.tipdm.org 一. 背景 近年来,随着互联网与通信技术的高速发展,学习资源的建设与共享呈现出 新的发展趋势,各种网课.慕课.直播课等层出不穷,各种在线教育平台和学 ...

  4. Python数据分析实践项目 教育平台的线上课程智能推荐

    嗨喽! 大家好,我是"流水不争先,争的是滔滔不绝"的翀,欢迎大家来交流学习,一起入坑数据分析,希望我们一起好好学习,天天向上,目前在社会毒打中~~ 文章目录 摘要 关键词:数据分析 ...

  5. 教育平台的线上课程推荐策略——课程分级

    文章列表 篇1:<在线教育平台的数据分析--用户精细化运营> 篇2:<在线教育平台的数据分析--课程分级> 篇3:<在线教育平台的数据分析--业务流程指标的计算> ...

  6. 教育平台的线上课程智能推荐策略-Python

    1 背景 近年来,随着互联网与通信技术的高速发展,学习资源共享与建设呈现出新的发展趋势,多样化的线上教育平台如雨后春笋般争相涌入大众视野.尤其是自2020年初,受新冠肺炎疫情的冲击,学生返校进行线下 ...

  7. 如何在 LearnDash 线上教育平台网站上构建和管理大型课程

    以课程的形式在 LearnDash 线上教育平台网站上上传大量内容.你的课程长度是否让学生不知所措? 这一切都是为了确保您的课程结构正确, 从而使您更容易管理内容.学生进度和参与度. 目录 隐藏 划分 ...

  8. 线上课程直播平台做推广的方式有哪几种?

    随着移动互联网时代的到来,在线教育平台如雨后春笋一般出现.根据相关数据的预测,2020年在线教育市场规模将会达到4330亿.然而线上课程直播平台的竞争是很激烈的,想要在这局面中脱颖而出,教育机构只能花 ...

  9. 医疗用户端app原型/问诊/挂号/开药/视频问诊/电子处方/预约/互联网医疗平台用户端/Axure原型/电话问诊/药品/就诊开药/远程医疗平台/线上问诊/线上看病/rp源文件/移动端医疗原型/门诊

    医疗用户端app原型/问诊/挂号/开药/视频问诊/电子处方/预约/互联网医疗平台用户端/Axure原型/电话问诊/药品/就诊开药/远程医疗平台/线上问诊/线上看病/rp源文件/移动端医疗原型 Axur ...

最新文章

  1. IOS问题汇总:2012-12-18 UIAlertView+UIActionSheet
  2. VMware15虚拟机安装教程
  3. jQuery成为微软.NET开发工具的一部分了
  4. STM32 基础系列教程 46 – RNG
  5. 计算机科学英文杂志,Journal of Computer Science Technology
  6. 微型计算机的分类有,微型计算机的分类
  7. Feign 简介和使用
  8. (15)VHDL测试激励编写(复位)
  9. 加速转型 高通绝地反攻
  10. Windows Server 2012 R2 DirectAccess功能测试(3)—App2服务器安装及配置
  11. PHP 连接SQLServer的方法
  12. python绘图——图片大小设置figsize
  13. 怎么判断冠词用a还是an_怎么判断英语量词改用a还是an
  14. vscode 支持 X11 Forwarding
  15. 【学习笔记】线段树详解(全)
  16. 模拟电话交换机和IPPBX之间进行连接
  17. setClickable,setEnabled,setFocusable 的区别
  18. description标签如何正确使用?
  19. 信息安全分级保护之我见
  20. js canvas迷宫

热门文章

  1. 公司要上线个新产品功能,后续的运营将由你来跟进。在项目推广前,你计划做哪些准备,具体描述一下。
  2. 今天科普一下 苹果开发者账号中:个人、公司、企业账号的区别
  3. MATLAB绘制空间曲线和曲面图像
  4. Linux下nvidia压力测试,一种服务器linux系统下GPU压力测试的监控方法与流程
  5. 2021 考完研的一年体会,宝贵经验(成功上岸)
  6. java毕业生设计在线学习系统计算机源码+系统+mysql+调试部署+lw
  7. CAD查找文字(com接口c#语言)
  8. 智慧法院(信息法院)
  9. 合伙创业 务必远离十种人
  10. word文档如何插入求和Σ公式