Lab scope: Reviewed September 4, 2026. This is a synthetic cohort-discovery exercise, not malware diagnosis. Record package versions with sessionInfo().
This lab generates 50 bounded CPU-utilization series, clusters them with constrained Dynamic Time Warping (DTW) and medoids, checks recovery against known synthetic classes, and scores distance to each assigned medoid. In production, CPU similarity is triage evidence only; corroborate it with process, network, identity, file, and alert telemetry.
Why representation and distance matter
Euclidean distance compares aligned time points. DTW allows limited temporal displacement, which can help when shapes occur slightly earlier or later. Unconstrained DTW may create implausible alignments and is computationally expensive, so choose a window from operational timing and sensitivity-test it. Scaling changes the question: per-series z-normalization emphasizes shape but removes level differences.
Reproducible synthetic lab
library(dtwclust)
library(dplyr)
library(purrr)
library(tibble)
set.seed(20260904)
n_endpoints <- 50
n_time <- 96
classes <- rep(c("baseline", "shifted", "spiky", "flat"), length.out = n_endpoints)
simulate_cpu <- function(class, n = n_time) {
t <- seq_len(n)
x <- 35 + 8 * sin(2 * pi * t / 24) + rnorm(n, 0, 2)
if (class == "shifted") x <- 35 + 8 * sin(2 * pi * (t - 4) / 24) + rnorm(n, 0, 2)
if (class == "spiky") x[sample(t, 5)] <- x[sample(t, 5)] + 35
if (class == "flat") x <- rep(25, n) + rnorm(n, 0, 1)
pmin(100, pmax(0, x))
}
series <- map(classes, simulate_cpu)
names(series) <- sprintf("endpoint_%02d", seq_len(n_endpoints))
fit <- tsclust(
series,
type = "partitional",
k = 4,
distance = "dtw_basic",
centroid = "pam",
seed = 20260904,
window.size = 8,
control = partitional_control(nrep = 10L)
)
assigned <- fit@cluster
medoids <- fit@centroids
score <- map_dbl(seq_along(series), function(i) {
dtw_basic(series[[i]], medoids[[assigned[i]]], window.size = 8)
})
results <- tibble(
endpoint = names(series),
known_class = classes,
cluster = assigned,
medoid_distance = score
)
print(table(results$known_class, results$cluster))
print(results %>% arrange(desc(medoid_distance)) %>% slice_head(n = 10))
sessionInfo()
Package interfaces can change; pin and test the exact environment. Here k = 4 uses knowledge of four generated classes to test recovery. It is not evidence that four clusters fit real telemetry. Cluster numbers can permute between runs and carry no inherent meaning.
Turn a score into a defensible alert
A high within-cluster distance means a series is atypical relative to its assigned cohort. It does not identify a cause, and a coherent cluster of compromised endpoints may not look unusual within that cluster. Estimate thresholds on a clean reference window, then evaluate later labeled periods. Report precision, recall, detection delay, alerts per analyst, subgroup or device-type effects, and missed incident severity. A sample 90th percentile mechanically flags about 10% of that sample and is not validation.
Production checklist
- Define the operational anomaly and response before modeling.
- Align sampling, missing-data, time-zone, maintenance, and device-cohort rules.
- Fit preprocessing and thresholds only on approved reference data.
- Test window size, normalization, cluster count, stability, runtime, and drift.
- Corroborate alerts, preserve evidence, require analyst review, and monitor outcomes.
Compare this approach with anomaly detection techniques, review sequence modeling in LSTM time-series forecasting, and operationalize safely with MLOps best practices.

Historical comments from Datanizant
No public comments on this article
No approved public comments were included in the WordPress export for this article.