Examples
Simple, generic usage example
This is a very simple example that outlines how the different components work and interact with each other. The problem we will be trying to solve is understanding how many people can be counted on a Friday in February from 3pm to 4pm in the surroundings of two given locations.
First of all, let us do the imports:
import pandas as pd
import geopandas as gpd
from pyportall.api.engine.core import APIClient
from pyportall.api.models.lbs import GeocodingOptions
from pyportall.api.engine.geopandas import GeocodingHelper, IsolineHelper, IsovistHelper, IndicatorHelper
from pyportall.api.models.indicators import DayOfWeek, Indicator, Moment, Month
Next, we need the API client to provide the helper components with the capability to send authenticated (technically, metadata requests do not require authentication, but this behavior may change in the future anyway) requests:
client = APIClient(api_key="MY_API_KEY") # API key can also be automatically detected from the PYPORTALL_API_KEY enviroment variable; a batch boolean is also available (defaults to False) to avoid timeouts
As a starting point for our actual analysis, let us build a DataFrame with the two addresses we want to work with:
addresses = pd.DataFrame({"street": ["Gran Vía 46", "Calle Alcalá 10"], "city": ["Madrid", "Madrid"]})
# addresses:
# street city
# 0 gran via 46 Madrid
# 1 calle alcalá 10 Madrid
We need to translate, i.e. geocode, those addreses into their corresponding longitude and latitude coordinates before we can actually do anything useful with them:
geocoding_helper = GeocodingHelper(client)
geocodings = geocoding_helper.resolve(addresses, options=GeocodingOptions(country="Spain"))
# geocodings:
# geometry country state county city district postal_code street
# 0 POINT (-3.70587 40.42048) Spain None None Madrid None None gran via 46
# 1 POINT (-3.37825 40.47281) Spain None None Madrid None None calle alcalá 10
Now we have a GeoDataFrame object with a geometry
column that contains the actual coordinates that correspond to the input addresses, plus other columns with the actual input parameters.
Note how we have used a GeocodingOptions object to set defaults for all the addresses. We could have added "Madrid" as a default option too, instead of repeating it for both addresses as we just did.
We will define two different "surroundings" for each location: one will be the area that can be reached in less than a 200-second walk from the given point ("isoline"), and the other will be the visible area in all directions from that spot (i.e., considering buildings as obstacles) up to 150 meters ("isovist").
Let us define and compute such areas:
isoline_helper = IsolineHelper(client)
isolines = isoline_helper.resolve(gpd.GeoDataFrame({"geometry": geocodings["geometry"], "mode": "pedestrian", "range_s": 200}, crs="EPSG:4326"))
# isolines:
# geometry destination mode range_s moment
# 0 POLYGON ((-3.70711 40.42145, -3.70668 40.42162... {'coordinates': [-3.70587, 40.42048], 'type': ... pedestrian 200 None
# 1 POLYGON ((-3.37975 40.47380, -3.37967 40.47432... {'coordinates': [-3.37825, 40.47281], 'type': ... pedestrian 200 None
isovist_helper = IsovistHelper(client)
isovists = isovist_helper.resolve(gpd.GeoDataFrame({"geometry": geocodings["geometry"]}, crs="EPSG:4326"), options=IsovistOptions(radius_m=100))
# isovists:
# geometry destination radius_m num_rays heading_deg fov_deg
# 0 POLYGON ((-3.70587 40.42087, -3.70586 40.42088... {'coordinates': [-3.70587, 40.42048], 'type': ... 100 -1 0 360
# 1 POLYGON ((-3.37825 40.47294, -3.37825 40.47295... {'coordinates': [-3.37825, 40.47281], 'type': ... 100 -1 0 360
Much like with geocoding above, we could have used IsolineOptions and/or IsovistOptions to set default input parameters.
In any case, the resulting geodataframes contain the geometry that correspond to the computed areas of interest, plus other columns with the actual input parameters.
Now let us find the people count (indicator code "pop_res") we are interested in for all four areas:
indicator_helper = IndicatorHelper(client)
isovist_results = indicator_helper.resolve_aggregated(isovists, indicator=Indicator(code="pop_res"), moment=Moment(dow=DayOfWeek.monday, month=Month.july, year=2021, hour=15))
# isovist_results:
# geometry destination fov_deg heading_deg num_rays radius_m value
# 0 POLYGON ((-3.70587 40.42087, -3.70586 40.42088... {'coordinates': [-3.70587, 40.42048], 'type': ... 360 0 -1 100 969
# 1 POLYGON ((-3.37825 40.47294, -3.37825 40.47295... {'coordinates': [-3.37825, 40.47281], 'type': ... 360 0 -1 100 80
isoline_results = indicator_helper.resolve_aggregated(isolines, indicator=Indicator(code="pop_res"), moment=Moment(dow=DayOfWeek.monday, month=Month.july, year=2021, hour=10))
# isoline_results:
# geometry destination mode moment range_s value
# 0 POLYGON ((-3.70711 40.42145, -3.70668 40.42162... {'coordinates': [-3.70587, 40.42048], 'type': ... pedestrian None 200 42034
# 1 POLYGON ((-3.37975 40.47380, -3.37967 40.47432... {'coordinates': [-3.37825, 40.47281], 'type': ... pedestrian None 200 5784
You can see now the people count on the new value
column of the dataframes.
Internally, Portall splits the areas of interest in [H3 cells]/https://eng.uber.com/h3/), obtains the people count for each cell and aggregates them. It is also possible to get the disaggregated results for each of the cells. In this case, only one input geometry can be used at a time:
isovist_disaggregated_results = indicator_helper.resolve_disaggregated(isovists["geometry"][0], indicator=Indicator(code="pop_res", aggregated=False), moment=Moment(dow=DayOfWeek.monday, month=Month.july, year=2021, hour=15))
# isovist_disaggregated_results:
# geometry id value weight
# 0 POLYGON ((-3.70606 40.42066, -3.70618 40.42063... 631507574769340927 157.692308 1
# 1 POLYGON ((-3.70584 40.42064, -3.70596 40.42060... 631507574769341439 250.000000 1
# 2 POLYGON ((-3.70593 40.42080, -3.70604 40.42076... 631507574769343487 123.076923 1
# 3 POLYGON ((-3.70571 40.42077, -3.70583 40.42074... 631507574769495551 296.153846 1
# 4 POLYGON ((-3.70579 40.42094, -3.70591 40.42090... 631507574769510399 142.307692 1
isoline_disaggregated_results = indicator_helper.resolve_disaggregated(isolines["geometry"][0], indicator=Indicator(code="pop_res", aggregated=False), moment=Moment(dow=DayOfWeek.monday, month=Month.july, year=2021, hour=15))
# isoline_disaggregated_results:
# geometry id value weight
# 0 POLYGON ((-3.70621 40.42176, -3.70633 40.42172... 631507574768870911 134.615385 1
# 1 POLYGON ((-3.70648 40.41948, -3.70660 40.41945... 631507574769392639 88.461538 1
# 2 POLYGON ((-3.70509 40.42116, -3.70521 40.42112... 631507574769484799 300.000000 1
# 3 POLYGON ((-3.70504 40.42146, -3.70515 40.42142... 631507574769506303 146.153846 1
# 4 POLYGON ((-3.70529 40.41995, -3.70541 40.41992... 631507574769377791 157.692308 1
# .. ... ... ... ...
# 259 POLYGON ((-3.70557 40.41891, -3.70569 40.41887... 631507574769420287 103.846154 1
# 260 POLYGON ((-3.70574 40.41924, -3.70586 40.41920... 631507574769420799 103.846154 1
# 261 POLYGON ((-3.70541 40.42058, -3.70552 40.42054... 631507574769494015 346.153846 1
# 262 POLYGON ((-3.70589 40.42033, -3.70601 40.42030... 631507574769327615 276.923077 1
# 263 POLYGON ((-3.70531 40.41919, -3.70542 40.41915... 631507574769425919 192.307692 1
# [264 rows x 4 columns]
The resulting geodataframes are different now, and represent one H3 cell per row, with its geometry and id as columns, together with the value
column itself and a weight
column that can be useful if you want to aggregate the data from this disaggregated geodataframe yourself.
Finally, you may want to save these dataframes back into Portall so that you can, for instance, show them on a dashboard. In order to do that, you need to first convert them to PortallDataFrame
objects:
from pyportall.api.models.geopandas import PortallDataFrame
isovist_results_pdf = PortallDataFrame.from_gdf(isovist_results, client=client, name="isovist")
isoline_results_pdf = PortallDataFrame.from_gdf(isoline_results, client=client, name="isoline")
isovist_disaggregated_results_pdf = PortallDataFrame.from_gdf(isovist_disaggregated_results, client=client, name="isovist_dis")
isoline_disaggregated_results_pdf = PortallDataFrame.from_gdf(isoline_disaggregated_results, client=client, name="isoline_dis")
PortallDataFrame
objects inherit from GeoDataFrame
objects, and differ only in the addition of the id
, name
and description
fields, and save
method (more on this below), so that you can use the former directly in place of the latter.
Once you have the PortallDataFrame
objects, you can save them into Portall:
isovist_results_pdf.save()
isoline_results_pdf.save()
isovist_disaggregated_results_pdf.save()
isoline_disaggregated_results_pdf.save()
There is also a PortallDataFrameHelper
that you can use to retrieve saved PortallDataFrame
objects from Portall.
Preflight
All those API requests incur in credit costs. To know about those costs in advance, you can create a preflight client and send the very same request beforehand. Preflight works for batch requests too.
import pandas as pd
from pyportall.api.engine.core import APIClient
from pyportall.api.models.lbs import GeocodingOptions
from pyportall.api.engine.geopandas import GeocodingHelper
from pyportall.exceptions import PreFlightException
preflight_client = APIClient(api_key="MY_API_KEY", preflight=True)
addresses = pd.DataFrame({"street": ["Gran Vía 46", "Calle Alcalá 10"], "city": ["Madrid", "Madrid"]})
addresses
preflight_geocoding_helper = GeocodingHelper(preflight_client)
try:
preflight_geocoding_helper.resolve(addresses, options=GeocodingOptions(country="Spain"))
except PreFlightException as e:
geocoding_cost = e.credits
# geocoding_cost: 2 (you may get a different value)
Now you can simple go ahead and execute the actual operation, knowing the number of credits that will be deducted from your account:
client = APIClient(api_key="MY_API_KEY")
geocoding_helper = GeocodingHelper(client)
geocodings = geocoding_helper.resolve(addresses, options=GeocodingOptions(country="Spain"))
# geocodings:
# geometry country state county city district postal_code street
# 0 POINT (-3.70587 40.42048) Spain None None Madrid None None gran via 46
# 1 POINT (-3.37825 40.47281) Spain None None Madrid None None calle alcalá 10
Metadata
You will need to look at the metadata catalog to learn about the different indicators that are available to you. Something like this:
from pyportall.api.engine.core import APIClient
from pyportall.api.engine.metadata import MetadataHelper
client = APIClient(api_key="MY_API_KEY")
metadata_helper = MetadataHelper(client)
footfall = metadata_helper.all()[0]
# footfall: IndicatorMetadata(code='pop', name='Footfall', description='Number of people', unit='', format='', coverage='Madrid - UFA', resolution='H3 (11, 12)', data_source='Orange', computed_date=datetime.date(2019, 2, 1), aggregate_fn=<Aggregate.sum: 'sum'>, data_type=<DataType.integer: 'integer'>)