房價預測案例
Step 1: 檢視源數據集
import numpy as np
import pandas as pd
讀入數據
-
一般來說源數據的index那一欄沒什么用,我們可以用來作為我們pandas dataframe的index。這樣之后要是檢索起來也省事兒。
-
有人的地方就有鄙視鏈。跟知乎一樣。Kaggle的也是個處處呵呵的危險地帶。Kaggle上默認把數據放在input文件夾下。所以我們沒事兒寫個教程什么的,也可以依據這個convention來,顯得自己很有逼格。。
train_df = pd.read_csv('train.csv', index_col=0)
test_df = pd.read_csv('test.csv', index_col=0)
檢視源數據
train_df.head()
# print(train_df.info)
# print(train_df.shape)
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長什么樣紙:
%matplotlib inline
prices = pd.DataFrame({"price":train_df["SalePrice"], "log(price + 1)":np.log1p(train_df["SalePrice"])})
prices.hist()
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7fbf9f8703c8>,<matplotlib.axes._subplots.AxesSubplot object at 0x7fbf97340400>]],dtype=object)
可見,label本身并不平滑。為了我們分類器的學習更加準確,我們會首先把label給“平滑化”(正態化)
這一步大部分同學會miss掉,導致自己的結果總是達不到一定標準。
這里我們使用最有逼格的log1p, 也就是 log(x+1),避免了復值的問題。
記住喲,如果我們這里把數據都給平滑化了,那么最后算結果的時候,要記得把預測到的平滑數據給變回去。
按照“怎么來的怎么去”原則,log1p()就需要expm1(); 同理,log()就需要exp(), … etc.
對于分類問題,不需要對標簽進行平滑處理,因為其結果本來就是需要離散化的;但是對于回歸問題,由于需要預測的是一個連續的值,所以需要訓練模型的數據的標簽也是平滑的,而對于訓練集,我們得到的標簽往往又是離散的,所以要先做平滑處理!
注:在比賽中,由于訓練集和測試集已經知道,所以常常為了更大的得到好的結果,常常將測試集和訓練集先混合在一起,然后在整體做數據的預處理;但是在實際的過程中,由于測試集是未知的,所以先對訓練集做處理,再用相同的方法去處理測試集。
y_train = np.log1p(train_df.pop('SalePrice'))
然后我們把剩下的部分合并起來
all_df = pd.concat((train_df, test_df), axis=0)
此刻,我們可以看到all_df就是我們合在一起的DF
all_df.shape
(2919, 79)
而y_train則是SalePrice那一列
y_train.head()
Id
1 12.247699
2 12.109016
3 12.317171
4 11.849405
5 12.429220
Name: SalePrice, dtype: float64
Step 3: 變量轉化
類似『特征工程』。就是把不方便處理或者不unify的數據給統一了。
正確化變量屬性
首先,我們注意到,MSSubClass 的值其實應該是一個category,
但是Pandas是不會懂這些事兒的。使用DF的時候,這類數字符號會被默認記成數字。
這種東西就很有誤導性,我們需要把它變回成string
all_df['MSSubClass'].dtypes
dtype('int64')
all_df['MSSubClass'] = all_df['MSSubClass'].astype(str) #這個根據給出的數據集的描述可知,該屬性應該是表示的為級別,但是PD在讀取數據的時候會自動將其認為是數字,所以需要轉換為字符串
變成str以后,做個統計,就很清楚了
all_df['MSSubClass'].value_counts()
20 1079
60 575
50 287
120 182
30 139
160 128
70 128
80 118
90 109
190 61
85 48
75 23
45 18
180 17
40 6
150 1
Name: MSSubClass, dtype: int64
把category的變量轉變成numerical表達形式
當我們用numerical來表達categorical的時候,要注意,數字本身有大小的含義,所以亂用數字會給之后的模型學習帶來麻煩。于是我們可以用One-Hot的方法來表達category。
pandas自帶的get_dummies方法,可以幫你一鍵做到One-Hot。
pd.get_dummies(all_df['MSSubClass'], prefix='MSSubClass').head()#將分類數據轉換為數值型數據
MSSubClass_120 | MSSubClass_150 | MSSubClass_160 | MSSubClass_180 | MSSubClass_190 | MSSubClass_20 | MSSubClass_30 | MSSubClass_40 | MSSubClass_45 | MSSubClass_50 | MSSubClass_60 | MSSubClass_70 | MSSubClass_75 | MSSubClass_80 | MSSubClass_85 | MSSubClass_90 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Id | ||||||||||||||||
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
此刻MSSubClass被我們分成了12個column,每一個代表一個category。是就是1,不是就是0。
同理,我們把所有的category數據,都給One-Hot了
all_dummy_df = pd.get_dummies(all_df)
all_dummy_df.head()
LotFrontage | LotArea | OverallQual | OverallCond | YearBuilt | YearRemodAdd | MasVnrArea | BsmtFinSF1 | BsmtFinSF2 | BsmtUnfSF | ... | SaleType_ConLw | SaleType_New | SaleType_Oth | SaleType_WD | SaleCondition_Abnorml | SaleCondition_AdjLand | SaleCondition_Alloca | SaleCondition_Family | SaleCondition_Normal | SaleCondition_Partial | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Id | |||||||||||||||||||||
1 | 65.0 | 8450 | 7 | 5 | 2003 | 2003 | 196.0 | 706.0 | 0.0 | 150.0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | 80.0 | 9600 | 6 | 8 | 1976 | 1976 | 0.0 | 978.0 | 0.0 | 284.0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
3 | 68.0 | 11250 | 7 | 5 | 2001 | 2002 | 162.0 | 486.0 | 0.0 | 434.0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
4 | 60.0 | 9550 | 7 | 5 | 1915 | 1970 | 0.0 | 216.0 | 0.0 | 540.0 | ... | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
5 | 84.0 | 14260 | 8 | 5 | 2000 | 2000 | 350.0 | 655.0 | 0.0 | 490.0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
5 rows × 303 columns
處理好numerical變量
就算是numerical的變量,也還會有一些小問題。
比如,有一些數據是缺失 的:
all_dummy_df.isnull().sum().sort_values(ascending=False).head(10) #將數據中有缺失項的屬性統計出來并從大到小排序
LotFrontage 486
GarageYrBlt 159
MasVnrArea 23
BsmtHalfBath 2
BsmtFullBath 2
BsmtFinSF2 1
GarageCars 1
TotalBsmtSF 1
BsmtUnfSF 1
GarageArea 1
dtype: int64
可以看到,缺失最多的column是LotFrontage
處理這些缺失的信息,得靠好好審題。一般來說,數據集的描述里會寫的很清楚,這些缺失都代表著什么。當然,如果實在沒有的話,也只能靠自己的『想當然』。。
在這里,我們用平均值來填滿這些空缺。
mean_cols = all_dummy_df.mean()#得到所有列的平均值
mean_cols.head(10) #打印出前10列數據的平均值
LotFrontage 69.305795
LotArea 10168.114080
OverallQual 6.089072
OverallCond 5.564577
YearBuilt 1971.312778
YearRemodAdd 1984.264474
MasVnrArea 102.201312
BsmtFinSF1 441.423235
BsmtFinSF2 49.582248
BsmtUnfSF 560.772104
dtype: float64
all_dummy_df = all_dummy_df.fillna(mean_cols) #用每一列的平均值填充每一列中的NAN項
看看是不是沒有空缺了?
all_dummy_df.isnull().sum().sum()
0
標準化numerical數據
這一步并不是必要,但是得看你想要用的分類器是什么。一般來說,regression的分類器都比較傲嬌,最好是把源數據給放在一個標準分布內。不要讓數據間的差距太大。
這里,我們當然不需要把One-Hot的那些0/1數據給標準化。我們的目標應該是那些本來就是numerical的數據:
先來看看 哪些是numerical的:
numeric_cols = all_df.columns[all_df.dtypes != 'object']
numeric_cols
Index(['LotFrontage', 'LotArea', 'OverallQual', 'OverallCond', 'YearBuilt','YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', '1stFlrSF', '2ndFlrSF', 'LowQualFinSF', 'GrLivArea','BsmtFullBath', 'BsmtHalfBath', 'FullBath', 'HalfBath', 'BedroomAbvGr','KitchenAbvGr', 'TotRmsAbvGrd', 'Fireplaces', 'GarageYrBlt','GarageCars', 'GarageArea', 'WoodDeckSF', 'OpenPorchSF','EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'PoolArea', 'MiscVal','MoSold', 'YrSold'],dtype='object')
計算標準分布:(X-X’)/s
讓我們的數據點更平滑,更便于計算。
注意:我們這里也是可以繼續使用Log的,我只是給大家展示一下多種“使數據平滑”的辦法。
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 #計算標準分布
Step 4: 建立模型
把數據集分回 訓練/測試集
dummy_train_df = all_dummy_df.loc[train_df.index]
dummy_test_df = all_dummy_df.loc[test_df.index]
dummy_train_df.shape, dummy_test_df.shape
((1460, 303), (1459, 303))
Ridge Regression
用Ridge Regression模型來跑一遍看看。(對于多因子的數據集,這種模型可以方便的把所有的var都無腦的放進去)
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
這一步不是很必要,只是把DF轉化成Numpy Array,這跟Sklearn更加配
X_train = dummy_train_df.values
X_test = dummy_test_df.values
用Sklearn自帶的cross validation方法來測試模型
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值更好(也就是『調參數』)
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
from sklearn.ensemble import RandomForestRegressor
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))
plt.plot(max_features, test_scores)
plt.title("Max Features vs CV Error");
用RF的最優值達到了0.137
Step 5: Ensemble
這里我們用一個Stacking的思維來汲取兩種或者多種模型的優點
首先,我們把最好的parameter拿出來,做成我們最終的model
ridge = Ridge(alpha=15)
rf = RandomForestRegressor(n_estimators=500, max_features=.3)
ridge.fit(X_train, y_train)
rf.fit(X_train, y_train)
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,max_features=0.3, max_leaf_nodes=None,min_impurity_decrease=0.0, min_impurity_split=None,min_samples_leaf=1, min_samples_split=2,min_weight_fraction_leaf=0.0, n_estimators=500, n_jobs=1,oob_score=False, random_state=None, verbose=0, warm_start=False)
上面提到了,因為最前面我們給label做了個log(1+x), 于是這里我們需要把predit的值給exp回去,并且減掉那個"1"
所以就是我們的expm1()函數。
y_ridge = np.expm1(ridge.predict(X_test))
y_rf = np.expm1(rf.predict(X_test))
一個正經的Ensemble是把這群model的預測結果作為新的input,再做一次預測。這里我們簡單的方法,就是直接『平均化』。
y_final = (y_ridge + y_rf) / 2 #對模型進行融合
Step 6: 提交結果
submission_df = pd.DataFrame(data= {'Id' : test_df.index, 'SalePrice': y_final})
我們的submission大概長這樣:
submission_df.head(10)
Id | SalePrice | |
---|---|---|
0 | 1461 | 120284.850938 |
1 | 1462 | 151366.179663 |
2 | 1463 | 174792.133120 |
3 | 1464 | 190099.605945 |
4 | 1465 | 195500.170436 |
5 | 1466 | 175876.563502 |
6 | 1467 | 177675.128914 |
7 | 1468 | 169318.651321 |
8 | 1469 | 184796.699921 |
9 | 1470 | 122328.836416 |
走你~