commit 88b3e5f5d6f5e416db6985838acb6d69cdfb4ac9
parent 75ea1d1d52247755eafe6cc791ec2727655a101b
Author: Antoine Amarilli <a3nm@a3nm.net>
Date:   Tue,  2 Sep 2025 16:20:20 +0200
2025
Diffstat:
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,,