#!/usr/bin/env python3 """Génère une carte interactive des arbres de Paris (travail complémentaire P02). Lit le CSV Open Data Paris (p2-trees-fr.csv), agrège par zone et par espèce, échantillonne des points pour la heatmap, et écrit une page HTML autonome (Leaflet + Leaflet.heat via CDN). Usage : python3 generer_carte_arbres.py ../p2-trees-fr.csv carte_arbres_paris_interactive.html """ import csv import json import random import sys from collections import defaultdict from pathlib import Path SAMPLE_POINTS = 8000 TOP_SPECIES = 40 def parse_float(value): try: return float(value) except (TypeError, ValueError): return None def zone_sort_key(name): upper = name.upper() if upper.startswith("PARIS ") and "ARRDT" in upper: digits = "".join(c for c in upper.split()[1] if c.isdigit()) return (0, int(digits) if digits else 99) if upper.startswith("BOIS DE"): return (1, upper) return (2, upper) def species_label(row): label = (row.get("libelle_francais") or "").strip() if label: return label genre = (row.get("genre") or "").strip() espece = (row.get("espece") or "").strip() label = f"{genre} {espece}".strip() return label or "Non renseigné" def main(csv_path, out_path): rows = [] zone_counts = defaultdict(int) species_counts = defaultdict(int) zone_species = defaultdict(lambda: [0, 0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0.0]) zone_lat = defaultdict(float) zone_lon = defaultdict(float) zone_n = defaultdict(int) zone_top = defaultdict(lambda: defaultdict(int)) species_top = defaultdict(lambda: defaultdict(int)) global_stats = [0, 0, 0.0, 0, 0.0, 0, 0, 0.0, 0, 0.0] reservoir = [] seen = 0 rng = random.Random(42) with open(csv_path, newline="", encoding="utf-8") as handle: reader = csv.DictReader(handle, delimiter=";") for row in reader: zone = (row.get("arrondissement") or "").strip() or "Non renseigné" species = species_label(row) height = parse_float(row.get("hauteur_m")) circ = parse_float(row.get("circonference_cm")) remarkable = (row.get("remarquable") or "").strip() == "1" lat = parse_float(row.get("geo_point_2d_a")) lon = parse_float(row.get("geo_point_2d_b")) zone_counts[zone] += 1 species_counts[species] += 1 zone_top[zone][species] += 1 species_top[species][zone] += 1 cell = zone_species[(zone, species)] cell[0] += 1 if height is not None: cell[1] += 1 cell[2] += height if circ is not None: cell[3] += 1 cell[4] += circ if remarkable: cell[5] += 1 if height is not None: cell[6] += 1 cell[7] += height if circ is not None: cell[8] += 1 cell[9] += circ global_stats[0] += 1 if height is not None: global_stats[1] += 1 global_stats[2] += height if circ is not None: global_stats[3] += 1 global_stats[4] += circ if remarkable: global_stats[5] += 1 if height is not None: global_stats[6] += 1 global_stats[7] += height if circ is not None: global_stats[8] += 1 global_stats[9] += circ if lat is not None and lon is not None: zone_lat[zone] += lat zone_lon[zone] += lon zone_n[zone] += 1 seen += 1 point = [round(lat, 5), round(lon, 5), zone, species, round(height, 1) if height is not None else None, round(circ, 1) if circ is not None else None, 1 if remarkable else 0] if len(reservoir) < SAMPLE_POINTS: reservoir.append(point) else: j = rng.randint(0, seen - 1) if j < SAMPLE_POINTS: reservoir[j] = point zones = sorted(zone_counts, key=zone_sort_key) species = sorted(species_counts, key=lambda s: (-species_counts[s], s)) zone_index = {name: i for i, name in enumerate(zones)} species_index = {name: i for i, name in enumerate(species)} zone_payload = [] for zone in zones: lat = zone_lat[zone] / zone_n[zone] if zone_n[zone] else None lon = zone_lon[zone] / zone_n[zone] if zone_n[zone] else None top = sorted(zone_top[zone].items(), key=lambda kv: (-kv[1], kv[0]))[:3] zone_payload.append({ "name": zone, "count": zone_counts[zone], "lat": round(lat, 5) if lat is not None else None, "lon": round(lon, 5) if lon is not None else None, "top": [[name, count] for name, count in top], }) species_payload = [] for name in species: top = sorted(species_top[name].items(), key=lambda kv: (-kv[1], kv[0]))[:1] species_payload.append({ "name": name, "count": species_counts[name], "top": [[zone, count] for zone, count in top], }) matrix = {} for (zone, sp), cell in zone_species.items(): matrix[f"{zone_index[zone]}|{species_index[sp]}"] = [round(v, 1) if isinstance(v, float) else v for v in cell] points = [] for lat, lon, zone, sp, height, circ, remarkable in reservoir: points.append([lat, lon, zone_index[zone], species_index[sp], height, circ, remarkable]) data = { "global": { "total": global_stats[0], "zones": len(zones), "species": len(species), "avgHeight": round(global_stats[2] / global_stats[1], 1) if global_stats[1] else None, "avgCirc": round(global_stats[4] / global_stats[3], 1) if global_stats[3] else None, "remarkable": global_stats[5], }, "zones": zone_payload, "species": species_payload, "matrix": matrix, "points": points, "topSpecies": species[:TOP_SPECIES], } html = HTML_TEMPLATE.replace("__DATA__", json.dumps(data, ensure_ascii=False, separators=(",", ":"))) Path(out_path).write_text(html, encoding="utf-8") print(f"écrit {out_path} ({Path(out_path).stat().st_size / 1024 / 1024:.2f} Mo)") print(f"{len(zones)} zones, {len(species)} espèces, {len(points)} points échantillonnés") HTML_TEMPLATE = r""" Arbres de Paris - carte interactive
Travail complémentaire - projet P02

Arbres de Paris - carte interactive

Exploration du patrimoine arboré parisien à partir de l'open data de la Ville de Paris. Filtres par zone et par espèce, trois vues (densité, heatmap, points), indicateurs recalculés à la volée. Le notebook et les cartes Folium d'origine restent dans le dépôt P02 ; cette page est un complément interactif.

Données : Open Data Ville de Paris - Les arbres. 200 137 arbres, 536 espèces, 184 remarquables. Le notebook d'origine traite le nettoyage et les analyses statistiques ; cette page ajoute les agrégats, les filtres et la restitution interactive.

""" if __name__ == "__main__": if len(sys.argv) != 3: print(__doc__) sys.exit(1) main(sys.argv[1], sys.argv[2])