Field Calculating Geometry Attributes in ArcGIS API for Python

2865
5
Jump to solution
09-09-2021 04:53 AM
AnneSantaMaria
New Contributor III

Good Morning,

I am trying to calculate geometry attributes (polygon extents, point centroids, areas etc) on a feature service hosted in our stand alone portal using notebooks and the ArcGIS API for Python. Our data is branch versioned and hosted on an enterprise portal. What I cannot figure out is how to field calculate things like geometry, extents, and point centroids in different coordinates. It seems the syntax on the API reference page doesn't work when trying to work with data accessed and assigned to a spatially enable data frame. Am I using the spatially enable data frame incorrectly or is there a different way this should be computed? 

This is what I would like to be able to do:

1. Calculate area (this can also be done using attribute rules)

2. Calculate the XY min/max extents of polygons in decimal degrees (not the spatial reference of the item)

3. Calculate centroids in decimal degrees.

Here is the code below. My problem is the feature set/spatially enable data frame is a dictionary and it appears the geometry module doesn't work the same with those items. The error I receive below: 

AttributeError: 'dict' object has no attribute 'area'

 

 

from arcgis.gis import GIS
#import modules
from arcgis.features import *
from arcgis.geometry import *
import os
import datetime

#access portal and pull the feature service and layer for analysis
gis = GIS('home')
search_result = gis.content.search('NotebookTesting', 'Feature Layer')
item = search_result[0]
item_layers = item.layers
feature_layer = item_layers[0]
#check if editing and updating are enabled
feature_layer.properties.capabilities 
#query the layer for items that meet criteria ie area is null
fset = feature_layer.query(where = 'AreaSqFeet IS NULL') 
flayer_rows = fset.sdf
fset.sdf
for index, row in flayer_rows.iterrows():
   if row ['Area_SqFeet'] == None:
           fset.features[index].geometry.area # here is where I think I would calculate area, centroids and extents
          feature_layer.edit_features(updates = fset.features)

 

 

 

 

0 Kudos
1 Solution

Accepted Solutions
jcarlson
MVP Esteemed Contributor

In short, yes, you're not quite using the spatially enabled dataframe correctly. Accomplishing your goal is quite simple, but you have to create an intermediate GeoSeriesAccessor object. This class allows you to perform operations like area, extent, etc., on an entire series of shapes, which avoids needing to iterate over your dataframe. You can also use the project_as method to get those outputs into degrees.

 

# Add this to your import statements
from arcgis.features import GeoSeriesAccessor

# Grab the layer directly, without as many intermediate outputs
# Using content.get avoids potentially inconsistent results from a search
lyr = gis.content.get('itemID').layers[0]

# Query features directly to a dataframe
df = lyr.query(where = 'AreaSqFeet IS NULL', as_df=True)

# Create GeoSeriesAccessor
gsa = GeoSeriesAccessor(df['SHAPE'])

# Use GSA to calculate fields
df['area_field'] = gsa.area
df['centroid_dd'] = gsa.centroid
df['extent_field'] = gsa.extent

 

 Mind you, centroid and extent return tuples, so you may need to do some extra work to reformat that as needed for your fields. It might look something like this:

 

new_col_list = ['xmin','ymin','xmax','ymax']
for n,col in enumerate(new_col_list):
    df[col] = df['extent'].apply(lambda extent: extent[n])

df.drop(columns='extent', inplace=True)

 

 If you have trouble projecting your dataframe / geoseriesaccessor, as I sometimes do, note that you can also specify a different spatial reference to be output from your query itself, by specifying out_sr=* as a parameter.

EDIT: I forgot to mention, when you're done, you can submit your edits in one go, rather than as part of an interative process. edit_features takes a featureset for the updates parameter, and you can convert your dataframe directly to a featureset once you've performed your calculations.

lyr.edit_features(updates = df.spatial.to_featureset())

 

- Josh Carlson
Kendall County GIS

View solution in original post

0 Kudos
5 Replies
jcarlson
MVP Esteemed Contributor

In short, yes, you're not quite using the spatially enabled dataframe correctly. Accomplishing your goal is quite simple, but you have to create an intermediate GeoSeriesAccessor object. This class allows you to perform operations like area, extent, etc., on an entire series of shapes, which avoids needing to iterate over your dataframe. You can also use the project_as method to get those outputs into degrees.

 

# Add this to your import statements
from arcgis.features import GeoSeriesAccessor

# Grab the layer directly, without as many intermediate outputs
# Using content.get avoids potentially inconsistent results from a search
lyr = gis.content.get('itemID').layers[0]

# Query features directly to a dataframe
df = lyr.query(where = 'AreaSqFeet IS NULL', as_df=True)

# Create GeoSeriesAccessor
gsa = GeoSeriesAccessor(df['SHAPE'])

# Use GSA to calculate fields
df['area_field'] = gsa.area
df['centroid_dd'] = gsa.centroid
df['extent_field'] = gsa.extent

 

 Mind you, centroid and extent return tuples, so you may need to do some extra work to reformat that as needed for your fields. It might look something like this:

 

new_col_list = ['xmin','ymin','xmax','ymax']
for n,col in enumerate(new_col_list):
    df[col] = df['extent'].apply(lambda extent: extent[n])

df.drop(columns='extent', inplace=True)

 

 If you have trouble projecting your dataframe / geoseriesaccessor, as I sometimes do, note that you can also specify a different spatial reference to be output from your query itself, by specifying out_sr=* as a parameter.

EDIT: I forgot to mention, when you're done, you can submit your edits in one go, rather than as part of an interative process. edit_features takes a featureset for the updates parameter, and you can convert your dataframe directly to a featureset once you've performed your calculations.

lyr.edit_features(updates = df.spatial.to_featureset())

 

- Josh Carlson
Kendall County GIS
0 Kudos
kmsmikrud
Occasional Contributor III

Hi @jcarlson ,

I was so excited last year to see your posts and have been using the code examples to query and edit an AGOL item to update the polyline length. Or I was until recently and now the code is throwing an error. After too many hours today, I upgraded my ArcGIS Pro version to the latest which fixed one error, but now I'm onto to a new one. 

I'm using ArcGIS Pro 3.1.0 and default conda environment, but I tried an older version of ArcGIS Pro 2.9.5 with arcgis 1.9.1 and the same code works there. 

 

 

lyr = gis.content.get('xxxxxxxxxxxxxxxxxxxxxxxxxxxxx').layers[2]
df = lyr.query(where = "LENGTH_NM IS NULL",out_sr = 26931, as_df=True)
gsa = GeoSeriesAccessor(df['SHAPE'])
df['LENGTH_NM'] = gsa.get_length('PLANAR', 'NAUTICALMILES')
df
lyr.edit_features(updates = df.spatial.to_featureset())

 

 

 This code does calculate the length of the polyline but fails on the edit statement. Would you have any ideas why? It worked last year and still works on the Pro 2.9.5 environment. It was amazing to be able to calculate length in the other projection. Did the edit_features syntax change in the latest version?

When the above runs in a notebook and a python script tool within ArcGIS Pro, the same error results "ValueError: StringArray requires a sequence of strings or pandas.NA" I've attached a screenshot.

Thanks in advance!,

Kathy

0 Kudos
kmsmikrud
Occasional Contributor III

Update:

As it turns out, I had a bad install of ArcGIS Pro. An uninstall/install solved both the missing packages in the package manager on this post (https://community.esri.com/t5/arcgis-api-for-python-questions/arcgis-pro-3-1-0-arcgis-package-does-n...)

The code above now works too! which is great but just wish I would have realized something wasn't right on the install earlier.

0 Kudos
kmsmikrud
Occasional Contributor III

Hi!,

I'm still running into issues with the code above in ArcGIS Pro 3.0.3 although it seems to work for 1 polyline layer and not another?  In the code below, the second line with .sdf will work and not throw an error, but the original code statement like your example throws an error. Can you help translate why this would be? Another weird thing happening, is when the code calculates the length, it is changing the CreationDate/EditDate fields or actually all DateTime fields which I don't remember happening. thanks for any insight/translation? 🙂

#df = lyr.query(where = "LENGTH_NM IS NULL", out_sr = 26931, as_df=True)
df = lyr.query(where = "LENGTH_NM IS NULL", out_sr = 26931).sdf

  

0 Kudos
AnneSantaMaria
New Contributor III

Thank you so much for helping. I would have never thought to look at the geoaccessorseries object. I was able to work through the scripting component and run the script until actually applying the updates. The edits cannot be applied "True curve update not allowed." Error code 400.  Which is weird because the service does Support True Curves : set to true. That may be something with our data/server and nothing to do with the script BUT again thank you! The script works like a charm. 

0 Kudos