← Overview
Averaging¶
averaging is the one time-based grouping method in tsam, and the simplest member of this
clustering section. It splits the ordered period list into $k$ consecutive equal-size
blocks — purely by temporal position, without looking at the values at all.
That makes it the odd one out among the clustering methods covered here: the partitional, agglomerative and extremal-prototype methods are all feature-based (they group by value similarity), whereas averaging is time-based. It still fits the same pipeline: it produces a cluster assignment, exactly like the others.
| In | a period count and a target $k$ — not the period matrix $D$ |
| Inside | cut the ordered periods into $k$ consecutive equal-size blocks; the remainder joins the last one |
| Out | one cluster label per period — the same output contract as every other method here |
The In row is the whole story of this notebook. Every other method in this section reads $D$ and decides which periods resemble each other. Averaging never opens it.
import numpy as np
import pandas as pd
# tsam's own clustering entry point — the same one the other notebooks in this
# section call, so `averaging` can be compared with them like for like.
from tsam.algorithms.clustering import assign_clusters
# The shared tiny six-day dataset from 01_preprocessing, as the period matrix D
# (see 01_partitional_clustering for the `day_p` shape legend). Averaging is the
# only method here that will not actually need it.
tiny_period_df = pd.read_csv(
"../../../data/tiny_periods.csv", header=[0, 1], index_col=0
)
tiny_period_array = tiny_period_df.values
N_PERIODS = tiny_period_array.shape[0]
print("period matrix D:", tiny_period_array.shape)
period matrix D: (6, 8)
1 Inside: consecutive equal-size blocks¶
Given $N$ periods and target $k$ clusters, the assignment is purely positional:
- Block $0$: periods $0 \ldots \lfloor N/k \rfloor - 1$
- Block $1$: periods $\lfloor N/k \rfloor \ldots 2\lfloor N/k \rfloor - 1$
- …
- Any remainder is appended to the last block.
There is no distance, no objective, and nothing to converge to — the formula below is the algorithm. The cell reproduces it by hand for $k=3$ on the six days.
k_avg = 3
block_size = N_PERIODS // k_avg
remainder = N_PERIODS - block_size * k_avg
print(f"k={k_avg}, N={N_PERIODS}, block_size={block_size}, remainder={remainder}")
avg_assignments = []
for c in range(k_avg):
avg_assignments.extend([c] * block_size)
if remainder > 0:
avg_assignments.extend([k_avg - 1] * remainder)
print("\nBy hand (purely positional — values not consulted):")
for i, a in enumerate(avg_assignments):
print(f" day_{i} -> cluster {a}")
# tsam's own clustering step, for comparison.
labels_tsam = assign_clusters(
tiny_period_array, n_clusters=k_avg, cluster_method="averaging"
)
print("\nby hand :", np.array(avg_assignments))
print("tsam :", labels_tsam)
print(
"identical? ", np.array_equal(np.array(avg_assignments), np.asarray(labels_tsam))
)
# The remainder rule, where it actually shows: k=4 over 6 periods gives blocks of
# 1, 1, 1, and then 3 — the leftovers all land in the last block.
print("\nThe remainder rule at other k:")
for k in (2, 3, 4, 5):
print(
f" k={k}: {assign_clusters(tiny_period_array, n_clusters=k, cluster_method='averaging')}"
)
k=3, N=6, block_size=2, remainder=0 By hand (purely positional — values not consulted): day_0 -> cluster 0 day_1 -> cluster 0 day_2 -> cluster 1 day_3 -> cluster 1 day_4 -> cluster 2 day_5 -> cluster 2 by hand : [0 0 1 1 2 2] tsam : [0 0 1 1 2 2] identical? True The remainder rule at other k: k=2: [0 0 0 1 1 1] k=3: [0 0 1 1 2 2] k=4: [0 1 2 3 3 3] k=5: [0 1 2 3 4 4]
2 What comes out — and the proof that nothing went in¶
The output is an assignment vector, [0 0 1 1 2 2], identical in shape and meaning to what
k-means, Ward and k-maxoids return. Downstream, nothing can tell which method produced it.
On this dataset it is also identical in content to what the feature-based methods find — but that is a coincidence of ordering, not a property of averaging. The six days happen to be laid out so that similar shapes are neighbors (sunny 0–1, overcast 2–3, cloudy 4–5), so the positional blocks land exactly on the shape-pairs. Scramble the calendar and the blocks would cut straight across those groups.
The clean way to see that averaging is genuinely blind to values is to feed it different values. Below, the same call runs on $D$, on a row-shuffled $D$, and on pure random noise of the same shape:
rng = np.random.default_rng(0)
variants = {
"the real period matrix D": tiny_period_array,
"D with its rows shuffled": tiny_period_array[rng.permutation(N_PERIODS)],
"pure random noise": rng.random(tiny_period_array.shape),
}
for name, matrix in variants.items():
labels = assign_clusters(matrix, n_clusters=3, cluster_method="averaging")
print(f" {name:26s} -> {labels}")
print(
"\nSame answer every time: the only thing averaging read was the *number* of rows."
)
the real period matrix D -> [0 0 1 1 2 2] D with its rows shuffled -> [0 0 1 1 2 2] pure random noise -> [0 0 1 1 2 2] Same answer every time: the only thing averaging read was the *number* of rows.
When averaging is the right choice. Blindness to values sounds like a pure defect, and for accuracy it is — averaging will generally reconstruct a series worse than any feature-based method at the same $k$. What it buys is structure: every cluster is a contiguous block of calendar time, of predictable size, chosen without any data-dependent decisions. That makes it useful as a cheap baseline, and as a grouping you can reason about before you have seen the data.
If what you actually want is a calendar-based grouping that averaging cannot express — week-of-year, season × weekday, or any rule of your own — you can supply the assignment vector yourself rather than have a method infer one. See Working with typical periods.
Up next:
- Representation — how each cluster (from any of these grouping methods) becomes a single profile
See also:
- Comparing clustering methods — averaging vs the feature-based methods on a realistic series