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