opensky

OpenSky flight trajectories

Flight path information for commercial flights is available for some regions of the USA and Europe from the crowd-sourced OpenSky Network. OpenSky collects data from a large number of users monitoring public air-traffic control information. Here we will use a subset of the data that was polled from their REST API at an interval of 1 minute over 4 days (September 5-13, 2016), using the scripts shown at the end of this notebook. In general the terms of use for OpenSky data do not allow redistribution, but we have obtained specific permission for distributing this subset of the data, which is a 200MB Parquet file (1.1GB as the original database). If you want more or different data, you can run the scripts at the end of this notebook to collect some yourself, or else you can contact OpenSky asking for a copy of the dataset.

NOTE: This dataset is also explorable through the Datashader example dashboard. From inside the examples directory, run: DS_DATASET=opensky panel serve --show dashboard.ipynb

We'll only use some of the fields provided by OpenSky, out of: icao24, callsign, origin, time_position, time_velocity, longitude, latitude, altitude, on_ground, velocity, heading, vertical_rate, sensors, timestamp

Here, we'll load the data and declare that some fields are categorical (which isn't information fully expressed in the Parquet file):

In [1]:
%%time
import pandas as pd

flightpaths = pd.read_parquet('../data/opensky.parq')
flightpaths['origin']    = flightpaths.origin.astype('category')
flightpaths['ascending'] = flightpaths.ascending.astype('category')
flightpaths.tail()
CPU times: user 2.73 s, sys: 1.2 s, total: 3.93 s
Wall time: 2.4 s

The default database has about 10 million points, with some metadata for each.

Now let's define a datashader-based processing pipeline to render images:

In [2]:
import datashader as ds
import datashader.transfer_functions as tf
from colorcet import fire
from matplotlib.colors import rgb2hex
from matplotlib.cm import get_cmap

import numpy as np
from cartopy import crs

plot_width  = 850
plot_height = 600
x_range = (-2.0e6, 2.5e6)
y_range = (4.1e6, 7.8e6)

def categorical_color_key(ncats,cmap):
    """Generate a color key from the given colormap with the requested number of colors"""
    mapper = get_cmap(cmap)
    return [str(rgb2hex(mapper(i))) for i in np.linspace(0, 1, ncats)]

def create_image(x_range=x_range, y_range=y_range, w=plot_width, h=plot_height,
                 aggregator=ds.count(), categorical=None, black=False, cmap="blue"):
    opts={}
    if categorical and cmap:
        opts['color_key'] = categorical_color_key(len(flightpaths[aggregator.column].unique()),cmap)

    cvs = ds.Canvas(plot_width=w, plot_height=h, x_range=x_range, y_range=y_range)
    agg = cvs.line(flightpaths, 'longitude', 'latitude',  aggregator)
    img = tf.shade(agg, cmap=cmap, **opts)

    if black: img = tf.set_background(img, 'black')
    return img

We can use this function to get a dump of all of the trajectory information:

In [3]:
%%time
create_image(aggregator=ds.count(), cmap=fire, black=True)
CPU times: user 1.77 s, sys: 92.2 ms, total: 1.86 s
Wall time: 1.86 s
Out[3]:

This plot shows all of the trajectories in this database, overlaid in a way that avoids overplotting. With this "fire" color map, a single trajectory shows up as black, while increasing levels of overlap show up as brighter colors.

A static image on its own like this is difficult to interpret, but if we overlay it on a map we can see where these flights originate, and can zoom in to see detail in specific regions:

In [4]:
from datashader.bokeh_ext import InteractiveImage
from bokeh.plotting import figure, output_notebook
from bokeh.models.tiles import WMTSTileSource

output_notebook()

def base_plot(tools='pan,wheel_zoom,reset',plot_width=plot_width, plot_height=plot_height,**plot_args):
    p = figure(tools=tools, plot_width=plot_width, plot_height=plot_height,
        x_range=x_range, y_range=y_range, outline_line_color=None,
        min_border=0, min_border_left=0, min_border_right=0,
        min_border_top=0, min_border_bottom=0, **plot_args)

    p.axis.visible = False
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    return p

ArcGIS=WMTSTileSource(url='http://server.arcgisonline.com/ArcGIS/rest/services/'
                      'World_Street_Map/MapServer/tile/{Z}/{Y}/{X}.png')
Loading BokehJS ...
In [5]:
p = base_plot()
p.add_tile(ArcGIS)
InteractiveImage(p, create_image, aggregator=ds.count())
Out[5]: