OpenStreetMapin ja Maanmittauslaitoksen nimistöjen vertailu
Monet Lajitietokeskuksen lajihavainnoista ovat kansalaistiedettä ja vieläpä usein ei-teknisten ihmisten syöttämiä. Luontoharrastajien keski-ikäkin melko korkea. Tämä johtaa väistämättä vahinkoihin lajihavaintoja syöttäessä. Koordinaatit ovat asiantuntijoillekin monimutkaisia, ja saattavat helposti mennä väärin.
Yksi keino havaintojen laadunvarmistamiseen on verrata koordinaatteja havaintoa tehdessä sanallisesti ilmoitettuun paikkaan. Jos luontoharrastaja on kertonut havaintonsa löytyneen Suomenlinnasta, mutta koordinaatit osoittavat Keravalle, jokin saattaa olla pielessä. Datan laadun kannalta se ei ole hyvä asia.
Tätä ongelmaa lähdin ratkomaan vertaamalla sanallisesti ilmoitettuja sijainteja Maanmittauslaitoksen nimistöaineistoon ja Nominatim-geokoodauspalveluun, joka perustuu OpenStreetMapin paikannimiin. Nominatim-tietokannan asensin paikallisesti Dockeriin (ohje) ja vertailun tein Pythonin Geopandas ja Pandas -kirjastoilla.
Ennen itse vertailua siivosin ilmoitetut paikannimet
poistamalla niistä ylimääräiset välilyönnit, numerot (esim. postinumerot ja
kadunnumerot) ja kaikki erikoismerkit. Tämän jälkeen tarkistin vielä
sisältävätkö ne kuntien nimiä. Jos kunnan nimi löytyi ja havainnon koordinaatit sijaitsivat kyseisen kunnan alueella, skippasin näiden havaintojen tarkemman analysoinnin. Tällöin datastakin sai karsittua kolmasosan pois.
Vertailu Maanmittauslaitoksen dataan
Sen jälkeen tarkastin, onko tekstimuotoinen sijainti
sellaisenaan Maanmittauslaitoksen nimistöaineistossa. Alkuun kokeilin erilaisia
fuzzy search -menetelmiä, jolloin osuman ei tarvitsisi olla täsmällinen, mutta
se tuntui vain sekoittavan asiaa. Arjalasta tuli helposti Karjala.
obs['is_in_MML_data'] = obs['Sijainti'].str.lower().isin(MML_Places['teksti'])
2 095 484 havainnoista 661 756:lle löytyi täsmällinen vastine Maanmittauslaitokselta. Sen jälkeen valitsin matchaavat havainnot.
obs_filtered = obs[obs['is_in_MML_data']]
Yhdistin ne Maanmittauslaitoksen paikannimiin niin, että yhdelle havainnoille voi tulla useampi osuma, jokainen uudelle riville. Merge()-funktion käyttäminen on huomattavasti for-looppia nopeampi tapa löytää vastaavuudet.
merged = obs_filtered.merge(place_data, left_on='Sijainti', right_on='teksti', suffixes=('_obs', '_mml'))
Tämän jälkeen laskin jokaisesta pisteestä sijainnin jokaiseen pisteeseen. Sitä varten Series-tietotyyppi pitää muuntaa GeoDataFrameksi.
merged = gpd.GeoDataFrame(merged, geometry='geometry_obs', crs=obs.crs)
merged['distance_MML'] = merged['geometry_obs'].distance(merged['geometry_place'])
Lähin samanniminen sijainti löytyy helposti grouppaamalla aineisto havaintojen tunnisteen perusteella ja aggregoimalla data pienimmän etäisyyden mukaan.
closest_places = merged.loc[merged.groupby('Havainnon_tunniste')['distance_MML'].idxmin()]
Lopuksi lasketut sijainnit voi vielä liittää alkuperäisiin havaintoihin. Havainnoilla, joille vastaavuutta ei referenssidatasta löytynyt, jää etäisyydeksi tyhjä arvo.
obs = obs.merge(closest_places, on='Havainnon_tunniste', how='left', suffixes=('', '_closest'))
Etäisyydet samannimisten referenssipisteiden ja havaintojen välillä vaihtelevat muutamasta sentistä satoihin kilometreihin. Variaatiota siis löytyy, mutta onneksi suurin osa havainnoista on lähellä referenssipistettä, kuten yllä olevasta kuvasta näkee.
Vertailu OpenStreetMapin dataan
OpenStreetMap sijaintien kanssa tein vähän samalla tavalla,
mutta havainnot hain Dockerissa pyörivästä Nominatim-tietokannasta. Alkuun yhdistin tietokannan Python-koodiin.
locator = Nominatim(user_agent="myGeocoder", domain="localhost:8080", scheme="http")
Sitten loin hyvin yksinkertaisen cache-mekaniikan, joka nopeutti paikannimien hakemista moninkertaisesti.
geocode_cache = {}
distance_cache = {}
Ja funktion, joka hakee merkkijonomuotoisen paikannimen tietokannasta.
def geocode_with_retry(location_str):
if location_str in geocode_cache: # Check cache first
return geocode_cache[location_str]
result = locator.geocode(location_str, exactly_one=False) # Get the location
if result:
geocode_cache[location_str] = result # Cache the result
return result
geocode_cache[location_str] = None
return None
Toinen funktio laskee pituuden Nominatim-tietokannasta saatujen tuloksien ja havainnon geometrian välille. Jos tietokannasta löytyy monta samannimistä paikkaa, funktio palauttaa lähimmän.def compute_distance(row):
location_str = row['Sijainti_cleaned']
if location_str in distance_cache: # Check cache first
return distance_cache[location_str]
locations = geocode_with_retry(location_str)
if not locations:
return None
row_geometry = row['geometry']
points_wgs84 = [Point(loc.longitude, loc.latitude) for loc in locations]
points_proj = gpd.GeoSeries(points_wgs84, crs=4326).to_crs(obs.crs)
distances = points_proj.distance(row_geometry)
min_distance = distances.min() if not distances.empty else float('inf') # Get the minimum distance
if min_distance != float('inf'):
distance_cache[location_str] = min_distance
return min_distance
return None
Kutsun funktiota progress_apply-komennolla, joka näyttää edistymisen kivasti tqdm-kirjaston edistymispalkissa. Tulokset tallentuvat MML-esimerkin mukaisesti omaan sarakkeeseensa.tqdm.pandas(desc="Computing distances")
obs['distance_osm'] = obs.progress_apply(compute_distance, axis=1)
obs['distance_osm'] = pd.to_numeric(obs['distance_osm'], errors='coerce').round(2)
Olisi myös mahdollista hakea sijainnit verkossa toimivalta Nominatim-geokoodauspalvelusta, joka olisi helpompaa, mutta se toimii sen verran hitaasti, että miljoonien havaintojen hakemiseen olisi mennyt päiviä.
Tulokset ja yhteenveto
Lopulta tallensin molemmat ’distance_osm’ ja ’distance_mml’ sarakkeet uuden tiedostoon ja visualisoin datan QGIS:ssä. Yksinkertaisimmillaan tulokset näyttävät kartalla tältä:
![]() |
Kuva 2. Strömmingsbådan nimellä kirjattu havainto on 22 metrin päästä MML:n referenssipisteestä ja 20 m päässä OSM paikannimestä. |
Ei sinänsä ollut yllättävää, että OSM:sta löytyi MML:n dataa enemmän matcheja, sillä aineisto oli isompi.
Paikannimiaineisto |
Kohteet aineistossa |
Löydetyt osumat |
Geometriatyyppi |
OSM/Nominatum |
2 116 866 |
711 138 / 2 065 692 (34.4 %) |
Alueet, viivat, pisteet |
MML Nimistö |
1 172 377 |
650 952 / 2 065 692 (31.5 %) |
Pisteet |
Kuitenkaan määrä ei korvaa laatua tällä kertaa. Alla olevasta kuvasta huomaa, että MML:n data antaa paljon lyhyempiä etäisyyksiä, kuin OSM:n.
Ero tuntuu kummalliselta, sillä OSM:n datassa on enemmän kohteita (2.1 milj > 1.1 milj) ja mukana on myös aluemuotoisia geometrioita. Toisaalta OSM:ssä on paljon muutakin sekalaista, kuin vain kuratoituja paikannimiä. Mukaan mahtuu risteyksiä, kauppoja ja vaikkapa tunneleita. Osalla kohteista ei myöskään ole nimeä
Johtopäätelmänä tuumaisin, että MML:n nimiaineisto on toistaiseksi laadukkaampi, kuin OSM:n, ainakin tähän tarkoitukseen. Suurin osa referenssipisteistä on ihan havainnon vieressä, kuten histogrammista näkyy.
![]() |
Kuva 4. Suurin osa MML:n etäisyyksistä on alle 1000 metriä. Tämä
antaa osviittaa siitä, että koordinaatit ja tekstimuotoisesti annettu sijainti pääosin
täsmäävät. |
Kun tulosten potentiaalisesti epätarkkoja havaintoja tutkii, useimmiten syy on yksinkertainen. Jos käyttäjä on kirjoittanut havainnon sijainniksi vaikkapa "viereinen koulu", "kesämökki", tai "puisto", on hakusanalla aika mahdotonta etsiä mitään vastaavuutta. Lisäksi haastavuuksia aiheutti mm. saamenkieliset nimet, homonyymit (esim. Kallio), kirjoitusvirheet tai erilaiset kirjoitusasut.
Toki mukaan mahtuu myös oikeasti ristiriitaisia sijainteja, mutta niitä täytyi hieman etsiä (hyvä juttu!). Esimerkiksi Larsholmen-sijaintiin nimetty havainto sijaitsee koordinaattien perusteella Raaseporissa, mutta lähin Larsholmen-niminen referenssipiste Uusikaarlepyyssä.
Tiivistettynä OSM ja MML tuottavat kyllä tuloksia, mutta niihin sisältyy aina epävarmuuksia. On hankala sanoa, onko kyseessä oikeasti havainnon väärä sijainti, vai onko kyseessä kirjoitusvirhe, eri kirjoitusasu vai puute referenssidatassa.
Olisi kiva kuulla myös muiden ajatuksia geokoodauspalveluiden käytöstä. Saa laittaa viestiä vaikka linkedinissä.
Kommentit
Lähetä kommentti