譯《利用Python進行數據分析》14章(中二)數據分析案例

譯《利用Python進行數據分析》14章(中二)數據分析案例

來自專欄 Python程序員5 人贊了文章

作者:SeanCheney Python愛好者社區專欄作者

簡書專欄:jianshu.com/u/130f76596

前文傳送門:

【翻譯】《利用Python進行數據分析·第2版》第1章 準備工作

【翻譯】《利用Python進行數據分析·第2版》第2章(上)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(中)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(下)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第3章(上)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第3章(中)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第3章(下)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第4章(上)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第4章(中)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第4章(下)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第5章(上)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第5章(中)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第5章(下)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第6章(上) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第6章(中) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第6章(下) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第7章(上)數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第7章(中) 數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第7章(下) 數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第8章(上) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第8章(中) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第8章(下) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第9章(上) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第9章(中) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第9章(下) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第10章(上) 數據聚合與分組運算

【翻譯】《利用Python進行數據分析·第2版》第10章(中) 數據聚合與分組運算

【翻譯】《利用Python進行數據分析·第2版》第10章(下) 數據聚合與分組運算

【翻譯】《利用Python進行數據分析·第2版》第11章(上) 時間序列

【翻譯】《利用Python進行數據分析·第2版》第11章(中) 時間序列

【翻譯】《利用Python進行數據分析·第2版》第11章(中二) 時間序列

【翻譯】《利用Python進行數據分析·第2版》第11章(下) 時間序列

【翻譯】《利用Python進行數據分析·第2版》第12章(上) pandas高級應用

【翻譯】《利用Python進行數據分析·第2版》第12章(中) pandas高級應用

【翻譯】《利用Python進行數據分析·第2版》第12章(下) pandas高級應用

【翻譯】《利用Python進行數據分析·第2版》第13章(上) Python建模庫介紹

【翻譯】《利用Python進行數據分析·第2版》第13章(中) Python建模庫介紹

【翻譯】《利用Python進行數據分析·第2版》第13章(中二) Python建模庫介紹

【翻譯】《利用Python進行數據分析·第2版》第13章(下) Python建模庫介紹

【翻譯】《利用Python進行數據分析·第2版》第14章(上)數據分析案例

【翻譯】《利用Python進行數據分析·第2版》第14章(中)數據分析案例

14.3 1880-2010年間全美嬰兒姓名

美國社會保障總署(SSA)提供了一份從1880年到現在的嬰兒名字頻率數據。Hadley Wickham(許多流行R包的作者)經常用這份數據來演示R的數據處理功能。

我們要做一些數據規整才能載入這個數據集,這麼做就會產生一個如下的DataFrame:

In [4]: names.head(10)Out[4]: name sex births year0 Mary F 7065 18801 Anna F 2604 18802 Emma F 2003 18803 Elizabeth F 1939 18804 Minnie F 1746 18805 Margaret F 1578 18806 Ida F 1472 18807 Alice F 1414 18808 Bertha F 1320 18809 Sarah F 1288 1880

你可以用這個數據集做很多事,例如:

  • 計算指定名字(可以是你自己的,也可以是別人的)的年度比例。
  • 計算某個名字的相對排名。
  • 計算各年度最流行的名字,以及增長或減少最快的名字。
  • 分析名字趨勢:母音、輔音、長度、總體多樣性、拼寫變化、首尾字母等。
  • 分析外源性趨勢:聖經中的名字、名人、人口結構變化等。

利用前面介紹過的那些工具,這些分析工作都能很輕鬆地完成,我會講解其中的一些。

到編寫本書時為止,美國社會保障總署將該資料庫按年度製成了多個數據文件,其中給出了每個性別/名字組合的出生總數。這些文件的原始檔案可以在這裡獲取:ssa.gov/oact/babynames/

如果你在閱讀本書的時候這個頁面已經不見了,也可以用搜索引擎找找。

下載"National data"文件names.zip,解壓後的目錄中含有一組文件(如yob1880.txt)。我用UNIX的head命令查看了其中一個文件的前10行(在Windows上,你可以用more命令,或直接在文本編輯器中打開):

In [94]: !head -n 10 datasets/babynames/yob1880.txtMary,F,7065Anna,F,2604Emma,F,2003Elizabeth,F,1939Minnie,F,1746Margaret,F,1578Ida,F,1472Alice,F,1414Bertha,F,1320Sarah,F,1288

由於這是一個非常標準的以逗號隔開的格式,所以可以用pandas.read_csv將其載入到DataFrame中:

In [95]: import pandas as pdIn [96]: names1880 =pd.read_csv(datasets/babynames/yob1880.txt, ....: names=[name, sex, births])In [97]: names1880Out[97]: name sex births0 Mary F 70651 Anna F 26042 Emma F 20033 Elizabeth F 19394 Minnie F 1746... ... .. ...1995 Woodie M 51996 Worthy M 51997 Wright M 51998 York M 51999 Zachariah M 5[2000 rows x 3 columns]

這些文件中僅含有當年出現超過5次的名字。為了簡單起見,我們可以用births列的sex分組小計表示該年度的births總計:

In [98]: names1880.groupby(sex).births.sum()Out[98]: sexF 90993M 110493Name: births, dtype: int64

由於該數據集按年度被分隔成了多個文件,所以第一件事情就是要將所有數據都組裝到一個DataFrame裡面,並加上一個year欄位。使用pandas.concat即可達到這個目的:

years = range(1880, 2011)pieces = []columns = [name, sex, births]for year in years: path = datasets/babynames/yob%d.txt % year frame = pd.read_csv(path, names=columns) frame[year] = year pieces.append(frame)# Concatenate everything into a single DataFramenames = pd.concat(pieces, ignore_index=True)

這裡需要注意幾件事情。第一,concat默認是按行將多個DataFrame組合到一起的;第二,必須指定ignore_index=True,因為我們不希望保留read_csv所返回的原始行號。現在我們得到了一個非常大的DataFrame,它含有全部的名字數據:

In [100]: namesOut[100]: name sex births year0 Mary F 7065 18801 Anna F 2604 18802 Emma F 2003 18803 Elizabeth F 1939 18804 Minnie F 1746 1880... ... .. ... ...1690779 Zymaire M 5 20101690780 Zyonne M 5 20101690781 Zyquarius M 5 20101690782 Zyran M 5 20101690783 Zzyzx M 5 2010[1690784 rows x 4 columns]

有了這些數據之後,我們就可以利用groupby或pivot_table在year和sex級別上對其進行聚合了,如圖14-4所示:

In [101]: total_births = names.pivot_table(births, index=year, .....: columns=sex, aggfunc=sum)In [102]: total_births.tail()Out[102]: sex F Myear 2006 1896468 20502342007 1916888 20692422008 1883645 20323102009 1827643 19733592010 1759010 1898382In [103]: total_births.plot(title=Total births by sex and year)

圖14-4 按性別和年度統計的總出生數

下面我們來插入一個prop列,用於存放指定名字的嬰兒數相對於總出生數的比例。prop值為0.02表示每100名嬰兒中有2名取了當前這個名字。因此,我們先按year和sex分組,然後再將新列加到各個分組上:

def add_prop(group): group[prop] = group.births / group.births.sum() return groupnames = names.groupby([year, sex]).apply(add_prop)

現在,完整的數據集就有了下面這些列:

In [105]: namesOut[105]: name sex births year prop0 Mary F 7065 1880 0.0776431 Anna F 2604 1880 0.0286182 Emma F 2003 1880 0.0220133 Elizabeth F 1939 1880 0.0213094 Minnie F 1746 1880 0.019188... ... .. ... ... ...1690779 Zymaire M 5 2010 0.0000031690780 Zyonne M 5 2010 0.0000031690781 Zyquarius M 5 2010 0.0000031690782 Zyran M 5 2010 0.0000031690783 Zzyzx M 5 2010 0.000003[1690784 rows x 5 columns]

在執行這樣的分組處理時,一般都應該做一些有效性檢查,比如驗證所有分組的prop的總和是否為1:

In [106]: names.groupby([year, sex]).prop.sum()Out[106]: year sex1880 F 1.0 M 1.01881 F 1.0 M 1.01882 F 1.0 ... 2008 M 1.02009 F 1.0 M 1.02010 F 1.0 M 1.0Name: prop, Length: 262, dtype: float64

工作完成。為了便於實現更進一步的分析,我需要取出該數據的一個子集:每對sex/year組合的前1000個名字。這又是一個分組操作:

def get_top1000(group): return group.sort_values(by=births, ascending=False)[:1000]grouped = names.groupby([year, sex])top1000 = grouped.apply(get_top1000)# Drop the group index, not neededtop1000.reset_index(inplace=True, drop=True)

如果你喜歡DIY的話,也可以這樣:

pieces = []for year, group in names.groupby([year, sex]): pieces.append(group.sort_values(by=births, ascending=False)[:1000])top1000 = pd.concat(pieces, ignore_index=True)

現在的結果數據集就小多了:

In [108]: top1000Out[108]: name sex births year prop0 Mary F 7065 1880 0.0776431 Anna F 2604 1880 0.0286182 Emma F 2003 1880 0.0220133 Elizabeth F 1939 1880 0.0213094 Minnie F 1746 1880 0.019188... ... .. ... ... ...261872 Camilo M 194 2010 0.000102261873 Destin M 194 2010 0.000102261874 Jaquan M 194 2010 0.000102261875 Jaydan M 194 2010 0.000102261876 Maxton M 193 2010 0.000102[261877 rows x 5 columns]

接下來的數據分析工作就針對這個top1000數據集了。

分析命名趨勢

有了完整的數據集和剛才生成的top1000數據集,我們就可以開始分析各種命名趨勢了。首先將前1000個名字分為男女兩個部分:

In [109]: boys = top1000[top1000.sex == M]In [110]: girls = top1000[top1000.sex == F]

這是兩個簡單的時間序列,只需稍作整理即可繪製出相應的圖表(比如每年叫做John和Mary的嬰兒數)。我們先生成一張按year和name統計的總出生數透視表:

In [111]: total_births = top1000.pivot_table(births, index=year, .....: columns=name, .....: aggfunc=sum)

現在,我們用DataFrame的plot方法繪製幾個名字的曲線圖(見圖14-5):

In [112]: total_births.info()<class pandas.core.frame.DataFrame>Int64Index: 131 entries, 1880 to 2010Columns: 6868 entries, Aaden to Zuridtypes: float64(6868)memory usage: 6.9 MBIn [113]: subset = total_births[[John, Harry, Mary, Marilyn]]In [114]: subset.plot(subplots=True, figsize=(12, 10), grid=False, .....: title="Number of births per year")

圖14-5 幾個男孩和女孩名字隨時間變化的使用數量

從圖中可以看出,這幾個名字在美國人民的心目中已經風光不再了。但事實並非如此簡單,我們在下一節中就能知道是怎麼一回事了。

評估命名多樣性的增長

一種解釋是父母願意給小孩起常見的名字越來越少。這個假設可以從數據中得到驗證。一個辦法是計算最流行的1000個名字所佔的比例,我按year和sex進行聚合併繪圖(見圖14-6):

In [116]: table = top1000.pivot_table(prop, index=year, .....: columns=sex, aggfunc=sum)In [117]: table.plot(title=Sum of table1000.prop by year and sex, .....: yticks=np.linspace(0, 1.2, 13), xticks=range(1880, 2020, 10))

圖14-6 分性別統計的前1000個名字在總出生人數中的比例

從圖中可以看出,名字的多樣性確實出現了增長(前1000項的比例降低)。另一個辦法是計算佔總出生人數前50%的不同名字的數量,這個數字不太好計算。我們只考慮2010年男孩的名字:

In [118]: df = boys[boys.year == 2010]In [119]: dfOut[119]: name sex births year prop260877 Jacob M 21875 2010 0.011523260878 Ethan M 17866 2010 0.009411260879 Michael M 17133 2010 0.009025260880 Jayden M 17030 2010 0.008971260881 William M 16870 2010 0.008887... ... .. ... ... ...261872 Camilo M 194 2010 0.000102261873 Destin M 194 2010 0.000102261874 Jaquan M 194 2010 0.000102261875 Jaydan M 194 2010 0.000102261876 Maxton M 193 2010 0.000102[1000 rows x 5 columns]

在對prop降序排列之後,我們想知道前面多少個名字的人數加起來才夠50%。雖然編寫一個for循環確實也能達到目的,但NumPy有一種更聰明的矢量方式。先計算prop的累計和cumsum,然後再通過searchsorted方法找出0.5應該被插入在哪個位置才能保證不破壞順序:

In [120]: prop_cumsum = df.sort_values(by=prop, ascending=False).prop.cumsum()In [121]: prop_cumsum[:10]Out[121]: 260877 0.011523260878 0.020934260879 0.029959260880 0.038930260881 0.047817260882 0.056579260883 0.065155260884 0.073414260885 0.081528260886 0.089621Name: prop, dtype: float64In [122]: prop_cumsum.values.searchsorted(0.5)Out[122]: 116

由於數組索引是從0開始的,因此我們要給這個結果加1,即最終結果為117。拿1900年的數據來做個比較,這個數字要小得多:

In [123]: df = boys[boys.year == 1900]In [124]: in1900 = df.sort_values(by=prop, ascending=False).prop.cumsum()In [125]: in1900.values.searchsorted(0.5) + 1Out[125]: 25

現在就可以對所有year/sex組合執行這個計算了。按這兩個欄位進行groupby處理,然後用一個函數計算各分組的這個值:

def get_quantile_count(group, q=0.5): group = group.sort_values(by=prop, ascending=False) return group.prop.cumsum().values.searchsorted(q) + 1diversity = top1000.groupby([year, sex]).apply(get_quantile_count)diversity = diversity.unstack(sex)

現在,diversity這個DataFrame擁有兩個時間序列(每個性別各一個,按年度索引)。通過IPython,你可以查看其內容,還可以像之前那樣繪製圖表(如圖14-7所示):

In [128]: diversity.head()Out[128]: sex F Myear 1880 38 141881 38 141882 38 151883 39 151884 39 16In [129]: diversity.plot(title="Number of popular names in top 50%")

圖14-7 按年度統計的密度表

從圖中可以看出,女孩名字的多樣性總是比男孩的高,而且還在變得越來越高。讀者們可以自己分析一下具體是什麼在驅動這個多樣性(比如拼寫形式的變化)。

「最後一個字母」的變革

2007年,一名嬰兒姓名研究人員Laura Wattenberg在她自己的網站上指出(babynamewizard.com):近百年來,男孩名字在最後一個字母上的分布發生了顯著的變化。為了了解具體的情況,我首先將全部出生數據在年度、性別以及末字母上進行了聚合:

# extract last letter from name columnget_last_letter = lambda x: x[-1]last_letters = names.name.map(get_last_letter)last_letters.name = last_lettertable = names.pivot_table(births, index=last_letters, columns=[sex, year], aggfunc=sum)

然後,我選出具有一定代表性的三年,並輸出前面幾行:

In [131]: subtable = table.reindex(columns=[1910, 1960, 2010], level=year)In [132]: subtable.head()Out[132]: sex F M year 1910 1960 2010 1910 1960 2010last_letter a 108376.0 691247.0 670605.0 977.0 5204.0 28438.0b NaN 694.0 450.0 411.0 3912.0 38859.0c 5.0 49.0 946.0 482.0 15476.0 23125.0d 6750.0 3729.0 2607.0 22111.0 262112.0 44398.0e 133569.0 435013.0 313833.0 28655.0 178823.0 129012.0

接下來我們需要按總出生數對該表進行規範化處理,以便計算出各性別各末字母佔總出生人數的比例:

In [133]: subtable.sum()Out[133]: sex yearF 1910 396416.0 1960 2022062.0 2010 1759010.0M 1910 194198.0 1960 2132588.02010 1898382.0dtype: float64In [134]: letter_prop = subtable / subtable.sum()In [135]: letter_propOut[135]: sex F M year 1910 1960 2010 1910 1960 2010last_letter a 0.273390 0.341853 0.381240 0.005031 0.002440 0.014980b NaN 0.000343 0.000256 0.002116 0.001834 0.020470c 0.000013 0.000024 0.000538 0.002482 0.007257 0.012181d 0.017028 0.001844 0.001482 0.113858 0.122908 0.023387e 0.336941 0.215133 0.178415 0.147556 0.083853 0.067959... ... ... ... ... ... ...v NaN 0.000060 0.000117 0.0001130.000037 0.001434w 0.000020 0.000031 0.001182 0.006329 0.007711 0.016148x 0.000015 0.000037 0.000727 0.003965 0.001851 0.008614y 0.110972 0.152569 0.116828 0.077349 0.160987 0.058168z 0.002439 0.000659 0.000704 0.000170 0.000184 0.001831[26 rows x 6 columns]

有了這個字母比例數據之後,就可以生成一張各年度各性別的條形圖了,如圖14-8所示:

import matplotlib.pyplot as pltfig, axes = plt.subplots(2, 1, figsize=(10, 8))letter_prop[M].plot(kind=bar, rot=0, ax=axes[0], title=Male)letter_prop[F].plot(kind=bar, rot=0, ax=axes[1], title=Female, legend=False)

圖14-8 男孩女孩名字中各個末字母的比例

可以看出,從20世紀60年代開始,以字母"n"結尾的男孩名字出現了顯著的增長。回到之前創建的那個完整表,按年度和性別對其進行規範化處理,並在男孩名字中選取幾個字母,最後進行轉置以便將各個列做成一個時間序列:

In [138]: letter_prop = table / table.sum()In [139]: dny_ts = letter_prop.loc[[d, n, y], M].TIn [140]: dny_ts.head()Out[140]: last_letter d n yyear 1880 0.083055 0.153213 0.0757601881 0.083247 0.153214 0.0774511882 0.085340 0.149560 0.0775371883 0.084066 0.151646 0.0791441884 0.086120 0.149915 0.080405

有了這個時間序列的DataFrame之後,就可以通過其plot方法繪製出一張趨勢圖了(如圖14-9所示):

In [143]: dny_ts.plot()

圖14-9 各年出生的男孩中名字以d/n/y結尾的人數比例

變成女孩名字的男孩名字(以及相反的情況)

另一個有趣的趨勢是,早年流行於男孩的名字近年來「變性了」,例如Lesley或Leslie。回到top1000數據集,找出其中以"lesl"開頭的一組名字:

In [144]: all_names = pd.Series(top1000.name.unique())In [145]: lesley_like = all_names[all_names.str.lower().str.contains(lesl)]In [146]: lesley_likeOut[146]: 632 Leslie2294 Lesley4262 Leslee4728 Lesli6103 Leslydtype: object

然後利用這個結果過濾其他的名字,並按名字分組計算出生數以查看相對頻率:

In [147]: filtered = top1000[top1000.name.isin(lesley_like)]In [148]: filtered.groupby(name).births.sum()Out[148]: nameLeslee 1082Lesley 35022Lesli 929Leslie 370429Lesly 10067Name: births, dtype: int64

接下來,我們按性別和年度進行聚合,並按年度進行規範化處理:

In [149]: table = filtered.pivot_table(births, index=year, .....: columns=sex, aggfunc=sum)In [150]: table = table.div(table.sum(1), axis=0)In [151]: table.tail()Out[151]: sex F Myear 2006 1.0 NaN2007 1.0 NaN2008 1.0 NaN2009 1.0 NaN2010 1.0 NaN

最後,就可以輕鬆繪製一張分性別的年度曲線圖了(如圖2-10所示):

In [153]: table.plot(stylex={M: , F: k--})

圖14-10 各年度使用「Lesley型」名字的男女比例


推薦閱讀:

TAG:數據挖掘 | Python | 數據分析 |