在本课程中,您将学习构建伟大机器学习模型的最重要步骤之一:特征工程。您将学习如何:
利用互信息确定哪些特征最重要
在多个实际问题域中发明新特性
使用目标编码对高基数分类进行编码
用k-means聚类创建分割特征
用主成分分析法将数据集的变化分解为特征
这些实践练习构成了一个完整的笔记本,它应用了所有这些技巧,向房价开始竞争提交意见。完成本课程后,你会有一些想法,可以用来进一步提高你的表现。
你准备好了吗?走吧!
特征工程的目标只是使您的数据更适合当前的问题。
考虑“表观温度”措施,如热指数和风寒。这些量试图根据空气温度、湿度和风速来测量人类感知到的温度,这些我们可以直接测量的东西。你可以把表面温度想象成一种特征工程的结果,一种试图让观测到的数据与我们真正关心的东西更相关的尝试:外界的真实感受!
您可以执行功能工程来:
提高模型的预测性能
减少计算或数据需求
提高结果的可解释性
为了使特性有用,它必须与模型能够学习的目标有关系。例如,线性模型只能学习线性关系。因此,使用线性模型时,您的目标是变换特征,使其与目标的关系呈线性。
这里的关键思想是,应用于特征的转换本质上成为模型本身的一部分。假设你试图从一边的长度来预测正方形地块的价格。将线性模型直接拟合到长度会产生很差的结果:这种关系不是线性的。
沿x轴的长度散点图,沿y轴的价格散点图,曲线中的点增加,叠加了一条拟合不良的线。
线性模型仅以长度作为特征,拟合度很差。
如果我们平方长度特征得到'面积',但是,我们创建一个线性关系。向特征集中添加面积意味着该线性模型现在可以拟合抛物线。换言之,对特征进行平方处理,使线性模型具有拟合平方特征的能力。
左:区域现在在x轴上。点以线性形状增加,并叠加一条拟合良好的线。右:现在x轴上的长度。曲线中的点会像以前一样增加,并且会叠加一条拟合良好的曲线。
左:适合的区域更好。右图:这也使得合身的长度更好。
这应该向您展示为什么在特性工程中可以获得如此高的时间回报。无论你的模型学不到什么关系,你都可以通过转换来提供你自己。在开发特性集时,请考虑模型可以使用哪些信息来实现最佳性能。
为了说明这些想法,我们将看到向数据集添加一些合成特征如何提高随机森林模型的预测性能。
混凝土数据集包含各种混凝土配方和最终产品的抗压强度,抗压强度是衡量这种混凝土能承受多大荷载的指标。该数据集的任务是根据混凝土的配方预测其抗压强度。
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
df = pd.read_csv("../input/fe-course-data/concrete.csv")
df.head()
你可以在这里看到各种各样的混凝土成分。稍后我们将看到,添加一些从这些特性派生的附加合成特性如何帮助模型了解它们之间的重要关系。
我们将首先通过在未扩充的数据集上训练模型来建立基线。这将帮助我们确定我们的新特性是否真的有用。
在特性工程过程的开始阶段,建立这样的基线是一个很好的实践。基准分数可以帮助你决定你的新特性是否值得保留,或者你是否应该放弃它们,或者尝试其他东西。
X = df.copy()
y = X.pop("CompressiveStrength")
# 训练和评分基线模型
baseline = RandomForestRegressor(criterion="mae", random_state=0)
baseline_score = cross_val_score(
baseline, X, y, cv=5, scoring="neg_mean_absolute_error"
)
baseline_score = -1 * baseline_score.mean()
print(f"MAE Baseline Score: {baseline_score:.4}")
如果你曾经在家做饭,你可能知道配方中的配料比例通常比绝对量更能预测配方的结果。我们可以推断,上述特征的比值将是压缩强度的一个很好的预测指标。
下面的单元格向数据集添加了三个新的比率特性。
X = df.copy()
y = X.pop("CompressiveStrength")
# Create synthetic features
X["FCRatio"] = X["FineAggregate"] / X["CoarseAggregate"]
X["AggCmtRatio"] = (X["CoarseAggregate"] + X["FineAggregate"]) / X["Cement"]
X["WtrCmtRatio"] = X["Water"] / X["Cement"]
# Train and score model on dataset with additional ratio features
model = RandomForestRegressor(criterion="mae", random_state=0)
score = cross_val_score(
model, X, y, cv=5, scoring="neg_mean_absolute_error"
)
score = -1 * score.mean()
print(f"MAE Score with Ratio Features: {score:.4}")
果然,性能提高了!这是证据,这些新的比率特征暴露了重要的信息模型,它以前没有检测到。
我们已经看到,设计新特性可以提高模型性能。但是如何识别数据集中可能有用的组合特征呢?利用互信息发现有用的特征。
第一次遇到一个新的数据集有时会感到难以承受。你可能会看到成百上千的特性,甚至连描述都没有。你从哪里开始?
一个伟大的第一步是建立一个排名与特征效用指标,一个功能衡量之间的联系特征和目标。然后,您可以选择一组较小的最有用的特性进行初步开发,并对您的时间会得到很好的利用有更多的信心。
我们将使用的度量称为“互信息”。互信息很像相关性,因为它衡量两个量之间的关系。互信息的优点是它可以检测任何类型的关系,而相关只能检测线性关系。
互信息是一个很好的通用度量,在特性开发的开始阶段,当您可能还不知道要使用什么模型时,它特别有用。它是:
易于使用和解释,
计算效率高,
理论上有充分的根据,
防止过度装配,以及,
能够发现任何一种关系
互信息用不确定性来描述关系。两个量之间的互信息(MI)是对一个量的知识减少另一个量的不确定性的程度的度量。如果你知道一个特性的价值,你会对目标更有信心吗?
这是一个来自艾姆斯住房数据的例子。该图显示了房屋外观质量与售价之间的关系。每一点代表一座房子。
四类资质:一般、典型、良好、优秀。每个类别内销售价格的散点图。
了解房子的外观质量可以减少销售价格的不确定性。
从图中我们可以看出,知道ExterQual的值应该会让你对相应的SalePrice更加确定——ExterQual的每一个类别都倾向于将SalePrice集中在一定的范围内。ExterQual与SalePrice之间的互信息是在ExterQual的四个值上SalePrice不确定性的平均减少。例如,由于Fair出现的频率比一般情况要低,Fair在MI得分中的权重就更小。
(技术说明:我们所说的不确定性是用信息论中称为“熵”的量来衡量的。一个变量的熵大致意思是:“平均来说,你需要多少个是或否的问题来描述这个变量的发生。”你要问的问题越多,你对这个变量的不确定性就越大。互信息是指您希望功能能够回答多少有关目标的问题。)
数量之间的最小可能互信息为0.0。当MI为零时,量是独立的:两者都不能告诉你关于另一个的任何事情。相反地,理论上MI的上限是不存在的。但实际上,高于2.0左右的值并不常见(互信息是一个对数量,因此它的增长非常缓慢。)
下一个图将告诉您MI值如何对应于特性与目标的关联类型和程度。
左:互信息随着特征和目标之间的依赖性变得更紧密而增加。右:互信息可以捕获任何类型的关联(不仅仅是线性的,比如相关性)
在应用互信息时,请记住以下几点:
MI可以帮助您了解特征作为目标预测因子的相对潜力,这是由它自己考虑的。
一个特性在与其他特性交互时可能信息量很大,但单独一个特性信息量不大。MI无法检测功能之间的交互。它是一个单变量度量。
功能的实际用途取决于使用它的模型。特征只有在其与目标的关系是您的模型可以了解的情况下才有用。仅仅因为一个特性有一个很高的MI分数并不意味着你的模型能够用这个信息做任何事情。您可能需要先转换功能以公开关联。
汽车数据集由1985车型年的193辆汽车组成。这个数据集的目标是从汽车的23个特征(如品牌、车身风格和马力)中预测汽车的价格(目标)。在本例中,我们将使用互信息对特征进行排序,并通过数据可视化研究结果。
这个隐藏单元导入一些库并加载数据集。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
plt.style.use("seaborn-whitegrid")
df = pd.read_csv("../input/fe-course-data/autos.csv")
df.head()
该算法对离散特征的处理与连续特征不同。因此,你需要告诉它是哪一个。根据经验,任何必须具有浮点dtype的内容都不是离散的。分类(对象或分类dtype)可以通过给它们一个标签编码来处理为离散的(您可以在分类变量课程中查看标签编码。)
X = df.copy()
y = X.pop("price")
# 分类的标签编码
for colname in X.select_dtypes("object"):
X[colname], _ = X[colname].factorize()
# 所有离散特性现在都应该具有整数数据类型(在使用MI之前请仔细检查此项!)
discrete_features = X.dtypes == int
Scikit learn在其特征选择模块中有两个互信息度量:一个用于实值目标(互信息回归),另一个用于分类目标(互信息分类)。我们的目标价格是实价。下一个单元格计算我们的功能的MI分数,并将它们封装在一个漂亮的数据帧中。
from sklearn.feature_selection import mutual_info_regression
def make_mi_scores(X, y, discrete_features):
mi_scores = mutual_info_regression(X, y, discrete_features=discrete_features)
mi_scores = pd.Series(mi_scores, name="MI Scores", index=X.columns)
mi_scores = mi_scores.sort_values(ascending=False)
return mi_scores
mi_scores = make_mi_scores(X, y, discrete_features)
mi_scores[::3] # show a few features with their MI scores
现在是一个条形图,便于比较:
def plot_mi_scores(scores):
scores = scores.sort_values(ascending=True)
width = np.arange(len(scores))
ticks = list(scores.index)
plt.barh(width, scores)
plt.yticks(width, ticks)
plt.title("Mutual Information Scores")
plt.figure(dpi=100, figsize=(8, 5))
plot_mi_scores(mi_scores)
数据可视化是一个伟大的后续效用排名。让我们仔细看看这两个。
正如我们所料,高分加铺重量特征与目标价格表现出很强的关系。
sns.relplot(x="curb_weight", y="price", data=df);
燃料类型的功能有一个相当低的MI分数,但正如我们可以从图中看到的,它清楚地区分了两个不同的马力功能趋势的价格人群。这表明,燃料类型有助于相互作用的影响,并可能不是不重要的毕竟。在从MI分数判断一个特性不重要之前,最好先研究一下任何可能的交互影响——领域知识可以在这里提供很多指导。
sns.lmplot(x="horsepower", y="price", hue="fuel_type", data=df);
数据可视化是对功能工程工具箱的一个很好的补充。与互信息等实用性度量一起,这些可视化可以帮助您发现数据中的重要关系。查看我们的数据可视化课程了解更多!
一旦您确定了一组具有某种潜力的特性,就应该开始开发它们了。在本课中,您将学习完全可以在Pandas中完成的一些常见转换。如果你感到生疏,我们有一个关于熊猫的好课程。
在本课中,我们将使用具有一系列特征类型的四个数据集:美国交通事故、1985年汽车、具体公式和客户终身价值。下面隐藏的单元格将加载它们。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True)
plt.rc(
"axes",
labelweight="bold",
labelsize="large",
titleweight="bold",
titlesize=14,
titlepad=10,
)
accidents = pd.read_csv("../input/fe-course-data/accidents.csv")
autos = pd.read_csv("../input/fe-course-data/autos.csv")
concrete = pd.read_csv("../input/fe-course-data/concrete.csv")
customer = pd.read_csv("../input/fe-course-data/customer.csv")
发现新功能的提示
了解特征。请参阅数据集的数据文档(如果可用)。
研究问题领域,获取领域知识。如果你的问题是预测房价,那就做一些房地产方面的研究。维基百科可以是一个很好的起点,但书籍和期刊文章往往会有最好的信息。
学习以前的工作。从过去的Kaggle比赛中总结的解决方案是一个很好的资源。
使用数据可视化。可视化可以揭示病变的分布特征或复杂的关系,可以简化。在完成特征工程过程时,一定要可视化数据集。
数字特征之间的关系通常是通过数学公式来表达的,这是你在领域研究中经常遇到的。在Pandas中,可以对列应用算术运算,就像它们是普通数字一样。
在汽车数据集中有描述汽车发动机的特征。研究产生了各种各样的公式来创建潜在有用的新功能。例如,“冲程比”是衡量发动机效率和性能的一个指标:
autos["stroke_ratio"] = autos.stroke / autos.bore
autos[["stroke", "bore", "stroke_ratio"]].head()
组合越复杂,模型学习起来就越困难,比如发动机的“排量”(衡量发动机功率的一种方法)公式:
autos["displacement"] = (
np.pi * ((0.5 * autos.bore) ** 2) * autos.stroke * autos.num_of_cylinders
)
数据可视化可以建议变换,通常是通过幂或对数对特征进行“重塑”。例如,美国事故中的风速分布是高度倾斜的。在这种情况下,对数可以有效地将其标准化:
# 如果功能的值为0.0,请使用np.log1p(log(1+x))而不是np.log
accidents["LogWindSpeed"] = accidents.WindSpeed.apply(np.log1p)
# 绘制比较图
fig, axs = plt.subplots(1, 2, figsize=(8, 4))
sns.kdeplot(accidents.WindSpeed, shade=True, ax=axs[0])
sns.kdeplot(accidents.LogWindSpeed, shade=True, ax=axs[1]);
查看我们关于数据清理中规范化的课程,您还将了解Box-Cox变换,这是一种非常通用的规范化器。
计数
描述某物的存在或不存在的特征通常是成套的,比如说,一组疾病的危险因素。可以通过创建计数来聚合这些功能。
这些特性将是二进制的(1表示存在,0表示不存在)或布尔的(True或False)。在Python中,布尔可以相加,就像它们是整数一样。
在交通事故中,有几个特征表明某个道路物体是否在事故附近。这将使用求和方法创建附近道路特征总数的计数:
roadway_features = ["Amenity", "Bump", "Crossing", "GiveWay",
"Junction", "NoExit", "Railway", "Roundabout", "Station", "Stop",
"TrafficCalming", "TrafficSignal"]
accidents["RoadwayFeatures"] = accidents[roadway_features].sum(axis=1)
accidents[roadway_features + ["RoadwayFeatures"]].head(10)
您还可以使用dataframe的内置方法来创建布尔值。在混凝土数据集中是混凝土配方中成分的数量。许多配方缺少一个或多个组分(即,该组分的值为0)。这将计算dataframe的内置大于gt方法的公式中有多少组件:
components = [ "Cement", "BlastFurnaceSlag", "FlyAsh", "Water",
"Superplasticizer", "CoarseAggregate", "FineAggregate"]
concrete["Components"] = concrete[components].gt(0).sum(axis=1)
concrete[components + ["Components"]].head(10)
构建和分解功能
通常你会有复杂的字符串,可以有效地分解成更简单的片段。一些常见的例子:
身份证号码:“123-45-6789”
电话号码:'(999)555-0123'
街道地址:“内华达州鹅市卡格尔路8241号”
Internet地址:'http://www.kaggle.com
产品代码:“0 36000 29145 2”
日期和时间:“2013年9月30日星期一07:06:05”
像这样的特性通常会有一些您可以利用的结构。例如,美国电话号码有一个区号(999)部分,告诉你呼叫者的位置。像往常一样,一些研究可以在这里得到回报。
str访问器允许您将string方法(如split)直接应用于列。客户终身价值数据集包含描述保险公司客户的特征。从策略功能中,我们可以将类型与覆盖级别分开:
customer[["Type", "Level"]] = ( # 创建两个新功能
customer["Policy"] # 从策略功能
.str # 通过字符串访问器
.split(" ", expand=True) # 在“”上拆分
# 并将结果展开为单独的列
)
customer[["Policy", "Type", "Level"]].head(10)
如果您有理由相信在组合中存在一些交互,那么您也可以将简单的功能连接到组合的功能中:
autos["make_and_style"] = autos["make"] + "_" + autos["body_style"]
autos[["make", "body_style", "make_and_style"]].head()
在Kaggle的其他地方学习
还有一些其他类型的数据,我们在这里没有谈到,特别是丰富的信息。幸运的是,我们帮你搞定了!
有关日期和时间,请参阅数据清理课程中的解析日期。
有关纬度和经度,请参阅我们的地理空间分析课程。
对于文本,尝试自然语言处理。
最后,我们有组转换,它将信息聚合到按某个类别分组的多个行中。通过群体转换,你可以创建诸如“一个人居住状态的平均收入”或“一个工作日按类型发布的电影比例”之类的功能。如果你发现了一个类别交互,那么在该类别上进行群体转换可能是一个很好的调查方法。
使用聚合函数,组转换组合了两个功能:提供分组的分类功能和要聚合其值的另一个功能。对于“州平均收入”,您可以为分组特征选择州,为聚合函数选择平均值,为聚合特征选择收入。为了在Pandas中计算这个,我们使用groupby和transform方法:
customer["AverageIncome"] = (
customer.groupby("State") # for each state
["Income"] # select the income
.transform("mean") # and compute its mean
)
customer[["State", "Income", "AverageIncome"]].head(10)
mean函数是一个内置的dataframe方法,这意味着我们可以将它作为字符串传递以进行转换。其他方便的方法包括max、min、median、var、std和count。下面是如何计算数据集中每个状态出现的频率:
customer["StateFreq"] = (
customer.groupby("State")
["State"]
.transform("count")
/ customer.State.count()
)
customer[["State", "StateFreq"]].head(10)
您可以使用这样的转换为分类特征创建“频率编码”。
如果您使用的是训练和验证拆分,为了保持它们的独立性,最好只使用训练集创建一个分组的特征,然后将其加入到验证集中。在训练集中创建了一组唯一的值,其中包含drop\u duplicates之后,我们可以使用验证集的merge方法:
# 创建拆分
df_train = customer.sample(frac=0.5)
df_valid = customer.drop(df_train.index)
# 在训练集中按覆盖类型创建平均索赔金额
df_train["AverageClaim"] = df_train.groupby("Coverage")["ClaimAmount"].transform("mean")
# 将值合并到验证集中
df_valid = df_valid.merge(
df_train[["Coverage", "AverageClaim"]].drop_duplicates(),
on="Coverage",
how="left",
)
df_valid[["Coverage", "AverageClaim"]].head(10)
创建要素的提示
在创建特性时,要记住模型自身的优势和弱点是很好的。以下是一些指导原则:
线性模型自然地学习和和和差,但不能学到更复杂的东西。
对于大多数模型来说,比率似乎很难学习。比率组合通常会导致一些简单的性能提高。
线性模型和神经网络通常在归一化特征下表现较好。神经网络特别需要将特征缩放到距离0不太远的值。基于树的模型(如随机森林和XGBoost)有时可以从标准化中获益,但通常情况下,这样做的效果要少得多。
树模型可以学习近似任何特征组合,但是当组合特别重要时,它们仍然可以从显式创建中受益,特别是在数据有限的情况下。
计数对于树模型特别有用,因为这些模型没有一种自然的方法,可以同时跨多个特性聚合信息。
这一课和下一课利用了所谓的无监督学习算法。无监督算法不使用目标;相反,它们的目的是学习数据的某些属性,以某种方式表示特征的结构。在用于预测的特征工程的上下文中,可以将无监督算法视为“特征发现”技术。
聚类仅仅意味着根据数据点之间的相似程度将数据点分配给组。聚类算法可以说是“物以类聚”。
当用于特征工程时,我们可以尝试发现代表某个细分市场的客户群,例如,或者具有相似天气模式的地理区域。添加一个聚类标签可以帮助机器学习模型解开复杂的空间或邻近关系。
将标签作为要素进行聚类
应用于单个实值特征,聚类就像传统的“分块”或“离散化”变换。在多个特征上,这就像“多维组合”(有时称为矢量量化)。
左:聚类单个特征。右:跨两个特征进行聚类。
添加到数据帧后,群集标签的功能可能如下所示:
记住这个集群特性是绝对的,这一点很重要。在这里,它显示了一个标签编码(即,作为一个整数序列)作为一个典型的聚类算法将产生;根据您的型号,单热编码可能更合适。
添加集群标签的动机是,集群将把跨功能的复杂关系分解为更简单的块。然后,我们的模型就可以一个接一个地学习简单的块,而不是一次学习复杂的整体。这是一种“分而治之”的策略。
对YearBuild特性进行聚类有助于此线性模型了解其与SalePrice的关系。
该图显示了集群如何改进简单的线性模型。对于这种模型来说,年生产价格和销售价格之间的曲线关系太复杂了——它不适合。然而,在较小的块上,这种关系几乎是线性的,而且模型很容易学习。
k-均值聚类
聚类算法有很多种。它们的区别主要在于如何衡量“相似性”或“接近性”,以及它们处理的是什么样的特征。我们将使用的算法,k-means,是直观的,并且很容易在特征工程环境中应用。根据您的应用程序,另一种算法可能更合适。
K-means聚类使用普通的直线距离(换句话说,欧氏距离)来度量相似性。它通过在特征空间中放置一些称为质心的点来创建簇。数据集中的每个点都被分配给它最接近的质心所在的簇。“k-means”中的“k”是它创建了多少个质心(即簇)。你自己定义k。
你可以想象每个质心通过一系列辐射圆捕捉点。当来自竞争质心的一组圆重叠时,它们形成一条线。结果就是所谓的Voronoi细分。tessallation向您显示未来数据将分配到哪些集群;细分本质上是k-means从其训练数据中学到的东西。
上述Ames数据集上的聚类是k-均值聚类。这是同一个图,显示了镶嵌和质心。
K-means聚类创建特征空间的Voronoi细分。
让我们回顾一下k-means算法是如何学习聚类的,以及这对特征工程意味着什么。我们将关注scikitlearn实现中的三个参数:n\u clusters、max\u iter和n\u init。
这是一个简单的两步过程。该算法首先随机初始化一些预定义数目的质心。然后迭代这两个操作:
将点指定给最近的簇质心
移动每个质心以最小化到其点的距离
它在这两个步骤上迭代,直到质心不再移动,或者直到通过了某个最大迭代次数(max)。
通常情况下,质心的初始随机位置以较差的聚类结束。为此,算法重复多次(n_init)并返回每个点与其形心之间总距离最小的聚类,即最优聚类。
下面的动画显示了正在运行的算法。它说明了结果对初始质心的依赖性以及迭代直到收敛的重要性。
纽约Airbnb租房的K-means聚类算法。
对于大量集群,您可能需要增加max\u iter;对于复杂数据集,您可能需要增加n\u init。通常情况下,您需要选择的唯一参数是n\u clusters(即k)。一组特性的最佳分区取决于您正在使用的模型和您试图预测的内容,因此最好像任何超参数一样对其进行优化(例如,通过交叉验证)。
示例-加州住房
作为空间特征,加州住宅的“纬度”和“经度”自然成为k-means聚类的候选。在本例中,我们将这些数据与“MedInc”(中等收入)进行聚类,以在加利福尼亚州的不同地区创建经济细分。
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from sklearn.cluster import KMeans
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True)
plt.rc(
"axes",
labelweight="bold",
labelsize="large",
titleweight="bold",
titlesize=14,
titlepad=10,
)
df = pd.read_csv("../input/fe-course-data/housing.csv")
X = df.loc[:, ["MedInc", "Latitude", "Longitude"]]
X.head()
由于k-means聚类对尺度非常敏感,因此可以使用极值重新缩放或规范化数据。我们的功能已经大致处于相同的规模,所以我们将保持原样。
现在让我们看看几个情节,看看这有多有效。首先,散点图显示了集群的地理分布。这一算法似乎为沿海收入较高的地区创建了单独的细分市场。
sns.relplot(
x="Longitude", y="Latitude", hue="Cluster", data=X, height=6,
);
此数据集中的目标是MedHouseVal(房屋价值中值)。这些方框图显示了目标在每个簇中的分布。如果聚类是信息性的,那么这些分布在很大程度上应该在MedHouseVal中分开,这确实是我们所看到的。
X["MedHouseVal"] = df["MedHouseVal"]
sns.catplot(x="MedHouseVal", y="Cluster", data=X, kind="boxen", height=6);
在上一课中,我们研究了第一种基于模型的特征工程方法:聚类。在这节课中,我们看下一节课:主成分分析(PCA)。就像聚类是基于邻近度对数据集进行分区一样,您可以将PCA看作是对数据中的变化进行分区。PCA是一个很好的工具,可以帮助您发现数据中的重要关系,也可以用来创建更多的信息特征。
(技术说明:PCA通常应用于标准化数据。对于标准化数据,“变化”意味着“相关性”。对于非标准数据,“变化”意味着“协方差”。在应用PCA之前,本课程中的所有数据都将标准化。)
鲍鱼数据集中有几千只塔斯马尼亚鲍鱼的物理测量数据(鲍鱼是一种海洋生物,很像蛤蜊或牡蛎。)我们现在只看几个特征:它们贝壳的“高度”和“直径”。
你可以想象,在这些数据中有“变异轴”,它们描述了鲍鱼彼此之间的差异。从图形上看,这些轴显示为沿着数据的自然维度的垂直线,每个原始特征对应一个轴。
通常,我们可以给这些变化轴命名。我们可以称之为“尺寸”组件的长轴:小高度和小直径(左下)与大高度和大直径(右上)形成对比。我们可以称之为“形状”的短轴组件:小高度和大直径(扁平形状)与大高度和小直径(圆形形状)形成对比。
请注意,与其用“高度”和“直径”来描述鲍鱼,不如用“大小”和“形状”来描述鲍鱼。事实上,这就是主成分分析的全部思想:我们用变异轴来描述数据,而不是用原始特征来描述数据。变异轴成为新的特征。
主成分通过特征空间中数据集的旋转而成为新的特征。
新特征PCA构造实际上只是原始特征的线性组合(加权和):
这些新特征被称为数据的主成分。重量本身称为载荷。主成分的数量将与原始数据集中的特征数量一样多:如果我们使用十个特征而不是两个特征,那么我们最终将得到十个成分。
组件的负载告诉我们它通过符号和大小表达的变化:
这张荷载表告诉我们,在尺寸构件中,高度和直径沿同一方向(同一符号)变化,但在形状构件中,它们沿相反方向(相反符号)变化。在每个分量中,荷载的大小都是相同的,因此特征对两者的贡献是相等的。
主成分分析还告诉我们每个成分的变化量。从图中可以看出,数据沿尺寸分量的变化比沿形状分量的变化大。主成分分析通过每个成分的解释方差百分比来精确计算。
大小约占96%,形状约占4%的变异之间的高度和直径。
尺寸分量捕捉高度和直径之间的大部分变化。然而,重要的是要记住,一个组成部分的方差量并不一定与它作为一个预测因子的好坏相对应:它取决于你试图预测什么。
特征工程的PCA方法
有两种方法可以将PCA用于特征工程。
第一种方法是使用它作为一种描述性的技术。因为组件告诉你变化,你可以计算组件的MI分数,看看什么样的变化最能预测你的目标。这可能会给你一些想法来创建各种特征——比如说,如果“尺寸”很重要,那么是“高度”和“直径”的乘积;如果形状很重要,那么是“高度”和“直径”的比值。您甚至可以尝试对一个或多个高分组件进行聚类。
第二种方法是使用组件本身作为特征。因为组件直接暴露了数据的变化结构,所以它们通常比原始特征信息更丰富。以下是一些用例:
降维:当特征高度冗余(特别是多重共线)时,PCA将把冗余划分成一个或多个接近零方差的分量,然后可以删除这些分量,因为它们将包含很少或没有信息。
异常检测:从原始特征看不明显的异常变化,通常会出现在低方差分量中。在异常或离群点检测任务中,这些组件可能具有很高的信息量。
降噪:传感器读数的集合通常会共享一些常见的背景噪声。PCA有时可以将(信息性的)信号收集成少量的特征,而不考虑噪声,从而提高信噪比。
去相关:一些ML算法难以处理高度相关的特征。主成分分析将相关特征转换为不相关的成分,这对你的算法来说更容易处理。
PCA基本上可以让您直接访问数据的相关结构。毫无疑问,你会提出自己的申请!
PCA最佳实践
在应用PCA时,需要记住以下几点:
PCA只适用于数字特征,如连续数量或计数。
PCA对尺度敏感。在应用PCA之前标准化你的数据是一个很好的做法,除非你知道你有充分的理由不这样做。
考虑去除或约束离群值,因为它们会对结果产生不适当的影响。
示例-1985汽车
在本例中,我们将返回到汽车数据集并应用PCA,将其用作发现特征的描述性技术。我们将在练习中查看其他用例。
这个隐藏单元格加载数据并定义函数plot\u variance和make\u mi\u scores。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from IPython.display import display
from sklearn.feature_selection import mutual_info_regression
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True)
plt.rc(
"axes",
labelweight="bold",
labelsize="large",
titleweight="bold",
titlesize=14,
titlepad=10,
)
def plot_variance(pca, width=8, dpi=100):
# Create figure
fig, axs = plt.subplots(1, 2)
n = pca.n_components_
grid = np.arange(1, n + 1)
# Explained variance
evr = pca.explained_variance_ratio_
axs[0].bar(grid, evr)
axs[0].set(
xlabel="Component", title="% Explained Variance", ylim=(0.0, 1.0)
)
# Cumulative Variance
cv = np.cumsum(evr)
axs[1].plot(np.r_[0, grid], np.r_[0, cv], "o-")
axs[1].set(
xlabel="Component", title="% Cumulative Variance", ylim=(0.0, 1.0)
)
# Set up figure
fig.set(figwidth=8, dpi=100)
return axs
def make_mi_scores(X, y, discrete_features):
mi_scores = mutual_info_regression(X, y, discrete_features=discrete_features)
mi_scores = pd.Series(mi_scores, name="MI Scores", index=X.columns)
mi_scores = mi_scores.sort_values(ascending=False)
return mi_scores
df = pd.read_csv("../input/fe-course-data/autos.csv")
我们选择了四个功能,涵盖了一系列属性。这些特性中的每一个都有一个很高的MI分数,目标是价格。我们将对数据进行标准化,因为这些特性自然不在同一范围内。
features = ["highway_mpg", "engine_size", "horsepower", "curb_weight"]
X = df.copy()
y = X.pop('price')
X = X.loc[:, features]
# Standardize
X_scaled = (X - X.mean(axis=0)) / X.std(axis=0)
现在我们可以拟合scikit-learn的PCA估计并创建主成分。您可以在这里看到转换数据集的前几行。
from sklearn.decomposition import PCA
# Create principal components
pca = PCA()
X_pca = pca.fit_transform(X_scaled)
# Convert to dataframe
component_names = [f"PC{i+1}" for i in range(X_pca.shape[1])]
X_pca = pd.DataFrame(X_pca, columns=component_names)
X_pca.head()
拟合之后,PCA实例在其components属性中包含加载(不幸的是,PCA的术语不一致。我们遵循的约定是,在X中调用转换的列(组件,否则没有名称。)我们将在数据帧中包装加载。
loadings = pd.DataFrame(
pca.components_.T, # transpose the matrix of loadings
columns=component_names, # so the columns are the principal components
index=X.columns, # and the rows are the original features
)
loadings
我们在本课程中看到的大多数技术都是针对数值特征的。我们将在本课中介绍的技术,目标编码,是用于分类特征的。它是一种将类别编码为数字的方法,就像热编码或标签编码一样,区别在于它还使用目标来创建编码。这就是我们所说的有监督的特征工程技术。
import pandas as pd
autos = pd.read_csv("../input/fe-course-data/autos.csv")
目标编码是用从目标派生的数字替换特征类别的任何一种编码。
一个简单而有效的版本是应用第3课中的组聚合,如平均值。使用Automobiles数据集,计算每种车型的平均价格:
autos["make_encoded"] = autos.groupby("make")["price"].transform("mean")
autos[["make", "price", "make_encoded"]].head(10)
这种目标编码有时称为平均编码。应用于二进制目标,也称为二进制计数(您可能遇到的其他名称包括:似然编码、影响编码和遗漏编码。)
然而,这样的编码会带来一些问题。首先是未知类别。目标编码会产生过度拟合的特殊风险,这意味着它们需要接受独立的“编码”分割训练。当您将编码加入到将来的拆分中时,Pandas将为编码拆分中不存在的任何类别填充缺少的值。这些缺失的值你将不得不以某种方式进行插补。
其次是稀有类。当一个类别在数据集中只出现几次时,对其组计算的任何统计数据都不太可能非常准确。在汽车数据集中,商品制造只发生一次。我们计算的“平均”价格只是那一辆车的价格,这可能不太能代表我们将来可能看到的任何一辆Mercuries。目标编码罕见的类别可以使过度拟合的可能性更大。
解决这些问题的方法是增加平滑度。这样做的目的是将类别内平均数与总体平均数相结合。稀有类别在其类别平均值中的权重较小,而缺失类别只得到总体平均值。
在伪代码中:
encoding = weight * in_category + (1 - weight) * overall
其中,权重是从类别频率计算得出的介于0和1之间的值。
确定权重值的一种简单方法是计算m-估计:
weight = n / (n + m)
其中n是该类别在数据中出现的总次数。参数m确定“平滑因子”。m值越大,对总体估计的权重就越大。
在汽车数据集中,有三辆车与雪佛兰制造。如果您选择m=2.0,那么雪佛兰类别将以雪佛兰平均价格的60%加上整体平均价格的40%进行编码。
chevrolet = 0.6 * 6000.00 + 0.4 * 13285.03
目标编码用例
目标编码非常适合:
高基数特性:具有大量类别的特性可能很难编码:一个热编码会生成太多特性,而替代方案(如标签编码)可能不适合该特性。目标编码使用特征最重要的属性(与目标的关系)为类别派生数字。
领域驱动的特性:根据以前的经验,您可能会怀疑分类特性应该是重要的,即使它在特性度量方面得分很低。目标编码有助于揭示特征的真正信息性。
MovieLens1M数据集包含了MovieLens网站用户对100万部电影的评价,其中的功能描述了每个用户和电影。这个隐藏的单元格设置了一切:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import warnings
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True)
plt.rc(
"axes",
labelweight="bold",
labelsize="large",
titleweight="bold",
titlesize=14,
titlepad=10,
)
warnings.filterwarnings('ignore')
df = pd.read_csv("../input/fe-course-data/movielens1m.csv")
df = df.astype(np.uint8, errors='ignore') # reduce memory footprint
print("Number of Unique Zipcodes: {}".format(df["Zipcode"].nunique()))
有超过3000个类别,Zipcode特性是一个很好的目标编码候选,而且这个数据集的大小(超过一百万行)意味着我们可以腾出一些数据来创建编码。
我们将首先创建一个25%的分割来训练目标编码器。
X = df.copy()
y = X.pop('Rating')
X_encode = X.sample(frac=0.25)
y_encode = y[X_encode.index]
X_pretrain = X.drop(X_encode.index)
y_train = y[X_pretrain.index]
scikit learn contrib中的category\u encoders包实现了一个m-estimate编码器,我们将使用它来编码Zipcode特性。
from category_encoders import MEstimateEncoder
# 创建编码器实例。选择m控制噪音。
encoder = MEstimateEncoder(cols=["Zipcode"], m=5.0)
# 将编码器安装在编码分割上。
encoder.fit(X_encode, y_encode)
# 对Zipcode列进行编码以创建最终的训练数据
X_train = encoder.transform(X_pretrain)
让我们将编码值与目标值进行比较,看看编码的信息量有多大。
plt.figure(dpi=90)
ax = sns.distplot(y, kde=False, norm_hist=True)
ax = sns.kdeplot(X_train.Zipcode, color='r', ax=ax)
ax.set_xlabel("Rating")
ax.legend(labels=['Zipcode', 'Rating']);
编码的Zipcode特性的分布大致遵循实际收视率的分布,这意味着电影观看者在Zipcode和Zipcode之间的收视率差异很大,因此我们的目标编码能够捕获有用的信息。
欢迎来到房价特色工程项目——高级回归技术大赛!本次比赛使用的数据与您在特征工程课程练习中使用的数据几乎相同。我们将把你所做的工作汇总成一个完整的项目,你可以用你自己的想法来构建这个项目。
叉开这本笔记本!
单击右上角的“复制和编辑”按钮,创建您自己的可编辑笔记本副本。
第1步-准备工作
我们将首先导入练习中使用的包并设置一些笔记本默认值。如果您想查看我们将使用的库,请取消隐藏此单元格:
import os
import warnings
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from IPython.display import display
from pandas.api.types import CategoricalDtype
from category_encoders import MEstimateEncoder
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.feature_selection import mutual_info_regression
from sklearn.model_selection import KFold, cross_val_score
from xgboost import XGBRegressor
# Set Matplotlib defaults
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True)
plt.rc(
"axes",
labelweight="bold",
labelsize="large",
titleweight="bold",
titlesize=14,
titlepad=10,
)
# Mute warnings
warnings.filterwarnings('ignore')
在进行任何特征工程之前,我们都需要对数据进行预处理,以获得适合分析的形式。我们在课程中使用的数据比竞争数据简单一些。对于艾姆斯竞赛数据集,我们需要:
从CSV文件加载数据
清理数据以修复任何错误或不一致
对统计数据类型进行编码(数字、分类)
填补任何缺失值
我们将把所有这些步骤封装在一个函数中,这样您就可以在需要时轻松获得新的数据帧。在读取CSV文件之后,我们将应用三个预处理步骤,清理、编码和插补,然后创建数据分割:一个(df\u train)用于训练模型,另一个(df\u test)用于做出预测,您将提交给竞争对手在排行榜上得分。
def load_data():
# Read data
data_dir = Path("../input/house-prices-advanced-regression-techniques/")
df_train = pd.read_csv(data_dir / "train.csv", index_col="Id")
df_test = pd.read_csv(data_dir / "test.csv", index_col="Id")
# Merge the splits so we can process them together
df = pd.concat([df_train, df_test])
# Preprocessing
df = clean(df)
df = encode(df)
df = impute(df)
# Reform splits
df_train = df.loc[df_train.index, :]
df_test = df.loc[df_test.index, :]
return df_train, df_test
清除数据
此数据集中的某些分类功能在其分类中有明显的拼写错误:
data_dir = Path("../input/house-prices-advanced-regression-techniques/")
df = pd.read_csv(data_dir / "train.csv", index_col="Id")
df.Exterior2nd.unique()
将这些数据与数据_description.txt进行比较,可以看出需要清理的内容。我们将在这里处理几个问题,但您可能需要进一步评估这些数据。
def clean(df):
df["Exterior2nd"] = df["Exterior2nd"].replace({"Brk Cmn": "BrkComm"})
# Some values of GarageYrBlt are corrupt, so we'll replace them
# with the year the house was built
df["GarageYrBlt"] = df["GarageYrBlt"].where(df.GarageYrBlt <= 2010, df.YearBuilt)
# Names beginning with numbers are awkward to work with
df.rename(columns={
"1stFlrSF": "FirstFlrSF",
"2ndFlrSF": "SecondFlrSF",
"3SsnPorch": "Threeseasonporch",
}, inplace=True,
)
return df
对统计数据类型进行编码
Pandas具有与标准统计类型(numeric、categorical等)相对应的Python类型。用正确的类型对每个特性进行编码有助于确保每个特性都被我们使用的函数恰当地对待,并使我们更容易一致地应用转换。此隐藏单元格定义编码函数:
# 数字特征已正确编码(`float`表示continuous,`int`表示discrete),但我们需要做我们自己。请特别注意,`MSSubClass`特性是读作“int”类型,但实际上是(主格)范畴。
# 主格(无序)范畴特征
features_nom = ["MSSubClass", "MSZoning", "Street", "Alley", "LandContour", "LotConfig", "Neighborhood", "Condition1", "Condition2", "BldgType", "HouseStyle", "RoofStyle", "RoofMatl", "Exterior1st", "Exterior2nd", "MasVnrType", "Foundation", "Heating", "CentralAir", "GarageType", "MiscFeature", "SaleType", "SaleCondition"]
# 有序范畴特征
# 熊猫称这些类别为“等级”
five_levels = ["Po", "Fa", "TA", "Gd", "Ex"]
ten_levels = list(range(10))
ordered_levels = {
"OverallQual": ten_levels,
"OverallCond": ten_levels,
"ExterQual": five_levels,
"ExterCond": five_levels,
"BsmtQual": five_levels,
"BsmtCond": five_levels,
"HeatingQC": five_levels,
"KitchenQual": five_levels,
"FireplaceQu": five_levels,
"GarageQual": five_levels,
"GarageCond": five_levels,
"PoolQC": five_levels,
"LotShape": ["Reg", "IR1", "IR2", "IR3"],
"LandSlope": ["Sev", "Mod", "Gtl"],
"BsmtExposure": ["No", "Mn", "Av", "Gd"],
"BsmtFinType1": ["Unf", "LwQ", "Rec", "BLQ", "ALQ", "GLQ"],
"BsmtFinType2": ["Unf", "LwQ", "Rec", "BLQ", "ALQ", "GLQ"],
"Functional": ["Sal", "Sev", "Maj1", "Maj2", "Mod", "Min2", "Min1", "Typ"],
"GarageFinish": ["Unf", "RFn", "Fin"],
"PavedDrive": ["N", "P", "Y"],
"Utilities": ["NoSeWa", "NoSewr", "AllPub"],
"CentralAir": ["N", "Y"],
"Electrical": ["Mix", "FuseP", "FuseF", "FuseA", "SBrkr"],
"Fence": ["MnWw", "GdWo", "MnPrv", "GdPrv"],
}
# 为缺少的值添加“无”级别
ordered_levels = {key: ["None"] + value for key, value in
ordered_levels.items()}
def encode(df):
# 名义类别
for name in features_nom:
df[name] = df[name].astype("category")
# 为缺少的值添加“无”类别
if "None" not in df[name].cat.categories:
df[name].cat.add_categories("None", inplace=True)
# 序数范畴
for name, levels in ordered_levels.items():
df[name] = df[name].astype(CategoricalDtype(levels,
ordered=True))
return df
处理缺少的值
现在处理缺失值将使特性工程更顺利。我们将为缺少的数值输入0,为缺少的分类值输入“None”。你可能想尝试其他的插补策略。特别是,您可以尝试创建“缺少值”指标:每当值被插补时为1,否则为0。
def impute(df):
for name in df.select_dtypes("number"):
df[name] = df[name].fillna(0)
for name in df.select_dtypes("category"):
df[name] = df[name].fillna("None")
return df
现在我们可以调用数据加载器并获得处理过的数据拆分:
df_train, df_test = load_data()
如果您想查看单元格包含的内容,请取消注释并运行此单元格。请注意,dfu test缺少SalePrice的值(在插补步骤中,将NAs设定为0。)
建立基线
最后,让我们建立一个基准分数来判断我们的特性工程。
下面是我们在第1课中创建的函数,它将为一个特性集计算交叉验证的RMSLE分数。我们已经将XGBoost用于我们的模型,但是您可能希望尝试其他模型。
def score_dataset(X, y, model=XGBRegressor()):
# 分类的标签编码
# 标签编码对于XGBoost和RandomForest很好,但是有一个热点
# 对套索或里奇这样的模特来说会更好。“类别代码”`
#
for colname in X.select_dtypes(["category"]):
X[colname] = X[colname].cat.codes
# Metric for Housing competition is RMSLE (Root Mean Squared Log Error)
log_y = np.log(y)
score = cross_val_score(
model, X, log_y, cv=5, scoring="neg_mean_squared_error",
)
score = -1 * score.mean()
score = np.sqrt(score)
return score
我们可以在任何时候重用这个评分函数来尝试一个新的特性集。我们现在将在处理过的数据上运行它,而不使用其他功能,并获得一个基线分数:
X = df_train.copy()
y = X.pop("SalePrice")
baseline_score = score_dataset(X, y)
print(f"Baseline score: {baseline_score:.5f} RMSLE")
这个基准分数可以帮助我们知道我们组装的一些特性是否真的带来了任何改进。
在第2课中,我们了解了如何使用互信息来计算功能的效用分数,从而指示该功能有多大的潜力。这个隐藏单元格定义了我们使用的两个实用函数,make\u mi\u scores和plot\u mi\u scores:
def make_mi_scores(X, y):
X = X.copy()
for colname in X.select_dtypes(["object", "category"]):
X[colname], _ = X[colname].factorize()
# All discrete features should now have integer dtypes
discrete_features = [pd.api.types.is_integer_dtype(t) for t in X.dtypes]
mi_scores = mutual_info_regression(X, y, discrete_features=discrete_features, random_state=0)
mi_scores = pd.Series(mi_scores, name="MI Scores", index=X.columns)
mi_scores = mi_scores.sort_values(ascending=False)
return mi_scores
def plot_mi_scores(scores):
scores = scores.sort_values(ascending=True)
width = np.arange(len(scores))
ticks = list(scores.index)
plt.barh(width, scores)
plt.yticks(width, ticks)
plt.title("Mutual Information Scores")
让我们再来看看我们的功能评分:
X = df_train.copy()
y = X.pop("SalePrice")
mi_scores = make_mi_scores(X, y)
mi_scores
您可以看到,我们有许多功能是高度信息性的,也有一些似乎根本没有信息性(至少就其本身而言)。正如我们在教程2中所讨论的,在特性开发过程中,得分最高的特性通常会获得最大的回报,因此将精力集中在这些特性上可能是一个好主意。另一方面,对非信息特征的训练会导致过度拟合。因此,得分为0.0的功能我们将完全放弃:
def drop_uninformative(df, mi_scores):
return df.loc[:, mi_scores > 0.0]
删除它们确实会带来适度的性能提升:
X = df_train.copy()
y = X.pop("SalePrice")
X = drop_uninformative(X, mi_scores)
score_dataset(X, y)
稍后,我们将把drop\u uninformative函数添加到特性创建管道中。
步骤3-创建要素
现在我们将开始开发我们的功能集。
为了使我们的特性工程工作流程更加模块化,我们将定义一个函数,该函数将获取一个准备好的数据帧,并通过一系列转换来获得最终的特性集。它看起来像这样:
def create_features(df):
X = df.copy()
y = X.pop("SalePrice")
X = X.join(create_features_1(X))
X = X.join(create_features_2(X))
X = X.join(create_features_3(X))
# ...
return X
现在让我们来定义一个转换,分类功能的标签编码:
def label_encode(df):
X = df.copy()
for colname in X.select_dtypes(["category"]):
X[colname] = X[colname].cat.codes
return X
当您使用XGBoost这样的树集合时,标签编码对于任何类型的分类特性都是可以的,即使对于无序的类别也是如此。如果您想尝试线性回归模型(在本次竞争中也很流行),那么您应该改为使用单热编码,特别是对于具有无序类别的功能。
这个单元复制了练习3中的工作,在练习3中,您应用了在熊猫中创建特征的策略。修改或添加这些函数以尝试其他特征组合。
def mathematical_transforms(df):
X = pd.DataFrame() # dataframe to hold new features
X["LivLotRatio"] = df.GrLivArea / df.LotArea
X["Spaciousness"] = (df.FirstFlrSF + df.SecondFlrSF) / df.TotRmsAbvGrd
# This feature ended up not helping performance
# X["TotalOutsideSF"] = \
# df.WoodDeckSF + df.OpenPorchSF + df.EnclosedPorch + \
# df.Threeseasonporch + df.ScreenPorch
return X
def interactions(df):
X = pd.get_dummies(df.BldgType, prefix="Bldg")
X = X.mul(df.GrLivArea, axis=0)
return X
def counts(df):
X = pd.DataFrame()
X["PorchTypes"] = df[[
"WoodDeckSF",
"OpenPorchSF",
"EnclosedPorch",
"Threeseasonporch",
"ScreenPorch",
]].gt(0.0).sum(axis=1)
return X
def break_down(df):
X = pd.DataFrame()
X["MSClass"] = df.MSSubClass.str.split("_", n=1, expand=True)[0]
return X
def group_transforms(df):
X = pd.DataFrame()
X["MedNhbdArea"] = df.groupby("Neighborhood")["GrLivArea"].transform("median")
return X
以下是您可以探索的其他转换的一些想法:
质量质量和条件特征之间的相互作用。例如,综合素质是一个高分特征。您可以尝试将它与OverallCond结合起来,将两者转换为整数类型并取一个乘积。
面积特征的平方根。这将把单位平方英尺转换成英尺。
数字特征的对数。如果特征具有倾斜分布,则应用对数可以帮助对其进行规范化。
描述同一事物的数字特征和范畴特征之间的相互作用。例如,您可以查看BsmtQual和TotalBsmtSF之间的交互。
邻里关系的其他群体统计。我们做了区域的中位数。看看平均值,性病,或计数可能是有趣的。您还可以尝试将组统计信息与其他功能结合起来。也许面积和中位数的差别很重要?
k-均值聚类
我们用来创建特征的第一个无监督算法是k-均值聚类。我们看到,您可以使用聚类标签作为特征(一个包含0、1、2、…)的列),也可以使用观测值到每个聚类的距离。我们看到了这些特征有时是如何有效地解开复杂的空间关系的。
cluster_features = [
"LotArea",
"TotalBsmtSF",
"FirstFlrSF",
"SecondFlrSF",
"GrLivArea",
]
def cluster_labels(df, features, n_clusters=20):
X = df.copy()
X_scaled = X.loc[:, features]
X_scaled = (X_scaled - X_scaled.mean(axis=0)) / X_scaled.std(axis=0)
kmeans = KMeans(n_clusters=n_clusters, n_init=50, random_state=0)
X_new = pd.DataFrame()
X_new["Cluster"] = kmeans.fit_predict(X_scaled)
return X_new
def cluster_distance(df, features, n_clusters=20):
X = df.copy()
X_scaled = X.loc[:, features]
X_scaled = (X_scaled - X_scaled.mean(axis=0)) / X_scaled.std(axis=0)
kmeans = KMeans(n_clusters=20, n_init=50, random_state=0)
X_cd = kmeans.fit_transform(X_scaled)
# Label features and join to dataset
X_cd = pd.DataFrame(
X_cd, columns=[f"Centroid_{i}" for i in range(X_cd.shape[1])]
)
return X_cd
PCA是我们用于特征创建的第二个无监督模型。我们看到了如何使用它来分解数据中的变化结构。PCA算法给出了描述变量各分量的载荷,以及作为变换数据点的分量。加载可以建议要创建的功能以及可以直接用作功能的组件。
以下是PCA课程中的实用函数:
def apply_pca(X, standardize=True):
# Standardize
if standardize:
X = (X - X.mean(axis=0)) / X.std(axis=0)
# Create principal components
pca = PCA()
X_pca = pca.fit_transform(X)
# Convert to dataframe
component_names = [f"PC{i+1}" for i in range(X_pca.shape[1])]
X_pca = pd.DataFrame(X_pca, columns=component_names)
# Create loadings
loadings = pd.DataFrame(
pca.components_.T, # transpose the matrix of loadings
columns=component_names, # so the columns are the principal components
index=X.columns, # and the rows are the original features
)
return pca, X_pca, loadings
def plot_variance(pca, width=8, dpi=100):
# Create figure
fig, axs = plt.subplots(1, 2)
n = pca.n_components_
grid = np.arange(1, n + 1)
# Explained variance
evr = pca.explained_variance_ratio_
axs[0].bar(grid, evr)
axs[0].set(
xlabel="Component", title="% Explained Variance", ylim=(0.0, 1.0)
)
# Cumulative Variance
cv = np.cumsum(evr)
axs[1].plot(np.r_[0, grid], np.r_[0, cv], "o-")
axs[1].set(
xlabel="Component", title="% Cumulative Variance", ylim=(0.0, 1.0)
)
# Set up figure
fig.set(figwidth=8, dpi=100)
return axs
下面是从练习5中生成特性的转换。如果您提出了不同的答案,您可能希望更改这些转换。
def pca_inspired(df):
X = pd.DataFrame()
X["Feature1"] = df.GrLivArea + df.TotalBsmtSF
X["Feature2"] = df.YearRemodAdd * df.TotalBsmtSF
return X
def pca_components(df, features):
X = df.loc[:, features]
_, X_pca, _ = apply_pca(X)
return X_pca
pca_features = [
"GarageArea",
"YearRemodAdd",
"TotalBsmtSF",
"GrLivArea",
]
这些只是使用主成分的几种方法。您还可以尝试使用一个或多个组件进行集群。需要注意的一点是,主成分分析不会改变点之间的距离,就像旋转一样。因此,使用全套组件进行聚类与使用原始特征进行聚类是一样的。取而代之的是,挑选一些成分的子集,可能是那些方差最大或MI分数最高的成分。
为了进一步分析,您可能需要查看数据集的相关矩阵:
def corrplot(df, method="pearson", annot=True, **kwargs):
sns.clustermap(
df.corr(method),
vmin=-1.0,
vmax=1.0,
cmap="icefire",
method="complete",
annot=annot,
**kwargs,
)
corrplot(df_train, annot=None)
一组高度相关的特征常常产生有趣的负荷。
在练习5中,您应用了主成分分析(PCA)来确定属于异常值的房屋,即在其余数据中没有很好地表示值的房屋。你看到爱德华兹附近有一组房子的销售条件是部分的,其价值特别极端。
有些模型可以从指示这些异常值中获益,这就是下一个转换将要做的。
def indicate_outliers(df):
X_new = pd.DataFrame()
X_new["Outlier"] = (df.Neighborhood == "Edwards") & (df.SaleCondition == "Partial")
return X_new
你也可以考虑从SIT学习SkLyn.Primor模块中应用某种健壮的定标器,使其成为离群值,特别是在GrLivArea。这里有一个教程说明了其中的一些。另一个选择是使用sciketlearn的离群点检测器创建一个“离群点分数”的特征。
需要一个单独的保持集来创建目标编码是相当浪费数据的。在教程6中,我们使用了25%的数据集来编码一个特性Zipcode。这25%的数据来自其他功能,我们根本无法使用。
但是,有一种方法可以使用目标编码,而不必使用保留的编码数据。这与交叉验证中使用的技巧基本相同:
将数据拆分为多个折叠,每个折叠具有数据集的两个拆分。
在一个分割上训练编码器,但变换另一个分割的值。
对所有裂口重复上述步骤。
这样,训练和转换总是在独立的数据集上进行,就像使用保持集一样,但不会浪费任何数据。
下一个隐藏单元格中有一个包装器,可用于任何目标编码器:
class CrossFoldEncoder:
def __init__(self, encoder, **kwargs):
self.encoder_ = encoder
self.kwargs_ = kwargs # keyword arguments for the encoder
self.cv_ = KFold(n_splits=5)
# Fit an encoder on one split and transform the feature on the
# other. Iterating over the splits in all folds gives a complete
# transformation. We also now have one trained encoder on each
# fold.
def fit_transform(self, X, y, cols):
self.fitted_encoders_ = []
self.cols_ = cols
X_encoded = []
for idx_encode, idx_train in self.cv_.split(X):
fitted_encoder = self.encoder_(cols=cols, **self.kwargs_)
fitted_encoder.fit(
X.iloc[idx_encode, :], y.iloc[idx_encode],
)
X_encoded.append(fitted_encoder.transform(X.iloc[idx_train, :])[cols])
self.fitted_encoders_.append(fitted_encoder)
X_encoded = pd.concat(X_encoded)
X_encoded.columns = [name + "_encoded" for name in X_encoded.columns]
return X_encoded
# To transform the test data, average the encodings learned from
# each fold.
def transform(self, X):
from functools import reduce
X_encoded_list = []
for fitted_encoder in self.fitted_encoders_:
X_encoded = fitted_encoder.transform(X)
X_encoded_list.append(X_encoded[self.cols_])
X_encoded = reduce(
lambda x, y: x.add(y, fill_value=0), X_encoded_list
) / len(X_encoded_list)
X_encoded.columns = [name + "_encoded" for name in X_encoded.columns]
return X_encoded
您可以将任何编码器从类别\u编码器库变成交叉折叠编码器。猫咪点播器值得一试。它类似于MEstimateEncoder,但使用了一些技巧来更好地防止过拟合。它的平滑参数称为a而不是m。
创建最终要素集
现在让我们把一切结合起来。将转换放在单独的函数中可以更容易地进行各种组合的实验。我发现那些没有注释的给出了最好的结果。你应该尝试你自己的想法!修改这些转换中的任何一个,或者提出一些您自己的转换来添加到管道中。
def create_features(df, df_test=None):
X = df.copy()
y = X.pop("SalePrice")
mi_scores = make_mi_scores(X, y)
# Combine splits if test data is given
#
# If we're creating features for test set predictions, we should
# use all the data we have available. After creating our features,
# we'll recreate the splits.
if df_test is not None:
X_test = df_test.copy()
X_test.pop("SalePrice")
X = pd.concat([X, X_test])
# Lesson 2 - Mutual Information
X = drop_uninformative(X, mi_scores)
# Lesson 3 - Transformations
X = X.join(mathematical_transforms(X))
X = X.join(interactions(X))
X = X.join(counts(X))
# X = X.join(break_down(X))
X = X.join(group_transforms(X))
# Lesson 4 - Clustering
# X = X.join(cluster_labels(X, cluster_features, n_clusters=20))
# X = X.join(cluster_distance(X, cluster_features, n_clusters=20))
# Lesson 5 - PCA
X = X.join(pca_inspired(X))
# X = X.join(pca_components(X, pca_features))
# X = X.join(indicate_outliers(X))
X = label_encode(X)
# Reform splits
if df_test is not None:
X_test = X.loc[df_test.index, :]
X.drop(df_test.index, inplace=True)
# Lesson 6 - Target Encoder
encoder = CrossFoldEncoder(MEstimateEncoder, m=1)
X = X.join(encoder.fit_transform(X, y, cols=["MSSubClass"]))
if df_test is not None:
X_test = X_test.join(encoder.transform(X_test))
if df_test is not None:
return X, X_test
else:
return X
df_train, df_test = load_data()
X_train = create_features(df_train)
y_train = df_train.loc[:, "SalePrice"]
score_dataset(X_train, y_train)
在这个阶段,您可能希望在创建最终提交之前使用XGBoost进行一些超参数调优。
X_train = create_features(df_train)
y_train = df_train.loc[:, "SalePrice"]
xgb_params = dict(
max_depth=6, # maximum depth of each tree - try 2 to 10
learning_rate=0.01, # effect of each tree - try 0.0001 to 0.1
n_estimators=1000, # number of trees (that is, boosting rounds) - try 1000 to 8000
min_child_weight=1, # minimum number of houses in a leaf - try 1 to 10
colsample_bytree=0.7, # fraction of features (columns) per tree - try 0.2 to 1.0
subsample=0.7, # fraction of instances (rows) per tree - try 0.2 to 1.0
reg_alpha=0.5, # L1 regularization (like LASSO) - try 0.0 to 10.0
reg_lambda=1.0, # L2 regularization (like Ridge) - try 0.0 to 10.0
num_parallel_tree=1, # set > 1 for boosted random forests
)
xgb = XGBRegressor(**xgb_params)
score_dataset(X_train, y_train, xgb)
只需手动调整这些就可以得到很好的结果。但是,您可能希望尝试使用scikitlearn的自动超参数调谐器之一。或者您可以探索更高级的调优库,如Optuna或scikit optimize。
以下是如何将Optuna与XGBoost结合使用:
import optuna
def objective(trial):
xgb_params = dict(
max_depth=trial.suggest_int("max_depth", 2, 10),
learning_rate=trial.suggest_float("learning_rate", 1e-4, 1e-1, log=True),
n_estimators=trial.suggest_int("n_estimators", 1000, 8000),
min_child_weight=trial.suggest_int("min_child_weight", 1, 10),
colsample_bytree=trial.suggest_float("colsample_bytree", 0.2, 1.0),
subsample=trial.suggest_float("subsample", 0.2, 1.0),
reg_alpha=trial.suggest_float("reg_alpha", 1e-4, 1e2, log=True),
reg_lambda=trial.suggest_float("reg_lambda", 1e-4, 1e2, log=True),
)
xgb = XGBRegressor(**xgb_params)
return score_dataset(X_train, y_train, xgb)
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=20)
xgb_params = study.best_params
如果您想使用它,请将其复制到一个代码单元中,但请注意运行它需要相当长的时间。完成后,您可能会喜欢使用Optuna的一些可视化效果。
一旦你对每件事都满意了,是时候做出最后的预测了!此单元格将:
从原始数据创建要素集
根据训练数据训练XGBoost
使用经过训练的模型从测试集进行预测
将预测保存到CSV文件
X_train,X_test=创建\u特征(df_train,df_test)
y_train=df_train.loc[:,“SalePrice”]
xgb=xgb回归系数(**xgb\u参数)
#XGB使MSE最小化,但竞争损失是RMSLE
#所以,我们需要对数变换y来训练和exp变换预测
xgb.fit(X_列,np.log(y))
预测=np.exp(xgb.predict(X_检验))
output=pd.DataFrame({Id':X_test.index,'SalePrice':predictions})
output.to_csv('my_submission.csv',index=False)
打印(“您的提交已成功保存!”)
要向竞赛提交这些预测,请遵循以下步骤:
首先单击窗口右上角的蓝色保存版本按钮。这将生成一个弹出窗口。
确保选择了Save and Run All选项,然后单击蓝色的Save按钮。
这将在笔记本的左下角生成一个窗口。完成运行后,单击“保存版本”按钮右侧的数字。这会在屏幕右侧显示一个版本列表。单击最新版本右侧的省略号(…),然后选择“在查看器中打开”。这将使您进入同一页面的查看模式。您需要向下滚动以返回这些说明。
单击屏幕右侧的输出选项卡。然后,单击要提交的文件,然后单击蓝色提交按钮将结果提交到排行榜。
你现在已经成功地提交了比赛!
下一步
如果您想继续工作以提高性能,请选择屏幕右上角的蓝色编辑按钮。然后您可以更改代码并重复该过程。还有很多改进的空间,你将在工作中爬上排行榜。
一定要看看其他用户的笔记本在这个竞争。你会发现很多关于新特性的好主意,以及其他发现数据集更多信息或做出更好预测的方法。还有一个讨论论坛,在那里你可以和其他卡格人分享想法。
玩得开心!