返回介绍

14.3 US Baby Names 1880–2010 1880年至2010年美国婴儿姓名

发布于 2023-07-21 00:48:48 字数 29799 浏览 0 评论 0 收藏 0

这个数据是从 1880 年到 2010 年婴儿名字频率数据。

这个数据集可以用来做很多事,例如:

  • 计算指定名字的年度比例
  • 计算某个名字的相对排名
  • 计算各年度最流行的名字,以及增长或减少最快的名字
  • 分析名字趋势:元音、辅音、长度、总体多样性、拼写变化、首尾字母等
  • 分析外源性趋势:圣经中的名字、名人、人口结构变化等

之后的教程会涉及到其中一些。另外可以去官网直接下载姓名数据, Popular Baby Names

下载 National data 之后,会得到 names.zip 文件,解压后,可以看到一系列类似于 yob1880.txt 这样名字的文件,说明这些文件是按年份记录的。这里使用 Unix head 命令查看一下文件的前 10 行:

!head -n 10 ../datasets/babynames/yob1880.txt

由于这是一个非常标准的以逗号隔开的格式(即 CSV 文件),所以可以用 pandas.read_csv 将其加载到 DataFrame 中:

import pandas as pd
# Make display smaller
pd.options.display.max_rows = 10
names1880 = pd.read_csv('../datasets/babynames/yob1880.txt', names=['names', 'sex', 'births'])
names1880
 namessexbirths
0MaryF7065
1AnnaF2604
2EmmaF2003
3ElizabethF1939
4MinnieF1746
............
1995WoodieM5
1996WorthyM5
1997WrightM5
1998YorkM5
1999ZachariahM5

2000 rows × 3 columns

这些文件中仅含有当年出现超过 5 次以上的名字。为了简单化,我们可以用 births 列的 sex 分组小计,表示该年度的 births 总计:

names1880.groupby('sex').births.sum()
sex
F     90993
M    110493
Name: births, dtype: int64

由于该数据集按年度被分割成了多个文件,所以第一件事情就是要将所有数据都组装到一个 DataFrame 里面,并加上一个 year 字段。使用 pandas.concat 可以做到:

# 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 看上去是:

names
 namesexbirthsyear
0MaryF70651880
1AnnaF26041880
2EmmaF20031880
3ElizabethF19391880
4MinnieF17461880
...............
1690779ZymaireM52010
1690780ZyonneM52010
1690781ZyquariusM52010
1690782ZyranM52010
1690783ZzyzxM52010

1690784 rows × 4 columns

有了这些数据后,我们就可以利用 groupby 或 pivot_table 在 year 和 sex 界别上对其进行聚合了:

total_births = names.pivot_table('births', index='year',
                                columns='sex', aggfunc=sum)
total_births.tail()
sexFM
year  
200618964682050234
200719168882069242
200818836452032310
200918276431973359
201017590101898382
import seaborn as sns
%matplotlib inline
total_births.plot(title='Total births by sex and year', figsize=(15, 8))
<matplotlib.axes._subplots.AxesSubplot at 0x1289ad710>

下面我们来插入一个 prop 列,用于存放指定名字的婴儿数相对于总出生数的比列。prop 值为 0.02 表示每 100 名婴儿中有 2 名取了当前这个名字。因此,我们先按 year 和 sex 分组,然后再将新列加到各个分组上:

def add_prop(group): 
    group['prop'] = group.births / group.births.sum()
    return group
names = names.groupby(['year', 'sex']).apply(add_prop)
names
 namesexbirthsyearprop
0MaryF706518800.077643
1AnnaF260418800.028618
2EmmaF200318800.022013
3ElizabethF193918800.021309
4MinnieF174618800.019188
..................
1690779ZymaireM520100.000003
1690780ZyonneM520100.000003
1690781ZyquariusM520100.000003
1690782ZyranM520100.000003
1690783ZzyzxM520100.000003

1690784 rows × 5 columns

在执行这样的分组处理时,一般都应该做一些有效性检查(sanity check),比如验证所有分组的 prop 的综合是否为 1。由于这是一个浮点型数据,所以我们应该用 np.allclose 来检查这个分组总计值是否够近似于(可能不会精确等于)1:

names.groupby(['year', 'sex']).prop.sum()
year  sex
1880  F      1.0
      M      1.0
1881  F      1.0
      M      1.0
1882  F      1.0
            ... 
2008  M      1.0
2009  F      1.0
      M      1.0
2010  F      1.0
      M      1.0
Name: 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 needed
top1000.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)
top1000
 namesexbirthsyearprop
0MaryF706518800.077643
1AnnaF260418800.028618
2EmmaF200318800.022013
3ElizabethF193918800.021309
4MinnieF174618800.019188
..................
261872CamiloM19420100.000102
261873DestinM19420100.000102
261874JaquanM19420100.000102
261875JaydanM19420100.000102
261876MaxtonM19320100.000102

261877 rows × 5 columns

接下来针对这个 top1000 数据集,我们就可以开始数据分析工作了

1 Analyzing Naming Trends(分析命名趋势)

有了完整的数据集和刚才生成的 top1000 数据集,我们就可以开始分析各种命名趋势了。首先将前 1000 个名字分为男女两个部分:

boys = top1000[top1000.sex=='M']
girls = top1000[top1000.sex=='F']

这是两个简单的时间序列,只需要稍作整理即可绘制出相应的图标,比如每年叫做 John 和 Mary 的婴儿数。我们先生成一张按 year 和 name 统计的总出生数透视表:

total_births = top1000.pivot_table('births', index='year', 
                                   columns='name', aggfunc=sum)

total_births
nameAadenAaliyahAaravAaronAarushAbAbagailAbbAbbeyAbbie...ZoaZoeZoeyZoieZolaZollieZonaZoraZulaZuri
year                     
1880NaNNaNNaN102.0NaNNaNNaNNaNNaN71.0...8.023.0NaNNaN7.0NaN8.028.027.0NaN
1881NaNNaNNaN94.0NaNNaNNaNNaNNaN81.0...NaN22.0NaNNaN10.0NaN9.021.027.0NaN
1882NaNNaNNaN85.0NaNNaNNaNNaNNaN80.0...8.025.0NaNNaN9.0NaN17.032.021.0NaN
1883NaNNaNNaN105.0NaNNaNNaNNaNNaN79.0...NaN23.0NaNNaN10.0NaN11.035.025.0NaN
1884NaNNaNNaN97.0NaNNaNNaNNaNNaN98.0...13.031.0NaNNaN14.06.08.058.027.0NaN
..................................................................
2006NaN3737.0NaN8279.0NaNNaN297.0NaN404.0440.0...NaN5145.02839.0530.0NaNNaNNaNNaNNaNNaN
2007NaN3941.0NaN8914.0NaNNaN313.0NaN349.0468.0...NaN4925.03028.0526.0NaNNaNNaNNaNNaNNaN
2008955.04028.0219.08511.0NaNNaN317.0NaN344.0400.0...NaN4764.03438.0492.0NaNNaNNaNNaNNaNNaN
20091265.04352.0270.07936.0NaNNaN296.0NaN307.0369.0...NaN5120.03981.0496.0NaNNaNNaNNaNNaNNaN
2010448.04628.0438.07374.0226.0NaN277.0NaN295.0324.0...NaN6200.05164.0504.0NaNNaNNaNNaNNaN258.0

131 rows × 6868 columns

接下来使用 DataFrame 中的 plot 方法:

total_births.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 131 entries, 1880 to 2010
Columns: 6868 entries, Aaden to Zuri
dtypes: float64(6868)
memory usage: 6.9 MB
subset = total_births[['John', 'Harry', 'Mary', 'Marilyn']]
subset.plot(subplots=True, figsize=(12, 10), grid=False,
            title="Number of births per year")
array([<matplotlib.axes._subplots.AxesSubplot object at 0x1132a4828>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x116933080>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x117d24710>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x117d70b70>], dtype=object)

评价命名多样性的增长

上图反应的降低情况可能意味着父母愿意给小孩起常见的名字越来越少。这个假设可以从数据中得到验证。一个办法是计算最流行的 1000 个名字所占的比例,我们按 year 和 sex 进行聚合并绘图:

import numpy as np
table = top1000.pivot_table('prop', index='year',
                           columns='sex', aggfunc=sum)
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))
<matplotlib.axes._subplots.AxesSubplot at 0x11b325128>

从图中可以看出,名字的多样性确实出现了增长(前 1000 项的比例降低)。另一个办法是计算占总出生人数前 50%的不同名字的数量,这个数字不太好计算。我们只考虑 2010 年男孩的名字:

df = boys[boys.year == 2010]
df
 namesexbirthsyearprop
260877JacobM2187520100.011523
260878EthanM1786620100.009411
260879MichaelM1713320100.009025
260880JaydenM1703020100.008971
260881WilliamM1687020100.008887
..................
261872CamiloM19420100.000102
261873DestinM19420100.000102
261874JaquanM19420100.000102
261875JaydanM19420100.000102
261876MaxtonM19320100.000102

1000 rows × 5 columns

对 prop 降序排列后,我们想知道前面多少个名字的人数加起来才够 50%。虽然编写一个 for 循环也能达到目的,但 NumPy 有一种更聪明的矢量方式。先计算 prop 的累计和 cumsum,,然后再通过 searchsorted 方法找出 0.5 应该被插入在哪个位置才能保证不破坏顺序:

prop_cumsum = df.sort_values(by='prop', ascending=False).prop.cumsum()
prop_cumsum[:10]
260877    0.011523
260878    0.020934
260879    0.029959
260880    0.038930
260881    0.047817
260882    0.056579
260883    0.065155
260884    0.073414
260885    0.081528
260886    0.089621
Name: prop, dtype: float64
prop_cumsum.searchsorted(0.5)
array([116])

由于数组索引是从 0 开始的,因此我们要给这个结果加 1,即最终结果为 117。拿 1900 年的数据来做个比较,这个数字要小得多:

df = boys[boys.year == 1900]
in1900 = df.sort_values(by='prop', ascending=False).prop.cumsum()
in1900[-10:]
41853    0.979223
41852    0.979277
41851    0.979330
41850    0.979383
41849    0.979436
41848    0.979489
41847    0.979542
41846    0.979595
41845    0.979648
41876    0.979702
Name: prop, dtype: float64
in1900.searchsorted(0.5) + 1
array([25])

现在就可以对所有 year/sex 组合执行这个计算了。按这两个字段进行 groupby 处理,然后用一个函数计算各分组的这个值:

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,可以看到其内容,还可以绘制图标

diversity.head()
sexFM
year  
1880[38][14]
1881[38][14]
1882[38][15]
1883[39][15]
1884[39][16]

可以看到上面表格中的值为 list,如果不加 diversity=diversity.astype(float)的话,会报错显示,“no numeric data to plot” error。通过加上这句来更改数据类型,就能正常绘图了:

diversity = diversity.astype('float')
diversity
sexFM
year  
188038.014.0
188138.014.0
188238.015.0
188339.015.0
188439.016.0
.........
2006209.099.0
2007223.0103.0
2008234.0109.0
2009241.0114.0
2010246.0117.0

131 rows × 2 columns

diversity.plot(title='Number of popular names in top 50%', figsize=(15, 8))
<matplotlib.axes._subplots.AxesSubplot at 0x11b3b7eb8>

从图中可以看出,女孩名字的多样性总是比男孩高,而且还变得越来越高。我们可以自己分析一下具体是什么在驱动这个多样性(比如拼写形式的变化)。

最后一个字母 的变革

一位研究人员指出:近百年来,男孩名字在最后一个字母上的分布发生了显著的变化。为了了解具体的情况,我们首先将全部出生数据在年度、性别以及末字母上进行了聚合:

# 从 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)
print(type(last_letters))
print(last_letters[:5])
<class 'pandas.core.series.Series'>
0    y
1    a
2    a
3    h
4    e
Name: last_letter, dtype: object

然后,我们选出具有一个代表性的三年,并输出前几行:

subtable = table.reindex(columns=[1910, 1960, 2010], level='year')
subtable.head()
sexFM
year191019602010191019602010
last_letter      
a108376.0691247.0670605.0977.05204.028438.0
bNaN694.0450.0411.03912.038859.0
c5.049.0946.0482.015476.023125.0
d6750.03729.02607.022111.0262112.044398.0
e133569.0435013.0313833.028655.0178823.0129012.0

接下来我们需要安总出生数对该表进行规范化处理,一遍计算出个性别各末字母站总出生人数的比例:

subtable.sum()
sex  year
F    1910     396416.0
     1960    2022062.0
     2010    1759010.0
M    1910     194198.0
     1960    2132588.0
     2010    1898382.0
dtype: float64
letter_prop = subtable / subtable.sum()
letter_prop
sexFM
year191019602010191019602010
last_letter      
a0.2733900.3418530.3812400.0050310.0024400.014980
bNaN0.0003430.0002560.0021160.0018340.020470
c0.0000130.0000240.0005380.0024820.0072570.012181
d0.0170280.0018440.0014820.1138580.1229080.023387
e0.3369410.2151330.1784150.1475560.0838530.067959
.....................
vNaN0.0000600.0001170.0001130.0000370.001434
w0.0000200.0000310.0011820.0063290.0077110.016148
x0.0000150.0000370.0007270.0039650.0018510.008614
y0.1109720.1525690.1168280.0773490.1609870.058168
z0.0024390.0006590.0007040.0001700.0001840.001831

26 rows × 6 columns

有了这个字母比例数据后,就可以生成一张各年度各性别的条形图了:

import matplotlib.pyplot as plt
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)
<matplotlib.axes._subplots.AxesSubplot at 0x11bb53b00>

从上图可以看出来,从 20 世纪 60 年代开始,以字母'n'结尾的男孩名字出现了显著的增长。回到之前创建的那个完整表,按年度和性别对其进行规范化处理,并在男孩名字中选取几个字母,最后进行转置以便将各个列做成一个时间序列:

letter_prop = table / table.sum()
letter_prop.head()
sexF...M
year1880188118821883188418851886188718881889...2001200220032004200520062007200820092010
last_letter                     
a0.3455870.3434400.3387640.3412510.3385500.3412700.3397030.3352580.3327640.328706...0.0201620.0200190.0191770.0195050.0184810.0176350.0167470.0161890.0159270.014980
bNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...0.0262560.0254180.0243680.0231710.0216450.0207780.0203570.0196550.0196930.020470
cNaNNaN0.0000460.000045NaNNaNNaNNaNNaNNaN...0.0139720.0140480.0140420.0135140.0130830.0129910.0129830.0124580.0121860.012181
d0.0066930.0066010.0068060.0072110.0071000.0064780.0069670.0070350.0072660.007703...0.0313520.0287940.0270690.0261180.0254200.0250750.0244510.0235740.0233980.023387
e0.3668190.3706160.3745820.3731590.3727220.3728960.3728020.3723240.3736750.373736...0.0749270.0746030.0733960.0717100.0707990.0697480.0694450.0693620.0686630.067959

5 rows × 262 columns

dny_ts = letter_prop.loc[['d', 'n', 'y'], 'M'].T
dny_ts.head()
last_letterdny
year   
18800.0830550.1532130.075760
18810.0832470.1532140.077451
18820.0853400.1495600.077537
18830.0840660.1516460.079144
18840.0861200.1499150.080405

有了这个时间序列的 DataFrame 后,就可以通过其 plot 方法绘制出一张趋势图:

dny_ts.plot(figsize=(10, 8))
<matplotlib.axes._subplots.AxesSubplot at 0x11bbb0390>

变成女孩名字的男孩名字(以及相反的情况)

另一个有趣的趋势是,早年流行于男孩的名字近年来“变性了”,列入 Lesley 或 Leslie。回到 top1000 数据集,找出其中以"lesl"开头的一组名字:

all_names = pd.Series(top1000.name.unique())
lesley_like = all_names[all_names.str.lower().str.contains('lesl')]
lesley_like
632     Leslie
2294    Lesley
4262    Leslee
4728     Lesli
6103     Lesly
dtype: object

然后利用这个结果过滤其他的名字,并按名字分组计算出生数以查看相对频率:

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

接下来,我们按性别和年度进行聚合,并按年度进行规范化处理:

table = filtered.pivot_table('births', index='year',
                             columns='sex', aggfunc='sum')

table = table.div(table.sum(1), axis=0)
table
sexFM
year  
18800.0919540.908046
18810.1067960.893204
18820.0656930.934307
18830.0530300.946970
18840.1071430.892857
.........
20061.000000NaN
20071.000000NaN
20081.000000NaN
20091.000000NaN
20101.000000NaN

131 rows × 2 columns

现在,我们可以轻松绘制一张分性别的年度曲线图了:

table.plot(style={'M': 'k-', 'F': 'k--'}, figsize=(10, 8))
<matplotlib.axes._subplots.AxesSubplot at 0x11f0640b8>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文