mirror of
https://github.com/wassname/pytorch-ts.git
synced 2026-07-15 11:25:33 +08:00
523 lines
17 KiB
Python
523 lines
17 KiB
Python
from typing import Tuple
|
|
|
|
import pytest
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from pts.dataset import (
|
|
ProcessStartField,
|
|
FieldName,
|
|
ListDataset,
|
|
ExpectedNumInstanceSampler,
|
|
UniformSplitSampler,
|
|
ScaleHistogram,
|
|
BucketInstanceSampler,
|
|
calculate_dataset_statistics,
|
|
)
|
|
from pts.feature import (
|
|
Chain,
|
|
AddTimeFeatures,
|
|
DayOfWeek,
|
|
DayOfMonth,
|
|
MonthOfYear,
|
|
AddAgeFeature,
|
|
VstackFeatures,
|
|
InstanceSplitter,
|
|
CanonicalInstanceSplitter,
|
|
AddObservedValuesIndicator,
|
|
)
|
|
|
|
|
|
FREQ = "1D"
|
|
|
|
TEST_VALUES = {
|
|
"is_train": [True, False],
|
|
"target": [np.zeros(0), np.random.rand(13), np.random.rand(100)],
|
|
"start": [
|
|
ProcessStartField.process("2012-01-02", freq="1D"),
|
|
ProcessStartField.process("1994-02-19 20:01:02", freq="3D"),
|
|
],
|
|
"use_prediction_features": [True, False],
|
|
"allow_target_padding": [True, False],
|
|
}
|
|
|
|
|
|
def test_align_timestamp():
|
|
def aligned_with(date_str, freq):
|
|
return str(ProcessStartField.process(date_str, freq=freq))
|
|
|
|
for _ in range(2):
|
|
assert aligned_with("2012-03-05 09:13:12", "min") == "2012-03-05 09:13:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "2min") == "2012-03-05 09:12:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "H") == "2012-03-05 09:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "D") == "2012-03-05 00:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "W") == "2012-03-11 00:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "4W") == "2012-03-11 00:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "M") == "2012-03-31 00:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "3M") == "2012-03-31 00:00:00"
|
|
assert aligned_with("2012-03-05 09:13:12", "Y") == "2012-12-31 00:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "min") == "2012-03-05 09:14:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "2min") == "2012-03-05 09:14:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "H") == "2012-03-05 09:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "D") == "2012-03-05 00:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "W") == "2012-03-11 00:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "4W") == "2012-03-11 00:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "M") == "2012-03-31 00:00:00"
|
|
assert aligned_with("2012-03-05 09:14:11", "3M") == "2012-03-31 00:00:00"
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
@pytest.mark.parametrize("target", TEST_VALUES["target"])
|
|
@pytest.mark.parametrize("start", TEST_VALUES["start"])
|
|
def test_AddTimeFeatures(start, target, is_train):
|
|
pred_length = 13
|
|
t = AddTimeFeatures(
|
|
start_field=FieldName.START,
|
|
target_field=FieldName.TARGET,
|
|
output_field="myout",
|
|
pred_length=pred_length,
|
|
time_features=[DayOfWeek(), DayOfMonth()],
|
|
)
|
|
|
|
# TODO
|
|
# assert_serializable(t)
|
|
|
|
data = {"start": start, "target": target}
|
|
res = t.map_transform(data, is_train=is_train)
|
|
mat = res["myout"]
|
|
expected_length = len(target) + (0 if is_train else pred_length)
|
|
assert mat.shape == (2, expected_length)
|
|
tmp_idx = pd.date_range(start=start, freq=start.freq, periods=expected_length)
|
|
assert np.alltrue(mat[0] == DayOfWeek()(tmp_idx))
|
|
assert np.alltrue(mat[1] == DayOfMonth()(tmp_idx))
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
@pytest.mark.parametrize("target", TEST_VALUES["target"])
|
|
@pytest.mark.parametrize("start", TEST_VALUES["start"])
|
|
def test_AddTimeFeatures_empty_time_features(start, target, is_train):
|
|
pred_length = 13
|
|
t = AddTimeFeatures(
|
|
start_field=FieldName.START,
|
|
target_field=FieldName.TARGET,
|
|
output_field="myout",
|
|
pred_length=pred_length,
|
|
time_features=[],
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
data = {"start": start, "target": target}
|
|
res = t.map_transform(data, is_train=is_train)
|
|
assert res["myout"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
@pytest.mark.parametrize("target", TEST_VALUES["target"])
|
|
@pytest.mark.parametrize("start", TEST_VALUES["start"])
|
|
def test_AddAgeFeatures(start, target, is_train):
|
|
pred_length = 13
|
|
t = AddAgeFeature(
|
|
pred_length=pred_length,
|
|
target_field=FieldName.TARGET,
|
|
output_field="age",
|
|
log_scale=True,
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
data = {"start": start, "target": target}
|
|
out = t.map_transform(data, is_train=is_train)
|
|
expected_length = len(target) + (0 if is_train else pred_length)
|
|
assert out["age"].shape[-1] == expected_length
|
|
assert np.allclose(
|
|
out["age"],
|
|
np.log10(2.0 + np.arange(expected_length)).reshape((1, expected_length)),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
@pytest.mark.parametrize("target", TEST_VALUES["target"])
|
|
@pytest.mark.parametrize("start", TEST_VALUES["start"])
|
|
def test_InstanceSplitter(start, target, is_train):
|
|
train_length = 100
|
|
pred_length = 13
|
|
t = InstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
train_sampler=UniformSplitSampler(p=1.0),
|
|
past_length=train_length,
|
|
future_length=pred_length,
|
|
time_series_fields=["some_time_feature"],
|
|
pick_incomplete=True,
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
other_feat = np.arange(len(target) + 100)
|
|
data = {
|
|
"start": start,
|
|
"target": target,
|
|
"some_time_feature": other_feat,
|
|
"some_other_col": "ABC",
|
|
}
|
|
|
|
out = list(t.flatmap_transform(data, is_train=is_train))
|
|
|
|
if is_train:
|
|
assert len(out) == max(0, len(target) - pred_length + 1)
|
|
else:
|
|
assert len(out) == 1
|
|
|
|
for o in out:
|
|
assert "target" not in o
|
|
assert "some_time_feature" not in o
|
|
assert "some_other_col" in o
|
|
|
|
assert len(o["past_some_time_feature"]) == train_length
|
|
assert len(o["past_target"]) == train_length
|
|
|
|
if is_train:
|
|
assert len(o["future_target"]) == pred_length
|
|
assert len(o["future_some_time_feature"]) == pred_length
|
|
else:
|
|
assert len(o["future_target"]) == 0
|
|
assert len(o["future_some_time_feature"]) == pred_length
|
|
|
|
# expected_length = len(target) + (0 if is_train else pred_length)
|
|
# assert len(out['age']) == expected_length
|
|
# assert np.alltrue(out['age'] == np.log10(2.0 + np.arange(expected_length)))
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
@pytest.mark.parametrize("target", TEST_VALUES["target"])
|
|
@pytest.mark.parametrize("start", TEST_VALUES["start"])
|
|
@pytest.mark.parametrize(
|
|
"use_prediction_features", TEST_VALUES["use_prediction_features"]
|
|
)
|
|
@pytest.mark.parametrize("allow_target_padding", TEST_VALUES["allow_target_padding"])
|
|
def test_CanonicalInstanceSplitter(
|
|
start, target, is_train, use_prediction_features, allow_target_padding
|
|
):
|
|
train_length = 100
|
|
pred_length = 13
|
|
t = CanonicalInstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
instance_sampler=UniformSplitSampler(p=1.0),
|
|
instance_length=train_length,
|
|
prediction_length=pred_length,
|
|
time_series_fields=["some_time_feature"],
|
|
allow_target_padding=allow_target_padding,
|
|
use_prediction_features=use_prediction_features,
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
other_feat = np.arange(len(target) + 100)
|
|
data = {
|
|
"start": start,
|
|
"target": target,
|
|
"some_time_feature": other_feat,
|
|
"some_other_col": "ABC",
|
|
}
|
|
|
|
out = list(t.flatmap_transform(data, is_train=is_train))
|
|
|
|
min_num_instances = 1 if allow_target_padding else 0
|
|
if is_train:
|
|
assert len(out) == max(min_num_instances, len(target) - train_length + 1)
|
|
else:
|
|
assert len(out) == 1
|
|
|
|
for o in out:
|
|
assert "target" not in o
|
|
assert "future_target" not in o
|
|
assert "some_time_feature" not in o
|
|
assert "some_other_col" in o
|
|
|
|
assert len(o["past_some_time_feature"]) == train_length
|
|
assert len(o["past_target"]) == train_length
|
|
|
|
if use_prediction_features and not is_train:
|
|
assert len(o["future_some_time_feature"]) == pred_length
|
|
|
|
|
|
def test_Transformation():
|
|
train_length = 100
|
|
ds = ListDataset(
|
|
[{"start": "2012-01-01", "target": [0.2] * train_length}], freq="1D"
|
|
)
|
|
|
|
pred_length = 10
|
|
|
|
t = Chain(
|
|
trans=[
|
|
AddTimeFeatures(
|
|
start_field=FieldName.START,
|
|
target_field=FieldName.TARGET,
|
|
output_field="time_feat",
|
|
time_features=[
|
|
DayOfWeek(),
|
|
DayOfMonth(),
|
|
MonthOfYear(),
|
|
],
|
|
pred_length=pred_length,
|
|
),
|
|
AddAgeFeature(
|
|
target_field=FieldName.TARGET,
|
|
output_field="age",
|
|
pred_length=pred_length,
|
|
log_scale=True,
|
|
),
|
|
AddObservedValuesIndicator(
|
|
target_field=FieldName.TARGET, output_field="observed_values"
|
|
),
|
|
VstackFeatures(
|
|
output_field="dynamic_feat",
|
|
input_fields=["age", "time_feat"],
|
|
drop_inputs=True,
|
|
),
|
|
InstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
train_sampler=ExpectedNumInstanceSampler(num_instances=4),
|
|
past_length=train_length,
|
|
future_length=pred_length,
|
|
time_series_fields=["dynamic_feat", "observed_values"],
|
|
),
|
|
]
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
for u in t(iter(ds), is_train=True):
|
|
print(u)
|
|
|
|
|
|
@pytest.mark.parametrize("is_train", TEST_VALUES["is_train"])
|
|
def test_multi_dim_transformation(is_train):
|
|
train_length = 10
|
|
|
|
first_dim = np.arange(1, 11, 1).tolist()
|
|
first_dim[-1] = "NaN"
|
|
|
|
second_dim = np.arange(11, 21, 1).tolist()
|
|
second_dim[0] = "NaN"
|
|
|
|
ds = ListDataset(
|
|
data_iter=[{"start": "2012-01-01", "target": [first_dim, second_dim]}],
|
|
freq="1D",
|
|
one_dim_target=False,
|
|
)
|
|
pred_length = 2
|
|
|
|
# Looks weird - but this is necessary to assert the nan entries correctly.
|
|
first_dim[-1] = np.nan
|
|
second_dim[0] = np.nan
|
|
|
|
t = Chain(
|
|
trans=[
|
|
AddTimeFeatures(
|
|
start_field=FieldName.START,
|
|
target_field=FieldName.TARGET,
|
|
output_field="time_feat",
|
|
time_features=[
|
|
DayOfWeek(),
|
|
DayOfMonth(),
|
|
MonthOfYear(),
|
|
],
|
|
pred_length=pred_length,
|
|
),
|
|
AddAgeFeature(
|
|
target_field=FieldName.TARGET,
|
|
output_field="age",
|
|
pred_length=pred_length,
|
|
log_scale=True,
|
|
),
|
|
AddObservedValuesIndicator(
|
|
target_field=FieldName.TARGET,
|
|
output_field="observed_values",
|
|
convert_nans=False,
|
|
),
|
|
VstackFeatures(
|
|
output_field="dynamic_feat",
|
|
input_fields=["age", "time_feat"],
|
|
drop_inputs=True,
|
|
),
|
|
InstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
train_sampler=ExpectedNumInstanceSampler(num_instances=4),
|
|
past_length=train_length,
|
|
future_length=pred_length,
|
|
time_series_fields=["dynamic_feat", "observed_values"],
|
|
batch_first=False,
|
|
),
|
|
]
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
if is_train:
|
|
for u in t(iter(ds), is_train=True):
|
|
assert_shape(u["past_target"], (2, 10))
|
|
assert_shape(u["past_dynamic_feat"], (4, 10))
|
|
assert_shape(u["past_observed_values"], (2, 10))
|
|
assert_shape(u["future_target"], (2, 2))
|
|
|
|
assert_padded_array(
|
|
u["past_observed_values"],
|
|
np.array([[1.0] * 9 + [0.0], [0.0] + [1.0] * 9]),
|
|
u["past_is_pad"],
|
|
)
|
|
assert_padded_array(
|
|
u["past_target"], np.array([first_dim, second_dim]), u["past_is_pad"],
|
|
)
|
|
else:
|
|
for u in t(iter(ds), is_train=False):
|
|
assert_shape(u["past_target"], (2, 10))
|
|
assert_shape(u["past_dynamic_feat"], (4, 10))
|
|
assert_shape(u["past_observed_values"], (2, 10))
|
|
assert_shape(u["future_target"], (2, 0))
|
|
|
|
assert_padded_array(
|
|
u["past_observed_values"],
|
|
np.array([[1.0] * 9 + [0.0], [0.0] + [1.0] * 9]),
|
|
u["past_is_pad"],
|
|
)
|
|
assert_padded_array(
|
|
u["past_target"], np.array([first_dim, second_dim]), u["past_is_pad"],
|
|
)
|
|
|
|
|
|
def test_ExpectedNumInstanceSampler():
|
|
N = 6
|
|
train_length = 2
|
|
pred_length = 1
|
|
ds = make_dataset(N, train_length)
|
|
|
|
t = Chain(
|
|
trans=[
|
|
InstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
train_sampler=ExpectedNumInstanceSampler(num_instances=4),
|
|
past_length=train_length,
|
|
future_length=pred_length,
|
|
pick_incomplete=True,
|
|
)
|
|
]
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
scale_hist = ScaleHistogram()
|
|
|
|
repetition = 2
|
|
for i in range(repetition):
|
|
for data in t(iter(ds), is_train=True):
|
|
target_values = data["past_target"]
|
|
# for simplicity, discard values that are zeros to avoid confusion with padding
|
|
target_values = target_values[target_values > 0]
|
|
scale_hist.add(target_values)
|
|
|
|
expected_values = {i: 2 ** i * repetition for i in range(1, N)}
|
|
|
|
assert expected_values == scale_hist.bin_counts
|
|
|
|
|
|
def test_BucketInstanceSampler():
|
|
N = 6
|
|
train_length = 2
|
|
pred_length = 1
|
|
ds = make_dataset(N, train_length)
|
|
|
|
dataset_stats = calculate_dataset_statistics(ds)
|
|
|
|
t = Chain(
|
|
trans=[
|
|
InstanceSplitter(
|
|
target_field=FieldName.TARGET,
|
|
is_pad_field=FieldName.IS_PAD,
|
|
start_field=FieldName.START,
|
|
forecast_start_field=FieldName.FORECAST_START,
|
|
train_sampler=BucketInstanceSampler(dataset_stats.scale_histogram),
|
|
past_length=train_length,
|
|
future_length=pred_length,
|
|
pick_incomplete=True,
|
|
)
|
|
]
|
|
)
|
|
|
|
# assert_serializable(t)
|
|
|
|
scale_hist = ScaleHistogram()
|
|
|
|
repetition = 200
|
|
for i in range(repetition):
|
|
for data in t(iter(ds), is_train=True):
|
|
target_values = data["past_target"]
|
|
# for simplicity, discard values that are zeros to avoid confusion with padding
|
|
target_values = target_values[target_values > 0]
|
|
scale_hist.add(target_values)
|
|
|
|
expected_values = {i: repetition for i in range(1, N)}
|
|
found_values = scale_hist.bin_counts
|
|
|
|
for i in range(1, N):
|
|
assert abs(expected_values[i] - found_values[i] < expected_values[i] * 0.3)
|
|
|
|
|
|
def make_dataset(N, train_length):
|
|
# generates 2 ** N - 1 timeseries with constant increasing values
|
|
n = 2 ** N - 1
|
|
targets = np.ones((n, train_length))
|
|
for i in range(0, n):
|
|
targets[i, :] = targets[i, :] * i
|
|
|
|
ds = ListDataset(
|
|
data_iter=[{"start": "2012-01-01", "target": targets[i, :]} for i in range(n)],
|
|
freq="1D",
|
|
)
|
|
|
|
return ds
|
|
|
|
|
|
def assert_shape(array: np.array, reference_shape: Tuple[int, int]):
|
|
assert (
|
|
array.shape == reference_shape
|
|
), f"Shape should be {reference_shape} but found {array.shape}."
|
|
|
|
def assert_padded_array(
|
|
sampled_array: np.array, reference_array: np.array, padding_array: np.array
|
|
):
|
|
num_padded = int(np.sum(padding_array))
|
|
sampled_no_padding = sampled_array[:, num_padded:]
|
|
|
|
reference_array = np.roll(reference_array, num_padded, axis=1)
|
|
reference_no_padding = reference_array[:, num_padded:]
|
|
|
|
# Convert nans to dummy value for assertion because
|
|
# np.nan == np.nan -> False.
|
|
reference_no_padding[np.isnan(reference_no_padding)] = 9999.0
|
|
sampled_no_padding[np.isnan(sampled_no_padding)] = 9999.0
|
|
|
|
reference_no_padding = np.array(reference_no_padding, dtype=np.float32)
|
|
|
|
assert (sampled_no_padding == reference_no_padding).all(), (
|
|
f"Sampled and reference arrays do not match. '"
|
|
f"Got {sampled_no_padding} but should be {reference_no_padding}."
|
|
)
|