Sklearn机器学习——预测明天是否会下雨

   

目录

    

1 案例介绍

1.1 案例目的

1.2 案例数据

2 代码实现

2.1 导库导数据、探索特征

2.1.1 导入需要的库

2.1.2 导入数据、探索数据

2.2 分集,优先探索数据

3 探索特征,开始处理特征矩阵

3.1 描述性统计和异常值

3.2 处理困难特征:日期

3.4 处理困难特征:地点

3.5 处理分类型变量:缺失值

3.6 处理分类型变量:将分类型变量编码

3.7 处理连续型变量:填补缺失值

3.8 处理分类型变量:将分类型变量编码

3.9 连续型变量:填补缺失值

3.10 处理连续型变量:无量纲化

4 建模与模型评估

4.1 模型调参

4.1.1 追求最高recall

4.1.2 追求最高准确率

4.1.3 追求平衡

1 案例介绍

在实际工作中,数据预处理往往比建模难得多,耗时多得多,因此合理的数据预处理是非常必要的。考虑到大家渴 望学习真实数据上的预处理的需求,以及SVM需要在比较规则的数据集上来表现的特性,我为大家准备了这个 Kaggle上下载的,未经过预处理的澳大利亚天气数据集。我们的目标是在这个数据集上来预测明天是否会下雨。

1.1 案例目的

这个案例的核心目的,是通过巧妙的预处理和特征工程来向大家展示,在现实数据集上我们往往如何做数据预处 理,或者我们都有哪些预处理的方式和思路。预测天气是一个非常非常困难的主题,因为影响天气的因素太多,而 Kaggle的这份数据也丝毫不让我们失望,是一份非常难的数据集,难到我们目前学过的所有算法在这个数据集上都 不会有太好的结果,尤其是召回率recall,异常地低。

1.2 案例数据

在这里,我为大家抛砖引玉,在这个15W行数据的数据集 上,随机抽样5000个样本来为大家演示我的数据预处理和特征工程的过程,为大家提供一些数据预处理和特征工 程的思路。不过,特征工程没有标准答案,因此大家应当多尝试,希望使用原数据集的小伙伴们可以到Kaggle下载 最原始版。

kaggle下载链接:Rain in Australia | Kaggle

2 代码实现

2.1 导库导数据、探索特征

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split

2.1.1 导入需要的库

2.1.2 导入数据、探索数据

weather = pd.read_csv(r"C:\work\learnbetter\micro-class\week 8 SVM (2)\data\weatherAUS5000.csv",index_col=0)
weather.head()

 5 rows × 22 columns

#将特征矩阵和标签Y分开
X = weather.iloc[:,:-1]
Y = weather.iloc[:,-1]
X.info()
#探索缺失值
X.isnull().mean() #缺失值所占总值的比例 isnull().sum(全部的True)/X.shape[0]
#我们要有不同的缺失值填补策略
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5000 entries, 0 to 4999
Data columns (total 21 columns):
Date             5000 non-null object
Location         5000 non-null object
MinTemp          4979 non-null float64
MaxTemp          4987 non-null float64
Rainfall         4950 non-null float64
Evaporation      2841 non-null float64
Sunshine         2571 non-null float64
WindGustDir      4669 non-null object
WindGustSpeed    4669 non-null float64
WindDir9am       4651 non-null object
WindDir3pm       4887 non-null object
WindSpeed9am     4949 non-null float64
WindSpeed3pm     4919 non-null float64
Humidity9am      4936 non-null float64
Humidity3pm      4880 non-null float64
Pressure9am      4506 non-null float64
Pressure3pm      4504 non-null float64
Cloud9am         3111 non-null float64
Cloud3pm         3012 non-null float64
Temp9am          4967 non-null float64
Temp3pm          4912 non-null float64
dtypes: float64(16), object(5)
memory usage: 859.4+ KB

一看有缺失值

np.unique(Y) #我们的标签是二分类
array(['No', 'Yes'], dtype=object)

2.2 分集,优先探索数据

  • 之前的数据都是先预处理,再分测试集和训练集,但现实是相反的。我们不希望我们建模过程受到测试集数据的影响。否则的话,就相当于提起告知模型一部分预测的答案。所以这个天气预测数据,我们先分测试集和训练集,再进行数据的预处理。
#分训练集和测试集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,Y,test_size=0.3,random_state=420) #随机抽样

 我们看到索引其实是乱掉的,所以我们要恢复索引。

恢复索引
for i in [Xtrain, Xtest, Ytrain, Ytest]:
    i.index = range(i.shape[0])
  • 是否存在样本不均衡问题。
Ytrain.value_counts()
Ytest.value_counts()
No     2704
Yes     796
Name: RainTomorrow, dtype: int64
No     1157
Yes     343
Name: RainTomorrow, dtype: int64
  • 求不平衡的比例
Ytrain.value_counts()[0]/Ytrain.value_counts()[1]
  • LabelEncounter是允许一维数据输入的,这里运用训练集里面数据进行实例化,如果我们的测试集中,出现了训练集中没有出现过的标签类别,比如说,测试集中有YES,NO,UNKNOWN 。
    而我们的训练集中只有YES和NO。就考虑重新建模。
#将标签编码
from sklearn.preprocessing import LabelEncoder #标签专用,第三章讲过
encorder = LabelEncoder().fit(Ytrain) #允许一维数据的输入的
#认得了:有两类,YES和NO,YES是1,NO是0
#使用训练集进行训练,然后在训练集和测试集上分别进行transform
Ytrain = pd.DataFrame(encorder.transform(Ytrain))
Ytest = pd.DataFrame(encorder.transform(Ytest))
#如果我们的测试集中,出现了训练集中没有出现过的标签类别
#比如说,测试集中有YES, NO, UNKNOWN
#而我们的训练集中只有YES和NO
  •  在现实中,数据非常复杂,一旦标签被覆盖需要重新运行,所以要把Ytrain给保存一下。
Ytrain.to_csv("你想要保存这个文件的地址.文件名.csv")

3 探索特征,开始处理特征矩阵

3.1 描述性统计和异常值

  • 描述性数据后面括号是分位数,默认是25%,50%和75%
  • 对于去kaggle上下载了数据的小伙伴们,以及对于坚持要使用完整版数据的(15W行)数据的小伙伴们 。如果你发现了异常值,首先你要观察,这个异常值出现的频率 ,如果异常值只出现了一次,多半是输入错误,直接把异常值删除 #如果异常值出现了多次,去跟业务人员沟通,人为造成的错误异常值留着是没有用的 。
  • 如果异常值占到你总数据量的10%左右了 。把异常值替换成非异常但是非干扰的项,比如说用0来进行替换,或者把异常当缺失,用众数或者均值来填补。
#描述性统计
Xtrain.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T

Xtest.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T

 Xtrain

Xtest 

#先查看原始的数据结构
Xtrain.shape
Xtest.shape
#观察异常值是大量存在,还是少数存在
Xtrain.loc[Xtrain.loc[:,"Cloud9am"] == 9,"Cloud9am"]
Xtest.loc[Xtest.loc[:,"Cloud9am"] == 9,"Cloud9am"]
Xtest.loc[Xtest.loc[:,"Cloud3pm"] == 9,"Cloud3pm"]
#少数存在,于是采取删除的策略
#注意如果删除特征矩阵,则必须连对应的标签一起删除,特征矩阵的行和标签的行必须要一一对应
Xtrain = Xtrain.drop(index = 71737)
Ytrain = Ytrain.drop(index = 71737)
#删除完毕之后,观察原始的数据结构,确认删除正确
Xtrain.shape
Xtest = Xtest.drop(index = [19646,29632])
Ytest = Ytest.drop(index = [19646,29632])
Xtest.shape
#进行任何行删除之后,千万记得要恢复索引
for i in [Xtrain, Xtest, Ytrain, Ytest]:
    i.index = range(i.shape[0])
Xtrain.head()
Xtest.head()

3.2 处理困难特征:日期

我们采集数据的日期是否和我们的天气有关系?我们可以探索一下我们的采集日期有什么样的顺序?

  • 日期是一年分了365类的分类型变量。我们的日期特征中,日期是否有重复。
     
type(Xtrain.iloc[0,0]) #字符串,需要把他处理成数字
Xtrain.iloc[:,0].value_counts().count()

 2141类

  • 如果我们把它当作分类型变量处理,类别太多,有2141类,如果换成数值型,会被直接当成连续型变量,如果做成哑变量,我们特征的维度会爆炸。

  • 首先,日期不是独一无二的,日期有重复
    其次,在我们分训练集和测试集之后,日期也不是连续的,而是分散的
    某一年的某一天倾向于会下雨?或者倾向于不会下雨吗?
    不是日期影响了下雨与否,反而更多的是这一天的日照时间,湿度,温度等等这些因素影响了是否会下雨
    光看日期,其实感觉它对我们的判断并无直接影响
    如果我们把它当作连续型变量处理,那算法会人为它是一系列1~3000左右的数字,不会意识到这是日期

如果我们的思考简单一些,我们可以直接删除日期这个特征。首先它不是一个直接影响我们标签的特征,并且要处 理日期其实是非常困难的。如果大家认可这种思路,那可以直接运行下面的代码来删除我们的日期

Xtrain = Xtrain.drop(["Date"],axis=1)
Xtest = Xtest.drop(["Date"],axis=1)

  但在这里,很多人可能会持不同意见,怎么能够随便删除一个特征(哪怕我们已经觉得它可能无关)?如果我们要 删除,我们可能需要一些统计过程,来判断说这个特征确实是和标签无关的,那我们可以先将“日期”这个特征编码 后对它和标签做方差齐性检验(ANOVA),如果检验结果表示日期这个特征的确和我们的标签无关,那我们就可 以愉快地删除这个特征了。但要编码“日期”这个特征,就又回到了它到底是否会被算法当成是分类变量的问题上。

  其实我们可以想到,日期必然是和我们的结果有关的,它会从两个角度来影响我们的标签: 首先,我们可以想到,昨天的天气可能会影响今天的天气,而今天的天气又可能会影响明天的天气。也就是说,随 着日期的逐渐改变,样本是会受到上一个样本的影响的。但是对于算法来说,普通的算法是无法捕捉到样本与样本 之间的联系的,我们的算法捕捉的是样本的每个特征与标签之间的联系(即列与列之间的联系),而无法捕捉样本 与样本之间的联系(行与行的联系)。 要让算法理解上一个样本的标签可能会影响下一个样本的标签,我们必须使用时间序列分析。

时间序列分析是指将 同一统计指标的数值按其发生的时间先后顺序排列而成的数列。时间序列分析的主要目的是根据已有的历史数据对 未来进行预测。然而,(据我所知)时间序列只能在单调的,唯一的时间上运行,即一次只能够对一个地点进行预 测,不能够实现一次性预测多个地点,除非进行循环。而我们的时间数据本身,不是单调的,也不是唯一的,经过 抽样之后,甚至连连续的都不是了,我们的时间是每个混杂在多个地点中,每个地点上的一小段时间。如何使用时间序列来处理这个问题,就会变得复杂。

Xtrainc = Xtrain.copy()
Xtrainc.sort_values(by="Location")
#不同地点上一段相似的时间的数据
Xtrain.loc[Xtrain.iloc[:,0] == "2015-08-24",:]

 我们发现同一日期下,有两个不同地点。你要明天会下雨,也没说明哪里会下雨。所以时间序列的方法也不成立。

  那我们可以换一种思路,既然算法处理的是列与列之间的关系,我是否可以把”今天的天气会影响明天的天气“这个 指标转换成一个特征呢?我们就这样来操作。 我们观察到,我们的特征中有一列叫做“Rainfall",这是表示当前日期当前地区下的降雨量,换句话说,也就是”今 天的降雨量“。凭常识我们认为,今天是否下雨,应该会影响明天是否下雨,比如有的地方可能就有这样的气候, 一旦下雨就连着下很多天,也有可能有的地方的气候就是一场暴雨来得快去的快。因此,我们可以将时间对气候的 连续影响,转换为”今天是否下雨“这个特征,巧妙地将样本对应标签之间的联系,转换成是特征与标签之间的联系 了。

  • loc[a,b]=c表示找出条件满足a的行并且在b这一列中填充yes或者no,没有raintoday这一列我就增加一列
Xtrain["Rainfall"].head(20)
Xtrain["Rainfall"].isnull().sum()##33个空值
#假设你没有下雨
#复制你的空值
Xtrain.loc[Xtrain["Rainfall"] >= 1,"RainToday"] = "Yes"
Xtrain.loc[Xtrain["Rainfall"] < 1,"RainToday"] = "No"
Xtrain.loc[Xtrain["Rainfall"] == np.nan,"RainToday"] = np.nan
Xtrain.loc[:,"RainToday"].value_counts()
No     2642
Yes     825
Name: RainToday, dtype: int64
  • apply是对dataframe上的某一列进行处理的一个函数
    ambda x 匿名函数,请在dataframe上这一列中的每一行帮我执行冒号后的命令
int(Xtrain.loc[0,"Date"].split("-")[1]) #提取出月份

    2015-8-15先用"-"分开,然后取第二个元素

Xtrain["Date"] = Xtrain["Date"].apply(lambda x:int(x.split("-")[1]))
#apply是对dataframe上的某一列进行处理的一个函数
#lambda x匿名函数,请在dataframe上这一列中的每一行帮我执行冒号后的命令
Xtrain.loc[:,"Data"].value_counts()
3     334    这下就变成了12类,日期都变成了以月份进行分类。
5     324
7     316
9     302
6     302
1     300
11    299
10    282
4     265
2     264
12    259
8     253
Name: Date, dtype: int64
  • 替换完毕后,我们需要修改列的名称
    rename是比较少用的,可以用来修改单个列名的函数
    我们通常都直接使用 df.columns = 某个列表 这样的形式来一次修改所有的列名
    但rename允许我们只修改某个单独的列

  • 这样就把date改成month
Xtrain = Xtrain.rename(columns={"Date":"Month"})
Xtest["Date"] = Xtest["Date"].apply(lambda x:int(x.split("-")[1]))
Xtest = Xtest.rename(columns={"Date":"Month"})

3.4 处理困难特征:地点

地点又是非常tricky的。常识上来说,我们认为地点肯定是对明天是否会下雨存在影响的。比如说,如 果其他信息都不给出,我们只猜测,“伦敦明天是否会下雨”和”北京明天是否会下雨“,我一定会猜测伦敦会下雨, 而北京不会,因为伦敦是常年下雨的城市,而北京的气候非常干燥。对澳大利亚这样面积巨大的国家来说,必然存 在着不同的城市有着不同的下雨倾向的情况。但尴尬的是,和时间一样,我们输入地点的名字对于算法来说,就是 一串字符,"London"和"Beijing"对算法来说,和0,1没有区别。同样,我们的样本中含有49个不同地点,如果做 成分类型变量,算法就无法辨别它究竟是否是分类变量。也就是说,我们需要让算法意识到,不同的地点因为气候 不同,所以对“明天是否会下雨”有着不同的影响。如果我们能够将地点转换为这个地方的气候的话,我们就可以将 不同城市打包到同一个气候中,而同一个气候下反应的降雨情况应该是相似的。

那我们如何将城市转换为气候呢?我在google找到了如下地图:

 这是由澳大利亚气象局和澳大利亚建筑规范委员会(ABCB)制作统计的,澳大利亚不同地区不同城市的所在的气 候区域划分。总共划分为八个区域,非常适合我们用来做分类。如果能够把49个地点转换成八种不同的气候,这个 信息应该会对是否下雨的判断比较有用。基于气象局和ABCB的数据,我为大家制作了澳大利亚主要城市所对应的 气候类型数据,并保存在csv文件city_climate.csv当中。然后,我使用以下代码,在google上进行爬虫,爬出了每 个城市所对应的经纬度,并保存在数据cityll.csv当中,大家可以自行导入,来查看这个数据。 

为什么我们会需要城市的经纬度呢?我曾经尝试过直接使用样本中的城市来爬取城市本身的气候,然而由于样本中 的地点名称,其实是气候站的名称,而不是城市本身的名称,因此不是每一个城市都能够直接获取到城市的气候。 比如说,如果我们搜索“海淀区气候”,搜索引擎返回的可能是海淀区现在的气温,而不是整个北京的气候类型。因 此,我们需要澳大利亚气象局的数据,来找到这些气候站所对应的城市。 我们有了澳大利亚全国主要城市的气候,也有了澳大利亚主要城市的经纬度(地点),我们就可以通过计算我们样 本中的每个气候站到各个主要城市的地理距离,来找出一个离这个气象站最近的主要城市,而这个主要城市的气候 就是我们样本点所在的地点的气候。 让我们把cityll.csv和cityclimate.csv来导入,来看看它们是什么样子:

cityll = pd.read_csv(r"C:\work\learnbetter\micro-class\week 8 SVM (2)\cityll.csv",index_col=0)
city_climate = pd.read_csv(r"C:\work\learnbetter\micro-class\week 8 SVM (2)\Cityclimate.csv")

#%%

cityll.head() #每个城市对应的经纬度,这些城市是澳大利亚统计局做的那张地图上的城市

接下来,我们来将这两张表处理成可以使用的样子,首先要去掉cityll中经纬度上带有的度数符号,然后要将两张表 合并起来

#去掉度数符号
cityll["Latitudenum"] = cityll["Latitude"].apply(lambda x:float(x[:-1]))
cityll["Longitudenum"] = cityll["Longitude"].apply(lambda x:float(x[:-1]))
#观察一下所有的经纬度方向都是一致的,全部是南纬,东经,因为澳大利亚在南半球,东半球
#所以经纬度的方向我们可以舍弃了
citylld = cityll.iloc[:,[0,5,6]]
#将city_climate中的气候添加到我们的citylld中
citylld["climate"] = city_climate.iloc[:,-1]
citylld.head()

接下来,我们如果想要计算距离,我们就会需要所有样本数据中的城市。我们认为,只有出现在训练集中的地点才 会出现在测试集中,基于这样的假设,我们来爬取训练集中所有的地点所对应的经纬度,并且保存在一个csv文件 samplecity.csv中:

#首先使用radians将角度转换成弧度
from math import radians, sin, cos, acos
citylld.loc[:,"slat"] = citylld.iloc[:,1].apply(lambda x : radians(x))
citylld.loc[:,"slon"] = citylld.iloc[:,2].apply(lambda x : radians(x))
samplecityd.loc[:,"elat"] = samplecityd.iloc[:,1].apply(lambda x : radians(x))
samplecityd.loc[:,"elon"] = samplecityd.iloc[:,2].apply(lambda x : radians(x))

import sys
for i in range(samplecityd.shape[0]):
    slat = citylld.loc[:,"slat"]
    slon = citylld.loc[:,"slon"]
    elat = samplecityd.loc[i,"elat"]
    elon = samplecityd.loc[i,"elon"]
    dist = 6371.01 * np.arccos(np.sin(slat)*np.sin(elat) + 
                          np.cos(slat)*np.cos(elat)*np.cos(slon.values - elon))
    city_index = np.argsort(dist)[0]
    #每次计算后,取距离最近的城市,然后将最近的城市和城市对应的气候都匹配到samplecityd中
    samplecityd.loc[i,"closest_city"] = citylld.loc[city_index,"City"]
    samplecityd.loc[i,"climate"] = citylld.loc[city_index,"climate"]

#查看最后的结果,需要检查城市匹配是否基本正确
samplecityd.head(5)
#查看气候的分布
samplecityd["climate"].value_counts()

#确认无误后,取出样本城市所对应的气候,并保存
locafinal = samplecityd.iloc[:,[0,-1]]
locafinal.head()
locafinal.columns = ["Location","Climate"]
#在这里设定locafinal的索引为地点,是为了之后进行map的匹配
locafinal = locafinal.set_index(keys="Location")
locafinal
locafinal.to_csv(r"C:\work\learnbetter\micro-class\week 8 SVM (2)\samplelocation.csv")

有了每个样本城市所对应的气候,我们接下来就使用气候来替掉原本的城市,原本的气象站的名称。在这里,我们 可以使用map功能,map能够将特征中的值一一对应到我们设定的字典中,并且用字典中的值来替换样本中原本的 值,我们在评分卡中曾经使用这个功能来用WOE替换我们原本的特征的值。

#是否还记得训练集长什么样呢?
Xtrain.head()
#将location中的内容替换,并且确保匹配进入的气候字符串中不含有逗号,气候两边不含有空格
#我们使用re这个模块来消除逗号
#re.sub(希望替换的值,希望被替换成的值,要操作的字符串) #去掉逗号
#x.strip()是去掉空格的函数
#把location替换成气候的是我们的map的映射
import re
#气象站的名字替换成了对应的城市对应的气候
Xtrain["Location"] = Xtrain["Location"].map(locafinal.iloc[:,0])
Xtrain.head()
Xtest.head()

到这里,地点就处理完毕了。其实,我们还没有将这个特征转化为数字,即还没有对它进行编码。我们稍后和其他 的分类型变量一起来编码。

3.5 处理分类型变量:缺失值

  接下来,我们总算可以开始处理我们的缺失值了。首先我们要注意到,由于我们的特征矩阵由两种类型的数据组 成:分类型和连续型,因此我们必须对两种数据采用不同的填补缺失值策略。传统地,如果是分类型特征,我们则 采用众数进行填补。如果是连续型特征,我们则采用均值来填补。 此时,由于我们已经分了训练集和测试集,我们需要考虑一件事:究竟使用哪一部分的数据进行众数填补呢?答案 是,使用训练集上的众数对训练集和测试集都进行填补。为什么会这样呢?按道理说就算用测试集上的众数对测试 集进行填补,也不会使测试集数据进入我们建好的模型,不会给模型透露一些信息。然而,在现实中,我们的测试 集未必是很多条数据,也许我们的测试集只有一条数据,而某个特征上是空值,此时此刻测试集本身的众数根本不 存在,要如何利用测试集本身的众数去进行填补呢?因此为了避免这种尴尬的情况发生,我们假设测试集和训练集 的数据分布和性质都是相似的,因此我们统一使用训练集的众数和均值来对测试集进行填补。 在sklearn当中,即便是我们的填补缺失值的类也需要由实例化,fit和接口调用执行填补三个步骤来进行,而这种 分割其实一部分也是为了满足我们使用训练集的建模结果来填补测试集的需求。我们只需要实例化后,使用训练集 进行fit,然后在调用接口执行填补时用训练集fit后的结果分别来填补测试集和训练集就可以了。

#查看缺失值的缺失情况
Xtrain.isnull().mean()
#首先找出,分类型特征都有哪些
cate = Xtrain.columns[Xtrain.dtypes == "object"].tolist()
#除了特征类型为"object"的特征们,还有虽然用数字表示,但是本质为分类型特征的云层遮蔽程度
cloud = ["Cloud9am","Cloud3pm"]
cate = cate + cloud
cate
#对于分类型特征,我们使用众数来进行填补
from sklearn.impute import SimpleImputer
si = SimpleImputer(missing_values=np.nan,strategy="most_frequent")
#注意,我们使用训练集数据来训练我们的填补器,本质是在生成训练集中的众数
si.fit(Xtrain.loc[:,cate])
#然后我们用训练集中的众数来同时填补训练集和测试集
Xtrain.loc[:,cate] = si.transform(Xtrain.loc[:,cate])
Xtest.loc[:,cate] = si.transform(Xtest.loc[:,cate])
Xtrain.head()
Xtest.head()
#查看分类型特征是否依然存在缺失值
Xtrain.loc[:,cate].isnull().mean()
Xtest.loc[:,
Tsai Tsai
cate].isnull().mean()

3.6 处理分类型变量:将分类型变量编码

在编码中,和我们的填补缺失值一样,我们也是需要先用训练集fit模型,本质是将训练集中已经存在的类别转换成 是数字,然后我们再使用接口transform分别在测试集和训练集上来编码我们的特征矩阵。当我们使用接口在测试 集上进行编码的时候,如果测试集上出现了训练集中从未出现过的类别,那代码就会报错,表示说“我没有见过这 个类别,我无法对这个类别进行编码”,此时此刻你就要思考,你的测试集上或许存在异常值,错误值,或者的确有一个新的类别出现了,而你曾经的训练数据中并没有这个类别。以此为基础,你需要调整你的模型。

#将所有的分类型变量编码为数字,一个类别是一个数字
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
#利用训练集进行fit
oe = oe.fit(Xtrain.loc[:,cate])
#用训练集的编码结果来编码训练和测试特征矩阵
#在这里如果测试特征矩阵报错,就说明测试集中出现了训练集中从未见过的类别
Xtrain.loc[:,cate] = oe.transform(Xtrain.loc[:,cate])
Xtest.loc[:,cate] = oe.transform(Xtest.loc[:,cate])
Xtrain.loc[:,cate].head()
Xtest.loc[:,cate].head()

3.7 处理连续型变量:填补缺失值

连续型变量的缺失值由均值来进行填补。连续型变量往往已经是数字,无需进行编码转换。与分类型变量中一样, 我们也是使用训练集上的均值对测试集进行填补。如果学过随机森林填补缺失值的小伙伴,可能此时会问,为什么 不使用算法来进行填补呢?使用算法进行填补也是没有问题的,但在现实中,其实我们非常少用到算法来进行填补,有以下几个理由:

1. 算法是黑箱,解释性不强。如果你是一个数据挖掘工程师,你使用算法来填补缺失值后,你不懂机器学习的 老板或者同事问你的缺失值是怎么来的,你可能需要从头到尾帮他/她把随机森林解释一遍,这种效率过低的 事情是不可能做的,而许多老板和上级不会接受他们无法理解的东西。

2. 算法填补太过缓慢,运行一次森林需要有至少100棵树才能够基本保证森林的稳定性,而填补一个列就需要很 长的时间。在我们并不知道森林的填补结果是好是坏的情况下,填补一个很大的数据集风险非常高,有可能 需要跑好几个小时,但填补出来的结果却不怎么优秀,这明显是一个低效的方法。 因此在现实工作时,我们往往使用易于理解的均值或者中位数来进行填补。

当然了,在算法比赛中,我们可以穷尽 一切我们能够想到的办法来填补缺失值以追求让模型的效果更好,不过现实中,除了模型效果之外,我们还要追求可解释性。

#查看缺失值的缺失情况
Xtrain.isnull().mean()
#首先找出,分类型特征都有哪些
cate = Xtrain.columns[Xtrain.dtypes == "object"].tolist()
#除了特征类型为"object"的特征们,还有虽然用数字表示,但是本质为分类型特征的云层遮蔽程度
cloud = ["Cloud9am","Cloud3pm"]
cate = cate + cloud
cate
#对于分类型特征,我们使用众数来进行填补
from sklearn.impute import SimpleImputer
si = SimpleImputer(missing_values=np.nan,strategy="most_frequent")
#注意,我们使用训练集数据来训练我们的填补器,本质是在生成训练集中的众数
si.fit(Xtrain.loc[:,cate])
#然后我们用训练集中的众数来同时填补训练集和测试集
Xtrain.loc[:,cate] = si.transform(Xtrain.loc[:,cate])
Xtest.loc[:,cate] = si.transform(Xtest.loc[:,cate])
Xtrain.head()
Xtest.head()
#查看分类型特征是否依然存在缺失值
Xtrain.loc[:,cate].isnull().mean()
Xtest.loc[:,
Tsai Tsai
cate].isnull().mean()

3.8 处理分类型变量:将分类型变量编码

   在编码中,和我们的填补缺失值一样,我们也是需要先用训练集fit模型,本质是将训练集中已经存在的类别转换成 是数字,然后我们再使用接口transform分别在测试集和训练集上来编码我们的特征矩阵。当我们使用接口在测试 集上进行编码的时候,如果测试集上出现了训练集中从未出现过的类别,那代码就会报错,表示说“我没有见过这个类别,我无法对这个类别进行编码”,此时此刻你就要思考,你的测试集上或许存在异常值,错误值,或者的确有一个新的类别出现了,而你曾经的训练数据中并没有这个类别。以此为基础,你需要调整你的模型。

#将所有的分类型变量编码为数字,一个类别是一个数字
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
#利用训练集进行fit
oe = oe.fit(Xtrain.loc[:,cate])
#用训练集的编码结果来编码训练和测试特征矩阵
#在这里如果测试特征矩阵报错,就说明测试集中出现了训练集中从未见过的类别
Xtrain.loc[:,cate] = oe.transform(Xtrain.loc[:,cate])
Xtest.loc[:,cate] = oe.transform(Xtest.loc[:,cate])
Xtrain.loc[:,cate].head()
Xtest.loc[:,cate].head()

3.9 连续型变量:填补缺失值

   连续型变量的缺失值由均值来进行填补。连续型变量往往已经是数字,无需进行编码转换。与分类型变量中一样, 我们也是使用训练集上的均值对测试集进行填补。如果学过随机森林填补缺失值的小伙伴,可能此时会问,为什么 不使用算法来进行填补呢?使用算法进行填补也是没有问题的,但在现实中,其实我们非常少用到算法来进行填补,有以下几个理由:

1. 算法是黑箱,解释性不强。如果你是一个数据挖掘工程师,你使用算法来填补缺失值后,你不懂机器学习的 老板或者同事问你的缺失值是怎么来的,你可能需要从头到尾帮他/她把随机森林解释一遍,这种效率过低的 事情是不可能做的,而许多老板和上级不会接受他们无法理解的东西。

2. 算法填补太过缓慢,运行一次森林需要有至少100棵树才能够基本保证森林的稳定性,而填补一个列就需要很 长的时间。在我们并不知道森林的填补结果是好是坏的情况下,填补一个很大的数据集风险非常高,有可能 需要跑好几个小时,但填补出来的结果却不怎么优秀,这明显是一个低效的方法。

因此在现实工作时,我们往往使用易于理解的均值或者中位数来进行填补。当然了,在算法比赛中,我们可以穷尽 一切我们能够想到的办法来填补缺失值以追求让模型的效果更好,不过现实中,除了模型效果之外,我们还要追求可解释性。

col = Xtrain.columns.tolist()
for i in cate:
    col.remove(i)
col
#实例化模型,填补策略为"mean"表示均值
impmean = SimpleImputer(missing_values=np.nan,strategy = "mean")
#用训练集来fit模型
impmean = impmean.fit(Xtrain.loc[:,col])
#分别在训练集和测试集上进行均值填补
Xtrain.loc[:,col] = impmean.transform(Xtrain.loc[:,col])
Xtest.loc[:,col] = impmean.transform(Xtest.loc[:,col])
Xtrain.head()
Xtest.head()

3.10 处理连续型变量:无量纲化

数据的无量纲化是SVM执行前的重要步骤,因此我们需要对数据进行无量纲化。但注意,这个操作我们不对分类型变量进行。

col.remove("Month")
col
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss = ss.fit(Xtrain.loc[:,col])
Xtrain.loc[:,col] = ss.transform(Xtrain.loc[:,col])
Xtest.loc[:,col] = ss.transform(Xtest.loc[:,col])
Xtrain.head()
Xtest.head()
Ytrain.head()
Ytest.head()

特征工程到这里就全部结束了。大家可以分别查看一下我们的Ytrain,Ytest,Xtrain,Xtest,确保我们熟悉他们的 结构并且确保我们的确已经处理完毕全部的内容。将数据处理完毕之后,建议大家都使用to_csv来保存我们已经处 理好的数据集,避免我们在后续建模过程中出现覆盖了原有的数据集的失误后,需要从头开始做数据预处理。在开 始建模之前,无比保存好处理好的数据,然后在建模的时候,重新将数据导入。

4 建模与模型评估

from time import time
import datetime
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
from sklearn.metrics import roc_auc_score, recall_score
Ytrain = Ytrain.iloc[:,0].ravel()
Ytest = Ytest.iloc[:,0].ravel()
#建模选择自然是我们的支持向量机SVC,首先用核函数的学习曲线来选择核函数
#我们希望同时观察,精确性,recall以及AUC分数
times = time() #因为SVM是计算量很大的模型,所以我们需要时刻监控我们的模型运行时间
for kernel in ["linear","poly","rbf","sigmoid"]:
    clf = SVC(kernel = kernel
             ,gamma="auto"
             ,degree = 1
             ,cache_size = 5000
             ).fit(Xtrain, Ytrain)
    result = clf.predict(Xtest)
    score = clf.score(Xtest,Ytest)
  recall = recall_score(Ytest, result)
    auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
    print("%s 's testing accuracy %f, recall is %f', auc is %f" %
(kernel,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

我们注意到,模型的准确度和auc面积还是勉勉强强,但是每个核函数下的recall都不太高。相比之下,其实线性模 型的效果是最好的。那现在我们可以开始考虑了,在这种状况下,我们要向着什么方向进行调参呢?我们最想要的是什么?

我们可以有不同的目标:

一,我希望不计一切代价判断出少数类,得到最高的recall。

二,我们希望追求最高的预测准确率,一切目的都是为了让accuracy更高,我们不在意recall或者AUC。

三,我们希望达到recall,ROC和accuracy之间的平衡,不追求任何一个也不牺牲任何一个

4.1 模型调参

4.1.1 追求最高recall

  如果我们想要的是最高的recall,可以牺牲我们准确度,希望不计一切代价来捕获少数类,那我们首先可以打开我 们的class_weight参数,使用balanced模式来调节我们的recall:

times = time()
for kernel in ["linear","poly","rbf","sigmoid"]:
    clf = SVC(kernel = kernel
             ,gamma="auto"
             ,degree = 1
             ,cache_size = 5000
             ,class_weight = "balanced"
             ).fit(Xtrain, Ytrain)
    result = clf.predict(Xtest)
    score = clf.score(Xtest,Ytest)
    recall = recall_score(Ytest, result)
    auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
    print("%s 's testing accuracy %f, recall is %f', auc is %f" %
(kernel,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

在锁定了线性核函数之后,我甚至可以将class_weight调节得更加倾向于少数类,来不计代价提升recall。

times = time()
clf = SVC(kernel = "linear"
         ,gamma="auto"
         ,cache_size = 5000
         ,class_weight = {1:10} #注意,这里写的其实是,类别1:10,隐藏了类别0:1这个比例
         ).fit(Xtrain, Ytrain)
result = clf.predict(Xtest)
score = clf.score(Xtest,Ytest)
recall = recall_score(Ytest, result)
auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
print("testing accuracy %f, recall is %f', auc is %f" %(score,recall,auc))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

随着recall地无节制上升,我们的精确度下降得十分厉害,不过看起来AUC面积却还好,稳定保持在0.86左右。如 果此时我们的目的就是追求一个比较高的AUC分数和比较好的recall,那我们的模型此时就算是很不错了。虽然现在,我们的精确度很低,但是我们的确精准地捕捉出了每一个雨天。

4.1.2 追求最高准确率

 在我们现有的目标(判断明天是否会下雨)下,追求最高准确率而不顾recall其实意义不大, 但出于练习的目的, 我们来看看我们能够有怎样的思路。此时此刻我们不在意我们的Recall了,那我们首先要观察一下,我们的样本不均衡状况。如果我们的样本非常不均衡,但是此时却有很多多数类被判错的话,那我们可以让模型任性地把所有地 样本都判断为0,完全不顾少数类。

valuec = pd.Series(Ytest).value_counts()
valuec
valuec[0]/valuec.sum()

初步判断,可以认为我们其实已经将大部分的多数类判断正确了,所以才能够得到现在的正确率。为了证明我们的 判断,我们可以使用混淆矩阵来计算我们的特异度,如果特异度非常高,则证明多数类上已经很难被操作了。

from sklearn.metrics import confusion_matrix as CM
clf = SVC(kernel = "linear"
         ,gamma="auto"
         ,cache_size = 5000
         ).fit(Xtrain, Ytrain)
result = clf.predict(Xtest)
cm = CM(Ytest,result,labels=(1,0))
cm
specificity = cm[1,1]/cm[1,:].sum()
specificity #几乎所有的0都被判断正确了,还有不少1也被判断正确了

可以看到,特异度非常高,此时此刻如果要求模型将所有的类都判断为0,则已经被判断正确的少数类会被误伤, 整体的准确率一定会下降。而如果我们希望通过让模型捕捉更多少数类来提升精确率的话,却无法实现,因为一旦 我们让模型更加倾向于少数类,就会有更多的多数类被判错。 可以试试看使用class_weight将模型向少数类的方向稍微调整,已查看我们是否有更多的空间来提升我们的准确 率。如果在轻微向少数类方向调整过程中,出现了更高的准确率,则说明模型还没有到极限。

irange = np.linspace(0.01,0.05,10)
for i in irange:
    times = time()
    clf = SVC(kernel = "linear"
             ,gamma="auto"
             ,cache_size = 5000
             ,class_weight = {1:1+i}
             ).fit(Xtrain, Ytrain)
    result = clf.predict(Xtest)
    score = clf.score(Xtest,Ytest)
    recall = recall_score(Ytest, result)
    auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
    print("under ratio 1:%f testing accuracy %f, recall is %f', auc is %f" %
(1+i,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

惊喜出现了,我们的最高准确度是84.53%,超过了我们之前什么都不做的时候得到的84.40%。可见,模型还是有 潜力的。我们可以继续细化我们的学习曲线来进行调整:

irange_ = np.linspace(0.018889,0.027778,10)
for i in irange_:
    times = time()
    clf = SVC(kernel = "linear"
             ,gamma="auto"
             ,cache_size = 5000
             ,class_weight = {1:1+i}
             ).fit(Xtrain, Ytrain)
    result = clf.predict(Xtest)
    score = clf.score(Xtest,Ytest)
    recall = recall_score(Ytest, result)
    auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
    print("under ratio 1:%f testing accuracy %f, recall is %f', auc is %f" %
(1+i,score,recall,auc))
    print(
Tsai Tsai
datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

模型的效果没有太好,并没有再出现比我们的84.53%精确度更高的取值。可见,模型在不做样本平衡的情况下, 准确度其实已经非常接近极限了,让模型向着少数类的方向调节,不能够达到质变。如果我们真的希望再提升准确 度,只能选择更换模型的方式,调整参数已经不能够帮助我们了。想想看什么模型在线性数据上表现最好呢?

from sklearn.linear_model import LogisticRegression as LR
logclf = LR(solver="liblinear").fit(Xtrain, Ytrain)
logclf.score(Xtest,Ytest)
C_range = np.linspace(3,5,10)
for C in C_range:
    logclf = LR(solver="liblinear",C=C).fit(Xtrain, Ytrain)
    print(C,logclf.score(Xtest,Ytest))

尽管我们实现了非常小的提升,但可以看出来,模型的精确度还是没有能够实现质变。也许,要将模型的精确度提 升到90%以上,我们需要集成算法:比如,梯度提升树。大家如果感兴趣,可以自己下去试试看

4.1.3 追求平衡

我们前面经历了多种尝试,选定了线性核,并发现调节class_weight并不能够使我们模型有较大的改善。现在我们 来试试看调节线性核函数的C值能否有效果:

###======【TIME WARNING:10mins】======###
import matplotlib.pyplot as plt
C_range = np.linspace(0.01,20,20)
recallall = []
aucall = []
scoreall = []
for C in C_range:
    times = time()
    clf = SVC(kernel = "linear",C=C,cache_size = 5000
             ,class_weight = "balanced"
             ).fit(Xtrain, Ytrain)
    result = clf.predict(Xtest)
    score = clf.score(Xtest,Ytest)
    recall = recall_score(Ytest, result)
    auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
    recallall.append(recall)
    aucall.append(auc)
    scoreall.append(score)
    print("under C %f, testing accuracy is %f,recall is %f', auc is %f" %
(C,score,recall,auc))
    print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
    
print(max(aucall),C_range[aucall.index(max(aucall))])
plt.figure()
plt.plot(C_range,recallall,c="red",label="recall")
plt.plot(C_range,aucall,c="black",label="auc")
plt.plot(C_range,scoreall,c="orange",label="accuracy")
plt.legend()
plt.show()

可以观察到几个现象。 首先,我们注意到,随着C值逐渐增大,模型的运行速度变得越来越慢。对于SVM这个本来运行就不快的模型来 说,巨大的C值会是一个比较危险的消耗。所以正常来说,我们应该设定一个较小的C值范围来进行调整。 其次,C很小的时候,模型的各项指标都很低,但当C到1以上之后,模型的表现开始逐渐稳定,在C逐渐变大之 后,模型的效果并没有显著地提高。可以认为我们设定的C值范围太大了,然而再继续增大或者缩小C值的范围, AUC面积也只能够在0.86上下进行变化了,调节C值不能够让模型的任何指标实现质变。

times = time()
clf = SVC(kernel = "linear",C=3.1663157894736838,cache_size = 5000
         ,class_weight = "balanced"
         ).fit(Xtrain, Ytrain)
result = clf.predict(Xtest)
score = clf.score(Xtest,Ytest)
recall = recall_score(Ytest, result)
auc = roc_auc_score(Ytest,clf.decision_function(Xtest))
print("testing accuracy %f,recall is %f', auc is %f" % (score,recall,auc))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

这种情况下模型的准确率,Recall和AUC都没有太差,但是也没有太好,这也许就是模型平衡后的一种 结果。现在,光是调整支持向量机本身的参数,已经不能够满足我们的需求了,要想让AUC面积更进一步,我们需 要绘制ROC曲线,查看我们是否可以通过调整阈值来对这个模型进行改进。

from sklearn.metrics import roc_curve as ROC
import matplotlib.pyplot as plt
FPR, Recall, thresholds = ROC(Ytest,clf.decision_function(Xtest),pos_label=1)
area = roc_auc_score(Ytest,clf.decision_function(Xtest))
plt.figure()
plt.plot(FPR, Recall, color='red',
         label='ROC curve (area = %0.2f)' % area)
plt.plot([0, 1], [0, 1], color='black', linestyle='--')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('Recall')
plt.title('Receiver operating characteristic example')
plt.legend(loc="lower right")
plt.show()

以此模型作为基础,我们来求解最佳阈值:

maxindex = (Recall - FPR).tolist().index(max(Recall - FPR))
thresholds[maxindex]

基于我们选出的最佳阈值,我们来认为确定y_predict,并确定在这个阈值下的recall和准确度的值:

from sklearn.metrics import accuracy_score as AC
times = time()
clf = SVC(kernel = "linear",C=3.1663157894736838,cache_size = 5000
         ,class_weight = "balanced"
         ).fit(Xtrain, Ytrain)
prob = pd.DataFrame(clf.decision_function(Xtest))
prob.loc[prob.iloc[:,0] >= thresholds[maxindex],"y_pred"]=1
prob.loc[prob.iloc[:,0] < thresholds[maxindex],"y_pred"]=0
prob.loc[:,"y_pred"].isnull().sum()
#检查模型本身的准确度
score = AC(Ytest,prob.loc[:,"y_pred"].values)
recall = recall_score(Ytest, prob.loc[:,"y_pred"])
print("testing accuracy %f,recall is %f" % (score,recall))
print(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))

反而还不如我们不调整时的效果好。可见,如果我们追求平衡,那SVC本身的结果就已经非常接近最优结果了。调 节阈值,调节参数C和调节class_weight都不一定有效果。但整体来看,我们的模型不是一个糟糕的模型,但这个 结果如果提交到kaggle参加比赛是绝对不足够的。如果大家感兴趣,还可以更加深入地探索模型,或者换别的方法 来处理特征,以达到AUC面积0.9以上,或是准确度或recall都提升到90%以上。

  • 6
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
scikit-learn(sklearn)是一个基于Python的机器学习库,它提供了丰富的工具和算法,用于数据预处理、特征选择、模型建立和评估等机器学习任务。在票房预测问题上,可以使用sklearn来构建预测模型。 首先,对于票房预测问题,我们需要收集相关的数据,包括电影的特征信息(如导演、演员、类型等)以及票房数据。然后,我们可以利用sklearn库中的数据预处理模块(如数据清洗、特征缩放等)对数据进行处理,以便于后续的模型建立。 接下来,我们可以使用sklearn中的特征选择模块,根据数据集的特征与目标变量的相关性进行特征选择。这将有助于减少冗余特征,提高预测模型的性能,并降低过拟合的风险。 然后,我们可以选择合适的机器学习算法来构建预测模型。sklearn提供了多种经典的机器学习算法,如线性回归、决策树、随机森林等。我们可以根据数据集的特点选择适合问题的算法,并使用sklearn库中的模型建立模块进行建模。 建立好模型后,我们可以使用sklearn提供的模型评估模块对模型进行评估。通过使用交叉验证等方法,我们可以了解模型的泛化能力和性能,在需要时进一步调整模型的参数,以改善预测结果。 最后,我们可以使用已训练的模型对新数据进行预测,以预测电影的票房。sklearn库提供了方便的接口,使得模型的应用和预测变得简单和高效。 总之,sklearn作为一个强大的机器学习库,可以帮助我们在票房预测问题上构建模型、选择特征、评估模型,并进行预测。通过合理利用sklearn库的功能和算法,我们能够提高票房预测的准确性和效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值