Integrating GPS Telemetry Data with Open-Source GIS for Wildlife Tracking and Home Range Analysis

Integrating GPS Telemetry Data with Open-Source GIS for Wildlife Tracking and Home Range Analysis

GPS telemetry data visualization in QGIS for wildlife tracking

Modern wildlife conservation and spatial ecology rely heavily on high-resolution GPS satellite telemetry. Traditional VHF radio-tracking methods—while foundational—have largely been superseded by collars equipped with GPS, Iridium, Argos, or cellular data transmission. These advanced devices log thousands of precise coordinate locations per animal over multi-year deployments, providing unprecedented insight into animal movement, habitat selection, migration corridors, and human-wildlife conflict zones.
However, raw telemetry feeds generated by collar hardware are rarely ready for immediate ecological analysis. They arrive as massive tabular CSV files laden with spatial noise, temporal irregularities, and duplicate coordinate fixes. Processing this data requires a robust geospatial workflow capable of cleaning telemetry streams, constructing movement trajectories, and calculating home range estimators.
In this applied guide, we explore a production-grade workflow using open-source GIS tools (QGIS, Python, and GeoPandas) to transform raw GPS tracking data into actionable home range models and utilization distributions.

The Role of Telemetry and Spatial Ecology in Conservation

Understanding how wildlife utilizes geographic space is critical for designing effective protected areas and wildlife corridors. The concept of a home range—defined by Burt (1943) as the area traversed by an individual in its normal activities of food gathering, mating, and caring for young—serves as the primary spatial unit in terrestrial ecology.
When conducting animal movement analysis in QGIS, researchers transition from simple static point plotting to dynamic spatial modeling. By combining open-source GIS utilities with scripting languages like Python and R, conservation scientists can automate ingestion pipelines, filter GPS error anomalies, and compute standard home range metrics such as Minimum Convex Polygons (MCP) and Kernel Density Estimation (KDE).

Step 1: Importing Raw CSV Telemetry Data and Constructing Trajectory Lines

GPS collars typically export time-series logs containing device IDs, timestamps, latitudes, longitudes, elevation values, and dilution of precision (DOP) metrics. Our first objective is to import these tabular records into a spatial environment and reconstruct continuous movement trajectories.

Importing GPS Telemetry Data in QGIS

  1. Open QGIS and navigate to Layer > Add Layer > Add Delimited Text Layer.
  2. Select your CSV file containing telemetry logs.
  3. Define the X field as longitude and the Y field as latitude.
  4. Set the Coordinate Reference System (CRS) to geographic WGS 84 (EPSG:4326).
  5. Click Add to load the point layer into the map canvas.

Constructing Trajectory Lines in Python

To analyze movement between consecutive fixes, point features must be sorted chronologically by timestamp and converted into ordered LineString vector trajectories. We accomplish this using Python and geopandas:

Python

import pandas as pd
import geopandas as geospatial
from shapely.geometry import LineString, Point

# Load raw telemetry CSV
df = pd.read_csv("wildlife_telemetry_raw.csv")

# Convert timestamp column to datetime and sort chronologically per animal ID
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values(by=['animal_id', 'timestamp'])

# Convert DataFrame to GeoDataFrame in WGS84
gdf_points = geospatial.GeoDataFrame(
    df, geometry=geospatial.points_from_xy(df['longitude'], df['latitude']), crs="EPSG:4326"
)

# Re-project to local metric CRS (e.g., UTM Zone 37S - EPSG:32737) for accurate distance calculations
gdf_projected = gdf_points.to_crs("EPSG:32737")

# Group points by individual animal to build trajectory linestrings
trajectories = []
for animal_id, group in gdf_projected.groupby('animal_id'):
    if len(group) > 1:
        coords = [Point(geom.x, geom.y) for geom in group.geometry]
        line = LineString(coords)
        trajectories.append({
            'animal_id': animal_id,
            'fix_count': len(group),
            'geometry': line
        })

gdf_trajectories = geospatial.GeoDataFrame(trajectories, crs="EPSG:32737")
print(f"Constructed movement trajectories for {len(gdf_trajectories)} individual animals.")

Step 2: Cleaning Outlier GPS Points (Speed and Fix-Rate Filters)

GPS telemetry data trajectory cleaning and outlier filtering

 

Raw GPS fixes inevitably contain erroneous locations caused by atmospheric interference, satellite geometry degradation, or physical obstruction (e.g., dense forest canopies or steep gorges). These errors manifest as unrealistic spatial jumps, where an animal appears to teleport across impossible distances between consecutive pings.

Implementing Speed and PDOP Filters

We clean the dataset by enforcing two primary validation criteria:
  1. Geometric Dilution of Precision (PDOP): Removing fixes where satellite configuration geometry yields high positional uncertainty (typically PDOP > 5.0).
  2. Physiological Speed Thresholds: Calculating instantaneous movement speed between consecutive fixes based on elapsed time and distance. If the required speed exceeds the maximum biological movement capacity of the species (e.g., > 30 km/h for large ungulates), the point is flagged as an outlier.

Python

import numpy as np

def clean_telemetry_data(group_df, max_speed_kmh=25.0, max_pdop=5.0):
    """
    Filters GPS telemetry points based on PDOP and biologically impossible movement speeds.
    """
    # Filter by PDOP if present in columns
    if 'pdop' in group_df.columns:
        group_df = group_df[group_df['pdop'] <= max_pdop]
    
    group_df = group_df.sort_values('timestamp').reset_index(drop=True)
    valid_indices = [0]  # Keep first fix
    
    for i in range(1, len(group_df)):
        p1 = group_df.loc[i-1, 'geometry']
        p2 = group_df.loc[i, 'geometry']
        t1 = group_df.loc[i-1, 'timestamp']
        t2 = group_df.loc[i, 'timestamp']
        
        # Distance in meters
        dist_m = p1.distance(p2)
        # Time difference in hours
        time_diff_hrs = (t2 - t1).total_seconds() / 3600.0
        
        if time_diff_hrs > 0:
            speed_kmh = (dist_m / 1000.0) / time_diff_hrs
            if speed_kmh <= max_speed_kmh:
                valid_indices.append(i)
        else:
            # Duplicate timestamps
            continue
            
    return group_df.loc[valid_indices]

# Apply cleaning filter per animal
cleaned_dfs = []
for animal_id, group in gdf_projected.groupby('animal_id'):
    cleaned_group = clean_telemetry_data(group)
    cleaned_dfs.append(cleaned_group)

gdf_cleaned = geospatial.concat(cleaned_dfs, ignore_index=True)
print(f"Retained {len(gdf_cleaned)} valid telemetry fixes out of {len(gdf_points)} total raw points.")

Step 3: Computing Minimum Convex Polygons (MCP) for Core Habitat Estimation

The Minimum Convex Polygon (MCP) is the oldest and most widely cited home range estimator in wildlife ecology. It constructs the smallest convex boundary that encloses all observed telemetry locations for an individual animal.

Executing MCP in QGIS

  1. Open the Processing Toolbox in QGIS (Ctrl + Alt + T).
  2. Search for and select the Minimum bounding geometry tool under the Vector geometry group.
  3. Set your cleaned GPS point layer as the Input layer.
  4. Set the Geometry type to Convex Hull.
  5. Under Field, select your animal_id attribute to generate individual MCP boundaries per animal.
  6. Run the tool.

Limitations of MCP

While MCP provides a standardized macro-scale metric for comparing range sizes across different species or management zones, it suffers from significant ecological drawbacks:
  • Inclusion of Unused Habitat: Because it connects outermost perimeter points, MCP often encompasses large swaths of terrain (such as deep water bodies or impassable cliffs) that the animal never actually visited.
  • Sample Size Sensitivity: MCP area scales monotonically with sample size. As collar deployment duration increases and more fixes are recorded, the polygon continuously expands to capture outlier exploratory movements.

Step 4: Generating Probabilistic Home Ranges Using Kernel Density Estimation (KDE) and Utilization Distributions

GPS telemetry data Kernel Density Estimation home range map

To overcome the geometric limitations of MCP, spatial ecologists rely on probabilistic home range estimators known as Utilization Distributions (UD). The most prevalent technique is Kernel Density Estimation (KDE), which calculates a continuous probability surface representing the relative frequency with which an animal occurs across space.

The Mechanics of KDE

KDE places a bivariate probability density function (kernel)—typically a 2D Gaussian bell curve—over each GPS fix location. The individual kernels are summed across a regular spatial grid and normalized to form a continuous surface where the volume under the surface integrates to 1.
  • 95% Home Range Contour: Represents the outer boundary enclosing 95% of the animal’s utilization probability volume, capturing its general home range.
  • 50% Core Area Contour: Encloses the inner 50% probability volume, highlighting core resting, foraging, or breeding hotspots.

Selecting the Bandwidth Parameter ($h$)

The smoothness of a KDE surface is governed by the smoothing parameter, or bandwidth ($h$). Selecting an inappropriate bandwidth severely distorts home range estimates:
  • Undersmoothing: Creates fragmented, isolated islands around individual GPS points.
  • Oversmoothing: Flattens the probability surface into a generalized blob that obscures localized habitat utilization.
Advanced practitioners utilize Reference Bandwidth ($h_{ref}$) or Least Squares Cross-Validation (LSCV) to determine optimal smoothing parameters.

Executing KDE in Python via SciPy

Using our cleaned metric GeoDataFrame, we can compute 2D Kernel Density Estimation surfaces and export the resulting probabilistic grid into georeferenced raster layers, leveraging efficient raster optimization strategies:

Python

from scipy.stats import gaussian_kde
import rasterio
from rasterio.transform import from_origin

def generate_kde_raster(animal_subset_gdf, output_filename, cell_size=30.0, bandwidth_factor=1.2):
    """
    Computes 2D Gaussian KDE for an individual animal and exports to GeoTIFF.
    """
    coords = np.array([[geom.x, geom.y] for geom in animal_subset_gdf.geometry])
    x = coords[:, 0]
    y = coords[:, 1]
    
    # Initialize Gaussian KDE
    kde = gaussian_kde(coords.T)
    
    # Apply bandwidth adjustment factor
    kde.set_bandwidth(kde.factor * bandwidth_factor)
    
    # Define bounding grid with cell_size resolution (e.g., 30m pixels)
    buffer = 1000.0  # 1km buffer around points
    xmin, ymin, xmax, ymax = x.min() - buffer, y.min() - buffer, x.max() + buffer, y.max() + buffer
    
    x_grid = np.arange(xmin, xmax, cell_size)
    y_grid = np.arange(ymin, ymax, cell_size)
    xx, yy = np.meshgrid(x_grid, y_grid)
    grid_coords = np.vstack([xx.ravel(), yy.ravel()])
    
    # Evaluate density across grid
    z = kde(grid_coords).reshape(xx.shape)
    
    # Normalize density values so integral equals 1
    z_sum = np.sum(z) * (cell_size ** 2)
    z_normalized = z / z_sum
    
    # Export to GeoTIFF using Rasterio
    transform = from_origin(xmin, ymax, cell_size, cell_size)
    raster_data = z_normalized.astype(np.float32)
    
    with rasterio.open(
        output_filename,
        'w',
        driver='GTiff',
        height=raster_data.shape[0],
        width=raster_data.shape[1],
        count=1,
        dtype=raster_data.dtype,
        crs="EPSG:32737",
        transform=transform,
    ) as dst:
        dst.write(raster_data, 1)
        
    print(f"Exported Utilization Distribution raster to {output_filename}")

# Run KDE generation for a specific animal
sample_animal = gdf_cleaned[gdf_cleaned['animal_id'] == 'ELEPHANT_042']
generate_kde_raster(sample_animal, "elephant_042_ud.tif")
For advanced multi-individual habitat analyses, these continuous utilization rasters can be paired with vector point layers to evaluate resource selection functions (RSF) or integrated directly into broader spatial point pattern analysis workflows to test for spatial clustering against environmental covariates.

Summary

Integrating GPS telemetry data with open-source GIS tools provides conservation scientists and wildlife managers with a powerful, flexible framework for monitoring animal movement and spatial ecology. By moving from raw tabular CSV logs to cleaned metric trajectories, filtering GPS outlier errors, computing traditional Minimum Convex Polygons, and generating probabilistic Kernel Density Estimation utilization distributions, researchers can accurately map core habitats, identify critical migration bottlenecks, and formulate data-driven wildlife conservation strategies.