--- title: "Getting started with rEMM" author: "Michael Hahsler" bibliography: rEMM.bib output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Getting started with rEMM} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4 ) set.seed(1234) ``` ## Introduction Traditional data stream clustering algorithms efficiently group massive real-time data points into clusters but typically disregard the chronological or temporal order in which those points arrive. TRACDS [@Hahsler+Dunham:2011] fixes this by superimposing a dynamically adapting Markov chain onto data stream clustering. As a data stream is processed and partitioned into clusters, the transitions between clusters over time are captured. The states of the Markov chain represent the clusters, and the edges capture the probability of moving from one cluster to another over time. A specific implementation is the Extensible Markov Model (EMM) [@emm:Dunham:2004], which combines threshold nearest neighbor clustering with a Markov chain. The package `rEMM` implements EMMs and also the general TRACDS framework that can add temporal structure learning on top of any data stream clustering algorithm. An interface for the algorithms in the [`stream`](https://michael.hahsler.net/stream/) package [@Hahsler:2017] is provided. This guide demonstrates how to build a model from an ordered sequence, inspect its clusters and transitions, score new sequences, and predict a future state. ## Install and load the package Install the released package from CRAN: ```{r install, eval=FALSE} install.packages("rEMM") ``` ```{r load-package} library(rEMM) ``` ## Prepare an ordered sequence Use a numeric matrix or data frame with one observation per row and one feature per column. Rows must be in sequence order because EMM learns transitions between consecutive rows. Choose feature scales and a distance measure that reflect similarity in your application; for Euclidean distance, features with larger scales can dominate the clustering. The included `EMMsim` data contain observations from four clusters in two dimensions. The training sequence repeats the pattern `1, 2, 1, 3, 4` forty times, giving 200 observations. A separate test sequence contains 25 observations generated using the same pattern. ```{r data} data("EMMsim") dim(EMMsim_train) head(EMMsim_train) head(EMMsim_sequence_train, 10) ``` We can visualize the training dataset as points where the different symbols identify the generating clusters. The grey lines show the order of observations. These cluster labels are supplied for illustration; the model uses only the coordinates in `EMMsim_train` and needs to learn the clusters and temporal relationship between them. ```{r sequence-plot} plot(EMMsim_train, type = "n", xlab = "x", ylab = "y") lines(EMMsim_train, col = "gray80") points(EMMsim_train, pch = EMMsim_sequence_train) legend("topleft", legend = paste("Cluster", 1:4), pch = 1:4, bty = "n") ``` ## Build and inspect an EMM `EMM()` creates an empty model. `build()` processes observations in row order, assigns them to clusters, and records transitions between states. For the threshold nearest neighbor clustering approach (`tNN`), a new observation joins the closest cluster when its distance is below the clustering threshold; otherwise, it creates a new cluster. A detailed description of how the model is created can be found in @Hahsler+Dunham:2011. ```{r build-model} emm <- EMM(threshold = 0.1, measure = "euclidean") build(emm, EMMsim_train) emm ``` The threshold controls the level of detail in the clustering. Smaller values generally produce more clusters, while larger values group more observations together. Here, the small threshold creates micro-clusters within the four generating clusters. The threshold is expressed in the units of the chosen distance measure and should be chosen for the scale of your data. `size()` returns the number of clusters, `cluster_centers()` returns their representative coordinates, and `cluster_counts()` reports how many observations have been assigned to each cluster. `last_clustering()` retains the cluster assignments of data points from the most recent `build()` call. ```{r inspect-model} size(emm) head(cluster_centers(emm)) head(sort(cluster_counts(emm), decreasing = TRUE)) head(last_clustering(emm), 10) ``` The cluster labels are just identifiers and do not need to match the labels used to generate the example data. We can visualize the learned model. The MDS method places clusters at the locations of their centers. For higher-dimensional data, it uses a 2D multidimensional scaling projection. ```{r first-model-plot} plot(emm, method = "MDS", main = "EMM") ``` We see that the clustering broke the 4 clusters each into several micro-clusters which is common for data stream clustering algorithms. The grey arrows show that it learned the general order. ## Simplifying the model When a data stream clustering model has many micro-clusters, reclustering can give a more compact view. In this example, we know there are four generating clusters, so we merge the micro-clusters into four groups using hierarchical clustering. For other data, choose the number of groups using domain knowledge or validation on separate sequences. ```{r recluster} emm4 <- recluster_hclust(emm, k = 4, method = "average") emm4 cluster_centers(emm4) cluster_counts(emm4) ``` `recluster_hclust()` merges both the clusters and their transition information. The reclustered model ```{r model-plot} plot(emm4, method = "MDS", main = "Reclustered EMM") ``` The reclustered model perfectly learned the temporal structure of the cluster order: bottom-left, top-left, bottom-left, top-right, bottom-right. This holds for this simple data set. Remember, cluster labels are identifiers and do not need to agree with the labels in the original data set. Transition matrices have source states in rows and destination states in columns. Transition probabilities are based on those counts. The default `prior = TRUE` to smooth counts by adding one to every transition count before calculating probabilities, which gives more stable results for low counts. Here, we show the transition probabilities without count smoothing. ```{r transitions} transition_matrix(emm4, prior = FALSE) ``` ## Score a new sequence `score()` measures how well a new sequence of data points agrees with the learned model. Points are assigned to existing clusters, and the transitions are scored. Here, we use `method = "product"`, which multiplies the transition probabilities along the path taken by the new data. The score is rescaled to a probability by accounting for the path length. ```{r score-sequences} set.seed(1234) test_shuffled <- EMMsim_test[sample(nrow(EMMsim_test)), , drop = FALSE] c( ordered = score(emm4, EMMsim_test, method = "product", match_cluster = "nn", prior = TRUE), shuffled = score(emm4, test_shuffled, method = "product", match_cluster = "nn", prior = TRUE) ) ``` We see that the original test sequence matches the model well with a very high probability, while the shuffled sequence, whose temporal structure was destroyed, has a probability close to 0. ## Predict a future state Given a current state, `predict()` returns a future state or a probability distribution over states. Here we request the distribution 2 steps ahead from the state with label `"1"`. ```{r predict-state} predict(emm4, current_state = "1", n = 2, probabilities = TRUE, prior = TRUE) ``` We see that states 1 and 5 are more likely then the others. ## Add observations incrementally For performance reasons, EMM objects are implemented as mutable objects using the S4 class system. `build()` therefore changes an EMM in place and to create a copy the `copy()` function has to be explicitly called. For additional batches from the same sequence, call `build()` again and the model records the transition from the previous batch's last state to the next batch's first state. For an independent sequence, call `reset()` first to avoid creating a transition between unrelated sequences. `reset()` clears the current state while retaining the learned clusters and counts. Next, we make a copy of the current EMM, start a new sequence and add more data. ```{r incremental-build} emm_updated <- copy(emm) reset(emm_updated) build(emm_updated, EMMsim_test) c( original_observations = sum(cluster_counts(emm)), updated_observations = sum(cluster_counts(emm_updated)) ) ``` For streams whose behavior changes over time, the `lambda` argument for `EMM()` enables fading so older observations and transitions have less influence. The default `lambda = 0` retains their full weight. See `fade()` and `prune()` for fading and removal of rare states or transitions. ## Next steps `DSC_EMM()` and `DSC_tNN()` interfaces let you use these algorithms as `stream::DSC()` clusterers with the [`stream`](https://michael.hahsler.net/stream/) package. See [Adding temporal structure modeling to stream clustering](stream.html) for examples using clusterers from **stream**. For standard clustering methods such as k-means, see [Adding temporal structure modeling to standard clustering](TRAC.html), which demonstrates the `TRAC()` interface. For the model's background and further applications, see the [original paper vignette](rEMM_paper_vignette.pdf) and @Hahsler+Dunham:2010. ## References