add computing from osm history

This commit is contained in:
Tykayn 2025-07-27 18:01:24 +02:00 committed by tykayn
parent da60f964ab
commit 66bbce5e85
13 changed files with 3921 additions and 0 deletions

8
counting_osm_objects/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
osm_data/*pbf
osm_data/*geojson
polygons/*.poly
test_data/*
test_temp/*
test_results/*
osm_config.txt
__pycache__

View file

@ -0,0 +1,151 @@
# Counting OSM Objects
Ce répertoire contient des scripts pour compter et analyser les objets OpenStreetMap dans différentes zones
administratives françaises.
Pour fonctionner vous aurez besoin du fichier historisé de la france, pour cela connectez vous à geofabrik avec votre
compte osm (oui c'est relou).
## Scripts disponibles
activez le venv python et les dépendances
```shell
python -m venv bin/venv activate
source bin/venv/bin/activate
pip install plotly pandas
```
```shell
# extraire un historique pour une ville à partir de l'historique de france et du polygone de la ville
# ici pour paris on peut utiliser l'historique de l'ile de france
wget https://osm-commerces.cipherbliss.com/admin/export_csv
py get_all_polys.py # ce qui utilise l'export csv des villes de osm mon commerce
osmium extract -p polygons/commune_75056.poly -H osm_data/ile-de-france-internal.osh.pbf -s complete_ways -O -o commune_75056.osh.pbf
```
```shell
# générer les historiques de la commune 76216 pour tous ses thèmes et tous ses graphes
py loop_thematics_history_in_zone_to_counts.py --input osm_data/commune_76216.osh.pbf --output-dir test_results --temp-dir test_temp --max-dates 100 --poly polygons/commune_76216.poly
```
```shell
# générer un graphe pour un thème dans une ville
py generate_graph.py --insee 91111 test_results/commune_75056_building.csv
Graphique sauvegardé: test_results/commune_75056_building_monthly_graph.png
Graphique de complétion sauvegardé: test_results/commune_75056_building_monthly_graph_completion.png
Graphiques générés avec succès!
```
### historize_zone.py
Ce script principal permet de lancer l'analyse historique d'une ville dans OpenStreetMap. Il:
1. Demande à l'utilisateur quelle ville il souhaite traiter
2. Trouve le code INSEE de la ville demandée
3. Vérifie si le polygone de la ville existe, sinon le récupère
4. Traite les données historiques OSM pour cette ville en utilisant loop_thematics_history_in_zone_to_counts.py
#### Utilisation
```bash
python historize_zone.py [--input fichier_historique.osh.pbf]
```
Si le fichier d'historique n'est pas spécifié, le script utilisera par défaut le fichier
`osm_data/france-internal.osh.pbf`.
#### Exemples
Lancer l'analyse avec le fichier d'historique par défaut:
```bash
python historize_zone.py
```
Lancer l'analyse avec un fichier d'historique spécifique:
```bash
python historize_zone.py --input osm_data/ile-de-france-internal.osh.pbf
```
### loop_thematics_history_in_zone_to_counts.py
Ce script compte les objets OSM par thématique sur une zone donnée à différentes dates. Il:
1. Filtre les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
2. Compte les objets correspondant à chaque thématique à chaque date
3. Calcule le pourcentage de complétion des attributs importants pour chaque thème
4. Sauvegarde les résultats dans des fichiers CSV
5. Génère des graphiques montrant l'évolution dans le temps
#### Utilisation
```bash
python loop_thematics_history_in_zone_to_counts.py --input fichier.osh.pbf --poly polygons/commune_XXXXX.poly
```
Ce script est généralement appelé par historize_zone.py et ne nécessite pas d'être exécuté directement.
### get_poly.py
Ce script permet de récupérer le polygone d'une commune française à partir de son code INSEE. Il interroge l'API
Overpass Turbo pour obtenir les limites administratives de la commune et sauvegarde le polygone dans un fichier au
format .poly (compatible avec Osmosis).
#### Utilisation
```bash
python get_poly.py [code_insee]
```
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
#### Exemples
Récupérer le polygone de la commune d'Étampes (code INSEE 91111) :
```bash
python get_poly.py 91111
```
Le polygone sera sauvegardé dans le fichier `polygons/commune_91111.poly`.
#### Format de sortie
Le fichier de sortie est au format .poly, qui est utilisé par Osmosis et d'autres outils OpenStreetMap. Il contient :
- Le nom de la commune
- Un numéro de section
- Les coordonnées des points du polygone (longitude, latitude)
- Des marqueurs "END" pour fermer le polygone et le fichier
Exemple de contenu :
```
commune_91111
1
2.1326337 48.6556426
2.1323684 48.6554398
...
2.1326337 48.6556426
END
END
```
## Dépendances
- Python 3.6+
- Modules Python : argparse, urllib, json
- Pour compare_osm_objects.sh : PostgreSQL, curl, jq
## Installation
Aucune installation spécifique n'est nécessaire pour ces scripts. Assurez-vous simplement que les dépendances sont
installées.
## Licence
Ces scripts sont distribués sous la même licence que le projet Osmose-Backend.

1050
counting_osm_objects/counting.sh Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
# on crée une page web présentant un tableau de bord pour la zone donnée
# le dashboard contient le nom de la ville et des graphiques pour l'évolution de chaque thématique

View file

@ -0,0 +1,395 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer un graphique montrant l'évolution du nombre d'objets OSM
à partir d'un fichier CSV
"""
import sys
import os
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
import argparse
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Génère un graphique à partir des données CSV d'objets OSM."
)
parser.add_argument(
"csv_file", help="Chemin vers le fichier CSV contenant les données"
)
parser.add_argument(
"--output", "-o", help="Chemin de sortie pour le graphique (PNG)", default=None
)
parser.add_argument(
"--insee", "-i", help="Code INSEE de la commune à analyser", default=None
)
parser.add_argument(
"--period",
"-p",
help="Période à analyser (annual, monthly, daily)",
default="monthly",
)
return parser.parse_args()
def load_data(csv_file, insee_code=None, period="monthly"):
"""
Charge les données depuis le fichier CSV.
Args:
csv_file: Chemin vers le fichier CSV
insee_code: Code INSEE de la commune à filtrer (optionnel)
period: Période à analyser (annual, monthly, daily)
Returns:
DataFrame pandas contenant les données filtrées
"""
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
try:
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
except TypeError: # Pour les versions plus récentes de pandas
df = pd.read_csv(csv_file, on_bad_lines="skip")
# Vérifier si le CSV a la structure attendue
if "date" in df.columns:
# Format de CSV avec colonne 'date' directement
try:
df["date"] = pd.to_datetime(df["date"])
except:
# Si la conversion échoue, essayer différents formats
try:
if df["date"].iloc[0].count("-") == 2: # Format YYYY-MM-DD
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")
elif df["date"].iloc[0].count("-") == 1: # Format YYYY-MM
df["date"] = pd.to_datetime(df["date"], format="%Y-%m")
else:
df["date"] = pd.to_datetime(df["date"])
except:
print("Erreur: Impossible de convertir la colonne 'date'.")
sys.exit(1)
elif "periode" in df.columns:
# Ancien format avec colonne 'periode'
# Filtrer par période
df = df[df["periode"] == period]
# Filtrer par code INSEE si spécifié
if insee_code and "code_insee" in df.columns:
df = df[df["code_insee"] == insee_code]
# Convertir les dates en objets datetime
if period == "annual" and "annee" in df.columns:
df["date"] = pd.to_datetime(df["annee"].astype(str), format="%Y")
elif period == "monthly":
# Vérifier si la première colonne contient déjà un format de date mensuel (YYYY-MM)
first_col = df.columns[0]
first_val = str(df.iloc[0, 0]) if not df.empty else ""
if first_col == "annee_mois" or (len(first_val) >= 7 and "-" in first_val):
# Si la première colonne est 'annee_mois' ou contient une date au format YYYY-MM
df["date"] = pd.to_datetime(df.iloc[:, 0].astype(str), format="%Y-%m")
elif "annee" in df.columns:
# Sinon, utiliser la colonne 'annee' et ajouter un mois fictif (janvier)
df["date"] = pd.to_datetime(
df["annee"].astype(str) + "-01", format="%Y-%m"
)
elif period == "daily":
# Vérifier si la première colonne contient déjà un format de date quotidien (YYYY-MM-DD)
first_col = df.columns[0]
first_val = str(df.iloc[0, 0]) if not df.empty else ""
if first_col == "jour" or (
len(first_val) >= 10 and first_val.count("-") == 2
):
# Si la première colonne est 'jour' ou contient une date au format YYYY-MM-DD
df["date"] = pd.to_datetime(
df.iloc[:, 0].astype(str), format="%Y-%m-%d"
)
elif "annee" in df.columns:
# Sinon, utiliser la colonne 'annee' et ajouter un jour fictif (1er janvier)
df["date"] = pd.to_datetime(
df["annee"].astype(str) + "-01-01", format="%Y-%m-%d"
)
else:
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
try:
df["date"] = pd.to_datetime(df.iloc[:, 0])
except:
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
sys.exit(1)
# Filtrer par code INSEE si spécifié et si la colonne 'zone' contient des codes INSEE
if insee_code and "zone" in df.columns and not "code_insee" in df.columns:
# Vérifier si la zone contient le code INSEE
if any(
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
):
df = df[df["zone"].str.endswith(insee_code)]
# Trier par date
df = df.sort_values("date")
return df
def generate_graph(df, output_path=None):
"""
Génère un graphique montrant l'évolution du nombre d'objets dans le temps.
Args:
df: DataFrame pandas contenant les données
output_path: Chemin de sortie pour le graphique (optionnel)
"""
# Créer une figure avec une taille adaptée
plt.figure(figsize=(12, 8))
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
type_column = "type_objet" if "type_objet" in df.columns else "theme"
label_objet = "objet"
# Obtenir la liste des types d'objets uniques
if type_column in df.columns:
object_types = df[type_column].unique()
# Créer un graphique pour chaque type d'objet
for obj_type in object_types:
# Filtrer les données pour ce type d'objet
obj_data = df[df[type_column] == obj_type]
# Filtrer les valeurs nulles
obj_data_filtered = obj_data[obj_data["nombre_total"].notna()]
if not obj_data_filtered.empty:
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
line, = plt.plot(
obj_data_filtered["date"],
obj_data_filtered["nombre_total"],
linestyle="-",
label=f"{obj_type}",
)
label_objet = obj_type
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
obj_data_filtered["date"],
obj_data_filtered["nombre_total"],
alpha=0.3,
color=line.get_color()
)
else:
# Si aucune colonne de type n'est trouvée, tracer simplement le nombre total
# Filtrer les valeurs nulles
df_filtered = df[df["nombre_total"].notna()]
if not df_filtered.empty:
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
line, = plt.plot(
df_filtered["date"],
df_filtered["nombre_total"],
linestyle="-",
label="",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
df_filtered["date"],
df_filtered["nombre_total"],
alpha=0.3,
color=line.get_color()
)
# Configurer les axes et les légendes
plt.xlabel("Date")
plt.ylabel("Nombre d'objets")
plt.grid(True, linestyle="--", alpha=0.7)
# Formater l'axe des x pour afficher les dates correctement
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.gcf().autofmt_xdate()
# Ajouter une légende
plt.legend()
zone_label = ""
# Ajouter des informations sur la commune
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
insee_code = df["code_insee"].iloc[0]
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
zone_label = insee_code
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
zone = df["zone"].iloc[0]
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
zone_label = zone
plt.title(f"{zone_label} : {label_objet} dans le temps")
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
# Ajuster la mise en page
plt.tight_layout()
# Sauvegarder ou afficher le graphique
if output_path:
plt.savefig(output_path, dpi=300, bbox_inches="tight")
print(f"Graphique sauvegardé: {output_path}")
else:
plt.show()
def generate_completion_graph(df, output_path=None):
"""
Génère un graphique montrant l'évolution du taux de complétion des attributs dans le temps.
Args:
df: DataFrame pandas contenant les données
output_path: Chemin de sortie pour le graphique (optionnel)
"""
# Vérifier si la colonne de pourcentage de complétion existe
if "pourcentage_completion" not in df.columns:
print(
"Avertissement: La colonne 'pourcentage_completion' n'existe pas dans le CSV. Le graphique de complétion ne sera pas généré."
)
return
# Créer une figure avec une taille adaptée
plt.figure(figsize=(12, 8))
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
type_column = "type_objet" if "type_objet" in df.columns else "theme"
# Obtenir la liste des types d'objets uniques
if type_column in df.columns:
object_types = df[type_column].unique()
# Créer un graphique pour chaque type d'objet
for obj_type in object_types:
# Filtrer les données pour ce type d'objet
obj_data = df[df[type_column] == obj_type]
# Filtrer les valeurs nulles
obj_data_filtered = obj_data[obj_data["pourcentage_completion"].notna()]
if not obj_data_filtered.empty:
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
line, = plt.plot(
obj_data_filtered["date"],
obj_data_filtered["pourcentage_completion"],
linestyle="-",
label=f"{obj_type} - Complétion (%)",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
obj_data_filtered["date"],
obj_data_filtered["pourcentage_completion"],
alpha=0.3,
color=line.get_color()
)
else:
# Si aucune colonne de type n'est trouvée, tracer simplement le taux de complétion global
# Filtrer les valeurs nulles
df_filtered = df[df["pourcentage_completion"].notna()]
if not df_filtered.empty:
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
line, = plt.plot(
df_filtered["date"],
df_filtered["pourcentage_completion"],
linestyle="-",
label="Complétion (%)",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
df_filtered["date"],
df_filtered["pourcentage_completion"],
alpha=0.3,
color=line.get_color()
)
# Configurer les axes et les légendes
plt.xlabel("Date")
plt.ylabel("Complétion (%)")
plt.title("Évolution du taux de complétion dans le temps")
plt.grid(True, linestyle="--", alpha=0.7)
# Définir les limites de l'axe y entre 0 et 100%
plt.ylim(0, 100)
# Formater l'axe des x pour afficher les dates correctement
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.gcf().autofmt_xdate()
# Ajouter une légende
plt.legend()
# Ajouter des informations sur la commune
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
insee_code = df["code_insee"].iloc[0]
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
zone = df["zone"].iloc[0]
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
# Ajuster la mise en page
plt.tight_layout()
# Sauvegarder ou afficher le graphique
if output_path:
# Modifier le nom du fichier pour indiquer qu'il s'agit du taux de complétion
base, ext = os.path.splitext(output_path)
completion_path = f"{base}_completion{ext}"
plt.savefig(completion_path, dpi=300, bbox_inches="tight")
print(f"Graphique de complétion sauvegardé: {completion_path}")
else:
plt.show()
def main():
"""Fonction principale."""
# Analyser les arguments de la ligne de commande
args = parse_args()
# Vérifier que le fichier CSV existe
if not os.path.isfile(args.csv_file):
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
sys.exit(1)
# Charger les données
df = load_data(args.csv_file, args.insee, args.period)
# Vérifier qu'il y a des données
if df.empty:
print(f"Aucune donnée trouvée pour la période {args.period}.")
sys.exit(1)
# Déterminer le chemin de sortie si non spécifié
if not args.output:
# Utiliser le même nom que le fichier CSV mais avec l'extension .png
base_name = os.path.splitext(args.csv_file)[0]
output_path = f"{base_name}_{args.period}_graph.png"
else:
output_path = args.output
# Générer les graphiques
generate_graph(df, output_path)
generate_completion_graph(df, output_path)
print("Graphiques générés avec succès!")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour récupérer les polygones de toutes les communes françaises listées dans un fichier CSV.
Ce script:
1. Ouvre le fichier osm-commerces-villes-export.csv
2. Extrait les codes INSEE (colonne 'zone')
3. Pour chaque code INSEE, vérifie si le polygone existe déjà
4. Si non, utilise get_poly.py pour récupérer le polygone
Usage:
python get_all_polys.py
"""
import os
import sys
import csv
from get_poly import query_overpass_api, extract_polygon, save_polygon_to_file
# Chemin vers le fichier CSV contenant les codes INSEE
CSV_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "osm-commerces-villes-export.csv"
)
# Chemin vers le dossier où sont stockés les polygones
POLYGONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "polygons")
def ensure_polygons_dir_exists():
"""
Vérifie que le dossier 'polygons' existe, sinon le crée.
"""
os.makedirs(POLYGONS_DIR, exist_ok=True)
print(f"Dossier de polygones: {POLYGONS_DIR}")
def polygon_exists(insee_code):
"""
Vérifie si le polygone pour le code INSEE donné existe déjà.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
bool: True si le polygone existe, False sinon
"""
polygon_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
return os.path.isfile(polygon_file)
def get_polygon(insee_code):
"""
Récupère le polygone pour le code INSEE donné.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
str: Le chemin du fichier polygone créé, ou None en cas d'erreur
"""
try:
print(f"Récupération du polygone pour la commune {insee_code}...")
# Interroger l'API Overpass
data = query_overpass_api(insee_code)
# Extraire le polygone
polygon = extract_polygon(data)
# Sauvegarder le polygone dans un fichier
output_file = save_polygon_to_file(polygon, insee_code)
print(f"Polygone pour la commune {insee_code} sauvegardé dans {output_file}")
return output_file
except Exception as e:
print(f"Erreur lors de la récupération du polygone pour {insee_code}: {e}")
return None
def read_insee_codes_from_csv():
"""
Lit le fichier CSV et extrait les codes INSEE (colonne 'zone').
Returns:
list: Liste des codes INSEE
"""
insee_codes = []
try:
print(f"Lecture du fichier CSV: {CSV_FILE}")
if not os.path.isfile(CSV_FILE):
print(f"Erreur: Le fichier {CSV_FILE} n'existe pas.")
return insee_codes
with open(CSV_FILE, "r", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
if "zone" in row and row["zone"]:
insee_codes.append(row["zone"])
print(f"Nombre de codes INSEE trouvés: {len(insee_codes)}")
return insee_codes
except Exception as e:
print(f"Erreur lors de la lecture du fichier CSV: {e}")
return insee_codes
def main():
"""
Fonction principale du script.
"""
try:
# S'assurer que le dossier des polygones existe
ensure_polygons_dir_exists()
# Lire les codes INSEE depuis le fichier CSV
insee_codes = read_insee_codes_from_csv()
if not insee_codes:
print("Aucun code INSEE trouvé dans le fichier CSV.")
return 1
# Compteurs pour les statistiques
total = len(insee_codes)
existing = 0
created = 0
failed = 0
# Pour chaque code INSEE, récupérer le polygone s'il n'existe pas déjà
for i, insee_code in enumerate(insee_codes, 1):
print(f"\nTraitement de la commune {i}/{total}: {insee_code}")
if polygon_exists(insee_code):
print(f"Le polygone pour la commune {insee_code} existe déjà.")
existing += 1
continue
# Récupérer le polygone
result = get_polygon(insee_code)
if result:
created += 1
else:
failed += 1
# Afficher les statistiques
print("\nRésumé:")
print(f"Total des communes traitées: {total}")
print(f"Polygones déjà existants: {existing}")
print(f"Polygones créés avec succès: {created}")
print(f"Échecs: {failed}")
return 0 # Succès
except KeyboardInterrupt:
print("\nOpération annulée par l'utilisateur.")
return 1 # Erreur
except Exception as e:
print(f"Erreur inattendue: {e}")
return 1 # Erreur
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,287 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour récupérer le polygone d'une commune française à partir de son code INSEE.
Ce script:
1. Demande un code INSEE
2. Interroge l'API Overpass Turbo pour obtenir les limites administratives
3. Extrait le polygone de la commune
4. Sauvegarde le polygone dans un fichier
Usage:
python get_poly.py [code_insee]
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
"""
import sys
import os
import json
import urllib.parse
import urllib.request
import argparse
def get_insee_code():
"""
Récupère le code INSEE soit depuis les arguments de ligne de commande,
soit en demandant à l'utilisateur.
Returns:
str: Le code INSEE de la commune
"""
parser = argparse.ArgumentParser(
description="Récupère le polygone d'une commune à partir de son code INSEE"
)
parser.add_argument("insee", nargs="?", help="Code INSEE de la commune")
args = parser.parse_args()
if args.insee:
return args.insee
# Si le code INSEE n'est pas fourni en argument, le demander
return input("Entrez le code INSEE de la commune: ")
def query_overpass_api(insee_code):
"""
Interroge l'API Overpass pour obtenir les limites administratives d'une commune.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
dict: Les données GeoJSON de la commune
"""
print(f"Récupération des limites administratives pour la commune {insee_code}...")
# Construire la requête Overpass QL pour obtenir la relation administrative
query = f"""
[out:json][timeout:60];
(
relation["boundary"="administrative"]["admin_level"="8"]["ref:INSEE"="{insee_code}"];
way(r);
node(w);
);
out geom;
"""
# Encoder la requête pour l'URL
encoded_query = urllib.parse.quote(query)
# Construire l'URL de l'API Overpass
url = f"https://overpass-api.de/api/interpreter?data={encoded_query}"
try:
# Envoyer la requête à l'API
print("Envoi de la requête à Overpass API...")
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode("utf-8"))
# Afficher des informations sur la réponse (version réduite pour production)
print(
f"Réponse reçue de l'API Overpass. Nombre d'éléments: {len(data.get('elements', []))}"
)
return data
except Exception as e:
print(f"Erreur lors de la requête à l'API Overpass: {e}")
raise RuntimeError(f"Erreur lors de la requête à l'API Overpass: {e}")
def extract_polygon(data):
"""
Extrait le polygone des données GeoJSON.
Args:
data (dict): Les données GeoJSON de la commune
Returns:
list: Liste des coordonnées du polygone
"""
print("Extraction du polygone des données...")
# Vérifier si des éléments ont été trouvés
if not data.get("elements"):
print("Aucune limite administrative trouvée pour ce code INSEE.")
raise ValueError("Aucune limite administrative trouvée pour ce code INSEE.")
try:
# Collecter tous les nœuds (points) avec leurs coordonnées
nodes = {}
for element in data["elements"]:
if element["type"] == "node":
nodes[element["id"]] = (element["lon"], element["lat"])
# Trouver les ways qui forment le contour de la commune
ways = []
for element in data["elements"]:
if element["type"] == "way":
ways.append(element)
# Si aucun way n'est trouvé, essayer d'extraire directement les coordonnées des nœuds
if not ways and nodes:
print("Aucun way trouvé. Utilisation directe des nœuds...")
polygon = list(nodes.values())
return polygon
# Trouver la relation administrative
relation = None
for element in data["elements"]:
if (
element["type"] == "relation"
and element.get("tags", {}).get("boundary") == "administrative"
):
relation = element
break
if not relation:
print("Aucune relation administrative trouvée.")
# Si nous avons des ways, nous pouvons essayer de les utiliser directement
if ways:
print("Tentative d'utilisation directe des ways...")
# Prendre le premier way comme contour
way = ways[0]
polygon = []
for node_id in way.get("nodes", []):
if node_id in nodes:
polygon.append(nodes[node_id])
return polygon
raise ValueError(
"Impossible de trouver une relation administrative ou des ways"
)
# Extraire les ways qui forment le contour extérieur de la relation
outer_ways = []
for member in relation.get("members", []):
if member.get("role") == "outer" and member.get("type") == "way":
# Trouver le way correspondant
for way in ways:
if way["id"] == member["ref"]:
outer_ways.append(way)
break
# Si aucun way extérieur n'est trouvé, utiliser tous les ways
if not outer_ways:
print("Aucun way extérieur trouvé. Utilisation de tous les ways...")
outer_ways = ways
# Construire le polygone à partir des ways extérieurs
polygon = []
for way in outer_ways:
for node_id in way.get("nodes", []):
if node_id in nodes:
polygon.append(nodes[node_id])
if not polygon:
raise ValueError("Impossible d'extraire le polygone de la relation")
print(f"Polygone extrait avec {len(polygon)} points.")
return polygon
except Exception as e:
print(f"Erreur lors de l'extraction du polygone: {e}")
raise RuntimeError(f"Erreur lors de l'extraction du polygone: {e}")
def save_polygon_to_file(polygon, insee_code):
"""
Sauvegarde le polygone dans un fichier.
Args:
polygon (list): Liste des coordonnées du polygone
insee_code (str): Le code INSEE de la commune
Returns:
str: Le chemin du fichier créé
Raises:
ValueError: Si le polygone est vide ou invalide
IOError: Si une erreur survient lors de l'écriture du fichier
"""
if not polygon:
raise ValueError("Le polygone est vide")
try:
# Créer le répertoire de sortie s'il n'existe pas
output_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "polygons"
)
os.makedirs(output_dir, exist_ok=True)
# Définir le nom du fichier de sortie
output_file = os.path.join(output_dir, f"commune_{insee_code}.poly")
print(f"Sauvegarde du polygone dans le fichier {output_file}...")
# Écrire le polygone dans le fichier au format .poly (format utilisé par Osmosis)
with open(output_file, "w") as f:
f.write(f"commune_{insee_code}\n")
f.write("1\n") # Numéro de section
# Écrire les coordonnées
for i, (lon, lat) in enumerate(polygon):
f.write(f" {lon:.7f} {lat:.7f}\n")
# Fermer le polygone en répétant le premier point
if len(polygon) > 1 and polygon[0] != polygon[-1]:
lon, lat = polygon[0]
f.write(f" {lon:.7f} {lat:.7f}\n")
f.write("END\n")
f.write("END\n")
print(f"Polygone sauvegardé avec succès dans {output_file}")
return output_file
except IOError as e:
print(f"Erreur lors de l'écriture du fichier: {e}")
raise # Re-raise the IOError
except Exception as e:
print(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
raise RuntimeError(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
def main():
"""
Fonction principale du script.
"""
try:
# Récupérer le code INSEE
insee_code = get_insee_code()
# Vérifier que le code INSEE est valide (format numérique ou alphanumérique pour les DOM-TOM)
if not insee_code:
raise ValueError("Le code INSEE ne peut pas être vide")
if not insee_code.isalnum() or len(insee_code) not in [5, 3]:
raise ValueError(
"Code INSEE invalide. Il doit être composé de 5 chiffres (ou 3 pour certains territoires)."
)
# Interroger l'API Overpass
data = query_overpass_api(insee_code)
# Extraire le polygone
polygon = extract_polygon(data)
# Sauvegarder le polygone dans un fichier
output_file = save_polygon_to_file(polygon, insee_code)
print(
f"Terminé. Le polygone de la commune {insee_code} a été sauvegardé dans {output_file}"
)
return 0 # Succès
except ValueError as e:
print(f"Erreur de validation: {e}")
return 1 # Erreur
except KeyboardInterrupt:
print("\nOpération annulée par l'utilisateur.")
return 1 # Erreur
except Exception as e:
print(f"Erreur inattendue: {e}")
return 1 # Erreur
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,301 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script principal pour lancer l'analyse historique d'une ville.
Ce script:
1. Demande à l'utilisateur quelle ville il souhaite traiter
2. Trouve le code INSEE de la ville demandée
3. Vérifie si le polygone de la ville existe, sinon le récupère
4. Traite les données historiques OSM pour cette ville
Usage:
python historize_zone.py [--input fichier_historique.osh.pbf]
"""
import os
import sys
import csv
import argparse
import subprocess
from pathlib import Path
# Chemin vers le répertoire du script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Chemin vers le fichier CSV contenant les données des villes
CITIES_CSV = os.path.join(SCRIPT_DIR, "osm-commerces-villes-export.csv")
# Chemin vers le répertoire des polygones
POLYGONS_DIR = os.path.join(SCRIPT_DIR, "polygons")
# Chemin par défaut pour le fichier d'historique OSM France
DEFAULT_HISTORY_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-internal.osh.pbf")
def run_command(command):
"""Exécute une commande shell et retourne la sortie"""
print(f"Exécution: {command}")
try:
result = subprocess.run(
command,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Erreur lors de l'exécution de la commande: {e}")
print(f"Sortie de la commande: {e.stdout}")
print(f"Erreur de la commande: {e.stderr}")
return None
def load_cities():
"""
Charge les données des villes depuis le fichier CSV.
Returns:
dict: Dictionnaire des villes avec le nom comme clé et les données comme valeur
"""
cities = {}
try:
with open(CITIES_CSV, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row.get("name") and row.get("zone"):
cities[row["name"].lower()] = row
except Exception as e:
print(f"Erreur lors du chargement du fichier CSV des villes: {e}")
sys.exit(1)
return cities
def find_city(city_name, cities):
"""
Recherche une ville par son nom dans le dictionnaire des villes.
Args:
city_name (str): Nom de la ville à rechercher
cities (dict): Dictionnaire des villes
Returns:
dict: Données de la ville si trouvée, None sinon
"""
# Recherche exacte
if city_name.lower() in cities:
return cities[city_name.lower()]
# Recherche partielle
matches = []
for name, data in cities.items():
if city_name.lower() in name:
matches.append(data)
if not matches:
return None
# Si plusieurs correspondances, demander à l'utilisateur de choisir
if len(matches) > 1:
print(f"Plusieurs villes correspondent à '{city_name}':")
for i, city in enumerate(matches):
print(f"{i+1}. {city['name']} (INSEE: {city['zone']})")
choice = input("Entrez le numéro de la ville souhaitée (ou 'q' pour quitter): ")
if choice.lower() == "q":
sys.exit(0)
try:
index = int(choice) - 1
if 0 <= index < len(matches):
return matches[index]
else:
print("Choix invalide.")
return None
except ValueError:
print("Veuillez entrer un numéro valide.")
return None
return matches[0]
def check_polygon_exists(insee_code):
"""
Vérifie si le polygone d'une commune existe déjà.
Args:
insee_code (str): Code INSEE de la commune
Returns:
str: Chemin vers le fichier polygone s'il existe, None sinon
"""
poly_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
if os.path.isfile(poly_file):
return poly_file
return None
def get_polygon(insee_code):
"""
Récupère le polygone d'une commune à partir de son code INSEE.
Args:
insee_code (str): Code INSEE de la commune
Returns:
str: Chemin vers le fichier polygone créé, None en cas d'erreur
"""
get_poly_script = os.path.join(SCRIPT_DIR, "get_poly.py")
command = f"python3 {get_poly_script} {insee_code}"
output = run_command(command)
if output:
# Vérifier si le polygone a été créé
poly_file = check_polygon_exists(insee_code)
if poly_file:
return poly_file
return None
def process_city_history(input_file, poly_file, cleanup=False, benchmark=False):
"""
Traite l'historique OSM pour une ville.
Args:
input_file (str): Chemin vers le fichier d'historique OSM
poly_file (str): Chemin vers le fichier polygone de la ville
cleanup (bool): Si True, nettoie les fichiers temporaires après traitement
benchmark (bool): Si True, affiche des informations de performance détaillées
Returns:
bool: True si le traitement a réussi, False sinon
"""
loop_script = os.path.join(
SCRIPT_DIR, "loop_thematics_history_in_zone_to_counts.py"
)
output_dir = os.path.join(SCRIPT_DIR, "test_results")
temp_dir = os.path.join(SCRIPT_DIR, "test_temp")
# Créer les répertoires de sortie si nécessaires
os.makedirs(output_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)
# Construire la commande avec les options supplémentaires
command = f"python3 {loop_script} --input {input_file} --poly {poly_file} --output-dir {output_dir} --temp-dir {temp_dir}"
# Ajouter les options de nettoyage et de benchmark si activées
if cleanup:
command += " --cleanup"
if benchmark:
command += " --benchmark"
print(f"Exécution de la commande: {command}")
output = run_command(command)
if output is not None:
return True
return False
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description="Analyse historique d'une ville dans OpenStreetMap."
)
parser.add_argument(
"--input",
"-i",
default=DEFAULT_HISTORY_FILE,
help=f"Fichier d'historique OSM (.osh.pbf). Par défaut: {DEFAULT_HISTORY_FILE}",
)
parser.add_argument(
"--cleanup",
"-c",
action="store_true",
help="Nettoyer les fichiers temporaires après traitement",
)
parser.add_argument(
"--benchmark",
"-b",
action="store_true",
help="Afficher des informations de performance détaillées",
)
parser.add_argument(
"--city",
"-v",
help="Nom de la ville à traiter (si non spécifié, demande interactive)",
)
args = parser.parse_args()
# Vérifier que le fichier d'historique existe
if not os.path.isfile(args.input):
print(f"Erreur: Le fichier d'historique {args.input} n'existe pas.")
print(
f"Veuillez spécifier un fichier d'historique valide avec l'option --input."
)
sys.exit(1)
# Charger les données des villes
cities = load_cities()
if not cities:
print("Aucune ville n'a été trouvée dans le fichier CSV.")
sys.exit(1)
print(f"Données chargées pour {len(cities)} villes.")
# Obtenir le nom de la ville à traiter
city_name = args.city
if not city_name:
# Mode interactif si aucune ville n'est spécifiée
city_name = input("Quelle ville souhaitez-vous traiter ? ")
# Rechercher la ville
city = find_city(city_name, cities)
if not city:
print(f"Aucune ville correspondant à '{city_name}' n'a été trouvée.")
sys.exit(1)
insee_code = city["zone"]
print(f"Ville trouvée: {city['name']} (INSEE: {insee_code})")
# Vérifier si le polygone existe
poly_file = check_polygon_exists(insee_code)
if poly_file:
print(f"Le polygone pour {city['name']} existe déjà: {poly_file}")
else:
print(f"Le polygone pour {city['name']} n'existe pas. Récupération en cours...")
poly_file = get_polygon(insee_code)
if not poly_file:
print(f"Erreur: Impossible de récupérer le polygone pour {city['name']}.")
sys.exit(1)
print(f"Polygone récupéré avec succès: {poly_file}")
# Afficher les options activées
if args.benchmark:
print("\n=== Options ===")
print(
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
)
print(f"Benchmark: Activé")
print("===============\n")
# Traiter l'historique pour cette ville
print(f"Traitement de l'historique OSM pour {city['name']}...")
success = process_city_history(args.input, poly_file, args.cleanup, args.benchmark)
if success:
print(f"Traitement terminé avec succès pour {city['name']}.")
else:
print(f"Erreur lors du traitement de l'historique pour {city['name']}.")
sys.exit(1)
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,945 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour compter les objets OSM par thématique sur une zone donnée à différentes dates.
Ce script utilise osmium pour:
1. Filtrer les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
2. Compter les objets correspondant à chaque thématique à chaque date
3. Calculer le pourcentage de complétion des attributs importants pour chaque thème
4. Sauvegarder les résultats dans des fichiers CSV
5. Générer des graphiques montrant l'évolution dans le temps
Exemple d'utilisation:
python3 loop_thematics_history_in_zone_to_counts.py --input france.osh.pbf --poly polygons/commune_91111.poly
"""
import os
import sys
import subprocess
import csv
import json
import argparse
import multiprocessing
from datetime import datetime, timedelta
from pathlib import Path
from functools import lru_cache
import time
# Définition des thématiques et leurs tags correspondants (repris de split_history_to_thematics.py)
THEMES = {
"borne-de-recharge": {
"tag_filter": "amenity=charging_station",
"important_tags": ["operator", "capacity"],
},
"borne-incendie": {
"tag_filter": "emergency=fire_hydrant",
"important_tags": ["ref", "colour"],
},
"arbres": {
"tag_filter": "natural=tree",
"important_tags": ["species", "leaf_type"],
},
"defibrillator": {
"tag_filter": "emergency=defibrillator",
"important_tags": ["operator", "access"],
},
"toilets": {
"tag_filter": "amenity=toilets",
"important_tags": ["access", "wheelchair"],
},
"bus_stop": {
"tag_filter": "highway=bus_stop",
"important_tags": ["name", "shelter"],
},
"camera": {
"tag_filter": "man_made=surveillance",
"important_tags": ["operator", "surveillance"],
},
"recycling": {
"tag_filter": "amenity=recycling",
"important_tags": ["recycling_type", "operator"],
},
"substation": {
"tag_filter": "power=substation",
"important_tags": ["operator", "voltage"],
},
"laboratory": {
"tag_filter": "healthcare=laboratory",
"important_tags": ["name", "operator"],
},
"school": {"tag_filter": "amenity=school", "important_tags": ["name", "operator"]},
"police": {"tag_filter": "amenity=police", "important_tags": ["name", "operator"]},
"healthcare": {
"tag_filter": "healthcare or amenity=doctors or amenity=pharmacy or amenity=hospital or amenity=clinic or amenity=social_facility",
"important_tags": ["name", "healthcare"],
},
"bicycle_parking": {
"tag_filter": "amenity=bicycle_parking",
"important_tags": ["capacity", "covered"],
},
"advertising_board": {
"tag_filter": "advertising=board and message=political",
"important_tags": ["operator", "content"],
},
"building": {
"tag_filter": "building",
"important_tags": ["building", "addr:housenumber"],
},
"email": {
"tag_filter": "email or contact:email",
"important_tags": ["email", "contact:email"],
},
"bench": {
"tag_filter": "amenity=bench",
# "important_tags": ["backrest", "material"]
},
"waste_basket": {
"tag_filter": "amenity=waste_basket",
# "important_tags": ["operator", "capacity"]
},
"street_lamp": {
"tag_filter": "highway=street_lamp",
# "important_tags": ["light:method", "operator"]
},
"drinking_water": {
"tag_filter": "amenity=drinking_water",
# "important_tags": ["drinking_water", "bottle"]
},
"power_pole": {
"tag_filter": "power=pole",
# "important_tags": ["ref", "operator"]
},
"manhole": {
"tag_filter": "man_made=manhole",
# "important_tags": ["man_made", "substance"]
},
"little_free_library": {
"tag_filter": "amenity=public_bookcase",
# "important_tags": ["amenity", "capacity"]
},
"playground": {
"tag_filter": "leisure=playground",
# "important_tags": ["name", "surface"]
},
"siret": {
"tag_filter": "ref:FR:SIRET",
# "important_tags": ["name", "surface"]
},
"restaurants": {
"tag_filter": "amenity=restaurant",
"important_tags": ["opening_hours", "contact:street", "contact:housenumber", "website", "contact:phone"]
},
"rnb": {
"tag_filter": "ref:FR:RNB",
# "important_tags": ["name", "surface"]
},
}
def run_command(command):
"""Exécute une commande shell et retourne la sortie"""
print(f"Exécution: {command}")
try:
result = subprocess.run(
command,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Erreur lors de l'exécution de la commande: {e}")
print(f"Sortie de la commande: {e.stdout}")
print(f"Erreur de la commande: {e.stderr}")
return None
@lru_cache(maxsize=128)
def run_command_cached(command):
"""Version mise en cache de run_command pour éviter les appels redondants"""
return run_command(command)
def count_objects_at_date(
input_file, date_str, tag_filter, output_dir=None, insee_code=None
):
"""
Compte les objets correspondant à un filtre de tag à une date spécifique.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
tag_filter: Filtre de tag (ex: "amenity=charging_station")
output_dir: Répertoire de sortie pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Nombre d'objets correspondant au filtre à la date spécifiée
"""
# Créer un répertoire temporaire si nécessaire
if output_dir:
os.makedirs(output_dir, exist_ok=True)
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
temp_file = os.path.join(
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
)
# Vérifier si le fichier temporaire existe déjà
if not os.path.exists(temp_file):
time_filter_cmd = f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
run_command(time_filter_cmd)
tags_count_cmd = f"osmium tags-count {temp_file} -F osm.pbf {tag_filter}"
else:
# Utiliser des pipes comme dans l'exemple
tags_count_cmd = f"osmium time-filter {input_file} {date_str} -O -o - -f osm.pbf | osmium tags-count - -F osm.pbf {tag_filter}"
# Exécuter la commande et récupérer le résultat (utiliser la version mise en cache)
output = run_command_cached(tags_count_cmd)
# Analyser la sortie pour obtenir le nombre d'objets
if output:
# La sortie d'osmium tags-count est au format "count "key" "value"" ou "count "key""
# Par exemple: "42 "amenity" "charging_station"" ou "42 "operator""
parts = output.strip().split()
if len(parts) >= 1:
try:
return int(parts[0])
except ValueError:
print(f"Impossible de convertir '{parts[0]}' en entier")
return None
return None
def export_to_geojson(
input_file, date_str, main_tag_filter, output_dir, insee_code=None
):
"""
Exporte les données OSM filtrées à une date spécifique vers un fichier GeoJSON.
Si le fichier GeoJSON ou les fichiers temporaires existent déjà, l'export est ignoré.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Chemin vers le fichier GeoJSON créé
"""
os.makedirs(output_dir, exist_ok=True)
# Ajouter le code INSEE au nom du fichier si disponible
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
# Définir le chemin du fichier GeoJSON de sortie
geojson_file = os.path.join(
output_dir, f"export_{date_str.replace(':', '_')}__{insee_suffix}.geojson"
)
# Vérifier si le fichier GeoJSON existe déjà
if os.path.exists(geojson_file):
return geojson_file
# Définir les chemins des fichiers temporaires
temp_file = os.path.join(
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
)
filtered_file = os.path.join(
output_dir,
f"filtered_{date_str.replace(':', '_')}__{main_tag_filter.replace('=', '_').replace(' ', '_')}__{insee_suffix}.osm.pbf",
)
# Vérifier si le fichier temporaire filtré par date existe déjà
if not os.path.exists(temp_file):
# Créer un fichier temporaire pour les données filtrées par date
time_filter_cmd = (
f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
)
run_command_cached(time_filter_cmd)
# Vérifier si le fichier filtré par tag existe déjà
if not os.path.exists(filtered_file):
# Filtrer les objets qui ont le tag principal
filter_cmd = f"osmium tags-filter {temp_file} {main_tag_filter} -f osm.pbf -O -o {filtered_file}"
run_command_cached(filter_cmd)
# Exporter vers GeoJSON
export_cmd = f"osmium export {filtered_file} -O -o {geojson_file} -f geojson --geometry-types point,linestring,polygon"
run_command_cached(export_cmd)
return geojson_file
def count_features_with_tags_in_geojson(geojson_file, attribute_tags):
"""
Compte les features dans un fichier GeoJSON qui ont des attributs spécifiques.
Args:
geojson_file: Chemin vers le fichier GeoJSON
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
Returns:
Dictionnaire avec le nombre de features ayant chaque attribut spécifié
"""
try:
with open(geojson_file, "r") as f:
geojson_data = json.load(f)
# Initialiser les compteurs pour chaque attribut
counts = {tag: 0 for tag in attribute_tags}
# Compter en une seule passe
for feature in geojson_data.get("features", []):
properties = feature.get("properties", {})
for tag in attribute_tags:
if tag in properties and properties[tag]:
counts[tag] += 1
return counts
except Exception as e:
print(
f"Erreur lors du comptage des features avec les attributs {attribute_tags}: {e}"
)
return {tag: None for tag in attribute_tags}
def count_objects_with_tags(
input_file,
date_str,
main_tag_filter,
attribute_tags,
output_dir=None,
insee_code=None,
):
"""
Compte les objets qui ont à la fois le tag principal et des attributs spécifiques.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Dictionnaire avec le nombre d'objets ayant chaque attribut spécifié
"""
# Utiliser l'export GeoJSON si un répertoire de sortie est spécifié
if output_dir:
# Exporter vers GeoJSON (la fonction export_to_geojson vérifie déjà si le fichier GeoJSON existe)
geojson_file = export_to_geojson(
input_file, date_str, main_tag_filter, output_dir, insee_code
)
# Compter les features avec les attributs spécifiques en une seule passe
return count_features_with_tags_in_geojson(geojson_file, attribute_tags)
else:
# Méthode alternative si pas de répertoire temporaire
counts = {}
for tag in attribute_tags:
combined_filter = f"{main_tag_filter} and {tag}"
counts[tag] = count_objects_at_date(
input_file, date_str, combined_filter, output_dir, insee_code
)
return counts
def count_objects_with_tag(
input_file,
date_str,
main_tag_filter,
attribute_tag,
output_dir=None,
insee_code=None,
):
"""
Compte les objets qui ont à la fois le tag principal et un attribut spécifique.
Version compatible avec l'ancienne API pour la rétrocompatibilité.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
attribute_tag: Attribut à vérifier (ex: "operator")
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Nombre d'objets ayant à la fois le tag principal et l'attribut spécifié
"""
result = count_objects_with_tags(
input_file, date_str, main_tag_filter, [attribute_tag], output_dir, insee_code
)
return result.get(attribute_tag, None)
def generate_time_slices(max_dates=None):
"""
Génère une liste de dates avec différentes fréquences selon l'ancienneté:
- 30 derniers jours: quotidien
- 12 derniers mois: mensuel
- Jusqu'à 2004: annuel
Args:
max_dates: Nombre maximum de dates à générer (pour les tests)
Returns:
Liste de dates au format ISO (YYYY-MM-DDThh:mm:ssZ)
"""
dates = []
now = datetime.now()
# 1. Dates quotidiennes pour les 30 derniers jours
current_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
for _ in range(30):
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
dates.append(date_str)
current_date -= timedelta(days=1)
# 2. Dates mensuelles pour les 12 derniers mois (en excluant le mois courant déjà couvert)
current_date = datetime(now.year, now.month, 1) - timedelta(
days=1
) # Dernier jour du mois précédent
current_date = datetime(
current_date.year, current_date.month, 1
) # Premier jour du mois précédent
for _ in range(
60
): # 11 mois supplémentaires (12 mois au total avec le mois courant)
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
if date_str not in dates: # Éviter les doublons
dates.append(date_str)
# Passer au mois précédent
if current_date.month == 1:
current_date = datetime(current_date.year - 1, 12, 1)
else:
current_date = datetime(current_date.year, current_date.month - 1, 1)
# 3. Dates annuelles de l'année précédente jusqu'à 2004
start_year = min(
now.year - 1, 2023
) # Commencer à l'année précédente (ou 2023 si nous sommes en 2024)
for year in range(start_year, 2003, -1):
date_str = f"{year}-01-01T00:00:00Z"
if date_str not in dates: # Éviter les doublons
dates.append(date_str)
# Limiter le nombre de dates si spécifié
if max_dates is not None and max_dates > 0:
dates = dates[:max_dates]
# Trier les dates par ordre chronologique
dates.sort()
return dates
def extract_zone_data(input_file, poly_file, output_dir):
"""
Extrait les données pour une zone spécifique à partir d'un fichier d'historique OSM.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
poly_file: Fichier de polygone (.poly) définissant la zone
output_dir: Répertoire de sortie
Returns:
Chemin vers le fichier extrait
"""
os.makedirs(output_dir, exist_ok=True)
# Obtenir le nom de la zone à partir du nom du fichier poly
zone_name = Path(poly_file).stem
# Créer le fichier de sortie
output_file = os.path.join(output_dir, f"{zone_name}.osh.pbf")
# Exécuter la commande osmium extract seulement si le fichier n'existe pas déjà
if os.path.exists(output_file):
print(f"Le fichier {output_file} existe déjà, utilisation du fichier existant.")
else:
print(f"Extraction des données pour la zone {zone_name}...")
command = f"osmium extract -p {poly_file} -H {input_file} -O -o {output_file}"
run_command(command)
return output_file
def process_theme(
theme_name,
theme_info,
zone_file,
zone_name,
dates,
output_dir,
temp_dir,
insee_code=None,
):
"""
Traite une thématique spécifique pour une zone donnée.
Args:
theme_name: Nom de la thématique
theme_info: Informations sur la thématique (tag_filter, important_tags)
zone_file: Fichier de la zone à traiter
zone_name: Nom de la zone
dates: Liste des dates à traiter
output_dir: Répertoire pour les fichiers de sortie
temp_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Chemin vers le fichier CSV généré
"""
start_time = time.time()
print(f"Traitement de la thématique '{theme_name}' pour la zone '{zone_name}'...")
# Préparer le fichier CSV de sortie
csv_file = os.path.join(output_dir, f"{zone_name}_{theme_name}.csv")
# Entêtes du CSV - colonnes de base
headers = ["date", "zone", "theme", "nombre_total"]
# Ajouter une colonne pour chaque tag important
# Vérifier si la clé 'important_tags' existe, sinon utiliser une liste vide
important_tags = theme_info.get("important_tags", [])
for attr in important_tags:
headers.append(f"nombre_avec_{attr}")
# Ajouter la colonne de pourcentage de complétion
headers.append("pourcentage_completion")
# Vérifier si le fichier CSV existe déjà et lire les données existantes
existing_dates = set()
existing_rows = []
existing_data = {} # Dictionnaire pour stocker les données existantes par date
file_exists = os.path.exists(csv_file)
if file_exists:
try:
with open(csv_file, "r", newline="") as f:
reader = csv.reader(f)
existing_headers = next(reader) # Lire les entêtes
# Vérifier que les entêtes correspondent
if existing_headers == headers:
for row in reader:
if len(row) >= 1: # S'assurer que la ligne a au moins une date
date = row[0] # La date est dans la première colonne
existing_dates.add(date)
existing_rows.append(row)
# Stocker les données existantes dans un dictionnaire pour un accès facile
existing_data[date] = row
else:
print(f"Les entêtes du fichier existant ne correspondent pas, création d'un nouveau fichier.")
file_exists = False
except Exception as e:
print(f"Erreur lors de la lecture du fichier CSV existant: {e}")
file_exists = False
# Filtrer les dates qui n'ont pas encore été traitées
dates_to_process = [date_str for date_str in dates if date_str.split("T")[0] not in existing_dates]
# Mode d'ouverture du fichier (écriture ou ajout)
mode = "a" if file_exists else "w"
with open(csv_file, mode, newline="") as f:
writer = csv.writer(f)
# Écrire les entêtes si c'est un nouveau fichier
if not file_exists:
writer.writerow(headers)
# Traiter chaque date qui n'a pas encore été traitée
for date_str in dates_to_process:
tag_filter = theme_info["tag_filter"]
# Compter le nombre total d'objets
total_count = count_objects_at_date(
zone_file, date_str, tag_filter, temp_dir, insee_code
)
# Compter les objets avec chaque attribut important en une seule passe
# Vérifier si la clé 'important_tags' existe et n'est pas vide
important_tags = theme_info.get("important_tags", [])
if important_tags:
attr_counts_dict = count_objects_with_tags(
zone_file,
date_str,
tag_filter,
important_tags,
temp_dir,
insee_code,
)
attr_counts = [
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
]
else:
attr_counts = []
# Formater la date pour le CSV (YYYY-MM-DD)
csv_date = date_str.split("T")[0]
# Vérifier si le total_count est 0 et s'il était > 0 dans le passé
recalculate = False
if total_count == 0:
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
was_greater_than_zero = False
for existing_date in sorted(existing_data.keys()):
if existing_date >= csv_date:
break # Ne pas regarder les dates futures
existing_row = existing_data[existing_date]
if len(existing_row) >= 4: # S'assurer que la ligne a une valeur de nombre_total
try:
existing_total = existing_row[3]
if existing_total and existing_total != "" and int(existing_total) > 0:
was_greater_than_zero = True
break
except (ValueError, TypeError):
# Ignorer les valeurs qui ne peuvent pas être converties en entier
pass
if was_greater_than_zero:
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
total_count = ""
print(f"Valeur passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
# Relancer le calcul osmium
recalculate = True
# Vérifier également pour chaque attribut important
for i, (attr, count) in enumerate(attr_counts):
if count == 0:
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
was_greater_than_zero = False
for existing_date in sorted(existing_data.keys()):
if existing_date >= csv_date:
break # Ne pas regarder les dates futures
existing_row = existing_data[existing_date]
if len(existing_row) >= 4 + i + 1: # S'assurer que la ligne a une valeur pour cet attribut
try:
existing_attr_count = existing_row[4 + i]
if existing_attr_count and existing_attr_count != "" and int(existing_attr_count) > 0:
was_greater_than_zero = True
break
except (ValueError, TypeError):
# Ignorer les valeurs qui ne peuvent pas être converties en entier
pass
if was_greater_than_zero:
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
attr_counts[i] = (attr, "")
print(f"Valeur de {attr} passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
# Relancer le calcul osmium
recalculate = True
# Si on doit recalculer, relancer le calcul osmium
if recalculate:
print(f"Relancement du calcul osmium pour {theme_name} à la date {csv_date}...")
# Supprimer les fichiers temporaires pour forcer un recalcul
temp_file_pattern = os.path.join(temp_dir, f"temp_{date_str.replace(':', '_')}__*")
run_command(f"rm -f {temp_file_pattern}")
# Recalculer le nombre total d'objets
if total_count == "":
total_count = count_objects_at_date(
zone_file, date_str, tag_filter, temp_dir, insee_code
)
# Recalculer les objets avec chaque attribut important
if important_tags:
attr_counts_dict = count_objects_with_tags(
zone_file,
date_str,
tag_filter,
important_tags,
temp_dir,
insee_code,
)
attr_counts = [
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
]
# Calculer le pourcentage de complétion
if total_count is not None and total_count != "" and total_count > 0 and len(attr_counts) > 0:
# Filtrer les comptages None ou vides avant de calculer la moyenne
valid_counts = [(attr, count) for attr, count in attr_counts if count is not None and count != ""]
if valid_counts:
# Moyenne des pourcentages de présence de chaque attribut important
completion_pct = sum(
count / total_count * 100 for _, count in valid_counts
) / len(valid_counts)
else:
completion_pct = 0
else:
completion_pct = 0
# Préparer la ligne CSV avec les colonnes de base
# Si le comptage total a échoué (None), ajouter une chaîne vide au lieu de 0
row = [csv_date, zone_name, theme_name, "" if total_count is None else total_count]
# Ajouter les compteurs pour chaque attribut important
for attr, count in attr_counts:
# Si le comptage a échoué (None), ajouter une chaîne vide au lieu de 0
row.append("" if count is None else count)
# Ajouter le pourcentage de complétion
row.append(round(completion_pct, 2))
# Écrire la ligne dans le CSV
writer.writerow(row)
# Si aucune nouvelle date n'a été traitée, afficher un message
if not dates_to_process:
print(f"Toutes les dates pour la thématique '{theme_name}' sont déjà traitées dans le fichier CSV existant.")
print(f"Résultats sauvegardés dans {csv_file}")
# Générer un graphique pour cette thématique
generate_graph(csv_file, zone_name, theme_name)
end_time = time.time()
print(f"Thématique '{theme_name}' traitée en {end_time - start_time:.2f} secondes")
return csv_file
def cleanup_temp_files(temp_dir, keep_zone_files=True):
"""
Nettoie les fichiers temporaires dans le répertoire spécifié.
Args:
temp_dir: Répertoire contenant les fichiers temporaires
keep_zone_files: Si True, conserve les fichiers de zone extraits (.osh.pbf)
"""
print(f"Nettoyage des fichiers temporaires dans {temp_dir}...")
count = 0
for file in os.listdir(temp_dir):
file_path = os.path.join(temp_dir, file)
# Conserver les fichiers de zone extraits si demandé
if (
keep_zone_files
and file.endswith(".osh.pbf")
and not file.startswith("temp_")
and not file.startswith("filtered_")
):
continue
# Supprimer les fichiers temporaires
if (
file.startswith("temp_")
or file.startswith("filtered_")
or file.startswith("export_")
):
try:
os.remove(file_path)
count += 1
except Exception as e:
print(f"Erreur lors de la suppression du fichier {file_path}: {e}")
print(f"{count} fichiers temporaires supprimés.")
def process_zone(
input_file, poly_file, output_dir, temp_dir, max_dates=None, cleanup=False
):
"""
Traite une zone spécifique pour toutes les thématiques en parallèle.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
poly_file: Fichier de polygone (.poly) définissant la zone
output_dir: Répertoire pour les fichiers de sortie
temp_dir: Répertoire pour les fichiers temporaires
max_dates: Nombre maximum de dates à traiter (pour les tests)
cleanup: Si True, nettoie les fichiers temporaires après traitement
"""
start_time = time.time()
# Créer les répertoires nécessaires
os.makedirs(output_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)
# Obtenir le nom de la zone à partir du nom du fichier poly
zone_name = Path(poly_file).stem
# Extraire le code INSEE à partir du nom du fichier poly (format: commune_XXXXX.poly)
insee_code = None
if zone_name.startswith("commune_"):
insee_code = zone_name.replace("commune_", "")
# Extraire les données pour la zone
zone_file = extract_zone_data(input_file, poly_file, temp_dir)
# Générer les dates avec différentes fréquences selon l'ancienneté
dates = generate_time_slices(max_dates) # Limité par max_dates si spécifié
print(f"Traitement de {len(THEMES)} thématiques pour la zone '{zone_name}'...")
# Déterminer le nombre de processus à utiliser (nombre de cœurs disponibles - 1, minimum 1)
num_processes = max(1, multiprocessing.cpu_count() - 1)
print(f"Utilisation de {num_processes} processus pour le traitement parallèle")
# Créer un pool de processus
with multiprocessing.Pool(processes=num_processes) as pool:
# Préparer les arguments pour chaque thématique
theme_args = [
(
theme_name,
theme_info,
zone_file,
zone_name,
dates,
output_dir,
temp_dir,
insee_code,
)
for theme_name, theme_info in THEMES.items()
]
# Exécuter le traitement des thématiques en parallèle
pool.starmap(process_theme, theme_args)
# Nettoyer les fichiers temporaires si demandé
if cleanup:
cleanup_temp_files(temp_dir, keep_zone_files=True)
end_time = time.time()
total_time = end_time - start_time
print(f"Traitement de la zone '{zone_name}' terminé en {total_time:.2f} secondes")
return total_time
def generate_graph(csv_file, zone_name, theme_name):
"""
Génère un graphique à partir des données CSV.
Args:
csv_file: Fichier CSV contenant les données
zone_name: Nom de la zone
theme_name: Nom de la thématique
"""
# Vérifier si le script generate_graph.py existe
if os.path.exists(os.path.join(os.path.dirname(__file__), "generate_graph.py")):
# Construire le chemin de sortie pour le graphique
output_path = os.path.splitext(csv_file)[0] + "_graph.png"
# Exécuter le script pour générer le graphique
command = f"python3 {os.path.join(os.path.dirname(__file__), 'generate_graph.py')} {csv_file} --output {output_path}"
run_command(command)
print(f"Graphique généré: {output_path}")
else:
print(
"Le script generate_graph.py n'a pas été trouvé. Aucun graphique n'a été généré."
)
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description="Compte les objets OSM par thématique sur une zone donnée à différentes dates."
)
parser.add_argument(
"--input", "-i", required=True, help="Fichier d'historique OSM (.osh.pbf)"
)
parser.add_argument(
"--poly",
"-p",
required=True,
help="Fichier de polygone (.poly) définissant la zone",
)
parser.add_argument(
"--output-dir",
"-o",
default="resultats",
help="Répertoire pour les fichiers de sortie",
)
parser.add_argument(
"--temp-dir",
"-t",
default="temp",
help="Répertoire pour les fichiers temporaires",
)
parser.add_argument(
"--max-dates",
"-m",
type=int,
default=None,
help="Nombre maximum de dates à traiter (pour les tests, par défaut: toutes les dates)",
)
parser.add_argument(
"--cleanup",
"-c",
action="store_true",
help="Nettoyer les fichiers temporaires après traitement",
)
parser.add_argument(
"--benchmark",
"-b",
action="store_true",
help="Afficher des informations de performance détaillées",
)
args = parser.parse_args()
# Vérifier que les fichiers d'entrée existent
if not os.path.isfile(args.input):
print(f"Erreur: Le fichier d'entrée {args.input} n'existe pas.")
sys.exit(1)
if not os.path.isfile(args.poly):
print(f"Erreur: Le fichier de polygone {args.poly} n'existe pas.")
sys.exit(1)
# Afficher des informations sur la configuration
if args.benchmark:
print("\n=== Configuration ===")
print(f"Nombre de processeurs: {multiprocessing.cpu_count()}")
print(f"Nombre de thématiques: {len(THEMES)}")
print(
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
)
print("=====================\n")
# Mesurer le temps d'exécution
start_time = time.time()
# Traiter la zone
total_time = process_zone(
args.input,
args.poly,
args.output_dir,
args.temp_dir,
args.max_dates,
args.cleanup,
)
# Afficher des informations de performance
if args.benchmark:
print("\n=== Performance ===")
print(f"Temps total d'exécution: {total_time:.2f} secondes")
print(f"Temps moyen par thématique: {total_time / len(THEMES):.2f} secondes")
print("===================\n")
print("Traitement terminé.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,193 @@
zone,name,lat,lon,population,budgetAnnuel,completionPercent,placesCount,avecHoraires,avecAdresse,avecSite,avecAccessibilite,avecNote,siren,codeEpci,codesPostaux
91111,Briis-sous-Forges,48.6246916,2.1243349,3375,4708125.00,30,67,12,8,9,7,3,219101110,249100074,91640
79034,Bessines,46.3020750,-0.5167969,1882,2676204.00,58,56,36,26,34,4,1,217900349,200041317,79000
76216,Déville-lès-Rouen,49.4695338,1.0495889,10690,18386800.00,20,110,16,3,11,1,1,217602168,200023414,76250
91249,Forges-les-Bains,48.6290401,2.0996273,4138,6310450.00,33,31,7,5,2,5,0,219102498,249100074,91470
59140,Caullery,50.0817000,3.3737000,464,824528.00,,,,,,,,,,
08122,Chooz,50.0924000,4.7996000,792,1375704.00,,,,,,,,,,
12084,Creissels,44.0621000,3.0572000,1564,2493016.00,,,,,,,,,,
59183,Dunkerque,51.0183000,2.3431000,87013,153229893.00,,,,,,,,,,
86116,Jazeneuil,46.4749000,0.0735000,797,1212237.00,,,,,,,,,,
75113,,48.8303000,2.3656000,177735,239764515.00,,,,,,,,,,
06088,Nice,43.7032000,7.2528000,353701,557079075.00,,,,,,,,,,
38185,Grenoble,45.1842000,5.7155000,156389,231299331.00,,,,,,,,,,
75117,,48.8874000,2.3050000,161206,231975434.00,,,,,,,,,,
75116,,48.8572000,2.2630000,159733,287199934.00,,,,,,,,,,
35236,Redon,47.6557000,-2.0787000,9336,15030960.00,,,,,,,,,,
35238,Rennes,48.1159000,-1.6884000,227830,408727020.00,,,,,,,,,,
78646,Versailles,48.8039000,2.1191000,83918,142912354.00,,,,,,,,,,
12230,Saint-Jean-Delnous,44.0391000,2.4912000,382,665062.00,,,,,,,,,,
76193,"La Crique",49.6929000,1.2051000,367,651425.00,,,,,,,,,,
49007,Angers,47.4819000,-0.5629000,157555,207815045.00,,,,,,,,,,
79003,Aiffres,46.2831000,-0.4147000,5423,9631248.00,,,,,,,,,,
29151,Morlaix,48.5971000,-3.8215000,15220,22129880.00,,,,,,,,,,
38544,Vienne,45.5221000,4.8803000,31555,55252805.00,,,,,,,,,,
42100,"La Gimond",45.5551000,4.4144000,278,428676.00,,,,,,,,,,
76008,Ancourt,49.9110000,1.1835000,627,850839.00,,,,,,,,,,
76618,Petit-Caux,49.9612000,1.2343000,9626,15334218.00,,,,,,,,,,
13202,Marseille,43.3225000,5.3497000,24153,40842723.00,,,,,,,,,,
46102,Figeac,44.6067000,2.0231000,9757,17025965.00,,,,,,,,,,
75107,,48.8548000,2.3115000,48196,70992708.00,,,,,,,,,,
7812,"Les Clayes-sous-Bois",,,,,,,,,,,,,,
76192,Criel-sur-Mer,50.0221000,1.3215000,2592,3659904.00,,,,,,,,,,
94080,Vincennes,48.8471000,2.4383000,48368,64232704.00,,,,,,,,,,
13055,Marseille,43.2803000,5.3806000,877215,1184240250.00,,,,,,,,,,
93055,Pantin,48.9006000,2.4085000,60954,90272874.00,,,,,,,,,,
69385,,45.7560000,4.8012000,48277,83277825.00,,,,,,,,,,
75109,,48.8771000,2.3379000,58419,79625097.00,,,,,,,,,,
44184,Saint-Nazaire,47.2768000,-2.2392000,73111,104694952.00,,,,,,,,,,
13208,,43.2150000,5.3256000,83414,120866886.00,,,,,,,,,,
59271,Grande-Synthe,51.0157000,2.2938000,20347,34284695.00,,,,,,,,,,
77379,Provins,48.5629000,3.2845000,11824,20183568.00,,,,,,,,,,
75108,,48.8732000,2.3111000,35418,51710280.00,,,,,,,,,,
77122,Combs-la-Ville,48.6602000,2.5765000,22712,29661872.00,,,,,,,,,,
79298,Saint-Symphorien,46.2688000,-0.4842000,1986,2909490.00,,,,,,,,,,
51454,Reims,49.2535000,4.0551000,178478,310908676.00,,,,,,,,,,
64102,Bayonne,43.4844000,-1.4611000,53312,82740224.00,,,,,,,,,,
55153,Dieppe-sous-Douaumont,49.2217000,5.5201000,193,302817.00,,,,,,,,,,
39300,Lons-le-Saunier,46.6758000,5.5574000,16942,25379116.00,,,,,,,,,,
85288,Talmont-Saint-Hilaire,46.4775000,-1.6299000,8327,10908370.00,,,,,,,,,,
13203,,43.3113000,5.3806000,55653,81642951.00,,,,,,,,,,
69387,,45.7321000,4.8393000,87491,134998613.00,,,,,,,,,,
64024,Anglet,43.4893000,-1.5193000,42288,73665696.00,,,,,,,,,,
69265,Ville-sur-Jarnioux,45.9693000,4.5942000,820,1209500.00,,,,,,,,,,
75112,,48.8342000,2.4173000,139788,228273804.00,,,,,,,,,,
69388,,45.7342000,4.8695000,84956,119703004.00,,,,,,,,,,
74010,Annecy,45.9024000,6.1264000,131272,235764512.00,,,,,,,,,,
74100,Desingy,46.0022000,5.8870000,759,1032240.00,,,,,,,,,,
75114,,48.8297000,2.3230000,137581,190412104.00,,,,,,,,,,
75118,,48.8919000,2.3487000,185825,275578475.00,,,,,,,,,,
85191,"La Roche-sur-Yon",46.6659000,-1.4162000,54699,80899821.00,,,,,,,,,,
85194,"Les Sables-d'Olonne",46.5264000,-1.7611000,48740,85002560.00,,,,,,,,,,
12220,Sainte-Eulalie-de-Cernon,43.9605000,3.1550000,321,558540.00,,,,,,,,,,
14341,Ifs,49.1444000,-0.3385000,11868,16377840.00,,,,,,,,,,
35288,Saint-Malo,48.6465000,-2.0066000,47255,81562130.00,,,,,,,,,,
86194,Poitiers,46.5846000,0.3715000,89472,130718592.00,,,,,,,,,,
91338,Limours,48.6463000,2.0823000,6408,8676432.00,,,,,,,,,,
91640,,,,,,,,,,,,,,,
79191,Niort,46.3274000,-0.4613000,60074,92934478.00,,,,,,,,,,
06004,Antibes,43.5823000,7.1048000,76612,134147612.00,,,,,,,,,,
13207,,43.2796000,5.3274000,34866,53031186.00,,,,,,,,,,
91657,Vigneux-sur-Seine,48.7021000,2.4274000,31233,41477424.00,,,,,,,,,,
73124,Gilly-sur-Isère,45.6549000,6.3487000,3109,4651064.00,,,,,,,,,,
92040,Issy-les-Moulineaux,48.8240000,2.2628000,67695,108108915.00,,,,,,,,,,
93051,Noisy-le-Grand,48.8327000,2.5560000,71632,104654352.00,,,,,,,,,,
14675,Soliers,49.1315000,-0.2898000,2190,2923650.00,,,,,,,,,,
76655,Saint-Valery-en-Caux,49.8582000,0.7094000,3884,5985244.00,,,,,,,,,,
34172,Montpellier,43.6100000,3.8742000,307101,508866357.00,,,,,,,,,,
76351,"Le Havre",49.4958000,0.1312000,166462,260346568.00,,,,,,,,,,
76665,Sauchay,49.9179000,1.2073000,448,618240.00,,,,,,,,,,
13204,,43.3063000,5.4002000,49744,75312416.00,,,,,,,,,,
75104,Paris,48.8541000,2.3569000,28039,44161425.00,,,,,,,,,,
45234,Orléans,47.8734000,1.9122000,116344,178355352.00,,,,,,,,,,
13100,Saint-Rémy-de-Provence,43.7815000,4.8455000,9547,16334917.00,,,,,,,,,,
27275,Gaillon,49.1602000,1.3405000,6785,11995880.00,,,,,,,,,,
44047,Couëron,47.2391000,-1.7472000,23541,31521399.00,,,,,,,,,,
16070,Chabanais,45.8656000,0.7076000,1564,2744820.00,,,,,,,,,,
12029,Bor-et-Bar,44.1971000,2.0822000,198,304128.00,,,,,,,,,,
59155,Coudekerque-Branche,51.0183000,2.3986000,20833,36707746.00,,,,,,,,,,
16106,Confolens,46.0245000,0.6639000,2726,3595594.00,,,,,,,,,,
75020,,,,,,,,,,,,,,,
76217,Dieppe,49.9199000,1.0838000,28599,48446706.00,,,,,,,,,,
95128,,,,,,,,,,,,,,,
44000,,,,,,,,,,,,,,,
79000,,,,,,,,,,,,,,,
27562,Saint-Marcel,49.0927000,1.4395000,4474,6259126.00,,,,,,,,,,
87011,Bellac,46.1013000,1.0325000,3569,5117946.00,,,,,,,,,,
94016,Cachan,48.7914000,2.3318000,30526,50642634.00,,,,,,,,,,
50237,"La Haye-Pesnel",48.8134000,-1.3732000,1261,2220621.00,,,,,,,,,,
91174,Corbeil-Essonnes,48.5973000,2.4646000,53712,72081504.00,,,,,,,,,,
11012,Argeliers,43.3090000,2.9137000,2128,3562272.00,,,,,,,,,,
94041,Ivry-sur-Seine,48.8125000,2.3872000,64526,85432424.00,,,,,,,,,,
95127,Cergy,49.0373000,2.0455000,69578,108263368.00,,,,,,,,,,
69001,Affoux,45.8448000,4.4116000,397,648698.00,,,,,,,,,,
44190,Saint-Sébastien-sur-Loire,47.2065000,-1.5023000,28373,39920811.00,,,,,,,,,,
69123,Lyon,45.7580000,4.8351000,520774,755643074.00,,,,,,,,,,
07336,Vernon,44.5074000,4.2251000,223,371295.00,,,,,,,,,,
91201,Draveil,48.6777000,2.4249000,29824,51506048.00,,,,,,,,,,
29174,Plonéour-Lanvern,47.9066000,-4.2678000,6403,9931053.00,,,,,,,,,,
75017,,,,,,,,,,,,,,,
17197,Jonzac,45.4413000,-0.4237000,3576,5188776.00,,,,,,,,,,
85195,,,,,,,,,,,,,,,
44162,Saint-Herblain,47.2246000,-1.6306000,50561,88481750.00,,,,,,,,,,
00000,"toutes les villes",,,,,,,,,,,,,,
57463,Metz,49.1048000,6.1962000,121695,192643185.00,,,,,,,,,,
57466,Metzing,49.1008000,6.9605000,693,945945.00,,,,,,,,,,
37000,,,,,,,,,,,,,,,
91312,Igny,48.7375000,2.2229000,10571,14831113.00,,,,,,,,,,
25462,Pontarlier,46.9167000,6.3796000,17928,23449824.00,,,,,,,,,,
33063,Bordeaux,44.8624000,-0.5848000,265328,467773264.00,,,,,,,,,,
16015,Angoulême,45.6458000,0.1450000,41423,66069685.00,,,,,,,,,,
02196,Clacy-et-Thierret,49.5546000,3.5651000,294,458934.00,,,,,,,,,,
24037,Bergerac,44.8519000,0.4883000,26852,47850264.00,,,,,,,,,,
78686,Viroflay,48.8017000,2.1725000,16943,27091857.00,,,,,,,,,,
83137,Toulon,43.1364000,5.9334000,180834,319172010.00,,,,,,,,,,
44026,Carquefou,47.2968000,-1.4687000,20535,28707930.00,,,,,,,,,,
87154,Saint-Junien,45.8965000,0.8853000,11382,19406310.00,,,,,,,,,,
79081,Chauray,46.3537000,-0.3862000,7173,12387771.00,,,,,,,,,,
73008,Aix-les-Bains,45.6943000,5.9035000,32175,53539200.00,,,,,,,,,,
86195,Port-de-Piles,46.9989000,0.5956000,548,744184.00,,,,,,,,,,
91470,,,,,,,,,,,,,,,
34008,"Les Aires",43.5687000,3.0676000,613,1081332.00,,,,,,,,,,
44270,,,,,,,,,,,,,,,
25056,Besançon,47.2602000,6.0123000,120057,158355183.00,,,,,,,,,,
75019,,,,,,,,,,,,,,,
78712,,,,,,,,,,,,,,,
11262,Narbonne,43.1493000,3.0337000,56692,79482184.00,,,,,,,,,,
60602,Saint-Valery,49.7251000,1.7303000,54,90450.00,,,,,,,,,,
91421,Montgeron,48.6952000,2.4638000,23890,40851900.00,,,,,,,,,,
82112,Moissac,44.1219000,1.1002000,13652,24027520.00,,,,,,,,,,
92033,Garches,48.8469000,2.1861000,17705,25442085.00,,,,,,,,,,
38053,Bourgoin-Jallieu,45.6025000,5.2747000,29816,41593320.00,,,,,,,,,,
34335,Villemagne-l'Argentière,43.6189000,3.1208000,420,586320.00,,,,,,,,,,
13213,,43.3528000,5.4301000,93425,144528475.00,,,,,,,,,,
44020,Bouguenais,47.1710000,-1.6181000,20590,34282350.00,,,,,,,,,,
36044,Châteauroux,46.8023000,1.6903000,43079,70864955.00,,,,,,,,,,
11164,Ginestas,43.2779000,2.8830000,1579,2837463.00,,,,,,,,,,
60009,Allonne,49.3952000,2.1157000,1737,2883420.00,,,,,,,,,,
41151,"Montrichard Val de Cher",47.3594000,1.1998000,3641,5559807.00,,,,,,,,,,
27554,"La Chapelle-Longueville",49.1085000,1.4115000,3283,4796463.00,,,,,,,,,,
34189,Olonzac,43.2816000,2.7431000,1683,2511036.00,,,,,,,,,,
34028,Bédarieux,43.6113000,3.1637000,5820,9550620.00,,,,,,,,,,
74112,"Épagny Metz-Tessy",45.9430000,6.0934000,8642,13956830.00,,,,,,,,,,
75102,,48.8677000,2.3411000,20433,35103894.00,,,,,,,,,,
60057,Beauvais,49.4425000,2.0877000,55906,84082624.00,,,,,,,,,,
59350,Lille,50.6311000,3.0468000,238695,403871940.00,,,,,,,,,,
91477,Igny,48.7155000,2.2293000,36067,46923167.00,,,,,,,,,,
12145,Millau,44.0982000,3.1176000,21859,34865105.00,,,,,,,,,,
12115,L'Hospitalet-du-Larzac,43.9755000,3.2074000,344,569320.00,,,,,,,,,,
79004,,,,,,,,,,,,,,,
75014,,,,,,,,,,,,,,,
75018,,,,,,,,,,,,,,,
55154,Dieue-sur-Meuse,49.0790000,5.4293000,1452,2099592.00,,,,,,,,,,
79192,,,,,,,,,,,,,,,
06018,Biot,43.6273000,7.0821000,10196,15192040.00,,,,,,,,,,
25393,Montécheroux,47.3469000,6.7965000,557,839399.00,,,,,,,,,,
14554,"Le Castelet",49.0870000,-0.2811000,1829,3076378.00,,,,,,,,,,
69384,,45.7805000,4.8260000,35232,49782816.00,,,,,,,,,,
78672,Villennes-sur-Seine,48.9372000,1.9975000,5792,9359872.00,,,,,,,,,,
78123,Carrières-sous-Poissy,48.9469000,2.0264000,19951,35752192.00,,,,,,,,,,
77000,,,,,,,,,,,,,,,
31056,Beauzelle,43.6680000,1.3753000,8184,11670384.00,,,,,,,,,,
38553,Villefontaine,45.6161000,5.1549000,19018,25579210.00,,,,,,,,,,
47001,Agen,44.2010000,0.6302000,32193,48965553.00,,,,,,,,,,
54700,,,,,,,,,,,,,,,
70279,Gray,47.4310000,5.6153000,5455,9071665.00,,,,,,,,,,
74000,,,,,,,,,,,,,,,
91339,Linas,48.6261000,2.2525000,7310,12412380.00,,,,,,,,,,
17306,Royan,45.6343000,-1.0127000,19322,33311128.00,,,,,,,,,,
17300,"La Rochelle",46.1620000,-1.1765000,79961,118182358.00,,,,,,,,,,
59000,,,,,,,,,,,,,,,
77284,Meaux,48.9573000,2.9035000,56659,84025297.00,,,,,,,,,,
76414,Martin-Église,49.9105000,1.1279000,1595,2177175.00,,,,,,,,,,
54528,Toul,48.6794000,5.8980000,15570,21626730.00,,,,,,,,,,
63124,Cournon-d'Auvergne,45.7420000,3.1885000,20020,30930900.00,,,,,,,,,,
87085,Limoges,45.8567000,1.2260000,129754,203194764.00,,,,,,,,,,
78000,,,,,,,,,,,,,,,
13205,,43.2925000,5.4006000,45020,61632380.00,,,,,,,,,,
31584,Villemur-sur-Tarn,43.8582000,1.4947000,6235,9813890.00,,,,,,,,,,
91228,Évry-Courcouronnes,48.6287000,2.4313000,66700,90245100.00,,,,,,,,,,
41018,Blois,47.5813000,1.3049000,47092,79350020.00,,,,,,,,,,
66136,Perpignan,42.6990000,2.9045000,120996,193593600.00,,,,,,,,,,
11041,Bize-Minervois,43.3364000,2.8719000,1292,1758412.00,,,,,,,,,,
75016,,,,,,,,,,,,,,,
1 zone name lat lon population budgetAnnuel completionPercent placesCount avecHoraires avecAdresse avecSite avecAccessibilite avecNote siren codeEpci codesPostaux
2 91111 Briis-sous-Forges 48.6246916 2.1243349 3375 4708125.00 30 67 12 8 9 7 3 219101110 249100074 91640
3 79034 Bessines 46.3020750 -0.5167969 1882 2676204.00 58 56 36 26 34 4 1 217900349 200041317 79000
4 76216 Déville-lès-Rouen 49.4695338 1.0495889 10690 18386800.00 20 110 16 3 11 1 1 217602168 200023414 76250
5 91249 Forges-les-Bains 48.6290401 2.0996273 4138 6310450.00 33 31 7 5 2 5 0 219102498 249100074 91470
6 59140 Caullery 50.0817000 3.3737000 464 824528.00
7 08122 Chooz 50.0924000 4.7996000 792 1375704.00
8 12084 Creissels 44.0621000 3.0572000 1564 2493016.00
9 59183 Dunkerque 51.0183000 2.3431000 87013 153229893.00
10 86116 Jazeneuil 46.4749000 0.0735000 797 1212237.00
11 75113 48.8303000 2.3656000 177735 239764515.00
12 06088 Nice 43.7032000 7.2528000 353701 557079075.00
13 38185 Grenoble 45.1842000 5.7155000 156389 231299331.00
14 75117 48.8874000 2.3050000 161206 231975434.00
15 75116 48.8572000 2.2630000 159733 287199934.00
16 35236 Redon 47.6557000 -2.0787000 9336 15030960.00
17 35238 Rennes 48.1159000 -1.6884000 227830 408727020.00
18 78646 Versailles 48.8039000 2.1191000 83918 142912354.00
19 12230 Saint-Jean-Delnous 44.0391000 2.4912000 382 665062.00
20 76193 La Crique 49.6929000 1.2051000 367 651425.00
21 49007 Angers 47.4819000 -0.5629000 157555 207815045.00
22 79003 Aiffres 46.2831000 -0.4147000 5423 9631248.00
23 29151 Morlaix 48.5971000 -3.8215000 15220 22129880.00
24 38544 Vienne 45.5221000 4.8803000 31555 55252805.00
25 42100 La Gimond 45.5551000 4.4144000 278 428676.00
26 76008 Ancourt 49.9110000 1.1835000 627 850839.00
27 76618 Petit-Caux 49.9612000 1.2343000 9626 15334218.00
28 13202 Marseille 43.3225000 5.3497000 24153 40842723.00
29 46102 Figeac 44.6067000 2.0231000 9757 17025965.00
30 75107 48.8548000 2.3115000 48196 70992708.00
31 7812 Les Clayes-sous-Bois
32 76192 Criel-sur-Mer 50.0221000 1.3215000 2592 3659904.00
33 94080 Vincennes 48.8471000 2.4383000 48368 64232704.00
34 13055 Marseille 43.2803000 5.3806000 877215 1184240250.00
35 93055 Pantin 48.9006000 2.4085000 60954 90272874.00
36 69385 45.7560000 4.8012000 48277 83277825.00
37 75109 48.8771000 2.3379000 58419 79625097.00
38 44184 Saint-Nazaire 47.2768000 -2.2392000 73111 104694952.00
39 13208 43.2150000 5.3256000 83414 120866886.00
40 59271 Grande-Synthe 51.0157000 2.2938000 20347 34284695.00
41 77379 Provins 48.5629000 3.2845000 11824 20183568.00
42 75108 48.8732000 2.3111000 35418 51710280.00
43 77122 Combs-la-Ville 48.6602000 2.5765000 22712 29661872.00
44 79298 Saint-Symphorien 46.2688000 -0.4842000 1986 2909490.00
45 51454 Reims 49.2535000 4.0551000 178478 310908676.00
46 64102 Bayonne 43.4844000 -1.4611000 53312 82740224.00
47 55153 Dieppe-sous-Douaumont 49.2217000 5.5201000 193 302817.00
48 39300 Lons-le-Saunier 46.6758000 5.5574000 16942 25379116.00
49 85288 Talmont-Saint-Hilaire 46.4775000 -1.6299000 8327 10908370.00
50 13203 43.3113000 5.3806000 55653 81642951.00
51 69387 45.7321000 4.8393000 87491 134998613.00
52 64024 Anglet 43.4893000 -1.5193000 42288 73665696.00
53 69265 Ville-sur-Jarnioux 45.9693000 4.5942000 820 1209500.00
54 75112 48.8342000 2.4173000 139788 228273804.00
55 69388 45.7342000 4.8695000 84956 119703004.00
56 74010 Annecy 45.9024000 6.1264000 131272 235764512.00
57 74100 Desingy 46.0022000 5.8870000 759 1032240.00
58 75114 48.8297000 2.3230000 137581 190412104.00
59 75118 48.8919000 2.3487000 185825 275578475.00
60 85191 La Roche-sur-Yon 46.6659000 -1.4162000 54699 80899821.00
61 85194 Les Sables-d'Olonne 46.5264000 -1.7611000 48740 85002560.00
62 12220 Sainte-Eulalie-de-Cernon 43.9605000 3.1550000 321 558540.00
63 14341 Ifs 49.1444000 -0.3385000 11868 16377840.00
64 35288 Saint-Malo 48.6465000 -2.0066000 47255 81562130.00
65 86194 Poitiers 46.5846000 0.3715000 89472 130718592.00
66 91338 Limours 48.6463000 2.0823000 6408 8676432.00
67 91640
68 79191 Niort 46.3274000 -0.4613000 60074 92934478.00
69 06004 Antibes 43.5823000 7.1048000 76612 134147612.00
70 13207 43.2796000 5.3274000 34866 53031186.00
71 91657 Vigneux-sur-Seine 48.7021000 2.4274000 31233 41477424.00
72 73124 Gilly-sur-Isère 45.6549000 6.3487000 3109 4651064.00
73 92040 Issy-les-Moulineaux 48.8240000 2.2628000 67695 108108915.00
74 93051 Noisy-le-Grand 48.8327000 2.5560000 71632 104654352.00
75 14675 Soliers 49.1315000 -0.2898000 2190 2923650.00
76 76655 Saint-Valery-en-Caux 49.8582000 0.7094000 3884 5985244.00
77 34172 Montpellier 43.6100000 3.8742000 307101 508866357.00
78 76351 Le Havre 49.4958000 0.1312000 166462 260346568.00
79 76665 Sauchay 49.9179000 1.2073000 448 618240.00
80 13204 43.3063000 5.4002000 49744 75312416.00
81 75104 Paris 48.8541000 2.3569000 28039 44161425.00
82 45234 Orléans 47.8734000 1.9122000 116344 178355352.00
83 13100 Saint-Rémy-de-Provence 43.7815000 4.8455000 9547 16334917.00
84 27275 Gaillon 49.1602000 1.3405000 6785 11995880.00
85 44047 Couëron 47.2391000 -1.7472000 23541 31521399.00
86 16070 Chabanais 45.8656000 0.7076000 1564 2744820.00
87 12029 Bor-et-Bar 44.1971000 2.0822000 198 304128.00
88 59155 Coudekerque-Branche 51.0183000 2.3986000 20833 36707746.00
89 16106 Confolens 46.0245000 0.6639000 2726 3595594.00
90 75020
91 76217 Dieppe 49.9199000 1.0838000 28599 48446706.00
92 95128
93 44000
94 79000
95 27562 Saint-Marcel 49.0927000 1.4395000 4474 6259126.00
96 87011 Bellac 46.1013000 1.0325000 3569 5117946.00
97 94016 Cachan 48.7914000 2.3318000 30526 50642634.00
98 50237 La Haye-Pesnel 48.8134000 -1.3732000 1261 2220621.00
99 91174 Corbeil-Essonnes 48.5973000 2.4646000 53712 72081504.00
100 11012 Argeliers 43.3090000 2.9137000 2128 3562272.00
101 94041 Ivry-sur-Seine 48.8125000 2.3872000 64526 85432424.00
102 95127 Cergy 49.0373000 2.0455000 69578 108263368.00
103 69001 Affoux 45.8448000 4.4116000 397 648698.00
104 44190 Saint-Sébastien-sur-Loire 47.2065000 -1.5023000 28373 39920811.00
105 69123 Lyon 45.7580000 4.8351000 520774 755643074.00
106 07336 Vernon 44.5074000 4.2251000 223 371295.00
107 91201 Draveil 48.6777000 2.4249000 29824 51506048.00
108 29174 Plonéour-Lanvern 47.9066000 -4.2678000 6403 9931053.00
109 75017
110 17197 Jonzac 45.4413000 -0.4237000 3576 5188776.00
111 85195
112 44162 Saint-Herblain 47.2246000 -1.6306000 50561 88481750.00
113 00000 toutes les villes
114 57463 Metz 49.1048000 6.1962000 121695 192643185.00
115 57466 Metzing 49.1008000 6.9605000 693 945945.00
116 37000
117 91312 Igny 48.7375000 2.2229000 10571 14831113.00
118 25462 Pontarlier 46.9167000 6.3796000 17928 23449824.00
119 33063 Bordeaux 44.8624000 -0.5848000 265328 467773264.00
120 16015 Angoulême 45.6458000 0.1450000 41423 66069685.00
121 02196 Clacy-et-Thierret 49.5546000 3.5651000 294 458934.00
122 24037 Bergerac 44.8519000 0.4883000 26852 47850264.00
123 78686 Viroflay 48.8017000 2.1725000 16943 27091857.00
124 83137 Toulon 43.1364000 5.9334000 180834 319172010.00
125 44026 Carquefou 47.2968000 -1.4687000 20535 28707930.00
126 87154 Saint-Junien 45.8965000 0.8853000 11382 19406310.00
127 79081 Chauray 46.3537000 -0.3862000 7173 12387771.00
128 73008 Aix-les-Bains 45.6943000 5.9035000 32175 53539200.00
129 86195 Port-de-Piles 46.9989000 0.5956000 548 744184.00
130 91470
131 34008 Les Aires 43.5687000 3.0676000 613 1081332.00
132 44270
133 25056 Besançon 47.2602000 6.0123000 120057 158355183.00
134 75019
135 78712
136 11262 Narbonne 43.1493000 3.0337000 56692 79482184.00
137 60602 Saint-Valery 49.7251000 1.7303000 54 90450.00
138 91421 Montgeron 48.6952000 2.4638000 23890 40851900.00
139 82112 Moissac 44.1219000 1.1002000 13652 24027520.00
140 92033 Garches 48.8469000 2.1861000 17705 25442085.00
141 38053 Bourgoin-Jallieu 45.6025000 5.2747000 29816 41593320.00
142 34335 Villemagne-l'Argentière 43.6189000 3.1208000 420 586320.00
143 13213 43.3528000 5.4301000 93425 144528475.00
144 44020 Bouguenais 47.1710000 -1.6181000 20590 34282350.00
145 36044 Châteauroux 46.8023000 1.6903000 43079 70864955.00
146 11164 Ginestas 43.2779000 2.8830000 1579 2837463.00
147 60009 Allonne 49.3952000 2.1157000 1737 2883420.00
148 41151 Montrichard Val de Cher 47.3594000 1.1998000 3641 5559807.00
149 27554 La Chapelle-Longueville 49.1085000 1.4115000 3283 4796463.00
150 34189 Olonzac 43.2816000 2.7431000 1683 2511036.00
151 34028 Bédarieux 43.6113000 3.1637000 5820 9550620.00
152 74112 Épagny Metz-Tessy 45.9430000 6.0934000 8642 13956830.00
153 75102 48.8677000 2.3411000 20433 35103894.00
154 60057 Beauvais 49.4425000 2.0877000 55906 84082624.00
155 59350 Lille 50.6311000 3.0468000 238695 403871940.00
156 91477 Igny 48.7155000 2.2293000 36067 46923167.00
157 12145 Millau 44.0982000 3.1176000 21859 34865105.00
158 12115 L'Hospitalet-du-Larzac 43.9755000 3.2074000 344 569320.00
159 79004
160 75014
161 75018
162 55154 Dieue-sur-Meuse 49.0790000 5.4293000 1452 2099592.00
163 79192
164 06018 Biot 43.6273000 7.0821000 10196 15192040.00
165 25393 Montécheroux 47.3469000 6.7965000 557 839399.00
166 14554 Le Castelet 49.0870000 -0.2811000 1829 3076378.00
167 69384 45.7805000 4.8260000 35232 49782816.00
168 78672 Villennes-sur-Seine 48.9372000 1.9975000 5792 9359872.00
169 78123 Carrières-sous-Poissy 48.9469000 2.0264000 19951 35752192.00
170 77000
171 31056 Beauzelle 43.6680000 1.3753000 8184 11670384.00
172 38553 Villefontaine 45.6161000 5.1549000 19018 25579210.00
173 47001 Agen 44.2010000 0.6302000 32193 48965553.00
174 54700
175 70279 Gray 47.4310000 5.6153000 5455 9071665.00
176 74000
177 91339 Linas 48.6261000 2.2525000 7310 12412380.00
178 17306 Royan 45.6343000 -1.0127000 19322 33311128.00
179 17300 La Rochelle 46.1620000 -1.1765000 79961 118182358.00
180 59000
181 77284 Meaux 48.9573000 2.9035000 56659 84025297.00
182 76414 Martin-Église 49.9105000 1.1279000 1595 2177175.00
183 54528 Toul 48.6794000 5.8980000 15570 21626730.00
184 63124 Cournon-d'Auvergne 45.7420000 3.1885000 20020 30930900.00
185 87085 Limoges 45.8567000 1.2260000 129754 203194764.00
186 78000
187 13205 43.2925000 5.4006000 45020 61632380.00
188 31584 Villemur-sur-Tarn 43.8582000 1.4947000 6235 9813890.00
189 91228 Évry-Courcouronnes 48.6287000 2.4313000 66700 90245100.00
190 41018 Blois 47.5813000 1.3049000 47092 79350020.00
191 66136 Perpignan 42.6990000 2.9045000 120996 193593600.00
192 11041 Bize-Minervois 43.3364000 2.8719000 1292 1758412.00
193 75016

View file

@ -0,0 +1,3 @@
<?xml version='1.0' encoding='UTF-8'?>
<osm version="0.6" generator="empty">
</osm>

View file

@ -0,0 +1,110 @@
local tables = {}
-- Table pour les bornes incendie
tables.fire_hydrants = osm2pgsql.define_node_table('fire_hydrants', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'ref', type = 'text' },
{ column = 'color', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les arbres
tables.trees = osm2pgsql.define_node_table('trees', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'species', type = 'text' },
{ column = 'height', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les bornes de recharge (nodes)
tables.charging_stations = osm2pgsql.define_node_table('charging_stations', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'operator', type = 'text' },
{ column = 'capacity', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les bornes de recharge (ways)
tables.charging_stations_ways = osm2pgsql.define_way_table('charging_stations_ways', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'linestring', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'operator', type = 'text' },
{ column = 'capacity', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Function to determine the INSEE code from multiple possible sources
function get_insee_code(tags)
-- Try to get INSEE code from different tags
if tags['ref:INSEE'] then
return tags['ref:INSEE']
elseif tags['addr:postcode'] then
-- French postal codes often start with the department code
-- For example, 91150 is in department 91, which can help identify the INSEE code
return tags['addr:postcode'] and string.sub(tags['addr:postcode'], 1, 2) .. "111"
elseif tags['addr:city'] and tags['addr:city'] == 'Étampes' then
-- If the city is Étampes, use the INSEE code 91111
return "91111"
else
-- Default to 91111 (Étampes) for this specific use case
-- In a production environment, you would use a spatial query to determine the INSEE code
return "91111"
end
end
function osm2pgsql.process_node(object)
-- Check for fire hydrants with different tagging schemes
if object.tags.emergency == 'fire_hydrant' or object.tags.amenity == 'fire_hydrant' then
tables.fire_hydrants:insert({
tags = object.tags,
ref = object.tags.ref,
color = object.tags.color,
insee = get_insee_code(object.tags)
})
end
-- Check for trees
if object.tags.natural == 'tree' then
tables.trees:insert({
tags = object.tags,
species = object.tags.species,
height = object.tags.height,
insee = get_insee_code(object.tags)
})
end
-- Check for charging stations
if object.tags.amenity == 'charging_station' then
tables.charging_stations:insert({
tags = object.tags,
operator = object.tags.operator,
capacity = object.tags.capacity,
insee = get_insee_code(object.tags)
})
end
end
function osm2pgsql.process_way(object)
-- Check for charging stations that might be mapped as ways
if object.tags.amenity == 'charging_station' then
tables.charging_stations_ways:insert({
tags = object.tags,
operator = object.tags.operator,
capacity = object.tags.capacity,
insee = get_insee_code(object.tags)
})
end
end
function osm2pgsql.process_relation(object)
return
end

View file

@ -0,0 +1,309 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer un graphique interactif avec plotly montrant l'évolution du nombre d'objets OSM
à partir d'un fichier CSV thématique d'une ville (code INSEE 91111 par défaut).
Ce script utilise la bibliothèque plotly pour créer des graphiques interactifs à partir des données
contenues dans des fichiers CSV thématiques. Par défaut, il utilise le code INSEE 91111.
Le titre du graphique inclut le tag principal (thème) et le nom de la ville.
Utilisation:
python plotly_city.py chemin/vers/fichier.csv [options]
Options:
--output, -o : Chemin de sortie pour le graphique HTML (optionnel)
--insee, -i : Code INSEE de la commune à analyser (par défaut: 91111)
--city_name, -c : Nom de la ville (si non spécifié, sera généré à partir du code INSEE)
Exemple:
python plotly_city.py test_results/commune_91111_borne-de-recharge.csv
Dépendances requises:
- pandas
- plotly
Installation des dépendances:
pip install pandas plotly
"""
import sys
import os
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import argparse
from datetime import datetime
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Génère un graphique interactif avec plotly à partir des données CSV d'objets OSM."
)
parser.add_argument(
"csv_file", help="Chemin vers le fichier CSV contenant les données"
)
parser.add_argument(
"--output", "-o", help="Chemin de sortie pour le graphique (HTML)", default=None
)
parser.add_argument(
"--insee", "-i", help="Code INSEE de la commune à analyser", default="91111"
)
parser.add_argument(
"--city_name",
"-c",
help="Nom de la ville (si non spécifié, sera extrait du CSV)",
default=None,
)
return parser.parse_args()
def get_city_name(insee_code):
"""
Récupère le nom de la ville à partir du code INSEE.
Cette fonction pourrait être améliorée pour utiliser une API ou une base de données.
Args:
insee_code: Code INSEE de la commune
Returns:
Nom de la ville ou le code INSEE si le nom n'est pas trouvé
"""
# Pour l'instant, on retourne simplement le code INSEE
# Dans une version future, on pourrait implémenter une recherche dans une base de données
return f"Commune {insee_code}"
def load_data(csv_file, insee_code="91111"):
"""
Charge les données depuis le fichier CSV.
Args:
csv_file: Chemin vers le fichier CSV
insee_code: Code INSEE de la commune à filtrer
Returns:
DataFrame pandas contenant les données filtrées
"""
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
try:
df = pd.read_csv(csv_file, on_bad_lines="skip")
except TypeError: # Pour les versions plus anciennes de pandas
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
# Vérifier si le CSV a la structure attendue
if "date" in df.columns:
# Format de CSV avec colonne 'date' directement
df["date"] = pd.to_datetime(df["date"])
else:
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
try:
df["date"] = pd.to_datetime(df.iloc[:, 0])
except:
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
sys.exit(1)
# Filtrer par code INSEE si la colonne 'zone' contient des codes INSEE
if "zone" in df.columns:
# Vérifier si la zone contient le code INSEE
if any(
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
):
df = df[df["zone"].str.endswith(insee_code)]
# Trier par date
df = df.sort_values("date")
return df
def generate_plotly_graph(
df, city_name=None, output_path=None, insee_code="91111", csv_file=None
):
"""
Génère un graphique interactif avec plotly montrant l'évolution du nombre d'objets dans le temps.
Args:
df: DataFrame pandas contenant les données
city_name: Nom de la ville (optionnel)
output_path: Chemin de sortie pour le graphique (optionnel)
insee_code: Code INSEE de la commune
csv_file: Chemin vers le fichier CSV source (pour générer un nom de fichier de sortie par défaut)
"""
# Si le nom de la ville n'est pas fourni, essayer de le récupérer
if not city_name:
city_name = get_city_name(insee_code)
# Déterminer la colonne pour les types d'objets (theme)
theme_column = "theme"
# Créer une figure avec deux sous-graphiques (nombre total et taux de complétion)
fig = make_subplots(
rows=2,
cols=1,
subplot_titles=("Nombre d'objets OSM", "Taux de complétion des attributs (%)"),
vertical_spacing=0.15,
)
# Obtenir la liste des thèmes uniques
if theme_column in df.columns:
themes = df[theme_column].unique()
# Créer un graphique pour chaque thème
for theme in themes:
# Filtrer les données pour ce thème
theme_data = df[df[theme_column] == theme]
# Tracer la ligne pour le nombre total d'objets
fig.add_trace(
go.Scatter(
x=theme_data["date"],
y=theme_data["nombre_total"],
mode="lines+markers",
name=f"{theme} - Total",
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
),
row=1,
col=1,
)
# Tracer la ligne pour le taux de complétion si disponible
if "pourcentage_completion" in theme_data.columns:
fig.add_trace(
go.Scatter(
x=theme_data["date"],
y=theme_data["pourcentage_completion"],
mode="lines+markers",
name=f"{theme} - Complétion (%)",
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
),
row=2,
col=1,
)
else:
# Si aucune colonne de thème n'est trouvée, tracer simplement le nombre total
fig.add_trace(
go.Scatter(
x=df["date"],
y=df["nombre_total"],
mode="lines+markers",
name="Total",
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
),
row=1,
col=1,
)
# Tracer la ligne pour le taux de complétion si disponible
if "pourcentage_completion" in df.columns:
fig.add_trace(
go.Scatter(
x=df["date"],
y=df["pourcentage_completion"],
mode="lines+markers",
name="Complétion (%)",
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
),
row=2,
col=1,
)
# Configurer les axes et les légendes
fig.update_xaxes(title_text="Date", row=1, col=1)
fig.update_xaxes(title_text="Date", row=2, col=1)
fig.update_yaxes(title_text="Nombre d'objets", row=1, col=1)
fig.update_yaxes(title_text="Taux de complétion (%)", range=[0, 100], row=2, col=1)
# Obtenir le thème principal (premier thème trouvé)
main_tag = (
themes[0] if theme_column in df.columns and len(themes) > 0 else "Objets OSM"
)
# Mettre à jour le titre du graphique avec le tag principal et le nom de la ville
fig.update_layout(
title=f"{main_tag} - {city_name}",
hovermode="x unified",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
height=800,
width=1000,
margin=dict(t=100, b=50, l=50, r=50),
)
# Ajouter des annotations pour les informations supplémentaires
fig.add_annotation(
text=f"Code INSEE: {insee_code}",
xref="paper",
yref="paper",
x=0.01,
y=-0.15,
showarrow=False,
font=dict(size=10),
)
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
fig.add_annotation(
text=f"Généré le: {now}",
xref="paper",
yref="paper",
x=0.99,
y=-0.15,
showarrow=False,
font=dict(size=8),
align="right",
)
# Sauvegarder ou afficher le graphique
if output_path:
fig.write_html(output_path)
print(f"Graphique interactif sauvegardé: {output_path}")
elif csv_file:
# Déterminer un chemin de sortie par défaut basé sur le fichier CSV
base_name = os.path.splitext(csv_file)[0]
default_output = f"{base_name}_plotly.html"
fig.write_html(default_output)
print(f"Graphique interactif sauvegardé: {default_output}")
else:
# Si aucun chemin de sortie n'est spécifié et aucun fichier CSV n'est fourni,
# utiliser un nom par défaut basé sur le code INSEE et le thème
default_output = f"commune_{insee_code}_{main_tag}_plotly.html"
fig.write_html(default_output)
print(f"Graphique interactif sauvegardé: {default_output}")
def main():
"""Fonction principale."""
# Analyser les arguments de la ligne de commande
args = parse_args()
# Vérifier que le fichier CSV existe
if not os.path.isfile(args.csv_file):
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
sys.exit(1)
# Charger les données
df = load_data(args.csv_file, args.insee)
# Vérifier qu'il y a des données
if df.empty:
print(f"Aucune donnée trouvée pour le code INSEE {args.insee}.")
sys.exit(1)
# Déterminer le chemin de sortie si non spécifié
if not args.output:
# Utiliser le même nom que le fichier CSV mais avec l'extension .html
base_name = os.path.splitext(args.csv_file)[0]
output_path = f"{base_name}_plotly.html"
else:
output_path = args.output
# Générer le graphique
generate_plotly_graph(df, args.city_name, output_path, args.insee, args.csv_file)
print("Graphique interactif généré avec succès!")
if __name__ == "__main__":
main()