Étudier les ordres d'insectes en interactions avec les plantes grâce aux données du spipoll¶

Objectifs¶

  • Importer les données
  • Trouver les plantes sur lesquelles il y a le plus de données
  • Vérifier que les données sont équilibrées et qu'il n'y a pas trop de biais pour pouvoir mener l'analyse
  • Comparer les proportions des differents types d'organismes floricoles (insectes et autres) en fonction des plantes

Marche à suivre¶

Chargement des packages¶

Les packages sont des ensembles de fonctions qui nous permettent de faire simplement des opérations complexes. Par exemple des graphiques, des calculs ou encore des cartes avec une syntaxe simplifiée (plus simple que s'il fallait le faire avec les fonctions de base de python).

In [5]:
import pandas as pd # package pour la gestion des données
from matplotlib import pyplot as plt # Pour les graphiques
import seaborn as sns # Pour des graphiques plus complexes (avec plusieurs catégories)
import folium # Pour faire des cartes

Importer les données¶

Pour toute analyse il faut importer les données nécessaires. Nous travaillerons ici sur les données du spipoll. Elle sont stockées à l'adresse suivante : https://depot.vigienature-ecole.fr/datasets/papers/spipoll.csv

Nous vous proposons une ligne de commande pour importer ce ficher depuis internet mais vous pouvez également travailler à partir d'un fichier téléchargé localement.

Attention, le fichier fait environ : 600000 lignes et 20 colonnes, le télécharement peut être un peu long.

In [6]:
# importer les données
# le chargement peut être long
spipoll_data = pd.read_csv('/home/simon/github/datasets/papers/spipoll.csv', low_memory=False)
spipoll_data # afficher l'objet importé (ici le tableau de données)
Out[6]:
Numero_observation Ordre Espece Nombre_individus Latitude Longitude Heure Mois Annee Temperature Vent Nebulosite Plante Famille_plante Pollinisation_plante Fleur_ombragee Type_plante Departement Date_observation
0 2 Hymenoptera Bourdons noirs à bande(s) jaune(s) et cul blanc je n'ai pas l'information 48.844975 2.358313 10 6 2019 03_20-30ºC 02_faible, irrégulier 02_25-50% Acanthe molle Acanthaceae entomogame Oui plantée 75 20/06/2019
1 2 Diptera Mouches à damier entre 2 et 5 48.844975 2.358313 10 6 2019 03_20-30ºC 02_faible, irrégulier 02_25-50% Acanthe molle Acanthaceae entomogame Oui plantée 75 20/06/2019
2 2 Hymenoptera Abeille mellifère entre 2 et 5 48.844975 2.358313 10 6 2019 03_20-30ºC 02_faible, irrégulier 02_25-50% Acanthe molle Acanthaceae entomogame Oui plantée 75 20/06/2019
3 17 Hemiptera Pucerons plus de 5 44.707981 4.667456 11 1 2019 02_10-20ºC 03_faible, continu 01_0-25% Le Sénéçon jacobée NaN NaN Non spontanée 07 03/01/2019
4 17 Hemiptera Pucerons plus de 5 44.707981 4.667456 11 1 2019 02_10-20ºC 03_faible, continu 01_0-25% Le Sénéçon jacobée NaN NaN Non spontanée 07 03/01/2019
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
583445 76214 Diptera Mouches pâles 1 43.555448 0.207342 16 12 2023 02_10-20ºC 01_nul 04_75-100% Euryops (Euryops chrysanthemoides) NaN NaN Non plantée NaN 10/12/2023
583446 76214 Diptera Moustiques, Tipules et autres diptères Nématoc... 1 43.555448 0.207342 16 12 2023 02_10-20ºC 01_nul 04_75-100% Euryops (Euryops chrysanthemoides) NaN NaN Non plantée NaN 10/12/2023
583447 76214 Hemiptera Pentatomes 1 43.555448 0.207342 16 12 2023 02_10-20ºC 01_nul 04_75-100% Euryops (Euryops chrysanthemoides) NaN NaN Non plantée NaN 10/12/2023
583448 76214 Hemiptera Homoptères autres plus de 5 43.555448 0.207342 16 12 2023 02_10-20ºC 01_nul 04_75-100% Euryops (Euryops chrysanthemoides) NaN NaN Non plantée NaN 10/12/2023
583449 76214 Hymenoptera Terebrants Chalcidiens et autres 1 43.555448 0.207342 16 12 2023 02_10-20ºC 01_nul 04_75-100% Euryops (Euryops chrysanthemoides) NaN NaN Non plantée NaN 10/12/2023

583450 rows × 19 columns

Compter le nombre d'observations réalisées par plante¶

Pour commencer, nous allons essayer de comprendre comment les données sont réparties dans la base de données. Dans le Spipoll, les participants et participantes choisissent librement les plantes sur lesquelles ils font leurs observations. Ils doivent noter les plantes sur lesquelles ils font des mesures. Il est donc possible de regarder les plantes les plus fréquentes pour avoir les données les plus fiables possibles.

Ici, nous allons compter le nombre de valeurs uniques de la variable 'Numero_observation' qui permet d'identifier chaque réalisation du protocole grâce à la fonction nunique(), en fonction de la plante et de la famille de la plante grâce à la fonction groupby() et de la liste des colonnes que l'on souhaite garder entre crochet.

Nous allons ensuite les ranger dans l'ordre décroissant avec la fonction sort_values() pour connaitre les plantes les plus vues.

La fonction head() nous permet de ne garder que les premières lignes du jeu de données.

In [18]:
# regrouper les résultats par plante et famille de plante et compter le nombre de valeurs unique de numero d'observation
result = spipoll_data.groupby(['Plante','Famille_plante'], as_index=False)['Numero_observation'].nunique()

# renommer la colonne résultat de la ligne précédente
result = result.rename(columns={'Numero_observation': 'Nombre_observation_unique'})

# ranger les résultats dans l'ordre décroissant pour choisir des plantes avec beaucoup de données
result_sorted = result.sort_values(by='Nombre_observation_unique', ascending=False)
# afficher les 10 premiers résultats
result_sorted.head(10)
Out[18]:
Plante Famille_plante Nombre_observation_unique
751 Le Romarin Lamiaceae 918
739 Le Lierre grimpant Araliaceae 839
232 Carotte sauvage Apiaceae 826
6 Achillée millefeuille Asteraceae 610
723 Le Buddléia de David Scrophulariaceae 594
1074 Pâquerette Asteraceae 526
714 Laurier tin Adoxaceae 404
716 Lavande Lamiaceae 389
145 Berce des prés Apiaceae 387
749 Le Prunier épineux Rosaceae 355

Sélectionner les plantes sur lesquelles nous allons travailler¶

Pour cet exemple, nous allons travailler sur le Romarin, le Buddléia de David et la carotte sauvage respectivement 1ère, 3éme et 5ème plantes les plus étudiées dans le spipoll (vous pourrez changer facilement les plantes en faisant des rechercher/remplacer dans votre propre document).

Nous allons créer un nouveau jeu de données en utilisant une technique de filtre classique pour les data.frame créés avec le package pandas.

In [10]:
# Filtrer le DataFrame pour ne conserver que les lignes où 'Plante' est égale à 'Le Buddléia de David' ou 'Carotte sauvage' ou 'Le Romarin'
spipoll_plante_data = spipoll_data[(spipoll_data['Plante'] == 'Le Buddléia de David') | (spipoll_data['Plante'] == 'Carotte sauvage') | (spipoll_data['Plante'] == 'Le Romarin')]

Vérifier que les données sont comparables¶

Avant de nous lancer dans une analyse plus poussée, nous allons vérifier que les données sont comparables. Il faut qu'elles soient comparables dans le temps (au sein d'une année et entre année) et au niveau spatial au moins partiellement.

Pour cela, nous allons effectuer une série de graphiques et de cartes.

Commençons par le mois :

Nous allons utiliser à nouveau la fonction groupby

In [11]:
# observation des données pour savoir si elles sont comparables
# dans le temps
# en fonction du mois
res_mois = spipoll_plante_data.groupby(['Plante','Mois'], as_index=False)['Numero_observation'].nunique()

# renommer la colonne résultat de la ligne précédente
res_mois = res_mois.rename(columns={'Numero_observation': 'Nombre_observation_unique'})


plt.figure(figsize=(8, 6)) # Définir la taille du graphique (largeur et hauteur)
sns.lineplot(x='Mois', # colonne à utiliser pour l'axe des x
             y='Nombre_observation_unique', # colonne à utiliser pour l'axe des y
             hue='Plante', # Créer une ligne par plante
             data=res_mois, # nom du tableau de données à utiliser
             marker='o', # code du style 
             palette={'Le Buddléia de David':'purple',
                      'Carotte sauvage':'grey',
                     'Le Romarin':'blue'}, # couleurs des lignes en fonction des noms des plantes
            )
plt.xlabel('Mois') # Nom de l'axe des x
plt.ylabel('Nombre d\'observations') # Nom de l'axe des y
plt.title('Nombre d\'observations en fonction du mois') # Ajouter un titre au graphique
plt.show() # Afficher le graphique

Nous pouvons voir sur ce graphique que les données du Romarin sont très différentes des données de la carotte sauvage et du buddléia. Il faudra donc en tenir compte dans la suite de notre analyse. En effet les résultats seront peut-être le résultat de la différence de phénologie des groupes d'espèces (les papillons par exemple sont plus nombreux en été).

Nous pouvons donc maintenant passer à la comparaison des années.

In [23]:
# dans le temps
# en fonction de l'année
res_annee = spipoll_plante_data.groupby(['Plante','Annee'], as_index=False)['Numero_observation'].nunique()
res_annee
plt.figure(figsize=(8, 6)) # Width and Height of the chart
sns.lineplot(x='Annee',
             y='Numero_observation',
             hue='Plante', # Create 2 line plots according to labels in 'z'
             data=res_annee,
             marker='o', # Style used to mark the join between 2 points
             palette={'Le Buddléia de David':'purple',
                      'Carotte sauvage':'grey',
                     'Le Romarin':'blue'}, # Colors of the lines
            )
plt.xlabel('Mois') # x-axis name
plt.ylabel('Nombre d\'observations') # y-axis name
plt.title('Nombre d\'observations en fonction de l\'année') # Add a title
# dans l'espace
Out[23]:
Text(0.5, 1.0, "Nombre d'observations en fonction de l'année")

Ici les résultats sont beaucoup plus proche pour les 3 plantes. La carotte sauvage est un peu différente des deux autres avec moins d'occurence au début de la période. ATTENTION : cette brusque augmentation est majoritairement due à un changement de referentiel pour les plantes (maintenant les noms des plantes sont issus d'une base de données, ils étaient auparavant saisis manuellement par les utilisateurs qui pouvaient utiliser des noms différents pour nommer la même plante).

Nous allons désormais passer à une comparaison spatiale et il faudra donc réaliser une carte.

Pour rendre cette étape un peu moins répétitive, nous allons utiliser une fonction qui prendra en entrée le jeu de donnée et le nom de la plante que nous souhaitons comparer.

In [29]:
# construction du jeu de donnée pour les carte

res_spacial = spipoll_plante_data.groupby(['Numero_observation','Plante','Latitude', 'Longitude'], as_index=False)['Numero_observation'].nunique()

# fonction pour représenter les points dans des couleurs différentes en fonction des espèces
def get_color(type):
    # si le nom de la plante, renvoyer la couleur correspondante
    if type == 'Le Buddléia de David':
        return 'purple'
    elif type == 'Le Romarin':
        return 'blue'
    else:
        return 'gray'
    
# fonction pour créer la carte
def make_map (data, plante):
    # sélectionner uniquement les données pour la plante qui nous intéresse
    res_spacial = data[data['Plante'] == plante]
    # créer la carte initiale à la moyenne des coordonnées de tous les points
    carte = folium.Map(location=[res_spacial['Latitude'].mean(), res_spacial['Longitude'].mean()], zoom_start=6)

    # Ajouter des marqueurs à la carte pour chaque participation
    for index, row in res_spacial.iterrows():
        folium.CircleMarker(
            location=[row['Latitude'], row['Longitude']], # les coordonnées
            radius=5,  # Taille du cercle
            color=get_color(row['Plante']), # couleur en fonction de la colonne plante
            fill_color=get_color(row['Plante']), # appliquer la fonction couleur proposée plus haut
            fill_opacity=0.6, # ajouter de la transparence (0 = totalement transparent 1 = totalement opaque)
        ).add_to(carte)
    return(carte)
In [30]:
make_map(res_spacial, 'Le Buddléia de David')
Out[30]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [31]:
make_map(res_spacial, 'Carotte sauvage')
Out[31]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [32]:
make_map(res_spacial, 'Le Romarin')
Out[32]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Ici aussi, le romarin semble avoir une répartition plus dense au sud même si cela reste assez équilibré à première vue.

Nous allons donc regarder les résultats sur ces trois plantes en gardant à l'esprit les résultats observés. Nous allons compter le nombre de fois où l'on observe un insecte selon son ordre et en fonction de l'espèce de plante.

In [34]:
Nombre_observations=spipoll_plante_data.groupby(['Plante', 'Ordre'], as_index = False ).size()

# renommer la colonne résultat de la ligne précédente
Nombre_observations = Nombre_observations.rename(columns={'size': 'Nombre_insectes'})

# Set the figure size
plt.figure(figsize=(8, 6))

# grouped barplot
ax = sns.barplot(x="Ordre", y="Nombre_insectes", hue="Plante", data=Nombre_observations, errorbar=None);
ax.set(xlabel='Ordre', ylabel='Nombre d\'observations')
plt.show()

Ce premier graphique n'est pas satisfaisant. On ne peut pas lire les ordres d'insectes. Mais plus important, on représente le nombre de protocoles réalisés. Comme ce nombre varie, une comparaison est donc une mauvaise idée. Nous allons donc travailler sur des proportions.

La première étape est de calculer le nombre de protocole réalisés.

In [36]:
res_plante = Nombre_observations.groupby('Plante', as_index=False).sum('Nombre_insectes')

# renommer la colonne résultat de la ligne précédente
res_plante = res_plante.rename(columns={'Nombre_insectes': 'Nombre_insectes_plante'})

res_plante
Out[36]:
Plante Nombre_insectes_plante
0 Carotte sauvage 25111
1 Le Buddléia de David 8656
2 Le Romarin 6187

Il faut ensuite fusionner les résultats (faire une jointure) et diviser les nombre d'insectes observés par ordre par le nombre total d'insectes observés par plante.

In [38]:
res_proportion = Nombre_observations.merge(res_plante, on='Plante')
res_proportion['proportion'] = res_proportion['Nombre_insectes'] / res_proportion['Nombre_insectes_plante']
In [39]:
# Set the figure size
plt.figure(figsize=(8, 6))

# grouped barplot
ax = sns.barplot(x="Ordre", y="proportion", hue="Plante", data=res_proportion, errorbar=None);
ax.set(xlabel='Ordre', ylabel='Proportion')
plt.xticks(rotation=30, ha='right', rotation_mode='anchor')
plt.tight_layout()
plt.show()

Pour un graphique plus lisible, nous n'allons garder que les 4 groupes les plus abondants.

In [40]:
# garder les pollinisateurs les plus courants
res_proportion_red = res_proportion[res_proportion['Ordre'].isin(['Coleoptera', 'Diptera', 'Hymenoptera', 'Lepidoptera'])]
# Set the figure size
plt.figure(figsize=(8, 6))

# grouped barplot
ax = sns.barplot(x="Ordre", y="proportion", hue="Plante", data=res_proportion_red, errorbar=None);
ax.set(xlabel='Ordre', ylabel='Proportion')
plt.tight_layout()
plt.show()
In [16]:
# Evolution des proportions des insectes en fonction du mois de l\année

def plot_mois(insect_order):
    # filtrer pour une plante
    data_carotte = spipoll_data
    # garder les pollinisateurs les plus courants
    data_carotte_poll = data_carotte[data_carotte['Ordre'].isin(['Coleoptera', 'Diptera', 'Hymenoptera', 'Lepidoptera'])]
    data_carotte_poll_mois = data_carotte_poll.groupby(['Ordre', 'Mois'], as_index = False).size()


    data_carotte_poll_mois_sum = data_carotte_poll_mois.groupby('Mois', as_index=False).sum('size')
    # passage en proportion
    data_carotte_poll_mois_proportion = data_carotte_poll_mois.merge(data_carotte_poll_mois_sum, on='Mois')
    data_carotte_poll_mois_proportion['proportion'] = data_carotte_poll_mois_proportion['size_x'] / data_carotte_poll_mois_proportion['size_y']
    data_carotte_poll_mois_proportion = data_carotte_poll_mois_proportion[data_carotte_poll_mois_proportion['Ordre'].isin(insect_order)]
    # grouped barplot
    ax = sns.barplot(x="Mois", y="proportion", hue="Ordre", data=data_carotte_poll_mois_proportion, errorbar=None);
    ax.set(xlabel='Mois', ylabel='Nombre d\'observations')
    plt.xticks(rotation=30, ha='right', rotation_mode='anchor')
    plt.tight_layout()
    plt.show()
In [17]:
plot_mois(['Coleoptera'])
plot_mois(['Diptera'])
plot_mois(['Hymenoptera'])
plot_mois(['Lepidoptera'])


# Adjust layout
plt.tight_layout()
<Figure size 640x480 with 0 Axes>
In [ ]: