可以直接使用浏览器下载数据文件,然后解压出其中的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()
hist()方法依赖于Matplotlib。在使用Matplotlib画图之前,需要指明它在哪个终端上输出。使用*%matplotlib inline* 声明Matplotlib在Jupyter notebook的终端上输出。此时,plot.show()就是可选的了,因为Jupyter在执行cell的时候会自动绘图
从直方图中可以看出:
在更进一步的学习数据集之前,现在就需要创建一个测试集,然后丢到一边再也不看它了。为啥要这么做呢?因为如果人们不停的看到测试集的数据,就会不由自主的依照测试集数据的样子去挑选模型,这样训练出来的模型也许在测试集上表现的很好,但是却无法保证其泛化的效果,这就是数据透视偏差。说白了就是要保证测试集的**“独立性”、“神秘性”** 。
创建测试集也不能简单的随机选取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()
然后使用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)
花了这么长的篇幅将测试集的划分是因为这个工作虽然经常被忽略,但是却是机器学习项目的重要部分,而且这里头所体现的思想在后续章节介绍交叉验证的时候很有用。