Helsingin kaupunkipyörät: Avoin data telinekohtaisista vapaiden pyörien määristä kausilta 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024
Author
Markus Kainu
Published
November 11, 2024
Päivitetty 2024-11-11: 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, 2023 ja 2024. 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.
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 1282 071 16 12 1 NA NA 1 2017… 19 1283 072 14 5 1 NA NA 1 2017… 19 1284 073 8 6 1 NA NA 1 2017… 19 1285 074 15 5 1 NA NA 1 2017… 19 1286 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>
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)
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
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, 2022, 2023 ja 2024",subtitle ="Mukana `Töölö`-alkuiset telineet",y ="Vapaiden pyörien määrä", x =NULL, color ="tellinki")
II Tellinkien elinvuodet kartalla / Stations lifespan on map
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$datefmi_tmp <-list()for (i in1: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 outside the scale range
(`stat_smooth()`).
Warning: Removed 32 rows containing missing values or values outside the scale range
(`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.
dbGetQuery(con,"SELECT * FROM PARQUET_SCAN('https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2017_2024.parquet') LIMIT 10;") -> resultsas_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_2024.parquet') GROUP BY id;")# telineiden nimetasemat <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2024.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) )