赞
踩
** 【续上篇】 用Python分析用户消费行为 Student Comsumption Analysis ① https://blog.csdn.net/weixin_44216391/article/details/89309643
# 【续】本次案例:用户消费行为分析
# 借用阿里天池【数智教育_数据可视化创新大赛】数据源中的学生消费数据来作为本次用户消费行为分析的数据来源。
# 本篇分析方法参考该帖子:http://www.woshipm.com/data-analysis/757648.html
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
%matplotlib inline
plt.style.use("ggplot")
import warnings
warnings.filterwarnings("ignore")
# plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签
# plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
# seaborn中文乱码解决方案
import seaborn as sns
from matplotlib.font_manager import FontProperties
myfont=FontProperties(fname=r'C:\Windows\Fonts\simhei.ttf',size=14)
sns.set(font=myfont.get_name())
df=pd.read_csv("D:/2018_BigData/Python/Python_files_Notebook/theme_practice/student_consumption_day.csv")
df.head()
DealTime | bf_StudentID | AccName | PerSex | MonDeal | avgMonDeal | transaction_times | month | |
---|---|---|---|---|---|---|---|---|
0 | 2018-07-01 | 13983 | 裘某某 | 男 | -3.7 | -3.70 | 1 | 2018-07-01 |
1 | 2018-07-01 | 14018 | 虞某某 | 男 | -9.5 | -9.50 | 1 | 2018-07-01 |
2 | 2018-07-01 | 14073 | 刘某某 | 男 | -8.0 | -8.00 | 1 | 2018-07-01 |
3 | 2018-07-01 | 14074 | 周某某 | 男 | -14.3 | -7.15 | 2 | 2018-07-01 |
4 | 2018-07-01 | 14097 | 毛某某 | 男 | -10.0 | -10.00 | 1 | 2018-07-01 |
# 承接上一章,将用户消费数据进行数据透视。
pivoted_counts=df.pivot_table(index="bf_StudentID",columns="month",values="transaction_times",aggfunc="sum").fillna(0)
columns_month=df.month.sort_values().astype("str").unique()
pivoted_counts.columns=columns_month
pivoted_counts.head()
2018-07-01 | 2018-08-01 | 2018-09-01 | 2018-10-01 | 2018-11-01 | 2018-12-01 | 2019-01-01 | |
---|---|---|---|---|---|---|---|
bf_StudentID | |||||||
13012 | 0.0 | 0.0 | 10.0 | 10.0 | 15.0 | 7.0 | 9.0 |
13564 | 17.0 | 25.0 | 82.0 | 63.0 | 69.0 | 75.0 | 50.0 |
13599 | 8.0 | 10.0 | 39.0 | 33.0 | 34.0 | 31.0 | 26.0 |
13685 | 8.0 | 12.0 | 28.0 | 33.0 | 34.0 | 39.0 | 17.0 |
13947 | 9.0 | 22.0 | 81.0 | 64.0 | 66.0 | 72.0 | 63.0 |
# 用applymap+lambda转换数据,只要当月消费超过30次,记为1,反之为0。
pivoted_purchase = pivoted_counts.applymap(lambda x: 1 if x > 30 else 0)
pivoted_purchase.head()
2018-07-01 | 2018-08-01 | 2018-09-01 | 2018-10-01 | 2018-11-01 | 2018-12-01 | 2019-01-01 | |
---|---|---|---|---|---|---|---|
bf_StudentID | |||||||
13012 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
13564 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
13599 | 0 | 0 | 1 | 1 | 1 | 1 | 0 |
13685 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
13947 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
# 原帖子: # 接下来进行用户分层,我们按照用户的消费行为,简单划分成几个维度:新用户、活跃用户、不活跃用户、回流用户。 # 新用户的定义是第一次消费。活跃用户即老客,在某一个时间窗口内有过消费。不活跃用户则是时间窗口内没有消费过的老客。回流用户是在上一个窗口中没有消费,而在当前时间窗口内有过消费。以上的时间窗口都是按月统计。 # 比如某用户在1月第一次消费,那么他在1月的分层就是新用户;他在2月消费国,则是活跃用户;3月没有消费,此时是不活跃用户;4月再次消费,此时是回流用户,5月还是消费,是活跃用户。 # 分层会涉及到比较复杂的逻辑判断。 # ---------------------------------------------------------- # 本次学生消费案例: # 接下来进行用户分层,我们按照学生的消费行为,简单划分成几个维度:新生(新用户)、超爱饭堂的学生(活跃用户)、不爱饭堂的学生(不活跃用户)、偶尔爱饭堂的学生(回流用户)。 # 新生的定义是第一次消费超过30次。超爱饭堂的学生即常客/老客,在某一个时间窗口内有过消费30次以上。不爱饭堂的学生则是时间窗口内没有消费超过30次的老客。回流用户是在上一个窗口中没有消费30次以上,而在当前时间窗口内有过消费30次以上。以上的时间窗口都是按月统计。 # 比如某学生在9月第一次消费30次以上,那么他在9月的分层就是新生(新用户); # 他在10月消费30次以上,则是超爱饭堂的学生(活跃用户); # 11月没有消费30次以上,此时是不爱饭堂的学生(不活跃用户); # 12月再次消费30次以上,此时是偶尔爱饭堂的学生(回流用户); # 1月还是消费30次以上,是超爱饭堂的学生(活跃用户)。 # 分层会涉及到比较复杂的逻辑判断。
def active_status(data): status=[] for i in range(7): # 若本月没有“消费30次以上” if data[i] == 0: if len(status) > 0: if status[i-1] == "unreg": status.append("unreg") else: status.append("unlike_canteen") else: status.append("unreg") # 若本月“消费30次以上” else: if len(status) == 0: status.append("new") else: if status[i-1] == "unlike_canteen": status.append("occasionally_like_canteen") elif status[i-1] == "unreg": status.append("new") else: status.append("love_canteen") return status # 函数写得比较复杂,主要分为两部分的判断,以本月是否“消费60次以上”为界。 # 本月没有“消费60次以上”,还要额外判断他是不是新生(新客), # 因为部分学生是9月份才“消费30次以上”成为新生(新客),那么在7、8月份他应该连新生(新客)都不是,用unreg表示。 # 如果是老客(即前一个月消费30次以上),则为unlike_canteen。 # 本月若有“月消费30次以上”,需要判断是不是第一次“月消费30次以上”,上一个时间窗口有没有“月消费30次以上”。 # 大家可以多调试几次理顺里面的逻辑关系,对用户进行分层,逻辑确实不会简单,而且这里只是简化版本的。
# pivoted_purchase_status = pivoted_purchase.apply(lambda x: active_status(x),axis = 1)
# pivoted_purchase_status.head()
# 运行报错:("'Series' object is not callable", 'occurred at index 13012')
# 那查看一下“index 13012”是何方神圣。
pivoted_purchase.loc[13012]
2018-07-01 0
2018-08-01 0
2018-09-01 0
2018-10-01 0
2018-11-01 0
2018-12-01 0
2019-01-01 0
Name: 13012, dtype: int64
# 看看其他行与索引13012有没有什么区别
pivoted_purchase.loc[13564]
# 没差别。。。
2018-07-01 0
2018-08-01 0
2018-09-01 1
2018-10-01 1
2018-11-01 1
2018-12-01 1
2019-01-01 1
Name: 13564, dtype: int64
pivoted_purchase.shape
# 1730行(学生学号),7列(月份)
(1730, 7)
pivoted_purchase.dtypes
2018-07-01 int64
2018-08-01 int64
2018-09-01 int64
2018-10-01 int64
2018-11-01 int64
2018-12-01 int64
2019-01-01 int64
dtype: object
type(pivoted_purchase)
pandas.core.frame.DataFrame
# 这么看来,前面报错(pivoted_purchase.shape)是因为,series不能用于apply函数? # 搜索无果。 # 回到报错提示去看详情,终于发现了点东东。 # 先看详情: # <ipython-input-31-17b35956730f> in <lambda>(x) # ----> 1 pivoted_purchase_status = pivoted_purchase.apply(lambda x: active_status(x),axis = 1) # 2 pivoted_purchase_status.head() # 3 # 4 # 运行报错:("'Series' object is not callable", 'occurred at index 13012') # <ipython-input-28-331c5573a3a9> in active_status(data) # 4 # 5 # 若本月没有“消费30次以上” # ----> 6 if data(i) < 30: # 7 if len(status) > 0: # 8 if status[i-1] == "unreg": # TypeError: ("'Series' object is not callable", 'occurred at index 13012') # ----------------------------------------------------------------- # 以上,首先提示错误在<lambda>(x); # 接下来提示错误在active_status(data)的第六行: # ----> 6 if data(i) < 30: # 缩小到行了,再一细看,果然发现了一个小括号data(i)。 # 而根据多出查询说明,报错"'XXX' object is not callable"一般是因为该用中括号的地方用了小括号。 # 回到原参考帖去看,果然人家是中括号啊~~~字太小了看不清,认错敲错了。。。
pivoted_purchase_status = pivoted_purchase.apply(lambda x: active_status(x),axis = 1)
pivoted_purchase_status.head(8)
# 一开始虽然没运行报错,不过又转化成坑爹的不知道是啥的格式了。
# 而且貌似逻辑不对。显示结果居然全是学生饭堂深度爱好者(love_canteen),这不科学。
# 然后又细细查看,原来是把for i in range(7)后面的if data[i]==0 的“0”误写成“30”了。
# 将if data[i]==30更正回if data[i]==0之后,逻辑正常,如下所示。只是格式问题还需搜索方法来处理。
bf_StudentID
13012 [unreg, unreg, unreg, unreg, unreg, unreg, unreg]
13564 [unreg, unreg, new, love_canteen, love_canteen...
13599 [unreg, unreg, new, love_canteen, love_canteen...
13685 [unreg, unreg, unreg, new, love_canteen, love_...
13947 [unreg, unreg, new, love_canteen, love_canteen...
13948 [unreg, unreg, new, love_canteen, love_canteen...
13949 [unreg, unreg, new, love_canteen, love_canteen...
13950 [unreg, unreg, new, love_canteen, love_canteen...
dtype: object
# 仍然试试,使用索引1来表示第二列[1]。
# month=pivoted_purchase_status[1].str.split(',',expand=True)
# month.columns=['201807','201808','201809','201810','201811','201812','201901']
# pivoted_purchase_status=pivoted_purchase_status.join(month)
# pivoted_purchase_status.head()
# 运行报错:KeyError: 1
# 那接下来看看要怎么确定列名、转化格式、完成分列和重建dataframe。
# 先看看现在都是些什么格式、什么列名。
type(pivoted_purchase_status)
# def和apply函数搞一下,
# 从pivoted_purchase的pandas.core.frame.DataFrame到了pivoted_purchase_status的pandas.core.series.Series。
pandas.core.series.Series
# pivoted_purchase_status.columns
# AttributeError: 'Series' object has no attribute 'columns'
# 原来是series格式。
# 那如何转成dataframe呢?来战。
pivoted_purchase_status=pd.DataFrame(pivoted_purchase_status)
# 或 pivoted_purchase_status.reset_index()
pivoted_purchase_status.head()
0 | |
---|---|
bf_StudentID | |
13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] |
13564 | [unreg, unreg, new, love_canteen, love_canteen... |
13599 | [unreg, unreg, new, love_canteen, love_canteen... |
13685 | [unreg, unreg, unreg, new, love_canteen, love_... |
13947 | [unreg, unreg, new, love_canteen, love_canteen... |
type(pivoted_purchase_status)
# 看起来像是成功转化成DataFrame了
pandas.core.frame.DataFrame
pivoted_purchase_status.columns
# 稀奇古怪的RangeIndex,是啥。。。能否分列?Excel好办,不过Python…应该更简单吧。。
# 问题是,没有列名,何来分列。。。
RangeIndex(start=0, stop=1, step=1)
pivoted_purchase_status.index
Int64Index([13012, 13564, 13599, 13685, 13947, 13948, 13949, 13950, 13951,
13952,
...
16153, 16154, 16155, 16156, 16157, 16158, 16159, 16160, 16161,
16162],
dtype='int64', name='bf_StudentID', length=1730)
# 重置行索引为默认索引,生成新的Dataframe——reset_index()函数
pivoted_purchase_status=pivoted_purchase_status.reset_index()
pivoted_purchase_status.head()
bf_StudentID | 0 | |
---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... |
2 | 13599 | [unreg, unreg, new, love_canteen, love_canteen... |
3 | 13685 | [unreg, unreg, unreg, new, love_canteen, love_... |
4 | 13947 | [unreg, unreg, new, love_canteen, love_canteen... |
pivoted_purchase_status.columns
# 出现列了。只是列名是一个数字0,那该如何?直接用0还是可以重新赋值?
Index(['bf_StudentID', 0], dtype='object')
# 先直接用列[0]试试.
# month=pivoted_purchase_status[0].str.split(',',expand=True)
# month.columns=['201807','201808','201809','201810','201811','201812','201901']
# pivoted_purchase_status=pivoted_purchase_status.join(month)
# pivoted_purchase_status.head()
# 运行报错 ValueError: Length mismatch: Expected axis has 1 elements, new values have 7 elements
# 报错出现在:第二行month.columns赋值那里。可能是太多参数了,那我们减成一个参数看看。
month=pivoted_purchase_status[0].str.split(',',expand=True)
month.columns=['month']
pivoted_purchase_status=pivoted_purchase_status.join(month)
pivoted_purchase_status.head()
# 用month.columns=['month']替换month.columns=['201807','201808','201809','201810','201811','201812','201901']
# 运行没有报错,只是得出的结果,稀奇古怪,说明其实没有将列中内容分开。可能是分列符号没用对?
# 不过起码可以证明,不需要重新给列名赋值,直接用[0]索引即可。
bf_StudentID | 0 | month | |
---|---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] | NaN |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... | NaN |
2 | 13599 | [unreg, unreg, new, love_canteen, love_canteen... | NaN |
3 | 13685 | [unreg, unreg, unreg, new, love_canteen, love_... | NaN |
4 | 13947 | [unreg, unreg, new, love_canteen, love_canteen... | NaN |
# 尝试删掉month.columns赋值语句,运行,
# month=pivoted_purchase_status[0].str.split(', ',expand=True)
# pivoted_purchase_status=pivoted_purchase_status.join(month)
# pivoted_purchase_status.head()
# 运行报错 ValueError: columns overlap but no suffix specified: Index([0], dtype='object')
# 报错在第二行:pivoted_purchase_status=pivoted_purchase_status.join(month)
pd.concat([pivoted_purchase_status,pivoted_purchase_status[0].str.split(',',expand=True)],axis=1).head(2)
# 尝试用concat替代join来拼接,还是没成功。估计问题不在join或者concat,而是在split函数里。
# 又或者,一个大胆的猜想:split函数中的列索引[0],并不是索引第二列,而是索引第一列bf_StudentID。这个认知十分震惊。
# 那我们重新给列赋值新列名,用列名来索引。
bf_StudentID | 0 | month | 0 | |
---|---|---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] | NaN | NaN |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... | NaN | NaN |
pivoted_purchase_status = pivoted_purchase_status.drop(["month"],axis=1) #删掉刚刚多出的month列,恢复纯净
pivoted_purchase_status.columns = ["bf_StudentID","User_Hierarchy"]
pivoted_purchase_status.head(2)
# 更改列名成功。刚刚猜想正确,列索引[0]果然表示第一列,而不是列名为0的第二列。
bf_StudentID | User_Hierarchy | |
---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... |
pd.concat([pivoted_purchase_status,pivoted_purchase_status["User_Hierarchy"].str.split(',',expand=True)],axis=1).head(2)
# 这厉害,还是分不开。。。
bf_StudentID | User_Hierarchy | 0 | |
---|---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] | NaN |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... | NaN |
a=pivoted_purchase_status.head(5)
a
bf_StudentID | User_Hierarchy | |
---|---|---|
0 | 13012 | [unreg, unreg, unreg, unreg, unreg, unreg, unreg] |
1 | 13564 | [unreg, unreg, new, love_canteen, love_canteen... |
2 | 13599 | [unreg, unreg, new, love_canteen, love_canteen... |
3 | 13685 | [unreg, unreg, unreg, new, love_canteen, love_... |
4 | 13947 | [unreg, unreg, new, love_canteen, love_canteen... |
a["User_Hierarchy"].str.split((" "),expand=True)
# 仍然失败。
0 | |
---|---|
0 | NaN |
1 | NaN |
2 | NaN |
3 | NaN |
4 | NaN |
# 导出到CSV,用Excel处理完再导回来。
pivoted_purchase_status.to_csv(r"D:/2018_BigData/Python/Python_files_Notebook/Theme_Practice/student_consumption_pivoted_purchase_status.csv",index=False)
# Excel已处理完,现在导回来。
pivoted_purchase_status=pd.read_csv("D:/2018_BigData/Python/Python_files_Notebook/theme_practice/student_consumption_pivoted_purchase_status_splited_above30per month.csv")
pivoted_purchase_status.tail(4)
# 妥。
bf_StudentID | User_Hierarchy | Unnamed: 2 | Unnamed: 3 | Unnamed: 4 | Unnamed: 5 | Unnamed: 6 | Unnamed: 7 | |
---|---|---|---|---|---|---|---|---|
1726 | 16159 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | love_canteen |
1727 | 16160 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | unlike_canteen |
1728 | 16161 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | love_canteen |
1729 | 16162 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | unlike_canteen |
# 更换列名
columns=["bf_StudentID",'201807','201808','201809','201810','201811','201812','201901']
pivoted_purchase_status.columns=columns
pivoted_purchase_status.tail(4)
# 妥。
bf_StudentID | 201807 | 201808 | 201809 | 201810 | 201811 | 201812 | 201901 | |
---|---|---|---|---|---|---|---|---|
1726 | 16159 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | love_canteen |
1727 | 16160 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | unlike_canteen |
1728 | 16161 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | love_canteen |
1729 | 16162 | unreg | unreg | new | love_canteen | love_canteen | love_canteen | unlike_canteen |
# 从结果看,学生(用户)每个月的分层状态以及变化已经被我们计算出来。我是根据透视出的宽表计算。
# 其实还有一种另外一种写法,只提取时间窗口内的数据和上个窗口对比判断,封装成函数做循环,它适合ETL的增量更新。
purchase_status_counts = pivoted_purchase_status.replace("unreg",np.NaN).apply(lambda x: pd.value_counts(x))
purchase_status_counts.tail(10)
# unreg状态排除掉,它是「未来」才作为新生(新客),这么能计数呢。换算成不同分层每月的统计量。
bf_StudentID | 201807 | 201808 | 201809 | 201810 | 201811 | 201812 | 201901 | |
---|---|---|---|---|---|---|---|---|
14938 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
14940 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
14942 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
14944 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
14946 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
14337 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
new | NaN | NaN | 7.0 | 1498.0 | 31.0 | 33.0 | 11.0 | 2.0 |
love_canteen | NaN | NaN | NaN | 7.0 | 1345.0 | 1324.0 | 1350.0 | 1120.0 |
unlike_canteen | NaN | NaN | NaN | NaN | 160.0 | 124.0 | 171.0 | 433.0 |
occasionally_like_canteen | NaN | NaN | NaN | NaN | NaN | 88.0 | 48.0 | 27.0 |
# 换算成不同分层每月的统计量。
purchase_status_counts=purchase_status_counts.tail(4)
purchase_status_counts
# 明显9月开学新生人数众多,高达近一千五百人。8月也有学生回校,但仅有7人。
# 然后,1300以上同学,9-12月都超爱饭堂啊哈哈。
# 不过呢,有些同学,每个月都有一百多位同学,经常往校外跑,常常不在学校吃饭。
# 这些常常往校外跑的同学,要重点关注,是不是去干坏事了哈哈(此处开个玩笑)
bf_StudentID | 201807 | 201808 | 201809 | 201810 | 201811 | 201812 | 201901 | |
---|---|---|---|---|---|---|---|---|
new | NaN | NaN | 7.0 | 1498.0 | 31.0 | 33.0 | 11.0 | 2.0 |
love_canteen | NaN | NaN | NaN | 7.0 | 1345.0 | 1324.0 | 1350.0 | 1120.0 |
unlike_canteen | NaN | NaN | NaN | NaN | 160.0 | 124.0 | 171.0 | 433.0 |
occasionally_like_canteen | NaN | NaN | NaN | NaN | NaN | 88.0 | 48.0 | 27.0 |
# 生成面积图,比较丑。
# 因为它只是某时间段消费过的用户的后续行为,蓝色(新生)和绿色区域(不爱饭堂)都可以不看。
# 只看红色回流(偶尔爱饭堂)和橙色活跃(超爱饭堂)这两个分层,学生数比较稳定。
# 这两个分层相加,就是消费用户占比(后期没新生/新客)。
purchase_status_counts.fillna(0).T.plot.area(figsize=(12,6))
plt.show()
# 其实这里多了一组x值(学生ID),不过反正显示0,不影响分析,先不管。后面再删除学生ID列。
# 原帖子没有本节fillna(0)赋值代码及删除学生ID列,导致“各月学生偶尔爱饭堂的占比”出图趋势不科学。
# 所以在这里提前加多两行语句赋空值。然后结果正确。
purchase_status_counts = purchase_status_counts.fillna(0).drop(["bf_StudentID"],axis=1)
purchase_status_counts
201807 | 201808 | 201809 | 201810 | 201811 | 201812 | 201901 | |
---|---|---|---|---|---|---|---|
new | 0.0 | 7.0 | 1498.0 | 31.0 | 33.0 | 11.0 | 2.0 |
love_canteen | 0.0 | 0.0 | 7.0 | 1345.0 | 1324.0 | 1350.0 | 1120.0 |
unlike_canteen | 0.0 | 0.0 | 0.0 | 160.0 | 124.0 | 171.0 | 433.0 |
occasionally_like_canteen | 0.0 | 0.0 | 0.0 | 0.0 | 88.0 | 48.0 | 27.0 |
return_rata = purchase_status_counts.apply(lambda x: x/x.sum(),axis=0)
return_rata
# 原贴的apply函数中 axis参数是1(即横向求和),感觉结果不科学;
# 所以改为axis=0(纵向求和),结果接近科学。
201807 | 201808 | 201809 | 201810 | 201811 | 201812 | 201901 | |
---|---|---|---|---|---|---|---|
new | NaN | 1.0 | 0.995349 | 0.020182 | 0.021033 | 0.006962 | 0.001264 |
love_canteen | NaN | 0.0 | 0.004651 | 0.875651 | 0.843850 | 0.854430 | 0.707965 |
unlike_canteen | NaN | 0.0 | 0.000000 | 0.104167 | 0.079031 | 0.108228 | 0.273704 |
occasionally_like_canteen | NaN | 0.0 | 0.000000 | 0.000000 | 0.056087 | 0.030380 | 0.017067 |
return_rata1 = return_rata.apply(lambda x: x*100,axis=0) return_rata1.loc["occasionally_like_canteen"].plot(figsize = (12,6)) plt.xlabel('时间(月)', fontsize=18) plt.ylabel('百分比(%)', fontsize=18) plt.title('各月偶尔爱饭堂学生的占比', fontsize=18) plt.show() # 原贴分析: # 用户回流占比在5%~8%,有下降趋势。所谓回流占比,就是回流用户在总用户中的占比。 # 另外一种指标叫回流率,指上个月多少不活跃/消费用户在本月活跃/消费。 # 因为不活跃的用户总量近似不变,所以这里的回流率也近似回流占比。 # 本学生消费案例分析: # 学生偶尔爱饭堂占比从7-9月一直为0,因为学生尚未回校。在11月达到峰值,接近5%,数量也不多,不到一百人。 # 也就是说,即便是峰值11月到春节的1月,每个月只有不到100的学生,是:之前在饭堂有过“月消费30次以上”,然后又比较少在饭堂吃饭(“月消费没有30次”),最后在当月又回饭堂消费30次以上。
return_rata2 = return_rata.apply(lambda x: x*100,axis=0) return_rata2.loc["love_canteen"].plot(figsize = (12,6)) plt.xlabel('时间(月)', fontsize=18) plt.ylabel('百分比(%)', fontsize=18) plt.title('各月超爱饭堂学生的比例', fontsize=18) plt.show() # 原贴分析: # 活跃用户的下降趋势更明显,占比在3%~5%间。这里用户活跃可以看作连续消费用户,质量在一定程度上高于回流用户。 # 结合回流用户和活跃用户看,在后期的消费用户中,60%是回流用户,40%是活跃用户/连续消费用户,整体质量还好。 # 但是针对这两个分层依旧有改进的空间,可以继续细化数据。 # ------------------------- # 本次学生消费案例分析: # 超爱饭堂学生自9月开学,10月开始习惯学校并开始爱上学校饭堂之后,一直到1月春节离校返家,占比在70%-90%之间。 # 这里超爱饭堂学生可以看做连续消费用户,爱校爱饭堂程度(即原贴的"质量")在一定程度上高于偶尔爱饭堂的学生。 # 结合偶尔爱饭堂的学生和超爱饭堂的学生看,在后期的学校消费用户中,93%是超爱饭堂学生,7%是偶尔爱饭堂的学生/连续消费用户。 # 从以上两个学生用户层次看,整体质量非常好,几乎全是超爱饭堂的学生,连续消费诶。 # 但是针对“不爱饭堂的学生”这个分层,依旧有改进的空间,可以继续细化数据。
# 接下来分析用户质量,因为消费行为有明显的二八倾向,我们需要知道高质量用户(高额饭堂消费学生)为消费贡献了多少份额。
df.head(1)
DealTime | bf_StudentID | AccName | PerSex | MonDeal | avgMonDeal | transaction_times | month | |
---|---|---|---|---|---|---|---|---|
0 | 2018-07-01 | 13983 | 裘某某 | 男 | -3.7 | -3.7 | 1 | 2018-07-01 |
# user_amount = df.groupby("bf_StudentID").MonDeal.sum().sort_values().reset_index()
# user_amount["amount_cumsum"]=user_amount.MonDeal.cumsum()
# user_amount.tail()
# 原贴代码如上,为了方便一步步阅览,下面我们分开运行和分步展示。
user_amount = df.groupby("bf_StudentID").MonDeal.sum().sort_values().reset_index()
user_amount.head(3)
# 消费总金额前三。
bf_StudentID | MonDeal | |
---|---|---|
0 | 15556 | -6239.95 |
1 | 14123 | -5698.20 |
2 | 16024 | -5125.69 |
user_amount["amount_cumsum"]=user_amount.MonDeal.cumsum()
user_amount.tail()
# 原贴:
# 新建一个对象,按用户的消费金额生序。使用cumsum,它是累加函数。
# 逐行计算累计的金额,最后的3883740便是总消费额。
# 本学生消费案例:
# 所有学生累计7个月总贡献消费金额388万。
# 系统数据显示为负数,为了方便后面画图展示,下面我们将负数变正数。
bf_StudentID | MonDeal | amount_cumsum | |
---|---|---|---|
1725 | 14892 | -32.7 | -3883701.02 |
1726 | 13956 | -11.0 | -3883712.02 |
1727 | 14546 | -10.0 | -3883722.02 |
1728 | 14363 | -9.5 | -3883731.52 |
1729 | 13967 | -9.0 | -3883740.52 |
user_amount["MonDeal"]=user_amount["MonDeal"]*(-1)
user_amount["amount_cumsum"]=user_amount["amount_cumsum"]*(-1)
# 此语句备用 user_amount["MonDeal","amount_cumsum"],或可思考:上述两句相似运算赋值语句是否可以合并成一句?
user_amount.tail()
bf_StudentID | MonDeal | amount_cumsum | |
---|---|---|---|
1725 | 14892 | 32.7 | 3883701.02 |
1726 | 13956 | 11.0 | 3883712.02 |
1727 | 14546 | 10.0 | 3883722.02 |
1728 | 14363 | 9.5 | 3883731.52 |
1729 | 13967 | 9.0 | 3883740.52 |
# amount_total=user_amount.amount_cumsum.max()
# user_amount["prop"]=user_amount.apply(lambda x:x.amount_cumsum/amount_total,axis=1)
# user_amount.tail()
# 下面将上述语句逐句分拆展示,方便理解。
amount_total=user_amount.amount_cumsum.max()
amount_total
3883740.520000005
user_amount["prop"]=user_amount.apply(lambda x:x.amount_cumsum/amount_total,axis=1)
user_amount.tail()
# 转换成百分比。
bf_StudentID | MonDeal | amount_cumsum | prop | |
---|---|---|---|---|
1725 | 14892 | 32.7 | 3883701.02 | 0.999990 |
1726 | 13956 | 11.0 | 3883712.02 | 0.999993 |
1727 | 14546 | 10.0 | 3883722.02 | 0.999995 |
1728 | 14363 | 9.5 | 3883731.52 | 0.999998 |
1729 | 13967 | 9.0 | 3883740.52 | 1.000000 |
user_amount.prop.plot()
plt.show()
# 绘制趋势图,横坐标是按贡献金额大小排序而成,纵坐标则是用户累计贡献。
# 可以很清楚的看到,差点呈线性分布,反而并没有呈现出著名的“二八定律”。
# 或许,因为学校学生消费是非市场经济,所以,此处二八定律失效。
plt.figure(figsize=(12,4)) plt.subplot(121) df.groupby("bf_StudentID").MonDeal.sum().hist(bins=30) plt.xlabel("学生消费总金额",fontsize=15) plt.title("2018.7-2019.1学生消费总金额分布",fontsize=18) plt.subplot(122) df.groupby("bf_StudentID").transaction_times.sum().hist(bins=30) plt.ylabel("学生消费总次数",fontsize=15) plt.title("2018.7-2019.1学生消费总次数分布",fontsize=18) plt.show() # 从下面每段区间样本数可计算,消费1800~3200之间人数1083人,占总人数60% # [97 159 153 156 163 181 164 110]
MonDealSum = df.groupby("bf_StudentID").MonDeal.sum().reset_index()
x1 = MonDealSum["MonDeal"]
counts, bin_edges = np.histogram(x1, bins=30)
print(counts)
#只需要简单的计算每段区间的样本数,而并不想画图显示它们,那么可以直接用np.histogram()
[ 1 0 1 0 0 2 3 6 8 8 11 34 39 59 76 97 159 153
156 163 181 164 110 87 61 70 29 20 11 21]
# 前面知道前60%的学生消费金额区间在1800~3200之间,所以,
# 想对user_amount数据进行区间求和,获得1800<x<3200的x的和
user_amount.head(2)
bf_StudentID | MonDeal | amount_cumsum | prop | |
---|---|---|---|---|
0 | 15556 | 6239.95 | 6239.95 | 0.001607 |
1 | 14123 | 5698.20 | 11938.15 | 0.003074 |
# 1730位学生,前20%是346位同学,看看消费前346位同学的总消费金额。
array = user_amount["MonDeal"].astype(int)
np.sum(array[1:346], axis=0)
#接近119万,占比学生总额388万的30%,确实不符合二八原则。
1188167
# 再看消费金额从高到低排列,排位20%到80%的总消费金额占比。
# [20%:80%],[347,1384]
np.sum(array[347:1384], axis=0)
# 这60%的同学,总消费金额232万,占总金额388万的60%。。。
# 没有二八原则,更是十分贴近线性规律。
2320956
print(user_amount.loc[346].astype(int))
print("")
print(user_amount.loc[1384].astype(int))
# 第346名同学消费2913元,第1384名同学消费1572元,
# 与我们刚刚直方图得出的金额聚集区间[1800,3200]有些出入,所以接下来修正一下看看。
bf_StudentID 16123
MonDeal 2913
amount_cumsum 1197484
prop 0
Name: 346, dtype: int32
bf_StudentID 15971
MonDeal 1572
amount_cumsum 3520532
prop 0
Name: 1384, dtype: int32
print(user_amount.loc[210].astype(int))
print("")
print(user_amount.loc[1220].astype(int))
# 比较接近。
# 从第210位同学(消费3216元),到第1220位同学(消费1798元),累计总消费金额246万(324万-78万)。
# 即,1010名同学(人数占比近60%),累计总消费金额占比246/388=63%。
# 连占比最高的区间,金额加总也接近1:1的线性比例,真的说明这个消费人群,没有二八原则的基因。。。
bf_StudentID 15901
MonDeal 3216
amount_cumsum 781881
prop 0
Name: 210, dtype: int32
bf_StudentID 14873
MonDeal 1798
amount_cumsum 3242862
prop 0
Name: 1220, dtype: int32
# 既然多次证明这个消费人群没有二八原则的基因,
# 那如果继续将其当做市场消费者来分析,可能意义不大。
# 所以,本次学生消费行为分析到此结束。
# 至于更深层次的用户行为分析,待下次遇到真实市场中的消费数据时再另行分析。
# 手痒,试试Python的几个小技能。可以结束了。
# 试试np.sum()某一列求和。
array = user_amount["MonDeal"]
np.sum(array, axis=0)
3883740.52
# 试试np.sum()所有列求和。
np.sum(user_amount, axis=0).astype(int)
bf_StudentID 25968788
MonDeal 3883740
amount_cumsum -2147483648
prop 1050
dtype: int32
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。