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

395 lines
14 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
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()