Spatial perceptions of bicycle safety in Marburg, Germany

Kayla Heinis, Dylan Lehmann, Matthew Haffner, Dirk Zeuss

2025-02-20

1 Introduction

The benefits of bike-friendly communities are numerous, yet a lack of safety is a formidable barrier to ridership. In this project, the authors surveyed residents of Marburg, Germany on their spatial perceptions of bicycle safety in order to better understand how transportation infrastructure could potentially be improved. This project is a replication and extension of a previous study conducted in Eau Claire, Wisconsin.

2 Survey instrument

The analysis of cycling safety in Marburg, Germany, relied on two primary data sets that were derived from an online survey that was given to local cyclists. This survey was available in both German and English. The first data set contains geometry and geospatial information that represents the locations users found unsafe.

The second data set provides additional information and context for the cycling safety survey conducted in Marburg. The CSV format includes responses to the survey. It covered various aspects such as: - Demographic information - Type of bicycle respondents owned - Reasons for cycling

The survey instrument was created with R’s web framework Shiny along with other packages such as shinydashboard, leaflet, and sf. Prospective respondents were presented with a map on which they were instructed to identify points where they feel unsafe riding a bicycle as well as answering the following questions:

  • “How confident do you feel in your cycling ability?” Respondents are given a scale from 1 ‘Not confident,’ to 7 ‘Very confident.’

  • “How much does safety influence where you ride?” This scale is also rated 1-7, one being not at all influenced by safety, and seven, very much influenced by safety.

The respondents are then asked six questions that use a drop down bar to respond. They ask:

  • “Approximately how many trips do you take by bike per week?” The options for responding are, 0-3, 4-7, 8-12, 13-20, and 20+.

  • “How often do you wear a helmet when riding a bike?” The options for responding to this question are, “Never,” “Sometimes,” and “Always.”

  • “How often do you ride with children?” The options for responding to this are, “Never, Rarely, Often, Always.”

The survey moves to demographic questions using the drop down response method, asking the respondent information about their age and gender.

3 Demographic comparisons

The survey found no statistically significant difference in cycling confidence between men and women. Although the mean scores show women reported slightly higher confidence than men in Marburg, with women having a mean of 6.22 and men 5.79, this difference is not statistically significant. The Wilcox test (W = 75.5, p = 0.859) confirmed that the p-value of 0.859 is much higher than the 0.05 threshold, indicating that gender does not appear to play a significant role in cycling confidence based on this data.

Men are often assumed to have higher confidence in cycling due to societal stereotypes, but this data challenges that notion. However, additional data collection may be necessary to obtain a more precise and reliable comparison of confidence levels, as subtle differences could emerge with a larger sample size.

## density plot showing level of confidence
gender_con <- df %>% filter(gender == "Female" | gender == "Male")

ggplot(gender_con, aes(x = confidence, color = gender, fill = gender )) +
  geom_density(alpha = 0.5) +
  scale_color_manual(values = c("Female" = female_color, "Male" = male_color)) +
  scale_fill_manual(values = c("Female" = female_color, "Male" = male_color)) +
  labs(x = "Confidence", y = "Density", color = "Gender", fill = "Gender")

## finding mean
mean_confidence <- gender_con %>%
  group_by(gender) %>%
  summarize(mean_conf = mean(confidence, na.rm = TRUE))
mean_confidence

Survey results indicated that women are more influenced by safety concerns when deciding where to cycle than men. As shown in the previous figure, this could suggest that women feel safer cycling in Marburg, potentially encouraging more women to cycle confidently. A Wilcox test (W = 108.5, p-value = 0.037) revealed a statistically significant difference, with women placing more importance on safety compared to men. The mean scores support this finding, with men having a mean of 4.083 and women 5.483. This gap in safety perceptions highlights the greater impact of safety concerns on women’s cycling behavior, influencing where and why they choose to ride.

knitr::opts_chunk$set(echo = TRUE)
## safety influence by gender density plot
gender_si <- df %>% filter(gender== "Female" | gender == "Male")

ggplot(gender_si, aes(x=safety_influence, color = gender, fill = gender)) +
  geom_density(alpha = 0.5) +
  scale_color_manual(values = c("Female" = female_color, "Male" = male_color)) +
  scale_fill_manual(values = c("Female" = female_color, "Male" = male_color)) +
  labs(x = "Safety Influence", y = "Density", color = "Gender", fill = "Gender")

## wilcox test
wilcox_test_safe <- wilcox.test(safety_influence ~ gender, data = gender_si)
wilcox_test_safe

## finding mean
mean_safe <- gender_si %>%
  group_by(gender) %>%
  summarize(mean_safe = mean(safety_influence, na.rm = TRUE))
mean_safe

There is no significant difference in the number of trips men and women take throughout the week, with both genders participating in cycling at similar rates. While gender may not strongly influence cycling frequency in Marburg, the previous figure suggests differing perceptions of safety between men and women. Therefore, fostering an inclusive and safe cycling environment remains crucial.

knitr::opts_chunk$set(echo = TRUE)

## number of trips bar chart 
df_num_trips <- df %>% 
  group_by(gender) %>% 
  count(number_of_trips) %>% 
  filter(gender %in% c("Female", "Male")) %>% 
  na.omit(number_of_trips) %>% 
  mutate(Gender = gender)

ggplot(df_num_trips, aes(fill = Gender, y = n, x = number_of_trips)) +
  geom_bar(position = "dodge",
           stat = "identity",
           alpha = 0.5,
           color = "black") +
  scale_fill_manual(values = c("Female" = female_color, "Male" = male_color)) +
  xlab("Number of trips") +
  ylab("Frequency")

4 Usage and Bike Type Tables

A significant majority of respondents use city bikes in Marburg, Germany, suggesting that the city’s infrastructure is particularly well-suited for this type of bicycle. This preference likely stems from the comfort and convenience city bikes offer for everyday travel. To further support local cyclists and encourage sustainable transportation, improving traffic safety measures, and ensuring the regular maintenance of roads would be key steps in enhancing Marburg’s cycling environment.

## displaying table
biketype_df %>%
  kable("html", col.names = c("Bike Type", "Total")) %>%
  kable_styling(bootstrap_options = c( "hover", "condensed"),
                full_width = TRUE)
Bike Type Total
City Bike 22
Mountain Bike 5
Cargo Bike 2
E-Bike 2
Road/Gravel Bike 2

Cyclists in the city of Marburg ride for a variety of reasons, including:

  • Recreation
  • Transportation
  • Exercise

Survey results show that cycling for transportation and recreation are the most common purposes. Investments in safety measures should address the needs of both recreational and transportation cyclists, improving safety and accessibility for all.

## displaying table
bikeusage_df %>%
  kable("html", col.names = c("Bike Usage", "Total")) %>%
  kable_styling(bootstrap_options = c( "hover", "condensed"),
                full_width = TRUE)
Bike Usage Total
Recreation 20
Transportation 20
Exercise 8

5 Heat Map

The heat map reveals a concentration of unsafe areas in the city center, with additional points extending to the outskirts. Many of these points are clustered around intersections or locations with limited visibility, indicating specific hazards such as heavy traffic, proximity to motor vehicles, poor signage, or challenging terrain. The concentration in the city center is likely due to higher traffic volumes, while points on the outskirts may reflect concerns about less developed infrastructure. Addressing these high-risk locations through targeted improvements can significantly enhance cyclist safety and confidence across the city.

pts_geom <- pts$geometry

pts_df <- pts %>%
  st_set_geometry(NULL)

heat_map <- leaflet(pts) %>%
  ## TODO use zoom of 12?
  setView(zoom = 12.3,
          lng = pts_df$x %>% mean,
          lat = pts_df$y %>% mean) %>%
  addProviderTiles("CartoDB.DarkMatter") %>%
  ## TODO play around with parameters
  addHeatmap(radius = 20, 
             blur = 24, 
             group = "Heat Map",
             intensity = 100
             ) %>%
  addCircleMarkers(lng = pts_df$x,
                   lat = pts_df$y,
                   fillOpacity = 0.5,
                   color = "white",
                   stroke = FALSE,
                   radius = 5,
                   group = "Individual points") %>%
  addLayersControl(overlayGroups = c("Heat Map",
                                     "Individual points"),
                   options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("Individual points")

heat_map

6 Cluster Map

Out of the four clusters identified, two stand out as particularly significant. Most points are concentrated near the city center, with fewer on the outskirts, yet the clusters consistently form around major intersections. These locations represent the areas where the majority of respondents have reported feeling unsafe. Each cluster includes either an intersection or a road segment involving a turn, highlighting the challenges cyclists face in navigating these complex areas. The proximity of these clusters to the city center suggests higher traffic volumes, both for cyclists and vehicles, leading to more potential conflict points.

Throughout the survey, cyclists in Marburg consistently highlighted these intersections and areas of low visibility as problematic. These findings align with the broader observation that cyclists often feel most vulnerable when sharing space with motor vehicles. Addressing these key intersections and improving visibility could enhance cyclists’ sense of safety, potentially increasing ridership.

pts_3070 <- pts %>%
  st_transform(3070) %>%
  arrange(user)

remove_dups_within_dist <- function(pts_df, dist) {
  ## create empty holding tank for new point object, have to give it a crs
  new_pts <- st_sf(st_sfc()) %>%
    st_set_crs(st_crs(pts_df))
  
  ## create vector of rownames; will remove duplicated stuff from here
  pts_df$index <- 1:nrow(pts_df)
  
  ## create empty vector of duplicated indexes; will use this to skip over duplicates
  dup_indexes <- c()
  
  for (i in 1:nrow(pts_df)) {
    ## if the point's index has not been flagged as dup, don't skip over
    if (sum(dup_indexes %in% pts_df$index[i]) == 0) {
      tmp_pt <- pts_df[i,]
      
      ## put a buffer around it
      tmp_pt_buffer <- st_buffer(tmp_pt, dist)
      
      ## find points that intersect
      int_pts <- st_intersection(pts_df, tmp_pt_buffer)
      
      ## for all intersected points
      for (j in 1:nrow(int_pts)) {
        if (j == 1) {
          next
        } else {
          if (tmp_pt$user == int_pts$user[j]) {
            dup_indexes <- c(dup_indexes, int_pts$index[j])
          }
        }
      }
      new_pts <- rbind(new_pts, pts_df[i,])
    }
  }
  return(new_pts)
}

reduced_pts <- remove_dups_within_dist(pts_3070, 200)

## TODO play around with parameters (minPts no less than 4)
clusters <- dbscan(reduced_pts %>% st_coordinates,
                   eps = 200,
                   minPts = 4)

reduced_pts$cluster <- clusters %>%
  pluck("cluster")

reduced_pts$cluster <- na_if(reduced_pts$cluster, 0)

cluster_pts_to_polygon <- function(pts, dist) {
  
  ## create placeholder for convex hull object
  c_hull <- st_sf(st_sfc()) %>%
    st_set_crs(st_crs(pts)) %>%
    st_as_sf() %>%
    mutate(cluster = NA)
  
  for (i in 1:length(na.omit(unique(pts$cluster)))) {
    
    ## filter by cluster number
    tmp_pts <- pts %>%
      filter(cluster == i)
    
    ## create convex hull object (returns geometry only)
    c_hull_geom_tmp <- st_convex_hull(st_union(tmp_pts))
    
    ## make in to sf object
    c_hull_sf_tmp <- st_as_sf(c_hull_geom_tmp)
    
    ## assign cluster number
    c_hull_sf_tmp$cluster <- i
    
    ## add to object
    c_hull <- rbind(c_hull, c_hull_sf_tmp)
  }
  
  ## buffer
  cluster_buff <- st_buffer(c_hull, dist)
  
  return(cluster_buff)
}

## use a small distance for cluster buffer
cluster_buff <- cluster_pts_to_polygon(reduced_pts, 20)

cluster_counts <- reduced_pts$cluster %>%
  table() %>%
  as.numeric()

cluster_buff$labels <- paste0(cluster_buff$cluster, " (n = ", cluster_counts, ")")

cluster_map <- tm_shape(cluster_buff) +
  tm_fill(col = "cluster",
          palette = "Set2",
          alpha = 0.8,
          title = "Cluster",
          labels = cluster_buff$labels,
          group = "Clusters") +
  tm_borders(lwd = 5,
             col = "black") +
  tm_shape(reduced_pts %>% mutate(as.factor(gender))) +
  tm_dots(col = "gender",
          title = "Gender",
          alpha = 0.5,
          palette = c(Female = female_color, Male = male_color, Other = other_color),
          group = "Unsafe points") +
  tm_layout(legend.just = "left")

## use tmap_leaflet object to get web map to appear as figure
cluster_map_leaflet <- tmap_leaflet(cluster_map)
cluster_map_leaflet