根据菜菜的课程进行整理,方便记忆理解
代码位置如下:
案例:预测明天是否会下雨
SVC在现实中的应用十分广泛,尤其实在图像和文字识别方面。但SVC真实应用的代码其实就是sklearn中的三行,真正能够展现出SVM强大之处的,反而很少是案例本身,而是我们之前所作的各种探索。
我们在学习算法的时候,会使用各种各样的数据集来进行演示,但这些数据往往非常干净并且规整,不需要做太多的数据预处理。在我们讲解第三章:数据预处理与特征工程时,用了自制的文字数据和kaggle上的高维数据来为大家讲解,然而这些数据依然不能够和现实中采集到的数据的复杂程度相比。因此大家学习了这门课程,却依然会对究竟怎样做预处理感到困惑。
在实际工作中,数据预处理往往比建模难得多,耗时多得多,因此合理的数据预处理是非常必要的。考虑到大家渴望学习真实数据上的预处理的需求,以及SVM需要在比较规则的数据集上来表现的特性,准备了这个Kaggle上下载的,未经过预处理的澳大利亚天气数据集。我们的目标是在这个数据集上来预测明天是否会下雨。
这个案例的核心目的,是通过巧妙的预处理和特征工程来向大家展示,在现实数据集上我们往往如何做数据预处理,或者我们都有哪些预处理的方式和思路。预测天气是一个非常非常困难的主题,因为影响天气的因素太多,而Kaggle的这份数据也丝毫不让我们失望,是一份非常难的数据集,难到我们目前学过的所有算法在这个数据集上都不会有太好的结果,尤其是召回率recall,异常地低。在这里,在这个15W行数据的数据集上,随机抽样5000个样本来为大家演示我的数据预处理和特征工程的过程,为大家提供一些数据预处理和特征工程的思路。不过,特征工程没有标准答案,因此大家应当多尝试,希望使用原数据集的小伙伴们可以到Kaggle下载最原始版本:
Kaggle下载链接走这里:www.kaggle.com/jsphyg/weat…
记得好好阅读Kaggle上的各种数据集说明哦~!有一些特征是不能够使用的
| 特征/标签 | 含义 |
|---|---|
| Date | 观察日期 |
| Location | 获取该信息的气象站的名称 |
| MinTemp | 以摄氏度为单位的最低温度 |
| MaxTemp | 以摄氏度为单位的最高温度 |
| Rainfall | 当天记录的降雨量,单位为mm |
| Evaporation | 到早上9点之前的24小时的A级蒸发量(mm) |
| Sunshine | 白日受到日照的完整小时 |
| WindGustDir | 在到午夜12点前的24小时中的最强风的风向 |
| WindGustSpeed | 在到午夜12点前的24小时中的最强风速(km / h) |
| WindDir9am | 上午9点时的风向 |
| WindDir3pm | 下午3点时的风向 |
| WindSpeed9am | 上午9点之前每个十分钟的风速的平均值(km / h) |
| WindSpeed3pm | 下午3点之前每个十分钟的风速的平均值(km / h) |
| Humidity9am | 上午9点的湿度(百分比) |
| Humidity3am | 下午3点的湿度(百分比) |
| Pressure9am | 上午9点平均海平面上的大气压(hpa) |
| Pressure3pm | 下午3点平均海平面上的大气压(hpa) |
| Cloud9am | 上午9点的天空被云层遮蔽的程度,这是以“oktas”来衡量的,这个单位记录了云层遮挡 天空的程度。0表示完全晴朗的天空,而8表示它完全是阴天 |
| Cloud3pm | 下午3点的天空被云层遮蔽的程度 |
| Temp9am | 上午9点的摄氏度温度 |
| Temp3pm | 下午3点的摄氏度温度 |
| RainTomorrow | 目标变量,我们的标签:明天下雨了吗? |
导库导数据,探索特征
- 导入需要的库
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
- 导入数据,探索数据
weather = pd.read_csv(r"./weatherAUS5000.csv",index_col=0)
weather.head()
#将特征矩阵和标签Y分开
X = weather.iloc[:,:-1]
Y = weather.iloc[:,-1]
X.shape #5000行是随机选的
# (5000, 21)
#探索数据类型
X.info()
"""
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5000 entries, 0 to 4999
Data columns (total 21 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Date 5000 non-null object
1 Location 5000 non-null object
2 MinTemp 4979 non-null float64
3 MaxTemp 4987 non-null float64
4 Rainfall 4950 non-null float64
5 Evaporation 2841 non-null float64
6 Sunshine 2571 non-null float64
7 WindGustDir 4669 non-null object
8 WindGustSpeed 4669 non-null float64
9 WindDir9am 4651 non-null object
10 WindDir3pm 4887 non-null object
11 WindSpeed9am 4949 non-null float64
12 WindSpeed3pm 4919 non-null float64
13 Humidity9am 4936 non-null float64
14 Humidity3pm 4880 non-null float64
15 Pressure9am 4506 non-null float64
16 Pressure3pm 4504 non-null float64
17 Cloud9am 3111 non-null float64
18 Cloud3pm 3012 non-null float64
19 Temp9am 4967 non-null float64
20 Temp3pm 4912 non-null float64
dtypes: float64(16), object(5)
memory usage: 859.4+ KB
"""
# 探索缺失值
X.isnull().mean() # 缺失值所占总值的比例 isnull().sum(全部的True)/X.shape[0]
# 我们要有不同的缺失值填补策略
"""
Date 0.0000
Location 0.0000
MinTemp 0.0042
MaxTemp 0.0026
Rainfall 0.0100
Evaporation 0.4318
Sunshine 0.4858
WindGustDir 0.0662
WindGustSpeed 0.0662
WindDir9am 0.0698
WindDir3pm 0.0226
WindSpeed9am 0.0102
WindSpeed3pm 0.0162
Humidity9am 0.0128
Humidity3pm 0.0240
Pressure9am 0.0988
Pressure3pm 0.0992
Cloud9am 0.3778
Cloud3pm 0.3976
Temp9am 0.0066
Temp3pm 0.0176
dtype: float64
"""
Y.shape
# (5000,)
Y.isnull().sum() #加和的时候,True是1,False是0
# 0
#探索标签的分类
np.unique(Y) #我们的标签是二分类
分集,优先探索标签
- 分训练集和测试集,并做描述性统计
#分训练集和测试集
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,Y,test_size=0.3,random_state=420) #随机抽样
Xtrain.head()
#恢复索引
for i in [Xtrain, Xtest, Ytrain, Ytest]:
i.index = range(i.shape[0])
在现实中,我们会先分训练集和测试集,再开始进行数据预处理。这是由于,测试集在现实中往往是不可获得的,或者被假设为是不可获得的,我们不希望我们建模的任何过程受到测试集数据的影响,否则的话,就相当于提前告诉了模型一部分预测的答案。在之前的课中,为了简便操作,都给大家忽略了这个过程,一律先进行预处理,再分训练集和测试集,这是一种不规范的做法。在这里,为了让案例尽量接近真实的样貌,所以采取了现实中所使用的这种方式:先分训练集和测试集,再一步步进行预处理。这样导致的结果是,我们对训练集执行的所有操作,都必须对测试集执行一次,工作量是翻倍的。
- 标签编码
#将标签编码
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
Ytest.head()
探索特征,开始处理特征矩阵
- 描述性统计
# 描述性统计
# 异常值的评判:正负、明显不合乎显示的数据(最大值)
# 降雨量:偏态
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
# 对于去kaggle上下载了数据的小伙伴们,以及对于坚持要使用完整版数据的(15W行)数据的小伙伴们
# 如果你发现了异常值,首先你要观察,这个异常值出现的频率
# 如果异常值只出现了一次,多半是输入错误,直接把异常值删除
# 如果异常值出现了多次,去跟业务人员沟通,人为造成的错误异常值留着是没有用的
# 如果异常值占到你总数据量的10%左右了 - 把异常值替换成非异常但是非干扰的项,比如说用0来进行替换,或者把异常当缺失
- 处理困难特征:日期
type(Xtrain.iloc[0,0]) #字符串
Xtrainc = Xtrain.copy()
Xtrainc.sort_values(by="Location")
# 我们现在拥有的日期特征,是连续型特征,还是分类型特征
# 2019-1-6
# 2019-1-6.5
# 日期是一年分了365类的分类型变量
# 我们的日期特征中,日期是否有重复
Xtrain.iloc[:,0].value_counts()
"""
2014-05-16 6
2015-07-03 6
2015-10-12 6
2009-07-17 5
2012-07-18 5
..
2015-06-13 1
2014-10-05 1
2012-07-17 1
2015-05-21 1
2010-01-09 1
Name: Date, Length: 2141, dtype: int64
"""
# 不同地点上一段相似的时间的数据
Xtrain.loc[Xtrain.iloc[:,0] == "2015-08-24",:]
但在这里,很多人可能会持不同意见,怎么能够随便删除一个特征(哪怕我们已经觉得它可能无关)?如果我们要删除,我们可能需要一些统计过程,来判断说这个特征确实是和标签无关的,那我们可以先将“日期”这个特征编码后对它和标签做方差齐性检验(ANOVA),如果检验结果表示日期这个特征的确和我们的标签无关,那我们就可以愉快地删除这个特征了。但要编码“日期”这个特征,就又回到了它到底是否会被算法当成是分类变量的问题上。
其实我们可以想到,日期必然是和我们的结果有关的,它会从两个角度来影响我们的标签:
-
我们可以想到,昨天的天气可能会影响今天的天气,而今天的天气又可能会影响明天的天气。也就是说,随着日期的逐渐改变,样本是会受到上一个样本的影响的。但是对于算法来说,普通的算法是无法捕捉到样本与样本之间的联系的,我们的算法捕捉的是样本的每个特征与标签之间的联系(即列与列之间的联系),而无法捕捉样本与样本之间的联系(行与行的联系)。
-
要让算法理解上一个样本的标签可能会影响下一个样本的标签,我们必须使用时间序列分析。时间序列分析是指将同一统计指标的数值按其发生的时间先后顺序排列而成的数列。时间序列分析的主要目的是根据已有的历史数据对未来进行预测。然而,(据我所知)时间序列只能在单调的,唯一的时间上运行,即一次只能够对一个地点进行预测,不能够实现一次性预测多个地点,除非进行循环。而我们的时间数据本身,不是单调的,也不是唯一的,经过抽样之后,甚至连连续的都不是了,我们的时间是每个混杂在多个地点中,每个地点上的一小段时间。如何使用时间序列来处理这个问题,就会变得复杂。
那我们可以换一种思路,既然算法处理的是列与列之间的关系,我是否可以把”今天的天气会影响明天的天气“这个指标转换成一个特征呢?我们就这样来操作。
我们观察到,我们的特征中有一列叫做“Rainfall",这是表示当前日期当前地区下的降雨量,换句话说,也就是”今天的降雨量“。凭常识我们认为,今天是否下雨,应该会影响明天是否下雨,比如有的地方可能就有这样的气候,一旦下雨就连着下很多天,也有可能有的地方的气候就是一场暴雨来得快去的快。因此,我们可以将时间对气候的连续影响,转换为”今天是否下雨“这个特征,巧妙地将样本对应标签之间的联系,转换成是特征与标签之间的联系了。
# 首先,日期不是独一无二的,日期有重复
# 其次,在我们分训练集和测试集之后,日期也不是连续的,而是分散的
# 某一年的某一天倾向于会下雨?或者倾向于不会下雨吗?
# 不是日期影响了下雨与否,反而更多的是这一天的日照时间,湿度,温度等等这些因素影响了是否会下雨
# 光看日期,其实感觉它对我们的判断并无直接影响
# 如果我们把它当作连续型变量处理,那算法会人为它是一系列1~3000左右的数字,不会意识到这是日期
Xtrain.iloc[:,0].value_counts().count()
# 如果我们把它当作分类型变量处理,类别太多,有2141类,如果换成数值型,会被直接当成连续型变量,如果做成哑变量,我们特征的维度会爆炸
# 2141
Xtrain["Rainfall"].head(20)
"""
0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
5 0.0
6 0.0
7 0.2
8 0.0
9 0.2
10 1.0
11 0.0
12 0.2
13 0.0
14 0.0
15 3.0
16 0.2
17 0.0
18 35.2
19 0.0
Name: Rainfall, dtype: float64
"""
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.head()
那现在,我们是否就可以将日期删除了呢?对于我们而言,日期本身并不影响天气,但是日期所在的月份和季节其实是影响天气的,如果任选梅雨季节的某一天,那明天下雨的可能性必然比非梅雨季节的那一天要大。虽然我们无法让机器学习体会不同月份是什么季节,但是我们可以对不同月份进行分组,算法可以通过训练感受到,“这个月或者这个季节更容易下雨”。因此,我们可以将月份或者季节提取出来,作为一个特征使用,而舍弃掉具体的日期。如此,我们又可以创造第二个特征,月份"Month"。
int(Xtrain.loc[0,"Date"].split("-")[1]) #提取出月份
# 8
Xtrain["Date"] = Xtrain["Date"].apply(lambda x:int(x.split("-")[1]))
# apply是对dataframe上的某一列进行处理的一个函数
# lambda x匿名函数,请在dataframe上这一列中的每一行帮我执行冒号后的命令
Xtrain.loc[:,"Date"].value_counts()
"""
3 334
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允许我们只修改某个单独的列
Xtrain = Xtrain.rename(columns={"Date":"Month"})
Xtest["Date"] = Xtest["Date"].apply(lambda x:int(x.split("-")[1]))
Xtest = Xtest.rename(columns={"Date":"Month"})
- 处理困难特征:地点
Xtrain.loc[:,"Location"].value_counts().count()
# 超过25个类别的分类型变量,都会被算法当成是连续型变量
# 49
地点,又是一个非常tricky的特征。常识上来说,我们认为地点肯定是对明天是否会下雨存在影响的。比如说,如果其他信息都不给出,我们只猜测,“伦敦明天是否会下雨”和”北京明天是否会下雨“,我一定会猜测伦敦会下雨,而北京不会,因为伦敦是常年下雨的城市,而北京的气候非常干燥。对澳大利亚这样面积巨大的国家来说,必然存在着不同的城市有着不同的下雨倾向的情况。但尴尬的是,和时间一样,我们输入地点的名字对于算法来说,就是一串字符,"London"和"Beijing"对算法来说,和0,1没有区别。同样,我们的样本中含有49个不同地点,如果做成分类型变量,算法就无法辨别它究竟是否是分类变量。也就是说,我们需要让算法意识到,不同的地点因为气候不同,所以对“明天是否会下雨”有着不同的影响。如果我们能够将地点转换为这个地方的气候的话,我们就可以将不同城市打包到同一个气候中,而同一个气候下反应的降雨情况应该是相似的。
那我们如何将城市转换为气候呢
这是由澳大利亚气象局和澳大利亚建筑规范委员会(ABCB)制作统计的,澳大利亚不同地区不同城市的所在的气候区域划分。总共划分为八个区域,非常适合我们用来做分类。如果能够把49个地点转换成八种不同的气候,这个信息应该会对是否下雨的判断比较有用。基于气象局和ABCB的数据,制作了澳大利亚主要城市所对应的气候类型数据,并保存在csv文件city_climate.csv当中。在google上进行爬虫,爬出了每个城市所对应的经纬度,并保存在数据cityll.csv当中,大家可以自行导入,来查看这个数据。
为什么我们会需要城市的经纬度呢?我曾经尝试过直接使用样本中的城市来爬取城市本身的气候,然而由于样本中的地点名称,其实是气候站的名称,而不是城市本身的名称,因此不是每一个城市都能够直接获取到城市的气候。比如说,如果我们搜索“海淀区气候”,搜索引擎返回的可能是海淀区现在的气温,而不是整个北京的气候类型。因此,我们需要澳大利亚气象局的数据,来找到这些气候站所对应的城市。
我们有了澳大利亚全国主要城市的气候,也有了澳大利亚主要城市的经纬度(地点),我们就可以通过计算我们样本中的每个气候站到各个主要城市的地理距离,来找出一个离这个气象站最近的主要城市,而这个主要城市的气候就是我们样本点所在的地点的气候。 让我们把cityll.csv和cityclimate.csv来导入,来看看它们是什么样子:
cityll = pd.read_csv(r"./cityll.csv",index_col=0)
city_climate = pd.read_csv(r"./Cityclimate.csv")
cityll.head() #每个城市对应的经纬度,这些城市是澳大利亚统计局做的那张地图上的城市
# 去除经纬度的源泉符号
float(cityll.loc[0,"Latitude"][:-1])
# 34.9285
# 澳大利亚位于南半球和东半球,都一样
cityll.loc[:,"Latitudedir"].value_counts()
"""
S, 100
Name: Latitudedir, dtype: int64
"""
city_climate.head() #澳大利亚统计局做的每个城市对应的气候
#去掉度数符号
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]]
citylld
#将city_climate中的气候添加到我们的citylld中
citylld["climate"] = city_climate.iloc[:,-1]
citylld.head()
citylld.loc[:,"climate"].value_counts()
"""
Hot dry summer, cool winter 24
Hot dry summer, warm winter 18
Warm temperate 18
High humidity summer, warm winter 17
Cool temperate 9
Mild temperate 9
Warm humid summer, mild winter 5
Name: climate, dtype: int64
"""
samplecity = pd.read_csv(r"./samplecity.csv",index_col=0)
samplecity.head()
#我们对samplecity也执行同样的处理:去掉经纬度中度数的符号,并且舍弃我们的经纬度的方向
samplecity["Latitudenum"] = samplecity["Latitude"].apply(lambda x:float(x[:-1]))
samplecity["Longitudenum"] = samplecity["Longitude"].apply(lambda x:float(x[:-1]))
samplecityd = samplecity.iloc[:,[0,5,6]]
samplecityd.head()
好了,我们现在有了澳大利亚主要城市的经纬度和对应的气候,也有了我们的样本的地点所对应的经纬度,接下来我们要开始计算我们样本上的地点到每个澳大利亚主要城市的距离,而离我们的样本地点最近的那个澳大利亚主要城市的气候,就是我们样本点的气候。
在地理上,两个地点之间的距离,由如下公式来进行计算:
其中R是地球的半径,6371.01km,arccos是三角反余弦函数,slat是起始地点的纬度,slon是起始地点的经度,elat是结束地点的纬度,elon是结束地点的经度。本质还是计算两点之间的距离。而我们爬取的经纬度,本质其实是角度,所以需要用各种三角函数和弧度公式将角度转换成距离。由于我们不是地理专业,拿到公式可以使用就okay了,不需要去纠结这个公式究竟怎么来的。
#首先使用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()
"""
Warm temperate 15
Mild temperate 10
Cool temperate 9
Hot dry summer, cool winter 6
High humidity summer, warm winter 4
Hot dry summer, warm winter 3
Warm humid summer, mild winter 2
Name: climate, dtype: int64
"""
#确认无误后,取出样本城市所对应的气候,并保存
locafinal = samplecityd.iloc[:,[0,-1]]
locafinal.head()
locafinal.columns = ["Location","Climate"]
#在这里设定locafinal的索引为地点,是为了之后进行map的匹配
locafinal = locafinal.set_index(keys="Location")
locafinal
# 保留中间数据
locafinal.to_csv(r"./samplelocation.csv")
# 将location中的内容替换,并且确保匹配进入的气候字符串中不含有逗号,气候两边不含有空格
# 我们使用re这个模块来消除逗号
# re.sub(希望替换的值,希望被替换成的值,要操作的字符串) #去掉逗号
# x.strip()是去掉空格的函数
# 把location替换成气候的是我们的map的映射
import re
#气象站的名字替换成了对应的城市对应的气候
Xtrain["Location"] = Xtrain["Location"].map(locafinal.iloc[:,0])
Xtrain.head()
#城市的气候中所含的逗号和空格都去掉
Xtrain["Location"] = Xtrain["Location"].apply(lambda x:re.sub(",","",x.strip()))
Xtest["Location"] = Xtest["Location"].map(locafinal.iloc[:,0]).apply(lambda x:re.sub(",","",x.strip()))
#修改特征内容之后,我们使用新列名“Climate”来替换之前的列名“Location”
#注意这个命令一旦执行之后,就再没有列"Location"了,使用索引时要特别注意
Xtrain = Xtrain.rename(columns={"Location":"Climate"})
Xtest = Xtest.rename(columns={"Location":"Climate"})
Xtrain.head()
- 处理分类型变量:缺失值
接下来,我们总算可以开始处理我们的缺失值了。首先我们要注意到,由于我们的特征矩阵由两种类型的数据组成:分类型和连续型,因此我们必须对两种数据采用不同的填补缺失值策略。传统地,如果是分类型特征,我们则采用众数进行填补。如果是连续型特征,我们则采用均值来填补。
此时,由于我们已经分了训练集和测试集,我们需要考虑一件事:究竟使用哪一部分的数据进行众数填补呢?答案是,使用训练集上的众数对训练集和测试集都进行填补。为什么会这样呢?按道理说就算用测试集上的众数对测试集进行填补,也不会使测试集数据进入我们建好的模型,不会给模型透露一些信息。然而,在现实中,我们的测试集未必是很多条数据,也许我们的测试集只有一条数据,而某个特征上是空值,此时此刻测试集本身的众数根本不存在,要如何利用测试集本身的众数去进行填补呢?因此为了避免这种尴尬的情况发生,我们假设测试集和训练集的数据分布和性质都是相似的,因此我们统一使用训练集的众数和均值来对测试集进行填补。
在sklearn当中,即便是我们的填补缺失值的类也需要由实例化,fit和接口调用执行填补三个步骤来进行,而这种分割其实一部分也是为了满足我们使用训练集的建模结果来填补测试集的需求。我们只需要实例化后,使用训练集进行fit,然后在调用接口执行填补时用训练集fit后的结果分别来填补测试集和训练集就可以了
#查看缺失值的缺失情况
Xtrain.isnull().mean()
"""
Month 0.000000
Climate 0.000000
MinTemp 0.004000
MaxTemp 0.003143
Rainfall 0.009429
Evaporation 0.433429
Sunshine 0.488571
WindGustDir 0.067714
WindGustSpeed 0.067714
WindDir9am 0.067429
WindDir3pm 0.024286
WindSpeed9am 0.009714
WindSpeed3pm 0.018000
Humidity9am 0.011714
Humidity3pm 0.026286
Pressure9am 0.098857
Pressure3pm 0.098857
Cloud9am 0.379714
Cloud3pm 0.401429
Temp9am 0.005429
Temp3pm 0.019714
RainToday 0.009429
dtype: float64
"""
Xtrain.info()
"""
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3500 entries, 0 to 3499
Data columns (total 22 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Month 3500 non-null int64
1 Climate 3500 non-null object
2 MinTemp 3486 non-null float64
3 MaxTemp 3489 non-null float64
4 Rainfall 3467 non-null float64
5 Evaporation 1983 non-null float64
6 Sunshine 1790 non-null float64
7 WindGustDir 3263 non-null object
8 WindGustSpeed 3263 non-null float64
9 WindDir9am 3264 non-null object
10 WindDir3pm 3415 non-null object
11 WindSpeed9am 3466 non-null float64
12 WindSpeed3pm 3437 non-null float64
13 Humidity9am 3459 non-null float64
14 Humidity3pm 3408 non-null float64
15 Pressure9am 3154 non-null float64
16 Pressure3pm 3154 non-null float64
17 Cloud9am 2171 non-null float64
18 Cloud3pm 2095 non-null float64
19 Temp9am 3481 non-null float64
20 Temp3pm 3431 non-null float64
21 RainToday 3467 non-null object
dtypes: float64(16), int64(1), object(5)
memory usage: 601.7+ KB
"""
Xtrain.dtypes == "object"
"""
Month False
Climate True
MinTemp False
MaxTemp False
Rainfall False
Evaporation False
Sunshine False
WindGustDir True
WindGustSpeed False
WindDir9am True
WindDir3pm True
WindSpeed9am False
WindSpeed3pm False
Humidity9am False
Humidity3pm False
Pressure9am False
Pressure3pm False
Cloud9am False
Cloud3pm False
Temp9am False
Temp3pm False
RainToday True
dtype: bool
"""
#首先找出,分类型特征都有哪些
cate = Xtrain.columns[Xtrain.dtypes == "object"].tolist()
cate
# ['Climate', 'WindGustDir', 'WindDir9am', 'WindDir3pm', 'RainToday']
#除了特征类型为"object"的特征们,还有虽然用数字表示,但是本质为分类型特征的云层遮蔽程度
cloud = ["Cloud9am","Cloud3pm"]
cate = cate + cloud
cate
"""
['Climate',
'WindGustDir',
'WindDir9am',
'WindDir3pm',
'RainToday',
'Cloud9am',
'Cloud3pm']
"""
#对于分类型特征,我们使用众数来进行填补
from sklearn.impute import SimpleImputer #0.20, conda, pip
si = SimpleImputer(missing_values=np.nan,strategy="most_frequent")
#注意,我们使用训练集数据来训练我们的填补器,本质是在生成训练集中的众数
si.fit(Xtrain.loc[:,cate])
# SimpleImputer(strategy='most_frequent')
#然后我们用训练集中的众数来同时填补训练集和测试集
Xtrain.loc[:,cate] = si.transform(Xtrain.loc[:,cate])
Xtest.loc[:,cate] = si.transform(Xtest.loc[:,cate])
Xtrain.head()
#查看分类型特征是否依然存在缺失值
Xtrain.loc[:,cate].isnull().mean()
"""
Climate 0.0
WindGustDir 0.0
WindDir9am 0.0
WindDir3pm 0.0
RainToday 0.0
Cloud9am 0.0
Cloud3pm 0.0
dtype: float64
"""
Xtest.loc[:,cate].isnull().mean()
"""
Climate 0.0
WindGustDir 0.0
WindDir9am 0.0
WindDir3pm 0.0
RainToday 0.0
Cloud9am 0.0
Cloud3pm 0.0
dtype: float64
"""
- 处理分类型变量:将分类型变量编码
在编码中,和我们的填补缺失值一样,我们也是需要先用训练集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])
cate
"""
['Climate',
'WindGustDir',
'WindDir9am',
'WindDir3pm',
'RainToday',
'Cloud9am',
'Cloud3pm']
"""
Xtrain.loc[:,cate].head()
- 处理连续型变量:填补缺失值
#实例化模型,填补策略为"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.isnull().mean()
"""
Month 0.0
Climate 0.0
MinTemp 0.0
MaxTemp 0.0
Rainfall 0.0
Evaporation 0.0
Sunshine 0.0
WindGustDir 0.0
WindGustSpeed 0.0
WindDir9am 0.0
WindDir3pm 0.0
WindSpeed9am 0.0
WindSpeed3pm 0.0
Humidity9am 0.0
Humidity3pm 0.0
Pressure9am 0.0
Pressure3pm 0.0
Cloud9am 0.0
Cloud3pm 0.0
Temp9am 0.0
Temp3pm 0.0
RainToday 0.0
dtype: float64
"""
- 处理连续型变量:无量纲化
数据的无量纲化是SVM执行前的重要步骤,因此我们需要对数据进行无量纲化。但注意,这个操作我们不对分类型变量进行
from sklearn.preprocessing import StandardScaler #数据转换为均值为0,方差为1的数据
#标准化不改变数据的分布,不会把数据变成正态分布的
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()
特征工程到这里就全部结束了。大家可以分别查看一下我们的Ytrain,Ytest,Xtrain,Xtest,确保我们熟悉他们的结构并且确保我们的确已经处理完毕全部的内容。将数据处理完毕之后,建议大家都使用to_csv来保存我们已经处理好的数据集,避免我们在后续建模过程中出现覆盖了原有的数据集的失误后,需要从头开始做数据预处理。在开始建模之前,无比保存好处理好的数据,然后在建模的时候,重新将数据导入
建模与模型评估
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"))
"""
linear 's testing accuracy 0.844000, recall is 0.469388', auc is 0.869029
00:02:662358
poly 's testing accuracy 0.840667, recall is 0.457726', auc is 0.868157
00:03:030896
rbf 's testing accuracy 0.813333, recall is 0.306122', auc is 0.814873
00:04:521132
sigmoid 's testing accuracy 0.655333, recall is 0.154519', auc is 0.437308
00:04:927670
"""
模型调参
- 最求最高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"))
"""
linear 's testing accuracy 0.795333, recall is 0.775510', auc is 0.870085
00:03:124730
poly 's testing accuracy 0.793333, recall is 0.763848', auc is 0.871448
00:03:643266
rbf 's testing accuracy 0.803333, recall is 0.600583', auc is 0.819713
00:05:344727
sigmoid 's testing accuracy 0.562000, recall is 0.282799', auc is 0.437119
00:06:355649
"""
times = time()
clf = SVC(kernel = "linear"
,gamma="auto"
,cache_size = 5000
,class_weight = {1:15} #注意,这里写的其实是,类别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"))
"""
testing accuracy 0.548000, recall is 0.970845', auc is 0.867172
00:05:905886
"""
随着recall地无节制上升,我们的精确度下降得十分厉害,不过看起来AUC面积却还好,稳定保持在0.86左右。如果此时我们的目的就是追求一个比较高的AUC分数和比较好的recall,那我们的模型此时就算是很不错了。虽然现在,我们的精确度很低,但是我们的确精准地捕捉出了每一个雨天
- 追求最高准确率
在我们现有的目标(判断明天是否会下雨)下,追求最高准确率而不顾recall其实意义不大, 但出于练习的目的,我们来看看我们能够有怎样的思路。此时此刻我们不在意我们的Recall了,那我们首先要观察一下,我们的样本不均衡状况。如果我们的样本非常不均衡,但是此时却有很多多数类被判错的话,那我们可以让模型任性地把所有地样本都判断为0,完全不顾少数类
valuec = pd.Series(Ytest).value_counts()
valuec
"""
0 1157
1 343
dtype: int64
"""
valuec[0]/valuec.sum()
# 0.7713333333333333
初步判断,可以认为我们其实已经将大部分的多数类判断正确了,所以才能够得到现在的正确率。为了证明我们的判断,我们可以使用混淆矩阵来计算我们的特异度,如果特异度非常高,则证明多数类上已经很难被操作了
# 查看模型的特异度
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
"""
array([[ 161, 182],
[ 52, 1105]], dtype=int64)
"""
specificity = cm[1,1]/cm[1,:].sum()
specificity #几乎所有的0都被判断正确了,还有不少1也被判断正确了
# 0.9550561797752809
可以看到,特异度非常高,此时此刻如果要求模型将所有的类都判断为0,则已经被判断正确的少数类会被误伤,整体的准确率一定会下降。而如果我们希望通过让模型捕捉更多少数类来提升精确率的话,却无法实现,因为一旦我们让模型更加倾向于少数类,就会有更多的多数类被判错。
可以试试看使用class_weight将模型向少数类的方向稍微调整,已查看我们是否有更多的空间来提升我们的准确率。如果在轻微向少数类方向调整过程中,出现了更高的准确率,则说明模型还没有到极限
irange = np.linspace(0.01,0.05,10)
irange
"""
array([0.01 , 0.01444444, 0.01888889, 0.02333333, 0.02777778,
0.03222222, 0.03666667, 0.04111111, 0.04555556, 0.05 ])
"""
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"))
"""
under ratio 1:1.010000 testing accuracy 0.844667, recall is 0.475219', auc is 0.869157
00:02:766668
under ratio 1:1.014444 testing accuracy 0.844667, recall is 0.478134', auc is 0.869185
00:02:776962
under ratio 1:1.018889 testing accuracy 0.844667, recall is 0.478134', auc is 0.869198
00:02:716698
under ratio 1:1.023333 testing accuracy 0.845333, recall is 0.481050', auc is 0.869175
00:02:660185
under ratio 1:1.027778 testing accuracy 0.844000, recall is 0.481050', auc is 0.869394
00:02:695940
under ratio 1:1.032222 testing accuracy 0.844000, recall is 0.481050', auc is 0.869528
00:02:671380
under ratio 1:1.036667 testing accuracy 0.844000, recall is 0.481050', auc is 0.869659
00:02:823926
under ratio 1:1.041111 testing accuracy 0.844667, recall is 0.483965', auc is 0.869629
00:02:732377
under ratio 1:1.045556 testing accuracy 0.845333, recall is 0.486880', auc is 0.869755
00:02:727441
under ratio 1:1.050000 testing accuracy 0.845333, recall is 0.486880', auc is 0.869863
00:02:771133
"""
惊喜出现了,我们的最高准确度是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(datetime.datetime.fromtimestamp(time()-times).strftime("%M:%S:%f"))
"""
under ratio 1:1.018889 testing accuracy 0.844667, recall is 0.478134', auc is 0.869225
00:02:725814
under ratio 1:1.019877 testing accuracy 0.844000, recall is 0.478134', auc is 0.869228
00:02:740823
under ratio 1:1.020864 testing accuracy 0.844000, recall is 0.478134', auc is 0.869218
00:02:717017
under ratio 1:1.021852 testing accuracy 0.844667, recall is 0.478134', auc is 0.869188
00:02:629180
under ratio 1:1.022840 testing accuracy 0.844667, recall is 0.478134', auc is 0.869220
00:02:738671
under ratio 1:1.023827 testing accuracy 0.844667, recall is 0.481050', auc is 0.869188
00:02:756102
under ratio 1:1.024815 testing accuracy 0.844667, recall is 0.481050', auc is 0.869231
00:02:642868
under ratio 1:1.025803 testing accuracy 0.844000, recall is 0.481050', auc is 0.869253
00:02:789311
under ratio 1:1.026790 testing accuracy 0.844000, recall is 0.481050', auc is 0.869314
00:02:715091
under ratio 1:1.027778 testing accuracy 0.844000, recall is 0.481050', auc is 0.869326
00:02:720775
"""
模型的效果没有太好,并没有再出现比我们的84.53%精确度更高的取值。可见,模型在不做样本平衡的情况下,准确度其实已经非常接近极限了,让模型向着少数类的方向调节,不能够达到质变。如果我们真的希望再提升准确度,只能选择更换模型的方式,调整参数已经不能够帮助我们了。想想看什么模型在线性数据上表现最好呢?
from sklearn.linear_model import LogisticRegression as LR
logclf = LR(solver="liblinear").fit(Xtrain, Ytrain)
logclf.score(Xtest,Ytest)
# 0.8486666666666667
C_range = np.linspace(5,10,10)
for C in C_range:
logclf = LR(solver="liblinear",C=C).fit(Xtrain, Ytrain)
print(C,logclf.score(Xtest,Ytest))
"""
5.0 0.8493333333333334
5.555555555555555 0.8493333333333334
6.111111111111111 0.8486666666666667
6.666666666666667 0.8493333333333334
7.222222222222222 0.8493333333333334
7.777777777777778 0.8493333333333334
8.333333333333334 0.8493333333333334
8.88888888888889 0.8493333333333334
9.444444444444445 0.8493333333333334
10.0 0.8493333333333334
"""
尽管我们实现了非常小的提升,但可以看出来,模型的精确度还是没有能够实现质变。也许,要将模型的精确度提升到90%以上,我们需要集成算法:比如,梯度提升树。大家如果感兴趣,可以自己下去试试看
- 追求平衡
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"))
"""
testing accuracy 0.795333,recall is 0.772595', auc is 0.870165
00:08:298814
"""
可以看到,这种情况下模型的准确率,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))
area
# 0.8701653769298805
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]
# -0.0895051736774739
基于我们选出的最佳阈值,我们来认为确定y_predict,并确定在这个阈值下的recall和准确度的值:
from sklearn.metrics import accuracy_score as AC
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()
times = time()
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"))
"""
testing accuracy 0.789333,recall is 0.804665
00:00:003000
"""
反而还不如我们不调整时的效果好。可见,如果我们追求平衡,那SVC本身的结果就已经非常接近最优结果了。调节阈值,调节参数C和调节class_weight都不一定有效果。但整体来看,我们的模型不是一个糟糕的模型,但这个结果如果提交到kaggle参加比赛是绝对不足够的。如果大家感兴趣,还可以更加深入地探索模型,或者换别的方法来处理特征,以达到AUC面积0.9以上,或是准确度或recall都提升到90%以上
SVM总结&结语
SVC在sklearn中学习了SVM原理,包括决策边界,损失函数,拉格朗日函数,拉格朗日对偶函数,软间隔硬间隔,核函数以及核函数的各种应用。我们了解了SVC类的各种重要参数,属性和接口,其中参数包括软间隔的惩罚系数C,核函数kernel,核函数的相关参数gamma,coef0和degree,解决样本不均衡的参数class_weight,解决多分类问题的参数decision_function_shape,控制概率的参数probability,控制计算内存的参数cache_size,属性主要包括调用支持向量的属性support_vectors_和查看特征重要性的属性coef_。接口中,我们学习了最核心的decision_function。除此之外,还有分类模型的模型评估指标:混淆矩阵和ROC曲线,还介绍了部分特征工程和数据预处理的思路。