
Connaissez votre véritable anniversaire : calcul astronomique et analyse géospatiale et temporelle en Python
planifient l’année prochaine l’anniversaire de trois amis : Gabriel, Jacques et Camille. Tous les trois sont nés en 1996, à Paris, en France, ils auront donc 30 ans l’année prochaine en 2026. Gabriel et Jacques seront à Paris le jour de leur anniversaire respectif, tandis que Camille sera à Tokyo, au Japon, le jour de son anniversaire. Gabriel et Camille ont tendance à célébrer leur anniversaire chaque année aux jours « officiels » mentionnés sur leurs actes de naissance – respectivement le 18 janvier et le 5 mai. Jacques, né un 29 février, préfère fêter son anniversaire (ou anniversaire civil) le 1er mars des années non bissextiles.
Nous utilisons années bissextiles pour garder notre calendrier synchronisé avec l’orbite de la Terre autour du Soleil. UN année solaire — le temps qu’il faut à la Terre pour effectuer une orbite complète autour du Soleil — est d’environ 365,25 jours. Par convention, le calendrier grégorien attribue 365 jours à chaque année, à l’exception des années bissextiles, qui disposent de 366 jours pour compenser la dérive fractionnaire au fil du temps. Cela vous amène à vous demander : est-ce que certains de vos amis fêteront leur anniversaire le « vrai » anniversaire de leur jour de naissance, c’est-à-dire le jour où le Soleil sera dans la même position dans le ciel (par rapport à la Terre) qu’il était à leur naissance ? Se pourrait-il que vos amis finissent par fêter leurs 30 ans – une étape spéciale – un jour trop tôt ou un jour trop tard ?
L’article suivant utilise ce problème d’anniversaire pour présenter aux lecteurs certains packages Python open source intéressants et largement applicables pour la science des données pour le calcul astronomique et l’analyse géospatiale-temporelle, notamment skyfield, timezonefinder, geopyet pytz. Pour acquérir une expérience pratique, nous utiliserons ces packages pour résoudre notre problème amusant de prédire avec précision le « véritable anniversaire » (ou la date de naissance). retour solaire) au cours d’une année future donnée. Nous discuterons ensuite de la manière dont ces packages peuvent être exploités dans d’autres applications réelles.
Véritable prédicteur d’anniversaire
Configuration du projet
Toutes les étapes de mise en œuvre ci-dessous ont été testées sur macOS Sequoia 15.6.1 et devraient être à peu près similaires sous Linux et Windows.
Commençons par configurer le répertoire du projet. Nous utiliserons uv pour gérer le projet (voir instructions d’installation ici). Vérifiez la version installée dans le terminal :
uv --version
Initialisez un répertoire de projet appelé real-birthday-predictor à un emplacement approprié sur votre ordinateur local :
uv init --bare real-birthday-predictor
Dans le répertoire du projet, créez un requirements.txt fichier avec les dépendances suivantes :
skyfield==1.53
timezonefinder==8.0.0
geopy==2.4.1
pytz==2025.2
Voici un bref aperçu de chacun de ces forfaits :
skyfieldfournit des fonctions pour le calcul astronomique. Il peut être utilisé pour calculer les positions précises des corps célestes (par exemple, le Soleil, la Lune, les planètes et les satellites) afin de déterminer les heures de lever/coucher, les éclipses et les trajectoires orbitales. Il s’appuie sur ce qu’on appelle éphémérides (tableaux de données de position de divers corps célestes extrapolés sur de nombreuses années), qui sont maintenus par des organisations telles que le Jet Propulsion Laboratory (JPL) de la NASA. Pour cet article, nous utiliserons le fichier éphéméride léger DE421, qui couvre les dates du 29 juillet 1899 au 9 octobre 2053.timezonefindera des fonctions pour mapper les coordonnées géographiques (latitudes et longitudes) aux fuseaux horaires (par exemple, « Europe/Paris »). Il peut le faire hors ligne.geopyoffre des fonctions d’analyse géospatiale, telles que la cartographie entre adresses et coordonnées géographiques. Nous l’utiliserons avec leNominatimgéocodeur pour les données OpenStreetMap pour mapper les noms de villes et de pays avec leurs coordonnées.pytzfournit des fonctions d’analyse temporelle et de conversion de fuseau horaire. Nous l’utiliserons pour convertir entre l’UTC et l’heure locale en utilisant les règles régionales d’heure d’été.
Nous utiliserons également quelques autres modules intégrés, tels que datetime pour analyser et manipuler les valeurs de date/heure, calendar pour vérifier les années bissextiles, et time pour dormir entre les tentatives de géocodage.
Ensuite, créez un environnement virtuel Python 3.12 dans le répertoire du projet, activez l’environnement et installez les dépendances :
uv venv --python=3.12
source .venv/bin/activate
uv add -r requirements.txt
Vérifiez que les dépendances ont été installées :
uv pip list
Mise en œuvre
Dans cette section, nous examinerons étape par étape le code permettant de prédire la date et l’heure « réelles » d’un anniversaire dans une année future et un lieu de célébration donnés. Tout d’abord, nous importons les modules nécessaires :
from datetime import datetime, timedelta
from skyfield.api import load, wgs84
from timezonefinder import TimezoneFinder
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
import pytz
import calendar
import time
Ensuite, nous définissons la méthode, en utilisant des noms de variables significatifs et du texte docstring :
def get_real_birthday_prediction(
official_birthday: str,
official_birth_time: str,
birth_country: str,
birth_city: str,
current_country: str,
current_city: str,
target_year: str = None
):
"""
Predicts the "real" birthday (solar return) for a given year,
accounting for the time zone at the birth location and the time zone
at the current location. Uses March 1 in non-leap years for the civil
anniversary if the official birth date is February 29.
"""
Noter que current_country et current_city font référence conjointement au lieu où l’anniversaire doit être célébré au cours de l’année cible.
Nous validons les entrées avant de travailler avec elles :
# Determine target year
if target_year is None:
target_year = datetime.now().year
else:
try:
target_year = int(target_year)
except ValueError:
raise ValueError(f"Invalid target year '{target_year}'. Please use 'yyyy' format.")
# Validate and parse birth date
try:
birth_date = datetime.strptime(official_birthday, "%d-%m-%Y")
except ValueError:
raise ValueError(
f"Invalid birth date '{official_birthday}'. "
"Please use 'dd-mm-yyyy' format with a valid calendar date."
)
# Validate and parse birth time
try:
birth_hour, birth_minute = map(int, official_birth_time.split(":"))
except ValueError:
raise ValueError(
f"Invalid birth time '{official_birth_time}'. "
"Please use 'hh:mm' 24-hour format."
)
if not (0 <= birth_hour <= 23):
raise ValueError(f"Hour '{birth_hour}' is out of range (0-23).")
if not (0 <= birth_minute <= 59):
raise ValueError(f"Minute '{birth_minute}' is out of range (0-59).")
Ensuite, nous utilisons geopy avec le Nominatim géocodeur pour déterminer la naissance et les emplacements actuels. Pour éviter les erreurs de délai d’attente, nous définissons une valeur de délai d’attente raisonnablement longue de dix secondes ; c’est combien de temps notre safe_geocode La fonction attend la réponse du service de géocodage avant de déclencher une geopy.exc.GeocoderTimedOut exception. Pour plus de sécurité, la fonction tente la procédure de recherche trois fois avec un délai d’une seconde avant d’abandonner :
geolocator = Nominatim(user_agent="birthday_tz_lookup", timeout=10)
# Helper function to call geocode API with retries
def safe_geocode(query, retries=3, delay=1):
for attempt in range(retries):
try:
return geolocator.geocode(query)
except GeocoderTimedOut:
if attempt < retries - 1:
time.sleep(delay)
else:
raise RuntimeError(
f"Could not retrieve location for '{query}' after {retries} attempts. "
"The geocoding service may be slow or unavailable. Please try again later."
)
birth_location = safe_geocode(f"{birth_city}, {birth_country}")
current_location = safe_geocode(f"{current_city}, {current_country}")
if not birth_location or not current_location:
raise ValueError("Could not find coordinates for one of the locations. Please check spelling.")
À l’aide des coordonnées géographiques de la naissance et des lieux actuels, nous identifions les fuseaux horaires respectifs ainsi que la date et l’heure UTC à la naissance. Nous supposons également que les individus comme Jacques, nés le 29 février, préféreront fêter leur anniversaire le 1er mars lors des années non bissextiles :
# Get time zones
tf = TimezoneFinder()
birth_tz_name = tf.timezone_at(lng=birth_location.longitude, lat=birth_location.latitude)
current_tz_name = tf.timezone_at(lng=current_location.longitude, lat=current_location.latitude)
if not birth_tz_name or not current_tz_name:
raise ValueError("Could not determine timezone for one of the locations.")
birth_tz = pytz.timezone(birth_tz_name)
current_tz = pytz.timezone(current_tz_name)
# Set civil anniversary date to March 1 for February 29 birthdays in non-leap years
birth_month, birth_day = birth_date.month, birth_date.day
if (birth_month, birth_day) == (2, 29):
if not calendar.isleap(birth_date.year):
raise ValueError(f"{birth_date.year} is not a leap year, so February 29 is invalid.")
civil_anniversary_month, civil_anniversary_day = (
(3, 1) if not calendar.isleap(target_year) else (2, 29)
)
else:
civil_anniversary_month, civil_anniversary_day = birth_month, birth_day
# Parse birth datetime in birth location's local time
birth_local_dt = birth_tz.localize(datetime(
birth_date.year, birth_month, birth_day,
birth_hour, birth_minute
))
birth_dt_utc = birth_local_dt.astimezone(pytz.utc)
En utilisant les données des éphémérides DE421, nous calculons où se trouvait le Soleil (c’est-à-dire sa longitude de l’écliptique) à l’heure et au lieu exacts de la naissance de la personne :
# Load ephemeris data and get Sun's ecliptic longitude at birth
eph = load("de421.bsp") # Covers dates 1899-07-29 through 2053-10-09
ts = load.timescale()
sun = eph["sun"]
earth = eph["earth"]
t_birth = ts.utc(birth_dt_utc.year, birth_dt_utc.month, birth_dt_utc.day,
birth_dt_utc.hour, birth_dt_utc.minute, birth_dt_utc.second)
# Birth longitude in tropical frame from POV of birth observer on Earth's surface
birth_observer = earth + wgs84.latlon(birth_location.latitude, birth_location.longitude)
ecl = birth_observer.at(t_birth).observe(sun).apparent().ecliptic_latlon(epoch='date')
birth_longitude = ecl[1].degrees
Notez que la première fois que la ligne eph = load("de421.bsp") est exécuté, le de421.bsp le fichier sera téléchargé et placé dans le répertoire du projet ; dans toutes les exécutions futures, le fichier téléchargé sera utilisé directement. Il est également possible de modifier le code pour charger un autre fichier d’éphémérides (par exemple, de440s.bspqui couvre les années jusqu’au 22 janvier 2150).
Vient maintenant une partie intéressante de la fonction : nous allons faire une première estimation de la date et de l’heure d’anniversaire « réelles » dans l’année cible, définir des limites supérieures et inférieures sûres pour la valeur réelle de la date et de l’heure (par exemple, deux jours de chaque côté de l’estimation initiale), et effectuer une recherche binaire avec un arrêt anticipé pour déterminer efficacement la vraie valeur :
# Initial guess for target year solar return
approx_dt_local_birth_tz = birth_tz.localize(datetime(
target_year, civil_anniversary_month, civil_anniversary_day,
birth_hour, birth_minute
))
approx_dt_utc = approx_dt_local_birth_tz.astimezone(pytz.utc)
# Compute Sun longitude from POV of current observer on Earth's surface
current_observer = earth + wgs84.latlon(current_location.latitude, current_location.longitude)
def sun_longitude_at(dt):
t = ts.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
ecl = current_observer.at
return ecl[1].degrees
def angle_diff(a, b):
return (a - b + 180) % 360 - 180
# Set safe upper and lower bounds for search space
dt1 = approx_dt_utc - timedelta(days=2)
dt2 = approx_dt_utc + timedelta(days=2)
# Use binary search with early-stopping to solve for exact solar return in UTC
old_angle_diff = 999
for _ in range(50):
mid = dt1 + (dt2 - dt1) / 2
curr_angle_diff = angle_diff(sun_longitude_at(mid), birth_longitude)
if old_angle_diff == curr_angle_diff: # Early-stopping condition
break
if curr_angle_diff > 0:
dt2 = mid
else:
dt1 = mid
old_angle_diff = curr_angle_diff
real_dt_utc = dt1 + (dt2 - dt1) / 2
Voir ce article pour plus d’exemples d’utilisation de la recherche binaire et pour comprendre pourquoi cet algorithme est important à maîtriser pour les data scientists.
Enfin, la date et l’heure du « vrai » anniversaire identifié par la recherche binaire sont converties dans le fuseau horaire de l’emplacement actuel, formatées selon les besoins, et renvoyées :
# Convert to current location's local time and format output
real_dt_local_current = real_dt_utc.astimezone(current_tz)
date_str = real_dt_local_current.strftime("%d/%m")
time_str = real_dt_local_current.strftime("%H:%M")
return date_str, time_str, current_tz_name
Essai
Nous sommes désormais en mesure de prédire les « vrais » anniversaires de Gabriel, Jacques et Camille en 2026.
Pour rendre le résultat de la fonction plus facile à digérer, voici une fonction d’assistance que nous utiliserons pour afficher joliment les résultats de chaque requête :
def print_real_birthday(
official_birthday: str,
official_birth_time: str,
birth_country: str,
birth_city: str,
current_country: str,
current_city: str,
target_year: str = None):
"""Pretty-print output while hiding verbose error traces."""
print("Official birthday and time:", official_birthday, "at", official_birth_time)
try:
date_str, time_str, current_tz_name = get_real_birthday_prediction(
official_birthday,
official_birth_time,
birth_country,
birth_city,
current_country,
current_city,
target_year
)
print(f"In year {target_year}, your real birthday is on {date_str} at {time_str} ({current_tz_name})\n")
except ValueError as e:
print("Error:", e)
Voici les cas de tests :
# Gabriel
print_real_birthday(
official_birthday="18-01-1996",
official_birth_time="02:30",
birth_country="France",
birth_city="Paris",
current_country="France",
current_city="Paris",
target_year="2026"
)
# Jacques
print_real_birthday(
official_birthday="29-02-1996",
official_birth_time="05:45",
birth_country="France",
birth_city="Paris",
current_country="France",
current_city="Paris",
target_year="2026"
)
# Camille
print_real_birthday(
official_birthday="05-05-1996",
official_birth_time="20:30",
birth_country="Paris",
birth_city="France",
current_country="Japan",
current_city="Tokyo",
target_year="2026"
)
Et voici les résultats :
Official birthday and time: 18-01-1996 at 02:30
In year 2026, your real birthday is on 17/01 at 09:21 (Europe/Paris)
Official birthday and time: 29-02-1996 at 05:45
In year 2026, your real birthday is on 28/02 at 12:37 (Europe/Paris)
Official birthday and time: 05-05-1996 at 20:30
In year 2026, your real birthday is on 06/05 at 09:48 (Asia/Tokyo)
Comme on le voit, le « vrai » anniversaire (ou moment du retour solaire) est différent de l’anniversaire officiel pour vos trois amis : Gabriel et Jacques pourraient théoriquement commencer à célébrer un jour avant leur anniversaire officiel à Paris, tandis que Camille devrait attendre un jour de plus avant de fêter ses 30 ans à Tokyo.
Comme alternative plus simple aux étapes ci-dessus, l’auteur de cet article a créé une bibliothèque Python appelée solarius pour obtenir le même résultat (voir détails ici). Installez la bibliothèque avec pip install solarius ou uv add solarius et utilisez-le comme indiqué ci-dessous :
from solarius.model import SolarReturnCalculator
calculator = SolarReturnCalculator(ephemeris_file="de421.bsp")
# Predict without printing
date_str, time_str, tz_name = calculator.predict(
official_birthday="18-01-1996",
official_birth_time="02:30",
birth_country="France",
birth_city="Paris",
current_country="France",
current_city="Paris",
target_year="2026"
)
print(date_str, time_str, tz_name)
# Or use the convenience printer
calculator.print_real_birthday(
official_birthday="18-01-1996",
official_birth_time="02:30",
birth_country="France",
birth_city="Paris",
current_country="France",
current_city="Paris",
target_year="2026"
)
Bien sûr, les anniversaires ne se limitent pas à prédire les retours solaires : ces jours spéciaux sont ancrés dans des siècles de tradition. Voici une courte vidéo sur les origines fascinantes des anniversaires :
Au-delà des anniversaires
L’intention de la section ci-dessus était de donner aux lecteurs un cas d’utilisation amusant et intuitif pour appliquer les différents packages pour le calcul astronomique et l’analyse géospatiale-temporelle. Cependant, l’utilité de tels packages va bien au-delà de la prévision des anniversaires.
Par exemple, tous ces packages peuvent être utilisés pour d’autres cas de prédiction d’événements astronomiques (par exemple, déterminer quand un lever, un coucher de soleil ou une éclipse se produira à une date ultérieure dans un endroit donné). La prévision du mouvement des satellites et autres corps célestes pourrait également jouer un rôle important dans la planification des missions spatiales.
Les packages pourraient également être utilisés pour optimiser le déploiement de panneaux solaires dans un endroit particulier, comme un quartier résidentiel ou un site commercial. L’objectif serait de prédire la quantité de lumière solaire susceptible de tomber à cet endroit à différents moments de l’année et d’utiliser ces connaissances pour ajuster l’emplacement, l’inclinaison et les programmes d’utilisation des panneaux solaires afin de capturer une énergie maximale.
Enfin, les packages peuvent être exploités pour la reconstruction d’événements historiques (par exemple, dans le contexte de recherches archéologiques ou historiques, ou même de criminalistique légale). L’objectif ici serait de recréer les conditions du ciel pour une date et un lieu passés spécifiques afin d’aider les chercheurs à mieux comprendre les conditions d’éclairage et de visibilité à ce moment-là.
En fin de compte, en combinant ces packages open source et modules intégrés de diverses manières, il est possible de résoudre des problèmes intéressants qui touchent de nombreux domaines.



