Riddler: Can You Tell When The Snow Started?

FiveThirtyEight’s Riddler Express

( Link to the original article.)

From Patrick Mayor comes a question about something we’re doing these days to keep ourselves and others safe: social distancing.

You’re walking along the middle of a wide sidewalk when you see someone walking toward you from the other direction, also down the middle of the sidewalk, 12 feet away. Being responsible citizens, you pass each other while maintaining a distance of at least 6 feet at all times. By the time you reach each other’s original positions, you should be back in the middle of the sidewalk again.

You should assume that the other person follows the same path you do, but flipped around (since they’re walking in the opposite direction). For example, you could both walk 3 feet to the left, 12 feet forward and finally 3 feet back to the right, walking a total of 18 feet before swapping positions.

Being lazy (I mean, efficient), you’d like to know the shortest distance you and the other person could walk so that you can switch positions, all while staying at least 6 feet apart at all times. What is this distance?

Plan

I am basically going to simulate the process with very small time steps and track the location of each person. These points can then be used to measure the distance traveled by each person.

Setup

knitr::opts_chunk$set(echo = TRUE, comment = "#>")

library(glue)
library(ggforce)
library(gganimate)
library(tidyverse)

theme_set(theme_minimal())

Simulation

Global constants

Below are a few constants to define from the beginning. time_step_size is the distance traveled at each time step of the simulation. minimum_distance is the minimum safe distance that the two people need to stay from each other.

time_step_size <- 0.01
minimum_distance <- 6

Subroutines

The update_tracker() function updates a data table with a person’s current location for a given time step.

update_tracker <- function(pos, time_step, tracker_tib) {
    bind_rows(
        tracker_tib,
        tibble(x = pos[[1]], y = pos[[2]], t = time_step)
    )
}

The move_person() function moves a person left or right by a given amount x.

move_person <- function(person, x, right = TRUE) {
    if (right) {
        person[[1]] <- person[[1]] + x
    } else {
        person[[1]] <- person[[1]] - x
    }
    return(person)
}

The shift_person() function shifts a person up or down by a given about y.

shift_person <- function(person, y, down = TRUE) {
    if (down) {
        person[[2]] <- person[[2]] - y
    } else {
        person[[2]] <- person[[2]] + y
    }
    return(person)
}

The measure_distance() function measures the distances between two ((x, y)) coordinates a and b.

measure_distance <- function(a, b) {
    dist(matrix(c(a, b), nrow = 2, byrow = TRUE))[[1]]
}

Main simulation loop

The following for-loop moves each person, A and B, towards each other from 12 feet away. At each step, the distance between one another is measured, and if they are too close to each other, they are slowly moved apart until they are again safe. The movement of each person is tracked in a data table.

One complication I ran into was how to move the people back towards the middle after they passed each other. Instead, I realized that the problem is symmetric, so I could just run the first half of the simulation - from 12 to 6 feet apart - and then use the symmetry to get the second half.

A <- c(0, 0)
B <- c(12, 0)

A_tracker <- update_tracker(A, 0, tibble())
B_tracker <- update_tracker(B, 0, tibble())

for (t in seq(time_step_size, 6, time_step_size)) {
    A <- move_person(A, time_step_size)
    B <- move_person(B, time_step_size, right = FALSE)
    
    while (measure_distance(A, B) < minimum_distance) {
        A <- shift_person(A, 0.01, down = TRUE)
        B <- shift_person(B, 0.01, down = FALSE)
    }

    A_tracker <- update_tracker(A, t, A_tracker)
    B_tracker <- update_tracker(B, t, B_tracker)
}

The plot below shows the path of A for the first half of the simulation.

A_tracker %>% 
    ggplot(aes(x = x, y = y)) +
    geom_line() +
    labs(title = "Person A's path or the first half of the simulation")

The simulation was completed by copying the tracker data tables for A and B and combing these into a single people_tracker data table.

A_tracker <- bind_rows(
    A_tracker, 
    A_tracker %>% 
        mutate(x = 6 + x, y = rev(y), t = 6 + t) %>%
        filter(t != 6)
)

B_tracker <- bind_rows(
    B_tracker,
    B_tracker %>% 
        mutate(x = x - 6, y = rev(y), t = 6 + t) %>%
        filter(t != 6)
)

ppl_tracker <- inner_join(A_tracker,
                          B_tracker,
                          by = "t",
                          suffix = c("_A", "_B"))

Finally, I could plot the paths taken by the two individuals while always remaining as close to the middle of the sidewalk as possible and keeping 6 feet apart.

ppl_tracker %>%
    mutate(y_A = y_A - 0.05,
           y_B = y_B + 0.05) %>%
    ggplot() +
    geom_line(aes(x = x_A, y = y_A), color = "red", size = 1.1) +
    geom_line(aes(x = x_B, y = y_B), color = "blue", size = 1.1) +
    coord_equal() +
    labs(x = "x", y = "y", 
         title = "Social distancing paths")

The social distancing rules guides are shown in the plot below, they they overlap a lot because there is no time dimension.

ppl_tracker %>%
    mutate(y_A = y_A - 0.05,
           y_B = y_B + 0.05) %>%
    ggplot() +
    geom_ribbon(aes(x = x_A, 
                    ymin = y_A-minimum_distance, ymax = y_A+minimum_distance), 
                fill = "red", color = NA, alpha = 0.1) +
    geom_ribbon(aes(x = x_B, 
                    ymin = y_B-minimum_distance, ymax = y_B+minimum_distance), 
                fill = "blue", color = NA, alpha = 0.1) +
    geom_line(aes(x = x_A, y = y_A), color = "red", size = 1.1) +
    geom_line(aes(x = x_B, y = y_B), color = "blue", size = 1.1) +
    coord_equal() +
    labs(x = "x", y = "y", 
         title = "Social distancing paths",
         subtitle = "The ribbons indicate the 6 ft radius\naround each person")

I used ‘gganimate’ to show the two people walking towards each other with their 6-foot social distancing guides.

ppl_tracker %>%
    mutate(y_A = y_A - 0.05,
           y_B = y_B + 0.05) %>%
    filter(row_number() %% 20 == 0) %>%
    ggplot() +
    geom_point(aes(x = x_A, y = y_A), color = "red", size = 3) +
    geom_point(aes(x = x_B, y = y_B), color = "blue", size = 3) +
    geom_circle(aes(x0 = x_A, y0 = y_A, r = minimum_distance / 2),
                fill = "red", alpha = 0.1, color = NA) +
    geom_circle(aes(x0 = x_B, y0 = y_B, r = minimum_distance / 2),
                fill = "blue", alpha = 0.1, color = NA) +
    coord_equal() +
    labs(x = "x", y = "y", title = "Social distancing paths") +
    transition_states(t, transition_length = 0.01, 
                      state_length = 0, wrap = FALSE)

Total distance traveled

Finally, as the Riddler requested, I calculated the total distance traveled by person A.

total_distance <- 0
for (i in seq(2, nrow(A_tracker))) {
    a <- c(A_tracker$x[[i - 1]], A_tracker$y[[i - 1]])
    b <- c(A_tracker$x[[i]], A_tracker$y[[i]])
    total_distance <- total_distance + measure_distance(a, b)
}

total_distance <- round(total_distance, 1)

Each person travels a total of 15.7 ft, 3.7 more feet than without the need for social distancing.

Graduate Student

My research interests include cancer genetics and evolution. I also learning about programming and computer science in general.