commit 6580ad1171d5caa4394e01fc1f555fe86d23de1b
parent 29e5f70295bb3e79236f4bc029fabf6145306675
Author: Antoine Amarilli <a3nm@a3nm.net>
Date: Fri, 25 Sep 2026 18:01:20 +0200
2026
Diffstat:
7 files changed, 599 insertions(+), 0 deletions(-)
diff --git a/2026/README.md b/2026/README.md
@@ -0,0 +1,99 @@
+This file explains how the carbon footprint of Highlights'26 was computed.
+
+## Data collection
+
+We collected information about the travel plans of participants via the
+registration form, containing the following fields:
+
+- Arriving from…
+- Arriving by…
+- Leaving to...
+- Leaving by…
+
+Unfortunately, this year, the form was only completed with these fields after
+participants had registered. So participants were asked during the conference to complete the extra fields, but not everyone did so. The missing fields were extrapolated as explained below.
+
+There are 155 participants, of which 37 are local and 118 are nonlocal.
+
+For those participants that did not provide complete travel information (36 of
+them), I assumed that they were travelling to and from the institution to which
+they were affiliated. Further, based on Highlights'25 data, I assumed that trips
+of <=600km were done by train, and trips of >600km were done by plane. I checked
+that the computation was not too sensitive to this choice.
+
+We prepared a file with the following tab-separated fields:
+- field 0 is the origin
+- field 1 is the transportation mode
+- field 2 is the destination
+- field 3 is the transportation mode
+
+(These files are not versioned because they can be considered private
+information.)
+
+We run:
+
+ ./generate_trips.py 48.198964 16.369916 0.2
+
+Where the arguments are the latitude and longitude of the conference venue, and
+0.2 is the noise to add. This generates a file trips_anonymized.csv containing,
+for each trip leg, the mode ("plane", "train", "bus/coach"), the distance (in
+km, rounded, with noise), and the information about extended stays. A file
+trips.csv is also produced for debugging (with the data without noise and with
+personal information). A file map.geojson is also produced with the map of
+participants and transportation modes and private information (to be used as an
+image only).
+
+The file trips_anonymized.csv can then be fed to co2.py which computes the
+carbon footprint (see below). This gives (from the anonymized data):
+
+total CO2e emissions (tons): 33.435452
+for mode train: CO2e emissions (tons): 2.391939
+for mode plane: CO2e emissions (tons): 30.958393
+for mode bus/coach: CO2e emissions (tons): 0.085120
+for distances <2000 km, plane is used for 107/222 trips
+for distances >=2000 km, plane is used for 14/14 trips
+flights of over 2000 km account for 7.821959 CO2e emissions (tons) i.e. 23.394207 percent of total for 14/236 total legs
+distance by plane: 152714
+num by mode train: 108
+num by mode plane: 121
+num by mode bus/coach: 7
+dist by mode train: 64647
+dist by mode plane: 152714
+dist by mode bus/coach: 3040
+
+Hence, the total CO2 footprint is 33 tons CO2e (it is the same with the
+non-anonymized file). Around 93% of emissions are due to plane travel.
+
+The average footprint per onsite non-local participant (118) is around
+283 kgCO2e. The average footprint per onsite participant (155) is around
+216 kgCO2e. (These figures are computed from the anonymized data.)
+
+### Carbon footprint (unchanged from 2024)
+
+Like in 2022, we compute the CO2 fotprint following the
+[labos1point5](https://labos1point5.org/ges-1point5) data, which is adapted from
+the French agency [Ademe](https://www.ademe.fr/). We use the values from 2022
+without updating them to ensure that the methodology is comparable.
+
+- For train, we count **37 gCO2e/pkm** (international train). This is pessimistic in France, very
+ pessimistic for TGV, but similar to the 41 gCO2e/pm for national (UK) rail
+ given by [Our World in
+ Data](https://ourworldindata.org/travel-carbon-footprint).
+- Plane is counted following
+ [labos1point5](https://labos1point5.org/ges-1point5), including the effect
+ of contrails:
+ - 258 gCO2e/pkm for less than 1000km
+ - 187 gCO2e/pkm between 1001km and 3500km
+ - 152 gCO2e/pkm above 3500km. This value is consistent to the 150 gCO2e/pkm
+ value for long-haul flight given by [Our World in
+ Data](https://ourworldindata.org/travel-carbon-footprint) (also including
+ contrails)
+- For bus/coach, we count 28 gCO2e/pkm as the coach value given by [Our World in
+ Data](https://ourworldindata.org/travel-carbon-footprint) as there is no
+ value in labos1point5.
+
+## Trends relative to 2025
+
+The footprint is slightly more than the one estimated in 2025 (29 tCO2e), but in
+the low range of the footprints since 2022 (27-42 tCO2e).
+
diff --git a/2026/co2.py b/2026/co2.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+
+# From a list of trip legs (mode, distance_in_km), compute the total CO2
+# footprint and statistics
+
+import sys
+from collections import defaultdict
+
+LONG_THRESH = 2000
+
+def co2(distance, mode):
+ if mode == "train":
+ g_km_person = 37
+ if mode == "bus/coach":
+ g_km_person = 28
+ if mode == "plane":
+ if distance <= 1000:
+ g_km_person = 258
+ elif 1000< distance <= 3500:
+ g_km_person = 187
+ elif 3500< distance:
+ g_km_person = 152
+ return distance * g_km_person
+
+co2_by_mode = defaultdict(lambda: 0)
+co2_total = 0
+co2_total_long_plane = 0
+num_total_long_plane = 0
+num_total_long_nonplane = 0
+num_total = 0
+num_short_plane = 0
+num_short = 0
+dist_plane = 0
+km_by_mode = defaultdict(lambda: 0)
+num_by_mode = defaultdict(lambda: 0)
+
+with open("footprints.txt", 'w') as fp:
+ for l in sys.stdin.readlines():
+ f = l.strip().split(",")
+ mode = f[0]
+ dist = float(f[1])
+ co2v = co2(dist, mode)
+ co2_by_mode[mode] += co2v
+ print(co2v, file=fp)
+ co2_total += co2v
+ num_total += 1
+ num_by_mode[mode] += 1
+ km_by_mode[mode] += dist
+ if mode == "plane":
+ dist_plane += dist
+ if dist >= LONG_THRESH:
+ if mode == "plane":
+ co2_total_long_plane += co2v
+ num_total_long_plane += 1
+ else:
+ num_total_long_nonplane += 1
+ if dist < LONG_THRESH:
+ num_short += 1
+ if mode == "plane":
+ num_short_plane += 1
+
+assert (num_short + num_total_long_plane + num_total_long_nonplane == num_total)
+
+print("total CO2e emissions (tons): %f" % (co2_total/1000000))
+for m in co2_by_mode.keys():
+ print("for mode %s: CO2e emissions (tons): %f" % (m, co2_by_mode[m]/1000000))
+
+print ("for distances <%d km, plane is used for %d/%d trips" %
+ (LONG_THRESH, num_short_plane, num_short))
+print ("for distances >=%d km, plane is used for %d/%d trips" %
+ (LONG_THRESH, num_total_long_plane, num_total_long_plane+num_total_long_nonplane))
+
+print( "flights of over %d km account for %f CO2e emissions (tons) i.e. %f percent of total for %d/%d total legs"
+ % (LONG_THRESH, co2_total_long_plane/1000000, 100*co2_total_long_plane/co2_total,
+ num_total_long_plane, num_total))
+print("distance by plane: %d" % dist_plane)
+
+for k in num_by_mode.keys():
+ print("num by mode %s: %d" % (k, num_by_mode[k]))
+for k in km_by_mode.keys():
+ print("dist by mode %s: %d" % (k, km_by_mode[k]))
diff --git a/2026/commands.txt b/2026/commands.txt
@@ -0,0 +1,7 @@
+sed 1d Highlights26_full_table_2026-09-12_remove_newlines.csv | cut -d ';' -f1,5,7-11,13,17-20,23,24 > registration_data.csv
+./compute_places.py < registration_data.csv | sort | uniq > locations.txt
+./geocode.py < locations.txt > locations_with_latlon.txt
+cat <(cat locations_with_latlon.txt | grep -vi oxford) locations_with_latlon_additions.txt > locations_all.txt
+./complete_participants.py < registration_data.csv > location_mode.csv
+./generate_trips.py 48.198964 16.369916 0.2
+./co2.py < trips_anonymized.csv
diff --git a/2026/generate_trips.py b/2026/generate_trips.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+
+# Process registration data to generate the list of trip legs
+
+import csv
+import sys
+import json
+from geopy.distance import geodesic
+from collections import defaultdict
+from random import uniform
+
+# place of the conference
+origin = (sys.argv[1], sys.argv[2])
+noise = float(sys.argv[3]) # how much multiplicative noise to add to distances
+
+# read locations
+location = {}
+with open("locations_all.txt", 'r') as floc:
+ for l in floc.readlines():
+ f = l.strip().split(' ')
+ lat = f[0]
+ lon = f[1]
+ loc = ' '.join(f[2:])
+ location[loc] = (lat, lon)
+
+FNAME = "location_mode.csv"
+
+modes = ["train", "plane", "bus/coach", "other", ""]
+places = defaultdict(lambda : [0, 0, ""])
+
+def complete_mode(dist):
+ # from highlights 2025 data, from 600km onwards most trips are by plane
+ if dist > 600:
+ return "plane"
+ else:
+ return "train"
+
+# compute trips
+with open("trips_anonymized.csv", 'w') as fout:
+ with open("trips.csv", 'w') as fout2:
+ with open(FNAME, 'r') as ftrip:
+ reader = csv.reader(ftrip, delimiter="\t")
+ for r in reader:
+ #university = r[5]
+ # first = r[2].replace(',', '')
+ # last = r[3].replace(',', '')
+ # assert(r[6] == "I'm coming to Bordeaux")
+ # assert(r[8] == "External Participant")
+ # typ = r[7].replace(',', '')
+ if (len(r) < 4):
+ print (r)
+ assert(False)
+ from_place = r[0].strip()
+ from_mode = r[1].replace(',', '').lower()
+ to_place = r[2].strip()
+ to_mode = r[3].replace(',', '').lower()
+ #annotation = ' '.join((first, last, university, typ))
+ annotation = ''
+ if from_mode == "bus":
+ from_mode = "bus/coach"
+ if to_mode == "bus":
+ to_mode = "bus/coach"
+ if not (from_mode in modes):
+ print (from_mode)
+ assert(False)
+ assert (from_mode in modes)
+ assert (to_mode in modes)
+ from_coord = location[from_place]
+ to_coord = location[to_place]
+
+ from_dist = geodesic(origin, from_coord).kilometers
+ to_dist = geodesic(origin, to_coord).kilometers
+ from_dist_anon = round(uniform(from_dist * (1-noise), from_dist * (1+noise)))
+ to_dist_anon = round(uniform(to_dist * (1-noise), to_dist * (1+noise)))
+ if from_mode == '' or from_mode == 'other':
+ from_mode = complete_mode(from_dist)
+ if to_mode == '' or to_mode == 'other':
+ to_mode = complete_mode(to_dist)
+
+ places[from_coord][1] += 1
+ places[to_coord][1] += 1
+ places[from_coord][2] += annotation + "\n"
+ places[to_coord][2] += annotation + "\n"
+ if from_mode == "plane":
+ places[from_coord][0] += 1
+ if to_mode == "plane":
+ places[to_coord][0] += 1
+
+
+ print(','.join((
+ from_mode.lower(),
+ str(from_dist),
+ from_place.replace(',', ''),
+ *from_coord, annotation)), file=fout2)
+ print(','.join((
+ to_mode.lower(),
+ str(to_dist),
+ to_place.replace(',', ''),
+ *to_coord, annotation)), file=fout2)
+ print(','.join((
+ from_mode.lower(),
+ str(from_dist_anon),
+ )), file=fout)
+ print(','.join((
+ to_mode.lower(),
+ str(to_dist_anon),
+ )), file=fout)
+
+ ## OUTPUT GEOJSON
+
+ features = []
+ for k in places.keys():
+ red = int(255.*places[k][0]/places[k][1])
+ green = 0
+ blue = int(255.*(places[k][1]-places[k][0])/places[k][1])
+ color = '#%02X%02X%02X' % (red, green, blue)
+ feature = {
+ "type": "Feature",
+ "properties": {
+ "name":places[k][2],
+ "_umap_options": {"color": color}
+ },
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ k[1], k[0]
+ ]
+ }
+ }
+ features.append(feature)
+
+output = {
+ "type": "FeatureCollection",
+ "features": features
+}
+
+with open("map.geojson", 'w') as f:
+ print (json.dumps(output), file=f)
+
diff --git a/2026/geocode.py b/2026/geocode.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+# read human-readable locations on stdin, produce same list with added GPS
+# coordinates on STDOUT
+
+from geopy.geocoders import GeoNames
+import os
+import sys
+from time import sleep
+
+USER="a3nm" # user on geonames.org, serves as API key
+geolocator = GeoNames(username=USER)
+
+def searchGeonames(place):
+ # chatgpt
+ global geolocator
+ location = geolocator.geocode(place, exactly_one=True)
+ if (location == None):
+ print("ERROR: %s" % place, file=sys.stderr)
+ return None, None
+ else:
+ return (location.latitude, location.longitude)
+
+for l in sys.stdin.readlines():
+ l = l.strip()
+ origin_lat, origin_lng = searchGeonames(l)
+ if origin_lat is not None and origin_lng is not None:
+ print(origin_lat, origin_lng, l)
+
+ sleep(1)
+
diff --git a/2026/locations_with_latlon_additions.txt b/2026/locations_with_latlon_additions.txt
@@ -0,0 +1,6 @@
+50.878703 4.700090 Leuven, Belguim
+51.50853 -0.12574 London (through Salzburg, by train)
+32.761502 35.019765 Yokneam Ilit, Israel
+51.7535 -1.2590 Oxford, United Kingdom
+31.2499 34.7865 Israel
+53.41058 -2.97794 Liverpool (CONCUR)
diff --git a/2026/trips_anonymized.csv b/2026/trips_anonymized.csv
@@ -0,0 +1,236 @@
+train,902
+train,608
+train,501
+train,551
+train,658
+train,726
+train,612
+train,543
+train,718
+train,688
+train,894
+train,872
+plane,1123
+plane,1069
+plane,729
+plane,1200
+train,295
+train,118
+plane,690
+plane,745
+plane,1478
+train,1004
+train,439
+train,407
+plane,2467
+plane,2610
+train,610
+train,459
+bus/coach,348
+bus/coach,308
+plane,898
+plane,928
+plane,935
+plane,840
+plane,1018
+plane,700
+plane,1204
+plane,1190
+train,419
+train,398
+train,895
+train,779
+plane,563
+plane,618
+train,647
+train,210
+plane,1099
+plane,1073
+train,552
+plane,1107
+train,630
+train,493
+plane,1714
+plane,1605
+train,528
+train,483
+bus/coach,841
+plane,1015
+plane,532
+plane,586
+train,439
+train,531
+plane,1195
+plane,841
+plane,581
+plane,469
+train,589
+train,748
+train,631
+train,515
+plane,571
+plane,654
+plane,1119
+plane,806
+train,640
+train,703
+plane,741
+plane,862
+train,521
+train,539
+plane,2681
+plane,2455
+train,580
+train,589
+train,101
+train,94
+train,538
+train,568
+plane,590
+plane,692
+plane,701
+train,863
+train,571
+train,519
+plane,1101
+plane,1184
+plane,690
+plane,536
+plane,1585
+plane,1551
+plane,518
+plane,540
+train,662
+train,664
+plane,1172
+plane,1210
+plane,518
+plane,620
+plane,1327
+plane,1457
+train,453
+train,655
+train,567
+train,294
+train,507
+train,438
+train,1003
+train,1003
+plane,632
+plane,613
+plane,2704
+plane,2229
+plane,2767
+plane,2653
+train,915
+train,884
+plane,1196
+plane,891
+train,121
+train,116
+plane,924
+plane,991
+train,532
+plane,1307
+plane,664
+plane,593
+plane,1459
+plane,1688
+plane,825
+plane,737
+train,629
+train,466
+train,426
+train,483
+train,1095
+train,898
+plane,744
+plane,947
+train,534
+train,445
+train,629
+plane,1869
+train,1212
+train,1205
+plane,1117
+plane,1509
+plane,906
+plane,844
+plane,1172
+plane,1192
+bus/coach,621
+bus/coach,682
+plane,2636
+plane,1942
+train,120
+train,124
+train,560
+train,658
+train,703
+train,826
+plane,1372
+train,853
+plane,1460
+train,671
+train,568
+train,624
+plane,1277
+plane,1117
+plane,1279
+plane,1213
+train,396
+train,344
+plane,658
+plane,681
+plane,1889
+plane,2254
+plane,495
+plane,868
+train,99
+train,132
+plane,1050
+plane,862
+plane,1237
+plane,1095
+plane,972
+plane,942
+plane,1267
+plane,1172
+plane,1602
+plane,1485
+train,1102
+plane,873
+plane,885
+plane,1144
+plane,2609
+plane,2436
+bus/coach,125
+train,112
+train,347
+train,288
+train,575
+train,691
+train,643
+train,637
+plane,538
+train,503
+plane,1152
+plane,1163
+plane,1165
+plane,967
+plane,1206
+plane,983
+train,546
+train,664
+bus/coach,115
+train,1018
+plane,956
+train,823
+train,882
+train,839
+train,1318
+train,1516
+plane,7716
+plane,6220
+train,291
+train,420