Resampling
We can resample by either:
- upsampling (moving data to a higher frequency)
- downsampling (moving data to a lower frequency)
- combinations of these e.g. first upsample and then downsample
Downsampling to a lower frequency
Polars
views downsampling as a special case of the group_by operation and you can do this with group_by_dynamic
and group_by_rolling
- see the temporal group by page for examples.
Upsampling to a higher frequency
Let's go through an example where we generate data at 30 minute intervals:
df = pl.DataFrame(
{
"time": pl.date_range(
start=datetime(2021, 12, 16),
end=datetime(2021, 12, 16, 3),
interval="30m",
eager=True,
),
"groups": ["a", "a", "a", "b", "b", "a", "a"],
"values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
}
)
print(df)
let df = df!(
"time" => date_range(
"time",
NaiveDate::from_ymd_opt(2021, 12, 16).unwrap().and_hms_opt(0, 0, 0).unwrap(),
NaiveDate::from_ymd_opt(2021, 12, 16).unwrap().and_hms_opt(3, 0, 0).unwrap(),
Duration::parse("30m"),
ClosedWindow::Both,
TimeUnit::Milliseconds, None)?,
"groups" => &["a", "a", "a", "b", "b", "a", "a"],
"values" => &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
)?;
println!("{}", &df);
shape: (7, 3)
┌─────────────────────┬────────┬────────┐
│ time ┆ groups ┆ values │
│ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ str ┆ f64 │
╞═════════════════════╪════════╪════════╡
│ 2021-12-16 00:00:00 ┆ a ┆ 1.0 │
│ 2021-12-16 00:30:00 ┆ a ┆ 2.0 │
│ 2021-12-16 01:00:00 ┆ a ┆ 3.0 │
│ 2021-12-16 01:30:00 ┆ b ┆ 4.0 │
│ 2021-12-16 02:00:00 ┆ b ┆ 5.0 │
│ 2021-12-16 02:30:00 ┆ a ┆ 6.0 │
│ 2021-12-16 03:00:00 ┆ a ┆ 7.0 │
└─────────────────────┴────────┴────────┘
Upsampling can be done by defining the new sampling interval. By upsampling we are adding in extra rows where we do not have data. As such upsampling by itself gives a DataFrame with nulls. These nulls can then be filled with a fill strategy or interpolation.
Upsampling strategies
In this example we upsample from the original 30 minutes to 15 minutes and then use a forward
strategy to replace the nulls with the previous non-null value:
out1 = df.upsample(time_column="time", every="15m").fill_null(strategy="forward")
print(out1)
let out1 = df
.clone()
.upsample::<[String; 0]>([], "time", Duration::parse("15m"), Duration::parse("0"))?
.fill_null(FillNullStrategy::Forward(None))?;
println!("{}", &out1);
shape: (13, 3)
┌─────────────────────┬────────┬────────┐
│ time ┆ groups ┆ values │
│ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ str ┆ f64 │
╞═════════════════════╪════════╪════════╡
│ 2021-12-16 00:00:00 ┆ a ┆ 1.0 │
│ 2021-12-16 00:15:00 ┆ a ┆ 1.0 │
│ 2021-12-16 00:30:00 ┆ a ┆ 2.0 │
│ 2021-12-16 00:45:00 ┆ a ┆ 2.0 │
│ … ┆ … ┆ … │
│ 2021-12-16 02:15:00 ┆ b ┆ 5.0 │
│ 2021-12-16 02:30:00 ┆ a ┆ 6.0 │
│ 2021-12-16 02:45:00 ┆ a ┆ 6.0 │
│ 2021-12-16 03:00:00 ┆ a ┆ 7.0 │
└─────────────────────┴────────┴────────┘
In this example we instead fill the nulls by linear interpolation:
upsample
· interpolate
· fill_null
out2 = (
df.upsample(time_column="time", every="15m")
.interpolate()
.fill_null(strategy="forward")
)
print(out2)
let out2 = df
.clone()
.upsample::<[String; 0]>([], "time", Duration::parse("15m"), Duration::parse("0"))?
.lazy()
.with_columns([col("values").interpolate(InterpolationMethod::Linear)])
.collect()?
.fill_null(FillNullStrategy::Forward(None))?;
println!("{}", &out2);
shape: (13, 3)
┌─────────────────────┬────────┬────────┐
│ time ┆ groups ┆ values │
│ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ str ┆ f64 │
╞═════════════════════╪════════╪════════╡
│ 2021-12-16 00:00:00 ┆ a ┆ 1.0 │
│ 2021-12-16 00:15:00 ┆ a ┆ 1.5 │
│ 2021-12-16 00:30:00 ┆ a ┆ 2.0 │
│ 2021-12-16 00:45:00 ┆ a ┆ 2.5 │
│ … ┆ … ┆ … │
│ 2021-12-16 02:15:00 ┆ b ┆ 5.5 │
│ 2021-12-16 02:30:00 ┆ a ┆ 6.0 │
│ 2021-12-16 02:45:00 ┆ a ┆ 6.5 │
│ 2021-12-16 03:00:00 ┆ a ┆ 7.0 │
└─────────────────────┴────────┴────────┘