18  Employment Centers

Code
import contextily as ctx
import geopandas as gpd
import matplotlib.pyplot as plt
import pandana as pdna
import pandas as pd
from geosnap import DataStore
from geosnap import analyze as gaz
from geosnap import io as gio

%load_ext watermark
%watermark -a 'eli knaap'  -iv
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
Author: eli knaap

pandana   : 0.7
geopandas : 0.14.4
matplotlib: 3.4.3
pandas    : 1.5.3
contextily: 1.5.2
geosnap   : 0.12.1.dev9+g3a1cb0f6de61.d20240110

Giuliano & Small (1991), Giuliano & A. Small (1999), Giuliano et al. (2007), Giuliano et al. (2012) Cervero & Wu (1998), Gordon & Richardson (1996), Helsley & Sullivan (1991), McDonald & McMillen (2000)

18.1 Start with an Accessibility Surface

Code
datasets = DataStore()
Code
sd_acs = gio.get_acs(datasets, county_fips="06073", years=[2021], level="tract")
/Users/knaaptime/Dropbox/projects/geosnap/geosnap/io/util.py:275: UserWarning: Unable to find local adjustment year for 2021. Attempting from online data
  warn(
/Users/knaaptime/Dropbox/projects/geosnap/geosnap/io/constructors.py:215: UserWarning: Currency columns unavailable at this resolution; not adjusting for inflation
  warn(
Code
sd_lodes = gio.get_lodes(datasets, county_fips="06073", years="2021")

instantiate the network

Code
sd_network = pdna.Network.from_hdf5(
    "../data/41740.h5"
)
Generating contraction hierarchies with 10 threads.
Setting CH node vector of size 332554
Setting CH edge vector of size 522484
Range graph removed 143094 edges of 1044968
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
Code
sd_network.precompute(2500)  # generate contraction hierarchies

get the intersection IDs nearest to the centroid of each census block (mapping the job data to the travel network)

save these IDs as a variable on the input dataframe so we can use them as a join index

Code
sd_lodes["node_ids"] = sd_network.get_node_ids(
    sd_lodes.centroid.geometry.x, sd_lodes.centroid.geometry.y
)

# get the node ids for tracts (for visualization
sd_acs["node_ids"] = sd_network.get_node_ids(
    sd_acs.centroid.geometry.x, sd_acs.centroid.geometry.y
)
/var/folders/79/cknfb1sx2pv16rztkpg6wzlw0000gn/T/ipykernel_1937/430745399.py:2: UserWarning: Geometry is in a geographic CRS. Results from 'centroid' are likely incorrect. Use 'GeoSeries.to_crs()' to re-project geometries to a projected CRS before this operation.

  sd_lodes.centroid.geometry.x, sd_lodes.centroid.geometry.y
/var/folders/79/cknfb1sx2pv16rztkpg6wzlw0000gn/T/ipykernel_1937/430745399.py:7: UserWarning: Geometry is in a geographic CRS. Results from 'centroid' are likely incorrect. Use 'GeoSeries.to_crs()' to re-project geometries to a projected CRS before this operation.

  sd_acs.centroid.geometry.x, sd_acs.centroid.geometry.y

set the job variable onto the network

Code
sd_network.set(sd_lodes.node_ids, sd_lodes.total_employees.values, name="jobs")
Removed 12657 rows because they contain missing values

create an aggregation query (here, a distance-weighted sum with a linear decay up to 2000 meters)

Code
job_access_exp = sd_network.aggregate(2000, name="jobs", decay="exp").rename(
    "job_access"
)
Code
sd_acs = sd_acs.merge(job_access_exp, left_on="node_ids", right_index=True)

sd_lodes = sd_lodes.merge(job_access_exp, left_on="node_ids", right_index=True)

18.2 Identifying Centers

Code
from esda.getisord import G_Local
from libpysal.graph import Graph

18.2.1 An Aggregate Approach

just use zones and contiguity relationships

Code
wacs = Graph.build_contiguity(sd_acs, rook=False).assign_self_weight(1)
Code
g_jobs = G_Local(sd_acs.job_access.values, wacs, star=True)
Code
centers = sd_acs[["geoid", "job_access", "geometry"]].copy()
centers["pvalue"] = g_jobs.p_sim
Code
centers["job_rank"] = centers.job_access.rank(pct=True)
Code
# define subcenter as: total job access in top quintile, significant (G_i*) spatial cluster

subcenters = centers[(centers.pvalue < 0.05) & (centers.job_rank > 0.8)]
Code
subcenters.explore(tiles="CartoDB Positron")
Make this Notebook Trusted to load map: File -> Trust Notebook

Use a spatial weights matrix to dissolve contiguous centers McMillen (2003)

Code
w_subcent = Graph.build_contiguity(subcenters, rook=False)
Code
subcenters["center"] = w_subcent.component_labels
subcenters["center"] = subcenters["center"].apply(lambda x: f"center {x}")
/Users/knaaptime/mambaforge/envs/urban_analysis/lib/python3.10/site-packages/geopandas/geodataframe.py:1528: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  super().__setitem__(key, value)
Code
subcenters = subcenters.dissolve("center")
Code
subcenters.explore(tiles="CartoDB Positron")
Make this Notebook Trusted to load map: File -> Trust Notebook

18.2.2 A Detailed Approach

Since we’re operating on census tracts, our resulting subcenters are a function of tract size and contiguity, but the data are actually much higher resolution than that. We have computed accessibility at the intersection level, rather than the tract level, so we could instead look for significant \(G_i\ast\) clusters of these intersections instead (then wrap a polygon around them). This is the approach taken in Knaap and Rey (forthcoming)

With the new libpysal Graph, we can also use network distances to create our weights object, rather than relying on contiguity between polygons

Code
nodes_df = sd_network.nodes_df.copy()
all_intersections = gpd.GeoDataFrame(
    nodes_df, geometry=gpd.points_from_xy(nodes_df.x, nodes_df.y, crs=4326)
)
Code
all_intersections = gpd.overlay(
    all_intersections.reset_index(),
    sd_acs[sd_acs.n_total_pop > 0],
)
/var/folders/79/cknfb1sx2pv16rztkpg6wzlw0000gn/T/ipykernel_1937/1609565934.py:1: UserWarning: CRS mismatch between the CRS of left geometries and the CRS of right geometries.
Use `to_crs()` to reproject one of the input geometries to match the CRS of the other.

Left CRS: EPSG:4326
Right CRS: EPSG:4269

  all_intersections = gpd.overlay(
Code
all_intersections.head()
id x y geoid n_mexican_pop n_cuban_pop n_puerto_rican_pop n_russian_pop n_italian_pop n_german_pop ... p_poverty_rate_children p_poverty_rate_white p_poverty_rate_black p_poverty_rate_hispanic p_poverty_rate_native p_poverty_rate_asian year node_ids job_access geometry
0 17413808 -117.482356 33.313768 06073018700 8123.0 436.0 915.0 22.0 403.0 1735.0 ... 2.181802 4.291922 0.775055 5.174499 0.0 0.0 2021 48972438 71.909646 POINT (-117.48236 33.31377)
1 17413859 -117.443005 33.272257 06073018700 8123.0 436.0 915.0 22.0 403.0 1735.0 ... 2.181802 4.291922 0.775055 5.174499 0.0 0.0 2021 48972438 71.909646 POINT (-117.44301 33.27226)
2 48858656 -117.312332 33.308842 06073018700 8123.0 436.0 915.0 22.0 403.0 1735.0 ... 2.181802 4.291922 0.775055 5.174499 0.0 0.0 2021 48972438 71.909646 POINT (-117.31233 33.30884)
3 48858662 -117.312363 33.309105 06073018700 8123.0 436.0 915.0 22.0 403.0 1735.0 ... 2.181802 4.291922 0.775055 5.174499 0.0 0.0 2021 48972438 71.909646 POINT (-117.31236 33.30911)
4 48859028 -117.259321 33.382158 06073018700 8123.0 436.0 915.0 22.0 403.0 1735.0 ... 2.181802 4.291922 0.775055 5.174499 0.0 0.0 2021 48972438 71.909646 POINT (-117.25932 33.38216)

5 rows × 163 columns

Code
all_intersections = all_intersections.set_index("id")[["geometry"]].join(job_access_exp)
Code
all_intersections
geometry job_access
id
17413808 POINT (-117.48236 33.31377) 411.692059
17413859 POINT (-117.44301 33.27226) 0.000000
48858656 POINT (-117.31233 33.30884) 59.862004
48858662 POINT (-117.31236 33.30911) 58.988646
48859028 POINT (-117.25932 33.38216) 1733.429682
... ... ...
6048892359 POINT (-117.14402 33.00649) 78.970270
6048892368 POINT (-117.14356 33.00638) 76.411785
6279965184 POINT (-117.12360 33.01108) 923.483956
6279966085 POINT (-117.12370 33.01112) 923.481666
6279966086 POINT (-117.12404 33.01164) 922.467477

245061 rows × 2 columns

Code
all_intersections.plot("job_access", scheme="quantiles", k=10, alpha=0.4)

Code
all_intersections.shape
(245061, 2)

That’s a really big problem, so let’s simplify and use the subset of nodes where blocks are attached

Code
sd_lodes = sd_lodes.to_crs(sd_lodes.estimate_utm_crs())
Code
all_intersections = all_intersections.to_crs(all_intersections.estimate_utm_crs())
Code
# only the nodes attached to a census block
lodes_nodes = all_intersections[all_intersections.index.isin(sd_lodes.node_ids)]

lodes_nodes.plot("job_access", scheme="quantiles", k=10, alpha=0.4)

This is still extremely detailed, but we are not including additional geometries where we have no actual data points (i.e. our resolution is only as fine as the block-level where the jobs data come from)

Now we use the nodes_in_range function to return an adjacency list of nodes within two kilometers and convert that into a libpysal.Graph

Code
w_dist = sd_network.nodes_in_range(lodes_nodes.index.unique(), 2000)
Code
w_dist
source destination distance
0 17413808 17413808 0.000000
1 17413808 622073853 95.065002
2 17413808 49717289 152.725006
3 17413808 49717290 181.914001
4 17413808 49717292 247.904999
... ... ... ...
32239675 5872452140 5872457594 446.095001
32239676 5872452140 5872457620 541.169006
32239677 5872452140 5872457595 682.549011
32239678 5872452140 5872457623 721.541992
32239679 5872452140 5872457621 741.148987

21084307 rows × 3 columns

Code
g = Graph.from_adjacency(w_dist, "source", "destination", "distance")
Code
# ignore distance for now and treat distance-band as discrete within threshold (then row-standardize)

g = g.transform("b").assign_self_weight(1)

Now, g is a PySAL graph object that encodes observations as neighbors if they are within a 2km shortest-path distance along the travel network.

Code
g_nodes = G_Local(lodes_nodes.job_access, g, permutations=9999)
Code
lodes_nodes["pvalue"] = g_nodes.p_sim
lodes_nodes["job_rank"] = lodes_nodes.job_access.rank(pct=True)
/Users/knaaptime/mambaforge/envs/urban_analysis/lib/python3.10/site-packages/geopandas/geodataframe.py:1528: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  super().__setitem__(key, value)
/Users/knaaptime/mambaforge/envs/urban_analysis/lib/python3.10/site-packages/geopandas/geodataframe.py:1528: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  super().__setitem__(key, value)
Code
center_nodes = lodes_nodes[(lodes_nodes.pvalue < 0.05) & (lodes_nodes.job_rank > 0.8)]
Code
center_nodes = center_nodes.set_crs(sd_lodes.crs)
Code
center_nodes.explore(tiles="CartoDB Positron")
Make this Notebook Trusted to load map: File -> Trust Notebook

Moving to the intersection scale results in a much larger employment center footprint, because there are many more observations, the surface is much more distinct, and its easier to pick out spatial outliers. The difficulty is that now our dataset is represented as a collection of points, when we want to identify discrete center polygons.

To do that, we first define a range wherein points are considered to belong to the same center (instead of setting this with a discrete threshold rule, you could alternatively use something like DBscan). Then, for each set of points, we wrap a tightly-fitted polygon that represents the “center”.

Code
# w_dist_subcenters = DistanceBand.from_dataframe(center_nodes, threshold=1000)
w_dist_subcenters = sd_network.nodes_in_range(center_nodes.index, 2000)
g_subcenters = Graph.from_adjacency(
    w_dist_subcenters, "source", "destination", "distance"
)
Code
center_nodes["center"] = g_subcenters.component_labels
Code
center_nodes.explore("center", categorical=True, tiles="CartoDB Positron")
Make this Notebook Trusted to load map: File -> Trust Notebook
Code
from libpysal.cg import alpha_shape_auto
Code
shapes = [
    alpha_shape_auto(
        center_nodes[center_nodes.center == i]
        .geometry.get_coordinates()[["x", "y"]]
        .values
    )
    for i in center_nodes.center.unique()
]
Code
from shapely.geometry import MultiPoint
Code
node_subcenters = gpd.GeoDataFrame(geometry=shapes, crs=center_nodes.crs)
Code
node_subcenters.reset_index()
index geometry
0 0 POLYGON ((476177.710 3628013.087, 476166.769 3...
1 1 POLYGON ((484138.922 3666537.721, 483506.237 3...
2 2 POLYGON ((492608.019 3612336.120, 493130.687 3...
3 3 POLYGON ((492229.835 3662858.498, 492101.140 3...
4 4 POLYGON ((477562.309 3639706.611, 478754.602 3...
5 5 POLYGON ((505544.334 3629305.399, 505602.290 3...
6 6 POLYGON ((479175.673 3667494.894, 478322.793 3...
7 7 POLYGON ((478266.078 3646848.947, 478427.199 3...
8 8 POLYGON ((484763.031 3633474.295, 485381.506 3...
9 9 POLYGON ((468051.323 3669650.875, 468298.265 3...
10 10 POLYGON ((500714.997 3626600.774, 500386.032 3...
11 11 POLYGON ((492040.490 3652703.770, 491506.739 3...
12 12 POLYGON ((465616.855 3673255.291, 465701.935 3...
13 13 POLYGON ((473215.150 3655093.988, 473085.962 3...
14 14 POLYGON EMPTY
15 15 POLYGON ((475501.515 3634708.668, 475598.712 3...
16 16 POLYGON EMPTY
17 17 POLYGON ((495116.735 3644719.160, 495513.397 3...
18 18 POLYGON ((492567.646 3650273.189, 492367.044 3...
19 19 POLYGON ((489869.499 3641092.273, 489329.957 3...
20 20 POLYGON ((485657.445 3638797.770, 485345.106 3...
21 21 POLYGON ((489582.936 3653723.104, 489636.379 3...
Code
node_subcenters = node_subcenters[~node_subcenters.geometry.is_empty]
Code
node_subcenters.reset_index().explore(
    "index", categorical=True, tiles="CartoDB Positron"
)
Make this Notebook Trusted to load map: File -> Trust Notebook

18.2.3 Alternative Polygons

Code
from shapely import concave_hull
from shapely.geometry import MultiPoint

rather than an alpha_shape, we could use other concave hull methods, e.g. from shapely

Code
MultiPoint(center_nodes[center_nodes.center == 1].geometry.tolist())

Code
hulls = {}
Code
for ratio in [.05, .1, .2]:
    hulls[f'ch {ratio}'] = concave_hull(MultiPoint(center_nodes[center_nodes.center == 1].geometry.tolist()),
    ratio,
    allow_holes=False,)
Code
hulls["ch 0.05"]

Code
hulls["ch 0.1"]

Code
hulls["ch 0.2"]

Code
hulls["alpha"] = alpha_shape_auto(
    center_nodes[center_nodes.center == 1]
    .geometry.geometry.get_coordinates()[["x", "y"]]
    .values
)
hulls["alpha"]

Code
hulls
{'ch 0.05': <POLYGON ((481497.467 3667165.837, 481421.436 3667219.129, 481466.467 366735...>,
 'ch 0.1': <POLYGON ((481497.467 3667165.837, 481421.436 3667219.129, 481466.467 366735...>,
 'ch 0.2': <POLYGON ((481497.467 3667165.837, 481421.436 3667219.129, 481466.467 366735...>,
 'alpha': <POLYGON ((484138.922 3666537.721, 483506.237 3666322.608, 483220.644 366619...>}
Code
gpd.GeoSeries(hulls)
ch 0.05    POLYGON ((481497.467 3667165.837, 481421.436 3...
ch 0.1     POLYGON ((481497.467 3667165.837, 481421.436 3...
ch 0.2     POLYGON ((481497.467 3667165.837, 481421.436 3...
alpha      POLYGON ((484138.922 3666537.721, 483506.237 3...
dtype: geometry
Code
isos = gpd.GeoDataFrame(geometry=gpd.GeoSeries(hulls), crs=node_subcenters.crs)
Code
isos
geometry
ch 0.05 POLYGON ((481497.467 3667165.837, 481421.436 3...
ch 0.1 POLYGON ((481497.467 3667165.837, 481421.436 3...
ch 0.2 POLYGON ((481497.467 3667165.837, 481421.436 3...
alpha POLYGON ((484138.922 3666537.721, 483506.237 3...
Code
f,ax = plt.subplots(2,2, figsize=(12,8))
ax=ax.flatten()

for i in range(4):
    center_nodes[center_nodes.center == 1].plot(ax=ax[i])
    isos.iloc[[i]].plot(ax=ax[i], alpha=0.4)
    ax[i].axis('off')
    ax[i].set_title(f"{isos.iloc[i].name}")

Code
shapes_ch = [
    concave_hull(
        MultiPoint(center_nodes[center_nodes.center == i].geometry.tolist()),
        0.2,
        allow_holes=False,
    )
    for i in center_nodes.center.unique()
]
node_subcenters_ch = gpd.GeoDataFrame(geometry=shapes_ch, crs=center_nodes.crs)
Code
node_subcenters_ch.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

18.3 Comparison

https://opendata.sandag.org/Geographic-Information-Systems/Employment-Centers-outlines-/ixp5-dfuk/about_data

Employment Centers are identified as areas with a high concentration of employment based on SANDAG’s 2016 employment inventory from Urbansim summarized to quarter-mile radius hexagons. Local-maxima were identified as starting points, and regions were grown to include neighboring hexagons meeting a minimum employment density threshold within an approximate 2-mile radius. The resulting boundaries were generalized (taking into account major barrier features such as topography and freeways) and used to select SANDAG Master Geographic Reference Areas (MGRAs) by activity-weighted (population and employment) centroid. Employment centers are ranked by Tier based on total number of jobs:

Tier 1: 50,000 or more jobs

Tier 2: 25,000 to 49,999 jobs

Tier 3: 15,000 to 24,999 jobs

Tier 4: 2,500 to 14,999 jobs

Hexbin geography (where people live map layers) – generated using a quarter-mile radius hex bin grid overlay on the region. Data is summarized by hex bin spatially using the centroid of an overlying geography.

Where people live by Employment Center they Work In –2015 LEHD census block level data; converted to hexbin (see hexbin geography). LEHD data was used as it contains information on both the home and work location. Data represents an employee residence by the employment center they work in. https://lehd.ces.census.gov/data/.

Code
sandag = gpd.read_file("https://opendata.sandag.org/resource/ixp5-dfuk.geojson")
Code
m = sandag.explore(tiles="CartoDB Positron")
m
Make this Notebook Trusted to load map: File -> Trust Notebook

These are the official SANDAG employment centers

SANDAG employment centers in blue versus the accessibility method in red

Code
node_subcenters.reset_index().explore(color="red", m=m)
Make this Notebook Trusted to load map: File -> Trust Notebook

Although the extent of the polygons differ, the accessibility method agrees with the SANDAG employment centers for all centers in Tiers 1-3. With the exception of Carlsbad, Oceanside, and Encinitas, most of SANDAG’s tier 4 centers fail to meet either the density or significant threshold (often both)

In the map below, the access-based employment centers are represented using intersecting census block geometries

Code
m = sandag.explore(tiles="CartoDB Positron")
gpd.sjoin(sd_lodes, node_subcenters).dissolve("index_right").explore(color="red", m=m)
Make this Notebook Trusted to load map: File -> Trust Notebook

ignoring the G statistic and looking only at nodes with job access in the top quartile

Code
m = sandag.explore(tiles="CartoDB Positron")
lodes_nodes[lodes_nodes.job_rank > 0.75].explore(m=m, color="red")
Make this Notebook Trusted to load map: File -> Trust Notebook

When employment accessibility, alone, is the selection criteria (as in the map above), using nodes in the top quintile usually finds the remaining Tier 4 centers, except for some exurban places like Ramona and Otay Mesa

If you really want to be generous, just take the top half of the access distribution

Code
m = sandag.explore(tiles="CartoDB Positron")
lodes_nodes[lodes_nodes.job_rank > 0.5].explore(m=m, color="red")
Make this Notebook Trusted to load map: File -> Trust Notebook

With this simple rule, we cover all the areas defined by SANDAG, albeit with a much higher resolution

Code
center_nodes.groupby('center')['geometry'].apply(lambda x:MultiPoint(x.tolist())).concave_hull(ratio=0.3)
center
0     POLYGON ((475740.863 3629550.069, 475785.895 3...
1     POLYGON ((481497.467 3667165.837, 481421.436 3...
2     POLYGON ((478626.940 3622389.733, 478746.507 3...
3     POLYGON ((487921.506 3665387.237, 487726.825 3...
4     POLYGON ((476854.967 3638339.245, 476477.465 3...
5     POLYGON ((500942.165 3632589.334, 500856.468 3...
6     POLYGON ((473715.932 3664532.879, 473280.378 3...
7     POLYGON ((477224.294 3645759.606, 477283.383 3...
8     POLYGON ((484098.196 3633232.231, 484001.103 3...
9     POLYGON ((467128.955 3669454.711, 466933.618 3...
10    POLYGON ((496215.626 3625559.179, 496180.952 3...
11    POLYGON ((492040.490 3652703.770, 491194.576 3...
12    POLYGON ((464277.053 3673251.237, 464326.380 3...
13    POLYGON ((472322.098 3656543.508, 472304.440 3...
14    LINESTRING (483474.242 3616202.125, 483339.776...
15    POLYGON ((473759.917 3633513.758, 473686.939 3...
16                       POINT (476644.221 3640184.472)
17    POLYGON ((495384.619 3643950.046, 494570.770 3...
18    POLYGON ((492367.044 3650010.250, 492355.525 3...
19    POLYGON ((489329.957 3640340.090, 489196.361 3...
20    POLYGON ((485345.106 3638754.916, 484729.840 3...
21    POLYGON ((489441.942 3653446.735, 489407.046 3...
dtype: geometry
Code
def _geom_to_alpha(geom, ratio=0.1, allow_holes=False):
    return concave_hull(MultiPoint(geom.tolist()),ratio=ratio, allow_holes=allow_holes)
Code
center_nodes.groupby('center')['geometry'].apply(_geom_to_alpha)
center
0     POLYGON ((475740.863 3629550.069, 475785.895 3...
1     POLYGON ((481497.467 3667165.837, 481421.436 3...
2     POLYGON ((478626.940 3622389.733, 478746.507 3...
3     POLYGON ((488089.723 3665809.739, 487726.825 3...
4     POLYGON ((477255.518 3638738.703, 476477.465 3...
5     POLYGON ((501308.304 3633209.555, 500856.468 3...
6     POLYGON ((473715.932 3664532.879, 473280.378 3...
7     POLYGON ((477400.303 3646328.334, 477283.383 3...
8     POLYGON ((484098.196 3633232.231, 484001.103 3...
9     POLYGON ((467128.955 3669454.711, 466933.618 3...
10    POLYGON ((496215.626 3625559.179, 496180.952 3...
11    POLYGON ((492040.490 3652703.770, 491194.576 3...
12    POLYGON ((464396.100 3673394.063, 464326.380 3...
13    POLYGON ((472369.451 3656549.050, 472304.440 3...
14    LINESTRING (483474.242 3616202.125, 483339.776...
15    POLYGON ((473904.386 3633609.823, 473686.939 3...
16                       POINT (476644.221 3640184.472)
17    POLYGON ((495384.619 3643950.046, 494570.770 3...
18    POLYGON ((492367.044 3650010.250, 492355.525 3...
19    POLYGON ((489329.957 3640340.090, 489196.361 3...
20    POLYGON ((485345.106 3638754.916, 484729.840 3...
21    POLYGON ((489441.942 3653446.735, 489407.046 3...
Name: geometry, dtype: geometry

:::