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

Author

Markus Kainu

Published

July 7, 2025

Päivitetty 2025-07-07: 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, 2024 ja 2025. 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] "./datat/data_2024.parquet"
[1] "./datat/data_2025.parquet"
[1] "./metadatat/tellingit_2017-2025.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.87M 7795205 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2018.parquet 18.49M 13604724 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2019.parquet 32.87M 23736787 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2020.parquet 35.54M 25956144 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2021.parquet 41.18M 27859010 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2022.parquet 45.65M 30366368 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2023.parquet 49.97M 30414260 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2024.parquet 48.45M 31019244 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2025.parquet 24.34M 14818374 16
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2025.parquet 904.26K 803552 7
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/metadata_data.parquet 1.37K 16 2
https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/metadata_tellingit.parquet 1016 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.4     ✔ tibble    3.3.0
✔ purrr     1.0.4     ✔ 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)
dat24 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2024.parquet") %>% filter(week == 24)
dat25 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2025.parquet") %>% filter(week == 25)

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

stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2025.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, 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

library(tidyverse)
library(sf)
stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2025.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"
                   ) %>% 
  addCircleMarkers(data = spatdat %>% filter(year == 2024),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2024"
                   ) %>%
  addCircleMarkers(data = spatdat %>% filter(year == 2025),
                   color = ~pal(year), 
                   label = ~paste(name, vuodet), 
                   group = "2025"
                   ) %>% 
    addLayersControl(
    baseGroups = c("2025","2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017"),
    options = layersControlOptions(collapsed = FALSE)
  )
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(max(3, n), palette): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors

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)
dat24 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2024.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)
dat25 <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/datat/data_2025.parquet") %>% 
  filter(week %in% 23:24,hour %in% 6:10)

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

stations <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2025.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 34 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 34 rows containing missing values or values outside the scale range
(`geom_point()`).

Ekstra

Olen laittanut jakoon myös parquet-tiedoston ( data_2017_2024.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_2024.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_2024.parquet')
  GROUP BY id;")

# telineiden nimet
asemat <- arrow::read_parquet("https://data.markuskainu.fi/opendata/kaupunkipyorat/metadatat/tellingit_2017-2025.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       35.78172     3       Hernesaarenranta
2  065       35.78172     3    Hylkeenpyytäjänkatu
3  710       32.00397     3   Kössi Koskisen aukio
4  240       27.08835     3   Viikin normaalikoulu
5  094       26.96408     3      Laajalahden aukio
6  241       25.73163     3          Agronominkatu
7  571       25.67346     3 Tapiolan urheilupuisto
8  401       25.28012     3         Koivusaari (M)
9  711       24.94854     3           Kirjurinkuja
10 012       24.63018     3            Kanavaranta
11 011       24.31046     3            Unioninkatu
tictoc::toc()
5.186 sec elapsed