GeoPandas objects are deliberately designed to resemble Pandas objects. There are two good reasons for this:
The concept of inheritance is something we've held off from mentioning until now, but it's definitely worth understanding if you are serious about learning how to code. In effect, geopandas 'imports' pandas and then extends it so that the more basic class (pandas in this case) learns how to work with geodata... pandas doesn't know how to read shapefiles or make maps, but geopandas does. Same for GeoJSON.
Here's a simple way to think about inheritance: think of the 'evolutionary trees' you might have seen charting the evolution of organisms over time. At the bottom of the tree is the single-celled animal, and at the other end are humans, whales, wildebeest, etc. We all inherit some basic functionality from that original, simple cell. In between us and that primitive, however, are a whole series of branches: different bits of the tree evolved in different directions and developed different 'functionality'. Some of us have bones. Some have cartilege. Some are vegetarian, and some are carnivorous. And so on. When you get to the primates we all share certain common 'features' (binocular vision, grasping hands, etc.), but we are still more similar to gorillas than we are to macaques. So gorillas and humans extend the primitive 'primate functionality' with some bonus features (bigger brains, greater strength, etc.) that are useful, while macaques extend it with a slightly different set of features (tails, etc.).
Inheritance in code works in a similar way: all Python classes (lists, pandas, plots, etc.) inherit their most basic functionality from a single primitive 'object' class that itself does very little except to provide a template for what an object should look like. As you move along the inheritance tree you will find more and more complex objects with increasingly advanced features: GeoPandas inherits from Pandas, Bokeh and Seaborn inherit from matplotlib, etc.
I can't find an image of Python base class inheritance, but I've found an equally useful example of how anything can be modelled using this 'family tree' approach... consider the following:
If we were trying to implement a vehicle registration scheme in Python, we would want to start with the most basic category of all: vehicle. The vehicle class itself might not do much, but it gives us a template for all vehicles (e.g. it must be registered, it must have a unique license number, etc.). We then extend the functionality of this 'base class' with three intermediate classes: two-wheeled vehicles, cars, and trucks. These, in turn, lead to eight actual vehicle types. These might have additional functionality: a bus might need have a passenger capacity associated with it, while a convertible might need to be hard- or soft-top. All of this could be expressed in Python as:
class vehicle(object): # Inherit from base class
def __init__(self):
... do something ...
class car(vehicle): # Inherit from vehicle
def __init__(self):
... do other stuff ...
class sedan(car): # Inherit from car
def __init__(self):
... do more stuff ...
This way, when we create a new sedan
, it automatically 'knows' about vehicles and cars, and can make use of functions like set_unique_id(<identification>)
even if that function is only specified in the base vehicle class! The thing to remember is that programmers are lazy: if they can avoid reinventing the wheel, they will. Object-Oriented Programming using inheritance is a good example of constructive laziness: it saves us having to constantly copy and paste code (for registering a new vehicle or reading in a CSV file) from one class to the next since we can just import it and extend it!
This also means that we are less likely to make mistakes: if we want to update our vehicle registration scheme then we don't need to update lots of functions all over the place, we just update the base class and all inheriting classes automatically gain the update because they are making use of the base class' function.
So if pandas is updated with a new 'load a zip file' feature then geopandas automatically benefits from it! The only thing that doesn't gain that benefit immediately is our ability to make use of specifically geographical data because pandas doesn't know about that type of data, only 'normal' tabular data.
Inheritance also means that you can always use an instance of a 'more evolved' class in place of one of its ancestors: simplifying things a bit, a sedan can automatically do anything that a car can do and, by extension, anything that a vehicle can do.
Similarly, since geopandas inherits from pandas if you need to use a geopandas object as if it's a pandas object then that will work! So everything you learned last term for pandas can still be used in geopandas. Kind of cool, right?
Finally, looking back at our example above: what about unicycles? Or tracked vehicles like a tank? This is where design comes into the picture: when we're planning out a family tree for our work we need to be careful about what goes where. And there isn't always a single right answer: perhaps we should distinguish between pedal-powered and motor-powered (in which case unicycles, bicycles and tricycles all belong in the same family)? Or perhaps we need to distinguish between wheeled and tracked (in which case we're missing a pair of classes [wheeled, tracked] between 'vehicle' and 'two-wheel, car, truck')? These choices are tremendously important but often very hard to get right.
OK, that's enough programming theory, let's see this in action...
In [1]:
from math import pi
class shape(object): # Inherit from base class
def __init__(self):
return
def volume(self):
raise Exception("Unimplmented method error.")
def diameter(self):
raise Exception("Unimplmented method error.")
def type(self):
return(self.shape_type)
class cube(shape): # Inherit from shape
def __init__(self, e):
self.shape_type = 'Cube'
self.edge = e
return
def volume(self):
return self.edge**3
def diameter(self):
return (self.edge**2 + self.edge**2)**(1/2)
class sphere(shape): # Inherit from shape
def __init__(self, r):
self.shape_type = 'Sphere'
self.radius = r
return
def volume(self):
return (4/3) * pi * self.radius**3
def diameter(self):
return self.radius*2
class pyramid(shape): # Inherit from shape
has_mummies = True # This is for *all* regular pyramids
def __init__(self, e):
self.shape_type = 'Regular Pyramid'
self.edge = e
return
class t_pyramid(pyramid): # Inherit from pyramid (this is a triangular pyramid)
has_mummies = False # This is for all triangular pyramids
def __init__(self, e):
self.shape_type = 'Triangular Pyramid'
self.edge = e
return
def area(self):
return (3**(1/2)/4) * self.edge**2
def height(self):
# https://www.youtube.com/watch?v=ivF3ndmkMsE
return (6**(1/2) * self.edge/3)
def volume(self):
# V = 1/3 * A * h
return (1/3) * self.area() * self.height()
s = sphere(10)
print(s.type())
print("\tVolume is: {0:5.2f}".format(s.volume()))
print("\tDiameter is: {0:5.2f}".format(s.diameter()))
print("")
c = cube(10)
print(c.type())
print("\tVolume is: {0:5.2f}".format(c.volume()))
print("\tDiameter is: {0:5.2f}".format(c.diameter()))
print("")
p = t_pyramid(10)
print(p.type())
print("\tVolume is: {0:5.2f}".format(p.volume()))
if p.has_mummies is True:
print("\tMummies? Aaaaaaaaagh!")
else:
print("\tPhew, no mummies!")
# The error here is deliberate -- note that diameter
# is not implemented in either triangular pyramid, pyramid,
# or shape, only for spheres.
print("\tDiameter is: {0:5.2f}".format(p.diameter()))
print("")
Have a really good think about how this kind of behaviour is useful!
Based on the above examples of classes and methods, I have two challenges for you to implement in the code below:
area
method that returns the surface area of each shape and then add that information to the output below.
In [ ]:
from math import pi
class shape(object): # Inherit from base class
def __init__(self):
return
def volume(self):
raise Exception("Unimplmented method error.")
def diameter(self):
raise Exception("Unimplmented method error.")
def type(self):
return(self.shape_type)
class cube(shape): # Inherit from shape
def __init__(self, e):
self.shape_type = 'Cube'
self.edge = e
return
def volume(self):
return self.edge**3
def diameter(self):
return (self.edge**2 + self.edge**2)**(1/2)
class sphere(shape): # Inherit from shape
def __init__(self, r):
self.shape_type = 'Sphere'
self.radius = r
return
def volume(self):
return (4/3) * pi * self.radius**3
def diameter(self):
return self.radius*2
class pyramid(shape): # Inherit from shape
has_mummies = True # This is for *all* regular pyramids
def __init__(self, e):
self.shape_type = 'Regular Pyramid'
self.edge = e
return
class t_pyramid(pyramid): # Inherit from pyramid (this is a triangular pyramid)
has_mummies = False # This is for all triangular pyramids
def __init__(self, e):
self.shape_type = 'Triangular Pyramid'
self.edge = e
return
def area(self):
return (3**(1/2)/4) * self.edge**2
def height(self):
# https://www.youtube.com/watch?v=ivF3ndmkMsE
return (6**(1/2) * self.edge/3)
def volume(self):
# V = 1/3 * A * h
return (1/3) * self.area() * self.height()
# How would you test these changes?
The following individuals have contributed to these teaching materials:
The content and structure of this teaching project itself is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license, and the contributing source code is licensed under The MIT License.
Supported by the Royal Geographical Society (with the Institute of British Geographers) with a Ray Y Gildea Jr Award.
This notebook may depend on the following libraries: None