接着上一篇的內容,我們繼續學習數據分析師用到的統計學的內容。
本篇博客包括學習:
散點圖矩陣、相關係數、單樣本t檢驗、基模型、特徵選擇、分箱操作以及殘差圖。
讓我們繼續開始對AQI的分析與預測吧!
AQI分析與預測(下)
5.4 空氣質量主要受哪些因素的影響?
對於空氣質量,我們很可能會關注這個問題,例如,我們可能會產生如下的疑問:
- 人口密度大,是否會對空氣質量造成負面影響?
- 綠化率高,是否會提高空氣質量?
我們可以通過畫圖進行查看:
5.4.1 散點圖矩陣
什麼是散點圖矩陣?
散點圖矩陣圖是可用於比較多個數據集以查找模式和關係。
散點圖矩陣是將任意兩個變量之間的關係都以散點圖的形式(一個二維平面圖)繪製出來。
這裏我們只選取空氣質量指數AQI、人口密度PopulationDensity和綠化覆蓋率GreenCoverageRate來進行繪製。
通過seaborn的pairplot
來繪製散點圖:
sns.pairplot(data[["AQI", "PopulationDensity", "GreenCoverageRate"]])
# sns.pairplot(data[["AQI", "PopulationDensity", "GreenCoverageRate"]], kind="reg")
圖中對腳線發現不是散點圖,因爲橫縱座標的對腳線處是一個變量,一個變量的話就不會給我們繪製散點圖了而是直方圖。
通過散點圖我們可以大致看出每兩個變量之間的關係。但是從上面的圖中我們還不是特別容易清晰的看出變量之間的關係,我們可以通過一種量化的方式來解決,那就是我們可以計算一下相關係數,相關係數就是衡量兩個變量之間的相關性。
5.4.2 相關係數
5.4.2.1 協方差定義
協方差體現的是兩個變量之間的分散性以及兩個變量變化步調是否一致。容易得知,當協方差的兩個變量相同時,協方差就是方差。
通俗的理解:
兩個變量在變化過程中是同方向變化?還是反方向變化?同向或反向程度如何?
5.4.2.2 相關係數定義
相關係數,可以用來體現兩個連續變量之間的相關性,最爲常用的爲皮爾遜相關係數。 兩個變量的相關係數定義爲:
公式含義:就是用X、Y的協方差除以X的標準差和Y的標準差。
統計學中常用相關係數 r 來表示兩變量之間的相關關係。
相關係數 r 的取值範圍爲[-1,1],我們可以根據相關係數的取值來衡量兩個變量的相關性:
r爲正時是正相關,反映當x增加(減少)時,y隨之相應增加(減少);呈正相關的兩個變量之間的相關係數一定爲正值,這個正值越大說明正相關的程度越高。
r爲負時是負相關,反映當x增加(減少)時,y 會相反的減少(增加);呈負相關的兩個變量之間的相關係數一定爲負數。
我們以空氣質量(AQI)與降雨量(Precipitation)爲例,計算二者的相關係數。
先計算一下二者的協方差和相關係數:
x = data["AQI"]
y = data["Precipitation"]
# 計算AQI與Precipitation的協方差。
a = (x - x.mean()) * (y - y.mean())
cov = np.sum(a) / (len(a) - 1)
print("協方差:", cov)
# 計算AQI與Precipitation的相關係數。
corr = cov / np.sqrt(x.var() * y.var())
print("相關係數:", corr)
結果:
協方差: -10278.683921772239
相關係數: -0.40159811621922753
我們有最簡便的求協方差和相關係數的方法:
通過
Series.cov( )
方法來計算協方差;
通過Series.corr( )
方法來計算相關係數;
print("協方差:", x.cov(y))
print("相關係數:", x.corr(y))
結果:
協方差: -10407.167470794398
相關係數: -0.4043784360785916
不過,DataFrame對象提供了計算相關係數的方法,我們可以直接使用。
data.corr()
將data中所有變量兩兩一組求出他們的相關係數。
結果:
爲了能夠更清晰的呈現相關數值,我們可以使用熱力圖來展示相關係數。
plt.figure(figsize=(15, 10))
ax = sns.heatmap(data.corr(), cmap=plt.cm.RdYlGn, annot=True, fmt=".2f")
注意:
Matplotlib 3.1.1版本的bug,heatmap的首行與末行會顯示不全。
可手動調整y軸的範圍來進行修復。(老版本的Matplotlib不需要調整y軸範圍。)
手動調整代碼:
a, b = ax.get_ylim()
ax.set_ylim(a + 0.5, b - 0.5)
使用seaborn中的heatmap( )製作熱力圖。
cmap=plt.cm.RdYlGn
指定顏色,此處指定的是綠色到紅色。
annot=True
顯示數字。
fmt=".2f"
保留兩位小數。
結果:
從圖中可以看到,維度Latitude 對AQI 的相關性是最大的,爲0.55。是正相關,也就意味着,隨着維度的增大AQI也是增大的,越往北維度越大,AQI就越大,AQI越大意味着空氣質量越差,所以南方空氣要比北方空氣要好。
5.4.3 結果統計
從結果可知,空氣質量指數主要受降雨量(-0.40)與維度(0.55)影響。
- 降雨量越多,空氣質量越好。
- 維度越低,空氣質量越好。
此外,我們還能夠發現其他一些明顯的細節:
5.5 關於空氣質量的驗證
江湖傳聞,全國所有城市的空氣質量指數均值在71左右,請問,這個消息可靠嗎?
城市平均空氣質量指數,我們可以很容易的進行計算:
計算均值:
data["AQI"].mean()
結果:
75.3343653250774
該需求是要驗證樣本均值是否等於總體均值,根據場景,我們可以使用單樣本 t 檢驗,置信度爲95%。
r = stats.ttest_1samp(data["AQI"], 71)
print("t值:", r.statistic)
print("p值:", r.pvalue)
這裏檢驗AQI空氣質量的均值與71是否顯著。
結果:
我們可以看到,P值大於0.05,故我們無法拒絕原假設,因此接受原假設。
什麼是單樣本 t 檢驗?
使用
ttest_1samp()
函數可以進行單樣本T檢驗,比如檢驗一列數據的均值與1的差異是否顯著。
stats.ttest_1samp(data,1)
返回結果會返回t值和p值。
這裏爲什麼不是選擇A選項,我們要清楚,±1.96倍的標準差,是正態分佈在置信度爲95%下的臨界值,嚴格來說,對 t 分佈不是如此。只不過,當樣本容量較大時,t 分佈近似於正態分佈。當樣本容量比較小時,二者會有較大的差異。
我們可以獲取更加準確的置信區間:
mean = data["AQI"].mean()
std = data["AQI"].std()
stats.t.interval(0.95,df=len(data)-1,loc=mean,scale=std/np.sqrt(len(data)))
結果:
由此,我們就計算出全國所有城市平均空氣質量指數,95%的可能大致在71.05~80.57之間。
5.6 對空氣質量進行檢測
對於某城市,如果我們已知降雨量,溫度,經緯度等指標,我們是否能夠預測該城市的空氣質量指數呢?答案是肯定的。如果我們通過對以往的數據,去建立一種模式,然後這種模式去應用於未知的數據,進而預測結果。
5.6.1 數據轉換
對於模型來說,內部進行的都是數學上的運算。在進行建模之前,我們需要首先進行轉換,將類別變量轉換爲離散變量。
data數據中是否是沿海城市這一列是類別變量,所以我們需要將其進行轉換:
data["Coastal"] = data["Coastal"].map({"是": 1, "否": 0})
data["Coastal"].value_counts()
結果:內陸城市(0)有245個,沿海城市(1)有80個。
5.6.2 基模型
首先,我們不進行任何處理,建立一個模型。後續的操作,可以在此基礎上進行改進。
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
X = data.drop(["City","AQI"], axis=1)
y = data["AQI"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
lr = LinearRegression()
lr.fit(X_train, y_train)
print(lr.score(X_train, y_train))
print(lr.score(X_test, y_test))
結果:
訓練集0.45,測試集0.40 。
接下來繪製下預測的結果:
y_hat = lr.predict(X_test)
plt.figure(figsize=(15, 5))
plt.plot(y_test.values, "-r", label="真實值", marker="o")
plt.plot(y_hat, "-g", label="預測值", marker="D")
plt.legend(loc="upper left")
plt.title("線性迴歸預測結果", fontsize=20)
5.6.3 特徵選擇
剛纔,我們使用所有可能的原始數據作爲特徵,建立模型,然而,特徵並非越多越好,有些特徵可能對模型質量並沒有什麼改善,我們可以進行刪除,同時,也能夠提高模型訓練速度。
5.6.3.1 RFECV(遞歸特徵消除)
from sklearn.feature_selection import RFECV
# estimator: 要操作的模型。
# step: 每次刪除的變量數。
# cv: 使用的交叉驗證折數。
# n_jobs: 併發的數量。
# scoring: 評估的方式。
rfecv = RFECV(estimator=lr, step=1, cv=5, n_jobs=-1, scoring="r2")
rfecv.fit(X_train, y_train)
# 返回經過選擇之後,剩餘的特徵數量。
print("返回經過選擇之後,剩餘的特徵數量",rfecv.n_features_)
# 返回經過特徵選擇後,使用縮減特徵訓練後的模型。
print("返回經過特徵選擇後,使用縮減特徵訓練後的模型",rfecv.estimator_)
# 返回每個特徵的等級,數值越小,特徵越重要。
print("返回每個特徵的等級,數值越小,特徵越重要",rfecv.ranking_)
# 返回布爾數組,用來表示特徵是否被選擇。
print("返回布爾數組,用來表示特徵是否被選擇",rfecv.support_)
# 返回對應數量特徵時,模型交叉驗證的評分。
print("返回對應數量特徵時,模型交叉驗證的評分",rfecv.grid_scores_)
結果:
結果[1 2 1 1 1 1 3 1 1 1]中,特徵1就是索引爲0的變量,按索引0開始排序。特徵等級越小越重要,這裏的2和3對應的兩個變量就不是很重要了。
False表示對應的特徵沒有用到。
通過結果可知,我們成功刪除了兩個特徵。我們可以繪製下,在特徵選擇過程中,使用交叉驗證獲取的R²值。
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_, marker="o")
plt.xlabel("特徵數量")
plt.ylabel("交叉驗證$R^2$值")
結果:
然後,我們對測試集應用這種特徵選擇(變換),進行測試,獲取測試結果。
print("剔除的變量:", X_train.columns[~rfecv.support_])
X_train_eli = rfecv.transform(X_train)
X_test_eli = rfecv.transform(X_test)
print(rfecv.estimator_.score(X_train_eli, y_train))
print(rfecv.estimator_.score(X_test_eli, y_test))
結果:
我們發現,經過特徵選擇後,消除了GDP與PopulationDensity兩個特徵,而使用剩餘8個特徵訓練的模型,與之前未消除特徵訓練的模型(使用全部10個特徵訓練的模型),無論在訓練集還是測試集的表現上,都幾乎相同,這就可以證明,我們清除的這兩個特徵,確實對擬合目標(y值)沒有什麼幫助,可以去掉。
5.7 異常的值處理
如果數據中存在異常值,很有可能會影響模型的效果,因此,我們在建模之間,非常有必要對異常值進行處理。
5.7.1 使用臨界值替換
我們可以依據箱線圖判斷羣點的原則去探索異常值,然後使用臨界值替換掉異常值。
我們應該使用什麼數據去計算臨界值呢?
答案是:訓練集(X_train)
因爲對於測試集來說對我們永遠都是未知的效果。也就是在我們訓練期間永遠不能使用測試集。
# Coastal是類別變量,映射爲離散變量,不會有異常值。
for col in X.columns.drop("Coastal"):
if pd.api.types.is_numeric_dtype(X_train[col]):
quartile = np.quantile(X_train[col], [0.25, 0.75])
IQR = quartile[1] - quartile[0]
lower = quartile[0] - 1.5 * IQR
upper = quartile[1] + 1.5 * IQR
X_train[col][X_train[col] < lower] = lower
X_train[col][X_train[col] > upper] = upper
X_test[col][X_test[col] < lower] = lower
X_test[col][X_test[col] > upper] = upper
5.7.2 效果對比
去除異常值後,我們使用新的訓練集與測試集來評估模型的效果。
lr.fit(X_train, y_train)
print(lr.score(X_train, y_train))
print(lr.score(X_test, y_test))
結果:
效果相對於之前,似乎有着輕微的改進,不過並不明顯,我們可以使用RFECV在去除異常值的數據上,再次嘗試。
rfecv = RFECV(estimator=lr, step=1, cv=5, n_jobs=-1, scoring="r2")
rfecv.fit(X_train, y_train)
print("返回經過選擇之後,剩餘的特徵數量",rfecv.n_features_)
print("返回每個特徵的等級,數值越小,特徵越重要",rfecv.ranking_)
print("返回布爾數組,用來表示特徵是否被選擇",rfecv.support_)
print("返回對應數量特徵時,模型交叉驗證的評分",rfecv.grid_scores_)
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_, marker="o")
plt.xlabel("特徵數量")
plt.ylabel("交叉驗證$R^2$值")
結果:
print("剔除的變量:", X_train.columns[~rfecv.support_])
# X_train_eli = rfecv.transform(X_train)
# X_test_eli = rfecv.transform(X_test)
# 爲了方便後面列的篩選操作,這裏我們換一種方式轉換。
X_train_eli = X_train[X_train.columns[rfecv.support_]]
X_test_eli = X_test[X_test.columns[rfecv.support_]]
print(rfecv.estimator_.score(X_train_eli, y_train))
print(rfecv.estimator_.score(X_test_eli, y_test))
結果:
5.7.3 分箱操作
注意:
分箱後,我們不能將每個區間映射爲離散數值,而是應當使用One-Hot編碼。
from sklearn.preprocessing import KBinsDiscretizer
# KBinsDiscretizer K個分箱的離散器。用於將數值(通常是連續變量)變量進行區間離散化操作。
# n_bins:分箱(區間)的個數。
# encode:離散化編碼方式。分爲:onehot,onehot-dense與ordinal。
# onehot:使用獨熱編碼,返回稀疏矩陣。
# onehot-dense:使用獨熱編碼,返回稠密矩陣。
# ordinal:使用序數編碼(0,1,2……)。
# strategy:分箱的方式。分爲:uniform,quantile,kmeans。
# uniform:每個區間的長度範圍大致相同。
# quantile:每個區間包含的元素個數大致相同。
# kmeans:使用一維kmeans方式進行分箱。
k = KBinsDiscretizer(n_bins=[4, 5, 14, 6], encode="onehot-dense", strategy="uniform")
# 定義離散化的特徵。
discretize = ["Longitude", "Temperature", "Precipitation", "Latitude"]
r = k.fit_transform(X_train_eli[discretize])
r = pd.DataFrame(r, index=X_train_eli.index)
# 獲取除離散化特徵之外的其他特徵。
X_train_dis = X_train_eli.drop(discretize, axis=1)
# 將離散化後的特徵與其他特徵進行重新組合。
X_train_dis = pd.concat([X_train_dis, r], axis=1)
# 對測試集進行同樣的離散化操作。
r = pd.DataFrame(k.transform(X_test_eli[discretize]), index=X_test_eli.index)
X_test_dis = X_test_eli.drop(discretize, axis=1)
X_test_dis = pd.concat([X_test_dis, r], axis=1)
# 查看轉換之後的格式。
display(X_train_dis.head())
代碼解析:
KBinsDiscretizer
K個分箱的離散器。用於將數值(通常是連續變量)變量進行區間離散化操作。
n_bins
:分箱(區間)的個數。
encode
:離散化編碼方式。分爲:onehot,onehot-dense與ordinal。
onehot:使用獨熱編碼,返回稀疏矩陣。
onehot-dense:使用獨熱編碼,返回稠密矩陣。
ordinal:使用序數編碼(0,1,2……)。
strategy
:分箱的方式。分爲:uniform,quantile,kmeans。
uniform:每個區間的長度範圍大致相同。
quantile:每個區間包含的元素個數大致相同。
kmeans:使用一維kmeans方式進行分箱。
結果:
這樣,我們就可以對轉換後的數據進行訓練了。
lr.fit(X_train_dis, y_train)
print(lr.score(X_train_dis, y_train))
print(lr.score(X_test_dis, y_test))
結果:
離散化後,模型效果有了進一步的提升。
注意:
此時,x對y階梯式的影響用分箱操作。
5.8 殘差圖分析
殘差,就是模型預測值與真實值之間的差異。
我們可以繪製殘差圖,來對迴歸模型進行評估。
殘差圖的橫座標爲預測值,縱座標爲殘差值。
5.8.1 異方差性
對於一個好的迴歸模型,誤差應該是隨機分配的。因此,殘差也應隨機分佈於中心線附近。如果我們從殘差圖中找出規律,這意味着模型遺漏了某些能夠影響殘差的解釋信息。
異方差性,是指殘差具有明顯的方差不一致性。這裏我們異常值處理前後的兩組數據,分別訓練模型,然後觀察殘差的效果。
fig, ax = plt.subplots(1, 2)
fig.set_size_inches(15, 5)
data = [X_train, X_train_dis]
title = ["原始數據", "處理後數據"]
for d, a, t in zip(data, ax, title):
model = LinearRegression()
model.fit(d, y_train)
y_hat_train = model.predict(d)
residual = y_hat_train - y_train.values
a.set_xlabel("預測值")
a.set_ylabel(" 殘差")
a.axhline(y=0, color="red")
a.set_title(t)
sns.scatterplot(x=y_hat_train, y=residual, ax=a)
結果:
在左圖中我們發現,隨着預測值的增大,模型的誤差也在增大,對於此種情況,我們可以使用對目標y值取對數的方式處理。
model = LinearRegression()
y_train_log = np.log(y_train)
y_test_log = np.log(y_test)
model.fit(X_train, y_train_log)
y_hat_train = model.predict(X_train)
residual = y_hat_train - y_train_log.values
plt.xlabel("預測值")
plt.ylabel(" 殘差")
plt.axhline(y=0, color="red")
sns.scatterplot(x=y_hat_train, y=residual)
結果:
此時,異方差性得到解決。同時,模型的效果也可能會得到一定的提升。
5.8.2 離羣點
然而,我們可以通過繪製殘差圖,通過預測值和實際值之間的關係,來檢測離羣點。
y_hat_train = lr.predict(X_train_dis)
residual = y_hat_train - y_train.values
r = (residual - residual.mean()) / residual.std()
plt.xlabel("預測值")
plt.ylabel(" 殘差")
plt.axhline(y=0, color="red")
sns.scatterplot(x=y_hat_train[np.abs(r) <= 2], y=residual[np.abs(r) <= 2], color="b", label="正常值")
sns.scatterplot(x=y_hat_train[np.abs(r) > 2], y=residual[np.abs(r) > 2], color="orange", label="異常值")
結果:
X_train_dis_filter = X_train_dis[np.abs(r) <= 2]
y_train_filter = y_train[np.abs(r) <= 2]
lr.fit(X_train_dis_filter, y_train_filter)
print(lr.score(X_train_dis_filter, y_train_filter))
print(lr.score(X_test_dis, y_test))
結果: