精品系列机器学习实战完整版区域房价中位数预测(挑战全网最全,没有之一,另附完整代码)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精品系列机器学习实战完整版区域房价中位数预测(挑战全网最全,没有之一,另附完整代码)相关的知识,希望对你有一定的参考价值。

参照《机器学习实战》第二版

1、下载数据

import os
import tarfile
import urllib.request

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"

def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()
fetch_housing_data()

2、读取下载的数据

import pandas as pd

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

2.1、数据显示

housing = load_housing_data()
housing.head()  # 默认显示前五列
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomemedian_house_valueocean_proximity
0-122.2337.8841.0880.0129.0322.0126.08.3252452600.0NEAR BAY
1-122.2237.8621.07099.01106.02401.01138.08.3014358500.0NEAR BAY
2-122.2437.8552.01467.0190.0496.0177.07.2574352100.0NEAR BAY
3-122.2537.8552.01274.0235.0558.0219.05.6431341300.0NEAR BAY
4-122.2537.8552.01627.0280.0565.0259.03.8462342200.0NEAR BAY

2.2、查看每列属性

housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
 #   Column              Non-Null Count  Dtype  
---  ------              --------------  -----  
 0   longitude           20640 non-null  float64
 1   latitude            20640 non-null  float64
 2   housing_median_age  20640 non-null  float64
 3   total_rooms         20640 non-null  float64
 4   total_bedrooms      20433 non-null  float64
 5   population          20640 non-null  float64
 6   households          20640 non-null  float64
 7   median_income       20640 non-null  float64
 8   median_house_value  20640 non-null  float64
 9   ocean_proximity     20640 non-null  object 
dtypes: float64(9), object(1)
memory usage: 1.6+ MB

2.3、查看某列数值统计

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

2.4、查看数值列属性摘要

housing.describe()
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomemedian_house_value
count20640.00000020640.00000020640.00000020640.00000020433.00000020640.00000020640.00000020640.00000020640.000000
mean-119.56970435.63186128.6394862635.763081537.8705531425.476744499.5396803.870671206855.816909
std2.0035322.13595212.5855582181.615252421.3850701132.462122382.3297531.899822115395.615874
min-124.35000032.5400001.0000002.0000001.0000003.0000001.0000000.49990014999.000000
25%-121.80000033.93000018.0000001447.750000296.000000787.000000280.0000002.563400119600.000000
50%-118.49000034.26000029.0000002127.000000435.0000001166.000000409.0000003.534800179700.000000
75%-118.01000037.71000037.0000003148.000000647.0000001725.000000605.0000004.743250264725.000000
max-114.31000041.95000052.00000039320.0000006445.00000035682.0000006082.00000015.000100500001.000000
  1. 上面看到total_bedrooms这一列的count的数值为20433而不是20640,是因为不统计为空的单元格,所以后面需要处理为空的数据。
  2. std行:显示的是标准差,用来测量数值的离散程度,也就是方差的平方根,一般符合高斯分布
  3. 25%、50%、75%:显示相应的百分位数,表示一组观测值中给定百分比的观测值都低于该值;50% 即 中位数。

2.5、快速了解数组类型的方法(直方图)

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

3、创建测试集

理论上,创建测试集非常简单,只需要随机选择一些实例,通常是数据集的20%(如果数据量巨大,比例将更小)

  • 为了即使在更新数据集之后也有一个稳定的训练测试分割,常见的解决方案是每个实例都使用一个标识符来决定是否进入测试集(假定每个实例都一个唯一且不变的标识符)
  • 你可以计算每个实例的标识符的哈希值,如果这个哈希值小于或等于最大哈希值的20%,则将该实例放入测试集。这样可以保证测试集在多个运行里都是一致的,即便更新数据集也依然一致。新实例的20%将被放如新的测试集,而之前训练集中的实例也不会被放入新测试集。

3.1、手动随机生成

from zlib import crc32
import numpy as np


def test_set_check(identifier, test_ratio):
    return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32


def splet_train_test_by_id(data, test_ratio, id_column):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
    return data.loc[~in_test_set], data.loc[in_test_set]
  • housing 数据集没有标识符列。最简单的解决方法是使用索引作为 ID
housing_with_id = housing.reset_index()
train_set, test_set = splet_train_test_by_id(housing_with_id, 0.2, "index")

3.2、使用 Scikit-Learn 提供的方法 train_test_split 随机生成

  • 最简单的方法就是使用:train_test_split(),它与前面定义的 split_train_test() 几乎相同,除了几个额外特征。
from sklearn.model_selection import train_test_split

# random_state: 设置随机生成器种子
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
  • 到目前为止,我们考虑的是纯随机的抽样方法。如果数据集足够庞大(特别是相较于属性的数量而言),这种方法通常不错
  • 如果不是,则有可能会导致明显的抽样偏差。即 应该按照比例分层抽样。

如果你咨询专家,他们会告诉你,要预测房价中位数,收入中位数是个非常重要的属性。于是你希望确保在收入属性上,测试集能够代表整个数据集中各种不同类型的收入。

我们由上面直方图可以看到:大部分收入中位数值聚集在1.5~6左右,但也有一部分超过了6,在数据集中,每个层都要有足够数量的数据,这一点至关重要,不然数据不足的层,其重要程度佷有可能会被错估。

3.3、使用 Scikit-Learn 提供的方法 StratifiedShuffleSplit 按类别比例生成

# 用 pd.cut() 来创建 5个收入类别属性(用 1~5 来做标签),0~1.5是类别 1, 1.5~3是类别2
# np.inf 代表无穷大
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0, 1.5, 3, 4.5, 6, np.inf],
                               labels=[1, 2, 3, 4, 5])
housing["income_cat"].hist()


现在根据收入类进行分层抽样,使用 Scikit-Learn 的 StratifiedShuffleSplit

from sklearn.model_selection import StratifiedShuffleSplit

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

看看上面运行是否如我们所料

compare_pd = pd.DataFrame()
# 全部数据:按收入分类的比例
compare_pd["全部数据"] = housing["income_cat"].value_counts() / len(housing)
# 按收入分类的比例 获取测试集比例
compare_pd["分类抽样"] = strat_test_set["income_cat"].value_counts() / len(strat_test_set)
# 随机获取测试集比例
_, test_set = train_test_split(housing, test_size=0.2, random_state=42)
compare_pd["随机抽样"] = test_set["income_cat"].value_counts() / len(test_set)
compare_pd
全部数据分类抽样随机抽样
30.3505810.3505330.358527
20.3188470.3187980.324370
40.1763080.1763570.167393
50.1144380.1145830.109496
10.0398260.0397290.040213

由上面数据我们看到,随机抽样的测试集,收入类别比例分布有些偏差。

现在可以删除 income_cat 属性,将数据恢复原样了:

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

我们花了相当长的时间在测试集的生成上,理由很充分:这是及机器学习项目中经常忽视但是却至关重要的一部分。并且,当讨论到交叉验证时,这里谈到的许多想法也对其大有裨益。

4、从数据探索和可视化中获取洞见

如果训练集非常庞大,你可以抽样一个探索集,这样后面的操作更简单快捷一些,不过我们这个案例的数据集非常小,完全可以直接在整个训练集上操作。让我们先创建一个副本,这样可以随便尝试而不损害训练集:

housing = strat_train_set.copy()

4.1、将地理数据可视化

housing.plot(kind="scatter", x="longitude", y="latitude")

# alpha=0.1 可以更清楚的看出高密度数据点的位置
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

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)


现在,再看看房价。每个圆的半径大小代表了每个区域的人口数量(选项 s),颜色代表价格(选项 c)。我们使用一个名叫jet的预定义颜色表(选项 cmap)来进行可视化,颜色范围从蓝(低)到红(高)

4.2、寻找相关性

由于数据集不太大,你可以使用 corr() 方法轻松计算出没对属性之间的标准相关系数(也称皮尔逊 r

corr_matrix = housing.corr()

现在查看每个属性与房价中位数的相关性分别是多少:

corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value    1.000000
median_income         0.687160
total_rooms           0.135097
housing_median_age    0.114110
households            0.064506
total_bedrooms        0.047689
population           -0.026920
longitude            -0.047432
latitude             -0.142724
Name: median_house_value, dtype: float64

相关系数的范围从 -1 变化到 1。

  • 越接近 1,表示有越强的正相关。当收入中位数上升时,房价中位数也趋于上升

  • 越接近 -1,表示有越强的负相关。可以发现纬度和房价中位数呈现轻微的负相关,也就是说,越往北走,房价倾向于下降

  • 越接近 0,表示两者之间没有线性相关性。可以发现纬度和房价中位数呈现轻微的负相关,也就是说,越往北走,房价倾向于下降

上图可知,相关系数仅测量线性相关性,所以她有可能彻底遗漏非线性相关性。注意最下面一排图像,他们的相关系数都是0,但是显然我们可以看出横轴和纵轴之间的关系并不是完全独立的。此外前两行,需要注意的是这个相关性跟斜率完全无关

还有一种方法可以检测属性之间的相关性,也就是使用pandasscatter_matix函数,它会绘制出每个数值属性相对于其他数值属性的相关性。现在我们有11个数值属性,可以得到 11^2 = 121 个图像,这里我们只关注这些与房价中位数属性最相关的,可算作最有潜力的属性

from pandas.plotting import scatter_matrix

# 选择了相关性靠前的 4 个属性
attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))


最有潜力能预测房价中位数的属性是收入中位数,所以我们放大开看看其相关属性的散点图

housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)

  1. 上图可以明显的看到上升趋势,并且点也不是太分散。
  2. 明显可以看到几条水平线,比如:50万、45万、35万、30万以下还有几条不太明显的线。
  3. 为了避免你的算法学习之后重现这些怪异数据,可以尝试删除这些相应区域。

4.3、试验不同属性的组合(为 5.3 自定义转换器的编写做准备)

应于发现其他有意思的数据关系。

# 房屋总数 / 住户
housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
# 卧室总数 / 房屋总数
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
# 人口 / 住户
housing["population_per_household"]=housing["population"]/housing["households"]
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value          1.000000
median_income               0.687160
rooms_per_household         0.146285
total_rooms                 0.135097
housing_median_age          0.114110
households                  0.064506
total_bedrooms              0.047689
population_per_household   -0.021985
population                 -0.026920
longitude                  -0.047432
latitude                   -0.142724
bedrooms_per_room          -0.259984
Name: median_house_value, dtype: float64

这一轮的探索不一定要多么彻底,关键是迈开这一步,快速获得洞见,这将有助于你获得非常非常好的第一个原型。这也是一个不断迭代的过程:

一旦你的原型产生并且开始运行,你可以分析它的输出以获得更多洞见,然后再次回到这个探索步骤。

5、机器学习算法的数据准备

现在,终于是时候给你的机器学习算法准备数据了。这里你应该编写函数来执行,而不是手动操作,原因如下:

  • 你可以在任何数据集上轻松重现这些转换(比如,获得更新之后的数据集之后)
  • 你可以逐渐建立起一个转换函数函数库,可以在以后的项目中重用。
  • 你可以在实现系统中使用这些函数来转换新数据,在输入给算法。
  • 你可以轻松尝试多种转换方式,查看哪种转换的组合效果最佳。

现在,让我们先回到一个干净的训练集(再次复制 strat_train_set),然后将预测期标签分开,因为这里我们不一定对它们使用相同的转换方式(需要注意drop()会创建一个数据副本,但是不影响 strat_train_set):

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

5.1、数据清理

5.1.1、常规方法(四种)

大部分的机器学习算法无法在缺失的特征上工作,所以我们要创建一些函数来辅助它。前面我们已经注意到total_bedrooms属性有部分值缺失,所以我们要先解决它。有一下三种选择:

  1. 放弃这些相应的区域
  2. 放弃整个属性
  3. 将缺失值设置为某个值(0、平均值或者中位数等)
  4. 将缺失值按分组设置为组内某个值(0、平均值或者中位数等)(我自己加的)

通过DataFramedropan()drop()fillan()groupby()方法,可以轻松完成这些操作:

# 获取有缺失值的行,方便显示
sample_incomplete_rows = housing[housing.isnull().any(axis=1)].head()
sample_incomplete_rows
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomeocean_proximity
4629-118.3034.0718.03759.0NaN3296.01462.02.2708<1H OCEAN
6068-117.8634.0116.04632.0NaN3038.0727.05.1762<1H OCEAN
17923-121.9737.3530.01955.0NaN999.0386.04.6328<1H OCEAN
13656-117.3034.056.02155.0NaN1039.0391.01.6675INLAND
19252-122.7938.487.06837.0NaN3468.01405.03.1662<1H OCEAN

5.1.1.1、方法一:dropna()

sample_incomplete_rows.dropna(subset=["total_bedrooms"])
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomeocean_proximity

5.1.1.2、方法二:drop()

sample_incomplete_rows.drop("total_bedrooms", axis=1)
longitudelatitudehousing_median_agetotal_roomspopulationhouseholdsmedian_incomeocean_proximity
4629-118.3034.0718.03759.03296.01462.02.2708<1H OCEAN
6068-117.8634.0116.04632.03038.0727.05.1762<1H OCEAN
17923-121.9737.3530.01955.0999.0386.04.6328<1H OCEAN
13656-117.3034.056.02155.01039.0391.01.6675INLAND
19252-122.7938.487.06837.03468.01405.03.1662<1H OCEAN

5.1.1.3、方法三:fillna()

median = housing["total_bedrooms"].median()
sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True)
sample_incomplete_rows
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomeocean_proximity
4629-118.3034.0718.03759.0433.03296.01462.02.2708<1H OCEAN
6068-117.8634.0116.04632.0433.03038.0727.05.1762<1H OCEAN
17923-121.9737.3530.01955.0433.0999.0386.04.6328<1H OCEAN
13656-117.3034.056.02155.0433.01039.0391.01.6675INLAND
19252-122.7938.487.06837.0433.03468.01405.03.1662<1H OCEAN

5.1.1.4、方法四:groupby()

housing_group = housing.copy()
housing_group["income_cat"] = pd.cut(housing["median_income"],
                                     bins=[0, 1.5, 3, 4.5, 6, np.inf],
                                     labels=[1, 2, 3, 4, 5])
housing_group[housing.isnull().any(axis=1)].head()
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomeocean_proximityincome_cat
4629-118.3034.0718.03759.0NaN3296.01462.02.2708<1H OCEAN2
6068-117.8634.0116.04632.0NaN3038.0727.05.1762<1H OCEAN4
17923-121.9737.3530.01955.0NaN999.0386.04.6328<1H OCEAN4
13656-117.3034.056.02155.0NaN1039.0391.01.6675INLAND2
19252-122.7938.487.06837.0NaN3468.01405.03.1662<1H OCEAN3
housing_group_median = housing_group.groupby("income_cat").transform(lambda x: x.fillna(x.median()))
housing_group_median[housing.isnull().any(axis=1)].head()
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_income
4629-118.3034.0718.03759.0444.03296.01462.02.2708
6068-117.8634.0116.04632.0427.03038.0727.05.1762
17923-121.9737.3530.01955.0427.0999.0386.04.6328
13656-117.3034.056.02155.0444.01039.0391.01.6675
19252-122.7938.487.06837.0453.03468.01405.03.1662

如果选择方法三,你需要计算出训练集的中位数,然后用它填充训练集中的缺失值,但也别忘了保存这个计算出的中位数,因为后面可能需要用到。当重新评估系统时,你需要更换测试集中的缺失值;或者在系统上线时,需要使用新数据替代缺失值。

5.1.2、Scikit-Learn 提供的 SimpleImputer 方法

Scikit-Learn提供了一个非常容易上手的类来处理缺失值:SimpleImputer。使用方法如下:首先,你需要创建一个 SimpleImputer 实例,指定你要用属性的中位数值替换该属性的缺失值:

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(strategy="median")

由于中位数只能在数值属性上计算,所以我们需要创建一个没有文本属性 ocean_proximity 的数据副本:

housing_num = housing.drop("ocean_proximity", axis=1)

使用fit()方法将imputer实例适配到训练数据:

imputer.fit(housing_num)
SimpleImputer(strategy='median')

这里imputer仅仅只是计算了每个属性的中位数值,并将结果储存在其实例变量statistics_中。虽然只是total_bedrooms这个属性存在缺失值,所以稳妥起见,还是将imputer应用于所有的数值属性:

# imputer 计算的每列中位数
imputer.statistics_
array([-118.51  ,   34.26  ,   29.    , 2119.5   ,  433.    , 1164.    , 408.    ,    3.5409])
# 直接计算的中位数
housing_num.median().values
array([-118.51  ,   34.26  ,   29.    , 2119.5   ,  433.    , 1164.    , 408.    ,    3.5409])

现在,你可以使用这个“训练有素”的imputer将缺失值替换成中位数从而完成训练集转换:

X = imputer.transform(housing_num)
type(X)
numpy.ndarray

结果是一个包含转换后特征的Numpy数组。如果你想将它放回Pandas DataFrame,也很简单:

housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing_num.index)
housing_tr.loc[sample_incomplete_rows.index.values]
longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_income
4629-118.3034.0718.03759.0433.03296.01462.02.2708
6068-117.8634.0116.04632.0433.03038.0727.05.1762
17923-121.9737.3530.01955.0433.0999.0386.04.6328
13656-117.3034.056.02155.0433.01039.0391.01.6675
19252-122.7938.487.06837.0433.03468.01405.03.1662

5.2、处理文本和分类属性

5.2.1、使用 Scikit-Learn 的 OrdinalEncoder 类

到目前为止,我们只处理数值属性,但现在让我们看一下文本属性。在此数据集中,只有一个:ocean_proximity属性。前面我们一直到它不是任意文本,而是有限个可能的取值,每个值代表一类别。因此,此属性是分类属性。大多数机器学习算法更喜欢使用数字,因此让我们将这些类别从文件转到数字。为此,我们可以使用Scikit-LearnOrdinalEncoder类:

housing["ocean_proximity"].value_counts()
<1H OCEAN     7276
INLAND        5263
NEAR OCEAN    2124
NEAR BAY      1847
ISLAND           2
Name: ocean_proximity, dtype: int64
from sklearn.preprocessing import OrdinalEncoder

housing_cat = housing[["ocean_proximity"]]
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
array([[0.],
       [0.],
       [4.],
       [1.],
       [0.],
       [1.],
       [0.],
       [1.],
       [0.],
       [0.]])

你可以使用categories_实例变量获取类别列表。这个列表包含每个类别属性的维一数组(在这种情况下,这个列表包含一个数组,因为只有一个类别属性):

ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'], dtype=object)]

这种表征方式产生的一个问题是,机器学习算法会认为两个相近的比值比两个离得较远的值更为相似一些,在某种情况下这是对的(比如一些有序类别,像“优”、“良”、“中”、“差”),但是对ocean_proximity而言情况并非如此(例如,类别0和类别4之间就比类别0和类别1之间的相似度更高)

5.2.2、使用 Scikit-Learn 的 OneHotEncoder 类

为了解决这个问题,常见的解决方案是给每个类别创建一个二进制的属性: