Migration of Barn Swallows

Visualizing the migration of Barn Swallows across the globe
Visualization
Vector
GeoPandas
Python
Author

Colin Hill

Published

December 22, 2024

Barn Swallow

Barn Swallow

Barn Swallows are the most widespread species of swallow in the world, occurring on all continents. They prefer habitats in the open countryside with low vegetation such as pasture, meadows and farmland, with nearby water. The majority of barn swallows breed across the Northern Hemisphere in the summer, migrating to southern Africa and South America for the winter. In this post we’ll visualize the migration of Barn Swallows across the globe using Python, GeoPandas and MovingPandas, with data sourced from Movebank.

import pandas as pd
import geopandas as gpd
import movingpandas as mpd

The first step is to download the relevant datasets from Movebank. We’ll use data from two studies to cover parts of Europe/Africa and North/South America, namely:

The data can be downloaded as CSV files, which we’ll use below

study_1 = pd.read_csv('Barn swallows breeding in Kraghede.csv')
study_2 = pd.read_csv('CLSW_Nebraska_2022.csv')

def preprocess(df):
    # Drop missing coordinates
    df.dropna(subset=['location-lat', 'location-long'], inplace=True)
    
    # Convert timstamp to datetime
    df['timestamp'] = pd.to_datetime(df['timestamp'])

    # Convert to GeoDataFrame
    gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df['location-long'], df['location-lat']), crs='EPSG:4326')

    return gdf

study_1 = preprocess(study_1)
study_2 = preprocess(study_2)

# Add migration direction
study_1.loc[study_1['timestamp'].dt.year.eq(2016), 'd'] = 'southward'
study_1.loc[study_1['timestamp'].dt.year.eq(2017), 'd'] = 'northward'
study_2.loc[study_2['timestamp'].dt.year.eq(2021), 'd'] = 'southward'
study_2.loc[study_2['timestamp'].dt.year.eq(2022), 'd'] = 'northward'
gdf = pd.concat([study_1, study_2])

Individual birds are identified by the ‘individual-local-identifier’ column. The dataframes have timestamps for each observation, with point coordinates in the ‘location-lat’ and ‘location-long’ columns. Let’s quickly check how many observations we have for each individual, per year

gdf.groupby(['study-name', 'individual-local-identifier', 'd']).size()
study-name                          individual-local-identifier  d        
Barn swallows breeding in Kraghede  9AD3712                      northward    232
                                                                 southward    220
                                    9AK1947                      northward    257
                                                                 southward    185
                                    9AK1973                      northward    310
                                                                 southward    219
                                    9AP0465                      northward    230
                                                                 southward    191
CLSW_Nebraska_2022                  2401-22580                   northward    251
                                                                 southward    310
dtype: int64

To plot the migration paths, we’ll need to create LineStrings from the point coordinates, which we can easily do with MovingPandas. We’ll split the data into two separate dataframes, one for the southward migration and one for the northward migration

gdf_southward = gdf.loc[gdf['d'].eq('southward')]
gdf_northward = gdf.loc[gdf['d'].eq('northward')]

Now we can generate the trajectories

southward_traj = mpd.TrajectoryCollection(gdf_southward, 'individual-local-identifier', t='timestamp')
northward_traj = mpd.TrajectoryCollection(gdf_northward, 'individual-local-identifier', t='timestamp')

With the trajectories, we can see the time taken and distance travelled by the birds in each direction

# The study in which individual birds belong
individual_study_map = gdf.groupby('individual-local-identifier')['study-name'].first().to_dict()

total = []
for direction, traj,  in zip(['southward', 'northward'], [southward_traj, northward_traj]):
    _gdf = traj.to_traj_gdf()
    _gdf['direction'] = direction
    _gdf['duration'] = _gdf['end_t'] - _gdf['start_t']
    _gdf['km'] = _gdf['length'] / 1_000
    _gdf['km/day'] = _gdf['km'] / _gdf['duration'].dt.days
    total.append(_gdf)
total = pd.concat(total)
total['study'] = total['individual-local-identifier'].map(individual_study_map)
total[['duration', 'km', 'km/day']].max(axis=0)
duration    158 days 22:49:55
km               23144.292571
km/day             178.636679
dtype: object

The barn swallows in these studies flew up to ~23,100 km in ~158 days, an average of ~179 km/day

total.groupby(['study', 'direction'])[['duration', 'km', 'km/day']].mean().round(1)
duration km km/day
study direction
Barn swallows breeding in Kraghede northward 137 days 00:45:49 14688.1 106.6
southward 108 days 21:29:45.750000 10282.7 94.9
CLSW_Nebraska_2022 northward 125 days 03:06:10 22329.6 178.6
southward 154 days 08:41:58 18345.9 119.1

On average, the birds flying from Northern Europe to Africa took less time to migrate South, but longer to migrate North compared to the one example from North America to South America. The average distance covered was also much lower for the Europe/Africa migration.

We can visualize these trajectories using the MovingPandas TrajectoryCollection objects. On the map the starting points are shown as triangles, where the solid lines represent the southward migration and the dashed lines represent the northward migration

params = dict(line_width=3, geo=True, tiles="EsriImagery", width=760, height=600)
southward_traj.hvplot(**params) * northward_traj.hvplot(**params, line_dash='dashed')