Time Series - Collections#
This tutorial builds a minimal TimeSeriesCollection for one (synthetic) catchment with five sensors that deliberately differ along every axis TimeSeriesCollection has to manage:
Series |
Frequency |
Range |
Gaps |
|---|---|---|---|
|
daily |
full year |
1 small (3d) + 1 large (20d) |
|
daily |
Mar–Nov (shorter) |
1 small (2d) |
|
daily |
Mar–Nov (shorter) |
1 small (2d) |
|
hourly |
Jan–Jun (shorter, different unit of time) |
1 small (5h) + 1 large (72h) |
|
daily |
full year |
2 separate medium gaps (10d, 15d) |
Covered here:
Building the five series with
.generate(), from the default parameters of each child classLoading them into one
TimeSeriesCollectionstandardize()and what it does to each seriesCross-series overlap via
get_epochs()Per-series gap/epoch structure via
merge_local_epochs()
Notebook setup#
For users running this tutorial as a Jupyter Notebook, this cell must be executed first:
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Install `plans` in `google.colab`.
# Use `pip install plans` for other environments.
if "google.colab" in sys.modules:
import os
os.system(f"{sys.executable} -m pip install -q plans")
from plans.datasets.core import TimeSeries, TimeSeriesCollection
# This avoids warnings related to uninstalled fonts
import logging
logging.getLogger('matplotlib.font_manager').setLevel(logging.ERROR)
OUTPUT_DIR = Path("outputs/time-series-collection")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(f"Outputs will be saved to: ./{OUTPUT_DIR}")
RNG_SEED = 1
np.random.seed(RNG_SEED)
Outputs will be saved to: ./outputs/time-series-collection
Build synthetic series#
from plans.datasets import DischargeSeries, ETSeries, RainSeries, TemperatureSeries
df_p = RainSeries(name="Rain").generate(start="2019-01-01", end="2019-12-31", freq="D", seed=1)
df_p.loc[20:22, "ppt"] = np.nan # small gap (3d)
df_p.loc[150:169, "ppt"] = np.nan # large gap (20d) -> a real Epoch 0 gap
df_q = DischargeSeries(name="Flow").generate(start="2019-03-01", end="2019-11-30", freq="D", seed=2)
df_q.loc[40:41, "Q"] = np.nan # small gap (2d)
df_q2 = DischargeSeries(name="Flow2").generate(start="2019-03-20", end="2019-11-30", freq="D", seed=3)
df_q2.loc[46:50, "Q"] = np.nan # small gap (2d)
df_t = TemperatureSeries(name="AirTemp").generate(start="2019-01-01", end="2019-06-30", freq="h", seed=4)
df_t.loc[100:104, "tas"] = np.nan # small gap (5h)
df_t.loc[2000:2071, "tas"] = np.nan # large gap (72h)
df_et = ETSeries(name="ET").generate(start="2019-01-01", end="2019-12-31", freq="D", seed=5)
df_et.loc[60:69, "et"] = np.nan # medium gap (10d)
df_et.loc[300:314, "et"] = np.nan # medium gap (15d), separate from the first
for df, fname in [(df_p, "rain.csv"), (df_q, "flow.csv"), (df_q2, "flow2.csv"), (df_t, "temp.csv"), (df_et, "et.csv")]:
df.to_csv(OUTPUT_DIR / fname, index=False, sep=";")
print("5 series written to", OUTPUT_DIR)
5 series written to outputs/time-series-collection
Load into a Time Series Collection#
TimeSeriesCollection reads one info table describing every series (name, alias, file, variable/datetime columns, units, coordinates):
info = pd.DataFrame({
"Name": ["Rain", "Flow", "Flow2", "AirTemp", "ET"],
"Alias": ["P1", "Q1", "Q2", "T1", "ET1"],
"File": [str(OUTPUT_DIR / f) for f in ["rain.csv", "flow.csv", "flow2.csv", "temp.csv", "et.csv"]],
"VarField": ["ppt", "Q", "Q", "tas", "et"],
"DtField": ["datetime"] * 5,
"Units": ["mm", "m3/s", "m3/s", "C", "mm"],
"X": [0, 0, 0, 0, 0],
"Y": [0, 0, 0, 0, 0],
"Code": ["P001", "Q001", "Q002", "T001", "ET001"],
"Source": ["synthetic"] * 5,
"Description": [
"daily rainfall",
"daily streamflow", "daily streamflow",
"hourly air temperature",
"daily evapotranspiration"
],
"Color": ["tab:blue", "tab:purple", "tab:purple", "tab:red", "tab:orange"],
})
info_file = OUTPUT_DIR / "info.csv"
info.to_csv(info_file, index=False, sep=";")
tsc = TimeSeriesCollection(name="MultiSensorCatchment")
tsc.load_data(table_file=str(info_file))
tsc.catalog
| name | alias | size | color | source | description | units | code | x | y | ... | datetime_res | start | end | is_standard | gap_size | n_epoch | n_gaps_small | file_data_datetime_field | file_data_variable_field | file_data | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | AirTemp | T1 | 4321 | tab:red | synthetic | hourly air temperature | C | T001 | 0 | 0 | ... | hour | 2019-01-01 | 2019-06-30 | False | 6 | None | None | datetime | tas | /home/runner/work/plans/plans/docs/tutorials/o... |
| 1 | ET | ET1 | 365 | tab:orange | synthetic | daily evapotranspiration | mm | ET001 | 0 | 0 | ... | day | 2019-01-01 | 2019-12-31 | False | 6 | None | None | datetime | et | /home/runner/work/plans/plans/docs/tutorials/o... |
| 2 | Flow | Q1 | 275 | tab:purple | synthetic | daily streamflow | m3/s | Q001 | 0 | 0 | ... | day | 2019-03-01 | 2019-11-30 | False | 6 | None | None | datetime | Q | /home/runner/work/plans/plans/docs/tutorials/o... |
| 3 | Flow2 | Q2 | 256 | tab:purple | synthetic | daily streamflow | m3/s | Q002 | 0 | 0 | ... | day | 2019-03-20 | 2019-11-30 | False | 6 | None | None | datetime | Q | /home/runner/work/plans/plans/docs/tutorials/o... |
| 4 | Rain | P1 | 365 | tab:blue | synthetic | daily rainfall | mm | P001 | 0 | 0 | ... | day | 2019-01-01 | 2019-12-31 | False | 6 | None | None | datetime | ppt | /home/runner/work/plans/plans/docs/tutorials/o... |
5 rows × 29 columns
Raw series at a glance#
Visualize individual TimeSeries accessing by the name
tsc.collection["Flow"].view()
tsc.collection["Flow2"].view()
tsc.collection["Rain"].view()
tsc.collection["AirTemp"].view()
tsc.collection["ET"].view()
Learn about the epochs#
Visualize epochs per Time Series
tsc.collection["ET"].view_specs["layout"] = "epochs"
tsc.collection["ET"].view()
tsc.collection["Rain"].view_specs["layout"] = "epochs"
tsc.collection["Rain"].view()
Learn collection-wise epochs
df_local_epochs = tsc.merge_local_epochs()
df_local_epochs
| name | id_epoch | n_epoch | n_gaps_small | start | end | color | |
|---|---|---|---|---|---|---|---|
| 0 | Rain | 1 | 150 | 3 | 2019-01-01 00:00:00 | 2019-05-30 00:00:00 | #1b9e77 |
| 1 | Rain | 2 | 195 | 0 | 2019-06-20 00:00:00 | 2019-12-31 00:00:00 | #666666 |
| 2 | Flow | 1 | 275 | 2 | 2019-03-01 00:00:00 | 2019-11-30 00:00:00 | #1b9e77 |
| 3 | Flow2 | 1 | 256 | 5 | 2019-03-20 00:00:00 | 2019-11-30 00:00:00 | #1b9e77 |
| 4 | AirTemp | 1 | 2000 | 5 | 2019-01-01 00:00:00 | 2019-03-25 07:00:00 | #1b9e77 |
| 5 | AirTemp | 2 | 2249 | 0 | 2019-03-28 08:00:00 | 2019-06-30 00:00:00 | #666666 |
| 6 | ET | 1 | 60 | 0 | 2019-01-01 00:00:00 | 2019-03-01 00:00:00 | #1b9e77 |
| 7 | ET | 2 | 230 | 0 | 2019-03-12 00:00:00 | 2019-10-27 00:00:00 | #66a61e |
| 8 | ET | 3 | 50 | 0 | 2019-11-12 00:00:00 | 2019-12-31 00:00:00 | #666666 |
Visualize collection-wise epochs
tsc.view()
Get merged data epochs
tsc.get_epochs()
| datetime | v_T1 | v_ET1 | v_Q1 | v_Q2 | v_P1 | Overlapping | id_epoch | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2019-01-01 00:00:00 | 26.870612 | 4.062957 | NaN | NaN | 10.285795 | 0.6 | 1 |
| 1 | 2019-01-01 01:00:00 | 26.841576 | NaN | NaN | NaN | NaN | 0.2 | 1 |
| 2 | 2019-01-01 02:00:00 | 27.269388 | NaN | NaN | NaN | NaN | 0.2 | 1 |
| 3 | 2019-01-01 03:00:00 | 27.441897 | NaN | NaN | NaN | NaN | 0.2 | 1 |
| 4 | 2019-01-01 04:00:00 | 27.040871 | NaN | NaN | NaN | NaN | 0.2 | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 4500 | 2019-12-27 00:00:00 | NaN | 4.598460 | NaN | NaN | 10.239898 | 0.4 | 5 |
| 4501 | 2019-12-28 00:00:00 | NaN | 4.682095 | NaN | NaN | 11.556343 | 0.4 | 5 |
| 4502 | 2019-12-29 00:00:00 | NaN | 4.329283 | NaN | NaN | 8.844813 | 0.4 | 5 |
| 4503 | 2019-12-30 00:00:00 | NaN | 4.136005 | NaN | NaN | 9.161709 | 0.4 | 5 |
| 4504 | 2019-12-31 00:00:00 | NaN | 4.438775 | NaN | NaN | 9.484644 | 0.4 | 5 |
4505 rows × 8 columns
Standardize collection#
Regularizes each series onto its own regular time step. Gaps stay as they are, since filling them is a separate decision (interpolate_gaps):
tsc.standardize()
for name in tsc.collection:
d = tsc.collection[name]
print(f"{name:>10}: n={len(d.data):5d} nan={d.data['v'].isna().sum():5d} "
f"freq={d.dtfreq:>3} start={d.start.date()} end={d.end.date()}")
Rain: n= 365 nan= 23 freq= D start=2019-01-01 end=2019-12-31
Flow: n= 275 nan= 2 freq= D start=2019-03-01 end=2019-11-30
Flow2: n= 256 nan= 5 freq= D start=2019-03-20 end=2019-11-30
AirTemp: n= 4344 nan= 100 freq= h start=2019-01-01 end=2019-06-30
ET: n= 365 nan= 25 freq= D start=2019-01-01 end=2019-12-31
Merge data#
The .merge_data() is a helper method that makes an outer join for the available time series, returning a dataframe with all the data.
df_merged = tsc.merge_data()
df_merged
| datetime | v_T1 | v_ET1 | v_Q1 | v_Q2 | v_P1 | |
|---|---|---|---|---|---|---|
| 0 | 2019-01-01 00:00:00 | 26.870612 | 4.062957 | NaN | NaN | 10.285795 |
| 1 | 2019-01-01 01:00:00 | 26.841576 | NaN | NaN | NaN | NaN |
| 2 | 2019-01-01 02:00:00 | 27.269388 | NaN | NaN | NaN | NaN |
| 3 | 2019-01-01 03:00:00 | 27.441897 | NaN | NaN | NaN | NaN |
| 4 | 2019-01-01 04:00:00 | 27.040871 | NaN | NaN | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 4523 | 2019-12-27 00:00:00 | NaN | 4.598460 | NaN | NaN | 10.239898 |
| 4524 | 2019-12-28 00:00:00 | NaN | 4.682095 | NaN | NaN | 11.556343 |
| 4525 | 2019-12-29 00:00:00 | NaN | 4.329283 | NaN | NaN | 8.844813 |
| 4526 | 2019-12-30 00:00:00 | NaN | 4.136005 | NaN | NaN | 9.161709 |
| 4527 | 2019-12-31 00:00:00 | NaN | 4.438775 | NaN | NaN | 9.484644 |
4528 rows × 6 columns
Recap#
Built 5 synthetic series with independently varied range, gap structure, and time step.
TimeSeriesCollection.load_data()reads them all from one info table into a shared catalog.get_epochs()/merge_local_epochs()give the cross-series and per-series gap/epoch pictures respectively.