├── .gitignore ├── 01-introduction-geospatial-data.ipynb ├── 02-spatial-relationships-operations.ipynb ├── 03-spatial-joins.ipynb ├── 04-more-on-visualization.ipynb ├── 05-mapclassification.ipynb ├── 06-exploratory-spatial-data-analysis.ipynb ├── 07-data-borrowing.ipynb ├── 08-clustering.ipynb ├── LICENSE ├── README.md ├── _solved ├── case-conflict-mapping.ipynb ├── case-gini-in-a-bottle-the-trump-vote.ipynb └── solutions │ ├── case-conflict-mapping10.py │ ├── case-conflict-mapping11.py │ ├── case-conflict-mapping12.py │ ├── case-conflict-mapping13.py │ ├── case-conflict-mapping14.py │ ├── case-conflict-mapping15.py │ ├── case-conflict-mapping16.py │ ├── case-conflict-mapping19.py │ ├── case-conflict-mapping20.py │ ├── case-conflict-mapping21.py │ ├── case-conflict-mapping22.py │ ├── case-conflict-mapping23.py │ ├── case-conflict-mapping24.py │ ├── case-conflict-mapping25.py │ ├── case-conflict-mapping26.py │ ├── case-conflict-mapping27.py │ ├── case-conflict-mapping28.py │ ├── case-conflict-mapping29.py │ ├── case-conflict-mapping3.py │ ├── case-conflict-mapping30.py │ ├── case-conflict-mapping31.py │ ├── case-conflict-mapping32.py │ ├── case-conflict-mapping33.py │ ├── case-conflict-mapping34.py │ ├── case-conflict-mapping35.py │ ├── case-conflict-mapping36.py │ ├── case-conflict-mapping37.py │ ├── case-conflict-mapping38.py │ ├── case-conflict-mapping39.py │ ├── case-conflict-mapping4.py │ ├── case-conflict-mapping40.py │ ├── case-conflict-mapping41.py │ ├── case-conflict-mapping5.py │ ├── case-trump-vote01.py │ ├── case-trump-vote02.py │ ├── case-trump-vote03.py │ ├── case-trump-vote04.py │ ├── case-trump-vote05.py │ ├── case-trump-vote06.py │ ├── case-trump-vote07.py │ ├── case-trump-vote08.py │ ├── case-trump-vote09.py │ ├── case-trump-vote10.py │ ├── case-trump-vote11.py │ ├── case-trump-vote12.py │ ├── case-trump-vote13.py │ ├── case-trump-vote14.py │ └── case-trump-vote15.py ├── case-conflict-mapping.ipynb ├── case-gini-in-a-bottle-the-trump-vote.ipynb ├── check_environment.py ├── data ├── berlin-districts.geojson ├── berlin-listings.csv.gz ├── cod_conservation.zip ├── cod_mines_curated_all_opendata_p_ipis.geojson ├── ne_110m_admin_0_countries.zip ├── ne_110m_populated_places.zip ├── ne_50m_rivers_lake_centerlines.zip └── uspres.zip ├── environment.yml └── img ├── TopologicSpatialRelations2.png └── download-button.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /01-introduction-geospatial-data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to geospatial vector data in Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%matplotlib inline\n", 17 | "\n", 18 | "import pandas as pd\n", 19 | "import geopandas\n", 20 | "\n", 21 | "pd.options.display.max_rows = 10" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## Importing geospatial data" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "Geospatial data is often available from specific GIS file formats or data stores, like ESRI shapefiles, GeoJSON files, geopackage files, PostGIS (PostgreSQL) database, ...\n", 36 | "\n", 37 | "We can use the GeoPandas library to read many of those GIS file formats (relying on the `fiona` library under the hood, which is an interface to GDAL/OGR), using the `geopandas.read_file` function.\n", 38 | "\n", 39 | "For example, let's start by reading a shapefile with all the countries of the world (adapted from http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/, zip file is available in the `/data` directory), and inspect the data:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "countries = geopandas.read_file(\"zip://./data/ne_110m_admin_0_countries.zip\")\n", 49 | "# or if the archive is unpacked:\n", 50 | "# countries = geopandas.read_file(\"data/ne_110m_admin_0_countries/ne_110m_admin_0_countries.shp\")" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "countries.head()" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "countries.plot()" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "What can we observe:\n", 76 | "\n", 77 | "- Using `.head()` we can see the first rows of the dataset, just like we can do with Pandas.\n", 78 | "- There is a 'geometry' column and the different countries are represented as polygons\n", 79 | "- We can use the `.plot()` method to quickly get a *basic* visualization of the data" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "## What's a GeoDataFrame?\n", 87 | "\n", 88 | "We used the GeoPandas library to read in the geospatial data, and this returned us a `GeoDataFrame`:" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "type(countries)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "A GeoDataFrame contains a tabular, geospatial dataset:\n", 105 | "\n", 106 | "* It has a **'geometry' column** that holds the geometry information (or features in GeoJSON).\n", 107 | "* The other columns are the **attributes** (or properties in GeoJSON) that describe each of the geometries\n", 108 | "\n", 109 | "Such a `GeoDataFrame` is just like a pandas `DataFrame`, but with some additional functionality for working with geospatial data:\n", 110 | "\n", 111 | "* A `.geometry` attribute that always returns the column with the geometry information (returning a GeoSeries). The column name itself does not necessarily need to be 'geometry', but it will always be accessible as the `.geometry` attribute.\n", 112 | "* It has some extra methods for working with spatial data (area, distance, buffer, intersection, ...), which we will see in later notebooks" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "countries.geometry" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "type(countries.geometry)" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "countries.geometry.area" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "**It's still a DataFrame**, so we have all the pandas functionality available to use on the geospatial dataset, and to do data manipulations with the attributes and geometry information together.\n", 147 | "\n", 148 | "For example, we can calculate average population number over all countries (by accessing the 'pop_est' column, and calling the `mean` method on it):" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "countries['pop_est'].mean()" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "Or, we can use boolean filtering to select a subset of the dataframe based on a condition:" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "africa = countries[countries['continent'] == 'Africa']" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "africa.plot()" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "---\n", 190 | "\n", 191 | "The rest of the tutorial is going to assume you already know some pandas basics, but we will try to give hints for that part for those that are not familiar. \n", 192 | "A few resources in case you want to learn more about pandas:\n", 193 | "\n", 194 | "- Pandas docs: https://pandas.pydata.org/pandas-docs/stable/10min.html\n", 195 | "- Other tutorials: chapter from pandas in https://jakevdp.github.io/PythonDataScienceHandbook/, https://github.com/jorisvandenbossche/pandas-tutorial, https://github.com/TomAugspurger/pandas-head-to-tail, ..." 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "
\n", 203 | "REMEMBER:
\n", 204 | "\n", 205 | "\n", 214 | "
" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "## Geometries: Points, Linestrings and Polygons\n", 222 | "\n", 223 | "Spatial **vector** data can consist of different types, and the 3 fundamental types are:\n", 224 | "\n", 225 | "* **Point** data: represents a single point in space.\n", 226 | "* **Line** data (\"LineString\"): represents a sequence of points that form a line.\n", 227 | "* **Polygon** data: represents a filled area.\n", 228 | "\n", 229 | "And each of them can also be combined in multi-part geometries (See https://shapely.readthedocs.io/en/stable/manual.html#geometric-objects for extensive overview)." 230 | ] 231 | }, 232 | { 233 | "cell_type": "markdown", 234 | "metadata": {}, 235 | "source": [ 236 | "For the example we have seen up to now, the individual geometry objects are Polygons:" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "print(countries.geometry[2])" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "Let's import some other datasets with different types of geometry objects.\n", 253 | "\n", 254 | "A dateset about cities in the world (adapted from http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/, zip file is available in the `/data` directory), consisting of Point data:" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "cities = geopandas.read_file(\"zip://./data/ne_110m_populated_places.zip\")" 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": null, 269 | "metadata": {}, 270 | "outputs": [], 271 | "source": [ 272 | "print(cities.geometry[0])" 273 | ] 274 | }, 275 | { 276 | "cell_type": "markdown", 277 | "metadata": {}, 278 | "source": [ 279 | "And a dataset of rivers in the world (from http://www.naturalearthdata.com/downloads/50m-physical-vectors/50m-rivers-lake-centerlines/, zip file is available in the `/data` directory) where each river is a (multi-)line:" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "rivers = geopandas.read_file(\"zip://./data/ne_50m_rivers_lake_centerlines.zip\")" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "metadata": {}, 295 | "outputs": [], 296 | "source": [ 297 | "print(rivers.geometry[0])" 298 | ] 299 | }, 300 | { 301 | "cell_type": "markdown", 302 | "metadata": {}, 303 | "source": [ 304 | "### The `shapely` library\n", 305 | "\n", 306 | "The individual geometry objects are provided by the [`shapely`](https://shapely.readthedocs.io/en/stable/) library" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "type(countries.geometry[0])" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": {}, 321 | "source": [ 322 | "To construct one ourselves:" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": null, 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [ 331 | "from shapely.geometry import Point, Polygon, LineString" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "p = Point(1, 1)" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": null, 346 | "metadata": {}, 347 | "outputs": [], 348 | "source": [ 349 | "print(p)" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "metadata": {}, 356 | "outputs": [], 357 | "source": [ 358 | "polygon = Polygon([(1, 1), (2,2), (2, 1)])" 359 | ] 360 | }, 361 | { 362 | "cell_type": "markdown", 363 | "metadata": {}, 364 | "source": [ 365 | "
\n", 366 | "REMEMBER:

\n", 367 | "\n", 368 | "Single geometries are represented by `shapely` objects:\n", 369 | "\n", 370 | "\n", 379 | "
" 380 | ] 381 | }, 382 | { 383 | "cell_type": "markdown", 384 | "metadata": {}, 385 | "source": [ 386 | "## Coordinate reference systems\n", 387 | "\n", 388 | "A **coordinate reference system (CRS)** determines how the two-dimensional (planar) coordinates of the geometry objects should be related to actual places on the (non-planar) earth.\n", 389 | "\n", 390 | "For a nice in-depth explanation, see https://docs.qgis.org/2.8/en/docs/gentle_gis_introduction/coordinate_reference_systems.html" 391 | ] 392 | }, 393 | { 394 | "cell_type": "markdown", 395 | "metadata": {}, 396 | "source": [ 397 | "A GeoDataFrame or GeoSeries has a `.crs` attribute which holds (optionally) a description of the coordinate reference system of the geometries:" 398 | ] 399 | }, 400 | { 401 | "cell_type": "code", 402 | "execution_count": null, 403 | "metadata": {}, 404 | "outputs": [], 405 | "source": [ 406 | "countries.crs" 407 | ] 408 | }, 409 | { 410 | "cell_type": "markdown", 411 | "metadata": {}, 412 | "source": [ 413 | "For the `countries` dataframe, it indicates that it used the EPSG 4326 / WGS84 lon/lat reference system, which is one of the most used. \n", 414 | "It uses coordinates as latitude and longitude in degrees, as can you be seen from the x/y labels on the plot:" 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": null, 420 | "metadata": {}, 421 | "outputs": [], 422 | "source": [ 423 | "countries.plot()" 424 | ] 425 | }, 426 | { 427 | "cell_type": "markdown", 428 | "metadata": {}, 429 | "source": [ 430 | "The `.crs` attribute is given as a dictionary. In this case, it only indicates the EPSG code, but it can also contain the full \"proj4\" string (in dictionary form). \n", 431 | "\n", 432 | "Under the hood, GeoPandas uses the `pyproj` / `proj4` libraries to deal with the re-projections.\n", 433 | "\n", 434 | "For more information, see also http://geopandas.readthedocs.io/en/latest/projections.html." 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "metadata": {}, 440 | "source": [ 441 | "---\n", 442 | "\n", 443 | "There are sometimes good reasons you want to change the coordinate references system of your dataset, for example:\n", 444 | "\n", 445 | "- different sources with different crs -> need to convert to the same crs\n", 446 | "- distance-based operations -> if you a crs that has meter units (not degrees)\n", 447 | "- plotting in a certain crs (eg to preserve area)\n", 448 | "\n", 449 | "We can convert a GeoDataFrame to another reference system using the `to_crs` function. \n", 450 | "\n", 451 | "For example, let's convert the countries to the World Mercator projection (http://epsg.io/3395):" 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": null, 457 | "metadata": {}, 458 | "outputs": [], 459 | "source": [ 460 | "# remove Antartica, as the Mercator projection cannot deal with the poles\n", 461 | "countries = countries[(countries['name'] != \"Antarctica\")]" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "metadata": {}, 468 | "outputs": [], 469 | "source": [ 470 | "countries_mercator = countries.to_crs(epsg=3395) # or .to_crs({'init': 'epsg:3395'})" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": null, 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "countries_mercator.plot()" 480 | ] 481 | }, 482 | { 483 | "cell_type": "markdown", 484 | "metadata": {}, 485 | "source": [ 486 | "Note the different scale of x and y." 487 | ] 488 | }, 489 | { 490 | "cell_type": "markdown", 491 | "metadata": {}, 492 | "source": [ 493 | "## Plotting our different layers together" 494 | ] 495 | }, 496 | { 497 | "cell_type": "code", 498 | "execution_count": null, 499 | "metadata": {}, 500 | "outputs": [], 501 | "source": [ 502 | "ax = countries.plot(edgecolor='k', facecolor='none', figsize=(15, 10))\n", 503 | "rivers.plot(ax=ax)\n", 504 | "cities.plot(ax=ax, color='red')\n", 505 | "ax.set(xlim=(-20, 60), ylim=(-40, 40))" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "metadata": {}, 511 | "source": [ 512 | "See the [04-more-on-visualization.ipynb](04-more-on-visualization.ipynb) notebook for more details on visualizing geospatial datasets." 513 | ] 514 | }, 515 | { 516 | "cell_type": "markdown", 517 | "metadata": {}, 518 | "source": [ 519 | "## A bit more on importing and creating GeoDataFrames" 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "metadata": {}, 525 | "source": [ 526 | "### Note on `fiona`\n", 527 | "\n", 528 | "Under the hood, GeoPandas uses the [Fiona library](http://toblerity.org/fiona/) (pythonic interface to GDAL/OGR) to read and write data. GeoPandas provides a more user-friendly wrapper, which is sufficient for most use cases. But sometimes you want more control, and in that case, to read a file with fiona you can do the following:\n" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": null, 534 | "metadata": {}, 535 | "outputs": [], 536 | "source": [ 537 | "import fiona\n", 538 | "from shapely.geometry import shape\n", 539 | "\n", 540 | "with fiona.drivers():\n", 541 | " with fiona.open(\"data/ne_110m_admin_0_countries/ne_110m_admin_0_countries.shp\") as collection:\n", 542 | " for feature in collection:\n", 543 | " # ... do something with geometry\n", 544 | " geom = shape(feature['geometry'])\n", 545 | " # ... do something with properties\n", 546 | " print(feature['properties']['name'])" 547 | ] 548 | }, 549 | { 550 | "cell_type": "markdown", 551 | "metadata": {}, 552 | "source": [ 553 | "### Constructing a GeoDataFrame manually" 554 | ] 555 | }, 556 | { 557 | "cell_type": "code", 558 | "execution_count": null, 559 | "metadata": {}, 560 | "outputs": [], 561 | "source": [ 562 | "geopandas.GeoDataFrame({\n", 563 | " 'geometry': [Point(1, 1), Point(2, 2)],\n", 564 | " 'attribute1': [1, 2],\n", 565 | " 'attribute2': [0.1, 0.2]})" 566 | ] 567 | }, 568 | { 569 | "cell_type": "markdown", 570 | "metadata": {}, 571 | "source": [ 572 | "### Creating a GeoDataFrame from an existing dataframe\n", 573 | "\n", 574 | "For example, if you have lat/lon coordinates in two columns:" 575 | ] 576 | }, 577 | { 578 | "cell_type": "code", 579 | "execution_count": null, 580 | "metadata": {}, 581 | "outputs": [], 582 | "source": [ 583 | "df = pd.DataFrame(\n", 584 | " {'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],\n", 585 | " 'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],\n", 586 | " 'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],\n", 587 | " 'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]})" 588 | ] 589 | }, 590 | { 591 | "cell_type": "code", 592 | "execution_count": null, 593 | "metadata": {}, 594 | "outputs": [], 595 | "source": [ 596 | "df['Coordinates'] = list(zip(df.Longitude, df.Latitude))" 597 | ] 598 | }, 599 | { 600 | "cell_type": "code", 601 | "execution_count": null, 602 | "metadata": {}, 603 | "outputs": [], 604 | "source": [ 605 | "df['Coordinates'] = df['Coordinates'].apply(Point)" 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": null, 611 | "metadata": {}, 612 | "outputs": [], 613 | "source": [ 614 | "gdf = geopandas.GeoDataFrame(df, geometry='Coordinates')" 615 | ] 616 | }, 617 | { 618 | "cell_type": "code", 619 | "execution_count": null, 620 | "metadata": {}, 621 | "outputs": [], 622 | "source": [ 623 | "gdf" 624 | ] 625 | }, 626 | { 627 | "cell_type": "markdown", 628 | "metadata": {}, 629 | "source": [ 630 | "See http://geopandas.readthedocs.io/en/latest/gallery/create_geopandas_from_pandas.html#sphx-glr-gallery-create-geopandas-from-pandas-py for full example" 631 | ] 632 | } 633 | ], 634 | "metadata": { 635 | "kernelspec": { 636 | "display_name": "Python 3", 637 | "language": "python", 638 | "name": "python3" 639 | }, 640 | "language_info": { 641 | "codemirror_mode": { 642 | "name": "ipython", 643 | "version": 3 644 | }, 645 | "file_extension": ".py", 646 | "mimetype": "text/x-python", 647 | "name": "python", 648 | "nbconvert_exporter": "python", 649 | "pygments_lexer": "ipython3", 650 | "version": "3.5.5" 651 | } 652 | }, 653 | "nbformat": 4, 654 | "nbformat_minor": 2 655 | } 656 | -------------------------------------------------------------------------------- /02-spatial-relationships-operations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spatial relationships and operations" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%matplotlib inline\n", 17 | "\n", 18 | "import pandas as pd\n", 19 | "import geopandas\n", 20 | "\n", 21 | "pd.options.display.max_rows = 10" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "countries = geopandas.read_file(\"zip://./data/ne_110m_admin_0_countries.zip\")\n", 31 | "cities = geopandas.read_file(\"zip://./data/ne_110m_populated_places.zip\")\n", 32 | "rivers = geopandas.read_file(\"zip://./data/ne_50m_rivers_lake_centerlines.zip\")" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Spatial relationships\n", 40 | "\n", 41 | "An important aspect of geospatial data is that we can look at *spatial relationships*: how two spatial objects relate to each other (whether they overlap, intersect, contain, .. one another).\n", 42 | "\n", 43 | "The topological, set-theoretic relationships in GIS are typically based on the DE-9IM model. See https://en.wikipedia.org/wiki/Spatial_relation for more information.\n", 44 | "\n", 45 | "![](img/TopologicSpatialRelations2.png)\n", 46 | "(Image by [Krauss, CC BY-SA 3.0](https://en.wikipedia.org/wiki/Spatial_relation#/media/File:TopologicSpatialRelarions2.png))" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "### Relationships between individual objects" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "Let's first create some small toy spatial objects:\n", 61 | "\n", 62 | "A polygon (note: we use `.squeeze()` here to to extract the scalar geometry object from the GeoSeries of length 1):" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "belgium = countries.loc[countries['name'] == 'Belgium', 'geometry'].squeeze()" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Two points:" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "paris = cities.loc[cities['name'] == 'Paris', 'geometry'].squeeze()\n", 88 | "brussels = cities.loc[cities['name'] == 'Brussels', 'geometry'].squeeze()" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "And a linestring:" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": null, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "from shapely.geometry import LineString\n", 105 | "line = LineString([paris, brussels])" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "Let's visualize those 4 geometry objects together (I only put them in a GeoSeries to easily display them together with the geopandas `.plot()` method):" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "geopandas.GeoSeries([belgium, paris, brussels, line]).plot(cmap='tab10')" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "You can recognize the abstract shape of Belgium.\n", 129 | "\n", 130 | "Brussels, the capital of Belgium, is thus located within Belgium. This is a spatial relationship, and we can test this using the individual shapely geometry objects as follow:" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "brussels.within(belgium)" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "And using the reverse, Belgium contains Brussels:" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "belgium.contains(brussels)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "On the other hand, Paris is not located in Belgium:" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "belgium.contains(paris)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "paris.within(belgium)" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": {}, 186 | "source": [ 187 | "The straight line we draw from Paris to Brussels is not fully located within Belgium, but it does intersect with it:" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "belgium.contains(line)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "line.intersects(belgium)" 206 | ] 207 | }, 208 | { 209 | "cell_type": "markdown", 210 | "metadata": {}, 211 | "source": [ 212 | "### Spatial relationships with GeoDataFrames\n", 213 | "\n", 214 | "The same methods that are available on individual `shapely` geometries as we have seen above, are also available as methods on `GeoSeries` / `GeoDataFrame` objects.\n", 215 | "\n", 216 | "For example, if we call the `contains` method on the world dataset with the `paris` point, it will do this spatial check for each country in the `world` dataframe:" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "countries.contains(paris)" 226 | ] 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "metadata": {}, 231 | "source": [ 232 | "Because the above gives us a boolean result, we can use that to filter the dataframe:" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "countries[countries.contains(paris)]" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "And indeed, France is the only country in the world in which Paris is located." 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "Another example, extracting the linestring of the Amazon river in South America, we can query through which countries the river flows:" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "amazon = rivers[rivers['name'] == 'Amazonas'].geometry.squeeze()" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "metadata": {}, 271 | "outputs": [], 272 | "source": [ 273 | "countries[countries.crosses(amazon)] # or .intersects" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "
\n", 281 | "REFERENCE:

\n", 282 | "\n", 283 | "Overview of the different functions to check spatial relationships (*spatial predicate functions*):\n", 284 | "\n", 285 | "\n", 296 | "\n", 297 | "

\n", 298 | "See https://shapely.readthedocs.io/en/stable/manual.html#predicates-and-relationships for an overview of those methods.\n", 299 | "

\n", 300 | "See https://en.wikipedia.org/wiki/DE-9IM for all details on the semantics of those operations.\n", 301 | "

\n", 302 | "
" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "metadata": {}, 308 | "source": [ 309 | "## Spatial operations\n", 310 | "\n", 311 | "Next to the spatial predicates that return boolean values, Shapely and GeoPandas aslo provide analysis methods that return new geometric objects.\n", 312 | "\n", 313 | "See https://shapely.readthedocs.io/en/stable/manual.html#spatial-analysis-methods for more details." 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "For example, using the toy data from above, let's construct a buffer around Brussels (which returns a Polygon):" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": null, 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "geopandas.GeoSeries([belgium, brussels.buffer(1)]).plot(alpha=0.5, cmap='tab10')" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": {}, 335 | "source": [ 336 | "and now take the intersection, union or difference of those two polygons:" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "brussels.buffer(1).intersection(belgium)" 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": null, 351 | "metadata": {}, 352 | "outputs": [], 353 | "source": [ 354 | "brussels.buffer(1).union(belgium)" 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": null, 360 | "metadata": {}, 361 | "outputs": [], 362 | "source": [ 363 | "brussels.buffer(1).difference(belgium)" 364 | ] 365 | }, 366 | { 367 | "cell_type": "markdown", 368 | "metadata": {}, 369 | "source": [ 370 | "Another useful method is the `unary_union` attribute, which converts the set of geometry objects in a GeoDataFrame into a single geometry object by taking the union of all those geometries.\n", 371 | "\n", 372 | "For example, we can construct a single object for the Africa continent:" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": null, 378 | "metadata": {}, 379 | "outputs": [], 380 | "source": [ 381 | "africa_countries = countries[countries['continent'] == 'Africa']" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": null, 387 | "metadata": {}, 388 | "outputs": [], 389 | "source": [ 390 | "africa = africa_countries.unary_union" 391 | ] 392 | }, 393 | { 394 | "cell_type": "code", 395 | "execution_count": null, 396 | "metadata": {}, 397 | "outputs": [], 398 | "source": [ 399 | "africa" 400 | ] 401 | }, 402 | { 403 | "cell_type": "code", 404 | "execution_count": null, 405 | "metadata": { 406 | "scrolled": false 407 | }, 408 | "outputs": [], 409 | "source": [ 410 | "print(str(africa)[:1000])" 411 | ] 412 | }, 413 | { 414 | "cell_type": "markdown", 415 | "metadata": {}, 416 | "source": [ 417 | "
\n", 418 | "REMEMBER:

\n", 419 | "\n", 420 | "GeoPandas (and Shapely for the individual objects) provides a whole lot of basic methods to analyse the geospatial data (distance, length, centroid, boundary, convex_hull, simplify, transform, ....), much more than the few that we can touch in this tutorial.\n", 421 | "\n", 422 | "\n", 423 | "\n", 426 | "\n", 427 | "
\n", 428 | "\n" 429 | ] 430 | } 431 | ], 432 | "metadata": { 433 | "kernelspec": { 434 | "display_name": "Python 3", 435 | "language": "python", 436 | "name": "python3" 437 | }, 438 | "language_info": { 439 | "codemirror_mode": { 440 | "name": "ipython", 441 | "version": 3 442 | }, 443 | "file_extension": ".py", 444 | "mimetype": "text/x-python", 445 | "name": "python", 446 | "nbconvert_exporter": "python", 447 | "pygments_lexer": "ipython3", 448 | "version": "3.6.3" 449 | } 450 | }, 451 | "nbformat": 4, 452 | "nbformat_minor": 2 453 | } 454 | -------------------------------------------------------------------------------- /03-spatial-joins.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Spatial joins" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": { 13 | "slideshow": { 14 | "slide_type": "fragment" 15 | } 16 | }, 17 | "source": [ 18 | "Goals of this notebook:\n", 19 | "\n", 20 | "- Based on the `countries` and `cities` dataframes, determine for each city the country in which it is located.\n", 21 | "- To solve this problem, we will use the the concept of a 'spatial join' operation: combining information of geospatial datasets based on their spatial relationship." 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "%matplotlib inline\n", 31 | "\n", 32 | "import pandas as pd\n", 33 | "import geopandas\n", 34 | "\n", 35 | "pd.options.display.max_rows = 10" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "countries = geopandas.read_file(\"zip://./data/ne_110m_admin_0_countries.zip\")\n", 45 | "cities = geopandas.read_file(\"zip://./data/ne_110m_populated_places.zip\")\n", 46 | "rivers = geopandas.read_file(\"zip://./data/ne_50m_rivers_lake_centerlines.zip\")" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Recap - joining dataframes\n", 54 | "\n", 55 | "Pandas provides functionality to join or merge dataframes in different ways, see https://chrisalbon.com/python/data_wrangling/pandas_join_merge_dataframe/ for an overview and https://pandas.pydata.org/pandas-docs/stable/merging.html for the full documentation." 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "To illustrate the concept of joining the information of two dataframes with pandas, let's take a small subset of our `cities` and `countries` datasets: " 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "cities2 = cities[cities['name'].isin(['Bern', 'Brussels', 'London', 'Paris'])].copy()\n", 72 | "cities2['iso_a3'] = ['CHE', 'BEL', 'GBR', 'FRA']" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "cities2" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "countries2 = countries[['iso_a3', 'name', 'continent']]\n", 91 | "countries2.head()" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "We added a 'iso_a3' column to the `cities` dataset, indicating a code of the country of the city. This country code is also present in the `countries` dataset, which allows us to merge those two dataframes based on the common column.\n", 99 | "\n", 100 | "Joining the `cities` dataframe with `countries` will transfer extra information about the countries (the full name, the continent) to the `cities` dataframe, based on a common key:" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "cities2.merge(countries2, on='iso_a3')" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "**But**, for this illustrative example, we added the common column manually, it is not present in the original dataset. However, we can still know how to join those two datasets based on their spatial coordinates." 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "## Recap - spatial relationships between objects\n", 124 | "\n", 125 | "In the previous notebook [02-spatial-relationships.ipynb](./02-spatial-relationships-operations.ipynb), we have seen the notion of spatial relationships between geometry objects: within, contains, intersects, ...\n", 126 | "\n", 127 | "In this case, we know that each of the cities is located *within* one of the countries, or the other way around that each country can *contain* multiple cities.\n", 128 | "\n", 129 | "We can test such relationships using the methods we have seen in the previous notebook:" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "france = countries.loc[countries['name'] == 'France', 'geometry'].squeeze()" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "cities.within(france)" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "The above gives us a boolean series, indicating for each point in our `cities` dataframe whether it is located within the area of France or not. \n", 155 | "Because this is a boolean series as result, we can use it to filter the original dataframe to only show those cities that are actually within France:" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "cities[cities.within(france)]" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "We could now repeat the above analysis for each of the countries, and add a column to the `cities` dataframe indicating this country. However, that would be tedious to do manually, and is also exactly what the spatial join operation provides us.\n", 172 | "\n", 173 | "*(note: the above result is incorrect, but this is just because of the coarse-ness of the countries dataset)*" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": { 179 | "slideshow": { 180 | "slide_type": "slide" 181 | } 182 | }, 183 | "source": [ 184 | "## Spatial join operation\n", 185 | "\n", 186 | "
\n", 187 | "SPATIAL JOIN = *transferring attributes from one layer to another based on their spatial relationship*

\n", 188 | "\n", 189 | "\n", 190 | "Different parts of this operations:\n", 191 | "\n", 192 | "\n", 198 | "\n", 199 | "
" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": { 205 | "slideshow": { 206 | "slide_type": "-" 207 | } 208 | }, 209 | "source": [ 210 | "In this case, we want to join the `cities` dataframe with the information of the `countries` dataframe, based on the spatial relationship between both datasets.\n", 211 | "\n", 212 | "We use the [`geopandas.sjoin`](http://geopandas.readthedocs.io/en/latest/reference/geopandas.sjoin.html) function:" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "joined = geopandas.sjoin(cities, countries, op='within', how='left')" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "joined" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "joined['continent'].value_counts()" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "## The overlay operation\n", 247 | "\n", 248 | "In the spatial join operation above, we are not changing the geometries itself. We are not joining geometries, but joining attributes based on a spatial relationship between the geometries. This also means that the geometries need to at least overlap partially.\n", 249 | "\n", 250 | "If you want to create new geometries based on joining (combining) geometries of different dataframes into one new dataframe (eg by taking the intersection of the geometries), you want an **overlay** operation." 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": null, 256 | "metadata": {}, 257 | "outputs": [], 258 | "source": [ 259 | "africa = countries[countries['continent'] == 'Africa']" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": null, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "africa.plot()" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "cities['geometry'] = cities.buffer(2)" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": null, 283 | "metadata": {}, 284 | "outputs": [], 285 | "source": [ 286 | "geopandas.overlay(africa, cities, how='difference').plot()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "
\n", 294 | "REMEMBER
\n", 295 | "\n", 296 | "\n", 300 | "\n", 301 | "
" 302 | ] 303 | } 304 | ], 305 | "metadata": { 306 | "kernelspec": { 307 | "display_name": "Python 3", 308 | "language": "python", 309 | "name": "python3" 310 | }, 311 | "language_info": { 312 | "codemirror_mode": { 313 | "name": "ipython", 314 | "version": 3 315 | }, 316 | "file_extension": ".py", 317 | "mimetype": "text/x-python", 318 | "name": "python", 319 | "nbconvert_exporter": "python", 320 | "pygments_lexer": "ipython3", 321 | "version": "3.5.5" 322 | } 323 | }, 324 | "nbformat": 4, 325 | "nbformat_minor": 2 326 | } 327 | -------------------------------------------------------------------------------- /04-more-on-visualization.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Visualizing spatial data with Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%matplotlib inline\n", 17 | "\n", 18 | "import pandas as pd\n", 19 | "import geopandas\n", 20 | "\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "\n", 23 | "pd.options.display.max_rows = 10" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "countries = geopandas.read_file(\"zip://./data/ne_110m_admin_0_countries.zip\")\n", 33 | "cities = geopandas.read_file(\"zip://./data/ne_110m_populated_places.zip\")\n", 34 | "rivers = geopandas.read_file(\"zip://./data/ne_50m_rivers_lake_centerlines.zip\")" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## GeoPandas visualization functionality" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "#### Basic plot" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "countries.plot()" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "#### Adjusting the figure size" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "countries.plot(figsize=(15, 15))" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "#### Removing the box / x and y coordinate labels" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "ax = countries.plot(figsize=(15, 15))\n", 90 | "ax.set_axis_off()" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "#### Coloring based on column values\n", 98 | "\n", 99 | "Let's first create a new column with the GDP per capita:" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "countries = countries[(countries['pop_est'] >0 ) & (countries['name'] != \"Antarctica\")]" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "countries['gdp_per_cap'] = countries['gdp_md_est'] / countries['pop_est'] * 100" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "and now we can use this column to color the polygons:" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "ax = countries.plot(figsize=(15, 15), column='gdp_per_cap')\n", 134 | "ax.set_axis_off()" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "ax = countries.plot(figsize=(15, 15), column='gdp_per_cap', scheme='quantiles', legend=True)\n", 144 | "ax.set_axis_off()" 145 | ] 146 | }, 147 | { 148 | "cell_type": "markdown", 149 | "metadata": {}, 150 | "source": [ 151 | "#### Combining different dataframes on a single plot\n", 152 | "\n", 153 | "The `.plot` method returns a matplotlib Axes object, which can then be re-used to add additional layers to that plot with the `ax=` keyword:" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "ax = countries.plot(figsize=(15, 15))\n", 163 | "cities.plot(ax=ax, color='red', markersize=10)\n", 164 | "ax.set_axis_off()" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "ax = countries.plot(edgecolor='k', facecolor='none', figsize=(15, 10))\n", 174 | "rivers.plot(ax=ax)\n", 175 | "cities.plot(ax=ax, color='C1')\n", 176 | "ax.set(xlim=(-20, 60), ylim=(-40, 40))" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "## Using `geoplot`\n", 184 | "\n", 185 | "The `geoplot` packages provides some additional functionality compared to the basic `.plot()` method on GeoDataFrames:\n", 186 | "\n", 187 | "- High-level plotting API (with more plot types as geopandas)\n", 188 | "- Native projection support\n", 189 | "\n", 190 | "https://residentmario.github.io/geoplot/index.html" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "import geoplot\n", 200 | "import geoplot.crs as gcrs" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "fig, ax = plt.subplots(figsize=(10, 10), subplot_kw={\n", 210 | " 'projection': gcrs.Orthographic(central_latitude=40.7128, central_longitude=-74.0059)\n", 211 | "})\n", 212 | "geoplot.choropleth(countries, hue='gdp_per_cap', projection=gcrs.Orthographic(), ax=ax,\n", 213 | " cmap='magma', linewidth=0.5, edgecolor='white', k=None)\n", 214 | "ax.set_global()\n", 215 | "ax.outline_patch.set_visible(True)\n", 216 | "#ax.coastlines()" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "## Using `cartopy`\n", 224 | "\n", 225 | "Cartopy is the base matplotlib cartographic library, and it is used by `geoplot` under the hood to provide projection-awareness.\n", 226 | "\n", 227 | "http://scitools.org.uk/cartopy/docs/latest/index.html\n", 228 | "\n", 229 | "The following example is taken from the docs: http://geopandas.readthedocs.io/en/latest/gallery/cartopy_convert.html#sphx-glr-gallery-cartopy-convert-py" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "from cartopy import crs as ccrs" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "# Define the CartoPy CRS object.\n", 248 | "crs = ccrs.AlbersEqualArea()\n", 249 | "\n", 250 | "# This can be converted into a `proj4` string/dict compatible with GeoPandas\n", 251 | "crs_proj4 = crs.proj4_init\n", 252 | "countries_ae = countries.to_crs(crs_proj4)\n", 253 | "\n", 254 | "# Here's what the plot looks like in GeoPandas\n", 255 | "countries_ae.plot()" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": {}, 261 | "source": [ 262 | "## Interactive web-based visualizations\n", 263 | "\n", 264 | "There are nowadays many libraries that target interactive web-based visualizations and that can handle geospatial data. Some packages with an example for each:\n", 265 | "\n", 266 | "- Bokeh: https://bokeh.pydata.org/en/latest/docs/gallery/texas.html\n", 267 | "- GeoViews (other interface to Bokeh/matplotlib): http://geo.holoviews.org\n", 268 | "- Altair: https://altair-viz.github.io/gallery/choropleth.html\n", 269 | "- Plotly: https://plot.ly/python/#maps\n", 270 | "- ..." 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "Another popular javascript library for online maps is [Leaflet.js](https://leafletjs.com/), and this has python bindings in the [folium](https://github.com/python-visualization/folium) and [ipyleaflet](https://github.com/jupyter-widgets/ipyleaflet) packages." 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "An example with folium:" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "import folium" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "m = folium.Map([48.8566, 2.3429], zoom_start=6, tiles=\"OpenStreetMap\")\n", 303 | "folium.GeoJson(countries.to_json()).add_to(m)\n", 304 | "m" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "metadata": {}, 311 | "outputs": [], 312 | "source": [] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "metadata": {}, 318 | "outputs": [], 319 | "source": [ 320 | "m = folium.Map([48.8566, 2.3429], zoom_start=6, tiles=\"OpenStreetMap\")\n", 321 | "folium.GeoJson(cities.to_json()).add_to(m)\n", 322 | "m" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": null, 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [] 331 | } 332 | ], 333 | "metadata": { 334 | "kernelspec": { 335 | "display_name": "Python 3", 336 | "language": "python", 337 | "name": "python3" 338 | }, 339 | "language_info": { 340 | "codemirror_mode": { 341 | "name": "ipython", 342 | "version": 3 343 | }, 344 | "file_extension": ".py", 345 | "mimetype": "text/x-python", 346 | "name": "python", 347 | "nbconvert_exporter": "python", 348 | "pygments_lexer": "ipython3", 349 | "version": "3.5.5" 350 | } 351 | }, 352 | "nbformat": 4, 353 | "nbformat_minor": 2 354 | } 355 | -------------------------------------------------------------------------------- /07-data-borrowing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Concepts in Spatial Linear Modelling\n", 8 | "\n", 9 | "### Data Borrowing in Supervised Learning" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 14, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "import libpysal as lp\n", 20 | "import geopandas as gpd\n", 21 | "import pandas as pd\n", 22 | "import shapely.geometry as shp\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "import seaborn as sns\n", 25 | "%matplotlib inline" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 15, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "listings = pd.read_csv('./data/berlin-listings.csv.gz')\n", 35 | "listings['geometry'] = listings[['longitude','latitude']].apply(shp.Point, axis=1)\n", 36 | "listings = gpd.GeoDataFrame(listings)\n", 37 | "listings.crs = {'init':'epsg:4269'}\n", 38 | "listings = listings.to_crs(epsg=3857)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 16, 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "data": { 48 | "text/plain": [ 49 | "" 50 | ] 51 | }, 52 | "execution_count": 16, 53 | "metadata": {}, 54 | "output_type": "execute_result" 55 | }, 56 | { 57 | "data": { 58 | "image/png": "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\n", 59 | "text/plain": [ 60 | "
" 61 | ] 62 | }, 63 | "metadata": { 64 | "needs_background": "light" 65 | }, 66 | "output_type": "display_data" 67 | } 68 | ], 69 | "source": [ 70 | "listings.sort_values('price').plot('price', cmap='plasma')" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "# Kernel Regressions" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "Kernel regressions are one exceptionally common way to allow observations to \"borrow strength\" from nearby observations. \n", 85 | "\n", 86 | "However, when working with spatial data, there are *two simultaneous senses of what is near:* \n", 87 | "- things that similar in attribute (classical kernel regression)\n", 88 | "- things that are similar in spatial position (spatial kernel regression)\n", 89 | "\n", 90 | "Below, we'll walk through how to use scikit to fit these two types of kernel regressions, show how it's not super simple to mix the two approaches together, and refer to an approach that does this correctly in another package. " 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "First, though, let's try to predict the log of an Airbnb's nightly price based on a few factors:\n", 98 | "- accommodates: the number of people the airbnb can accommodate\n", 99 | "- review_scores_rating: the aggregate rating of the listing\n", 100 | "- bedrooms: the number of bedrooms the airbnb has\n", 101 | "- bathrooms: the number of bathrooms the airbnb has\n", 102 | "- beds: the number of beds the airbnb offers" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 17, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "model_data = listings[['accommodates', 'review_scores_rating', \n", 112 | " 'bedrooms', 'bathrooms', 'beds', \n", 113 | " 'price', 'geometry']].dropna()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 18, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "Xnames = ['accommodates', 'review_scores_rating', \n", 123 | " 'bedrooms', 'bathrooms', 'beds' ]\n", 124 | "X = model_data[Xnames].values\n", 125 | "X = X.astype(float)\n", 126 | "y = np.log(model_data[['price']].values)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "Further, since each listing has a location, I'll extract the set of spatial coordinates coordinates for each listing." 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 19, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "coordinates = np.vstack(model_data.geometry.apply(lambda p: np.hstack(p.xy)).values)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "scikit neighbor regressions are contained in the `sklearn.neighbors` module, and there are two main types:\n", 150 | "- `KNeighborsRegressor`, which uses a k-nearest neighborhood of observations around each focal site\n", 151 | "- `RadiusNeighborsRegressor`, which considers all observations within a fixed radius around each focal site.\n", 152 | "\n", 153 | "Further, these methods can use inverse distance weighting to rank the relative importance of sites around each focal; in this way, near things are given more weight than far things, even when there's a lot of near things. " 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 20, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "import sklearn.neighbors as skn\n", 163 | "import sklearn.metrics as skm" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 21, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "shuffle = np.random.permutation(len(y))\n", 173 | "train,test = shuffle[:14000],shuffle[14000:]" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "So, let's fit three models:\n", 181 | "- `spatial`: using inverse distance weighting on the nearest 500 neighbors geograpical space\n", 182 | "- `attribute`: using inverse distance weighting on the nearest 500 neighbors in attribute space\n", 183 | "- `both`: using inverse distance weighting in both geographical and attribute space. " 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 22, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "KNNR = skn.KNeighborsRegressor(weights='distance', n_neighbors=500)\n", 193 | "spatial = KNNR.fit(coordinates[train,:],\n", 194 | " y[train,:])\n", 195 | "KNNR = skn.KNeighborsRegressor(weights='distance', n_neighbors=500)\n", 196 | "attribute = KNNR.fit(X[train,:],\n", 197 | " y[train,])\n", 198 | "KNNR = skn.KNeighborsRegressor(weights='distance', n_neighbors=500)\n", 199 | "both = KNNR.fit(np.hstack((coordinates,X))[train,:],\n", 200 | " y[train,:])" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "To score them, I'm going to grab their out of sample prediction accuracy and get their % explained variance:" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 23, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "sp_ypred = spatial.predict(coordinates[test,:])\n", 217 | "att_ypred = attribute.predict(X[test,:])\n", 218 | "both_ypred = both.predict(np.hstack((X,coordinates))[test,:])" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 24, 224 | "metadata": {}, 225 | "outputs": [ 226 | { 227 | "data": { 228 | "text/plain": [ 229 | "(0.1443088606590084, 0.3149860849884514, -5.684468673550214e-09)" 230 | ] 231 | }, 232 | "execution_count": 24, 233 | "metadata": {}, 234 | "output_type": "execute_result" 235 | } 236 | ], 237 | "source": [ 238 | "(skm.explained_variance_score(y[test,], sp_ypred),\n", 239 | " skm.explained_variance_score(y[test,], att_ypred),\n", 240 | " skm.explained_variance_score(y[test,], both_ypred))" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "If you don't know $X$, using $Wy$ would be better than nothing, but it works nowhere near as well... less than half of the variance that is explained by nearness in feature/attribute space is explained by nearness in geographical space. \n", 248 | "\n", 249 | "Making things even worse, simply glomming on the geographical information to the feature set makes the model perform horribly. \n", 250 | "\n", 251 | "*There must be another way!*\n", 252 | "\n", 253 | "One method that can exploit the fact that local data may be more informative in predicting $y$ at site $i$ than distant data is Geographically Weighted Regression, a type of Generalized Additive Spatial Model. Kind of like a Kernel Regression, GWR conducts a bunch of regressions at each training site only considering data near that site. This means it works like the kernel regressions above, but uses *both* the coordinates *and* the data in $X$ to predict $y$ at each site. It optimizes its sense of \"local\" depending on some information criteria or fit score.\n", 254 | "\n", 255 | "You can find this in the `gwr` package, and significant development is ongoing on this at `https://github.com/pysal/gwr`." 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": {}, 261 | "source": [ 262 | "# Data Borrowing" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "Another common case where these weights are used are in \"feature engineering.\" Using the weights matrix, you can construct neighbourhood averages of the data matrix and use these as synthetic features in your model. These often have a strong relationship to the outcome as well, since spatial data is often smooth and attributes of nearby sites often have a spillover impact on each other. " 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 25, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "from libpysal.weights.util import fill_diagonal" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "metadata": {}, 284 | "source": [ 285 | "First, we'll construct a Kernel weight from the data that we have, make it an adaptive Kernel bandwidth, and make sure that our kernel weights don't have any self-neighbors. Since we've got the data at each site anyway, we probably shouldn't use that data *again* when we construct our neighborhood-smoothed syntetic features. " 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 26, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "kW = lp.weights.Kernel.from_dataframe(model_data, fixed=False, function='gaussian', k=100)\n", 295 | "kW = fill_diagonal(kW, 0)" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 27, 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "WX = lp.weights.lag_spatial(kW, X)" 305 | ] 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "metadata": {}, 310 | "source": [ 311 | "I like `statsmodels` regression summary tables, so I'll pop it up here. " 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": 28, 317 | "metadata": {}, 318 | "outputs": [], 319 | "source": [ 320 | "import statsmodels.api as sm" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 29, 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "Xtable = pd.DataFrame(X, columns=Xnames)" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": {}, 335 | "source": [ 336 | "Below are the results for the model with only the covariates used above:\n", 337 | "- accommodates: the number of people the airbnb can accommodate\n", 338 | "- review_scores_rating: the aggregate rating of the listing\n", 339 | "- bedrooms: the number of bedrooms the airbnb has\n", 340 | "- bathrooms: the number of bathrooms the airbnb has\n", 341 | "- beds: the number of beds the airbnb offers\n", 342 | "\n", 343 | "We've not used any of our synthetic features in `WX`. " 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 30, 349 | "metadata": {}, 350 | "outputs": [ 351 | { 352 | "data": { 353 | "text/html": [ 354 | "\n", 355 | "\n", 356 | "\n", 357 | " \n", 358 | "\n", 359 | "\n", 360 | " \n", 361 | "\n", 362 | "\n", 363 | " \n", 364 | "\n", 365 | "\n", 366 | " \n", 367 | "\n", 368 | "\n", 369 | " \n", 370 | "\n", 371 | "\n", 372 | " \n", 373 | "\n", 374 | "\n", 375 | " \n", 376 | "\n", 377 | "\n", 378 | " \n", 379 | "\n", 380 | "\n", 381 | " \n", 382 | "\n", 383 | "
OLS Regression Results
Dep. Variable: y R-squared: 0.290
Model: OLS Adj. R-squared: 0.290
Method: Least Squares F-statistic: 1269.
Date: Fri, 21 Sep 2018 Prob (F-statistic): 0.00
Time: 15:41:48 Log-Likelihood: -9260.8
No. Observations: 15516 AIC: 1.853e+04
Df Residuals: 15510 BIC: 1.858e+04
Df Model: 5
Covariance Type: nonrobust
\n", 384 | "\n", 385 | "\n", 386 | " \n", 387 | "\n", 388 | "\n", 389 | " \n", 390 | "\n", 391 | "\n", 392 | " \n", 393 | "\n", 394 | "\n", 395 | " \n", 396 | "\n", 397 | "\n", 398 | " \n", 399 | "\n", 400 | "\n", 401 | " \n", 402 | "\n", 403 | "\n", 404 | " \n", 405 | "\n", 406 | "
coef std err t P>|t| [0.025 0.975]
const 2.9682 0.044 67.590 0.000 2.882 3.054
accommodates 0.1882 0.004 44.362 0.000 0.180 0.197
review_scores_rating 0.0033 0.000 7.378 0.000 0.002 0.004
bedrooms 0.1427 0.008 18.503 0.000 0.128 0.158
bathrooms 0.0062 0.012 0.497 0.619 -0.018 0.031
beds -0.0482 0.005 -9.221 0.000 -0.058 -0.038
\n", 407 | "\n", 408 | "\n", 409 | " \n", 410 | "\n", 411 | "\n", 412 | " \n", 413 | "\n", 414 | "\n", 415 | " \n", 416 | "\n", 417 | "\n", 418 | " \n", 419 | "\n", 420 | "
Omnibus: 156.822 Durbin-Watson: 1.716
Prob(Omnibus): 0.000 Jarque-Bera (JB): 265.898
Skew: -0.005 Prob(JB): 1.82e-58
Kurtosis: 3.641 Cond. No. 1.17e+03


Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.17e+03. This might indicate that there are
strong multicollinearity or other numerical problems." 421 | ], 422 | "text/plain": [ 423 | "\n", 424 | "\"\"\"\n", 425 | " OLS Regression Results \n", 426 | "==============================================================================\n", 427 | "Dep. Variable: y R-squared: 0.290\n", 428 | "Model: OLS Adj. R-squared: 0.290\n", 429 | "Method: Least Squares F-statistic: 1269.\n", 430 | "Date: Fri, 21 Sep 2018 Prob (F-statistic): 0.00\n", 431 | "Time: 15:41:48 Log-Likelihood: -9260.8\n", 432 | "No. Observations: 15516 AIC: 1.853e+04\n", 433 | "Df Residuals: 15510 BIC: 1.858e+04\n", 434 | "Df Model: 5 \n", 435 | "Covariance Type: nonrobust \n", 436 | "========================================================================================\n", 437 | " coef std err t P>|t| [0.025 0.975]\n", 438 | "----------------------------------------------------------------------------------------\n", 439 | "const 2.9682 0.044 67.590 0.000 2.882 3.054\n", 440 | "accommodates 0.1882 0.004 44.362 0.000 0.180 0.197\n", 441 | "review_scores_rating 0.0033 0.000 7.378 0.000 0.002 0.004\n", 442 | "bedrooms 0.1427 0.008 18.503 0.000 0.128 0.158\n", 443 | "bathrooms 0.0062 0.012 0.497 0.619 -0.018 0.031\n", 444 | "beds -0.0482 0.005 -9.221 0.000 -0.058 -0.038\n", 445 | "==============================================================================\n", 446 | "Omnibus: 156.822 Durbin-Watson: 1.716\n", 447 | "Prob(Omnibus): 0.000 Jarque-Bera (JB): 265.898\n", 448 | "Skew: -0.005 Prob(JB): 1.82e-58\n", 449 | "Kurtosis: 3.641 Cond. No. 1.17e+03\n", 450 | "==============================================================================\n", 451 | "\n", 452 | "Warnings:\n", 453 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n", 454 | "[2] The condition number is large, 1.17e+03. This might indicate that there are\n", 455 | "strong multicollinearity or other numerical problems.\n", 456 | "\"\"\"" 457 | ] 458 | }, 459 | "execution_count": 30, 460 | "metadata": {}, 461 | "output_type": "execute_result" 462 | } 463 | ], 464 | "source": [ 465 | "onlyX = sm.OLS(y,sm.add_constant(Xtable)).fit()\n", 466 | "onlyX.summary()" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": {}, 472 | "source": [ 473 | "Then, we could fit a model using the neighbourhood average synthetic features as well:" 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": 31, 479 | "metadata": {}, 480 | "outputs": [], 481 | "source": [ 482 | "WXtable = pd.DataFrame(WX, columns=['lag_{}'.format(name) for name in Xnames])" 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 32, 488 | "metadata": {}, 489 | "outputs": [], 490 | "source": [ 491 | "XWXtable = pd.concat((Xtable,WXtable),axis=1)" 492 | ] 493 | }, 494 | { 495 | "cell_type": "code", 496 | "execution_count": 33, 497 | "metadata": {}, 498 | "outputs": [ 499 | { 500 | "data": { 501 | "text/html": [ 502 | "\n", 503 | "\n", 504 | "\n", 505 | " \n", 506 | "\n", 507 | "\n", 508 | " \n", 509 | "\n", 510 | "\n", 511 | " \n", 512 | "\n", 513 | "\n", 514 | " \n", 515 | "\n", 516 | "\n", 517 | " \n", 518 | "\n", 519 | "\n", 520 | " \n", 521 | "\n", 522 | "\n", 523 | " \n", 524 | "\n", 525 | "\n", 526 | " \n", 527 | "\n", 528 | "\n", 529 | " \n", 530 | "\n", 531 | "
OLS Regression Results
Dep. Variable: y R-squared: 0.311
Model: OLS Adj. R-squared: 0.311
Method: Least Squares F-statistic: 701.4
Date: Fri, 21 Sep 2018 Prob (F-statistic): 0.00
Time: 15:41:50 Log-Likelihood: -9026.9
No. Observations: 15516 AIC: 1.808e+04
Df Residuals: 15505 BIC: 1.816e+04
Df Model: 10
Covariance Type: nonrobust
\n", 532 | "\n", 533 | "\n", 534 | " \n", 535 | "\n", 536 | "\n", 537 | " \n", 538 | "\n", 539 | "\n", 540 | " \n", 541 | "\n", 542 | "\n", 543 | " \n", 544 | "\n", 545 | "\n", 546 | " \n", 547 | "\n", 548 | "\n", 549 | " \n", 550 | "\n", 551 | "\n", 552 | " \n", 553 | "\n", 554 | "\n", 555 | " \n", 556 | "\n", 557 | "\n", 558 | " \n", 559 | "\n", 560 | "\n", 561 | " \n", 562 | "\n", 563 | "\n", 564 | " \n", 565 | "\n", 566 | "\n", 567 | " \n", 568 | "\n", 569 | "
coef std err t P>|t| [0.025 0.975]
const 2.7158 0.114 23.723 0.000 2.491 2.940
accommodates 0.1829 0.004 43.616 0.000 0.175 0.191
review_scores_rating 0.0035 0.000 8.036 0.000 0.003 0.004
bedrooms 0.1465 0.008 19.126 0.000 0.131 0.161
bathrooms -0.0135 0.012 -1.087 0.277 -0.038 0.011
beds -0.0448 0.005 -8.616 0.000 -0.055 -0.035
lag_accommodates 0.0162 0.001 16.632 0.000 0.014 0.018
lag_review_scores_rating -0.0004 4.29e-05 -9.279 0.000 -0.000 -0.000
lag_bedrooms 0.0001 0.002 0.086 0.931 -0.003 0.003
lag_bathrooms 0.0240 0.002 10.595 0.000 0.020 0.028
lag_beds -0.0150 0.001 -13.860 0.000 -0.017 -0.013
\n", 570 | "\n", 571 | "\n", 572 | " \n", 573 | "\n", 574 | "\n", 575 | " \n", 576 | "\n", 577 | "\n", 578 | " \n", 579 | "\n", 580 | "\n", 581 | " \n", 582 | "\n", 583 | "
Omnibus: 163.213 Durbin-Watson: 1.786
Prob(Omnibus): 0.000 Jarque-Bera (JB): 278.468
Skew: -0.023 Prob(JB): 3.40e-61
Kurtosis: 3.655 Cond. No. 9.71e+04


Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 9.71e+04. This might indicate that there are
strong multicollinearity or other numerical problems." 584 | ], 585 | "text/plain": [ 586 | "\n", 587 | "\"\"\"\n", 588 | " OLS Regression Results \n", 589 | "==============================================================================\n", 590 | "Dep. Variable: y R-squared: 0.311\n", 591 | "Model: OLS Adj. R-squared: 0.311\n", 592 | "Method: Least Squares F-statistic: 701.4\n", 593 | "Date: Fri, 21 Sep 2018 Prob (F-statistic): 0.00\n", 594 | "Time: 15:41:50 Log-Likelihood: -9026.9\n", 595 | "No. Observations: 15516 AIC: 1.808e+04\n", 596 | "Df Residuals: 15505 BIC: 1.816e+04\n", 597 | "Df Model: 10 \n", 598 | "Covariance Type: nonrobust \n", 599 | "============================================================================================\n", 600 | " coef std err t P>|t| [0.025 0.975]\n", 601 | "--------------------------------------------------------------------------------------------\n", 602 | "const 2.7158 0.114 23.723 0.000 2.491 2.940\n", 603 | "accommodates 0.1829 0.004 43.616 0.000 0.175 0.191\n", 604 | "review_scores_rating 0.0035 0.000 8.036 0.000 0.003 0.004\n", 605 | "bedrooms 0.1465 0.008 19.126 0.000 0.131 0.161\n", 606 | "bathrooms -0.0135 0.012 -1.087 0.277 -0.038 0.011\n", 607 | "beds -0.0448 0.005 -8.616 0.000 -0.055 -0.035\n", 608 | "lag_accommodates 0.0162 0.001 16.632 0.000 0.014 0.018\n", 609 | "lag_review_scores_rating -0.0004 4.29e-05 -9.279 0.000 -0.000 -0.000\n", 610 | "lag_bedrooms 0.0001 0.002 0.086 0.931 -0.003 0.003\n", 611 | "lag_bathrooms 0.0240 0.002 10.595 0.000 0.020 0.028\n", 612 | "lag_beds -0.0150 0.001 -13.860 0.000 -0.017 -0.013\n", 613 | "==============================================================================\n", 614 | "Omnibus: 163.213 Durbin-Watson: 1.786\n", 615 | "Prob(Omnibus): 0.000 Jarque-Bera (JB): 278.468\n", 616 | "Skew: -0.023 Prob(JB): 3.40e-61\n", 617 | "Kurtosis: 3.655 Cond. No. 9.71e+04\n", 618 | "==============================================================================\n", 619 | "\n", 620 | "Warnings:\n", 621 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n", 622 | "[2] The condition number is large, 9.71e+04. This might indicate that there are\n", 623 | "strong multicollinearity or other numerical problems.\n", 624 | "\"\"\"" 625 | ] 626 | }, 627 | "execution_count": 33, 628 | "metadata": {}, 629 | "output_type": "execute_result" 630 | } 631 | ], 632 | "source": [ 633 | "withWX = sm.OLS(y,sm.add_constant(XWXtable)).fit()\n", 634 | "withWX.summary()" 635 | ] 636 | }, 637 | { 638 | "cell_type": "markdown", 639 | "metadata": {}, 640 | "source": [ 641 | "This gains a nice bump in the model fit with no significant hit to model complexity. " 642 | ] 643 | }, 644 | { 645 | "cell_type": "markdown", 646 | "metadata": {}, 647 | "source": [ 648 | "## Going Further" 649 | ] 650 | }, 651 | { 652 | "cell_type": "markdown", 653 | "metadata": {}, 654 | "source": [ 655 | "We could also use a spatial autoregressive model to further improve fit. This ceases to be estimatable in `statsmodels` and instead requires `pysal.spreg`, the spatial regression submodule of PySAL. Generalized method of moments estimators are available in `pysal.spreg.GM_Lag`, and maximum likelihood methods in `pysal.spreg.ML_Lag`. \n", 656 | "\n", 657 | "These methods are often harder to fit, though, so like `gwr`, they may be less performant on big data. But, you can usually achieve a gain in fit for no significant increase in the number of terms by using these models. " 658 | ] 659 | } 660 | ], 661 | "metadata": { 662 | "kernelspec": { 663 | "display_name": "Python [conda env:scipygeo18]", 664 | "language": "python", 665 | "name": "conda-env-scipygeo18-py" 666 | }, 667 | "language_info": { 668 | "codemirror_mode": { 669 | "name": "ipython", 670 | "version": 3 671 | }, 672 | "file_extension": ".py", 673 | "mimetype": "text/x-python", 674 | "name": "python", 675 | "nbconvert_exporter": "python", 676 | "pygments_lexer": "ipython3", 677 | "version": "3.6.6" 678 | } 679 | }, 680 | "nbformat": 4, 681 | "nbformat_minor": 2 682 | } 683 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Joris Van den Bossche, Levi John Wolf, Sergio Rey, Dani Arribas-Bel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scipy 2018 Tutorial - Introduction to Geospatial Data Analysis with Python 2 | 3 | [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/geopandas/scipy2018-geospatial-data/master) 4 | 5 | ### Instructors 6 | 7 | - [Levi John Wolf](https://ljwolf.org) - [University of Bristol](http://www.bristol.ac.uk/geography/levi-j-wolf/overview.html) 8 | - Sergio Rey - [Center for Geospatial Sciences, University of California, Riverside](http://spatial.ucr.edu/peopleRey.html) 9 | - [Dani Arribas-Bel](http://darribas.org/) - University of Liverpool 10 | - [Joris Van den Bossche](https://jorisvandenbossche.github.io/) - Université Paris-Saclay Center for Data Science 11 | 12 | This tutorial is an introduction to geospatial data analysis in Python, with a focus on tabular vector data. It first focuses on introducing the participants to the different libraries to work with geospatial data and will cover munging geo-data and exploring relations over space. This includes importing data in different formats (e.g. shapefile, GeoJSON), visualizing, combining and tidying them up for analysis, and will use libraries such as `pandas`, `geopandas`, `shapely`, `PySAL`, or `rasterio`. The second part will build upon this and focus on more more advanced geographic data science and statistical methods to gain insight from the data. No previous experience with those geospatial python libraries is needed, but basic familiarity with geospatial data and concepts (shapefiles, vector vs raster data) and pandas will be helpful. 13 | 14 | ## Outline of the Workshop 15 | 16 | - 7:50 - 8:20: **Installation & setup** 17 | 18 | If you cannot complete the installation instructions ahead of time, please come slightly early so we can work on ensuring everyone can get the required packages installed and so that you can run the workshop material & exercises 19 | 20 | - 8:20 - 9:00: **Working with spatial data** 21 | 22 | 23 | 24 | - 9:00-10:00: **Spatial Relationships & Joins** 25 | 26 | 27 | 28 | - 10:00 - 10:10: **Break** 29 | 30 | - 10:10 - 11:00: **Exploratory spatial data analysis** 31 | 32 | 33 | 34 | - 11:00 - 12:00: **Leveraging space in modeling** 35 | 36 | 37 | 38 | ## Installation notes 39 | 40 | Following this tutorial will require recent installations of: 41 | 42 | - Python >= 3.5 (it will probably work on python 2.7 as well, but I didn't test it specifically) 43 | - pandas 44 | - geopandas >= 0.3.0 45 | - matplotlib 46 | - rtree 47 | - PySAL 48 | - scikit-learn 49 | - mgwr 50 | - cartopy 51 | - geoplot 52 | - [Jupyter Notebook](http://jupyter.org) 53 | 54 | If you do not yet have these packages installed, we recommend to use the [conda](http://conda.pydata.org/docs/intro.html) package manager to install all the requirements 55 | (you can install [miniconda](http://conda.pydata.org/miniconda.html) or install the (larger) Anaconda 56 | distribution, found at https://www.anaconda.com/download/). 57 | 58 | Once this is installed, the following command will install all required packages in your Python environment: 59 | 60 | ``` 61 | conda env create -f environment.yml 62 | ``` 63 | 64 | But of course, using another distribution (e.g. Enthought Canopy) or ``pip`` is fine as well (a requirements file is provided as well), as long as you have the above packages installed. 65 | 66 | 67 | ## Downloading the tutorial materials 68 | 69 | **NOTE:** *We may update the materials up until the workshop. So, please make sure that, if you download the materials, you refresh the downloaded material close to the workshop.* 70 | 71 | If you have git installed, you can get the tutorial materials by cloning this repo: 72 | 73 | git clone https://github.com/geopandas/scipy2018-geospatial-data 74 | 75 | Otherwise, you can download the repository as a .zip file by heading over 76 | to the GitHub repository (https://github.com/geopandas/scipy2018-geospatial-data) in 77 | your browser and click the green "Download" button in the upper right: 78 | 79 | ![](img/download-button.png) 80 | 81 | 82 | ## Test the tutorial environment 83 | 84 | To make sure everything was installed correctly, open a terminal, and change its directory (`cd`) so that your working directory is the tutorial materials you downloaded in the step above. Then enter the following: 85 | 86 | ```sh 87 | python check_environment.py 88 | ``` 89 | 90 | Make sure that this scripts prints "All good. Enjoy the tutorial!" 91 | 92 | -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping10.py: -------------------------------------------------------------------------------- 1 | protected_areas = geopandas.read_file("data/Conservation/RDC_aire_protegee_2013.shp") 2 | # or to read it directly from the zip file: 3 | # protected_areas = geopandas.read_file("/Conservation", vfs="zip://./data/cod_conservation.zip") -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping11.py: -------------------------------------------------------------------------------- 1 | protected_areas.plot() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping12.py: -------------------------------------------------------------------------------- 1 | from shapely.geometry import Point -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping13.py: -------------------------------------------------------------------------------- 1 | goma = Point(29.22, -1.66) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping14.py: -------------------------------------------------------------------------------- 1 | dist_goma = data.distance(goma) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping15.py: -------------------------------------------------------------------------------- 1 | dist_goma.nsmallest(5) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping16.py: -------------------------------------------------------------------------------- 1 | ax = protected_areas.plot() 2 | data.plot(ax=ax, color='C1') -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping19.py: -------------------------------------------------------------------------------- 1 | data_utm = data.to_crs(epsg=32735) 2 | protected_areas_utm = protected_areas.to_crs(epsg=32735) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping20.py: -------------------------------------------------------------------------------- 1 | ax = protected_areas_utm.plot() 2 | data_utm.plot(ax=ax, color='C1') -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping21.py: -------------------------------------------------------------------------------- 1 | ax = protected_areas_utm.plot(figsize=(10, 10), color='green') 2 | data_utm.plot(ax=ax, markersize=5, alpha=0.5) 3 | ax.set_axis_off() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping22.py: -------------------------------------------------------------------------------- 1 | # alternative with constructing the matplotlib figure first 2 | fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(aspect='equal')) 3 | protected_areas_utm.plot(ax=ax, color='green') 4 | data_utm.plot(ax=ax, markersize=5, alpha=0.5) 5 | ax.set_axis_off() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping23.py: -------------------------------------------------------------------------------- 1 | ax = protected_areas_utm.plot(figsize=(10, 10), color='green') 2 | data_utm.plot(ax=ax, markersize=5, alpha=0.5, column='interference') 3 | ax.set_axis_off() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping24.py: -------------------------------------------------------------------------------- 1 | ax = protected_areas_utm.plot(figsize=(10, 10), color='green') 2 | data_utm.plot(ax=ax, markersize=5, alpha=0.5, column='mineral1', legend=True) 3 | ax.set_axis_off() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping25.py: -------------------------------------------------------------------------------- 1 | kahuzi = protected_areas_utm[protected_areas_utm['NAME_AP'] == "Kahuzi-Biega National park"].geometry.squeeze() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping26.py: -------------------------------------------------------------------------------- 1 | mines_kahuzi = data_utm[data_utm.within(kahuzi)] 2 | mines_kahuzi -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping27.py: -------------------------------------------------------------------------------- 1 | len(mines_kahuzi) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping28.py: -------------------------------------------------------------------------------- 1 | single_mine = data_utm.geometry[0] -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping29.py: -------------------------------------------------------------------------------- 1 | dist = protected_areas_utm.distance(single_mine) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping3.py: -------------------------------------------------------------------------------- 1 | data_visits = geopandas.read_file("data/cod_mines_curated_all_opendata_p_ipis.geojson") -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping30.py: -------------------------------------------------------------------------------- 1 | idx = dist.idxmin() 2 | closest_area = protected_areas_utm.loc[idx, 'NAME_AP'] 3 | closest_area -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping31.py: -------------------------------------------------------------------------------- 1 | def closest_protected_area(mine, protected_areas): 2 | dist = protected_areas.distance(mine) 3 | idx = dist.idxmin() 4 | closest_area = protected_areas.loc[idx, 'NAME_AP'] 5 | return closest_area -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping32.py: -------------------------------------------------------------------------------- 1 | result = data_utm.geometry.apply(lambda site: closest_protected_area(site, protected_areas_utm)) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping33.py: -------------------------------------------------------------------------------- 1 | data_within_protected = geopandas.sjoin(data_utm, protected_areas_utm[['NAME_AP', 'geometry']], 2 | op='within', how='inner') -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping34.py: -------------------------------------------------------------------------------- 1 | len(data_within_protected) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping35.py: -------------------------------------------------------------------------------- 1 | data_within_protected['NAME_AP'].value_counts() 2 | # or data_within_protected.groupby('NAME_AP').size() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping36.py: -------------------------------------------------------------------------------- 1 | data_within_protected.groupby('NAME_AP')['workers_numb'].sum() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping37.py: -------------------------------------------------------------------------------- 1 | protected_areas_border = protected_areas_utm[['NAME_AP', 'geometry']].copy() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping38.py: -------------------------------------------------------------------------------- 1 | protected_areas_border['geometry'] = protected_areas_border.buffer(10000).difference(protected_areas_utm.unary_union) -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping39.py: -------------------------------------------------------------------------------- 1 | protected_areas_border.plot() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping4.py: -------------------------------------------------------------------------------- 1 | data_visits.head() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping40.py: -------------------------------------------------------------------------------- 1 | data_within_border = geopandas.sjoin(data_utm, protected_areas_border, 2 | op='within', how='inner') -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping41.py: -------------------------------------------------------------------------------- 1 | data_within_border['NAME_AP'].value_counts() -------------------------------------------------------------------------------- /_solved/solutions/case-conflict-mapping5.py: -------------------------------------------------------------------------------- 1 | len(data_visits) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote01.py: -------------------------------------------------------------------------------- 1 | pres = gpd.read_file("zip://./data/uspres.zip") 2 | pres.head(3) 3 | -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote02.py: -------------------------------------------------------------------------------- 1 | pres.crs = {'init':'epsg:4269'} 2 | pres = pres.to_crs(epsg=5070) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote03.py: -------------------------------------------------------------------------------- 1 | import seaborn as sns 2 | facets = sns.pairplot(data=pres.filter(like='dem_')) 3 | facets.map_offdiag(lambda *arg, **kw: plt.plot((0,1),(0,1), color='k')) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote04.py: -------------------------------------------------------------------------------- 1 | import seaborn as sns 2 | facets = sns.pairplot(x_vars=pres.filter(like='dem_').columns, 3 | y_vars=['gini_2015'], data=pres) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote05.py: -------------------------------------------------------------------------------- 1 | pres['swing_2012'] = pres.eval("dem_2012 - dem_2008") 2 | pres['swing_2016'] = pres.eval("dem_2016 - dem_2012") 3 | pres['swing_full'] = pres.eval("dem_2016 - dem_2008") -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote06.py: -------------------------------------------------------------------------------- 1 | f,ax = plt.subplots(3,1, 2 | subplot_kw=dict(aspect='equal', 3 | frameon=False), 4 | figsize=(60,15)) 5 | pres.plot('dem_2008', ax=ax[0], cmap='RdYlBu') 6 | pres.plot('swing_full', ax=ax[1], cmap='bwr_r') 7 | pres.plot('dem_2016', ax=ax[2], cmap='RdYlBu') 8 | for i,ax_ in enumerate(ax): 9 | ax_.set_xticks([]) 10 | ax_.set_yticks([]) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote07.py: -------------------------------------------------------------------------------- 1 | import libpysal as lp 2 | w = lp.weights.Rook.from_dataframe(pres) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote08.py: -------------------------------------------------------------------------------- 1 | from pysal import esda as esda 2 | np.random.seed(1) 3 | moran = esda.moran.Moran(pres.swing_full, w) 4 | print(moran.I) 5 | -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote09.py: -------------------------------------------------------------------------------- 1 | f = plt.figure(figsize=(6,6)) 2 | plt.scatter(pres.swing_full, lp.weights.lag_spatial(w, pres.swing_full)) 3 | plt.plot((-.3,.1),(-.3,.1), color='k') 4 | plt.title('$I = {:.3f} \ \ (p < {:.3f})$'.format(moran.I,moran.p_sim)) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote10.py: -------------------------------------------------------------------------------- 1 | np.random.seed(11) 2 | lmos = esda.moran.Moran_Local(pres.swing_full, w, 3 | permutations=70000) #min for a bonf. bound 4 | (lmos.p_sim <= (.05/len(pres))).sum() -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote11.py: -------------------------------------------------------------------------------- 1 | f = plt.figure(figsize=(10,4)) 2 | ax = plt.gca() 3 | ax.set_aspect('equal') 4 | is_weird = lmos.p_sim <= (.05/len(pres)) 5 | pres.plot(color='lightgrey', ax=ax) 6 | pres.assign(quads=lmos.q)[is_weird].plot('quads', 7 | legend=True, 8 | k=4, categorical=True, 9 | cmap='bwr_r', ax=ax) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote12.py: -------------------------------------------------------------------------------- 1 | f = plt.figure(figsize=(10,4)) 2 | ax = plt.gca() 3 | ax.set_aspect('equal') 4 | is_weird = lmos.p_sim <= (.05/len(pres)) 5 | pres.assign(quads=lmos.q)[is_weird].plot('quads', 6 | legend=True, 7 | k=4, categorical='True', 8 | cmap='bwr_r', ax=ax) 9 | bounds = ax.axis() 10 | pres.plot(color='lightgrey', ax=ax, zorder=-1) 11 | ax.axis(bounds) -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote13.py: -------------------------------------------------------------------------------- 1 | pres.assign(local_score = lmos.Is, 2 | pval = lmos.p_sim, 3 | quad = lmos.q)\ 4 | .sort_values('local_score')\ 5 | .query('pval < 1e-3 & local_score < 0')[['name','state_name','dem_2008','dem_2016', 6 | 'local_score','pval', 'quad']] -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote14.py: -------------------------------------------------------------------------------- 1 | np.random.seed(21) 2 | lmos16 = esda.moran.Moran_Local(pres.swing_2016, w, 3 | permutations=70000) #min for a bonf. bound 4 | (lmos16.p_sim <= (.05/len(pres))).sum() 5 | pres.assign(local_score = lmos16.Is, 6 | pval = lmos16.p_sim, 7 | quad = lmos16.q)\ 8 | .sort_values('local_score')\ 9 | .query('pval < 1e-3 & local_score < 0')[['name','state_name','dem_2008','dem_2016', 10 | 'local_score','pval', 'quad']] -------------------------------------------------------------------------------- /_solved/solutions/case-trump-vote15.py: -------------------------------------------------------------------------------- 1 | #% load _solved/solutions/case-trump-vote14.py 2 | sns.regplot(pres.gini_2015, 3 | pres.swing_full) -------------------------------------------------------------------------------- /case-conflict-mapping.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib inline\n", 10 | "\n", 11 | "import pandas as pd\n", 12 | "import geopandas\n", 13 | "import matplotlib.pyplot as plt" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "# Case study - Conflict mapping: mining sites in eastern DR Congo\n", 21 | "\n", 22 | "In this case study, we will explore a dataset on artisanal mining sites located in eastern DR Congo.\n", 23 | "\n", 24 | "**Note**: this tutorial is meant as a hands-on session, and most code examples are provided as exercises to be filled in. I highly recommend actually trying to do this yourself, but if you want to follow the solved tutorial, you can find this in the `_solved` directory.\n", 25 | "\n", 26 | "---\n", 27 | "\n", 28 | "#### Background\n", 29 | "\n", 30 | "[IPIS](http://ipisresearch.be/), the International Peace Information Service, manages a database on mining site visits in eastern DR Congo: http://ipisresearch.be/home/conflict-mapping/maps/open-data/\n", 31 | "\n", 32 | "Since 2009, IPIS has visited artisanal mining sites in the region during various data collection campaigns. As part of these campaigns, surveyor teams visit mining sites in the field, meet with miners and complete predefined questionnaires. These contain questions about the mining site, the minerals mined at the site and the armed groups possibly present at the site.\n", 33 | "\n", 34 | "Some additional links:\n", 35 | "\n", 36 | "* Tutorial on the same data using R from IPIS (but without geospatial aspect): http://ipisresearch.be/home/conflict-mapping/maps/open-data/open-data-tutorial/\n", 37 | "* Interactive web app using the same data: http://www.ipisresearch.be/mapping/webmapping/drcongo/v5/" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "## 1. Importing and exploring the data" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "### The mining site visit data\n", 52 | "\n", 53 | "IPIS provides a WFS server to access the data. We can send a query to this server to download the data, and load the result into a geopandas GeoDataFrame:" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "import requests\n", 63 | "import json\n", 64 | "\n", 65 | "wfs_url = \"http://geo.ipisresearch.be/geoserver/public/ows\"\n", 66 | "params = dict(service='WFS', version='1.0.0', request='GetFeature',\n", 67 | " typeName='public:cod_mines_curated_all_opendata_p_ipis', outputFormat='json')\n", 68 | "\n", 69 | "r = requests.get(wfs_url, params=params)\n", 70 | "data_features = json.loads(r.content.decode('UTF-8'))\n", 71 | "data_visits = geopandas.GeoDataFrame.from_features(data_features)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "However, the data is also provided in the tutorial materials as a GeoJSON file, so it is certainly available during the tutorial." 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "
\n", 86 | " EXERCISE:\n", 87 | "
    \n", 88 | "
  • Read the GeoJSON file `data/cod_mines_curated_all_opendata_p_ipis.geojson` using geopandas, and call the result `data_visits`.
  • \n", 89 | "
  • Inspect the first 5 rows, and check the number of observations
  • \n", 90 | "
\n", 91 | "\n", 92 | "
" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "clear_cell": true 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "# %load _solved/solutions/case-conflict-mapping3.py" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": { 110 | "clear_cell": true 111 | }, 112 | "outputs": [], 113 | "source": [ 114 | "# %load _solved/solutions/case-conflict-mapping4.py" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": { 121 | "clear_cell": true 122 | }, 123 | "outputs": [], 124 | "source": [ 125 | "# %load _solved/solutions/case-conflict-mapping5.py" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "The provided dataset contains a lot of information, much more than we are going to use in this tutorial. Therefore, we will select a subset of the column:" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "data_visits = data_visits[['vid', 'project', 'visit_date', 'name', 'pcode', 'workers_numb', 'interference', 'armed_group1', 'mineral1', 'geometry']]" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "data_visits.head()" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "Before starting the actual geospatial tutorial, we will use some more advanced pandas queries to construct a subset of the data that we will use further on: " 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "# Take only the data of visits by IPIS\n", 167 | "data_ipis = data_visits[data_visits['project'].str.contains('IPIS') & (data_visits['workers_numb'] > 0)]" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "# For those mining sites that were visited multiple times, take only the last visit\n", 177 | "data_ipis_lastvisit = data_ipis.sort_values('visit_date').groupby('pcode', as_index=False).last()\n", 178 | "data = geopandas.GeoDataFrame(data_ipis_lastvisit, crs=data_visits.crs)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "### Data on protected areas in the same region\n", 186 | "\n", 187 | "Next to the mining site data, we are also going to use a dataset on protected areas (national parks) in Congo. This dataset was downloaded from http://www.wri.org/our-work/project/congo-basin-forests/democratic-republic-congo#project-tabs and included in the tutorial repository: `data/cod_conservation.zip`." 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "
\n", 195 | " EXERCISE:\n", 196 | "
    \n", 197 | "
  • Extract the `data/cod_conservation.zip` archive, and read the shapefile contained in it. Assign the resulting GeoDataFrame to a variable named `protected_areas`.
  • \n", 198 | "
  • Quickly plot the GeoDataFrame.
  • \n", 199 | "
\n", 200 | "
" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": { 207 | "clear_cell": true 208 | }, 209 | "outputs": [], 210 | "source": [ 211 | "# %load _solved/solutions/case-conflict-mapping10.py" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": { 218 | "clear_cell": true 219 | }, 220 | "outputs": [], 221 | "source": [ 222 | "# %load _solved/solutions/case-conflict-mapping11.py" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "### Conversion to a common Coordinate Reference System\n", 230 | "\n", 231 | "We will see that both datasets use a different Coordinate Reference System (CRS). For many operations, however, it is important that we use a consistent CRS, and therefore we will convert both to a commong CRS.\n", 232 | "\n", 233 | "But first, we explore problems we can encounter related to CRSs.\n", 234 | "\n", 235 | "---" 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "[Goma](https://en.wikipedia.org/wiki/Goma) is the capital city of North Kivu province of Congo, close to the border with Rwanda. It's coordinates are 1.66°S 29.22°E.\n", 243 | "\n", 244 | "
\n", 245 | " EXERCISE:\n", 246 | "
    \n", 247 | "
  • Create a single Point object representing the location of Goma. Call this `goma`.
  • \n", 248 | "
  • Calculate the distances of all mines to Goma, and show the 5 smallest distances (mines closest to Goma).
  • \n", 249 | "
\n", 250 | "
" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": null, 256 | "metadata": { 257 | "clear_cell": true 258 | }, 259 | "outputs": [], 260 | "source": [ 261 | "# %load _solved/solutions/case-conflict-mapping12.py" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": { 268 | "clear_cell": true 269 | }, 270 | "outputs": [], 271 | "source": [ 272 | "# %load _solved/solutions/case-conflict-mapping13.py" 273 | ] 274 | }, 275 | { 276 | "cell_type": "code", 277 | "execution_count": null, 278 | "metadata": { 279 | "clear_cell": true 280 | }, 281 | "outputs": [], 282 | "source": [ 283 | "# %load _solved/solutions/case-conflict-mapping14.py" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": null, 289 | "metadata": { 290 | "clear_cell": true 291 | }, 292 | "outputs": [], 293 | "source": [ 294 | "# %load _solved/solutions/case-conflict-mapping15.py" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "The distances we see here in degrees, which is not helpful for interpreting those distances. That is a reason we will convert the data to another coordinate reference system (CRS) for the remainder of this tutorial." 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "metadata": {}, 307 | "source": [ 308 | "
\n", 309 | " EXERCISE:\n", 310 | "
    \n", 311 | "
  • Make a visualization of the national parks and the mining sites on a single plot.
  • \n", 312 | "
\n", 313 | " \n", 314 | "

Check the first section of the [04-more-on-visualization.ipynb](04-more-on-visualization.ipynb) notebook for tips and tricks to plot with GeoPandas.

\n", 315 | "
" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "metadata": { 322 | "clear_cell": true 323 | }, 324 | "outputs": [], 325 | "source": [ 326 | "# %load _solved/solutions/case-conflict-mapping16.py" 327 | ] 328 | }, 329 | { 330 | "cell_type": "markdown", 331 | "metadata": {}, 332 | "source": [ 333 | "You will notice that the protected areas and mining sites do not map to the same area on the plot. This is because the Coordinate Reference Systems (CRS) differ for both datasets. Another reason we will need to convert the CRS!\n", 334 | "\n", 335 | "Let's check the Coordinate Reference System (CRS) for both datasets.\n", 336 | "\n", 337 | "The mining sites data uses the [WGS 84 lat/lon (EPSG 4326)](http://spatialreference.org/ref/epsg/4326/) CRS:" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": null, 343 | "metadata": {}, 344 | "outputs": [], 345 | "source": [ 346 | "data.crs" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "metadata": {}, 352 | "source": [ 353 | "The protected areas dataset, on the other hand, uses a [WGS 84 / World Mercator (EPSG 3395)](http://spatialreference.org/ref/epsg/wgs-84-world-mercator/) projection (with meters as unit):" 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": null, 359 | "metadata": {}, 360 | "outputs": [], 361 | "source": [ 362 | "protected_areas.crs" 363 | ] 364 | }, 365 | { 366 | "cell_type": "markdown", 367 | "metadata": {}, 368 | "source": [ 369 | "We will convert both datasets to a local UTM zone, so we can plot them together and that distance-based calculations give sensible results.\n", 370 | "\n", 371 | "To find the appropriate UTM zone, you can check http://www.dmap.co.uk/utmworld.htm or https://www.latlong.net/lat-long-utm.html, and in this case we will use UTM zone 35, which gives use EPSG 32735: https://epsg.io/32735\n", 372 | "\n", 373 | "
\n", 374 | " EXERCISE:\n", 375 | "
    \n", 376 | "
  • Convert both datasets (`data` and `protected_areas`) to EPSG 32735. Name the results `data_utm` and `protected_areas_utm`.
  • \n", 377 | "
  • Try again to visualize both datasets on a single map.
  • \n", 378 | "
\n", 379 | "\n", 380 | "
" 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": null, 386 | "metadata": { 387 | "clear_cell": true 388 | }, 389 | "outputs": [], 390 | "source": [ 391 | "# %load _solved/solutions/case-conflict-mapping19.py" 392 | ] 393 | }, 394 | { 395 | "cell_type": "code", 396 | "execution_count": null, 397 | "metadata": { 398 | "clear_cell": true 399 | }, 400 | "outputs": [], 401 | "source": [ 402 | "# %load _solved/solutions/case-conflict-mapping20.py" 403 | ] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "metadata": {}, 408 | "source": [ 409 | "### More advanced visualizations\n", 410 | "\n", 411 | "

For the following exercises, check the first section of the [04-more-on-visualization.ipynb](04-more-on-visualization.ipynb) notebook for tips and tricks to plot with GeoPandas.

" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "
\n", 419 | " EXERCISE:\n", 420 | "
    \n", 421 | "
  • Make a visualization of the national parks and the mining sites on a single plot.
  • \n", 422 | "
  • Pay attention to the following details:\n", 423 | "
      \n", 424 | "
    • Make the figure a bit bigger.
    • \n", 425 | "
    • The protected areas should be plotted in green
    • \n", 426 | "
    • For plotting the mining sites, adjust the markersize and use an `alpha=0.5`.
    • \n", 427 | "
    • Remove the figure border and x and y labels (coordinates)
    • \n", 428 | "
    \n", 429 | "
  • \n", 430 | "
\n", 431 | "
" 432 | ] 433 | }, 434 | { 435 | "cell_type": "code", 436 | "execution_count": null, 437 | "metadata": { 438 | "clear_cell": true 439 | }, 440 | "outputs": [], 441 | "source": [ 442 | "# %load _solved/solutions/case-conflict-mapping21.py" 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": null, 448 | "metadata": { 449 | "clear_cell": true 450 | }, 451 | "outputs": [], 452 | "source": [ 453 | "# %load _solved/solutions/case-conflict-mapping22.py" 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "metadata": {}, 459 | "source": [ 460 | "
\n", 461 | " EXERCISE:\n", 462 | " \n", 463 | " In addition to the previous figure:\n", 464 | "
    \n", 465 | "
  • Give the mining sites a distinct color based on the `'interference'` column, indicating whether an armed group is present at the mining site or not.
  • \n", 466 | "
\n", 467 | "
" 468 | ] 469 | }, 470 | { 471 | "cell_type": "code", 472 | "execution_count": null, 473 | "metadata": { 474 | "clear_cell": true 475 | }, 476 | "outputs": [], 477 | "source": [ 478 | "# %load _solved/solutions/case-conflict-mapping23.py" 479 | ] 480 | }, 481 | { 482 | "cell_type": "markdown", 483 | "metadata": {}, 484 | "source": [ 485 | "
\n", 486 | " EXERCISE:\n", 487 | " \n", 488 | " In addition to the previous figure:\n", 489 | "
    \n", 490 | "
  • Give the mining sites a distinct color based on the `'mineral1'` column, indicating which mineral is the primary mined mineral.
  • \n", 491 | "
\n", 492 | "
" 493 | ] 494 | }, 495 | { 496 | "cell_type": "code", 497 | "execution_count": null, 498 | "metadata": { 499 | "clear_cell": true 500 | }, 501 | "outputs": [], 502 | "source": [ 503 | "# %load _solved/solutions/case-conflict-mapping24.py" 504 | ] 505 | }, 506 | { 507 | "cell_type": "markdown", 508 | "metadata": {}, 509 | "source": [ 510 | "## 2. Spatial operations" 511 | ] 512 | }, 513 | { 514 | "cell_type": "markdown", 515 | "metadata": {}, 516 | "source": [ 517 | "
\n", 518 | " EXERCISE:\n", 519 | " \n", 520 | "
    \n", 521 | "
  • Access the geometry of the \"Kahuzi-Biega National park\".
  • \n", 522 | "
  • Filter the mining sites to select those that are located in this national park.
  • \n", 523 | "
\n", 524 | "
" 525 | ] 526 | }, 527 | { 528 | "cell_type": "code", 529 | "execution_count": null, 530 | "metadata": { 531 | "clear_cell": true 532 | }, 533 | "outputs": [], 534 | "source": [ 535 | "# %load _solved/solutions/case-conflict-mapping25.py" 536 | ] 537 | }, 538 | { 539 | "cell_type": "code", 540 | "execution_count": null, 541 | "metadata": { 542 | "clear_cell": true 543 | }, 544 | "outputs": [], 545 | "source": [ 546 | "# %load _solved/solutions/case-conflict-mapping26.py" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "execution_count": null, 552 | "metadata": { 553 | "clear_cell": true 554 | }, 555 | "outputs": [], 556 | "source": [ 557 | "# %load _solved/solutions/case-conflict-mapping27.py" 558 | ] 559 | }, 560 | { 561 | "cell_type": "markdown", 562 | "metadata": {}, 563 | "source": [ 564 | "
\n", 565 | " EXERCISE: Determine for each mining site the \"closest\" protected area:\n", 566 | " \n", 567 | "
    \n", 568 | "
  • PART 1 - do this for a single mining site:\n", 569 | "
      \n", 570 | "
    • Get a single mining site, e.g. the first of the dataset.
    • \n", 571 | "
    • Calculate the distance (in km's) to all protected areas for this mining site
    • \n", 572 | "
    • Get the index of the minimum distance (tip: `idxmin()`) and get the name of the protected are corresponding to this index.
    • \n", 573 | "
    \n", 574 | "
  • \n", 575 | "
  • PART 2 - apply this procedure on each geometry:\n", 576 | "
      \n", 577 | "
    • Write the above procedure as a function that gets a single site and the protected areas dataframe as input and returns the name of the closest protected area as output.
    • \n", 578 | "
    • Apply this function to all sites using the `.apply()` method on `data_utm.geometry`.
    • \n", 579 | "
    \n", 580 | "
  • \n", 581 | "
\n", 582 | "
" 583 | ] 584 | }, 585 | { 586 | "cell_type": "code", 587 | "execution_count": null, 588 | "metadata": { 589 | "clear_cell": true 590 | }, 591 | "outputs": [], 592 | "source": [ 593 | "# %load _solved/solutions/case-conflict-mapping28.py" 594 | ] 595 | }, 596 | { 597 | "cell_type": "code", 598 | "execution_count": null, 599 | "metadata": { 600 | "clear_cell": true 601 | }, 602 | "outputs": [], 603 | "source": [ 604 | "# %load _solved/solutions/case-conflict-mapping29.py" 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": null, 610 | "metadata": { 611 | "clear_cell": true 612 | }, 613 | "outputs": [], 614 | "source": [ 615 | "# %load _solved/solutions/case-conflict-mapping30.py" 616 | ] 617 | }, 618 | { 619 | "cell_type": "code", 620 | "execution_count": null, 621 | "metadata": { 622 | "clear_cell": true 623 | }, 624 | "outputs": [], 625 | "source": [ 626 | "# %load _solved/solutions/case-conflict-mapping31.py" 627 | ] 628 | }, 629 | { 630 | "cell_type": "code", 631 | "execution_count": null, 632 | "metadata": { 633 | "clear_cell": true 634 | }, 635 | "outputs": [], 636 | "source": [ 637 | "# %load _solved/solutions/case-conflict-mapping32.py" 638 | ] 639 | }, 640 | { 641 | "cell_type": "markdown", 642 | "metadata": {}, 643 | "source": [ 644 | "## 3. Using spatial join to determine mining sites in the protected areas\n", 645 | "\n", 646 | "Based on the analysis and visualizations above, we can already see that there are mining sites inside the protected areas. Let's now do an actual spatial join to determine which sites are within the protected areas." 647 | ] 648 | }, 649 | { 650 | "cell_type": "markdown", 651 | "metadata": {}, 652 | "source": [ 653 | "### Mining sites in protected areas\n", 654 | "\n", 655 | "
\n", 656 | " EXERCISE:\n", 657 | "
    \n", 658 | "
  • Add information about the protected areas to the mining sites dataset, using a spatial join:\n", 659 | "
      \n", 660 | "
    • Call the result `data_within_protected`
    • \n", 661 | "
    • If the result is empty, this is an indication that the coordinate reference system is not matching. Make sure to re-project the data (see above).
    • \n", 662 | " \n", 663 | "
    \n", 664 | "
  • \n", 665 | "
  • How many mining sites are located within a national park?
  • \n", 666 | "
  • Count the number of mining sites per national park (pandas tip: check `value_counts()`)
  • \n", 667 | "\n", 668 | "
\n", 669 | "\n", 670 | "
" 671 | ] 672 | }, 673 | { 674 | "cell_type": "code", 675 | "execution_count": null, 676 | "metadata": { 677 | "clear_cell": true 678 | }, 679 | "outputs": [], 680 | "source": [ 681 | "# %load _solved/solutions/case-conflict-mapping33.py" 682 | ] 683 | }, 684 | { 685 | "cell_type": "code", 686 | "execution_count": null, 687 | "metadata": { 688 | "clear_cell": true 689 | }, 690 | "outputs": [], 691 | "source": [ 692 | "# %load _solved/solutions/case-conflict-mapping34.py" 693 | ] 694 | }, 695 | { 696 | "cell_type": "code", 697 | "execution_count": null, 698 | "metadata": { 699 | "clear_cell": true 700 | }, 701 | "outputs": [], 702 | "source": [ 703 | "# %load _solved/solutions/case-conflict-mapping35.py" 704 | ] 705 | }, 706 | { 707 | "cell_type": "code", 708 | "execution_count": null, 709 | "metadata": { 710 | "clear_cell": true 711 | }, 712 | "outputs": [], 713 | "source": [ 714 | "# %load _solved/solutions/case-conflict-mapping36.py" 715 | ] 716 | }, 717 | { 718 | "cell_type": "markdown", 719 | "metadata": {}, 720 | "source": [ 721 | "### Mining sites in the borders of protected areas\n", 722 | "\n", 723 | "And what about the borders of the protected areas? (just outside the park)\n", 724 | "\n", 725 | "
\n", 726 | " EXERCISE:\n", 727 | "
    \n", 728 | "
  • Create a new dataset, `protected_areas_borders`, that contains the border area (10 km wide) of each protected area:\n", 729 | "
      \n", 730 | "
    • Tip: one way of doing this is with the `buffer` and `difference` function.
    • \n", 731 | "
    • Plot the resulting borders as a visual check of correctness.
    • \n", 732 | "
    \n", 733 | "
  • \n", 734 | "
  • Count the number of mining sites per national park that are located within its borders
  • \n", 735 | "\n", 736 | "
\n", 737 | "\n", 738 | "
" 739 | ] 740 | }, 741 | { 742 | "cell_type": "code", 743 | "execution_count": null, 744 | "metadata": { 745 | "clear_cell": true 746 | }, 747 | "outputs": [], 748 | "source": [ 749 | "# %load _solved/solutions/case-conflict-mapping37.py" 750 | ] 751 | }, 752 | { 753 | "cell_type": "code", 754 | "execution_count": null, 755 | "metadata": { 756 | "clear_cell": true 757 | }, 758 | "outputs": [], 759 | "source": [ 760 | "# %load _solved/solutions/case-conflict-mapping38.py" 761 | ] 762 | }, 763 | { 764 | "cell_type": "code", 765 | "execution_count": null, 766 | "metadata": { 767 | "clear_cell": true 768 | }, 769 | "outputs": [], 770 | "source": [ 771 | "# %load _solved/solutions/case-conflict-mapping39.py" 772 | ] 773 | }, 774 | { 775 | "cell_type": "code", 776 | "execution_count": null, 777 | "metadata": { 778 | "clear_cell": true 779 | }, 780 | "outputs": [], 781 | "source": [ 782 | "# %load _solved/solutions/case-conflict-mapping40.py" 783 | ] 784 | }, 785 | { 786 | "cell_type": "code", 787 | "execution_count": null, 788 | "metadata": { 789 | "clear_cell": true 790 | }, 791 | "outputs": [], 792 | "source": [ 793 | "# %load _solved/solutions/case-conflict-mapping41.py" 794 | ] 795 | } 796 | ], 797 | "metadata": { 798 | "kernelspec": { 799 | "display_name": "Python 3", 800 | "language": "python", 801 | "name": "python3" 802 | }, 803 | "language_info": { 804 | "codemirror_mode": { 805 | "name": "ipython", 806 | "version": 3 807 | }, 808 | "file_extension": ".py", 809 | "mimetype": "text/x-python", 810 | "name": "python", 811 | "nbconvert_exporter": "python", 812 | "pygments_lexer": "ipython3", 813 | "version": "3.5.5" 814 | } 815 | }, 816 | "nbformat": 4, 817 | "nbformat_minor": 2 818 | } 819 | -------------------------------------------------------------------------------- /case-gini-in-a-bottle-the-trump-vote.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib inline\n", 10 | "\n", 11 | "import pandas as pd\n", 12 | "import geopandas as gpd\n", 13 | "import libpysal as lp\n", 14 | "import esda\n", 15 | "import numpy as np\n", 16 | "import matplotlib.pyplot as plt" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "# Case Study: *Gini in a bottle: Income Inequality and the Trump Vote*" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "#### Read in the table and show the first three rows" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "%load _solved/solutions/case-trump-vote01.py" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "#### Set the CRS and reproject it into a suitable projection for mapping the contiguous US\n", 47 | "\n", 48 | "*hint: the epsg code useful here is 5070, for Albers equal area conic*" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "%load _solved/solutions/case-trump-vote02.py" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "#### Plot each year's vote against each other year's vote\n", 65 | "In this instance, it also helps to include the line ($y=x$) on each plot, so that it is clearer the directions the aggregate votes moved. " 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "%load _solved/solutions/case-trump-vote03.py" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "#### Show the relationship between the dem two-party vote and the Gini coefficient by county." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "% load _solved/solutions/case-trump-vote04.py" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "#### Compute the swings (change in vote from year to year)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "%load _solved/solutions/case-trump-vote05.py" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "Negative swing means the Democrat voteshare in 2016 (what Clinton won) is lower than Democrat voteshare in 2008 (what Obama won).\n", 114 | "So, counties where swing is negative mean that Obama \"outperformed\" Clinton. \n", 115 | "Equivalently, these would be counties where McCain (in 2008) \"beat\" Trump's electoral performance in 2016.\n", 116 | "\n", 117 | "Positive swing in a county means that Clinton (in 2016) outperformed Obama (in 2008), or where Trump (in 2016) did better than McCain (in 2008). \n", 118 | "\n", 119 | "The national average swing was around -9% from 2008 to 2016. Further, swing does not directly record who \"won\" the county, only which direction the county \"moved.\"" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "#### map the swing from 2008 to 2016 alongside the votes in 2008 and 2016:" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "%load _solved/solutions/case-trump-vote06.py" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "#### Build a spatial weights object to model the spatial relationships between US counties" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "%load _solved/solutions/case-trump-vote07.py" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Note that this is just one of many valid solutions. But, all the remaining exercises are predicated on using this weight. If you choose a different weight structure, your results may differ." 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "#### Is swing \"contagious?\" Do nearby counties tend to swing together? " 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "%load _solved/solutions/case-trump-vote08.py" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "#### Visually show the relationship between places' swing and their surrounding swing, like in a scatterplot. " 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "%load _solved/solutions/case-trump-vote09.py" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "metadata": {}, 196 | "source": [ 197 | "#### Are there any outliers or clusters in swing using a Local Moran's $I$?" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "%load _solved/solutions/case-trump-vote10.py" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "#### Where are these outliers or clusters?" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": null, 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "%load _solved/solutions/case-trump-vote11.py" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "#### Can you focus in on the regions which are outliers?" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "%load _solved/solutions/case-trump-vote12.py" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "Group 3 moves surprisingly strongly from Obama to Trump relative to its surroundings, and group 1 moves strongly from Obama to Hilary relative to its surroundings.\n", 246 | "\n", 247 | "Group 4 moves surprisingly away from Trump while its area moves towards Trump. Group 2 moves surprisingly towards Trump while its area moves towards Hilary. " 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "#### Relaxing the significance a bit, where do we see significant spatial outliers?" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "%load _solved/solutions/case-trump-vote13.py" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "mainly in ohio, indiana, and west virginia" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "#### What about when comparing the voting behavior from 2012 to 2016?" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": null, 283 | "metadata": {}, 284 | "outputs": [], 285 | "source": [ 286 | "%load _solved/solutions/case-trump-vote14.py" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "##### What is the relationship between the Gini coefficient and partisan swing?" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "% load _solved/solutions/case-trump-vote15.py" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "metadata": {}, 308 | "source": [ 309 | "Hillary tended to do better than Obama in counties with higher income inequality.\n", 310 | "In contrast, Trump fared better in counties with lower income inequality. \n", 311 | "If you're further interested in the sometimes-counterintuitive relationship between income, voting, & geographic context, check out Gelman's [Red State, Blue State](https://www.amazon.com/Red-State-Blue-Rich-Poor/dp/0691143935). " 312 | ] 313 | } 314 | ], 315 | "metadata": { 316 | "kernelspec": { 317 | "display_name": "Python 3", 318 | "language": "python", 319 | "name": "python3" 320 | }, 321 | "language_info": { 322 | "codemirror_mode": { 323 | "name": "ipython", 324 | "version": 3 325 | }, 326 | "file_extension": ".py", 327 | "mimetype": "text/x-python", 328 | "name": "python", 329 | "nbconvert_exporter": "python", 330 | "pygments_lexer": "ipython3", 331 | "version": "3.6.5" 332 | } 333 | }, 334 | "nbformat": 4, 335 | "nbformat_minor": 2 336 | } 337 | -------------------------------------------------------------------------------- /check_environment.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | packages = ['geopandas', 'sklearn', 'contextily', 'folium', 'mgwr', 'pysal'] 4 | 5 | bad = [] 6 | for package in packages: 7 | try: 8 | importlib.import_module(package) 9 | except ImportError: 10 | bad.append("Can't import %s" % package) 11 | else: 12 | if len(bad) > 0: 13 | print('Your tutorial environment is not yet fully set up:') 14 | print('\n'.join(bad)) 15 | else: 16 | try: 17 | import geopandas 18 | countries = geopandas.read_file("zip://./data/ne_110m_admin_0_countries.zip") 19 | print("All good. Enjoy the tutorial!") 20 | except Exception as e: 21 | print("Couldn't read countries shapefile.") 22 | print(e) 23 | 24 | -------------------------------------------------------------------------------- /data/berlin-listings.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/berlin-listings.csv.gz -------------------------------------------------------------------------------- /data/cod_conservation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/cod_conservation.zip -------------------------------------------------------------------------------- /data/ne_110m_admin_0_countries.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/ne_110m_admin_0_countries.zip -------------------------------------------------------------------------------- /data/ne_110m_populated_places.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/ne_110m_populated_places.zip -------------------------------------------------------------------------------- /data/ne_50m_rivers_lake_centerlines.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/ne_50m_rivers_lake_centerlines.zip -------------------------------------------------------------------------------- /data/uspres.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/data/uspres.zip -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: scipygeo18 2 | channels: 3 | - defaults 4 | - conda-forge 5 | dependencies: 6 | - geopandas 7 | - geoplot 8 | - pysal 9 | - folium 10 | - scikit-learn 11 | - rasterio 12 | - pip 13 | - pip: 14 | - contextily 15 | - libpysal 16 | - mgwr 17 | - mapclassify 18 | - esda 19 | -------------------------------------------------------------------------------- /img/TopologicSpatialRelations2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/img/TopologicSpatialRelations2.png -------------------------------------------------------------------------------- /img/download-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/scipy2018-geospatial-data/57afd9a46a75b281adb0d36b0492142eb7ca29c9/img/download-button.png --------------------------------------------------------------------------------