Python 时间序列分析秘籍第二版(三)

原文:annas-archive.org/md5/7277c6f80442eb633bdbaf16dcd96fad

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:6 在 Python 中处理日期和时间

加入我们的 Discord 书籍社区

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png

packt.link/zmkOY

时间序列数据的核心是时间时间序列数据是按顺序和定期时间间隔捕捉的一系列观测值或数据点。在 pandas 的 DataFrame 上下文中,时间序列数据有一个按顺序排列的DatetimeIndex类型的索引,如你在前面的章节中所见。DatetimeIndex提供了一个简便且高效的数据切片、索引和基于时间的数据分组方式。

熟悉在时间序列数据中操作日期和时间是时间序列分析和建模的基本组成部分。在本章中,你将找到一些常见场景的解决方案,帮助你在处理时间序列数据中的日期和时间时得心应手。

Python 有几个内置模块用于处理日期和时间,如datetimetimecalendarzoneinfo模块。此外,Python 还有一些流行的库进一步扩展了日期和时间的处理能力,例如dateutilpytzarrow等。

本章将介绍datetime模块,但你将转向使用pandas进行更复杂的日期和时间操作,以及生成带有DatetimeIndex序列的时间序列 DataFrame。此外,pandas库包含多个继承自上述 Python 模块的日期和时间相关类。换句话说,你无需导入额外的日期/时间 Python 库。

你将会接触到 pandas 中的类,如TimestampTimedeltaPeriodDateOffset。你会发现它们之间有很多相似性——例如,pandas 的Timestamp类相当于 Python 的Datetime类,且在大多数情况下可以互换。类似地,pandas.Timedelta等同于 Python 的datetime.timedelta对象。pandas库提供了一个更简洁、直观和强大的接口,帮助你处理大多数日期和时间操作,无需额外导入模块。使用 pandas 时,你将享受一个包含所有时间序列数据处理所需工具的库,轻松应对许多复杂的任务。

以下是我们将在本章中讨论的解决方案列表:

  • 使用DatetimeIndex

  • DateTime提供格式参数

  • 使用 Unix 纪元时间戳

  • 处理时间差

  • 转换带有时区信息的DateTime

  • 处理日期偏移

  • 处理自定义工作日

在实际场景中,你可能并不会使用所有这些技巧或技术,但了解这些选项是至关重要的,尤其是在面对某些特定场景时,需要调整或格式化日期。

技术要求

在本章及之后的内容中,我们将广泛使用 pandas 2.1.3(发布于 2023 年 11 月 10 日)。这适用于本章中的所有示例。

请提前加载这些库,因为你将在本章中贯穿使用它们:

import pandas as pd
import numpy as np
import datetime as dt

你将在接下来的内容中使用 dtnppd 别名。

你可以从 GitHub 仓库下载 Jupyter 笔记本,github.com/PacktPublishing/Time-Series-Analysis-with-Python-Cookbook./tree/main/code/Ch6 来进行跟随操作。

处理 DatetimeIndex

pandas 库提供了许多选项和功能,可以简化在处理时间序列数据、日期和时间时繁琐的任务。

在 Python 中处理时间序列数据时,通常将数据加载到具有 DatetimeIndex 类型索引的 pandas DataFrame 中。作为索引,DatetimeIndex 类扩展了 pandas DataFrame 的功能,使其能够更高效、智能地处理时间序列数据。这个概念在第二章《从文件中读取时间序列数据》和第三章《从数据库中读取时间序列数据》中已经多次展示。

在完成本示例后,你将充分理解 pandas 提供的丰富日期功能,以处理数据中几乎所有日期/时间的表示方式。此外,你还将学习如何使用 pandas 中的不同函数将类似日期的对象转换为 DatetimeIndex。

如何做到这一点……

在本示例中,你将探索 Python 的 datetime 模块,并了解 TimestampDatetimeIndex 类以及它们之间的关系。

  1. 为了理解 Python 的 datetime.datetime 类与 pandas 的 TimestampDatetimeIndex 类之间的关系,你将创建三个不同的 datetime 对象,表示日期 2021, 1, 1。然后,你将比较这些对象以获得更好的理解:
dt1 = dt.datetime(2021,1,1)
dt2 = pd.Timestamp('2021-1-1')
dt3 = pd.to_datetime('2021-1-1')

检查日期时间表示:

print(dt1)
print(dt2)
print(dt3)
>>
2021-01-01 00:00:00
2021-01-01 00:00:00
2021-01-01 00:00:00

检查它们的数据类型:

print(type(dt1))
print(type(dt2))
print(type(dt3))
>>
<class 'datetime.datetime'>
<class 'pandas._libs.tslibs.timestamps.Timestamp'>
<class 'pandas._libs.tslibs.timestamps.Timestamp'>

最后,让我们看看它们的比较:

dt1 == dt2 == dt3
>> True
isinstance(dt2, dt.datetime)
>> True   
isinstance(dt2, pd.Timestamp)
>> True
isinstance(dt1, pd.Timestamp)
>> False

从前面的代码中可以看到,pandas 的 Timestamp 对象等同于 Python 的 Datetime 对象:

issubclass(pd.Timestamp, dt.datetime)
>> True

请注意,dt2pandas.Timestamp 类的一个实例,而 Timestamp 类是 Python 的 dt.datetime 类的子类(但反之不成立)。

  1. 当你使用 pandas.to_datetime() 函数时,它返回了一个 Timestamp 对象。现在,使用 pandas.to_datetime() 处理一个列表并检查结果:
dates = ['2021-1-1', '2021-1-2']
pd_dates = pd.to_datetime(dates)
print(pd_dates)
print(type(pd_dates))
>>
DatetimeIndex(['2021-01-01', '2021-01-02'], dtype='datetime64[ns]', freq=None)
<class 'pandas.core.indexes.datetimes.DatetimeIndex'>

有趣的是,输出现在是 DatetimeIndex 类型,它是使用你之前使用的相同 pandas.to_datetime() 函数创建的。此前,当对单个对象使用相同的函数时,结果是 Timestamp 类型,但当作用于列表时,它生成了一个 DatetimeIndex 类型的序列。你将执行另一个任务,以便更清楚地理解。

打印出 pd_dates 变量中的第一个元素(切片):

print(pd_dates[0])
print(type(pd_dates[0]))
>>
2021-01-01 00:00:00
<class 'pandas._libs.tslibs.timestamps.Timestamp'>

从前面的输出中,你可以推测出两个类之间的关系:DatetimeIndexTimestampDatetimeIndex 是一个 Timestamp 对象的序列(列表)。

  1. 现在你已经知道如何使用 pandas.to_datetime() 函数创建 DatetimeIndex,让我们进一步扩展,看看你还能使用这个函数做些什么。例如,你将看到如何轻松地将不同的 datetime 表示形式(包括字符串、整数、列表、pandas 系列或其他 datetime 对象)转换为 DatetimeIndex

让我们创建一个 dates 列表:

dates = ['2021-01-01',
         '2/1/2021',
         '03-01-2021',
         'April 1, 2021',
         '20210501',
          np.datetime64('2021-07-01'), # numpy datetime64
          datetime.datetime(2021, 8, 1), # python datetime
          pd.Timestamp(2021,9,1) # pandas Timestamp
          ]

使用 pandas.to_datetime() 解析列表:

parsed_dates = pd.to_datetime(
                 dates,
                 infer_datetime_format=True,
                 errors='coerce'
                 )
print(parsed_dates)
>>
DatetimeIndex(['2021-01-01', '2021-02-01', '2021-03-01', '2021-04-01', '2021-05-01', '2021-07-01', '2021-08-01', '2021-09-01'],
              dtype='datetime64[ns]', freq=None)

请注意,to_datetime() 函数如何正确解析不同字符串表示形式和日期类型的整个列表,如 Python 的 Datetime 和 NumPy 的 datetime64。类似地,你也可以直接使用 DatetimeIndex 构造函数,如下所示:

pd.DatetimeIndex(dates)

这将产生类似的结果。

  1. DatetimeIndex 对象提供了许多有用的属性和方法,用于提取附加的日期和时间属性。例如,你可以提取 day_namemonthyeardays_in_monthquarteris_quarter_startis_leap_yearis_month_startis_month_endis_year_start。以下代码展示了如何做到这一点:
print(f'Name of Day : {parsed_dates.day_name()}')
print(f'Month : {parsed_dates.month}')
print(f'Month Name: {parsed_dates.month_name()}')
print(f'Year : {parsed_dates.year}')
print(f'Days in Month : {parsed_dates.days_in_month}')
print(f'Quarter {parsed_dates.quarter}')
print(f'Is Quarter Start : {parsed_dates.is_quarter_start}')
print(f'Days in Month: {parsed_dates.days_in_month}')
print(f'Is Leap Year : {parsed_dates.is_leap_year}')
print(f'Is Month Start : {parsed_dates.is_month_start}')
print(f'Is Month End : {parsed_dates.is_month_end}')
print(f'Is Year Start : {parsed_dates.is_year_start}')

上述代码产生了以下结果:

Name of Day : Index(['Friday', 'Monday', 'Monday', 'Thursday', 'Saturday', 'Thursday',
       'Sunday', 'Wednesday'],
      dtype='object')
Month : Index([1, 2, 3, 4, 5, 7, 8, 9], dtype='int32')
Month Name: Index(['January', 'February', 'March', 'April', 'May', 'July', 'August',
       'September'],
      dtype='object')
Year : Index([2021, 2021, 2021, 2021, 2021, 2021, 2021, 2021], dtype='int32')
Days in Month : Index([31, 28, 31, 30, 31, 31, 31, 30], dtype='int32')
Quarter Index([1, 1, 1, 2, 2, 3, 3, 3], dtype='int32')
Is Quarter Start : [ True False False  True False  True False False]
Days in Month: Index([31, 28, 31, 30, 31, 31, 31, 30], dtype='int32')
Is Leap Year : [False False False False False False False False]
Is Month Start : [ True  True  True  True  True  True  True  True]
Is Month End : [False False False False False False False False]
Is Year Start : [ True False False False False False False False]

这些属性和方法在转换你的时间序列数据集以进行分析时非常有用。

它是如何工作的……

pandas.to_datetime() 是一个强大的函数,可以智能地解析不同的日期表示形式(如字符串)。正如你在之前的 第 4 步 中看到的那样,字符串示例,如 '2021-01-01''2/1/2021''03-01-2021''April 1, 2021''20210501',都被正确解析。其他日期表示形式,如 'April 1, 2021''1 April 2021',也可以通过 to_datetime() 函数解析,我将留给你探索更多可以想到的其他示例。

to_datetime 函数包含了 errors 参数。在以下示例中,你指定了 errors='coerce',这指示 pandas 将无法解析的任何值设置为 NaT,表示缺失值。在第七章《处理缺失数据》的数据质量检查部分,你将进一步了解 NaT

pd.to_datetime(
                 dates,
                 infer_datetime_format=True,
                 errors='coerce'
                 )

在 pandas 中,有不同的表示方式来表示缺失值——np.NaN 表示缺失的数值 (Not a Number),而 pd.NaT 表示缺失的 datetime 值 (Not a Time)。最后,pandas 的 pd.NA 用于表示缺失的标量值 (Not Available)。

to_datetime 中的 errors 参数可以接受以下三种有效的字符串选项:

  • raise,意味着它将抛出一个异常(error out)。

  • coerce 不会导致抛出异常。相反,它将替换为 pd.NaT,表示缺失的日期时间值。

  • ignore 同样不会导致抛出异常。相反,它将保留原始值。

以下是使用 ignore 值的示例:

pd.to_datetime(['something 2021', 'Jan 1, 2021'],
               errors='ignore')
>> Index(['something 2021', 'Jan 1, 2021'], dtype='object')

errors 参数设置为 'ignore' 时,如果 pandas 遇到无法解析的日期表示,它将不会抛出错误。相反,输入值将按原样传递。例如,从前面的输出中可以看到,to_datetime 函数返回的是 Index 类型,而不是 DatetimeIndex。此外,索引序列中的项是 object 类型(而不是 datetime64)。在 pandas 中,object 类型表示字符串或混合类型。

此外,你还探索了如何使用内置属性和方法提取额外的日期时间属性,例如:

  • day_name(): 返回星期几的名称(例如,星期一,星期二)。

  • month: 提供日期的月份部分,作为整数(1 到 12)。

  • month_name(): 返回月份的完整名称(例如,1 月,2 月)。

  • year: 提取日期的年份部分,作为整数。

  • days_in_month: 给出给定日期所在月份的天数。

  • quarter: 表示该日期所在年的季度(1 到 4)。

  • is_quarter_start: 布尔值,指示该日期是否为季度的第一天(True 或 False)。

  • is_leap_year: 布尔值,指示该日期年份是否为闰年(True 或 False)。

  • is_month_start: 布尔值,指示该日期是否为该月份的第一天(True 或 False)。

  • is_month_end: 布尔值,指示该日期是否为该月份的最后一天(True 或 False)。

  • is_year_start: 布尔值,指示该日期是否为该年份的第一天(True 或 False)。

还有更多内容……

生成 DatetimeIndex 的另一种方式是使用 pandas.date_range() 函数。以下代码提供了一个起始日期和生成的周期数,并指定了每日频率 D

pd.date_range(start=2021-01-01, periods=3, freq=‘D’)
>>
DatetimeIndex([2021-01-01,2021-01-02,2021-01-03], dtype=‘datetime64[ns], freq=‘D’)

pandas.date_range() 至少需要提供四个参数中的三个——startendperiodsfreq。如果没有提供足够的信息,将会抛出 ValueError 异常,并显示以下信息:

ValueError: Of the four parameters: start, end, periods, and freq, exactly three must be specified

让我们探索使用 date_range 函数所需的不同参数组合。在第一个示例中,提供开始日期、结束日期,并指定每日频率。该函数将始终返回一个等间隔的时间点范围:

pd.date_range(start=2021-01-01,
               end=2021-01-03,
               freq=‘D’)
>>
DatetimeIndex([2021-01-01,2021-01-02,2021-01-03], dtype=‘datetime64[ns], freq=‘D’)

在第二个示例中,提供开始日期和结束日期,但不提供频率,而是提供周期数。请记住,该函数将始终返回一个等间隔的时间点范围:

pd.date_range(start=2021-01-01,
               end=2021-01-03,
               periods=2)
>>
DatetimeIndex([2021-01-01,2021-01-03], dtype=‘datetime64[ns], freq=None)
pd.date_range(start=2021-01-01,
               end=2021-01-03,
               periods=4)
>>
DatetimeIndex([2021-01-01 00:00:00,2021-01-01 16:00:00,2021-01-02 08:00:00,2021-01-03 00:00:00],
              dtype=‘datetime64[ns], freq=None)

在以下示例中,提供结束日期和返回的周期数,并指定每日频率:

pd.date_range(end=2021-01-01, periods=3, freq=‘D’)
DatetimeIndex([2020-12-30,2020-12-31,2021-01-01], dtype=‘datetime64[ns], freq=‘D’)

请注意,如果信息足够生成等间隔的时间点并推断缺失的参数,pd.date_range() 函数最少可以接受两个参数。以下是只提供开始和结束日期的示例:

pd.date_range(start=2021-01-01,
               end=2021-01-03)
>>
DatetimeIndex([2021-01-01,2021-01-02,2021-01-03], dtype=‘datetime64[ns], freq=‘D’)

请注意,pandas 能够使用开始和结束日期构造日期序列,并默认采用每日频率。这里有另一个例子:

pd.date_range(start=2021-01-01,
               periods=3)
>>
DatetimeIndex([2021-01-01,2021-01-02,2021-01-03], dtype=‘datetime64[ns], freq=‘D’)

通过 startperiods,pandas 有足够的信息来构造日期序列,并默认采用每日频率。

现在,这里有一个例子,它缺乏足够的信息来生成序列,并且会导致 pandas 抛出错误:

pd.date_range(start=2021-01-01,
               freq=‘D’)
>>
ValueError: Of the four parameters: start, end, periods, and freq, exactly three must be specified

请注意,仅凭开始日期和频率,pandas 并没有足够的信息来构造日期序列。因此,添加 periodsend 日期中的任何一个即可。

让我们总结从生成 DatetimeIndex 到提取 datetime 属性的所有内容。在以下示例中,您将使用 date_range() 函数创建一个包含日期列的 DataFrame。然后,您将使用不同的属性和方法创建附加列:

df = pd.DataFrame(pd.date_range(start=2021-01-01,
               periods=5), columns=[‘Date’])
df[‘days_in_month’] = df[‘Date’].dt.days_in_month
df[‘day_name’] = df[‘Date’].dt.day_name()
df[‘month’] = df[‘Date’].dt.month
df[‘month_name’] = df[‘Date’].dt.month_name()
df[‘year’] = df[‘Date’].dt.year
df[‘days_in_month’] = df[‘Date’].dt.days_in_month
df[‘quarter’] = df[‘Date’].dt.quarter
df[‘is_quarter_start’] = df[‘Date’].dt.is_quarter_start
df[‘days_in_month’] = df[‘Date’].dt.days_in_month
df[‘is_leap_year’] = df[‘Date’].dt.is_leap_year
df[‘is_month_start’] = df[‘Date’].dt.is_month_start
df[‘is_month_end’] = df[‘Date’].dt.is_month_end
df[‘is_year_start’] = df[‘Date’].dt.is_year_start
df

上述代码应生成以下 DataFrame:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file43.png

图 6.1 一个包含 5 行和 12 列的时间序列 DataFrame

Series.dt 访问器

请注意,在前面的代码示例中,当处理 pandas Seriesdatetime 对象时,使用了 .dt 访问器。pandas 中的 .dt 访问器是用于访问 Series 的各种 datetime 属性的一个属性。在之前的例子中,您使用 .dt 访问了 df[‘Date’] Series 的 datetime 属性。

另见:

要了解更多关于 pandas 的 to_datetime() 函数和 DatetimeIndex 类的信息,请查看以下资源:

提供格式参数给 DateTime

在处理从不同数据源提取的数据集时,您可能会遇到以字符串格式存储的日期列,无论是来自文件还是数据库。在之前的例子中,使用 DatetimeIndex,您探索了 pandas.to_datetime() 函数,它可以通过最小的输入解析各种日期格式。然而,您可能希望有更精细的控制,以确保日期被正确解析。例如,接下来您将了解 strptimestrftime 方法,并查看如何在 pandas.to_datetime() 中指定格式化选项,以处理不同的日期格式。

在本例中,您将学习如何将表示日期的字符串解析为 datetimedate 对象(datetime.datetimedatetime.date 类的实例)。

如何操作…

Python 的 datetime 模块包含 strptime() 方法,用于从包含日期的字符串创建 datetimedate 对象。您将首先探索如何在 Python 中做到这一点,然后将其扩展到 pandas 中:

  1. 让我们探索一些示例,使用 datetime.strptime 解析字符串为 datetime 对象。您将解析四种不同表示方式的 2022 年 1 月 1 日,它们会生成相同的输出 – datetime.datetime(2022, 1, 1, 0, 0)
dt.datetime.strptime('1/1/2022', '%m/%d/%Y')
dt.datetime.strptime('1 January, 2022', '%d %B, %Y')
dt.datetime.strptime('1-Jan-2022', '%d-%b-%Y')
dt.datetime.strptime('Saturday, January 1, 2022', '%A, %B %d, %Y')
>>
datetime.datetime(2022, 1, 1, 0, 0)

请注意,输出是一个 datetime 对象,表示年份、月份、日期、小时和分钟。您可以仅指定日期表示方式,如下所示:

dt.datetime.strptime('1/1/2022', '%m/%d/%Y').date()
>>
datetime.date(2022, 1, 1)

现在,您将获得一个 date 对象,而不是 datetime 对象。您可以使用 print() 函数获取 datetime 的可读版本:

dt_1 = dt.datetime.strptime('1/1/2022', '%m/%d/%Y')
print(dt_1)
>>
2022-01-01 00:00:00
  1. 现在,让我们比较一下使用 datetime.strptime 方法与 pandas.to_datetime 方法的差异:
pd.to_datetime('1/1/2022', format='%m/%d/%Y')
pd.to_datetime('1 January, 2022', format='%d %B, %Y')
pd.to_datetime('1-Jan-2022', format='%d-%b-%Y')
pd.to_datetime('Saturday, January 1, 2022', format='%A, %B %d, %Y')
>>
Timestamp('2022-01-01 00:00:00')

同样,您可以使用 print() 函数获取 Timestamp 对象的字符串(可读)表示:

dt_2 = pd.to_datetime('1/1/2022', format='%m/%d/%Y')
print(dt_2)
>>
2022-01-01 00:00:00
  1. 使用 pandas.to_datetime() 相较于 Python 的 datetime 模块有一个优势。to_datetime() 函数可以解析多种日期表示方式,包括带有最少输入或规格的字符串日期格式。以下代码解释了这个概念;请注意省略了 format
pd.to_datetime('Saturday, January 1, 2022')
pd.to_datetime('1-Jan-2022')
>>
Timestamp('2022-01-01 00:00:00')

请注意,与需要整数值或使用 strptime 方法解析字符串的 datetime 不同,pandas.to_datetime() 函数可以智能地解析不同的日期表示方式,而无需指定格式(大多数情况下是如此)。

它是如何工作的…

在本教程中,您使用了 Python 的 datetime.datetimepandas.to_datetime 方法来解析字符串格式的日期。当使用 datetime 时,您需要使用 dt.datetime.strptime() 函数来指定字符串中日期格式的表示方式,并使用格式代码(例如 %d%B%Y)。

例如,在 datetime.strptime('1 January, 2022', '%d %B, %Y') 中,您按确切顺序和间距提供了 %d%B%Y 格式代码,以表示字符串中的格式。让我们详细解析一下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file44.jpg

图 6.2 – 理解格式

  • %d 表示第一个值是一个零填充的数字,表示月份中的日期,后面跟一个空格,用于显示数字与下一个对象之间的间隔。

  • %B 用于表示第二个值代表月份的完整名称。请注意,这后面跟着逗号(,),用于描述字符串中准确的格式,例如 "January,"。因此,在解析字符串时,匹配格式至关重要,必须包含任何逗号、破折号、反斜杠、空格或使用的任何分隔符。

  • 为了遵循字符串格式,逗号(,)后面有一个空格,接着是%Y,表示最后一个值代表四位数的年份。

格式指令

记住,你总是使用百分号(%)后跟格式代码(一个字母,可能带有负号)。这称为格式化指令。例如,小写的y,如%y,表示年份22(没有世纪),而大写的Y,如%Y,表示年份2022(包含世纪)。以下是可以在strptime()函数中使用的常见 Python 指令列表:docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes

回想一下,你使用了pandas.to_datetime()来解析与dt.datetime.strptime()相同的字符串对象。最大不同之处在于,pandas 函数能够准确地解析字符串,而无需显式提供格式参数。这是使用 pandas 进行时间序列分析的诸多优势之一,尤其是在处理复杂日期和datetime场景时。

还有更多内容…

现在你已经知道如何使用pandas.to_datetime()将字符串对象解析为datetime。那么,让我们看看如何将包含日期信息的字符串格式的 DataFrame 列转换为datetime数据类型。

在下面的代码中,你将创建一个小的 DataFrame:

df = pd.DataFrame(
        {'Date': ['January 1, 2022', 'January 2, 2022', 'January 3, 2022'],
         'Sales': [23000, 19020, 21000]}
            )
df
>>
Date  Sales
0     January 1, 2022   23000
1     January 2, 2022   19020
2     January 3, 2022   21000
df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   Date    3 non-null      object
 1   Sales   3 non-null      int64
dtypes: int64(1), object(1)
memory usage: 176.0+ bytes

要更新 DataFrame 以包含 DatetimeIndex,你需要将Date列解析为datetime,然后将其作为索引分配给 DataFrame:

df['Date'] = pd.to_datetime(df['Date'])
df.set_index('Date', inplace=True)
df.info()
>>
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 3 entries, 2022-01-01 to 2022-01-03
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   Sales   3 non-null      int64
dtypes: int64(1)
memory usage: 48.0 bytes

注意现在索引是DatetimeIndex类型,并且 DataFrame 中只有一列(Sales),因为Date现在是索引。

另请参见

要了解更多关于pandas.to_datetime的信息,请访问官方文档页面:pandas.pydata.org/docs/reference/api/pandas.to_datetime.html

使用 Unix 纪元时间戳

纪元时间戳,有时称为Unix 时间POSIX 时间,是一种常见的以整数格式存储datetime的方式。这个整数表示自参考点以来经过的秒数,对于基于 Unix 的时间戳,参考点是1970 年 1 月 1 日午夜(00:00:00 UTC)。这个任意的日期和时间代表了基准,从0开始。所以,我们每经过一秒钟就增加 1 秒。

许多数据库、应用程序和系统将日期和时间存储为数字格式,这样在数学上更容易处理、转换、递增、递减等。请注意,在 Unix 纪元 的情况下,它是基于 UTC(协调世界时),UTC 代表 Universal Time Coordinated。使用 UTC 是构建全球应用程序时的明确选择,它使得存储日期和时间戳以标准化格式变得更加简单。这也使得在处理日期和时间时,无需担心夏令时或全球各地的时区问题。UTC 是航空系统、天气预报系统、国际空间站等使用的标准国际时间。

你在某个时刻会遇到 Unix 纪元时间戳,想要更好地理解数据,你需要将其转换为人类可读格式。这就是本节要介绍的内容。再次提醒,你将体验使用 pandas 内置函数处理 Unix 纪元时间戳的简便性。

如何操作……

在我们开始将 Unix 时间转换为可读的 datetime 对象(这部分比较简单)之前,首先让我们对将日期和时间存储为数字对象(浮点数)这一概念有一些直观的理解:

  1. 你将使用 time 模块(Python 的一部分)来请求当前的秒级时间。这将是从纪元开始以来的秒数,对于 Unix 系统来说,纪元从 1970 年 1 月 1 日 00:00:00 UTC 开始:
import time
epoch_time = time.time()
print(epoch_time)
print(type(epoch_time))
>>
1700596942.589581
<class 'float'>
  1. 现在,复制你得到的数字值并访问 www.epoch101.com。该网站应该会显示你当前的纪元时间。如果你向下滚动,可以粘贴该数字并将其转换为人类可读格式。确保点击 ,如图所示:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file45.png

图 6.3:将 Unix 时间戳转换为 GMT 和本地时间的可读格式

注意,GMT 格式是 Tue, 21 Nov 2023 20:02:22 GMT,而我的本地格式是 Wed Nov 22 2023, 00:02:22 GMT+0400 (海湾标准时间)

  1. 让我们看看 pandas 如何转换纪元时间戳。这里的便利之处在于,你将使用你现在应该已经熟悉的相同的 pandas.to_datetime() 函数,因为你在本章的前两部分中已经用过它。这就是使用 pandas 的众多便利之一。例如,在以下代码中,你将使用 pandas.to_datetime() 来解析 Unix 纪元时间 1700596942.589581
import pandas as pd
t = pd.to_datetime(1700596942.589581, unit='s')
print(t)
>>
2023-11-21 20:02:22.589581056

注意需要将单位指定为秒。输出与 图 6.3 中的 GMT 格式相似。

  1. 如果你希望 datetime 具有时区感知功能——例如,美国/太平洋时区——可以使用 tz_localize('US/Pacific')。不过,为了获得更精确的转换,最好分两步进行:

    1. 使用 tz_localize('UTC') 将时区不敏感的对象转换为 UTC。

    2. 然后,使用 tz_convert() 将其转换为所需的时区。

以下代码展示了如何将时间转换为太平洋时区:

t.tz_localize('UTC').tz_convert('US/Pacific')
>>
Timestamp(2023-11-21 12:02:22.589581056-0800', tz='US/Pacific')
  1. 让我们把这些内容整合在一起。你将把包含datetime列(以 Unix 纪元格式表示)的 DataFrame 转换为人类可读的格式。你将通过创建一个包含 Unix 纪元时间戳的新 DataFrame 开始:
df = pd.DataFrame(
        {'unix_epoch': [1641110340,  1641196740, 1641283140, 1641369540],
                'Sales': [23000, 19020, 21000, 17030]}
                )
df
>>
      unix_epoch  Sales
0     1641110340  23000
1     1641196740  19020
2     1641283140  21000
3     1641369540  17030
  1. 创建一个新列,命名为Date,通过将unix_epoch列解析为datetime(默认为 GMT),然后将输出本地化为 UTC,并转换为本地时区。最后,将Date列设为索引:
df['Date'] = pd.to_datetime(df['unix_epoch'], unit='s')
df['Date'] = df['Date'].dt.tz_localize('UTC').dt.tz_convert('US/Pacific')
df.set_index('Date', inplace=True)
df
>>                            unix_epoch  Sales
Date       
2022-01-01 23:59:00-08:00     1641110340  23000
2022-01-02 23:59:00-08:00     1641196740  19020
2022-01-03 23:59:00-08:00     1641283140  21000
2022-01-04 23:59:00-08:00     1641369540  17030

注意,由于Date列是datetime类型(而不是DatetimeIndex),你必须使用Series.dt访问器来访问内建的方法和属性。最后一步,你将datetime转换为DatetimeIndex对象(即 DataFrame 索引)。如果你还记得本章中的操作 DatetimeIndex示例,DatetimeIndex对象可以访问所有datetime方法和属性,而无需使用dt访问器。

  1. 如果你的数据是按天分布的,并且没有使用时间的需求,那么你可以仅请求日期,如以下代码所示:
df.index.date
>>
array([datetime.date(2022, 1, 1), datetime.date(2022, 1, 2), datetime.date(2022, 1, 3), datetime.date(2022, 1, 4)], dtype=object)

注意,输出只显示日期,不包括时间。

它是如何工作的……

理解 Unix 纪元时间在从事需要精确和标准化时间表示的技术领域时尤为重要。

到目前为止,你使用了pandas.to_datetime()将字符串格式的日期解析为datetime对象,并通过利用格式属性(请参阅提供格式参数给 DateTime的示例)。在本例中,你使用了相同的函数,但这次没有提供格式值,而是传递了一个值给unit参数,如unit='s'

unit参数告诉 pandas 在计算与纪元起始的差异时使用哪种单位。在此示例中,请求的是秒。然而,还有一个重要的参数你通常不需要调整,即origin参数。例如,默认值为origin='unix',表示计算应基于 Unix(或 POSIX)时间,起始时间为1970-01-01 00:00:00 UTC

下面是实际代码的样子:

pd.to_datetime(1635220133.855169, unit='s', origin='unix')
>>
Timestamp('2021-10-26 03:48:53.855169024')

你可以修改origin,使用不同的参考日期来计算日期时间值。在以下示例中,unit 被指定为天,origin 设置为2023 年 1 月 1 日

pd.to_datetime(45, unit='D', origin='2023-1-1')
>>
Timestamp('2023-02-15 00:00:00')

还有更多内容……

如果你希望将datetime值存储为 Unix 纪元时间,可以通过减去1970-01-01,然后用1秒进行整除来实现。Python 使用/作为除法运算符,//作为整除运算符返回整除结果,%作为取余运算符返回除法的余数。

从创建一个新的 pandas DataFrame 开始:

df = pd.DataFrame(
        {'Date': pd.date_range('01-01-2022', periods=5),
        'order' : range(5)}
                 )
df
>>
      Date        order
0     2022-01-01    0
1     2022-01-02    1
2     2022-01-03    2
3     2022-01-04    3
4     2022-01-05    4

你可以按如下方式进行转换:

df['Unix Time'] = (df['Date'] -  pd.Timestamp("1970-01-01")) // pd.Timedelta("1s")
df
>>
      Date        order Unix Time
0     2022-01-01    0       1640995200
1     2022-01-02    1       1641081600
2     2022-01-03    2       1641168000
3     2022-01-04    3       1641254400
4     2022-01-05    4       1641340800

你现在已经生成了 Unix 时间戳。实现相似结果有多种方法。上述示例是 pandas 推荐的方法,详情请参阅此链接:pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#from-timestamps-to-epoch

你将在下一个食谱中学到更多关于Timedelta的知识。

另请参见

要了解更多关于pandas.to_datetime的信息,请访问官方文档页面:pandas.pydata.org/docs/reference/api/pandas.to_datetime.html

使用时间差进行操作

在处理时间序列数据时,你可能需要对datetime列进行一些计算,比如加减操作。例子包括将 30 天加到购买datetime上,以确定产品的退货政策何时到期或保修何时结束。例如,Timedelta类使得通过在不同的时间范围或增量(如秒、天、周等)上加减时间,得出新的datetime对象成为可能。这包括时区感知的计算。

在本节中,你将探索 pandas 中两种捕获日期/时间差异的实用方法——pandas.Timedelta 类和 pandas.to_timedelta 函数。

如何操作……

在本节中,你将使用假设的零售商店销售数据。你将生成一个销售数据框,其中包含商店购买的商品及其购买日期。然后,你将使用Timedelta类和to_timedelta()函数探索不同的场景:

  1. 首先导入pandas库并创建一个包含两列(itempurchase_dt)的数据框,这些列会标准化为 UTC 时间:
df = pd.DataFrame(
        {      
        'item': ['item1', 'item2', 'item3', 'item4', 'item5', 'item6'],
        'purchase_dt': pd.date_range('2021-01-01', periods=6, freq='D', tz='UTC')
        }
)
df

上述代码应输出一个包含六行(items)和两列(itempurchase_dt)的数据框:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file46.jpg

图 6.4:包含购买商品和购买日期时间(UTC)数据的数据框

  1. 添加另一个datetime列,用于表示过期日期,设为购买日期后的 30 天:
df['expiration_dt'] = df['purchase_dt'] + pd.Timedelta(days=30)
df

上述代码应向数据框中添加一个第三列(expiration_dt),该列设置为购买日期后的 30 天:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file47.jpg

图 6.5:更新后的数据框,增加了一个反映过期日期的第三列

  1. 现在,假设你需要创建一个特殊的退货延长期,这个时间设置为从购买日期起 35 天、12 小时和 30 分钟:
df['extended_dt'] = df['purchase_dt'] +\
                pd.Timedelta('35 days 12 hours 30 minutes')
df

上述代码应向数据框中添加一个第四列(extended_dt),根据额外的 35 天、12 小时和 30 分钟,反映新的日期时间:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file48.jpg

图 6.6:更新后的 DataFrame,新增第四列日期时间列,反映扩展后的日期

  1. 假设你被要求将时区从 UTC 转换为零售商店总部所在时区,也就是洛杉矶时区:
df.iloc[:,1:] = df.iloc[: ,1:].apply(
            lambda x: x.dt.tz_convert('US/Pacific')
                )
df

在将时区从 UTC 转换为美国/太平洋时区(洛杉矶)后,你将会覆盖datetime列(purchased_dtexpiration_dtextended_dt)。DataFrame 的结构应该保持不变——六行四列——但数据现在看起来有所不同,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file49.jpg

图 6.7:更新后的 DataFrame,所有日期时间列都显示洛杉矶(美国/太平洋时区)时间

  1. 最后,你可以计算扩展后的到期日期与原始到期日期之间的差异。由于它们都是datetime数据类型,你可以通过简单的两个列之间的减法来实现这一点:
df['exp_ext_diff'] = (
         df['extended_dt'] - df['expiration_dt']
        )
df

最终的 DataFrame 应该会有一个第五列,表示扩展日期与到期日期之间的差异:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file50.jpg

图 6.8:更新后的 DataFrame,新增第五列

由于 pandas 内置了处理时间序列数据和datetime的功能,这些类型的转换和计算变得更加简化,无需任何额外的库。

它是如何工作的…

时间差可以帮助捕捉两个日期或时间对象之间的差异。在 pandas 中,pandas.Timedelta类等同于 Python 的datetime.timedelta类,行为非常相似。然而,pandas 的优势在于它包含了大量用于处理时间序列数据的类和函数。这些内置的 pandas 函数在处理 DataFrame 时通常更简洁、高效。让我们做个简短实验,展示 pandas 的Timedelta类是 Python timedelta类的子类:

import datetime as dt
import pandas as pd
pd.Timedelta(days=1) == dt.timedelta(days=1)
>> True

让我们验证一下pandas.Timedelta是否是datetime.timedelta的一个实例:

issubclass(pd.Timedelta, dt.timedelta)
>> True
dt_1 = pd.Timedelta(days=1)
dt_2 = dt.timedelta(days=1)
isinstance(dt_1, dt.timedelta)
>> True
isinstance(dt_1, pd.Timedelta)
>> True

Python 的datetime.timedelta类接受这些参数的整数值——dayssecondsmicrosecondsmillisecondsminuteshoursweeks。另一方面,pandas.Timedelta接受整数和字符串,如下代码所示:

pd.Timedelta(days=1, hours=12, minutes=55)
>> Timedelta('1 days 12:55:00')
pd.Timedelta('1 day 12 hours 55 minutes')
>> Timedelta('1 days 12:55:00')
pd.Timedelta('1D 12H 55T')
>> Timedelta('1 days 12:55:00')

一旦你定义了Timedelta对象,就可以将其用于对datetimedatetime对象进行计算:

week_td = pd.Timedelta('1W')
pd.to_datetime('1 JAN 2022') + week_td
>> Timestamp('2022-01-08 00:00:00')

在前面的示例中,week_td表示一个 1 周的Timedelta对象,可以将其加到(或减去)datetime中,得到时间差。通过添加week_td,你增加了 1 周。如果你想增加 2 周呢?你也可以使用乘法:

pd.to_datetime('1 JAN 2022') + 2*week_td
>> Timestamp('2022-01-15 00:00:00')

还有更多…

使用pd.Timedelta非常简单,并且使得处理大规模时间序列 DataFrame 变得高效,无需导入额外的库,因为它已内置于 pandas 中。

在之前的*如何实现…*部分,你创建了一个 DataFrame,并基于timedelta计算添加了额外的列。你也可以将timedelta对象添加到 DataFrame 中,并通过它的列进行引用。最后,让我们看看它是如何工作的。

首先,让我们构建与之前相同的 DataFrame:

import pandas as pd
df = pd.DataFrame(
        {      
        'item': ['item1', 'item2', 'item3', 'item4', 'item5', 'item6'],
        'purchase_dt': pd.date_range('2021-01-01', periods=6, freq='D', tz='UTC')
        }
)

这应该会生成一个如图 6.4所示的 DataFrame。现在,你将添加一个包含Timedelta对象(1 周)的新列,然后使用该列对purchased_dt列进行加减操作:

df['1 week'] = pd.Timedelta('1W')
df['1_week_more'] = df['purchase_dt'] + df['1 week']
df['1_week_less'] = df['purchase_dt'] - df['1 week']
df

上述代码应该会生成一个带有三个额外列的 DataFrame。1 week列包含Timedelta对象,因此,你可以引用该列来计算所需的任何时间差:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file51.jpg

图 6.9:更新后的 DataFrame,新增三个列

让我们检查 DataFrame 中每一列的数据类型:

df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 5 columns):
 #   Column       Non-Null Count  Dtype             
---  ------       --------------  -----             
 0   item         6 non-null      object            
 1   purchase_dt  6 non-null      datetime64[ns, UTC]
 2   1 week       6 non-null      timedelta64[ns]   
 3   1_week_more  6 non-null      datetime64[ns, UTC]
 4   1_week_less  6 non-null      datetime64[ns, UTC]
dtypes: datetime64ns, UTC, object(1), timedelta64ns
memory usage: 368.0+ bytes

请注意,1 week列是一个特定的数据类型,timedelta64(我们的Timedelta对象),它允许你对 DataFrame 中的datetimedatetime列进行算术运算。

与 DatetimeIndex 一起工作的食谱中,你探讨了pandas.date_range()函数来生成一个带有DatetimeIndex的 DataFrame。该函数基于开始时间、结束时间、周期和频率参数,返回一系列等间隔的时间点。

同样,你也可以使用pandas.timedelta_range()函数生成具有固定频率的TimedeltaIndex,它与pandas.date_range()函数的参数相似。下面是一个快速示例:

df = pd.DataFrame(
        {      
        'item': ['item1', 'item2', 'item3', 'item4', 'item5'],
        'purchase_dt': pd.date_range('2021-01-01', periods=5, freq='D', tz='UTC'),
        'time_deltas': pd.timedelta_range('1W 2 days 6 hours', periods=5)
        }
)
df

输出结果如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file52.jpg

图 6.10:包含 Timedelta 列的 DataFrame

另见

转换带有时区信息的 DateTime

在处理需要关注不同时区的时间序列数据时,事情可能会变得不可控且复杂。例如,在开发数据管道、构建数据仓库或在系统之间集成数据时,处理时区需要在项目的各个利益相关者之间达成一致并引起足够的重视。例如,在 Python 中,有几个专门用于处理时区转换的库和模块,包括 pytzdateutilzoneinfo 等。

让我们来讨论一个关于时区和时间序列数据的启发性示例。对于跨越多个大陆的大型公司来说,包含来自全球不同地方的数据是很常见的。如果忽略时区,将很难做出基于数据的商业决策。例如,假设你想确定大多数客户是早上还是晚上访问你的电子商务网站,或者是否顾客在白天浏览,晚上下班后再进行购买。为了进行这个分析,你需要意识到时区差异以及它们在国际范围内的解读。

如何操作…

在这个示例中,你将处理一个假设场景——一个小型数据集,代表从全球各地不同时间间隔访问网站的数据。数据将被标准化为 UTC,并且你将处理时区转换。

  1. 你将首先导入 pandas 库并创建时间序列 DataFrame:
df = pd.DataFrame(
        {      
        'Location': ['Los Angeles',
                     'New York',
                     'Berlin',
                     'New Delhi',
                     'Moscow',
                     'Tokyo',
                     'Dubai'],
        'tz': ['US/Pacific',
               'US/Eastern',
               'Europe/Berlin',
               'Asia/Kolkata',
               'Europe/Moscow',
               'Asia/Tokyo',
               'Asia/Dubai'],
        'visit_dt': pd.date_range(start='22:00',periods=7, freq='45min'),
        }).set_index('visit_dt')
df

这将生成一个 DataFrame,其中 visit_dtDatetimeIndex 类型的索引,两个列 Locationtz 表示时区:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file53.png

图 6.11:以 UTC 时间为索引的 DataFrame

  1. 假设你需要将这个 DataFrame 转换为与公司总部东京所在的时区相同。你可以通过对 DataFrame 使用 DataFrame.tz_convert() 来轻松完成,但如果这样做,你将遇到 TypeError 异常。这是因为你的时间序列 DataFrame 并不具有时区感知能力。所以,你需要先使用 tz_localize() 将其本地化,才能使其具备时区感知能力。在这种情况下,你将其本地化为 UTC:
df = df.tz_localize('UTC')
  1. 现在,你将把 DataFrame 转换为总部所在的时区(东京):
df_hq = df.tz_convert('Asia/Tokyo')
df_hq

DataFrame 索引 visit_dt 将转换为新的时区:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file54.png

图 6.12:DataFrame 索引转换为总部所在时区(东京)

请注意,你能够访问 tz_localize()tz_convert() 方法,因为 DataFrame 的索引类型是 DatetimeIndex。如果不是这种情况,你将会遇到 TypeError 异常,错误信息如下:

TypeError: index is not a valid DatetimeIndex or PeriodIndex
  1. 现在,你将把每一行本地化到适当的时区。你将添加一个新列,反映基于访问网站用户位置的时区。你将利用tz列来完成这个操作:
df['local_dt'] = df.index
df['local_dt'] = df.apply(lambda x: pd.Timestamp.tz_convert(x['local_dt'], x['tz']), axis=1)
df

这应该会生成一个新的列local_dt,该列基于visit_dt中的 UTC 时间,并根据tz列中提供的时区进行转换:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file55.png

图 6.13:更新后的 DataFrame,local_dt基于每次访问的本地时区

你可能会问,如果没有tz列怎么办?在哪里可以找到正确的tz字符串?这些被称为时区TZ)数据库名称。它们是标准名称,你可以在 Python 文档中找到这些名称的一个子集,或者你可以访问这个链接查看更多信息:en.wikipedia.org/wiki/List_of_tz_database_time_zones

它是如何工作的…

将时间序列的 DataFrame 从一个时区转换到另一个时区,可以使用DataFrame.tz_convert()方法,并提供像US/Pacific这样的时区字符串作为参数。在使用DataFrame.tz_convert()时,有几个假设需要记住:

  • DataFrame 应该具有DatetimeIndex类型的索引。

  • DatetimeIndex需要具备时区感知功能。

你使用了DataFrame.tz_localize()函数来使索引具备时区感知功能。如果你在处理不同的时区和夏令时,建议标准化为UTC,因为 UTC 始终一致且不变(无论你身在何处,或者是否应用了夏令时)。一旦使用 UTC,转换到其他时区非常简单。

我们首先在前面的步骤中将数据本地化为 UTC 时间,然后分两步将其转换为不同的时区。你也可以通过链式调用这两个方法一步完成,如以下代码所示:

df.tz_localize('UTC').tz_convert('Asia/Tokyo')

如果你的索引已经具备时区感知功能,那么使用tz_localize()将会引发TypeError异常,并显示以下信息:

TypeError: Already tz-aware, use tz_convert to convert

这表示你不需要再次本地化它。相反,只需将其转换为另一个时区即可。

还有更多…

看着图 6.12中的 DataFrame,很难立刻判断时间是上午(AM)还是下午(PM)。你可以使用strftime格式化datetime(我们在提供格式参数给 DateTime的例子中讨论过)。

你将构建相同的 DataFrame,将其本地化为 UTC 时间,然后转换为总部所在的时区,并应用新的格式:

df = pd.DataFrame(
        {      
        'Location': ['Los Angeles',
                     'New York',
                     'Berlin',
                     'New Delhi',
                     'Moscow',
                     'Tokyo',
                     'Dubai'],
        'tz': ['US/Pacific',
               'US/Eastern',
               'Europe/Berlin',
               'Asia/Kolkata',
               'Europe/Moscow',
               'Asia/Tokyo',
               'Asia/Dubai'],
        'visit_dt': pd.date_range(start='22:00',periods=7, freq='45min'),
        }).set_index('visit_dt').tz_localize('UTC').tz_convert('Asia/Tokyo')

我们已经将这些步骤结合起来,生成的 DataFrame 应该类似于图 6.12中的数据。

现在,你可以更新格式,使用YYYY-MM-DD HH:MM AM/PM的模式:

df.index = df.index.strftime('%Y-%m-%d %H:%M %p')
df

索引将从格式/布局的角度进行更新。然而,它仍然是基于东京时区的时区感知,并且索引仍然是DatetimeIndex。唯一的变化是datetime布局:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file56.png

图 6.14 – 更新后的 DataFrame 索引,基于提供的日期格式字符串进行格式化

我相信你会同意,这样做能更轻松地向用户展示,快速确定访问是上午还是下午。

另见

要了解更多关于 tz_convert 的信息,你可以阅读官方文档 pandas.pydata.org/docs/reference/api/pandas.Series.dt.tz_convert.html 和 https://pandas.pydata.org/docs/reference/api/pandas.Timestamp.tz_convert.html

处理日期偏移

在处理时间序列时,了解你所处理的数据以及它如何与正在解决的问题相关联至关重要。例如,在处理制造或销售数据时,你不能假设一个组织的工作日是周一到周五,或者它是否使用标准的日历年或财年。你还应该考虑了解任何假期安排、年度停产以及与业务运营相关的其他事项。

这时,偏移量就派上用场了。它们可以帮助将日期转换为对业务更有意义和更具相关性的内容。它们也可以帮助修正可能不合逻辑的数据条目。

我们将在这个示例中通过一个假设的例子,展示如何利用 pandas 偏移量。

如何做…

在本示例中,你将生成一个时间序列 DataFrame 来表示一些生产数量的每日日志。该公司是一家总部位于美国的公司,希望通过分析数据来更好地理解未来预测的生产能力:

  1. 首先导入 pandas 库,然后生成我们的 DataFrame:
np.random.seed(10)
df = pd.DataFrame(
        {      
        'purchase_dt': pd.date_range('2021-01-01', periods=6, freq='D'),
        'production' : np.random.randint(4, 20, 6)
        }).set_index('purchase_dt')
df
>>
             production
purchase_dt           
2021-01-01           13
2021-01-02           17
2021-01-03            8
2021-01-04           19
2021-01-05            4
2021-01-06            5
  1. 让我们添加星期几的名称:
df['day'] = df.index.day_name()
df
>>
             production        day
purchase_dt                      
2021-01-01           13     Friday
2021-01-02           17   Saturday
2021-01-03            8     Sunday
2021-01-04           19     Monday
2021-01-05            4    Tuesday
2021-01-06            5  Wednesday

在处理任何数据时,始终理解其背后的业务背景。没有领域知识或业务背景,很难判断一个数据点是否可以接受。在这个情境中,公司被描述为一家总部位于美国的公司,因此,工作日是从周一到周五。如果有关于周六或周日(周末)的数据,未经与业务确认,不应该做任何假设。你应该确认是否在特定的周末日期有生产例外情况。此外,意识到 1 月 1 日是节假日。经过调查,确认由于紧急例外,生产确实发生了。业务高层不希望在预测中考虑周末或节假日的工作。换句话说,这是一个一次性、未发生的事件,他们不希望以此为基础建立模型或假设。

  1. 公司要求将周末/节假日的生产数据推迟到下一个工作日。在这里,您将使用 pandas.offsets.BDay(),它表示工作日:
df['BusinessDay'] = df.index + pd.offsets.BDay(0)
df['BDay Name'] = df['BusinessDay'].dt.day_name()
df
>>
             production        day BusinessDay  BDay Name
purchase_dt                                          
2021-01-01           13     Friday  2021-01-01     Friday
2021-01-02           17   Saturday  2021-01-04     Monday
2021-01-03            8     Sunday  2021-01-04     Monday
2021-01-04           19     Monday  2021-01-04     Monday
2021-01-05            4    Tuesday  2021-01-05    Tuesday
2021-01-06            5  Wednesday  2021-01-06  Wednesday

由于周六和周日是周末,它们的生产数据被推迟到下一个工作日,即周一,1 月 4 日。

  1. 让我们执行一个汇总聚合,按工作日添加生产数据,以更好地理解此更改的影响:
df.groupby(['BusinessDay', 'BDay Name']).sum()
>>
                       production
BusinessDay BDay Name           
2021-01-01  Friday             137
2021-01-04  Monday             44
2021-01-05  Tuesday             4
2021-01-06  Wednesday           5

现在,周一被显示为该周最具生产力的一天,因为它是节假日后的第一个工作日,并且是一个长周末之后的工作日。

  1. 最后,企业提出了另一个请求——他们希望按月(MonthEnd)和按季度(QuarterEnd)跟踪生产数据。您可以再次使用 pandas.offsets 来添加两列新数据:
df['QuarterEnd'] = df.index + pd.offsets.QuarterEnd(0)
df['MonthEnd'] = df.index + pd.offsets.MonthEnd(0)
df['BusinessDay'] = df.index + pd.offsets.BDay(0)
>>
             production QuarterEnd   MonthEnd BusinessDay
purchase_dt                                         
2021-01-01           13 2021-03-31 2021-01-31  2021-01-01
2021-01-02           17 2021-03-31 2021-01-31  2021-01-04
2021-01-03            8 2021-03-31 2021-01-31  2021-01-04
2021-01-04           19 2021-03-31 2021-01-31  2021-01-04
2021-01-05            4 2021-03-31 2021-01-31  2021-01-05
2021-01-06            5 2021-03-31 2021-01-31  2021-01-06

现在,您已经有了一个 DataFrame,它应该能满足大多数企业的报告需求。

它是如何工作的…

使用日期偏移量使得根据特定规则增减日期或将日期转换为新的日期范围成为可能。pandas 提供了多种偏移量,每种偏移量都有其规则,可以应用到您的数据集上。以下是 pandas 中常见的偏移量列表:

  • BusinessDayBday

  • MonthEnd

  • BusinessMonthEndBmonthEnd

  • CustomBusinessDayCday

  • QuarterEnd

  • FY253Quarter

要查看更全面的列表及其描述,请访问文档:pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#dateoffset-objects

在 pandas 中应用偏移量与进行加法或减法一样简单,如以下示例所示:

df.index + pd.offsets.BDay()
df.index - pd.offsets.BDay()

还有更多…

根据我们的示例,您可能已经注意到,在使用 BusinessDayBDay)偏移量时,它并未考虑到新年假期(1 月 1 日)。那么,如何既考虑到新年假期,又考虑到周末呢?

为了实现这一点,pandas 提供了两种处理标准节假日的方法。第一种方法是定义自定义节假日。第二种方法(在适用时)使用现有的节假日偏移量。

让我们从现有的偏移量开始。以新年为例,您可以使用 USFederalHolidayCalendar 类,它包含了标准节假日,如新年、圣诞节以及其他美国特定的节假日。接下来,我们来看看它是如何工作的。

首先,生成一个新的 DataFrame,并导入所需的库和类:

import pandas as pd
from pandas.tseries.holiday import (
    USFederalHolidayCalendar
)
df = pd.DataFrame(
        {      
        'purchase_dt': pd.date_range('2021-01-01', periods=6, freq='D'),
        'production' : np.random.randint(4, 20, 6)
        }).set_index('purchase_dt')

USFederalHolidayCalendar 有一些假期规则,您可以使用以下代码检查:

USFederalHolidayCalendar.rules
>>
[Holiday: New Years Day (month=1, day=1, observance=<function nearest_workday at 0x7fedf3ec1a60>),
 Holiday: Martin Luther King Jr. Day (month=1, day=1, offset=<DateOffset: weekday=MO(+3)>),
 Holiday: Presidents Day (month=2, day=1, offset=<DateOffset: weekday=MO(+3)>),
 Holiday: Memorial Day (month=5, day=31, offset=<DateOffset: weekday=MO(-1)>),
 Holiday: July 4th (month=7, day=4, observance=<function nearest_workday at 0x7fedf3ec1a60>),
 Holiday: Labor Day (month=9, day=1, offset=<DateOffset: weekday=MO(+1)>),
 Holiday: Columbus Day (month=10, day=1, offset=<DateOffset: weekday=MO(+2)>),
 Holiday: Veterans Day (month=11, day=11, observance=<function nearest_workday at 0x7fedf3ec1a60>),
 Holiday: Thanksgiving (month=11, day=1, offset=<DateOffset: weekday=TH(+4)>),
 Holiday: Christmas (month=12, day=25, observance=<function nearest_workday at 0x7fedf3ec1a60>)]

要应用这些规则,您将使用 CustomerBusinessDayCDay 偏移量:

df['USFederalHolidays'] = df.index + pd.offsets.CDay(calendar=USFederalHolidayCalendar())
df

输出结果如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file57.jpg

图 6.15 – 添加到 DataFrame 中的 USFederalHolidays 列,识别新年假期

自定义假期选项将以相同的方式工作。你需要导入Holiday类和nearest_workday函数。你将使用Holiday类来定义你的具体假期。在本例中,你将确定新年规则:

from pandas.tseries.holiday import (
    Holiday,
    nearest_workday,
    USFederalHolidayCalendar
)
newyears = Holiday("New Years",
                   month=1,
                   day=1,
                   observance=nearest_workday)
newyears
>>
Holiday: New Years (month=1, day=1, observance=<function nearest_workday at 0x7fedf3ec1a60>)

类似于你如何将USFederalHolidayCalendar类应用于CDay偏移量,你将把你的新newyears对象应用于Cday

df['NewYearsHoliday'] = df.index + pd.offsets.CDay(calendar=newyears)
df

你将获得以下输出:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file58.jpg

图 6.16 – 使用自定义假期偏移量添加的 NewYearsHoliday 列

如果你对nearest_workday函数以及它如何在USFederalHolidayCalendar规则和你的自定义假期中被使用感到好奇,那么以下代码展示了它的工作原理:

nearest_workday(pd.to_datetime('2021-1-3'))
>>
Timestamp('2021-01-04 00:00:00')
nearest_workday(pd.to_datetime('2021-1-2'))
>>
Timestamp('2021-01-01 00:00:00')

如所示,函数主要判断一天是否为工作日,然后根据判断结果,它将使用前一天(如果是星期六)或后一天(如果是星期日)。nearest_workday还有其他可用规则,包括以下几种:

  • Sunday_to_Monday

  • Next_Monday_or_Tuesday

  • Previous_Friday

  • Next_monday

参见

欲了解更多有关pandas.tseries.holiday的详细信息,你可以查看实际的代码,它展示了所有的类和函数,可以作为一个很好的参考,访问github.com/pandas-dev/pandas/blob/master/pandas/tseries/holiday.py

使用自定义工作日

不同地区和领土的公司有不同的工作日。例如,在处理时间序列数据时,根据你需要进行的分析,了解某些交易是否发生在工作日或周末可能会产生差异。例如,假设你正在进行异常检测,并且你知道某些类型的活动只能在工作时间内进行。在这种情况下,任何超出这些时间范围的活动都可能触发进一步的分析。

在本示例中,你将看到如何定制一个偏移量以适应你的需求,特别是在进行依赖于已定义的工作日和非工作日的分析时。

如何操作…

在本示例中,你将为总部位于约旦安曼的公司创建自定义的工作日和假期。在约旦,工作周是从星期日到星期四,而星期五和星期六是为期两天的周末。此外,他们的国庆节(一个假期)是在每年的 5 月 25 日:

  1. 你将从导入 pandas 并定义约旦的工作日开始:
import pandas as pd
from pandas.tseries.holiday import AbstractHolidayCalendar, Holiday
from pandas.tseries.offsets import CustomBusinessDay
jordan_workdays = "Sun Mon Tue Wed Thu"
  1. 你将定义一个自定义类假期JordanHolidayCalendar和一个新的rule用于约旦的独立日:
class JordanHolidayCalendar(AbstractHolidayCalendar):
    rules = [
        Holiday('Jordan Independence Day', month=5, day=25)
    ]
  1. 你将定义一个新的CustomBusinessDay实例,并包含自定义假期和工作日:
jordan_bday = CustomBusinessDay(
    holidays=JordanHolidayCalendar().holidays(),
    weekmask=jordan_workdays)
  1. 你可以验证规则是否已正确注册:
jordan_bday.holidays[53]
>>
numpy.datetime64('2023-05-25')
jordan_bday.weekmask
>>
'Sun Mon Tue Wed Thu'
  1. 现在,你可以使用pd.date_range生成从 2023 年 5 月 20 日开始的 10 个工作日。生成的日期将排除周末(周五和周六)以及独立日假期(5 月 25 日):
df = pd.DataFrame({'Date': pd.date_range('12-1-2021', periods=10, freq=dubai_uae_bday )})
  1. 为了更容易判断功能是否按预期工作,添加一个新列来表示星期名称:
df['Day_name'] = df.Date.dt.day_name()
df

生成的时间序列应该具有新的自定义工作日,并包含约旦的节假日规则。

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file59.png

图 6.17:基于阿联酋自定义工作日和节假日生成的时间序列

图 6.17中,你可以看到自定义的工作日从周日到周一,除了 5 月 25 日(星期四),因为这是约旦的国庆日,所有该日期被跳过。

这个示例可以进一步扩展,涵盖不同国家和节假日,以适应你正在进行的分析类型。

它是如何工作的……

这个配方基于“处理日期偏移量”配方,但重点是自定义偏移量。pandas 提供了几种偏移量,可以使用自定义日历、节假日weekmask。这些包括以下内容:

  • CustomBusinessDayCday

  • CustomBusinessMonthEndCBMonthEnd

  • CustomBusinessMonthBeginCBMonthBegin

  • CustomBusinessHour

它们的行为就像其他偏移量;唯一的区别是,它们允许你创建自己的规则。

在这个配方中,你导入了CustomBusinessDay类,创建了它的一个实例来为工作日创建自定义频率,考虑了周末和节假日。以下是你使用的代码:

jordan_bday = CustomBusinessDay(
    holidays=JordanHolidayCalendar().holidays(),
    weekmask=jordan_workdays
)

这也等同于以下代码:

jordan_bday = pd.offsets.CustomBusinessDay(
    holidays=JordanHolidayCalendar().holidays(),
    weekmask=jordan_workdays,
)

请注意,在定义工作日时,使用的是一串缩写的星期名称。这被称为weekmask,在自定义星期时,pandas 和 NumPy 都使用它。在 pandas 中,你还可以通过扩展AbstractHolidayCalendar类并包含特定的日期或节假日规则来定义自定义假期日历,正如前面关于约旦独立日的代码所展示的那样。

还有更多内容……

让我们扩展前面的示例,并向 DataFrame 中添加自定义工作时间。这将是另一个自定义偏移量,你可以像使用CustomBusinessDay一样使用它:

jordan_bhour = pd.offsets.CustomBusinessHour(
    start="8:30",
    end="15:30",
    holidays=JordanHolidayCalendar().holidays(),
    weekmask=jordan_workdays)

在这里,你应用了相同的规则,自定义的holidaysweekmask用于自定义工作日,确保自定义的工作时间也遵守定义的工作周和节假日。你通过提供startend时间(24 小时制)来定义自定义工作时间。

以下是你刚刚创建的自定义工作时间的使用示例:

start_datetime = '2023-05-24 08:00'
end_datetime = '2023-05-24 16:00'
business_hours = pd.date_range(start=start_datetime, end=end_datetime, freq=jordan_bhour)
print(business_hours)
>>
DatetimeIndex(['2023-05-24 08:30:00', '2023-05-24 09:30:00',
               '2023-05-24 10:30:00', '2023-05-24 11:30:00',
               '2023-05-24 12:30:00', '2023-05-24 13:30:00',
               '2023-05-24 14:30:00'],
              dtype='datetime64[ns]', freq='CBH')

请注意,像CustomBusinessDayCustomBusinessHour这样的自定义偏移量可以应用于无时区感知和有时区感知的日期时间对象。然而,是否需要将 DataFrame 设置为时区感知取决于具体的使用案例。在需要时,你可以使用tz.localize()tz.convert()将 DataFrame 设置为时区感知(例如,先本地化再转换到你的时区),然后再应用自定义偏移量以获得更好的结果。

另请参见

第七章:7 处理缺失数据

加入我们的书籍社区,访问 Discord

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file0.png

packt.link/zmkOY

作为数据科学家、数据分析师或业务分析师,您可能已经发现,指望获得完美的干净数据集是过于乐观的。然而,更常见的情况是,您正在处理的数据存在缺失值、错误数据、重复记录、数据不足或数据中存在异常值等问题。

时间序列数据也不例外,在将数据输入任何分析或建模流程之前,您必须先对数据进行调查。理解时间序列数据背后的业务背景对于成功检测和识别这些问题至关重要。例如,如果您处理的是股票数据,其背景与 COVID 数据或传感器数据有很大不同。

拥有这种直觉或领域知识将使您能够预见分析数据时的期望结果以及哪些结果是可以接受的。始终尝试理解数据背后的业务背景。例如,数据最初为什么要被收集?数据是如何收集的?数据是否已经应用了某些业务规则、逻辑或变换?这些修改是在数据采集过程中应用的,还是内置在生成数据的系统中?

在发现阶段,这些前期知识将帮助您确定最佳的方法来清理和准备数据集,以进行分析或建模。缺失数据和异常值是数据清理和准备过程中需要处理的两个常见问题。您将在第八章《使用统计方法检测异常值》和第十四章《使用无监督机器学习检测异常值》中深入探讨异常值检测。本章将探索通过插补插值技术处理缺失数据的方法。

以下是本章将涵盖的配方列表:

  • 执行数据质量检查

  • 使用 pandas 进行单变量插补处理缺失数据

  • 使用 scikit-learn 进行单变量插补处理缺失数据

  • 使用多变量插补处理缺失数据

  • 使用插值处理缺失数据

技术要求

您可以从 GitHub 仓库下载 Jupyter 笔记本和所需的数据集,以便跟随教程:

在本章及后续章节中,您将广泛使用 pandas 2.1.3(2023 年 11 月 10 日发布)。另外,您还将使用四个额外的库:

  • numpy (1.26.0)

  • matplotlob (3.8.1)

  • statsmodels (0.14.0)

  • scikit-learn (1.3.2)

  • SciPy (1.11.3)

如果您使用pip,则可以通过终端使用以下命令安装这些包:

pip install matplotlib numpy statsmodels scikit-learn scipy

如果您使用conda,则可以通过以下命令安装这些包:

conda install matplotlib numpy statsmodels scikit-learn scipy

在本章中,将广泛使用两个数据集进行插补和插值操作:CO2 排放数据集和电子商店点击流数据集。点击流数据集的来源为在线购物的点击流数据,来自UCI 机器学习库,您可以在这里找到:

archive.ics.uci.edu/ml/datasets/clickstream+data+for +online+shopping

CO2 排放数据集的来源为Our World in Data的年度CO2 排放报告,您可以在此处找到:ourworldindata.org/co2-emissions

为了演示,两个数据集已经被修改,去除了部分观测值(缺失数据)。提供了原始版本和修改版本,用于评估本章讨论的不同技术。

在本章中,您将遵循类似的步骤来处理缺失数据:将数据导入 DataFrame,识别缺失数据,对缺失数据进行插补,评估与原始数据的对比,最后可视化并比较不同的插补技术。

这些步骤可以转化为函数以便重用。您可以为这些步骤创建函数:一个用于将数据读入 DataFrame 的函数,一个用于使用 RMSE 评分评估的函数,以及一个用于绘制结果的函数。

首先加载将在本章中使用的标准库:

import pandas as pd
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np

函数 1 – read_datasets

read_datasets函数接受文件夹路径、CSV 文件名和包含日期变量的列名。

read_datasets函数定义如下:

def read_dataset(folder, file, date_col=None, format=None, index=False):
    '''
    folder: is a Path object
    file: the CSV filename in that Path object.
    date_col: specify a column which has datetime
    index_col: True if date_col should be the index

    returns: a pandas DataFrame with a DatetimeIndex
    '''
    index_col = date_col if index is True else None

    df = pd.read_csv(folder / file,
                     index_col=index_col,
                     parse_dates=[date_col],
                     date_format=format)
    return df

函数 2 – plot_dfs

plot_dfs()函数接受两个 DataFrame:没有缺失数据的原始 DataFrame(df1,作为基准)和经过插补的 DataFrame(df2,用于对比)。该函数使用指定的响应列(col)创建多个时间序列子图。注意,插补后的 DataFrame 将包含额外的列(每个插补技术的输出列),绘图函数会考虑到这一点。通过遍历列来完成这一操作。该函数将为每个插补技术绘制图形以便视觉比较,并将在本章中多次使用。

这个plot_dfs函数定义如下:

def plot_dfs(df1, df2, col, title=None, xlabel=None, ylabel=None):
    '''   
    df1: original dataframe without missing data
    df2: dataframe with missing data
    col: column name that contains missing data
    '''   
    df_missing = df2.rename(columns={col: 'missing'})

    columns = df_missing.loc[:, 'missing':].columns.tolist()
    subplots_size = len(columns)   
    fig, ax = plt.subplots(subplots_size+1, 1, sharex=True)
    plt.subplots_adjust(hspace=0.25)
    fig.suptitle = title

    df1[col].plot(ax=ax[0], figsize=(10, 12))
    ax[0].set_title('Original Dataset')
    ax[0].set_xlabel(xlabel)
    ax[0].set_ylabel(ylabel)   

    for i, colname in enumerate(columns):
        df_missing[colname].plot(ax=ax[i+1])
        ax[i+1].set_title(colname.upper())
    plt.show()

函数 3 – rmse_score

除了使用plot_dfs函数进行插补技术的视觉比较外,您还需要一种方法来数字化比较不同的插补技术(使用统计度量)。

这时,rmse_score 函数将派上用场。它接受两个 DataFrame:原始 DataFrame(df1)作为基准,以及要进行比较的填补后的 DataFrame(df2)。该函数允许你指定哪个列包含响应列(col),用于计算的基础。

rmse_score 函数定义如下:

def rmse_score(df1, df2, col=None):
    '''
    df1: original dataframe without missing data
    df2: dataframe with missing data
    col: column name that contains missing data
    returns: a list of scores
    '''
    df_missing = df2.rename(columns={col: 'missing'})
    columns = df_missing.loc[:, 'missing':].columns.tolist()
    scores = []
    for comp_col in columns[1:]:
        rmse = np.sqrt(np.mean((df1[col] - df_missing[comp_col])**2))
        scores.append(rmse)
        print(f'RMSE for {comp_col}: {rmse}')
    return scores

理解缺失数据

数据缺失可能有多种原因,例如意外的停电、设备意外断电、传感器损坏、调查参与者拒绝回答某个问题,或者出于隐私和合规性原因故意删除数据。换句话说,缺失数据是不可避免的。

通常,缺失数据是非常常见的,但有时在制定处理策略时并未给予足够重视。处理缺失数据行的一种方法是删除这些观察值(删除行)。然而,如果你的数据本就有限,这种方法可能不是一个好的策略。例如,若数据的收集过程复杂且昂贵,则删除记录的缺点在于,如果过早删除,你将无法知道缺失数据是由于审查(观察仅部分收集)还是由于偏差(例如,高收入参与者拒绝在调查中共享家庭总收入)造成的。

第二种方法可能是通过添加一列描述或标记缺失数据的列来标记缺失数据的行。例如,假设你知道在某一天发生了停电。此时,你可以添加“停电”来标记缺失数据,并将其与其他标记为“缺失数据”的缺失数据区分开来,如果其原因未知的话。

本章讨论的第三种方法是估算缺失的数据值。这些方法可以从简单的、初步的,到更复杂的技术,后者使用机器学习和复杂的统计模型。但你怎么衡量最初缺失数据的估算值的准确性呢?

处理缺失数据时有多种选择和措施需要考虑,答案并非那么简单。因此,你应该探索不同的方法,强调彻底的评估和验证过程,以确保所选方法最适合你的情况。在本章中,你将使用均方根误差RMSE)来评估不同的填补技术。

计算 RMSE 的过程可以分为几个简单的步骤:首先,计算误差,即实际值与预测值或估计值之间的差异。这是针对每个观测值进行的。由于误差可能是负值或正值,为了避免零求和,误差(差异)会被平方。最后,将所有误差求和并除以观测值的总数来计算平均值。这会给你均方误差(MSE)。RMSE 只是 MSE 的平方根。

RMSE 方程可以写成:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file61.jpg在我们估算缺失观测值时,https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file62.png是插补值,https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file63.png是实际(原始)值,N是观测值的数量。

用于评估多重插补方法的 RMSE

我想指出,RMSE 通常用于衡量预测模型的性能(例如,比较回归模型)。通常,较低的 RMSE 是理想的;它告诉我们模型能够拟合数据集。简单来说,它告诉我们预测值与实际值之间的平均距离(误差)。你希望最小化这个距离。

在比较不同的插补方法时,我们希望插补的值尽可能接近实际数据,这些数据包含随机效应(不确定性)。这意味着我们并不寻求完美的预测,因此较低的 RMSE 分数不一定表示更好的插补方法。理想情况下,你希望找到一个平衡,因此在本章中,RMSE 与可视化结合使用,以帮助说明不同技术如何比较和工作。

提醒一下,我们有意删除了一些值(人为造成缺失数据),但保留了原始数据,以便在使用 RMSE 时进行对比。

执行数据质量检查

缺失数据是指在数据集中没有捕获或没有观察到的值。值可能会缺失于特定特征(列)或整个观测(行)。使用 pandas 加载数据时,缺失值将显示为NaNNaTNA

有时,在给定的数据集中,缺失的观测值会被源系统中的其他值替换;例如,这可以是像999990这样的数字填充值,或者像missingN/A这样的字符串。当缺失值被表示为0时,需要小心,并进一步调查以确定这些零值是否合法,还是缺失数据的标志。

在本教程中,你将探索如何识别缺失数据的存在。

准备工作

你可以从 GitHub 仓库下载 Jupyter 笔记本和所需的数据集。请参考本章的技术要求部分。

你将使用来自Ch7文件夹的两个数据集:clicks_missing_multiple.csvco2_missing.csv

如何操作…

pandas库提供了方便的方法来发现缺失数据并总结 DataFrame 中的数据:

  1. 通过read_dataset()函数开始读取两个 CSV 文件(co2_missing.csvclicks_missing.csv):
folder = Path('../../datasets/Ch7/')
co2_file = Path('co2_missing.csv')
ecom_file = Path('clicks_missing_multiple.csv')
co2_df = read_dataset(folder,
                      co2_file,
                      index=True,
                      date_col='year')
ecom_df = read_dataset(folder,
                       ecom_file,
                       index=True,
                       date_col='date')
ecom_df.head()

这将显示ecom_df DataFrame 的前五行:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file64.png

图 7.1:ecom_df DataFrame 的前五行,显示了 NaN 和 NaT

上述代码的输出显示源数据集中有五个缺失值。NaN是 pandas 表示空数值的方式(NaNNot a Number的缩写)。NaT是 pandas 表示缺失的Datetime值的方式(NaTNot a Time的缩写)。

  1. 要统计两个 DataFrame 中的缺失值数量,你可以使用DataFrame.isnull()DataFrame.isna()方法。这会返回True(如果缺失)或False(如果不缺失)对于每个值。例如,要获取每一列缺失值的总数,你可以使用DataFrame.isnull().sum()DataFrame.isna().sum()

在 Python 中,布尔值(TrueFalse)是整数的一个子类型。True等于1False等于0。要验证这一概念,可以尝试以下操作:

isinstance(True, int)
>> True
int(True)
>> 1

现在,让我们获取每个 DataFrame 中缺失值的总数:

co2_df.isnull().sum()
>>
co2     25
dtype: int64
ecom_df.isnull().sum()
>>
price        1
location     1
clicks      14
dtype: int64

请注意,在上面的代码中同时使用了.isnull().isna()。它们可以互换使用,因为.isnull().isna()的别名。

  1. 在前一步中,co2_dfyear列和ecom_dfdate列未包含在计数结果中。这是因为isnull()isna()关注的是 DataFrame 的列,而不包括索引。我们的read_datasets()函数在技术要求部分将它们设置为索引列。一种简单的方法是将索引重置为列,如下所示:
co2_df.reset_index(inplace=True)
ecom_df.reset_index(inplace=True)

现在,如果你执行isnull().sum(),你应该能看到co2_df的年份列和ecom_df的日期列包含在计数结果中:

co2_df.isnull().sum()
>>
year     0
co2     25
dtype: int64
ecom_df.isnull().sum()
>>
date         4
price        1
location     1
clicks      14
dtype: int64

从结果来看,co2_dfco2列有25个缺失值,而ecom_df总共有20个缺失值(其中4个来自date列,1个来自price列,1个来自location列,14个来自clicks列)。

  1. 要获取整个ecom_df DataFrame 的总计,只需在语句末尾再链式调用.sum()函数:
ecom_df.isnull().sum().sum()
>> 20

类似地,对于co2_df,你可以再链式调用一个.sum()

co2_df.isnull().sum().sum()
>> 25
  1. 如果你使用文本/代码编辑器(如 Excel 或 Jupyter Lab)检查co2_missing.csv文件,并向下滚动到第 192-194 行,你会发现其中有一些字符串占位符值:NAN/Anull

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file65.jpg

图 7.2:co2_missing.csv 显示了由 pandas 转换为 NaN(缺失)的字符串值

图 7.2 显示了这三种字符串值。有趣的是,pandas.read_csv()将这三种字符串值解释为NaN。这是read_csv()的默认行为,可以通过na_values参数进行修改。要查看 pandas 如何表示这些值,可以运行以下命令:

co2_df[190:195]

这将产生以下输出:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file66.png

图 7.3:pandas.read_csv()将 NA、N/A 和 null 字符串解析为 NaN 类型

  1. 如果你只需要检查 DataFrame 是否包含任何缺失值,请使用isnull().values.any()。如果 DataFrame 中有任何缺失值,这将输出True
ecom_df.isnull().values.any()
>> True
co2_df.isnull().values.any()
>> True
  1. 到目前为止,isnull()帮助识别了 DataFrame 中的所有缺失值。但如果缺失值被掩盖或替换为其他占位符值,例如?99999,会怎样呢?这些值会被跳过并视为缺失(NaN)值。在技术上,它们并不是空单元格(缺失),而是具有值的。另一方面,领域知识或先验知识告诉我们,CO2 排放数据集是按年测量的,应该具有大于 0 的值。

同样,我们期望点击流数据的点击次数是数值型的。如果该列不是数值型的,应该引发调查,看看为什么 pandas 不能将该列解析为数值型。例如,这可能是由于存在字符串值。

为了更好地了解 DataFrame 的模式和数据类型,可以使用DataFrame.info()来显示模式、总记录数、列名、列的数据类型、每列非缺失值的计数、索引数据类型和 DataFrame 的总内存使用情况:

ecom_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 135 entries, 0 to 134
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype        
---  ------    --------------  -----        
 0   date      132 non-null    datetime64[ns]
 1   price     134 non-null    float64      
 2   location  134 non-null    float64      
 3   clicks    121 non-null    object       
dtypes: datetime64ns, float64(2), object(1)
memory usage: 4.3+ KB
co2_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 226 entries, 0 to 225
Data columns (total 2 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   year     226 non-null    datetime64[ns]
 1   co2      201 non-null    float64
dtypes: float64(1), int64(1)
memory usage: 3.7 KB

co2_df的汇总输出看起来合理,确认我们有25个缺失值(226 个总记录减去 221 个非空值,得到 25 个缺失值)在co2列中。

另一方面,ecom_df的汇总显示clicks列的数据类型为object(表示混合类型),而不是预期的float64。我们可以进一步通过基础的汇总统计信息进行调查。

  1. 要获取 DataFrame 的汇总统计信息,请使用DataFrame.describe()方法:
co2_df.describe(include='all')

输出如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file67.png

图 7.4:co2_df 的汇总统计信息,表明数据中存在零值

请注意使用include='all'来替代默认值include=None。默认行为是仅显示数字列的汇总统计信息。通过将值更改为'all',结果将包括所有列类型。

co2_df DataFrame 的摘要统计确认了我们在 co2 列下有零值(最小值 = 0.00)。正如之前所指出的,先验知识告诉我们,0 代表一个空值(或缺失值)。因此,零值需要被替换为 NaN,以便将这些值纳入插补过程。现在,查看 ecom_df 的摘要统计:

ecom_df.describe(include='all')

输出如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file68.jpg

图 7.5:ecom_df 摘要统计,显示点击列中的 ?

如你所见,ecom_df DataFrame 的摘要统计表明,我们在 clicks 列下有一个 ? 值。这也解释了为什么 pandas 没有将该列解析为数值类型(因为存在混合类型)。类似地,? 值需要被替换为 NaN,以便将其视为缺失值并进行插补。

  1. 0? 的值转换为 NaN 类型。这可以通过使用 DataFrame.replace() 方法来完成:
co2_df.replace(0, np.NaN, inplace=True)
ecom_df.replace('?', np.NaN, inplace=True)
ecom_df['clicks'] = ecom_df['clicks'].astype('float')

为了验证,运行 DataFrame.isnull().sum(),你应该注意到缺失值的计数已经增加:

co2_df.isnull().sum()
>>
year        0
missing    35
dtype: int64
ecom_df.isnull().sum()
>>
date         4
price        1
location     1
clicks      16
dtype: int64

新的数字更好地反映了两个 DataFrame 中实际缺失值的数量。

它是如何工作的……

在使用 pandas.read_csv() 读取 CSV 文件时,默认行为是识别并解析某些字符串值,例如 NAN/Anull,并将其转换为 NaN 类型(缺失值)。因此,一旦这些值变为 NaN,CSV 阅读器就可以基于剩余的非空值将 co2 列解析为 float64(数值型)。

这可以通过两个参数来实现:na_valueskeep_default_na。默认情况下,na_values 参数包含一个字符串列表,这些字符串会被解释为 NaN。该列表包括 #N/A#N/A N/A#NA-1.#IND-1.#QNAN-NaN-nan1.#IND1.#QNAN<NA>N/ANANULLNaNn/anannull

你可以通过提供额外的值给 na_values 参数来将其附加到此列表中。此外,keep_default_na 默认设置为 True,因此使用(附加)na_values 时,会与默认列表一起用于解析。

如果你将 keep_default_na 设置为 False,而没有为 na_values 提供新的值,则不会将任何字符串(如 NAN/Anull)解析为 NaN,除非你提供自定义列表。例如,如果将 keep_default_na 设置为 False 且未为 na_values 提供值,则整个 co2 列会被解析为 string(对象类型),任何缺失值将以字符串的形式出现;换句话说,它们会显示为 '',即一个空字符串。

这是一个例子:

co2_df = pd.read_csv(folder/co2_file,
                     keep_default_na=False)
co2_df.isna().sum()
>>
year    0
co2     0
dtype: int64
co2_df.shape
>> (226, 2)

请注意,我们没有丢失任何数据(226 条记录),但是没有显示任何 NaN(或缺失)值。我们来检查一下 DataFrame 的结构:

co2_df.info()
>>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 226 entries, 0 to 225
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype        
---  ------  --------------  -----        
 0   year    226 non-null    datetime64[ns]
 1   co2     226 non-null    object       
dtypes: datetime64ns, object(1)
memory usage: 3.7+ KB

注意 co2 列的 dtype 发生了变化。我们再检查一下从索引 190195 的数据:

co2_df[190:195]

输出如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file69.jpg

图 7.6:没有 NaN 解析的 co2_df DataFrame 输出

最后,你可以检查缺失值是如何处理的:

co2_df.iloc[132:139]

你会注意到所有七行都有空值(空字符串)。

在这个示例中,你探索了.isna()方法。一旦数据被读取到 DataFrame 或系列中,你可以访问.isna().isnull()这两个可互换的方法,如果数据缺失,它们会返回True,否则返回False。要获取每列的计数,我们只需链式调用.sum()函数,而要获取总计,则继续链式调用另一个.sum()函数:

co2_df.isnull().sum()
co2_df.isnull().sum().sum()

还有更多内容…

如果你知道数据中总是会包含?,并且应该将其转换为NaN(或其他值),你可以利用pd.read_csv()函数并更新na_values参数。这将减少在创建 DataFrame 后清理数据所需的步骤:

ecom_df = pd.read_csv(folder/ecom_file,
                      parse_dates=['date'],
                      na_values={'?'})

这将把所有的?替换为NaN

另见

使用 pandas 进行单变量插补处理缺失数据

通常,插补缺失数据有两种方法:单变量插补多变量插补。本示例将探讨 pandas 中可用的单变量插补技术。

在单变量插补中,你使用单一变量中的非缺失值(比如某一列或特征)来填补该变量的缺失值。例如,如果你的数据集中的销售列有一些缺失值,你可以使用单变量插补方法,通过平均销售额来填补缺失的销售数据。在这里,使用了单一列(sales)来计算均值(来自非缺失值)进行填补。

一些基本的单变量插补技术包括以下内容:

  • 使用均值进行填补。

  • 使用最后一个观察值进行前向填补(前向填充)。这可以称为最后观察值向前填充LOCF)。

  • 使用下一个观察值进行向后填补(向后填充)。这可以称为下一个观察值向后填充NOCB)。

你将使用两个数据集,通过不同的技术进行缺失数据插补,然后比较结果。

准备工作

你可以从 GitHub 仓库下载 Jupyter 笔记本和所需的数据集。请参考本章的技术要求部分。

你将使用 Ch7 文件夹中的四个数据集:clicks_original.csvclicks_missing.csvclicks_original.csv,和 co2_missing_only.csv。这些数据集可以从 GitHub 仓库获取。

如何执行……

你将从导入库开始,然后读取四个 CSV 文件。你将使用数据集的原始版本来比较插补结果,以便更好地理解它们的表现。作为比较度量,你将使用 RMSE 来评估每种技术,并通过可视化输出结果来直观比较插补结果:

  1. 使用 read_dataset() 函数读取四个数据集:
co2_original = read_dataset(folder, 'co2_original.csv', 'year', index=True)
co2_missing = read_dataset(folder, 'co2_missing_only.csv', 'year', index=True)
clicks_original = read_dataset(folder, 'clicks_original.csv', 'date', index=True)
clicks_missing = read_dataset(folder, 'clicks_missing.csv', 'date', index=True)
  1. 可视化 CO2 数据框(原始数据与缺失数据),并指定包含缺失值的列(co2):
plot_dfs(co2_original,
         co2_missing,
         'co2',
         title="Annual CO2 Emission per Capita",
         xlabel="Years",
         ylabel="x100 million tons")

plot_dfs 函数将生成两个图:一个是原始的无缺失值的 CO2 数据集,另一个是带有缺失值的修改后的数据集。

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file70.jpg

图 7.7:CO2 数据集,展示缺失值与原始数据的比较

图 7.7 可以看到 CO2 水平随时间显著上升,并且在三个不同的地方存在缺失数据。现在,开始可视化点击流数据框:

plot_dfs(clicks_original,
         clicks_missing,
         'clicks',
         title="Page Clicks per Day",
         xlabel="date",
         ylabel="# of clicks")

plot_dfs 函数将生成两个图:一个是原始的无缺失值的点击流数据集,另一个是带有缺失值的修改后的数据集。

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file71.jpg

图 7.8:点击流数据集,展示缺失值与原始数据的比较

注意,输出显示从 5 月 15 日到 5 月 30 日的数据缺失。你可以通过运行以下代码来确认这一点:

clicks_missing[clicks_missing['clicks'].isna()]
  1. 现在你准备进行第一次插补。你将使用 .fillna() 方法,该方法有一个 value 参数,接受数字或字符串值,用来替代所有的 NaN 实例。此外,你还将使用 .ffill() 进行前向填充,使用 .bfill() 进行后向填充。

让我们利用 method 参数来插补缺失值,并将结果作为新列追加到数据框中。首先从 CO2 数据框开始:

co2_missing['ffill'] = co2_missing['co2'].ffill()
co2_missing['bfill'] = co2_missing['co2'].bfill()
co2_missing['mean'] = co2_missing['co2'].fillna(co2_missing['co2'].mean())

使用 rmse_score 函数获取得分:

_ = rmse_score(co2_original,
                    co2_missing,
                    'co2')
>>
RMSE for ffil: 0.05873012599267133
RMSE for bfill: 0.05550012995280968
RMSE for mean: 0.7156383637041684

现在,使用 plot_dfs 函数可视化结果:

plot_dfs(co2_original, co2_missing, 'co2')

上面的代码生成的结果如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file72.jpg

图 7.9:CO2 数据框架中三种插补方法的比较

图 7.9 中的结果与 图 7.7 中的原始数据进行比较。注意,无论是 ffill 还是 bfill 都比使用 mean 方法产生了更好的结果。两种技术都具有较低的 RMSE 得分,并且视觉效果更好。

  1. 现在,在点击流数据框上执行相同的插补方法:
clicks_missing['ffil'] = clicks_missing['clicks'].ffill()
clicks_missing['bfill'] = clicks_missing['clicks'].bfill()
clicks_missing['mean'] = clicks_missing['clicks'].fillna(clicks_missing['clicks'].mean())

现在,计算 RMSE 得分:

_ = rmse_score(clicks_original,
                    clicks_missing,
                    'clicks')
>>
RMSE for ffil: 1034.1210689204554
RMSE for bfill: 2116.6840489225033
RMSE for mean: 997.7600138929953

有趣的是,对于 Clickstream 数据集,均值插补的 RMSE 分数最低,这与 CO2 数据集的结果形成对比。我们通过可视化结果来从另一个角度审视性能:

plot_dfs(clicks_original, clicks_missing, 'clicks')

你会得到如下图形:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file73.jpg

图 7.10:三种插补方法在 Clickstream 数据框中的比较

比较 图 7.10 中的结果与 图 7.8 中的原始数据。请注意,通过对两个不同数据集(CO2 和 Clickstream)进行插补,处理缺失数据时并没有一个 放之四海而皆准的策略。相反,每个数据集都需要不同的策略。因此,你应该始终检查结果,并根据数据的性质调整输出与预期的匹配。

它是如何工作的……

使用 DataFrame.fillna() 是最简单的插补方法。在前一节中,你使用了 .fillna() 中的 value 参数,并传递了均值(一个标量数字值)来填充所有缺失值。

使用的其他选项包括 向后填充.bfill()),该方法使用缺失位置后的下一个观测值,并向后填充空缺。此外,还使用了 向前填充.ffill()),该方法使用缺失位置前的最后一个值,并向前填充空缺。

还有更多……

.fillna() 中的 value 参数也可以接受一个 Python 字典、一个 pandas Series 或一个 pandas DataFrame,而不仅仅是一个 标量

使用 Python 字典

让我们通过另一个示例演示如何使用 Python 字典为多列插补缺失值。首先读取 clicks_missing_more.csv 文件:

clicks_missing = read_dataset(folder, 'clicks_missing_more.csv', 'date', index=True)
clicks_missing.isna().sum()

这应该会生成以下结果:

price        5
location     6
clicks      16
dtype: int64

这里有三列包含缺失值。我们可以使用字典来定义一个映射,其中每个 键值对 对应 clicks_missing 数据框中的一列。我们可以为不同的列定义不同的统计量(中位数均值众数)作为插补策略。以下代码展示了这一点:

values = {'clicks': clicks_missing['clicks'].median(),
         'price': clicks_missing['clicks'].mean(),
         'location': clicks_missing['location'].mode()}
clicks_missing.fillna(value=values, inplace=True)
clicks_missing.isna().sum()

前面的代码应该会生成以下结果,显示所有三列的缺失值都已填充。

price       0
location    0
clicks      0
dtype: int64

inplace=True 参数会直接修改 clicks_missing 数据框,意味着所做的更改会直接应用于该数据框。

使用另一个 DataFrame

你还可以使用另一个 pandas DataFrame(或 Series)来插补缺失值,前提是列名需要匹配,以便正确映射各列。在以下示例中,你将读取 clicks_missing_more.csv 文件和 clicks_original.csv 文件进行演示。

clicks_missing = read_dataset(folder, 'clicks_missing_more.csv', 'date', index=True)
clicks_original = read_dataset(folder, 'clicks_original.csv', 'date', index=True)

你将使用 clicks_original 数据框来插补 clicks_missing 数据框中的缺失值。

clicks_missing.fillna(value=clicks_original, inplace=True)
clicks_missing.isna().sum()

前面的代码应该会生成以下结果,显示所有三列的缺失值都已填充。

price       0
location    0
clicks      0
dtype: int64

另请参见

要了解更多关于DataFrame.fillna()的信息,请访问官方文档页面:pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html

在以下的步骤中,你将执行类似的单变量插补,但这次使用的是Scikit-Learn库。

使用 scikit-learn 进行单变量插补处理缺失数据

Scikit-Learn是 Python 中非常流行的机器学习库。scikit-learn库提供了大量的选项,涵盖日常机器学习任务和算法,如分类、回归、聚类、降维、模型选择和预处理。

此外,库还提供了多种选项来进行单变量和多变量数据插补。

准备工作

你可以从 GitHub 存储库下载 Jupyter 笔记本和必需的数据集。请参考本章的技术要求部分。

本步骤将使用之前准备好的三个函数(read_datasetrmse_scoreplot_dfs)。

你将使用Ch7文件夹中的四个数据集:clicks_original.csvclicks_missing.csvco2_original.csvco2_missing_only.csv。这些数据集可以从 GitHub 存储库下载。

如何操作…

你将从导入库开始,然后读取所有四个 CSV 文件:

  1. 你将使用scikit-learn库中的SimpleImputer类来执行单变量插补:
from sklearn.impute import SimpleImputer
folder = Path('../../datasets/Ch7/')
co2_original = read_dataset(folder,
                            'co2_original.csv', 'year', index=True)
co2_missing = read_dataset(folder,
                           'co2_missing_only.csv', 'year', index=True)
clicks_original = read_dataset(folder,
                               'clicks_original.csv', 'date', index=True)
clicks_missing = read_dataset(folder,
                              'clicks_missing.csv', 'date', index=True)
  1. SimpleImputer接受不同的strategy参数值,包括meanmedianmost_frequent。让我们探索这三种策略,并看看它们的比较。为每种方法创建一个元组列表:
strategy = [
    ('Mean Strategy', 'mean'),
    ('Median Strategy', 'median'),
    ('Most Frequent Strategy', 'most_frequent')]

你可以循环遍历Strategy列表,应用不同的插补策略。SimpleImputer有一个fit_transform方法。它将两个步骤合并为一个:先拟合数据(.fit),然后转换数据(.transform)。

请记住,SimpleImputer接受 NumPy 数组,因此你需要使用Series.values属性,然后调用.reshape(-1, 1)方法来创建二维 NumPy 数组。简单来说,这样做的目的是将形状为(226, )的 1D 数组从.values转换为形状为(226, 1)的二维数组,即列向量:

co2_vals = co2_missing['co2'].values.reshape(-1,1)
clicks_vals = clicks_missing['clicks'].values.reshape(-1,1)
for s_name, s in strategy:
    co2_missing[s_name] = (
        SimpleImputer(strategy=s).fit_transform(co2_vals))
    clicks_missing[s_name] = (
        SimpleImputer(strategy=s).fit_transform(clicks_vals))

现在,clicks_missingco2_missing这两个 DataFrame 已经有了三列额外的列,每一列对应实现的插补策略。

  1. 使用rmse_score函数,你现在可以评估每个策略。从 CO2 数据开始。你应该获得如下输出:
_ = rmse_score(co2_original, co2_missing, 'co2')
>>
RMSE for Mean Strategy: 0.7156383637041684
RMSE for Median Strategy: 0.8029421606859859
RMSE for Most Frequent Strategy: 1.1245663822743381

对于 Clickstream 数据,你应该获得如下输出:

_ = rmse_score(clicks_original, clicks_missing, 'clicks')
>>
RMSE for Mean Strategy: 997.7600138929953
RMSE for Median Strategy: 959.3580492530756
RMSE for Most Frequent Strategy: 1097.6425985146868

注意,RMSE 策略排名在两个数据集之间的差异。例如,Mean策略在 CO2 数据上表现最好,而Median策略在 Clickstream 数据上表现最好。

  1. 最后,使用plot_dfs函数绘制结果。从 CO2 数据集开始:
plot_dfs(co2_original, co2_missing, 'co2')

它会生成如下图表:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file74.jpg

Figure 7.11: 比较 CO2 数据集的三种 SimpleImputer 策略

Figure 7.11 中的结果与 Figure 7.7 中的原始数据进行比较。对于 Clickstream 数据集,你应该使用以下内容:

plot_dfs(clicks_original, clicks_missing, 'clicks')

这应该绘制出所有三种策略:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file75.jpg

Figure 7.12: 比较 Clickstream 数据集的三种 SimpleImputer 策略

Figure 7.12 中的结果与 Figure 7.8 中的原始数据进行比较。

SimpleImputer 提供了一些基本策略,对某些数据可能合适,对其他数据则不一定。这些简单的填充策略(包括前一节 使用 pandas 进行单变量填充处理缺失数据 中的方法)的优点在于它们快速且易于实现。

它是如何工作的…

你使用 SimpleImputer 类实现了三种简单策略来填补缺失值:均值、中位数和最频繁值(众数)。

这是一种单变量填充技术,意味着仅使用一个特征或列来计算均值、中位数和最频繁值。

SimpleImputer 类有三个参数是你需要了解的:

  • missing_values 默认设置为 nan,更具体地说是 np.nan。NumPy 的 nan 和 pandas 的 NaN 很相似,正如下面的例子所示:
test = pd.Series([np.nan, np.nan, np.nan])
test
>>
0   NaN
1   NaN
2   NaN
dtype: float64

SimpleImputer 将填补所有 missing_values 的出现情况,你可以使用 pandas.NA、整数、浮点数或字符串值来更新它。

  • strategy 默认为 mean,接受字符串值。

  • fill_value 可以用特定值替换所有 missing_values 的实例。这可以是字符串或数值。如果 strategy 设置为 constant,则需要提供自定义的 fill_value

在 Scikit-Learn 中,用于预处理数据(如填补)的常见工作流程包括两个主要步骤:

  1. 拟合 Imputer:首先,你使用 .fit() 方法对数据进行拟合。这一步涉及“训练” imputer,在填补的上下文中意味着从提供的数据中计算所需的统计数据(如均值、中位数等)。拟合过程通常在训练数据集上完成。

  2. 应用转换:在拟合后,使用 .transform() 方法将 imputer 应用于数据。这一步骤实际上执行了填充操作,用计算出的统计数据替换缺失值。

在我们的示例中,这两个步骤被合并为一个,使用 .fit_transform() 方法。该方法首先对数据进行拟合(即计算所需的统计数据),然后立即应用转换(即替换缺失值)。在初始数据预处理阶段使用 .fit_transform() 是一种方便的方法。

此外,pandas 的 DataFrame 中,.fillna() 方法可以提供与 SimpleImputer 相同的功能。例如,mean 策略可以通过使用 pandas 的 DataFrame.mean() 方法,并将其传递给 .fillna() 来实现。

以下示例说明了这一点,并比较了 Scikit-Learn 和 pandas 的两个结果:

avg = co2_missing['co2'].mean()
co2_missing['pands_fillna'] = co2_missing['co2'].fillna(avg)
cols = ['co2', 'Mean Strategy', 'pands_fillna']
_ = rmse_score(co2_original, co2_missing[cols], 'co2')
>>
RMSE for Mean Strategy: 0.7156383637041684
RMSE for pands_fillna: 0.7156383637041684

注意你是如何与 scikit-learn 的 SimpleImputer 类实现相同的结果的。.fillna() 方法使得在整个 DataFrame 中进行插补变得更加容易(逐列处理)。例如,如果你有一个包含多个缺失数据列的 sales_report_data DataFrame,你可以通过一行代码 sales_report_data.fillna(sales_report_data.mean()) 来执行均值插补。

还有更多内容

scikit-learn 中的 SimpleImputer 的 add_indicator 选项是一个有用的功能,可以增强插补过程。它的作用是将一个 MissingIndicator 转换器添加到输出中(添加一个额外的二进制列,指示原始数据是否缺失,1 表示缺失,0 表示已观察到。这个功能可以用于将缺失信息编码为特征,从而提供额外的洞察)。

以下是如何使用 add_indicator=True 启用此功能的示例。在此示例中,你将使用 .fit() 方法,接着使用 .transform()

co2_missing = read_dataset(folder,
                           'co2_missing_only.csv', 'year', index=True)
co2_vals = co2_missing['co2'].values.reshape(-1,1)
imputer = SimpleImputer(strategy='mean', add_indicator=True)
imputer.fit(co2_vals)
imputer.get_params()
>>
{'add_indicator': True,
 'copy': True,
 'fill_value': None,
 'keep_empty_features': False,
 'missing_values': nan,
 'strategy': 'mean'}

然后你可以使用 .transform() 方法,并将两列添加到原始的 co2_missing DataFrame 中:

co2_missing[['imputed', 'indicator']] = (imputer.transform(co2_vals))
co2_missing.head(5)

前面的代码应该会产生以下输出:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file76.png

图 7.13:更新 co2_missing DataFrame,添加两列

注意如何添加了指示列,其中 0 表示原始观察值,1 表示缺失值。

参见

若要了解更多关于 scikit-learn 的 SimpleImputer 类,请访问官方文档页面:scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html#sklearn.impute.SimpleImputer

到目前为止,你一直在处理单变量插补。一个更强大的方法是多变量插补,你将在接下来的示例中学习到这一方法。

使用多变量插补处理缺失数据

之前,我们讨论了填补缺失数据的两种方法:单变量 插补多变量 插补

正如你在之前的示例中看到的,单变量插补是使用一个变量(列)来替代缺失的数据,而忽略数据集中的其他变量。单变量插补技术通常实现起来更快、更简单,但在大多数情况下,多变量插补方法可能会产生更好的结果。

与使用单一变量(列)不同,在多元插补中,该方法使用数据集中的多个变量来插补缺失值。其理念很简单:让数据集中的更多变量参与进来,以提高缺失值的可预测性。

换句话说,单变量插补方法仅处理特定变量的缺失值,而不考虑整个数据集,专注于该变量来推导估算值。在多元插补中,假设数据集中的变量之间存在一定的协同作用,并且它们可以共同提供更好的估算值来填补缺失值。

在本示例中,你将使用 Clickstream 数据集,因为它有额外的变量(clickspricelocation 列),可以用来对 clicks 进行多元插补。

准备工作

你可以从 GitHub 仓库下载 Jupyter 笔记本和所需的数据集。请参考本章的 技术要求 部分。

此外,你还将利用本章前面定义的三个函数(read_datasetrmse_scoreplot_dfs)。

如何实现…

在这个示例中,你将使用 scikit-learn 进行多元插补。该库提供了 IterativeImputer 类,允许你传递一个回归模型来根据数据集中其他变量(列)预测缺失值:

  1. 首先,导入必要的库、方法和类:
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import ExtraTreesRegressor, BaggingRegressor
from sklearn.linear_model import ElasticNet, LinearRegression
from sklearn.neighbors import KneighborsRegressor

将两个 Clickstream 数据集加载到数据框中:

folder = Path('../../datasets/Ch7/')
clicks_original = read_dataset(folder,
                            'clicks_original.csv', 'date')
clicks_missing = read_dataset(folder,
                            'clicks_missing.csv', 'date')
  1. 使用 IterativeImputer,你可以测试不同的估算器。那么,让我们尝试不同的回归器并比较结果。创建一个要在 IterativeImputer 中使用的回归器(估算器)列表:
estimators = [
    ('bayesianRidge', BayesianRidge()),
    ('extra_trees', ExtraTreesRegressor(n_estimators=50)),
    ('bagging', BaggingRegressor(n_estimators=50)),
    ('elastic_net', ElasticNet()),
    ('linear_regression', LinearRegression()),
    ('knn', KNeighborsRegressor(n_neighbors=3))
]
  1. 遍历估算器并使用 .fit() 在数据集上进行训练,从而构建不同的模型,最后通过在缺失数据的变量上应用 .transform() 进行插补。每个估算器的结果将作为新列附加到 clicks_missing 数据框中,以便进行得分并直观地比较结果:
clicks_vals = clicks_missing.iloc[:,0:3].values
for e_name, e in estimators:
    est = IterativeImputer(
                random_state=15,
                estimator=e).fit(clicks_vals)
    clicks_missing[e_name] = est.transform(clicks_vals)[: , 2]
  1. 使用 rmse_score 函数评估每个估算器:
_ = rmse_score(clicks_original, clicks_missing, 'clicks')

这将打印出以下得分:

RMSE for bayesianRidge: 949.4393973455851
RMSE for extra_trees: 1577.3003394830464
RMSE for bagging: 1237.4433923801062
RMSE for elastic_net: 945.40752093431
RMSE for linear_regression: 938.9419831427184
RMSE for knn: 1336.8798392251822

观察到贝叶斯岭回归、弹性网和线性回归产生了相似的结果。

  1. 最后,绘制结果以便于不同估算器之间的可视化比较:
plot_dfs(clicks_original, clicks_missing, 'clicks')

输出结果如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file77.jpg

图 7.14:使用迭代插补比较不同的估算器

比较 图 7.14 中的结果与 图 7.8 中的原始数据。

在本章开始时,我们讨论了使用 RMSE(均方根误差)来评估填补方法可能会有些误导。这是因为我们进行填补的目标不一定是为了得到“最佳”分数(即最小的 RMSE 值),就像在预测建模中我们所追求的那样。相反,填补的目标是以一种尽可能接近原始数据集的真实特性和分布的方式来填补缺失的数据。

尽管 RMSE 存在局限性,但在比较不同填补方法时,它仍然能提供有价值的洞察。它帮助我们理解哪种方法能更接近实际值地估算缺失值,基于可用数据。

然而,必须认识到,在真实数据的背景下,较低的 RMSE 并不总意味着更“准确”的填补。这是因为真实数据集通常包含噪音和随机性,而一些填补方法可能无法捕捉到这些特性,尤其是那些产生最低 RMSE 值的方法。像 BayesianRidgeElasticNetLinear Regression 这样的算法可能会产生较低的 RMSE 值,但可能会过度平滑数据(请参见图 7.14中的这三种估计器),未能反映真实数据集中固有的随机性和变异性。

后续,在使用填补后的数据建立预测模型(如预测模型)时,我们需要认识到填补值的某些不完美性是可以接受的。这是因为我们通常不知道缺失数据的真实性质,而我们的目标是创建一个能为模型训练和分析提供“足够好”代表性的数据集。本质上,目标是实现平衡——一种提供合理缺失值估计的填补方法,同时保持数据的整体特性,包括其随机性和变异性。

它是如何工作的……

R 语言中的 MICE 包启发了 IterativeImputer 类的设计,后者由 scikit-learn 库实现,用于执行 多元链式方程法填补www.jstatsoft.org/article/view/v045i03)。IterativeImputer 与原始实现有所不同,你可以在这里阅读更多内容:scikit-learn.org/stable/modules/impute.html#id2

请记住,IterativeImputer 仍处于实验模式。在下一节中,你将使用 statsmodels 库中的另一种 MICE 实现。

还有更多……

statsmodels 库中有一个 MICE 的实现,你可以用它来与 IterativeImputer 进行比较。这个实现更接近于 R 中的 MICE 实现。

你将使用相同的 DataFrame(clicks_originalclicks_missing),并将 statsmodels MICE 填补输出作为附加列添加到 clicks_missing DataFrame 中。

首先,加载所需的库:

from statsmodels.imputation.mice import MICE, MICEData, MICEResults
import statsmodels.api as sm

由于你的目标是插补缺失数据,你可以使用MICEData类来封装clicks_missing数据框。首先创建MICEData的实例,并将其存储在mice_data变量中:

# create a MICEData object
fltr = ['price', 'location','clicks']
mice_data = MICEData(clicks_missing[fltr],
                     perturbation_method='gaussian')
# 20 iterations
mice_data.update_all(n_iter=20)
mice_data.set_imputer('clicks', formula='~ price + location', model_class=sm.OLS)

MICEData为 MICE 插补准备数据集,并在循环中调用update_all()方法(执行 20 次)进行多次插补,每次基于数据集中的其他变量优化插补值。perturbation_method='gaussian'指定在插补过程中用于扰动缺失数据的方法。'gaussian'方法从正态(高斯)分布中添加噪声。

将结果存储在新列中,并命名为MICE。这样,你可以将得分与IterativeImputer的结果进行比较:

clicks_missing['MICE']  = mice_data.data['clicks'].values.tolist()
_ = rmse_score(clicks_original, clicks_missing, 'clicks')
>>
RMSE for bayesianRidge: 949.4393973455851
RMSE for extra_trees: 1577.3003394830464
RMSE for bagging: 1237.4433923801062
RMSE for elastic_net: 945.40752093431
RMSE for linear_regression: 938.9419831427184
RMSE for knn: 1336.8798392251822
RMSE for MICE: 1367.190103013395

最后,绘制结果进行最终比较。这将包括来自IterativeImputer的一些插补结果:

cols = ['clicks','bayesianRidge', 'bagging', 'knn', 'MICE']
plot_dfs(clicks_original, clicks_missing[cols], 'clicks')

输出如下:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file78.jpg

图 7.15 比较 statsmodels MICE 实现与 scikit-learn IterativeImputer

图 7.15中的结果与图 7.8中的原始数据进行比较。

总体而言,多元插补技术通常比单变量方法产生更好的结果。在处理具有更多特征(列)和记录的复杂时间序列数据集时,这一点尤其成立。尽管单变量插补器在速度和解释简便性方面更高效,但仍然需要平衡复杂性、质量和分析需求。

另见

使用插值处理缺失数据

另一种常用的缺失值插补技术是插值。pandas 库提供了DataFrame.interpolate()方法,用于更复杂的单变量插补策略。

例如,插值方法之一是线性插值。线性插值可以通过在两个围绕缺失值的点之间画一条直线来填补缺失数据(在时间序列中,这意味着对于缺失的数据点,它会查看前一个过去值和下一个未来值,并在这两者之间画一条直线)。而多项式插值则尝试在两个点之间画一条曲线。因此,每种方法都会采用不同的数学运算来确定如何填充缺失数据。

pandas 的插值功能可以通过SciPy库进一步扩展,后者提供了额外的单变量和多变量插值方法。

在这个例子中,你将使用 pandas 的DataFrame.interpolate()函数来检查不同的插值方法,包括线性插值、多项式插值、二次插值、最近邻插值和样条插值。

准备工作

你可以从 GitHub 仓库下载 Jupyter 笔记本和必要的数据集。请参考本章的技术要求部分。

你将使用之前准备好的三个函数(read_datasetrmse_scoreplot_dfs)。

你将使用来自Ch7文件夹的四个数据集:clicks_original.csvclicks_missing.csvco2_original.csvco2_missing_only.csv。这些数据集可以从 GitHub 仓库中获取。

操作步骤…

你将对两个不同的数据集进行多次插值,然后使用 RMSE 和可视化对比结果:

  1. 首先导入相关库并将数据读取到 DataFrame 中:
folder = Path('../../datasets/Ch7/')
co2_original = read_dataset(folder,
                            'co2_original.csv', 'year', index=True)
co2_missing = read_dataset(folder,
                           'co2_missing_only.csv', 'year', index=True)
clicks_original = read_dataset(folder,
                               'clicks_original.csv', 'date', index=True)
clicks_missing = read_dataset(folder,
                              'clicks_missing.csv', 'date', index=True)
  1. 创建一个待测试的插值方法列表:linearquadraticnearestcubic
interpolations = [
    'linear',
    'quadratic',
    'nearest',
    'cubic'
]
  1. 你将遍历列表,使用.interpolate()进行不同的插值操作。为每个插值输出附加一个新列,以便进行对比:
for intp in interpolations:
    co2_missing[intp] = co2_missing['co2'].interpolate(method=intp)
    clicks_missing[intp] = clicks_missing['clicks'].interpolate(method=intp)
  1. 还有两种附加的方法值得测试:样条插值多项式插值。要使用这些方法,你需要为order参数提供一个整数值。你可以尝试order = 2来使用样条插值方法,尝试order = 5来使用多项式插值方法。例如,样条插值方法的调用方式如下:.interpolate(method="spline", order = 2)
co2_missing['spline'] = \
        co2_missing['co2'].interpolate(method='spline', order=2)
clicks_missing['spline'] = \
        clicks_missing['clicks'].interpolate(method='spline',order=2)
co2_missing['polynomial'] = \
        co2_missing['co2'].interpolate(method='polynomial',order=5)
clicks_missing['polynomial'] = \
        clicks_missing['clicks'].interpolate(method='polynomial',order=5)
  1. 使用rmse_score函数比较不同插值策略的结果。从 CO2 数据开始:
_ = rmse_score(co2_original, co2_missing, 'co2')
>>
RMSE for linear: 0.05507291327761665
RMSE for quadratic: 0.08367561505614347
RMSE for nearest: 0.05385422309469095
RMSE for cubic: 0.08373627305833133
RMSE for spline: 0.1878602347541416
RMSE for polynomial: 0.06728323553134927

现在,让我们检查 Clickstream 数据:

_ = rmse_score(clicks_original, clicks_missing, 'clicks')
>>
RMSE for linear: 1329.1448378562811
RMSE for quadratic: 5224.641260626975
RMSE for nearest: 1706.1853705030173
RMSE for cubic: 6199.304875782831
RMSE for spline: 5222.922993448641
RMSE for polynomial: 56757.29323647127
  1. 最后,进行可视化以更好地了解每种插值方法的效果。从 CO2 数据集开始:
cols = ['co2', 'linear', 'nearest', 'polynomial']
plot_dfs(co2_original, co2_missing[cols], 'co2')

这应该会绘制出所选列:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file79.jpg

图 7.16:对 CO2 数据集的不同插值策略进行比较

图 7.16中的结果与图 7.7中的原始数据进行对比。

线性最近邻方法在缺失值填充效果上似乎类似。可以通过 RMSE 评分和图表看到这一点。

现在,创建 Clickstream 数据集的图表:

cols = ['clicks', 'linear', 'nearest', 'polynomial', 'spline']
plot_dfs(clicks_original, clicks_missing[cols], 'clicks')

这应该绘制所选列:

https://github.com/OpenDocCN/freelearn-ds-pt4-zh/raw/master/docs/ts-anal-py-cb-2e/img/file80.jpg

图 7.17: 比较 Clickstream 数据集上的不同插值策略

比较图 7.17中的结果与图 7.8中的原始数据。

从输出中,你可以看到当使用5作为多项式阶数时,polynomial方法如何夸大曲线。另一方面,Linear方法则尝试绘制一条直线。

值得注意的是,在实现的策略中,只有线性插值忽略了索引,而其他方法则使用数值索引。

它是如何工作的…

总体而言,插值技术通过检测相邻数据点(缺失点周围的数据)的模式,来预测缺失值应该是什么。最简单的形式是线性插值,它假设两个相邻数据点之间存在一条直线。另一方面,多项式则定义了两个相邻数据点之间的曲线。每种插值方法使用不同的函数和机制来预测缺失数据。

在 pandas 中,你将使用DataFrame.interpolate函数。默认的插值方法是线性插值(method = "linear")。还有其他参数可以提供更多控制,决定如何进行插值填充。

limit参数允许你设置填充的最大连续NaN数量。回想之前的配方,执行数据质量检查,Clickstream 数据集有16个连续的缺失值。你可以限制连续NaN的数量,例如设置为5

clicks_missing['clicks'].isna().sum()
>> 16
example = clicks_missing['clicks'].interpolate(limit = 5)
example.isna().sum()
>> 11

仅填充了 5 个数据点,其余 11 个未填充。

还有更多…

其他库也提供插值,包括以下内容:

另见

若要了解更多关于DataFrame.interpolate的信息,请访问官方文档页面:pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值