回到一个干净的训练集(再次复制strat_train_set),然后将预测器和标签分开(因为不一定对它们使用相同的转换方式)。
# 获取删除标签列后的数据集
housing = strat_train_set.drop("median_house_value", axis=1) #drop()会创建一个数据副本,但不影响strat_train_set
# 标签列
housing_labels = strat_train_set["median_house_value"].copy()
2.5.1 数据清理
由于大部分机器学习算法不能在缺失特征上工作,所以这里创建一些函数辅助它。我们注意到在2.3节,属性describe告诉我们total_bedroom有部分属性值缺失,对此有三种解决方案:
1、放弃这些区域;
2、放弃整个属性;
3、将缺失的值填补为0、平均数或中位数。
#option 1
housing.dropna(subset=["total_bedrooms"])
#option 2
housing.drop("total_bedrooms",axis=1)
#option 3
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median,inplace=True)
如果选择方法3,那么需要计算出训练集的中位数值,然后用其填充缺失值,但是别忘了保存这个中位数值。
Scikit-Learn提供了一个非常容易上手的类来处理缺失值:SimpleImputer。使用方法如下:首先创建一个SimpleImputer实例,指定要用中位数替换缺失值
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
因为中位数只能在数值属性上计算,所有需要创建一个没有文本属性ocean_proximity的副本s数据集,再将imputer实例适配到housing_num:
housing_num = housing.drop("ocean_proximity", axis=1)
imputer.fit(housing_num)
这里,imputer仅仅计算了每个属性的中位数值并将其储存在自己的实例变量statistics_中。此时虽然只有total_bedroom缺失,但不能保证后续不会有其他的属性缺失值,所以稳妥起见,将imputer应用于所有的数值属性上:
imputer.statistics_
housing_num.median().values
# 执行中位数替换缺失值的转化。
X = imputer.transform(housing_num)
# 将numpy数组转换成DataFrame
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing_num.index)
2.5.2 处理文本和分类属性
先看一下ocean_proximity的属性特征:
# 注意:[["ocean_proximity"]]获得DataFrame类型的数据,因为后面需要对二维数据进行转化
housing_cat = housing[["ocean_proximity"]]
housing_cat.value_counts()
再看一下前十个ocean_proximity的值:
housing_cat.head(10)
可以看出它的值不是任意文本,而是每个值代表一个类别,可以使用 Scikit-Learn
中的 OrdinalEncoder
类将这些类别从文本转成数字。
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
# 拟合并转换
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
使用 Categories_
实例变量获取类别列表,列表包括每个类别属性的一维数组(这种情况下这个列表包含一个数组,因为只有一个类别属性)
ordinal_encoder.categories_
这样表征方式会产生一个问题,模型训练时会认为两个相近的值比两个相远的值更相似一些。在某些情况下是对的( ['坏',‘平均’,‘好’,‘优秀 ’]
),但在目前的序列 categories
中,情况可能就不是这样( 1H OCEAN
与 NEAR OCEAN
的相似度比相邻情况下的 1H OCEAN
与 INLAND
的相似度高)。常见的解决方案是给每个类别创建一个二进制属性:当类别为 1H OCEAN
时,它的某个属性为 1
(其他属性为 0
),当类别是 INLAND
时,另一个属性为 1
(其他属性为 0
),以此类推,这就是独热编码可以使用 sklearn
中的 OneHotEncoder
编码器,将整数类别值转换为独热向量。在独热编码完成后,我们会得到一个几千列的矩阵,并且全是0,每行仅有一个1。
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
输出结果是一个 Scipy
稀疏矩阵(仅存储非0元素的位置),而不是一个numpy数组。当然,如果需要也可以使用 toarray()
方法得到一个(密集的) NumPy
数组:
此时可以再次使用编码器的categories_实例变量来得到类别列表:
注意: 如果类别中的种类有很多,那么独热编码会导致过多的输入特征,可能会减慢训练速度并降低性能。如果出现这种情况,可以使用相关的数字特征代替类别。如上面的 ocean_proximity
特征,可以使用海洋的距离作为特征进行替换。
2.5.3 自定义转换器
Scikit-Learn
中有许多有用的转换器,但对于一些自定义清理操作或者组合特定属性等任务需要编写自己的转换器。由于 Scikit-Learn
的编译特性,编写转换器也十分方便,我们只需创建一个类,然后实现 fit()
、 transformm()
、 fit_transform()
。该类可以通过添加 TransformMixin
作为基类,直接得到 fit_transform()
方法,同时可以添加 BaseEstimator
作为基类(并在构造函数中避免 *arg
和 **kargs
),还可以获得自动调整参数的方法( get_params()
和 set_params()
)。
下面实现一个简单的转换器类,用来添加之前的组合属性。
from sklearn.base import BaseEstimator, TransformerMixin
# 选取列名
col_names = ["total_rooms", "total_bedrooms", "population", "households"]
rooms_ix, bedrooms_ix, population_ix, households_ix = [housing.columns.get_loc(c) for c in col_names]
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def __init__(self, add_bedrooms_per_room=True): # no *args or **kargs
self.add_bedrooms_per_room = add_bedrooms_per_room
def fit(self, X, y=None):
return self
def transform(self, X):
rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
population_per_household = X[:, population_ix] / X[:, households_ix]
# 根据超参数add_bedrooms_per_room判断是否需要添加该组合属性
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
return np.c_[X, rooms_per_household, population_per_household,
bedrooms_per_room]
else:
return np.c_[X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)
在本例中,转换器有一个超参数add_bedrooms_per_room默认设置为True,这个超参数可以让你轻松知晓这个属性是否有助于机器学习算法。
# 将housing_extra_attribs从array转为DataFrame
housing_extra_attribs = pd.DataFrame(
housing_extra_attribs,
columns=list(housing.columns)+["rooms_per_household", "population_per_household"],
index=housing.index)
housing_extra_attribs.head()
2.5.5 转换流水线
许多数据转换的步骤需要以正确的顺序来执行,而Sickit-Learn
中的 Pipeline
类正好支持这样的转换,下面是一个数值属性的流水线示例:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
# 中位数替换缺失值
('imputer', SimpleImputer(strategy="median")),
# 添加组合属性
('attribs_adder', CombinedAttributesAdder()),
# 归一化,统一量纲
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
housing_num_tr
Pipeline
构造函数会通过一系列的名称/估算器的配对来定义步骤序列。除了最后一个是估算器之外,前面都必须是转换器(必须有 fit_transform()
方法)。至于命名随意,只要不含双下划线。
当调用流水线的 fit()
方法时,会在所有转换器上按照顺序依次调用 fit_transform()
,每次将一个调用的输出作为输出传递给下一个调用方法,直到传递到最终的估算器,则只会调用 fit()
方法。
流水线的方法与最终估算器的方法相同。本例中最后一个估算器是Standard-Scaler,这是一个转换器,因此流水线有一个transform()办法,可以i按顺序将所有的转换应用到数据中。
目前为止,我们已经分别处理了类别列和数值列。但是,一个能够处理所有列的转换器是更好的选择:将适当的转换应用到每个列(可以使用 ColumnTransformer,
最好搭配pandas DataFrames使用)
。下面用它来将所有的转换应用到房屋数据:
#首先导入ColumnTransformer类
from sklearn.compose import ColumnTransformer
# 获得数值列名称列表
num_attribs = list(housing_num)
# 获得类别列名称列表
cat_attribs = ["ocean_proximity"]
#构造一个ColumnTransformer
# 元组中的三个参数分别代表:名字(自定),转换器,以及一个该转换器能够应用的列名字(或索引)的列表
full_pipeline = ColumnTransformer([
# 数值属性列转换器
("num", num_pipeline, num_attribs),
# 文本属性列转换器
("cat", OneHotEncoder(), cat_attribs),
])
# 将ColumnTranformer应用到房屋数据:将每个转换器应用于适当的列,并沿第二个轴合并输出(转换器必须返回相同数量的行)
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
这个输出结果是一个密集矩阵。还记得转换器 OneHotEncoder
返回的是什么吗?对,是一个稀疏矩阵。当一个稀疏矩阵和密集矩阵混合在一起时, ColumnTransformer
会估算最终矩阵的密度(即单元格的非零比率),如果密度低于给定的阈值(通过默认值 sparse_threshold=0.3
),则返回一个稀疏矩阵。我们有一个预处理流水线,可以获取全部的房屋数据并对每一列进行适当的转换。
在不使用转换器的情况下,要想删除一列,可以使用指定字符串“drop”。如果希望列能保持不变,也可以指定“pass through”。默认情况下,其余未列出的列会被删除。如果希望用不同方式处理这些列,则可以将reminder这个超参数设置成任意转换器(或“pass through”)。