機器學習開放課程(二):使用Python可視化數據
來自專欄論智8 人贊了文章
【編者按】機器學習開放課程第二課,與http://Mail.Ru Search數據分析負責人Egor Polusmak和Mail.Ru Group數據科學家Yury Kashnitsky一起探索如何使用Python可視化數據。
原文地址:Open Machine Learning Course. Topic 2. Visual Data Analysis with Python
聲明:感謝原作者Egor Polusmak和Yury Kashnitsky授權論智編譯,未經授權請勿轉載。詳情見轉載須知在機器學習領域中,可視化並不僅僅用來製作漂亮的報表。項目的各個階段都大量使用可視化技術。
在開始一項新任務時,通過可視化手段探索數據往往是任務的第一步。我們通過圖表匯總數據,放棄無關緊要的細節。相比直接閱讀許多行原始數據,可視化能更好地幫助人類把握數據的要點。令人驚嘆的是,僅僅通過可視化工具創建一些看上去再簡單不過的圖表,就能獲得大量洞見。
接著,在分析模型表現和模型報告的結果時,我們也常常使用可視化。有時候,為了理解複雜的模型,我們需要將高維空間映射為視覺上更直觀的二維或三維圖形。
總而言之,可視化是一個相對快捷的從數據中獲取新知的手段。因此,學習這一極為重要的技術,並將其納入你的日常機器學習工具箱,是至關重要的。
本文將使用pandas
、matplotlib
和seaborn
等流行的庫,帶你上手可視化。
概覽
- 數據集
- 單變數可視化 數量和類型分布
- 多變數可視化 變數間的相互作用
- 全數據集 一窺高維空間
- 作業二
- 相關資源
下面的材料以Jupyter notebook的形式查看效果最佳。如果你克隆了本教程的代碼倉庫,你也可以在本地運行。
1. 數據集
首先初始化環境:
import numpy as npimport pandas as pdpd.options.display.max_columns = 12# 禁用Anaconda警告import warningswarnings.simplefilter(ignore)# 在Jupyter Notebook內部顯示圖形%matplotlib inlineimport matplotlib.pyplot as plt# 我們將使用Seaborn庫import seaborn as snssns.set()# SVG格式的圖像更清晰%config InlineBackend.figure_format = svg# 增加默認的繪圖尺寸from pylab import rcParamsrcParams[figure.figsize] = 5, 4
在第一篇文章中,我們使用的是某電信運營商的客戶離網率數據。這裡我們仍舊使用這個數據集。
df = pd.read_csv(../../data/telecom_churn.csv)df.head()
數據集的特徵:
2. 單變數可視化
單變數(univariate)分析一次只關注一個變數。當我們獨立地分析一個特徵時,我們通常最關心的是該特徵的值的分布,而忽略數據集中的其他變數。
下面我們將考慮不同統計類型的變數,以及相應的可視化工具。
2.1 數量特徵
數量特徵(quantitative feature)的值為有序數值。這些值可能是離散的,例如整數,也可能是連續的,例如實數,通常用於表示技術和度量。
直方圖和密度圖
最簡單的查看數值變數分布的方法是使用DataFrame
的hist()
方法繪製它的直方圖(histogram)。
features = [Total day minutes, Total intl calls]df[features].hist(figsize=(12, 4));
直方圖依照相等的範圍將值分組為柱。直方圖的形狀可能包含了數據分布的線索:高斯、指數,等等。當分布基本上很有規律,但有一些異常值時,你也可以通過直方圖辨認出形狀的歪斜之處。當你使用預設某一特定分布類型(通常是高斯)的機器學習方法時,知道特徵值的分布是非常重要的。
在以上圖形中,我們看到變數Total day minutes(每日通話時長)呈正態分布(譯者註:正態分布即高斯分布),而Total intl calls(總國際呼叫數)顯著右傾(它右側的尾巴更長)。
除了直方圖,理解分布的另一個(經常更清楚的)方法是密度圖(density plots),也叫(更正式的名稱)核密度圖(Kernel Density Plots)。它們可以看成是直方圖平滑過的版本。相比直方圖,它們的主要優勢是不依賴於柱的尺寸。讓我們為上面兩個變數創建密度圖:
df[features].plot(kind=density, subplots=True, layout=(1, 2), sharex=False, figsize=(12, 4));
我們也可以使用seaborn
的distplot()
方法繪製觀測數據的分布。例如,Total day minutes(每日通話時長)的分布。默認情況下,圖形將同時顯示直方圖和核密度估計(kernel density estimation,KDE)。
sns.distplot(df[Total intl calls]);
這裡直方圖的柱形的高度已經歸一過了,表示的是密度而不是樣本數。
箱形圖
箱形圖(box plot)是另一種有用的可視化圖形。使用seaborn
繪製箱形圖:
_, ax = plt.subplots(figsize=(3, 4))sns.boxplot(data=df[Total intl calls], ax=ax);
箱形圖的主要組成部分是箱子(box)(顯然,這是它被稱為箱形圖的原因),須(whisker)和一些單獨的數據點(離群值)。
箱子顯示了分布的四分位距;它的長度由25%(Q1,下四分位數)和75%(Q3,上司分位數)決定。箱中的水平線表示中位數(50%)。
從箱子處延伸出來的線被稱為須。它們表示數據點的總體散布,具體而言,是位於區間(Q1 - 1.5xIQR, Q3 + 1.5xIQR)的數據點,其中IQR = Q3 - Q1,也就是四分位距。
離群值是須之外的數據點,它們作為單獨的數據點,沿著中軸繪製。
我們可以看到,在我們的數據中,大量的國際呼叫是相當少見的。
提琴形圖
我們最後考慮的分布圖形是提琴形圖(violin plot)。
下圖左側是箱形圖,右側是提琴形圖。
_, axes = plt.subplots(1, 2, sharey=True, figsize=(6, 4))sns.boxplot(data=df[Total intl calls], ax=axes[0]); sns.violinplot(data=df[Total intl calls], ax=axes[1]);
箱形圖和提琴形圖的區別是,箱形圖顯示了單獨樣本的特定統計數據,而提琴形圖聚焦於平滑後的整體分布。
describe()
圖形工具之外,我們可以使用DataFrame
的describe()
方法來獲取分布的精確數值統計:
df[features].describe()
describe()
的輸出基本上是自解釋性的。
2.2 類別和二元特徵
類別特徵(categorical features take)具有固定數目的值。每個值將一個觀測數據分配到相應的組,這些組稱為類別(category)。類別反映了樣本的某個定性屬性。二元(binary)變數是一個重要的類別變數的特例,其中類別的可能值正好為2. 如果類別變數的值具有順序,稱為有序(ordinal)類別變數。
頻率表
讓我們查看下數據集的分類平滑:目標變數離網率的分布。首先,我們使用value_counts()
得到一張頻率表:
df[Churn].value_counts()False 2850True 483Name: Churn, dtype: int64
默認情況下,頻率由高到低排列。
在我們的例子中,數據是失衡的,也就是說,數據集中忠實客戶和不忠實客戶的比例並不相等。只有一小部分的客戶取消了他們的電信服務訂閱。我們將在以後的文章中看到,這一事實可能暗示衡量分類表現時存在一些限制,以後我們可能額外懲罰我們的模型在預測少數「離網」分類時所犯的錯誤。
條形圖
頻率表的圖形化表示是條形圖。創建條形圖最簡單的方法是使用seaborn
的countplot()
函數。seaborn
中還有一個函數,起了一個令人困惑的名字(barplot()
),barplot()
絕大部分情況下用於表示以某個類別特徵分組的數值變數的一些基本統計數據。
_, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))sns.countplot(x=Churn, data=df, ax=axes[0]);sns.countplot(x=Customer service calls, data=df, ax=axes[1]);
儘管條形圖和上面提到的直方圖看起來很像,它們是不一樣的:
- 直方圖用於查看數值變數的分布,而條形圖用於類別特徵。
- 直方圖的X軸是數值;條形圖的X軸可能是任何類型:數字、字元串、布爾值。
- 直方圖的X軸是一根笛卡爾坐標軸;條形的順序沒有事先定義。不過,值得注意的是,條形經常按照高度排序,也就是值的頻率。同時,當我們考慮有序變數(例如Customer service calls(客服呼叫數))時,條形通常按照變數的值排序。
左側的圖形生動地顯示了目標變數的失衡性。右側Customer service calls(客服呼叫數)的條形圖暗示了大部分客戶最多打了2-3個客服電話就解決了他們的問題。不過,既然我們想要預測少數分類,我們可能對少數不滿意的客戶的表現更感興趣。很可能條形圖的尾巴包含了大部分的離網客戶。目前這只是假想,讓我們來看一些更有趣、更強大的可視化技術。
3. 多變數可視化
多變數(multivariate)圖形讓我們得以在單張圖像中查看兩個以上變數的聯繫。和單變數圖形一樣,可視化的類型取決於將要分析的變數的類型。
3.1 數量——數量
我們先來看看數量變數之間的相互作用。
相關矩陣
讓我們看下數據集中的數值變數的相關性。這一信息很重要,因為有一些機器學習演算法(比如,線性回歸和邏輯回歸)不能很好地處理高度相關的輸入變數。
首先,我們使用DataFrame
的corr()
方法計算出每對特徵間的相關性。接著,我們將所得的相關矩陣(correlation matrix)傳給seaborn
的heatmap()
方法,該方法根據提供的數值,渲染出一個基於色彩編碼的矩陣:
# 丟棄非數值變數numerical = list(set(df.columns) - set([State, International plan, Voice mail plan, Area code, Churn, Customer service calls]))# 計算和繪圖corr_matrix = df[numerical].corr()sns.heatmap(corr_matrix);
從上圖我們可以看到,Total day charge(日話費總額)直接基於電話的分鐘數計算得到(Total day minutes),這樣的變數有4個。這4個變數稱為因變數(dependent variable),可以直接去除,因為它們並不貢獻任何額外信息。讓我們去掉它們:
numerical = list(set(numerical) - set([Total day charge, Total eve charge, Total night charge, Total intl charge]))
散點圖
散點圖(scatter plot)將兩個數值變數的值顯示為二位空間中的笛卡爾坐標(Cartesian coordinate)。還有三維的散點圖。
讓我們試下matplotlib
庫的scatter()
方法:
plt.scatter(df[Total day minutes], df[Total night minutes]);
我們得到了兩個正態分布變數的散點圖,這張圖沒什麼意思。看起來這兩個變數並不相關,因為類似橢圓的形狀和軸是對齊的。
seaborn
庫創建的散點圖有一個略微奇特的選項:
sns.jointplot(x=Total day minutes, y=Total night minutes, data=df, kind=scatter);
jointplot()
函數繪製了兩張直方圖,某些情形下它們可能會有用。這一函數還可以讓我們繪製平滑過的joint plot:
sns.jointplot(Total day minutes, Total night minutes, data=df, kind="kde", color="g");
這個基本上是我們之前討論過的核密度圖的雙變數版本。
散點圖矩陣
在某些情形下,我們可能想要繪製如下所示的散點圖矩陣(scatterplot matrix)。它的對角線包含變數的分布,而每對變數的散點圖填充了矩陣的其餘部分。
# 使用SVG格式可能導致pairplot變得非常慢%config InlineBackend.figure_format = pngsns.pairplot(df[numerical]);
有時候,這樣的可視化可能幫我們從數據中得出一些結論。
3.2 數量——類別
在這一小節中,讓我們的圖形更有趣一點。我們將嘗試從數值和類別特徵的相互作用中得到離網預測的新洞見。
更具體地,讓我看看輸入變數和目標變數離網的關係。
先前我們了解了散點圖。散點圖中的數據點可以通過色彩或尺寸進行編碼,以便在同一張圖像中包括第三個類別變數的值。我們可以通過之前的scatter()
函數達成這一點,不過,這次讓我們換換花樣,用lmplot()
函數的hue
參數來指定感興趣的類別特徵:
sns.lmplot(Total day minutes, Total night minutes, data=df, hue=Churn, fit_reg=False);
看起來佔少數的不忠實客戶偏向右上角;也就是傾向於在白天和夜間打更多電話的客戶。但這不是非常明顯,我們也不會基於這一圖形下任何確定的結論。
現在,讓我們創建箱形圖,以可視化兩個互斥分組中的數值變數分布的統計數據:忠實客戶(Churn=False
)和離網客戶(Churn=True
)。
# 有時我們可以將有序變數作為數值變數分析numerical.append(Customer service calls) fig, axes = plt.subplots(nrows=3, ncols=4, figsize=(10, 7))for idx, feat in enumerate(numerical): ax = axes[int(idx / 4), idx % 4] sns.boxplot(x=Churn, y=feat, data=df, ax=ax) ax.set_xlabel() ax.set_ylabel(feat) fig.tight_layout();
從這一圖表中,我們可以看到,兩組之間分歧最大的分布是這三個變數:Total day minutes(日通話分鐘數)、Customer service calls(客服呼叫數)、Number vmail messages(語音郵件數)。在後續的課程中,我們將學習如何使用隨機森林(Random Forest)或梯度提升(Gradient Boosting)來判定特徵對分類的重要性;那時我們將看到,前兩個特徵對於離網預測而言確實非常重要。
讓我們分別看下忠實客戶和不忠實客戶的日通話分鐘數。我們將創建箱形圖和提琴形圖。
_, axes = plt.subplots(1, 2, sharey=True, figsize=(10, 4)) sns.boxplot(x=Churn, y=Total day minutes, data=df, ax=axes[0]);sns.violinplot(x=Churn, y=Total day minutes, data=df, ax=axes[1]);
在這一情形下,提琴形圖並沒有提供關於數據的額外信息,因為箱形圖已經告訴了我們一切:不忠實客戶傾向於打更多的電話。
一個有趣的觀察:平均而言,終止他們的協議的客戶是通訊服務更活躍的用戶。也許他們對話費不滿意,所以預防離網的一個可能措施是降低通話費率。公司需要進行額外的經濟分析,以查明這樣的措施是否有利。
當我們想要一次分析兩個類別維度下的數量變數時,可以用seaborn
庫的factorplot()
函數。例如,在同一圖形中可視化Total day minutes(日通話分鐘數)和兩個類別變數的相互作用:
sns.factorplot(x=Churn, y=Total day minutes, col=Customer service calls, data=df[df[Customer service calls] < 8], kind="box", col_wrap=4, size=3, aspect=.8);
從上圖我們可以總結出,從4次呼叫開始,Total day minutes(日通話分鐘數)可能不再是客戶離網的主要因素。也許,除了我們之前猜測的話費,有些客戶因為其他問題對服務不滿意,或許這導致了日通話分鐘數較少。
3.3 類別——類別
正如我們之前提到的,變數Customer service calls(客服呼叫數)的唯一值極少,因此,既可以看成數值變數,也可以看成有序類別變數。我們已經通過計數圖(countc plot)查看過它的分布了。現在我們感興趣的是這一有序特徵和目標變數Churn(離網)之間的關係。
讓我們再一次使用計數圖看下客服呼叫數的分布。這次,我們同時傳入hue=Churn
參數,以便在圖形中加入類別維度:
sns.countplot(x=Customer service calls, hue=Churn, data=df);
觀察:呼叫客服達到4次以上後,離網率顯著增加了。
現在讓我們看下Churn(離網)和二元特徵International plan(國際套餐)、Voice mail plan(語音郵件套餐)的關係。
_, axes = plt.subplots(1, 2, sharey=True, figsize=(10, 4)) sns.countplot(x=International plan, hue=Churn, data=df, ax=axes[0]);sns.countplot(x=Voice mail plan, hue=Churn, data=df, ax=axes[1]);
觀察:開通國際套餐後,離網率會高很多;使用國際套餐是一個強烈的特徵。我們在語音郵件套餐上沒有觀察到相同的效應。
列聯表
除了使用圖形進行類別分析之外,還可以使用統計學的傳統工具:列聯表(contingency table),又稱為交叉製表(cross tabulation),使用表格形式表示多個類別變數的頻率分布。特別地,它讓我們可以通過查看一列或一行來得知某個變數在另一變數的作用下的分布。
讓我們通過交叉製表看看Churn(離網)和類別變數State(州)的關係:
pd.crosstab(df[State], df[Churn]).T
State(州)的不同值很多:51. 我們看到每個周只有少量數據點——每個州只有3到17個客戶拋棄了運營商。讓我們暫時忽略這一點,計算每個州的離網率,由高到低排列:
df.groupby([State])[Churn]. agg([np.mean]). sort_values(by=mean, ascending=False).T
乍看起來,新澤西和加利福尼亞的離網率超過了25%,夏威夷和阿拉斯加的離網率則不到6%. 然而,這些結論是基於極少的樣本得出的,我們的觀察可能僅僅是這一特定數據集的性質。我們可以通過Matthews和Cramer相關性假說確認這一點,不過這個超出了這篇文章的範圍。
4. 全數據集
4.1 幼稚方法
上面我們查看了數據集的不同刻面(facet),猜測感興趣的特徵,每次選擇其中的一小部分進行可視化。我們一次僅僅處理兩到三個變數,能比較容易地觀察到數據的結構和關係。但是,如果我們想一下子顯示所有特徵呢?如何確保最終的可視化仍然是可解釋的?
我們可以為整個數據集使用hist()
或者pairplot()
方法,同時查看所有的特徵。不過,當特徵數目足夠多的時候,這樣的可視化分析很快就變得緩慢和低效。另外,我們其實仍然可以成對地分析變數,而不用一下子分析所有變數。
4.2 降維
大多數現實世界的數據集有很多特徵,有時有上萬個特徵。每一個特徵都可以被看成數據點空間的一維。因此,我們經常需要處理高維數據集,可視化整個高維數據集相當難。
為了從整體上查看一個數據集,我們需要在不損失很多數據信息的前提下,降低用於可視化的維度。這一任務稱為降維(dimensionality reduction)。降維是一個無監督學習(unsupervised learning)問題,因為我們需要在不藉助任何監督輸入的前提下,從數據自身得到新的低維特徵。
主成分分析(Principal Component Analysis, PCA)是一個著名的降維方法,我們會在之後的課程中討論它。主成分分析有一個限制,它是線性(linear)演算法,因而對數據有某些特定的限制。
有許多非線性方法,統稱流形學習(Manifold Learning)。最著名的流形學習方法之一是t-SNE。
4.3 t-SNE
讓我們為離網數據創建一個t-SNE表示。
這一方法的名字看起來很複雜,有些嚇人:t分布隨機近鄰嵌入(t-distributed Stohastic Neighbor Embedding)。它的數學也很令人印象深刻(我們不會在這裡深究數學,勇敢的讀者可以閱讀Laurens van der Maaten和Geoffrey Hinton在JMLR上發表的原論文)。它的基本思路很簡單:為高維特徵空間在二維平面(或三維超平面,不過基本上總是使用二維空間)上尋找一個投影,使得在原本的n維空間中相距很遠的數據點在屏幕上同樣相距較遠。而原本相近的點在平面上仍然相近。
本質上,近鄰嵌入尋找保留了樣本的鄰居關係的新的維度較低的數據表示。
現在讓我們做些練習。首先,載入類:
from sklearn.manifold import TSNEfrom sklearn.preprocessing import StandardScaler
我們去除State(州)和離網(Churn)變數,然後用 pandas.Series.map()
方法將二元特徵的「Yes」/「No」轉換成數值:
X = df.drop([Churn, State], axis=1)X[International plan] = X[International plan]. map({Yes: 1, No: 0})X[Voice mail plan] = X[Voice mail plan]. map({Yes: 1, No: 0})
我們同樣需要歸一化數據。我們從每個變數中減去均值,然後除以標準差。這些都可以使用StandardScaler
來完成。
scaler = StandardScaler()X_scaled = scaler.fit_transform(X)
現在可以構建t-SNE表示了:
tsne = TSNE(random_state=17)tsne_repr = tsne.fit_transform(X_scaled)
然後可視化它的圖形:
plt.scatter(tsne_repr[:, 0], tsne_repr[:, 1]);
讓我們根據離網情況給t-SNE表示加上色彩(綠色表示忠實用戶,紅色表示不忠實用戶)。
plt.scatter(tsne_repr[:, 0], tsne_repr[:, 1], c=df[Churn].map({False: green, True: red}));
我們可以看到,離網的客戶集中在低維特徵空間的一小部分區域。
為了更好地理解這一圖像,我們可以使用剩下的兩個二元特徵給圖像著色:International plan(國際套餐)和Voice mail plan(語音郵件套餐)。綠色代表相應的二元特徵是正值。
_, axes = plt.subplots(1, 2, sharey=True, figsize=(12, 5))for i, name in enumerate([International plan, Voice mail plan]): axes[i].scatter(tsne_repr[:, 0], tsne_repr[:, 1], c=df[name].map({Yes: green, No: red})) axes[i].set_title(name)
現在很清楚了,許多退訂的不滿意客戶集中在西南聚類(表示開通了國際套餐但沒有開通語音郵件套餐)。
最後,讓我們了解下t-SNE的缺陷:
- 高計算複雜度。
scikit-learn
的實現在真實任務中往往不太管用。如果你有大量樣本,你應該轉而使用Multicore-TSNE(多核)。 - 隨機數種子的不同會導致圖形大不相同,這給解釋帶來了困難。請參考文末相關資源給出的t-SNE教程。通常而言,你不應該基於這些圖像做出任何深遠的結論,因為它可能和單純的猜測差不多。當然,t-SNE圖像中的某些發現可能會啟發一個想法,這個想法可以通過更全面深入的研究得到確認,但這並不經常發生。
偶爾,t-SNE可以讓你從數據中得到非常好的直覺。下面的論文展示了一個這樣的例子:Visualizing MNIST(可視化MNIST)。
有時t-SNE真的能夠幫助你更好地理解數據,有時t-SNE能夠幫助你畫出聖誕樹玩具
5. 作業二
第二次作業將分析心血管疾病數據集。克隆教程倉庫,在本地編輯這個Jupyter notebook(你需要填充缺失的Python代碼),並在這個Google表單中選擇正確答案(提交表單之後,在截止日期之前,仍然可以編輯你的回答)。請保持電子郵件地址和之前的課程與作業中登記的一致。
截止日期: February 18, 23:59 CET
6. 相關資源
- 本次教程中使用的庫的官方文檔:matplotlib、seaborn、pandas。
- 使用
seaborn
創建的圖形樣例畫廊。 scikit-learn
的流形學習文檔。- 高效的t-SNE實現Multicore-TSNE。
- Distill.pub上的How to Use t-SNE Effectively(如何有效地使用t-SNE)
推薦閱讀:
※循環神經網路綜述-語音識別與自然語言處理的利器(上篇)
※深度卷積神經網路演化歷史及結構改進脈絡-40頁長文全面解讀
※一份關於數據科學家應該具備的技能清單
※簡單邏輯回歸
※重拾基礎 - Word2Vec