Authors: Nhi Pham, Adrianna Lazuga

1 Introduction

This project analyses the spatial distribution of Mevo cycling rental stations in Gdańsk. Specifically, it investigates whether Mevo stations are randomly distributed, clustered, or spatially associated with cycling infrastructure.

2 Literature Review

Spatial point pattern analysis is used to study whether locations are clustered, random, or dispersed. In this project, MEVO stations are treated as point features, while bike lanes are treated as line features.

Kernel Density Estimation (KDE) is used to identify station hotspots:

\[\hat{\lambda}(u) = \sum_i k(u - x_i)\]

where \(x_i\) is a station location and \(k\) is the kernel function.

Nearest neighbour distance measures how close each station is to its nearest neighbour:

\[d_i = \min_j \|x_i - x_j\|\]

Ripley’s K function compares the observed pattern with complete spatial randomness (CSR):

\[K(r) = \frac{1}{\lambda} \mathbb{E}[N(r)]\]

If the observed K function lies above the CSR line, the pattern suggests clustering.

Finally, point–line analysis examines whether MEVO stations are located close to bike lanes. A Poisson point process model is fitted to test whether station intensity varies with distance to cycling infrastructure.

3 Data Exploration and Preparation

3.1 Libraries

3.2 Study Area: Gdańsk Administrative Boundary

area <- getbb("Gdansk, Poland")

query_city <- opq(bbox = area) %>%
  add_osm_feature(key = "boundary", value = "administrative") %>%
  add_osm_feature(key = "admin_level", value = "8")

df_city <- osmdata_sf(query_city)
df_gdansk_borders <- df_city$osm_multipolygons %>% filter(name == "Gdańsk")

ggplot() +
  geom_sf(data = df_gdansk_borders, fill = "lightsteelblue1") +
  labs(title = "Administrative boundary of Gdańsk") +
  theme_minimal()

3.3 MEVO Bicycle Rental Stations

query_bicycle_rental <- opq(bbox = area) %>%
  add_osm_feature(key = "amenity", value = "bicycle_rental")

df_bicycle_rental <- osmdata_sf(query_bicycle_rental)
df_rental_points   <- df_bicycle_rental$osm_points
head(df_rental_points)
## Simple feature collection with 6 features and 32 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 18.55065 ymin: 54.37667 xmax: 18.63211 ymax: 54.47601
## Geodetic CRS:  WGS 84
##                osm_id          name addr:city addr:housenumber addr:postcode
## 2420642350 2420642350 Rower Partner    Gdańsk                1        80-512
## 2903387105 2903387105   MEVO GDY118      <NA>             <NA>          <NA>
## 4317375214 4317375214   MEVO GDA156      <NA>             <NA>          <NA>
## 6005303666 6005303666   MEVO GDA127      <NA>             <NA>          <NA>
## 6005303667 6005303667   MEVO GDA170      <NA>             <NA>          <NA>
## 6005303668 6005303668   MEVO GDA169      <NA>             <NA>          <NA>
##            addr:street        amenity bicycle_parking bicycle_rental brand
## 2420642350    Północna bicycle_rental            <NA>           <NA>  <NA>
## 2903387105        <NA> bicycle_rental            <NA>           <NA>  MEVO
## 4317375214        <NA> bicycle_rental            <NA>  dropoff_point  MEVO
## 6005303666        <NA> bicycle_rental            <NA>           <NA>  MEVO
## 6005303667        <NA> bicycle_rental            <NA>  dropoff_point  MEVO
## 6005303668        <NA> bicycle_rental            <NA>  dropoff_point  MEVO
##            brand:wikidata brand:wikipedia capacity check_date covered
## 2420642350           <NA>            <NA>     <NA>       <NA>    <NA>
## 2903387105      Q60860236         pl:Mevo       40       <NA>    <NA>
## 4317375214      Q60860236         pl:Mevo       10       <NA>    <NA>
## 6005303666      Q60860236         pl:Mevo       10       <NA>    <NA>
## 6005303667      Q60860236         pl:Mevo       10 2026-04-14    <NA>
## 6005303668      Q60860236         pl:Mevo       10       <NA>    <NA>
##                      email  fee fixme network network:wikidata
## 2420642350 pro-polis@wp.pl <NA>  <NA>    <NA>             <NA>
## 2903387105            <NA> <NA>  <NA>    MEVO        Q60860236
## 4317375214            <NA> <NA>  <NA>    MEVO        Q60860236
## 6005303666            <NA> <NA>  <NA>    MEVO        Q60860236
## 6005303667            <NA> <NA>  <NA>    MEVO        Q60860236
## 6005303668            <NA> <NA>  <NA>    MEVO        Q60860236
##            network:wikipedia     opening_hours        operator operator:type
## 2420642350              <NA> Mo-Fr 10:00-20:00            <NA>          <NA>
## 2903387105           pl:Mevo              24/7 CityBike Global          <NA>
## 4317375214           pl:Mevo              24/7 CityBike Global          <NA>
## 6005303666           pl:Mevo              24/7 CityBike Global          <NA>
## 6005303667           pl:Mevo              24/7 CityBike Global          <NA>
## 6005303668           pl:Mevo              24/7 CityBike Global          <NA>
##            payment payment:app          phone  ref ref:mevo shop was:amenity
## 2420642350    <NA>        <NA> +48 58 3428795 <NA>     <NA> <NA>        <NA>
## 2903387105    <NA>        <NA>           <NA> <NA>     4309 <NA>        <NA>
## 4317375214    <NA>        <NA>           <NA> <NA>     3984 <NA>        <NA>
## 6005303666    <NA>        <NA>           <NA> <NA>     3955 <NA>        <NA>
## 6005303667    <NA>        <NA>           <NA> <NA>     3998 <NA>        <NA>
## 6005303668    <NA>        <NA>           <NA> <NA>     3997 <NA>        <NA>
##            website                  geometry
## 2420642350    <NA> POINT (18.63211 54.40934)
## 2903387105    <NA> POINT (18.55065 54.47601)
## 4317375214    <NA> POINT (18.59841 54.38291)
## 6005303666    <NA> POINT (18.59223 54.38612)
## 6005303667    <NA> POINT (18.61126 54.37667)
## 6005303668    <NA> POINT (18.61106 54.37708)

3.3.1 Filtering to MEVO Stations Within Gdańsk

df_mevo <- df_rental_points %>% filter(brand == "MEVO")
df_mevo <- df_mevo[st_within(df_mevo, df_gdansk_borders, sparse = FALSE), ]
cat("Number of MEVO stations:", nrow(df_mevo))
## Number of MEVO stations: 424

424 MEVO bicycle rental stations were identified within the Gdańsk administrative boundary.

3.3.2 Map: MEVO Stations

df_mevo <- st_as_sf(df_mevo)

ggplot() +
  geom_sf(data = df_gdansk_borders, fill = "lightsteelblue1") +
  geom_sf(data = df_mevo, color = "royalblue4", size = 1) +
  labs(title = "MEVO bicycle rental stations in Gdańsk") +
  theme_minimal()

3.4 Bike Lane Data

query_bike_cycleways <- opq(bbox = area) %>%
  add_osm_feature(key = "cycleway", value = c("lane", "track"))
data_cycleways <- osmdata_sf(query_bike_cycleways)
df_cycleways   <- data_cycleways$osm_lines
df_cycleways   <- df_cycleways[st_within(df_cycleways, df_gdansk_borders, sparse = FALSE), ]

query_bike_highways <- opq(bbox = area) %>%
  add_osm_feature(key = "highway", value = "cycleway")
data_highways <- osmdata_sf(query_bike_highways)
df_highways   <- data_highways$osm_lines
df_highways   <- df_highways[st_within(df_highways, df_gdansk_borders, sparse = FALSE), ]

df_lanes <- bind_rows(df_cycleways, df_highways)

3.4.1 Map: MEVO Stations and Bike Lanes

ggplot() +
  geom_sf(data = df_gdansk_borders, fill = "lightsteelblue1") +
  geom_sf(data = df_lanes, color = "green3", linewidth = 0.4) +
  geom_sf(data = df_mevo, color = "royalblue4", size = 1) +
  labs(title = "MEVO stations and bike lanes in Gdańsk") +
  theme_minimal()

4 Analysis of Patterns Between the Points and Bike Lanes

4.1 Spatial Point Pattern Object

border     <- st_transform(df_gdansk_borders, 3857)
df_mevo    <- st_transform(df_mevo, 3857)
w          <- as.owin(border)
pattern_mevo <- ppp(
  x      = st_coordinates(df_mevo)[, 1],
  y      = st_coordinates(df_mevo)[, 2],
  window = w
)
pines_mevo <- rescale.ppp(pattern_mevo)

plot(st_geometry(border), col = "lightsteelblue1",
     main = "MEVO stations — spatial point pattern")
plot(pattern_mevo, col = "royalblue4", add = TRUE)

4.2 Kernel Density Estimation

d <- density(pines_mevo)

4.2.1 Density Map

plot(d, main = "Kernel density of MEVO rental stations")
plot(pines_mevo, add = TRUE, pch = 16, cex = 0.3)
contour(d, add = TRUE)

The KDE map indicates that MEVO stations are not evenly distributed across Gdańsk. The highest density is observed in the central and western-central parts of the city, where stations form a clear hotspot. Peripheral areas — particularly toward the eastern side — show much lower station density, suggesting spatial clustering rather than uniform distribution.

4.2.2 Perspective Plot

persp(d, theta = 30, phi = 20, col = "lightsteelblue1",
      main = "Perspective plot of station density")

The perspective plot confirms that most stations are concentrated in the city centre and the western coastal areas.

4.3 Nearest Neighbour Distance Analysis

nn <- nndist(pattern_mevo)
summary(nn)
##      Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
##     5.231   325.620   479.596   596.746   643.208 11654.193
hist(nn,
     main = "Nearest neighbour distances between MEVO stations",
     xlab = "Distance (m)",
     col  = "lightgray")

The histogram shows that the majority of MEVO stations have short nearest-neighbour distances, consistent with spatial clustering. Only a small proportion of stations are isolated far from the main network.

clarkevans.test(pattern_mevo)
## 
##  Clark-Evans test
##  CDF correction
##  Z-test
## 
## data:  pattern_mevo
## R = 0.6465, p-value < 2.2e-16
## alternative hypothesis: two-sided

The Clark-Evans test confirms significant spatial clustering (R = 0.65, p < 2.2e-16). It indicates that stations are on average closer to each other than expected under complete spatial randomness.

4.4 Ripley’s K Function

K.pines <- Kest(pines_mevo)
summary(K.pines)
##        r             theo               border              trans          
##  Min.   :   0   Min.   :        0   Min.   :        0   Min.   :        0  
##  1st Qu.:2288   1st Qu.: 16439031   1st Qu.: 81625802   1st Qu.: 81460894  
##  Median :4575   Median : 65756122   Median :245779070   Median :255882954  
##  Mean   :4575   Mean   : 87760450   Mean   :286265194   Mean   :308129833  
##  3rd Qu.:6863   3rd Qu.:147951276   3rd Qu.:483776744   3rd Qu.:513143250  
##  Max.   :9150   Max.   :263024490   Max.   :718000409   Max.   :813594952  
##       iso           
##  Min.   :        0  
##  1st Qu.: 75516025  
##  Median :228465051  
##  Mean   :262754848  
##  3rd Qu.:435245416  
##  Max.   :659710280
#plot(K.pines)
K_env <- envelope(pines_mevo, Kest, nsim = 99)
## Generating 99 simulations of CSR  ...
## 1, 2, 
## [10:16 remaining, estimate finish 2026-05-20 20:24:23]
## 3, 
## [10:32 remaining, estimate finish 2026-05-20 20:24:45]
## 4, 
## [10:29 remaining, estimate finish 2026-05-20 20:24:49]
## 5, 
## [10:24 remaining, estimate finish 2026-05-20 20:24:51]
## 6,  [9:48 remaining] 7,  [9:45 remaining] 8,  [9:38 remaining] 9,  [9:42 remaining] 10,  [9:47 remaining] 11,  [9:53 remaining] 12,  [9:43 remaining] 13,  [9:37 remaining] 14,  [9:20 remaining] 15,  [9:10 remaining] 16,  [9:08 remaining] 17,  [9:10 remaining] 18,  [8:55 remaining] 19,  [8:52 remaining] 20,  [8:45 remaining] 21,  [8:38 remaining] 22,  [8:36 remaining] 23,  [8:26 remaining] 24,  [8:20 remaining] 25,
##  [8:13 remaining] 26,  [8:09 remaining] 27,  [8:01 remaining] 28,  [7:54 remaining] 29,  [7:48 remaining] 30,  [7:42 remaining] 31,  [7:30 remaining] 32,  [7:23 remaining] 33,  [7:11 remaining] 34,  [7:07 remaining] 35,  [7:01 remaining] 36,  [6:54 remaining] 37,  [6:43 remaining] 38,  [6:36 remaining] 39,  [6:27 remaining] 40,  [6:22 remaining] 41,  [6:16 remaining] 42,  [6:10 remaining] 43,  [6:04 remaining] 44,  [5:58 remaining] 45,
##  [5:51 remaining] 46,  [5:44 remaining] 47,  [5:38 remaining] 48,  [5:32 remaining] 49,  [5:25 remaining] 50,  [5:19 remaining] 51,  [5:13 remaining] 52,  [5:03 remaining] 53,  [4:52 remaining] 54,  [4:46 remaining] 55,  [4:40 remaining] 56,  [4:35 remaining] 57,  [4:28 remaining] 58,  [4:24 remaining] 59,  [4:15 remaining] 60,  [4:06 remaining] 61,  [3:57 remaining] 62,  [3:49 remaining] 63,  [3:40 remaining] 64,  [3:33 remaining] 65,
##  [3:25 remaining] 66,  [3:18 remaining] 67,  [3:10 remaining] 68,  [3:03 remaining] 69,  [2:56 remaining] 70,  [2:50 remaining] 71,  [2:45 remaining] 72,  [2:39 remaining] 73,  [2:33 remaining] 74,  [2:28 remaining] 75,  [2:21 remaining] 76,  [2:15 remaining] 77,  [2:09 remaining] 78,  [2:03 remaining] 79,  [1:57 remaining] 80,  [1:52 remaining] 81,  [1:46 remaining] 82,  [1:40 remaining] 83,  [1:34 remaining] 84,  [1:28 remaining] 85,
##  [1:23 remaining] 86,  [1:17 remaining] 87,  [1:11 remaining] 88,  [1:05 remaining] 89,  [59 sec remaining] 90,  [53 sec remaining] 91,  [48 sec remaining] 92,  [42 sec remaining] 93,  [36 sec remaining] 94,  [30 sec remaining] 95,  [23 sec remaining] 96,  [17 sec remaining] 97,  [12 sec remaining] 98,  [6 sec remaining] 
## 99.
## 
## Done.
plot(K_env)

The observed K function (black line) lies far above the simulation envelope (grey band) across all distances, confirming that MEVO stations are significantly more clustered than a random pattern. The gap widens with increasing distance, suggesting clustering occurs at multiple spatial scales.

4.5 Point–Line Pattern: Stations and Bike Lanes

4.5.1 Distance from Bike Lane Network to Nearest MEVO Station

df_lanes <- st_transform(df_lanes, 3857)
L           <- as.linnet(as.psp(st_geometry(df_lanes)))
mevo_linnet <- lpp(st_coordinates(df_mevo), L = L)
dist_lin    <- distfun(mevo_linnet)
dist_lin_im <- as.linim(dist_lin)

plot(st_geometry(border), col = "lightsteelblue1",
     main = "Distance from bike lane network to nearest MEVO station")
plot(dist_lin_im, style = "colour", add = TRUE)

The network-distance map shows that most cycleway segments (dark blue) are within short distances of a MEVO station. However, several segments in the western and northern parts of Gdańsk display higher values, indicating parts of the cycling network less directly served by rental stations.

4.5.2 Distance from MEVO Stations to Nearest Bike Lane

nearest_lane_dist <- st_distance(df_mevo, df_lanes) %>%
  apply(1, min)

summary(as.numeric(nearest_lane_dist))
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
##    2.091   98.647  397.979  522.280  753.325 4066.214

The distance summary shows that most MEVO stations are located relatively close to cycling infrastructure. The median distance to the nearest bike lane is approximately 398 metres, and 75% of stations are within about 753 metres. The maximum distance exceeds 4 km, suggesting that a small number of stations are located far from the mapped network.

4.5.3 Histogram of Station-to-Lane Distances

cat("Number of bike lane segments:", nrow(df_lanes))
## Number of bike lane segments: 687
summary(st_length(df_lanes))
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
##    1.654   17.959   46.328  157.011  127.639 7008.846
hist(
  as.numeric(nearest_lane_dist),
  main = "Distance from MEVO stations to nearest bike lane",
  xlab = "Distance (metres)",
  col  = "lightgray"
)

The majority of MEVO stations are located within 500 metres of a bike lane, with the distribution strongly right-skewed. Only a small number of stations exceed 2,000 metres, indicating that most stations are well-integrated with the cycling network.

4.5.4 Poisson Point Process Model

df_lanes_model <- df_lanes %>%
  st_cast("LINESTRING", warn = FALSE)

lanes_psp    <- as.psp(st_geometry(df_lanes_model))
dist_lanes_im <- as.im(distfun(lanes_psp), W = w)
model_dist   <- ppm(pattern_mevo ~ dist_lanes_im)

coef(model_dist)
##   (Intercept) dist_lanes_im 
## -13.088107316  -0.001476234

The Poisson model reveals a negative coefficient for distance to bike lanes, indicating that station intensity decreases as distance from the cycling network increases. This is consistent with the visual evidence: MEVO stations tend to be placed near or alongside existing cycling infrastructure.

4.5.5 Model Residuals

plot(
  residuals.ppm(model_dist),
  main = "Residuals of the Poisson point process model"
)

The residual map shows that most of the city appears yellow, meaning the model overestimates station density — it predicts more stations than actually exist. The darker purple areas in the centre-west are where the model underestimates, i.e. stations are more concentrated there than distance to bike lanes alone can explain. This suggests that other factors, such as population density or city centre activity, also influence where stations are placed.

5 Conclusion

The analysis shows that MEVO stations in Gdańsk are spatially clustered rather than evenly distributed. Evidence from KDE, nearest neighbour distances, and Ripley’s K function all indicate that stations are concentrated in central and western areas of the city. The point–line analysis further shows that stations tend to be located near bike lanes, and the Poisson model confirms a statistically significant negative relationship between station intensity and distance to cycling infrastructure. Overall, MEVO stations appear to be well-integrated with the cycling network, although some peripheral areas remain less served. This analysis is limited by the completeness and tagging quality of OpenStreetMap data.