{ "cells": [ { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Bird migration analysis example\n", "\n", "\n", "\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/movingpandas/movingpandas-examples/main?filepath=2-analysis-examples/bird-migration.ipynb)\n", "[![IPYNB](https://img.shields.io/badge/view-ipynb-hotpink)](https://github.com/movingpandas/movingpandas-examples/blob/main/2-analysis-examples/bird-migration.ipynb)\n", "[![HTML](https://img.shields.io/badge/view-html-green)](https://movingpandas.github.io/movingpandas-website/2-analysis-examples/bird-migration.html)\n", "\n", "This tutorial uses data published on Movebank, specifically: [Navigation experiments in lesser black-backed gulls (data from Wikelski et al. 2015)-gps.csv](https://www.datarepository.movebank.org/handle/10255/move.494)\n", "\n", "This tutorial covers: \n", "1. Trajectory data preprocessing\n", " * Loading movement data from common geospatial file formats\n", " * Exploring spatial & non-spatial data distributions\n", " * Converting GeoDataFrames into Trajectories describing continuous tracks of moving objects\n", "1. Trajectory data analysis\n", " * Investigating individual trajectories\n", " * Comparing different years\n", " * Investigating trajectories of multiple individuals" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import geopandas as gpd\n", "import movingpandas as mpd\n", "import shapely as shp\n", "import hvplot.pandas \n", "import matplotlib.pyplot as plt\n", "\n", "from geopandas import GeoDataFrame, read_file\n", "from shapely.geometry import Point, LineString, Polygon\n", "from datetime import datetime, timedelta\n", "from holoviews import opts, dim\n", "from os.path import exists\n", "from urllib.request import urlretrieve\n", "\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "plot_defaults = {'linewidth':5, 'capstyle':'round', 'figsize':(9,3), 'legend':True}\n", "opts.defaults(opts.Overlay(active_tools=['wheel_zoom'], frame_width=300, frame_height=500))\n", "hvplot_defaults = {'tiles':None, 'cmap':'Viridis', 'colorbar':True}\n", "\n", "mpd.show_versions()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Loading the bird movement data \n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "df = read_file('../data/gulls.gpkg')\n", "print(f\"Finished reading {len(df)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is what the data looks like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see how many individuals we have in the dataset:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['individual-local-identifier'].unique()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The records per individual are not evenly distributed:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['individual-local-identifier'].value_counts().plot(kind='bar', figsize=(17,3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, let's create trajectories:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tc = mpd.TrajectoryCollection(df, 'individual-local-identifier', t='timestamp', min_length=100) \n", "tc" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And let's generalize them to speed up the following analyses:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tc = mpd.MinTimeDeltaGeneralizer(tc).generalize(tolerance=timedelta(days=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Investigating individual trajectories" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's pick out a specific individual. For example, '91916A' is the individual with most records in our dataset:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "filtered = tc.filter('individual-local-identifier', '91916A')\n", "my_traj = filtered.trajectories[0].copy()\n", "my_traj.df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_traj.hvplot(title=f'Movement of {my_traj.id}', line_width=2) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This individual has been travelling back and forth for quite a few years!\n", "\n", "One way to take a closer look at this individual's travels is to split the overall track into yearly trips:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "trips_by_year = mpd.TemporalSplitter(filtered).split(mode='year')\n", "trips_by_year.to_traj_gdf()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can explore individual years:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "one_year = trips_by_year.get_trajectory('91916A_2010-12-31 00:00:00')\n", "one_year" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "one_year.add_speed(units=(\"km\", \"h\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "one_year.hvplot(title=f'Movement speed of {one_year.id}', \n", " line_width=5.0, c='speed', cmap='RdYlGn', colorbar=True, clim=(0,20)) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see where this individual was on a specific day:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_location_at_timestamp(traj, t, fig_size=250):\n", " loc = GeoDataFrame([traj.get_row_at(t)])\n", " return (loc.hvplot(title=str(t), geo=True, tiles='OSM', size=200, color='red') * \n", " traj.hvplot(line_width=1.0, color='black', tiles=False, width=fig_size, height=fig_size))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "( plot_location_at_timestamp(one_year, datetime(2010,9,1)) + \n", " plot_location_at_timestamp(one_year, datetime(2010,10,1)) +\n", " plot_location_at_timestamp(one_year, datetime(2010,11,1)) )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, it might also be of interest to see the different locations on a certain day each year:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_location_at_day_of_year(traj, month, day, ax=None):\n", " ts = [datetime(year, month, day) for year in traj.df.index.year.unique()]\n", " return plot_locations_at_timestamps(traj, ts, ax=ax)\n", "\n", "def plot_locations_at_timestamps(traj, ts, ax=None): \n", " loc = GeoDataFrame([traj.get_row_at(t) for t in ts])\n", " loc['date_label'] = loc.index.strftime('%Y-%m-%d')\n", " return (loc.hvplot(title=f'Movement of {traj.id}', c='date_label', size=200, geo=True, tiles='OSM') *\n", " traj.hvplot(line_width=1.0, color='black', geo=True, tiles=False) )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_location_at_day_of_year(my_traj, month=10, day=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's pretty clear that this individual does not follow the same schedule and route every year. However, it seems to always be heading to the same area Red Sea coast to spend the winter there.\n", "\n", "Let's find its arrival times in this area:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "area_of_interest = Polygon([(30, 25), (50, 25), (50, 15), (30, 15), (30, 25)])\n", "plotted_area_of_interest = GeoDataFrame(pd.DataFrame([{'geometry': area_of_interest, 'id': 1}]), crs=4326).hvplot(geo=True, color='yellow', alpha=0.5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "arrivals = [traj for traj in my_traj.clip(area_of_interest)]\n", "print(f\"Found {len(arrivals)} arrivals\")\n", "\n", "for traj in arrivals:\n", " print(f\"Individual '{traj.df['individual-local-identifier'].iloc[0]}' arrived at {traj.get_start_time()}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "( plot_locations_at_timestamps(my_traj, [traj.get_start_time() for traj in arrivals]) * plotted_area_of_interest )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Investigating trajectories of multiple individuals\n", "\n", "Multiple individuals travel to this area every year. Let's have a closer look:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "year_of_interest = 2010\n", "trajs_in_aoi = tc.clip(area_of_interest)\n", "relevant = [ traj for traj in trajs_in_aoi if traj.get_start_time().year <= year_of_interest and traj.get_end_time().year >= year_of_interest]\n", "print(\"Found {} arrivals\".format(len(relevant)))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for traj in relevant:\n", " print(\"Individual '{}' arrived at {} (duration: {})\".format(\n", " traj.df['individual-local-identifier'].iloc[0], traj.get_start_time().date(), \n", " traj.get_end_time()-traj.get_start_time()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Based on the duration of the individuals' trajectory segments within our area of interest, it looks like some individuals spend the winter here while others only pass through.\n", "\n", "For example, Individual '91761A' passed through twice? What has it been up to?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_traj = tc.get_trajectory('91761A')\n", "segment = my_traj.get_segment_between(datetime(year_of_interest,1,1), datetime(year_of_interest,12,31))\n", "\n", "segment.hvplot(color='black', line_width=1.0) * plotted_area_of_interest " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Turns out that this individual does not stay at the Red Sea but continues its journey into Africa." ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Continue exploring MovingPandas\n", "\n", "1. [Bird migration analysis](bird-migration.ipynb)\n", "1. [Ship data analysis](ship-data.ipynb)\n", "1. [Horse collar data exploration](horse-collar.ipynb)\n", "1. [OSM traces](osm-traces.ipynb)\n", "1. [Soccer game](soccer-game.ipynb)\n", "1. [Mars rover & heli](mars-rover.ipynb)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" } }, "nbformat": 4, "nbformat_minor": 4 }