diff --git a/README.fr.md b/README.fr.md index 909988e..83c54c0 100644 --- a/README.fr.md +++ b/README.fr.md @@ -47,9 +47,14 @@ Le dataset est inclus dans le projet sous le nom `p2-trees-fr.csv`. ├── data_analysis_functions.py ├── map_arrondissement.html ├── map_lieu.html +├── demo/ (travail complémentaire : carte interactive) └── p2-trees-fr.csv ``` +## Travail complémentaire (`demo/`) + +Une carte interactive Leaflet, générée à partir du même CSV, avec filtres par zone et par espèce, trois vues (densité, heatmap, points) et indicateurs recalculés. Elle ne remplace pas le notebook ni les cartes Folium d'origine. Page en ligne : https://demo.gregoiremureau.com/arbres-paris-carte-interactive.html. Détail : [`demo/README.fr.md`](./demo/README.fr.md). + ### 1. `Paris_trees_analysis.ipynb` **Notebook Principal d'Analyse des Arbres Parisiens** diff --git a/README.md b/README.md index 4863dd2..7c8fe30 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,14 @@ The dataset is included in the project under the name `p2-trees-fr.csv`. ├── data_analysis_functions.py ├── map_arrondissement.html ├── map_lieu.html +├── demo/ (complementary work: interactive map) └── p2-trees-fr.csv ``` +## Complementary work (`demo/`) + +An interactive Leaflet map generated from the same CSV, with filters by district and species, three views (density, heatmap, points) and live KPIs. It does not replace the notebook or the original Folium maps. Live page: https://demo.gregoiremureau.com/arbres-paris-carte-interactive.html. Details: [`demo/README.md`](./demo/README.md). + ### 1. `Paris_trees_analysis.ipynb` **Main Notebook for Paris Trees Analysis** diff --git a/demo/README.fr.md b/demo/README.fr.md new file mode 100644 index 0000000..92e9b07 --- /dev/null +++ b/demo/README.fr.md @@ -0,0 +1,53 @@ +# Travail complémentaire - carte interactive des arbres de Paris + +📘 This folder is also documented in [English 🇬🇧](./README.md) + +Ce dossier est un **complément** au projet P02, pas une pièce du rendu d'origine. +Le notebook `Paris_trees_analysis.ipynb`, le module `data_analysis_functions.py` +et les cartes Folium `map_arrondissement.html` / `map_lieu.html` restent la +source de référence et ne sont pas modifiés. + +## Ce que ça ajoute + +Le notebook produit des cartes Folium statiques. Cette page va plus loin côté +restitution : + +- **filtres** par zone (arrondissements, bois, départements limitrophes) et par + espèce (top 40 + « autres espèces ») ; +- **trois vues** : densité par zone (cercles proportionnels), heatmap, points + individuels ; +- **KPI recalculés à la volée** : nombre d'arbres, hauteur moyenne, + circonférence moyenne, nombre de remarquables ; +- **top espèces / top zones** mis à jour selon les filtres ; +- page **autonome** (un seul fichier HTML). Leaflet et Leaflet.heat sont + chargés depuis un CDN, avec des empreintes Subresource Integrity. + +C'est le même moteur que Folium (Leaflet), piloté directement en JavaScript +pour avoir des filtres utilisables. + +Page en ligne (le même fichier) : https://demo.gregoiremureau.com/arbres-paris-carte-interactive.html + +## Régénérer la page + +Le CSV n'est pas versionné (environ 29 Mo). Voir `../fetch-data.sh` pour le +récupérer depuis l'open data de la Ville de Paris. + +```bash +cd demo +python3 generer_carte_arbres.py ../p2-trees-fr.csv carte_arbres_paris_interactive.html +``` + +Le script n'a besoin que de la bibliothèque standard Python (csv, json, random). +Il échantillonne 8 000 points pour la heatmap et agrège les 200 137 arbres +par zone et par espèce. + +## Données + +- Source : [Open Data Ville de Paris - Les arbres](https://opendata.paris.fr/explore/dataset/les-arbres/) +- 200 137 arbres, 536 espèces, 184 remarquables. +- Les valeurs manquantes et aberrantes sont traitées dans le notebook + d'origine. Cette page part du CSV brut pour la restitution. + +## Auteur + +Grégoire Mureau - travail complémentaire au projet P02 (Master AI Engineer). diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..0b3b4e3 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,52 @@ +# Complementary work - interactive map of Paris trees + +📘 Ce dossier est également documenté en [français 🇫🇷](./README.fr.md) + +This folder is a **complement** to project P02, not part of the original submission. +The notebook `Paris_trees_analysis.ipynb`, the module `data_analysis_functions.py` +and the Folium maps `map_arrondissement.html` / `map_lieu.html` stay the +reference and are not modified. + +## What it adds + +The notebook produces static Folium maps. This page goes further on the +user-facing side: + +- **filters** by area (districts, woods, neighbouring departments) and by + species (top 40 + "other species"); +- **three views**: density by area (proportional circles), heatmap, individual points; +- **KPIs recomputed on the fly**: tree count, mean height, mean circumference, + number of remarkable trees; +- **top species / top areas** updated with the filters; +- a **single HTML file**. Leaflet and Leaflet.heat are loaded from a CDN, + with Subresource Integrity hashes. + +It uses the same engine as Folium (Leaflet), driven directly in JavaScript +so the filters are practical to use. + +Live page (same file): https://demo.gregoiremureau.com/arbres-paris-carte-interactive.html + +## Regenerate the page + +The CSV is not versioned (about 29 MB). See `../fetch-data.sh` to get it +from the City of Paris open data. + +```bash +cd demo +python3 generer_carte_arbres.py ../p2-trees-fr.csv carte_arbres_paris_interactive.html +``` + +The script only needs the Python standard library (csv, json, random). +It samples 8,000 points for the heatmap and aggregates the 200,137 trees +by area and by species. + +## Data + +- Source: [Open Data City of Paris - Trees](https://opendata.paris.fr/explore/dataset/les-arbres/) +- 200,137 trees, 536 species, 184 remarkable trees. +- Missing and aberrant values are handled in the original notebook. This page + starts from the raw CSV for the display. + +## Author + +Grégoire Mureau - complementary work on project P02 (Master AI Engineer). diff --git a/demo/carte_arbres_paris_interactive.html b/demo/carte_arbres_paris_interactive.html new file mode 100644 index 0000000..a8c1734 --- /dev/null +++ b/demo/carte_arbres_paris_interactive.html @@ -0,0 +1,376 @@ + + + + + + +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.

+ + +
+ + + + diff --git a/demo/generer_carte_arbres.py b/demo/generer_carte_arbres.py new file mode 100644 index 0000000..d356856 --- /dev/null +++ b/demo/generer_carte_arbres.py @@ -0,0 +1,571 @@ +#!/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])