现在我们已经读到了本书的最后一章,我们将看看一些真实世界的数据集。对于每个数据集,我们将使用本书中介绍的技术从原始数据中提取意义。所演示的技术可以应用到其他所有类型的数据集。本章包含了一组各种示例数据集,您可以使用本书中的工具进行练习。
13.1来自1.USA.gov的Bitly数据
2011年,网址缩短服务商Bitly与美国政府网站USA.gov合作,提供一份匿名数据,这些数据来自那些短链接以.gov或.mil结尾的用户。在2011年,实时动态和每小时快照都可以作为可下载的文本文件提供。该服务在撰写本文时(2022年)已关闭,但我们保留了本书示例的一个数据文件。
在每小时快照的情况下,每个文件中的每一行都包含一种常见的web数据形式,即JSON,即JavaScript对象记号。例如,如果我们只读取文件的第一行,我们可能会看到如下内容:
path = 'datasets/bitly_usagov/example.txt'
with open(path) as f:
print(f.readline()) # 读取第一行
Python具有内置库和第三方库,用于将JSON字符串转换为Python字典。在这里,我们将使用json模块和它的loads方法,并在我们下载的示例文件中的每一行中调用它们
import json
with open(path) as f:
records = [json.loads(line) for line in f] # 生成的records对象是Python字典的列表
# f的每一行line是json字符串,调用json.loads函数后,json字符串被转换为了Python字典
在纯Python中计算时区
假设我们对查找数据集(tz字段)中最常出现的时区感兴趣。我们有很多方法可以做到这一点。
# 使用列表推倒式提取时区列表
time_zones = [rec['tz'] for rec in records] # 会报错,因为并非所有记录都有时区字段
# 在列表生成式末尾加检查来处理这个问题
time_zones = [rec['tz'] for rec in records if 'tz' in rec] # 但可以发现其中一些是记录的时区是未知的(空字符串),可以考虑过滤掉这些或者保留(这里暂时保留)
# 为了按时区生成计数,采用两种方法
# 方法一:仅使用Python标准库(较难):在遍历时区时使用字典来存储计数
def get_counts(sequence):
counts = {} # 创建空字典
for x in sequence:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
return counts
# 方法二:使用pandas(更简单):使用Python标准库中更高级的工具
from collections import defaultdict
def get_counts(sequence):
counts = defaultdict(int) # 值会初始化为0
for x in sequence:
counts[x] += 1
return counts
# 将time_zones列表传递给函数,得到对应的计数字典
counts = get_counts(time_zones)
# 如果想要前10个时区和它们的计数
# 方法一:可以创建(count, timezone)元组的列表,并对它们进行排序:
def top_couts(count_dict, n=10):
value_key_pairs = [(cout, tz) for tz, count in count_dict.items()]
value_key_pairs.sort() # 按照count和tz从低到高排序
return value_key_pairs[-n:]
# 传递counts字典
top_counts(counts)
# 方法二:使用collections.Counter类,更简答
from collections import Counter
counts = Counter(time_zones) # 将列表传递给Counter函数中
counts.most_common(10) # 返回前10个时区和它们对应的计数
用pandas计算时区
# 通过传递记录的列表给pandas.DataFrame可以从原始记录集合创建DataFrame:
frame = pd.DataFrame(records)
# 可以使用frame.info()方法查看有关这个新DataFrame的一些基本信息,如列名、推断的列类型或缺失值数量
frame.info()
# 在Series上使用value_counts方法对'tz'列进行计数
tz_counts = frame['tz'].value_counts()
我们可以使用matplotlib可视化这些数据。我们可以通过在记录中为未知或者缺失值的时区填充替代值来使绘图更好一些。
clean_tz = frame['tz'].fillna('Missing') # 使用fillna方法替换缺失值
clean_tz[clean_tz == ''] = 'Unknown' # 使用布尔数组索引空字符串
tz_counts = clean_tz.value_counts() # 计数
# 使用seaborn包制作一个水平条形图
import seaborn as sns
subset = tz_counts.head() # 获得前5个时区的时区名和计数
sns.barplot(y=subset.index, x=subset.to_numpy()) # 时区名作为y轴,计数值作为x轴绘制水平条形图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpwUbfap-1669683442822)(https://secure2.wostatic.cn/static/u5BYjvRFL375JcC6JaiLWR/image.png?auth_key=1669683424-5krh83WenqLJEjAsa1kVh5-0-b2d9856a9c2ab548297484ab1cc2c614)]
# 字段a包含有关用于执行URL短地址的浏览器、设备或应用程序的信息
frame['a'][1] # 返回'a'列的第0条记录
frame['a'][50] # 返回'a'列的第50条记录
frame['a'][51][:50] # 返回'a'列的第51条记录的前50个字符
解析这些’代理’字符串中的所有有趣信息似乎是一项艰难的任务。一种可能的策略是拆分字符串中的第一个token(大致对应于浏览器功能),并对用户行为进行总结。
results = pd.Series([x.split()[0] for x in frame['a'].dropna()])
results.value_counts()
现在,假设你想要将顶级时区分解为Windows和非Windows用户。为了简化起见,如果字符串’Windows’在代理字符串中,则假设这个用户在Windows上。由于某些代理缺失,我们将从数据中排除这些代理。
cframe = frame[frame['a'].notna()].copy() # 备份frame['a']非空对应的记录
# 创建新的'os'操作系统列,检查'a'是否包含'Windows',包含则对应记录填入'Windows',否则填入'Not Windows'
cframe['os'] = np.where(cframe['a'].str.contains('Windows'), 'Windows', 'Not Windows')
# 按时区列和新的操作系统'os'列对数据进行分组
by_tz_os = cframe.groupby(['tz', 'os'])
# 通过size方法实现组计数(类似于value_count函数)。结果可以通过unstack方法被重塑成一个表
agg_counts = by_tz_os.size().unstack().fillna(0) # 创建'tz'列元素作为行索引,'os'列元素作为列索引的DataFrame,缺失值用0填充
最后,让我们选择排名靠前的时区。为此,在agg_counts上对行计数,构造一个间接索引数组。
# 使用agg_counts.sum('columns')计算行计数后,可以调用argsor()函数来获得升序排序的Series
indexer = agg_counts.sum('columns').argsort() # agg_counts按列求和,并用argsort()函数进行升序排序,并返回相应序列的数组下标
indexer.values # 获得Series值对应的数组
# 使用take方法按顺序选择行,然后切掉最后10个(最大值)
count_subset = agg_counts.take(indexer[-10:]) # take方法会沿轴返回给定索引中的元素,所以这里返回行计数最大的10个时区(这里的indexer[-10:]可以替换为indexer.values[-10:])
# pandas有一个方法称为'nlargest'可以实现相同的功能
agg_counts.sum(axis='columns').nlargetst(10) # series.nlargest(n)可以获取series值最大的10条记录,但是是从大到小的排列顺序
# 然后,可以使用seaborn的barplot函数将其绘制到分组条形图中,用于比较Windows和非Windows用户的数量(见图13.2)
# 首先调用count_subset.stack()并重置索引以重新排列数据以更好地与seaborn兼容
count_subset = count_subset.stack() # 将DataFrame变成Series
count_subset.name = 'total' # 为了后面重置索引时,值的列名为'total'
count_subset = count_subset.reset_index() # 重置索引为初始数字索引
sns.barplot(x='total', y='tz', hue='os', data=count_subset)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooF6DACk-1669683442824)(https://secure2.wostatic.cn/static/47r9P5NSmAEqBu8xpRJ6bj/image.png?auth_key=1669683424-jrRkgKriG3HeFCNmWfhZCJ-0-307e28827c16a27ce11ce1f9f30da893)]
在较小的组中查看Windows用户的相对百分比有点困难,因此我们将组百分比规一化为和为1
def norm_total(group):
group['normed_total'] = group['total']/group['total'].sum()
return group
results = count_subset.groupby('tz').apply(norm_total) # 归一化
sns.barplot(x='normed_total', y='tz', hue='os', data=results)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7MDrN54-1669683442824)(https://secure2.wostatic.cn/static/onryNigKRY3ihJcfwE1rXS/image.png?auth_key=1669683424-dSNfZCaQ3FTxQ2vhXA2zAe-0-847d48e31c56eec2ed931de3c85d6ec6)]
通过使用transform方法可以更有效地计算归一化和
g = count_subset.groupby('tz')
results2 = count_subset['total']/g['total'].transform('sum')
13.2电影镜头1M数据集
GroupLens Research提供了许多在1990年代末和2000年代初从MovieLens用户那里收集的电影评级数据集合。这些苏剧提供电影评级、电影元数据(流派和年份)以及有关用户的人口统计数据(年龄、邮政编码、性别标识和职业)。这些数据通常在开发基于机器学习算法的推荐系统引起人们的兴趣。虽然我们在本书中没有详细探讨机器学习技术,但下面会展示如何将此类数据集切成你需要的确切形式。
MovieLens 1M数据集包含从6000名用户对4000部电影收集的100万个评分。它分布在三个表中:评级、用户信息和电影信息。我们可以使用pands.read_table方法将每个表加载到DataFrame对象。
unames = ['user_id', 'gender', 'age', 'occupation', 'zip']
users = pd.read_table('datasets/movielens/users.dat', sep='::', header=None, names=unames, engine='python') # 读取用户信息表
rnames = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_table('datasets/movielens/ratings.dat', sep='::', header=None, names=rnames, engine='python') # 读取评级表
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('datasets/movielens/movies.dat', sep='::', header=None, names=mnames, engine='python') # 读取电影信息表
注意,年龄和职业编码为整数。分析分布在三个表中的数据并非易事。例如,假设你要按照性别身份和年龄计算特定电影的平均评分。如你所见,将所有数据合并到一个表中比较方便。使用pandas的merge函数,我们首先将ratings和users合并,然后将该结果与movies数据合并。pandas根据重叠的名称推断哪些列用于合并(或连接)键。
data = pd.merge(pd.merge(ratings, users), movies)
# 要获得按性别分组的每部电影的平均电影评分,可以使用pivot_table方法
mean_ratings = data.pivot_table('rating', index='title', columns='gender', aggfunc='mean') # data按照title分组,组内按照'gender'分组,并计算rating的平均值
# 这将生成另一个DataFrame,其中包含以电影标题作为行标签('索引')和性别作为列标签的平均评分。
# 我们首先过滤接收到至少250(也可以是其他任意数字)个评分的电影,我按照标题对数据进行分组,并使用size()函数获取Series的每个标题的组大小
ratings_by_title = data.groupby('title').size()
active_titles = ratings_by_title.index[ratings_by_title >= 250] # 根据布尔数组筛选至少含250个评分的电影的标题(认为是活跃电影,可以参考的评分数据)
# 然后,接收到至少250个评分的标题索引可以被用于在mean_ratings中通过.loc方法选择行
mean_ratings = mean_ratings.loc[active_titles]
# 要查看女性观众中的热门电影,可以按列降序排序
top_female_ratings = mean_ratings.sort_values('F', ascendings=False)
衡量评级分歧
假设你想找到男性和女性观众之间分歧最大的电影。一种方法是添加一个包含均值差异的列到mean_ratings中,然后按照该列排序:
mean_ratings['diff'] = mean_ratings['M'] - mean_ratings['F']
# 按顺序排序会产生评分差异最大的电影,以便我们可以看到女性更喜欢哪些电影
sorted_by_diff = mean_ratings.sort_values('diff')
# 颠倒行的顺序,再筛选前10行,则可以得到男性偏爱,而女性的评价不高的电影
sorted_by_diff[::-1].head()
# 假设你想要引起观众最多分歧的电影,独立于性别认同。分歧可以通过评价的方差或者标准差来衡量
# 为了得到这一点,我们首先按照标题计算评级标准偏差,然后过滤到活动标题
rating_std_by_title = data.groupby('title')['rating'].std() # 按照'title'分组,组内对'rating'值计算标准差
rating_std_by_title = rating_std_by_title.loc[active_titles] # 获取活跃电影的相关标准差
# 按照降序排列,并筛选前10部评分最有争议的电影
rating_std_by_title.sort_values(ascending=False)[:10]
你可能已经注意到,电影类型以以管道分割符’|'给出,因为单个电影可以属于多个类型。为了帮助按照流派对评级数据进行分组,我们可以在DataFrame上 使用explode方法。
# 首先,我们可以在Series上使用str.split方法将流派字符串氛围流派列表
movies['genres'].head().str.split('|') # 对Series的前十个流派字符串使用str.split('|')方法,将它的值分为列表
movies['genre'] = movies.pop('genres').str.split('|') # 从DataFrame中弹出'genres'列,并将它的值从字符串处理成字符串的列表,然后赋值回DataFrame
# 现在,调用movies.explode('genre')会产生一个新的DataFrame,其中每个电影类型列表中的每个'内部'元素都对应一行
# 例如,如果一部电影被归为'Comedy'和'Romance',则结果中将有两行,一行为'Comedy',一行为'Romance'
movies_exploded = movies.explode('genre')
现在,我们可以将所有三个表合并在一起并按流派分组
ratings_with_genre = pd.merge(pd.merge(movies_exploded, ratings), users) # 先合并分解后的movies表和ratings表,再合并users表
ratings_with_genre.iloc[0] # 观察ratings_with_genre的第0行记录
# 按照'genre'和'age'分组,并对组内的'rating'值求均值,然后将'age'列展开为列索引
genre_ratings = ratings_with_genre.groupby(['genre', 'age'])['rating'].mean().unstack('age')
13.3美国婴儿名称1880到2010年
美国社会保障局(SSA)提供了从1880年至今婴儿名字频率的数据。Hadley Wickham是一些流行的R包的作者,他用这个数据集来说明R中的数据操作。
我们需要做一些数据整理来加载这个数据集,但是一旦我们这么做,我们将有一个看起来像这样的数据帧:
names.head(10)
结果: 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
你可能喜欢对数据集执行许多操作:
- 可视化一段时间内被赋予特定名字(你自己或者其他名字)的婴儿比例
- 确定名称的相对排名
- 确定每年最受欢迎的名称或受欢迎程度上升或下降最多的名称
- 分析名称的趋势:元音、辅音、长度、整体多样性、拼写变化、第一个和最后一个字母
- 分析趋势的外部来源:圣经名称,名人,人口统计
在撰写本文时,美国社会保障局每年提供一份数据文件,其中包含每种性别/姓名组合的出生总数。你可以下载这些文件的原始存档。
下载’National data’.zip文件后,解压缩它,你将拥有一个包含一系列文件的目录,例如yob1880.txt。我使用Unix head命令来查看其中一个文件的前十行(在Windows上,你可以使用more命令或在文本编辑器中打开它)
!head -n 10 datasets/babynames/yob1880.txt # 查看文件前10条记录
由于这已经采用逗号分隔的形式,因此可以使用pandas.read_csv命令将其加载到DataFrame中
names1880 = pd.read_csv('datasets/babynames/yob1880.txt', names=['name', 'sex', 'births'])
这些文件仅包含每年至少出现五次的名称,因此为了简单起见,我们可以将‘sex’划分的’births’列的总和用作该年的出生总数
names1880.groupby('sex')['births'].sum() # 按照'sex'分组,组内对'births'值求和
由于数据集按年份拆分为文件,因此首先要做的一件事情就是将所有数据组合到单个DataFrame中,并进一步添加字段。你可以使用pandas.concat命令运行以下Jupyter cell。
pieces = []
for year in range(1880, 2011):
path = f'datasets/babynames/yob{year}.txt' # 这里的f就是format的意思,即格式化字符串。这里等价于'datasets/babynames/yob{}.txt'.format(year)
frame = pd.read_csv(path, names=['name', 'sex', 'births']) # 读取对应年份的文件
frame['year'] = year # 向DataFrame添加年份信息
pieces.append(frame) # 将DataFrame放入列表中
# 将所有DataFrame拼接成一个DataFrame,并重排index
names = pd.concat(pieces, ignore_index=True)
这里有几点需要注意。首先,请记住,concat方法默认情况下按行合并DataFrame对象。其次,你必须传递ignore_index=True,因为我们对保留pandas.read_csv返回的原始行号并不感兴趣。因此,我们现在有一个DataFrame,其中包含所有年份的所有名称数据。
# 有了这些数据,我们已经可以使用groupby和pivot_table汇总年份和性别级别的数据(见图13.4)
# 按照'year'(值作为行索引)和'sex'(值作为列索引)分组,组内对‘births'求和
total_births = names.pivot_table('births', index='year', columns='sex', aggfunc=sum)
total_births.plot(title='Total births by sex and year')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Am0PrsgC-1669683442824)(https://secure2.wostatic.cn/static/sQdKUsugYNimJAcAR3hGBi/image.png?auth_key=1669683424-mCxrDxKxKM7egaAhXcosGT-0-6b8353c94024ba772fbdbb1f904a130f)]
接下来,让我们插入一个prop列,其中包含每个名字的婴儿相对于出生总数的比例。prop值为0.02表示每100个婴儿中有2个被赋予特定的名字。因此,我们按照年份和性别对分组,然后将新列添加到每个组
def add_prop(group):
group['prop'] = group['births'] / group['births'].sum()
return group
names = names.groupby(['year', 'sex']).apply(add_prop)
执行这样的组操作时,执行健全性检验通常很有价值,例如验证所有组的列总和是否为1
names.groupby(['year', 'sex'])['prop'].sum()
现在,我们将提取数据的子集以方便进一步分析:每个性别/年份组合的前1000个名字。
def get_top1000(group):
return group.sort_value('births', ascending=False)[:1000]
grouped = names.groupby(['year', 'sex'])
top1000 = grouped.apply(get_top1000)
# 我们可以删除组索引,因为year,sex已经在列索引名中了
top1000 = top1000.reset_index(drop=True) # 去掉原索引,并重排index,并赋值给top1000。等价于top1000.reset_index(drop=True, inplace=True)
我们可以在以下数据调查中使用这个前1000数据集。
分析命名规则
有了完整的数据集和前1000数据集,我们可以分析感兴趣的各种命名趋势了。首先,我们可以将前1000个名字分为男孩和女孩部分:
boys = top1000[top1000['sex'] == 'M']
girls = top1000[top1000['sex'] == 'F']
简单的时间序列(如每年的Johns或Marys的数量)可以被绘制,但需要一些操作才能更有用。
total_births = top1000.pivot_table('births', index='year', columns='name', aggfunc=sum) # 按'year'和'name'分组,组内对'births'求和
现在,可以使用DataFrame的plot方法绘制少数名称(图13.5显示了结果):
total_births.info()
subset = total_births[['John', 'Harry', 'Mary', 'Marilyn']] # 只选择其中的['John', 'Harry', 'Mary', 'Marilyn']列
subset.plot(subplots=True, figsize=(12, 10), title='Number of births per year') # 设置长为12,宽为10的幕布,并为每个列绘制子图(而不是挤在一张图里)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Er1JkcgD-1669683442825)(https://secure2.wostatic.cn/static/m7LpaghrRyTgJyQgGeuBBx/image.png)]
看到这里,你可能会得出结论,这些名字已经不受美国人口的青睐,但故事实际上比这更复杂,这将在下一节中探讨。
衡量命名多样性的增加
图形中的减少的一种解释是,为孩子选择通用名字的父母越来越少了。这个假设可以在数据中探索和证实。一个衡量标准是前1000个最受欢迎的名字所代表的出生比例,我将其汇总并按年份和性别绘制(图13.6显示了结果)
table = top1000.pivot_table('prop', index='year', columns='sex', aggfunc=sum) # 按'year'和'sex'分组,组内对'prop'求和
table.plot(title='Sum of table1000.prop by year and sex', yticks=np.linspace(0, 1.2, 13)) # 绘图,并指定y轴范围
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLYW8PdN-1669683442825)(https://secure2.wostatic.cn/static/qzSxScieD2Ba6c4r8vXQRe/image.png)]
你可以看到,其实,名称多样性似乎在增加(前1000的名字的总比例在下降)。另一个有趣的指标是前50%的新生儿中不同名字的数量,按照受欢迎程度从高到低的顺序排列。这个数字比较难计算。
# 让我们只考虑2010年的男孩名称
df = boys[boys['year'] == 2010]
在对prop进行降序排列后,我们想知道达到50%需要多少个受欢迎的名称。你可以写一个for循环来实现这一点,但是矢量化的NumPy方法在计算上更有效。通过cumsum方法计算prop列的累积和,然后调用searchsorted返回累积总和中,0.5可以插入并保持排序顺序的位置
prop_cumsum = df['prop'].sort_value(ascending=False).cumsum() # 'prop'降序排列,并求累积和
prop_cumsum[:10]
prop_cumsum.searchsorted(0.5) # 0.5应该插入Series的下标位置(从0开始记)
由于数组的索引从零开始,因此在此结果中添加1得到117(即前117个不同名字的新生儿的数量恰好超过50%)。相比之下,在1990年,,这个数字要小得多:
df = boys[boys.year == 1990]
in1900 = df.sort_values('prop', ascending=False).prop.cumsum()
in1900.searchsorted(0.5) + 1 # 1990年,0.5对应的位置为25(从1开始记)
现在你可以将此操作应用于每个年份/性别组合,groupby这些字段,并且应用一个函数使每个组返回一个数
def get_quantile_count(group, q=0.5):
group = group.sort_value('prop', ascending=False) # 组内数据按照'prop'降序排序
return group.prop.cumsum().searchsorted(q) + 1 # q值对应的位置(从1开始记)
diversity = top1000.groupby(['year', 'sex']).apply(get_quantile_count) # 按照'year'和'sex'分组,组内
diversity = diversity.unstack() # 展开为DataFrame
生成的diversity现在有两个时间序列,每个性别一个,按照年份索引。这可以像之前一样进行检查和绘制图片(见图13.7)
diversity.plot(title='Number of popular names in top 50%')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XqAUUJkc-1669683442826)(https://secure2.wostatic.cn/static/bheJrQiTGZL8Ctin2qZwyC/image.png)]
如你所见,女孩的名字一直比男孩的名字更多样化,而且随着时间的推移,它们只会变得更加多样化。
'最后一个字母’革命
2007年,婴儿名字研究员Laura Wattenberg指出,在过去的100年里,男孩名字按最后字母排列的分布发生了重大变化。为了看到这一点,我们首先按年份、性别和最后一个字母汇总完整数据集合的所有出生:
def get_last_letter(x):
return x[-1]
last_letters = names['name'].map(get_last_letter) # 构造Series
last_letters.name = 'last_letter' # 重命名
# 按'last_letters'、'sex'和'year'分组,组内对'births'求和
table = names.pivot_table('births', index=last_letters, columns=['sex', 'year'], aggfunc=sum)
# 然后我们选择历史上有代表性的三年('1910', '1960'和'2010'),打印前几行
subtable = table.reindex(columns=[1910, 1960, 2010], level='year')
subtable.head()
接下来,按总出生人数将表格归一化,计算出一个新的表格,表格中包含以每个字母结尾的性别在总出生人数中所占的比例
subtable.sum() # 跨行求和
letter_prop = subtable/subtable.sum()
有了字母比例,我们就可以制作出按年划分的性别柱状图(见图13.8)
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') # rot为轴标签的旋转度数, 在第一个坐标轴上根据letter_prop['M']绘制条形图
letter_prop['F'].plot(kind='bar', rot=0, ax=axes[1], title='Female', legend=False) # 第二个坐标轴上的条形图不需要图例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBfluJHx-1669683442826)(https://secure2.wostatic.cn/static/uL7HBG6LrdZhszuaJsWFpE/image.png)]
正如你所看到的,以n结尾的男孩名字自20世纪60年代以来经历了显著的增长。回到之前创建的完整表格,我再次按照年份和性别进行归一化,并为男孩的名字选择一个字母子集,最后换位使每一列成为时间序列:
letter_group = table / table.sum() # 归一化
# 筛选男孩,并且最后一个字母为'd','n'和'y'的每年的births比例
dny_ts = letter_group.loc[['d', 'n', 'y'], 'M'].T
dny_ts.head()
# 有了这个时间序列的DataFrame,我可以用它的plot方法再次绘制随时间变化的趋势图(见图13.9):
dny_ts.plot()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BnWXav4P-1669683442826)(https://secure2.wostatic.cn/static/31CsWoWybwBNGMQpzpYmC8/image.png)]
男孩的名字变成了女孩的名字(反之亦然)
另一个有趣的趋势是,研究样本中早些时候在某一性别中更受欢迎的名字,但随着时间的推移,这些名字成为了另一性别的首选。一个例子是Lesley或Leslie这个名字。回到top1000 DataFrame,我计算了一个以“Lesl”开头的数据集中出现的名称列表:
all_names = pd.Series(top1000['name'].unique()) # 构建不重名的姓名的Series
lesley_like = all_names[all_names.str.contains('Lesl')] # 返回值字符串中包含'Lesl'的名称构成的Series
我们可以过滤这些名字,并按名字分组对births求和,以查看相对频率:
filtered = top1000[top1000['name'].isin(lesley_like)] # 等价于filtered = top1000[top1000["name"].str.contains('Lesl')]
filtered.groupby('name')['births'].sum() # 按照'name'分组,组内对'births'求和
接下来,我们可以按照’sex’和’year’聚合,并按照‘year’进行分组归一化
table = filtered.pivot_table('births', index='year', columns='sex', aggfunc='sum') # 按照'year'和'sex'分组,组内对'births'求和
table = table.div(table.sum(axis='columns'), axis='index') # 跨列求和,逐行做除法
table.tail() # 查看最后5条记录
# 最后,现在可以按性别绘制随时间变化的分解图(见图13.10)。
table.plot(style={'M': 'k-', 'F': 'k--'})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ThJMR7UX-1669683442827)(https://secure2.wostatic.cn/static/6XzNp628jH2nECTAVwX3JB/image.png)]
13.4美国农业部食品数据库
美国农业部(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模块
import json
db = json.load(open('datasets/usda_food/datanbase.json'))
db中的每一项都是一个字典,其中包含单个食物的所有数据。'nutrients’字段是一个字典列表,每个元素对应一个营养素。
db[0].keys() # 返回键:dict_keys(['id', 'description', 'tags', 'manufacturer', 'group', 'portions', 'nutrients'])
db[0]['nutrients'][0] # 第一项食物中的第一个营养素信息
nutrients = pd.DataFrame(db[0]['nutrients']) # 将第一项食物的营养素信息转化为DataFrame
nutrients.head(7) # 查看DataFrame前7项
将字典列表转化为DataFrame时,我们可以指定要提取的字段列表。我们将提取食品名称、组、ID和制造商:
info_keys = ['description', 'group', 'id', 'manufacturer']
info = pd.DataFrame(db, columns=info_keys)
info.head()
info.info()
从info.info()的输出中,我们可以看到manufacturer列中有缺失值
# 你可以通过使用value_counts方法查看食物组的分布
pd.value_counts(info['group'])[:10] # 查看各组的计数,并筛选数量靠前的十个组
现在,要对所有营养数据进行一些分析,最简单的方法是将每种食物的营养成分组成一个大表。为此,我们需要采取几个步骤。首先,我将食物营养素的每个列表转换为DataFrame,为食物的’id’添加一列,并将DataFrame追加到列表中。然后,用concat函数将它们拼接起来:
nutrients []
for rec in db:
fnuts = pd.DataFrame(rec['nutrients']) # 营养素提取出来构成DataFrame
fnuts['id'] = rec['id'] # DataFrame中添加'id'列
nutrients.append(fnuts) # 追加到列表中
nutrients = pd.concat(nutrients, ignore_index=True) # 拼接列表中的DataFrame并重排索引
事实上,DataFrame中有重复项,所以考虑删除它们:
nutrients.duplicated().sum() # 统计重复的数量,其中duplicated()方法返回的是布尔数组
nutrients = nutrients.drop_duplicates()
由于’group’和’description’在info和nutrients两个DataFrame对象中,为了清楚起见,我们重命名info中的名称:
col_mapping = {'description': 'food', 'group': 'fgroup'}
info = info.rename(columns=col_mapping, copy=False) # 重命名
info.info()
col_mapping = {'description': 'nutrient', 'group': 'nutgroup'}
nutrients = nutrients.rename(columns=col_mapping, copy=False) # 不复制底层数据
# 完成所有这些操作后,将info和nutrients按照'id'合并
ndata = pd.merge(nutrients, info, on='id')
ndata.info()
ndata.iloc[30000]
现在,我们可以按照食物组和营养类型绘制中位数图(如图13.11)
result = ndata.groupby(['nutrient', 'fgroup'])['value'].quantile(0.5) # 按照'nutrient', 'fgroup'分组,组内对'value'求0.5分位数
result['Zinc, Zn'].sort_values().plot(kind='barh') # 对行索引值为'Zinc, Zn'的子Series按值排序并绘制水平条形图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaHyI6Uh-1669683442827)(https://secure2.wostatic.cn/static/fg5tZBH7PHKX4qFFzaQa1G/image.png)]
使用Series方法idmax或argmax,你可以找到最富集每种营养素的食物。
by_nutrient = ndata.groupby(['nutgroup', 'nutrient'])
def get_maximum(x):
return x.loc[x.value.idxmax()] # idxmax方法返回轴上最大值第一次出现的索引,这里是得到组内的最大value值对应的索引
max_foods = by_nutrient.apply(get_maximum)[['value', 'food']] # 组内筛选value最大值的记录,并只提取记录中的'value'值和'food'值
max_foods['food'] = max_foods['food'].str[:50] # 'food'字段只显示前50个字符
max_foods.loc['Amino Acids']['food'] # 显示'Amino Acids'营养素组对应'food'字段的Series
13.5 2012年联邦选举委员会数据库
美国联邦选举委员会(FEC)公布了对政治竞选的贡献数据。这包括贡献者姓名、职业和雇主、地址和贡献金额。2012年美国总统大选的贡献数据以单个150兆字节的CSV文件P00000001-ALL.csv的形式提供,该文件可以通过pandas.read_csv加载:
fec = pd.read_csv('datasets/fec/P00000001-ALL.csv', low_memory=False)
# 文件里的数据列有混合类型,而pandas默认要找到可以使所占空间最小的类型来储存数据
# low_memory=False之后,pandas不再寻找,直接采用较大的数据类型来储存
fec.info()
你可能会想到一些方法来开始对这些数据进行切片和切块,以提取有关捐助者和活动捐款模式的信息统计数据。我将向你展示一些不同的分析,这些分析应用了本书中的技术。
你可以看到数据中没有政党隶属关系,因此添加这将很有用。你可以使用unique方法获取所有唯一政治候选人的列表。
unique_cands = fec['cand_nm'].unique() # 筛选['cand_nm']列对应的Series,并通过unique()方法获得唯一值对应的数组
unique_cands[2] # 获得数组对应的第二个值
# 指示党派关系的一种方法是使用字典
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方法,你可以从候选人名称计算
fec['cand_nm'][123456:123461] # ['cand_nm']列的第123456到123461(不含)对应的记录
fec['cand_nm'][123456:123461].map(parties) # 值进行映射
fec['party'] = fec['cand_nm'].map(parties) # 构造'party'列
fec['party'].value_counts() # 对'party'列计数
几个数据准备点。首先,此数据包含贡献款和退款(负的贡献数量)
(fec['contb_receipt_amt'] > 0).value_counts() # 贡献款和退款的计数
# 为了简化分析,我们将数据集限制为正贡献
fec = fec[fec['contb_receipt_amt'] > 0]
由于Barack Obama和Mitt Romney是主要的两个候选人,我还将准备一个对他们的竞选活动有贡献的子集:
fec_mrbo = fec[fec['cand_nm'].isin(['Obama, Barack', 'Romney, Mitt'])] # 构造'cand_nm'为'Barack Obama'和'Mitt Romney'的子集
按职业和雇主划分的捐款统计数字
按职业划分的捐款是另一个经常研究的统计数据。例如,律师倾向于向民主党捐赠更多资金,而企业高管倾向于向共和党捐赠更多资金。
# 首先,按职业划分的捐赠总数可以计算如下
fec['contbr_occupation'].value_counts()[:10] # 统计贡献数量最多的十个职业
通过查看职业,你会注意到许多都是相同的基本工作类型,或者同一事物的多种辩题。以下代码片段说明了一种通过从一个职业映射到另一个职业来清理其中一些职业的技术;请注意使用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):
return occ_mapping.get(x, x) # 如果occ_mapping中有x对应的值,则返回对应的值,否则返回x
fec['contr_occupation'] = fec['contr_occupation'].map(get_occ) # 对'contr_occupation'列进行映射
# 对'contbr_employer'列也做类似操作
emp_mapping = {
"INFORMATION REQUESTED PER BEST EFFORTS" : "NOT PROVIDED",
"INFORMATION REQUESTED" : "NOT PROVIDED",
"SELF" : "SELF-EMPLOYED",
"SELF EMPLOYED" : "SELF-EMPLOYED",
}
def get_emp(x):
return emp_mapping.get(x, x)
fec['contbr_employer'] = fec['contbr_employer'].map(get_emp)
现在,你可以使用pivot_table方法来按党派和职业聚合数据,然后筛选到总共捐赠至少200万美元的子集
by_occupation = fec.pivot_table('contb_receipt_amt', index='contbr_occupation', columns='party', aggfunc='sum')
over_2mm = by_occupation[by_occupation.sum(axis='columns') > 2000000] # 跨列求和,并筛选和大于2000000的职业对应的记录
以条形图的形式查看此数据可能更容易('barh’意味着水平条形图)
over_2mm.plot(kind="barh")
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EIAdDOuq-1669683442828)(https://secure2.wostatic.cn/static/aGjJr2Uh1pqtCHiEJDua3F/image.png)]
你可能对向奥巴马和罗姆尼捐款的顶级捐赠职业或顶级公司感兴趣。为此,你可以按照候选者名称分组,并使用本章前面的top方法的变体
def get_top_amounts(group, key, n=5):
totals = group.groupby(key)['contb_receipt_amt'].sum() # 对groupby对象继续按key分组,组内对贡献额求和
return totals.nlargest(n) # 返回前n条记录
# 然后按职业和雇主汇总
grouped = fec_mrbo.groupby('cand_nm') # 按照候选人名称分组
grouped.apply(get_top_amounts, 'contbr_occupation', n=7) # 对groupby对象应用get_top_amounts方法并传递参数
grouped.apply(get_top_amounts, 'contbr_employer', n=10)
捐赠金额分箱
分析此数据的一种有用方法是使用cut函数按贡献大小将贡献者数量离散到箱中
bins = np.array([0, 1, 10, 100, 1000, 10000, 100_000, 1_000_000, 10_000_000])
labels = pd.cut(fec_mrbo['contb_receipt_amt'], bins) # 利用cut函数分箱,获得category对象组成的Series
# 然后我们可以按照姓名和箱标签对奥巴马和罗姆尼的数据进行分组,以按捐赠大小获得直方图
grouped = fec_mrbo.grouped(['cand_nm', labels]) # 按照候选人姓名和箱标签分组
grouped.size().unstack(level=0) # 组内通过size()函数计数,并将第0个层级('cand_nm')展开成列
数据显示,奥巴马收到的小额捐款数量明显多于罗姆尼。
你还可以对捐款金额求和并在箱内进行归一化,以可视化候选人每种规模的捐款总额的百分比(图13.13显示了结果图)
bucket_sums = grouped['contb_receipt_amt'].sum().unstack(level=0) # 组内对贡献额求和,并将第0个层级('cand_nm')展开成列
normed_sums = bucket_sums.div(bucket_sums.sum(axis='columns'), axis='index') # 跨列求和,逐行做除法
normed_sums
normed_sums[:-2].plot(kind='barh') # 排除最后两个级别的捐款水平数据(因为它们不是个人的捐款)外,对其他数据绘制水平条形图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJzjY2DI-1669683442828)(https://secure2.wostatic.cn/static/qxzVXhgtgUWxLBeQg9aze5/image.png)]
各州捐赠统计
我们先按候选人和州统计数据汇总数据
grouped = fec_mrbo.groupby(['cand_nm', 'contbr_st']) # 按候选人和州分组
totals = grouped['contb_receipt_amt'].sum().unstack(level=0).fillna(0) # 组内对捐献额求和,并将第0个层级('cand_nm')展开成列,对缺失值填充0
totals = totals[totals.sum(axis='columns') > 100000] # 筛选跨列求和大于100000的记录
totals.head(10) # 返回前10条记录
如果将每行除以总捐款金额,则可以得到每个候选人的捐款总额占各州捐款的相对百分比
percent = totals.div(totals.sum(axis='columns'), axis='index')
percent.head(10)