conference_footprint

compute the CO2 footprint of an academic conference
git clone https://a3nm.net/git/conference_footprint/
Log | Files | Refs

commit 88b3e5f5d6f5e416db6985838acb6d69cdfb4ac9
parent 75ea1d1d52247755eafe6cc791ec2727655a101b
Author: Antoine Amarilli <a3nm@a3nm.net>
Date:   Tue,  2 Sep 2025 16:20:20 +0200

2025

Diffstat:
2025/LICENSE | 18++++++++++++++++++
2025/README.md | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2025/co2.py | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2025/commands.txt | 16++++++++++++++++
2025/generate_trips.py | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2025/geocode.py | 30++++++++++++++++++++++++++++++
2025/trips_anonymized.csv | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 653 insertions(+), 0 deletions(-)

diff --git a/2025/LICENSE b/2025/LICENSE @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/2025/README.md b/2025/README.md @@ -0,0 +1,110 @@ +This file explains how the carbon footprint of Highlights'25 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… +- Other scientific activities during your stay +- I am planning to attend the Highlights Collaborative Research Week (September 6 – September 12, no extra fees) + +We only kept participants with status "paid" and with an onsite ticket, we +arrive at 144 on-site participants. We checked from the email addresses that +there are no duplicate email addresses, and from the names that there are no +duplicate names. + +We note that the transportation mode was not indicated by all participants: out +of 144 participants, 24 did not specify it at all, and 3 more specified it only +partially. + +We eliminated participants who were apparently local, and 130 onsite nonlocal +participants remain. + +We completed the travel destinations of the participants when missing or when +obviously inconsistent, using the location of their affiliation. + +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 origin +- field 3 is the transportation mode +- field 4 is the information about whether the participant is extending their + trip for other scientific reasons +- field 5 is the information about whether the participant is attending HCRW + +(These files are not versioned because they can be considered private +information.) + +We run: + + ./generate_trips.py 49.2569809 7.0420618 0.2 + +Where the arguments are the latitude and longitude of Bordeaux, 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): 29.158425 +for mode plane: CO2e emissions (tons): 26.689530 +for mode train: CO2e emissions (tons): 2.428939 +for mode bus/coach: CO2e emissions (tons): 0.039956 +for distances <2000 km, plane is used for 54/239 trips +for distances >=2000 km, plane is used for 21/21 trips +flights of over 2000 km account for 15.987136 CO2e emissions (tons) i.e. 54.828531 percent of total for 21/260 total legs +distance by plane: 143539 +num by mode plane: 75 +num by mode train: 177 +num by mode bus/coach: 8 +dist by mode plane: 143539 +dist by mode train: 65647 +dist by mode bus/coach: 1427 + +Hence, the total CO2 footprint is 29 tons CO2e (it is the same with the +non-anonymized file). Around 92% of emissions are due to plane travel, and 55% +of the emissions are due to 8% of the transportation legs, namely, +the plane trips of over 2000 km. (All trips of more than 2000 km are done by +plane) + +The average footprint per onsite non-local participant (130) is around +224 kgCO2e. The average footprint per onsite participant (144) is around +202 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 2024 + +The number of onsite participants is a little higher, and the number of nonlocal onsite participants is a little lower. The footprints per nonlocal participant are significantly lower -- perhaps the conference venue is more central? + diff --git a/2025/co2.py b/2025/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/2025/commands.txt b/2025/commands.txt @@ -0,0 +1,16 @@ +# Here are the raw operations that were used to process the data +# Note that the registation file is not versioned here because it contains +# personal information + +- select the right columns in libreoffice +- replace the newlines in libreoffice +- cat high2025_orders_2025_08_29_filter.csv| grep '^paid;' | grep -v "Online only ticket" | cut -d';' -f2,3,4,6- > onsites.csv + - checked that the other statuses than "paid" look bad + - checked that the prices paid make sense + - checked that there are no duplicates +- eliminate local participants, yielding onsites_anon_nolocal.csv +- manually complete locations, yielding onsites_anon_nolocal_completed.csv +- cut -d';' -f7-12 onsites_anon_nolocal_completed.csv > location_mode_extension.csv +- cat location_mode_extension.csv| cut -d';' -f1 > locations.txt +- cat location_mode_extension.csv| cut -d';' -f3 >> locations.txt +- cat locations.txt| sort | uniq > locations_uniq.txt diff --git a/2025/generate_trips.py b/2025/generate_trips.py @@ -0,0 +1,138 @@ +#!/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_with_latlon.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_extension.csv" + +modes = ["train", "plane", "bus/coach", "other", ""] +places = defaultdict(lambda : [0, 0, ""]) + +def complete_mode(dist): + if dist > 400: + 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=";") + 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(',', '') + from_place = r[0].strip() + from_mode = r[1].replace(',', '').lower() + to_place = r[2].strip() + to_mode = r[3].replace(',', '').lower() + if r[4].startswith("Yes"): + extended_1 = 'X' + else: + extended_1 = '' + if r[5].startswith("Yes"): + extended_2 = 'X' + else: + extended_2 = '' + #annotation = ' '.join((first, last, university, typ)) + annotation = '' + 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), + extended_1, extended_2, + from_place.replace(',', ''), + *from_coord, annotation)), file=fout2) + print(','.join(( + to_mode.lower(), + str(to_dist), + extended_1, extended_2, + to_place.replace(',', ''), + *to_coord, annotation)), file=fout2) + print(','.join(( + from_mode.lower(), + str(from_dist_anon), + extended_1, extended_2)), file=fout) + print(','.join(( + to_mode.lower(), + str(to_dist_anon), + extended_1, extended_2)), 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/2025/geocode.py b/2025/geocode.py @@ -0,0 +1,30 @@ +#!/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 is None: + print("Error: no result for %s" % place, file=sys.stderr) + sys.exit(42) + return (location.latitude, location.longitude) + +for l in sys.stdin.readlines(): + l = l.strip() + origin_lat, origin_lng = searchGeonames(l) + + print(origin_lat, origin_lng, l) + + sleep(1) + diff --git a/2025/trips_anonymized.csv b/2025/trips_anonymized.csv @@ -0,0 +1,260 @@ +plane,739,, +plane,888,, +train,496,, +train,617,, +train,1186,, +train,887,, +train,839,X, +train,300,X, +bus/coach,66,, +bus/coach,50,, +train,834,X, +plane,2571,X, +train,278,, +train,274,, +plane,502,, +plane,525,, +train,503,X,X +train,536,X,X +bus/coach,305,, +bus/coach,269,, +train,895,, +train,314,, +train,205,,X +train,232,,X +train,622,, +train,749,, +train,760,, +train,709,, +train,400,, +train,393,, +plane,1099,, +plane,1063,, +plane,1061,X, +plane,808,X, +train,375,, +train,396,, +plane,2511,, +plane,3016,, +train,548,, +train,330,, +train,357,,X +train,394,,X +plane,556,, +plane,551,, +train,256,, +train,303,, +train,974,, +train,965,, +plane,923,X, +plane,1038,X, +plane,7095,X,X +plane,7735,X,X +plane,593,X, +train,48,X, +train,337,, +train,383,, +train,433,, +train,386,, +plane,942,, +plane,997,, +train,225,, +train,219,, +plane,1021,, +plane,939,, +plane,5784,X, +plane,7339,X, +train,325,, +train,287,, +train,1032,, +train,739,, +train,160,, +train,212,, +train,811,, +train,313,, +train,47,, +train,49,, +plane,502,, +plane,391,, +train,333,, +train,408,, +train,774,, +train,681,, +train,47,, +train,3,, +train,812,, +train,781,, +train,1000,, +train,768,, +train,219,, +train,292,, +train,882,,X +train,759,,X +plane,583,, +plane,794,, +train,796,, +train,604,, +train,767,, +train,759,, +train,60,, +train,56,, +train,225,, +train,213,, +plane,817,, +plane,957,, +train,345,, +train,373,, +train,316,, +train,668,, +train,390,, +train,383,, +train,236,, +train,339,, +plane,927,, +train,392,, +train,258,, +train,259,, +train,373,,X +train,324,,X +plane,1053,,X +plane,1201,,X +train,945,, +train,965,, +train,271,, +train,274,, +train,351,, +train,333,, +train,101,X, +train,87,X, +train,558,, +train,339,, +plane,2847,X,X +plane,2371,X,X +train,237,X,X +train,216,X,X +plane,1334,X,X +plane,1115,X,X +train,407,, +train,396,, +train,16,,X +train,12,,X +plane,833,, +plane,1176,, +train,53,X,X +train,67,X,X +plane,2822,, +plane,2414,, +train,677,, +train,257,, +plane,937,X,X +plane,1134,X,X +train,377,, +train,368,, +train,274,,X +train,321,,X +plane,8117,X,X +plane,8316,X,X +train,311,, +train,303,, +plane,2720,, +plane,3313,, +train,887,, +train,1007,, +train,408,, +train,280,, +plane,8288,X,X +plane,7567,X,X +plane,714,, +plane,644,, +train,163,, +train,176,, +train,319,, +train,294,, +train,402,X,X +train,426,X,X +train,67,, +train,66,, +train,210,, +train,282,, +train,97,, +train,71,, +bus/coach,327,, +bus/coach,290,, +plane,3486,, +plane,2729,, +train,53,X, +train,62,X, +train,62,,X +train,59,,X +train,48,, +train,49,, +plane,670,, +plane,803,, +train,55,,X +train,62,,X +train,330,,X +train,292,,X +plane,582,,X +plane,739,,X +train,157,, +train,210,, +plane,989,,X +plane,1027,,X +train,186,, +train,166,, +train,112,, +train,93,, +train,642,X,X +train,650,X,X +plane,469,, +plane,474,, +plane,969,, +plane,868,, +plane,1109,, +plane,908,, +train,312,, +train,369,, +plane,841,, +plane,958,, +plane,1108,, +plane,1245,, +train,105,, +train,88,, +train,681,, +train,752,, +train,350,, +train,283,, +bus/coach,53,, +bus/coach,67,, +plane,2816,X, +plane,3581,X, +train,767,, +train,769,, +train,54,, +train,63,, +plane,807,, +plane,963,, +train,553,, +train,458,, +train,1148,X, +train,218,X, +train,350,X,X +train,162,X,X +train,52,, +train,50,, +train,143,, +train,182,, +plane,617,, +plane,598,, +train,304,, +train,248,, +train,63,, +train,65,, +train,253,, +train,302,, +train,65,, +train,54,, +train,48,, +train,53,, +train,223,, +train,273,,