一、快速查看数据结构
1.1 pandas读取csv数据
# 导入pandas库,并为其设置别名pd(这是数据分析中常用的做法,简化后续调用)
import pandas as pd
# 定义一个函数,用于加载住房数据集
# 参数file_path指定数据文件的路径,默认值为'D:\\deskTop\\机器学习实战\\第二章\\housing.csv'
# 这样调用函数时如果不指定路径,会自动使用这个默认路径
def load_housing_data(file_path='D:\\deskTop\\机器学习实战\\第二章\\housing.csv'):
# 使用pandas的read_csv函数读取CSV格式的数据文件
# 该函数会将CSV文件内容转换为pandas的DataFrame数据结构(类似表格的结构)
csv_data = pd.read_csv(file_path)
# 函数返回读取到的DataFrame数据,供后续分析使用
return csv_data
# 调用上面定义的load_housing_data函数,加载住房数据
# 这里没有指定参数,所以使用默认的文件路径
housing = load_housing_data()
# 打印数据集的前5行数据(默认head()返回前5行)
# to_string()方法确保所有列都能完整显示,而不会被省略
print(housing.head().to_string())
1.2 查看数据集的简单描述
# 使用info()方法查看数据集的基本信息,并通过print()函数打印出来
# info()方法会返回数据集的总体描述,包括:
# 1. 数据类型(DataFrame)
# 2. 样本数量(行数)和特征数量(列数)
# 3. 每一列的名称、非空值数量、数据类型(dtype)
# 4. 数据集占用的内存大小
print(housing.info())
1.3查询某个属性中类别的出现次数
# 打印'housing['ocean_proximity']列中每个类别的出现次数
# housing['ocean_proximity']:选取数据集中名为'ocean_proximity'的列(这是一个类别型特征)
# value_counts():pandas的内置方法,用于统计该列中每个唯一值(类别)出现的次数
# 结果会按出现次数从多到少排序,展示每个类别在数据集中的分布情况
print(housing['ocean_proximity'].value_counts())
1.4 获取数值属性的摘要
# 打印数据集的描述性统计信息
# describe() 是 pandas 中 DataFrame 的内置方法,用于生成数值型列的统计摘要
# 它会计算并返回每个数值列的计数、均值、标准差、最小值、四分位数和最大值
print(housing.describe())
count
:非空值的数量(可验证是否有缺失值)mean
:平均值(数据的中心趋势)std
:标准差(数据的离散程度,值越大说明数据越分散)min
:最小值25%
、50%
、75%
:四分位数(分别代表数据中 25%、50%、75% 位置的值,50% 即中位数)max
:最大值
1.5使用hist()方法绘制全部属性的直方图
import pandas as pd
# 导入matplotlib库的pyplot模块,重命名为plt
# matplotlib是Python中最常用的绘图库,pyplot模块提供了类似MATLAB的绘图接口
import matplotlib.pyplot as plt
def load_housing_data(file_path='D:\\deskTop\\机器学习实战\\第二章\\housing.csv'):
csv_data = pd.read_csv(file_path)
return csv_data
housing = load_housing_data()
# 对housing数据集绘制直方图
# .hist()是DataFrame的方法,会为数据集中所有数值型列分别绘制直方图
# bins=50:指定每个直方图的分箱数量为50,分箱越多,图形越精细,能展示更多分布细节
# figsize=(20,15):设置整个图形的尺寸,宽20英寸,高15英寸(数值越大,图形越大)
housing.hist(bins=50, figsize=(20, 15))
# 显示绘制好的直方图
# 这是matplotlib的显示命令,没有这一行,在多数环境中无法看到图像
plt.show()
"分箱" 可以理解成 "小柱子" 的数量。
比如你有一组数据是 1 到 100 的年龄:
- 如果 bins=10,就会把数据分成 10 个区间(1-10 岁、11-20 岁...91-100 岁),画出来 10 根柱子
- 而 bins=50,就会分成 50 个区间(1-2 岁、3-4 岁...99-100 岁),画出来 50 根柱子
分箱越多(柱子越多):
- 能看到更细的细节,比如 "27 岁" 和 "28 岁" 的人数差异
- 图形看起来更 "密",但可能显得乱
- 适合想仔细研究数据分布细节的时候用
分箱少的时候:
- 图形更简洁,但细节会被合并
- 比如 bins=5 的话,可能 20-40 岁都算一个区间,看不出中间的变化
打个比方:就像用放大镜看东西,bins 越大,放大镜倍数越高,能看到的细节越多。
二、创建测试集
常见的方法是为每条数据添加标识符(每条数据的标识符是唯一的),通过标识符决定该数据是否进入测试。确保更新数据时的测试集仍不变,新添加的数据同样按照此规则选取部分数据添加到测试集。
2.1 为数据添加唯一标识
# 为数据集添加ID列,目的是给每个样本生成唯一标识,以便后续稳定地拆分训练集和测试集
# 稳定拆分指的是:多次运行代码时,同一个样本始终会被分到同一个集合(训练集或测试集)
# 第一步:重置数据集的索引,并将原来的索引作为新的列('index')添加到数据集中
# reset_index()会创建一个新的连续整数索引,同时保留原来的索引作为新列
# 这一步是为了生成一个临时的标识列,也可以作为备选的ID方案
housing_with_id = housing.reset_index()
# 第二步:生成更具业务意义的唯一ID(推荐方案)
# 这里利用了数据中的地理信息(经度和纬度)来生成ID:
# 用经度(longitude)乘以1000,再加上纬度(latitude)
# 原因:每个样本对应的地理位置(经纬度)是唯一的,因此计算结果也会是唯一的
# 例如:经度-122.23,纬度37.88,计算后ID为-122.23*1000 + 37.88 = -122192.12
# 这样生成的ID既唯一,又能反映样本的地理属性,适合后续可能的地理相关分析
housing_with_id['id'] = housing['longitude'] * 1000 + housing['latitude']
为什么这样做?
- 唯一标识需求:拆分训练集和测试集时需要每个样本有唯一 ID,确保同一样本不会被重复划分
- 地理坐标的优势:
- 经纬度组合具有天然唯一性(每个地理位置对应唯一经纬度)
- 相比简单的数字索引,更适合房地产等与地理位置强相关的数据集
- 即使数据更新(新增样本),原有样本的 ID 也不会改变,保证拆分稳定性
这种 ID 生成方式在处理地理数据时非常实用,既满足了唯一性要求,又保留了数据的地理特征。
为什么不使用dataForm自带的索引呢?
简单说:原来的索引不靠谱,因为它会变。
比如你今天给数据编了号(索引 0、1、2...),明天删了一行,后面的编号就会跟着变(原来的 2 可能变成 1)。这时候再用索引分训练集 / 测试集,同一个样本可能昨天在训练集,今天就跑到测试集了,结果乱了。
而用经纬度生成的 ID 不一样,只要这个房子还在那儿,经纬度就不变,ID 就不变。不管数据怎么增删改,它的编号始终固定,拆分结果才稳定。
2.2 判断数据是不是属于测试集
'''
判断一个样本是否应该被划分到测试集中
参数说明:
- id:样本的唯一标识(如ID、序号等,用于区分不同样本)
- ratio:测试集占总数据的比例(例如0.2表示20%的数据作为测试集)
- hash:哈希函数(默认可用MD5等),用于将标识转换为随机序列
返回值:布尔值(True表示属于测试集,False表示不属于)
'''
def judge_test(id, ratio, hash):
# 将输入的样本标识转换为64位整数
# 原因:标识可能是字符串、数字等不同类型,统一转为整数便于后续哈希计算
id_int = np.int64(id)
# 使用指定的哈希函数对整数标识进行计算,得到哈希对象
# 哈希的作用:把原始标识转换成一串看似随机的字符,相同标识会得到相同结果
# 这样既保留了标识的唯一性,又能产生均匀分布的数值
hash_obj = hash(id_int)
# 将哈希对象转换为二进制字节串(由0和1组成的计算机可直接处理的序列)
# 例如MD5哈希会生成16个字节的二进制数据
hash_bytes = hash_obj.digest()
# 取二进制字节串的最后一个字节(8位二进制数)
# 一个字节的取值范围是0~255(因为8位二进制最大是2^8-1=255)
# 这一步是为了从哈希结果中提取一个0~255的随机数
last_byte = hash_bytes[-1]
# 核心判断逻辑:
# 1. 计算阈值:256乘以测试集比例(因为上一步得到的是0~255的数字,共256种可能)
# 2. 如果提取的随机数(last_byte)小于这个阈值,就判定为测试集样本
# 效果:长期来看,会有约ratio比例的样本被选入测试集,且划分结果稳定
return last_byte < 256 * ratio
通过哈希函数把样本 ID 转换成 0~255 之间的随机数,然后根据设定的测试集比例(ratio)设置一个阈值,最后通过比较随机数和阈值来决定样本是否属于测试集。这样既能保证每次运行的划分结果一致(相同 ID 始终在同一集合),又能让测试集比例接近设定值。
2.3 将数据划分为测试集和训练集
"""
功能:按照指定比例,基于样本唯一ID将数据集拆分为训练集和测试集
参数说明:
- data:待拆分的完整数据集(pandas DataFrame格式)
- ratio:测试集占总数据的比例(例如0.2表示20%数据作为测试集)
- id_column:数据集中存储样本唯一标识的列名(字符串)
- hash:哈希函数,默认为md5,用于生成ID的哈希值
返回值:两个DataFrame,分别是训练集和测试集
"""
def split_test_by_id(data, ratio, id_column, hash=hashlib.md5):
# 1. 从数据集中提取所有样本的唯一标识(ID)
# 例如:如果id_column是'用户编号',ids就是所有样本的'用户编号'列数据
ids = data[id_column]
# 2. 对每个ID判断是否属于测试集,生成布尔值序列
# apply()会遍历ids中的每个ID,执行lambda函数
# lambda函数调用前面定义的judge_test()函数,传入当前ID、比例和哈希函数
# 结果in_test_set是一个Series,每个位置对应一个样本:
# True表示该样本属于测试集,False表示不属于
in_test_set = ids.apply(lambda id_: judge_test(id_, ratio, hash))
# 3. 根据判断结果拆分数据集并返回
# ~in_test_set表示取反(False变为True),筛选出训练集样本
# in_test_set直接筛选出测试集样本
# 返回格式:(训练集数据, 测试集数据)
return data.loc[~in_test_set], data.loc[in_test_set]
这个函数通过样本的唯一 ID 来划分数据集,确保同一份数据多次运行拆分时,同一个 ID 的样本始终在同一个集合(训练集或测试集)中,解决了随机拆分时结果不稳定的问题。
拆分流程:
- 提取所有样本的唯一标识
- 逐个判断每个 ID 是否属于测试集(使用之前定义的 judge_test 函数)
- 根据判断结果分别筛选出训练集(非测试集样本)和测试集
这种方法特别适合需要多次运行模型、或数据会持续更新的场景(新数据加入时,老数据的划分结果保持不变)。
2.4 按照0.2的比例拆分训练集和测试集
# 按照8:2的比例拆分训练集和测试集,使用'id'列作为标识符
train, test = split_test_by_id(housing_with_id, 0.2, 'id')
2.5 分层抽样
为啥要分层抽样?
简单说:分层抽样就是让训练集和测试集 “长得差不多”。比如你要训练一个模型识别猫和狗,原始数据里 70% 是猫、30% 是狗。如果随便抽(随机抽样),可能抽出来的测试集里全是猫,那模型哪怕不会识别狗,测试结果也会很好看,但实际用的时候就掉链子了。分层抽样就会强制要求:训练集和测试集里,也得是 70% 猫、30% 狗。这样测出来的效果才靠谱,能真实反映模型好坏。
核心就是:别让抽样时的运气影响对模型的判断。
2.5.1 绘制收入类别直方图
这里根据收入中位数来分层抽样。
# 导入必要的库(假设已导入,如numpy和matplotlib)
# import numpy as np
# import matplotlib.pyplot as plt
# 第一步:将连续的收入数据转换为离散的类别
# housing['median_income']是原始的连续收入数据
# 除以1.5后向上取整(np.ceil),把收入分成若干个区间
# 例如:3.0/1.5=2.0→ceil后为2;4.0/1.5≈2.67→ceil后为3
housing['income_cat'] = np.ceil(housing['median_income'] / 1.5)
# 第二步:处理高收入类别,将大于等于5的类别统一归为5
# where(条件, 满足条件时的值, 不满足条件时的值)
# 这里表示:如果income_cat < 5,保持原值;否则设为5.0
# 目的是避免高收入类别数量过少,便于后续分层抽样
housing['income_cat'] = housing['income_cat'].where(housing['income_cat'] < 5, 5.0)
# 第三步:提取收入类别特征,准备绘制直方图
attribute = housing['income_cat']
# 绘制收入类别的直方图
attribute.hist(
bins=30, # 分箱数量(柱子数量)
figsize=(10, 5) # 图表尺寸(宽10,高5)
)
# 添加坐标轴标签
plt.xlabel('category') # x轴:收入类别(1,2,3,4,5)
plt.ylabel('income') # y轴:收入
# 显示直方图
plt.show()
2.5.2 分层抽样
# 从scikit-learn库导入分层抽样工具
from sklearn.model_selection import StratifiedShuffleSplit
'''
创建分层抽样器实例
StratifiedShuffleSplit的作用:按指定类别(分层依据)进行抽样,
确保训练集和测试集中各分类的比例与原始数据一致,避免抽样偏差
'''
sample = StratifiedShuffleSplit(
n_splits=1, # 只进行1次数据分割(生成1组训练集和测试集)
test_size=0.2, # 测试集占总数据的20%,训练集占80%
random_state=42 # 随机数种子,保证每次运行得到相同的分割结果(结果可复现)
)
'''
使用抽样器分割数据
split()方法接收两个参数:
1. 第一个参数housing:要分割的完整数据集
2. 第二个参数housing['income_cat']:分层的依据(按收入类别进行分层)
返回的是一个生成器,包含训练集和测试集的索引
'''
# 遍历生成器(这里只进行1次分割,所以循环只执行1次)
for train_index, test_index in sample.split(housing, housing['income_cat']):
# 根据训练集索引从原始数据中提取训练集样本
train = housing.loc[train_index]
# 根据测试集索引从原始数据中提取测试集样本
test = housing.loc[test_index]
'''
验证分层抽样效果:检查原始数据中各收入类别的比例
value_counts():统计每个收入类别的样本数量
除以总样本数len(housing),得到每个类别的占比
通过对比训练集和测试集的同类比例,可确认抽样是否保持了原始分布
'''
print(housing['income_cat'].value_counts() / len(housing))
基于 ID 的哈希拆分(
split_test_by_id
)和分层抽样(StratifiedShuffleSplit
)哪个更好?这两种方法不是替代关系,而是适用于不同场景的数据集拆分方式,需要根据你的需求选择 —— 它们的核心目标不同:
1. 基于 ID 的哈希拆分(
split_test_by_id
):核心是 “稳定性”
- 适用场景:数据会持续更新(比如定期加入新样本)。
例如:今天有 1000 条数据,拆分后训练集 800、测试集 200;下周新增 200 条数据,此时老的 1000 条数据的划分结果不变(原来在训练集的仍在训练集),只需对新增的 200 条按同样规则拆分。- 优势:同一样本的 ID 固定,无论数据怎么更新,它的 “训练 / 测试” 身份不变,避免老样本在集合间 “跳槽”。
- 缺点:不保证训练集和测试集的特征分布完全一致(比如可能碰巧测试集里高收入样本偏多)。
2. 分层抽样(
StratifiedShuffleSplit
):核心是 “分布一致性”
- 适用场景:数据是静态的(一次性拆分,后续不频繁加新数据),且存在重要的类别特征(如收入类别、标签类别)。
例如:原始数据中收入类别 1-5 的比例是 [30%, 25%, 20%, 15%, 10%],分层抽样后,训练集和测试集的比例会几乎一致,避免抽样偏差。- 优势:确保训练集和测试集 “长得像”,模型评估结果更可靠(不会因为测试集分布特殊而误判模型好坏)。
- 缺点:如果数据更新,新增样本需要重新整体分层,可能导致老样本的 “训练 / 测试” 身份变化(不稳定)。
结论:是否需要保留?
- 如果你的数据是静态的(比如一次性分析,不需要后续加新样本),且需要保证特征分布一致,那么用分层抽样更合适,此时基于 ID 的方法可以不用。
- 如果你的数据会持续更新,且需要老样本的划分结果稳定,那么基于 ID 的方法是必要的(甚至可以结合两者:先按类别分层,再在每层内用 ID 哈希拆分,既保证分布又保证稳定)。
简单说:前者是 “让样本身份不变”,后者是 “让两个集合长得像”,根据你的数据是否会更新、是否在意分布一致性来选,不是非此即彼的关系。
三、从数据探索和可视化中获得洞见
3.1 将地理数据可视化
# 遍历训练集(train)和测试集(test)两个数据集
# 元组 (train, test) 包含了需要处理的两个数据集
# 循环中,item 会依次代表 train 和 test
for item in (train, test):
# 从数据集中删除名为 'income_cat' 的列
# columns=['income_cat'] 指定要删除的列名
# inplace=True 表示直接在原数据集上进行修改,不创建新的副本,节省内存
item.drop(columns=['income_cat'], inplace=True)
# 从训练集中复制一份数据到 housing 变量,用于后续可视化操作
# 这样做不会影响原始的 train 数据集
housing = train.copy()
# 使用 pandas 的 plot 方法绘制散点图
# kind='scatter' 表示绘制散点图
# x='longitude' 指定 x 轴数据为 'longitude'(经度)列
# y='latitude' 指定 y 轴数据为 'latitude'(纬度)列
# 该散点图用于展示数据中所有样本的地理位置分布(经纬度对应关系)
housing.plot(kind='scatter', x='longitude', y='latitude')
# 显示绘制好的散点图
plt.show()
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import StratifiedShuffleSplit
def load_housing_data(file_path='D:\\deskTop\\机器学习实战\\第二章\\housing.csv'):
csv_data = pd.read_csv(file_path)
return csv_data
housing = load_housing_data()
housing['income_cat'] = np.ceil(housing['median_income'] / 1.5)
housing['income_cat'] = housing['income_cat'].where(housing['income_cat'] < 5, 5.0)
sample = StratifiedShuffleSplit(
n_splits=1,
test_size=0.2,
random_state=42
)
train=[]
test=[]
for train_index, test_index in sample.split(housing, housing['income_cat']):
train = housing.loc[train_index]
test = housing.loc[test_index]
housing =train.copy()
# 绘制散点图,融合地理位置、人口和房价三重信息
housing.plot(
kind='scatter', # 指定图表类型为散点图
x='longitude', # x轴为经度,代表水平地理坐标
y='latitude', # y轴为纬度,代表垂直地理坐标
alpha=0.4, # 点的透明度(0.4适中,既避免过度重叠又能显示密度)
# s 参数:控制点的大小(size)。点的大小与人口数量成正比(除以100是为了缩放大小,避免点过大)
s=housing['population'] / 100,
label='population', # 为人口规模这个维度添加标签(用于图例)
# c 参数:控制点的颜色(color)。点的颜色由房价中位数决定(核心维度:用颜色区分房价高低)
c='median_house_value',
cmap=plt.get_cmap('jet'), # 使用'jet'颜色映射(从蓝色到红色,代表数值从低到高)
colorbar=True # 显示颜色条(用于解释颜色对应的房价数值)
)
plt.show()
靠近海岸的地方人口的密度越大,房价也越高。
3.2 寻找相关性
# 1. 识别数据集中的非数值列
# select_dtypes(exclude=['number']) 会筛选出所有不是数值类型的列
# .columns 获取这些非数值列的列名,存储在 non_numeric_cols 变量中
non_numeric_cols = housing.select_dtypes(exclude=['number']).columns
# 2. 对非数值列进行独热编码处理
# pd.get_dummies() 是 pandas 中处理分类变量的常用函数
# 参数说明:
# - housing: 原始数据集
# - columns=non_numeric_cols: 指定需要进行独热编码的列
# 独热编码会将每个分类值转换为一个新的二进制列(0或1),避免了类别间的虚假数值关系
housing_encoded = pd.get_dummies(housing, columns=non_numeric_cols)
# 3. 计算处理后数据集的相关系数矩阵
# corr() 方法默认计算各列之间的皮尔逊相关系数,范围在[-1, 1]之间
# 仅适用于数值型数据,因此需要先处理非数值列
corr_matrix = housing_encoded.corr()
# 4. 查看与目标变量的相关性,并按降序排列
# 'median_house_value' 是我们关注的目标变量(房价中位数)
# sort_values(ascending=False) 按相关性从高到低排序,便于观察哪些因素与房价相关性最强
print(corr_matrix['median_house_value'].sort_values(ascending=False))
补充说明:
- 独热编码的意义:对于像 "ocean_proximity" 这样的分类变量(包含 "INLAND"、"NEAR OCEAN" 等取值),直接转换为数字可能会引入错误的顺序关系(比如 1<2<3),而独热编码通过创建多个二进制列避免了这个问题。
- 相关系数解读:结果中数值越接近 1,表示与房价正相关性越强;越接近 - 1,表示负相关性越强;接近 0 则表示相关性较弱。
说明最有潜力预测房价中位数的是收入(median_income)。
3.2.1绘制收入中位数和收入关系的散点图
# 使用pandas的plot方法绘制散点图
# kind='scatter' 指定绘制散点图,用于观察两个变量之间的关系
# x='median_income' 指定x轴数据为收入中位数列
# y='median_house_value' 指定y轴数据为房价中位数列(目标变量)
# alpha=0.1 设置点的透明度(0-1之间),值越小越透明
# 透明度设置在数据点密集时非常有用,能更清晰地看到数据分布的密度
housing.plot(kind='scatter', x='median_income', y='median_house_value', alpha=0.1)
# 显示绘制的图形
# 必须调用plt.show()才能在屏幕上显示图像
plt.show()
3.3机器学习算法的数据准备
3.3.1将特征数据和标签数据分开
# 从训练数据里,去掉"房价中位数"这一列
# 剩下的都是各种特征(比如地段、收入、房龄这些),给模型当"线索"用
housing = train.drop('median_house_value', axis=1)
# 专门把"房价中位数"这一列拿出来,单独存好
# 这就是模型要学的"答案",后面用它来判断模型猜得对不对
housing_labels = train['median_house_value'].copy()
简单来说:
housing
是 "特征集",包含了用于预测的所有输入变量housing_labels
是 "标签集",包含了我们要预测的目标值在机器学习中,模型训练的过程就是学习特征 (
housing
) 和标签 (housing_labels
) 之间的映射关系,以便未来能根据新的特征预测出对应的标签。
3.3.2 数据清理
total_bedrooms有部分值是缺失的,需要数据清理。大部分的机器学习算法 无法在缺失特征值的数据上工作。有如下三个选择:
放弃这些相应的地区、放弃这个属性、将缺失的数据设置为某个值(0、平均数、中位数等)通过
# 处理数据中'total_bedrooms'列的缺失值,以下是三种常见方法:
# 方法1:删除包含'total_bedrooms'缺失值的行
# subset=['total_bedrooms'] 表示只看这一列的缺失值
# 效果:直接把有缺失的样本(地区)丢掉,简单粗暴但可能损失有用数据
housing.dropna(subset=['total_bedrooms']) # 放弃相应的地区
# 方法2:删除'total_bedrooms'这一列
# axis=1 表示操作列(而不是行)
# 效果:既然这一列缺值多,干脆不用它了,适合该特征不重要的情况
housing.drop('total_bedrooms', axis=1) # 放弃这个属性
# 方法3:用中位数填充缺失值(最常用)
# 先算出'total_bedrooms'列的中位数(中间值,受极端值影响小)
median = housing['total_bedrooms'].median()
# 用上面算的中位数填补该列所有缺失的位置
# inplace=True 表示直接在原数据上修改,不用新建变量
housing['total_bedrooms'].fillna(median, inplace=True) # 使用中位数填充缺失值
scikit-learn提供了一个imputer来处理缺失值。
# 造一个"填坑工具",用中位数填那些空值
imputer = SimpleImputer(strategy='median')
# 把文字类的列(比如'ocean_proximity'里的"内陆"之类)去掉
# 因为这个工具只认数字,不认文字
housing_num = housing.drop('ocean_proximity', axis=1)
# 让工具先看看所有数字列,算出每列的中位数记下来
imputer.fit(housing_num)
# 打印工具记下来的每列中位数(以后就用这些数填对应列的空)
print(imputer.statistics_)
# 用工具把所有空值都填上刚才记的中位数
# 出来的X是填好后的数字表格(格式是数组)
X = imputer.transform(housing_num)
# 把填好的数组转回表格形式,列名还跟原来一样
# 现在housing_tr就是没有空值的数字表格了
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
print(housing_tr)
3.3.3 处理文本和分类属性
在之前是直接将ocean_proximity列删除,因为这是个文本属性,机器学习擅长的是和数字打交道。所以。还需要将这些文本类的数据转为数字类型的数据。scikit-learn提供了一个转换器LabelEncoder。
from sklearn.preprocessing import LabelEncoder
....
# 造一个"文字转数字"的工具(标签编码器)
encoder = LabelEncoder()
# 取出要处理的文字列(比如'ocean_proximity'里存着"近海"、"内陆"这些描述)
housing_cat = housing['ocean_proximity']
# 让工具先"认识"这些文字,然后把它们转成数字(比如"内陆"→0,"近海"→1)
housing_cat_encoded = encoder.fit_transform(housing_cat)
# 打印转换后的数字结果,方便查看文字变成了哪些数字
print(housing_cat_encoded)
# 打印编码器记住的所有原始类别(文字标签)
# 顺序和之前转换的数字一一对应(比如第一个类别对应0,第二个对应1,以此类推)
print(encoder.classes_)
简单说就是:把像 "内陆"、"近海" 这样的文字标签,转换成机器能看懂的数字(比如 0、1、2),方便后续计算。
LabelEncoder 这种将文字类别直接转成数字(如 0、1、2、3)的方法,主要缺点是会让机器误以为类别之间有大小或顺序关系,但实际上它们可能并没有这种关系。
比如对于 'ocean_proximity' 列的分类:
- 假设转换后是:'INLAND'→0,'NEAR BAY'→1,'NEAR OCEAN'→2
- 机器可能会错误地认为:0 < 1 < 2,就像 "内陆" < "近海湾" < "近海洋"
- 但实际上这三个类别只是不同的位置描述,没有这种大小或顺序关系
这种错误的 "顺序暗示" 可能会影响模型的判断,尤其是对于线性回归、SVM 这类对数值大小敏感的算法,可能会让模型学到错误的规律。
所以:
- 处理目标变量(标签) 时可以用 LabelEncoder(因为标签本身可能确实有顺序,比如评分 1-5 星)
- 处理特征列的分类变量时,更推荐用独热编码(OneHotEncoder),它会给每个类别单独建一列(0 或 1),避免这种顺序误解。
使用OneHotEncoder转换器。
# 导入需要的工具:LabelEncoder用于将文字转为数字,OneHotEncoder用于独热编码
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
# 创建一个标签编码器对象,这个工具能把文字转换成数字
encoder = LabelEncoder()
# 假设我们有一个数据集housing,其中有一列叫'ocean_proximity'
# 这一列存的是文字描述,比如"近海"、"内陆"、"岛屿"、"离海岸很远"等
# 这里我们取出这一列数据来处理
housing_cat = housing['ocean_proximity']
# 让编码器先"学习"这些文字类别(fit),然后转换成数字(transform)
# 比如可能会把"内陆"→0,"近海"→1,"岛屿"→2,"离海岸很远"→3
# 这样机器学习算法就能理解这些类别了
housing_cat_encoded = encoder.fit_transform(housing_cat)
# 创建一个独热编码器对象
# 为什么需要这个?因为LabelEncoder转换的数字可能会让算法误以为它们有大小关系
# 比如0 < 1 < 2,但实际上"内陆"和"近海"之间没有这种大小关系
encoder = OneHotEncoder()
# 独热编码要求输入是二维数组,所以我们用reshape(-1, 1)把数据转换成列形式
# 然后进行独热编码:为每个类别创建一个单独的列
# 比如"内陆"可能变成[1,0,0,0],"近海"变成[0,1,0,0],以此类推
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1, 1))
# 打印独热编码的结果(转换成普通数组形式)
# 这样我们就能直观地看到每个文字类别被转换成了什么样的数字形式
print(housing_cat_1hot.toarray())
简单来说,这段代码做了两件事:
- 先用 LabelEncoder 把文字标签转换成数字(比如 "近海"→1)
- 再用 OneHotEncoder 把这些数字转换成独热编码形式,避免机器学习算法误解数字之间的大小关系
这种处理方式在机器学习中非常常见,因为大多数算法只能处理数字,而不能直接理解文字类别。
使用LabelBinarizer类,该类能直接完成对文本转为整数并从整数类别转为独热向量。
# 从数据表housing中取出名为'ocean_proximity'的列
# 这一列存的是文字描述,比如"近海"、"内陆"、"岛屿"等类别信息
housing_cat = housing['ocean_proximity']
# 创建一个LabelBinarizer编码器对象
# 这个工具的作用:直接把文字类别转换成计算机能理解的数字形式(独热编码)
encoder = LabelBinarizer()
# 让编码器完成两件事:
# 1. 学习(fit)housing_cat中的所有文字类别(比如记住有"近海"、"内陆"这些类别)
# 2. 转换(transform):把每个文字转换成对应的独热编码
# 例如"内陆"可能变成[0,1,0,0],"近海"变成[0,0,1,0]
housing_cat_1hot = encoder.fit_transform(housing_cat)
# 打印转换后的结果,看看文字都变成了什么样的数字
print(housing_cat_1hot)
简单解释:
假设
housing['ocean_proximity']
里的内容是:["近海", "内陆", "岛屿", "近海", "离海岸很远"]
运行代码后,输出可能是这样的数字矩阵:
[[0 0 1 0] # 对应"近海" [0 1 0 0] # 对应"内陆" [1 0 0 0] # 对应"岛屿" [0 0 1 0] # 对应"近海" [0 0 0 1]] # 对应"离海岸很远"
每一行代表一个原始文字,每一列代表一个类别,1 所在的位置就是这个文字对应的类别。这样转换后,机器学习算法就能理解这些文字信息了。