GEOG 485:
GIS Programming and Software Development

Lesson 3 Practice Exercise A Solution

PrintPrint

Below is one possible solution to Practice Exercise A with comments to explain what is going on. If you find a more efficient way to code a solution, please share it through the discussion forums. Please note that in order to make the changes to citiesLayer permanent, you have to write the layer back to disk using the arcpy.CopyFeatures_management(...) function. This is not shown in the solution here.

# This script determines the percentage of cities in the
#  state with park and ride facilities
 
import arcpy

arcpy.env.overwriteOutput = True
arcpy.env.workspace = r"C:\PSU\geog485\L3\PracticeExerciseA\Washington.gdb"
cityBoundariesFC = "CityBoundaries"
parkAndRideFC = "ParkAndRide"
parkAndRideField = "HasParkAndRide"   # Name of column with Park & Ride information
citiesWithParkAndRide = 0             # Used for counting cities with Park & Ride 

try:      
    # Narrow down the cities layer to only the cities that contain a park and ride
    citiesLayer = arcpy.SelectLayerByLocation_management(cityBoundariesFC, "CONTAINS", parkAndRideFC)
 
    # Create an update cursor and loop through the selected records
    with arcpy.da.UpdateCursor(citiesLayer, (parkAndRideField)) as cursor:
        for row in cursor:
            # Set the park and ride field to TRUE and keep a tally
            row[0] = "True"
            cursor.updateRow(row)
            citiesWithParkAndRide += 1
except:
    print ("There was a problem performing the spatial selection or updating the cities feature class")

# Delete the feature layers even if there is an exception (error) raised
finally: 
    arcpy.Delete_management(citiesLayer)
    del row, cursor
     
# Count the total number of cities (this tool saves you a loop)
numCitiesCount = arcpy.GetCount_management(cityBoundariesFC)
numCities = int(numCitiesCount[0])

# Get the number of cities in the feature layer
#citiesWithParkAndRide = int(citiesLayer[2])

# Calculate the percentage and print it for the user
percentCitiesWithParkAndRide = (citiesWithParkAndRide / numCities) * 100
 
print (str(round(percentCitiesWithParkAndRide,1)) + " percent of cities have a park and ride.")

Below is a video offering some line-by-line commentary on the structure of this solution:

 

Video: One Solution to Lesson 3, Practice Exercise A (9:37)

Click here for transcript of the Solution to Lesson 3, Practice Exercise A.

This video shows one solution to Lesson 3, Practice Exercise A, which determines the percentage of cities in the State of Washington that have park and ride facilities.

This solution uses the syntax currently shown in the ArcGIS Pro documentation for Select Layer By Location, in which the FeatureLayer returned by the tool is stored in a variable.

Looking at this solution, I start by importing the arcpy site package. Then I added this line 6 to set the overwriteOutput property to True, which is useful when you're writing scripts like these where you'll be doing some testing, and you'll often want to overwrite the output files. Otherwise, ArcGIS will return an error message saying that it cannot overwrite an output that already exists. So, that's a tip to take from this code.

On line 7, I set the workspace to the Washington file geodatabase that stores the data used in the exercise. That enables me to refer to the input feature classes using only their names, not their full paths. This script doesn’t produce any new output feature classes, but if it did, I could likewise specify them using only their names and not their full paths.

So, in lines 8 & 9, I define variables to refer to the CityBoundaries and ParkAndRide feature classes.

The purpose of this script is to update a field in the CityBoundaries feature class called HasParkAndRide with either a True or False value. So on line 10 I define a parkAndRideField variable to store the name of that field I’m going to be updating.

I also want to report the percentage of cities containing a park and ride at the end of the script through a print statement, so on line 11 I define a variable to store a count of those cities and I initialize that variable to 0.

Now, before I talk about the meat of the script, it’s often helpful to walk through your processing steps manually in Pro. So here I have the ParkAndRides, symbolized by black dots, and the city boundaries that are the orange polygons.

So, I can use the Select By Location tool and specify that I want to select the CityBoundaries features that contain ParkAndRide features. You can see all the city features selected, and I can open the attribute table to see the selected tabular records. I've got 74 of 108 selected. And then over here is the HasParkAndRide field that I would want to update to True for these selected records. 

Getting back to my script, I’m going to implement the same Select By Location tool. It’s going to return to me a Feature Layer of cities that contain a ParkAndRide, which I store in my citiesLayer variable. I then open up an update cursor on that Feature Layer. This update cursor will only be able to update the fields that I specify in the second parameter. I could include any number of fields here, as a tuple, but for this scenario I really only need to include one. And the name of that field is stored in my parkAndRideField variable, which I had defined up on line 10.

And so what I have is a cursor that I can iterate through row by row. And I do that using a for loop, which starts on line 19. And on line 21, I take that HasParkAndRide field, and I set it to “True”.

Now, why do I say 0 there in square brackets? 0 is the index position of the field that I passed in that tuple in line 18. The first, and in this case the only, field in the tuple is at position 0.

If I was updating three or four fields, I would have row and then the field index position inside brackets. It could be 1, 2, 3, and so on.

After I've made my changes, I need to call updateRow() in order for them to be applied. A mistake that a lot of beginners make is they forget to do this step. So in line 22, I'm calling updateRow().

And in line 23, I'm incrementing the counter that I'm keeping of the number of cities with a park and ride facility found. So I just add 1 to the counter. The plus-equals-1 syntax is a shortcut for saying take the number in this variable and add 1.

I'm using try/except here to provide a custom error message. An important part of this script that goes along with this try/except block is this finally block on lines 28-30. This contains code that I want to run whether or not the try code failed. I don’t want to leave a lock in place on the cities feature class, so I delete the Feature Layer based on it on line 29. I also use a Python del statement to clear the row and cursor objects out of memory, for the same reason I used arcpy’s Delete tool: to make sure no locks are left on the feature classes.

Now, I could have put these statements in the try block, say after the update cursor, and assuming there were no problems in running my code, those clean-up statements would get executed too. However, let’s say there was a problem in my update cursor. If the script crashes there, then my clean-up statements won’t get executed, and if I try to run the script again, I could run into a problem with the cities feature class having a lock on it. Putting the clean-up statements in the finally block ensures they’ll get executed in all situations.

Now that I've done the important stuff, it's time to do some math to figure out the percentage of cities that have a park and ride facility. So in line 33, I'm running the GetCount tool in ArcGIS. When you use the GetCount tool, it’s a bit unintuitive because what it returns isn’t a number, but a result object. What you can do with that object isn’t well documented, so you can just follow on the example here and use [0] to get at the count in string form, and use the int() function to convert that string to an integer.

So what we have in the end, on line 34, is a variable called numCities that is the total number of cities in the entire data set. And then, we've already been keeping the counter of cities that have a park and ride. So to find the percentage, all we need to do is divide one by the other. And that's what's going on in line 40.

Now, you may have noticed line 37 refers to the citiesWithParkAndRide variable, but is commented out. It turns out that the object returned by SelectLayerByLocation (and SelectLayerByAttribute) isn’t just the Feature Layer of selected cities. Like the GetCount tool we just saw, the Select tools actually return a Result object. This object can be treated kind of like a list. If you want the Feature Layer, you can specify [0] or simply not include a number in brackets at all. But you can also get a count of the selected features by specifying [2]. You can see this in the documentation of the tool, in the Derived Output section.

So, it turns out we don’t really need to implement our own counter inside the loop through the cursor rows. We can get the count through the Result object, and thus we could uncomment line 37 and comment out lines 23 and 11. We decided to include the counter approach in this solution because as a programmer, there are bound to be times where you really do need to maintain your own counter, and we wanted you to see an example of how to do that.

Finally, we can print out the result at the end of the script in line 42. The percentage is a number with several digits after the decimal point, so the print statement first rounds the number to one digit after the decimal and then converts that value to a string so that it can be concatenated with the literal text.

Credit: S. Quinn and J. Detwiler © Penn State is licensed under CC BY-NC-SA 4.0.

Here is a different solution to Practice Exercise A, which uses the alternate syntax discussed in the lesson:

# This script determines the percentage of cities in the
#  state with park and ride facilities
 
import arcpy

arcpy.env.overwriteOutput = True
arcpy.env.workspace = r"C:\PSU\geog485\L3\PracticeExerciseA\Washington.gdb"
cityBoundariesFC = "CityBoundaries"
parkAndRideFC = "ParkAndRide"
parkAndRideField = "HasParkAndRide"   # Name of column with Park & Ride information
citiesWithParkAndRide = 0             # Used for counting cities with Park & Ride
 
try:
    # Make a feature layer of all the park and ride facilities
    arcpy.MakeFeatureLayer_management(parkAndRideFC, "ParkAndRideLayer")
 
    # Make a feature layer of all the cities polygons   
    arcpy.MakeFeatureLayer_management(cityBoundariesFC, "CitiesLayer")
 
except:
    print ("Could not create feature layers")
 
try:      
    # Narrow down the cities layer to only the cities that contain a park and ride
    arcpy.SelectLayerByLocation_management("CitiesLayer", "CONTAINS", "ParkAndRideLayer")
 
    # Create an update cursor and loop through the selected records
    with arcpy.da.UpdateCursor("CitiesLayer", (parkAndRideField)) as cursor:
        for row in cursor:
            # Set the park and ride field to TRUE and keep a tally
            row[0] = "True"
            cursor.updateRow(row)
            citiesWithParkAndRide +=1
except:
    print ("There was a problem performing the spatial selection or updating the cities feature class")
    
# Delete the feature layers even if there is an exception (error) raised
finally: 
    arcpy.Delete_management("ParkAndRideLayer")
    arcpy.Delete_management("CitiesLayer")
    del row, cursor
     
# Count the total number of cities (this tool saves you a loop)
numCitiesCount = arcpy.GetCount_management(cityBoundariesFC)
numCities = int(numCitiesCount[0])
 
# Calculate the percentage and print it for the user
percentCitiesWithParkAndRide = (citiesWithParkAndRide / numCities) * 100
 
print (str(round(percentCitiesWithParkAndRide,1)) + " percent of cities have a park and ride.")

Below is a video offering some line-by-line commentary on the structure of this solution:

 

Video: Alternate Solution to Lesson 3, Practice Exercise A. (2:47)

Click here for transcript of the Alternate Solution to Lesson 3, Practice Exercise A.

This video shows an alternate solution to Lesson 3, Practice Exercise A, which uses the SelectLayerByLocation tool’s older syntax.

This solution starts out much like the first one in terms of the variables defined at the beginning.

Where it differs is that instead of specifying feature classes as inputs to SelectLayerByLocation, it specifies FeatureLayer objects. These objects are created on lines 15 and 18 using the MakeFeatureLayer tool.

We make a feature layer that has all the park and rides, and a feature layer that has all of the city boundaries. And for both of these, the MakeFeatureLayer tool has two parameters that we supply, first the name of the feature class that's going to act as the source of the features and then the second parameter is a name that we will use to refer to this FeatureLayer throughout the rest of the script. That’s one aspect of this that takes some getting used to: that the object you’re creating gets referred to using a string. In this case, ParkAndRideLayer and CitiesLayer.

Another thing to remember is that you’re not creating a new dataset on disk with this tool. A FeatureLayer is an object that exists only temporarily in memory while the script runs.

So we've got these two feature layers that we can now perform selections on. And in line 25, we're going to use SelectLayerByLocation to select all cities that contain features from the ParkAndRideLayer.

It’s important to note that when using the tool with this approach, the CitiesLayer comes into line 25 referring to all 108 city features and after having the selection applied to it in line 25, it refers to just the 74 features that contain a ParkAndRide.

I then go on to open an update cursor on that FeatureLayer so that I can iterate over those 74 features.

The rest of the script is mostly the same, though there are a couple of differences:
1. I’m not storing the Result object returned by SelectLayerByLocation in this version, so I’m not able to retrieve the count of selected features from that object as we saw in the first solution. I really do need to implement my counter variable in this version. And

2. This version creates 2 FeatureLayers, so when doing the clean-up, I want to delete both of these layers.

Credit: S. Quinn and J. Detwiler © Penn State is licensed under CC BY-NC-SA 4.0.