R Conditional Formatting in Loops

Viewed 74

I'm trying to iterate through a df with a number of addresses in different neighborhoods, and for each neighborhood I would like to randomly divide each address into one of two equal groups. My df looks roughly like this:

neighborhood <- c("armatage", "armatage", "armatage", "windom", "windom", "windom", "windom")
address <- c("a", "b", "c", "d", "e", "f", "g")
df <- data.frame(address, neighborhood)

but with many more neighborhoods with varying numbers of addresses. Using the randomizr package, so far I have been able to come up with this script, which iterates through each neighborhood name and comes up with a randomized list of 0s and 1s with the length of the number of rows within each neighborhood. The problem seems to be the second for loop, and actually assigning the randomized value to each row

for (i in df$neighborhood)
  n <- nrow(df[df$neighborhood == i, ])
  z <- complete_ra((n))
  for (row in 1:nrow(df[df$neighborhood == i, ]))
    df$group[row] <- z[row]

where df$group is where I would like to store the randomly assigned value. I would greatly appreciate any advice anyone might have. Thanks!

2 Answers

Here is another way and avoids a double loop:

library(data.table)

dt = as.data.table(df)
dt[, .(grp = sample(.N) %% 2, 
       address)
   , by = neighborhood]
#>    neighborhood grp address
#> 1:     armatage   1       a
#> 2:     armatage   0       b
#> 3:     armatage   1       c
#> 4:       windom   0       d
#> 5:       windom   1       e
#> 6:       windom   1       f
#> 7:       windom   0       g

Basically, if we take the modulo while also doing the sequence of the total number of addresses in each neighborhood, we can assign randomness.

Background

Let's take a look at what the modulo operator %% does to the number sequence 1 to 4:

seq(from = 1, to = 4) ## or 1:4 or seq(4)
## [1] 1 2 3 4

seq(from = 1, to = 4) %% 2
## [1] 1 0 1 0

Mathematically, it tells us the remainder. That is, 1 / 2 has a remainder of 1; 2 / 2 has a remainder of 0; and so on. We can use this to make groupings. The problem is that this isn't random. That's where sample() comes in play

sample(4) ## or sample(1:4) or sample(seq(1, 4))
## [1] 2 1 4 3

So if we combine modulo with sample(), we can effectively randomize these by groups if we know how many are in each group. That's where grouping such as dt[i, j, by] syntax could help or tibble %>% group_by() %>% mutate() syntax are of use. Yes, we could subset the unique neighborhoods in a loop, but it is more efficient to do groupings.

Since is what helped me initially, let's take a look at that version:

library(dplyr)
df %>%
  group_by(neighborhood) %>%
  mutate(group = sample(n()) %% 2)

## # A tibble: 7 x 3
## # Groups:   neighborhood [2]
##   address neighborhood group
##   <chr>   <chr>        <dbl>
## 1 a       armatage         1
## 2 b       armatage         0
## 3 c       armatage         1
## 4 d       windom           1
## 5 e       windom           0
## 6 f       windom           1
## 7 g       windom           0

An approach using dplyr, purrr

neighborhood <- c("armatage", "armatage", "armatage", "windom", "windom", "windom", "windom")
address <- c("a", "b", "c", "d", "e", "f", "g")
df <- data.frame(address, neighborhood)

library(dplyr)
library(purrr)

df %>%
  # split original data into group of neighborhod by group_split from dplyr
  group_split(neighborhood) %>%
  # then for group of neighborhood apply function to split them into 2 group
  # based on their row number and number of group is 2
  map(.f = function(x) {
     x %>% group_by((row_number() - 1) %/% (n() / 2)) %>%
        nest %>% pull(.)
    })

Result of above code

[[1]]
[[1]][[1]]
# A tibble: 2 x 2
  address neighborhood
  <chr>   <chr>       
1 a       armatage    
2 b       armatage    

[[1]][[2]]
# A tibble: 1 x 2
  address neighborhood
  <chr>   <chr>       
1 c       armatage    


[[2]]
[[2]][[1]]
# A tibble: 2 x 2
  address neighborhood
  <chr>   <chr>       
1 d       windom      
2 e       windom      

[[2]][[2]]
# A tibble: 2 x 2
  address neighborhood
  <chr>   <chr>       
1 f       windom      
2 g       windom    

In case you just want to add an index column to categorize each row into separate group.

df %>%
  group_by(neighborhood) %>%
  # cur_group_id gave group index + some math to calculate proper index
  # for each group base on their row number.
  mutate(group = (cur_group_id() - 1) * 2 + (row_number() - 1) %/% (n() / 2) + 1)

Output

# A tibble: 7 x 3
# Groups:   neighborhood [2]
  address neighborhood group
  <chr>   <chr>        <dbl>
1 a       armatage         1
2 b       armatage         1
3 c       armatage         2
4 d       windom           3
5 e       windom           3
6 f       windom           4
7 g       windom           4
Related