本文概述
在上一篇文章中, 你通过拆分应用合并的原理了解了groupby操作是如何自然产生的。你检出了Netflix用户评级的数据集, 并按电影的发行年份对行进行了分组, 以生成下图:
这是通过按单个列进行分组来实现的。顺便说一句, 我提到你可能希望按几列进行分组, 在这种情况下, 生成的pandas DataFrame最终会具有多索引或层次结构索引。在本文中, 你将学习什么层次索引, 并查看它们在按数据的几个功能分组时如何产生。你可以在我们的”用熊猫操作数据框”课程中找到有关所有这些概念和实践的更多信息。
首先, 什么是层次结构索引?
分层索引和熊猫数据框
什么是数据帧的索引?
在介绍层次结构索引之前, 我想让你回顾一下熊猫DataFrame的索引是什么。 DataFrame的索引是一个集合, 由每行的标签组成。让我们来看一个例子。我将首先导入假设的srcmini学生Ellie在srcmini上的活动的综合数据集。这些列是日期, 编程语言以及当天Ellie用该语言完成的练习次数。加载数据:
# Import pandas
import pandas as pd
# Load in data
df = pd.read_csv('data/user_ex_python.csv')
df
日期 | 语言 | ex_complete | |
---|---|---|---|
0 | 2017-01-01 | python | 6 |
1 | 2017-01-02 | python | 5 |
2 | 2017-01-03 | python | 10 |
你可以在DataFrame的左侧看到Index, 它由整数组成。这是一个RangeIndex:
# Check out index
df.index
RangeIndex(start=0, stop=3, step=1)
我们可以使用此索引切出一行df:
# Slice and dice data
df.loc[:1]
日期 | 语言 | ex_complete | |
---|---|---|---|
0 | 2017-01-01 | python | 6 |
1 | 2017-01-02 | python | 5 |
但是, 该索引的信息不足。如果要标记DataFrame的行, 则尽可能以有意义的方式标记它们。你可以对相关数据集执行此操作吗?考虑这一挑战的一种好方法是, 你希望每行都有一个唯一且有意义的标识符。检查列, 看是否符合这些条件。注意, date列包含唯一的日期, 因此在date列上标记每一行是有意义的。也就是说, 你可以使用.set_index()方法使date列成为DataFrame的索引(n.b. inplace = True表示你实际上是就地更改了DataFrame df):
# Set new index
df.set_index(pd.DatetimeIndex(df['date']), inplace=True)
df
日期 | 语言 | ex_complete | |
---|---|---|---|
日期 | |||
2017-01-01 | 2017-01-01 | python | 6 |
2017-01-02 | 2017-01-02 | python | 5 |
2017-01-03 | 2017-01-03 | python | 10 |
然后给df一个DateTimeIndex:
# Check out new index
df.index
DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], dtype='datetime64[ns]', name='date', freq=None)
现在, 你可以使用创建的DateTimeIndex分割行:
# Slice and dice data w/ new index
df.loc['2017-01-02']
date 2017-01-02
language python
ex_complete 5
Name: 2017-01-02 00:00:00, dtype: object
还请注意, .columns属性返回包含df列名称的索引:
# Check out columns
df.columns
Index(['date', 'language', 'ex_complete'], dtype='object')
这可能会造成一些混乱, 因为这表示df.columns是Index类型。这并不意味着这些列是DataFrame的索引。 df的索引始终由df.index给出。查看我们的pandas DataFrames教程以获取更多有关索引的信息。现在是时候满足层次结构索引了。
熊猫DataFrame的多索引
如果像srcmini一样, 我们的数据集有多种语言怎么办?看一看:
# Import and check out data
df = pd.read_csv('data/user_ex.csv')
df
日期 | 语言 | ex_complete | |
---|---|---|---|
0 | 2017-01-01 | python | 6 |
1 | 2017-01-02 | python | 5 |
2 | 2017-01-03 | python | 10 |
3 | 2017-01-01 | [R | 8 |
4 | 2017-01-02 | [R | 8 |
5 | 2017-01-03 | [R | 8 |
现在, 每个日期对应于几行, 每种语言对应一行。例如, 对于语言Python和R, 日期’2017-01-02’分别出现在第1和第4行中。因此, 日期不再唯一地指定行。但是, “日期”和”语言”共同可以唯一地指定行。因此, 我们将两者都用作索引:
# Set index
df.set_index(['date', 'language'], inplace=True)
df
ex_complete | ||
---|---|---|
日期 | 语言 | |
2017-01-01 | python | 6 |
2017-01-02 | python | 5 |
2017-01-03 | python | 10 |
2017-01-01 | [R | 8 |
2017-01-02 | [R | 8 |
2017-01-03 | [R | 8 |
现在, 你已经创建了一个多索引或分层索引(对这两个术语都感到满意, 因为你会发现它们可以互换使用), 你可以通过按以下方式检出索引来看到这一点:
# Check out multi-index
df.index
MultiIndex(levels=[['2017-01-01', '2017-01-02', '2017-01-03'], ['python', 'r']], labels=[[0, 1, 2, 0, 1, 2], [0, 0, 0, 1, 1, 1]], names=['date', 'language'])
上面告诉你, DataFrame df现在具有一个MultiIndex, 它具有两个级别, 第一个级别由日期给出, 第二个级别由语言给出。回想一下, 上面你可以使用索引和.loc访问器来切片DataFrame:df.loc [‘2017-01-02’]。为了能够使用多索引切片, 你需要首先对索引进行排序:
# Sort index
df.sort_index(inplace=True)
df
ex_complete | ||
---|---|---|
日期 | 语言 | |
2017-01-01 | python | 6 |
[R | 8 | |
2017-01-02 | python | 5 |
[R | 8 | |
2017-01-03 | python | 10 |
[R | 8 |
现在, 你可以通过将元组传递给.loc访问器, 从而切出2017年1月2日完成的R练习的数量:
# Slice & dice your DataFrame
df.loc[('2017-01-02', 'r')]
ex_complete 8
Name: (2017-01-02, r), dtype: int64
你现在对分层索引(或多索引)有所了解。现在该看看在使用groupby对象时它们如何产生。
层次结构索引, groupby对象和Split-Apply-Combine
在上一篇文章中, 我们探讨了使用netflix数据进行分组的对象以及split-apply-combine的数据分析原理。让我们快速浏览一下另一个数据集, 这些数据集内置在seaborn软件包中。 “提示”包含小费, total_bill, 星期几和一天中的时间等功能。首先加载并浏览数据:
# Import and check out data
import seaborn as sns
tips = sns.load_dataset("tips")
tips.head()
total_bill | 小费 | 性别 | 吸烟者 | 天 | 时间 | 尺寸 | |
---|---|---|---|---|---|---|---|
0 | 16.99 | 1.01 | 女 | No | Sun | 晚餐 | 2 |
1 | 10.34 | 1.66 | 男 | No | Sun | 晚餐 | 3 |
2 | 21.01 | 3.50 | 男 | No | Sun | 晚餐 | 3 |
3 | 23.68 | 3.31 | 男 | No | Sun | 晚餐 | 2 |
4 | 24.59 | 3.61 | 女 | No | Sun | 晚餐 | 4 |
请注意, 技巧的索引是RangeIndex:
# Check out index
tips.index
RangeIndex(start=0, stop=244, step=1)
在深入研究计算之前, 总是可以做一些可视化的EDA, 而Seaborn的pairplot函数可以让你大致了解所有数值变量:
# Import pyplot, figures inline, set style, plot pairplot
import matplotlib.pyplot as plt
%matplotlib inline
sns.set()
sns.pairplot(tips, hue='day');
如果要查看”吸烟者”和”不吸烟者”之间的平均小费之间的差异, 可以将原始数据框架按”吸烟者”划分(使用groupby), 应用功能”均值”并合并为一个新的DataFrame :
# Get mean of smoker/non-smoker groups
df = tips.groupby('smoker').mean()
df
total_bill | 小费 | 尺寸 | |
---|---|---|---|
吸烟者 | |||
是 | 20.756344 | 3.008710 | 2.408602 |
No | 19.188278 | 2.991854 | 2.668874 |
DataFrame df的结果索引是原始” tips” DataFrame的”吸烟者”列/特征:
# Check out new index
df.index
CategoricalIndex(['Yes', 'No'], categories=['Yes', 'No'], ordered=False, name='smoker', dtype='category')
如果需要, 你可以重置索引, 以使”吸烟者”成为DataFrame的一列:
# Reset the index
df.reset_index()
吸烟者 | total_bill | 小费 | 尺寸 | |
---|---|---|---|---|
0 | 是 | 20.756344 | 3.008710 | 2.408602 |
1 | No | 19.188278 | 2.991854 | 2.668874 |
现在是时候找出分层索引是如何从split-apply-combine和groupby操作中产生的。
多个分组和层次结构索引
上面, 你根据”吸烟者”功能对提示数据集进行了分组。有时, 你需要根据两个功能对数据集进行分组。例如, 将小费数据集归类为吸烟者/不吸烟者和晚餐/午餐是很自然的。为此, 将希望分组的列名作为列表传递:
# Group by two columns
df = tips.groupby(['smoker', 'time']).mean()
df
total_bill | 小费 | 尺寸 | ||
---|---|---|---|---|
吸烟者 | 时间 | |||
是 | 午餐 | 17.399130 | 2.834348 | 2.217391 |
晚餐 | 21.859429 | 3.066000 | 2.471429 | |
No | 午餐 | 17.050889 | 2.673778 | 2.511111 |
晚餐 | 20.095660 | 3.126887 | 2.735849 |
综上所述, 你也许可以看到”吸烟者”和”时间”都是df的指数。这是事实, 这是有道理的:如果按”吸烟者”分组导致索引为原始”吸烟者”列, 那么按两列分组将为你提供两个索引。检查索引以确认它是分层的:
# Check out index
df.index
MultiIndex(levels=[['Yes', 'No'], ['Lunch', 'Dinner']], labels=[[0, 0, 1, 1], [0, 1, 0, 1]], names=['smoker', 'time'])
是的。你现在可以做很多有用的事情, 例如获取每个分组中的计数:
# Group by two features
tips.groupby(['smoker', 'time']).size()
smoker time
Yes Lunch 23
Dinner 70
No Lunch 45
Dinner 106
dtype: int64
你还可以交换层次结构索引的级别, 以便在索引中的”吸烟者”之前出现”时间”:
# Swap levels of multi-index
df.swaplevel()
total_bill | 小费 | 尺寸 | ||
---|---|---|---|---|
时间 | 吸烟者 | |||
午餐 | 是 | 17.399130 | 2.834348 | 2.217391 |
晚餐 | 是 | 21.859429 | 3.066000 | 2.471429 |
午餐 | No | 17.050889 | 2.673778 | 2.511111 |
晚餐 | No | 20.095660 | 3.126887 | 2.735849 |
然后, 你可能希望从层次结构索引中删除这些功能之一, 并针对该功能形成不同的列。你可以使用unstack方法:
# Unstack your multi-index
df.unstack()
total_bill | 小费 | 尺寸 | ||||
---|---|---|---|---|---|---|
时间 | 午餐 | 晚餐 | 午餐 | 晚餐 | 午餐 | 晚餐 |
吸烟者 | ||||||
是 | 17.399130 | 21.859429 | 2.834348 | 3.066000 | 2.217391 | 2.471429 |
No | 17.050889 | 20.095660 | 2.673778 | 3.126887 | 2.511111 | 2.735849 |
你可以使用关键字参数” level”在索引的外部特征上进行堆叠:
# Unsstack the outer index
df.unstack(level=0)
total_bill | 小费 | 尺寸 | ||||
---|---|---|---|---|---|---|
吸烟者 | 是 | No | 是 | No | 是 | No |
时间 | ||||||
午餐 | 17.399130 | 17.050889 | 2.834348 | 2.673778 | 2.217391 | 2.511111 |
晚餐 | 21.859429 | 20.095660 | 3.066000 | 3.126887 | 2.471429 | 2.735849 |
如你所料, 拆栈的结果具有非分层索引:
# Check out index
df.unstack().index
CategoricalIndex(['Yes', 'No'], categories=['Yes', 'No'], ordered=False, name='smoker', dtype='category')
结果, 你现在可以针对这些分组执行所有类型的数据分析。我鼓励你这样做。
日常使用的分层索引
在这篇文章中, 向你介绍了层次结构索引(或多索引), 并看到了它们是如何希望DataFrame索引唯一且有意义地标记DataFrame的行的自然结果。你还了解了当你需要将数据按多列进行分组时, 它们是如何产生的, 并采用了split-apply-combine的原理。我希望你对工作中的层次结构索引感到乐趣。
该帖子来自Jupyter Notebook;你可以在此存储库中找到它。如果你有任何想法, 回应和/或反省, 请随时通过twitter @ hugobowne与我联系。
评论前必须登录!
注册