用Python分析用户消费行为 Student Comsumption Analysis ①

# 本次案例:用户消费行为分析

# 借用阿里天池【数智教育_数据可视化创新大赛】数据源中的学生消费数据来作为本次用户消费行为分析的数据来源。
# 阿里天池数智教育数据可视化竞赛网址 https://tianchi.aliyun.com/competition/entrance/231704/introduction?spm=5176.12281949.1003.6.7b4576d8HSXTTc

# 本篇分析方法参考该帖子: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")
df=pd.read_csv("D:/2018_BigData/Python/Python_files_Notebook/theme_practice/student_consumption.csv")
df.head()
DealTimeMonDealbf_StudentIDAccNamePerSex
02018/7/1 06:32:51-4.514877张某某
12018/7/1 11:43:05-11.014917高某某
22018/7/1 14:21:42-9.214921胡某某
32018/7/1 14:33:06-6.914898牛某某
42018/7/1 15:05:45-7.014917高某某
# 数据一共有交易时间、消费金额、学生ID、学生姓名、性别五个字段。
# 观察数据,DealTime表示交易时间,但是它具体到了分秒,反而显得数据量太过冗余,颗粒度太小难以进一步分析。
# 所以接下来我们可以把交易时间统一为日期。
# 然后通过pandas数据透视表将同一日期的多次交易汇总为一次交易总额及当日交易量。
# 将交易时间转化成日期(同时去掉时分秒)
df["DealTime"]=pd.to_datetime(df.DealTime)
df.head(2)

# 没能成功去掉时分秒,再试。
DealTimeMonDealbf_StudentIDAccNamePerSex
02018-07-01 06:32:51-4.514877张某某
12018-07-01 11:43:05-11.014917高某某
# 通过提取的方式提取出来
df["DealTime"]=df["DealTime"].dt.strftime('%Y-%m-%d')
df.head(2)

# 成功。
DealTimeMonDealbf_StudentIDAccNamePerSex
02018-07-01-4.514877张某某
12018-07-01-11.014917高某某
print(df.head(2))
print(df.tail(2))
# 数据起始日期,从2018.7-2019.1
     DealTime  MonDeal  bf_StudentID AccName PerSex
0  2018-07-01     -4.5         14877     张某某      女
1  2018-07-01    -11.0         14917     高某某      男
          DealTime  MonDeal  bf_StudentID AccName PerSex
463902  2019-01-27     -3.5         16075     王某某      女
463903  2019-01-27     -7.8         16103     邱某某      女
df.describe()

# 共有46万条消费交易数据
# 平均每位学生每单消费8.4元,最大消费是404元。一位同学一天之中一次消费404,很好奇买了啥呢哈哈~
# 中位数是8.0,四分位数是10.9,说明大部分同学消费金额都不高,大部分集中在小额消费。
# 另外,同一位同学在同一天可能有多笔消费记录。所以接下来用数据透视表看看。
MonDealbf_StudentID
count463904.000000463904.000000
mean-8.37186315099.442035
std5.878407718.459919
min-404.20000013012.000000
25%-10.90000014459.000000
50%-8.00000014942.000000
75%-4.45000015784.000000
max-0.01000016162.000000
# unique函数可以查看某列总共有多少种取值:df['DealTime'].unique()
# value_count函数可以统计各不同取值的计数。
DealTime_counts=df['DealTime'].value_counts()
print("\n",DealTime_counts.shape)
print("\n")
print(DealTime_counts.head(6))
print("\n")
print(DealTime_counts.tail(6))

# 总共有161天的学生消费数据,交易量最大集中在2018.9-2018.11,刚好是开学第一学期期间。

# 如果可以按照日期排序看看时间区间,也是一个技能,暂时搁置先。
# 不过从之前的数据查看来看,时间区间是在2018年7月至2019年1月之间。
 (161,)


2018-11-26    5245
2018-09-04    5182
2018-11-20    5167
2018-11-19    5121
2018-11-29    5116
2018-09-05    5108
Name: DealTime, dtype: int64


2018-08-13    19
2018-08-19    14
2018-10-05    13
2018-11-17     5
2018-09-08     4
2019-01-30     1
Name: DealTime, dtype: int64
# 透视表方法一:groupby
df1=df.groupby(['DealTime',"bf_StudentID","AccName","PerSex"])["MonDeal"].agg(["sum","mean","count"])
df1.head(12)

# 对比了方法二pivot_table,明显感觉这次groupby方法处理速度更快啊,起码省一半时间。
summeancount
DealTimebf_StudentIDAccNamePerSex
2018-07-0113983裘某某-3.7-3.701
14018虞某某-9.5-9.501
14073刘某某-8.0-8.001
14074周某某-14.3-7.152
14097毛某某-10.0-10.001
14099李某某-10.5-10.501
14139敖某某-3.5-3.501
14140王某某-11.5-11.501
14169查某某-10.0-10.001
14187叶某某-4.5-4.501
14200王某某-3.5-3.501
14208方某某-15.5-7.752
# 透视表方法二:pivot_table
df2=pd.pivot_table(df,index=["DealTime","bf_StudentID","AccName","PerSex"],values=["MonDeal"],aggfunc=[sum,np.mean,len])
df2.head(12)

# len 表示计数,即,len=2 则代表该同学在当天消费了两笔。
summeanlen
MonDealMonDealMonDeal
DealTimebf_StudentIDAccNamePerSex
2018-07-0113983裘某某-3.7-3.701.0
14018虞某某-9.5-9.501.0
14073刘某某-8.0-8.001.0
14074周某某-14.3-7.152.0
14097毛某某-10.0-10.001.0
14099李某某-10.5-10.501.0
14139敖某某-3.5-3.501.0
14140王某某-11.5-11.501.0
14169查某某-10.0-10.001.0
14187叶某某-4.5-4.501.0
14200王某某-3.5-3.501.0
14208方某某-15.5-7.752.0
df1.describe()

# 原46万条消费交易数据,汇总成16万条,说明所有同学人均每天消费接近3单,贴近一日早午晚三餐消费。
# 人均日消费总额23.6元,人均每次交易金额8.7元,说明同学们平均每顿饭消费近9元,每天消费近24元。——由此得该校同学的人均消费水平。

# 一般而言,消费类的数据分布,都是长尾状态。大部分用户都是小额,然而小部分用户贡献了收入的大头,俗称二八原则。
summeancount
count164422.000000164422.000000164422.000000
mean-23.620565-8.7022362.821423
std14.8325583.8762271.487303
min-439.250000-170.0000001.000000
25%-30.150000-10.0000002.000000
50%-21.000000-8.0000003.000000
75%-13.200000-6.5500004.000000
max-0.090000-0.09000014.000000
df1.info()
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 164422 entries, (2018-07-01, 13983, 裘某某, 男) to (2019-01-30, 15472, 陈某某, 男)
Data columns (total 3 columns):
sum      164422 non-null float64
mean     164422 non-null float64
count    164422 non-null int64
dtypes: float64(2), int64(1)
memory usage: 4.9+ MB
df2.info()
# 除了计数len的格式float与df1的count格式int不一样之外,其他的df1和df2一致,说明两种透视方式接近相同。
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 164422 entries, (2018-07-01, 13983, 裘某某, 男) to (2019-01-30, 15472, 陈某某, 男)
Data columns (total 3 columns):
(sum, MonDeal)     164422 non-null float64
(mean, MonDeal)    164422 non-null float64
(len, MonDeal)     164422 non-null float64
dtypes: float64(3)
memory usage: 4.9+ MB
df.info()
# 对比透视表df1,透视表就是轻量一些,占用空间没那么大。。只需4.9M。。
# 没有空值,很干净的数据。
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 463904 entries, 0 to 463903
Data columns (total 5 columns):
DealTime        463904 non-null object
MonDeal         463904 non-null float64
bf_StudentID    463904 non-null int64
AccName         463904 non-null object
PerSex          463904 non-null object
dtypes: float64(1), int64(1), object(3)
memory usage: 17.7+ MB
# 接下来,用astype将时间格式进行转换,例如[M]转换成月份。我们将月份作为消费行为的主要事件窗口,选择哪种时间窗口取决于消费频率。
df["month"]=df.DealTime.values.astype("datetime64[M]")
print(df.head(2))
print(df.tail(2))

# 下面是转化后的格式。月份依旧显示日,只是变为月初的形式。
     DealTime  MonDeal  bf_StudentID AccName PerSex      month
0  2018-07-01     -4.5         14877     张某某      女 2018-07-01
1  2018-07-01    -11.0         14917     高某某      男 2018-07-01
          DealTime  MonDeal  bf_StudentID AccName PerSex      month
463902  2019-01-27     -3.5         16075     王某某      女 2019-01-01
463903  2019-01-27     -7.8         16103     邱某某      女 2019-01-01
# pandas中有专门的时间序列方法tseries,它可以用来进行时间偏移,也是处理时间类型的好方法。
# 时间格式也能作为索引,在金融、财务等领域使用较多,这里不过多赘述。

# df.date - pd.tseries.offsets.MonthBegin(1)
# 上面的消费行为颗粒度是每笔订单以及透视之后的每天每位同学,下面我们转换成每位同学看一下。

student_group=df.groupby(["bf_StudentID","AccName","PerSex"]).agg(["sum","mean","count"])
# student_group.head()
print(student_group.head(3))
print(student_group.tail(3))
                            MonDeal                
                                sum      mean count
bf_StudentID AccName PerSex                        
13012        张某某     女       -381.1 -7.472549    51
13564        吴某某     男      -3272.8 -8.590026   381
13599        曹某某     男      -1686.8 -9.319337   181
                             MonDeal                
                                 sum      mean count
bf_StudentID AccName PerSex                         
16160        周某某     女      -2135.25 -9.490000   225
16161        韩某某     女      -1519.60 -6.970642   218
16162        陈某某     男      -1754.65 -9.588251   183
type(student_group)
pandas.core.frame.DataFrame
# 尝试(可忽略)
# student_group.index
# # 索引 加 names=['bf_StudentID', 'AccName', 'PerSex'])
student_group.columns
MultiIndex(levels=[['MonDeal'], ['sum', 'mean', 'count']],
           labels=[[0, 0, 0], [0, 1, 2]])
df1.columns
Index(['sum', 'mean', 'count'], dtype='object')
df2.columns
MultiIndex(levels=[['sum', 'mean', 'len'], ['MonDeal']],
           labels=[[0, 1, 2], [0, 0, 0]])
df.columns
Index(['DealTime', 'MonDeal', 'bf_StudentID', 'AccName', 'PerSex', 'month'], dtype='object')
# 用reset_index()将student_group转化成dataframe后进行排序
student_group0=student_group.reset_index() 
print(student_group0.head())
print ('\n'*2)
print(student_group0.columns)
# 奇怪,为什么student_group的reset_index()转化结果,与下面df1转化成df3的结果不一样呢?格式还没变成DataFrame,columns还是MultiIndex。4.14

# 对比了一下:
# df1=df.groupby(['DealTime',"bf_StudentID","AccName","PerSex"])["MonDeal"].agg(["sum","mean","count"])
# student_group=df.groupby(["bf_StudentID","AccName","PerSex"]).agg(["sum","mean","count"])
# 以上两者,函数agg之前的赋值有差异,df1的agg前加了["MonDeal"],而student_group没有加。
# 这个问题,我们这里暂不解决,到后面 df3 统计分析的时候再一并解决。4.14
  bf_StudentID AccName PerSex  MonDeal                 
                                   sum       mean count
0        13012     张某某      女  -381.10  -7.472549    51
1        13564     吴某某      男 -3272.80  -8.590026   381
2        13599     曹某某      男 -1686.80  -9.319337   181
3        13685     毛某某      男 -1854.93 -10.847544   171
4        13947     李某某      女 -3009.40  -7.982493   377



MultiIndex(levels=[['MonDeal', 'PerSex', 'AccName', 'bf_StudentID'], ['sum', 'mean', 'count', '']],
           labels=[[3, 2, 1, 0, 0, 0], [3, 3, 3, 0, 1, 2]])
# 要先将按日统计的透视表 df1 或 df2 转化成数据表格,才能用于接下来的操作。
# 从项目管理的角度,这一步是先导活动,必须先执行,不然后面的无法进行。咳咳,关键点所在。
# 明日再战。记于2019.4.12临近凌晨。
# 终于找到。reset_index 函数。
# Converting a Pandas GroupBy object to DataFrame-Groupby对象转换为DataFrame  网址 https://blog.csdn.net/tanzuozhev/article/details/78011198

df3=df1.reset_index() 
print(df3.head(3))
print(df3.tail(3))

# 总共16万条数据。
     DealTime  bf_StudentID AccName PerSex  sum  mean  count
0  2018-07-01         13983     裘某某      男 -3.7  -3.7      1
1  2018-07-01         14018     虞某某      男 -9.5  -9.5      1
2  2018-07-01         14073     刘某某      男 -8.0  -8.0      1
          DealTime  bf_StudentID AccName PerSex  sum  mean  count
164419  2019-01-27         16149     李某某      女 -4.0  -4.0      1
164420  2019-01-27         16150     陈某某      男 -4.5  -4.5      1
164421  2019-01-30         15472     陈某某      男 -5.8  -5.8      1
df3.columns   
# 查看reset_index转化后的列,发现已正常。成功。
Index(['DealTime', 'bf_StudentID', 'AccName', 'PerSex', 'sum', 'mean',
       'count'],
      dtype='object')
# 为了更直观一些,改一下列名
df3.rename(columns={'sum':'MonDeal', 'mean':'avgMonDeal', 'count':'transaction_times'}, inplace = True)
df3.head(5)
DealTimebf_StudentIDAccNamePerSexMonDealavgMonDealtransaction_times
02018-07-0113983裘某某-3.7-3.701
12018-07-0114018虞某某-9.5-9.501
22018-07-0114073刘某某-8.0-8.001
32018-07-0114074周某某-14.3-7.152
42018-07-0114097毛某某-10.0-10.001
# 接下来,按照前文方法,用astype将时间格式进行转换,例如[M]转换成月份。我们将月份作为消费行为的主要事件窗口,选择哪种时间窗口取决于消费频率。
df3["month"]=df3.DealTime.values.astype("datetime64[M]")
df3.tail(2)

# 下面是转化后的格式。月份依旧显示日,只是变为月初的形式。
DealTimebf_StudentIDAccNamePerSexMonDealavgMonDealtransaction_timesmonth
1644202019-01-2716150陈某某-4.5-4.512019-01-01
1644212019-01-3015472陈某某-5.8-5.812019-01-01
# pandas中有专门的时间序列方法tseries,它可以用来进行时间偏移,也是处理时间类型的好方法。
# 时间格式也能作为索引,在金融、财务等领域使用较多,这里不过多赘述。

# df.date - pd.tseries.offsets.MonthBegin(1)
# 上面的消费行为颗粒度是每天订单的各位同学,下面我们转换成每位同学看一下。

student_group1=df3.groupby(["bf_StudentID","AccName","PerSex"])["MonDeal"].agg(["sum","mean","count"])
print(student_group1.head(3))
print ('\n'*1)
print(student_group1.columns)
                                sum       mean  count
bf_StudentID AccName PerSex                          
13012        张某某     女       -381.1 -11.909375     32
13564        吴某某     男      -3272.8 -26.393548    124
13599        曹某某     男      -1686.8 -17.755789     95


Index(['sum', 'mean', 'count'], dtype='object')
student_group1.columns   # 终于是index而不是MultiIndex了,看来agg函数前加上["MonDeal"]是有效的。
Index(['sum', 'mean', 'count'], dtype='object')
student_group2=student_group1.reset_index() 
print(student_group2.head(3))
print ('\n'*1)
print(student_group2.columns)
print ('\n'*1)
print(student_group2.shape)
# 搞定,成功转化成dataframe,也知道了总共有1730位同学。
# 接下来可以排序看看各位同学的消费情况了。
   bf_StudentID AccName PerSex     sum       mean  count
0         13012     张某某      女  -381.1 -11.909375     32
1         13564     吴某某      男 -3272.8 -26.393548    124
2         13599     曹某某      男 -1686.8 -17.755789     95


Index(['bf_StudentID', 'AccName', 'PerSex', 'sum', 'mean', 'count'], dtype='object')


(1730, 6)
student_group2.describe()

# 从学生(用户)角度看,1730位同学中,每位同学平均消费95次;最多的同学消费了146次,属于常年在校的学生。
# 学生的平均日消费金额23元(类比客户日均消费/客单价),平均总消费金额2245元(类比用户贡献收入),结合分位数看,平均值接近50分位,说明同学消费分布比较均匀。
bf_StudentIDsummeancount
count1730.0000001730.0000001730.0000001730.000000
mean15010.860116-2244.936717-23.29364795.041618
std710.216323860.3717307.49514318.722765
min13012.000000-6239.950000-100.0000001.000000
25%14386.250000-2790.475000-27.54571091.000000
50%14834.500000-2216.500000-22.79365098.000000
75%15721.750000-1698.650000-18.324731105.000000
max16162.000000-9.000000-4.950000146.000000
student_group3=student_group2.sort_values(by="sum",ascending=True)
print(student_group3.head(5))
print ('\n'*1)
print(student_group3.tail(5))

# 各位同学之中,消费最高的前三位是陶某某、汤某某、余某某,半年总消费总额分别是6240、5698、5125元,他们消费天数都是100天左右。九月开学到1月春节放假,刚好三个多月在校。
# 另外也有一些同学,平时很少在学校吃饭消费,半年来只有几次,可能是大四即将毕业的同学,外出外地实习或者秋季招聘求职去了。
# 如果要看学生在校时长的分布,可近似于观察学生在校消费天数的分布。下面我们用图。
      bf_StudentID AccName PerSex      sum       mean  count
1132         15556     陶某某      男 -6239.95 -66.382447     94
176          14123     汤某某      男 -5698.20 -53.756604    106
1593         16024     余某某      男 -5125.69 -50.251863    102
1607         16038     王某某      男 -5080.40 -49.807843    102
1113         15537     潘某某      男 -4798.80 -47.047059    102


     bf_StudentID AccName PerSex   sum   mean  count
922         14892     林某某      男 -32.7 -16.35      2
12          13956     陆某某      男 -11.0 -11.00      1
581         14546     俞某某      男 -10.0 -10.00      1
412         14363     钱某某      女  -9.5  -9.50      1
22          13967     权某某      女  -9.0  -9.00      1
import seaborn as sns
# import matplotlib.pyplot as plt   # plt模块前面已导入,这里就不重复导入了。

import warnings
warnings.filterwarnings("ignore")

# plt.rcParams['font.sans-serif']=['SimHei']   # 用来正常显示中文标签
# plt.rcParams['axes.unicode_minus']=False   # 用来正常显示负号

# seaborn中文乱码解决方案
from matplotlib.font_manager import FontProperties
myfont=FontProperties(fname=r'C:\Windows\Fonts\simhei.ttf',size=14)
sns.set(font=myfont.get_name())
x=student_group3["count"]
sns.distplot(x)

# 可见大部分同学在校时间是在70-120天之间。
# 少部分同学常年不在校,可能是海外交换学习或者异地社会实践或者已经开始异地实习工作。
# 另外一少部分同学在校天数达到了140天以上,除了秋季学期的四个月120天,另外还有二三十天也待在学校,可能是假期复习考研考证,或者假期在学校所在地实习。

# 这个直方图虽然分布很直观,但是我们都不知道每个分布区间的具体数量,所以接下来我们得润色一下。

在这里插入图片描述

png

# 润色的同时,看每位同学的消费总额sum、日均消费mean、和消费天数(近似看做在校天数)。

plt.hist(x, bins=15, color=sns.desaturate("indianred", .9), alpha=.8)
plt.xlabel("在校天数(消费天数)",fontproperties="SimHei",fontsize=15)
plt.ylabel("人数",fontsize=15)
plt.title("2018.7-2019.1学生在校时长分布",fontsize=18)
plt.show() 

# 前面我们知道总共有1730位同学。
# 根据该直方图,大部分同学在校时间是在70-125天之间,其人数高达约1590人,占比高达90%以上。

在这里插入图片描述
png

x1=student_group3["sum"]
x2=student_group3["mean"]
kwargs = dict(histtype='stepfilled',alpha=0.8, bins=15)

plt.hist(x1, **kwargs)
plt.xlabel("各学生总消费金额",fontproperties="SimHei",fontsize=15)
plt.ylabel("人数",fontsize=15)
plt.title("2018.7-2019.1学生总消费情况",fontsize=18)
plt.show() 

# 总消费1800-2900元之间高达950人,占到了总人数的50%以上。所以可将这个区间代表该校学生的整体消费水平。

在这里插入图片描述
png

plt.hist(x2,bins=18)
plt.xlabel("各学生日均消费金额",fontproperties="SimHei",fontsize=15)
plt.ylabel("人数",fontsize=15)
plt.title("2018.7-2019.1学生日均消费情况",fontsize=18)
plt.show() 

# 日均消费15-32之间,人数高达1300人,所以该区间,亦可代表整体学生的日消费情况。

在这里插入图片描述
png

#如果只需要简单的计算每段区间的样本数,而并不想画图显示它们,那么可以直接用np.histogram()
counts, bin_edges = np.histogram(x2, bins=18)
print(counts)
[  1   0   0   0   0   0   2   0   2   5  12  51 131 339 508 453 192  34]
# 好像离题已久,我们回到高手的参考帖子上,继续跟着帖子分析一波先。
# 前面按日维度分析了一波,接下来按月你的维度分析。
df3.groupby("month").MonDeal.sum().plot()
plt.show()

# 按月统计每个月学生总消费金额。
# 从图中可以看到,7/8月份消费较低,均在11万元以下,但从9月开始直到12月,消费骤增至68万元以上,直到1月才稍微下降到61万左右。
# 由此我们推测,七八月份暑假期间学生留校人数较少,消费也少,而9月份开学,学生家长送学生回校整体消费增高(也比10-12月高),后1月稍微降低,或因月底接近春节假期,学生离校返家所导致。

在这里插入图片描述
png

df3.groupby("month").transaction_times.sum().plot()
plt.show()

# 消费次数和消费金额趋势一致,七、八月份消费次数约只有一万不到,而9月飙升至10万以上,一直到12月都有8万次以上,然后1月因春节期至而回落至7万。
# 趋势变化原因,如上所述,受暑假、开学新学期、春节寒假等影响。
# 只是奇怪,为啥上面那个图,越靠近x轴的y轴数值(消费总金额)越大反而远离x轴的消费总金额越小呢。。。留个悬念先,反正不影响分析结果。

在这里插入图片描述
png

# 绘制每位同学的消费次数及消费金额散点图。
df3.groupby("bf_StudentID").sum().plot.scatter(x="MonDeal",y="transaction_times")
plt.show()

# 好神奇的X轴。。。先不管。趋势已经有了。
# 从图中观察,每位同学消费总金额和消费次数呈规律性,每次消费8元左右。
# 消费的极值较少,超出5000元的就四个。对消费整体波动没什么影响,数据整体健康。
# 消费能力特别强的同学有,但是数量不多。为了更好的观察,用直方图。

在这里插入图片描述
png

plt.figure(figsize=(12,4))

plt.subplot(121)
df3.groupby("bf_StudentID").MonDeal.sum().hist(bins=30)
plt.xlabel("学生消费总金额",fontsize=15)
plt.title("2018.7-2019.1学生消费总金额分布",fontsize=18)

plt.subplot(122)
df3.groupby("bf_StudentID").transaction_times.sum().hist(bins=30)
plt.ylabel("学生消费总次数",fontsize=15)
plt.title("2018.7-2019.1学生消费总次数分布",fontsize=18)

plt.show()

# plt.subplot用于绘制子图,子图用数字参数表示。
# 121表示分成1*2个图片区域,占用第一个,即第一行第一列,122表示占用第二个。
# figure是尺寸函数,为了容纳两张子图,宽设置的大一点即可。

# 至于两张图的内容,其实前文已述。这里用了另一种方式,增加了figure分区对比。
# 从直方图看,大部分学生的消费能力确实不高,高消费学生(暂定4千元以上)在图中只有极少数,加起来总共不到100人。
# 假设学生在学校消费几乎全为饮食消费,那绝大部分同学的半年饮食花费均在一千元以上、三四千元以下,大部分同学集中在2200-3200之间。
# 由此推测,该校一个学生一个学期的饮食费用约为三千元——即,读书的饮食类生活成本三千元/学期。可做小孩读书成本参考。

在这里插入图片描述
png

# 观察完学生消费的金额和次数,接下来看消费的时间节点。

df3.groupby("bf_StudentID").month.min().value_counts()

# 用groupby函数将学生分组,并且求月份的最小值,最小值即学生消费行为中的第一次消费时间。
# ok,结果出来了,除了11位同学,其他1719位同学的第一次消费都集中在前三个月。
# 我们可以这样近似认为,天津这家学校的学生消费数据,7/8月份就开始消费的学生是老生,共有1488名,9月份开始消费的学生是新生,共有231名。
2018-07-01    918
2018-08-01    570
2018-09-01    231
2018-12-01      4
2018-11-01      4
2018-10-01      3
Name: month, dtype: int64
df3.groupby("bf_StudentID").month.max().value_counts()

# 观察用户的最后一次消费时间。
# 绝大部分数据依然集中在1月份,这正好是春节假期开始的月份,学生几乎都在1月份离校返家,再次印证前面的推断。
2019-01-01    1692
2018-12-01      13
2018-10-01       9
2018-11-01       8
2018-09-01       5
2018-07-01       2
2018-08-01       1
Name: month, dtype: int64
# 由于本次案例所使用数据的特殊性和局限性:主体作为学校学生,其消费属于硬性消费,难以存在“复购”和“回购”等说法。
# 但是为了继续把分析用户消费行为的方法实践下去,我们暂且把学生在校消费也当做普通用户消费好了。

# 接下来分析结果可能有些牵强,但是我们的重点在于:Python在用户消费行为分析上的实操。
# 所以建议读者选择性浏览。
# 首先将用户消费数据进行数据透视。

pivoted_counts=df3.pivot_table(index="bf_StudentID",columns="month",values="transaction_times",aggfunc="sum").fillna(0)
columns_month=df3.month.sort_values().astype("str").unique()
pivoted_counts.columns=columns_month
pivoted_counts.head()
2018-07-012018-08-012018-09-012018-10-012018-11-012018-12-012019-01-01
bf_StudentID
130120.00.010.010.015.07.09.0
1356417.025.082.063.069.075.050.0
135998.010.039.033.034.031.026.0
136858.012.028.033.034.039.017.0
139479.022.081.064.066.072.063.0
# 在pandas中,数据透视有专门的函数pivot_table,功能非常强大。

# pivot_table参数中,index是设置数据透视后的索引,column是设置数据透视后的列,简而言之,index是你想要的行,column是想要的列。
# 案例中,我希望统计每个学生在每月的消费次数,所以bf_StudentID是index,month是column。

# values是将哪个值进行计算,aggfunc是用哪种方法。
# 于是这里用values=transaction_times和aggfunc=sum,统计里transaction_times出现的日消费次数之和,即每月总共消费多少次。

# 使用数据透视表,需要明确获得什么结果。有些学生在某月没有进行过消费,会用NaN表示,这里用fillna填充。

# 生成的数据透视,月份是1997-01-01 00:00:00表示,比较丑。上面通过astype函数将其优化成标准格式。
# 首先原参考帖子求复购率,复购率的定义是在某时间窗口内消费两次及以上的用户在总消费用户中占比。
# 这里的时间窗口是月,如果一个用户在同一天消费了两次,这里也将他算作复购用户。

# 回到本案例,因为是学校学生消费数据,我们将“复购率/爱饭堂率”定义为:在某时间窗口内消费60次及以上的学生(爱饭堂)在总消费学生中占比。
# 如果一个学生在一个月内消费了60次,那我们将这位同学算作“复购用户/爱饭堂用户”。
# 重新定义复购率的数值分段说明:假设学生开学期间一直在学校,每天在学校就餐(午+晚),那月消费次数应该有60次。
# 将数据转换一下,消费60次及以上(爱饭堂)记为1,消费60次以下记为0,没有消费记为NaN。

pivoted_counts_transf=pivoted_counts.applymap(lambda x: 1 if x>59 else np.NaN if x==0 else 0)
pivoted_counts_transf.head()

# applymap针对DataFrame里的所有数据。
# 用lambda进行判断,因为这里涉及了多个结果,所以要两个if else,记住,lambda没有elif的用法。
2018-07-012018-08-012018-09-012018-10-012018-11-012018-12-012019-01-01
bf_StudentID
13012NaNNaN0.00.00.00.00.0
135640.00.01.01.01.01.00.0
135990.00.00.00.00.00.00.0
136850.00.00.00.00.00.00.0
139470.00.01.01.01.01.01.0
# 用sum和count相除即可计算出复购率。
# 因为这两个函数都会忽略NaN,而NaN是没有消费的用户;
# count不论0还是1都会统计,所以是总的消费用户数,而sum求和计算了60次以上的消费用户。
# 这里用了比较巧妙的替代法计算复购率,SQL中也可以用。

(pivoted_counts_transf.sum()/pivoted_counts_transf.count()).plot(figsize=(10,4))
plt.xlabel('时间(月)', fontsize=18) 
plt.ylabel('百分比(%)', fontsize=18) 
plt.title('各月学生爱饭堂率', fontsize=18)

plt.show()

# 发现横坐标没有标签和说明,即,从图看不出各数值点分别对应几月份(当然,猜是可以猜的,从左到右依次是7月到1月。)
# 可能是透视表的列标签(月份)失效?这个先不管。

# 这个趋势有点有趣。
# 9月开学,接近一半的学生“爱饭堂”,几乎天天在饭堂吃饭,当然也有可能是开学那几天学生和家长一起在饭堂就餐,贡献了多几次消费
# 然后,10月学生对饭堂的热爱程度(吃饭次数)迅速下滑,推测是学生熟悉环境之后,约伴往学校周边找吃的去,或者社团活动在外聚餐等。
# 12月回升到四成以上,推测是临近期末,学生久居学校,全力复习,因此在学校饭堂吃饭的次数多了。
# 1月断崖式回落,如前文所说,1月底春节假期开始学生离校返家,自然在学校消费次数降低。

在这里插入图片描述
png

# 接下来计算回购率。(原贴)
# 回购率是某一个时间窗口内消费的用户,在下一个时间窗口仍旧消费的占比。
# 我1月消费用户1000,他们中有300个2月依然消费,回购率是30%。

# 回到学生消费例子。
# 9月1500人在学校有消费记录(假设是饭堂吃饭),他们中如果有1200人10月份依然在饭堂吃饭,回购率是80%。

# 回购率的计算比较难,因为它设计了横向跨时间窗口的对比。
pivoted_MonDeal=df3.pivot_table(
    index="bf_StudentID",columns="month",values="MonDeal",aggfunc="mean").fillna(0)
columns_month=df3.month.sort_values().astype("str").unique()
pivoted_MonDeal.columns=columns_month
pivoted_MonDeal.head()

# 将消费金额进行数据透视,这里作为练习,使用了平均值。
2018-07-012018-08-012018-09-012018-10-012018-11-012018-12-012019-01-01
bf_StudentID
130120.0000000.000000-12.720000-11.483333-11.844444-11.016667-12.650000
13564-30.080000-20.180000-27.912000-28.309524-27.842500-27.668750-21.442105
13599-26.450000-13.333333-20.478947-16.734375-16.116667-16.238235-19.833333
13685-26.375000-15.122222-16.628235-20.656250-20.747059-23.277778-28.556250
13947-19.566667-15.550000-28.981818-22.380000-29.767500-24.147826-31.069444
pivoted_purchase=pivoted_MonDeal.applymap(lambda x: 0 if x==0 else 1)
pivoted_purchase.head()

# 再次用applymap+lambda转换数据,只要有过消费,记为1,反之为0。
2018-07-012018-08-012018-09-012018-10-012018-11-012018-12-012019-01-01
bf_StudentID
130120011111
135641111111
135991111111
136851111111
139471111111
# 新建一个判断函数。
# data是输入的数据,即学生在7个月内是否消费的记录;
# status是空列表,后续用来保存学生是否回购的字段。

# 因为有7个月,所以每个月都要进行一次判断,需要用到循环。
# if的主要逻辑是,如果学生本月进行过消费,且下月消费过,记为1,没有消费过是0。
# 本月若没有进行过消费,为NaN,后续的统计中进行排除。

def purchase_return(data):
    status = []
    for i in range(6):
        if data[i] == 1:
            if data[i+1] == 1:
                status.append(1)
            if data[i+1] == 0:
                status.append(0)
        else:
            status.append(np.NaN)
    status.append(np.NaN)
    return status
# 用apply函数应用在所有行上,获得想要的结果。

pivoted_purchase_return = pivoted_purchase.apply(purchase_return,axis=1)
pivoted_purchase_return.head(5)
bf_StudentID
13012    [nan, nan, 1, 1, 1, 1, nan]
13564        [1, 1, 1, 1, 1, 1, nan]
13599        [1, 1, 1, 1, 1, 1, nan]
13685        [1, 1, 1, 1, 1, 1, nan]
13947        [1, 1, 1, 1, 1, 1, nan]
dtype: object
# 最后的计算和复购率大同小异,用count和sum求出。

# (pivoted_purchase_return.sum()/pivoted_purchase_return.count()).plot(figsize=(12,4))
# plt.show()

#  先是报错:'numpy.ndarray' object has no attribute 'plot'
# 解决方法:ndarray.tolist()
# pivoted_purchase_return = pivoted_purchase_return.tolist()
# (pivoted_purchase_return.sum()/pivoted_purchase_return.count()).plot(figsize=(12,4))
# plt.show()

#刚刚报ndarray的错,现在又报list的错。。。 'list' object has no attribute 'sum'
# 继续尝试解决:flatten()函数
# pivoted_purchase_return = pivoted_purchase_return.flatten()

# 这次报错变成 Series 了。。'Series' object has no attribute 'flatten'
pivoted_purchase_return_mean = (pivoted_purchase_return.sum()/pivoted_purchase_return.count())
pivoted_purchase_return_mean.plot(figsize=(12,4))

plt.xlabel('时间(月)', fontsize=18) 
plt.ylabel('百分比(%)', fontsize=18) 
plt.title('各月学生复购率', fontsize=18)

plt.show()

# 和前面学生爱饭堂率(复购率)几乎一样的处理方法,除了applymap换成apply有差别。然后就画不了图了。
# 原因应该是applymap和apply两个函数出来出来的数据,格式不一致,可能是ndarray和dataframe的区别,可能是dataframe和series的区别,暂时不得而知。
# 容后再研究。
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-163-45678b38adb8> in <module>()
      1 pivoted_purchase_return_mean = (pivoted_purchase_return.sum()/pivoted_purchase_return.count())
----> 2 pivoted_purchase_return_mean.plot()
      3 
      4 plt.xlabel('时间(月)', fontsize=18)
      5 plt.ylabel('百分比(%)', fontsize=18)


AttributeError: 'numpy.ndarray' object has no attribute 'plot'

下篇续:用Python分析用户消费行为 Student Comsumption Analysis ② https://blog.csdn.net/weixin_44216391/article/details/89329804

  • 3
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值