minor changes

This commit is contained in:
Grégoire Mureau 2025-11-11 08:39:23 +01:00
parent b7e510b727
commit 1ee81b826d
6 changed files with 160635 additions and 0 deletions

File diff suppressed because one or more lines are too long

221
README.MD Normal file
View file

@ -0,0 +1,221 @@
# Projet d'Analyse des Arbres Parisiens
## 📋 Contexte du projet
Ce projet a été réalisé dans le cadre d'une mission d'analyse de données pour la **Ville de Paris**. L'objectif est d'explorer et d'analyser le patrimoine arboré parisien à partir d'un jeu de données public recensant les arbres présents sur le territoire de la capitale.
Le projet met l'accent sur les fondamentaux de la data science : le nettoyage des données, la gestion des anomalies, l'analyse exploratoire et la visualisation statistique pour préparer le dataset à des analyses ultérieures ou à des applications métier (gestion urbaine, biodiversité, aménagement).
## 🎯 Objectifs
Les objectifs principaux du projet sont :
1. **Explorer le dataset** : Comprendre la structure, les types de variables et les caractéristiques des arbres parisiens
2. **Nettoyer les données** : Identifier et traiter les valeurs manquantes, aberrantes et incohérentes
3. **Analyser les distributions** : Réaliser des analyses univariées et bivariées pour comprendre les variables
4. **Visualiser les données** : Créer des graphiques et des cartes interactives pour faciliter la compréhension
5. **Préparer les données** : Fournir un dataset propre et exploitable pour des analyses futures
## 📊 Données
Les données proviennent de l'**Open Data de la Ville de Paris** et contiennent des informations détaillées sur les arbres :
- Localisation géographique (coordonnées GPS, arrondissement, adresse)
- Caractéristiques botaniques (genre, espèce, variété)
- Dimensions (hauteur, circonférence)
- Statut (arbre remarquable, domanialité)
- Date de plantation
Le dataset est inclus dans le projet sous le nom `p2-trees-fr.csv`.
**Source** : [Open Data Ville de Paris - Les arbres](https://opendata.paris.fr/explore/dataset/les-arbres/)
## 📁 Structure du projet
```
├── Paris_trees_analysis.ipynb
├── soutenance_projet2.pptx
├── data_analysis_functions.py
├── map_arrondissement.html
├── map_lieu.html
└── p2-trees-fr.csv
```
### 1. `Paris_trees_analysis.ipynb`
**Notebook Principal d'Analyse des Arbres Parisiens**
Ce notebook constitue le cœur du projet et couvre l'ensemble du processus d'analyse exploratoire :
**Chargement et aperçu des données** :
- Import du fichier CSV contenant les données des arbres
- Affichage des premières lignes pour une compréhension rapide
- Inspection de la structure du DataFrame (types, dimensions)
**Analyse exploratoire initiale** :
- Utilisation des méthodes pandas (`info()`, `head()`, `describe()`, `value_counts()`)
- Identification de la taille du dataset
- Analyse des types de colonnes et des valeurs manquantes
- Statistiques descriptives pour les variables numériques
**Identification et nettoyage des anomalies** :
- **Valeurs aberrantes** :
- Détection pour la circonférence et la hauteur des arbres
- Utilisation de méthodes statistiques (IQR, z-score)
- Correction ou remplacement des valeurs extrêmes par `NaN`
- **Valeurs manquantes** :
- Analyse approfondie des colonnes avec NaN
- Imputation intelligente pour certaines variables (`domanialite`, `libelle_francais`)
- Utilisation de colonnes corrélées (`genre`, `espece`, `variete`) pour l'imputation
**Analyse univariée** :
- **Variables quantitatives** : distribution de la hauteur, circonférence, année de plantation
- **Variables catégorielles** : répartition par genre, espèce, arrondissement, statut remarquable
- Visualisations : histogrammes, boxplots, diagrammes circulaires, barplots
**Analyse bivariée** :
- Relations entre le statut `remarquable` et les variables quantitatives (`hauteur_m`, `circonference_cm`)
- Relations entre `remarquable` et variables catégorielles (`espece`, `genre`)
- Corrélations entre variables numériques
- Tests statistiques pour valider les observations
**Analyse géographique** :
- Distribution des arbres par arrondissement
- Cartographie interactive des arbres parisiens
- Identification des zones à forte densité arborée
**Export des données nettoyées** :
- Sauvegarde du DataFrame préparé pour analyses ultérieures
### 2. `soutenance_projet2.pptx`
**Présentation de Soutenance**
Cette présentation synthétise :
- Le contexte et les objectifs du projet
- La méthodologie d'exploration et de nettoyage des données
- Les principales découvertes sur le patrimoine arboré parisien
- Les analyses statistiques et visualisations clés
- Les insights métier (arbres remarquables, distribution par arrondissement, espèces dominantes)
- Les recommandations pour la gestion du patrimoine arboré
- Les perspectives d'amélioration et d'exploitation des données
### 3. `data_analysis_functions.py`
**Module de Fonctions Utilitaires d'Analyse**
Ce module Python contient des fonctions réutilisables pour :
- Le nettoyage des données (suppression de doublons, gestion des NaN)
- L'identification des valeurs aberrantes (méthodes IQR, z-score, percentiles)
- Le traitement des outliers (remplacement, suppression, cap)
- La transformation des variables (normalisation, encodage)
- La génération de rapports de qualité des données
- Des fonctions de visualisation standardisées
Ces fonctions ont été développées pour être réutilisables sur d'autres projets d'analyse de données.
### 4. `map_arrondissement.html`
**Carte Interactive des Arbres par Arrondissement**
Fichier HTML contenant une carte interactive (probablement Folium ou Plotly) qui visualise :
- La distribution des arbres par arrondissement de Paris
- Des statistiques agrégées par zone géographique
- Des visualisations interactives permettant de zoomer et d'explorer les données
- Potentiellement des indicateurs sur la densité arborée, les espèces dominantes par arrondissement
### 5. `map_lieu.html`
**Carte Interactive des Arbres par Lieu**
Fichier HTML contenant une carte interactive détaillée qui présente :
- La localisation précise des arbres individuels sur le territoire parisien
- Des marqueurs interactifs avec informations détaillées (espèce, hauteur, circonférence)
- Des filtres pour explorer les arbres par caractéristiques
- Des clusters pour améliorer la lisibilité dans les zones à forte densité
### 6. `p2-trees-fr.csv`
**Dataset des Arbres Parisiens**
Fichier CSV contenant les données brutes ou légèrement prétraitées sur les arbres de Paris, incluant :
- Identifiants et références
- Localisation (coordonnées GPS, arrondissement, adresse)
- Caractéristiques botaniques (genre, espèce, variété, nom français)
- Dimensions (hauteur en mètres, circonférence en cm)
- Statut (remarquable ou non, domanialité)
- Date de plantation
## 🛠️ Technologies utilisées
- **Python** : Langage principal pour l'analyse de données
- pandas, numpy : manipulation et analyse de données
- matplotlib, seaborn : visualisation de données
- folium ou plotly : cartographie interactive
- scipy : tests statistiques
- scikit-learn : détection d'outliers
- **Jupyter Notebook** : développement et documentation
- **HTML/JavaScript** : cartes interactives
## 📈 Méthodologie
1. **Exploration initiale** : Compréhension de la structure et du contenu du dataset
2. **Nettoyage** : Identification et traitement des valeurs manquantes et aberrantes
3. **Analyses univariées** : Étude de la distribution de chaque variable
4. **Analyses bivariées** : Exploration des relations entre variables
5. **Visualisation géographique** : Cartographie interactive des arbres
6. **Documentation** : Présentation des résultats et insights
## 🎯 Résultats attendus
- ✅ Compréhension approfondie du patrimoine arboré parisien
- ✅ Dataset nettoyé et prêt pour des analyses avancées
- ✅ Identification des arbres remarquables et de leurs caractéristiques
- ✅ Insights sur la répartition géographique des arbres
- ✅ Cartes interactives pour explorer les données visuellement
- ✅ Module Python réutilisable pour des analyses similaires
## 💡 Points clés du projet
### Qualité des données
Comme tout jeu de données réel, le dataset présente des défis :
- **Valeurs manquantes** : certaines caractéristiques botaniques incomplètes
- **Valeurs aberrantes** : erreurs de saisie dans les mesures (hauteurs et circonférences impossibles)
- **Incohérences** : variations dans les formats et les appellations
### Arbres remarquables
Une attention particulière a été portée aux arbres remarquables :
- Caractéristiques différenciant les arbres remarquables des autres
- Relations entre dimensions (hauteur, circonférence) et statut remarquable
- Répartition géographique des arbres remarquables
### Analyse géographique
Le projet intègre une dimension spatiale importante :
- Distribution des arbres par arrondissement
- Identification des zones à forte densité arborée
- Cartes interactives pour une exploration intuitive
- Possibilité d'identifier des patterns géographiques
### Biodiversité urbaine
Les analyses révèlent des informations sur la biodiversité :
- Espèces d'arbres les plus fréquentes à Paris
- Diversité botanique par arrondissement
- Âge moyen des arbres (via les dates de plantation)
## 🎓 Compétences développées
- Chargement et exploration de données tabulaires
- Nettoyage de données réelles avec anomalies
- Gestion des valeurs manquantes avec stratégies d'imputation
- Détection et traitement des valeurs aberrantes
- Analyses statistiques univariées et bivariées
- Visualisation de données (graphiques et cartes)
- Développement de fonctions Python réutilisables
- Communication de résultats d'analyse de données
## 👤 Auteur
**Grégoire Mureau**
---
*Ce projet illustre les fondamentaux de l'analyse exploratoire de données : exploration, nettoyage, visualisation et communication d'insights, appliqués à un cas concret d'open data urbain.*

351
data_analysis_functions.py Normal file
View file

@ -0,0 +1,351 @@
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import folium
from folium.plugins import MarkerCluster
import branca
from IPython.display import display
from scipy.spatial import cKDTree
from geopy.distance import geodesic
import numpy as np
def plot_univariate_variable_analysis(
df: pd.DataFrame,
variable_name: str,
graph_type: str,
variable_title_type: str = None,
) -> None:
"""
Plots the distribution of a specified variable in a DataFrame based on the graph type.
Args:
df (pd.DataFrame): The DataFrame containing the data.
variable_name (str): The name of the variable to plot.
graph_type (str): The type of graph to plot (e.g., 'pie', 'heat_map', 'bars', 'hist', 'bars_log').
variable_title_type (str, optional): An optional title type for the variable. Defaults to None.
Raises:
ValueError: If the graph_type is not recognized or if latitude/longitude columns are missing.
"""
if variable_title_type is None:
variable_title_type = graph_type
graph_types = ['pie', 'heat_map', 'bars', 'hist', 'bars_log', 'boxplot']
graph_type = graph_type.lower()
if graph_type not in graph_types:
raise ValueError('Error: wrong graph_type')
if graph_type == 'pie':
value_counts = df[variable_name].value_counts() # Get counts without normalization
total = value_counts.sum() # Total count for percentage calculation
# Group classes with less than 2% into "Others" (using raw counts)
others_count = value_counts[value_counts / total * 100 < 2]
if not others_count.empty:
others_label = f"Others ({', '.join(others_count.index)})"
value_counts = value_counts[value_counts / total * 100 >= 2]
value_counts[others_label] = others_count.sum() # Add the "Others" category
plt.pie(
value_counts,
labels=[f"{label} ({count/total*100:.1f}%)" for label, count in zip(value_counts.index, value_counts.values)], # Percentage in labels
autopct=lambda p: f'{int(p * total / 100)}' # Count on wedges
)
plt.title(f"{variable_name} Distribution")
elif graph_type == 'heat_map':
# Check if the DataFrame contains the necessary geographic columns
if 'geo_point_2d_a' in df.columns and 'geo_point_2d_b' in df.columns and variable_name in df.columns:
# Group by the variable of interest (e.g., 'city', 'region') and count the occurrences in each sector
sector_data = df.groupby([variable_name]).agg({
'geo_point_2d_a': 'mean',
'geo_point_2d_b': 'mean',
}).reset_index()
# Add a "population density" column based on the number of occurrences of the variable in each sector
sector_data['counts'] = df.groupby(variable_name).size().values
# Calculate the occurrence percentages
total_counts = sector_data['counts'].sum()
sector_data['percentage'] = (sector_data['counts'] / total_counts) * 100
# Common zoom level used for city-level maps in Folium (and Leaflet)
zoom_level = 12
# Create a map centered around the average geographic points
m = folium.Map(location=[sector_data['geo_point_2d_a'].mean(), sector_data['geo_point_2d_b'].mean()], zoom_start=zoom_level)
# Normalize population densities for the heat map
min_density = sector_data['counts'].min()
max_density = sector_data['counts'].max()
# Create the colormap with branca
colormap = branca.colormap.LinearColormap(
colors=['yellow', 'orange', 'red'], # Choose a classic heatmap palette (yellow-orange-red)
vmin=min_density, vmax=max_density
).to_step(10) # Divide the colormap into 10 steps for the legend
# Extract coordinates of the sectors
coords = sector_data[['geo_point_2d_a', 'geo_point_2d_b']].values
tree = cKDTree(coords) # Create the k-d tree with the coordinates
# Initialize the minimum distance to a very large number
min_distance = float('inf')
closest_pair = None
# Find the closest pairs
for i, point in enumerate(coords):
# Find the closest neighbor from point i (excluding the point itself)
distances, indices = tree.query(point, k=2) # k=2 to get the closest neighbor and itself
# The second closest will be the nearest (the first is the point itself)
closest_distance = distances[1]
closest_index = indices[1]
# If the found distance is smaller, update the minimum distance and associated points
if closest_distance < min_distance:
min_distance = closest_distance
closest_pair = (point, coords[closest_index])
# Convert the minimum Euclidean distance to geodesic distance for more accuracy
min_distance = geodesic(closest_pair[0], closest_pair[1]).meters
# Limit the circle size to half the smallest distance between two points
max_circle_radius = min_distance / 2
# Average Earth radius in meters
EARTH_RADIUS = 6371000
# Calculate the size of a pixel in meters (at the given average latitude)
lat_rad = np.radians(np.mean(sector_data['geo_point_2d_b']))
meters_per_pixel = (2 * np.pi * EARTH_RADIUS) / (256 * (2 ** zoom_level)) * np.cos(lat_rad)
max_circle_radius_in_pixels = max_circle_radius / meters_per_pixel
# Add each sector as a colored circle based on population density
for _, row in sector_data.iterrows():
color = colormap(row['counts']) # Use the colormap to get the color based on density
# Calculate the size of the marker based on density
# size = np.log(row['population_density'] + 1) * 5 # Logarithmic for a reasonable scale
# size = max(10, min(size, max_circle_radius)) # Limit the size to max_circle_radius
size = max_circle_radius_in_pixels
# Add a circle to the map
folium.CircleMarker(
location=[row['geo_point_2d_a'], row['geo_point_2d_b']],
radius=size, # Circle size based on density and minimum distance
color=color,
fill=True,
fill_color=color, # Fill with the same color
fill_opacity=0.7,
popup=f"{variable_name}: {row[variable_name]}<br>Trees: {row['counts']}<br>Percentage: {row['percentage']:.3}%",
# Allow the size to adjust based on zoom level
zoom_on_click=True
).add_to(m)
# Add the colormap legend to the map
colormap.caption = f"Trees per {variable_name}"
colormap.add_to(m)
# Force the legend to the bottom left corner using CSS style
m.get_root().html.add_child(folium.Element("""
<style>
.leaflet-control-colormap {
position: absolute !important;
bottom: 10px;
left: 10px;
z-index: 1000;
}
</style>
"""))
# Display the map
display(m)
m.save(f'./map_{variable_name}.html')
return # End the function
else:
raise ValueError("The DataFrame must contain 'geo_point_2d_a', 'geo_point_2d_b', and the sector variable to display a map.")
elif graph_type in ['bars', 'bars_log']:
value_counts = df[variable_name].value_counts()
# Group smaller classes into "others" if there are more than 26
if len(value_counts) > 26:
top_categories = value_counts[:25]
others_count = value_counts[25:].sum()
value_counts = pd.concat([top_categories, pd.Series({f'others ({len(value_counts[25:])})': others_count})])
total = value_counts.sum()
plt.figure(figsize=(10, 6))
ax = sns.barplot(x=value_counts.index, y=value_counts.values)
plt.title(f"{variable_name} Distribution")
# Add percentages inside the bars
for p, percentage in zip(ax.patches, value_counts / total * 100):
rotation = 90 if len(value_counts) > 10 else 0
ax.annotate(
f"{percentage:.1f}%",
(p.get_x() + p.get_width() * 0.50, p.get_height() * 0.90),
ha='center', va='center', fontsize=10, rotation=rotation, color='black'
)
plt.xlabel(variable_name)
plt.ylabel('Count')
if graph_type == 'bars_log':
plt.yscale('log')
plt.xticks(rotation=90 if len(value_counts) > 10 else 0) # Rotate labels if too many
plt.show()
elif graph_type == 'hist':
plt.figure(figsize=(10, 6))
# sns.histplot(df[variable_name][df[variable_name] <= df[variable].quantile(0.99)], kde=True)
sns.histplot(df[variable_name], kde=True, color='blue', bins=15)
plt.title(f"{variable_name} Distribution")
plt.xlabel(variable_name)
plt.ylabel('Count')
plt.show()
elif graph_type == "boxplot":
# sns.boxplot(y=df[variable_name][df[variable_name] <= df[variable_name].quantile(0.99)])
sns.boxplot(y=df[variable_name])
plt.title(f"{variable_name} Distribution (Boxplot)")
plt.ylabel(variable_name)
plt.show()
def plot_bivariate_variable_analysis(
df: pd.DataFrame,
variable1_name: str,
variable2_name: str,
graph_type: str,
variable_title_type: str = None,
) -> None:
"""
Plots the distribution of a specified variable in a DataFrame based on the graph type.
Args:
df (pd.DataFrame): The DataFrame containing the data.
variable1_name (str): The name of the first variable to plot. (x)
variable2_name (str): The name of the second variable to plot. (y)
graph_type (str): The type of graph to plot (e.g., 'pie', 'heat_map', 'bars', 'hist', 'bars_log').
variable_title_type (str, optional): An optional title type for the variable. Defaults to None.
Raises:
ValueError: If the graph_type is not recognized or if latitude/longitude columns are missing.
"""
if variable_title_type is None:
variable_title_type = graph_type
graph_types = ['boxplot']
graph_type = graph_type.lower()
if graph_type == "boxplot":
plt.figure(figsize=(8, 6))
sns.boxplot(x=variable1_name, y=variable2_name, data=df)
plt.title(f'Boxplot {variable1_name} vs. {variable2_name}')
plt.xlabel(variable1_name)
plt.ylabel(variable2_name)
plt.show()
def compute_bivariate_stats_indicators(
df: pd.DataFrame,
variable1_name: str,
variable2_name: str,
) -> None:
"""
Computes statistic indicator in order to determine possible correspondance between two variables.
Args:
df (pd.DataFrame): The DataFrame containing the data.
variable1_name (str): The name of the first variable. (x)
variable2_name (str): The name of the second variable. (y)
"""
import statsmodels.formula.api as sm
from statsmodels.stats.anova import anova_lm
import pandas as pd
from scipy.stats import pearsonr
variables_types = {}
variable_names = [variable1_name, variable2_name]
for variable_name in variable_names:
if type(df[df[variable_name].notna()][variable_name].values[0]) == str:
variables_types[variable_name] = 'categorical'
else:
variables_types[variable_name] = 'quantitative'
if ('quantitative' in variables_types.values()) and ('categorical' in variables_types.values()): # ANOVA analysis
print("Both quantitative and categorical variables : ANOVA analysis")
# Build the formula string
if variables_types[variable1_name] == 'quantitative' and variables_types[variable2_name] == 'categorical':
formula_string = f'{variable1_name} ~ C({variable2_name})'
elif variables_types[variable1_name] == 'categorical' and variables_types[variable2_name] == 'quantitative':
formula_string = f'{variable2_name} ~ C({variable1_name})'
# Fit the ANOVA model
model = sm.ols(formula_string, data=df).fit()
anova_table = anova_lm(model, typ=2)
# Extract eta_squared
eta_squared = anova_table['sum_sq'][0] / sum(anova_table['sum_sq'])
result = eta_squared, anova_table
elif 'quantitative' in variables_types.values(): # Pearson correlation analysis
print("Both quantitative variables : Pearson correlation analysis")
# Remove rows with NaN values in either 'remarquable', 'circonference_cm', or 'hauteur_m'
data_for_correlation = df.dropna(subset=[variable1_name, variable2_name])
# Calculate Pearson correlation coefficient between 'remarquable' and 'circonference_cm'
correlation_circonference, p_value_circonference = pearsonr(
data_for_correlation[variable1_name], data_for_correlation[variable2_name]
)
result = correlation_circonference, p_value_circonference
else: # 2 categorical variables, khi2 to add
pass
return result
def compute_distances(df, tree_index):
"""
Computes distances between a specific tree and all other trees in the DataFrame.
Args:
df: DataFrame containing tree data with 'geo_point_2d_a' (longitude) and 'geo_point_2d_b' (latitude) columns.
tree_index: Index of the specific tree in the DataFrame.
Returns:
A pandas Series containing distances to all other trees from the specified tree.
Returns None if the tree_index is out of range or if necessary columns are missing.
"""
# Safety checks
if not all(col in df.columns for col in ['geo_point_2d_a', 'geo_point_2d_b']):
print("Error: 'geo_point_2d_a' or 'geo_point_2d_b' columns not found in DataFrame.")
return None
if tree_index not in df.index:
print(f"Error: tree_index {tree_index} is out of range.")
return None
# Get coordinates of the chosen tree
tree_coords = (df.loc[tree_index, 'geo_point_2d_a'], df.loc[tree_index, 'geo_point_2d_b'])
# Calculate distances to all other trees
distances = []
for index, row in df.iterrows():
other_tree_coords = (row['geo_point_2d_a'], row['geo_point_2d_b']) # Assuming latitude, longitude order
distance = geodesic(tree_coords, other_tree_coords).meters
distances.append(distance)
return pd.Series(distances, index=df.index) # Use original index

727
map_arrondissement.html Normal file

File diff suppressed because one or more lines are too long

159335
map_lieu.html Normal file

File diff suppressed because one or more lines are too long

BIN
soutenance_projet2.pptx Normal file

Binary file not shown.