为了准备数据进行分析,我们需要执行数据处理。在本节中,我们将学习如何清理和重新格式化数据(例如,重命名列和修复数据类型不匹配)、对其进行重构/整形,以及对其进行丰富(例如,离散化列、计算聚合和组合数据源)。
在本节中,我们将介绍如何创建、重命名和删除列;类型转换;和排序-所有这些都使我们的分析更容易。我们将使用NYC Open Data提供的2019年黄色出租车出行数据。
import pandas as pd
taxis = pd.read_csv('../data/2019_Yellow_Taxi_Trip_Data.csv')
taxis.head()
数据源:NYC Open Data
删除列
让我们从删除ID列和store_and_fwd_flag列开始,我们不会使用这些列。
mask = taxis.columns.str.contains('id$|store_and_fwd_flag', regex=True)
columns_to_drop = taxis.columns[mask]
columns_to_drop
'''
Index(['vendorid', 'ratecodeid', 'store_and_fwd_flag', 'pulocationid',
'dolocationid'],
dtype='object')
'''
taxis = taxis.drop(columns=columns_to_drop)
taxis.head()
提示:另一种方法是选择要保留的列:taxis.loc[:,~mask]
重命名列
taxis = taxis.rename(
columns={
'tpep_pickup_datetime': 'pickup',
'tpep_dropoff_datetime': 'dropoff'
}
)
taxis.columns
'''
Index(['pickup', 'dropoff', 'passenger_count', 'trip_distance', 'payment_type',
'fare_amount', 'extra', 'mta_tax', 'tip_amount', 'tolls_amount',
'improvement_surcharge', 'total_amount', 'congestion_surcharge'],
dtype='object')
'''
类型转换
注意到数据类型有什么问题吗?
taxis.dtypes
'''
pickup object
dropoff object
passenger_count int64
trip_distance float64
payment_type int64
fare_amount float64
extra float64
mta_tax float64
tip_amount float64
tolls_amount float64
improvement_surcharge float64
total_amount float64
congestion_surcharge float64
dtype: object
'''
pickup 和 dropoff 都应存储为日期时间。让我们解决这个问题:
taxis[['pickup', 'dropoff']] = \
taxis[['pickup', 'dropoff']].apply(pd.to_datetime)
taxis.dtypes
'''
pickup datetime64[ns]
dropoff datetime64[ns]
passenger_count int64
trip_distance float64
payment_type int64
fare_amount float64
extra float64
mta_tax float64
tip_amount float64
tolls_amount float64
improvement_surcharge float64
total_amount float64
congestion_surcharge float64
dtype: object
'''
提示:还有其他方法可以执行类型转换。对于数值,我们可以使用pd.to_numeric()函数,稍后我们将看到astype()方法,这是一个更通用的方法。
创建新列
让我们为每一行计算以下内容:
taxis = taxis.assign(
elapsed_time=lambda x: x.dropoff - x.pickup, # 1
cost_before_tip=lambda x: x.total_amount - x.tip_amount,
tip_pct=lambda x: x.tip_amount / x.cost_before_tip, # 2
fees=lambda x: x.cost_before_tip - x.fare_amount, # 3
avg_speed=lambda x: x.trip_distance.div(
x.elapsed_time.dt.total_seconds() / 60 / 60
) # 4
)
我们的新列将添加到右侧:
taxis.head(2)
注意事项:
我们使用lambda函数是为了:1)避免重复输入taxis; 2)能够以创建cost_before_tip和elapsed_time列的相同方法访问它们。
要创建一个新列,我们也可以使用df['new_col'] =
。
按值排序
我们可以使用sort_values()方法根据任意数量的列进行排序:
taxis.sort_values(['passenger_count', 'pickup'], ascending=[False, True]).head()
要挑选最大/最小的行,请使用nlargest()
/nsmallest()
。查看3个运行时间最长的行程:
taxis.nlargest(3, 'elapsed_time')
到目前为止,我们还没有真正使用索引,因为它只是一个行号;然而,我们可以改变索引中的值来访问pandas的其他功能。
设置和排序索引
目前,我们有一个RangeIndex,但我们可以通过在调用set_index()时指定一个datetime列来切换到DatetimeIndex:
taxis = taxis.set_index('pickup')
taxis.head(3)
既然我们有一个完整数据集的样本,让我们按取件时间对索引进行排序:
taxis = taxis.sort_index()
提示:taxis.sort_index(axis=1)将按名称对列进行排序。轴参数在整个pandas中都存在:axis=0目标行,axis=1目标列。
现在我们可以根据日期时间从数据中选择范围,就像我们选择行号一样:
taxis['2019-10-23 07:45':'2019-10-23 08']
taxis.loc['2019-10-23 08']
重置索引
我们将在本节后面处理时间序列,但有时我们希望将索引重置为行号并恢复列。我们可以使用reset_index()方法:
taxis = taxis.reset_index()
taxis.head()
我们正在使用的出租车数据集是一种有利于分析的格式。但情况并非总是如此。现在我们来看一下TSA的旅客吞吐量数据,它将2021年的吞吐量与2020年和2019年的同一天进行了比较:
tsa = pd.read_csv('../data/tsa_passenger_throughput.csv', parse_dates=['Date'])
tsa.head()
首先,我们将列名称小写,并将第一个单词(例如,2021年旅客吞吐量),以使其更易于处理:
tsa = tsa.rename(columns=lambda x: x.lower().split()[0])
tsa.head()
Melting(熔化)
Melting(熔化)有助于将数据转换为长格式。现在,我们在一列中显示了所有的旅客吞吐量:
tsa_melted = tsa.melt(
id_vars='date', # 唯一标识行的列(可以是多个)
var_name='year', # 通过熔化创建的新列的名称
value_name='travelers' # 包含来自已经融化列的值的新列的名称
)
tsa_melted.sample(5, random_state=1) # show some random entries
为了将其转换为旅客吞吐量的时间序列,我们需要将date列中的年份替换为year列中的年份。否则,我们就把往年的数字标错了年份。
tsa_melted = tsa_melted.assign(
date=lambda x: pd.to_datetime(x.year + x.date.dt.strftime('-%m-%d'))
)
tsa_melted.sample(5, random_state=1)
tsa_melted.sort_values('date').tail(3)
tsa_melted = tsa_melted.dropna()
tsa_melted.sort_values('date').tail(3)
旋转
使用融合的数据,我们可以透视数据,以比较不同年份特定日期的TSA旅客吞吐量:
tsa_pivoted = tsa_melted\
.query('date.dt.month == 3 and date.dt.day <= 10')\
.assign(day_in_march=lambda x: x.date.dt.day)\
.pivot(index='year', columns='day_in_march', values='travelers')
tsa_pivoted
重要提示:我们没有介绍unstack()和stack()方法,它们分别是透视和融化的附加方法。
当我们有一个多层次的索引时,这些就派上用场了(例如,如果我们对多列运行set_index())
转置
tsa_pivoted.T
我们通常会在假期前后观察航空旅行的变化,因此在TSA数据集中添加有关日期的信息可以提供更多上下文。holidays.csv文件包含美国的几个主要节日:
holidays = pd.read_csv('../data/holidays.csv', parse_dates=True, index_col='date')
holidays.loc['2019']
将假日与TSA旅客吞吐量数据合并将为我们的分析提供更多背景:
tsa_melted_holidays = tsa_melted\
.merge(holidays, left_on='date', right_index=True, how='left')\
.sort_values('date')
tsa_melted_holidays.head()
提示:这个方法有很多参数,所以一定要查阅文档。要追加行,请查看pd.concat()函数。
我们可以更进一步,把每个假期的前后几天标记为假期的一部分。这将更容易比较跨年的假日旅行,并寻找假日前后旅行的任何上升:
tsa_melted_holiday_travel = tsa_melted_holidays.assign(
holiday=lambda x:
x.holiday\
.fillna(method='ffill', limit=1)\
.fillna(method='bfill', limit=2)
)
提示:查看文档以获得fillna()方法可用功能的完整列表。
请注意,我们现在有了每个假日后一天和前两天的值。2019年的感恩节是11月28日,所以26日、27日、29日都排满了。因为我们只替换空值,所以我们不会用平安夜的前向填充覆盖Christmas Day:
tsa_melted_holiday_travel.query(
'year == "2019" and '
'(holiday == "Thanksgiving" or holiday.str.contains("Christmas"))'
)
在重塑和清理数据之后,我们可以执行聚合,以各种方式汇总数据。在本节中,我们将探索如何使用透视表、交叉表和分组依据操作来聚合数据。
数据透视表
我们可以构建一个透视表来比较数据集中各年的假日旅行情况:
tsa_melted_holiday_travel.pivot_table(
index='year', columns='holiday',
values='travelers', aggfunc='sum'
)
我们可以在这个结果上使用pct_change()方法来查看哪些假日旅行时段的旅行变化最大:
tsa_melted_holiday_travel.pivot_table(
index='year', columns='holiday',
values='travelers', aggfunc='sum'
).pct_change()
让我们创建最后一个包含列和行小计的透视表,沿着对格式进行一些改进。首先,我们为所有浮动设置一个显示选项:
pd.set_option('display.float_format', '{:,.0f}'.format)
接下来,我们将圣诞前夜和圣诞节分组在一起,同样,将新年前夜和元旦分组在一起,并创建透视表:
import numpy as np
tsa_melted_holiday_travel.assign(
holiday=lambda x: np.where(
x.holiday.str.contains('Christmas|New Year', regex=True),
x.holiday.str.replace('Day|Eve', '', regex=True).str.strip(),
x.holiday
)
).pivot_table(
index='year', columns='holiday',
values='travelers', aggfunc='sum',
margins=True, margins_name='Total'
)
pd.reset_option('display.float_format')
在处理时间序列数据时,pandas为我们提供了额外的功能,不仅可以比较数据集中的观测结果,还可以利用它们在时间上的关系来分析数据。在本节中,我们将看到一些这样的操作,用于选择日期/时间范围、计算随时间的变化、执行窗口计算以及将数据重新采样到不同的日期/时间间隔。
根据日期和时间选择
taxis = taxis.set_index('dropoff').sort_index()
我们前面看到可以对日期时间进行切片:
taxis['2019-10-24 12':'2019-10-24 13']
我们也可以用简写来表示这个范围。注意我们必须在这里使用loc[]:
taxis.loc['2019-10-24 12']
我们可以使用between_time()方法提取任意一天某个特定时间范围内发生的下降:
taxis.between_time('12:00', '13:00')
提示:at_time()
方法可用于提取给定时间的所有条目(e.g., 12:35:27)。
最后,head()和tail()将我们限制在一定数量的行,但是我们可能对数据的前/后2小时(或任何其他时间间隔)内的行感兴趣,在这种情况下,我们应该使用first()/ last():
taxis.first('2H')
在本节的其余部分,我们将使用TSA旅客吞吐量数据。让我们从设置date列的索引开始:
tsa_melted_holiday_travel = tsa_melted_holiday_travel.set_index('date')
计算随时间的变化
tsa_melted_holiday_travel.loc['2020'].assign(
one_day_change=lambda x: x.travelers.diff(),
seven_day_change=lambda x: x.travelers.diff(7),
).head(10)
提示:要执行减法以外的运算,请查看shift()
方法。它还使跨列执行操作成为可能。
重采样
我们可以使用重采样将时间序列数据聚合到一个新的频率:
tsa_melted_holiday_travel['2019':'2021-Q1'].select_dtypes(include='number')\
.resample('Q').agg(['sum', 'mean', 'std'])
窗口计算类似于分组依据计算,不同之处在于执行计算的组不是静态的-它可以移动或扩展。Pandas提供用于构造各种窗口的功能,包括移动/滚动窗口,展开窗口(例如,在时间序列中直到当前日期的累积和或平均值),以及指数加权移动窗口(对更近的观测值比更远的观测值更加权)。这里我们只看滚动和展开计算。
执行窗口计算与按计算分组非常相似-我们首先定义窗口,然后指定聚合:
tsa_melted_holiday_travel.loc['2020'].assign(
**{
'7D MA': lambda x: x.rolling('7D').travelers.mean(),
'YTD mean': lambda x: x.expanding().travelers.mean()
}
).head(10)
要想了解到底发生了什么,最好把原始数据和结果可视化,下面我们就来先睹为快,用pandas绘图。首先,在notebook中嵌入SVG格式图的一些设置:
import matplotlib_inline
from utils import mpl_svg_config
matplotlib_inline.backend_inline.set_matplotlib_formats(
'svg', # output images using SVG format
**mpl_svg_config('section-2') # optional: configure metadata
)
现在,我们调用plot()方法来可视化数据:
_ = tsa_melted_holiday_travel.loc['2020'].assign(
**{
'7D MA': lambda x: x.rolling('7D').travelers.mean(),
'YTD mean': lambda x: x.expanding().travelers.mean()
}
).plot(title='2020 TSA Traveler Throughput', ylabel='travelers', alpha=0.8)