天天看點

【Python資料科學手冊】專題:線性回歸

線性回歸模型是解決回歸任務的好起點。

你可能對線性回歸模型最簡單的形式(即對資料拟合一條直線)已經很熟悉了,不過經過擴充,這些模型可以對更複雜的資料行為進行模組化。

首先導入常用的程式庫:

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
import numpy as np           

1、簡單線性回歸

首先來介紹最廣為人知的線性回歸模型——将資料拟合成一條直線。直線拟合的模型方程為y=ax+by=ax+b,其中aa是直線斜率,bb是直線截距。

下面的資料,它們是從斜率為2、截距為-5 的直線中抽取的散點

rng· = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = 2 * x - 5 + rng.randn(50)
plt.scatter(x, y);           
【Python資料科學手冊】專題:線性回歸

用Scikit-Learn 的LinearRegression 評估器來拟合資料,并獲得最佳拟合直線

from sklearn.linear_model import LinearRegression
model = LinearRegression(fit_intercept=True)

model.fit(x[:, np.newaxis], y)

xfit = np.linspace(0, 10, 1000)
yfit = model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit);           
【Python資料科學手冊】專題:線性回歸

資料的斜率和截距都在模型的拟合參數中,Scikit-Learn 通常會在參數後面加一條下劃線,即coef_ 和intercept_:

print("Model slope:    ", model.coef_[0])
print("Model intercept:", model.intercept_)           
【Python資料科學手冊】專題:線性回歸

可以看到,拟合結果與真實值非常接近。

LinearRegression 評估器能做的可遠不止這些——除了簡單的直線拟合,它還可以處理多元度的線性回歸模型:

y=a0+a1x1+a2x2+⋯

裡面有多個x 變量。從幾何學的角度看,這個模型是拟合三維空間中的一個平面,或者是為更高次元的資料點拟合一個超平面。

rng = np.random.RandomState(1)
X = 10 * rng.rand(100, 3)
y = 0.5 + np.dot(X, [1.5, -2., 1.])

model.fit(X, y)
print(model.intercept_)
print(model.coef_)
0.5
[ 1.5 -2.   1. ]           

其中y 變量是由3個随機的x變量線性組合而成,線性回歸模型還原了方程的系數。

通過這種方式,就可以用一個LinearRegression 評估器拟合資料的回歸直線、平面和超平面了。

局限性: 變量限制線上性關系上

2、基函數回歸

可以通過基函數對原始資料進行變換,進而将變量間的線性回歸模型轉換為非線性回歸模型。

這個方法的多元模型是:y=a0+a1x1+a2x2+a3x3+⋯

其中一維的輸入變量xx轉換成了三維變量x₁,x₂,x₃。讓xn=fn(x),這裡的fn(x)是轉換資料的函數。

假如fn(x)=xn,那麼模型就會變成多項式回歸:y=a0+a1x+a2x2+a3x3+⋯

需要注意的是,這個模型仍然是一個線性模型,也就是說系數an彼此不會相乘或相除。

1、多項式函數

多項式投影非常有用,是以Scikit-Learn 内置了PolynomialFeatures 轉換器實作這個功能:

from sklearn.preprocessing import PolynomialFeatures
x = np.array([2, 3, 4])
poly = PolynomialFeatures(3, include_bias=False)
poly.fit_transform(x[:, None])           

轉換器通過指數函數,将一維數組轉換成了三維數組。這個新的高維數組之後可以放在多項式回歸模型中。

我們建立一個7 次多項式回歸模型:

from sklearn.pipeline import make_pipeline
poly_model = make_pipeline(PolynomialFeatures(7),
                    LinearRegression())           

資料經過轉換之後,我們就可以用線性模型來拟合x 和y 之間更複雜的關系了。

rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = np.sin(x) + 0.1 * rng.randn(50)

poly_model.fit(x[:, np.newaxis], y)
yfit = poly_model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit);           
【Python資料科學手冊】專題:線性回歸

通過運用7 次多項式基函數,這個線性模型可以對非線性資料拟合出極好的效果!

2. 高斯基函數

例如,有一種常用的拟合模型方法使用的并不是一組多項式 基函數,而是一組高斯基函數。

【Python資料科學手冊】專題:線性回歸

圖中的陰影部分代表不同規模基函數,把它們放在一起時就會産生平滑的曲線。

from sklearn.base import BaseEstimator, TransformerMixin

class GaussianFeatures(BaseEstimator, TransformerMixin):
    """Uniformly spaced Gaussian features for one-dimensional input"""
    
    def __init__(self, N, width_factor=2.0):
        self.N = N
        self.width_factor = width_factor
    
    @staticmethod
    def _gauss_basis(x, y, width, axis=None):
        arg = (x - y) / width
        return np.exp(-0.5 * np.sum(arg ** 2, axis))
        
    def fit(self, X, y=None):
        # create N centers spread along the data range
        self.centers_ = np.linspace(X.min(), X.max(), self.N)
        self.width_ = self.width_factor * (self.centers_[1] - self.centers_[0])
        return self
        
    def transform(self, X):
        return self._gauss_basis(X[:, :, np.newaxis], self.centers_,
                                 self.width_, axis=1)
    
gauss_model = make_pipeline(GaussianFeatures(20),
                            LinearRegression())
gauss_model.fit(x[:, np.newaxis], y)
yfit = gauss_model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit)
plt.xlim(0, 10);           

3、正則化

雖然線上性回歸模型中引入基函數會讓模型變得更加靈活,但是也很容易造成過拟合。例如,如果選擇了太多高斯基函數,那麼最終的拟合結果看起來可能并不好。

model = make_pipeline(GaussianFeatures(30),
                      LinearRegression())
model.fit(x[:, np.newaxis], y)

plt.scatter(x, y)
plt.plot(xfit, model.predict(xfit[:, np.newaxis]))

plt.xlim(0, 10)
plt.ylim(-1.5, 1.5);           
【Python資料科學手冊】專題:線性回歸

如果将資料投影到30 維的基函數上,模型就會變得過于靈活,進而能夠适應資料中不同位置的異常值。如果将高斯基函數的系數畫出來,就可以看到過拟合的原因。

def basis_plot(model, title=None):
    fig, ax = plt.subplots(2, sharex=True)
    model.fit(x[:, np.newaxis], y)
    ax[0].scatter(x, y)
    ax[0].plot(xfit, model.predict(xfit[:, np.newaxis]))
    ax[0].set(xlabel='x', ylabel='y', ylim=(-1.5, 1.5))
    
    if title:
        ax[0].set_title(title)

    ax[1].plot(model.steps[0][1].centers_,
               model.steps[1][1].coef_)
    ax[1].set(xlabel='basis location',
              ylabel='coefficient',
              xlim=(0, 10))
    
model = make_pipeline(GaussianFeatures(30), LinearRegression())
basis_plot(model)           
【Python資料科學手冊】專題:線性回歸

下面那幅圖顯示了每個位置上基函數的振幅。當基函數重疊的時候,通常就表明出現了過拟合:相鄰基函數的系數互相抵消。這顯然是有問題的,如果對較大的模型參數進行懲罰(penalize),進而抑制模型劇烈波動,應該就可以解決這個問題了。這個懲罰機制被稱為正則化(regularization),有幾種不同的表現形式。

1. 嶺回歸(L2範數正則化)

正則化最常見的形式可能就是嶺回歸(ridge regression,或者L2 範數正則化),有時也被稱為吉洪諾夫正則化(Tikhonov regularization)。其處理方法是對模型系數平方和(L2 範數)進行懲罰,模型拟合的懲罰項為:

【Python資料科學手冊】專題:線性回歸

α 是一個自由參數,用來控制懲罰的力度。這種帶懲罰項的模型内置在Scikit-Learn的Ridge 評估器中

from sklearn.linear_model import Ridge
model = make_pipeline(GaussianFeatures(30), Ridge(alpha=0.1))
basis_plot(model, title='Ridge Regression')           
【Python資料科學手冊】專題:線性回歸

參數α 是控制最終模型複雜度的關鍵。如果α → 0,那麼模型就恢複到标準線性回歸結果;如果α → ∞,那麼所有模型響應都會被壓制。

2. Lasso正則化(L1範數)

另一種常用的正則化被稱為Lasso,其處理方法是對模型系數絕對值的和(L1 範數)進行懲罰:

【Python資料科學手冊】專題:線性回歸

雖然它在形式上非常接近嶺回歸,但是其結果與嶺回歸差别很大。例如,由于其幾何特性,Lasso 正則化傾向于建構稀疏模型;也就是說,它更喜歡将模型系數設定為0。

模型系數的L1- 範數正則化實作的

from sklearn.linear_model import Lasso
model = make_pipeline(GaussianFeatures(30), Lasso(alpha=0.001))
basis_plot(model, title='Lasso Regression')           
【Python資料科學手冊】專題:線性回歸

4、案例:預測自行車流量

首先加載兩個資料集,用日期作索引:

import pandas as pd
counts = pd.read_csv('datalab/5666/FremontBridge.csv', index_col='Date', parse_dates=True)
weather = pd.read_csv('datalab/5666/BicycleWeather.csv', index_col='DATE', parse_dates=True)           

計算每一天的自行車流量,将結果放到一個新的DataFrame中

daily = counts.resample('d').sum()
daily['Total'] = daily.sum(axis=1)
daily = daily[['Total']] # remove other columns           

我們發現同一周内每一天的模式都是不一樣的。是以,我們在資料中加上7 列0~1 值表示星期幾:

days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
for i in range(7):
    daily[days[i]] = (daily.index.dayofweek == i).astype(float)           

我們覺得騎車人數在節假日也有所變化。是以,再增加一清單示當天是否為節假日:

from pandas.tseries.holiday import USFederalHolidayCalendar
cal = USFederalHolidayCalendar()
holidays = cal.holidays('2012', '2016')
daily = daily.join(pd.Series(1, index=holidays, name='holiday'))
daily['holiday'].fillna(0, inplace=True)           

我們還認為白晝時間也會影響騎車人數。是以,用标準的天文計算來添加這列資訊:

def hours_of_daylight(date, axis=23.44, latitude=47.61):
    """Compute the hours of daylight for the given date"""
    days = (date - pd.datetime(2000, 12, 21)).days
    m = (1. - np.tan(np.radians(latitude))
         * np.tan(np.radians(axis) * np.cos(days * 2 * np.pi / 365.25)))
    return 24. * np.degrees(np.arccos(1 - np.clip(m, 0, 2))) / 180.

daily['daylight_hrs'] = list(map(hours_of_daylight, daily.index))
daily[['daylight_hrs']].plot()
plt.ylim(8, 17)           
【Python資料科學手冊】專題:線性回歸

我們還可以增加每一天的平均氣溫和總降雨量。除了降雨量的數值之外,再增加一個标記表示是否下雨(是否降雨量為0)

# 溫度是按照1/10攝氏度統計的,首先轉換為攝氏度
weather['TMIN'] /= 10
weather['TMAX'] /= 10
weather['Temp (C)'] = 0.5 * (weather['TMIN'] + weather['TMAX'])

# precip is in 1/10 mm; convert to inches
weather['PRCP'] /= 254
weather['dry day'] = (weather['PRCP'] == 0).astype(int)

daily = daily.join(weather[['PRCP', 'Temp (C)', 'dry day']])           

最後,增加一個從1 開始遞增的計數器,表示一年已經過去了多少天。這個特征可以讓我們看到每一年自行車流量的增長或減少:

daily['annual'] = (daily.index - daily.index[0]).days / 365.           

資料已經準備就緒,來看看前幾行:

daily.head()           
【Python資料科學手冊】專題:線性回歸

有了這些資料之後,就可以選擇需要使用的列,然後對資料建立線性回歸模型。我們不在模型中使用截距,而是設定fit_intercept = False,因為每一天的總流量(Total 字段)基本上可以作為當天的截距。

# Drop any rows with null values
daily.dropna(axis=0, how='any', inplace=True)

column_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'holiday',
                'daylight_hrs', 'PRCP', 'dry day', 'Temp (C)', 'annual']
X = daily[column_names]
y = daily['Total']

model = LinearRegression(fit_intercept=False)
model.fit(X, y)
daily['predicted'] = model.predict(X)           

最後,對比自行車真實流量(Total 字段)與預測流量(predicted 字段)

daily[['Total', 'predicted']].plot(alpha=0.5);           
【Python資料科學手冊】專題:線性回歸

顯然,我們丢失了一些關鍵特征,尤其是夏天的預測資料。要麼是由于特征沒有收集全(即可能還有其他因素會影響人們是否騎車),要麼是有一些非線性關系我們沒有考慮到(例如,可能人們在溫度過高或過低時都不願意騎車)

評估各個特征對每日自行車流量的影響:

params = pd.Series(model.coef_, index=X.columns)
params           
【Python資料科學手冊】專題:線性回歸

如果不對這些資料的不确定性進行評估,那麼它們很難具有解釋力。可以用自舉重采樣方法快速計算資料的不确定性:

from sklearn.utils import resample
np.random.seed(1)
err = np.std([model.fit(*resample(X, y)).coef_
              for i in range(1000)], 0)
有了估計誤差之後,再來看這些結果:

print(pd.DataFrame({'effect': params.round(0),
                    'error': err.round(0)}))           
【Python資料科學手冊】專題:線性回歸

首先,星期特征是比較穩定的,工作日騎車的人數顯然比周末和節假日要多。其次,白晝時間每增加1 小時,就平均增加129 ± 9 個騎車的人;而溫度每上升1 度,則增加65 ± 4 個騎車的人;如果那天沒下雨,那麼騎車人數增加546 ± 33 人;降雨量每增加1 英寸,騎車人數減少665 ± 62 人。當所有影響因素都生效之後,一年中每多一天騎車人數增加(日環比增幅)28 ± 18 人。

我們的模型的确丢失了一些重要資訊。例如,變量的非線性影響因素(例如降雨和寒冷天氣的影響)和非線性趨勢(例如人們在溫度過高或過低時可能都不願意騎車)在模型中都沒有展現。另外,我們丢掉了一些細顆粒度的資料(例如下雨天的早晨和下雨天的傍晚之間的差異),還忽略了相鄰日期彼此間的相關性(例如下雨的星期二對星期三騎車人數的影響,或者滂沱大雨之後意外的雨過天晴對騎車人數的影響),這些都可能對騎車人數産生影響。現在你手上已經有了工具,如果願意,可以進一步進行分析。