在Pandas和Polars中,时间序列概念之间有一些不同。
不再有字符串日期时间
在Pandas中,我们可以用日期字符串处理日期和时间。而在Polars中,我们使用Python的日期时间对象,且我们从不使用字符串来进行日期时间操作。
为了说明这一点,我们在Polars中创建了一个时间序列,然后将其转换为Pandas。为了在Polars中创建日期列,我们在Python中使用名称令人困惑的datetime.datetime类。
from datetime import datetime
import pandas as pd
import polars as pl
df_polars = pl.DataFrame({
"datetime": [
datetime(2021,1,1), datetime(2021,1,2), datetime(2021,1,3)
],
"value": [1, 2, 3]
})
df_pandas = df_polars.to_pandas()
df_polars
shape: (3, 2)
在Pandas中,我们可以使用日期时间字符串来过滤日期时间,如下:
df_pandas.loc[df_pandas["datetime"] > "2021-01-02"
但在Polars中,我们使用datetime.datetime类来过滤日期:
df_polars.filter(pl.col("datetime") > datetime(2021,1,2))
shape: (1, 2)
Polars开发者选择不支持字符串日期时间表示,因为它们很不明确。例如,2021-01-02年可能是1月2日或2月1日,具体情况取决于不同的地区。
当然,我们仍然可以使用dt.strftime方法提取一个字符串表示为一个字符串列:
df_polars.with_columns(pl.col("date").dt.strftime("%Y-%m-%d").alias("date_str"))
shape: (3, 3)
Polars有不同的时间间隔字符串
在Pandas和Polars中,我们可以用字符串来表示间隔。例如,在Pandas中,我们使用30T表示30分钟。在Polars中,我们使用30m表示30分钟。以下是一些在Polars中出现的间隔字符串的例子:
- 1ns (1 nanosecond)
- 1us (1 microsecond)
- 1ms (1 millisecond)
- 1s (1 second)
- 1m (1 minute)
- 1h (1 hour)
- 1d (1 calendar day)
- 1w (1 calendar week)
- 1mo (1 calendar month)
- 1q (1 calendar quarter)
- 1y (1 calendar year)
我们可以组合这些间隔字符串来创建更复杂的间隔。例如,我们可以用1h30m表示1小时30分钟。
默认情况下,Polars可以使用微秒钟来工作
在这两个库中,日期时间、日期和持续时间的数据类型都是基于时间的底层整数表示的。例如,使用 pl.Datetime数据类型,整数表示自Unix历元开始后的计数。
在Pandas中,整数计数默认以纳秒为单位,但在Polars中,整数计数默认以微秒为单位。微秒由我们在下面的数据框架模式中表示:
df_polars.schemaOrderedDict(
[
('datetime', Datetime(time_unit='us', time_zone=None)),
('value', Int64)
])
Polars支持纳秒精度,而Pandas支持微秒精度。
如果我们将Pandas数据帧转换为Polars数据帧,那么整数表示仍然以纳秒为单位。如果一个有纳秒精度,而另一个有微秒精度,我们就不能在一个日期时间上加入两个Polars数据帧。所以当我从Pandas转换为Polars时,我通常使用dt.cast_time_unit表达式将日期时间列直接转换为微秒:
df_polars = pl.from_pandas(df_pandas).with_columns(pl.col("datetime").dt.cast_time_unit("us"))
Polars中缺失的日期时间是空值,而不是NaT
在Pandas中,日期时间列中缺失的日期时间用NaT(not a time)表示。在Polars中,缺失的日期时间用它在每一列中相同的值表示: null。
df_polars = pl.DataFrame(
{
"datetime": [
datetime(2021,1,1), None, datetime(2021,1,3)
],
"value": [1, 2, 3]
}
)
shape: (3, 2)
我发现,对于每列中的缺失值有相同的表示,使得处理Polars中的缺失值更容易。这是因为我不需要记住在不同数据类型中缺失值的不同方法,例如在Pandas中的isna和isnull。
Polars中的时间组群有自己的方法
在Pandas中,你通过传递pd.Grouper方法来进行时间分组:
df_pandas.set_index("datetime").groupby(pd.Grouper(freq='D')).mean()
在Polars中,我们有一种特殊的时间分组方法group_by_dynamic。在这个例子中,我们得到了每一天的平均值:
df_polars.sort("datetime").group_by_dynamic("datetime", every="1d").agg(pl.col("value").mean())
要注意,在进行分组操作之前,我们先按日期时间列对数据帧进行排序。这是因为 group_by_dynamic方法要求按我们要分组的列对数据进行排序。
与在Pandas中一样,我们在如何设置分组窗口方面有很多的灵活性。例如,如果我们想平移窗口的开始2个小时,我们可以这样做:
df_polars.sort("datetime").group_by_dynamic("datetime", every="1d", offset="2h").agg(pl.col("value").mean())
Pandas中的重采样在Polars中是向上采样或分组动态的
在Pandas中,我们使用resample(重采样)方法来改变一个时间序列的频率。
在Polars中,我们使用upsample(上采样)方法改变到比数据更高的频率。
例如,要将1小时的频率上采样到30分钟,我们可以这样做:
df_polars.sort("datetime").upsample("30m", "datetime")
Polars对已排序的数据有快速路径操作
Polars通过对已排序数据使用快速通道来加速操作。这些快速通道发生在Polars知道一个列被排序的地方,因此可以使用更快的算法来执行该操作。由于时间序列数据具有自然的排序顺序,因此了解时间序列分析的快速通道尤为重要。
我们可以将上面的过滤器代码用于对时间序列数据的快速路径操作的简单示例。这次我们找出1月2日之前的日期时间。
df_polars.filter(pl.col("datetime") < datetime(2021,1,2))
如果Polars知道日期时间列已被排序,那么快速通道操作是在找到大于或等于过滤器值的第一行后停止扫描该列。这比扫描整个列要快得多。
支持快速通道操作的其他重要时间序列方法包括group_by和 join。