Hyperparameter Tuning#
This notebook demonstrates how to automatically find optimal aggregation parameters using find_optimal_combination.
Instead of manually choosing the number of periods and segments, you can specify a target data reduction and let tsam find the best combination.
[1]:
%load_ext autoreload
%autoreload 2
import pandas as pd
import plotly.express as px
import plotly.io as pio
import tsam
from tsam.tuning import find_optimal_combination
pio.renderers.default = "notebook"
Load test data#
[2]:
raw = pd.read_csv("testdata.csv", index_col=0, parse_dates=True)
print(f"Shape: {raw.shape}")
print(f"Timesteps: {len(raw)}")
raw.head()
Shape: (8760, 4)
Timesteps: 8760
[2]:
| GHI | T | Wind | Load | |
|---|---|---|---|---|
| 2009-12-31 23:30:00 | 0 | -2.1 | 7.1 | 375.478394 |
| 2010-01-01 00:30:00 | 0 | -2.8 | 8.6 | 364.541326 |
| 2010-01-01 01:30:00 | 0 | -3.3 | 9.7 | 357.416844 |
| 2010-01-01 02:30:00 | 0 | -3.2 | 9.8 | 350.191306 |
| 2010-01-01 03:30:00 | 0 | -3.2 | 9.4 | 345.161449 |
Find optimal combination for 2% data reduction#
The find_optimal_combination function searches for the best period/segment combination that achieves the target data reduction while minimizing RMSE.
Use n_jobs=-1 to utilize all available CPUs for faster search.
[3]:
result = find_optimal_combination(
raw,
data_reduction=0.02, # Target: 2% of original timesteps
period_duration=24,
n_jobs=-1, # Use all CPUs
show_progress=True,
)
print("\nOptimal configuration:")
print(f" Periods: {result.n_clusters}")
print(f" Segments: {result.n_segments}")
print(f" RMSE: {result.rmse:.4f}")
print(f" Timesteps: {result.n_clusters * result.n_segments}")
print(f" Reduction: {result.n_clusters * result.n_segments / len(raw) * 100:.2f}%")
Searching configurations (2 workers): 100%|██████████| 19/19 [00:03<00:00, 4.89it/s]
Optimal configuration:
Periods: 35
Segments: 5
RMSE: 0.0899
Timesteps: 175
Reduction: 2.00%
View the search history#
The tuning result includes the history of all tested configurations.
[4]:
history_df = pd.DataFrame(result.history)
history_df.sort_values("rmse")
[4]:
| n_clusters | n_segments | rmse | |
|---|---|---|---|
| 4 | 35 | 5 | 0.089913 |
| 2 | 58 | 3 | 0.090464 |
| 3 | 43 | 4 | 0.090869 |
| 5 | 29 | 6 | 0.092497 |
| 6 | 25 | 7 | 0.094422 |
| 7 | 21 | 8 | 0.096284 |
| 8 | 19 | 9 | 0.097095 |
| 9 | 17 | 10 | 0.099079 |
| 11 | 14 | 12 | 0.100057 |
| 10 | 15 | 11 | 0.100222 |
| 12 | 13 | 13 | 0.101080 |
| 13 | 12 | 14 | 0.101607 |
| 14 | 11 | 15 | 0.103531 |
| 15 | 10 | 17 | 0.104613 |
| 16 | 9 | 19 | 0.106700 |
| 17 | 8 | 21 | 0.109790 |
| 18 | 7 | 24 | 0.111982 |
| 1 | 87 | 2 | 0.113877 |
| 0 | 175 | 1 | 0.133896 |
Use the optimal result#
The best_result attribute contains the full AggregationResult for the optimal configuration.
[5]:
best = result.best_result
print(f"Typical periods shape: {best.cluster_representatives.shape}")
print("\nAccuracy metrics:")
print(best.accuracy)
Typical periods shape: (175, 4)
Accuracy metrics:
AccuracyMetrics(
rmse=0.0883 (mean),
mae=0.0623 (mean),
rmse_duration=0.0211 (mean)
)
[6]:
reconstructed = best.reconstructed
unstacked = tsam.unstack_to_periods(reconstructed, period_duration=24)
px.imshow(
unstacked["T"].values.T,
labels={"x": "Day", "y": "Hour", "color": "Temperature"},
title=f"Optimal: {result.n_clusters} periods x {result.n_segments} segments",
aspect="auto",
)
Compare different reduction targets#
Let’s see how the optimal configuration changes with different data reduction targets.
[7]:
reductions = [0.01, 0.02, 0.05, 0.10]
results_comparison = {}
for reduction in reductions:
r = find_optimal_combination(
raw,
data_reduction=reduction,
period_duration=24,
n_jobs=-1,
show_progress=False,
)
results_comparison[f"{int(reduction * 100)}%"] = r
print(
f"{int(reduction * 100)}% reduction: {r.n_clusters} periods x {r.n_segments} segments, RMSE={r.rmse:.4f}"
)
1% reduction: 29 periods x 3 segments, RMSE=0.0989
2% reduction: 35 periods x 5 segments, RMSE=0.0899
5% reduction: 87 periods x 5 segments, RMSE=0.0768
10% reduction: 175 periods x 5 segments, RMSE=0.0637
[8]:
comparison_data = {"Original": raw}
for label, r in results_comparison.items():
comparison_data[label] = r.best_result.reconstructed
# Duration curve comparison using plotly express
frames = []
for name, df in comparison_data.items():
sorted_vals = df["Load"].sort_values(ascending=False).reset_index(drop=True)
frames.append(
pd.DataFrame(
{"Hour": range(len(sorted_vals)), "Load": sorted_vals, "Method": name}
)
)
long_df = pd.concat(frames, ignore_index=True)
px.line(
long_df,
x="Hour",
y="Load",
color="Method",
title="Duration Curve Comparison - Different Reduction Targets",
)
Helper functions#
Use find_clusters_for_reduction and find_segments_for_reduction to calculate parameters for a specific reduction target.
[9]:
from tsam.tuning import find_clusters_for_reduction, find_segments_for_reduction
n_timesteps = len(raw)
target_reduction = 0.01 # 1% of original
# How many periods can we have with 24 segments?
max_periods = find_clusters_for_reduction(
n_timesteps, n_segments=24, data_reduction=target_reduction
)
print(f"With 24 segments: max {max_periods} periods for 1% reduction")
# How many segments can we have with 8 periods?
max_segments = find_segments_for_reduction(
n_timesteps, n_clusters=8, data_reduction=target_reduction
)
print(f"With 8 periods: max {max_segments} segments for 1% reduction")
With 24 segments: max 3 periods for 1% reduction
With 8 periods: max 10 segments for 1% reduction