Authors: Nhi Pham, Adrianna Lazuga
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.
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.
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()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)
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.
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)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()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)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.
## 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.
##
## 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.
## 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
## 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.
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.
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.
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.
## Number of bike lane segments: 687
## 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.
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.
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.
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.