Helsingin kaupunkipyörät: Avoin data telinekohtaisista vapaiden pyörien määristä kausilta 2017, 2018, 2019, 2020, 2021, 2022, 2023

Author

Markus Kainu

Published

February 19, 2024

Päivitetty 2024-02-19: datat julkaistaan kerran viikossa maanantaiaamuisin

Helsingissä on toiminut kaupunkipyöräjärjestelmä vuodesta 2016 alkaen. Järjestelmä tarjoaa avoimen rajapinnan, josta voi hakea reaaliaikaisen vapaiden pyörien määrän kullakin telineellä. Olen kerännyt tätä dataa viiden minuutin välein kaudet 2017, 2018, 2019, 2020, 2021, 2022 ja 2023. Tältä sivulta voit ladata nuo datat vapaasti käyttöösi Nimeä 4.0 Kansainvälinen (CC BY 4.0)-lisenssin ehdoilla.

There has been a public bike sharing scheme in Helsinki since 2016. System offers a public API that provides real-time data on availability of bikes at each station. I have collected that data every five minutes since season 2017. You can download the files for any use from this site according to Attribution 4.0 International (CC BY 4.0)-license.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.

Tällä projektilla ei ole mitään yhteyksiä HKL;ään, HSL:ään tai CityBikeFinland:iin! This project is not connected with HKL, HSL or CityBikeFinland!

Ladattavat tiedostot / files to download

Data: Vapaat pyörät / available bikes

[1] "./datat/data_2017.parquet"
[1] "./datat/data_2018.parquet"
[1] "./datat/data_2019.parquet"
[1] "./datat/data_2020.parquet"
[1] "./datat/data_2021.parquet"
[1] "./datat/data_2022.parquet"
[1] "./datat/data_2023.parquet"
[1] "./metadatat/tellingit_2017-2023.parquet"
[1] "./metadatat/metadata_data.parquet"
[1] "./metadatat/metadata_tellingit.parquet"
tiedosto tiedostokoko riveja sarakkeita
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017.parquet 11.86M 7795205 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2018.parquet 18M 13604724 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2019.parquet 28.89M 23736787 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2020.parquet 29.03M 25956144 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2021.parquet 39.17M 27859010 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2022.parquet 40.04M 30366368 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2023.parquet 51.14M 34020158 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2023.parquet 534.72K 613535 7
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/metadata_data.parquet 1.48K 16 2
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/metadata_tellingit.parquet 1.08K 7 2

Ensimmäiset rivit / First rows

  id    bikesAvailable spacesAvailable allowDropoff isFloatingBike state realTimeData time   week  yday
  <chr>          <dbl>           <dbl>        <dbl> <lgl>          <lgl>        <dbl> <chr> <dbl> <dbl>
1 070                4              10            1 NA             NA               1 2017…    19   128
2 071               16              12            1 NA             NA               1 2017…    19   128
3 072               14               5            1 NA             NA               1 2017…    19   128
4 073                8               6            1 NA             NA               1 2017…    19   128
5 074               15               5            1 NA             NA               1 2017…    19   128
6 075                3              17            1 NA             NA               1 2017…    19   128
# … with 6 more variables: day <dbl>, month <dbl>, hour <dbl>, minute <dbl>, yhour <dbl>, year <dbl>

Data: Tellingit / Stations

Ensimmäiset rivit / First rows

  id    name                        x     y time                 yday  year
  <chr> <chr>                   <dbl> <dbl> <chr>               <dbl> <dbl>
1 120   Mäkelänkatu              25.0  60.2 2017/05/09 12:00:03   129  2017
2 088   Kiskontie                24.9  60.2 2017/05/09 12:00:03   129  2017
3 121   Vilhonvuorenkatu         25.0  60.2 2017/05/09 12:00:03   129  2017
4 070   Sammonpuistikko          24.9  60.2 2017/05/09 12:00:03   129  2017
5 071   Hietaniemenkatu          24.9  60.2 2017/05/09 12:00:03   129  2017
6 072   Eteläinen Hesperiankatu  24.9  60.2 2017/05/09 12:00:03   129  2017

Metadatat / metadatas

Vapaat pyörät / available bikes

   varname         description                                                        
   <chr>           <chr>                                                              
 1 id              Station id (character)                                             
 2 bikesAvailable  Number of available bikes (integer)                                
 3 spacesAvailable Number of available spaces (integer)                               
 4 allowDropoff    Does the station allow dropoff of a bike: 1 = yes, 2 = no (integer)
 5 isFloatingBike  isFloatingBike (all values NA) (logical)                           
 6 state           state (all values NA) (logical)                                    
 7 realTimeData    realTimeData (integer)                                             
 8 time            Timestamp. Format 'yyyy/mm/dd hh:mm:ss' (character)                
 9 week            Week of the year (integer)                                         
10 yday            Day of the year (integer)                                          
11 day             Day of the month (integer)                                         
12 month           Month of the year (integer)                                        
13 hour            Hour of the day (integer)                                          
14 minute          Minute of the hour (integer)                                       
15 yhour           Hour of the year (integer)                                         
16 year            Year (integer)     

Tellingit / bike stations

 varname description                                        
  <chr>   <chr>                                              
1 id      Station id (character)                             
2 name    Station name  (character)                          
3 x       longitude (double)                                 
4 y       latitude (double)                                  
5 time    Timestamp. Format 'yyyy/mm/dd hh:mm:ss' (character)
6 yday    Day of the year (integer)                          
7 year    Year (integer)   

Esimerkkejä R-kielellä / Examples using R-language

I Vapaiden pyörien määrät viikolla 25 vuosina 2018-2022 tellingeillä, jotka alkavat sanalla ‘Töölö’

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ lubridate 1.9.3     ✔ tibble    3.2.1
✔ purrr     1.0.2     ✔ tidyr     1.3.1
✔ readr     2.1.5     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
dat17 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017.parquet") %>% filter(week == 24)
dat18 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2018.parquet") %>% filter(week == 24)
dat19 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2019.parquet") %>% filter(week == 24)
dat20 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2020.parquet") %>% filter(week == 24)
dat21 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2021.parquet") %>% filter(week == 24)
dat22 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2022.parquet") %>% filter(week == 24)
dat23 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2023.parquet") %>% filter(week == 24)


dat <- bind_rows(
  dat17,
  dat18,
  dat19,
  dat20,
  dat21,
  dat22,
  dat23
)

stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2023.parquet")

dat25 <- dat %>% 
  left_join(stations %>% 
              select(-time)) %>% 
  filter(grepl("Töölö", name)) %>% 
  mutate(time = as.POSIXct(time))
Joining with `by = join_by(id, yday, year)`
ggplot(dat25, aes(x = time, y = bikesAvailable, color = name, group = name)) +
  geom_line() +
  facet_wrap(~year, scales = "free_x", ncol = 1) +
  theme_minimal(base_family = "PT Sans") +
  labs(title = "Vapaiden pyörien määrä viikolla 24 vuosina 2017, 2018, 2019, 2020, 2021 ja 2022",
       subtitle = "Mukana `Töölö`-alkuiset telineet",
       y = "Vapaiden pyörien määrä", x = NULL, color = "tellinki")

II Tellinkien elinvuodet kartalla / Stations lifespan on map

library(tidyverse)
library(sf)
stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2023.parquet")
stations2 <- stations %>% 
  distinct(id,name,year, .keep_all = TRUE) %>%
  filter(x <= 30, y >= 55)
spatdat <- stations2 %>%  
  sf::st_as_sf(coords = c(3,4)) %>% 
  left_join(stations2 %>% 
              group_by(id) %>% 
              summarise(years = list(unique(year))) %>% 
              ungroup()
            ) %>% 
  group_by(id,name,year) %>% 
  mutate(vuodet =  glue::glue_collapse(sort(unlist(years)), sep = ", ", last = " ja ")) %>% 
  ungroup()


  
library(leaflet)
pal <- colorFactor(
    palette = "Set2",domain = unique(spatdat$year))
  
leaflet(spatdat) %>% 
  leaflet::addProviderTiles(providers$CartoDB.Positron) %>% 
  addCircleMarkers(data = spatdat %>% filter(year == 2017),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2017"
                   ) %>% 
    addCircleMarkers(data = spatdat %>% filter(year == 2018),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2018"
                   ) %>% 
      addCircleMarkers(data = spatdat %>% filter(year == 2019),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2019"
                   ) %>% 
        addCircleMarkers(data = spatdat %>% filter(year == 2020),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2020"
                   ) %>% 
          addCircleMarkers(data = spatdat %>% filter(year == 2021),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2021"
                   ) %>%
  addCircleMarkers(data = spatdat %>% filter(year == 2022),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2022"
                   ) %>% 
    addCircleMarkers(data = spatdat %>% filter(year == 2023),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2023"
                   ) %>% 
    addLayersControl(
    baseGroups = c("2023", "2022", "2021", "2020", "2019", "2018", "2017"),
    options = layersControlOptions(collapsed = FALSE)
  )

III Säätila ja aseman vilkkaus touko-kesäkuun taitteen aamuina Meilahden sairaalan ja Unioninkadun tellingeillä

library(tidyverse)
dat17 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat18 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2018.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat19 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2019.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat20 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2020.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat21 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2021.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat22 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2022.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat23 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2023.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)


dat <- bind_rows(
  dat17,
  dat18,
  dat19,
  dat20,
  dat21,
  dat22,
  dat23
)

stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2023.parquet")

mo <- dat %>% 
  left_join(stations %>% 
              select(-time)) %>% 
  filter(grepl("Meilahden|Unionin", name)) %>% 
  mutate(time = as.POSIXct(time)) %>% 
  group_by(id,name) %>% 
  mutate(freq = abs(bikesAvailable-lag(bikesAvailable))) %>% 
  ungroup()
Joining with `by = join_by(id, yday, year)`
mohour <- 
  mo %>% 
  group_by(id,name,year,week,month,yday,day,yhour,hour) %>% 
  summarise(freqs = sum(freq, na.rm = TRUE)) %>% 
  ungroup() %>% 
  arrange(yhour)
`summarise()` has grouped output by 'id', 'name', 'year', 'week', 'month',
'yday', 'day', 'yhour'. You can override using the `.groups` argument.
# Data from FMI
## Install fmi2 with remotes::install_github("ropengov/fmi2")
library(fmi2)
dat_date <- mo %>% 
  distinct(year,yday, .keep_all = TRUE) %>% 
  mutate(date = as.Date(time)) %>% 
  select(year,date)

library(glue)
dates <- dat_date$date
fmi_tmp <- list()
for (i in 1:length(dates)){  
  pvm <- dates[i]
  fmi_tmp[[pvm]] <- fmi2::obs_weather_hourly(starttime = glue("{pvm}T06:00:00Z"), 
                                             endtime = glue("{pvm}T10:00:00Z"), 
                                             fmisid = "100971") %>% 
    filter(variable %in% c("TA_PT1H_AVG","PRI_PT1H_MAX","WS_PT1H_AVG","RH_PT1H_AVG")) %>% 
    sf::st_set_geometry(value = NULL) %>% 
    as_tibble() %>% 
    mutate(var = case_when(
      variable == "TA_PT1H_AVG" ~ "Ilman lämpötila / Air temperature (C)",
      variable == "PRI_PT1H_MAX" ~ "Sateen intensiteetti / Maximum precipitation intensity (mm/h)",
      variable == "WS_PT1H_AVG" ~ "Tuulen nopeus / Wind speed (m/s)",
      variable == "RH_PT1H_AVG" ~ "Ilmankosteus / Relative humidity (%)"),
      station = "Kaisaniemi") %>%
    select(-variable)
}
fmi <- do.call(bind_rows, fmi_tmp) %>% 
  mutate(year = lubridate::year(time),
         yday = lubridate::yday(time),
         hour = lubridate::hour(time),
         yhour = (yday - 93) * 24 + hour) %>% 
  select(-hour,-yday)

df <- left_join(mohour, fmi) %>% 
  filter(!is.na(var))
Joining with `by = join_by(year, yhour)`
Warning in left_join(mohour, fmi): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 1 of `x` matches multiple rows in `y`.
ℹ Row 561 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
  "many-to-many"` to silence this warning.
ggplot(df, aes(x = value, y = freqs, color = factor(year), group = factor(year))) +
  geom_point(shape = 21, alpha = .6) +
  facet_grid(name~sub("/", "\n", var), scales = "free") +
  geom_smooth(method = "lm", se = FALSE) +
  theme_minimal(base_family = "PT Sans") +
  labs(y = "Lainauksia tai palautuksia tunnissa / Rentals or return per hour",
       x = NULL, color = NULL)
`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 32 rows containing non-finite values (`stat_smooth()`).
Warning: Removed 32 rows containing missing values (`geom_point()`).

Ekstra 2024

Olen laittanut jakoon myös parquet-tiedoston ( data_2017_2023.parquet ), jossa on yhdessä kaikki kerätty data, yhteensä 163 338 396 riviä. Datan lataaminen kokonaisuudessa R:ään vaatii ~50GB muistia, eikä siksi onnistu tavanomaisissa ympäristöissä.

duckdb-tietokantaohjelmisto mahdollistaa parquet-muotoisten datojen kyselyn http:n yli SQL:ää käyttäen ikäänkuin parquet olisi “avoimen datan rajapinta”. Alla pari esimerkki R:ssä miten tämä käy päinsä.

Aluksi luodaan muisinvarainen duckdb-tietokantayhteys ja asennetaan siihen httphs-laajennos.

library(DBI)
library(duckdb)
library(dplyr)

con <- dbConnect(duckdb())
dbExecute(con, "FORCE INSTALL httpfs")
dbExecute(con, "LOAD httpfs")

Haetaan ensin datan kymmenen ensimmäistä riviä.

dbGetQuery(con,
           "SELECT *
   FROM PARQUET_SCAN('https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017_2023.parquet')
   LIMIT 10;") -> results
as_tibble(results)
# A tibble: 10 × 16
   id    bikesAvailable spacesAvailable allowDropoff isFloatingBike state
   <chr>          <dbl>           <dbl>        <dbl>          <dbl> <chr>
 1 070                4              10            1             NA <NA> 
 2 071               16              12            1             NA <NA> 
 3 072               14               5            1             NA <NA> 
 4 073                8               6            1             NA <NA> 
 5 074               15               5            1             NA <NA> 
 6 075                3              17            1             NA <NA> 
 7 076                2              18            1             NA <NA> 
 8 110                8              12            1             NA <NA> 
 9 078                0              24            1             NA <NA> 
10 111                5              15            1             NA <NA> 
# ℹ 10 more variables: realTimeData <dbl>, time <dttm>, week <dbl>, yday <dbl>,
#   day <int>, month <dbl>, hour <int>, minute <dbl>, yhour <dbl>, year <dbl>

Sitten lasketaan kaikille telineille saatavilla olevien pyörien määrän keskiarvo koko aineistosta. Ja näytetään top10 joissa on keskimäärin ollut eniten vapaita pyöriä koko aikana. Ao. analyysi kestää vanhalla läppärillä ja kaapeli-internetillä noin puoli minuuttia!

tictoc::tic()
vapaa_avg <- dbGetQuery(con,
           "SELECT id,
          AVG(bikesAvailable) AS bikesAvailable
   FROM PARQUET_SCAN('https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017_2023.parquet')
  GROUP BY id;")

# telineiden nimet
asemat <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2023.parquet")
# datassa on paljon ns. testiasemia, otetaan vaan ne, joiden id:ssä on max kolme merkkiä
# ja sitten otetaan asemat joissa ollut eniten keskimäärin vapaita pyöriä
vapaa_avg %>% 
  mutate(nchar = nchar(id)) %>% 
  filter(nchar <= 3) %>%
  arrange(desc(bikesAvailable)) %>% 
  slice(1:10) %>% left_join(
    asemat %>% distinct(id,name)
  )
Joining with `by = join_by(id)`
    id bikesAvailable nchar                   name
1  065       36.24571     3       Hernesaarenranta
2  065       36.24571     3    Hylkeenpyytäjänkatu
3  710       33.42304     3   Kössi Koskisen aukio
4  094       27.72547     3      Laajalahden aukio
5  571       27.05003     3 Tapiolan urheilupuisto
6  241       26.11467     3          Agronominkatu
7  240       26.10464     3   Viikin normaalikoulu
8  401       25.54474     3         Koivusaari (M)
9  711       24.94854     3           Kirjurinkuja
10 012       24.94445     3            Kanavaranta
11 001       24.91055     3            Kaivopuisto
tictoc::toc()
290.354 sec elapsed