#!/usr/bin/env python # coding: utf-8 # # 14.3 US Baby Names 1880–2010(1880年至2010年美国婴儿姓名) # # 这个数据是从1880年到2010年婴儿名字频率数据。我们先看一下这个数据长什么样子: # # ![](http://oydgk2hgw.bkt.clouddn.com/pydata-book/r5ofm.png) # # 个数据集可以用来做很多事,例如: # # - 计算指定名字的年度比例 # - 计算某个名字的相对排名 # - 计算各年度最流行的名字,以及增长或减少最快的名字 # - 分析名字趋势:元音、辅音、长度、总体多样性、拼写变化、首尾字母等 # - 分析外源性趋势:圣经中的名字、名人、人口结构变化等 # # 之后的教程会涉及到其中一些。另外可以去官网直接下载姓名数据,[Popular Baby Names](https://www.ssa.gov/oact/babynames/limits.html)。 # # 下载National data之后,会得到names.zip文件,解压后,可以看到一系列类似于yob1880.txt这样名字的文件,说明这些文件是按年份记录的。这里使用Unix head命令查看一下文件的前10行: # In[1]: get_ipython().system('head -n 10 ../datasets/babynames/yob1880.txt') # 由于这是一个非常标准的以逗号隔开的格式(即CSV文件),所以可以用pandas.read_csv将其加载到DataFrame中: # In[2]: import pandas as pd # In[5]: # Make display smaller pd.options.display.max_rows = 10 # In[6]: names1880 = pd.read_csv('../datasets/babynames/yob1880.txt', names=['names', 'sex', 'births']) # In[7]: names1880 # 这些文件中仅含有当年出现超过5次以上的名字。为了简单化,我们可以用births列的sex分组小计,表示该年度的births总计: # In[8]: names1880.groupby('sex').births.sum() # 由于该数据集按年度被分割成了多个文件,所以第一件事情就是要将所有数据都组装到一个DataFrame里面,并加上一个year字段。使用pandas.concat可以做到: # In[9]: # 2010是最后一个有效统计年度 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) # 将所有数据整合到单个DataFrame中 names = pd.concat(pieces, ignore_index=True) # 这里要注意几件事。 # # - 第一,concat默认是按行将多个DataFrame组合到一起的; # - 第二,必须指定ignore_index=True,因为我们不希望保留read_csv所返回的原始索引。 # # 现在我们得到了一个非常大的DataFrame,它含有全部的名字数据。现在names这个DataFrame看上去是: # In[10]: names # 有了这些数据后,我们就可以利用groupby或pivot_table在year和sex界别上对其进行聚合了: # In[11]: total_births = names.pivot_table('births', index='year', columns='sex', aggfunc=sum) # In[12]: total_births.tail() # In[14]: import seaborn as sns get_ipython().run_line_magic('matplotlib', 'inline') # In[16]: total_births.plot(title='Total births by sex and year', figsize=(15, 8)) # 下面我们来插入一个prop列,用于存放指定名字的婴儿数相对于总出生数的比列。prop值为0.02表示每100名婴儿中有2名取了当前这个名字。因此,我们先按year和sex分组,然后再将新列加到各个分组上: # In[18]: def add_prop(group): group['prop'] = group.births / group.births.sum() return group names = names.groupby(['year', 'sex']).apply(add_prop) # In[19]: names # 在执行这样的分组处理时,一般都应该做一些有效性检查(sanity check),比如验证所有分组的prop的综合是否为1。由于这是一个浮点型数据,所以我们应该用np.allclose来检查这个分组总计值是否够近似于(可能不会精确等于)1: # In[20]: names.groupby(['year', 'sex']).prop.sum() # 这样就算完活了。为了便于实现进一步的分析,我们需要取出该数据的一个子集:每对sex/year组合的前1000个名字。这又是一个分组操作: # In[22]: 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 needed top1000.reset_index(inplace=True, drop=True) # 如果喜欢DIY的话,也可以这样: # In[23]: 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[25]: top1000 # 接下来针对这个top1000数据集,我们就可以开始数据分析工作了 # # # 1 Analyzing Naming Trends(分析命名趋势) # # 有了完整的数据集和刚才生成的top1000数据集,我们就可以开始分析各种命名趋势了。首先将前1000个名字分为男女两个部分: # # In[26]: boys = top1000[top1000.sex=='M'] girls = top1000[top1000.sex=='F'] # 这是两个简单的时间序列,只需要稍作整理即可绘制出相应的图标,比如每年叫做John和Mary的婴儿数。我们先生成一张按year和name统计的总出生数透视表: # In[27]: total_births = top1000.pivot_table('births', index='year', columns='name', aggfunc=sum) total_births # 接下来使用DataFrame中的plot方法: # In[28]: total_births.info() # In[29]: subset = total_births[['John', 'Harry', 'Mary', 'Marilyn']] # In[30]: subset.plot(subplots=True, figsize=(12, 10), grid=False, title="Number of births per year") # ## 评价命名多样性的增长 # # 上图反应的降低情况可能意味着父母愿意给小孩起常见的名字越来越少。这个假设可以从数据中得到验证。一个办法是计算最流行的1000个名字所占的比例,我们按year和sex进行聚合并绘图: # In[33]: import numpy as np # In[34]: table = top1000.pivot_table('prop', index='year', columns='sex', aggfunc=sum) # In[36]: table.plot(title='Sum of table1000.prop by year and sex', yticks=np.linspace(0, 1.2, 13), xticks=range(1880, 2020, 10), figsize=(15, 8)) # 从图中可以看出,名字的多样性确实出现了增长(前1000项的比例降低)。另一个办法是计算占总出生人数前50%的不同名字的数量,这个数字不太好计算。我们只考虑2010年男孩的名字: # In[37]: df = boys[boys.year == 2010] # In[38]: df # 对prop降序排列后,我们想知道前面多少个名字的人数加起来才够50%。虽然编写一个for循环也能达到目的,但NumPy有一种更聪明的矢量方式。先计算prop的累计和cumsum,,然后再通过searchsorted方法找出0.5应该被插入在哪个位置才能保证不破坏顺序: # In[39]: prop_cumsum = df.sort_values(by='prop', ascending=False).prop.cumsum() # In[40]: prop_cumsum[:10] # In[41]: prop_cumsum.searchsorted(0.5) # 由于数组索引是从0开始的,因此我们要给这个结果加1,即最终结果为117。拿1900年的数据来做个比较,这个数字要小得多: # In[42]: df = boys[boys.year == 1900] in1900 = df.sort_values(by='prop', ascending=False).prop.cumsum() in1900[-10:] # In[43]: in1900.searchsorted(0.5) + 1 # 现在就可以对所有year/sex组合执行这个计算了。按这两个字段进行groupby处理,然后用一个函数计算各分组的这个值: # In[44]: def get_quantile_count(group, q=0.5): group = group.sort_values(by='prop', ascending=False) return group.prop.cumsum().searchsorted(q) + 1 diversity = top1000.groupby(['year', 'sex']).apply(get_quantile_count) diversity = diversity.unstack('sex') # 现在,这个diversity有两个时间序列(每个性别各一个,按年度索引)。通过IPython,可以看到其内容,还可以绘制图标 # In[45]: diversity.head() # 可以看到上面表格中的值为list,如果不加diversity=diversity.astype(float)的话,会报错显示,“no numeric data to plot” error。通过加上这句来更改数据类型,就能正常绘图了: # In[49]: diversity = diversity.astype('float') diversity # In[51]: diversity.plot(title='Number of popular names in top 50%', figsize=(15, 8)) # 从图中可以看出,女孩名字的多样性总是比男孩高,而且还变得越来越高。我们可以自己分析一下具体是什么在驱动这个多样性(比如拼写形式的变化)。 # ## “最后一个字母”的变革 # # 一位研究人员指出:近百年来,男孩名字在最后一个字母上的分布发生了显著的变化。为了了解具体的情况,我们首先将全部出生数据在年度、性别以及末字母上进行了聚合: # In[52]: # 从name列中取出最后一个字母 get_last_letter = lambda x: x[-1] last_letters = names.name.map(get_last_letter) last_letters.name = 'last_letter' table = names.pivot_table('births', index=last_letters, columns=['sex', 'year'], aggfunc=sum) # In[53]: print(type(last_letters)) print(last_letters[:5]) # 然后,我们选出具有一个代表性的三年,并输出前几行: # In[54]: subtable = table.reindex(columns=[1910, 1960, 2010], level='year') subtable.head() # 接下来我们需要安总出生数对该表进行规范化处理,一遍计算出个性别各末字母站总出生人数的比例: # In[55]: subtable.sum() # In[56]: letter_prop = subtable / subtable.sum() letter_prop # 有了这个字母比例数据后,就可以生成一张各年度各性别的条形图了: # In[57]: import matplotlib.pyplot as plt # In[58]: fig, 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='Femal', legend=False) # 从上图可以看出来,从20世纪60年代开始,以字母'n'结尾的男孩名字出现了显著的增长。回到之前创建的那个完整表,按年度和性别对其进行规范化处理,并在男孩名字中选取几个字母,最后进行转置以便将各个列做成一个时间序列: # In[59]: letter_prop = table / table.sum() letter_prop.head() # In[61]: dny_ts = letter_prop.loc[['d', 'n', 'y'], 'M'].T dny_ts.head() # 有了这个时间序列的DataFrame后,就可以通过其plot方法绘制出一张趋势图: # In[63]: dny_ts.plot(figsize=(10, 8)) # ## 变成女孩名字的男孩名字(以及相反的情况) # # 另一个有趣的趋势是,早年流行于男孩的名字近年来“变性了”,列入Lesley或Leslie。回到top1000数据集,找出其中以"lesl"开头的一组名字: # In[64]: all_names = pd.Series(top1000.name.unique()) lesley_like = all_names[all_names.str.lower().str.contains('lesl')] lesley_like # 然后利用这个结果过滤其他的名字,并按名字分组计算出生数以查看相对频率: # In[66]: filtered = top1000[top1000.name.isin(lesley_like)] filtered.groupby('name').births.sum() # 接下来,我们按性别和年度进行聚合,并按年度进行规范化处理: # In[67]: table = filtered.pivot_table('births', index='year', columns='sex', aggfunc='sum') table = table.div(table.sum(1), axis=0) # In[68]: table # 现在,我们可以轻松绘制一张分性别的年度曲线图了: # In[71]: table.plot(style={'M': 'k-', 'F': 'k--'}, figsize=(10, 8))