用户新增预测挑战赛

1.数据说明

赛题数据由约62万条训练集、20万条测试集数据组成,共包含13个字段。其中uuid为样本唯一标识,eid为访问行为ID,udmap为行为属性,其中的key1到key9表示不同的行为属性,如项目名、项目id等相关字段,common_ts为应用访问记录发生时间(毫秒时间戳),其余字段x1至x8为用户相关的属性,为匿名处理字段。target字段为预测目标,即是否为新增用户。

2.评估指标

本次竞赛的评价标准采用f1_score,分数越高,效果越好。

3.解题思路

参赛选手的任务是基于训练集的样本数据,构建一个模型来预测测试集中用户的新增情况。这是一个二分类任务,其中目标是根据用户的行为、属性以及访问时间等特征,预测该用户是否属于新增用户。具体来说,选手需要利用给定的数据集进行特征工程、模型选择和训练,然后使用训练好的模型对测试集中的用户进行预测,并生成相应的预测结果。

4.遇到的问题

  • 数据量比较大,但是特征比较少,经过处理的特征没几个,因此目的是先增加特征然后再对特征进行处理以及特征降维
  • 还不知道数据集的具体情况,可以对数据集进行筛选(暂时还没进行)

5.方案

相关模块和数据的导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder
#简单来说LabelEncoder就是把n个类别值编码为0~n-1之间的整数,建立起1-1映射
from sklearn.linear_model import LogisticRegression
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
#load() missing 1 required positional argument: 'Loader'
#E:\software\anaconda\anaconda3\Lib\site-packages\distributed\config.py文件里的
#yaml.load(f)改成yaml.safe_load(f)
from catboost import CatBoostClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.ensemble import StackingClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score
from sklearn.model_selection import train_test_split
import warnings
from tqdm import tqdm
warnings.filterwarnings('ignore')
1
2
3
4
5
6
7
# 读取训练集和测试集
# 使用 read_csv() 函数从文件中读取训练集数据,文件名为 'train.csv'
train_data = pd.read_csv('用户新增预测挑战赛公开数据/train.csv')
# 使用 read_csv() 函数从文件中读取测试集数据,文件名为 'test.csv'
test_data = pd.read_csv('用户新增预测挑战赛公开数据/test.csv')

train_data#用于观察数据集

udmap的处理,将 字典中的数据和unknown数据以one-hot的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. 将 'udmap' 列进行 One-Hot 编码 
# 数据样例:
# udmap key1 key2 key3 key4 key5 key6 key7 key8 key9
# 0 {'key1': 2} 2 0 0 0 0 0 0 0 0
# 1 {'key2': 1} 0 1 0 0 0 0 0 0 0
# 2 {'key1': 3, 'key2': 2} 3 2 0 0 0 0 0 0 0

# 在 python 中, 形如 {'key1': 3, 'key2': 2} 格式的为字典类型对象, 通过key-value键值对的方式存储
# 而在本数据集中, udmap实际是以字符的形式存储, 所以处理时需要先用eval 函数将'udmap' 解析为字典
# 具体实现代码:
# 定义函数 udmap_onethot,用于将 'udmap' 列进行 One-Hot 编码
def udmap_onethot(d):
v = np.zeros(9) # 创建一个长度为 9 的零数组
if d == 'unknown': # 如果 'udmap' 的值是 'unknown'
return v # 返回零数组
d = eval(d) # 将 'udmap' 的值解析为一个字典
for i in range(1, 10): # 遍历 'key1' 到 'key9', 注意, 这里不包括10本身
if 'key' + str(i) in d: # 如果当前键存在于字典中
v[i-1] = d['key' + str(i)] # 将字典中的值存储在对应的索引位置上

return v # 返回 One-Hot 编码后的数组

数据集特征提取

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

# 注: 对于不理解的步骤, 可以逐行 print 内容查看
# 使用 apply() 方法将 udmap_onethot 函数应用于每个样本的 'udmap' 列
# np.vstack() 用于将结果堆叠成一个数组
train_udmap_df = pd.DataFrame(np.vstack(train_data['udmap'].apply(udmap_onethot)))
test_udmap_df = pd.DataFrame(np.vstack(test_data['udmap'].apply(udmap_onethot)))
'''
apply() 函数的自由度较高,可以直接对 Series 或者 DataFrame 中元素进行逐元素遍历操作,方便且高效,具有类似于 Numpy 的特性。
'''

# 为新的特征 DataFrame 命名列名
train_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
test_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
# 将编码后的 udmap 特征与原始数据进行拼接,沿着列方向拼接
train_data = pd.concat([train_data, train_udmap_df], axis=1)
test_data = pd.concat([test_data, test_udmap_df], axis=1)


# 4. 编码 udmap 是否为空
# 使用比较运算符将每个样本的 'udmap' 列与字符串 'unknown' 进行比较,返回一个布尔值的 Series
# 使用 astype(int) 将布尔值转换为整数(0 或 1),以便进行后续的数值计算和分析
train_data['udmap_isunknown'] = (train_data['udmap'] == 'unknown').astype(int)
test_data['udmap_isunknown'] = (test_data['udmap'] == 'unknown').astype(int)


# 5. 提取 eid 的频次特征
# 使用 map() 方法将每个样本的 eid 映射到训练数据中 eid 的频次计数
# train_data['eid'].value_counts() 返回每个 eid 出现的频次计数
train_data['eid_freq'] = train_data['eid'].map(train_data['eid'].value_counts())
test_data['eid_freq'] = test_data['eid'].map(train_data['eid'].value_counts())#这里在测试数据集上用的是训练集的eid的频率
'''
map可以接受函数,字典,以及series(和字典类似)。然后这里会进行匹配。
'''

# 6. 提取 eid 的标签特征
# 使用 groupby() 方法按照 eid 进行分组,然后计算每个 eid 分组的目标值均值
# train_data.groupby('eid')['target'].mean() 返回每个 eid 分组的目标值均值
train_data['eid_mean'] = train_data['eid'].map(train_data.groupby('eid')['target'].mean())
test_data['eid_mean'] = test_data['eid'].map(train_data.groupby('eid')['target'].mean())
'''
这里现根据eid进行分组
'''

# 7. 提取时间戳
# 使用 pd.to_datetime() 函数将时间戳列转换为 datetime 类型
# 样例:1678932546000->2023-03-15 15:14:16
# 注: 需要注意时间戳的长度, 如果是13位则unit 为 毫秒, 如果是10位则为 秒, 这是转时间戳时容易踩的坑
# 具体实现代码:
train_data['common_ts'] = pd.to_datetime(train_data['common_ts'], unit='ms')
test_data['common_ts'] = pd.to_datetime(test_data['common_ts'], unit='ms')

# 使用 dt.hour 属性从 datetime 列中提取小时信息,并将提取的小时信息存储在新的列 'common_ts_hour'
train_data['common_ts_hour'] = train_data['common_ts'].dt.hour
test_data['common_ts_hour'] = test_data['common_ts'].dt.hour


train_data['common_ts_day'] = train_data['common_ts'].dt.day
test_data['common_ts_day'] = test_data['common_ts'].dt.day

train_data['x1_freq'] = train_data['x1'].map(train_data['x1'].value_counts())
test_data['x1_freq'] = test_data['x1'].map(train_data['x1'].value_counts())
train_data['x1_mean'] = train_data['x1'].map(train_data.groupby('x1')['target'].mean())
test_data['x1_mean'] = test_data['x1'].map(train_data.groupby('x1')['target'].mean())

train_data['x2_freq'] = train_data['x2'].map(train_data['x2'].value_counts())
test_data['x2_freq'] = test_data['x2'].map(train_data['x2'].value_counts())
train_data['x2_mean'] = train_data['x2'].map(train_data.groupby('x2')['target'].mean())
test_data['x2_mean'] = test_data['x2'].map(train_data.groupby('x2')['target'].mean())

#train_data['x3_freq'] = train_data['x3'].map(train_data['x3'].value_counts())
#test_data['x3_freq'] = test_data['x3'].map(train_data['x3'].value_counts())

#train_data['x4_freq'] = train_data['x4'].map(train_data['x4'].value_counts())
#test_data['x4_freq'] = test_data['x4'].map(train_data['x4'].value_counts())
'''
这两个数据有问题,在test中会因为数据不匹配导致NaN的出现因此这两个数据剔除
'''

train_data['x6_freq'] = train_data['x6'].map(train_data['x6'].value_counts())
test_data['x6_freq'] = test_data['x6'].map(train_data['x6'].value_counts())
train_data['x6_mean'] = train_data['x6'].map(train_data.groupby('x6')['target'].mean())
test_data['x6_mean'] = test_data['x6'].map(train_data.groupby('x6')['target'].mean())

train_data['x7_freq'] = train_data['x7'].map(train_data['x7'].value_counts())
test_data['x7_freq'] = test_data['x7'].map(train_data['x7'].value_counts())
train_data['x7_mean'] = train_data['x7'].map(train_data.groupby('x7')['target'].mean())
test_data['x7_mean'] = test_data['x7'].map(train_data.groupby('x7')['target'].mean())

train_data['x8_freq'] = train_data['x8'].map(train_data['x8'].value_counts())
test_data['x8_freq'] = test_data['x8'].map(train_data['x8'].value_counts())
train_data['x8_mean'] = train_data['x8'].map(train_data.groupby('x8')['target'].mean())
test_data['x8_mean'] = test_data['x8'].map(train_data.groupby('x8')['target'].mean())

#df.groupby(分组依据)[数据来源].使用操作


train=train_data.drop(['udmap','uuid','target'],axis=1)
test=test_data.drop(['udmap','uuid',],axis=1)
#我们保留了common_ts这个数据,接下来对这个特征的归一化

数据归一化处理

1
2
3
4
5
6
7
#对数据进行归一化处理
for i in train.columns:
MAX=max(train[i])
MIN=min(train[i])#用训练集的数据区归一化测试集的数据
LEN=MAX-MIN
train[i]=train[i].apply(lambda x:(x-MIN)/LEN)
test[i]=test[i].apply(lambda x:(x-MIN)/LEN)

特征组合

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
# 暴力Feature 行为
# 暴力Feature 时间
# 暴力Feature 用户属性
#这里暂时不考虑特征的随机组合

# 暴力Feature 行为
f = ['key1', 'key2', 'key3', 'key4', 'key5', 'key6', 'key7', 'key8', 'key9','udmap_isunknown']
for df in [train, test]:
for i in range(len(f)):
for j in range(i + 1, len(f)):
#加f后可以在字符串里面使用用花括号括起来的变量和表达式,如果字符串里面没有表达式,那么前面加不加f输出应该都一样。
df[f'{f[i]}+{f[j]}'] = df[f[i]] + df[f[j]]
# 暴力Feature 时间
f = ['common_ts_hour','common_ts_day']
for df in [train, test]:
for i in range(len(f)):
for j in range(i + 1, len(f)):
df[f'{f[i]}+{f[j]}'] = df[f[i]] + df[f[j]]
df[f'{f[i]}-{f[j]}'] = df[f[i]] - df[f[j]]
df[f'{f[i]}*{f[j]}'] = df[f[i]] * df[f[j]]
df[f'{f[i]}/{f[j]}'] = df[f[i]] / (df[f[j]]+1)
# 暴力Feature 用户属性
f = ['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8']
for df in [train, test]:
for i in range(len(f)):
for j in range(i + 1, len(f)):
df[f'{f[i]}+{f[j]}'] = df[f[i]] + df[f[j]]


数据降维

  • 利用xgboost进行特征选择,最终选出70组特征
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
#采用xgboost的特征筛选的功能
xgbc = XGBClassifier(
objective='binary:logistic',
eval_metric='auc',
n_estimators=100,
max_depth=6,
learning_rate=0.1
)
xgbc.fit(train, label)
importances_xgb = xgbc.feature_importances_/np.sum( xgbc.feature_importances_)
# print(importances)
indices_xgb = np.argsort(importances_xgb)[::-1]
# print(indices)

#查看结果
feat_labels = train.columns
for f in range(train.shape[1]):
print("%2d) %-*s %f" % \
(f + 1, 30, feat_labels[indices_xgb[f]], importances_xgb[indices_xgb[f]]))

features=np.array(feat_labels)
num_imo=features[list(indices_xgb[0:60])]#选择60个特征

train=train[num_imo]
test=test[num_imo]

交叉验证模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 常见的交叉验证模型框架
def model_train(model, model_name, kfold=5):
oof_preds = np.zeros((train.shape[0]))#构造一个series令所有行全部为0
test_preds = np.zeros(test.shape[0])
skf = StratifiedKFold(n_splits=kfold)
print(f"Model = {model_name}")
print(len(train.columns))
for k, (train_index, test_index) in enumerate(skf.split(train, label)):
x_train, x_test = train.iloc[train_index, :], train.iloc[test_index, :]
y_train, y_test = label.iloc[train_index], label.iloc[test_index]
print(1)
model.fit(x_train,y_train)
#print(2)
y_pred = model.predict_proba(x_test)[:,1]
##在这里第一列是预测为0的概率,第二列是预测为1的概率
oof_preds[test_index] = y_pred.ravel()
auc = roc_auc_score(y_test,y_pred)
print("- KFold = %d, val_auc = %.4f" % (k, auc))
test_fold_preds = model.predict_proba(test)[:, 1]
test_preds += test_fold_preds.ravel()#将给定Series对象的基础数据作为ndarray返回。
print("Overall Model = %s, F1 = %.4f" % (model_name, f1_score(label, oof_preds, average='macro')))
return test_preds / kfold#取平均值

数据清洗,通过10交叉验证判断数据是否存在问题

1
2
3
4
5
6
7
8
9
10
11
12
13
'''
xgbc = XGBClassifier(
objective='binary:logistic',
eval_metric='auc',
n_estimators=100,
max_depth=6,
learning_rate=0.1
)
xgbc_test_preds = model_train(xgbc, "XGBClassifier", 10)
'''

#这里用于挑选异常训练集
#看误差是否过大

验证集和训练集构建

1
2
3
#先将训练数据划分成训练集和验证集
X_train, X_test, y_train, y_test = train_test_split( train, label, stratify=label, random_state=2022)
#75%的训练集

模型选择

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# xgboost实验
# max_depth不能太小否则会出问题
xgbc = XGBClassifier(
objective='binary:logistic',
eval_metric='auc',
n_estimators=100,
max_depth=50,
learning_rate=0.1
)
xgbc.fit(x_train,y_train)
y_pred = xgbc.predict_proba(x_test)[:, 1]
threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

# 决策树实验
DT = DecisionTreeClassifier()
DT.fit(x_train,y_train)
y_pred = DT.predict_proba(x_test)[:, 1]
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

#随机森林实验
RF=RandomForestClassifier(n_estimators=50)
RF.fit(x_train,y_train)
y_pred = RF.predict_proba(x_test)[:, 1]
threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

# GDBT实验
#是不是树的深度太浅导致的
gbc = GradientBoostingClassifier(
n_estimators=10,
learning_rate=0.1,
max_depth=50
)
gbc.fit(x_train,y_train)
y_pred = gbc.predict_proba(x_test)[:, 1]
threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

#HGBC 实验
hgbc = HistGradientBoostingClassifier(
max_iter=20,
max_depth=50
)
gbc.fit(x_train,y_train)
y_pred = gbc.predict_proba(x_test)[:, 1]
threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)


# #LGMB 实验
# gbm = LGBMClassifier(
# objective='binary',
# boosting_type='gbdt,
# num_leaves=2 ** 6,
# max_depth=50,
# colsample_bytree=0.8,
# subsample_freq=1,
# max_bin=255,
# learning_rate=0.05,
# n_estimators=4000,
# metrics='auc'
# )
# gbm.fit(x_train,y_train)
# y_pred = gbm.predict_proba(x_train)[:, 1]
# threshold=0.5
# y_pred = (y_pred >= threshold).astype(int)
# f1 = f1_score(y_train, y_pred, average='macro')
# print('F1 = %.8f' % f1)

# cbc = CatBoostClassifier(
# iterations=20,
# depth=16,
# learning_rate=0.03,
# l2_leaf_reg=1,
# loss_function='Logloss',
# verbose=0
# )
# cbc.fit(x_train,y_train)
# y_pred = cbc.predict_proba(x_train)[:, 1]
# threshold=0.5
# y_pred = (y_pred >= threshold).astype(int)
# f1 = f1_score(y_train, y_pred, average='macro')
# print('F1 = %.8f' % f1)

ada=AdaBoostClassifier(
DecisionTreeClassifier(max_depth=50),
n_estimators=100,
learning_rate=0.01
)#默认是CART决策树作为单模型
ada.fit(x_train,y_train)
y_pred = ada.predict_proba(x_test)[:, 1]
threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

模型融合

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
# 最终决定:决策树,xgboost, RF,GBDT,HGBC,adaboost这几个模型stack
xgbc = XGBClassifier(
objective='binary:logistic',
eval_metric='auc',
n_estimators=100,
max_depth=50,
learning_rate=0.1
)
DT = DecisionTreeClassifier()
RF=RandomForestClassifier(n_estimators=50)
gbc = GradientBoostingClassifier(
n_estimators=10,
learning_rate=0.1,
max_depth=50
)

hgbc = HistGradientBoostingClassifier(
max_iter=20,
max_depth=50
)
ada=AdaBoostClassifier(
DecisionTreeClassifier(max_depth=50),
n_estimators=100,
learning_rate=0.01
)


estimators = [
('xgbc', xgbc),
('DT',DT),
('RF',RF),
('gbc', gbc),
('hgbc', hgbc),
('ada', ada),
]
clf = StackingClassifier(
estimators=estimators,
final_estimator=LogisticRegression()
)

#用组合模型训练
clf.fit(x_train, y_train)
y_pred = clf.predict_proba(x_test)[:, 1]

threshold=0.5
y_pred = (y_pred >= threshold).astype(int)
f1 = f1_score(y_test, y_pred, average='macro')
print('F1 = %.8f' % f1)

结果提交

1
2
3
4
5
6
7
8
9
10
11
# #这里的分类器我不单单想用上面的,我打算重新训练所有数据集来进行预测
# clf_test_preds = model_train(clf, "StackingClassifier")
# #还是用全部的数据进行训练
# clf.fit(train,label)

result_df = pd.DataFrame({
'uuid': test_data['uuid'], # 使用测试数据集中的 'uuid' 列作为 'uuid' 列的值
'target': clf.predict(test) # 使用模型 clf 对测试数据集进行预测,并将预测结果存储在 'target' 列中
})

result_df.to_csv('submit.csv', index=None)