来自G胖的微笑:使用python监督学习预测Steam游戏打折的概率(二)

简介: 来自G胖的微笑:使用python监督学习预测Steam游戏打折的概率(二)

本期文章分为两期,第一篇我们先解决是否Steam平台的游戏会不会打折?下一期我们会详细分析影响Steam的打折因素640.gif


基本目标

使用有监督的机器学习分类模型来确定某款Steam游戏是否可以在正常一周内(没有大规模的折扣事件)出现折扣

640.png

数据

在Steam官网上获得的数据。

为了更容易访问,我们将收集的数据集上传到我的AWS实例中。为了访问数据,我们将使用外部Python软件包SQLAlchemy和独立的数据库工具DBeaver来与AWS服务器通信,以检查和清理数据。最后,数据库在PostgreSQL中处理完毕。

您可以找到我用来从Jupyter Notebook中加载此项目的数据的代码。

640.png

数据清洗

因为原始数据集包含许多空值,以及不同的大小写,例如('Free'与'free')。以下SQL语句将缩小范围,并在此过程中创建自定义视图“cleaned_raw”:

create view cleaned_raw as
select * from steam_3t_strat sts
where title is not null
and reviewcount is not null
and originalprice is not null
and afterdiscount is not null
and originalprice not like '%ree%'
and originalprice like '%.%'

现在我们已经去掉了一些空条目,现在我们对每一列进行格式化,使每一列的数据类型(int、float、string、datetime等)符合本身的含义:

delete from cleaned_raw where releasedate is null
create view baseline as
 select platform, reviewcount, positivepercent , releasedate::date,
        cast(originalprice as double precision) , discountpercentage , alltags
 from cleaned_raw;

准备好分析数据之前的最后几个步骤将包含一些基本的特征工程:

  1. 将列“platform” 二值化为“multi platform” (也即这一款游戏是否在多平台上发售);
  2. 在原始的“discount percentage”列基础上创建一个模型需要预测的目标列,其含义为0代表没有打折,1代表打折。
  3. 最后,从上一个项目中,我们知道“days_since_release”(游戏发售多久了)是最重要的特性之一,因此我们将从“release date”列进行特征工程,但这次是用SQL完成的。

现在,有了新视图“basic_fe”,我们就可以在Jupyter Notebook中进行特征工程模型构建

探索性数据分析

由于这是我上一个项目的延续,大部分的EDA都已经完成了,这里我们将直接进入特征工程。

定义评价指标

对于这个项目,模型能够同时达到有效与平衡。因此,我将主要研究两个指标:

  • ROC-AUC评分(模型有效性)
  • F1分数(查准率和查全率之间的调和平均值)

注:我也会看查准率和查全率的平衡,例如,如果查准率是0.8,查全率是0.02,这对我来说太不平衡了,所以即使它产生了一个整体更好的F1分数,我也不会采取这种模式。

建立基线模型

现在我们进入该项目最有趣的部分,但首先我们导入在AWS进行数据清洗后的特征并建立基线模型,以便我们可以将其与将来的模型进行比较。

basic_fe = pd.read_sql_query('''SELECT * FROM basic_fe''', cnx)

640.png

在我们进一步讨论之前,让我们先来观察一下类别不平衡性

640.png

类别不平衡非常严重,但我们可以对少数类计算类别权重,用于分类模型的构建:

640.png

现在,在删除一些不需要的观察值和列之后,运行模型:

# copy the feature engineered view for exploratory purposes
df = basic_fe.copy()
# dropping unneeded columns
df.drop(['platform', 'releasedate', 'alltags', 'discountpercentage'], axis=1, inplace = True)
# cleaning NaN values
df.fillna(0, inplace=True)
# splitting data and target
X, y = df.drop('onsale',axis=1), df['onsale']
# run baseline models (train-test split is conducted within this function)
baseline_model = classification(X, y, {0: 1, 1: 15})

请注意,我正在运行的classification()函数是一个自定义函数,包含7种模型,其中包括:

  1. 最近邻分类器(n_neighbors=5)
  2. 逻辑回归(C=0.95)
  3. 高斯型朴素贝叶斯分类器
  4. 支持向量机(gamma=’auto’,probability=True)
  5. 决策树(random_state=5)*
  6. 随机森林(random_state=5)
  7. 梯度提升机(n_estimators = 90, max_depth = 100)

classification()函数将对每个模型打印AUC值与ROC曲线。它还将自动确定并返回七个模型中评估指标最高的模型以及特征重要性分数。在这种情况下,对于基线模型:

640.png

特征工程

由于该项目的重点是利用手头的可用数据获得最佳模型,因此我们将不得不在迭代过程中尝试使用不同的特征工程方法。

以下是我在此项目中使用的三种有效方法,尽管过程绝对不那么顺利。

总结一下我的过程,整体流程如下:

image.png

1. 特征变换

首先,让我们用从StackOverflow获得的一个很有用的函数对特征进行多项式变换:

def PolynomialFeatures_labeled(input_df,power):
   '''
  Basically this is a cover for the sklearn preprocessing function.
  The problem with that function is if you give it a labeled dataframe, it ouputs an unlabeled dataframe with potentially
  a whole bunch of unlabeled columns.
  Inputs:
  input_df = Your labeled pandas dataframe (list of x's not raised to any power)
  power = what order polynomial you want variables up to. (use the same power as you want entered into pp.PolynomialFeatures(power) directly)
  Output:
  Output: This function relies on the powers_ matrix which is one of the preprocessing function's outputs to create logical labels and
  outputs a labeled pandas dataframe  
  '''
   poly = PolynomialFeatures(power)
   output_nparray = poly.fit_transform(input_df)
   powers_nparray = poly.powers_
   input_feature_names = list(input_df.columns)
   target_feature_names = ["Constant Term"]
   for feature_distillation in powers_nparray[1:]:
       intermediary_label = ""
       final_label = ""
       for i in range(len(input_feature_names)):
           if feature_distillation[i] == 0:
               continue
           else:
               variable = input_feature_names[i]
               power = feature_distillation[i]
               intermediary_label = "%s^%d" % (variable,power)
               if final_label == "":         #If the final label isn't yet specified
                   final_label = intermediary_label
               else:
                   final_label = final_label + " x " + intermediary_label
       target_feature_names.append(final_label)
   output_df = pd.DataFrame(output_nparray, columns = target_feature_names)
   return output_df
构建模型并返回分类评价指标:
# poly transform the data
explore_X_poly=PolynomialFeatures_labeled(X,2)
# run modeling to see metrics
classification(explore_X_poly, y, {0: 1, 1: 15})

image.png

可以看到相比基线模型我们的结果还稍微变差了一些,看来多项式变换无法完成这一分类任务,下面我们换另一种方法:

2. 特征结合

我编写了一个自定义算法来探索不同的特征组合,看这样做是否可以提高模型分数:

def explore_fe(df, target):
   '''
  A function to do exploratory feature engineering.
  It's flexible in its purpose, and is currently configured for this project only.
  Inputs:
  df (like X) = Your dataset without the target (y)
  target (like y) = Your target, whatever you are trying to predict ---Binary.
  Output:
  Returns engineered X (dataframe without target) based on the engineering logic.
  '''
   df = df.astype(float)
   df = df.replace({0:1 , 1:2})
   for i in range (0, len(df.columns)):
       df[f'{df.columns[i]}^2'] = np.square(df[df.columns[i]])
       df[f'{df.columns[i]}^1/2'] = np.sqrt(df[df.columns[i]])
       df[f'{df.columns[i]} * {df.columns[i+1]}'] = df[df.columns[i]] * df[df.columns[i+1]]
       df[f'{df.columns[i]} / {df.columns[i+1]}'] = df[df.columns[i]] / df[df.columns[i+1]]
#         df[f'{df.columns[i]} + {df.columns[i+1]}'] = df[df.columns[i]] + df[df.columns[i+1]]
#         df[f'{df.columns[i]} - {df.columns[i+1]}'] = df[df.columns[i]] - df[df.columns[i+1]]
#     df.fillna(0, inplace = True)
#     df.replace([np.inf, -np.inf], np.nan).dropna(axis=1)
   df[~df.isin([np.nan, np.inf, -np.inf]).any(1)].astype(np.float64)
   X,y= df, target
   X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state=4444)
   ran = RandomForestClassifier(random_state=5)
   ran.fit(X_train, y_train)
   print ('Accuracy: ', accuracy_score(y_test, ran.predict(X_test)))
   print("Precision: {:6.4f},   Recall: {:6.4f},   f1: {:6.4f}".format(precision_score(y_test, ran.predict(X_test)),
                                                recall_score(y_test, ran.predict(X_test)), f1_score(y_test, ran.predict(X_test))), '\n')
   k = list(X.columns)
   pp = pprint.PrettyPrinter(indent=4)
   pp.pprint(sorted(list(zip(k, ran.feature_importances_)), key=lambda x: x[1], reverse=True))
   return df

这一函数基本就是对所有特征逐列进行特征的乘除操作以非线性组合出新特征。处理完毕后我们再次运行模型并且获得了以下结果:

image.png

3. 特征选择

从最后两个步骤开始,我现在有了一个包含21组特征的的数据集,其中16组特征都是经过特征工程获得的,这可能会导致过拟合,并且“噪声”水平可能过高。因此,我们对特征进行了特征选择:

def feature_selection(X, y, score_to_keep = 5):
   '''
  A function to select features by votes of 6 models who can calculate feature importances.
  Also prints out how many original features there are, how many selected, and a list of selected features.
  Original idea from https://www.kaggle.com/mlwhiz/feature-selection-using-football-data
  Inputs:
  X = Your dataset without the target (y)
  y = Your target, whatever you are trying to predict --- Binary.
  score_to_keep = Pick features that have a 'score_to_keep' amount of votes --- max is 6 votes, default is 5.
  Output:
  Returns selected_X as a dataframe without target(y).
  '''
   feature_name = list(X.columns)
   num_feats=len(X.columns)
   def cor_selector(X, y,num_feats):
       cor_list = []
       feature_name = X.columns.tolist()
       # calculate the correlation with y for each feature
       for i in X.columns.tolist():
           cor = np.corrcoef(X[i], y)[0, 1]
           cor_list.append(cor)
       # replace NaN with 0
       cor_list = [0 if np.isnan(i) else i for i in cor_list]
       # feature name
       cor_feature = X.iloc[:,np.argsort(np.abs(cor_list))[-num_feats:]].columns.tolist()
       # feature selection? 0 for not select, 1 for select
       cor_support = [True if i in cor_feature else False for i in feature_name]
       return cor_support, cor_feature
   cor_support, cor_feature = cor_selector(X, y,num_feats)
   X_norm = MinMaxScaler().fit_transform(X)
   chi_selector = SelectKBest(chi2, k=num_feats)
   chi_selector.fit(X_norm, y)
   chi_support = chi_selector.get_support()
   chi_feature = X.loc[:,chi_support].columns.tolist()
   rfe_selector = RFE(estimator=LogisticRegression(), n_features_to_select=num_feats, step=10, verbose=5)
   rfe_selector.fit(X_norm, y)
   rfe_support = rfe_selector.get_support()
   rfe_feature = X.loc[:,rfe_support].columns.tolist()
   embeded_lr_selector = SelectFromModel(LogisticRegression(penalty="l2"), max_features=num_feats)
   embeded_lr_selector.fit(X_norm, y)
   embeded_lr_support = embeded_lr_selector.get_support()
   embeded_lr_feature = X.loc[:,embeded_lr_support].columns.tolist()
   embeded_rf_selector = SelectFromModel(RandomForestClassifier(n_estimators=100), max_features=num_feats)
   embeded_rf_selector.fit(X_norm, y)
   embeded_rf_support = embeded_rf_selector.get_support()
   embeded_rf_feature = X.loc[:,embeded_rf_support].columns.tolist()
   lgbc=LGBMClassifier(n_estimators=500, learning_rate=0.05, num_leaves=32, colsample_bytree=0.2,
           reg_alpha=3, reg_lambda=1, min_split_gain=0.01, min_child_weight=40)
   embeded_lgb_selector = SelectFromModel(lgbc, max_features=num_feats)
   embeded_lgb_selector.fit(X_norm, y)
   embeded_lgb_support = embeded_lgb_selector.get_support()
   embeded_lgb_feature = X.loc[:,embeded_lgb_support].columns.tolist()
   feature_selection_df = pd.DataFrame({'Feature':feature_name, 'Pearson':cor_support, 'Chi-2':chi_support, 'RFE':rfe_support, 'Logistics':embeded_lr_support,
                                       'Random Forest':embeded_rf_support, 'LightGBM':embeded_lgb_support})
   feature_selection_df['Total'] = np.sum(feature_selection_df, axis=1)
   feature_selection_df = feature_selection_df.sort_values(['Total','Feature'] , ascending=False)
   feature_selection_df.index = range(1, len(feature_selection_df)+1)
   selected_X = X.copy()
   to_drop = []
   for i in range (0, len(feature_selection_df)):
       if feature_selection_df.Total.values[i] < score_to_keep:
           to_drop.append(feature_selection_df.Feature.values[i])
   selected_X = selected_X.drop(to_drop, axis = 1)
   print ("Number of orginal features: ", num_feats)
   print ("Number of selected features: ", len(selected_X.columns), '\n')
   pp = pprint.PrettyPrinter(indent=4)
   print("Selected Features:")
   pp.pprint(list(selected_X.columns))
   return selected_X

结合步骤1和2,然后根据投票数选择有效特征,以下为运行结果:


image.png

我们最终制作了一个比基线模型稍好的模型。我对这个基于有限数据的模型很满意,但我们还并没有完成,让我们试着通过调整阈值使它变得更好。

阈值调整

阈值调整不仅仅是建模的最后一步,这对模型的表现也至关重要,因为调整查准率与查全率之间的平衡——也即阈值——直接影响最后评估指标。

我们将使用以下代码检查最佳阈值:

# Splitting the data again to make sure we are using the dataset that was fed into the best model
X, y = exp_Xpoly_sel_5, y
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state=42)
X_val, y_val = X_test, y_test # explicitly calling this validation since we're using it for selection
thresh_ps = np.linspace(.10,.50,1000)
model_val_probs = best_model.predict_proba(X_val)[:,1] # positive class probs, same basic logistic model we fit in section 2
f1_scores, prec_scores, rec_scores, acc_scores = [], [], [], []
for p in thresh_ps:
   model_val_labels = model_val_probs >= p
   f1_scores.append(f1_score(y_val, model_val_labels))    
   prec_scores.append(precision_score(y_val, model_val_labels))
   rec_scores.append(recall_score(y_val, model_val_labels))
   acc_scores.append(accuracy_score(y_val, model_val_labels))
plt.plot(thresh_ps, f1_scores)
plt.plot(thresh_ps, prec_scores)
plt.plot(thresh_ps, rec_scores)
plt.plot(thresh_ps, acc_scores)
plt.title('Metric Scores vs. Positive Class Decision Probability Threshold')
plt.legend(['F1','Precision','Recall','Accuracy'])
plt.xlabel('P threshold')
plt.ylabel('Metric score')
best_f1_score = np.max(f1_scores)
best_thresh_p = thresh_ps[np.argmax(f1_scores)]
print('best_model best F1 score %.3f at prob decision threshold >= %.3f'
     % (best_f1_score, best_thresh_p))

结论

现在我们有了阈值为0.45,ROC-AUC分数为0.7097的最佳模型。

既然我们已经建立了这个模型,那么为什么不用呢?所以我创建了一个把我先前的项目与这个项目结合在一起的应用程序。

image.png

目录
相关文章
|
1月前
|
存储 Java C语言
【python】——使用嵌套列表实现游戏角色管理
【python】——使用嵌套列表实现游戏角色管理
35 0
|
3天前
|
算法 Python
Python 一步一步教你用pyglet制作汉诺塔游戏
Python 一步一步教你用pyglet制作汉诺塔游戏
16 0
|
16天前
|
存储 Python
如何使用Python实现“猜数字”游戏
本文介绍了使用Python实现“猜数字”游戏的过程。游戏规则是玩家在给定范围内猜一个由计算机随机生成的整数,猜对则获胜。代码中,首先导入random模块生成随机数,然后在循环中获取玩家输入并判断大小,提供猜小、猜大提示。通过增加猜测次数限制、难度选择、优化输入提示和图形化界面等方式可优化游戏。这篇文章旨在帮助初学者通过实际操作学习Python编程。
28 2
|
1月前
|
机器学习/深度学习 算法 数据挖掘
python数据分析——在数据分析中有关概率论的知识
参数和统计量在数据分析中起着至关重要的作用。参数是对总体特征的描述,如均值、方差等,而统计量则是基于样本数据计算得出的,用于估计或推断总体参数的值。 在统计学中,参数通常被视为未知的固定值,而统计量则是随机变量,因为它们的值会随着样本的不同而变化。这种差异使得统计量在推断总体参数时具有重要意义。例如,我们可以通过计算样本均值来估计总体均值,这就是一个典型的统计量应用。
72 1
|
1月前
|
存储 Python Windows
10分钟学会用python写游戏,实例教程
10分钟学会用python写游戏,实例教程
36 0
|
2月前
|
Python
Python猜字游戏是一种常见的编程练习
Python猜字游戏是一种常见的编程练习
24 2
|
2月前
|
Python
用 Python 写一个猜数字游戏并运行它
用 Python 写一个猜数字游戏并运行它
15 0
|
2月前
|
机器学习/深度学习 数据采集 算法
GEE python——基于多源遥感影像和随机森林分类器进行洪水概率预测
GEE python——基于多源遥感影像和随机森林分类器进行洪水概率预测
51 0
|
2月前
|
UED 开发者 Python
制作你的第一个 Python 游戏
想要制作一个 Python 游戏?这是一个令人兴奋的项目!在这篇文章中,我将引导你完成制作第一个 Python 游戏的步骤。即使你没有编程经验,也不用担心,我们将从基础开始,一起探索游戏开发的乐趣。
|
2月前
|
计算机视觉 Python
用 Python 开发简单的游戏
游戏开发是一个充满乐趣和挑战的领域,而 Python 作为一种强大的编程语言,为游戏开发提供了丰富的工具和可能性。在本文中,我们将探讨如何使用 Python 开发简单的游戏,并提供一些基本的示例和指导。