A long while ago the powers that be decided to cut air passenger duty (APD) on long haul flights. Yesterday the Scottish government started a consultation on cutting the APD. So the question is, how many flights are actually effected by this APD cut in Northern Ireland?


Northern Ireland’s Air Passenger Duty

Any flight from Northern Ireland that flies direct to a destination over 2000 miles is classed as long haul and exempt from APD. Changeover routes, for example if you flew from Belfast International to London Heathrow and then legged it to Dubai, well that doesn’t count.

So how to do find out the flights that apply? With some open data and some Clojure.

Airports and Routes

Open flights has CSV files for airports, airlines, routes and potentially schedules. I’m only interested in the first two.

Airports have a name, IATA code and location information.

465,"Belfast Intl","Belfast","United Kingdom","BFS","EGAA",54.6575,-6.215833,268,0,"E","Europe/London"

Routes are a source and destination airport by IATA code and an operating airline.


A Quick Checklist

So, we’ve got a question and we’ve got some open data. Now I need a check list of what needs doing to get to the answer.

  • Load the CSV files.
  • Get the airport info of the departure airport (BFS in our case)
  • Get the matching routes where BFS is the source airport.
  • Calculate the distance between the two lat/lon points of each airport.
  • Check the flight is classed as long haul.
  • Calculate the average.

Loading CSV Files

Always worth knowing how to do in Clojure, especially how to open a CSV file and convert it to a map that has keys. First of all we need to know the header information of the CSV file, as the routes and airports don’t have that information we have to find it out and create our own references.

(def airport-fields [:airportid :name :city :country :iata :icao :lat :lon :alt :timezone :dst :tz])
(def route-fields [:airline :airlineid :source-airport :source-airport-id :dest-airport :dest-airport-id :codeshare :stops :equip])

The actual loading is done with Clojure’s Data CSV library.

;; Things that will load files for us.
(defn load-csv [filename]
 (with-open [in-file (io/reader (.toString (io/resource filename)))]
 (csv/read-csv in-file))))

I need a way of connecting the field names with the data, using zipmap will let me do that so I’m going to create two functions that will handle the airports and routes respectively.

(defn load-airports [filename]
 (->> (load-csv filename)
 (map #(zipmap airport-fields %))))
(defn load-routes [filename]
 (->> (load-csv filename)
 (map #(zipmap route-fields %))))

Finding Specific Airport Information

With an IATA code I can find out information about the airport, which we’ve loaded in already. The get-airport function takes a string as a parameter with the IATA code and returns a map of the airport info.

(defn get-airport [iata-code airports]
 (first (filter #(= iata-code (:iata %)) airports)))

The filter command rattles through each entry and will return you a sequence of maps that match the criteria, so it’s just a case of getting the first entry (there should only be one anyway).

Get The Matching Routes For the Source Airport

Now we’ve got some helper functions to do some of the work we can get to the meat of what needs to happen. Finding the matching routes is a case of filtering all the routes and finding ones that match our source airport.

Assuming the routes cvs file is loaded in we can use the filter function as so:

(filter #(= (:iata dept-airport) (:source-airport %)) routes)

Once I have routes I need to map through each of them with the aim of creating a map with the source, destination, lat/lon for each airport, a distance, the airline and whether the flight is long haul or not. While the function looks complex it’s actually fairly simple.

(defn find-routes [departure-airport]
 (let [airports (load-airports "airports.csv")
 routes (load-routes "routes.csv")
 dept-airport (get-airport departure-airport airports)
 matching-routes (filter #(= (:iata dept-airport) (:source-airport %)) routes)]
 (->> matching-routes
 (map (fn [route] (try
 (let [dest-airport (get-airport (:dest-airport route) airports)
 distance (calc-distance (Double/parseDouble (:lat dept-airport))
 (Double/parseDouble (:lon dept-airport))
 (Double/parseDouble (:lat dest-airport))
 (Double/parseDouble (:lon dest-airport)))
 long-haul (is-long-haul? distance)]
 {:dept (:name dept-airport)
 :dept-iata (:iata dept-airport)
 :dept-lat (:lat dept-airport)
 :dept-lon (:lon dept-airport)
 :dest (:name dest-airport)
 :dest-iata (:iata dest-airport)
 :dest-lat (:lat dest-airport)
 :dest-lon (:lon dest-airport)
 :distance distance
 :long-haul long-haul
 :airline (:airline route)})
 (catch Exception e )))))))

It may be better to look at the formatted code in Github: https://github.com/jasebell/ni-airpassengerduty/blob/master/src/ni_apd/core.clj#L57 

There’s two functions I need to create before we can test this, the distance and if the flight is classed as long haul.

Calculating Distances

With two latitude and longitude points we can calculate the distance in miles. I actually covered this a long while ago when I was first dabbling with Clojure (thrown in at the deep end may be a better description) when I started working with Mastodon C.

(defn- deg2rad [deg]
 (/ (* deg (. Math PI)) 180))
(defn- rad2deg [rad]
 (/ (* rad 180) (. Math PI)))
(defn get-distance [lat1 lon1 lat2 lon2]
 (+ (* (Math/sin (deg2rad lat1)) (Math/sin (deg2rad lat2)))
 (* (Math/cos (deg2rad lat1))
 (Math/cos (deg2rad lat2))
 (Math/cos (deg2rad (- lon1 lon2))))
(defn calc-distance [lat1 lon1 lat2 lon2]
 (int (* 60 1.1515 (rad2deg (Math/acos (get-distance lat1 lon1 lat2 lon2))))))

You can read the original blog post for a better explanation.

Is the flight long haul?

With a distance we can figure this out quite easily. My threshold is 2000 miles so I’m going to wrap that up as a piece of data in Clojure.

(def apd-threshold 2000)

Next it’s case of finding out if the distance is greater than the threshold.

(defn is-long-haul? [distance]
 (> distance apd-threshold))

Calculating the Percentage

With the total number of routes and the total number of routes that are classed as long haul I can work out a percentage. Using a mixture of Clojure’s count function and filter function we can find out with ease.

Total number of long haul flights / Total number of flights * 100

My Clojure function looks like this:

(defn get-longhaul-percentage [departure-airport]
 (let [routes (find-routes departure-airport)]
 (double (* 100 (/ (count (filter #(= true (:long-haul %)) routes)) (count routes))))))

I’m passing in the source airport IATA code and using the find-routes function created earlier to get a list of the all the matching routes from the source airport. The first count is filtered on whether the :long-haul value is true, the second count is the total number of routes in the list.

That’s the checklist complete. Now it’s time to see what percentage of the routes for an airport are actually classed as long haul.

Testing With the REPL

First of all I’m going to test the find-routes function, that will give you an idea of the data structure as it looks before the percentage is calculated.

ni-apd.core> (find-routes "LDY")
;; => ({:airline "FR", :dest-iata "BHX", :long-haul false, :dest-lon "-1.748028", :dest-lat "52.453856", :distance 284, :dest "Birmingham", :dept-lat "55.042778", :dept "City of Derry", :dept-lon "-7.161111", :dept-iata "LDY"}
 {:airline "FR", :dest-iata "FAO", :long-haul false, :dest-lon "-7.965911", :dest-lat "37.014425", :distance 1246, :dest "Faro", :dept-lat "55.042778", :dept "City of Derry", :dept-lon "-7.161111", :dept-iata "LDY"}
 {:airline "FR", :dest-iata "LPL", :long-haul false, :dest-lon "-2.849722", :dest-lat "53.333611", :distance 210, :dest "Liverpool", :dept-lat "55.042778", :dept "City of Derry", :dept-lon "-7.161111", :dept-iata "LDY"}
 {:airline "FR", :dest-iata "PIK", :long-haul false, :dest-lon "-4.586667", :dest-lat "55.509444", :distance 106, :dest "Prestwick", :dept-lat "55.042778", :dept "City of Derry", :dept-lon "-7.161111", :dept-iata "LDY"}
 {:airline "FR", :dest-iata "STN", :long-haul false, :dest-lon "0.235", :dest-lat "51.885", :distance 374, :dest "Stansted", :dept-lat "55.042778",;; => :dept "City of Derry", :dept-lon "-7.161111", :dept-iata "LDY"})

A quick test with City Of Derry Airport (LDY) shows us the map and it’s keys. So that’s working and no long haul flights there, no surprise either.

Looking at Belfast International we get a lot more data.

ni-apd.core> (find-routes "BFS")
;; => ({:airline "LH", :dest-iata "EWR", :long-haul true, :dest-lon "-74.168667", :dest-lat "40.6925", :distance 3168, :dest "Newark Liberty Intl", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "ACE", :long-haul false, :dest-lon "-13.605225", :dest-lat "28.945464", :distance 1814, :dest "Lanzarote", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "AGP", :long-haul false, :dest-lon "-4.499106", :dest-lat "36.6749", :distance 1245, :dest "Malaga", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "ALC", :long-haul false, :dest-lon "-0.558156", :dest-lat "38.282169", :distance 1162, :dest "Alicante", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "DBV", :long-haul false, :dest-lon "18.268244", :dest-lat "42.561353", :distance 1384, :dest "Dubrovnik", :dept-lat "54.6;; => 575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "FAO", :long-haul false, :dest-lon "-7.965911", :dest-lat "37.014425", :distance 1221, :dest "Faro", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "MJV", :long-haul false, :dest-lon "-0.812389", :dest-lat "37.774972", :distance 1193, :dest "Murcia San Javier", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "PMI", :long-haul false, :dest-lon "2.727778", :dest-lat "39.55361", :distance 1122, :dest "Son Sant Joan", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "REU", :long-haul false, :dest-lon "1.167172", :dest-lat "41.147392", :distance 992, :dest "Reus", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "LS", :dest-iata "TFS", :long-haul false, :dest-lon "-16.572489", :dest-lat;; => "28.044475", :distance 1910, :dest "Tenerife Sur", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "TCX", :dest-iata "DLM", :long-haul true, :dest-lon "28.7925", :dest-lat "36.713056", :distance 2061, :dest "Dalaman", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "TCX", :dest-iata "LCA", :long-haul true, :dest-lon "33.62485", :dest-lat "34.875117", :distance 2336, :dest "Larnaca", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "TCX",
 :dest-iata "NBE",
 :long-haul false,
 :dest-lon "10.438611",
 :dest-lat "36.075833",
 :distance 1508,
 :dest "Enfidha - Zine El Abidine Ben Ali International Airport",
 :dept-lat "54.6575",
 :dept "Belfast Intl",
 :dept-lon "-6.215833",
 :dept-iata "BFS"}
 {:airline "TCX", :dest-iata "PMI", :long-haul false, :dest-lon "2.727778", :dest-lat "39.55361", :distance 1122, :dest "Son Sant Joan", :dept-lat "54.6575", :dept "Belfast I;; => ntl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "TCX", :dest-iata "TFS", :long-haul false, :dest-lon "-16.572489", :dest-lat "28.044475", :distance 1910, :dest "Tenerife Sur", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "AGP", :long-haul false, :dest-lon "-4.499106", :dest-lat "36.6749", :distance 1245, :dest "Malaga", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "ALC", :long-haul false, :dest-lon "-0.558156", :dest-lat "38.282169", :distance 1162, :dest "Alicante", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "AMS", :long-haul false, :dest-lon "4.763889", :dest-lat "52.308613", :distance 479, :dest "Schiphol", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "BCN", :long-haul false, :dest-lon "2.078464", :dest-lat "41.297078", :distance 997;; => , :dest "Barcelona", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "BHX", :long-haul false, :dest-lon "-1.748028", :dest-lat "52.453856", :distance 238, :dest "Birmingham", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "BRS", :long-haul false, :dest-lon "-2.719089", :dest-lat "51.382669", :distance 268, :dest "Bristol", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "CDG", :long-haul false, :dest-lon "2.55", :dest-lat "49.012779", :distance 539, :dest "Charles De Gaulle", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "EDI", :long-haul false, :dest-lon "-3.3725", :dest-lat "55.95", :distance 143, :dest "Edinburgh", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "FAO", :long-haul false, :dest-lon;; => "-7.965911", :dest-lat "37.014425", :distance 1221, :dest "Faro", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "GLA", :long-haul false, :dest-lon "-4.433056", :dest-lat "55.871944", :distance 109, :dest "Glasgow", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "KRK", :long-haul false, :dest-lon "19.784836", :dest-lat "50.077731", :distance 1134, :dest "Balice", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "LGW", :long-haul false, :dest-lon "-0.190278", :dest-lat "51.148056", :distance 348, :dest "Gatwick", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "LPL", :long-haul false, :dest-lon "-2.849722", :dest-lat "53.333611", :distance 164, :dest "Liverpool", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline ;; => "U2", :dest-iata "LTN", :long-haul false, :dest-lon "-0.368333", :dest-lat "51.874722", :distance 308, :dest "Luton", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "MAN", :long-haul false, :dest-lon "-2.27495", :dest-lat "53.353744", :distance 183, :dest "Manchester", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "MLA", :long-haul false, :dest-lon "14.4775", :dest-lat "35.857497", :distance 1630, :dest "Luqa", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "NCE", :long-haul false, :dest-lon "7.215872", :dest-lat "43.658411", :distance 969, :dest "Cote D\\\\'Azur", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "NCL", :long-haul false, :dest-lon "-1.691667", :dest-lat "55.0375", :distance 181, :dest "Newcastle", :dept-lat "54.6575", :dept "Belfast Intl";; => , :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "PMI", :long-haul false, :dest-lon "2.727778", :dest-lat "39.55361", :distance 1122, :dest "Son Sant Joan", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "U2", :dest-iata "STN", :long-haul false, :dest-lon "0.235", :dest-lat "51.885", :distance 328, :dest "Stansted", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"}
 {:airline "UA", :dest-iata "EWR", :long-haul true, :dest-lon "-74.168667", :dest-lat "40.6925", :distance 3168, :dest "Newark Liberty Intl", :dept-lat "54.6575", :dept "Belfast Intl", :dept-lon "-6.215833", :dept-iata "BFS"})

Now to turn our attention the final percentage, we know the find-routes is working (if you look at the long haul key on the last entry for BFS it’s true and the distance is 3168 miles.

So what’s the percentage of long haul flights from Belfast International?

ni-apd.core> (get-longhaul-percentage "BFS")
;; => 11.11111111111111

There we are, 11%. So 89% of flights from BFS are still liable for APD. Back to the consultation in Scotland, I wonder what Edinburgh looks like.

ni-apd.core> (get-longhaul-percentage "EDI")
;; => 5.882352941176471

Less than Belfast it turns out at 5.88%.

Imaging scrapping long haul APD from Heathrow?

ni-apd.core> (get-longhaul-percentage "LHR")
;; => 59.58254269449715

No me neither, not at 59%….


Openflights Data: http://openflights.org/data.html

Github Repository for this blog: https://github.com/jasebell/ni-airpassengerduty