基于集成学习预测某类闯关游戏用户流失

1 简介

新用户流失的问题在这些游戏中依然严重,大量新用户在试玩不久后就选择放弃。如果能在用户彻底卸载游戏之前对可能流失的用户进行有效干预,比如赠送游戏道具或发送鼓励的消息,就有可能挽留住他们,进而提升游戏的活跃度和公司的潜在利润。因此,预测用户流失已经成为一个极具挑战性的重要问题。

我们将以真实游戏的非结构化日志数据为出发点,构建一个用户流失预测模型,并根据已有知识设计合适的算法来解决实际问题。

数据结构,一共五个csv文件

image-20231113194201375
1
2
3
4
5
6
7
8
9
10
# train.csv  test.csv  dev.csv
# 分别对应训练集 测试集 验证集
# 分别是 userid 和 对应的流失情况,如果为1表示流失 如果为0表示没流失

# level_seq.csv 包含每个用户在每个关卡中的一些数据
# 包括f_success 表示通关 1 表示通关 0 表示没通关
# f_duration 表示所用的时间
# f_reststep 表示剩余步数和游戏限定步数之比
# f_help 表示是否使用道具提示等 1 表示使用 0表示未使用
# time 表示这个游戏打开时的时间戳

代码在我的仓库中可见。https://github.com/Guoxn1/ai。

2 数据预处理

2.1 特征工程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从以下几个维度进行特征工程,刻画不同用户:
- 是否喜欢:
- `day`:登录天数。
- `login`:登录次数。
- `time`:游戏总花费的时间
- `try`:尝试记录次数
- 游玩体验:
- `success`:通关数/尝试次数
- `maxlevel`:最大闯关数
- `maxwin`:最大连赢数
- `maxfail`:最大连输数
- `winof20`:最后20局的胜率
- 个人特性:
- `beginday`:开始玩的时间
- `help`:使用帮助的频率
- `retry`:最大愿意重试的次数
- `duration`:平均每一关超出平均时长
- `restep`:成功通关的记录中,平均剩余步数与限定步数之比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from datetime import datetime

def cvttime(data):
data,time = str(data).split()
year,month,day = map(int,data.split("-"))
hour,minute,second = map(int,time.split(":"))
return datetime(year,month,day,hour,minute,second)
# .map()方法的参数中指定一个函数,该函数将被应用于列中的每个元素,以实现映射转换的逻辑
seq_df["time"] = seq_df["time"].map(lambda x: cvttime(x))

# 计算平均每次花费的时间
f_avg = meta_df["f_avg_duration"].values

# 计算用户的游戏时长和所有人平均游戏的平均时长的差距
# apply应用一个函数到DataFrame的每一行或每一列
seq_df["avg_time"] = seq_df.apply(lambda x:x["f_duration"] - f_avg[int(x["level_id"]-1)],axis=1)

print(seq_df.head())

计算登录次数

1
2
3
4
5
6
7
8
9
10
def cal_login(series):
# 计算用户的登录次数
ans = 1
# 如果 两次登录时间间隔大于900s 则认为是两次登录
for start,end in zip(*[iter(series)]*2):
if(end-start).total_seconds() > 900:
ans+=1

return ans

计算最大连胜和连败次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def cal_fail_retry(df):
# 连败
fail_series = df["f_success"].eq(0)
fail_counts = fail_series.groupby((fail_series!=fail_series.shift()).cumsum()).cumsum()
max_fail = fail_counts.max()
# 连赢
win_series = df["f_success"].eq(1)
win_counts = win_series.groupby((win_series!=win_series.shift()).cumsum()).cumsum()
max_win = win_counts.max()
# 重试的最大次数
retry_series = df["level_id"]
retry_counts = retry_series.groupby((retry_series!=retry_series.shift()).cumsum()).cumcount()
max_retry = retry_counts.max()
return max_win,max_fail,max_retry

2.2 组合数据集

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
import numpy as np
from tqdm import tqdm

user_df = seq_df.groupby("user_id")
# 初始化训练集、验证集和测试集的 X 和 y
train_X, valid_X, test_X = [], [], []
train_y, valid_y, test_y = [], [], []

for user_id,df in tqdm(user_df):
user = []
# 统计用户登录次数
login = cal_login(df["time"])
user.append(login)
# 计算总的游戏时长
user.append(sum(df["f_duration"]))
# 计算总尝试次数
try_sum = df.shape[0]
user.append(try_sum)

# 计算游戏体验
# 计算用户平均成功率
user.append(np.nanmean(df["f_success"]))
# 计算最高关卡id
user.append(max(df["level_id"]))
# 计算最大连胜次数
win,fail,retry = cal_fail_retry(df)
user.append(win)
user.append(fail)
# 最近20场的胜率
user.append(np.nanmean(df["f_success"][-20 if try_sum >=20 else 0:]))

# 个人特性
# 平均求助次数
user.append(np.nanmean(df["f_help"]))
# 最大求助次数
user.append(retry)
# 与平均游戏时长的差距
user.append(np.nanmean(df["avg_time"]))
# 计算用户成功时的平均剩余步数
if not df[df["f_success"]==1].shape[0]:
user.append(0)
else:
user.append(np.nanmean(df[df["f_success"]==1]["f_reststep"]))

if user_id in set(train_df["user_id"]):
train_X.append(user)
train_y.append(train_df[train_df["user_id"]==user_id]["label"])
elif user_id in set(dev_df['user_id']): # 如果用户在验证集中
valid_X.append(user)
valid_y.append(dev_df[dev_df['user_id'] == user_id]['label'])
else: # 如果用户在测试集中
test_X.append(user)

1
2
3
4
5
6
7
8
9
train_X, valid_X, test_X = np.array(train_X), np.array(valid_X), np.array(test_X)
train_y, valid_y, test_y = np.array(train_y), np.array(valid_y), np.array(test_y)
print(train_X.shape, train_y.shape)
print(valid_X.shape, valid_y.shape)
print(test_X.shape, test_y.shape)

feature_df = pd.DataFrame(np.concatenate((train_X, train_y),axis =1), columns=[ 'login', 'time', 'try', 'success', 'maxlevel', 'maxwin', 'maxfail', 'winof20', 'help', 'retry', 'duration', 'restep', 'label'])
feature_df.describe()

2.3 去除共线性变量

1
2
3
4
5
6
7
8
9
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# hist()方法对DataFrame中的所有列进行直方图绘制
feature_df.hist(bins=30,figsize=(20,15))
plt.tight_layout()
plt.show()
1
2
3
4
5
# 计算相关性
corr_matrix = feature_df.corr()
plt.figure(figsize=(20,10))
sns.heatmap(corr_matrix,annot=True,cmap="coolwarm")
plt.show()
image-20231114100342769

去除共线性比较高的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 热力图 可以考虑去掉相关性较高的变量。
# time和try有较强的相关性 maxfail和retry有较强的相关性 最后20把的胜率和总体胜率有较强的相关性
# 考虑删除大于0.9
row_indices, col_indices = np.where(np.abs(corr_matrix) >= 0.90)
col_set= set()
for row, col in zip(row_indices, col_indices):
if row != col:
col_set.add(corr_matrix.columns[col])
col_list = list(col_set)
for i in range(len(col_list)):
if i % 2==0:
feature_df = feature_df.drop(col_list[i],axis=1)
# 不考虑删除和结果关系较小的,比如最大连胜和帮助,因为可能存在非线性关系 根据经验来看 也不能删除
feature_df.head()

再画一下看一下

1
2
3
4
corr_matrix = feature_df.corr()
plt.figure(figsize=(20,10))
sns.heatmap(corr_matrix,annot=True,cmap="coolwarm")
plt.show()
image-20231114100433164
1
2
3
4
5
6
7
# 数据归一化
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
train_x = np.array(scaler.fit_transform(train_X))
valid_x = np.array(scaler.transform(valid_X))
test_x = np.array(scaler.transform(test_X))

3 模型训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn import svm, tree
from sklearn.naive_bayes import GaussianNB, MultinomialNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn import calibration
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

#CalibratedClassifierCV对LinearSVC进行校准,以获得概率输出
models = {
"LinearSVM":calibration.CalibratedClassifierCV(svm.LinearSVC(loss="squared_hinge",dual=False)),
"DecisionTree":tree.DecisionTreeClassifier(),
"GaussianNB":GaussianNB(),
"MultBayes":MultinomialNB(),
"Knn":KNeighborsClassifier()
}

这里使用calibration.CalibratedClassifierCV是想使用AdaBoostClassifier中的SAMME.R算法,使其变为可预测的概率输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from sklearn import metrics
from sklearn.ensemble import BaggingClassifier, AdaBoostClassifier
from sklearn.metrics import accuracy_score

for name,clf in models.items():
bcclf = BaggingClassifier(estimator=clf,n_estimators=50,max_samples=0.7,max_features=0.7,bootstrap=True,bootstrap_features=True,n_jobs=1,random_state=42)

bcclf.fit(train_x,train_y.flatten())
pre = [x[1] for x in bcclf.predict_proba(valid_x)]
fpr,tpr,thresholds = metrics.roc_curve(valid_y.flatten(),pre,pos_label=1)
auc = metrics.auc(fpr, tpr)
print("Bagging auc", name, auc)

if name!="Knn":
adclf = AdaBoostClassifier(estimator=clf,n_estimators=30,learning_rate=1,algorithm="SAMME.R")
adclf.fit(train_x,train_y.flatten())
pre = [x[1] for x in adclf.predict_proba(valid_x)]
fpr, tpr, thresholds = metrics.roc_curve(
valid_y.flatten(), pre, pos_label=1)
auc = metrics.auc(fpr, tpr)
print("Boosting auc", name, auc)

print()

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
Bagging auc LinearSVM 0.7558224372211488
Boosting auc LinearSVM 0.7494562735264745

Bagging auc DecisionTree 0.7437375912554002
Boosting auc DecisionTree 0.5925222528310731

Bagging auc GaussianNB 0.7425291698277446
Boosting auc GaussianNB 0.646366492173055

Bagging auc MultBayes 0.7529109817271267
Boosting auc MultBayes 0.7399392441333446

Bagging auc Knn 0.7597205912358178

knn和bagging svc和boosting svc都不错。


基于集成学习预测某类闯关游戏用户流失
http://example.com/2023/11/13/基于集成学习预测某类闯关游戏用户流失/
作者
Guoxin
发布于
2023年11月13日
许可协议