Kaggle——Rain in Australia (Predict rain tomorrow in Australia)

写在前面

  • 本博客分享在Kaggle网站上的完整案例“Rain in Australia”,即利用数据去预测澳大利亚明天是否会下雨,所用的数据集为Kaggle提供的澳大利亚气象局十年的气象数据,共142193条数据。本文采用SVM、LR、RF分别进行预测,并将特征工程部分详细讲解,仅供分享交流。
    此Kaggle案例的网址为:Rain in Australia
  • 源码已放在GitHub上:GitHub项目源码

1. 案例背景

  • ①Kaggle帮我们获取到了澳大利亚气象局十年的各个地区的气象数据,我们要做的事,从该数据集中简历模型去预测澳大利亚明天是否会下雨,即数据集的标签为"YES" OR “NO”,所以这是一个二分类问题,重点在于我们怎么去进行特征工程,以及选取什么样的模型。
  • 注意:训练二进制分类模型时,应排除变量Risk-MM。不排除它会泄漏您模型的答案并降低其可预测性。

2. 解读数据

  • 数据大小:13.51M,共142193条数据
  • 数据格式:csv文件,我们使用pandas来读取
  • 数据内容:共21个原始特征和标签,以下来自数据提供者的解释。
  • 我是用的IDE:JupyterNotebook,python3.7
    在这里插入图片描述

2. 导入数据进行数据分析及特征工程

2.1 概览数据

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

# 导入数据
weather = pd.read_csv(r"your_path\weather.csv",index_col=0)

# 观察前五行数据
print(weather.head(5))

#将特征矩阵和标签Y分开
X = weather.iloc[:,:-1]
Y = weather.iloc[:,-1]

在这里插入图片描述
通过简单的观察数据,我们发现有很多需要我们要作的事情,例如Nan值、字符型变量的处理,这些都是特征工程中的难点。

2.2 探索数据

2.2.1 探索数据类型

#探索数据类型
X.info()

在这里插入图片描述

  • 可以发现数据类型有两种:object和float,而且float居多,这很友好
  • 数据存在严重的缺失:除了第一个和第二个特征,其他全部有缺失

2.2.2 探索缺失值

#探索缺失值
X.isnull().mean() #缺失值所占总值的比例
#我们要有不同的缺失值填补策略{均值,众数,中位数,....}

在这里插入图片描述

  • 数据含义:上面图中的数值表示该特征中样本缺失占总样本的比例,例如MinTemp:0.004480表示有0.448%的样本缺失了该值
探索标签的缺失情况
Y.isnull().sum() 

# result
0
  • 信息:样本的标签没有缺失

2.2.3 产生训练集和测试集

  • 分离出训练集和测试集,并查看一下
#分训练集和测试集
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])

2.2.4 分析是否存在样本不平衡问题

#是否有样本不平衡问题?
print(Ytrain.value_counts())
print(Ytest.value_counts())

在这里插入图片描述

  • 信息:有轻微的不平衡现象,正负样本比例大概为3:1,没下雨居多。后续会考虑这个问题

2.3 特征工程

2.3.1 标签处理

  • 由于标签是字符串形式,要转换为离散数值形式方便计算机进行计算
#将标签编码
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的前5行,可以发现,标签已经变为了{0,1}形式,0代表“NO”
    在这里插入图片描述

2.3.2 特征处理

2.3.2.1 描述性统计与异常值处理

  • 这里使用了分位数的概念
#描述性统计
Xtrain.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T

在这里插入图片描述

  • 如何处理异常值:通过观察数据,我们发现云的密度这块存在异常值9
#先查看原始的数据结构
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()
Xtrainc = Xtrain.copy() 

2.3.2.2 日期的处理

Xtrainc.sort_values(by="Location")
 
Xtrain.iloc[:,0].value_counts() 
#首先,日期不是独一无二的,日期有重复 
#其次,在我们分训练集和测试集之后,日期也不是连续的,而是分散的
#某一年的某一天倾向于会下雨?或者倾向于不会下雨吗? 
#不是日期影响了下雨与否,反而更多的是这一天的日照时间,湿度,温度等等这些因素影响了是否会下雨 
#光看日期,其实感觉它对我们的判断并无直接影响 #如果我们把它当作连续型变量处理,那算法会人为它是一系列1~3000左右的数字,不会意识到这是日期
 
Xtrain.iloc[:,0].value_counts().count() 
#如果我们把它当作分类型变量处理,类别太多,有2141类,如果换成数值型,会被直接当成连续型变量,如果做成哑 变量,我们特征的维度会爆炸
  • 关于日期的思考:
    ①其实我们可以想到,日期必然是和我们的结果有关的,它会从两个角度来影响我们的标签:
    首先,我们可以想到,昨天的天气可能会影响今天的天气,而今天的天气又可能会影响明天的天气。也就是说,随 着日期的逐渐改变,样本是会受到上一个样本的影响的。但是对于算法来说,普通的算法是无法捕捉到样本与样本 之间的联系的,我们的算法捕捉的是样本的每个特征与标签之间的联系(即列与列之间的联系),而无法捕捉样本 与样本之间的联系(行与行的联系)。
    ②**要让算法理解上一个样本的标签可能会影响下一个样本的标签,我们必须使用时间序列分析。**时间序列分析是指将 同一统计指标的数值按其发生的时间先后顺序排列而成的数列。时间序列分析的主要目的是根据已有的历史数据对 未来进行预测。然而,(据我所知)时间序列只能在单调的,唯一的时间上运行,即一次只能够对一个地点进行预 测,不能够实现一次性预测多个地点,除非进行循环。而我们的时间数据本身,不是单调的,也不是唯一的,经过 抽样之后,甚至连连续的都不是了,我们的时间是每个混杂在多个地点中,每个地点上的一小段时间。如何使用时 间序列来处理这个问题,就会变得复杂。
    ③那我们可以换一种思路,既然算法处理的是列与列之间的关系,我是否可以把”今天的天气会影响明天的天气“这个 指标转换成一个特征呢?我们就这样来操作。
    我们观察到,我们的特征中有一列叫做“Rainfall",这是表示当前日期当前地区下的降雨量,换句话说,也就是”今 天的降雨量“。凭常识我们认为,今天是否下雨,应该会影响明天是否下雨,比如有的地方可能就有这样的气候, 一旦下雨就连着下很多天,也有可能有的地方的气候就是一场暴雨来得快去的快。因此,我们可以将时间对气候的 连续影响,转换为”今天是否下雨“这个特征,巧妙地将样本对应标签之间的联系,转换成是特征与标签之间的联系 了。
Xtrain["Rainfall"].head(20)
 
Xtrain.loc[Xtrain["Rainfall"] >= 1,"RainToday"] = "Yes" Xtrain.loc[Xtrain["Rainfall"] < 1,"RainToday"] = "No" Xtrain.loc[Xtrain["Rainfall"] == np.nan,"RainToday"] = np.nan
 
Xtest.loc[Xtest["Rainfall"] >= 1,"RainToday"] = "Yes" Xtest.loc[Xtest["Rainfall"] < 1,"RainToday"] = "No" Xtest.loc[Xtest["Rainfall"] == np.nan,"RainToday"] = np.nan
 
Xtrain.head()
 
Xtest.head()
  • 如此,我们就创造了一个特征,今天是否下雨“RainToday”。
    那现在,我们是否就可以将日期删除了呢?对于我们而言,日期本身并不影响天气,但是日期所在的月份和季节其 实是影响天气的,如果任选梅雨季节的某一天,那明天下雨的可能性必然比非梅雨季节的那一天要大。虽然我们无 法让机器学习体会不同月份是什么季节,但是我们可以对不同月份进行分组,算法可以通过训练感受到,“这个月 或者这个季节更容易下雨”。因此,我们可以将月份或者季节提取出来,作为一个特征使用,而舍弃掉具体的日 期。如此,我们又可以创造第二个特征,月份"Month"
int(Xtrain.loc[0,"Date"].split("-")[1]) #提取出月份
 
Xtrain["Date"] = Xtrain["Date"].apply(lambda x:int(x.split("-")[1])) #替换完毕后,我们需要修改列的名称 #rename是比较少有的,可以用来修改单个列名的函数 #我们通常都直接使用 df.columns = 某个列表 这样的形式来一次修改所有的列名 #但rename允许我们只修改某个单独的列 Xtrain = Xtrain.rename(columns={"Date":"Month"})
 
Xtrain.head()
 
Xtest["Date"] = Xtest["Date"].apply(lambda x:int(x.split("-")[1])) Xtest = Xtest.rename(columns={"Date":"Month"})
 
Xtest.head() 

在这里插入图片描述

2.3.2.3 地点的处理

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

  • 所以现在面临一个很难得问题,就是如何将地点转换为气候,我们可以看一下澳大利亚得气候分布,图是从澳大利亚气象局官网上截取的。
  • 可以看出澳大利亚总共有8中气候,如果能够把49个地点转换成八种不同的气候,这个 信息应该会对是否下雨的判断比较有用。

在这里插入图片描述

  • 基于气象局和ABCB的数据,我制作了澳大利亚主要城市所对应的 气候类型数据,并保存在csv文件city_climate.csv当中。然后,我使用以下代码,在google上进行爬虫,爬出了每 个城市所对应的经纬度,并保存在数据cityll.csv当中,代码网上都有,这里不在展示,可以直接参考“写在前面“得连接查看源码

  • **为什么我们会需要城市的经纬度呢?**我曾经尝试过直接使用样本中的城市来爬取城市本身的气候,然而由于样本中 的地点名称,其实是气候站的名称,而不是城市本身的名称,因此不是每一个样本都能够直接获取到城市的气候。 比如说,如果我们搜索“海淀区气候”,搜索引擎返回的可能是海淀区现在的气温,而不是整个北京的气候类型。因 此,我们需要澳大利亚气象局的数据,来找到这些气候站所对应的城市。

  • 我们有了澳大利亚全国主要城市的气候,也有了澳大利亚主要城市的经纬度(地点),我们就可以通过计算我们样 本中的每个气候站到各个主要城市的地理距离,来找出一个离这个气象站近的主要城市,而这个主要城市的气候 就是我们样本点所在的地点的气候。

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

  • 接下来 我们要开始计算我们样本上的地点到每个澳大利亚主要城市的距离,而离我们的样本地点近的那个澳大利亚主要 城市的气候,就是我们样本点的气候。

  • 地理上,两点之间的距离公式可以查阅,这真的不太重要,又不是学地理的,直接拿过来用就行了。
    在这里插入图片描述

  • 有了每个样本城市所对应的气候,我们接下来就使用气候来替掉原本的城市,原本的气象站的名称。

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

总结一下地点的处理:
1.获取主要城市的气候数据
2.获取主要城市的经纬度
3.获取样本城市(气象局)的经纬度,利用最近邻原则获取样本城市对应的气候数据
4.将样本的地点数据-------->气候数据

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

  • 分类型变量的缺失值填补肯定要考虑众数,那就存在一个问题,用哪个数据集上的众数,考虑三种情况:用训练集;用测试集;用整个数据集。考虑到数据量非常充足,我们直接可以使用训练集的众数,也就是说使用训练集的众数不仅填补训练集的离散缺失值,而且填补连续缺失值。
  • 首先找出dtype=object的特征,他们一定是分类型特征,其次还要分析一下是否还有其它分类型特征。我们发现云的密度等级也是分类型的特征,数据类型为int
#首先找出,分类型特征都有哪些
cate = Xtrain.columns[Xtrain.dtypes == "object"].tolist()
#除了特征类型为"object"的特征们,还有虽然用数字表示,但是本质为分类型特征的云层遮蔽程度
cloud = ["Cloud9am","Cloud3pm"]
cate = cate + cloud
print(cate)

在这里插入图片描述

  • 开始填补,填补完之后查看填补效果
#对于分类型特征,我们使用众数来进行填补
from sklearn.impute import SimpleImputer #0.20, conda, pip

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.loc[:,cate].isnull().mean()

在这里插入图片描述

  • 发现离散特征已经没有缺失值了

2.3.2.4 处理分类型变量:编码

  • 填补完分类型变量,我们还要对其进行编码,使计算机可以识别计算,编码这里不采用独热编码,就是普通的编码,当然,也可以使用独热,很灵活
#将所有的分类型变量编码为数字,一个类别是一个数字
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])
print(Xtrain.loc[:,cate].head())
print(Xtest.loc[:,cate].head())

下图是训练集编码完成后
在这里插入图片描述
下图是测试集编码完成后
在这里插入图片描述

  • OK,到这里分类型数据就处理完了。

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

  • 对于连续型变量,我们仍然需要填补缺失值,我们不需要进行编码,但是需要进行无量纲化。
  • 我们直接使用训练集的均值来填充
#实例化模型,填补策略为"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])

print(Xtrain.isnull().mean())

在这里插入图片描述

  • 填补完毕,目前在整个数据集上已经没有了缺失值

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

  • 这里标准化,把数据处理为均值为0,方差为1的数据,注意这并不改变原始数据的分布,不会将数据的分布变为高斯分布,只是修改了均值和方差。
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])

print(Xtrain.head())

在这里插入图片描述

2.4 特征工程结束

至此,我们终于完成了特征工程。

3. 建模(Use SVM)

#建模选择自然是我们的支持向量机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"))

在这里插入图片描述

  • 我们发现无论从精确度还是召回率来说,线性核都表现的很好,这说明,我们的数据基本上就是线性的了,我们就可以不考虑其它核函数了。

4. 模型调参

注意到,模型的准确率和auc曲线都还可以,recall可太低了,这代表我们能把雨天预测对的概率很小,我们需要着重考虑一下recall的优化

4.1 Recall调节

  • 从原理上来说,我们可以改变样本的权重,我们对少数类(雨天)的权重加大,这样就可以对少数类的预测更加正确,即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"))

在这里插入图片描述

  • 验证成功,Recall大大增加,发生了质的变化,同时准确率并没有很大的下降,这就是我们想要的。通过上述分析,我们也可以总结出一个道理,样本不平衡问题在传统机器学习中可以通过改变权重来解决

4.2 模型换成逻辑回归,获取更高的准确率

from sklearn.linear_model import LogisticRegression as LR
logclf = LR(solver="liblinear").fit(Xtrain, Ytrain)
logclf.score(Xtest,Ytest)

# result
0.8486666666666667,比SVM稍微好一点

4.2.1 逻辑回归的调参C

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))

在这里插入图片描述

  • 可以看到逻辑回归的精确度极限差不多在84.933%

4.3 模型换成随机森林,获取更高的准确率

  • 准确率:0.8584021005251313
  • n_estimator=160
# 随机森林
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
superpa = []

for i in range(200):
    rfc = RandomForestClassifier(n_estimators=i+1)
    rfc.fit(Xtrain,Ytrain)
    rfc_s = rfc.score(Xtest,Ytest)
    
    superpa.append(rfc_s)
print("最大的准确率为:{}".format(max(superpa)),"对应的树的数量为:{}".format(superpa.index(max(superpa))))

在这里插入图片描述

4.4 追寻Recall和假正率的平衡

我们追寻的是Recall和假正率差值最大的时候的两个值。因为,随着Recall增加,我们捕捉少数类的能力越来越高,但同时也会将多数类判成少数类,我们希望在尽量捕捉到少数类的同时减少多数类判成少数类也就是FPR。换成数学语言就是,Recall越大越好,FPR越小越好。

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()

在这里插入图片描述

  • 可以看到我们的ROC曲线还是很好的,下面我们找出对应的阈值就可以进行预测了。这里的阈值在SVM中被称为置信度,蕴含距离的意思。
from sklearn.metrics import accuracy_score as AC
maxindex = (Recall - FPR).tolist().index(max(Recall - FPR))

thresholds[maxindex]

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

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"))

在这里插入图片描述

5. 总结

SVM的最佳准确率为84.40%;
LR的最佳准确率为84.93%
RF的最佳准确率为85.84%
最终我们选择RF模型作为RF

6. 写在后面

  • 本博客肯定有很多不足,仅供学习和交流

猜你喜欢

转载自blog.csdn.net/weixin_44441131/article/details/106505160