房价预测案例

Step 1: 检视源数据集


In [5]:
import numpy as np
import pandas as pd

读入数据

  • 一般来说源数据的index那一栏没什么用,我们可以用来作为我们pandas dataframe的index。这样之后要是检索起来也省事儿。

  • 有人的地方就有鄙视链。跟知乎一样。Kaggle的也是个处处呵呵的危险地带。Kaggle上默认把数据放在input文件夹下。所以我们没事儿写个教程什么的,也可以依据这个convention来,显得自己很有逼格。。


In [6]:
train_df = pd.read_csv('../input/train.csv', index_col=0)
test_df = pd.read_csv('../input/test.csv', index_col=0)

检视源数据


In [7]:
train_df.head()


Out[7]:
MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour Utilities LotConfig ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold YrSold SaleType SaleCondition SalePrice
Id
1 60 RL 65.0 8450 Pave NaN Reg Lvl AllPub Inside ... 0 NaN NaN NaN 0 2 2008 WD Normal 208500
2 20 RL 80.0 9600 Pave NaN Reg Lvl AllPub FR2 ... 0 NaN NaN NaN 0 5 2007 WD Normal 181500
3 60 RL 68.0 11250 Pave NaN IR1 Lvl AllPub Inside ... 0 NaN NaN NaN 0 9 2008 WD Normal 223500
4 70 RL 60.0 9550 Pave NaN IR1 Lvl AllPub Corner ... 0 NaN NaN NaN 0 2 2006 WD Abnorml 140000
5 60 RL 84.0 14260 Pave NaN IR1 Lvl AllPub FR2 ... 0 NaN NaN NaN 0 12 2008 WD Normal 250000

5 rows × 80 columns

这时候大概心里可以有数,哪些地方需要人为的处理一下,以做到源数据更加好被process。

Step 2: 合并数据

这么做主要是为了用DF进行数据预处理的时候更加方便。等所有的需要的预处理进行完之后,我们再把他们分隔开。

首先,SalePrice作为我们的训练目标,只会出现在训练集中,不会在测试集中(要不然你测试什么?)。所以,我们先把SalePrice这一列给拿出来,不让它碍事儿。

我们先看一下SalePrice长什么样纸:


In [1]:
%matplotlib inline
prices = pd.DataFrame({"price":train_df["SalePrice"], "log(price + 1)":np.log1p(train_df["SalePrice"])})
prices.hist()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-56d8156ddf28> in <module>()
      1 get_ipython().magic('matplotlib inline')
----> 2 prices = pd.DataFrame({"price":train_df["SalePrice"], "log(price + 1)":np.log1p(train_df["SalePrice"])})
      3 prices.hist()

NameError: name 'pd' is not defined

可见,label本身并不平滑。为了我们分类器的学习更加准确,我们会首先把label给“平滑化”(正态化)

这一步大部分同学会miss掉,导致自己的结果总是达不到一定标准。

这里我们使用最有逼格的log1p, 也就是 log(x+1),避免了复值的问题。

记住哟,如果我们这里把数据都给平滑化了,那么最后算结果的时候,要记得把预测到的平滑数据给变回去。

按照“怎么来的怎么去”原则,log1p()就需要expm1(); 同理,log()就需要exp(), ... etc.


In [ ]:
y_train = np.log1p(train_df.pop('SalePrice'))

然后我们把剩下的部分合并起来


In [ ]:
all_df = pd.concat((train_df, test_df), axis=0)

此刻,我们可以看到all_df就是我们合在一起的DF


In [ ]:
all_df.shape

y_train则是SalePrice那一列


In [2]:
y_train.head()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-184ceb084de2> in <module>()
----> 1 y_train.head()

NameError: name 'y_train' is not defined

Step 3: 变量转化

类似『特征工程』。就是把不方便处理或者不unify的数据给统一了。

正确化变量属性

首先,我们注意到,MSSubClass 的值其实应该是一个category,

但是Pandas是不会懂这些事儿的。使用DF的时候,这类数字符号会被默认记成数字。

这种东西就很有误导性,我们需要把它变回成string


In [3]:
all_df['MSSubClass'].dtypes


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-f229f9a07ad8> in <module>()
----> 1 all_df['MSSubClass'].dtypes

NameError: name 'all_df' is not defined

In [4]:
all_df['MSSubClass'] = all_df['MSSubClass'].astype(str)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-4-568582949051> in <module>()
----> 1 all_df['MSSubClass'] = all_df['MSSubClass'].astype(str)

NameError: name 'all_df' is not defined

变成str以后,做个统计,就很清楚了


In [5]:
all_df['MSSubClass'].value_counts()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-5-642b35f92aae> in <module>()
----> 1 all_df['MSSubClass'].value_counts()

NameError: name 'all_df' is not defined

把category的变量转变成numerical表达形式

当我们用numerical来表达categorical的时候,要注意,数字本身有大小的含义,所以乱用数字会给之后的模型学习带来麻烦。于是我们可以用One-Hot的方法来表达category。

pandas自带的get_dummies方法,可以帮你一键做到One-Hot。


In [6]:
pd.get_dummies(all_df['MSSubClass'], prefix='MSSubClass').head()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-6-a2957f185f18> in <module>()
----> 1 pd.get_dummies(all_df['MSSubClass'], prefix='MSSubClass').head()

NameError: name 'pd' is not defined

此刻MSSubClass被我们分成了12个column,每一个代表一个category。是就是1,不是就是0。

同理,我们把所有的category数据,都给One-Hot了


In [7]:
all_dummy_df = pd.get_dummies(all_df)
all_dummy_df.head()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-7-fc4242c46f0c> in <module>()
----> 1 all_dummy_df = pd.get_dummies(all_df)
      2 all_dummy_df.head()

NameError: name 'pd' is not defined

处理好numerical变量

就算是numerical的变量,也还会有一些小问题。

比如,有一些数据是缺失的:


In [8]:
all_dummy_df.isnull().sum().sort_values(ascending=False).head(10)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-8-5b0e8189db0d> in <module>()
----> 1 all_dummy_df.isnull().sum().sort_values(ascending=False).head(10)

NameError: name 'all_dummy_df' is not defined

可以看到,缺失最多的column是LotFrontage

处理这些缺失的信息,得靠好好审题。一般来说,数据集的描述里会写的很清楚,这些缺失都代表着什么。当然,如果实在没有的话,也只能靠自己的『想当然』。。

在这里,我们用平均值来填满这些空缺。


In [9]:
mean_cols = all_dummy_df.mean()
mean_cols.head(10)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-9-8a694a92fb65> in <module>()
----> 1 mean_cols = all_dummy_df.mean()
      2 mean_cols.head(10)

NameError: name 'all_dummy_df' is not defined

In [10]:
all_dummy_df = all_dummy_df.fillna(mean_cols)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-10-121a8fbe7ad6> in <module>()
----> 1 all_dummy_df = all_dummy_df.fillna(mean_cols)

NameError: name 'all_dummy_df' is not defined

看看是不是没有空缺了?


In [11]:
all_dummy_df.isnull().sum().sum()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-11-94a3aa96570e> in <module>()
----> 1 all_dummy_df.isnull().sum().sum()

NameError: name 'all_dummy_df' is not defined

标准化numerical数据

这一步并不是必要,但是得看你想要用的分类器是什么。一般来说,regression的分类器都比较傲娇,最好是把源数据给放在一个标准分布内。不要让数据间的差距太大。

这里,我们当然不需要把One-Hot的那些0/1数据给标准化。我们的目标应该是那些本来就是numerical的数据:

先来看看 哪些是numerical的:


In [12]:
numeric_cols = all_df.columns[all_df.dtypes != 'object']
numeric_cols


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-8908b5200b5b> in <module>()
----> 1 numeric_cols = all_df.columns[all_df.dtypes != 'object']
      2 numeric_cols

NameError: name 'all_df' is not defined

计算标准分布:(X-X')/s

让我们的数据点更平滑,更便于计算。

注意:我们这里也是可以继续使用Log的,我只是给大家展示一下多种“使数据平滑”的办法。


In [13]:
numeric_col_means = all_dummy_df.loc[:, numeric_cols].mean()
numeric_col_std = all_dummy_df.loc[:, numeric_cols].std()
all_dummy_df.loc[:, numeric_cols] = (all_dummy_df.loc[:, numeric_cols] - numeric_col_means) / numeric_col_std


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-13-eec4f86642e0> in <module>()
----> 1 numeric_col_means = all_dummy_df.loc[:, numeric_cols].mean()
      2 numeric_col_std = all_dummy_df.loc[:, numeric_cols].std()
      3 all_dummy_df.loc[:, numeric_cols] = (all_dummy_df.loc[:, numeric_cols] - numeric_col_means) / numeric_col_std

NameError: name 'all_dummy_df' is not defined

Step 4: 建立模型

把数据集分回 训练/测试集


In [14]:
dummy_train_df = all_dummy_df.loc[train_df.index]
dummy_test_df = all_dummy_df.loc[test_df.index]


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-14-62cc3501e2d0> in <module>()
----> 1 dummy_train_df = all_dummy_df.loc[train_df.index]
      2 dummy_test_df = all_dummy_df.loc[test_df.index]

NameError: name 'all_dummy_df' is not defined

In [15]:
dummy_train_df.shape, dummy_test_df.shape


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-15-c33e6db13826> in <module>()
----> 1 dummy_train_df.shape, dummy_test_df.shape

NameError: name 'dummy_train_df' is not defined

Ridge Regression

用Ridge Regression模型来跑一遍看看。(对于多因子的数据集,这种模型可以方便的把所有的var都无脑的放进去)


In [16]:
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score

这一步不是很必要,只是把DF转化成Numpy Array,这跟Sklearn更加配


In [17]:
X_train = dummy_train_df.values
X_test = dummy_test_df.values


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-17-2b1a6afa583a> in <module>()
----> 1 X_train = dummy_train_df.values
      2 X_test = dummy_test_df.values

NameError: name 'dummy_train_df' is not defined

用Sklearn自带的cross validation方法来测试模型


In [ ]:
alphas = np.logspace(-3, 2, 50)
test_scores = []
for alpha in alphas:
    clf = Ridge(alpha)
    test_score = np.sqrt(-cross_val_score(clf, X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
    test_scores.append(np.mean(test_score))

存下所有的CV值,看看哪个alpha值更好(也就是『调参数』)


In [ ]:
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(alphas, test_scores)
plt.title("Alpha vs CV Error");

可见,大概alpha=10~20的时候,可以把score达到0.135左右。

Random Forest


In [ ]:
from sklearn.ensemble import RandomForestRegressor

In [ ]:
max_features = [.1, .3, .5, .7, .9, .99]
test_scores = []
for max_feat in max_features:
    clf = RandomForestRegressor(n_estimators=200, max_features=max_feat)
    test_score = np.sqrt(-cross_val_score(clf, X_train, y_train, cv=5, scoring='neg_mean_squared_error'))
    test_scores.append(np.mean(test_score))

In [ ]:
plt.plot(max_features, test_scores)
plt.title("Max Features vs CV Error");

用RF的最优值达到了0.137

Step 5: Ensemble

这里我们用一个Stacking的思维来汲取两种或者多种模型的优点

首先,我们把最好的parameter拿出来,做成我们最终的model


In [ ]:
ridge = Ridge(alpha=15)
rf = RandomForestRegressor(n_estimators=500, max_features=.3)

In [ ]:
ridge.fit(X_train, y_train)
rf.fit(X_train, y_train)

上面提到了,因为最前面我们给label做了个log(1+x), 于是这里我们需要把predit的值给exp回去,并且减掉那个"1"

所以就是我们的expm1()函数。


In [18]:
y_ridge = np.expm1(ridge.predict(X_test))
y_rf = np.expm1(rf.predict(X_test))


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-18-4458938e3f81> in <module>()
----> 1 y_ridge = np.expm1(ridge.predict(X_test))
      2 y_rf = np.expm1(rf.predict(X_test))

NameError: name 'np' is not defined

一个正经的Ensemble是把这群model的预测结果作为新的input,再做一次预测。这里我们简单的方法,就是直接『平均化』。


In [19]:
y_final = (y_ridge + y_rf) / 2


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-19-d100dcf9e331> in <module>()
----> 1 y_final = (y_ridge + y_rf) / 2

NameError: name 'y_ridge' is not defined

Step 6: 提交结果


In [20]:
submission_df = pd.DataFrame(data= {'Id' : test_df.index, 'SalePrice': y_final})


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-20-af4dd2d42660> in <module>()
----> 1 submission_df = pd.DataFrame(data= {'Id' : test_df.index, 'SalePrice': y_final})

NameError: name 'pd' is not defined

我们的submission大概长这样:


In [21]:
submission_df.head(10)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-21-d509a54d9b7f> in <module>()
----> 1 submission_df.head(10)

NameError: name 'submission_df' is not defined

走你~


In [ ]: