Week 3B: Geospatial Data Analysis and GeoPandas

Sep 17, 2020

Housekeeping

  • Homework #2 due a week from today (9/24)
  • Choose a dataset to visualize and explore
    • OpenDataPhilly or one your choosing
    • Email me if you want to analyze one that's not on OpenDataPhilly

Agenda for Week #3

Last lecture

  • Vector data and introduction to GeoPandas
  • Spatial relationships and joins
  • Visualization for geospatial data

Today

  • Demo: 311 requests by neighborhood in Philadelphia (continued)
  • Exercise: Property assessments by neighborhood
In [1]:
# Let's setup the imports we'll need first
import numpy as np
from matplotlib import pyplot as plt
import pandas as pd
import geopandas as gpd

%matplotlib inline

Last time: 311 requests in 2020

Load 311 requests in Philadelphia from the data/ directory.

Source: OpenDataPhilly

In [2]:
# Load the data from a CSV file into a pandas DataFrame
requests = pd.read_csv('./data/public_cases_fc_2020.csv')
/Users/nhand/opt/miniconda3/envs/musa-550-fall-2020/lib/python3.7/site-packages/IPython/core/interactiveshell.py:3146: DtypeWarning: Columns (12) have mixed types.Specify dtype option on import or set low_memory=False.
  interactivity=interactivity, compiler=compiler, result=result)
In [3]:
print("number of requests = ", len(requests))
number of requests =  562549
In [4]:
requests.head()
Out[4]:
objectid service_request_id status status_notes service_name service_code agency_responsible service_notice requested_datetime updated_datetime expected_datetime address zipcode media_url lat lon
0 7890359 13127945 Closed Question Answered Information Request SR-IR01 Police Department NaN 2020-02-05 2020-02-05 2020-02-05 NaN NaN NaN NaN NaN
1 8433329 13376073 Closed NaN Information Request SR-IR01 License & Inspections NaN 2020-05-22 2020-05-22 2020-05-22 NaN NaN NaN NaN NaN
2 8421006 13370944 Open NaN Abandoned Vehicle SR-PD01 Police Department 60 Business Days 2020-05-20 2020-05-20 2020-08-20 1826 PENNINGTON RD NaN NaN 39.977058 -75.270591
3 8433331 13376078 Closed NaN Information Request SR-IR01 Streets Department NaN 2020-05-22 2020-05-22 2020-05-22 NaN NaN NaN NaN NaN
4 8288384 13325114 Closed Question Answered Information Request SR-IR01 Department of Records NaN 2020-04-30 2020-04-30 2020-04-30 NaN NaN NaN NaN NaN

First, convert to a GeoDataFrame

Remove the requests missing lat/lon coordinates

In [5]:
requests = requests.dropna(subset=['lat', 'lon']) 

Create Point objects for each lat and lon combination.

We can use the helper utility function: geopandas.points_from_xy()

In [6]:
requests['Coordinates'] = gpd.points_from_xy(requests['lon'], requests['lat'])
In [7]:
requests['Coordinates'].head()
Out[7]:
2     POINT (-75.27059 39.97706)
9     POINT (-75.24549 39.92376)
14    POINT (-75.16257 40.04816)
19    POINT (-75.18500 40.03733)
22    POINT (-75.20961 39.94040)
Name: Coordinates, dtype: geometry

Now, convert to a GeoDataFrame.

Important

  • Don't forget to set the CRS manually!
  • The CRS you specify when creating a GeoDataFrame should tell geopandas what the coordinate system the input data is in.
  • Usually you will be reading lat/lng coordinates, and will need to specify the crs as EPSG code 4326
  • You should specify the crs as a string using the syntax: ESPG:4326

Since we're only using a few EPSG codes in this course, you can usually tell what the CRS is by looking at the values in the Point() objects.

Philadelphia has a latitude of about 40 deg and longitude of about -75 deg.

Our data must be in the usual lat/lng EPSG=4326.

Screen%20Shot%202020-09-12%20at%204.15.11%20PM.png

In [8]:
requests = gpd.GeoDataFrame(requests, 
                            geometry="Coordinates", 
                            crs="EPSG:4326")

Next, identify the top 20 most common requests

Group by the service name and calculate the size of each group:

In [9]:
service_types = requests.groupby('service_name').size()

Sort by the number (in descending order):

In [10]:
service_types = service_types.sort_values(ascending=False)

Slice the data to take the first 20 elements:

In [11]:
top20 = service_types.iloc[:20]
top20  
Out[11]:
service_name
Rubbish/Recyclable Material Collection    40776
Illegal Dumping                           19246
Maintenance Complaint                     17969
Abandoned Vehicle                         15933
Information Request                       15240
Graffiti Removal                          11652
Street Light Outage                       10107
Street Defect                              7370
Fire Safety Complaint                      4874
Street Trees                               4450
Sanitation / Dumpster Violation            4244
Construction Complaints                    3859
Agency Receivables                         3451
Maintenance Residential or Commercial      3389
Other (Streets)                            2922
LI Escalation                              2807
Parks and Rec Safety and Maintenance       2228
Traffic Signal Emergency                   2157
Complaint (Streets)                        2100
Alley Light Outage                         1650
dtype: int64
In [12]:
trash_requests = requests.loc[
    requests["service_name"] == "Rubbish/Recyclable Material Collection"
].copy()

print("The nuumber of trash-related requests = ", len(trash_requests))
The nuumber of trash-related requests =  40776

Trash collection has been a BIG issue in Philadelphia recently

See for example, this article in the Philadelphia Inquirer

Let's plot the monthly totals for 2020

In [13]:
# Convert the requested datetime to a column of Datetime objects
trash_requests['requested_datetime'] = pd.to_datetime(trash_requests['requested_datetime'])

# Use the .dt attribute to extract out the month name
trash_requests['month'] = trash_requests['requested_datetime'].dt.month_name()

Note: Setting with a copy warning

TL;DR: This is usually fine!

If you select a subset of a dataframe (a "slice") and then make changes (like adding a new column), you will get this warning. There is a good discussion of the issue on StackOverflow.

You can usually make this go away if you add a .copy() after you perform your selection. For example, this warning will go away if we had done:

trash_requests = requests.loc[requests["service_name"] == "Rubbish/Recyclable Material Collection"].copy()
In [14]:
totals_by_week = trash_requests.groupby("month", as_index=False).size()

totals_by_week.head()
Out[14]:
month size
0 April 5773
1 August 4414
2 February 2067
3 January 2710
4 July 9622

Note: I've used the as_index=False syntax here

This will force the size() function to return a DataFrame instead of having the month column as the index of the resulted groupby operation.

It saves us from having to do the .reset_index() function call after running the .size() function.

Plot a bar chart with seaborn

For making static bar charts with Python, seaborn's sns.barplot() is the best option

In [15]:
import seaborn as sns
In [16]:
# Initialize figure/axes
fig, ax = plt.subplots(figsize=(12, 6))

# Plot!
sns.barplot(
    x="month",
    y="size",
    data=totals_by_week,
    color="#2176d2",
    ax=ax,
    order=["January", "February", "March", "April", "May", "June", "July", "August", "September"],
);

Example: Improving the aesthetics of matplotlib

The trend is clear in the previous chart, but can we do a better job with the aesthetics? Yes!

For reference, here is a common way to clean up charts in matplotlib:

In [17]:
# Initialize figure/axes
fig, ax = plt.subplots(figsize=(12, 6))

# Plot!
sns.barplot(
    x="month",
    y="size",
    data=totals_by_week,
    color="#2176d2",
    ax=ax,
    order=["January", "February", "March", "April", "May", "June", "July", "August", "September"],
    zorder=999 # Make sure the bar charts are on top of the grid
)

# Remove x/y axis labels
ax.set_xlabel("")
ax.set_ylabel("")

# Format the ytick labels to use a comma and no decimal places
ax.set_yticklabels([f"{yval:,.0f}" for yval in ax.get_yticks()] )

# Add a grid backgrou d
ax.grid(True, axis='y')

# Remove the top and right axes lines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Add a title
ax.set_title("Philadelphia's Trash-Related 311 Requests in 2020", weight='bold', fontsize=16);
/Users/nhand/opt/miniconda3/envs/musa-550-fall-2020/lib/python3.7/site-packages/ipykernel_launcher.py:20: UserWarning: FixedFormatter should only be used together with FixedLocator

Now let's look at some geospatial trends!

Let's convert from lat/lng to Web Mercator

The original data has EPSG=4326. We'll convert to EPSG=3857.

In [18]:
trash_requests = trash_requests.to_crs(epsg=3857)
In [19]:
trash_requests.head()
Out[19]:
objectid service_request_id status status_notes service_name service_code agency_responsible service_notice requested_datetime updated_datetime expected_datetime address zipcode media_url lat lon Coordinates month
26 8180042 13269656 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-04-02 2020-04-06 2020-04-06 624 FOULKROD ST NaN NaN 40.034389 -75.106518 POINT (-8360819.322 4870940.907) April
27 8180043 13266979 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-04-02 2020-04-06 2020-04-05 1203 ELLSWORTH ST NaN NaN 39.936164 -75.163497 POINT (-8367162.212 4856670.199) April
57 7744426 13066443 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-02 2020-01-04 2020-01-06 9054 WESLEYAN RD NaN NaN 40.058737 -75.018345 POINT (-8351004.015 4874481.442) January
58 7744427 13066540 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-03 2020-01-04 2020-01-06 2784 WILLITS RD NaN NaN 40.063658 -75.022347 POINT (-8351449.489 4875197.202) January
160 7801094 13089345 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-15 2020-01-16 2020-01-16 6137 LOCUST ST NaN NaN 39.958186 -75.244732 POINT (-8376205.240 4859867.796) January

Calculate statistics by Zillow neighborhood

A GeoJSON holding Zillow definitions for Philadelphia neighborhoods is available in the data/ directory.

In [20]:
zillow = gpd.read_file('data/zillow_neighborhoods.geojson')
zillow = zillow.to_crs(epsg=3857)
In [21]:
zillow.head()
Out[21]:
ZillowName geometry
0 Academy Gardens POLYGON ((-8348795.677 4875297.327, -8348355.9...
1 Airport POLYGON ((-8370923.380 4850336.405, -8370799.2...
2 Allegheny West POLYGON ((-8367432.106 4866417.820, -8367436.0...
3 Andorra POLYGON ((-8373967.120 4875663.024, -8374106.1...
4 Aston Woodbridge POLYGON ((-8349918.770 4873746.906, -8349919.8...
In [22]:
fig, ax = plt.subplots(figsize=(8, 8))
ax = zillow.plot(ax=ax, facecolor='none', edgecolor='black')
ax.set_axis_off()
ax.set_aspect("equal")

Use the sjoin() function to match point data (requests) to polygon data (neighborhoods)

In [23]:
joined = gpd.sjoin(trash_requests, zillow, op='within', how='left')
In [24]:
joined.head()
Out[24]:
objectid service_request_id status status_notes service_name service_code agency_responsible service_notice requested_datetime updated_datetime expected_datetime address zipcode media_url lat lon Coordinates month index_right ZillowName
26 8180042 13269656 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-04-02 2020-04-06 2020-04-06 624 FOULKROD ST NaN NaN 40.034389 -75.106518 POINT (-8360819.322 4870940.907) April 70.0 Lawndale
27 8180043 13266979 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-04-02 2020-04-06 2020-04-05 1203 ELLSWORTH ST NaN NaN 39.936164 -75.163497 POINT (-8367162.212 4856670.199) April 105.0 Passyunk Square
57 7744426 13066443 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-02 2020-01-04 2020-01-06 9054 WESLEYAN RD NaN NaN 40.058737 -75.018345 POINT (-8351004.015 4874481.442) January 109.0 Pennypack Woods
58 7744427 13066540 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-03 2020-01-04 2020-01-06 2784 WILLITS RD NaN NaN 40.063658 -75.022347 POINT (-8351449.489 4875197.202) January 107.0 Pennypack
160 7801094 13089345 Closed NaN Rubbish/Recyclable Material Collection SR-ST03 Streets Department 2 Business Days 2020-01-15 2020-01-16 2020-01-16 6137 LOCUST ST NaN NaN 39.958186 -75.244732 POINT (-8376205.240 4859867.796) January 21.0 Cobbs Creek

Note that this operation can be slow

Group by neighborhood and calculate the size:

In [25]:
totals = joined.groupby('ZillowName', as_index=False).size()
type(totals)
Out[25]:
pandas.core.frame.DataFrame

Note: we're once again using the as_index=False to ensure the result of the .size() function is a DataFrame rather than a Series with the ZillowName as its index

In [26]:
totals.head()
Out[26]:
ZillowName size
0 Academy Gardens 74
1 Allegheny West 281
2 Andorra 64
3 Aston Woodbridge 82
4 Bartram Village 29

Lastly, merge Zillow geometries (GeoDataFrame) with the total # of requests per neighborhood (DataFrame).

Important

When merging a GeoDataFrame (spatial) and DataFrame (non-spatial), you should always call the .merge() function of the spatial data set to ensure that the merged data is a GeoDataFrame.

For example...

In [27]:
totals = zillow.merge(totals, on='ZillowName')
In [28]:
totals.head()
Out[28]:
ZillowName geometry size
0 Academy Gardens POLYGON ((-8348795.677 4875297.327, -8348355.9... 74
1 Allegheny West POLYGON ((-8367432.106 4866417.820, -8367436.0... 281
2 Andorra POLYGON ((-8373967.120 4875663.024, -8374106.1... 64
3 Aston Woodbridge POLYGON ((-8349918.770 4873746.906, -8349919.8... 82
4 Bartram Village POLYGON ((-8372041.314 4856283.292, -8372041.6... 29

Visualize as a choropleth map

Choropleth maps color polygon regions according to the values of a specific data attribute. They are built-in to GeoDataFrame objects.

First, plot the total number of requests per neighborhood.

In [29]:
# Create the figure/axes
fig, ax = plt.subplots(figsize=(8, 8))

# Plot
totals.plot(
    ax=ax, 
    column="size", 
    edgecolor="white", 
    linewidth=0.5, 
    legend=True, 
    cmap="viridis"
)

# Format
ax.set_axis_off()
ax.set_aspect("equal")

Can we make the aesthetics better?

Yes!

  • Make the colorbar line up with the axes. The default configuration will always overshoot the axes.
  • Explicitly set the limits of the x-axis and y-axis to zoom in and center the map
In [30]:
# Needed to line up the colorbar properly
from mpl_toolkits.axes_grid1 import make_axes_locatable
In [31]:
# Create the figure
fig, ax = plt.subplots(figsize=(8, 8))

# NEW: Create a nice, lined up colorbar axes (called "cax" here)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2)

# Plot
totals.plot(
    ax=ax,
    cax=cax,
    column="size",
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    cmap="viridis",
)

# NEW: Get the limits of the GeoDataFrame
xmin, ymin, xmax, ymax = totals.total_bounds

# NEW: Set the xlims and ylims
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)

# Format
ax.set_axis_off()
ax.set_aspect("equal")

These improvements are optional, but they definitely make for nicer plots!

Can we classify the data into bins?

Yes, built-in to the plot() function!

Classification schemes

Many different schemes, but here are some of the most common ones:

  1. "Quantiles" : assigns the same number of data points per bin
  2. "EqualInterval" : divides the range of the data into equally sized bins
  3. "FisherJenks": scheme that tries to minimize the variance within each bin and maximize the variances between different bins.
  4. "UserDefined": allows you to specify your own bins
In [32]:
# Quantiles Scheme
fig, ax = plt.subplots(figsize=(10, 7), facecolor="#cfcfcf")

totals.plot(
    ax=ax,
    column="size",
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    legend_kwds=dict(loc="lower right"),
    cmap="Reds",
    scheme="Quantiles",
    k=5,
)
ax.set_title("Quantiles: k = 5")
ax.set_axis_off()
ax.set_aspect("equal")
In [33]:
## Equal Interval Scheme
fig, ax = plt.subplots(figsize=(10,7), facecolor='#cfcfcf')
totals.plot(
    ax=ax,
    column="size",
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    legend_kwds=dict(loc='lower right'),
    cmap="Reds",
    scheme="EqualInterval",
    k=5 
) 
ax.set_title("Equal Interval: k = 5")
ax.set_axis_off()
ax.set_aspect("equal")
In [34]:
## Fisher Jenks Scheme
fig, ax = plt.subplots(figsize=(10,7), facecolor='#cfcfcf')
totals.plot(
    ax=ax,
    column="size",
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    legend_kwds=dict(loc='lower right'),
    cmap="Reds",
    scheme="FisherJenks",
     
)
ax.set_title("Fisher Jenks: k = 5")
ax.set_axis_off()
ax.set_aspect("equal")
In [35]:
## User Defined Scheme
fig, ax = plt.subplots(figsize=(10,7), facecolor='lightgray')
totals.plot(
    ax=ax,
    column="size",
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    legend_kwds=dict(loc='lower right'),
    cmap="Reds",
    scheme="UserDefined", 
    classification_kwds=dict(bins=[100, 300, 500, 800, 1400]) ## NEW: specify user defined bins
)
ax.set_title("User Defined Bins")
ax.set_axis_off()
ax.set_aspect("equal")

Documentation for classification schemes

The documentation can be found here: https://pysal.org/mapclassify/api.html

Contains the full list of schemes and the function definitions for each.

Neighborhood sizes still make it hard to compare raw counts

Better to normalize by area: use the .area attribute of the geometry series

In [36]:
totals['N_per_area'] = totals['size'] / (totals.geometry.area)

Now plot the normalized totals:

In [37]:
# Create the figure
fig, ax = plt.subplots(figsize=(8, 8))

# NEW: Create a nice, lined up colorbar axes (called "cax" here)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2)

# Plot
totals.plot(
    ax=ax,
    cax=cax,
     
    edgecolor="white",
    linewidth=0.5,
    legend=True,
    cmap="viridis",
)

# NEW: Get the limits of the GeoDataFrame
xmin, ymin, xmax, ymax = totals.total_bounds

# NEW: Set the xlims and ylims
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)

# Format
ax.set_axis_off()
ax.set_aspect("equal")

Even smarter

Since households are driving the 311 requests, it would be even better to normalize by the number of properties in a given neighborhood rather than neighborhood area

More advanced: hex bins

Hexagonal bins aggregate quantities over small spatial regions.

Use matplotlib's hexbin() function

In [38]:
# create the axes
fig, ax = plt.subplots(figsize=(12, 12))


# Extract out the x/y coordindates of the Point objects
xcoords = trash_requests.geometry.x
ycoords = trash_requests.geometry.y

# Plot a hexbin chart
hex_vals = ax.hexbin(xcoords, ycoords, gridsize=50)

# Add the zillow geometry boundaries
zillow.plot(ax=ax, facecolor="none", edgecolor="white", linewidth=0.25)


# add a colorbar and format
fig.colorbar(hex_vals, ax=ax)
ax.set_axis_off()
ax.set_aspect("equal")

More advanced: adding a basemap

Let's plot a random sample of the requests, with a nice basemap underneath.

We'll use the contextily utility package.

In [39]:
import contextily as ctx
In [40]:
# load the city limits data
city_limits = gpd.read_file('./data/City_Limits')
In [41]:
# create the axes
fig, ax = plt.subplots(figsize=(12, 12))

# Plot a random sample of 1,000 requests as points
random_requests = trash_requests.sample(1000)

# Plot
random_requests.plot(ax=ax, marker='.', color='crimson')

# Add the city limits
city_limits.to_crs(trash_requests.crs).plot(ax=ax, edgecolor='black', linewidth=3, facecolor='none')

# NEW: plot the basemap underneath
ctx.add_basemap(ax=ax, crs=trash_requests.crs, source=ctx.providers.CartoDB.Positron)

# remove axis lines
ax.set_axis_off()

Lots of different tile providers available..

Easiest to use tab complete on ctx.providers

In [42]:
ctx.providers
Out[42]:
{'OpenStreetMap': {'Mapnik': {'url': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'attribution': '(C) OpenStreetMap contributors',
   'name': 'OpenStreetMap.Mapnik'},
  'DE': {'url': 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'attribution': '(C) OpenStreetMap contributors',
   'name': 'OpenStreetMap.DE'},
  'CH': {'url': 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'attribution': '(C) OpenStreetMap contributors',
   'bounds': [[45, 5], [48, 11]],
   'name': 'OpenStreetMap.CH'},
  'France': {'url': 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
   'max_zoom': 20,
   'attribution': '(C) Openstreetmap France | (C) OpenStreetMap contributors',
   'name': 'OpenStreetMap.France'},
  'HOT': {'url': 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'attribution': '(C) OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France',
   'name': 'OpenStreetMap.HOT'},
  'BZH': {'url': 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'attribution': '(C) OpenStreetMap contributors, Tiles courtesy of Breton OpenStreetMap Team',
   'bounds': [[46.2, -5.5], [50, 0.7]],
   'name': 'OpenStreetMap.BZH'}},
 'OpenSeaMap': {'url': 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
  'attribution': 'Map data: (C) OpenSeaMap contributors',
  'name': 'OpenSeaMap'},
 'OpenPtMap': {'url': 'http://openptmap.org/tiles/{z}/{x}/{y}.png',
  'max_zoom': 17,
  'attribution': 'Map data: (C) OpenPtMap contributors',
  'name': 'OpenPtMap'},
 'OpenTopoMap': {'url': 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
  'max_zoom': 17,
  'attribution': 'Map data: (C) OpenStreetMap contributors, SRTM | Map style: (C) OpenTopoMap (CC-BY-SA)',
  'name': 'OpenTopoMap'},
 'OpenRailwayMap': {'url': 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
  'max_zoom': 19,
  'attribution': 'Map data: (C) OpenStreetMap contributors | Map style: (C) OpenRailwayMap (CC-BY-SA)',
  'name': 'OpenRailwayMap'},
 'OpenFireMap': {'url': 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png',
  'max_zoom': 19,
  'attribution': 'Map data: (C) OpenStreetMap contributors | Map style: (C) OpenFireMap (CC-BY-SA)',
  'name': 'OpenFireMap'},
 'SafeCast': {'url': 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
  'max_zoom': 16,
  'attribution': 'Map data: (C) OpenStreetMap contributors | Map style: (C) SafeCast (CC-BY-SA)',
  'name': 'SafeCast'},
 'Thunderforest': {'OpenCycleMap': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'cycle',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.OpenCycleMap'},
  'Transport': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'transport',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.Transport'},
  'TransportDark': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'transport-dark',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.TransportDark'},
  'SpinalMap': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'spinal-map',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.SpinalMap'},
  'Landscape': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'landscape',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.Landscape'},
  'Outdoors': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'outdoors',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.Outdoors'},
  'Pioneer': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'pioneer',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.Pioneer'},
  'MobileAtlas': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'mobile-atlas',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.MobileAtlas'},
  'Neighbourhood': {'url': 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
   'attribution': '(C) Thunderforest, (C) OpenStreetMap contributors',
   'variant': 'neighbourhood',
   'apikey': '<insert your api key here>',
   'max_zoom': 22,
   'name': 'Thunderforest.Neighbourhood'}},
 'OpenMapSurfer': {'Roads': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'variant': 'roads',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
   'name': 'OpenMapSurfer.Roads'},
  'Hybrid': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'variant': 'hybrid',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
   'name': 'OpenMapSurfer.Hybrid'},
  'AdminBounds': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'adminb',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
   'name': 'OpenMapSurfer.AdminBounds'},
  'ContourLines': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'asterc',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data ASTER GDEM',
   'min_zoom': 13,
   'name': 'OpenMapSurfer.ContourLines'},
  'Hillshade': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'asterh',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data ASTER GDEM, SRTM',
   'name': 'OpenMapSurfer.Hillshade'},
  'ElementsAtRisk': {'url': 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'variant': 'elements_at_risk',
   'attribution': 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
   'name': 'OpenMapSurfer.ElementsAtRisk'}},
 'Hydda': {'Full': {'url': 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'full',
   'attribution': 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
   'name': 'Hydda.Full'},
  'Base': {'url': 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'base',
   'attribution': 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
   'name': 'Hydda.Base'},
  'RoadsAndLabels': {'url': 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
   'max_zoom': 18,
   'variant': 'roads_and_labels',
   'attribution': 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
   'name': 'Hydda.RoadsAndLabels'}},
 'MapBox': {'url': 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}',
  'attribution': '(C) Mapbox (C) OpenStreetMap contributors Improve this map',
  'subdomains': 'abcd',
  'id': 'mapbox.streets',
  'accessToken': '<insert your access token here>',
  'name': 'MapBox'},
 'Stamen': {'Toner': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner',
   'ext': 'png',
   'name': 'Stamen.Toner'},
  'TonerBackground': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner-background',
   'ext': 'png',
   'name': 'Stamen.TonerBackground'},
  'TonerHybrid': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner-hybrid',
   'ext': 'png',
   'name': 'Stamen.TonerHybrid'},
  'TonerLines': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner-lines',
   'ext': 'png',
   'name': 'Stamen.TonerLines'},
  'TonerLabels': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner-labels',
   'ext': 'png',
   'name': 'Stamen.TonerLabels'},
  'TonerLite': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toner-lite',
   'ext': 'png',
   'name': 'Stamen.TonerLite'},
  'Watercolor': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 1,
   'max_zoom': 16,
   'variant': 'watercolor',
   'ext': 'jpg',
   'name': 'Stamen.Watercolor'},
  'Terrain': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 18,
   'variant': 'terrain',
   'ext': 'png',
   'name': 'Stamen.Terrain'},
  'TerrainBackground': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 18,
   'variant': 'terrain-background',
   'ext': 'png',
   'name': 'Stamen.TerrainBackground'},
  'TopOSMRelief': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toposm-color-relief',
   'ext': 'jpg',
   'bounds': [[22, -132], [51, -56]],
   'name': 'Stamen.TopOSMRelief'},
  'TopOSMFeatures': {'url': 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
   'attribution': 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
   'subdomains': 'abcd',
   'min_zoom': 0,
   'max_zoom': 20,
   'variant': 'toposm-features',
   'ext': 'png',
   'bounds': [[22, -132], [51, -56]],
   'opacity': 0.9,
   'name': 'Stamen.TopOSMFeatures'}},
 'Esri': {'WorldStreetMap': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Street_Map',
   'attribution': 'Tiles (C) Esri -- Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012',
   'name': 'Esri.WorldStreetMap'},
  'DeLorme': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'Specialty/DeLorme_World_Base_Map',
   'attribution': 'Tiles (C) Esri -- Copyright: (C)2012 DeLorme',
   'min_zoom': 1,
   'max_zoom': 11,
   'name': 'Esri.DeLorme'},
  'WorldTopoMap': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Topo_Map',
   'attribution': 'Tiles (C) Esri -- Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community',
   'name': 'Esri.WorldTopoMap'},
  'WorldImagery': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Imagery',
   'attribution': 'Tiles (C) Esri -- Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
   'name': 'Esri.WorldImagery'},
  'WorldTerrain': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Terrain_Base',
   'attribution': 'Tiles (C) Esri -- Source: USGS, Esri, TANA, DeLorme, and NPS',
   'max_zoom': 13,
   'name': 'Esri.WorldTerrain'},
  'WorldShadedRelief': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Shaded_Relief',
   'attribution': 'Tiles (C) Esri -- Source: Esri',
   'max_zoom': 13,
   'name': 'Esri.WorldShadedRelief'},
  'WorldPhysical': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'World_Physical_Map',
   'attribution': 'Tiles (C) Esri -- Source: US National Park Service',
   'max_zoom': 8,
   'name': 'Esri.WorldPhysical'},
  'OceanBasemap': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'Ocean_Basemap',
   'attribution': 'Tiles (C) Esri -- Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri',
   'max_zoom': 13,
   'name': 'Esri.OceanBasemap'},
  'NatGeoWorldMap': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'NatGeo_World_Map',
   'attribution': 'Tiles (C) Esri -- National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC',
   'max_zoom': 16,
   'name': 'Esri.NatGeoWorldMap'},
  'WorldGrayCanvas': {'url': 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
   'variant': 'Canvas/World_Light_Gray_Base',
   'attribution': 'Tiles (C) Esri -- Esri, DeLorme, NAVTEQ',
   'max_zoom': 16,
   'name': 'Esri.WorldGrayCanvas'}},
 'OpenWeatherMap': {'Clouds': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'clouds',
   'name': 'OpenWeatherMap.Clouds'},
  'CloudsClassic': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'clouds_cls',
   'name': 'OpenWeatherMap.CloudsClassic'},
  'Precipitation': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'precipitation',
   'name': 'OpenWeatherMap.Precipitation'},
  'PrecipitationClassic': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'precipitation_cls',
   'name': 'OpenWeatherMap.PrecipitationClassic'},
  'Rain': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'rain',
   'name': 'OpenWeatherMap.Rain'},
  'RainClassic': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'rain_cls',
   'name': 'OpenWeatherMap.RainClassic'},
  'Pressure': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'pressure',
   'name': 'OpenWeatherMap.Pressure'},
  'PressureContour': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'pressure_cntr',
   'name': 'OpenWeatherMap.PressureContour'},
  'Wind': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'wind',
   'name': 'OpenWeatherMap.Wind'},
  'Temperature': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'temp',
   'name': 'OpenWeatherMap.Temperature'},
  'Snow': {'url': 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
   'max_zoom': 19,
   'attribution': 'Map data (C) OpenWeatherMap',
   'apiKey': '<insert your api key here>',
   'opacity': 0.5,
   'variant': 'snow',
   'name': 'OpenWeatherMap.Snow'}},
 'HERE': {'normalDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDay'},
  'normalDayCustom': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.custom',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayCustom'},
  'normalDayGrey': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.grey',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayGrey'},
  'normalDayMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayMobile'},
  'normalDayGreyMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.grey.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayGreyMobile'},
  'normalDayTransit': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.transit',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayTransit'},
  'normalDayTransitMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day.transit.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalDayTransitMobile'},
  'normalNight': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNight'},
  'normalNightMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNightMobile'},
  'normalNightGrey': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night.grey',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNightGrey'},
  'normalNightGreyMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night.grey.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNightGreyMobile'},
  'normalNightTransit': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night.transit',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNightTransit'},
  'normalNightTransitMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.night.transit.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.normalNightTransitMobile'},
  'reducedDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'reduced.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.reducedDay'},
  'reducedNight': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'reduced.night',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.reducedNight'},
  'basicMap': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day',
   'max_zoom': 20,
   'type': 'basetile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.basicMap'},
  'mapLabels': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'normal.day',
   'max_zoom': 20,
   'type': 'labeltile',
   'language': 'eng',
   'format': 'png',
   'size': '256',
   'name': 'HERE.mapLabels'},
  'trafficFlow': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'traffic',
   'variant': 'normal.day',
   'max_zoom': 20,
   'type': 'flowtile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.trafficFlow'},
  'carnavDayGrey': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'carnav.day.grey',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.carnavDayGrey'},
  'hybridDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'hybrid.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.hybridDay'},
  'hybridDayMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'hybrid.day.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.hybridDayMobile'},
  'hybridDayTransit': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'hybrid.day.transit',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.hybridDayTransit'},
  'hybridDayGrey': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'hybrid.grey.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.hybridDayGrey'},
  'pedestrianDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'pedestrian.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.pedestrianDay'},
  'pedestrianNight': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'base',
   'variant': 'pedestrian.night',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.pedestrianNight'},
  'satelliteDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'satellite.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.satelliteDay'},
  'terrainDay': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'terrain.day',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.terrainDay'},
  'terrainDayMobile': {'url': 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
   'attribution': 'Map (C) 1987-2019 HERE',
   'subdomains': '1234',
   'mapID': 'newest',
   'app_id': '<insert your app_id here>',
   'app_code': '<insert your app_code here>',
   'base': 'aerial',
   'variant': 'terrain.day.mobile',
   'max_zoom': 20,
   'type': 'maptile',
   'language': 'eng',
   'format': 'png8',
   'size': '256',
   'name': 'HERE.terrainDayMobile'}},
 'FreeMapSK': {'url': 'http://t{s}.freemap.sk/T/{z}/{x}/{y}.jpeg',
  'min_zoom': 8,
  'max_zoom': 16,
  'subdomains': '1234',
  'bounds': [[47.204642, 15.996093], [49.830896, 22.576904]],
  'attribution': '(C) OpenStreetMap contributors, vizualization CC-By-SA 2.0 Freemap.sk',
  'name': 'FreeMapSK'},
 'MtbMap': {'url': 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png',
  'attribution': '(C) OpenStreetMap contributors & USGS',
  'name': 'MtbMap'},
 'CartoDB': {'Positron': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'light_all',
   'name': 'CartoDB.Positron'},
  'PositronNoLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'light_nolabels',
   'name': 'CartoDB.PositronNoLabels'},
  'PositronOnlyLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'light_only_labels',
   'name': 'CartoDB.PositronOnlyLabels'},
  'DarkMatter': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'dark_all',
   'name': 'CartoDB.DarkMatter'},
  'DarkMatterNoLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'dark_nolabels',
   'name': 'CartoDB.DarkMatterNoLabels'},
  'DarkMatterOnlyLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'dark_only_labels',
   'name': 'CartoDB.DarkMatterOnlyLabels'},
  'Voyager': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'rastertiles/voyager',
   'name': 'CartoDB.Voyager'},
  'VoyagerNoLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'rastertiles/voyager_nolabels',
   'name': 'CartoDB.VoyagerNoLabels'},
  'VoyagerOnlyLabels': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'rastertiles/voyager_only_labels',
   'name': 'CartoDB.VoyagerOnlyLabels'},
  'VoyagerLabelsUnder': {'url': 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
   'attribution': '(C) OpenStreetMap contributors (C) CARTO',
   'subdomains': 'abcd',
   'max_zoom': 19,
   'variant': 'rastertiles/voyager_labels_under',
   'name': 'CartoDB.VoyagerLabelsUnder'}},
 'HikeBike': {'HikeBike': {'url': 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
   'max_zoom': 19,
   'attribution': '(C) OpenStreetMap contributors',
   'variant': 'hikebike',
   'name': 'HikeBike.HikeBike'},
  'HillShading': {'url': 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
   'max_zoom': 15,
   'attribution': '(C) OpenStreetMap contributors',
   'variant': 'hillshading',
   'name': 'HikeBike.HillShading'}},
 'BasemapAT': {'basemap': {'url': 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
   'max_zoom': 20,
   'attribution': 'Datenquelle: basemap.at',
   'subdomains': ['', '1', '2', '3', '4'],
   'format': 'png',
   'bounds': [[46.35877, 8.782379], [49.037872, 17.189532]],
   'variant': 'geolandbasemap',
   'name': 'BasemapAT.basemap'},
  'grau': {'url': 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
   'max_zoom': 19,
   'attribution': 'Datenquelle: basemap.at',
   'subdomains': ['', '1', '2', '3', '4'],
   'format': 'png',
   'bounds': [[46.35877, 8.782379], [49.037872, 17.189532]],
   'variant': 'bmapgrau',
   'name': 'BasemapAT.grau'},
  'overlay': {'url': 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
   'max_zoom': 19,
   'attribution': 'Datenquelle: basemap.at',
   'subdomains': ['', '1', '2', '3', '4'],
   'format': 'png',
   'bounds': [[46.35877, 8.782379], [49.037872, 17.189532]],
   'variant': 'bmapoverlay',
   'name': 'BasemapAT.overlay'},
  'highdpi': {'url': 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
   'max_zoom': 19,
   'attribution': 'Datenquelle: basemap.at',
   'subdomains': ['', '1', '2', '3', '4'],
   'format': 'jpeg',
   'bounds': [[46.35877, 8.782379], [49.037872, 17.189532]],
   'variant': 'bmaphidpi',
   'name': 'BasemapAT.highdpi'},
  'orthofoto': {'url': 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
   'max_zoom': 20,
   'attribution': 'Datenquelle: basemap.at',
   'subdomains': ['', '1', '2', '3', '4'],
   'format': 'jpeg',
   'bounds': [[46.35877, 8.782379], [49.037872, 17.189532]],
   'variant': 'bmaporthofoto30cm',
   'name': 'BasemapAT.orthofoto'}},
 'nlmaps': {'standaard': {'url': 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
   'min_zoom': 6,
   'max_zoom': 19,
   'bounds': [[50.5, 3.25], [54, 7.6]],
   'attribution': 'Kaartgegevens (C) Kadaster',
   'variant': 'brtachtergrondkaart',
   'name': 'nlmaps.standaard'},
  'pastel': {'url': 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
   'min_zoom': 6,
   'max_zoom': 19,
   'bounds': [[50.5, 3.25], [54, 7.6]],
   'attribution': 'Kaartgegevens (C) Kadaster',
   'variant': 'brtachtergrondkaartpastel',
   'name': 'nlmaps.pastel'},
  'grijs': {'url': 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
   'min_zoom': 6,
   'max_zoom': 19,
   'bounds': [[50.5, 3.25], [54, 7.6]],
   'attribution': 'Kaartgegevens (C) Kadaster',
   'variant': 'brtachtergrondkaartgrijs',
   'name': 'nlmaps.grijs'},
  'luchtfoto': {'url': 'https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wmts/1.0.0/2016_ortho25/EPSG:3857/{z}/{x}/{y}.png',
   'min_zoom': 6,
   'max_zoom': 19,
   'bounds': [[50.5, 3.25], [54, 7.6]],
   'attribution': 'Kaartgegevens (C) Kadaster',
   'name': 'nlmaps.luchtfoto'}},
 'NASAGIBS': {'ModisTerraTrueColorCR': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 9,
   'format': 'jpg',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_CorrectedReflectance_TrueColor',
   'name': 'NASAGIBS.ModisTerraTrueColorCR'},
  'ModisTerraBands367CR': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 9,
   'format': 'jpg',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_CorrectedReflectance_Bands367',
   'name': 'NASAGIBS.ModisTerraBands367CR'},
  'ViirsEarthAtNight2012': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 8,
   'format': 'jpg',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'VIIRS_CityLights_2012',
   'name': 'NASAGIBS.ViirsEarthAtNight2012'},
  'ModisTerraLSTDay': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 7,
   'format': 'png',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_Land_Surface_Temp_Day',
   'opacity': 0.75,
   'name': 'NASAGIBS.ModisTerraLSTDay'},
  'ModisTerraSnowCover': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 8,
   'format': 'png',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_Snow_Cover',
   'opacity': 0.75,
   'name': 'NASAGIBS.ModisTerraSnowCover'},
  'ModisTerraAOD': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 6,
   'format': 'png',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_Aerosol',
   'opacity': 0.75,
   'name': 'NASAGIBS.ModisTerraAOD'},
  'ModisTerraChlorophyll': {'url': 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
   'attribution': 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
   'bounds': [[-85.0511287776, -179.999999975],
    [85.0511287776, 179.999999975]],
   'min_zoom': 1,
   'max_zoom': 7,
   'format': 'png',
   'time': '',
   'tilematrixset': 'GoogleMapsCompatible_Level',
   'variant': 'MODIS_Terra_Chlorophyll_A',
   'opacity': 0.75,
   'name': 'NASAGIBS.ModisTerraChlorophyll'}},
 'NLS': {'url': 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg',
  'attribution': 'National Library of Scotland Historic Maps',
  'bounds': [[49.6, -12], [61.7, 3]],
  'min_zoom': 1,
  'max_zoom': 18,
  'subdomains': '0123',
  'name': 'NLS'},
 'JusticeMap': {'income': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'income',
   'name': 'JusticeMap.income'},
  'americanIndian': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'indian',
   'name': 'JusticeMap.americanIndian'},
  'asian': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'asian',
   'name': 'JusticeMap.asian'},
  'black': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'black',
   'name': 'JusticeMap.black'},
  'hispanic': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'hispanic',
   'name': 'JusticeMap.hispanic'},
  'multi': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'multi',
   'name': 'JusticeMap.multi'},
  'nonWhite': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'nonwhite',
   'name': 'JusticeMap.nonWhite'},
  'white': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'white',
   'name': 'JusticeMap.white'},
  'plurality': {'url': 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
   'attribution': 'Justice Map',
   'size': 'county',
   'bounds': [[14, -180], [72, -56]],
   'variant': 'plural',
   'name': 'JusticeMap.plurality'}},
 'Wikimedia': {'url': 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png',
  'attribution': 'Wikimedia',
  'min_zoom': 1,
  'max_zoom': 19,
  'name': 'Wikimedia'},
 'GeoportailFrance': {'parcels': {'url': 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
   'attribution': 'Geoportail France',
   'bounds': [[-75, -180], [81, 180]],
   'min_zoom': 2,
   'max_zoom': 20,
   'apikey': 'choisirgeoportail',
   'format': 'image/png',
   'style': 'bdparcellaire',
   'variant': 'CADASTRALPARCELS.PARCELS',
   'name': 'GeoportailFrance.parcels'},
  'ignMaps': {'url': 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
   'attribution': 'Geoportail France',
   'bounds': [[-75, -180], [81, 180]],
   'min_zoom': 2,
   'max_zoom': 18,
   'apikey': 'choisirgeoportail',
   'format': 'image/jpeg',
   'style': 'normal',
   'variant': 'GEOGRAPHICALGRIDSYSTEMS.MAPS',
   'name': 'GeoportailFrance.ignMaps'},
  'maps': {'url': 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
   'attribution': 'Geoportail France',
   'bounds': [[-75, -180], [81, 180]],
   'min_zoom': 2,
   'max_zoom': 18,
   'apikey': 'choisirgeoportail',
   'format': 'image/jpeg',
   'style': 'normal',
   'variant': 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD',
   'name': 'GeoportailFrance.maps'},
  'orthos': {'url': 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
   'attribution': 'Geoportail France',
   'bounds': [[-75, -180], [81, 180]],
   'min_zoom': 2,
   'max_zoom': 19,
   'apikey': 'choisirgeoportail',
   'format': 'image/jpeg',
   'style': 'normal',
   'variant': 'ORTHOIMAGERY.ORTHOPHOTOS',
   'name': 'GeoportailFrance.orthos'}},
 'OneMapSG': {'Default': {'url': 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
   'variant': 'Default',
   'min_zoom': 11,
   'max_zoom': 18,
   'bounds': [[1.56073, 104.11475], [1.16, 103.502]],
   'attribution': '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority',
   'name': 'OneMapSG.Default'},
  'Night': {'url': 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
   'variant': 'Night',
   'min_zoom': 11,
   'max_zoom': 18,
   'bounds': [[1.56073, 104.11475], [1.16, 103.502]],
   'attribution': '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority',
   'name': 'OneMapSG.Night'},
  'Original': {'url': 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
   'variant': 'Original',
   'min_zoom': 11,
   'max_zoom': 18,
   'bounds': [[1.56073, 104.11475], [1.16, 103.502]],
   'attribution': '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority',
   'name': 'OneMapSG.Original'},
  'Grey': {'url': 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
   'variant': 'Grey',
   'min_zoom': 11,
   'max_zoom': 18,
   'bounds': [[1.56073, 104.11475], [1.16, 103.502]],
   'attribution': '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority',
   'name': 'OneMapSG.Grey'},
  'LandLot': {'url': 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
   'variant': 'LandLot',
   'min_zoom': 11,
   'max_zoom': 18,
   'bounds': [[1.56073, 104.11475], [1.16, 103.502]],
   'attribution': '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority',
   'name': 'OneMapSG.LandLot'}}}

Let's try the "Dark Matter" theme...

In [43]:
# create the axes
fig, ax = plt.subplots(figsize=(12, 12))

# plot a random sample of requests
trash_requests.sample(1000).plot(ax=ax, marker='.', color='crimson')

# add the city limits
city_limits.to_crs(trash_requests.crs).plot(ax=ax, edgecolor='white', linewidth=3, facecolor='none')

# plot the basemap underneath
ctx.add_basemap(ax=ax, crs=trash_requests.crs, source=ctx.providers.CartoDB.DarkMatter) # NEW: DarkMatter

# remove axis lines
ax.set_axis_off()

Can we do better?

Yes! Let's add interactivity. We'll start with altair...

Altair recently add full support for GeoDataFrames, making interactive choropleths very easy to make!

In [44]:
import altair as alt
In [45]:
# IMPORTANT: Altair needs the GeoDataFrame to be in EPSG:4326
totals_4326 = totals.to_crs(epsg=4326)

# plot map, where variables ares nested within `properties`,
alt.Chart(totals_4326).mark_geoshape(stroke="white").encode(
    tooltip=["N_per_area:Q", "ZillowName:N", "size:Q"],
    color=alt.Color("N_per_area:Q", scale=alt.Scale(scheme="viridis")),
).properties(width=500, height=400)
Out[45]:

Challenge for later: use altair's repeated charts to show several choropleths for different 311 request types at once.

A similar example (using a different dataset) is available in the altair gallery.

Exercise: property assessments in Philadelphia

Goals: Visualize the property assessment values by neighborhood in Philadelphia, using a

  1. static choropleth map
  2. hex bin map
  3. interactive choropleth with altair

Challenge (if time remaining): Visualize the highest-valued residential and commercial properties as points on top of a contextily basemap

Dataset

2019 property assessment data:

  • from OpenDataPhilly
  • residential properties only — over 460,000 properties

Step 1: Load the assessment data

In [46]:
data = pd.read_csv('./data/opa_residential.csv')
data.head()
Out[46]:
parcel_number lat lng location market_value building_value land_value total_land_area total_livable_area
0 71361800 39.991575 -75.128994 2726 A ST 62200.0 44473.0 17727.0 1109.69 1638.0
1 71362100 39.991702 -75.128978 2732 A ST 25200.0 18018.0 7182.0 1109.69 1638.0
2 71362200 39.991744 -75.128971 2734 A ST 62200.0 44473.0 17727.0 1109.69 1638.0
3 71362600 39.991994 -75.128895 2742 A ST 15500.0 11083.0 4417.0 1109.69 1638.0
4 71363800 39.992592 -75.128743 2814 A ST 31300.0 22400.0 8900.0 643.50 890.0

We'll focus on the market_value column for this analysis

Step 2: Convert to a GeoDataFrame

Remember to set the EPSG of the input data — this data is in the typical lat/lng coordinates (EPSG=4326)

In [47]:
data = data.dropna(subset=['lat', 'lng'])
data["Coordinates"] = gpd.points_from_xy(data["lng"], data["lat"])
data = gpd.GeoDataFrame(data, geometry="Coordinates", crs="EPSG:4326")
In [48]:
len(data)
Out[48]:
461453

Step 3: Do a spatial join with Zillow neighbohoods

Use the sjoin() function.

Make sure you CRS's match before doing the sjoin!

In [49]:
zillow.crs
Out[49]:
<Projected CRS: EPSG:3857>
Name: WGS 84 / Pseudo-Mercator
Axis Info [cartesian]:
- X[east]: Easting (metre)
- Y[north]: Northing (metre)
Area of Use:
- name: World - 85°S to 85°N
- bounds: (-180.0, -85.06, 180.0, 85.06)
Coordinate Operation:
- name: Popular Visualisation Pseudo-Mercator
- method: Popular Visualisation Pseudo Mercator
Datum: World Geodetic System 1984
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
In [50]:
data = data.to_crs(epsg=3857)
In [51]:
gdata = gpd.sjoin(data, zillow, op='within', how='left')
In [52]:
gdata.head()
Out[52]:
parcel_number lat lng location market_value building_value land_value total_land_area total_livable_area Coordinates index_right ZillowName
0 71361800 39.991575 -75.128994 2726 A ST 62200.0 44473.0 17727.0 1109.69 1638.0 POINT (-8363321.403 4864718.063) 79.0 McGuire
1 71362100 39.991702 -75.128978 2732 A ST 25200.0 18018.0 7182.0 1109.69 1638.0 POINT (-8363319.628 4864736.535) 79.0 McGuire
2 71362200 39.991744 -75.128971 2734 A ST 62200.0 44473.0 17727.0 1109.69 1638.0 POINT (-8363318.762 4864742.658) 79.0 McGuire
3 71362600 39.991994 -75.128895 2742 A ST 15500.0 11083.0 4417.0 1109.69 1638.0 POINT (-8363310.304 4864778.984) 79.0 McGuire
4 71363800 39.992592 -75.128743 2814 A ST 31300.0 22400.0 8900.0 643.50 890.0 POINT (-8363293.378 4864865.767) 79.0 McGuire

Step 4: Make a choropleth of the median market value by neighborhood

Hints:

  • You will need to group by Zillow neighborhood
  • Calculate the median market value per neighborhood
  • Join with the Zillow neighborhood GeoDataFrame
In [53]:
grouped = gdata.groupby('ZillowName', as_index=False)
median_values = grouped['market_value'].median()
In [54]:
median_values.head()
Out[54]:
ZillowName market_value
0 Academy Gardens 185950.0
1 Allegheny West 34750.0
2 Andorra 251900.0
3 Aston Woodbridge 183800.0
4 Bartram Village 48300.0
In [55]:
median_values = zillow.merge(median_values, on='ZillowName')
median_values['market_value'] /= 1e3 # in thousands
In [56]:
median_values.head()
Out[56]:
ZillowName geometry market_value
0 Academy Gardens POLYGON ((-8348795.677 4875297.327, -8348355.9... 185.95
1 Allegheny West POLYGON ((-8367432.106 4866417.820, -8367436.0... 34.75
2 Andorra POLYGON ((-8373967.120 4875663.024, -8374106.1... 251.90
3 Aston Woodbridge POLYGON ((-8349918.770 4873746.906, -8349919.8... 183.80
4 Bartram Village POLYGON ((-8372041.314 4856283.292, -8372041.6... 48.30
In [57]:
# Create the figure
fig, ax = plt.subplots(figsize=(8, 8))

# Create a nice, lined up colorbar axes (called "cax" here)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.2)

# Plot
median_values.plot(
    ax=ax, cax=cax, column="market_value", edgecolor="white", linewidth=0.5, legend=True
)

# Get the limits of the GeoDataFrame
xmin, ymin, xmax, ymax = median_values.total_bounds

# Set the xlims and ylims
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)

# Format
ax.set_axis_off()
ax.set_aspect("equal")

# Format cax labels
cax.set_yticklabels([f"${val:.0f}k" for val in cax.get_yticks()]);
/Users/nhand/opt/miniconda3/envs/musa-550-fall-2020/lib/python3.7/site-packages/ipykernel_launcher.py:25: UserWarning: FixedFormatter should only be used together with FixedLocator

Step 5: Make a hex bin map of median assessments

Hints:

  • You will need to use the C and reduce_C_function of the hexbin() function
  • Run plt.hexbin? for more help
  • Try testing the impact of setting bins='log' on the resulting map

Note: you should pass in the raw point data rather than any aggregated data to the hexbin() function

In [58]:
# Create the axes
fig, ax = plt.subplots(figsize=(10, 8))

# Use the .x and .y attributes
# NOTE: we are passing in the raw point values here! 
# Matplotlib is doing the binning and aggregation work for us!
xcoords = gdata.geometry.x
ycoords = gdata.geometry.y
hex_vals = ax.hexbin(
    xcoords, 
    ycoords,
    C=gdata.market_value / 1e3,
    reduce_C_function=np.median,
    bins="log",
    gridsize=30,
)

# Add the zillow geometry boundaries
zillow.plot(ax=ax, facecolor="none", edgecolor="white", linewidth=1, alpha=0.5)

# Add a colorbar and format
cbar = fig.colorbar(hex_vals, ax=ax)
ax.set_axis_off()
ax.set_aspect("equal")

# Format cbar labels
cbar.set_ticks([100, 1000])
cbar.set_ticklabels(["$100k", "$1M"]);

Step 6: Use altair to make an interactive choropleth

In [59]:
# Convert median values to EPSG=4326
median_values_4326 = median_values.to_crs(epsg=4326)

# Plot the map
alt.Chart(median_values_4326).mark_geoshape(stroke="white").encode(
    tooltip=["market_value:Q", "ZillowName:N"],
    color=alt.Color("market_value:Q", scale=alt.Scale(scheme='viridis'))
).properties(
    width=500, height=400
)
Out[59]:

Properties with the highest assessed values

They are all located in Center City

In [60]:
sorted_properties = gdata.sort_values(by='market_value', ascending=False)
top20 = sorted_properties.head(n=20)

top20
Out[60]:
parcel_number lat lng location market_value building_value land_value total_land_area total_livable_area Coordinates index_right ZillowName
110862 888501268 39.947159 -75.149844 500-06 WALNUT ST 17000000.0 15640000.0 1360000.0 0.0 8900.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
83350 888089410 39.948112 -75.169786 1706 RITTENHOUSE SQ 13596500.0 12508780.0 1087720.0 0.0 7725.0 POINT (-8367862.267 4858404.907) 117.0 Rittenhouse
119309 888095880 39.950683 -75.171026 130 S 18TH ST 12098000.0 11009180.0 1088820.0 0.0 9131.0 POINT (-8368000.327 4858778.265) 117.0 Rittenhouse
177180 888095438 39.951485 -75.164937 1414 S PENN SQ 10800000.0 9936000.0 864000.0 0.0 7622.0 POINT (-8367322.551 4858894.655) 117.0 Rittenhouse
230027 888092327 39.951553 -75.167150 50 S 16TH ST 10325200.0 9292680.0 1032520.0 0.0 7250.0 POINT (-8367568.825 4858904.648) 117.0 Rittenhouse
124117 888092206 39.951553 -75.167150 50 S 16TH ST 8928900.0 8036010.0 892890.0 0.0 5850.0 POINT (-8367568.825 4858904.648) 117.0 Rittenhouse
111012 888501266 39.947159 -75.149844 500-06 WALNUT ST 8100000.0 7452000.0 648000.0 0.0 4155.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
154027 888092208 39.951553 -75.167150 50 S 16TH ST 7800000.0 7020000.0 780000.0 0.0 5550.0 POINT (-8367568.825 4858904.648) 117.0 Rittenhouse
245208 888501264 39.947159 -75.149844 500-06 WALNUT ST 7800000.0 7176000.0 624000.0 0.0 4200.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
5324 888800320 39.954993 -75.170422 1800 ARCH ST 7556900.0 6801210.0 755690.0 0.0 7738.0 POINT (-8367933.114 4859404.156) 73.0 Logan Square
437426 888501262 39.947159 -75.149844 500-06 WALNUT ST 7500000.0 6900000.0 600000.0 0.0 4200.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
300869 888501260 39.947159 -75.149844 500-06 WALNUT ST 7200000.0 6624000.0 576000.0 0.0 4200.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
119306 888095876 39.950683 -75.171026 130 S 18TH ST 7150600.0 6507046.0 643554.0 0.0 5384.0 POINT (-8368000.327 4858778.265) 117.0 Rittenhouse
390741 888501244 39.947159 -75.149844 500-06 WALNUT ST 7150000.0 6578000.0 572000.0 0.0 5500.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
383998 888501258 39.947159 -75.149844 500-06 WALNUT ST 7000000.0 6440000.0 560000.0 0.0 4300.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
83348 888089404 39.948112 -75.169786 1706 RITTENHOUSE SQ 6980000.0 6421600.0 558400.0 0.0 4166.0 POINT (-8367862.267 4858404.907) 117.0 Rittenhouse
245627 888089406 39.948112 -75.169786 1706 RITTENHOUSE SQ 6980000.0 6421600.0 558400.0 0.0 4166.0 POINT (-8367862.267 4858404.907) 117.0 Rittenhouse
83349 888089408 39.948112 -75.169786 1706 RITTENHOUSE SQ 6980000.0 6421600.0 558400.0 0.0 4166.0 POINT (-8367862.267 4858404.907) 117.0 Rittenhouse
83347 888089402 39.948112 -75.169786 1706 RITTENHOUSE SQ 6980000.0 6421600.0 558400.0 0.0 4166.0 POINT (-8367862.267 4858404.907) 117.0 Rittenhouse
111011 888501256 39.947159 -75.149844 500-06 WALNUT ST 6800000.0 6256000.0 544000.0 0.0 4300.0 POINT (-8365642.328 4858266.547) 122.0 Society Hill
In [61]:
# Create the axes
fig, ax = plt.subplots(figsize=(12, 12))

# Plot the top 20 properties by market value
top20.plot(ax=ax, marker=".", color="crimson", markersize=200)

# Add the city limits (with the same CRS!)
city_limits = city_limits.to_crs(top20.crs)
city_limits.plot(
    ax=ax, edgecolor="black", linewidth=3, facecolor="none"
)

# Plot the basemap underneath
ctx.add_basemap(ax=ax, crs=top20.crs, source=ctx.providers.CartoDB.Positron)

# remove axis lines
ax.set_axis_off()

Let's zoom in on Center City

We can use the total_bounds of the top 20 properties dataframe.

In [62]:
# Create the axes
fig, ax = plt.subplots(figsize=(12, 12))

# Plot the top 20 properties by market value
top20.plot(ax=ax, marker=".", color="crimson", markersize=200)

# Add the city limits (with the same CRS!)
city_limits = city_limits.to_crs(top20.crs)
city_limits.plot(
    ax=ax, edgecolor="black", linewidth=3, facecolor="none"
)

# Set the xlims and ylims using the boundaries 
# of the top 20 properties and a padding (in meters)
xmin, ymin, xmax, ymax = top20.total_bounds
PAD = 1000
ax.set_xlim(xmin-PAD, xmax+PAD)
ax.set_ylim(ymin-PAD, ymax+PAD)

# Plot the basemap underneath
ctx.add_basemap(ax=ax, crs=top20.crs, source=ctx.providers.CartoDB.Positron)

# Remove axis lines
ax.set_axis_off()

Note

The axes limits must be set before the call to the ctx.add_basemap(). This enables contextily to load a basemap with the proper level of zoom!

--> The top 20 properties are actually all contained within just 6 buildings in Center City

That's it!

  • More interactive viz libraries and raster datasets next week!
  • Pre-recorded lecture will be posted on Sunday
  • See you next Thursday!
In [ ]: