Yesterday, I found myself wanting to compute a large subset of the second order principal minors of a matrix (diagonal-preserving minors; the ones for which the rows and columns kept are the same). Don’t judge me for wanting to do this, and bear with me, there is a lesson about R programming here (mostly for me, read the comments for testing).
If you don’t know what a principal minor is or forgot what it was (because most of us don’t spend much time with principal minors), here is a short refresher on how to get a second-order principal minor (other orders are possible and there’s a lot of related theory, but let’s try to focus on this one task for now).
Step 1: Deletion. Take any matrix. Delete all but two rows and all but two columns. For example, suppose the matrix is 5×5 and let’s keep rows 1 and 3 and columns 2 and 4 (lots of other combinations are possible).
Step 2: Calculation. Once we delete the irrelevant parts of the big matrix to get our 2×2 submatrix, obtain the principal minor by taking the determinant of the resulting 2×2 matrix. For our example, we compute 3×2 – 3×1 = 3.
Both steps in this calculation are easy to implement in R. For the first step, merely use R’s fantastic syntax for extracting rows and columns of matrices. If the matrix is called mat, we efficiently perform the first step by:
little_mat = mat[c(1,3), c(2,4)]
That’s easy enough. With little_mat in hand, the second step is even easier.
pm = det(little_mat)
The real trick to this programming problem is picking out all of principal minors I want to compute (diagonal-preserving ones that keep the same-numbered rows and columns; little_mat won’t do because c(1,3) is not c(2,4)), and doing it efficiently. I also don’t want to rewrite my code as my matrix changes size.
So, what do we do?
A really bad An idea that will eventually compute the right answer is to write this using for() loops.
Barring a coding error, this code should eventually work, but it will be slow (or fast depending on how you write it). If you want to run through this code many times (like I did yesterday), efficiency is at a premium and you should immediately think of faster techniques — like using the apply() function (or preallocating your storage vector, which actually saves time).
Before moving to apply(), we need a quick way to enumerate all of the ways to keep two out of N rows. If we were counting, that’s N choose 2, also known as a combination. Fortunately, this step has already been implemented in the gregmisc library with a function called combinations(). Conveniently, combinations(N, 2) will return a matrix with all of the ways to select two numbers less than or equal to N as its rows. There are other options, but this is the default, which is perfect for our application.
For example, here is some output for N=5.
With this way of computing the rows/ columns we want to keep, all we need to do is put our simple code for computing a second order principal minor into a function. Let’s call it minor2(). Then, use apply() to apply this function to each row (MARGIN=1 is for rows) of the combinations matrix. Here’s the complete set of code that will work if you have a matrix called mat for which you want the diagonal-second order principal minors.
Because we avoid doing loops, this method is much faster. For a big project, this added efficiency is worth it. More generally, this technique of using apply() to avoid repeatedly looping is a good lesson to apply to other programming problems.
Update: Thanks to Berend for going through the trouble of system.time() testing the code in this post (and spiceman for a good comment as well). Read on into the comments for more, but for a preview of what we learned: (1) The copying via c() is what slows the loop down, (2) apply is slightly slower than writing the loop yourself, (3) combn(), the combinations() alternative, is really slow, and (4) one should always stand behind system.time() methods before talking about the efficiency of a method.
I much appreciate the feedback. One of the reasons I have this blog is to learn more about R (and those comments were definitely informative).