《Hands-On Machine Learning》学习笔记-2.3 获取数据

端到端机器学习项目

获取数据

下载数据

可以直接使用浏览器下载数据文件,然后解压出其中的CSV文件,但是更好的办法是写一个函数来实现它,特别是当数据会变化的时候,使用函数的形式能够随时随地获取最新的数据。

import pdb
# pdb.set_trace()
import os
import tarfile
from six.moves import urllib


DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = "datasets/housing"
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/housing.tgz"
HOUSING_LOCAL_PATH = r"E:\Hands-On ML data"

def fetch_housing_data(housing_url = HOUSING_URL, housing_path = HOUSING_LOCAL_PATH):
    if not os.path.isdir(housing_path):
        os.mkdirs(housing_path)
    
    tgz_path = os.path.join(housing_path, "housing.tgz")
#     从网络地址获取tgz文件
    urllib.request.urlretrieve(housing_url, tgz_path)
    #打开tgz文件
    housing_tgz = tarfile.open(tgz_path)
    #解压tgz
    housing_tgz.extractall(path=housing_path)
    #关闭tgz
    housing_tgz.close()
fetch_housing_data()

调用fetch_housing_data()函数,就会从网络上下载housing.tgz并解压其中的housing.csv
使用Pandas库来加载数据

import pandas as pd

def load_housing_data(housing_path = HOUSING_LOCAL_PATH):
    csv_path = os.path.join(housing_path, "housing.csv")
    return pd.read_csv(csv_path)

该函数调用pandas库的read_csv()函数读取csv文件,并返回一个包含csv文件中所有数据的Pandas DataFrame对象。

快速浏览一下数据的结构

通常加载完数据之后需要先打印一些数据的内容和属性,一方面验证数据是否加载正确,另一方面先对数据有一个直观的印象。
调用DataFrame的head()函数打印前5行

housing = load_housing_data()
housing.head()
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value ocean_proximity
0 -122.23 37.88 41.0 880.0 129.0 322.0 126.0 8.3252 452600.0 NEAR BAY
1 -122.22 37.86 21.0 7099.0 1106.0 2401.0 1138.0 8.3014 358500.0 NEAR BAY
2 -122.24 37.85 52.0 1467.0 190.0 496.0 177.0 7.2574 352100.0 NEAR BAY
3 -122.25 37.85 52.0 1274.0 235.0 558.0 219.0 5.6431 341300.0 NEAR BAY
4 -122.25 37.85 52.0 1627.0 280.0 565.0 259.0 3.8462 342200.0 NEAR BAY

每一行代表一个街区的数据。如上所示,每个样本有10个属性。
info()函数能够帮助我们快速了解数据的基本情况,包括一共有多少行(即多少个样本),每个属性的数据类型以及非空值的数量

housing.info()

RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
longitude             20640 non-null float64
latitude              20640 non-null float64
housing_median_age    20640 non-null float64
total_rooms           20640 non-null float64
total_bedrooms        20433 non-null float64
population            20640 non-null float64
households            20640 non-null float64
median_income         20640 non-null float64
median_house_value    20640 non-null float64
ocean_proximity       20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB

可以看到数据集中一共有20640组数据,即20640个样本。对于ML而言的确有点小,但是却是个很好的入门的数据。要注意到,total_bedrooms属性只有20433个非空值,这也就意味着有207个样本中缺失了该部分数据,要引起注意。
所有的属性都是数值类型的,除了ocean_proximity,这个属性的类型是个对象,它可能是任何的Python对象,但是由于数据是存放在csv文件中的,可以推断这个属性应该是文本对象。通过之前的head()方法查看数据的前5行,可以看到该属性的值是重复的,这说明这个属性值很可能会是个分类属性。可以通过value_counts()方法查看一共有多少个类,每一类又有多少个实例。

housing['ocean_proximity'].value_counts()
<1H OCEAN     9136
INLAND        6551
NEAR OCEAN    2658
NEAR BAY      2290
ISLAND           5
Name: ocean_proximity, dtype: int64

使用describe()方法可以看到数值类型属性的概要。

housing.describe()
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value
count 20640.000000 20640.000000 20640.000000 20640.000000 20433.000000 20640.000000 20640.000000 20640.000000 20640.000000
mean -119.569704 35.631861 28.639486 2635.763081 537.870553 1425.476744 499.539680 3.870671 206855.816909
std 2.003532 2.135952 12.585558 2181.615252 421.385070 1132.462122 382.329753 1.899822 115395.615874
min -124.350000 32.540000 1.000000 2.000000 1.000000 3.000000 1.000000 0.499900 14999.000000
25% -121.800000 33.930000 18.000000 1447.750000 296.000000 787.000000 280.000000 2.563400 119600.000000
50% -118.490000 34.260000 29.000000 2127.000000 435.000000 1166.000000 409.000000 3.534800 179700.000000
75% -118.010000 37.710000 37.000000 3148.000000 647.000000 1725.000000 605.000000 4.743250 264725.000000
max -114.310000 41.950000 52.000000 39320.000000 6445.000000 35682.000000 6082.000000 15.000100 500001.000000

count,mean,max和min的含义不言而喻。注意忽略了空值,因此total_bedrooms的counts值为20433,而不是20460。std行为标准差,25%,50%和75%行为相应的百分位数。例如25%的街区的housing_median_age值小于18,50%的街区的housing_median_age值小于29,75%的街区小于37。
另一个快速浏览数据的方式是画出数值型属性的直方图。可以一个属性一个属性的画,也可以在整个数据集上调用hist()方法,这样就会一次性画出所有数值型属性的直方图。

%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()

《Hands-On Machine Learning》学习笔记-2.3 获取数据_第1张图片

hist()方法依赖于Matplotlib。在使用Matplotlib画图之前,需要指明它在哪个终端上输出。使用*%matplotlib inline* 声明Matplotlib在Jupyter notebook的终端上输出。此时,plot.show()就是可选的了,因为Jupyter在执行cell的时候会自动绘图

从直方图中可以看出:

  1. median income属性看起来不像是使用美元作为单位的。经过确认得知该数据经过了缩放,且上限设置为15,下限设置为0.5。在机器学习中,使用经过处理后的属性是很常见的,通常没啥问题,但是我们需要知晓这个事情
  2. housing median age和median house value这两个属性同样做了限幅。这就有点问题了,因为median house value属性就是我们要预测的值,使用限幅后的值进行训练,机器学习算法就会认为该值永远不会超出所设置的限幅。这就需要跟下游系统的人员进行交流,确认这是否是个问题。如果他们说他们需要精确的数值,即使超出了所设置的限幅,那么通常有两个做法:
  • 将限幅了的值恢复为原始值
  • 将限幅了的实例从训练集和测试集中剔除。
  1. 这些属性值的尺度(比例)各不相同。后续章节将介绍特征尺度
  2. 大部分的直方图都tail heavy(即值较多的分布在两侧,直方图的形状是两边高),这对机器学习算法而言不太友好,需要对其进行转换,使其分布图形bell-shaped(即像正态分布那样,中间高,两边低)

创建一个测试集

在更进一步的学习数据集之前,现在就需要创建一个测试集,然后丢到一边再也不看它了。为啥要这么做呢?因为如果人们不停的看到测试集的数据,就会不由自主的依照测试集数据的样子去挑选模型,这样训练出来的模型也许在测试集上表现的很好,但是却无法保证其泛化的效果,这就是数据透视偏差。说白了就是要保证测试集的**“独立性”、“神秘性”** 。
创建测试集也不能简单的随机选取20%的数据作为测试集数据,因为这样的话,每次运行程序得到的测试集的数据都不一样,久而久之,机器学习的算法就能遍历到数据集中的所有数据,这显然与设立训练集和测试集的初衷是不符的。可以通过固定随机种子的方式保证每次运行程序时,测试集的内容都是相同的,但同样有缺陷,即当整个数据集发生更新的时候,通过这种方式得到的测试集数据有可能会包含原来的训练集数据中的内容。我们希望的情况是当数据集发生变更的时候,测试集随之更新,但不能包含原来训练集的数据
可以使用hash映射的方式,将数据集的某个不变的且唯一的属性(或者构造一个这样的临时属性)hash映射为[0,255]的值,然后选取hash值小于51(即256的20%)的样本作为测试集的数据。
书中先是介绍了使用数据的行号作为种子进行hash映射,但是使用这种方式就要求新加入的数据必须加在尾部,不能插入已有数据的中间,而且原来的数据不能删除(保证属性不变),所以又提出了将数据中的经度和维度值拼成一个新的属性,作为hash映射的输入。每个街区的经度和维度不可能都相同,而且街区的经度和维度值是固定的,满足hash映射的要求。这个方法是可行的。
但是说了半天,其实可以直接调用Scikit-Learn库中的train_test_split()函数来搞定。

from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

完全随机的选择测试集有个缺陷就是无法保证测试集数据的代表性。举个例子,美国的人口比例大约为51.3%的女性,48.7%的男性,如果一个调查公司要做一个1000个样本的调查,那么它最好选择513个女性和487个男性,以使得样本数据的结构与真实数据一致。这就是所谓分层抽样
假设现在我们得知median income这个属性对预测median housing price至关重要,那么我们就希望测试集数据的median income属性能真实的反应整个data set中所有median income 的分布情况。通过对median income直方图的观察, 我们发现,大部分的值都落在2-5的区间内,但是也有些值落在6的右边。为了保证测试集的数据具有代表性,可以将median income这个属性分级,使得分出来的级别的个数不是那么多,且每级中的数据个数足够多的。通过将media income属性的值除以1.5,然后使用ceil方法向上取整将其划分级别,并将所有大于5的级别归到5级(即限幅至5)

import numpy as np
#添加一个income_cat属性,其值是将median_income的值除以1.5,然后向上取整
housing['income_cat'] = np.ceil(housing['median_income']/1.5) 
#将income_cat的值大于5的,限幅至5
#housing是pandas的dataFrame类型的变量,pandas的where函数的用法是
#DataFrame.where(cond, other=nan, inplace=False, axis=None, level=None, try_cast=False, raise_on_error=True)
#cond为真的话,保留原值,为假的话,替换为other的值,inplace为True表示在原数据上操作
#下边的代码的意思就是housing['income_cat']的值如果小于5,保留原值,大于5则置为5,
housing['income_cat'].where(housing['income_cat']<5, 5.0, inplace=True)
housing['income_cat'].hist()

《Hands-On Machine Learning》学习笔记-2.3 获取数据_第2张图片
然后使用Scikit-Learn库的StratifiedShuffleSplit类来实现分层采样。具体代码如下:

from sklearn.model_selection import StratifiedShuffleSplit

ss = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in ss.split(housing, housing['income_cat']):
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

StratifiedShuffleSplit的官方文档
n_splits:划分的次数
test_size:0.2表示测试集所占比例为整个数据集的20%
random_state:可以理解为随机数种子
首先调用StratifiedShuffleSplit方法生成一个对象,命名为ss,然后调用该对象的split方法返回训练集数据和测试集数据在原数据集中的索引
split方法中,第一个参数代表整个数据集,第二个参数即代表分层采样所依据的属性,即训练集和测试集中该项数据的分布与整个数据集中该项数据的分布保持一致

检查一下程序运行的结果。

housing['income_cat'].value_counts()/len(housing)
3.0    0.350581
2.0    0.318847
4.0    0.176308
5.0    0.114438
1.0    0.039826
Name: income_cat, dtype: float64
strat_test_set['income_cat'].value_counts()/len(strat_test_set)
3.0    0.350533
2.0    0.318798
4.0    0.176357
5.0    0.114583
1.0    0.039729
Name: income_cat, dtype: float64
strat_train_set['income_cat'].value_counts()/len(strat_train_set)
3.0    0.350594
2.0    0.318859
4.0    0.176296
5.0    0.114402
1.0    0.039850
Name: income_cat, dtype: float64

通过上边的代码可以看到,在整个数据集housing、测试集strat_test_set和训练集strat_train_set上,各个income_cat所占的比例保持一致。这也保证了测试集数据的代表性。

income_cat属性是我们造出来用来进行测试集和训练集的划分的,对实际机器学习算法的训练没有用,现在训练集和测试集拆分完成,可以把这个属性去掉了。

for set in (strat_train_set, strat_test_set):
    set.drop(['income_cat'], axis=1, inplace=True)

花了这么长的篇幅将测试集的划分是因为这个工作虽然经常被忽略,但是却是机器学习项目的重要部分,而且这里头所体现的思想在后续章节介绍交叉验证的时候很有用。

你可能感兴趣的:(Hands-On,Machine,Learning学习笔记,机器学习)