Énoncé (résumé)
Le 2 février 2026, Gaëlle, CEO d’AlpAnalytics, déclare à la presse que l’entreprise ne sera jamais vendue. On dit pourtant qu’elle a rencontré des acheteurs en Suisse centrale quelques semaines plus tôt. On nous fournit les données ouvertes de l’entreprise (version v1, téléchargée en février et retirée en mars), un extrait du registre du commerce et le site web http://alpanalytics.ctf.blackalps.ch. Il faut trouver le premier voyage de Gaëlle en Suisse centrale et donner son ticket ID.
Fichiers
register_extract.txt: AlpAnalytics SA, Avenue des Remparts 31, Yverdon-les-Bains. « Gaëlle P., of Lucens, in Grandson: founder, chair and CEO » ; Lorris G. habite aussi à Grandson.places.csv: gares, parkings et remontées mécaniques, avec leur code postal (NPA).v1/people.parquet: 14 347 utilisateurs (pid,birth_year,home_npa,ev).v1/events.parquet: 2 168 018 événements (pid,ts,place_id,mode,ticket_id,tap,charge_min), du 01.11.2025 au 06.02.2026.
Démarche OSINT
-
Registre : Gaëlle habite à Grandson, code postal 1422. Dans
people.parquet, 152 utilisateurs ont ce code postal. -
Données seules, insuffisant : une vingtaine d’habitants de 1422 vont en Suisse centrale (cantons LU, ZG, SZ, OW, NW, UR : Zoug, Lucerne, Rigi, Pilatus, Titlis…). Une dizaine font des allers-retours en semaine vers Zoug. Ce sont des leurres : il faut d’autres critères.
-
Site web, critères discriminants :
team.html: « Gaëlle P. founded AlpAnalytics in May 2019, at 34 ». Elle est donc née en 1984 ou 1985.blog/2024-05-21-going-electric.html: elle a acheté une voiture électrique (ev = true). « I drive to the office every working day and plug in each morning at the Yverdon station car park ». Ce sont des événementsev_chargequasi quotidiens àP-YVE-GARE.blog/2025-04-08-charger-wars.html: depuis novembre, les bornes sont parfois toutes prises, d’où quelques jours en simpleparking. Lorris G. recharge lui aussi à ces bornes.press/2026-04-30-acquisition.html: l’acheteur, BlackData Switzerland GmbH, est installé à Zoug.blog/2026-03-09-withdrawing-v1.htmlet2026-03-16-v2-released.html: la v1 a été retirée pour une faille de vie privée. Elle contenait l’année de naissance exacte et le code postal complet, une combinaison quasi identifiante. La v2 est k-anonyme : tranches d’âge, deux premiers chiffres du code postal, zones, heure tronquée. Elle ne couvre que la période du 16.01 au 28.02. C’est le piège « off the record » : seule la v1 permet de réidentifier Gaëlle et de voir ses voyages de décembre.
-
Filtre : code postal 1422, naissance en 1984 ou 1985, voiture électrique. Il reste 4
pid:pid naiss. recharges à P-YVE-GARE stationnements sans recharge à P-YVE-GARE remarque 471c06bd 1984 51 10 va au bureau en voiture et recharge presque chaque jour → Gaëlle a0a3b4db 1985 9 49 se gare surtout sans recharger 189d2230 1984 13 0 pas de trajet quotidien (44 événements) f2f223eb 1984 0 0 travaille à Berne À noter : 728d18ae recharge aussi chaque jour, mais il est né en 1979 (probablement Lorris).
-
Voyages de 471c06bd : trois allers-retours en train Yverdon → Zoug avant le 2 février :
- 04.12.2025 : ticket
QD8SM8KA(in YVE 07:35, out ZUG 09:49, in ZUG 18:51, out YVE 21:05) - 18.12.2025 :
YKQBQATM - 22.01.2026 :
HMQDQ4VU
Le premier voyage en Suisse centrale porte le ticket
QD8SM8KA. - 04.12.2025 : ticket
Reproduction
cd .
uv venv .venv && uv pip install --python .venv/bin/python pandas pyarrow
unzip alpanalytics-1.zip -d files
.venv/bin/python -I gaelle.py files/v1 # filtre 1422 / 1984-85 / VE + habitudes de recharge
.venv/bin/python -I candidates.py files/v1 # voyages en Suisse centrale des habitants de 1422
Requête minimale (pandas) :
e = pd.read_parquet('files/v1/events.parquet'); p = pd.read_parquet('files/v1/people.parquet')
c = p[(p.home_npa == 1422) & p.birth_year.isin([1984, 1985]) & p.ev]
# -> garder le pid avec ev_charge quotidien à P-YVE-GARE : 471c06bd
e[(e.pid == '471c06bd') & (e['mode'] == 'train')]
Flag
QD8SM8KA
Code
import pandas as pd, sys
e=pd.read_parquet(sys.argv[1]+'/events.parquet'); p=pd.read_parquet(sys.argv[1]+'/people.parquet')
c=p[(p.home_npa==1422)&p.birth_year.isin([1984,1985])&p.ev]
print(c.to_string())
bd=pd.bdate_range('2025-11-03','2026-02-06')
for pid in c.pid:
d=e[e.pid==pid]; days=set(d.ts.dt.normalize())
ch=d[(d.place_id=='P-YVE-GARE')]
print(pid,len(d),'charge days',(ch['mode']=='ev_charge').sum(),'park',(ch['mode']=='parking').sum(),'missing weekdays',sum(x not in days for x in bd), d.place_id.value_counts().to_dict()) import pandas as pd, sys
e=pd.read_parquet(sys.argv[1]+'/events.parquet'); p=pd.read_parquet(sys.argv[1]+'/people.parquet')
print(e['mode'].value_counts()); print(e['tap'].value_counts(dropna=False)); print(e.ts.min(), e.ts.max())
print(p.home_npa.value_counts())
CS={'ST-LUZ','ST-VITZ','ST-RIGI','ST-KALT','ST-PIL','ST-STH','ST-STO','ST-TIT','ST-ZUG','ST-CHA','P-ZUG-METALLI'}
g=p[p.home_npa==1422]; print('Grandson residents',len(g)); print(g.to_string())
eg=e[e.pid.isin(g.pid)]
cs=eg[eg.place_id.isin(CS)]
print(cs.sort_values('ts').to_string())