VII. Веб-карта с Folium (II)#

и данные портала открытых данных Правительства Москвы …#

На этой неделе мы посмотрим, как выгружать геоданные с Портала Открытых данных Правительства Москвы, а также визуализировать на интерактивной карте точки с фотографиями

Импортируем библиотеки#

import geopandas as gpd
import requests
import pandas as pd

import folium 
import base64
import os

1. DataMos API: никогда не было проще скачивать данные с data_mos…#

1.1 Определяем параметры для запроса#

# datamos_api = 'yourAPIcode'
data_set = 62883 

url_data = f'https://apidata.mos.ru/v1/datasets/{data_set}/features?api_key={datamos_api}'

1.2 Отправляем запрос#

data_mos = requests.get(url = url_data )
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[4], line 1
----> 1 data_mos = requests.get(url = url_data )

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/api.py:73, in get(url, params, **kwargs)
     62 def get(url, params=None, **kwargs):
     63     r"""Sends a GET request.
     64 
     65     :param url: URL for the new :class:`Request` object.
   (...)
     70     :rtype: requests.Response
     71     """
---> 73     return request("get", url, params=params, **kwargs)

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/api.py:59, in request(method, url, **kwargs)
     55 # By using the 'with' statement we are sure the session is closed, thus we
     56 # avoid leaving sockets open which can trigger a ResourceWarning in some
     57 # cases, and look like a memory leak in others.
     58 with sessions.Session() as session:
---> 59     return session.request(method=method, url=url, **kwargs)

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/sessions.py:587, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    582 send_kwargs = {
    583     "timeout": timeout,
    584     "allow_redirects": allow_redirects,
    585 }
    586 send_kwargs.update(settings)
--> 587 resp = self.send(prep, **send_kwargs)
    589 return resp

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/sessions.py:701, in Session.send(self, request, **kwargs)
    698 start = preferred_clock()
    700 # Send the request
--> 701 r = adapter.send(request, **kwargs)
    703 # Total elapsed time of the request (approximately)
    704 elapsed = preferred_clock() - start

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/adapters.py:489, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    487 try:
    488     if not chunked:
--> 489         resp = conn.urlopen(
    490             method=request.method,
    491             url=url,
    492             body=request.body,
    493             headers=request.headers,
    494             redirect=False,
    495             assert_same_host=False,
    496             preload_content=False,
    497             decode_content=False,
    498             retries=self.max_retries,
    499             timeout=timeout,
    500         )
    502     # Send the request.
    503     else:
    504         if hasattr(conn, "proxy_pool"):

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/connectionpool.py:670, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    667     self._prepare_proxy(conn)
    669 # Make the request on the httplib connection object.
--> 670 httplib_response = self._make_request(
    671     conn,
    672     method,
    673     url,
    674     timeout=timeout_obj,
    675     body=body,
    676     headers=headers,
    677     chunked=chunked,
    678 )
    680 # If we're going to release the connection in ``finally:``, then
    681 # the response doesn't need to know about the connection. Otherwise
    682 # it will also try to release it and we'll have a double-release
    683 # mess.
    684 response_conn = conn if not release_conn else None

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/connectionpool.py:381, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    379 # Trigger any extra validation we need to do.
    380 try:
--> 381     self._validate_conn(conn)
    382 except (SocketTimeout, BaseSSLError) as e:
    383     # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
    384     self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/connectionpool.py:978, in HTTPSConnectionPool._validate_conn(self, conn)
    976 # Force connect early to allow us to validate the connection.
    977 if not getattr(conn, "sock", None):  # AppEngine might not have  `.sock`
--> 978     conn.connect()
    980 if not conn.is_verified:
    981     warnings.warn(
    982         (
    983             "Unverified HTTPS request is being made to host '%s'. "
   (...)
    988         InsecureRequestWarning,
    989     )

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/connection.py:309, in HTTPSConnection.connect(self)
    307 def connect(self):
    308     # Add certificate verification
--> 309     conn = self._new_conn()
    310     hostname = self.host
    312     # Google App Engine's httplib does not define _tunnel_host

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/connection.py:159, in HTTPConnection._new_conn(self)
    156     extra_kw["socket_options"] = self.socket_options
    158 try:
--> 159     conn = connection.create_connection(
    160         (self._dns_host, self.port), self.timeout, **extra_kw
    161     )
    163 except SocketTimeout:
    164     raise ConnectTimeoutError(
    165         self,
    166         "Connection to %s timed out. (connect timeout=%s)"
    167         % (self.host, self.timeout),
    168     )

File /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/urllib3/util/connection.py:74, in create_connection(address, timeout, source_address, socket_options)
     72     if source_address:
     73         sock.bind(source_address)
---> 74     sock.connect(sa)
     75     return sock
     77 except socket.error as e:

KeyboardInterrupt: 

смотрим на статус запроса

data_mos.status_code
200

смотрим на ответ

data_mos.json()
{'features': [{'geometry': {'coordinates': [37.900526, 55.414307],
    'type': 'Point'},
   'properties': {'datasetId': 62883,
    'rowId': None,
    'attributes': {'is_deleted': 0,
     'ID': 1,
     'Name': 'Домодедово',
     'Aeroexpress': 'есть',
     'AeroexpressStation': {'global_id': 1508979300,
      'value': 'Аэропорт Домодедово'},
     'AeroexpressTerminal': 'есть',
     'AeroexpressTicketWindow': 'есть',
     'AeroexpressTicketAutomat': 'есть',
     'AeroexpressWorkingHours': '05:30 - 23:30 (с Павелецкого вокзала), 06:00 - 23:30 (из аэропорта)',
     'MCDStation': [],
     'RailwayStation': [],
     'Latitude_WGS84': '55.414307',
     'Longitude_WGS84': '37.900526',
     'global_id': 1272818040},
    'releaseNumber': 41,
    'versionNumber': 1},
   'type': 'Feature'},
  {'geometry': {'coordinates': [37.415713, 55.966771], 'type': 'Point'},
   'properties': {'datasetId': 62883,
    'rowId': None,
    'attributes': {'is_deleted': 0,
     'ID': 2,
     'Name': 'Шереметьево',
     'Aeroexpress': 'есть',
     'AeroexpressStation': {'global_id': 1508983189,
      'value': 'Аэропорт Шереметьево'},
     'AeroexpressTerminal': 'есть',
     'AeroexpressTicketWindow': 'есть',
     'AeroexpressTicketAutomat': 'есть',
     'AeroexpressWorkingHours': '5:15 - 00:31 (с Белорусского вокзала), 05:14 - 00:06 (из аэропорта)',
     'MCDStation': [],
     'RailwayStation': [],
     'Latitude_WGS84': '55.966771',
     'Longitude_WGS84': '37.415713',
     'global_id': 1272818309},
    'releaseNumber': 41,
    'versionNumber': 1},
   'type': 'Feature'},
  {'geometry': {'coordinates': [37.286287, 55.605059], 'type': 'Point'},
   'properties': {'datasetId': 62883,
    'rowId': None,
    'attributes': {'is_deleted': 0,
     'ID': 3,
     'Name': 'Внуково',
     'Aeroexpress': 'нет',
     'AeroexpressStation': [],
     'AeroexpressTerminal': 'нет',
     'AeroexpressTicketWindow': 'нет',
     'AeroexpressTicketAutomat': 'нет',
     'AeroexpressWorkingHours': '',
     'MCDStation': [],
     'RailwayStation': [],
     'Latitude_WGS84': '55.605059',
     'Longitude_WGS84': '37.286287',
     'global_id': 1272818481},
    'releaseNumber': 41,
    'versionNumber': 1},
   'type': 'Feature'},
  {'geometry': {'coordinates': [37.510824, 55.502859], 'type': 'Point'},
   'properties': {'datasetId': 62883,
    'rowId': None,
    'attributes': {'is_deleted': 0,
     'ID': 4,
     'Name': 'Остафьево',
     'Aeroexpress': 'нет',
     'AeroexpressStation': [],
     'AeroexpressTerminal': 'нет',
     'AeroexpressTicketWindow': 'нет',
     'AeroexpressTicketAutomat': 'нет',
     'AeroexpressWorkingHours': '',
     'MCDStation': [],
     'RailwayStation': [],
     'Latitude_WGS84': '55.502859',
     'Longitude_WGS84': '37.510824',
     'global_id': 1272818826},
    'releaseNumber': 41,
    'versionNumber': 1},
   'type': 'Feature'},
  {'geometry': {'coordinates': [38.117812, 55.56155], 'type': 'Point'},
   'properties': {'datasetId': 62883,
    'rowId': None,
    'attributes': {'is_deleted': 0,
     'ID': 5,
     'Name': 'Жуковский',
     'Aeroexpress': 'нет',
     'AeroexpressStation': [],
     'AeroexpressTerminal': 'нет',
     'AeroexpressTicketWindow': 'нет',
     'AeroexpressTicketAutomat': 'нет',
     'AeroexpressWorkingHours': '',
     'MCDStation': [],
     'RailwayStation': [],
     'Latitude_WGS84': '55.561550',
     'Longitude_WGS84': '38.117812',
     'global_id': 1272818941},
    'releaseNumber': 41,
    'versionNumber': 1},
   'type': 'Feature'}],
 'type': 'FeatureCollection'}

1.3. Создаем GeoDataFrame#

dataGDF = gpd.GeoDataFrame.from_features(data_mos.json()["features"], crs="EPSG:4326")
dataGDF.head()
Loading ITables v2.4.2 from the init_notebook_mode cell... (need help?)

1.4 Разбиваем атрибуты на разные поля#

dataGDF_attributes = pd.DataFrame(dataGDF['attributes'].values.tolist(), index=dataGDF.index).applymap(str)
dataGDF_final = pd.concat([dataGDF, dataGDF_attributes], axis = 1).drop('attributes', axis = 1)

dataGDF_final.head()
/var/folders/ry/9bb7wrz54vq_kn2ytlj6ynzm0000gn/T/ipykernel_39923/1609880950.py:1: FutureWarning:

DataFrame.applymap has been deprecated. Use DataFrame.map instead.
Loading ITables v2.4.2 from the init_notebook_mode cell... (need help?)

Посмотрим на объекты на карте

dataGDF_final.explore(tiles="cartodb positron")
Make this Notebook Trusted to load map: File -> Trust Notebook

1.5 Сохраняем данные#

#dataGDF_final.to_file('data_gdf.shp')
/var/folders/ry/9bb7wrz54vq_kn2ytlj6ynzm0000gn/T/ipykernel_39923/2206928343.py:1: UserWarning:

Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.

2. Фотографии для объектов на интерактивной карте#

df = dataGDF_final

# Папка с фото
photo_folder = "photos"

# Создаём карту
m = folium.Map(location=[55.75, 37.6], zoom_start=9, tiles="cartodb positron")

# Обход строк GeoDataFrame
for _, row in df.iterrows():
    lat = row.geometry.y
    lon = row.geometry.x
    name = row["Name"]
    photo_filename = f"{name}.jpeg"
    photo_path = os.path.join(photo_folder, photo_filename)

    if os.path.exists(photo_path):
        with open(photo_path, "rb") as f:
            img_base64 = base64.b64encode(f.read()).decode("utf-8")
        html = f'<h4>{name}</h4><img src="data:image/jpeg;base64,{img_base64}" width="300">'
    else:
        html = f"<h4>{name}</h4><p><i>Фото не найдено</i></p>"

    iframe = folium.IFrame(html=html, width=310, height=250)
    popup = folium.Popup(iframe, max_width=310)

    folium.Marker(
        location=[lat, lon],
        popup=popup,
        tooltip=name,
        icon=folium.Icon(icon="plane", prefix="fa", color="blue")
    ).add_to(m)
m
Make this Notebook Trusted to load map: File -> Trust Notebook

3. Интерактивная таблица#

Создадим интерактинвную таблицу c помощью ITables на основе атрибутивной таблицы слоя

Импорт нужных библиотек / установка параметров

from itables import init_notebook_mode
init_notebook_mode(all_interactive=True)
from itables import show

Визуализация

df = dataGDF_final.drop(columns='geometry')
show(df, buttons=["copyHtml5", "csvHtml5", "excelHtml5"])
Loading ITables v2.4.2 from the init_notebook_mode cell... (need help?)