Python 数据分析(PYDA)第三版(六)(4)

简介: Python 数据分析(PYDA)第三版(六)

Python 数据分析(PYDA)第三版(六)(3)https://developer.aliyun.com/article/1482398


13.3 美国婴儿姓名 1880-2010

美国社会保障管理局(SSA)提供了从 1880 年到现在的婴儿名字频率数据。Hadley Wickham,几个流行 R 包的作者,在 R 中说明数据操作时使用了这个数据集。

我们需要进行一些数据整理来加载这个数据集,但一旦我们这样做了,我们将得到一个看起来像这样的 DataFrame:

In [4]: names.head(10)
Out[4]:
 name sex  births  year
0       Mary   F    7065  1880
1       Anna   F    2604  1880
2       Emma   F    2003  1880
3  Elizabeth   F    1939  1880
4     Minnie   F    1746  1880
5   Margaret   F    1578  1880
6        Ida   F    1472  1880
7      Alice   F    1414  1880
8     Bertha   F    1320  1880
9      Sarah   F    1288  1880

有许多事情你可能想要对数据集做:

  • 可视化随着时间推移给定名字(您自己的名字或其他名字)的婴儿比例
  • 确定一个名字的相对排名
  • 确定每年最受欢迎的名字或受欢迎程度增长或下降最多的名字
  • 分析名字的趋势:元音、辅音、长度、整体多样性、拼写变化、首尾字母
  • 分析趋势的外部来源:圣经名字、名人、人口统计学

使用本书中的工具,许多这类分析都可以实现,所以我会带你走一些。

截至目前,美国社会保障管理局提供了数据文件,每年一个文件,其中包含每个性别/名字组合的总出生数。您可以下载这些文件的原始存档

如果您在阅读此页面时发现已移动,很可能可以通过互联网搜索再次找到。下载“国家数据”文件names.zip并解压缩后,您将获得一个包含一系列文件如yob1880.txt的目录。我使用 Unix 的head命令查看其中一个文件的前 10 行(在 Windows 上,您可以使用more命令或在文本编辑器中打开):

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

由于这已经是逗号分隔形式,可以使用pandas.read_csv将其加载到 DataFrame 中:

In [107]: names1880 = pd.read_csv("datasets/babynames/yob1880.txt",
 .....:                         names=["name", "sex", "births"])
In [108]: names1880
Out[108]: 
 name sex  births
0          Mary   F    7065
1          Anna   F    2604
2          Emma   F    2003
3     Elizabeth   F    1939
4        Minnie   F    1746
...         ...  ..     ...
1995     Woodie   M       5
1996     Worthy   M       5
1997     Wright   M       5
1998       York   M       5
1999  Zachariah   M       5
[2000 rows x 3 columns]

这些文件只包含每年至少有五次出现的名字,所以为了简单起见,我们可以使用按性别的出生列的总和作为该年出生的总数:

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

由于数据集按年份分成文件,首先要做的事情之一是将所有数据组装到一个单独的 DataFrame 中,并进一步添加一个year字段。您可以使用pandas.concat来做到这一点。在 Jupyter 单元格中运行以下内容:

pieces = []
for year in range(1880, 2011):
 path = f"datasets/babynames/yob{year}.txt"
 frame = pd.read_csv(path, names=["name", "sex", "births"])
 # Add a column for the year
 frame["year"] = year
 pieces.append(frame)
# Concatenate everything into a single DataFrame
names = pd.concat(pieces, ignore_index=True)

这里有几件事情需要注意。首先,记住concat默认按行组合 DataFrame 对象。其次,您必须传递ignore_index=True,因为我们不关心从pandas.read_csv返回的原始行号。因此,现在我们有一个包含所有年份的所有名字数据的单个 DataFrame:

In [111]: names
Out[111]: 
 name sex  births  year
0             Mary   F    7065  1880
1             Anna   F    2604  1880
2             Emma   F    2003  1880
3        Elizabeth   F    1939  1880
4           Minnie   F    1746  1880
...            ...  ..     ...   ...
1690779    Zymaire   M       5  2010
1690780     Zyonne   M       5  2010
1690781  Zyquarius   M       5  2010
1690782      Zyran   M       5  2010
1690783      Zzyzx   M       5  2010
[1690784 rows x 4 columns]

有了这些数据,我们可以开始使用groupbypivot_table在年份和性别水平上对数据进行聚合(参见按性别和年份统计的总出生数):

In [112]: total_births = names.pivot_table("births", index="year",
 .....:                                  columns="sex", aggfunc=sum)
In [113]: total_births.tail()
Out[113]: 
sex         F        M
year 
2006  1896468  2050234
2007  1916888  2069242
2008  1883645  2032310
2009  1827643  1973359
2010  1759010  1898382
In [114]: total_births.plot(title="Total births by sex and year")

图 13.4:按性别和年份统计的总出生数

接下来,让我们插入一个名为prop的列,该列显示每个名字相对于总出生数的比例。prop值为0.02表示每 100 个婴儿中有 2 个被赋予特定的名字。因此,我们按年份和性别对数据进行分组,然后向每个组添加新列:

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

现在得到的完整数据集现在具有以下列:

In [116]: names
Out[116]: 
 name sex  births  year      prop
0             Mary   F    7065  1880  0.077643
1             Anna   F    2604  1880  0.028618
2             Emma   F    2003  1880  0.022013
3        Elizabeth   F    1939  1880  0.021309
4           Minnie   F    1746  1880  0.019188
...            ...  ..     ...   ...       ...
1690779    Zymaire   M       5  2010  0.000003
1690780     Zyonne   M       5  2010  0.000003
1690781  Zyquarius   M       5  2010  0.000003
1690782      Zyran   M       5  2010  0.000003
1690783      Zzyzx   M       5  2010  0.000003
[1690784 rows x 5 columns]

在执行这样的组操作时,通常很有价值进行一些合理性检查,比如验证所有组中prop列的总和是否为 1:

In [117]: names.groupby(["year", "sex"])["prop"].sum()
Out[117]: 
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

现在这样做了,我将提取数据的一个子集以便进一步分析:每个性别/年份组合的前 1000 个名字。这是另一个组操作:

In [118]: def get_top1000(group):
 .....:     return group.sort_values("births", ascending=False)[:1000]
In [119]: grouped = names.groupby(["year", "sex"])
In [120]: top1000 = grouped.apply(get_top1000)
In [121]: top1000.head()
Out[121]: 
 name sex  births  year      prop
year sex 
1880 F   0       Mary   F    7065  1880  0.077643
 1       Anna   F    2604  1880  0.028618
 2       Emma   F    2003  1880  0.022013
 3  Elizabeth   F    1939  1880  0.021309
 4     Minnie   F    1746  1880  0.019188

我们可以删除组索引,因为我们不需要它进行分析:

In [122]: top1000 = top1000.reset_index(drop=True)
• 1

现在得到的数据集要小得多:

In [123]: top1000.head()
Out[123]: 
 name sex  births  year      prop
0       Mary   F    7065  1880  0.077643
1       Anna   F    2604  1880  0.028618
2       Emma   F    2003  1880  0.022013
3  Elizabeth   F    1939  1880  0.021309
4     Minnie   F    1746  1880  0.019188

我们将在接下来的数据调查中使用这个前一千个数据集。

分析命名趋势

有了完整的数据集和前一千个数据集,我们可以开始分析各种有趣的命名趋势。首先,我们可以将前一千个名字分为男孩和女孩部分:

In [124]: boys = top1000[top1000["sex"] == "M"]
In [125]: girls = top1000[top1000["sex"] == "F"]

简单的时间序列,比如每年约翰或玛丽的数量,可以绘制,但需要一些操作才能更有用。让我们形成一个按年份和姓名总数的数据透视表:

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

现在,可以使用 DataFrame 的plot方法为一些名字绘制图表(一些男孩和女孩名字随时间变化显示了结果):

In [127]: total_births.info()
<class 'pandas.core.frame.DataFrame'>
Index: 131 entries, 1880 to 2010
Columns: 6868 entries, Aaden to Zuri
dtypes: float64(6868)
memory usage: 6.9 MB
In [128]: subset = total_births[["John", "Harry", "Mary", "Marilyn"]]
In [129]: subset.plot(subplots=True, figsize=(12, 10),
 .....:             title="Number of births per year")

图 13.5:一些男孩和女孩名字随时间变化

看到这个,你可能会得出结论,这些名字已经不再受到美国人口的青睐。但事实实际上比这更复杂,将在下一节中探讨。

衡量命名多样性的增加

减少图表的原因之一是越来越少的父母选择常见的名字给他们的孩子。这个假设可以在数据中进行探索和确认。一个度量是由前 1000 个最受欢迎的名字代表的出生比例,我按年份和性别进行汇总和绘制(性别在前一千个名字中所代表的出生比例显示了结果图):

In [131]: table = top1000.pivot_table("prop", index="year",
 .....:                             columns="sex", aggfunc=sum)
In [132]: table.plot(title="Sum of table1000.prop by year and sex",
 .....:            yticks=np.linspace(0, 1.2, 13))

图 13.6:性别在前一千个名字中所代表的出生比例

您可以看到,确实存在着越来越多的名字多样性(前一千名中总比例减少)。另一个有趣的指标是在出生的前 50%中按照从高到低的流行度顺序取的不同名字的数量。这个数字更难计算。让我们只考虑 2010 年的男孩名字:

In [133]: df = boys[boys["year"] == 2010]
In [134]: df
Out[134]: 
 name sex  births  year      prop
260877    Jacob   M   21875  2010  0.011523
260878    Ethan   M   17866  2010  0.009411
260879  Michael   M   17133  2010  0.009025
260880   Jayden   M   17030  2010  0.008971
260881  William   M   16870  2010  0.008887
...         ...  ..     ...   ...       ...
261872   Camilo   M     194  2010  0.000102
261873   Destin   M     194  2010  0.000102
261874   Jaquan   M     194  2010  0.000102
261875   Jaydan   M     194  2010  0.000102
261876   Maxton   M     193  2010  0.000102
[1000 rows x 5 columns]

在对prop进行降序排序后,我们想知道最受欢迎的名字中有多少个名字达到了 50%。您可以编写一个for循环来执行此操作,但使用矢量化的 NumPy 方法更具计算效率。对prop进行累积求和cumsum,然后调用searchsorted方法返回0.5需要插入的累积和位置,以保持其按顺序排序:

In [135]: prop_cumsum = df["prop"].sort_values(ascending=False).cumsum()
In [136]: prop_cumsum[:10]
Out[136]: 
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
In [137]: prop_cumsum.searchsorted(0.5)
Out[137]: 116

由于数组是从零开始索引的,将此结果加 1 将得到 117 的结果。相比之下,在 1900 年,这个数字要小得多:

In [138]: df = boys[boys.year == 1900]
In [139]: in1900 = df.sort_values("prop", ascending=False).prop.cumsum()
In [140]: in1900.searchsorted(0.5) + 1
Out[140]: 25

现在,您可以将此操作应用于每个年份/性别组合,对这些字段进行groupby,并apply一个返回每个组计数的函数:

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

这个结果 DataFrame diversity 现在有两个时间序列,一个用于每个性别,按年份索引。这可以像以前一样进行检查和绘制(参见按年份绘制的多样性指标):

In [143]: diversity.head()
Out[143]: 
sex    F   M
year 
1880  38  14
1881  38  14
1882  38  15
1883  39  15
1884  39  16
In [144]: diversity.plot(title="Number of popular names in top 50%")

图 13.7:按年份绘制的多样性指标

正如你所看到的,女孩名字一直比男孩名字更多样化,而且随着时间的推移,它们变得更加多样化。关于到底是什么推动了这种多样性的进一步分析,比如替代拼写的增加,留给读者自行探讨。

“最后一个字母”革命

在 2007 年,婴儿姓名研究员劳拉·瓦滕伯格指出,过去 100 年来,以最后一个字母结尾的男孩名字的分布发生了显著变化。为了看到这一点,我们首先按年份、性别和最后一个字母聚合完整数据集中的所有出生情况:

def get_last_letter(x):
 return 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 [146]: subtable = table.reindex(columns=[1910, 1960, 2010], level="year")
In [147]: subtable.head()
Out[147]: 
sex                 F                            M 
year             1910      1960      2010     1910      1960      2010
last_letter 
a            108376.0  691247.0  670605.0    977.0    5204.0   28438.0
b                 NaN     694.0     450.0    411.0    3912.0   38859.0
c                 5.0      49.0     946.0    482.0   15476.0   23125.0
d              6750.0    3729.0    2607.0  22111.0  262112.0   44398.0
e            133569.0  435013.0  313833.0  28655.0  178823.0  129012.0

接下来,通过总出生数对表进行标准化,计算一个包含每个性别以每个字母结尾的总出生比例的新表:

In [148]: subtable.sum()
Out[148]: 
sex  year
F    1910     396416.0
 1960    2022062.0
 2010    1759010.0
M    1910     194198.0
 1960    2132588.0
 2010    1898382.0
dtype: float64
In [149]: letter_prop = subtable / subtable.sum()
In [150]: letter_prop
Out[150]: 
sex                 F                             M 
year             1910      1960      2010      1910      1960      2010
last_letter 
a            0.273390  0.341853  0.381240  0.005031  0.002440  0.014980
b                 NaN  0.000343  0.000256  0.002116  0.001834  0.020470
c            0.000013  0.000024  0.000538  0.002482  0.007257  0.012181
d            0.017028  0.001844  0.001482  0.113858  0.122908  0.023387
e            0.336941  0.215133  0.178415  0.147556  0.083853  0.067959
...               ...       ...       ...       ...       ...       ...
v                 NaN  0.000060  0.000117  0.000113  0.000037  0.001434
w            0.000020  0.000031  0.001182  0.006329  0.007711  0.016148
x            0.000015  0.000037  0.000727  0.003965  0.001851  0.008614
y            0.110972  0.152569  0.116828  0.077349  0.160987  0.058168
z            0.002439  0.000659  0.000704  0.000170  0.000184  0.001831
[26 rows x 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="Female",
 legend=False)

图 13.8:以每个字母结尾的男孩和女孩名字的比例

正如您所看到的,自 20 世纪 60 年代以来,以n结尾的男孩名字经历了显著增长。回到之前创建的完整表格,再次按年份和性别进行标准化,并选择男孩名字的一部分字母,最后转置使每一列成为一个时间序列:

In [153]: letter_prop = table / table.sum()
In [154]: dny_ts = letter_prop.loc[["d", "n", "y"], "M"].T
In [155]: dny_ts.head()
Out[155]: 
last_letter         d         n         y
year 
1880         0.083055  0.153213  0.075760
1881         0.083247  0.153214  0.077451
1882         0.085340  0.149560  0.077537
1883         0.084066  0.151646  0.079144
1884         0.086120  0.149915  0.080405

有了这个时间序列的 DataFrame,我可以再次使用其plot方法制作时间趋势图(请参见随时间变化以 d/n/y 结尾的男孩出生比例):

In [158]: dny_ts.plot()

图 13.9:随时间变化以 d/n/y 结尾的男孩出生比例

男孩名字变成女孩名字(反之亦然)

另一个有趣的趋势是查看在样本早期更受一性别欢迎,但随着时间推移已成为另一性别的首选名字的名字。一个例子是 Lesley 或 Leslie 这个名字。回到top1000 DataFrame,我计算出数据集中以"Lesl"开头的名字列表:

In [159]: all_names = pd.Series(top1000["name"].unique())
In [160]: lesley_like = all_names[all_names.str.contains("Lesl")]
In [161]: lesley_like
Out[161]: 
632     Leslie
2294    Lesley
4262    Leslee
4728     Lesli
6103     Lesly
dtype: object

然后,我们可以筛选出那些名字,按名字分组对出生进行求和,以查看相对频率:

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

接下来,让我们按性别和年份进行聚合,并在年份内进行归一化:

In [164]: table = filtered.pivot_table("births", index="year",
 .....:                              columns="sex", aggfunc="sum")
In [165]: table = table.div(table.sum(axis="columns"), axis="index")
In [166]: table.tail()
Out[166]: 
sex     F   M
year 
2006  1.0 NaN
2007  1.0 NaN
2008  1.0 NaN
2009  1.0 NaN
2010  1.0 NaN

最后,现在可以制作按性别随时间变化的分布图(请参见随时间变化男/女 Lesley 样式名字的比例):

In [168]: table.plot(style={"M": "k-", "F": "k--"})

图 13.10:随时间变化男/女 Lesley 样式名字的比例

13.4 USDA 食品数据库

美国农业部(USDA)提供了一个食品营养信息数据库。程序员 Ashley Williams 以 JSON 格式创建了这个数据库的一个版本。记录看起来像这样:

{
 "id": 21441,
 "description": "KENTUCKY FRIED CHICKEN, Fried Chicken, EXTRA CRISPY,
Wing, meat and skin with breading",
 "tags": ["KFC"],
 "manufacturer": "Kentucky Fried Chicken",
 "group": "Fast Foods",
 "portions": [
 {
 "amount": 1,
 "unit": "wing, with skin",
 "grams": 68.0
 },
 ...
 ],
 "nutrients": [
 {
 "value": 20.8,
 "units": "g",
 "description": "Protein",
 "group": "Composition"
 },
 ...
 ]
}

每种食物都有一些标识属性,还有两个营养素和分量大小的列表。这种形式的数据不太适合分析,因此我们需要做一些工作,将数据整理成更好的形式。

您可以使用您选择的任何 JSON 库将此文件加载到 Python 中。我将使用内置的 Python json模块:

In [169]: import json
In [170]: db = json.load(open("datasets/usda_food/database.json"))
In [171]: len(db)
Out[171]: 6636

db中的每个条目都是一个包含单个食物所有数据的字典。"nutrients"字段是一个字典列表,每个营养素一个:

In [172]: db[0].keys()
Out[172]: dict_keys(['id', 'description', 'tags', 'manufacturer', 'group', 'porti
ons', 'nutrients'])
In [173]: db[0]["nutrients"][0]
Out[173]: 
{'value': 25.18,
 'units': 'g',
 'description': 'Protein',
 'group': 'Composition'}
In [174]: nutrients = pd.DataFrame(db[0]["nutrients"])
In [175]: nutrients.head(7)
Out[175]: 
 value units                  description        group
0    25.18     g                      Protein  Composition
1    29.20     g            Total lipid (fat)  Composition
2     3.06     g  Carbohydrate, by difference  Composition
3     3.28     g                          Ash        Other
4   376.00  kcal                       Energy       Energy
5    39.28     g                        Water  Composition
6  1573.00    kJ                       Energy       Energy

将字典列表转换为 DataFrame 时,我们可以指定要提取的字段列表。我们将提取食物名称、组、ID 和制造商:

In [176]: info_keys = ["description", "group", "id", "manufacturer"]
In [177]: info = pd.DataFrame(db, columns=info_keys)
In [178]: info.head()
Out[178]: 
 description                   group    id 
0                     Cheese, caraway  Dairy and Egg Products  1008  \
1                     Cheese, cheddar  Dairy and Egg Products  1009 
2                        Cheese, edam  Dairy and Egg Products  1018 
3                        Cheese, feta  Dairy and Egg Products  1019 
4  Cheese, mozzarella, part skim milk  Dairy and Egg Products  1028 
 manufacturer 
0 
1 
2 
3 
4 
In [179]: info.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6636 entries, 0 to 6635
Data columns (total 4 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   description   6636 non-null   object
 1   group         6636 non-null   object
 2   id            6636 non-null   int64 
 3   manufacturer  5195 non-null   object
dtypes: int64(1), object(3)
memory usage: 207.5+ KB

info.info()的输出中,我们可以看到manufacturer列中有缺失数据。

您可以使用value_counts查看食物组的分布:

In [180]: pd.value_counts(info["group"])[:10]
Out[180]: 
group
Vegetables and Vegetable Products    812
Beef Products                        618
Baked Products                       496
Breakfast Cereals                    403
Legumes and Legume Products          365
Fast Foods                           365
Lamb, Veal, and Game Products        345
Sweets                               341
Fruits and Fruit Juices              328
Pork Products                        328
Name: count, dtype: int64

现在,要对所有营养数据进行一些分析,最简单的方法是将每种食物的营养成分组装成一个单独的大表格。为此,我们需要采取几个步骤。首先,我将把每个食物营养列表转换为一个 DataFrame,添加一个食物id的列,并将 DataFrame 附加到列表中。然后,可以使用concat将它们连接起来。在 Jupyter 单元格中运行以下代码:

nutrients = []
for rec in db:
 fnuts = pd.DataFrame(rec["nutrients"])
 fnuts["id"] = rec["id"]
 nutrients.append(fnuts)
nutrients = pd.concat(nutrients, ignore_index=True)

如果一切顺利,nutrients应该是这样的:

In [182]: nutrients
Out[182]: 
 value units                         description        group     id
0        25.180     g                             Protein  Composition   1008
1        29.200     g                   Total lipid (fat)  Composition   1008
2         3.060     g         Carbohydrate, by difference  Composition   1008
3         3.280     g                                 Ash        Other   1008
4       376.000  kcal                              Energy       Energy   1008
...         ...   ...                                 ...          ...    ...
389350    0.000   mcg                 Vitamin B-12, added     Vitamins  43546
389351    0.000    mg                         Cholesterol        Other  43546
389352    0.072     g        Fatty acids, total saturated        Other  43546
389353    0.028     g  Fatty acids, total monounsaturated        Other  43546
389354    0.041     g  Fatty acids, total polyunsaturated        Other  43546
[389355 rows x 5 columns]

我注意到这个 DataFrame 中有重复项,所以删除它们会更容易:

In [183]: nutrients.duplicated().sum()  # number of duplicates
Out[183]: 14179
In [184]: nutrients = nutrients.drop_duplicates()

由于 DataFrame 对象中都有"group""description",我们可以重命名以便更清晰:

In [185]: col_mapping = {"description" : "food",
 .....:                "group"       : "fgroup"}
In [186]: info = info.rename(columns=col_mapping, copy=False)
In [187]: info.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6636 entries, 0 to 6635
Data columns (total 4 columns):
 #   Column        Non-Null Count  Dtype 
---  ------        --------------  ----- 
 0   food          6636 non-null   object
 1   fgroup        6636 non-null   object
 2   id            6636 non-null   int64 
 3   manufacturer  5195 non-null   object
dtypes: int64(1), object(3)
memory usage: 207.5+ KB
In [188]: col_mapping = {"description" : "nutrient",
 .....:                "group" : "nutgroup"}
In [189]: nutrients = nutrients.rename(columns=col_mapping, copy=False)
In [190]: nutrients
Out[190]: 
 value units                            nutrient     nutgroup     id
0        25.180     g                             Protein  Composition   1008
1        29.200     g                   Total lipid (fat)  Composition   1008
2         3.060     g         Carbohydrate, by difference  Composition   1008
3         3.280     g                                 Ash        Other   1008
4       376.000  kcal                              Energy       Energy   1008
...         ...   ...                                 ...          ...    ...
389350    0.000   mcg                 Vitamin B-12, added     Vitamins  43546
389351    0.000    mg                         Cholesterol        Other  43546
389352    0.072     g        Fatty acids, total saturated        Other  43546
389353    0.028     g  Fatty acids, total monounsaturated        Other  43546
389354    0.041     g  Fatty acids, total polyunsaturated        Other  43546
[375176 rows x 5 columns]

完成所有这些后,我们准备将infonutrients合并:

In [191]: ndata = pd.merge(nutrients, info, on="id")
In [192]: ndata.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 375176 entries, 0 to 375175
Data columns (total 8 columns):
 #   Column        Non-Null Count   Dtype 
---  ------        --------------   ----- 
 0   value         375176 non-null  float64
 1   units         375176 non-null  object 
 2   nutrient      375176 non-null  object 
 3   nutgroup      375176 non-null  object 
 4   id            375176 non-null  int64 
 5   food          375176 non-null  object 
 6   fgroup        375176 non-null  object 
 7   manufacturer  293054 non-null  object 
dtypes: float64(1), int64(1), object(6)
memory usage: 22.9+ MB
In [193]: ndata.iloc[30000]
Out[193]: 
value                                             0.04
units                                                g
nutrient                                       Glycine
nutgroup                                   Amino Acids
id                                                6158
food            Soup, tomato bisque, canned, condensed
fgroup                      Soups, Sauces, and Gravies
manufacturer 
Name: 30000, dtype: object

现在我们可以制作按食物组和营养类型中位数值的图表(请参见各食物组的锌中位数值):

In [195]: result = ndata.groupby(["nutrient", "fgroup"])["value"].quantile(0.5)
In [196]: result["Zinc, Zn"].sort_values().plot(kind="barh")

图 13.11:各食物组的锌中位数值

使用idxmaxargmax Series 方法,您可以找到每种营养素中最密集的食物。在 Jupyter 单元格中运行以下内容:

by_nutrient = ndata.groupby(["nutgroup", "nutrient"])
def get_maximum(x):
 return x.loc[x.value.idxmax()]
max_foods = by_nutrient.apply(get_maximum)[["value", "food"]]
# make the food a little smaller
max_foods["food"] = max_foods["food"].str[:50]

生成的 DataFrame 太大,无法在书中显示;这里只有"Amino Acids"营养组:

In [198]: max_foods.loc["Amino Acids"]["food"]
Out[198]: 
nutrient
Alanine                            Gelatins, dry powder, unsweetened
Arginine                                Seeds, sesame flour, low-fat
Aspartic acid                                    Soy protein isolate
Cystine                 Seeds, cottonseed flour, low fat (glandless)
Glutamic acid                                    Soy protein isolate
Glycine                            Gelatins, dry powder, unsweetened
Histidine                 Whale, beluga, meat, dried (Alaska Native)
Hydroxyproline    KENTUCKY FRIED CHICKEN, Fried Chicken, ORIGINAL RE
Isoleucine        Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Leucine           Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Lysine            Seal, bearded (Oogruk), meat, dried (Alaska Native
Methionine                     Fish, cod, Atlantic, dried and salted
Phenylalanine     Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Proline                            Gelatins, dry powder, unsweetened
Serine            Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Threonine         Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Tryptophan          Sea lion, Steller, meat with fat (Alaska Native)
Tyrosine          Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Valine            Soy protein isolate, PROTEIN TECHNOLOGIES INTERNAT
Name: food, dtype: object


13.5 2012 年联邦选举委员会数据库

美国联邦选举委员会(FEC)发布了有关政治竞选捐款的数据。这包括捐助者姓名、职业和雇主、地址以及捐款金额。2012 年美国总统选举的捐款数据作为一个 150 兆字节的 CSV 文件P00000001-ALL.csv可用(请参阅本书的数据存储库),可以使用pandas.read_csv加载:

In [199]: fec = pd.read_csv("datasets/fec/P00000001-ALL.csv", low_memory=False)
In [200]: fec.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1001731 entries, 0 to 1001730
Data columns (total 16 columns):
 #   Column             Non-Null Count    Dtype 
---  ------             --------------    ----- 
 0   cmte_id            1001731 non-null  object 
 1   cand_id            1001731 non-null  object 
 2   cand_nm            1001731 non-null  object 
 3   contbr_nm          1001731 non-null  object 
 4   contbr_city        1001712 non-null  object 
 5   contbr_st          1001727 non-null  object 
 6   contbr_zip         1001620 non-null  object 
 7   contbr_employer    988002 non-null   object 
 8   contbr_occupation  993301 non-null   object 
 9   contb_receipt_amt  1001731 non-null  float64
 10  contb_receipt_dt   1001731 non-null  object 
 11  receipt_desc       14166 non-null    object 
 12  memo_cd            92482 non-null    object 
 13  memo_text          97770 non-null    object 
 14  form_tp            1001731 non-null  object 
 15  file_num           1001731 non-null  int64 
dtypes: float64(1), int64(1), object(14)
memory usage: 122.3+ MB

注意:

有几个人要求我将数据集从 2012 年选举更新到 2016 年或 2020 年选举。不幸的是,联邦选举委员会提供的最新数据集变得更大更复杂,我决定在这里使用它们会分散我想要说明的分析技术。

数据框中的一个示例记录如下:

In [201]: fec.iloc[123456]
Out[201]: 
cmte_id                             C00431445
cand_id                             P80003338
cand_nm                         Obama, Barack
contbr_nm                         ELLMAN, IRA
contbr_city                             TEMPE
contbr_st                                  AZ
contbr_zip                          852816719
contbr_employer      ARIZONA STATE UNIVERSITY
contbr_occupation                   PROFESSOR
contb_receipt_amt                        50.0
contb_receipt_dt                    01-DEC-11
receipt_desc                              NaN
memo_cd                                   NaN
memo_text                                 NaN
form_tp                                 SA17A
file_num                               772372
Name: 123456, dtype: object

您可能会想到一些方法来开始切片和切块这些数据,以提取有关捐赠者和竞选捐款模式的信息统计。我将展示一些应用本书中技术的不同分析方法。

您会发现数据中没有政党隶属关系,因此添加这些信息会很有用。您可以使用unique获取所有唯一的政治候选人列表:

In [202]: unique_cands = fec["cand_nm"].unique()
In [203]: unique_cands
Out[203]: 
array(['Bachmann, Michelle', 'Romney, Mitt', 'Obama, Barack',
 "Roemer, Charles E. 'Buddy' III", 'Pawlenty, Timothy',
 'Johnson, Gary Earl', 'Paul, Ron', 'Santorum, Rick',
 'Cain, Herman', 'Gingrich, Newt', 'McCotter, Thaddeus G',
 'Huntsman, Jon', 'Perry, Rick'], dtype=object)
In [204]: unique_cands[2]
Out[204]: 'Obama, Barack'

表示政党隶属关系的一种方法是使用字典:¹

parties = {"Bachmann, Michelle": "Republican",
 "Cain, Herman": "Republican",
 "Gingrich, Newt": "Republican",
 "Huntsman, Jon": "Republican",
 "Johnson, Gary Earl": "Republican",
 "McCotter, Thaddeus G": "Republican",
 "Obama, Barack": "Democrat",
 "Paul, Ron": "Republican",
 "Pawlenty, Timothy": "Republican",
 "Perry, Rick": "Republican",
 "Roemer, Charles E. 'Buddy' III": "Republican",
 "Romney, Mitt": "Republican",
 "Santorum, Rick": "Republican"}

现在,使用这个映射和 Series 对象上的map方法,您可以从候选人姓名计算一个政党数组:

In [206]: fec["cand_nm"][123456:123461]
Out[206]: 
123456    Obama, Barack
123457    Obama, Barack
123458    Obama, Barack
123459    Obama, Barack
123460    Obama, Barack
Name: cand_nm, dtype: object
In [207]: fec["cand_nm"][123456:123461].map(parties)
Out[207]: 
123456    Democrat
123457    Democrat
123458    Democrat
123459    Democrat
123460    Democrat
Name: cand_nm, dtype: object
# Add it as a column
In [208]: fec["party"] = fec["cand_nm"].map(parties)
In [209]: fec["party"].value_counts()
Out[209]: 
party
Democrat      593746
Republican    407985
Name: count, dtype: int64

一些数据准备要点。首先,这些数据包括捐款和退款(负捐款金额):

In [210]: (fec["contb_receipt_amt"] > 0).value_counts()
Out[210]: 
contb_receipt_amt
True     991475
False     10256
Name: count, dtype: int64

为简化分析,我将限制数据集为正捐款:

In [211]: fec = fec[fec["contb_receipt_amt"] > 0]

由于巴拉克·奥巴马和米特·罗姆尼是主要的两位候选人,我还将准备一个只包含对他们竞选活动的捐款的子集:

In [212]: fec_mrbo = fec[fec["cand_nm"].isin(["Obama, Barack", "Romney, Mitt"])]

按职业和雇主的捐款统计

按职业捐款是另一个经常研究的统计数据。例如,律师倾向于向民主党捐款更多,而商业高管倾向于向共和党捐款更多。您没有理由相信我;您可以在数据中自己看到。首先,可以使用value_counts计算每个职业的总捐款数:

In [213]: fec["contbr_occupation"].value_counts()[:10]
Out[213]: 
contbr_occupation
RETIRED                                   233990
INFORMATION REQUESTED                      35107
ATTORNEY                                   34286
HOMEMAKER                                  29931
PHYSICIAN                                  23432
INFORMATION REQUESTED PER BEST EFFORTS     21138
ENGINEER                                   14334
TEACHER                                    13990
CONSULTANT                                 13273
PROFESSOR                                  12555
Name: count, dtype: int64

通过查看职业,您会注意到许多职业都指的是相同的基本工作类型,或者有几种相同事物的变体。以下代码片段演示了一种通过从一个职业映射到另一个职业来清理其中一些职业的技术;请注意使用dict.get的“技巧”,以允许没有映射的职业“通过”:

occ_mapping = {
 "INFORMATION REQUESTED PER BEST EFFORTS" : "NOT PROVIDED",
 "INFORMATION REQUESTED" : "NOT PROVIDED",
 "INFORMATION REQUESTED (BEST EFFORTS)" : "NOT PROVIDED",
 "C.E.O.": "CEO"
}
def get_occ(x):
 # If no mapping provided, return x
 return occ_mapping.get(x, x)
fec["contbr_occupation"] = fec["contbr_occupation"].map(get_occ)

我也会为雇主做同样的事情:

emp_mapping = {
 "INFORMATION REQUESTED PER BEST EFFORTS" : "NOT PROVIDED",
 "INFORMATION REQUESTED" : "NOT PROVIDED",
 "SELF" : "SELF-EMPLOYED",
 "SELF EMPLOYED" : "SELF-EMPLOYED",
}
def get_emp(x):
 # If no mapping provided, return x
 return emp_mapping.get(x, x)
fec["contbr_employer"] = fec["contbr_employer"].map(get_emp)

现在,您可以使用pivot_table按政党和职业对数据进行聚合,然后筛选出总捐款至少为 200 万美元的子集:

In [216]: by_occupation = fec.pivot_table("contb_receipt_amt",
 .....:                                 index="contbr_occupation",
 .....:                                 columns="party", aggfunc="sum")
In [217]: over_2mm = by_occupation[by_occupation.sum(axis="columns") > 2000000]
In [218]: over_2mm
Out[218]: 
party                 Democrat   Republican
contbr_occupation 
ATTORNEY           11141982.97   7477194.43
CEO                 2074974.79   4211040.52
CONSULTANT          2459912.71   2544725.45
ENGINEER             951525.55   1818373.70
EXECUTIVE           1355161.05   4138850.09
HOMEMAKER           4248875.80  13634275.78
INVESTOR             884133.00   2431768.92
LAWYER              3160478.87    391224.32
MANAGER              762883.22   1444532.37
NOT PROVIDED        4866973.96  20565473.01
OWNER               1001567.36   2408286.92
PHYSICIAN           3735124.94   3594320.24
PRESIDENT           1878509.95   4720923.76
PROFESSOR           2165071.08    296702.73
REAL ESTATE          528902.09   1625902.25
RETIRED            25305116.38  23561244.49
SELF-EMPLOYED        672393.40   1640252.54

这些数据以条形图形式更容易查看("barh"表示水平条形图;请参见按职业和政党分组的总捐款):

In [220]: over_2mm.plot(kind="barh")

图 13.12:按职业分组的政党总捐款

您可能对捐赠最多的职业或向奥巴马和罗姆尼捐款最多的公司感兴趣。为此,您可以按候选人姓名分组,并使用本章早期的top方法的变体:

def get_top_amounts(group, key, n=5):
 totals = group.groupby(key)["contb_receipt_amt"].sum()
 return totals.nlargest(n)

然后按职业和雇主进行汇总:

In [222]: grouped = fec_mrbo.groupby("cand_nm")
In [223]: grouped.apply(get_top_amounts, "contbr_occupation", n=7)
Out[223]: 
cand_nm        contbr_occupation 
Obama, Barack  RETIRED                                   25305116.38
 ATTORNEY                                  11141982.97
 INFORMATION REQUESTED                      4866973.96
 HOMEMAKER                                  4248875.80
 PHYSICIAN                                  3735124.94
 LAWYER                                     3160478.87
 CONSULTANT                                 2459912.71
Romney, Mitt   RETIRED                                   11508473.59
 INFORMATION REQUESTED PER BEST EFFORTS    11396894.84
 HOMEMAKER                                  8147446.22
 ATTORNEY                                   5364718.82
 PRESIDENT                                  2491244.89
 EXECUTIVE                                  2300947.03
 C.E.O.                                     1968386.11
Name: contb_receipt_amt, dtype: float64
In [224]: grouped.apply(get_top_amounts, "contbr_employer", n=10)
Out[224]: 
cand_nm        contbr_employer 
Obama, Barack  RETIRED                                   22694358.85
 SELF-EMPLOYED                             17080985.96
 NOT EMPLOYED                               8586308.70
 INFORMATION REQUESTED                      5053480.37
 HOMEMAKER                                  2605408.54
 SELF                                       1076531.20
 SELF EMPLOYED                               469290.00
 STUDENT                                     318831.45
 VOLUNTEER                                   257104.00
 MICROSOFT                                   215585.36
Romney, Mitt   INFORMATION REQUESTED PER BEST EFFORTS    12059527.24
 RETIRED                                   11506225.71
 HOMEMAKER                                  8147196.22
 SELF-EMPLOYED                              7409860.98
 STUDENT                                     496490.94
 CREDIT SUISSE                               281150.00
 MORGAN STANLEY                              267266.00
 GOLDMAN SACH & CO.                          238250.00
 BARCLAYS CAPITAL                            162750.00
 H.I.G. CAPITAL                              139500.00
Name: contb_receipt_amt, dtype: float64

将捐款金额分桶

分析这些数据的一个有用方法是使用cut函数将捐助金额分成不同的桶:

In [225]: bins = np.array([0, 1, 10, 100, 1000, 10000,
 .....:                  100_000, 1_000_000, 10_000_000])
In [226]: labels = pd.cut(fec_mrbo["contb_receipt_amt"], bins)
In [227]: labels
Out[227]: 
411         (10, 100]
412       (100, 1000]
413       (100, 1000]
414         (10, 100]
415         (10, 100]
 ... 
701381      (10, 100]
701382    (100, 1000]
701383        (1, 10]
701384      (10, 100]
701385    (100, 1000]
Name: contb_receipt_amt, Length: 694282, dtype: category
Categories (8, interval[int64, right]): [(0, 1] < (1, 10] < (10, 100] < (100, 100
0] <
 (1000, 10000] < (10000, 100000] < (10000
0, 1000000] <
 (1000000, 10000000]]

然后,我们可以按姓名和 bin 标签对 Obama 和 Romney 的数据进行分组,以获得按捐款大小分组的直方图:

In [228]: grouped = fec_mrbo.groupby(["cand_nm", labels])
In [229]: grouped.size().unstack(level=0)
Out[229]: 
cand_nm              Obama, Barack  Romney, Mitt
contb_receipt_amt 
(0, 1]                         493            77
(1, 10]                      40070          3681
(10, 100]                   372280         31853
(100, 1000]                 153991         43357
(1000, 10000]                22284         26186
(10000, 100000]                  2             1
(100000, 1000000]                3             0
(1000000, 10000000]              4             0

这些数据显示,奥巴马收到的小额捐款数量明显多于罗姆尼。您还可以对捐款金额进行求和,并在桶内进行归一化,以可视化每个候选人每个大小的总捐款的百分比(每个捐款大小收到的候选人总捐款的百分比显示了结果图):

In [231]: bucket_sums = grouped["contb_receipt_amt"].sum().unstack(level=0)
In [232]: normed_sums = bucket_sums.div(bucket_sums.sum(axis="columns"),
 .....:                               axis="index")
In [233]: normed_sums
Out[233]: 
cand_nm              Obama, Barack  Romney, Mitt
contb_receipt_amt 
(0, 1]                    0.805182      0.194818
(1, 10]                   0.918767      0.081233
(10, 100]                 0.910769      0.089231
(100, 1000]               0.710176      0.289824
(1000, 10000]             0.447326      0.552674
(10000, 100000]           0.823120      0.176880
(100000, 1000000]         1.000000      0.000000
(1000000, 10000000]       1.000000      0.000000
In [234]: normed_sums[:-2].plot(kind="barh")

图 13.13:每个捐款大小收到的候选人总捐款的百分比

我排除了两个最大的桶,因为这些不是个人捐款。

这种分析可以以许多方式进行细化和改进。例如,您可以按捐赠人姓名和邮政编码对捐款进行汇总,以调整给出许多小额捐款与一笔或多笔大额捐款的捐赠者。我鼓励您自己探索数据集。

按州的捐款统计

我们可以通过候选人和州对数据进行汇总:

In [235]: grouped = fec_mrbo.groupby(["cand_nm", "contbr_st"])
In [236]: totals = grouped["contb_receipt_amt"].sum().unstack(level=0).fillna(0)
In [237]: totals = totals[totals.sum(axis="columns") > 100000]
In [238]: totals.head(10)
Out[238]: 
cand_nm    Obama, Barack  Romney, Mitt
contbr_st 
AK             281840.15      86204.24
AL             543123.48     527303.51
AR             359247.28     105556.00
AZ            1506476.98    1888436.23
CA           23824984.24   11237636.60
CO            2132429.49    1506714.12
CT            2068291.26    3499475.45
DC            4373538.80    1025137.50
DE             336669.14      82712.00
FL            7318178.58    8338458.81

如果您将每一行都除以总捐款金额,您将得到每位候选人每个州的总捐款相对百分比:


13.6 结论

在这本书第一版出版以来的 10 年里,Python 已经成为数据分析中流行和广泛使用的语言。您在这里所学习的编程技能将在未来很长一段时间内保持相关性。希望我们探讨过的编程工具和库能够为您提供帮助。

我们已经到达了这本书的结尾。我在附录中包含了一些您可能会发现有用的额外内容。

  1. 这做出了一个简化的假设,即 Gary Johnson 是共和党人,尽管后来成为了自由党候选人。
相关文章
|
24天前
|
机器学习/深度学习 数据采集 数据可视化
Python 数据分析:从零开始构建你的数据科学项目
【10月更文挑战第9天】Python 数据分析:从零开始构建你的数据科学项目
48 2
|
24天前
|
机器学习/深度学习 数据可视化 算法
使用Python进行数据分析:从零开始的指南
【10月更文挑战第9天】使用Python进行数据分析:从零开始的指南
35 1
|
6天前
|
数据采集 存储 数据挖掘
Python数据分析:Pandas库的高效数据处理技巧
【10月更文挑战第27天】在数据分析领域,Python的Pandas库因其强大的数据处理能力而备受青睐。本文介绍了Pandas在数据导入、清洗、转换、聚合、时间序列分析和数据合并等方面的高效技巧,帮助数据分析师快速处理复杂数据集,提高工作效率。
23 0
|
1月前
|
数据采集 数据可视化 数据挖掘
Python中的数据分析:从零开始的指南
Python中的数据分析:从零开始的指南
48 2
|
1月前
|
数据采集 数据可视化 数据挖掘
基于Python的数据分析与可视化实战
本文将引导读者通过Python进行数据分析和可视化,从基础的数据操作到高级的数据可视化技巧。我们将使用Pandas库处理数据,并利用Matplotlib和Seaborn库创建直观的图表。文章不仅提供代码示例,还将解释每个步骤的重要性和目的,帮助读者理解背后的逻辑。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供有价值的见解和技能。
68 0
|
7天前
|
存储 数据挖掘 数据处理
Python数据分析:Pandas库的高效数据处理技巧
【10月更文挑战第26天】Python 是数据分析领域的热门语言,Pandas 库以其高效的数据处理功能成为数据科学家的利器。本文介绍 Pandas 在数据读取、筛选、分组、转换和合并等方面的高效技巧,并通过示例代码展示其实际应用。
19 1
|
12天前
|
数据采集 数据可视化 数据挖掘
R语言与Python:比较两种数据分析工具
R语言和Python是目前最流行的两种数据分析工具。本文将对这两种工具进行比较,包括它们的历史、特点、应用场景、社区支持、学习资源、性能等方面,以帮助读者更好地了解和选择适合自己的数据分析工具。
19 2
|
24天前
|
数据采集 数据可视化 数据挖掘
使用Python进行高效的数据分析
【10月更文挑战第9天】使用Python进行高效的数据分析
21 1
|
24天前
|
数据采集 机器学习/深度学习 数据挖掘
如何使用Python进行高效的数据分析
【10月更文挑战第9天】如何使用Python进行高效的数据分析
23 1
|
26天前
|
机器学习/深度学习 存储 数据挖掘
深入浅出:使用Python进行数据分析
【10月更文挑战第7天】本文将带你进入Python数据分析的世界,从基础的数据结构开始,逐步深入到数据处理、数据可视化以及机器学习等高级主题。我们将通过实际案例和代码示例,让你了解如何使用Python进行有效的数据分析。无论你是初学者还是有一定经验的开发者,都能从中获得新的知识和启发。