产品经理数据分析不求人(5) - 数据采样与特征处理
产品经理数据分析不求人(2) - Pandas处理Excel
在现实世界中,数据通常是复杂冗余,富有变化的,人工选取出来的特征依赖人力和专业知识,局限性太大。于是我们需要通过机器来学习和抽取特征,促进特征工程的工作更加快速有效。前面几篇文章讲的都是如何通过人工选取关键因子来分析入参和目标之间的关联,其实效率是比较低的,效果也有很大提升空间。
image.png所谓效率低是因为当入参庞大的时候组合太多,无法一一穷举,所谓效果还需提升因为人工选择的组合会有遗漏,而且很难判断多个关键因子的权重,所以这事还是交给机器学习更合适,开始机器学习所面临的第一个问题就是特征工程,哪些入参可用? 入参如何向量化,如何对特征进行降维,如果标注化,归一化,对缺省值如何处理等等,都是这一篇文章希望探讨的方向。
在机器学习和模式识别中,特征是在观测现象中的一种独立、可测量的属性。选择信息量大的、有差别性的、独立的特征是模式识别、分类和回归问题的关键一步。
最初的原始特征数据集可能太大,或者信息冗余,因此在机器学习的应用中,一个初始步骤就是选择特征的子集,或构建一套新的特征集,减少功能来促进算法的学习,提高泛化能力和可解释性。在表格数据中,观测数据或实例(对应表格的一行)由不同的变量或者属性(表格的一列)构成,这里属性其实就是特征。但是与属性一词不同的是,特征是对于分析和解决问题有用、有意义的属性。
为了更形象的表述,让我们依然从一个实际案例出发,现在我希望通过对用户数据的分析,找到用户的关键特征用以预测哪些用户可能会转化为付费会员。那么我有哪些数据可以使用呢? 一方面是用户画像的基本属性,如年龄,性别,会员等级,地区等,另一方面是用户行为数据,但是一个用户有多条行为数据,必须聚合成一维向量才能使用。
1. 数据采样
原始数据非常庞大,针对需求场景如何采样是需要仔细考虑的问题。
采样时间: 正如人类大脑记忆符合艾宾浩斯遗忘曲线,数据也应遵循同样规律,年代太久远的数据价值会慢慢衰减,在数据量足够大的情况下我们尽量拿比较新的数据。
采样均衡: 如果完全随机采样的话,容易出现过拟或者欠拟的情况,尽量让标注分布的更平均一些,有几类标准就让数据几等分。
业务逻辑: 已成为付费会员的用户,订单量和购买金额必然上涨,我们要预测普通会员是否愿意加入付费会员就必须要剔除掉成为
付费会员后的行为数据,所以应该自成为付费会员之日起向前推一段时间取数。
遵循以上逻辑先取了四十万记录出来。
2. 记录打散
取数逻辑可能会造成记录分布不均匀影响算法模型,最好先做一个搅乱的预处理。
下面的代码从excel中读取记录,打乱顺序后重新生成新的excel,四十万条也花了几分钟才处理完毕。
df = pd.read_excel("d:/dev/svip_data_400k.xlsx")
df_shuffled = df.reindex(np.random.permutation(df.index))
df_shuffled.to_excel("d:/dev/svip_data_shuffled.xlsx")
3. 数据切分
因为数据量过大调试费时,先按行列切分一个子集出来,取11个参数(挑选比较有意义的字段)和1个标注,10万条记录。
这11个参数中有部分是用户基本属性,还有一部分是从多条用户行为数据中聚合出来的变量,如月均订单数,平均客单价,是否开通消费贷,累计订单数等都会对开通付费会员的意愿产生影响。
df = pd.read_excel("d:/dev/svip_data_shuffled.xlsx")
# slice with specified column
df_sub = df[['sex', 'age', 'vmark_name', 'city_level', 'add_time', 'last_ord_dt', 'pcl_flag',
'brand_like_num', 'value_per_order', 'monthly_order_num', 'order_num', 'svip_status']]
# slice by row index
df_sub = df_sub.iloc[0:100000]
df_sub.to_excel("d:/dev/svip_data_100k.xlsx")
4. 数据标准化
4.1 连续特征离散化
业界很少直接将连续值作为逻辑回归模型的特征输入,而是将连续特征离散化为一系列分类特征交给逻辑回归模型,主要原因是迭代速度更快,模型更稳定和健壮,降低了模型过拟合的风险。年龄,注册时间,最近一单时间都属于需要离散化的连续特征。年龄可以分成几个年龄段,注册时间可以转化为一个月内注册,三个月内注册,半年内注册。。。离散化的规则就凭经验和感觉了。
将年龄映射到四个年龄段,有些年龄值的变量类型不对,还有负值以及特别大的数字,也要先清理掉再分类。
def derive_age_range(age):
if ( (type(age)) != int or (age<0) or (age>80)):
return 'nan'
else:
value = ('20-', '20-29', '30-39', '40+')
range = (0, 20, 30, 40)
seq = list(filter(lambda x:x<=int(age), range))
return value[len(seq)-1]
以月为单位将注册时间映射到距离现在的时间周期
def derive_reg_distance(reg_date):
today = datetime.datetime.now()
diff = (today.year - reg_date.year) * 12 + (today.month - reg_date.month)
value = (u'一个月内', u'一到三个月', u'三到六个月', u'半年到一年', u'一年到两年', u'两年到三年', u'三年以上')
range = (0, 1, 3, 6, 12, 24, 36)
seq = list(filter(lambda x:x<diff, range))
return value[len(seq)-1]
以天为单位将最后订单日期映射为距离现在的时间周期
def derive_ord_distance(ord_date):
today = datetime.datetime.now()
diff = (today - ord_date).days
value = (u'3天内', u'3到7天', u'7到15天', u'15天到30天', u'30天到80天', u'80天到160天', u'160天以上')
range = (0, 3, 7, 15, 30, 80, 160)
seq = list(filter(lambda x:x<diff, range))
return value[len(seq)-1]
4.2 数值特征标准化
挑选了三个变量: 收藏品牌数量,月均订单量,平均客单价,用数值建议column均值再除以column方差的方式标准化。
有少量的记录数值为Null, 我手工处理了一下excel将Null替换为0.
以月均订单数这个字段为例,我先写段小代码看看数值的散点分布情况
df = pd.read_excel("d:/dev/svip_data_100k.xlsx")
fig, ax = plt.subplots()
fig.set_size_inches(15,15)
plt.plot(np.arange(0,100000,1), df['monthly_order_num'].values,'ro')
plt.show()
大部分值聚集在5以内,少量值零星分布在10以上的区间里
image.png
把均值, 标准差,方差打印出来看看
print('mean:', df['monthly_order_num'].mean())
print('std:', df['monthly_order_num'].std())
print('var:', df['monthly_order_num'].var())
mean: 1.0306720000000242
std: 1.059033353550142
var: 1.12155164393166
用方差来实现标准化,重新生成散点图
norm_mon = []
for index, row in df.iterrows():
monthly_order_num = row['monthly_order_num']
norm_mon.append((monthly_order_num - df['monthly_order_num'].mean()) / df['monthly_order_num'].var())
fig, ax = plt.subplots()
fig.set_size_inches(15,15)
plt.plot(np.arange(0,100000,1), norm_mon,'ro')
plt.show()
现在分布在10以上区间的落点数量减少,差异也变小了。
image.png
4.1和4.2中转化的新字段插入到dataframe中,并将原有字段删除。
4.3 分类特征数值化
会员等级就是一个典型的分类特征,从低到高分别有铁牌,铜牌,银牌,金牌,钻石,皇冠。这种离散值需要用独热编码(‘one-hot encoder‘)来处理, 使用二进制数来表示每个变量的特征: 铁牌即[1,0,0,0,0], 钻石即[0,0,0,1,0] etc. 我们并没有直接用1,2,3,4,5这几个数字来表示5个等级,因为数字的大小不应具备实际意义对计算产生影响,它只是一个标识而已。省份,城市,性别,包括前面已做离散化处理的变量都应该用这种方式来处理,Python也提供了便利的函数将分类值转化为独热码。
def to_one_hot(df, columns, nan=True):
for column in columns:
# use pd.concat to join the new columns with original dataframe
df = pd.concat([df, pd.get_dummies(df[column], prefix=column, dummy_na=nan)], axis=1)
# drop the original columns
df.drop([column], axis=1, inplace=True)
return df
flat_columns_nan = ['sex', 'age_range']
flat_columns = ['vmark_name', 'city_level', 'reg_distance', 'last_ord_distance', 'pcl_flag', 'svip_status']
df_sub = to_one_hot(df_sub, flat_columns_nan)
df_sub = to_one_hot(df_sub, flat_columns, False)
经过这样的处理,原有的每个字段扁平化展开变为数值矩阵
image.png
5. 数据保存
最后将数据序列化到HDFS格式的文件中保存,到这里数据准备工作就完成了。
# write to binary db
df_sub.to_hdf('store.h5', 'w', format='table', complevel=3, complib='bzip2')
References: