Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Build a square-ish matrix with a specified number of cells

Tags:

r

matrix

sequence

I would like to write a function that transforms an integer, n, (specifying the number of cells in a matrix) into a square-ish matrix that contain the sequence 1:n. The goal is to make the matrix as "square" as possible.

This involves a couple of considerations:

  1. How to maximize "square"-ness? I was thinking of a penalty equal to the difference in the dimensions of the matrix, e.g. penalty <- abs(dim(mat)[1]-dim(mat)[2]), such that penalty==0 when the matrix is square and is positive otherwise. Ideally this would then, e.g., for n==12 lead to a preference for a 3x4 rather than 2x6 matrix. But I'm not sure the best way to do this.

  2. Account for odd-numbered values of n. Odd-numbered values of n do not necessarily produce an obvious choice of matrix (unless they have an integer square root, like n==9. I thought about simply adding 1 to n, and then handling as an even number and allowing for one blank cell, but I'm not sure if this is the best approach. I imagine it might be possible to obtain a more square matrix (by the definition in 1) by adding more than 1 to n.

  3. Allow the function to trade-off squareness (as described in #1) and the number of blank cells (as described in #2), so the function should have some kind of parameter(s) to address this trade-off. For example, for n==11, a 3x4 matrix is pretty square but not as square as a 4x4, but the 4x4 would have many more blank cells than the 3x4.

  4. The function needs to optionally produce wider or taller matrices, so that n==12 can produce either a 3x4 or a 4x3 matrix. But this would be easy to handle with a t() of the resulting matrix.


Here's some intended output:

> makemat(2)
     [,1]
[1,]    1
[2,]    2

> makemat(3)
     [,1] [,2]
[1,]    1    3
[2,]    2    4

> makemat(9)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

> makemat(11)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7    10
[2,]    2    5    8    11
[3,]    3    6    9    12

Here's basically a really terrible start to this problem.

makemat <- function(n) {
    n <- abs(as.integer(n))
    d <- seq_len(n)
    out <- d[n %% d == 0]
    if(length(out)<2)
        stop('n has fewer than two factors')
    dim1a <- out[length(out)-1]
    m <- matrix(1:n, ncol=dim1a)
    m
}

As you'll see I haven't really been able to account for odd-numbered values of n (look at the output of makemat(7) or makemat(11) as described in #2, or enforce the "squareness" rule described in #1, or the trade-off between them as described in #3.

like image 636
Thomas Avatar asked Nov 30 '22 00:11

Thomas


1 Answers

I think the logic you want is already in the utility function n2mfrow(), which as its name suggests is for creating input to the mfrow graphical parameter and takes an integer input and returns the number of panels in rows and columns to split the display into:

> n2mfrow(11)
[1] 4 3

It favours tall layouts over wide ones, but that is easily fixed via rev() on the output or t() on a matrix produced from the results of n2mfrow().

makemat <- function(n, wide = FALSE) {
  if(isTRUE(all.equal(n, 3))) {
    dims <- c(2,2)
  } else {
    dims <- n2mfrow(n)
  }
  if(wide)
    dims <- rev(dims)
  m <- matrix(seq_len(prod(dims)), nrow = dims[1], ncol = dims[2])
  m
}

Notice I have to special-case n = 3 as we are abusing a function intended for another use and a 3x1 layout on a plot makes more sense than a 2x2 with an empty space.

In use we have:

> makemat(2)
     [,1]
[1,]    1
[2,]    2
> makemat(3)
     [,1] [,2]
[1,]    1    3
[2,]    2    4
> makemat(9)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9
> makemat(11)
     [,1] [,2] [,3]
[1,]    1    5    9
[2,]    2    6   10
[3,]    3    7   11
[4,]    4    8   12
> makemat(11, wide = TRUE)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12

Edit:

The original function padded seq_len(n) with NA, but I realised the OP wanted to have a sequence from 1 to prod(nrows, ncols), which is what the version above does. The one below pads with NA.

makemat <- function(n, wide = FALSE) {
  if(isTRUE(all.equal(n, 3))) {
    dims <- c(2,2)
  } else {
    dims <- n2mfrow(n)
  }
  if(wide)
    dims <- rev(dims)
  s <- rep(NA, prod(dims))
  ind <- seq_len(n)
  s[ind] <- ind
  m <- matrix(s, nrow = dims[1], ncol = dims[2])
  m
}
like image 168
Gavin Simpson Avatar answered Dec 04 '22 12:12

Gavin Simpson