Random Walks in Lattices

Jose M Sallan 2023-11-08 5 min read

In mathematics, a random walk is a succession of random steps on some mathematical space. A popular random walk model is that of a random walk on a regular lattice, where at each step the location jumps to another site according to some probability distribution. In a simple random walk, the location can only jump to neighboring sites of the lattice, forming a lattice path.

In this post, I will present a function to generate a two-dimensional random walk and another function to plot it. This exercise will allow me to present an example of vectorial programming and some functionalities of the tidyverse.

library(tidyverse)

Generating Lattice Random Walks

Here is a function random_walk() that returns the evolution of a random walk for a number of steps in a lattice of dimension d. The setup values are 100 steps in a 2-dimensional lattice.

random_walk <- function(steps = 100, d = 2){
  
  dim <- sample(1:d, steps, replace = TRUE)
  move <- sample(c(-1, 1), steps, replace = TRUE)
  
  moves <- map_dfc(1:d, ~ c(0, cumsum(ifelse(dim == ., move, 0))))
  
  return(moves)
  
}

I have defined the path of the random walk defining randomly the dimension along the lattice of each movement with dim and the orientation along the dimension with move. dim takes values between one and d, and move takes values -1 or 1, representing the two possible opposite directions. Rather than defining those values iteratively, they are defined as vectors of length steps.

moves is a tibble with d columns and steps rows, which is defined as follows: for each of the d dimensions:

  • we define a vector with components equal to the component of move if dim == d, and zero otherwise. In R, this can be obtained with ifelse(dim == d, move, 0).
  • we use cumsum() to obtain the position in the dimension at each step doing cumsum(ifelse(dim == d, move, 0)).
  • we add the position zero at the beginning, as the process starts at the origin of coordinates: c(0, cumsum(ifelse(dim == d, move, 0))).
  • we wrap the list of d vectors into a tibble of d columns using map_dfc() from purrr.

Let’s obtain a random walk of 100 steps in a 2-dimensional lattice:

set.seed(1111)
rw <- random_walk()
rw
## # A tibble: 101 × 2
##     ...1  ...2
##    <dbl> <dbl>
##  1     0     0
##  2     0    -1
##  3     0     0
##  4     0    -1
##  5     0    -2
##  6     0    -3
##  7    -1    -3
##  8    -1    -2
##  9    -1    -3
## 10    -1    -2
## # ℹ 91 more rows

Plotting 2-Dimensional Lattice Random Walks

Let’s define a plot_random_walk() function to plot a two-dimensional random walk.

plot_random_walk <- function(coords){
  
  names(coords) <- c("x0", "y0")
  
  segments <- coords |>
    mutate(x1 = lead(x0), y1 = lead(y0)) |>
    slice(-n())
  
  segments <- segments |>
    group_by(x0, y0, x1, y1) |>
    count()
  
  s_segments <- full_join(segments, 
                          segments, 
                          by =c("x0" = "x1", "y0" = "y1",
                                "x1" = "x0", "y1" = "y0")) |>
    replace_na(list(n.x =0, n.y = 0)) |>
    mutate(n = n.x + n.y)
  
  ggplot(coords, aes(x0, y0)) +
    geom_point() +
    geom_segment(data = s_segments, aes(x = x0, y = y0, xend = x1, yend = y1, linewidth = n)) +
    scale_linewidth(range = c(1, 3)) +
    theme_void() +
    theme(legend.position = "none")
  
}

The function takes as input the output of random_walk() and prepares a suitable table ready to be used with ggplot with the following steps:

  • Rename the columns of the table as x0 and y0.
  • Define the segments table with the start (x0 and y0) and end (x1 and y1) point of each segment. We use dplyr::lead() to obtain the end points of segments and slice(-n()) to remove the last row.
  • Some of the segments may appear more than once along the path. It can be interesting to check how many times appears each segment. We use dplyr::group_by() and dplyr::count() to redefine segments in this way. The number of occurrences of each segment is stored in the column n generated by count().
  • In the table s_segments, I add the number of occurrences of segments from x0, y0 to x1, y1 and of segments from x1, y1 to x0, y0. As I am doing a merge of segments with itself, the variable n will appear as n.x and n.y in the merged table.

Now we are ready to do the plot:

  • with geom_point() we draw each of the points covered by the random walk using coords.
  • with geom_segment() we draw each of the steps usign s_segments. The linewidth of the step is proportional to the number of times the process crosses it in either direction. That’s why it is included in aes().
  • scale_linewidth(range = c(1, 3)) is used to tune the minimum and maximum line width. This feature is available in recent versions of ggplot2.
plot_random_walk(rw)

If we set more steps to the random walk, it will cover a much larger area:

set.seed(2222)
rw_1000 <- random_walk(steps = 1000)
plot_random_walk(rw_1000)

A random walk is a succession of random steps on some mathematical space. We can use random walks to model processes like paths of foraging animals, the evolution of a stock price or the financial status of a gambler. In this post, I have used the functionalities of the tidyverse to generate random walks of lattices of arbitrary number of dimensions, and to plot random walk in two-dimensional lattices.

## R version 4.3.2 (2023-10-31)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Linux Mint 21.1
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.10.0 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
## 
## locale:
##  [1] LC_CTYPE=es_ES.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=es_ES.UTF-8        LC_COLLATE=es_ES.UTF-8    
##  [5] LC_MONETARY=es_ES.UTF-8    LC_MESSAGES=es_ES.UTF-8   
##  [7] LC_PAPER=es_ES.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=es_ES.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Europe/Madrid
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] lubridate_1.9.2 forcats_1.0.0   stringr_1.5.0   dplyr_1.1.2    
##  [5] purrr_1.0.1     readr_2.1.4     tidyr_1.3.0     tibble_3.2.1   
##  [9] ggplot2_3.4.2   tidyverse_2.0.0
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.3     jsonlite_1.8.4   highr_0.10       compiler_4.3.2  
##  [5] tidyselect_1.2.0 jquerylib_0.1.4  scales_1.2.1     yaml_2.3.7      
##  [9] fastmap_1.1.1    R6_2.5.1         labeling_0.4.2   generics_0.1.3  
## [13] knitr_1.42       bookdown_0.33    munsell_0.5.0    tzdb_0.3.0      
## [17] bslib_0.5.0      pillar_1.9.0     rlang_1.1.0      utf8_1.2.3      
## [21] stringi_1.7.12   cachem_1.0.7     xfun_0.39        sass_0.4.5      
## [25] timechange_0.2.0 cli_3.6.1        withr_2.5.0      magrittr_2.0.3  
## [29] digest_0.6.31    grid_4.3.2       rstudioapi_0.14  hms_1.1.3       
## [33] lifecycle_1.0.3  vctrs_0.6.2      evaluate_0.20    glue_1.6.2      
## [37] farver_2.1.1     blogdown_1.16    fansi_1.0.4      colorspace_2.1-0
## [41] rmarkdown_2.21   tools_4.3.2      pkgconfig_2.0.3  htmltools_0.5.5