osm-commerces/counting_osm_objects/plotly_city.py
2025-07-27 18:01:24 +02:00

309 lines
10 KiB
Python
Executable file

#!/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()