osm-commerces/counting_osm_objects/generate_graph.py
2025-07-28 16:37:50 +02:00

448 lines
16 KiB
Python
Executable file

#!/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
import csv
def get_city_name(insee_code):
"""
Récupère le nom de la ville à partir du code INSEE.
Args:
insee_code: Code INSEE de la commune
Returns:
Nom de la ville ou None si non trouvé
"""
try:
csv_path = os.path.join(os.path.dirname(__file__), "osm-commerces-villes-export.csv")
if os.path.exists(csv_path):
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if row.get('zone') == insee_code:
return row.get('name')
return None
except Exception as e:
print(f"Erreur lors de la récupération du nom de la ville: {e}")
return None
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 = ""
city_name = None
# 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]
city_name = get_city_name(insee_code)
if city_name:
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
zone_label = f"{insee_code} - {city_name}"
else:
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]
city_name = get_city_name(zone)
if city_name:
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
zone_label = f"{zone} - {city_name}"
else:
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
zone_label = ""
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
insee_code = df["code_insee"].iloc[0]
city_name = get_city_name(insee_code)
if city_name:
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
zone_label = f"{insee_code} - {city_name}"
else:
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]
city_name = get_city_name(zone)
if city_name:
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
zone_label = f"{zone} - {city_name}"
else:
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
zone_label = zone
# Mettre à jour le titre avec le nom de la zone
if zone_label:
plt.title(f"{zone_label} : Évolution du taux de complétion 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:
# 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()