GeoSpatial analysis in Python and Jupyter Notebooks | by Fran Pérez | Towards Data Science

Most capitals in the world are using public city bicycle service, which reduces fuel consumption, emissions, and congestion in city centers. Bike-sharing also encourages physical activity and can contribute to the health of city residents [1].
Fortunately, it exists an open CityBikes API that can be used to examine in real-time the status of the bike’s stations (for example, we can check the number of free slots in any station). Furthermore, there is a python package python-citybikesfor querying the CityBikes API.
Barcelona is one of these cities that expose data using CytyBikes API. Using the above-mentioned package, Barcelona’s bicycle service (aka bicing) can be queried using the following code:
client = citybikes.Client()bicing = citybikes.Network(client, uid='bicing')bicing.stations
The former call returns the station’s data in JSON format:
{    "company": ["Barcelona de Serveis Municipals, S.A. (BSM)", "CESPA", "PBSC"],    "href": "/v2/networks/bicing",    "id": "bicing",    "location": {        "city": "Barcelona",        "country": "ES",        "latitude": 41.3850639,        "longitude": 2.1734035    },    "name": "Bicing",    "stations": [        {            "empty_slots": 17,            "extra": {                "ebikes": 0,                "has_ebikes": true,                "normal_bikes": 16,                "online": true,                "uid": 361            },            "free_bikes": 16,            "id": "ed25291d0f5edd91615d154f243f82f9",            "latitude": 41.376433,            "longitude": 2.17871,            "name": "PG. DE COLOM (LES RAMBLES)",            "timestamp": "2020-10-16T18:19:06.097000Z"        },        ...    ]}
After processing the data, we turned the data from a JSON schema into a tabular schema:
The API doesn’t provide a historic service, but you can develop your own script for downloading the data every 5 minutes (Bicing refresh rate). The companion notebook shows how to download and process the data; a sample of the processed data is also provided as a parquet file indata/bicing_saturday_morning.parquet.
The first spatial operation will be to plot the stations on the map, using the longitude and latitude information available when downloading the data from the CityBikes API:
df_stations = df[["name","latitude","longitude"]].drop_duplicates()
The first map will place all these stations on the map:
stations_map = KeplerGl(height=600, data={"stations": df_stations})stations_map
kepler.gl is smart enough to infer that the columns latitude and longitude represent the station's geographical coordinates. This map can be displayed inside a Jupyter notebook, and the map can be configured using the UI to switch between different map styles (flat or satellite) and layers (road, buildings, etc). These UI adjustments can be exported as a python dictionary and then be used next time you load the map. Check out kepler.gl documentation for more information about customizing the maps.
The CityBikes API doesn’t provide the information about stations by district (only the name of the station — aka address of the station). But fear no more, we can still use open data provided by the community. In this case, we will use the Barcelona open geodata repository, and the district's geographical location provided there.
Geopandas can read .geojson files using:
df_districts = geopandas.read_file("data/districtes.geojson")
The object df_districts is similar to a regular Pandas dataframe, but includes a column named geometry. Geopandas dataframes can contain as many geographical columns as needed, but I highly recommend using just one per dataframe, and named it asgeometry. In this case, the file contains ten districts (rows) and 47 columns (with information such as the name of the district, website, area, etc).
The geopandas dataframe can be plotted as it is:
Or we can project the borders of the district alongside the bike stations in another map instance:
district_map = KeplerGl(height=600, data={"stations": df_stations,                                          "districts": df_districts})
In the above figure we can visually check which stations are in the Gracia district, but is there a way to extract this information in a tabular way? Yes, you can: welcome to spatial joins.
First, we need to transform the dataframe containing the stations' locations to a Geopandas dataframe:
df_stations = geopandas.GeoDataFrame(df_stations.name, geometry=geopandas.points_from_xy(df_stations.longitude, df_stations.latitude, crs='epsg:4326'))
In the above snippet, you can observe a parameter named crs. To summarize: the longitude and latitude numbers are regular numbers, but when you apply a coordinate reference system (CRS) to them, these numbers can be projected on a map. There are many reference systems, and the subject is complex, so I will reduce my study to only two CRS's (please GIS experts forgive my rudeness):
the generic WGS 86 (EPSG: 4326) for locations in the world expressed as longitude/latitude
the Mercator (EPSG: 3395) when working with distances
The difference between both reference systems can be appreciated when we project the world map using both CRS:
If you need more information about projections and working with different reference systems, I recommend checking Managing Projections — GeoPandas documentation.
The spatial join will return the points (= bike stations) included inside each district:
df_stations_districte = geopandas.sjoin(df_stations,  df_districts[["NOM","geometry"]], how="inner", op='intersects')
From there, filtering out the stations in Gracia district is as easy as that:
df_stations_district_gracia = df_stations_district[df_stations_district.NOM=='Gràcia']
The area of influence can be displayed as a circle that is centered around each station. The radio of the circle can be a user-defined parameter (for example, 300 meters). As we mentioned before, to work with distances we need to temporarily switch the reference system to Mercator:
def buffer_in_mercator(geometry, distance_x_meters):    crs = geometry.crs    return geometry.to_crs(epsg=3395).buffer(distance_x_meters).to_crs(crs)
After applying the function to the location of the stations, we can plot the area of influence around each bike stations:
In this case, the approach taken is that every zone in the map might be influenced by many stations, so the number of bikes available in the zone would be the sum of bikes available in the areas of influence touching the zone.
We need to calculate the union of all intersection between all areas of influence. This operation can be solved using the library shapely (kudos to stackoverflow):
shapes = areas.to_list()areas_overlay = list(polygonize(unary_union(list(x.exterior for x in shapes))))
The operation creates a shape (zone) for each overlay intersection, thus sixty-eight shapes are produced in total. As seen before, we can plot the shapes to better understand the operation itself:
The number of free bikes can be obtained by performing a spatial join between zones and stations areas of influence, and adding up all rows in the same zone:
df_gracia_area_stats = gpd.sjoin(df_areas_overlay,                                  df_gracia_stats.reset_index(),                                  how="left", op='intersects')\                                .reset_index()\                                .rename(columns={'index':'index_left'})df_gracia_area_stats['free_bikes_area'] = df_gracia_area_stats.groupby("index_left")['free_bikes'].transform(sum)
Again, the instance df_gracia_area_stats is a geopandas dataframe that can be projected on the map to visually check the availability of bikes in the Gràcia district:
free_bikes_map = KeplerGl(height=600, data={"areas": df_gracia_free_bikes})
In the same way as before, the best zone can be extracted in a tabular way:
df_gracia_free_bikes.loc[df_gracia_free_bikes.free_bikes_median.idxmax()]
In conclusion, geopandas and kepler.gl are a powerful addition to your data analytics toolbox. geopandas is based on the Pandas dataframe API and kepler.gl supports Pandas dataframes at its core, so the learning curve for both libraries is swift, while the benefits are great.
The complete code of this post is available here — https://github.com/franperezlopez/blog_spatial
Installing this library is a bit tricky, and the documentation is lacking on this side, so I will try to facilitate this step. First of all, it seems this library can’t work in Windows, so you will need to use MacOS, Linux or WSL(2).
Second, this library needs a Jupyter extension to be installed. Following my own recommendations about how to install Jupyter Notebook, you should install this extension in your base environment executing the following commands:
conda activate basepip install --user --use-feature=2020-resolver keplergljupyter nbextension install --system --py keplergljupyter nbextension enable --system --py keplergl
[1] Smart City Bike Services: Economic, Environmental & Societal Impacts