Machine Learning-实战 电信行业智能套餐个性化匹配

本文是关于BDCI 2018-面向电信行业存量用户的智能套餐个性化匹配模型大赛 TOP1开源代码的实现

运行环境

  • windows
  • python3.6
  • pandas sklearn xgboost

    赛事说明

赛事链接面向电信行业存量用户的智能套餐个性化匹配模型

可以参看链接,里面有详细的训练集数据说明,里面包含了如下信息:
数据说明
作品要求
评分方式

解决方案

我复现了top1的解决方案,以下是top1的解决方案:

1.数据探查

分类label

根据赛事说明,该比赛是一个多分类的问题,对应着有11种套餐,首先看看这11种套餐的分布情况,可以看出它们的分布较为不均。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#导入数据
path = 'D:/Pywork/2018_CCF_BDCI_CHINA_TOP1/project/preliminary_training_set/train_all.csv'

def import_data(path):
train_data = pd.read_csv(path, low_memory=False)
return train_data

train_data = import_data(path)

# current_service 条形图
def service_view(train_data,categories):
data_by_service = train_data.groupby(categories).size().reset_index(name='counts')
data_by_service[categories] = data_by_service[categories].astype(str)
print(data_by_service)
# 随机生成几种颜色,reshape第二个参数-1指随着N变化,第一维度填满有剩就来填第二维
# 生成的是随机的N组三通道(r,g,b)的颜色
N = 7
colors = np.random.rand(N * 3).reshape(N, -1)
fig, ax = plt.subplots()

ax.bar(data_by_service[categories], data_by_service['counts'], color=colors)
plt.xticks(rotation=270)
plt.xlabel(categories)
plt.ylabel('counts')
plt.show()
str = 'current_service'
service_view(train_data,str)

多分类问题,典型的评价指标为macro-f1:首先针对每个套餐类别,统计分别统计TP(预测答案正确),FP(错将其他类预测为本类),FN(本类标签预测为其他类标)。计算Precision(精确率)和Recall(召回率)

举个具体场景的例子:

假如某个班级有男生80人,女生20人,共计100人.
目标是找出所有女生.
现在某人挑选出50个人,其中20人是女生,另外还错误的把30个男生也当作女生挑选出来了.
作为评估者的你需要来评估(evaluation)下他的工作

通过表格可以得出:
TP:20
FP:30
FN:0
TN:50

精确率(precision)$$P = \frac{TP}{TP+FP}$$
召回率(recall)的公式是:$$R=\frac{TP}{TP+NP}$$,

首先我们可以计算精确率(precision):很容易,我们可以得到,他把其中70(20女+50男)人判定正确了,而总人数是100人,所以它的accuracy就是70%(70 / 100).

召回率(recall):它计算的是所有”正确被检索的item(TP)”占所有”应该检索到的item(TP+FN)”的比例,所以它的recall为100%.
如果单看某一个指标,预测结果是不会被正确衡量的。比如在以上例子中,如果我全部判断为女生,则召回率可以达到100%,单看召回率它是一个非常准确的预测,但此时你能说该模型的预测效果好吗,很显然不能。所以必须结合P,R一起看。结合起来最常见的方法应该就是F-Measure了,有些地方也叫做F-Score:
$$F=\frac{(a^2+1)P*R} {a^2(P+R)}$$
当参数a=1时,就是最常见的F1了:
$$F1 = \frac{2PR} {P+R} $$

回到赛题,在得到每个类别下的F1-score后,对各个类别的F1-score求均值,得到最后的评测结果,计算方式如下:
$$score=\frac 1n\sum{f1_k}$$

原始特征与标签相关性

以下是关于原始特征service_type和label相关性的观察图表

可以明显的看出一个规律,service_type可以将套餐分为两个部分,这两个部分是没有交叉的,其中一类有8个,另外一类有3个。这给我们比赛带来一个思路是,可以分模型预测。

关于age和label相关性的观察图表:

我们可以看到对于年龄来讲,基本上符合电信用户群体的分布,但是有很多0岁的异常值,对于异常值,可以先不做处理,或填充-1.

关于gender和label相关性的观察图表:

我们观察到性别中有0 的缺省值,对于这部分,我们使用了两种方法处理,一种是填充service_type对应字段的众数,和原始值。最终选取了原始值.

2.数据预处理

首先将分类的label映射到0-11数字当中便于处理

1
train['label'] = train['current_service'].map(p)

为了避免利用pandas.read_csv()导入的类型不统一。可以传入dtype=set_str,pandas.read_csv(dtype=set_str),来指定类型,set_str是自己创建的字典,每个字段指向的类型,均可自己设定。

对于年龄,话费不合理的值,用nan值替换。

3.特征工程

(1)关联规则
受max-encoding的方法的启发,对有关联关系的话费字段,1_total_fee, 2_total_fee, 3_total_fee, 4_total_fee四个字段的构建关联规则,这样可以利用整体信息,对话费字段这个强特征进行降维编码。使得其数据更加具有代表性
(2) 业务特征
业务特征的部分,我们深入研究了联通的套餐消费场景,从比赛的一开始,首先就通过联通官网以及消费论坛认真的开始调研工作,深入了解了联通的各种套餐特点和用户群差别。通过熟悉套餐的特性,我们可以为各种特定用户群推荐适宜他们的套餐,比如腾讯天王卡玩腾讯游戏看腾讯视频不花钱是深度腾讯用户的福音,蚂蚁大宝卡则可以赠送2g无差别流量给高流量消费者。联通传统套餐的各种优惠活动,比如预充值冲100返流量和话费,适合平时那些流量和话费不够用的用户,充值返话费则适合那些薅羊毛的用户。对此我们针对用户的流量和通话做了一系列特征,比例、差值,求和等,力求尽可能的描绘出一幅用户画像。

提出了以下几种针对业务的特征:

  1. 话费减去16元是否是整数
  2. 流量的有效数字是否是27的整数倍
  3. 话费的有效数字能否被15乘除
  4. 话费是否是整数(用户可能未超套餐)
  5. 连续两个月套餐的差值能否被5,10,15,27,30等计费单元整除
  6. 四个月话费的最小值
  7. 计算流量的平均单价
  8. 计算通话时间的平均单价
  9. 等等…

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 平均值 按照行 axis=1
data[month_fee[:4]].mean(axis=1)
data[month_fee[:3]].mean(axis=1)
data[month_fee[:2]].mean(axis=1)
data[month_fee[:4]].std(axis=1)

# CV 变异系数 = 标准差/平均数
# 反映数据离散程度的绝对值,可以认为变异系数和极差、标准差和方差一样,可以消除测量尺度和量纲的影响
data['total_fee_std4'] / (data['total_fee_mean4'] + 0.1)

# 四个月最大,最小话费
data[month_fee[:4]].max(axis=1)
data[month_fee[:4]].min(axis=1)

# 电话时间 差,和,最小,最大
data['service2_caller_time']-data['service1_caller_time']
data['service2_caller_time']+data['service1_caller_time']
data[['service2_caller_time', 'service1_caller_time']].min(axis=1)
data[['service2_caller_time', 'service1_caller_time']].max(axis=1)

# 话费是否是整数
data['{}_1'.format(fee)] = ((data[fee] % 1 == 0) & (data[fee] != 0))
data['{}_01'.format(fee)] = ((data[fee] % 0.1 == 0) & (data[fee] != 0))

# 分组标准化
def grp_standard(data,keys,names,drop=False):
for name in names:
new_name = name if drop else name + '_' + keys + '_' + 'standardize'
mean_std = data.groupby(keys, as_index=False)[name].agg({'mean': 'mean', 'std': 'std'})
data = data.merge(mean_std, on=keys, how='left')
data[new_name] = ((data[name]-data['mean'])/data['std']).fillna(0).astype(np.float32)
# 防止除0报错
data[new_name] = data[new_name].replace(-np.inf, 0).fillna(0)
data.drop(['mean', 'std'], axis=1, inplace=True)
return data

# 按照 合约类型 标准化
data = grp_standard(data, 'contract_type', ['1_total_fee_log'], drop=False)
..............
# 年龄分组
data['age_scatter'] = pd.qcut(data['age'], 5)
data = grp_standard(data, 'age_scatter', ['1_total_fee_log'], drop=False)
..............
# online_time 分组
data['online_time_scatter'] = pd.qcut(data['online_time'], 5)
data = grp_standard(data, 'online_time_scatter', ['1_total_fee_log'], drop=False)
..................
# 热编码
data = pd.get_dummies(data, columns=['contract_type'], dummy_na=-1)
data = pd.get_dummies(data, columns=['net_service'], dummy_na=-1)
data = pd.get_dummies(data, columns=['complaint_level'], dummy_na=-1)

3.构建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
params = {'objective': 'multi:softprob',
'eta': 0.5,
'max_depth': 6,
'silent': 1,
'num_class': 3,
'eval_metric': "mlogloss",
'min_child_weight': 3,
'subsample': 0.7,
'colsample_bytree': 0.7,
'seed': 66
}
# 单独拿 service_type=1出来训练 train_feat1
train_preds1, test_preds1 = xgb_cv(params, train_feat1, test_feat1, predictors1)
print("xgb_cv函数返回的test_preds1的类{}".format(type(test_preds1)))

def xgb_cv(params, train_feat, test_feat, predictors, label='label',groups=None,cv=5,stratified=True):
print('开始CV 5折训练...')
t0 = time.time()
# train_feat[label].nunique() 不同label的个数
train_preds = np.zeros((len(train_feat), train_feat[label].nunique()))
test_preds = np.zeros((len(test_feat), train_feat[label].nunique()))
xgb_test = xgb.DMatrix(test_feat[predictors])
models = []
# len(train_feat)个样本分成 5个样本集
kf = KFold(len(train_feat), n_folds=5, shuffle=True, random_state=520)
for i, (train_index, test_index) in enumerate(kf):
# 将测试集均分 取一份当测试集
xgb_train = xgb.DMatrix(train_feat[predictors].iloc[train_index], train_feat[label].iloc[train_index])
xgb_eval = xgb.DMatrix(train_feat[predictors].iloc[test_index], train_feat[label].iloc[test_index])
print("..........开始第{}轮训练".format(i))
params = {'objective': 'multi:softprob', # 多分类的问题
'eta': 0.1, # 如同学习率
'max_depth': 6, # 构建树的深度,越大越容易过拟合
'silent': 1, # 取0时表示打印出运行时信息,取1时表示以缄默方式运行,不打印运行时信息。缺省值为0
'num_class': 11, # 类别数,与 multisoftmax 并用
'eval_metric': "mlogloss", # 评价指标 negative log-likelihood
'min_child_weight': 3,
'subsample': 0.7, # 随机采样训练样本
'colsample_bytree': 0.7, # 生成树时进行的列采样
'seed': 66
} if params is None else params
watchlist = [(xgb_train, 'train'), (xgb_eval, 'val')]
clf = xgb.train(params,
xgb_train,
num_boost_round=3000,
evals=watchlist,
verbose_eval=50,
early_stopping_rounds=50)
# 每次更新的是从训练集中划出来的一部分测试集
train_preds[test_index] += clf.predict(xgb_eval)
# xgb_test 是 xgb.DMatrix(test_feat) 的返回值
test_preds += clf.predict(xgb_test)
models.append(clf)
pickle.dump(models, open('xgb_{}.model'.format(datetime.datetime.now().strftime('%Y%m%d_%H%M%S')), '+wb'))
print('用时{}秒'.format(time.time()-t0))
return train_preds, test_preds/5

xgb_cv函数解读:划分数据集的方法采用K折交叉验证,K折交叉验证具体实现是

  • 将数据集平均分割成K个等份
    sklearn.cross_validation.KFold(len(train_feat), n_folds=5, shuffle=True, random_state=520)
    n_folds=5,K折验证的K值;默认3,最小为2
    shuffle默认False;shuffle会对数据产生随机搅动(洗牌)
    random_state默认None,随机种子
  • 使用1份数据作为测试数据,其余作为训练数据
  • 计算测试准确率
  • 使用不同的测试集,重复2、3步骤
  • 对测试准确率做平均,作为对未知数据预测准确率的估计
1
2
3
4
5
6
7
8
9
10
11
params = {'objective': 'multi:softprob', # 多分类的问题
'eta': 0.1, # 如同学习率
'max_depth': 6, # 构建树的深度,越大越容易过拟合
'silent': 1, # 取0时表示打印出运行时信息,取1时表示以缄默方式运行,不打印运行时信息。缺省值为0
'num_class': 11, # 类别数,与 multisoftmax 并用
'eval_metric': "mlogloss", # 评价指标 negative log-likelihood
'min_child_weight': 3,
'subsample': 0.7, # 随机采样训练样本
'colsample_bytree': 0.7, # 生成树时进行的列采样
'seed': 66
} if params is None else params

xgb.train(params, xgb_train,num_boost_round=3000,
evals=watchlist, verbose_eval=50, early_stopping_rounds=50)

**params:**训练的参数,上面代码块中有详细的解释
**xgb_train:**训练的数据

测试集的预测结果取5次的平均。

代码链接:https://pan.baidu.com/s/1VcY_BWTLPcONr2WGgr7bZA
密码:idgg

参考:



觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭



wechat pay



alipay

Machine Learning-实战 电信行业智能套餐个性化匹配
http://yuting0907.github.io/2018/12/30/Machine-Learning-实战 电信行业智能套餐个性化匹配/
作者
yuting
发布于
2018年12月30日
许可协议