TNA Clustering: Discovering and Analysis of Clusters

Companion Tutorial for Transition Network Analysis

tutorial
R
Authors
Affiliation

University of Eastern Finland

University of Eastern Finland

Published

February 6, 2026

1 Introduction

This tutorial covers data-driven clustering of temporal sequences using the tna package. It is a companion to the main TNA tutorial and the group comparison tutorial. We assume familiarity with the basics of building and analyzing a TNA model.

When a meaningful grouping variable is available (e.g., achievement level, experimental condition), the group tutorial shows how to split data by that variable and compare group-specific models. But in many research settings, no such variable exists — or the researcher suspects that the true structure of behavioral differences does not align with any available categorical variable. In these cases, data-driven clustering discovers naturally occurring subgroups directly from the sequence data, without imposing predefined categories.

This tutorial demonstrates how to:

  1. Cluster sequences based on structural dissimilarity.
  2. Build group-specific TNA models from the discovered clusters.
  3. Compare and visualize cluster-specific networks.
  4. Validate cluster differences with permutation testing.
  5. Assess within-cluster edge reliability with bootstrapping.
  6. Choose an appropriate number of clusters.

1.1 Installation

The tna package is the only package required. It provides all the functions needed for data preparation, model building, visualization, clustering, permutation testing, and bootstrapping.

Install from CRAN:

install.packages("tna")

Or install the development version from GitHub:

# install.packages("remotes")
remotes::install_github("sonsoleslp/tna")

1.2 Data Preparation

We use the built-in group_regulation_long dataset, which contains coded collaborative regulation behaviors from student groups. The prepare_data() function converts this long-format event log into the structure required for TNA.

# Load the built-in collaborative regulation dataset
data("group_regulation_long")

# Convert long-format event log into TNA data
prepared_data <- prepare_data(
  group_regulation_long,
  action = "Action",   # behavioral states (network nodes)
  actor = "Actor",     # participant IDs (one sequence per actor)
  time = "Time"        # timestamps (for ordering and session splitting)
)

# Build the aggregate TNA model (all sequences combined)
model <- tna(prepared_data)
Argument Description
action (Required) Column containing the events or states to model. These become the network nodes.
actor Column identifying who performed the action. Creates one sequence per actor.
time Column with timestamps. Sorts events and splits sequences at temporal gaps (default: 15 minutes).
order Numeric column for event ordering when timestamps are unavailable.
time_threshold Gap duration in seconds that starts a new session (default: 900).

Any columns not specified as action, actor, time, or order are automatically preserved as metadata.

This tutorial uses long-format event data, but tna() accepts several other input formats:

Input Format Function Description
Long event log prepare_data() then tna() Timestamped events with actors
Wide data frame tna(df) Rows = sequences, columns = time points
Pre-computed matrix tna(mat) Square weight matrix with named rows and columns
TraMineR sequence tna(seqobj) Object from TraMineR::seqdef()
One-hot binary data import_onehot() Co-occurrence model from binary feature data

2 Why Cluster?

A single TNA model computed from all sequences describes the average transition dynamics across all individuals. But averages can mask substantial heterogeneity. If one subgroup of learners follows a strategic regulatory cycle (plan → monitor → adapt) while another subgroup is stuck in social loops (discuss → consensus → discuss), the aggregate model shows a blend of both patterns that accurately represents neither.

Clustering addresses this by partitioning sequences into subgroups with distinct transition structures, allowing each cluster to be modeled separately. The result is a set of group-specific TNA models — one per cluster — that capture the actual behavioral patterns present in the data rather than an uninformative average.

This is exploratory analysis: we let the data reveal structure rather than imposing predefined categories. The discovered clusters may correspond to meaningful behavioral profiles — different learning strategies, different levels of engagement, different regulatory styles — that would be invisible in an aggregate analysis.

3 Running cluster_sequences()

The cluster_sequences() function takes a sequence data frame, computes pairwise dissimilarities between sequences, and partitions them into k groups:

# Partition sequences into 2 clusters based on pairwise dissimilarity
clustering <- cluster_sequences(prepared_data$sequence_data, k = 2)
print(clustering)
Clustering method: pam 
Number of clusters: 2 
Silhouette score: 0 
Cluster sizes:
   1    2 
1010  990 

The procedure has two steps:

  1. Compute pairwise dissimilarity between all sequences using a string distance metric. Each pair of sequences receives a dissimilarity score reflecting how structurally different they are.
  2. Cluster the dissimilarity matrix using the chosen algorithm and assign each sequence to one of k groups.
Argument Description Default
data Sequence data frame (e.g., prepared_data$sequence_data) —
k Number of clusters —
dissimilarity Distance metric: "hamming", "lcs", "cosine", "jaccard", "osa", etc. "hamming"
method Clustering algorithm: "pam", "ward.D2", "complete", "average", etc. "pam"

Dissimilarity metrics:

  • "hamming" — counts the number of positions where two sequences differ. Fast and interpretable; requires equal-length sequences.
  • "lcs" — longest common subsequence. Handles variable-length sequences and is sensitive to ordering.
  • "osa" — optimal string alignment. Allows insertions, deletions, and substitutions; flexible but slower.
  • "cosine" / "jaccard" — set-based metrics that compare state compositions regardless of order.

Clustering algorithms:

  • "pam" — Partitioning Around Medoids. Robust to outliers; identifies a representative sequence (medoid) for each cluster.
  • "ward.D2" — Ward’s hierarchical clustering. Minimizes within-cluster variance; produces compact clusters.
  • "complete" / "average" — other hierarchical linkage methods for different cluster shape assumptions.

4 Building Group TNA from Clusters

The clustering result can be passed directly to group_tna(), which builds a separate TNA model for each cluster:

Once clusters are identified, we pass the clustering result directly to group_tna(), which builds a separate TNA model for each cluster. We also rename the clusters to give them more descriptive labels.

# Build one TNA model per cluster and assign descriptive names
gtna_clust <- group_tna(clustering)
gtna_clust <- rename_groups(gtna_clust, c("Pattern A", "Pattern B"))

Plotting the cluster-specific models side by side reveals how the discovered subgroups differ in their transition structures:

plot(gtna_clust, cut = 0.1, minimum = 0.05)
Figure 1: Transition networks by discovered cluster
Figure 2: Transition networks by discovered cluster

The summary provides network-level statistics for each cluster, including density, reciprocity, and the number of sequences assigned to each group:

summary(gtna_clust)

5 Comparing Clusters

5.1 Difference Network

The difference plot highlights where the two clusters diverge. Edges are colored by direction: one color for transitions stronger in Pattern A, another for transitions stronger in Pattern B.

# Edges colored by which cluster has a stronger transition
plot_compare(gtna_clust$`Pattern A`, gtna_clust$`Pattern B`, minimum = 0.01, cut = 0.1)
Figure 3: Difference network between clusters

5.2 Community Structure

Community detection identifies groups of states that are more densely connected to each other than to the rest of the network. Comparing community structures across clusters reveals whether the modular organization of the process differs between subgroups:

# Detect community structure within each cluster's network
# igraph may override the `communities` function. That is why we use the prefix `tna::`
comm_clust <- tna::communities(gtna_clust) 
plot(comm_clust, cut = 0.1, minimum = 0.01)

Communities by cluster

Communities by cluster

5.3 Centralities

Centrality measures summarize the structural importance of each state within each cluster. Comparing centrality profiles across clusters identifies states that play different roles in different subgroups:

# Compute and compare centrality measures across clusters
cent_clust <- centralities(gtna_clust)
plot(cent_clust)
Figure 4: Centralities by cluster

6 Permutation Test for Clusters

6.1 Why Permutation Testing Is Essential for Clusters

Permutation testing is particularly important for data-driven clusters. Unlike known groups (where group membership is defined independently of the transition data), clustering algorithms partition sequences to maximize between-group separation. Some degree of apparent difference is therefore guaranteed by construction. The permutation test provides the necessary corrective: by randomly reassigning sequences to groups while preserving internal sequential structure, it constructs the null distribution of differences expected under random partitioning. Only differences that exceed this null distribution constitute evidence of genuine structural divergence rather than algorithmic artifacts.

The permutation approach preserves the temporal dependency structure that parametric tests on summary statistics necessarily violate, and provides a principled basis for distinguishing substantive cluster differences from stochastic variation (van Borkulo et al., 2023).

set.seed(265)  # for reproducibility
# Test whether cluster differences exceed what random partitioning would produce
perm_clust <- permutation_test(
  gtna_clust$`Pattern A`, gtna_clust$`Pattern B`,
  iter = 1000,  # number of random reassignments
  measures = c("InStrength", "OutStrength", "Betweenness")
)

6.2 Results

Filtering for p < 0.05 identifies the transitions for which the observed cluster difference exceeds what would be expected under the null hypothesis:

# Filter for statistically significant edges and sort by effect size
sig_clust <- perm_clust$edges$stats[perm_clust$edges$stats$p_value < 0.05, ]
sig_clust <- sig_clust[order(-abs(sig_clust$effect_size)), ]
sig_clust
Column Meaning Interpretation
edge_name The transition (from → to) Which behavioral transition is being tested
diff_true Observed difference (Pattern A minus Pattern B) Direction and magnitude of the cluster difference
effect_size Difference / SD of permuted differences Standardized effect size (analogous to Cohen’s d). Values > 0.5 moderate; > 0.8 large
p_value Proportion of permutations as extreme as observed Statistical significance. Values < 0.05 are conventionally significant

6.3 Visualization

The permutation plot displays only the edges that reached statistical significance:

# Only edges with p < 0.05 are displayed
plot(perm_clust, minimum = 0.01, cut = 0.1)
Figure 5: Significant differences between clusters

7 Bootstrap Validation of Clusters

7.1 Why Bootstrap Cluster Models?

The same replicability concern that applies to any TNA model applies with particular force to cluster-specific models. Because cluster assignments are derived from the data itself, cluster-specific transition estimates are susceptible to overfitting: edges that appear in the point-estimate model for a given cluster may not survive resampling.

Bootstrapping addresses this directly. By resampling sequences with replacement and reconstructing the transition matrix across a large number of iterations, the procedure generates empirical sampling distributions for each edge weight, thereby distinguishing transitions that are stable properties of the discovered pattern from those whose presence is contingent on the particular sample at hand (Saqr, Lopez-Pernas, & Tikka, 2025). Edges that do not consistently exceed a defined threshold are identified as non-significant, yielding a model in which every retained transition has demonstrated resampling support.

NotePermutation and Bootstrap: Complementary Confirmatory Roles
Method Inferential Target What It Confirms
Permutation test Between-cluster differences Whether observed differences exceed what would be expected under random cluster assignment
Bootstrap Within-cluster edge stability Whether individual edges within each cluster model are replicable or sample-contingent

Neither test subsumes the other. Together, they provide a complete inferential framework: the bootstrap identifies which transitions reliably exist within each cluster, and the permutation test identifies which transitions reliably differ between clusters.

7.2 Running the Bootstrap

We apply bootstrap() to each cluster model separately. Each bootstrap resamples the sequences assigned to that cluster and reconstructs the transition matrix, repeating 1,000 times to build empirical sampling distributions for each edge.

set.seed(265)  # for reproducibility
# Bootstrap each cluster model to assess within-cluster edge stability
boot_patA <- bootstrap(gtna_clust$`Pattern A`, iter = 1000, level = 0.05)
boot_patB <- bootstrap(gtna_clust$`Pattern B`, iter = 1000, level = 0.05)

7.3 Results

An edge marked sig = TRUE appeared consistently across 1,000 resampled datasets; an edge marked sig = FALSE did not survive this replicability criterion:

Pattern A: significant edges

# Edges that survived the bootstrap stability criterion for Pattern A
boot_patA$summary[boot_patA$summary$sig == TRUE, ] 

Pattern B: significant edges

# Edges that survived the bootstrap stability criterion for Pattern B
boot_patB$summary[boot_patB$summary$sig == TRUE, ] 
Column Meaning
from, to The transition being evaluated
mean Average edge weight across bootstrap resamples
ci_lower, ci_upper 95% confidence interval for the edge weight
sig TRUE if the edge demonstrated resampling stability

7.4 Visualizing Bootstrapped Cluster Networks

The plots below display only the edges that survived the bootstrap procedure — the confirmed, replicable transition structure of each cluster:

# Side-by-side: only edges that survived the bootstrap are displayed
par(mfrow = c(1, 2))
plot(boot_patA, cut = 0.1)
plot(boot_patB, cut = 0.1)
par(mfrow = c(1, 1))
Figure 6: Bootstrap-validated networks for each cluster

8 Choosing the Number of Clusters

The choice of k is a substantive decision. There is no single “correct” number of clusters — the goal is to find a solution where clusters have meaningfully different structures and each cluster is interpretable.

A practical approach is to try several values and compare:

# Try several values of k and compare cluster summaries
for (k in 2:5) {
  clust_k <- cluster_sequences(prepared_data$sequence_data, k = k)
  gtna_k <- group_tna(clust_k)
  cat("\n--- k =", k, "---\n")
  print(summary(gtna_k))
}

If increasing k only splits an existing cluster into two nearly identical subgroups, fewer clusters suffice. If a new k reveals a genuinely distinct behavioral pattern (e.g., a cluster with a strong plan → monitor → adapt cycle that was previously merged with a different pattern), the higher k may be warranted.

Approach When to Use How
Known groups Pre-existing grouping variable group_tna(data, group = "Variable")
Data-driven clusters No grouping variable, or exploring latent patterns cluster_sequences() then group_tna()

These are complementary. Cluster for exploration, then validate against known variables. If clusters align with a known variable (e.g., high vs. low achievers), this provides convergent evidence. If they do not, the clusters may reveal behavioral dimensions that the known variable does not capture.

9 Summary

This tutorial demonstrated a complete workflow for data-driven discovery of behavioral subgroups:

  1. Cluster sequences with cluster_sequences() based on structural dissimilarity.
  2. Build group-specific TNA models with group_tna() and visualize them.
  3. Compare clusters with difference plots, community detection, and centrality profiles.
  4. Validate differences with permutation_test() to confirm that cluster differences exceed what would be expected by chance.
  5. Validate edges with bootstrap() to confirm that within-cluster transitions are replicable.
  6. Report only edges and differences that survive the relevant inferential test.

For group comparisons using known variables (e.g., achievement group, experimental condition), see the group comparison tutorial.

References

Citation

BibTeX citation:
@misc{saqr2026,
  author = {Saqr, Mohammed and López-Pernas, Sonsoles},
  title = {TNA {Clustering:} {Discovering} and {Analysis} of {Clusters}},
  date = {2026-02-06},
  url = {https://sonsoleslp.github.io/posts/tna-clustering/},
  langid = {en}
}
For attribution, please cite this work as:
Saqr, Mohammed, and Sonsoles López-Pernas. 2026. “TNA Clustering: Discovering and Analysis of Clusters.” Preprint, February 6. https://sonsoleslp.github.io/posts/tna-clustering/.