{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Introduction to GeoPandas\n", "\n", "This quick tutorial introduces the key concepts and basic features of GeoPandas to help you get started with your projects.\n", "\n", "## Concepts\n", "\n", "GeoPandas, as the name suggests, extends the popular data science library [pandas](https://pandas.pydata.org) by adding support for geospatial data. If you are not familiar with `pandas`, we recommend taking a quick look at its [Getting started documentation](https://pandas.pydata.org/docs/getting_started/index.html#getting-started) before proceeding.\n", "\n", "The core data structure in GeoPandas is the `geopandas.GeoDataFrame`, a subclass of `pandas.DataFrame`, that can store geometry columns and perform spatial operations. The `geopandas.GeoSeries`, a subclass of `pandas.Series`, handles the geometries. Therefore, your `GeoDataFrame` is a combination of `pandas.Series`, with traditional data (numerical, boolean, text etc.), and `geopandas.GeoSeries`, with geometries (points, polygons etc.). You can have as many columns with geometries as you wish; there's no limit typical for desktop GIS software.\n", "\n", "![geodataframe schema](../_static/dataframe.svg)\n", "\n", "Each `GeoSeries` can contain any geometry type (you can even mix them within a single array) and has a `GeoSeries.crs` attribute, which stores information about the projection (CRS stands for Coordinate Reference System). Therefore, each `GeoSeries` in a `GeoDataFrame` can be in a different projection, allowing you to have, for example, multiple versions (different projections) of the same geometry.\n", "\n", "Only one `GeoSeries` in a `GeoDataFrame` is considered the _active_ geometry, which means that all geometric operations applied to a `GeoDataFrame` operate on this _active_ column.\n", "\n", "\n", "
\n", "User guide\n", " \n", "See more on [data structures](../docs/user_guide/data_structures.rst) in the user guide.\n", "
\n", "\n", "\n", "Let's see how some of these concepts work in practice.\n", "\n", "## Reading and writing files\n", "\n", "First, we need to read some data.\n", "\n", "### Reading files\n", "\n", "Assuming you have a file containing both data and geometry (e.g. GeoPackage, GeoJSON, Shapefile), you can read it using `geopandas.read_file()`, which automatically detects the filetype and creates a `GeoDataFrame`. This tutorial uses the `\"nybb\"` dataset, a map of New York boroughs, which is available through the `geodatasets` package. Therefore, we use `geodatasets.get_path()` to download the dataset and retrieve the path to the local copy." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import geopandas\n", "from geodatasets import get_path\n", "\n", "path_to_data = get_path(\"nybb\")\n", "gdf = geopandas.read_file(path_to_data)\n", "\n", "gdf" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Writing files\n", "\n", "To write a `GeoDataFrame` back to file use `GeoDataFrame.to_file()`. The default file format is Shapefile, but you can specify your own with the `driver` keyword." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf.to_file(\"my_file.geojson\", driver=\"GeoJSON\")" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "
\n", "User guide\n", " \n", "See more on [reading and writing files](../docs/user_guide/io.rst) in the user guide.\n", "
\n", "\n", "\n", "\n", "## Simple accessors and methods\n", "\n", "Now we have our `GeoDataFrame` and can start working with its geometry. \n", "\n", "Since there was only one geometry column in the New York Boroughs dataset, this column automatically becomes the _active_ geometry and spatial methods used on the `GeoDataFrame` will be applied to the `\"geometry\"` column.\n", "\n", "### Measuring area\n", "\n", "To measure the area of each polygon (or MultiPolygon in this specific case), access the `GeoDataFrame.area` attribute, which returns a `pandas.Series`. Note that `GeoDataFrame.area` is just `GeoSeries.area` applied to the _active_ geometry column.\n", "\n", "But first, to make the results easier to read, set the names of the boroughs as the index:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf = gdf.set_index(\"BoroName\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"area\"] = gdf.area\n", "gdf[\"area\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Getting polygon boundary and centroid\n", "\n", "To get the boundary of each polygon (LineString), access the `GeoDataFrame.boundary`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"boundary\"] = gdf.boundary\n", "gdf[\"boundary\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we have saved boundary as a new column, we now have two geometry columns in the same `GeoDataFrame`.\n", "\n", "We can also create new geometries, which could be, for example, a buffered version of the original one (i.e., `GeoDataFrame.buffer(10)`) or its centroid:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"centroid\"] = gdf.centroid\n", "gdf[\"centroid\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Measuring distance\n", "\n", "We can also measure how far each centroid is from the first centroid location." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "first_point = gdf[\"centroid\"].iloc[0]\n", "gdf[\"distance\"] = gdf[\"centroid\"].distance(first_point)\n", "gdf[\"distance\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that `geopandas.GeoDataFrame` is a subclass of `pandas.DataFrame`, so we have all the pandas functionality available to use on the geospatial dataset — we can even perform data manipulations with the attributes and geometry information together.\n", "\n", "For example, to calculate the average of the distances measured above, access the 'distance' column and call the mean() method on it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"distance\"].mean()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Making maps\n", "\n", "GeoPandas can also plot maps, so we can check how the geometries appear in space. To plot the active geometry, call `GeoDataFrame.plot()`. To color code by another column, pass in that column as the first argument. In the example below, we plot the active geometry column and color code by the `\"area\"` column. We also want to show a legend (`legend=True`)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf.plot(\"area\", legend=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also explore your data interactively using `GeoDataFrame.explore()`, which behaves in the same way `plot()` does but returns an interactive map instead." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf.explore(\"area\", legend=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Switching the active geometry (`GeoDataFrame.set_geometry`) to centroids, we can plot the same data using point geometry." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf = gdf.set_geometry(\"centroid\")\n", "gdf.plot(\"area\", legend=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we can also layer both `GeoSeries` on top of each other. We just need to use one plot as an axis for the other." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ax = gdf[\"geometry\"].plot()\n", "gdf[\"centroid\"].plot(ax=ax, color=\"black\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we set the active geometry back to the original `GeoSeries`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf = gdf.set_geometry(\"geometry\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "User guide\n", " \n", "See more on [mapping](../docs/user_guide/mapping.rst) in the user guide.\n", "
\n", "\n", "## Geometry creation\n", "\n", "We can further work with the geometry and create new shapes based on those we already have. \n", "\n", "### Convex hull\n", "\n", "If we are interested in the convex hull of our polygons, we can access `GeoDataFrame.convex_hull`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"convex_hull\"] = gdf.convex_hull" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# saving the first plot as an axis and setting alpha (transparency) to 0.5\n", "ax = gdf[\"convex_hull\"].plot(alpha=0.5)\n", "# passing the first plot and setting linewidth to 0.5\n", "gdf[\"boundary\"].plot(ax=ax, color=\"white\", linewidth=0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Buffer\n", "\n", "In other cases, we may need to buffer the geometry using `GeoDataFrame.buffer()`. Geometry methods are automatically applied to the active geometry, but we can apply them directly to any `GeoSeries` as well. Let's buffer the boroughs and their centroids and plot both on top of each other." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# buffering the active geometry by 10 000 feet (geometry is already in feet)\n", "gdf[\"buffered\"] = gdf.buffer(10000)\n", "\n", "# buffering the centroid geometry by 10 000 feet (geometry is already in feet)\n", "gdf[\"buffered_centroid\"] = gdf[\"centroid\"].buffer(10000)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# saving the first plot as an axis and setting alpha (transparency) to 0.5\n", "ax = gdf[\"buffered\"].plot(alpha=0.5)\n", "# passing the first plot as an axis to the second\n", "gdf[\"buffered_centroid\"].plot(ax=ax, color=\"red\", alpha=0.5)\n", "# passing the first plot and setting linewidth to 0.5\n", "gdf[\"boundary\"].plot(ax=ax, color=\"white\", linewidth=0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "User guide\n", " \n", "See more on [geometry manipulations](../docs/user_guide/geometric_manipulations.rst) in the user guide.\n", "
\n", "\n", "## Geometry relations\n", "\n", "We can also ask about the spatial relations of different geometries. Using the geometries above, we can check which of the buffered boroughs intersect the original geometry of Brooklyn, i.e., is within 10 000 feet from Brooklyn.\n", "\n", "First, we get a polygon of Brooklyn." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "brooklyn = gdf.loc[\"Brooklyn\", \"geometry\"]\n", "brooklyn" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The polygon is a [shapely geometry object](https://shapely.readthedocs.io/en/stable/manual.html#geometric-objects), as any other geometry used in GeoPandas." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "type(brooklyn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then we can check which of the geometries in `gdf[\"buffered\"]` intersects it." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"buffered\"].intersects(brooklyn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Only Bronx (on the north) is more than 10 000 feet away from Brooklyn. All the others are closer and intersect our polygon.\n", "\n", "Alternatively, we can check which buffered centroids are entirely within the original boroughs polygons. In this case, both `GeoSeries` are aligned, and the check is performed for each row." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf[\"within\"] = gdf[\"buffered_centroid\"].within(gdf)\n", "gdf[\"within\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can plot the results on the map to confirm the finding." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "gdf = gdf.set_geometry(\"buffered_centroid\")\n", "# using categorical plot and setting the position of the legend\n", "ax = gdf.plot(\n", " \"within\", legend=True, categorical=True, legend_kwds={\"loc\": \"upper left\"}\n", ")\n", "# passing the first plot and setting linewidth to 0.5\n", "gdf[\"boundary\"].plot(ax=ax, color=\"black\", linewidth=0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Projections\n", "\n", "Each `GeoSeries` has its Coordinate Reference System (CRS) accessible at `GeoSeries.crs`. The CRS tells GeoPandas where the coordinates of the geometries are located on the earth's surface. In some cases, the CRS is geographic, which means that the coordinates are in latitude and longitude. In those cases, its CRS is WGS84, with the authority code `EPSG:4326`. Let's see the projection of our NY boroughs `GeoDataFrame`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf.crs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Geometries are in `EPSG:2263` with coordinates in feet. We can easily re-project a `GeoSeries` to another CRS, like `EPSG:4326` using `GeoSeries.to_crs()`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdf = gdf.set_geometry(\"geometry\")\n", "boroughs_4326 = gdf.to_crs(\"EPSG:4326\")\n", "boroughs_4326.plot()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "boroughs_4326.crs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice the difference in coordinates along the axes of the plot. Where we had 120 000 - 280 000 (feet) before, we now have 40.5 - 40.9 (degrees). In this case, `boroughs_4326` has a `\"geometry\"` column in WGS84 but all the other (with centroids etc.) remain in the original CRS.\n", "\n", "
\n", "Warning\n", " \n", "For operations that rely on distance or area, you always need to use a projected CRS (in meters, feet, kilometers etc.) not a geographic one (in degrees). GeoPandas operations are planar, whereas degrees reflect the position on a sphere. Therefore, spatial operations using degrees may not yield correct results. For example, the result of `gdf.area.sum()` (projected CRS) is 8 429 911 572 ft2 but the result of `boroughs_4326.area.sum()` (geographic CRS) is 0.083.\n", "
\n", "\n", "
\n", "User guide\n", " \n", "See more on [projections](../docs/user_guide/projections.rst) in the user guide.\n", "
\n", "\n", "## What next?\n", "\n", "With GeoPandas we can do much more than what has been introduced so far, from [aggregations](../docs/user_guide/aggregation_with_dissolve.rst), to [spatial joins](../docs/user_guide/mergingdata.rst), to [geocoding](../docs/user_guide/geocoding.rst), and [much more](../gallery/index.rst).\n", "\n", "Head over to the [user guide](../docs/user_guide.rst) to learn more about the different features of GeoPandas, the [Examples](../gallery/index.rst) to see how they can be used, or to the [API reference](../docs/reference.rst) for the details." ] } ], "metadata": { "kernelspec": { "display_name": "geopandas_dev", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.0" }, "vscode": { "interpreter": { "hash": "b1fe2ae8565152c84d3dbd08488d3746f754c9bdf2de9b61cf939da5306d3793" } } }, "nbformat": 4, "nbformat_minor": 4 }