在阿里天池下载的淘宝天猫婴儿用品销量数据集,数据的背景介绍可参见数据出处或者这里。
在用Python对该数据进行初步业务指标分析后,我遇到了一些问题,这里做下总结。
本文主要内容,包含以下两方面:
1 . 在数据清洗过程,遇到了什么问题?有什么需要注意的?
2 . 模型构建过程,即分析了哪些业务指标,需要注意什么问题?
数据清洗
选择子集
对于要分析的业务指标,不需要用到交易记录数据中的auction_id
,property
等字段信息。
选择子集的方式,有不同方法:可以将不需要的字段删除,也可以选择只保留要使用的字段。
# 方式一,删除不用的
sub_trade_data = trade_data.drop(columns=['auction_id','property'])
# 方式二,保留需要的
sub_trade_data = trade_data.loc[:, ['user_id','cat_id','cat1','buy_mount','day']]
规范列名-重命名
原始数据中所有字段名为英文缩写,为了在分析时便于查看和获取指标计算要使用的字段,将所有列名改为中文形式。原始数据集包含两个数据文件,没合并,一开始我以为需要为每个数据集单独定义一个列名字典来进行列名修改,但其实只需要定义一个,直接应用在两个数据集上。
newColName = {'user_id':'用户编号','cat_id':'商品编号','cat1':'商品种类','buy_mount':'销售数量','day':'销售时间','birthday':'出生日期','gender':'性别'}
sub_trade_data.rename(columns=newColName,inplace=True)
baby_data.rename(columns=newColName,inplace=True)
缺失值查看
先查看两个数据集是否存在缺失值,若存在,需要进行处理。对缺失值的处理,要么删除,要么用其它统计值填补。这两个数据集比较好,未存在任何缺失值。
查看数据集是否存在缺失值的方式可以使用isnull()
函数,后面加any()
查看列是否存在缺失,若存在会返回True
;也可以使用info()函数
,这个函数会显示数据有哪些列,每列有多少项,是否有缺失等信息。
# 方式一,使用isnull()
sub_trade_data.isnull().any()
#方式二,使用info()
sub_trade_data.info()
数据类型转换
使用.dtypes
查看数据字段类型时,发现都是int64
类型,这对于销售时间
和出生日期
两个字段很明显是不合理的。需要将这两个字段转换为日期类型
。
下面说下我在这个过程中经历的几个阶段:
- 第一阶段
一开始,我直接使用Pandas的to_datetime()
函数将字段进行格式转换,程序不报错,如下:
sub_trade_data['销售时间'] = pd.to_datetime(sub_trade_data['销售时间'],format='%Y-%m-%d')
得到结果:
图中展示的都是同一个时间,有点奇怪。第一时间查看原始表格中销售时间字段,发现并不是如图中那样,原始数据表中的销售时间是8位数字表示的时间:
那么就是时间函数解析的问题,网上查了一下,发现如果直接将一串数字作为参数传入to_datetime()
函数,它会将这串数字当作unix时间戳
,并把unix时间戳默认转为GMT标准时间。而北京时间比GMT标准时间要加8小时,所以如果是unix时间戳形式的时间,转换时还需要自己处理时区问题,具体解决方案可参考这里。
回到这里的问题,看了原始表格的时间发现它并不是unix时间戳形式,所以转换结果不对。
- 第二阶段
重新开始排查问题,查了Pandas的to_datetime文档,第一个参数的类型可以是整型,字符串型,也可以是日期型。既然直接传入数字转换结果不对,那么就想着传入正确格式(想要转换成的日期格式是年-月-日这种)的字符串试试。
所以就有了以下步骤:
1 . 先将原始表格里日期字段的 数字类型转成字符串类型
sub_trade_data['销售时间'] = sub_trade_data['销售时间'].astype('str')
2 . 定义函数:将字符串类型的日期转成年-月-日
格式
def dateStrFormat(timeCol):
'''
输入:timeCol销售时间列,Series类型
输出:转成'y-m-d'字符串形式,返回也是Series类型
'''
timeList = []
for time in timeCol:
year = time[:4]
month = time[4:6]
day = time[6:]
timeFormat = str(year) + '-' + str(month) + '-' + str(day)
timeList.append(timeFormat)
timeColSer = pd.Series(timeList)
return timeColSer
# 获取销售时间字段
timeCol_sale = sub_trade_data['销售时间']
# 调用函数将数字日期转成字符串日期
dateSer_sale = dateStrFormat(timeCol_sale)
# 修改销售时间这一列的值
sub_trade_data['销售时间'] = dateSer_sale
3 . 将格式调整后的字符串传入to_datetime
函数,转成时间类型
sub_trade_data['销售时间'] = pd.to_datetime(sub_trade_data['销售时间'])
最终得到了正确结果:
- 第三阶段
调用函数调整格式后再作为参数这种做法,效果挺好的,但是缺点就是太繁琐。其实上面第二步中转换字符串格式的步骤,pandas的to_datetime()
会自动完成的,后来我将转换成string类型的那串数字直接传入函数,发现也能得到正确的结果>.< 。大道至简,所以还是不用费时间写一个格式转换函数了。
去掉上面的第二个步骤,得到同样的结果:
sub_trade_data['销售时间'] = sub_trade_data['销售时间'].astype('str')
sub_trade_data['销售时间'] = pd.to_datetime(sub_trade_data['销售时间'], format='%Y-%m-%d') #第二个参数可选
数据排序
这个步骤,没什么问题,按照时间字段对数据集排序就好。注意排序后,要重新设置行索引值。
sub_trade_data = sub_trade_data.sort_values(by='销售时间',ascending=True)
sub_trade_data = sub_trade_data.reset_index(drop=True)
查看/处理异常值
对于交易数据集,使用describe()
函数查看统计信息,发现销售数量
字段的值都为正数,没问题。
对于婴儿信息数据集,需要查看婴儿性别
字段的值是否正常,发现除了0(男)
,1(女)
两个值外,还存在2
这个值, 应该是用户不愿意透露婴儿的性别信息,这部分值只有26个 ,故删除这部分数据,并将0和1分别替换成男和女。
baby_data['性别'].value_counts() #查看性别字段的各个值及对应数量
baby_data = baby_data[baby_data['性别']<2] #保留0,1两个值
#将0和1分别替换成男和女
baby_data['性别'] = baby_data['性别'].map({0:'男',1:'女'})
#将原来的int64类型转成string字符串类型
baby_data['性别'] = baby_data['性别'].astype('str')
模型构建
总共分析了6个业务指标,包括:
1 . 月均购买次数
2 . 最畅销的10个商品
3 . 每类商品的月均销量
4 . 每类商品的月销量趋势
5 . 每类商品的销量峰值是在哪个月
6 . 每类商品的用户画像(年龄段,性别)
业务指标1:月均购买次数 = 总消费次数/月份数
第一个需要注意的是:总消费次数的计算要排除同一天内,同一个用户的重复消费。
'''
根据字段(用户编号,销售时间),如果两列值同时相同,只保留1条,将重复的数据删除
'''
kpi1_df = sub_trade_data.drop_duplicates(subset= ['用户编号','销售时间'])
total_times = kpi1_df.shape[0]
第二个需要注意的是:计算月份数,通过(销售时间的最大值-销售时间的最小值).days
得到天数,再用天数整除30得到月份数。
销售时间的最值可以在排序数据后获取第一个和最后一个销售时间,也可以对销售时间列应用max()
和min()
函数得到。
#方法一,先排序再获取最值
# startTime = kpi1_df.loc[0,'销售时间']
# endTime = kpi1_df.loc[total_times-1,'销售时间']
# 方法二,使用函数
startTime = min(kpi1_df['销售时间'])
endTime = max(kpi1_df['销售时间'])
days = (endTime - startTime).days
total_months = days // 30
业务指标2:最畅销的10个商品
计算步骤:
1 . 根据商品编号
进行分组,统计不同商品的销量
2 . 根据销量排序,找出销量前10的商品
在步骤一中需要注意,统计的是商品的总销量,而不是销量次数,所以应该应用sum()
函数,而不是count()
函数
'''
不同商品的销售量:按商品编号分组,统计每个商品的销售数量
'''
item_total_times = sub_trade_data.groupby(['商品编号']).sum()['销售数量']
在步骤二中注意,在排序前,需要将分组后的数据转成dataframe
结构,否则用sort_values()
排序要报错。
'''
降序排序,取销售数量前10
'''
item_total_times = pd.DataFrame(item_total_times)
item_total_times = item_total_times.sort_values(by=['销售数量'],ascending=False)[:10]
业务指标3:每类商品的月均销量 = 各类别商品的销售总量 / 月份数
计算步骤:
1 . 先求各个类别商品的消费总次数:按商品种类
字段分组,统计每个类别的销售总量
2 . 月份数在业务指标1中已计算,根据公式求每类商品的月均销量
同样,在步骤一中注意,统计的是每种类别商品的总销量,不是销售次数。
业务指标4:每类商品的月销量趋势
计算步骤:
1 . 先利用销售时间字段创建一个新的销售月份字段
2 . 再按商品种类
和销售月份
进行分组,统计各个商品种类的月销量
3 . 根据月销量值绘制每类商品的月销量趋势折线图
在步骤一中注意,创建销售时间的月份字段时,直接使用.month
属性来提取月份,会报错AttributeError: ‘Series’ object has no attribute 'month'
;
如果运行的是最新版本的pandas,那么解决办法是,可使用datetime属性dt
来访问datetime组件
'''
创建销售月份字段
'''
sub_trade_data['销售月份'] = sub_trade_data['销售时间'].dt.month
如果运行的是旧版本pandas,可以用以下办法:
sub_trade_data['销售月份'] = sub_trade_data['销售时间'].apply(lambda x: x.month)
步骤二中,是按商品种类
和销售月份
两个字段进行分组,并且统计的也是总销量。
cate_sale_month = sub_trade_data.groupby(['商品种类','销售月份']).sum()['销售数量']
这个指标是观察销量趋势,需要可视化每种类别商品的月销量折线图,这里暂不提可视化内容,放在后续。
业务指标5:每类商品的销量峰值是在哪个月
每个商品种类的月销量已由上个业务指标4得到,所以指标5的分析只需排序后提取销量峰值和对应的月份即可。
需要注意:
1 . 在排序前将数据转成dataframe
格式
2 . 使用sort_values()
函数排序,排序的目标是每种类别的月销量按从高到底排,那么by
参数需要传入两个,一个是商品种类
,另一个是销售数量
。对于销售数量
字段可直接传入,但是商品种类
字段,不是cate_sale_month
数据中的字段,而是index
,因为之前按照商品种类
进行的分组。所以这里需要使用cate_sale_month.index.names[0]
来获取商品种类
字段,并作为参数传入排序函数。
cate_sale_month = pd.DataFrame(cate_sale_month)
cate_sale_sort = cate_sale_month.sort_values(by=[cate_sale_month.index.names[0],'销售数量'],ascending=False)
3 . 因为每个类别都有12个月份的销量信息,所以排序后,每隔12行提取一个销量,即为每个类别的销量峰值。
# 提取每个类别的销量峰值
cate_max_sales = []
for i in range(0,len(cate_sale_sort),12):
cate_max_sales.append(cate_sale_sort.iloc[i,0])
# 根据销量峰值查询峰值对应的商品种类和月份
top_month = cate_sale_sort.query('销售数量 == {}'.format(cate_max_sales))
业务指标6:每类商品的用户画像(年龄段,性别)
分析步骤:
1 . 先将交易数据表和婴儿信息数据表合并
2 . 分析购买每类商品的用户性别分布情况
3 . 分析购买每类商品的用户年龄分布情况
步骤一,合并数据集,注意交易数据集有两万多条,而婴儿信息数据只有930条,所以合并时主要以婴儿信息数据表为主。
combined_data = pd.merge(sub_trade_data,baby_data,how='right')
步骤二,分析购买每类商品的婴儿性别分布情况,先根据商品种类
和性别
字段对数据进行分组,并使用sum()
函数统计总销售数量。
cate_baby_sex = combined_data.groupby(['商品种类','性别']).sum()['销售数量']
第二步就是排序问题,与业务指标5中相同,需要注意排序传入的参数。这里排序目标是每种类别下不同用户性别的购买量按从高到底排,那么by
参数需要传入两个,一个是商品种类
,另一个是销售数量
。对于销售数量
字段可直接传入,但是商品种类
字段,不是cate_baby_sex
数据中的字段,而是index
,因为之前按照商品种类
进行的分组。所以这里需要使用cate_baby_sex.index.names[0]
来获取商品种类
字段,并作为参数传入排序函数。
cate_baby_sex = pd.DataFrame(cate_baby_sex)
cate_baby_sex = cate_baby_sex.sort_values(by=[cate_baby_sex.index.names[0],'销售数量'], ascending=False)
步骤三,分析购买每类商品的用户年龄分布情况
计算步骤:
1 . 首先计算婴儿的年龄,增加婴儿年龄
列
2 . 将年龄进行分段,增加年龄区间
列
3 . 统计不同类商品在不同年龄段的销售情况
步骤一,计算婴儿的年龄:由购买日期(销售时间)- 婴儿出生日期
得到年份差值,从而得到购买商品时婴儿的年龄。
这里要注意的问题与业务指标4相同,即从时间中提取年份,直接使用.year
属性来提取年份,会报错。这里使用datetime属性dt
来访问datetime组件
combined_data['婴儿年龄'] = (combined_data['销售时间'].dt.year - combined_data['出生日期'].dt.year)
通过describe()
查看 婴儿年龄
字段的统计信息,发现最大年龄为28岁,这明显不合理,是个异常值,所以删去。分析到这里也从侧面反映了,在数据分析流程中数据清洗的工作无处不在。
combined_data = combined_data[combined_data['婴儿年龄'] != 28]
步骤二,对年龄进行分段,并增加年龄区间
列。注意区间临界值,可以根据婴儿年龄
字段的5个分位数(最小值,3个分位数,最大值)来定,例如通过describe()
查看到婴儿年龄分位数信息如下:
那么,分段临界值就可取bin_edges = [-3,0,1,2,3,4,5,12]
,同时指明每个年龄段的区间名bin_labels = ['未出生','1岁','2岁','3岁','4岁','5岁','5岁以上']
,之后再用pandas的cut
函数进行分段:
combined_data['年龄区间'] = pd.cut(combined_data['婴儿年龄'], bin_edges, labels=bin_labels)
# 确认所有年龄都被分到正确区间
combined_data['年龄区间'].isnull().sum() # 0
最后一步,统计每种商品的用户年龄分布情况。除了需要注意传入排序函数的参数外,还需要注意,按照商品种类
和年龄区间
分组后,可能存在某些年龄段,销售数量为空的情况,所以需要对空值进行填充。
cate_baby_age = combined_data.groupby(['商品种类','年龄区间']).sum()['销售数量']
# 存在某些年龄段,销售数量为空,将其填充为0
cate_baby_age = cate_baby_age.fillna(0)
# 排序得到,各类商品在哪个年龄段最畅销
cate_baby_age = pd.DataFrame(cate_baby_age)
cate_baby_age = cate_baby_age.sort_values(by=[cate_baby_age.index.names[0],'销售数量'],ascending=False)