Let’s design a method that stop the number of iterations when a specific criterion is reached and provide an estimation of when the convergence is reached.

test_convergence <- function(values) {
  n <- length(values)
  if (n == 1)
    return(0)
  first_half <- head(values, n / 2)
  second_half <- tail(values, n / 2)
  sum(first_half >= min(second_half) & first_half <= max(second_half)) / (n / 2)
}
termination_criterion <- function(values) {
  test_convergence(values) >= 0.9
}
convergence_func <- function(values) {
  max(which(map_dbl(seq_along(values), ~ test_convergence(values[1:.])) <= 0.5)) / 2
}

detect_convergence <- function(object, modif_func, measures_func,
                               termination_criterion, convergence_func,
                               min_iteration = 0, max_iteration = 1e6) {
  require(purrr)
  values <- matrix(NA, nrow = max_iteration, ncol = length(measures_func))
  iteration <- 1
  conv <- rep(NA, length(measures_func))
  max_iter <- conv
  while (iteration <= max_iteration) {
    object <- modif_func(object)
    values[iteration,] <- map_dbl(measures_func, ~.(object))
    stopifnot(all(!is.na(values[1:iteration,])))
    term <- apply(matrix(values[1:iteration,], nrow = iteration),
                  2, termination_criterion)
    while (any(term) && iteration >= min_iteration) {
      meas <- which(term)[1]
      res_idx <- which(map_int(seq_along(conv), ~ sum(is.na(conv[1:.]))) == meas)[1]
      conv[res_idx] <- convergence_func(values[1:iteration,meas])
      max_iter[res_idx] <- iteration
      new_meas <- setdiff(seq_along(measures_func), meas)
      values <- matrix(values[,new_meas], ncol = length(new_meas))
      measures_func <- measures_func[new_meas]
      term <- term[new_meas]
    }
    if (length(measures_func) == 0)
      break
    iteration <- iteration + 1
  }
  data.frame(conv = conv, max_iter = max_iter)
}

Let’s test the method with some settings:

n <- 30
m <- 10
M <- 10
col <- sample(1:M, n, replace = TRUE)
row <- sample(1:M, m, replace = TRUE)
mat <- matrix(col) %*% t(matrix(row))

modif_mat <- function(mat) {
  rows <- sample(nrow(mat), 2)
  cols <- sample(ncol(mat), 2)
  submat <- mat[rows,cols]
  shift_min <- -min(submat[1,1], submat[2,2])
  shift_max <- min(submat[1,2], submat[2,1])
  shift <- sample(shift_min:shift_max, size = 1)
  submat[1,1] <- submat[1,1] + shift
  submat[2,2] <- submat[2,2] + shift
  submat[1,2] <- submat[1,2] - shift
  submat[2,1] <- submat[2,1] - shift
  mat[rows,cols] <- submat
  stopifnot(all(mat >= 0))
  mat
}

measures <- list(sd_meas, chi2_meas, sd_meas, chi2_meas)

detect_convergence(mat, modif_mat, measures, termination_criterion,
                   convergence_func, min_iteration = n * m, max_iteration = 3e4)
##     conv max_iter
## 1 3678.5    20849
## 2 3680.0    18210
## 3 3678.5    20849
## 4 3680.0    18210

This is consistent with previous results. Let’s repeat the test with more measures several times to see the robustness:

measures <- list(sd_meas, mean_CV_row, mean_CV_col,
                 mean_cor_row, mean_cor_col, chi2_meas)
detect_convergence(mat, modif_mat, measures, termination_criterion,
                   convergence_func, min_iteration = n * m, max_iteration = 3e4)
##     conv max_iter
## 1 1672.5    10260
## 2 2212.0     9140
## 3 2442.5    13023
## 4  862.5     6788
## 5 1098.5     8320
## 6 2130.5    10000
detect_convergence(mat, modif_mat, measures, termination_criterion,
                   convergence_func, min_iteration = n * m, max_iteration = 3e4)
##     conv max_iter
## 1 3058.5    12807
## 2 2292.5    12565
## 3 4653.0    12563
## 4 1550.0    11620
## 5 2446.0    12836
## 6 2975.0    12661
detect_convergence(mat, modif_mat, measures, termination_criterion,
                   convergence_func, min_iteration = n * m, max_iteration = 3e4)
##     conv max_iter
## 1 2841.0    14955
## 2 2752.0     7220
## 3 2795.0    22374
## 4 3358.0    11494
## 5 3147.0    20620
## 6 2803.5    14930

There are some consistencies (convergence between 1000 and 4000 and analyzing 10000 to 20000 iterations is sufficient). Let’s gather more data with a smaller matrix.

n <- 5
m <- 5
M <- 10
col <- sample(1:M, n, replace = TRUE)
row <- sample(1:M, m, replace = TRUE)
mat <- matrix(col) %*% t(matrix(row))
results <- replicate(30, detect_convergence(mat, modif_mat, measures,
                                            termination_criterion,
                                            convergence_func,
                                            min_iteration = n * m))

Let’s plot the convergence values:

convs <- do.call(rbind, results[1,])
plot.ecdf(convs[,1])
for (i in 2:length(measures))
  plot.ecdf(convs[,i], col = i, add = TRUE)
legend("bottomright", c("sd", "meanCVrow", "meanCVcol", "corrow", "corcol", "chi2"),
       fill = seq_along(measures))

Most measures are consistent. The convergence occurs with some variability between 20 and 100 most of the times.

iters <- do.call(rbind, results[2,])
plot.ecdf(iters[,1])
for (i in 2:length(measures))
  plot.ecdf(iters[,i], col = i, add = TRUE)
legend("bottomright", c("sd", "meanCVrow", "meanCVcol", "corrow", "corcol", "chi2"),
       fill = seq_along(measures))

400 iterations is often what is necessary to identify the convergence value.

Let’s see what happen with a 10 by 5 matrix:

n <- 10
m <- 5
M <- 10
col <- sample(1:M, n, replace = TRUE)
row <- sample(1:M, m, replace = TRUE)
mat <- matrix(col) %*% t(matrix(row))
results <- replicate(30, detect_convergence(mat, modif_mat, measures,
                                            termination_criterion,
                                            convergence_func,
                                            min_iteration = n * m))
par(mfrow = c(1, 2))
convs <- do.call(rbind, results[1,])
plot.ecdf(convs[,1])
for (i in 2:length(measures))
  plot.ecdf(convs[,i], col = i, add = TRUE)
iters <- do.call(rbind, results[2,])
plot.ecdf(iters[,1])
for (i in 2:length(measures))
  plot.ecdf(iters[,i], col = i, add = TRUE)
legend("bottomright", c("sd", "meanCVrow", "meanCVcol", "corrow", "corcol", "chi2"),
       fill = seq_along(measures))

We observe a time to converge of 300 with some variation between the measures for a total number of necessary iterations of 2000. It is around 4 times more than with a 5 by 5 matrix and 10 times less than with a 30 by 10 matrix. Let’s increase further with a 10 by 10 matrix:

n <- 10
m <- 10
M <- 10
col <- sample(1:M, n, replace = TRUE)
row <- sample(1:M, m, replace = TRUE)
mat <- matrix(col) %*% t(matrix(row))
results <- replicate(30, detect_convergence(mat, modif_mat, measures,
                                            termination_criterion,
                                            convergence_func,
                                            min_iteration = n * m))
par(mfrow = c(1, 2))
convs <- do.call(rbind, results[1,])
plot.ecdf(convs[,1])
for (i in 2:length(measures))
  plot.ecdf(convs[,i], col = i, add = TRUE)
iters <- do.call(rbind, results[2,])
plot.ecdf(iters[,1])
for (i in 2:length(measures))
  plot.ecdf(iters[,i], col = i, add = TRUE)
legend("bottomright", c("sd", "meanCVrow", "meanCVcol", "corrow", "corcol", "chi2"),
       fill = seq_along(measures))

Convergence around 800 and max iterations around 5000.

The next step is to design a general method that repeats the markov process 30 times from the same initial matrix and select the 0.8-quantile.

## R version 3.4.0 (2017-04-21)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 16.04.2 LTS
## 
## Matrix products: default
## BLAS: /usr/lib/openblas-base/libblas.so.3
## LAPACK: /usr/lib/libopenblasp-r0.2.18.so
## 
## locale:
##  [1] LC_CTYPE=fr_FR.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=fr_FR.UTF-8        LC_COLLATE=fr_FR.UTF-8    
##  [5] LC_MONETARY=fr_FR.UTF-8    LC_MESSAGES=fr_FR.UTF-8   
##  [7] LC_PAPER=fr_FR.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C       
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] stringr_1.0.0 tidyr_0.2.0   ggplot2_2.2.1 dplyr_0.4.3   purrr_0.2.0  
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.10     knitr_1.15.1     magrittr_1.5     munsell_0.4     
##  [5] colorspace_1.2-2 R6_2.1.0         rlang_0.1        plyr_1.8.1      
##  [9] tools_3.4.0      parallel_3.4.0   grid_3.4.0       gtable_0.1.2    
## [13] pacman_0.4.6     DBI_0.3.1        htmltools_0.3.6  yaml_2.1.13     
## [17] lazyeval_0.2.0   rprojroot_1.2    digest_0.6.3     assertthat_0.1  
## [21] tibble_1.3.1     evaluate_0.10    rmarkdown_1.5    stringi_0.5-5   
## [25] compiler_3.4.0   scales_0.4.1     backports_1.0.5