Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 之 [ 数据探索 ] [ 可视化 ] [数据清洗] | 2/3(含分析过程)
目录
Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 之 [ 数据探索 ] [ 可视化 ] [数据清洗] | 2/3(含分析过程)
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、机器学习
机器学习(Machine Learning)是人工智能(AI)的一个分支领域,其核心思想是通过计算机系统的学习和自动化推理,使计算机能够从数据中获取知识和经验,并利用这些知识和经验进行模式识别、预测和决策。机器学习算法能够自动地从数据中学习并改进自己的性能,而无需明确地编程。这一过程涉及对大量输入数据的分析和解释,以识别数据中的模式和趋势,并生成可以应用于新数据的预测模型。
1、为什么使用机器学习?
使用机器学习的原因主要包括以下几点:
- 高效性和准确性:机器学习算法能够处理大规模数据集,并从中提取有价值的信息,其预测和决策的准确性往往高于传统方法。
- 自动化和智能化:机器学习能够自动学习和改进,减少了对人工干预的依赖,提高了工作效率和智能化水平。
- 广泛应用性:机器学习在各个领域中都有广泛的应用,如图像识别、语音识别、自然语言处理、推荐系统、金融预测等,为许多实际问题的解决提供了有效的方法和工具。
- 未来趋势:随着人工智能技术的不断发展,机器学习已成为未来的趋势,掌握机器学习技能将有助于提高职业竞争力和创造力。
2、机器学习系统的类型,及其对应的学习算法
机器学习系统可以根据不同的学习方式和目标进行分类,主要包括以下几种类型及其对应的学习算法:
监督学习(Supervised Learning)
- 定义:使用带有标签的训练数据来训练模型,以预测新数据的标签或目标值。
- 常见算法:
- 线性回归(Linear Regression):用于预测连续值。
- 逻辑回归(Logistic Regression):用于分类问题,尤其是二分类问题。
- 支持向量机(SVM, Support Vector Machines):用于分类和回归问题,通过寻找最优的超平面来分割数据。
- 决策树(Decision Trees)和随机森林(Random Forests):通过构建决策树或决策树集合来进行分类或回归。
- 神经网络(Neural Networks):模仿人脑神经元的工作方式,通过多层节点之间的连接和权重调整来进行学习和预测。
无监督学习(Unsupervised Learning)
- 定义:在没有标签的情况下,从数据中发现隐藏的结构和模式。
- 常见算法:
- 聚类算法(Clustering Algorithms):如K均值(K-Means)、层次聚类分析(HCA)等,用于将数据分组为具有相似特征的簇。
- 降维算法(Dimensionality Reduction Algorithms):如主成分分析(PCA)、t-分布邻域嵌入算法(t-SNE)等,用于减少数据的维度以便于分析和可视化。
- 异常检测(Anomaly Detection):用于识别数据中的异常点或离群点。
强化学习(Reinforcement Learning)
- 定义:通过与环境的交互学习,以最大化累积奖励为目标。
- 特点:强化学习不需要明确的标签或监督信号,而是根据环境给出的奖励或惩罚来指导学习过程。
- 应用场景:游戏AI、机器人控制、自动驾驶等领域。
半监督学习(Semi-Supervised Learning)
- 定义:处理部分带标签的训练数据,通常是大量不带标签数据加上小部分带标签数据。
- 特点:结合了监督学习和无监督学习的特点,旨在利用未标记数据来提高模型的泛化能力。
- 常见算法:多数半监督学习算法是非监督和监督算法的结合,如自训练(Self-Training)、协同训练(Co-Training)等。
3、机器学习可利用的开源数据
开源数据集可以根据需要进行选择,涵盖多个领域。以下是一些可以查找的数据的地方,供参考:
(注意:代码执行的时候,可能需要科学上网)
三、从房价数据集中,进行数据探索、可视化发现规律
目前为止,你只是快速查看了数据,对要处理的数据有了整体了解。现在的目标是更深的探索数据。
首先,保证你将测试集放在了一旁,只是研究训练集。另外,如果训练集非常大,你可能需要再采样一个探索集,保证操作方便快速。在我们的案例中,数据集很小,所以可以在全集上直接工作。创建一个副本,以免损伤训练集:
# 使用copy方法从strat_train_set创建一个新的DataFrame对象housing。
# copy方法用于深拷贝DataFrame,确保新的对象housing与原始的strat_train_set完全独立,
# 后续对housing的任何修改都不会影响到strat_train_set。
# 这在数据预处理和特征工程中非常有用,因为它允许我们在不改变原始训练集的情况下进行各种尝试。
housing = strat_train_set.copy()
1、地理数据可视化
因为存在地理信息(纬度和经度),创建一个所有街区的散点图来数据可视化是一个不错的主意(图 3-1):
# 使用pandas的plot方法绘制一个散点图,展示housing数据集中的经度(longitude)和纬度(latitude)的关系。
# 'kind="scatter"'参数指定图表类型为散点图。
# 'x="longitude"'和'y="latitude"'参数指定散点图的横轴和纵轴分别对应经度和纬度。
# 这种图表有助于直观地观察数据点在地理空间上的分布情况。
housing.plot(kind="scatter", x="longitude", y="latitude")
# 这里调用save_fig函数,将图表保存为名为"bad_visualization_plot"的图片。
# 这个名称可能是为了指出该图表可能在可视化方面存在一些问题,比如过于密集的点使得观察困难。
plt.savefig("images/bad_visualization_plot.png",bbox_inches='tight')
运行结果:
这张图看起来很像加州,但是看不出什么特别的规律。将alpha
设为 0.1,可以更容易看出数据点的密度(图 3-2):
# 使用pandas的plot方法绘制一个散点图,展示housing数据集中的经度(longitude)和纬度(latitude)的关系。
# 'kind="scatter"'参数指定图表类型为散点图。
# 'x="longitude"'和'y="latitude"'参数分别指定散点图的横轴和纵轴对应经度和纬度。
# 'alpha=0.1'参数设置点的透明度,这有助于在点非常密集时仍然能够看到它们的分布情况,
# 而不是一个不透明的大色块,从而改善了可视化效果。
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
# 这里调用save_fig函数,将图表保存为名为"better_visualization_plot"的图片。
# 这个名称表明相比于之前的可视化,当前的图表在可视化方面有所改进,更易于观察和分析。
plt.savefig("images/better_visualization_plot.png",bbox_inches='tight')
运行结果:
现在看起来好多了:可以非常清楚地看到高密度区域,湾区、洛杉矶和圣迭戈,以及中央谷,特别是从萨克拉门托和弗雷斯诺。
通常来讲,人类的大脑非常善于发现图片中的规律,但是需要调整可视化参数使规律显现出来。
现在来看房价(图 3-3)。每个圈的半径表示街区的人口(选项s
),颜色代表价格(选项c
)。我们用预先定义的名为jet
的颜色图(选项cmap
),它的范围是从蓝色(低价)到红色(高价):
# 使用matplotlib的scatter函数(通过pandas的plot方法调用)绘制一个散点图,
# 展示housing数据集中的经度(longitude)和纬度(latitude)的关系,并根据房屋中位数价值(median_house_value)进行颜色编码。
# 'kind="scatter"'指定图表类型为散点图。
# 'x="longitude"'和'y="latitude"'指定散点图的横轴和纵轴分别对应经度和纬度。
# 'alpha=0.4'设置点的透明度,使得点的分布更加清晰。
# 's=housing["population"]/100'设置散点的大小,这里将人口除以100以便于可视化。
# 'label="population"'为散点图的图例提供标签。
# 'figsize=(10,7)'设置图表的大小为10英寸宽,7英寸高。
# 'c="median_house_value"'指定使用中位数房屋价值来为点着色。
# 'cmap=plt.get_cmap("jet")'设置颜色映射,这里使用“jet”颜色图。
# 'colorbar=True'显示颜色条,方便读者根据颜色判断中位数房屋价值的大小。
# 'sharex=False'确保x轴在不同的图表中独立显示,不共享。
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
s=housing["population"]/100, label="population", figsize=(10,7),
c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
sharex=False)
# 使用matplotlib的legend函数添加图例,图例将显示每个点的标签。
plt.legend()
# 这里调用save_fig函数,将图表保存为名为"housing_prices_scatterplot"的图片。
# 这个名称表明图表展示了房屋价值的地理分布。
plt.savefig("images/housing_prices_scatterplot.png",bbox_inches='tight')
运行结果:
为了更加形象的展示效果,我们接下来加载地图进入图中:
下载加州地图
# 导入os模块,它提供了与操作系统交互的功能,如文件路径操作。
import os
# 导入urllib.request模块,它用于请求网络上的资源。
import urllib.request
# 假设PROJECT_ROOT_DIR是一个已经定义好的变量,包含了项目的根目录路径。
PROJECT_ROOT_DIR = "E:/Projects/PycharmProjects/MachineL_SKLearn_TensorF_Project/01HousePriceDataHandler"
# 使用os.path.join方法拼接出存放图像的完整路径。
# "images/end_to_end_project"是相对PROJECT_ROOT_DIR的子目录路径。
images_path = os.path.join(PROJECT_ROOT_DIR, "images", "end_to_end_project")
# 使用os.makedirs方法创建上述路径表示的目录。exist_ok=True参数表示如果目录已经存在,则不抛出异常。
os.makedirs(images_path, exist_ok=True)
# 定义一个变量DOWNLOAD_ROOT,存储了用于下载资源的基础URL。
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
# 定义要下载的文件名。
filename = "california.png"
# 打印一条消息,告知用户正在下载的文件名。
print("Downloading", filename)
# 拼接完整的下载URL。
url = DOWNLOAD_ROOT + "images/end_to_end_project/" + filename
# 使用urllib.request.urlretrieve函数从指定的URL下载文件,并保存到images_path目录下。
# 第一个参数是URL,第二个参数是文件保存的路径。
urllib.request.urlretrieve(url, os.path.join(images_path, filename))
运行结果:
Downloading california.png
添加下载的加州地图一并显示,效果见图 3-4
# 导入matplotlib的image模块,它提供了图像读取和显示的功能。
import matplotlib.image as mpimg
# 导入numpy库,用于数学运算,如生成ticks。
import numpy as np
# 导入matplotlib的pyplot模块,用于绘图和显示图表。
import matplotlib.pyplot as plt
# 读取之前下载并保存的加利福尼亚州地图图片。
california_img = mpimg.imread(PROJECT_ROOT_DIR + '/images/end_to_end_project/california.png')
# 创建一个散点图,展示housing数据集中的经度和纬度,并用颜色表示房屋的中位数价值。
ax = housing.plot(kind="scatter", x="longitude", y="latitude", figsize=(10,7),
s=housing['population']/100, label="Population", # 根据人口大小调整散点大小
c="median_house_value", cmap=plt.get_cmap("jet"), # 使用颜色映射
colorbar=False, alpha=0.4, # 设置透明度
)
# 在散点图的基础上,叠加显示加利福尼亚州的地图图片。
# 'extent'参数定义了图片在图表上的边界。
plt.imshow(california_img, extent=[-124.55, -113.80, 32.45, 42.05], alpha=0.5,
cmap=plt.get_cmap("jet"))
# 设置y轴和x轴的标签。
plt.ylabel("Latitude", fontsize=14)
plt.xlabel("Longitude", fontsize=14)
# 设置颜色条的刻度值和标签。
prices = housing["median_house_value"]
tick_values = np.linspace(prices.min(), prices.max(), 11) # 生成价格范围的刻度值
cbar = plt.colorbar() # 创建颜色条
cbar.ax.set_yticklabels(["$%dk" % (round(v/1000)) for v in tick_values], fontsize=14) # 设置刻度标签格式
cbar.set_label('Median House Value', fontsize=16) # 设置颜色条的标签
# 添加图例,设置图例的字体大小。
plt.legend(fontsize=16)
# 假设save_fig是一个自定义函数,用于将当前图表保存为图片文件。
# 这里调用save_fig函数,将图表保存为名为"california_housing_prices_plot"的图片。
plt.savefig("images/california_housing_prices_plot.png",bbox_inches='tight')
# 显示图表。
plt.show()
运行结果:
这张图说明房价和位置(比如,靠海)和人口密度联系密切,这点你可能早就知道。可以使用聚类算法来检测主要的聚集,用一个新的特征值测量聚集中心的距离。尽管北加州海岸区域的房价不是非常高,但离大海距离属性也可能很有用,所以这不是用一个简单的规则就可以定义的问题。
2、查找关联
因为数据集并不是非常大,你可以很容易地使用corr()
方法计算出每对属性间的标准相关系数(standard correlation coefficient,也称作皮尔逊相关系数):
# 使用pandas的corr方法计算housing DataFrame中数值列的成对相关系数。
# corr方法会返回一个新的DataFrame,称为相关系数矩阵(corr_matrix),
# 其中包含了各列之间的相关性度量。
# 相关系数矩阵的对角线上的值始终为1.0,表示每列与其自身的完全相关。
# 非对角线上的值范围在-1.0到1.0之间,表示不同列之间的线性关系强度和方向:
# - 接近1.0表示强正相关,
# - 接近-1.0表示强负相关,
# - 接近0表示没有或很弱的线性关系。
# 使用pandas的corr方法计算housing DataFrame中数值列的成对相关系数。
# 设置numeric_only=True以确保只计算数值列之间的相关系数,忽略非数值列。
corr_matrix = housing.corr(numeric_only=True)
现在来看下每个属性和房价中位数的关联度:
# 使用pandas的corr方法计算得到的corr_matrix是相关系数矩阵,其中包含了housing DataFrame中数值列之间的相关性度量。
# 通过选择corr_matrix中的"median_house_value"列,我们可以得到一个Series,
# 这个Series表示"median_house_value"与其他数值列之间的相关系数。
# 调用sort_values方法对这个Series进行排序,ascending=False参数表示降序排序,
# 这样相关系数的绝对值越大(无论是正相关还是负相关),对应的列名会出现在Series的前面。
# 这有助于快速识别与"median_house_value"最相关的其他特征列。
# 例如,如果某个特征与房屋中位数价值高度相关,它可能是影响房价的重要因素。
# 通过这种排序,我们可以更好地理解数据特征之间的关系,并为特征选择和模型构建提供依据。
corr_matrix["median_house_value"].sort_values(ascending=False)
运行结果:
median_house_value 1.000000 median_income 0.687151 total_rooms 0.135140 housing_median_age 0.114146 households 0.064590 total_bedrooms 0.047781 population -0.026882 longitude -0.047466 latitude -0.142673 Name: median_house_value, dtype: float64
相关系数的范围是 -1 到 1。当接近 1 时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。当相关系数接近 -1 时,意味强负相关;你可以看到,纬度和房价中位数有轻微的负相关性(即,越往北,房价越可能降低)。最后,相关系数接近 0,意味没有线性相关性。图 3-5 展示了相关系数在横轴和纵轴之间的不同图形。
注意:相关系数只测量线性关系(如果
x
上升,y
则上升或下降)。相关系数可能会完全忽略非线性关系(例如,如果x
接近 0,则y
值会变高)。在上面图片的最后一行中,他们的相关系数都接近于 0,尽管它们的轴并不独立:这些就是非线性关系的例子。另外,第二行的相关系数等于 1 或 -1;这和斜率没有任何关系。例如,你的身高(单位是英寸)与身高(单位是英尺或纳米)的相关系数就是 1。
另一种检测属性间相关系数的方法是使用 Pandas 的scatter_matrix
函数,它能画出每个数值属性对每个其它数值属性的图。因为现在共有 11 个数值属性,你可以得到11 ** 2 = 121
张图,在一页上画不下,所以只关注几个和房价中位数最有可能相关的属性(图 3-6):
# 从pandas的plotting模块导入scatter_matrix函数,该函数用于绘制多个变量的散点矩阵图。
from pandas.plotting import scatter_matrix
# 定义一个列表attributes,包含我们感兴趣的几个特征列名。
# 这些特征包括房屋的中位数价值(median_house_value)、家庭收入中位数(median_income)、
# 房间总数(total_rooms)和房屋年龄中位数(housing_median_age)。
attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
# 使用scatter_matrix函数绘制这些属性的散点矩阵图。
# 这个图可以帮助我们理解不同特征之间的关系和它们之间的相关性。
# 'figsize=(12, 8)'参数用于设置图表的大小,使其更容易阅读和分析。
scatter_matrix(housing[attributes], figsize=(12, 8))
# 这里调用save_fig函数,将图表保存为名为"scatter_matrix_plot"的图片。
# 保存图表是一种良好的实践,特别是在进行数据探索和呈现结果时。
plt.savefig("images/scatter_matrix_plot.png",bbox_inches='tight')
运行结果:
如果 pandas 将每个变量对自己作图,主对角线(左上到右下)都会是直线图。所以 Pandas 展示的是每个属性的柱状图(也可以是其它的,请参考 Pandas 文档)。
最有希望用来预测房价中位数的属性是收入中位数,因此将这张图放大(图 3-7):
# 使用pandas的plot方法绘制一个散点图,展示housing数据集中的"median_income"(家庭收入中位数)
# 与"median_house_value"(房屋价值中位数)之间的关系。
# 'kind="scatter"'指定图表类型为散点图,这种图表有助于观察两个数值变量之间的趋势和关系。
# 'x="median_income"'和'y="median_house_value"'分别指定散点图的横轴和纵轴。
# 'alpha=0.1'设置点的透明度,使得在点非常密集的地方,图表仍然清晰可读。
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)
# 使用matplotlib的axis方法设置图表的显示范围。
# 这里将x轴的范围限制在[0, 16],y轴的范围限制在[0, 550000]。
# 这有助于聚焦在数据的主要分布区域,避免图表过于稀疏的部分分散注意力。
plt.axis([0, 16, 0, 550000])
# 这里调用save_fig函数,将图表保存为名为"income_vs_house_value_scatterplot"的图片。
# 这个名称表明图表展示了收入与房屋价值的对比关系。
plt.savefig("images/income_vs_house_value_scatterplot.png",bbox_inches='tight')
运行结果:
这张图说明了几点。首先,相关性非常高;可以清晰地看到向上的趋势,并且数据点不是非常分散。第二,我们之前看到的最高价,清晰地呈现为一条位于 500000 美元的水平线。这张图也呈现了一些不是那么明显的直线:一条位于 450000 美元的直线,一条位于 350000 美元的直线,一条在 280000 美元的线,和一些更靠下的线。你可能希望去除对应的街区,以防止算法重复这些巧合。
3、属性组合试验
希望前面的一节能教给你一些探索数据、发现规律的方法。你发现了一些数据的巧合,需要在给算法提供数据之前,将其去除。你还发现了一些属性间有趣的关联,特别是目标属性。你还注意到一些属性具有长尾分布,因此你可能要将其进行转换(例如,计算其log
对数)。当然,不同项目的处理方法各不相同,但大体思路是相似的。
给算法准备数据之前,你需要做的最后一件事是尝试多种属性组合。例如,如果你不知道某个街区有多少户,该街区的总房间数就没什么用。你真正需要的是每户有几个房间。相似的,总卧室数也不重要:你可能需要将其与房间数进行比较。每户的人口数也是一个有趣的属性组合。让我们来创建这些新的属性:
# 对housing DataFrame进行数据预处理,创建三个新的特征,以便于后续的数据分析和模型训练。
# 计算每个家庭的平均房间数,通过将总房间数(total_rooms)除以家庭数(households)。
# 这个新特征被称为'rooms_per_household',它可以帮助我们了解每个家庭大约拥有多少房间。
housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
# 计算每个房间的平均卧室数,通过将总卧室数(total_bedrooms)除以总房间数(total_rooms)。
# 这个新特征被称为'bedrooms_per_room',它可以提供有关房屋布局的信息,例如,平均每个房间有多少卧室。
housing["bedrooms_per_room"] = housing["total_bedrooms"] / housing["total_rooms"]
# 计算每个家庭的平均人口数,通过将总人口数(population)除以家庭数(households)。
# 这个新特征被称为'population_per_household',它是了解家庭规模的一个关键指标。
housing["population_per_household"] = housing["population"] / housing["households"]
现在,再来看相关矩阵:
# 计算housing DataFrame中所有数值型列之间的相关系数矩阵。
# 使用pandas的corr方法得到corr_matrix,其中包含了列与列之间的成对相关系数。
# 相关系数是度量两个变量之间线性关系强度和方向的统计量,取值范围在-1到1之间。
# 设置numeric_only=True以确保只计算数值列之间的相关系数,忽略非数值列。
corr_matrix = housing.corr(numeric_only=True)
# 从corr_matrix中选择"median_house_value"这一列,得到一个Series,
# 这个Series包含了"median_house_value"与其它数值型列的相关系数。
# 然后,使用sort_values方法对这个Series进行降序排序(ascending=False),
# 以便快速识别与"median_house_value"(房屋中位数价值)相关性最强的特征。
# 这有助于我们理解哪些特征与房价有较大的关联,可能对房价预测模型的构建有指导意义。
# 例如,如果某个特征与房价高度相关,它可能是房价的一个强有力的预测因子。
top_features = corr_matrix["median_house_value"].sort_values(ascending=False)
top_features
运行结果:
median_house_value 1.000000 median_income 0.687151 rooms_per_household 0.146255 total_rooms 0.135140 housing_median_age 0.114146 households 0.064590 total_bedrooms 0.047781 population_per_household -0.021991 population -0.026882 longitude -0.047466 latitude -0.142673 bedrooms_per_room -0.259952 Name: median_house_value, dtype: float64
看起来不错!与总房间数或卧室数相比,新的bedrooms_per_room
属性与房价中位数的关联更强。显然,卧室数/总房间数的比例越低,房价就越高。每户的房间数也比街区的总房间数的更有信息,很明显,房屋越大,房价就越高。
我们用来预测房价中位数的属性是房间数,因此将这张图放大(图 3-8):
# 使用pandas的plot方法绘制一个散点图,展示housing数据集中的"rooms_per_household"(每个家庭的房间数)
# 与"median_house_value"(房屋中位数价值)之间的关系。
# 这种可视化有助于我们理解房间数量与房屋价值之间是否存在某种趋势或相关性。
# 'kind="scatter"'指定图表类型为散点图,这适用于展示两个数值变量之间的关系。
# 'x="rooms_per_household"'和'y="median_house_value"'分别设置散点图的横轴和纵轴。
# 'alpha=0.2'设置点的透明度,较低的透明度可以在点重叠时提供更好的视觉效果。
# 绘制散点图。
housing.plot(kind="scatter", x="rooms_per_household", y="median_house_value", alpha=0.2)
# 使用matplotlib的axis方法设置图表的显示范围,限制x轴在[0, 5]区间,y轴在[0, 520000]区间。
# 这有助于聚焦于数据的主要分布区域,避免图表过于稀疏的部分分散注意力。
# 显示图表。
plt.axis([0, 5, 0, 520000])
# 这里调用save_fig函数,将图表保存为名为"rooms_per_household_median_house_value_scatterplot"的图片。
# 这个名称表明图表展示了收入与房屋价值的对比关系。
plt.savefig("images/rooms_per_household_median_house_value_scatterplot.png",bbox_inches='tight')
plt.show()
运行结果:
describe()
方法展示了数值属性的概括,见图 3-9
# 使用pandas的describe方法快速获取housing DataFrame中数值型列的统计摘要。
# describe方法会返回一个新的DataFrame,其中包含了每个数值型列的以下统计信息:
# - count: 非空值的数量
# - mean: 平均值,即所有数值的总和除以数值的个数
# - std: 标准差,表示数据分布的离散程度
# - min: 最小值
# - 25%: 第一四分位数,即所有数值中位于25%位置的值
# - 50%: 中位数,即所有数值中位于中间位置的值
# - 75%: 第三四分位数,即所有数值中位于75%位置的值
# - max: 最大值
#
# 这些统计信息为数据的分布特征、中心趋势和离散程度提供了一个概览,对于初步的数据探索和理解数据集非常有用。
housing.describe()
运行结果:
这一步的数据探索不必非常完备,此处的目的是有一个正确的开始,快速发现规律,以得到一个合理的原型。但是这是一个交互过程:一旦你得到了一个原型,并运行起来,你就可以分析它的输出,进而发现更多的规律,然后再回到数据探索这步。
四、为机器学习算法准备数据
现在来为机器学习算法准备数据。不要手工来做,你需要写一些函数,理由如下:
-
函数可以让你在任何数据集上(比如,你下一次获取的是一个新的数据集)方便地进行重复数据转换。
-
你能慢慢建立一个转换函数库,可以在未来的项目中复用。
-
在将数据传给算法之前,你可以在实时系统中使用这些函数。
-
这可以让你方便地尝试多种数据转换,查看哪些转换方法结合起来效果最好。
但是,还是先回到干净的训练集(通过再次复制strat_train_set
),将预测量和标签分开,因为我们不想对预测量和目标值应用相同的转换(注意drop()
创建了一份数据的备份,而不影响strat_train_set
):
# 从strat_train_set DataFrame中删除"median_house_value"列,用于准备训练数据集。
# 在机器学习中,通常需要将特征数据(住房信息)和标签数据(房屋中位数价值)分开,
# 以便于训练模型进行预测。axis=1参数指定我们要操作的是列而不是行。
# 通过删除这一列,我们得到了一个只包含特征的DataFrame,即housing,用于训练模型。
housing = strat_train_set.drop("median_house_value", axis=1)
# 创建housing_labels变量,它是strat_train_set中"median_house_value"列的深拷贝。
# 这样我们就得到了一个仅包含标签数据的Series,用于训练模型时作为真实值进行比较。
# 使用.copy()确保原始数据集strat_train_set不会被修改,保持数据的完整性。
housing_labels = strat_train_set["median_house_value"].copy()
1、数据清洗
大多机器学习算法不能处理缺失的特征,因此先创建一些函数来处理特征缺失的问题。前面,你应该注意到了属性total_bedrooms
有一些缺失值。有三个解决选项:
-
去掉对应的街区;
-
去掉整个属性;
-
进行赋值(0、平均值、中位数等等)。
# 使用pandas的isnull方法结合any方法来识别housing DataFrame中包含空值的行。
# isnull()返回一个与housing形状相同的布尔DataFrame,其中的元素会标记为空值的地方为True。
# any(axis=1)沿着每行进行操作,如果行中存在任何True(即空值),则返回True。
# 这样我们就得到了一个布尔Series,True表示对应的行包含至少一个空值。
# 然后,使用这个布尔Series作为条件索引,从housing DataFrame中选择出包含空值的行。
# 最后,使用head()方法从这些行中取出前几行作为样本,以便于进一步的检查或处理。
# 这个样本可以帮助我们了解数据中缺失值的分布情况,以及决定如何处理这些缺失值。
sample_incomplete_rows = housing[housing.isnull().any(axis=1)].head()
sample_incomplete_rows
运行结果:
用DataFrame
的dropna()
,drop()
,和fillna()
方法,可以方便地实现:
1)dropna()
# 在处理包含空值的样本数据集sample_incomplete_rows时,我们可以选择删除那些具有缺失值的行。
# 这里使用dropna方法,它是pandas库中用于删除缺失值的函数之一。
# 'subset=["total_bedrooms"]'参数指定只考虑'total_bedrooms'列中的缺失值,
# 只有当'total_bedrooms'列中存在缺失值时,对应的行才会被删除。
# 这是处理缺失数据的一种策略,特别是在缺失值不多,或者某个特定列的缺失值对分析影响较大时。
# 选项1:删除'total_bedrooms'列中含有缺失值的行。
# 这种方法适用于我们认为'total_bedrooms'是一个重要的特征,不希望用任何估算值来替代缺失值。
# 应用dropna方法后,我们得到一个新的DataFrame,其中不包含任何在'total_bedrooms'列有缺失值的行。
cleaned_sample = sample_incomplete_rows.dropna(subset=["total_bedrooms"]) #选项 1
cleaned_sample
运行结果:
2)drop()
# 处理包含空值的样本数据集sample_incomplete_rows时,除了删除整行之外,还可以选择删除含有缺失值的列。
# 这里使用drop方法来删除'total_bedrooms'列,axis=1参数指定我们要删除的是列而不是行。
# 选项2:删除包含缺失值的'total_bedrooms'列,而不是删除整行。
# 这种方法适用于我们认为缺失的列对于模型训练不是非常关键,或者我们不想处理缺失值,只是简单地移除该特征。
# 删除'total_bedrooms'列后,我们得到了一个新的DataFrame,其中不再包含任何关于卧室总数的信息。
# 这可能会影响模型的预测能力,因为失去了一部分数据,但在某些情况下,为了简化问题或避免复杂的缺失数据处理,
# 这种方法是一个可行的选择。
# 应用drop方法后,我们得到一个新的DataFrame,不再包含'total_bedrooms'列。
cleaned_sample = sample_incomplete_rows.drop("total_bedrooms", axis=1)
cleaned_sample
运行结果:
fillna()
# 计算housing DataFrame中"total_bedrooms"列的中位数。
# 中位数是一个统计量,表示一半的值在这个数值之上,另一半的值在这个数值之下。
# 这是一个对异常值不敏感的度量,常用于代替平均数来估计中心趋势,特别是在数据分布偏斜时。
median = housing["total_bedrooms"].median()
# 使用fillna方法填充sample_incomplete_rows DataFrame中"total_bedrooms"列的缺失值。
# 这里选择用中位数来填充缺失值,这是一种常见的处理缺失值的策略,特别是对于数值型数据。
# median作为fillna的参数,表示缺失值将被中位数所替代。
# inplace=True参数表示直接在原始DataFrame上修改,而不是创建一个新的DataFrame。
# 选项3:填充'total_bedrooms'列中的缺失值,使用中位数作为填充值。
# 这种方法适用于我们认为缺失值用中位数替代是合理的,并且希望保留所有的行数据。
# 应用fillna方法后,sample_incomplete_rows中的"total_bedrooms"列不再包含缺失值,
# 所有的缺失值都被中位数所替代。这样可以保留数据集中更多的信息,并且避免了删除行或列带来的潜在信息丢失。
sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True)
sample_incomplete_rows
运行结果:
如果选择选项 3,你需要计算训练集的中位数,用中位数填充训练集的缺失值,不要忘记保存该中位数。后面用测试集评估系统时,需要替换测试集中的缺失值,也可以用来实时替换新数据中的缺失值。
Scikit-Learn 提供了一个方便的类来处理缺失值:Imputer
。下面是其使用方法:首先,需要创建一个Imputer
实例,指定用某属性的中位数来替换该属性所有的缺失值:
# 尝试从Scikit-Learn库导入SimpleImputer类。
# SimpleImputer是一个用于填充缺失值的工具,提供了多种策略来处理缺失数据。
try:
from sklearn.impute import SimpleImputer # 适用于Scikit-Learn 0.20及以上版本
except ImportError:
# 如果上述导入失败(例如Scikit-Learn版本低于0.20),则从preprocessing模块导入Imputer类,并将其别名为SimpleImputer。
# 这确保了代码的兼容性,可以在不同版本的Scikit-Learn上运行。
from sklearn.preprocessing import Imputer as SimpleImputer
# 创建SimpleImputer实例,指定strategy参数为"median"。
# 这意味着SimpleImputer将使用列的中位数来填充缺失值。
# 这是一种常见的缺失数据处理策略,特别是当数据集的缺失值不是很多,且数据近似正态分布时。
imputer = SimpleImputer(strategy="median")
因为只有数值属性才能算出中位数,我们需要创建一份不包括文本属性ocean_proximity
的数据副本:
# 在housing DataFrame中,我们可能需要对数值型数据和非数值型数据分别进行处理。
# 'ocean_proximity'列可能包含非数值型数据,例如描述性文字,这使得它不适合直接用于某些机器学习模型。
# 使用drop方法从housing DataFrame中删除'ocean_proximity'列,axis=1参数表示操作对象是列。
# 这样,我们得到了一个只包含数值型数据的新DataFrame,命名为housing_num,它将用于后续的数值分析和模型训练。
# 通过排除非数值列,我们可以更专注于处理和分析那些对预测目标变量(如房屋中位数价值)可能更有用的数值特征。
housing_num = housing.drop('ocean_proximity', axis=1)
现在,就可以用fit()
方法将imputer
实例拟合到训练数据:
# 使用SimpleImputer类的实例imputer来拟合housing_num DataFrame。
# fit方法用于学习数据中的统计属性,SimpleImputer将根据指定的策略(本例中为"median"),
# 计算每个列的中位数,以便后续用于填充缺失值。
# 拟合过程中,imputer将分析housing_num中的数值型列,并为每个列确定中位数。
# 请注意,这一步骤不涉及任何数据的实际修改,它仅仅是一个学习过程。
# 一旦imputer完成拟合,它就准备好了用于转换数据的中位数信息,这些信息将用于填充缺失值,
# 使得数据集适合进一步的分析或模型训练。
# 调用fit方法。
imputer.fit(housing_num)
imputer
计算出了每个属性的中位数,并将结果保存在了实例变量statistics_
中。虽然此时只有属性total_bedrooms
存在缺失值,但我们不能确定在以后的新的数据中会不会有其他属性也存在缺失值,所以安全的做法是将imputer
应用到每个数值:
# 在使用SimpleImputer的fit方法对数据进行拟合之后,该实例会获得一些属性,
# 其中statistics_属性存储了用于填充缺失值的统计量。
# 更具体地说,对于使用"median"策略的SimpleImputer,statistics_属性将包含每个数值型列的中位数。
# 这些中位数是在fit方法执行期间计算得出的,用于代替数据中的缺失值。
# 访问imputer.statistics_将允许我们查看或使用这些统计量,例如进行数据的转换或进一步分析。
# 打印imputer.statistics_以查看计算得到的用于填充的中位数。
print(imputer.statistics_)
运行结果:
[-118.51 34.26 29. 2119. 433. 1164. 408. 3.54155]
# 使用pandas的median方法计算housing_num DataFrame中每个数值型列的中位数。
# median方法是一个用于描述性统计的函数,它为我们提供了一种衡量中心趋势的方式,
# 与平均数相比,中位数对异常值不敏感,因此是描述偏斜分布数据的一个更好选择。
# 调用median方法后,我们得到了一个Series,其中的索引是列名,数据是对应的中位数。
# 通过调用.values属性,我们可以得到一个NumPy数组,其中包含了所有列的中位数。
# 这个数组可以用于进一步的计算或分析,例如作为SimpleImputer的填充值。
# 打印housing_num的中位数数组。
print(housing_num.median().values)
运行结果:
[-118.51 34.26 29. 2119. 433. 1164. 408. 3.54155]
现在,你就可以使用这个“训练过的”imputer
来对训练集进行转换,将缺失值替换为中位数:
# 使用SimpleImputer的transform方法将拟合后的填充策略应用到housing_num DataFrame上。
# transform方法会根据之前fit方法学习到的统计量(例如中位数)来填充数据中的缺失值。
# 这一步骤是数据预处理流程的一部分,目的是确保输入到机器学习模型的数据是干净且完整的。
# 通过transform方法,imputer将housing_num中每个列的缺失值替换为相应的中位数,
# 从而返回一个填充了缺失值的NumPy数组。
# 将transform方法的输出赋值给变量X,这个数组现在可以被用作特征输入到机器学习模型中。
# X是清洗并填充了缺失值之后的特征集,准备好了进行训练或其他分析。
# 执行transform操作,并存储结果。
X = imputer.transform(housing_num)
结果是一个包含转换后特征的普通的 Numpy 数组。如果你想将其放回到 PandasDataFrame
中,也很简单:
# 使用pandas的DataFrame构造函数将由SimpleImputer的transform方法返回的NumPy数组X转换为DataFrame。
# 这个新的DataFrame,命名为housing_tr,将包含经过缺失值填充处理后的特征数据。
# 我们使用原始housing_num DataFrame的列名作为新DataFrame的列名,以保持数据的一致性。
# 同时,我们使用原始housing DataFrame的索引作为新DataFrame的索引,以确保数据的对应关系。
# 将转换后的DataFrame存储在变量housing_tr中,现在它可以用于代替原始的housing_num DataFrame,
# 用于后续的数据分析或模型训练。
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing.index)
# 使用.loc方法访问housing_tr DataFrame中特定行的子集。
# 这里我们使用sample_incomplete_rows.index.values,它是之前识别出的包含缺失值的样本行的索引数组。
# 通过这种方式,我们可以快速定位并访问那些原始数据中存在缺失值的行,以便于进一步的检查或操作。
# 例如,我们可以比较填充缺失值后的这些行与原始数据中的对应行,以确保填充操作的效果符合预期。
# 执行.loc索引操作,并存储结果(这里没有赋予新的变量,所以结果不会显示,除非进一步操作)。
housing_tr.loc[sample_incomplete_rows.index.values]
运行结果:
顺便看看 ocean_proximity
# 从housing DataFrame中选择'ocean_proximity'列,该列可能包含非数值型数据,例如文本描述。
# 我们创建一个新的DataFrame命名为housing_cat,只包含这一列。
# 这个步骤是数据预处理的一部分,特别是当我们要对分类数据进行特定的处理或转换时。
# 选择特定列后,我们可能想要查看这些数据的前几个条目,以了解它们的分布和可能的类别。
# 这有助于我们为后续的编码或分析任务做准备。
# 创建一个新的DataFrame,housing_cat,仅包含'ocean_proximity'列。
housing_cat = housing[['ocean_proximity']]
# 使用head方法并设置参数为10,打印housing_cat DataFrame的前10行数据。
# 这为我们提供了一个快速查看数据概览的手段,帮助我们理解'ocean_proximity'列中包含的信息。
# 例如,我们可以检查文本描述的多样性,是否有缺失值,以及是否需要进一步的数据清洗。
housing_cat.head(10)
运行结果:
2、处理文本和类别属性
前面,我们丢弃了类别属性ocean_proximity
,因为它是一个文本属性,不能计算出中位数。大多数机器学习算法更喜欢和数字打交道,所以让我们把这些文本标签转换为数字。
Scikit-Learn 为这个任务提供了一个转换器OrdinalEncoder:
Warning: earlier versions of the book used the
LabelEncoder
class or Pandas'Series.factorize()
method to encode string categorical attributes as integers. However, theOrdinalEncoder
class that was introduced in Scikit-Learn 0.20 (see PR #10521) is preferable since it is designed for input features (X
instead of labelsy
) and it plays well with pipelines (introduced later in this notebook). If you are using an older version of Scikit-Learn (<0.20), then you can import it fromfuture_encoders.py
instead.
# 尝试从Scikit-Learn的preprocessing模块导入OrdinalEncoder类。
# OrdinalEncoder是一个用于将分类数据(即具有有序关系的数据)转换为数字编码的工具。
try:
from sklearn.preprocessing import OrdinalEncoder
except ImportError:
# 如果在当前环境中导入OrdinalEncoder失败(可能是因为Scikit-Learn版本低于0.20),
# 则从future_encoders模块导入OrdinalEncoder。这是为了确保代码的兼容性。
from future_encoders import OrdinalEncoder # Scikit-Learn < 0.20
# 创建OrdinalEncoder的实例。
ordinal_encoder = OrdinalEncoder()
# 使用fit_transform方法对housing_cat DataFrame进行拟合和转换。
# fit_transform首先学习每个类别的顺序,然后将其转换为相应的有序整数编码。
# 这通常用于那些分类特征具有自然顺序的情况,例如"small", "medium", "large"。
# 转换后的数据存储在housing_cat_encoded中。
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
# 打印转换后的前10个编码样本,以便于检查OrdinalEncoder的工作效果。
# 这可以帮助我们快速了解编码后的数值分布和类别的排序。
housing_cat_encoded[:10]
运行结果:
array([[1.], [4.], [1.], [4.], [0.], [3.], [0.], [0.], [0.], [0.]])
好了一些,现在就可以在任何 ML 算法里用这个数值数据了。你可以查看映射表,编码器是通过属性 categories_ 来学习的(<1H OCEAN
被映射为 0,INLAND
被映射为 1,等等):
# 在使用OrdinalEncoder的fit方法拟合数据后,encoder会记录下每个特征的类别顺序。
# 这个顺序被存储在ordinal_encoder的categories_属性中,它是一个列表,其中包含了每个特征的已知类别。
# 更具体地说,对于DataFrame中的每个分类特征,categories_属性会提供一个包含该特征所有已知类别的列表。
# 这些类别是按照它们在原始数据中出现的顺序排列的,并且它们将被用作编码的基础。
# 访问ordinal_encoder.categories_属性可以让我们查看这些类别,这对于理解数据特征的分布和编码逻辑很有帮助。
# 打印OrdinalEncoder学习到的类别列表。
print(ordinal_encoder.categories_)
运行结果:
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'], dtype=object)]
这种做法的问题是,ML 算法会认为两个临近的值比两个疏远的值要更相似。显然这样不对(比如,分类 0 和分类 4 就比分类 0 和分类 1 更相似)。要解决这个问题,一个常见的方法是给每个分类创建一个二元属性:当分类是<1H OCEAN
,该属性为 1(否则为 0),当分类是INLAND
,另一个属性等于 1(否则为 0),以此类推。这称作独热编码(One-Hot Encoding),因为只有一个属性会等于 1(热),其余会是 0(冷)。
Scikit-Learn 提供了一个编码器OneHotEncoder
,用于将整数分类值转变为独热向量。注意fit_transform()
用于 2D 数组,而housing_cat_encoded
是一个 1D 数组,所以需要将其变形:
# 尝试从Scikit-Learn的preprocessing模块导入OrdinalEncoder和OneHotEncoder类。
# OneHotEncoder是一个用于将分类数据转换为一组布尔特征(也称为独热编码)的工具。
# 这里首先导入OrdinalEncoder,即使我们不使用它,这样做的目的是为了在Scikit-Learn版本低于0.20时触发ImportError。
try:
from sklearn.preprocessing import OrdinalEncoder # 用于触发ImportError
from sklearn.preprocessing import OneHotEncoder
except ImportError:
# 如果导入失败(即Scikit-Learn版本低于0.20),则从future_encoders模块导入OneHotEncoder。
# 这确保了代码在旧版本的Scikit-Learn上也能正常工作。
from future_encoders import OneHotEncoder # Scikit-Learn < 0.20
# 创建OneHotEncoder的实例。
cat_encoder = OneHotEncoder()
# 使用fit_transform方法对housing_cat DataFrame进行拟合和转换。
# fit_transform方法首先学习数据中的类别,然后将其转换为独热编码的形式。
# 独热编码是一种常见的编码方式,特别适用于没有自然顺序的分类特征。
# 转换后的数据是一个稀疏矩阵,其中包含了原始分类特征的编码表示。
# 执行OneHotEncoder的拟合和转换操作,并将结果存储在housing_cat_1hot中。
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
# 打印转换后的独热编码矩阵。这个矩阵可以作为特征输入到机器学习模型中。
# 稀疏矩阵通常只显示非零元素,以节省空间和提高效率。
housing_cat_1hot
运行结果:
<Compressed Sparse Row sparse matrix of dtype 'float64' with 16512 stored elements and shape (16512, 5)>
注意输出结果是一个 SciPy 稀疏矩阵,而不是 NumPy 数组。当类别属性有数千个分类时,这样非常有用。经过独热编码,我们得到了一个有数千列的矩阵,这个矩阵每行只有一个 1,其余都是 0。使用大量内存来存储这些 0 非常浪费,所以稀疏矩阵只存储非零元素的位置。你可以像一个 2D 数据那样进行使用,但是如果你真的想将其转变成一个(密集的)NumPy 数组,只需调用toarray()
方法:
# housing_cat_1hot是由OneHotEncoder.fit_transform方法返回的稀疏矩阵。
# 稀疏矩阵是一种存储方式,它仅保存非零元素的位置和值,从而节省内存和计算资源。
# 然而,在某些情况下,我们可能需要将稀疏矩阵转换为一个常规的NumPy数组格式。
# toarray方法将稀疏矩阵转换为一个密集的NumPy数组。
# 这在需要使用非稀疏格式的数据进行计算或分析时非常有用,例如在某些机器学习算法中。
# 调用toarray方法将housing_cat_1hot稀疏矩阵转换为NumPy数组,并打印结果。
# 转换后的数组将包含与原始分类特征相同信息的独热编码表示,但占用更多的内存。
housing_cat_1hot.toarray()
运行结果:
array([[0., 1., 0., 0., 0.], [0., 0., 0., 0., 1.], [0., 1., 0., 0., 0.], ..., [1., 0., 0., 0., 0.], [1., 0., 0., 0., 0.], [0., 1., 0., 0., 0.]])
3、自定义转换器
尽管 Scikit-Learn 提供了许多有用的转换器,你还是需要自己动手写转换器执行任务,比如自定义的清理操作,或属性组合。你需要让自制的转换器与 Scikit-Learn 组件(比如流水线)无缝衔接工作,因为 Scikit-Learn 是依赖鸭子类型的(而不是继承),你所需要做的是创建一个类并执行三个方法:fit()
(返回self
),transform()
,和fit_transform()
。通过添加TransformerMixin
作为基类,可以很容易地得到最后一个。另外,如果你添加BaseEstimator
作为基类(且构造器中避免使用*args
和**kargs
),你就能得到两个额外的方法(get_params()
和set_params()
),二者可以方便地进行超参数自动微调。例如,一个小转换器类添加了上面讨论的属性:
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np
# 获取住房数据集中特定列的正确索引。这种方法比硬编码列的索引更安全和灵活。
rooms_ix, bedrooms_ix, population_ix, household_ix = [
list(housing.columns).index(col) # 使用list转换和index方法来获取列索引
for col in ("total_rooms", "total_bedrooms", "population", "households")
]
# 定义一个自定义的转换器CombinedAttributesAdder,继承自BaseEstimator和TransformerMixin。
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def __init__(self, add_bedrooms_per_room=True): # 初始化函数,带有一个可配置的参数
self.add_bedrooms_per_room = add_bedrooms_per_room
def fit(self, X, y=None): # 拟合函数,这里不需要做任何操作
return self
def transform(self, X, y=None): # 转换函数,根据配置计算新特征
# 计算每个家庭的房间数和人口数
rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
population_per_household = X[:, population_ix] / X[:, household_ix]
# 如果配置为True,则计算每个房间的卧室数
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
# 将新特征列添加到原始数据X中
return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room]
else:
# 否则,只添加房间和人口特征
return np.c_[X, rooms_per_household, population_per_household]
# 创建CombinedAttributesAdder的实例,设置add_bedrooms_per_room为False,表示不添加每个房间的卧室数特征。
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
# 使用transform方法计算新特征,并将结果存储在housing_extra_attribs中。
# 这里将housing的原始数据(作为NumPy数组)传递给转换器。
housing_extra_attribs = attr_adder.transform(housing.values)
在这个例子中,转换器有一个超参数add_bedrooms_per_room
,默认设为True
(提供一个合理的默认值很有帮助)。这个超参数可以让你方便地发现添加了这个属性是否对机器学习算法有帮助。更一般地,你可以为每个不能完全确保的数据准备步骤添加一个超参数。数据准备步骤越自动化,可以自动化的操作组合就越多,越容易发现更好用的组合(并能节省大量时间)。
或者,你可以使用Scikit-Learn的FunctionTransformer类,它让你可以轻松地基于转换函数创建一个转换器(感谢Hanmin Qin建议了这段代码)。请注意,我们需要设置validate=False,因为数据包含非浮点值(在Scikit-Learn 0.22中,validate默认为False)。
from sklearn.preprocessing import FunctionTransformer
import numpy as np
# 定义一个函数add_extra_features,用于计算额外的特征。
# 这个函数接受一个NumPy数组X和一个可选的布尔参数add_bedrooms_per_room。
def add_extra_features(X, add_bedrooms_per_room=True):
# 计算每个家庭的房间数和人口数特征。
rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
population_per_household = X[:, population_ix] / X[:, household_ix]
# 如果add_bedrooms_per_room为True,则计算每个房间的卧室数特征。
if add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
# 将原始特征矩阵X与新计算的特征水平连接(concatenate)。
return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room]
else:
# 如果add_bedrooms_per_room为False,则只添加房间和人口特征。
return np.c_[X, rooms_per_household, population_per_household]
# 创建FunctionTransformer的实例,用于将add_extra_features函数转换为一个Scikit-Learn转换器。
# FunctionTransformer允许我们使用自定义的函数作为转换器,并且可以整合到Scikit-Learn的Pipeline中。
# validate=False参数关闭了对输入X的验证,kw_args提供了传递给函数的额外参数。
attr_adder = FunctionTransformer(
add_extra_features,
validate=False,
kw_args={"add_bedrooms_per_room": False}
)
# 使用FunctionTransformer的fit_transform方法计算新特征。
# 这里将housing的原始数据(作为NumPy数组)传递给转换器。
# fit_transform首先拟合转换器(在这里没有实际的拟合操作,因为add_extra_features是一个无状态的函数),
# 然后转换数据。
housing_extra_attribs = attr_adder.fit_transform(housing.values)
housing_extra_attribs
# 将由数值数组表示的住房额外属性转换为pandas DataFrame。
# 这个新的DataFrame将包含原始的住房数据以及新计算的属性。
# 使用pd.DataFrame构造函数,将数值数组housing_extra_attribs转换为DataFrame。
# 我们指定列名为原始住房数据的列名加上新计算的属性列名。
# 同时,我们使用原始housing DataFrame的索引作为新DataFrame的索引,以保持数据的一致性。
housing_extra_attribs = pd.DataFrame(
housing_extra_attribs, # 从FunctionTransformer得到的NumPy数组
columns=list(housing.columns) + ["rooms_per_household", "population_per_household"], # 列名
index=housing.index # 索引
)
# 使用head方法打印新DataFrame的前几行,以便于检查新添加的属性是否正确地合并到了数据集中。
# 这为我们提供了一个直观的方式来查看数据,并确认新属性的计算和添加操作是成功的。
housing_extra_attribs.head()
运行结果:
4、特征缩放
数据要做的最重要的转换之一是特征缩放。除了个别情况,当输入的数值属性量度不同时,机器学习算法的性能都不会好。这个规律也适用于房产数据:总房间数分布范围是 6 到 39320,而收入中位数只分布在 0 到 15。注意通常情况下我们不需要对目标值进行缩放。
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。
线性函数归一化(许多人称其为归一化(normalization))很简单:值被转变、重新缩放,直到范围变成 0 到 1。我们通过减去最小值,然后再除以最大值与最小值的差值,来进行归一化。Scikit-Learn 提供了一个转换器MinMaxScaler
来实现这个功能。它有一个超参数feature_range
,可以让你改变范围,如果不希望范围是 0 到 1。
标准化就很不同:首先减去平均值(所以标准化值的平均值总是 0),然后除以方差,使得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值得范围是 0 到 1)。但是,标准化受到异常值的影响很小。例如,假设一个街区的收入中位数由于某种错误变成了 100,归一化会将其它范围是 0 到 15 的值变为 0-0.15,但是标准化不会受什么影响。Scikit-Learn 提供了一个转换器StandardScaler
来进行标准化。
5、转换流水线
你已经看到,存在许多数据转换步骤,需要按一定的顺序执行。幸运的是,Scikit-Learn 提供了类Pipeline
,来进行这一系列的转换。下面是一个数值属性的小流水线:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# 创建一个名为num_pipeline的Pipeline,它将多个数据转换步骤串联起来。
# Pipeline是一种高效的数据处理方式,可以按顺序应用多个转换器,并且自动传递数据。
# 这里定义的Pipeline包含三个步骤:
# 第一步是'imputer',使用SimpleImputer来填充数值特征中的缺失值,策略为"median"。
# 第二步是'attribs_adder',使用FunctionTransformer来应用add_extra_features函数,
# 该函数计算每个家庭的房间数和人口数特征,validate=False表示不进行数据验证。
# 第三步是'std_scaler',使用StandardScaler进行标准化处理,使特征具有零均值和单位方差。
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', FunctionTransformer(add_extra_features, validate=False)),
('std_scaler', StandardScaler()),
])
# 使用定义好的Pipeline对housing_num数据集进行拟合和转换。
# fit_transform方法首先拟合Pipeline中的每个转换器(SimpleImputer和StandardScaler需要拟合),
# 然后对数据应用所有转换步骤,最终返回转换后的数据。
# 这样,housing_num_tr将包含填充缺失值、添加额外属性和标准化处理后的特征。
housing_num_tr = num_pipeline.fit_transform(housing_num)
housing_num_tr
运行结果:
array([[-0.94135046, 1.34743822, 0.02756357, ..., 0.01739526, 0.00622264, -0.12112176], [ 1.17178212, -1.19243966, -1.72201763, ..., 0.56925554, -0.04081077, -0.81086696], [ 0.26758118, -0.1259716 , 1.22045984, ..., -0.01802432, -0.07537122, -0.33827252], ..., [-1.5707942 , 1.31001828, 1.53856552, ..., -0.5092404 , -0.03743619, 0.32286937], [-1.56080303, 1.2492109 , -1.1653327 , ..., 0.32814891, -0.05915604, -0.45702273], [-1.28105026, 2.02567448, -0.13148926, ..., 0.01407228, 0.00657083, -0.12169672]])
Pipeline
构造器需要一个定义步骤顺序的名字/估计器对的列表。除了最后一个估计器,其余都要是转换器(即,它们都要有fit_transform()
方法)。名字可以随意起。
当你调用流水线的fit()
方法,就会对所有转换器顺序调用fit_transform()
方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个估计器,它只执行fit()
方法。
流水线暴露相同的方法作为最终的估计器。在这个例子中,最后的估计器是一个StandardScaler
,它是一个转换器,因此这个流水线有一个transform()
方法,可以顺序对数据做所有转换(它还有一个fit_transform
方法可以使用,就不必先调用fit()
再进行transform()
)。
如果不需要手动将 PandasDataFrame
中的数值列转成 Numpy 数组的格式,而可以直接将DataFrame
输入 pipeline 中进行处理就好了。Scikit-Learn 没有工具来处理 PandasDataFrame
,因此我们需要写一个简单的自定义转换器来做这项工作:
from sklearn.base import BaseEstimator, TransformerMixin
# 创建一个名为OldDataFrameSelector的类,用于从原始数据集中选择特定的数值型或分类型列。
# 这个类继承自BaseEstimator和TransformerMixin,使其遵循Scikit-Learn的转换器接口,
# 从而可以轻松地集成到Scikit-Learn的Pipeline和其他工具中。
class OldDataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
# 初始化函数接收一个参数attribute_names,这是一个包含列名的列表,
# 指定了要从DataFrame X中选择哪些列进行处理。
self.attribute_names = attribute_names
def fit(self, X, y=None):
# fit方法用于对转换器进行拟合。对于这个选择器类,我们不需要从数据X中学习任何内容,
# 因为我们已经预先知道了要使用的列名。因此,这个方法直接返回self实例。
return self
def transform(self, X):
# transform方法用于对数据集X进行实际的转换操作。
# 这个方法将DataFrame X中指定的attribute_names列作为子集提取出来,
# 并将结果转换为NumPy数组格式返回。这使得选择的列可以被后续的Pipeline步骤进一步处理。
# 使用.values属性将DataFrame列转换为NumPy数组。
return X[self.attribute_names].values
每个子流水线都以一个选择转换器开始:通过选择对应的属性(数值或分类)、丢弃其它的,来转换数据,并将输出DataFrame
转变成一个 NumPy 数组。这样,你就可以很简单的写出一个以 Pandas DataFrame
为输入并且可以处理数值的流水线: 该流水线从DataFrameSelector
开始获取数值属性,前面讨论过的其他数据处理步骤紧随其后。 并且你也可以通过使用DataFrameSelector
选择类别属性并为其写另一个流水线然后应用LabelBinarizer
.
你现在就有了一个对数值的流水线,你还需要对分类值应用LabelBinarizer
:如何将这些转换写成一个流水线呢?Scikit-Learn 提供了一个类FeatureUnion
实现这个功能。你给它一列转换器(可以是所有的转换器),当调用它的transform()
方法,每个转换器的transform()
会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的fit()
方法就会调用每个转换器的fit()
)。一个完整的处理数值和类别属性的流水线如下所示:
# 尝试从Scikit-Learn的compose模块导入ColumnTransformer类。
# ColumnTransformer是用于对数据集中的不同列应用不同转换流程的工具。
try:
from sklearn.compose import ColumnTransformer
except ImportError:
# 如果导入失败(可能是因为Scikit-Learn版本低于0.20),则从future_encoders模块导入ColumnTransformer。
from future_encoders import ColumnTransformer # Scikit-Learn < 0.20
# 定义数值型属性的列表,这些是我们将要选择并处理的数值列。
num_attribs = list(housing_num)
# 定义分类属性的列表,这里我们选择"ocean_proximity"作为分类属性列。
cat_attribs = ["ocean_proximity"]
# 创建一个ColumnTransformer实例,命名为full_pipeline。
# ColumnTransformer允许我们分别为数值型和分类属性定义不同的处理Pipeline。
# 这里我们使用之前定义的num_pipeline来处理数值型属性(num_attribs),
# 并使用OneHotEncoder来处理分类属性(cat_attribs)。
full_pipeline = ColumnTransformer([
("num", num_pipeline, num_attribs), # 为数值型属性定义的Pipeline
("cat", OneHotEncoder(), cat_attribs), # 为分类属性定义的处理步骤
])
你可以很简单地运行整个流水线:
# 使用full_pipeline的fit_transform方法对原始housing数据集进行拟合和转换。
# 这将应用我们为数值型和分类属性定义的所有转换步骤,并返回转换后的数据。
housing_prepared = full_pipeline.fit_transform(housing)
# 打印转换后的数据集housing_prepared的形状。
# 这有助于我们了解转换后数据的维度,包括特征的数量和样本的数量。
housing_prepared.shape
运行结果:
(16512, 16)
作为参考,以下是基于DataFrameSelector转换器的旧解决方案(仅选择Pandas DataFrame列的一个子集)和FeatureUnion:
from sklearn.base import BaseEstimator, TransformerMixin
# 创建一个名为OldDataFrameSelector的类,用于从原始数据集中选择特定的数值型或分类型列。
# 这个类继承自BaseEstimator和TransformerMixin,使其遵循Scikit-Learn的转换器接口,
# 从而可以轻松地集成到Scikit-Learn的Pipeline和其他工具中。
class OldDataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
# 初始化函数接收一个参数attribute_names,这是一个包含列名的列表,
# 指定了要从DataFrame X中选择哪些列进行处理。
self.attribute_names = attribute_names
def fit(self, X, y=None):
# fit方法用于对转换器进行拟合。对于这个选择器类,我们不需要从数据X中学习任何内容,
# 因为我们已经预先知道了要使用的列名。因此,这个方法直接返回self实例。
return self
def transform(self, X):
# transform方法用于对数据集X进行实际的转换操作。
# 这个方法将DataFrame X中指定的attribute_names列作为子集提取出来,
# 并将结果转换为NumPy数组格式返回。这使得选择的列可以被后续的Pipeline步骤进一步处理。
# 使用.values属性将DataFrame列转换为NumPy数组。
return X[self.attribute_names].values
现在,让我们将所有这些组件连接到一个大型管道中,它将预处理数值型和类别型特征(同样,如果愿意,我们可以使用CombinedAttributesAdder()而不是FunctionTransformer(…)):
# 定义数值型属性的列表,这些是我们将要选择并处理的数值列。
num_attribs = list(housing_num)
# 定义分类属性的列表,这里我们只有一个分类属性 "ocean_proximity"。
cat_attribs = ["ocean_proximity"]
# 创建一个用于处理数值型属性的Pipeline,命名为old_num_pipeline。
# Pipeline是一种高效的数据处理方式,可以按顺序应用多个转换步骤,并且自动传递数据。
# 在old_num_pipeline中:
# 'selector'步骤使用OldDataFrameSelector来选择数值型属性。
# 'imputer'步骤使用SimpleImputer填充缺失值,策略为中位数。
# 'attribs_adder'步骤使用FunctionTransformer来应用add_extra_features函数,添加额外的特征。
# 'std_scaler'步骤使用StandardScaler进行标准化处理。
old_num_pipeline = Pipeline([
('selector', OldDataFrameSelector(num_attribs)),
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', FunctionTransformer(add_extra_features, validate=False)),
('std_scaler', StandardScaler()),
])
# 创建一个用于处理分类属性的Pipeline,命名为old_cat_pipeline。
# 这个Pipeline专门用于处理分类数据,将其转换为模型可以处理的格式。
# 在old_cat_pipeline中:
# 'selector'步骤使用OldDataFrameSelector来选择分类属性。
# 'cat_encoder'步骤使用OneHotEncoder进行独热编码,sparse=False表示输出为密集的NumPy数组。
old_cat_pipeline = Pipeline([
('selector', OldDataFrameSelector(cat_attribs)),
('cat_encoder', OneHotEncoder()),
])
运行代码,查看结果
from sklearn.pipeline import FeatureUnion
# 创建一个FeatureUnion实例,命名为old_full_pipeline。
# FeatureUnion是Scikit-Learn中用于合并多个数据转换Pipeline的类,
# 它允许我们将数值型和分类型数据的转换过程合并到一个统一的Pipeline中。
# transformer_list参数是一个列表,其中包含了两个Pipeline的元组:
# "num_pipeline"对应于处理数值型特征的Pipeline(old_num_pipeline),
# "cat_pipeline"对应于处理分类型特征的Pipeline(old_cat_pipeline)。
# 每个Pipeline可以包含多个转换步骤,例如特征选择、缺失值填充、特征构造和标准化。
old_full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", old_num_pipeline), # 处理数值型特征的Pipeline
("cat_pipeline", old_cat_pipeline), # 处理分类型特征的Pipeline
])
# 使用之前定义的FeatureUnion实例old_full_pipeline来处理housing数据集。
# fit_transform方法首先对每个Pipeline进行拟合(fit),然后对数据进行转换(transform)。
# 这个方法会依次执行数值型和分类型Pipeline中的所有转换步骤。
# 对于数值型Pipeline(old_num_pipeline):
# - 首先通过OldDataFrameSelector选择数值型列。
# - 然后使用SimpleImputer填充缺失值。
# - 通过FunctionTransformer添加额外的数值特征。
# - 最后使用StandardScaler进行标准化。
# 对于分类型Pipeline(old_cat_pipeline):
# - 首先通过OldDataFrameSelector选择分类列。
# - 然后使用OneHotEncoder进行独热编码。
# 这些步骤完成后,我们将得到一个融合了数值型和分类型特征的新数据集。
# 这个新数据集old_housing_prepared已经准备好被用于机器学习模型的训练和评估。
old_housing_prepared = old_full_pipeline.fit_transform(housing)
# 打印转换后的数据集old_housing_prepared。
# 这可以帮助我们检查转换结果,例如数据的形状、是否有缺失值等。
old_housing_prepared
运行结果:
<Compressed Sparse Row sparse matrix of dtype 'float64' with 198144 stored elements and shape (16512, 16)>
结果与ColumnTransformer相同:
import numpy as np
import numpy as np
from scipy.sparse import issparse
# 使用numpy的allclose函数来比较两个数组:housing_prepared和old_housing_prepared。
# allclose函数检查两个数组是否在某个容忍度内相等,即它们是否足够接近,
# 考虑到可能的浮点数运算误差。
# 这个函数返回一个布尔值,如果两个数组在指定的容忍度内相等,则返回True,否则返回False。
# 在这里,我们使用allclose来验证新的数据处理流程(生成housing_prepared)是否与
# 旧的数据处理流程(生成old_housing_prepared)产生了相同的结果。
# 如果返回True,这意味着新的数据处理流程在数值上与旧的相同,这是对新流程正确性的一种验证。
# 如果返回False,这意味着两个数组至少在一个元素上存在显著差异,可能需要进一步调查。
# 假设 housing_prepared 是一个密集的NumPy数组,而 old_housing_prepared 是一个稀疏矩阵
# 首先,检查 old_housing_prepared 是否为稀疏矩阵
if issparse(old_housing_prepared):
# 如果是稀疏矩阵,我们将其转换为密集的NumPy数组
old_housing_prepared = old_housing_prepared.toarray()
# 现在,两个数据集都是密集的NumPy数组或者相同的数据类型,我们可以使用np.allclose进行比较
# 执行比较操作,检查两个数组是否在某个容忍度内相等
np.allclose(housing_prepared, old_housing_prepared)
运行结果:
True
附录:
一、一些知识点
1、Scikit-Learn 设计
Scikit-Learn 设计的 API 设计的非常好。它的主要设计原则是:
一致性:所有对象的接口一致且简单:
- 估计器(estimator)。任何可以基于数据集对一些参数进行估计的对象都被称为估计器(比如,
imputer
就是个估计器)。估计本身是通过fit()
方法,只需要一个数据集作为参数(对于监督学习算法,需要两个数据集;第二个数据集包含标签)。任何其它用来指导估计过程的参数都被当做超参数(比如imputer
的strategy
),并且超参数要被设置成实例变量(通常通过构造器参数设置)。- 转换器(transformer)。一些估计器(比如
imputer
)也可以转换数据集,这些估计器被称为转换器。API 也是相当简单:转换是通过transform()
方法,被转换的数据集作为参数。返回的是经过转换的数据集。转换过程依赖学习到的参数,比如imputer
的例子。所有的转换都有一个便捷的方法fit_transform()
,等同于调用fit()
再transform()
(但有时fit_transform()
经过优化,运行的更快)。- 预测器(predictor)。最后,一些估计器可以根据给出的数据集做预测,这些估计器称为预测器。例如,上一章的
LinearRegression
模型就是一个预测器:它根据一个国家的人均 GDP 预测生活满意度。预测器有一个predict()
方法,可以用新实例的数据集做出相应的预测。预测器还有一个score()
方法,可用于评估测试集(如果是监督学习算法的话,还要给出相应的标签)的预测质量。可检验。所有估计器的超参数都可以通过实例的公共变量直接访问(比如,
imputer.strategy
),并且所有估计器学习到的参数也可以通过在实例变量名后加下划线来访问(比如,imputer.statistics_
)。类不可扩散。数据集被表示成 NumPy 数组或 SciPy 稀疏矩阵,而不是自制的类。超参数只是普通的 Python 字符串或数字。
可组合。尽可能使用现存的模块。例如,用任意的转换器序列加上一个估计器,就可以做成一个流水线,后面会看到例子。
合理的默认值。Scikit-Learn 给大多数参数提供了合理的默认值,很容易就能创建一个系统。
二、源码工程
https://github.com/XANkui/PythonMachineLearnIntermediateLevel
三、该案例的环境 package 信息如下
Package Version
------------------------- --------------
anyio 4.4.0
argon2-cffi 23.1.0
argon2-cffi-bindings 21.2.0
arrow 1.3.0
asttokens 2.4.1
async-lru 2.0.4
attrs 23.2.0
Babel 2.15.0
beautifulsoup4 4.12.3
bleach 6.1.0
certifi 2024.7.4
cffi 1.16.0
charset-normalizer 3.3.2
colorama 0.4.6
comm 0.2.2
contourpy 1.2.1
cycler 0.12.1
debugpy 1.8.2
decorator 5.1.1
defusedxml 0.7.1
executing 2.0.1
fastjsonschema 2.20.0
fonttools 4.53.1
fqdn 1.5.1
h11 0.14.0
httpcore 1.0.5
httpx 0.27.0
idna 3.7
ipykernel 6.29.5
ipython 8.26.0
ipywidgets 8.1.3
isoduration 20.11.0
jedi 0.19.1
Jinja2 3.1.4
joblib 1.4.2
json5 0.9.25
jsonpointer 3.0.0
jsonschema 4.23.0
jsonschema-specifications 2023.12.1
jupyter 1.0.0
jupyter_client 8.6.2
jupyter-console 6.6.3
jupyter_core 5.7.2
jupyter-events 0.10.0
jupyter-lsp 2.2.5
jupyter_server 2.14.2
jupyter_server_terminals 0.5.3
jupyterlab 4.2.4
jupyterlab_pygments 0.3.0
jupyterlab_server 2.27.3
jupyterlab_widgets 3.0.11
kiwisolver 1.4.5
MarkupSafe 2.1.5
matplotlib 3.9.1
matplotlib-inline 0.1.7
mistune 3.0.2
nbclient 0.10.0
nbconvert 7.16.4
nbformat 5.10.4
nest-asyncio 1.6.0
notebook 7.2.1
notebook_shim 0.2.4
numpy 2.0.1
overrides 7.7.0
packaging 24.1
pandas 2.2.2
pandocfilters 1.5.1
parso 0.8.4
pillow 10.4.0
pip 24.1.2
platformdirs 4.2.2
prometheus_client 0.20.0
prompt_toolkit 3.0.47
psutil 6.0.0
pure_eval 0.2.3
pycparser 2.22
Pygments 2.18.0
pyparsing 3.1.2
python-dateutil 2.9.0.post0
python-json-logger 2.0.7
pytz 2024.1
pywin32 306
pywinpty 2.0.13
PyYAML 6.0.1
pyzmq 26.0.3
qtconsole 5.5.2
QtPy 2.4.1
referencing 0.35.1
requests 2.32.3
rfc3339-validator 0.1.4
rfc3986-validator 0.1.1
rpds-py 0.19.1
scikit-learn 1.5.1
scipy 1.14.0
Send2Trash 1.8.3
setuptools 70.1.1
six 1.16.0
sniffio 1.3.1
soupsieve 2.5
stack-data 0.6.3
terminado 0.18.1
threadpoolctl 3.5.0
tinycss2 1.3.0
tornado 6.4.1
traitlets 5.14.3
types-python-dateutil 2.9.0.20240316
typing_extensions 4.12.2
tzdata 2024.1
uri-template 1.3.0
urllib3 2.2.2
wcwidth 0.2.13
webcolors 24.6.0
webencodings 0.5.1
websocket-client 1.8.0
wheel 0.43.0
widgetsnbextension 4.0.11