💻 Geokoding i geopandas#

Geopandas støtter geokoding via et bibliotek kalt geopy, som må være installert for å bruke geopandas ‘ geopandas.tools.geocode() funksjon. geocode() forventer en liste eller pandas.Series av adresser (strenger) og returnerer en GeoDataFrame med løste adresser og punktgeometrier.

La oss prøve dette ut.

Vi vil geokode adresser lagret i en semikolon-separert tekstfil kalt adresser.txt. Disse adressene ligger i alle i Oslo.

import pathlib
NOTEBOOK_PATH = pathlib.Path().resolve()
DATA_MAPPE = NOTEBOOK_PATH / "data"
import pandas
adresser = pandas.read_csv(
    DATA_MAPPE / "oslo_adresser" / "adresser.txt",
    sep=";"
)

adresser.head()
id adr
0 100 Statsråd Mathiesens vei 25, 0594 OSLO
1 101 Slimeveien 15, 1275 OSLO
2 102 Sognsveien 80, 0855 OSLO
3 103 Ullevålsveien 5, 0165 OSLO
4 104 Nydalsveien 30b, 0484 OSLO

Vi har en id for hver rad og en adresse i adr kolonnen.

Geokode adresser ved hjelp av Nominatim#

I vårt eksempel vil vi bruke Nominatim som en geokodingstilbyder. Nominatim er et bibliotek og en tjeneste som bruker OpenStreetMap-data, og drives av OpenStreetMap Foundation. Geopandas ‘ geocode() funksjon støtter den innebygd.

God bruk

Nominatims brukervilkår krever at brukere av tjenesten sørger for at de ikke sender mer hyppige forespørsler enn en per sekund, og at en tilpasset bruker-agent streng er knyttet til hver forespørsel.

Geopandas’ implementering lar oss spesifisere en user_agent; biblioteket tar også hånd om å respektere hastighetsbegrensningen til Nominatim.

Å slå opp en adresse er en ganske kostbar databaseoperasjon. Derfor er det, noen ganger, den offentlige og gratise Nominatim-serveren bruker litt lenger tid å på svare. I dette eksempelet legger vi til en parameter timeout=10 for å vente opptil 10 sekunder for et svar.

import geopandas

geokodede_adresser = geopandas.tools.geocode(
    adresser["adr"],
    provider="nominatim",
    user_agent="gmgi221",
    timeout=10
)
geokodede_adresser.head()
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[3], line 3
      1 import geopandas
----> 3 geokodede_adresser = geopandas.tools.geocode(
      4     adresser["adr"],
      5     provider="nominatim",
      6     user_agent="gmgi221",
      7     timeout=10
      8 )
      9 geokodede_adresser.head()

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopandas/tools/geocoding.py:67, in geocode(strings, provider, **kwargs)
     64     provider = "photon"
     65 throttle_time = _get_throttle_time(provider)
---> 67 return _query(strings, True, provider, throttle_time, **kwargs)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopandas/tools/geocoding.py:141, in _query(data, forward, provider, throttle_time, **kwargs)
    139 try:
    140     if forward:
--> 141         results[i] = coder.geocode(s)
    142     else:
    143         results[i] = coder.reverse((s.y, s.x), exactly_one=True)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopy/geocoders/nominatim.py:297, in Nominatim.geocode(self, query, exactly_one, timeout, limit, addressdetails, language, geometry, extratags, country_codes, viewbox, bounded, featuretype, namedetails)
    295 logger.debug("%s.geocode: %s", self.__class__.__name__, url)
    296 callback = partial(self._parse_json, exactly_one=exactly_one)
--> 297 return self._call_geocoder(url, callback, timeout=timeout)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopy/geocoders/base.py:368, in Geocoder._call_geocoder(self, url, callback, timeout, is_json, headers)
    366 try:
    367     if is_json:
--> 368         result = self.adapter.get_json(url, timeout=timeout, headers=req_headers)
    369     else:
    370         result = self.adapter.get_text(url, timeout=timeout, headers=req_headers)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopy/adapters.py:472, in RequestsAdapter.get_json(self, url, timeout, headers)
    471 def get_json(self, url, *, timeout, headers):
--> 472     resp = self._request(url, timeout=timeout, headers=headers)
    473     try:
    474         return resp.json()

File ~/miniconda3/envs/book/lib/python3.12/site-packages/geopy/adapters.py:482, in RequestsAdapter._request(self, url, timeout, headers)
    480 def _request(self, url, *, timeout, headers):
    481     try:
--> 482         resp = self.session.get(url, timeout=timeout, headers=headers)
    483     except Exception as error:
    484         message = str(error)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/requests/sessions.py:602, in Session.get(self, url, **kwargs)
    594 r"""Sends a GET request. Returns :class:`Response` object.
    595 
    596 :param url: URL for the new :class:`Request` object.
    597 :param \*\*kwargs: Optional arguments that ``request`` takes.
    598 :rtype: requests.Response
    599 """
    601 kwargs.setdefault("allow_redirects", True)
--> 602 return self.request("GET", url, **kwargs)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    584 send_kwargs = {
    585     "timeout": timeout,
    586     "allow_redirects": allow_redirects,
    587 }
    588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
    591 return resp

File ~/miniconda3/envs/book/lib/python3.12/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
    700 start = preferred_clock()
    702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
    705 # Total elapsed time of the request (approximately)
    706 elapsed = preferred_clock() - start

File ~/miniconda3/envs/book/lib/python3.12/site-packages/requests/adapters.py:667, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    664     timeout = TimeoutSauce(connect=timeout, read=timeout)
    666 try:
--> 667     resp = conn.urlopen(
    668         method=request.method,
    669         url=url,
    670         body=request.body,
    671         headers=request.headers,
    672         redirect=False,
    673         assert_same_host=False,
    674         preload_content=False,
    675         decode_content=False,
    676         retries=self.max_retries,
    677         timeout=timeout,
    678         chunked=chunked,
    679     )
    681 except (ProtocolError, OSError) as err:
    682     raise ConnectionError(err, request=request)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/urllib3/connectionpool.py:789, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    786 response_conn = conn if not release_conn else None
    788 # Make the request on the HTTPConnection object
--> 789 response = self._make_request(
    790     conn,
    791     method,
    792     url,
    793     timeout=timeout_obj,
    794     body=body,
    795     headers=headers,
    796     chunked=chunked,
    797     retries=retries,
    798     response_conn=response_conn,
    799     preload_content=preload_content,
    800     decode_content=decode_content,
    801     **response_kw,
    802 )
    804 # Everything went great!
    805 clean_exit = True

File ~/miniconda3/envs/book/lib/python3.12/site-packages/urllib3/connectionpool.py:536, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    534 # Receive the response from the server
    535 try:
--> 536     response = conn.getresponse()
    537 except (BaseSSLError, OSError) as e:
    538     self._raise_timeout(err=e, url=url, timeout_value=read_timeout)

File ~/miniconda3/envs/book/lib/python3.12/site-packages/urllib3/connection.py:464, in HTTPConnection.getresponse(self)
    461 from .response import HTTPResponse
    463 # Get the response from http.client.HTTPConnection
--> 464 httplib_response = super().getresponse()
    466 try:
    467     assert_header_parsing(httplib_response.msg)

File ~/miniconda3/envs/book/lib/python3.12/http/client.py:1428, in HTTPConnection.getresponse(self)
   1426 try:
   1427     try:
-> 1428         response.begin()
   1429     except ConnectionError:
   1430         self.close()

File ~/miniconda3/envs/book/lib/python3.12/http/client.py:331, in HTTPResponse.begin(self)
    329 # read until we get a non-100 response
    330 while True:
--> 331     version, status, reason = self._read_status()
    332     if status != CONTINUE:
    333         break

File ~/miniconda3/envs/book/lib/python3.12/http/client.py:292, in HTTPResponse._read_status(self)
    291 def _read_status(self):
--> 292     line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    293     if len(line) > _MAXLINE:
    294         raise LineTooLong("status line")

File ~/miniconda3/envs/book/lib/python3.12/socket.py:720, in SocketIO.readinto(self, b)
    718 while True:
    719     try:
--> 720         return self._sock.recv_into(b)
    721     except timeout:
    722         self._timeout_occurred = True

File ~/miniconda3/envs/book/lib/python3.12/ssl.py:1252, in SSLSocket.recv_into(self, buffer, nbytes, flags)
   1248     if flags != 0:
   1249         raise ValueError(
   1250           "non-zero flags not allowed in calls to recv_into() on %s" %
   1251           self.__class__)
-> 1252     return self.read(nbytes, buffer)
   1253 else:
   1254     return super().recv_into(buffer, nbytes, flags)

File ~/miniconda3/envs/book/lib/python3.12/ssl.py:1104, in SSLSocket.read(self, len, buffer)
   1102 try:
   1103     if buffer is not None:
-> 1104         return self._sslobj.read(len, buffer)
   1105     else:
   1106         return self._sslobj.read(len)

KeyboardInterrupt: 

Et voilà! Som et resultat fikk vi tilbake en GeoDataFrame som inneholder en analysert versjon av våre originale adresser og en geometry kolonne med shapely.geometry.Points som vi kan bruke, for eksempel, til å eksportere dataene til et romlig dataformat.

Imidlertid ble id-kolonnen forkastet i prosessen. For å kombinere inputdatasettet med resultatsettet vårt, kan vi bruke pandas’ join operasjoner.

Koble sammen dataframes#

Koble sammen datasett ved hjelp av pandas

For en omfattende oversikt over forskjellige måter å kombinere dataframes og Series, ta en titt på pandas dokumentasjon om merge, join og concatenate.

Å koble data fra to eller flere dataframes eller tabeller er en vanlig oppgave i mange (romlige) dataanalysearbeidsflyter. Som du kanskje husker fra våre tidligere timer, kan kombinering av data fra forskjellige tabeller basert på en felles nøkkel-attributt gjøres enkelt i pandas/geopandas ved hjelp av merge() funksjonen.

Men, noen ganger er det nyttig å koble to dataframes sammen basert på deres indeks. Dataframes må ha samme antall rader og dele den samme indeksen (enkelt forklart, de skal ha samme rekkefølge av rader).

Vi kan bruke denne tilnærmingen, for å koble informasjon fra den originale dataframen adresser til de geokodede adressene geokodede_adresser, rad for rad. join()-funksjonen, som standard, kobler to dataframes basert på indeksen deres. Dette fungerer for eksemplet vårt, da rekkefølgen på de to dataframesene er identiske.

geokodede_adresser_med_id = geokodede_adresser.join(adresser)
geokodede_adresser_med_id
geometry address id adr
0 POINT (10.83648 59.94104) 25, Statsråd Mathiesens vei, Linderud, Bjerke,... 100 Statsråd Mathiesens vei 25, 0594 OSLO
1 POINT (10.83432 59.83557) 15, Slimeveien, Bjørnholt, Søndre Nordstrand, ... 101 Slimeveien 15, 1275 OSLO
2 POINT (10.72956 59.95011) Sognsveien 80, Konvallveien, Sogn, Nordre Aker... 102 Sognsveien 80, 0855 OSLO
3 POINT (10.74356 59.91863) 5, Ullevålsveien, Hammersborg, St. Hanshaugen,... 103 Ullevålsveien 5, 0165 OSLO
4 POINT (10.76402 59.9503) 30B, Nydalsveien, Nydalen, Nordre Aker, Oslo, ... 104 Nydalsveien 30b, 0484 OSLO
5 POINT (10.75292 59.919) 3, Vestre Elvebakke, Fredensborg, Grünerløkka,... 105 Vestre Elvebakke 3, 0182 OSLO
6 POINT (10.79645 59.90968) 5, Etterstadsletta, Gamle Oslo, Oslo, 0660, Norge 106 Etterstadsletta 5, 0660 OSLO
7 POINT (10.75544 59.92704) 20, Steenstrups gate, Grünerløkka, Oslo, 0554,... 107 Steenstrups gate 20, 0554 OSLO
8 POINT (10.79432 59.91526) 21, Fyrstikkalléen, Lilleberg, Gamle Oslo, Osl... 108 Fyrstikkalleen 21, 0661 OSLO
9 POINT (10.71724 59.91854) Niels Juels gate, Uranienborg, Frogner, Oslo, ... 109 Niels Juels gate 56, 0259 OSLO
10 POINT (10.84019 59.91439) 6, Wilhelm Stenersens vei, Tveita, Alna, Oslo,... 110 Wilhelm Stenersens vei 6, 0671 OSLO
11 POINT (10.76403 59.91812) 20B, Herslebs gate, Grønland, Gamle Oslo, Oslo... 111 Herslebs gate 20B, 0561 OSLO
12 POINT (10.78564 59.88545) 124, Ekebergveien, Holtet, Nordstrand, Oslo, 1... 112 Ekebergveien 124, 1178 OSLO
13 POINT (10.74013 59.93902) Ullevål sykehus, Gäbleins vei, Ullevål hageby,... 113 Ullevål sykehus, 0450 OSLO
14 POINT (10.75824 59.89739) 30, Kongsveien, Grønlia, Gamle Oslo, Oslo, 019... 114 Kongsveien 30, 0193 OSLO
15 POINT (10.7903 59.91467) Gladengveien 3B, Gladengveien, Ensjø, Gamle Os... 115 Gladengveien 3B, 0661 OSLO
16 POINT (10.81631 59.92734) 10, Kabelgata, Mellom-Hovin, Bjerke, Oslo, 058... 116 Kabelgata 10, 0580 OSLO
17 POINT (10.81041 59.87888) 6, Cecilie Thoresens vei, Karlsrud, Nordstrand... 117 Cecilie Thoresens vei 6, 1153 OSLO
18 POINT (10.71752 59.95709) 67, Sognsvannsveien, Rabben, Nordre Aker, Oslo... 118 Sognsvannsveien 67, 0372 OSLO
19 POINT (10.76286 59.95056) 30C, Nydalsveien, Nydalen, Nordre Aker, Oslo, ... 119 Nydalsveien 30c, 0484 OSLO
20 POINT (10.7206 59.91399) 65, Parkveien, Ruseløkka, Frogner, Oslo, 0254,... 120 Parkveien 65, 0254 OSLO
21 POINT (10.7416 59.92115) 31, Ullevålsveien, Hammersborg, St. Hanshaugen... 121 Ullevålsveien 31, 0131 OSLO
22 POINT (10.65787 59.94535) 1, Gamle Hovsetervei, Nordre Huseby, Vestre Ak... 122 Gamle Hovsetervei 1, 0768 OSLO
23 POINT (10.92678 59.9605) 25, Karl Fossums vei, Fossum, Stovner, Oslo, 0... 123 Karl Fossums vei 25, 0913 OSLO
24 POINT (10.71712 59.94804) 11, Sognsvannsveien, Gaustad, Nordre Aker, Osl... 124 Sognsvannsveien 11, 0372 OSLO
25 POINT (10.66345 59.93113) 66, Ullernchausséen, Montebello, Ullern, Oslo,... 125 Ullernchaussèen 66, 0379 OSLO
26 POINT (10.85068 59.88579) 5, Tor Jonssons veg, Myrvoll, Østensjø, Oslo, ... 126 Tor Jonssons veg 5, 0688 OSLO
27 POINT (10.80587 59.91724) 16G, Innspurten, Gullhaug, Gamle Oslo, Oslo, 0... 127 Innspurten 16, 0663 OSLO
28 POINT (10.72413 59.91304) 30, Cort Adelers gate, Ruseløkka, Frogner, Osl... 128 Cort Adelers gate 30, 0254 OSLO
29 POINT (10.78756 59.93246) Lørenveien 11, Lørenveien, Sinsen, Grünerløkka... 129 Lørenveien 11, 0585 OSLO

Utdataen fra join() er en ny geopandas.GeoDataFrame:

type(geokodede_adresser_med_id)
geopandas.geodataframe.GeoDataFrame

Den nye data rammen har alle originale kolonner pluss nye kolonner for geometry og for en analysert adresse som kan brukes til å spot-sjekke resultatene.

Note

Hvis du skulle gjøre join den andre veien, dvs. adresser.join(geokodede_adresser), ville utdata være en pandas.DataFrame, ikke en geopandas.GeoDataFrame.


Det er nå enkelt å lagre det nye datasettet som en geospatial fil, for eksempel, i GeoPackage format:

geokodede_adresser_med_id.to_file(DATA_MAPPE / "oslo_adresser" / "adresser.gpkg")