---
title: "Kolme eri lukua samasta asiasta"
subtitle: "Latausverkko jakaumana, osa 1: montako sähköauton latausasemaa Suomessa on?"
description: |
Sähköautojen latauspisteiden lukumäärä toistetaan uutisissa yhtenä lukuna.
Kun kaksi rekisteriä avataan ja katsotaan sisään, selviää ettei yksikään
niistä ole edes itsensä kanssa yksimielinen. Rakennamme asemamäärästä
jakauman ja kerromme matkalla, mitä rekistereistä löytyi.
date: 2026-09-01
categories: [avoin data, bayes, latausverkko, rekisterit, datan laatu]
---
```{r}
#| label: setup
#| include: false
#| cache: false
library(here)
library(tidyverse)
library(httr2)
library(sf)
library(stringdist)
library(ggdist)
library(patchwork)
library(qs2)
library(scales)
select <- dplyr::select
filter <- dplyr::filter
count <- dplyr::count
source(here("R", "theme_kristian.R"))
stopifnot(
"httr2 puuttuu tai on liian vanha" =
requireNamespace("httr2", quietly = TRUE) && packageVersion("httr2") >= "1.0.0"
)
knitr::opts_chunk$set(
echo = FALSE, warning = FALSE, message = FALSE,
fig.width = 9, fig.height = 5.5, dpi = 150
)
theme_set(theme_kristian())
pal <- c(punainen = "#e63946", turkoosi = "#2a9d8f", oranssi = "#f4a261",
navy = "#1d3557", sininen = "#457b9d")
data_dir <- here("data", "latausverkko")
dir.create(data_dir, showWarnings = FALSE, recursive = TRUE)
set.seed(20260811)
# Turvalliset poimijat: NULL ja puuttuva kenttä palautuvat NA:na
pluck_num <- function(x, ...) {
v <- purrr::pluck(x, ..., .default = NA)
if (is.null(v) || length(v) == 0) NA_real_ else as.numeric(v)
}
pluck_chr <- function(x, ...) {
v <- purrr::pluck(x, ..., .default = NA)
if (is.null(v) || length(v) == 0) NA_character_ else as.character(v)
}
normalisoi <- function(x) {
x |> str_to_lower() |> str_replace_all("[^a-zåäö0-9 ]", " ") |> str_squish()
}
```
## Luku, jota kukaan ei ole tarkistanut
Kun latausinfrasta puhutaan, keskustelussa kelluu aina yksi luku. Latauspisteitä
on tuhansia. Tavoite on kymmeniä tuhansia. EU:n AFIR-asetus edellyttää tiettyä
tehoa tietyin välein. Luvut esitetään ilman epävarmuutta, ikään kuin joku olisi
käynyt laskemassa ne.
Kukaan ei ole käynyt laskemassa niitä. Ja mikä olennaisempaa: **kysymykseen ei
ole yhtä oikeaa vastausta, ennen kuin kysyjä kertoo mitä hän laskee.**
Tämä ei ole saivartelua. Kun avaan Fintrafficin ylläpitämän kansallisen
latausverkkoaineiston ja lasken rivit kolmella eri tasolla, saan kolme lukua,
jotka eroavat toisistaan yli viisinkertaisesti. Kaikki kolme ovat oikein.
Tämä postaus aloittaa kolmentoista osan sarjan, jossa Suomen latausverkosta
rakennetaan graafi ja etsitään sen murtumiskohdat. Ennen kuin verkkoa voi
analysoida, on tiedettävä mistä se koostuu. Osoittautuu, että kysymys on
odotettua hankalampi — ja hankaluus on itsessään tulos.
## Aineistot
Käytän kahta lähdettä, jotka ovat syntyneet täysin eri tavoin.
**Open Charge Map** on maailmanlaajuinen, yhteisön ylläpitämä rekisteri. Osa
tiedoista tulee vapaaehtoisilta, osa tuodaan automaattisesti operaattoreiden
syötteistä. Rajapinta on avoin ja vaatii ilmaisen API-avaimen.
**Fintrafficin AFIR-rajapinta** on kansallinen kooste, johon latausoperaattoreilla
on lakisääteinen velvollisuus toimittaa tietonsa. Se julkaistaan Digitrafficin
kautta OCPI-standardiin pohjautuvassa muodossa.
Ensimmäinen on joukkoistettu, toinen viranomaisvetoinen. Ensimmäisessä havainto
syntyy siitä, että joku huomaa aseman. Toisessa siitä, että operaattori täyttää
velvollisuutensa. Nämä ovat erilaisia havaintomekanismeja, ja juuri se tekee
niiden vertailusta hyödyllistä.
```{r}
#| label: ocm-fetch
#| echo: true
#| cache: false
ocm_path <- file.path(data_dir, "ocm_raw.qs")
hae_ocm <- function(maxresults = 50000) {
avain <- Sys.getenv("OCM_API_KEY")
stopifnot("OCM_API_KEY puuttuu .Renviron-tiedostosta" = nzchar(avain))
resp <- request("https://api.openchargemap.io/v3/poi/") |>
req_headers(`X-API-Key` = avain,
`User-Agent` = "kristianvepsalainen.com / latausverkko") |>
req_url_query(countrycode = "FI", output = "json", compact = "false",
verbose = "false", maxresults = maxresults) |>
req_retry(max_tries = 3) |>
req_perform()
stopifnot("OCM palautti muun kuin 200" = resp_status(resp) == 200)
poi <- resp_body_json(resp)
# Hiljainen katkaisu näyttäisi täysin normaalilta aineistolta,
# joten se on tarkistettava erikseen.
stopifnot(
"OCM-vastaus on katkaistu: kasvata maxresults" = length(poi) < maxresults,
"OCM palautti tyhjän vastauksen" = length(poi) > 0
)
poi
}
if (!file.exists(ocm_path)) {
ocm_raw <- hae_ocm()
qs2::qs_save(ocm_raw, ocm_path)
} else {
ocm_raw <- qs2::qs_read(ocm_path)
}
```
```{r}
#| label: afir-fetch
#| echo: true
#| cache: false
afir_path <- file.path(data_dir, "afir_raw.qs")
afir <- function(polku, cursor = NULL) {
req <- request("https://afir.digitraffic.fi") |>
req_url_path_append(polku) |>
req_headers(`Digitraffic-User` = "kristianvepsalainen.com/latausverkko") |>
req_retry(max_tries = 3)
if (!is.null(cursor)) req <- req_url_query(req, cursor = cursor)
resp <- req_perform(req)
stopifnot("AFIR palautti muun kuin 200" = resp_status(resp) == 200)
resp_body_json(resp)
}
# Sivutus on kursoripohjaista. Käytän sitä koosteen (/all) sijaan, koska
# sivu kerrallaan eteneminen paljastaa jos rajapinta jää silmukkaan.
hae_kaikki <- function(polku, max_sivuja = 200) {
sivut <- list(); cursor <- NULL; i <- 0
repeat {
i <- i + 1
stopifnot("Sivutus ei pääty – tarkista nextCursor" = i <= max_sivuja)
v <- afir(polku, cursor)
sivut[[i]] <- v
cursor <- purrr::pluck(v, "pagination", "nextCursor", .default = NULL)
if (is.null(cursor)) break
}
sivut
}
if (!file.exists(afir_path)) {
afir_raw <- list(
operaattorit = afir("/api/charging-network/v1/operators"),
sijainnit = hae_kaikki("/api/charging-network/v1/locations")
)
qs2::qs_save(afir_raw, afir_path)
} else {
afir_raw <- qs2::qs_read(afir_path)
}
```
## Ensimmäinen kerros: mitä yksi rivi tarkoittaa?
AFIR-aineisto on kolmitasoinen. Ylimpänä on **sijainti** — esimerkiksi
kauppakeskuksen parkkihalli. Sen alla on yksi tai useampi **latauslaite**,
standardin kielellä EVSE. Ja laitteen alla on **konnektorit**, eli fyysiset
pistokkeet.
Yksi latauspylväs, jossa on sekä CHAdeMO- että CCS-pistoke, on yksi laite ja
kaksi konnektoria. Kymmenen pylvään latauskenttä on yksi sijainti ja kymmenen
laitetta.
```{r}
#| label: afir-litista
#| echo: true
piirteet <- purrr::list_flatten(
map(afir_raw$sijainnit, \(s) purrr::pluck(s, "features", .default = list()))
)
stopifnot("Piirteitä ei löytynyt" = length(piirteet) > 0)
nap_sijainnit <- map_dfr(piirteet, \(f) {
p <- f$properties
tibble(
raaka_id = pluck_chr(p, "id"),
nimi = pluck_chr(p, "name"),
op_id = pluck_chr(p, "operator", "id"),
op_nimi = pluck_chr(p, "operator", "details", "name"),
kunta = pluck_chr(p, "address", "city"),
postinro = pluck_chr(p, "address", "postalCode"),
aina_auki = as.logical(purrr::pluck(p, "openingTimes", "twentyFourSeven",
.default = NA)),
luotu = pluck_chr(p, "createdAt"),
muokattu = pluck_chr(p, "modifiedAt"),
lon = pluck_num(f, "geometry", "coordinates", 1),
lat = pluck_num(f, "geometry", "coordinates", 2),
.rows = 1
)
}) |>
mutate(avain = paste(op_id, raaka_id, sep = "|"))
nap_kon <- map_dfr(piirteet, \(f) {
p <- f$properties
avain <- paste(pluck_chr(p, "operator", "id"), pluck_chr(p, "id"), sep = "|")
map_dfr(purrr::pluck(p, "evses", .default = list()), \(e) {
map_dfr(purrr::pluck(e, "connectors", .default = list()), \(k) tibble(
avain,
evse_id = pluck_chr(e, "id"),
standardi = pluck_chr(k, "standard"),
virta = pluck_chr(k, "powerType"),
muoto = pluck_chr(k, "format"),
volttia = pluck_num(k, "maxVoltage"),
ampeeria = pluck_num(k, "maxAmperage"),
# maxElectricPower on OCPI:ssa watteja, ei kilowatteja
teho_kw = pluck_num(k, "maxElectricPower") / 1000,
tariffi = pluck_chr(k, "tariffIds", 1)
))
})
})
stopifnot(
"Sijaintitaulu on tyhjä" = nrow(nap_sijainnit) > 0,
"Konnektoritaulu on tyhjä" = nrow(nap_kon) > 0,
"Koordinaatteja puuttuu" = all(!is.na(nap_sijainnit$lat)),
"Koordinaatit Suomen ulkopuolella" =
all(between(nap_sijainnit$lat, 59.4, 70.2) &
between(nap_sijainnit$lon, 18.9, 31.7))
)
kolme_lukua <- c(
sijainteja = nrow(nap_sijainnit),
laitteita = n_distinct(nap_kon$evse_id),
konnektorit = nrow(nap_kon)
)
```
Kolme lukua samasta aineistosta:
**`r format(kolme_lukua[["sijainteja"]], big.mark = " ")` sijaintia,
`r format(kolme_lukua[["laitteita"]], big.mark = " ")` latauslaitetta ja
`r format(kolme_lukua[["konnektorit"]], big.mark = " ")` pistoketta.**
Kun uutisessa sanotaan, että latauspisteitä on noin
`r round(kolme_lukua[["laitteita"]] / 1000)` 000, se on tosi. Kun sanotaan, että
latausasemia on noin `r round(kolme_lukua[["sijainteja"]] / 1000)` 000, sekin on
tosi. Kumpikaan ei ole väärin. Ne mittaavat eri asiaa, eikä kumpikaan yleensä
kerro kumpaa tarkoitetaan.
Päättäjälle tällä on suora seuraus. Jos tavoitteeksi asetetaan "50 000
latauspistettä vuoteen 2030 mennessä", tavoite on kolmella eri tulkinnalla
kolmen eri kokoinen. Ero ei ole prosentteja vaan kertaluokka.
```{r}
#| label: kuva-tasot
#| fig-cap: "Sama aineisto kolmella eri laskentatasolla."
tibble(
taso = factor(c("Sijainteja\n(latauskenttiä)", "Latauslaitteita\n(pylväitä)",
"Pistokkeita\n(konnektoreita)"),
levels = c("Sijainteja\n(latauskenttiä)",
"Latauslaitteita\n(pylväitä)",
"Pistokkeita\n(konnektoreita)")),
n = as.numeric(kolme_lukua)
) |>
ggplot(aes(taso, n)) +
geom_col(fill = pal["navy"], width = 0.6) +
geom_text(aes(label = format(n, big.mark = " ")), vjust = -0.6,
colour = "grey85", size = 4) +
scale_y_continuous(labels = label_number(big.mark = " "),
expand = expansion(mult = c(0, 0.15))) +
labs(x = NULL, y = NULL,
title = "Kolme oikeaa vastausta samaan kysymykseen",
caption = "Lähde: Fintraffic AFIR / Digitraffic")
```
## Toinen kerros: rekisteri ei ole yksimielinen itsensä kanssa
Valitaan taso — sanotaan sijainti, koska verkostoanalyysissä solmu on paikka
johon ajetaan, ei pistoke johon kytketään. Nyt pitäisi voida laskea rivit.
Ei pitäisi.
### Sama tunniste, kaksi eri asemaa
```{r}
#| label: id-tormaykset
#| echo: true
tormaykset <- nap_sijainnit |> group_by(raaka_id) |> filter(n() > 1) |> ungroup()
n_tormays_ryhmia <- n_distinct(tormaykset$raaka_id)
n_tormays_riveja <- nrow(tormaykset)
# Ovatko rivit saman aseman toistoja vai aidosti eri asemia?
tormays_luonne <- tormaykset |>
group_by(raaka_id) |>
summarise(eri_nimia = n_distinct(nimi), eri_op = n_distinct(op_id),
.groups = "drop")
kaikki_eri <- all(tormays_luonne$eri_nimia > 1 & tormays_luonne$eri_op > 1)
tunnistetyyli <- nap_sijainnit |>
mutate(numeerinen = str_detect(raaka_id, "^[0-9]+$")) |>
group_by(op_id, op_nimi) |>
summarise(n = n(), numeerisia = sum(numeerinen), .groups = "drop")
n_numeerisia <- sum(tunnistetyyli$numeerisia)
n_op_numeerisia <- sum(tunnistetyyli$numeerisia > 0)
```
Aineistossa on **`r n_tormays_ryhmia` tunnistetta, jotka viittaavat useampaan
kuin yhteen sijaintiin**, yhteensä `r n_tormays_riveja` riviä. Kaikissa
tapauksissa nimi, koordinaatit ja operaattori ovat eri — kyse ei siis ole saman
rivin toistumisesta vaan aidosti eri asemista, jotka jakavat tunnisteen.
Tunniste `10` on sekä St1:n asema Pihtiputaalla että Evenlin asema Nokialla.
Tunniste `29` on St1 Porvoossa ja Wattery Muhoksella.
Syy näkyy tunnisteiden muodossa. Suurin osa on satunnaisia merkkijonoja, mutta
törmäävät ovat pieniä kokonaislukuja. `r n_op_numeerisia` operaattoria —
yhteensä `r n_numeerisia` sijaintia — käyttää omia juoksevia numeroitaan, jotka
on välitetty koosteeseen sellaisenaan. OCPI-standardissa sijainnin yksilöi vasta
kolmikko maakoodi, osapuolitunnus ja sijaintitunnus, mutta koosteen
tunnistekenttä sisältää vain viimeisen.
Korjaus on rakentaa yhdistelmäavain itse. Se on triviaali — mutta vain jos
huomaa, että se tarvitaan. Kuka tahansa, joka lukee aineiston tietokantaan
sijaintitunniste avaimena, saa hiljaisesti väärän tuloksen.
### Toistuvat konnektorit
```{r}
#| label: kon-toistot
#| echo: true
n_ennen <- nrow(nap_kon)
# Rajapinta toistaa osan konnektoreista sellaisenaan. Sarakkeet
# luetellaan nimeltä eikä käytetä pelkkää kolmikkoa avain–laite–standardi,
# koska aidossa kaksipistokkeisessa laturissa sama standardi voi
# esiintyä eri teholla.
nap_kon <- nap_kon |>
distinct(avain, evse_id, standardi, virta, muoto,
volttia, ampeeria, teho_kw, tariffi, .keep_all = TRUE)
n_toistoja <- n_ennen - nrow(nap_kon)
stopifnot(
"Yli 1 % konnektoririveistä oli toistoja" = n_toistoja / n_ennen < 0.01,
"Konnektoririvit eivät ole uniikkeja puhdistuksen jälkeen" =
!any(duplicated(select(nap_kon, avain, evse_id, standardi, teho_kw))),
"Yhdistelmäavaimessa duplikaatteja" = !any(duplicated(nap_sijainnit$avain))
)
```
Rajapinta palautti `r n_toistoja` konnektoririviä kahteen kertaan täysin
identtisinä, tariffitunnistetta myöten. Määrä on mitätön, mutta korjaus on tehtävä
ennen kuin mitään lasketaan.
### Sama laite, kaksi eri asemaa
```{r}
#| label: evse-jaetut
#| echo: true
jaetut_evse <- nap_kon |>
group_by(evse_id) |>
filter(n_distinct(avain) > 1) |>
ungroup()
n_jaettuja <- n_distinct(jaetut_evse$evse_id)
n_paallekkaisia <- n_distinct(jaetut_evse$avain)
```
`r n_jaettuja` latauslaitetta on kirjattu **kahden eri sijainnin alle**. Turun
Toriparkissa sama laite on sekä "ylempi krs" että "K1". Espoossa sama neljä
laitetta on sekä "Toyota Espoo" että "Toyota Espoo Julkinen".
Nämä eivät ole kaksi asemaa vaan yksi asema kahdella kirjauksella. Sijaintien
lukumäärä on siis yliarvio, ja ainoa keino havaita se on mennä tasoa alemmas ja
katsoa laitetunnisteita.
### Sama operaattori, kolme eri nimeä
```{r}
#| label: op-nimet
#| echo: true
ops <- map_dfr(afir_raw$operaattorit, \(o) tibble(
op_id = pluck_chr(o, "id"),
op_virallinen = pluck_chr(o, "details", "name"),
maakoodi = pluck_chr(o, "countryCode")
))
nimivariantit <- nap_sijainnit |>
distinct(op_id, op_nimi) |>
count(op_id, name = "nimia") |>
filter(nimia > 1) |>
left_join(ops, by = "op_id") |>
arrange(desc(nimia))
stopifnot(
"Sijainneissa operaattori jota ei ole rekisterissä" =
length(setdiff(unique(nap_sijainnit$op_id), ops$op_id)) == 0,
"Rekisterissä operaattori jolla ei ole sijainteja" =
length(setdiff(ops$op_id, unique(nap_sijainnit$op_id))) == 0
)
```
Operaattorin nimi on vapaa tekstikenttä, ja se näkyy. Sama tunnus esiintyy
kirjoitusasuissa, jotka eroavat yhden kirjaimen kokoisesti. Yksi operaattori on
lisäksi eri niminen operaattorirajapinnassa kuin sijaintiaineistossa.
Avain on siis tunnus, ei nimi. Tämä on tavanomainen datankäsittelyn opetus, mutta
se kannattaa sanoa ääneen, koska nimen käyttäminen avaimena olisi tässä
tuottanut kolme operaattoria siellä missä on yksi.
### Ja yksi operaattori ei ole operaattori
```{r}
#| label: alusta
#| echo: true
alustatunnus <- nimivariantit |> slice_max(nimia, n = 1)
alustan_asiakkaat <- nap_sijainnit |>
filter(op_id == alustatunnus$op_id[1]) |>
count(op_nimi, sort = TRUE)
```
Yhden operaattoritunnuksen alla esiintyy `r nrow(alustan_asiakkaat)` eri
toimijan nimeä: suuri latausoperaattori ja sen lisäksi joukko pieniä toimijoita
— isännöintiyhtiö, autokauppa, teknologiayritys.
Kyse ei ole virheestä. Kyse on siitä, että **alustatoimittaja hoitaa
AFIR-ilmoituksen asiakkaidensa puolesta**. Pieni isännöintiyhtiö ei ole tehnyt
itsenäistä päätöstä ilmoittaa latauspisteensä kansalliseen rekisteriin. Se on
valinnut latausoperaattorinsa, ja operaattori on hoitanut loput.
Tällä on suora seuraus siihen, mitä seuraavaksi teen. Jos halutaan arvioida
kuinka moni asema puuttuu rekisteristä, ei voi olettaa että jokainen asema
päätyy sinne itsenäisesti. Havaitseminen ei tapahdu asema kerrallaan eikä
edes toimija kerrallaan, vaan alusta kerrallaan.
```{r}
#| label: kanonisointi
#| echo: true
# Puretaan päällekkäiskirjaukset: jos kaksi sijaintia jakaa latauslaitteen,
# ne ovat sama asema. Yhdistetään ne ensimmäisen avaimen alle.
evse_ryhmat <- nap_kon |>
distinct(avain, evse_id) |>
group_by(evse_id) |>
mutate(ryhma = min(avain)) |>
ungroup() |>
distinct(avain, ryhma) |>
group_by(avain) |>
summarise(kanoninen = min(ryhma), .groups = "drop")
nap <- nap_sijainnit |>
left_join(evse_ryhmat, by = "avain") |>
mutate(kanoninen = coalesce(kanoninen, avain),
luotu_apu = coalesce(luotu, muokattu)) |>
group_by(kanoninen) |>
slice_min(luotu_apu, n = 1, with_ties = FALSE) |>
ungroup() |>
select(-luotu_apu)
n_poistettu <- nrow(nap_sijainnit) - nrow(nap)
stopifnot(
"Kanonisointi poisti epäuskottavan monta riviä" =
n_poistettu / nrow(nap_sijainnit) < 0.05
)
```
### Tehokenttä ja sen naapurit
```{r}
#| label: tehot
#| echo: true
# Yli 400 kW on henkilöautolatauksessa epäuskottava, mutta raskaan
# kaluston pikalataus yltää korkeammalle. Erotellaan nämä toisistaan
# pistoketyypin perusteella.
nap_kon <- nap_kon |>
mutate(
raskas = teho_kw >= 350 & standardi %in% c("IEC_62196_T2_COMBO", "MCS"),
teho_epailyttava = teho_kw > 400 &
standardi %in% c("DOMESTIC_F", "DOMESTIC_H")
)
n_epailyttavia <- sum(nap_kon$teho_epailyttava, na.rm = TRUE)
stopifnot(
"Kotitalouspistoke mahdottomalla teholla useammin kuin odotettua" =
mean(nap_kon$teho_epailyttava, na.rm = TRUE) < 0.005
)
nap_kon <- nap_kon |>
mutate(teho_kw = if_else(teho_epailyttava, NA_real_, teho_kw))
stopifnot(
"Tehoja yli 600 kW myös roskan poiston jälkeen" =
all(nap_kon$teho_kw <= 600 | is.na(nap_kon$teho_kw)),
"Negatiivisia tehoja" = all(nap_kon$teho_kw >= 0, na.rm = TRUE)
)
```
Aineistossa on `r n_epailyttavia` riviä, joissa kotitalouspistorasialle on
merkitty yli 400 kilowatin teho — yhdessä tapauksessa 1 200 kilowattia.
Kotitalouspistorasiasta saa noin kaksi. Nämä on merkitty puuttuviksi.
Sen sijaan CCS-pistokkeiden 560 kilowatin merkinnät satamissa ja
liikenneasemilla ovat uskottavia: ne ovat raskaan kaluston latureita, joita
Suomeen on rakennettu. Ne säilytetään mutta merkitään erikseen, koska ne eivät
kuulu henkilöautoverkkoon.
## OCM:n puolella samat ongelmat, eri muodossa
```{r}
#| label: ocm-litista
#| echo: true
ocm <- map_dfr(ocm_raw, function(p) {
liitokset <- purrr::pluck(p, "Connections", .default = list())
tibble(
ocm_id = pluck_num(p, "ID"),
nimi = pluck_chr(p, "AddressInfo", "Title"),
kunta = pluck_chr(p, "AddressInfo", "Town"),
postinro = pluck_chr(p, "AddressInfo", "Postcode"),
lat = pluck_num(p, "AddressInfo", "Latitude"),
lon = pluck_num(p, "AddressInfo", "Longitude"),
operaattori = pluck_chr(p, "OperatorInfo", "Title"),
julkinen = as.logical(purrr::pluck(p, "UsageType", "IsPublicAccess",
.default = NA)),
toiminnassa = as.logical(purrr::pluck(p, "StatusType", "IsOperational",
.default = NA)),
datalahde = pluck_num(p, "DataProviderID"),
pisteita = pluck_num(p, "NumberOfPoints"),
paivitetty = pluck_chr(p, "DateLastStatusUpdate"),
varmistettu = pluck_chr(p, "DateLastVerified"),
liitokset = list(map_dfr(liitokset, \(c) tibble(
pistoketyyppi = pluck_chr(c, "ConnectionType", "Title"),
teho_kw = pluck_num(c, "PowerKW"),
lkm = pluck_num(c, "Quantity")
))),
.rows = 1
)
}) |>
mutate(
max_kw = map_dbl(liitokset, \(d)
if (nrow(d) == 0 || all(is.na(d$teho_kw))) NA_real_
else max(d$teho_kw, na.rm = TRUE)),
hpc = !is.na(max_kw) & max_kw >= 150
)
# Roskatietueet. Sanaraja \\btest\\b on tarkoituksellinen: pelkkä "test"
# osuisi paikannimiin ja poistaisi oikeita asemia hiljaisesti.
ocm_luokiteltu <- ocm |>
mutate(hylkaysyy = case_when(
lat == 0 & lon == 0 ~ "nollakoordinaatit",
!(between(lat, 59.4, 70.2) & between(lon, 18.9, 31.7)) ~ "Suomen ulkopuolella",
str_detect(str_to_lower(nimi), "\\btest\\b|testi|demo|dummy|sample") ~ "testitietue",
TRUE ~ NA_character_
))
hylatyt <- ocm_luokiteltu |> filter(!is.na(hylkaysyy)) |> count(hylkaysyy)
stopifnot(
"Yli 2 % tietueista hylättiin – tarkista haun rajaus" =
sum(hylatyt$n) / nrow(ocm) < 0.02,
"OCM-tunnisteissa duplikaatteja" = !any(duplicated(ocm$ocm_id))
)
ocm <- ocm_luokiteltu |> filter(is.na(hylkaysyy)) |> select(-hylkaysyy)
```
Hylättyjä tietueita on `r sum(hylatyt$n)`. Osa on koordinaateiltaan mahdottomia:
yksi asema väittää sijaitsevansa Tyynellämerellä, toinen Norjanmeren
pohjoisosassa. Yksi on pisteessä nolla-nolla — Guineanlahdella, mikä on
käytännössä aina merkki siitä, että sijainti puuttuu ja jokin järjestelmä on
täyttänyt tyhjän kentän nollilla.
Loput ovat testitietueita, joita joku on syöttänyt rajapintaa kokeillessaan
eikä ole poistanut. Yksi niistä on koordinaateiltaan täysin uskottava ja olisi
läpäissyt jokaisen geometrisen tarkistuksen — sen löytämiseen tarvittiin
nimihaku.
### Kenttä, joka näyttää laadunvarmistukselta
```{r}
#| label: varmistus
#| echo: true
varmistustesti <- ocm |>
filter(!is.na(varmistettu), !is.na(paivitetty)) |>
mutate(ero_h = as.numeric(difftime(
as.POSIXct(varmistettu, tz = "UTC"),
as.POSIXct(paivitetty, tz = "UTC"), units = "hours"))) |>
summarise(n = n(), sama_hetki = mean(abs(ero_h) < 1),
mediaani_ero = median(ero_h))
```
OCM:ssä on kenttä nimeltä "viimeksi varmistettu". Se kuulostaa siltä, että joku
olisi käynyt tarkistamassa aseman olemassaolon.
Kaikilla `r format(varmistustesti$n, big.mark = " ")` asemalla varmistuspäivä on
**täsmälleen sama kuin päivityspäivä**. Mediaaniero on nolla tuntia, eikä
hajontaa ole lainkaan. Kenttä päivittyy automaattisesti aina kun syöte
päivittyy.
Se ei siis mittaa tiedon oikeellisuutta vaan integraation aktiivisuutta. Ero on
olennainen: rekisterissä on kenttä, joka näyttää laadunvarmistukselta mutta ei
ole sitä, ja jokainen joka lukee sen nimen mukaan päättelee väärin.
```{r}
#| label: datalahteet
#| echo: true
lahteet <- ocm |> count(datalahde, sort = TRUE) |> mutate(osuus = n / sum(n))
```
OCM:n Suomen aineisto tulee kolmesta lähteestä: käsin syötetyistä
yhteisömerkinnöistä, automaattisesta OCPI-tuonnista ja pohjoismaisesta
NOBIL-rekisteristä. Nämä olisivat periaatteessa erilaisia havaintomekanismeja,
mutta OCM yhdistää päällekkäiset tietueet ennen julkaisua — sama asema näkyy
kerran, ei kolmesti. Lähteiden välistä peittävyyttä ei siis voi mitata OCM:n
sisältä, ja siihen tarvitaan ulkopuolinen vertailukohta.
## Kolmas kerros: kuinka moni puuttuu molemmista?
Vasta nyt päästään alkuperäiseen kysymykseen.
### Menetelmä: miten hylkeitä lasketaan
Biologi haluaa tietää, montako hyljettä lahdessa on. Hän pyydystää sata,
merkitsee ne ja päästää vapaaksi. Viikon päästä hän pyydystää sata uudestaan.
Niistä kaksikymmentä on merkittyjä. Jos toisessa pyynnissä viidennes osui
merkittyihin, merkityt sata ovat noin viidennes populaatiosta — hylkeitä on siis
noin viisisataa.
Menetelmä on nimeltään merkintä-takaisinpyynti. Sitä käytetään ekologian lisäksi
epidemiologiassa aliraportoinnin korjaamiseen, ihmisoikeustyössä konfliktien
uhrilukujen arviointiin ja ohjelmistotestauksessa löytämättömien virheiden
määrän arviointiin.
Latausasemien kohdalla hylkeitä ovat asemat, ensimmäinen pyynti on OCM ja toinen
AFIR-kooste. Päällekkäisyys kertoo, kuinka moni asema ei ole kummassakaan.
### Miksi naiivi laskenta on tässä väärin
Menetelmä olettaa, että jokainen asema päätyy rekisteriin toisistaan
riippumatta. Edellinen luku osoitti, ettei näin ole. Operaattori joko toimittaa
tietonsa AFIR-koosteeseen tai ei — ja jos toimittaa, käytännössä kaikki sen
asemat tulevat kerralla. Alustatoimittajan tapauksessa päätös tehdään vielä
tasoa ylempänä.
Havaintoja ei siis ole tuhansia vaan kymmeniä. Naiivi laskenta olettaa
tuhansia riippumattomia kolikonheittoja siellä missä niitä on muutamia
kymmeniä, ja tuottaa siksi aivan liian kapean epävarmuusvälin.
Oikea rakenne on kaksitasoinen. Ensin arvioidaan, kuinka moni **operaattori**
puuttuu. Sitten arvioidaan, kuinka monta asemaa puuttuvilla operaattoreilla on,
käyttäen havaittua operaattorikokojen jakaumaa.
### Operaattorit linkitetään käsin
```{r}
#| label: op-linkitys
#| echo: true
ocm_operaattorit <- ocm |>
filter(!is.na(operaattori)) |>
distinct(operaattori)
# Kahdenkymmenen toimijan linkitys tehdään käsin. Sumea nimivertailu ei
# tunnista, että "VIRTA" ja "Liikennevirta" ovat sama yhtiö, eikä erota
# operaattoria merkinnästä aseman hallinnasta.
vastaavuudet_op <- tibble::tribble(
~ocm, ~afir,
"ABC Lataus (FI)", "ABC",
"Ionity", "IONITY",
"K Lataus", "K-lataus",
"Lidl", "Lidl",
"Neste Lataus", "Neste Markkinointi Oy",
"Plugit", "Plugit Finland oy",
"Recharge (Formerly Fortum Charge & Drive)", "Recharge",
"Tesla (Tesla-only charging)", "Tesla",
"VIRTA", "Liikennevirta",
"eparking (Finland)", "eParking"
)
# Merkinnät, jotka eivät ole operaattoreita vaan tietoa aseman
# hallinnasta, tai saman toimijan toisia merkintöjä.
ei_operaattoreita <- c(
"(Business Owner at Location)",
"(Private Residence/Individual)",
"Charge & Drive (Fortum - NO)", # Rechargen entinen nimi
"Tesla (including non-tesla)" # sama yhtiö, eri latausverkko
)
ocm_op_puhdas <- ocm_operaattorit |> filter(!operaattori %in% ei_operaattoreita)
stopifnot(
"Vastaavuustaulussa tuntematon OCM-nimi" =
all(vastaavuudet_op$ocm %in% ocm_operaattorit$operaattori),
"Vastaavuustaulussa tuntematon AFIR-nimi" =
all(vastaavuudet_op$afir %in% ops$op_virallinen),
"Vastaavuudet eivät ole yksikäsitteisiä" =
!any(duplicated(vastaavuudet_op$ocm)) && !any(duplicated(vastaavuudet_op$afir)),
"Poistettu nimi ei löydy aineistosta" =
all(ei_operaattoreita %in% ocm_operaattorit$operaattori)
)
n1_op <- nrow(ocm_op_puhdas)
n2_op <- nrow(ops)
m_op <- nrow(vastaavuudet_op)
vain_afir <- setdiff(ops$op_virallinen, vastaavuudet_op$afir)
vain_ocm <- setdiff(ocm_op_puhdas$operaattori, vastaavuudet_op$ocm)
```
Linkitys tehdään käsin eikä sumealla nimivertailulla. Kokeilin ensin
automaattista vertailua, ja se löysi kymmenestä vastaavuudesta kolme. Kaksikymmentä
toimijaa on niin vähän, että käsityö on sekä tarkempaa että läpinäkyvämpää:
taulukosta näkee mitä on tehty ja miksi.
OCM tuntee `r n1_op` operaattoria, AFIR `r n2_op`, ja yhteisiä on `r m_op`.
**Kiinnostavin havainto on siinä, keitä puuttuu.** AFIR-rekisterissä on
`r length(vain_afir)` operaattoria, joita OCM ei tunne. Joukossa on
merkittäviä toimijoita, ei pelkkiä pieniä. Se kertoo, ettei joukkoistettu
rekisteri kata edes suuria latausoperaattoreita — mikä on juuri se peittävyyden
puute, jota tässä yritetään mitata.
Vastaavasti OCM tuntee `r length(vain_ocm)` operaattoria, joita AFIR ei tunne.
Nämä ovat toimijoita, jotka eivät ole täyttäneet lakisääteistä
ilmoitusvelvollisuuttaan — tai jotka ovat lopettaneet toimintansa ilman että
OCM:n merkintä on poistettu.
```{r}
#| label: crc
#| echo: true
# Bayesläinen posteriori hilalla: hypergeometrinen uskottavuus ja
# Jeffreysin priori. Yksi parametri, joten MCMC:tä ei tarvita.
posteriori_N <- function(n1, n2, m, kerroin = 8, max_kerroin = 200) {
stopifnot("m ei voi ylittää otoskokoja" = m <= min(n1, n2),
"m on nolla" = m > 0)
N_min <- n1 + n2 - m
repeat {
N <- N_min:ceiling(N_min * kerroin)
lp <- lchoose(n2, m) + lchoose(N - n2, n1 - m) - lchoose(N, n1) - log(N)
lp[!is.finite(lp)] <- -Inf
p <- exp(lp - max(lp)); p <- p / sum(p)
hanta <- sum(p[N > max(N) * 0.99])
if (hanta < 1e-6 || kerroin >= max_kerroin) break
kerroin <- kerroin * 2
}
stopifnot(
"Posteriorin häntä ei mene nollaan – hila on liian lyhyt" = hanta < 1e-4,
"Posteriorin huippu on hilan reunassa" = which.max(p) < length(p) * 0.95
)
tibble(N = N, tiheys = p)
}
post_op <- posteriori_N(n1_op, n2_op, m_op)
naytteet_op <- sample(post_op$N, 20000, replace = TRUE, prob = post_op$tiheys)
op_arvio <- c(
mediaani = median(naytteet_op),
q025 = unname(quantile(naytteet_op, 0.025)),
q975 = unname(quantile(naytteet_op, 0.975))
)
```
Operaattoreiden kokonaismäärän arvio on
**`r round(op_arvio["mediaani"])`** (95 %:n väli
`r round(op_arvio["q025"])`–`r round(op_arvio["q975"])`), kun havaittuja on
`r n1_op + n2_op - m_op`.
```{r}
#| label: asemamaara
#| echo: true
# Asemien kokonaismäärä: havaitut asemat + puuttuvien operaattoreiden
# asemat. Puuttuville arvotaan koko havaitusta kokojakaumasta.
koot <- nap |> count(op_id) |> pull(n)
havaitut_asemat <- nrow(nap)
naytteet_asemat <- map_dbl(1:8000, function(i) {
n_op_yht <- sample(naytteet_op, 1)
n_piilossa_op <- max(n_op_yht - n2_op, 0)
piilossa <- if (n_piilossa_op > 0)
sum(sample(koot, n_piilossa_op, replace = TRUE)) else 0
havaitut_asemat + piilossa
})
asema_arvio <- c(
mediaani = median(naytteet_asemat),
q025 = unname(quantile(naytteet_asemat, 0.025)),
q975 = unname(quantile(naytteet_asemat, 0.975))
)
stopifnot(
"Arvio on pienempi kuin havaittu" = asema_arvio["mediaani"] >= havaitut_asemat
)
```
```{r}
#| label: kuva-posteriori
#| fig-cap: "Latausasemien kokonaismäärän posteriorijakauma."
#| fig-height: 6.5
p1 <- tibble(N = naytteet_op) |>
ggplot(aes(N)) +
stat_halfeye(fill = pal["sininen"], colour = "grey85",
.width = c(0.5, 0.95), alpha = 0.6) +
geom_vline(xintercept = n1_op + n2_op - m_op,
colour = pal["punainen"], linetype = 2) +
annotate("text", x = n1_op + n2_op - m_op, y = Inf, hjust = -0.08, vjust = 1.8,
label = " havaitut", colour = pal["punainen"], size = 3.2) +
labs(x = "Latausoperaattoreita", y = NULL,
title = "Ensin: montako operaattoria on olemassa?",
subtitle = "Havaintoyksikkö on operaattori, ei asema")
p2 <- tibble(N = naytteet_asemat) |>
ggplot(aes(N)) +
stat_halfeye(fill = pal["turkoosi"], colour = "grey85",
.width = c(0.5, 0.95), alpha = 0.6) +
geom_vline(xintercept = havaitut_asemat,
colour = pal["punainen"], linetype = 2) +
annotate("text", x = havaitut_asemat, y = Inf, hjust = -0.08, vjust = 1.8,
label = " rekisterissä näkyvät", colour = pal["punainen"],
size = 3.2) +
scale_x_continuous(labels = label_number(big.mark = " ")) +
labs(x = "Latausasemia (sijaintitasolla)", y = NULL,
title = "Sitten: montako asemaa niillä on?",
subtitle = "Epävarmuus operaattoritasolta siirretään asematasolle")
p1 / p2
```
**Miten kuvaa luetaan.** Molemmissa paneeleissa muodon korkein kohta on
todennäköisin arvo ja leveys kertoo epävarmuuden. Alla oleva paksu viiva näyttää
välin, jolle arvo osuu puolen todennäköisyydellä, ja ohut viiva 95 prosentin
välin. Punainen katkoviiva näyttää havaitun määrän.
Ylempi paneeli koskee operaattoreita, alempi asemia. Jälkimmäinen on leveämpi,
koska siihen kertyy epävarmuutta kahdesta lähteestä: siitä montako operaattoria
puuttuu ja siitä kuinka suuria ne ovat.
::: {.callout-warning}
## Kolme varausta, jotka pitää sanoa ääneen
**Rekisterit eivät ole riippumattomia.** Molemmat saavat tietonsa osittain
samoilta operaattoreilta. Positiivinen riippuvuus johtaa populaatiokoon
aliarvioon. Kahdesta lähteestä riippuvuutta ei voi testata — siihen tarvittaisiin
kolmas todella riippumaton lähde.
**Puuttuvat operaattorit eivät ole satunnainen otos havaituista.** Malli arpoo
niille asemamäärän havaitusta kokojakaumasta. Todellisuudessa rekisteristä
puuttuva toimija on todennäköisemmin pieni kuin suuri, joten arvio on
todennäköisesti yläkanttiin.
**Otantavirhettä ei ole.** Tässä ei ole otosta perusjoukosta. Epävarmuus syntyy
havaitsemisprosessista ja linkityksestä, ei otannasta. Siksi perinteinen
merkitsevyystestaus on tässä osin väärä työkalu ja painopiste on jakaumissa.
:::
### Herkkyysanalyysi
Riippuvuutta ei voi testata, mutta sen vaikutuksen voi laskea. Riippuvuuden
voimakkuutta kuvaava kerroin saa arvon yksi, kun lähteet ovat riippumattomia, ja
ykköstä suurempia arvoja, kun molemmissa esiintyminen on yleisempää kuin sattuma
antaisi olettaa.
```{r}
#| label: herkkyys
#| fig-cap: "Kuinka paljon arvio muuttuu, jos rekisterit eivät ole riippumattomia."
tibble(psi = seq(0.7, 2.0, by = 0.01)) |>
mutate(N_op = n1_op * n2_op / (m_op * psi)) |>
ggplot(aes(psi, N_op)) +
geom_line(colour = pal["oranssi"], linewidth = 0.9) +
geom_vline(xintercept = 1, colour = "grey60", linetype = 2) +
annotate("text", x = 1, y = Inf, hjust = -0.08, vjust = 1.8,
label = " riippumattomuus", colour = "grey65", size = 3.2) +
labs(x = "Riippuvuuden voimakkuus", y = "Arvioitu operaattorimäärä",
title = "Mitä riippuvuusoletus maksaa",
subtitle = "Oikealle mentäessä lähteet kopioivat toisiaan enemmän")
```
Käyrä on analyysin rehellisin osa. Se sanoo: jos rekisterit ovat riippumattomia,
arvio on tämä. Jos ne kopioivat toisiaan, arvio on merkittävästi suurempi. En voi
tietää kumpi pitää paikkansa. Voin kertoa, kuinka paljon sillä on väliä.
## Sarjan pohja-aineisto
```{r}
#| label: tallenna
#| echo: true
asemat <- nap |>
transmute(avain = kanoninen, nimi, op_id, op_nimi, kunta, postinro,
lat, lon, aina_auki, lahde = "AFIR") |>
bind_rows(
ocm |> transmute(avain = paste0("OCM|", ocm_id), nimi,
op_nimi = operaattori, kunta, postinro, lat, lon,
lahde = "OCM")
) |>
mutate(haettu = Sys.Date())
# Konnektoritaulu viittaa kanonisoituun avaimeen
liitokset <- nap_kon |>
left_join(select(nap, avain, kanoninen), by = "avain") |>
mutate(avain = coalesce(kanoninen, avain)) |>
select(-kanoninen)
stopifnot(
"Asemataulu tyhjä" = nrow(asemat) > 0,
"Avaimissa duplikaatteja" = !any(duplicated(asemat$avain)),
"Liitokset eivät täsmää asemiin" =
mean(liitokset$avain %in% asemat$avain) > 0.95,
"Tariffisarake puuttuu" = "tariffi" %in% names(liitokset)
)
qs2::qd_save(asemat, file.path(data_dir, "asemat.qs"))
qs2::qd_save(liitokset, file.path(data_dir, "liitokset.qs"))
qs2::qd_save(ops, file.path(data_dir, "operaattorit.qs"))
qs2::qd_save(tibble(N = naytteet_asemat),
file.path(data_dir, "posteriori_asemamaara.qs"))
```
Viimeinen tiedosto erottaa tämän sarjan tavanomaisesta verkostoanalyysistä. Kun
myöhemmin lasketaan, mitkä asemat ovat verkon kriittisimpiä, tieto siitä että
rekisteri on epätäydellinen ei jää tähän postaukseen vaan kulkee mukana.
## Takaisin siihen lukuun
Uutisessa toistettu latauspisteiden lukumäärä ei ole väärä. Se on
epätäydellinen tavalla, joka on mitattavissa mutta jota ei mitata.
Viisi asiaa jää käteen.
**Kysymys pitää esittää tarkemmin.** Sijainti, latauslaite ja pistoke ovat eri
asioita, ja niiden lukumäärät eroavat kertaluokalla. Tavoite, joka ei kerro
tasoa, ei ole tavoite.
**Rekisteri on mittalaite, ei todellisuus.** Molemmissa tarkastelluissa
rekistereissä on rakenteellisia ongelmia: tunnisteita jotka eivät yksilöi,
laitteita jotka on kirjattu kahdesti, kenttä joka väittää olevansa
laadunvarmistus mutta on aikaleima.
**Havaintoyksikkö ei ole se, miltä näyttää.** Asemat eivät päädy rekisteriin
yksi kerrallaan vaan operaattorin ja viime kädessä alustatoimittajan
päätöksellä. Menetelmä, joka olettaa muuta, tuottaa liian kapean
epävarmuusvälin — eli näyttää varmemmalta kuin on.
**Automaattinen ei aina voita käsityötä.** Sumea nimivertailu löysi kymmenestä
operaattoriparista kolme. Käsin tehty taulukko löysi kaikki ja on lisäksi
tarkistettavissa. Kahdenkymmenen rivin kohdalla automaatio oli väärä työkalu.
**Epävarmuus ei ole heikkous.** Jos AFIR-tavoitteiden toteutumista seurataan
luvulla, jonka virhemarginaalia ei tunneta, seuranta mittaa
rekisteröintikäytäntöjen muutosta yhtä paljon kuin infrastruktuurin
rakentumista.
Seuraavassa osassa siirrytään asemien lukumäärästä niiden tehoon. Sielläkin
julkisuudessa liikkuu yksi luku — keskimääräinen latausteho — ja sekin
osoittautuu jakaumaksi, jolla on useampi huippu kuin yksi.
---
## Mitä tästä on hyötyä yrityksellesi
Merkintä-takaisinpyynti kuulostaa ekologiasta lainatulta kuriositeetilta. Se on
yksi käyttökelpoisimmista menetelmistä, joita tiedän, ja sitä käytetään
liike-elämässä hämmästyttävän vähän. Se vastaa kysymykseen, johon mikään muu
menetelmä ei vastaa: **kuinka paljon on sitä, mitä et ole havainnut?**
**Kuinka monta järjestelmää organisaatiollasi oikeasti on?** DORA ja NIS2
edellyttävät kriittisten toimintojen ja ICT-omaisuuden tunnistamista. Jokainen
laitehallintatietokanta, jonka olen nähnyt, on epätäydellinen. Mutta jos on
tietokanta ja erikseen verkkoskannauksen tuottama laitelista, päällekkäisyys
kertoo kuinka monta järjestelmää puuttuu molemmista. Se luku on johdolle yleensä
epämiellyttävä yllätys — ja juuri siksi arvokas ennen kuin valvoja kysyy.
**Kuinka paljon petoksia jää havaitsematta?** Jos kaksi toisistaan riippumatonta
kontrollia löytävät osittain samoja tapauksia, päällekkäisyydestä voi estimoida
havaitsemattomien määrän. Tämä muuttaa raportoinnin luonteen: sen sijaan että
kerrottaisiin "löysimme 40 tapausta", voidaan kertoa "kontrollien peittävyys on
noin 60 %, joten tapauksia on todennäköisesti 60–80". Ensimmäinen luku kertoo
kontrollien aktiivisuudesta, jälkimmäinen riskistä.
**Tarkista, onko avaimesi avain.** Tämän postauksen työläin osuus ei ollut
mallinnus vaan sen huomaaminen, ettei tunniste yksilöi. Sama ongelma on lähes
jokaisessa integraatiossa, jossa kaksi järjestelmää yhdistetään kentällä, jonka
oletetaan olevan uniikki. Virhe ei näy virheilmoituksena. Se näkyy väärinä
lukuina, joita kukaan ei kyseenalaista.
**Älä automatisoi sitä, mikä on nopeampi tehdä käsin.** Kahdenkymmenen
toimijan linkitys sumealla haulla oli virhealtis ja läpinäkymätön; käsin tehtynä
se vei viisi minuuttia ja on tarkistettavissa. Automaatio kannattaa siellä missä
volyymi on suuri tai työ toistuu — ei siellä missä se korvaa harkintaa
kahdenkymmenen rivin kohdalla.
**Ja kysy, mitä kenttä oikeasti mittaa.** Löysin rekisteristä kentän nimeltä
"viimeksi varmistettu", joka ei mittaa varmistusta vaan integraation
aktiivisuutta. Vastaavia kenttiä on jokaisessa järjestelmässä: "viimeksi
päivitetty", "aktiivinen", "vahvistettu". Nimi kertoo mitä kentän piti mitata,
ei sitä mitä se mittaa.
Jos organisaatiossasi on kaksi rekisteriä, joiden pitäisi kuvata samaa asiaa
mutta jotka eivät täsmää, siinä on analyysi odottamassa. Teen tätä työtä
vuokrattavana Head of Datana ja projektikohtaisesti — hinnasto ja yhteystiedot
löytyvät [sivustolta](https://kristianvepsalainen.com).
---
*Aineistot: Open Charge Map API v3 ja Fintraffic AFIR / Digitraffic, molemmat
haettu `r Sys.Date()`. Koodi on kokonaisuudessaan tässä postauksessa.
Maailma on jakauma.*