Geospatial adventures. Step 1: Shapely.
By: Dmitry Selemir
Tags: Geospatial, Shapely, Data Science, Python, Geometry

Image generated using KeplerGL

Geospatial adventures. Step 1: Shapely.

A quick look at the basics of working with geometrical objects in Python using Shapely library.

This is a first in a series of posts summarising some of the key outtakes from working with geospatial data with a PropTech twist over the last couple of years. These are going to have a bit of everything — geospatial datasets, geometric shapes, raster files, maps, visualisations. Starting from the very basics and building up towards more interesting and challenging things in the later posts...

Let’s kick things off with introducing Shapely. Without a doubt one of my favourite libraries in Python — very central and absolutely essential to any geometry/geography related work you will end up doing.

The library allows you to work with three main types of geometric objects: Point, LineString and Polygons+ geometry collections if you want to combine them. There’s a bunch of others — linear rings, multi-points, multi-polygons, etc., but for now these will do, the methodologies are very much transferable.

Installation

Pretty standard installation, using pip. If, like me, you are using jupyter — you can just run

!pip install shapely

Note that GeoPandas is using Shapely under the hood, so if you have installed GeoPandas— you probably have a recent version of Shapely already. You can perform some of the Shapely operations after importing Geopandas without a separate import, however, if you want to work directly with Point and Polygon objects — you would still need to load them in first. Enough about Geopandas however, we are going to look at it in more detail in the next post.

Once installed, import it into your notebook and load the main geometry types:

import shapely

from shapely.geometry import Point, Polygon, LineString, GeometryCollection

import numpy as np

I am also importing numpy because I like it so much. Seriously though, I very often find myself jumping back and forth between shapely objects and equivalent numpy arrays of coordinates as numpy allows you to do some of the operations in explicit vectorised form a lot quicker, so it’s a good idea to look at the connection between the two right from the start.

Point objects

As the name suggests, this is just a point on a two-dimensional plane, characterised by a pair of coordinates.

One of the super convenient features of Shapely is — it allows you to view all the geometric objects without having to resort to any graphical package. Note that regardless of the coordinate system positioning of the object, it always centres on the object for you when you want to view it.

pt = Point(10, 10)

pt1 = Point(100, 101)

You can also display the string representation of the object, by just wrapping str() around it, or convert it to a numpy array of its coordinates. As I mentioned, I find the latter particularly useful as I often find myself working with large arrays of geometric objects (for example over 6mln building polygons from OSM) and if I want to do calculations in vectorised format, numpy is absolutely irreplaceable.

There is also a method to load this string representation back into geometric format, which will come really handy when you have to load the data stored in a non-geometric format, for example from a csv file.

If you want to quickly look at several objects and see how they scale vs. each other all you have to do is turn them into a geometry collection:

A few other handy methods — distance, coordinates collection.

In[8]:

pt.distance(pt1)

Out[8]:

127.98828071350908

In[9]:

pt.x, pt.y, pt.xy

Out[9]:

(10.0, 10.0, (array('d', [10.0]), array('d', [10.0])))

One final thing before moving on to lines. All shapely objects have a .name attribute. This can be useful, for example, when you are transforming each of your polygons from a large collection stored in a GeoPandas or Pandas DataFrame into an array of smaller polygons, like a grid and want to have an easy way of relating them back to the original polygons.

In[10]:

pt.name = 'My Point'

pt.name

Out[10]:

'My Point'

LineStrings

LineStrings are initiated in a very similar way, only this time we have a list of tuples, rather than a single one. They can cross themselves and pass through the same points multiple times, however, the latter is not recommended as it adversely impacts performance and you are better off splitting them into individual components. Note that the order of points is important as it determines the order in which you pass through them (same applies to polygons as you’ll see below).

ln = LineString([(0, 1), (20, 100), (100, 3), (120, 102), (200, 5)])

As with points, you can convert LineString object to an array of point coordinates. Order is preserved here, so this can be used to quickly get the coordinates of the first and last point — handy for constructing tree objects representing road networks for example.

In[13]:

np.array(ln)

Out[13]:

array([[ 0., 1.],

[ 20., 100.],

[100., 3.],

[120., 102.],

[200., 5.]])

Or if you want the same list of tuples representation used to create your LineString in the first place:

In[14]:

list(ln.coords)

Out[14]:

[(0.0, 1.0), (20.0, 100.0), (100.0, 3.0), (120.0, 102.0), (200.0, 5.0)]

You can also split out only X coordinates or only Y coordinates (of course, you can do that using numpy as well):

In[15]:

list(ln.xy[0]), list(ln.xy[-1])

Out[15]:

([0.0, 20.0, 100.0, 120.0, 200.0], [1.0, 100.0, 3.0, 102.0, 5.0])

A quick look at the graphical representation together with the points we created earlier:

Calculating a point to line distance, point projection on the line (distance from start along the line) and the length of the line is as trivial as:

In[17]:

pt.distance(ln)

Out[17]:

8.01980198019802

In[18]:

ln.project(pt), ln.length

Out[18]:

(10.801980198019802, 453.46769176178475)

In[19]:

list(ln.interpolate(ln.project(Point(1, 1))).coords)

Out[20]:

[(0.039211841976276834, 1.1940986177825703)]

Note that if the projection of the point onto the line happens to be outside of the defined area — the distance will be calculated to the nearest end of line. If you are after the actual projection — you would need to do a bit of extra geometry. The easiest thing would be to extend the first and last segments of the line and still use the same projection method.

Line intersections are pretty straightforward too, even when you end up with multiple intersections.

The result is a MultiPoint object, which you can iterate through, just as if this was a regular list, with Point objects as iterables:

In[21]:

str(ln.intersection(LineString([(0, 0), (200, 100)])))

Out[22]:

'MULTIPOINT (72.55474452554745 36.27737226277372, 110.561797752809 55.28089887640449, 144.5255474452555 72.26277372262774)'

In[22]:

[np.array(a) for a in ln.intersection(

LineString([

(0, 0),

(200, 100)

])

)]

Out[22]:

[array([72.55474453, 36.27737226]),

array([110.56179775, 55.28089888]),

array([144.52554745, 72.26277372])]

Polygons

Not surprisingly, creating one is very similar to creating a LineString and, as in LineStrings , the order in which points are listed matters.

Polygons can have holes inside and the way these are defined follows a simple rule: Polygon([list of polygon coordinates],[list of holes]), where each hole is itself a polygon. Note that polygons representing holes have to be either fully inside your original polygon or can touch it in no more than one place.

Note that polygons can not be directly converted to a collection of points like we did with LineStrings and Points. Instead, we have to deal with their exterior and interior outlines (which are LinerRings themselves, i.e. a lineString, which loops on itself). For interior boundaries we get back an iterator:

In[25]:

np.array(poly.exterior)

Out[25]:

array([[0., 0.],

[0., 1.],

[1., 1.],

[1., 0.],

[0., 0.]])

In[26]:

[np.array(a) for a in poly.interiors]

Out[26]: