This tests an alternative to Erwin Kalvelagen’s script for generating a sparse random digraph.

We start by loading the magrittr library.

library(magrittr)

Erwin’s script

Now we time Erwin’s script (excluding output). Since we are not writing out the results, the removal of duplicates is done at the data frame rather than in the output.

# Set the number of nodes.
n <- 5000
# Set the desired number of arcs.
nn <- n^2 / 100
# Use a fixed random number seed for reproducibility (hopefully).
set.seed(123)
start.time <- proc.time()
# Generate a random collection of arcs in a data frame.
df <- data.frame(
         ni = sample(n, nn, replace=TRUE),
         nj = sample(n, nn, replace=TRUE))
# Sort the arcs.
df <- df[order(df$ni, df$nj),]
# Remove duplicates.
df <- unique(df)
# Show the time consumed.
proc.time() - start.time
   user  system elapsed 
  0.294   0.023   0.318 

How many arcs did we actually get (compared to how many we wanted)?

cat(paste0("Generated ", nrow(df), " of desired ", nn, " arcs."))
Generated 248744 of desired 250000 arcs.

How many loops (arcs with head = tail) did we get?

cat(sum(df[,1] == df[,2]))
48

How many nodes are isolated (no arcs in or out)?

tails <- unique(df$nj)
heads <- unique(df$ni)
orphans <- setdiff(1:n, union(tails, heads))
cat(paste0(length(tails), " of ", n , " nodes have indegree >= 1.\n"))
5000 of 5000 nodes have indegree >= 1.
cat(paste0(length(heads), " of ", n, " nodes have outdegree >= 1.\n"))
5000 of 5000 nodes have outdegree >= 1.
cat(paste0(length(orphans), " nodes are orphans."))
0 nodes are orphans.

My script

For the alternative approach, we initially use zero-based indexing for nodes and arcs. The index of arc (i, j) is i*n + j. We will need the inverse of that function (i.e., a function to convert index k back to the indices of the tail and head nodes).

toArc <- function(k) {
  c(k %/% n, k %% n)
}

We will also want a function to drop selected indices of a vector within a pipe. To generate the arcs, we start with the arc indices 0, …, n^2-1 and remove indices corresponding to loops. Arc k is a loop if k %/% n == k %% n, or equivalently if k = m * (n + 1) for some m in {0, …, n - 1}. We then map the surviving indices to arcs.

# Use a fixed random number seed for reproducibility (hopefully).
set.seed(123)
# Generate the data frame of arcs.
start.time <- proc.time()
df2 <- seq.int(0, n^2 - 1, 1) %>%                  # start with all arc indices
         extract(-seq.int(1, n^2, n + 1)) %>%      # weed out loops
         sample(size = nn, replace = FALSE) %>%    # take a random subset
         sort() %>%                                # sort into index order
         sapply(toArc) %>%                         # convert to arcs
         t() %>%                                   # transpose
         as.data.frame() %>%                       # make a dataframe
         add(1)                                    # revert to 1-based indexing
# Set the column names.
colnames(df2) <- c("ni", "nj")
# Show the time consumed.
proc.time() - start.time
   user  system elapsed 
  0.730   0.236   0.964 

How many arcs did we actually get (compared to how many we wanted)?

cat(paste0("Generated ", nrow(df2), " of desired ", nn, " arcs."))
Generated 250000 of desired 250000 arcs.

How many loops (arcs with head = tail) did we get?

cat(sum(df2[,1] == df2[,2]))
0

How many nodes are isolated (no arcs in or out)?

tails <- unique(df2$nj)
heads <- unique(df2$ni)
orphans <- setdiff(1:n, union(tails, heads))
cat(paste0(length(tails), " of ", n , " nodes have indegree >= 1.\n"))
5000 of 5000 nodes have indegree >= 1.
cat(paste0(length(heads), " of ", n, " nodes have outdegree >= 1.\n"))
5000 of 5000 nodes have outdegree >= 1.
cat(paste0(length(orphans), " nodes are orphans."))
0 nodes are orphans.
LS0tCnRpdGxlOiAiU3BhcnNlIFJhbmRvbSBHcmFwaHMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KClRoaXMgdGVzdHMgYW4gYWx0ZXJuYXRpdmUgdG8gRXJ3aW4gS2FsdmVsYWdlbidzIHNjcmlwdCBmb3IgZ2VuZXJhdGluZyBhIHNwYXJzZSByYW5kb20gZGlncmFwaC4KCldlIHN0YXJ0IGJ5IGxvYWRpbmcgdGhlIG1hZ3JpdHRyIGxpYnJhcnkuCgpgYGB7cn0KbGlicmFyeShtYWdyaXR0cikKYGBgCgojIEVyd2luJ3Mgc2NyaXB0CgpOb3cgd2UgdGltZSBFcndpbidzIHNjcmlwdCAoZXhjbHVkaW5nIG91dHB1dCkuIFNpbmNlIHdlIGFyZSBub3Qgd3JpdGluZyBvdXQgdGhlIHJlc3VsdHMsIHRoZSByZW1vdmFsIG9mIGR1cGxpY2F0ZXMgaXMgZG9uZSBhdCB0aGUgZGF0YSBmcmFtZSByYXRoZXIgdGhhbiBpbiB0aGUgb3V0cHV0LgoKYGBge3J9CiMgU2V0IHRoZSBudW1iZXIgb2Ygbm9kZXMuCm4gPC0gNTAwMAojIFNldCB0aGUgZGVzaXJlZCBudW1iZXIgb2YgYXJjcy4Kbm4gPC0gbl4yIC8gMTAwCiMgVXNlIGEgZml4ZWQgcmFuZG9tIG51bWJlciBzZWVkIGZvciByZXByb2R1Y2liaWxpdHkgKGhvcGVmdWxseSkuCnNldC5zZWVkKDEyMykKc3RhcnQudGltZSA8LSBwcm9jLnRpbWUoKQojIEdlbmVyYXRlIGEgcmFuZG9tIGNvbGxlY3Rpb24gb2YgYXJjcyBpbiBhIGRhdGEgZnJhbWUuCmRmIDwtIGRhdGEuZnJhbWUoCiAgICAgICAgIG5pID0gc2FtcGxlKG4sIG5uLCByZXBsYWNlPVRSVUUpLAogICAgICAgICBuaiA9IHNhbXBsZShuLCBubiwgcmVwbGFjZT1UUlVFKSkKIyBTb3J0IHRoZSBhcmNzLgpkZiA8LSBkZltvcmRlcihkZiRuaSwgZGYkbmopLF0KIyBSZW1vdmUgZHVwbGljYXRlcy4KZGYgPC0gdW5pcXVlKGRmKQojIFNob3cgdGhlIHRpbWUgY29uc3VtZWQuCnByb2MudGltZSgpIC0gc3RhcnQudGltZQpgYGAKCkhvdyBtYW55IGFyY3MgZGlkIHdlIGFjdHVhbGx5IGdldCAoY29tcGFyZWQgdG8gaG93IG1hbnkgd2Ugd2FudGVkKT8KCmBgYHtyfQpjYXQocGFzdGUwKCJHZW5lcmF0ZWQgIiwgbnJvdyhkZiksICIgb2YgZGVzaXJlZCAiLCBubiwgIiBhcmNzLiIpKQpgYGAKCkhvdyBtYW55IGxvb3BzIChhcmNzIHdpdGggaGVhZCA9IHRhaWwpIGRpZCB3ZSBnZXQ/CgpgYGB7cn0KY2F0KHN1bShkZlssMV0gPT0gZGZbLDJdKSkKYGBgCgpIb3cgbWFueSBub2RlcyBhcmUgaXNvbGF0ZWQgKG5vIGFyY3MgaW4gb3Igb3V0KT8KCmBgYHtyfQp0YWlscyA8LSB1bmlxdWUoZGYkbmopCmhlYWRzIDwtIHVuaXF1ZShkZiRuaSkKb3JwaGFucyA8LSBzZXRkaWZmKDE6biwgdW5pb24odGFpbHMsIGhlYWRzKSkKY2F0KHBhc3RlMChsZW5ndGgodGFpbHMpLCAiIG9mICIsIG4gLCAiIG5vZGVzIGhhdmUgaW5kZWdyZWUgPj0gMS5cbiIpKQpjYXQocGFzdGUwKGxlbmd0aChoZWFkcyksICIgb2YgIiwgbiwgIiBub2RlcyBoYXZlIG91dGRlZ3JlZSA+PSAxLlxuIikpCmNhdChwYXN0ZTAobGVuZ3RoKG9ycGhhbnMpLCAiIG5vZGVzIGFyZSBvcnBoYW5zLiIpKQpgYGAKCiMgTXkgc2NyaXB0CgpGb3IgdGhlIGFsdGVybmF0aXZlIGFwcHJvYWNoLCB3ZSBpbml0aWFsbHkgdXNlIHplcm8tYmFzZWQgaW5kZXhpbmcgZm9yIG5vZGVzIGFuZCBhcmNzLiBUaGUgaW5kZXggb2YgYXJjIChpLCBqKSBpcyBpKm4gKyBqLiBXZSB3aWxsIG5lZWQgdGhlIGludmVyc2Ugb2YgdGhhdCBmdW5jdGlvbiAoaS5lLiwgYSBmdW5jdGlvbiB0byBjb252ZXJ0IGluZGV4IGsgYmFjayB0byB0aGUgaW5kaWNlcyBvZiB0aGUgdGFpbCBhbmQgaGVhZCBub2RlcykuCgpgYGB7cn0KdG9BcmMgPC0gZnVuY3Rpb24oaykgewogIGMoayAlLyUgbiwgayAlJSBuKQp9CmBgYAoKV2Ugd2lsbCBhbHNvIHdhbnQgYSBmdW5jdGlvbiB0byBkcm9wIHNlbGVjdGVkIGluZGljZXMgb2YgYSB2ZWN0b3Igd2l0aGluIGEgcGlwZS4KVG8gZ2VuZXJhdGUgdGhlIGFyY3MsIHdlIHN0YXJ0IHdpdGggdGhlIGFyYyBpbmRpY2VzIDAsIC4uLiwgbl4yLTEgYW5kIHJlbW92ZSBpbmRpY2VzIGNvcnJlc3BvbmRpbmcgdG8gbG9vcHMuIEFyYyBrIGlzIGEgbG9vcCBpZiBrICUvJSBuID09IGsgJSUgbiwgb3IgZXF1aXZhbGVudGx5IGlmIGsgPSBtICogKG4gKyAxKSBmb3Igc29tZSBtIGluIHswLCAuLi4sIG4gLSAxfS4gV2UgdGhlbiBtYXAgdGhlIHN1cnZpdmluZyBpbmRpY2VzIHRvIGFyY3MuCgpgYGB7cn0KIyBVc2UgYSBmaXhlZCByYW5kb20gbnVtYmVyIHNlZWQgZm9yIHJlcHJvZHVjaWJpbGl0eSAoaG9wZWZ1bGx5KS4Kc2V0LnNlZWQoMTIzKQojIEdlbmVyYXRlIHRoZSBkYXRhIGZyYW1lIG9mIGFyY3MuCnN0YXJ0LnRpbWUgPC0gcHJvYy50aW1lKCkKZGYyIDwtIHNlcS5pbnQoMCwgbl4yIC0gMSwgMSkgJT4lICAgICAgICAgICAgICAgICAgIyBzdGFydCB3aXRoIGFsbCBhcmMgaW5kaWNlcwogICAgICAgICBleHRyYWN0KC1zZXEuaW50KDEsIG5eMiwgbiArIDEpKSAlPiUgICAgICAjIHdlZWQgb3V0IGxvb3BzCiAgICAgICAgIHNhbXBsZShzaXplID0gbm4sIHJlcGxhY2UgPSBGQUxTRSkgJT4lICAgICMgdGFrZSBhIHJhbmRvbSBzdWJzZXQKICAgICAgICAgc29ydCgpICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyBzb3J0IGludG8gaW5kZXggb3JkZXIKICAgICAgICAgc2FwcGx5KHRvQXJjKSAlPiUgICAgICAgICAgICAgICAgICAgICAgICAgIyBjb252ZXJ0IHRvIGFyY3MKICAgICAgICAgdCgpICU+JSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyB0cmFuc3Bvc2UKICAgICAgICAgYXMuZGF0YS5mcmFtZSgpICU+JSAgICAgICAgICAgICAgICAgICAgICAgIyBtYWtlIGEgZGF0YWZyYW1lCiAgICAgICAgIGFkZCgxKSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgcmV2ZXJ0IHRvIDEtYmFzZWQgaW5kZXhpbmcKIyBTZXQgdGhlIGNvbHVtbiBuYW1lcy4KY29sbmFtZXMoZGYyKSA8LSBjKCJuaSIsICJuaiIpCiMgU2hvdyB0aGUgdGltZSBjb25zdW1lZC4KcHJvYy50aW1lKCkgLSBzdGFydC50aW1lCmBgYAoKSG93IG1hbnkgYXJjcyBkaWQgd2UgYWN0dWFsbHkgZ2V0IChjb21wYXJlZCB0byBob3cgbWFueSB3ZSB3YW50ZWQpPwoKYGBge3J9CmNhdChwYXN0ZTAoIkdlbmVyYXRlZCAiLCBucm93KGRmMiksICIgb2YgZGVzaXJlZCAiLCBubiwgIiBhcmNzLiIpKQpgYGAKCkhvdyBtYW55IGxvb3BzIChhcmNzIHdpdGggaGVhZCA9IHRhaWwpIGRpZCB3ZSBnZXQ/CgpgYGB7cn0KY2F0KHN1bShkZjJbLDFdID09IGRmMlssMl0pKQpgYGAKCkhvdyBtYW55IG5vZGVzIGFyZSBpc29sYXRlZCAobm8gYXJjcyBpbiBvciBvdXQpPwoKYGBge3J9CnRhaWxzIDwtIHVuaXF1ZShkZjIkbmopCmhlYWRzIDwtIHVuaXF1ZShkZjIkbmkpCm9ycGhhbnMgPC0gc2V0ZGlmZigxOm4sIHVuaW9uKHRhaWxzLCBoZWFkcykpCmNhdChwYXN0ZTAobGVuZ3RoKHRhaWxzKSwgIiBvZiAiLCBuICwgIiBub2RlcyBoYXZlIGluZGVncmVlID49IDEuXG4iKSkKY2F0KHBhc3RlMChsZW5ndGgoaGVhZHMpLCAiIG9mICIsIG4sICIgbm9kZXMgaGF2ZSBvdXRkZWdyZWUgPj0gMS5cbiIpKQpjYXQocGFzdGUwKGxlbmd0aChvcnBoYW5zKSwgIiBub2RlcyBhcmUgb3JwaGFucy4iKSkKYGBgCg==