Importation des librairies¶

In [1]:
import pandas as pd
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
import numpy as np
import plotly.io as pio
import plotly
import os

Importation des données¶

In [2]:
df = pd.read_csv('formatted_data.csv')  # dernière mise à jour le 12 avril 2022 à 16h00.

dfHistPivot = None
if os.path.exists('./dfHistPivot.csv'):
    dfHistPivot = pd.read_csv('dfHistPivot.csv')
    dfHistPivot.set_index

Préparations des données¶

In [3]:
global counter
def modeEntity(x):
    if x.mode().shape[0] > 1:
        global counter
        counter +=1
        return x.iloc[-1]
    else:
        return x.mode().iat[0]
In [4]:
df = df[df['fiscYear'].apply(lambda x : x >= 2015)]
dfPCQ = df[df['entity'] == 'P.C.Q./C.P.Q.']
dfMP = df[df['entity'].isin(['C.A.Q.- É.F.L.', 'P.C.Q./C.P.Q.','P.L.Q./Q.L.P.', 'P.Q.', 'Q.S.'])]
df = df[['firstName', 'lastName', 'city', 'postalCode', 'entity', 'amount', 'fiscYear']]



# Une personne unique s'identifie par son prenom, nom, ville et code postal
idPerson = ['firstName', 'lastName', 'city', 'postalCode']


#trouvons le parti pour lequel une personne a donnée le plus de fois dans une année et ce, pour chaque année
if dfHistPivot is None:
    #takes 10-30 sec
    dfHistPivot = dfMP.pivot_table(index=idPerson, columns='fiscYear', values='entity',aggfunc=lambda x: x.mode().iat[0])
    counter = 0
    dfHistPivot = dfMP.pivot_table(index=idPerson, columns='fiscYear', values='entity',aggfunc=modeEntity)
    pourc_odd_donation_in_one_year = counter / (dfHistPivot.shape[0] * dfHistPivot.shape[1]) * 100
    dfHistPivot.to_csv('dfHistPivot.csv')
else:
    dfHistPivot.set_index(idPerson, inplace = True)

dfHistPivot.rename(columns=lambda x : float(x), inplace = True)
In [5]:
def nouveau(dons:list[str])->bool:
    """Regarde si toutes les donations sont de 2019 a 2022.
    si oui, retourne True, si non, retourne False"""
    cycle1AllNull = dons.loc[2015:2018].isna().values.all() == True # toutes les valeurs sont nulles dans le premier cycle.
    cycle2NotAllNull = dons.loc[2019:2022].isna().values.all() != True # Il y a au moins une valeur qui n'est pas null dans le deuxieme cycle
    if cycle1AllNull and cycle2NotAllNull:
        return True
    else:
        return False

def fidele_fragile_volatile(dons:list[str], nouveau:bool):
    """Identifie un donateur fidele, fragile ou volatile:
    un donateur fidele a donnée 100% au même parti et plus d'une fois.
    un donateur fragile donne 1 fois seulement
    un donateur volatile donne à plusieurs partis
    """
    entity = dons.loc[2015:2022].dropna().iloc[-1]
    # cas nouveau
    if nouveau:
        allSameEntity = np.all(dons.loc[2019:2022].dropna() == entity)
        n_dons = dons.loc[2019:2022].dropna().shape[0]
        # cas fidele
        if allSameEntity and n_dons > 1:
            return 'fidele'
        # cas fragile
        elif n_dons == 1:
            return 'unique'
        # cas volatile
        else:
            return 'volatile'
    # cas ancien
    else:
        allSameEntity = np.all(dons.loc[2015:2022].dropna() == entity)
        n_dons = dons.loc[2015:2022].dropna().shape[0]
        # cas fidele
        if allSameEntity and n_dons > 1:
            return 'fidele'
        # cas fragile
        elif n_dons == 1:
            return 'unique'
        # cas volatile
        else:
            return 'volatile'
        
def format_type_label(nouveau:bool, f_f_v:str):
    label = ''
    if nouveau:
        return 'nouveau_' + f_f_v
    else:
        return 'ancien_' + f_f_v
    
def ancienEntite(row:list[str]):
    entite = row['dernierDon']
    if row['type'] == 'volatile':
        return row.loc[2015:2022].dropna().iloc[-2]
    else:
        entite
In [6]:
sumDonsYearMP = pd.pivot_table(dfMP, index=['fiscYear', 'entity'], values=['amount'], aggfunc='sum')
nbrDonsYearMP = pd.pivot_table(dfMP, index=['fiscYear', 'entity'], values=['amount'], aggfunc='count')
meanDonsYearMP = pd.pivot_table(dfMP, index=['fiscYear', 'entity'], values=['amount'], aggfunc='mean')
modeDonsYearMP = pd.pivot_table(dfMP, index=['fiscYear', 'entity'], values=['amount'], aggfunc=pd.Series.mode)

pivotDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc=['count', 'sum', 'mean'])
dfPCQ['fiscYear'] = dfPCQ['fiscYear'].astype(str)
nbrDonsCarte = dfPCQ.groupby(['latitude', 'longitude', 'city', 'fiscYear']).count()['amount']
sumDonsCarte = dfPCQ.groupby(['latitude', 'longitude', 'city', 'fiscYear']).sum()['amount']
In [7]:
dfHistPivot['nouveau_ancien'] = dfHistPivot.apply(lambda x : nouveau(x), axis=1)
dfHistPivot['type'] = dfHistPivot.apply(lambda x : fidele_fragile_volatile(x.loc[2015:2022], x.loc['nouveau_ancien']), axis=1)
dfHistPivot['label_type'] = dfHistPivot.loc[:,['nouveau_ancien','type']].apply(lambda x : format_type_label(x.loc['nouveau_ancien'], x.loc['type']), axis=1)
dfHistPivot['dernierDon'] = dfHistPivot.loc[:,2015.0:2022.0].apply(lambda x : x.dropna().iloc[-1], axis=1)
dfHistPivot['ancienEntite'] = dfHistPivot.loc[:,:].apply(lambda x : ancienEntite(x), axis=1)
In [8]:
typeDonateurs = dfHistPivot.groupby(['dernierDon', 'label_type']).size()
typeDonateurs.name = 'nbrType'
typeDonateurs = typeDonateurs.reset_index()
typeDonateurs['pourcentageType'] = typeDonateurs.apply(lambda row : row.loc['nbrType'] *100/typeDonateurs[typeDonateurs['dernierDon'] == row.loc['dernierDon']]['nbrType'].sum(), axis=1)
In [9]:
typeDonateurRaw = dfHistPivot.groupby(['dernierDon', 'nouveau_ancien', 'type']).size()
typeDonateurRaw.name = 'nbrType'
typeDonateurRaw = typeDonateurRaw.reset_index()
typeDonateurRaw = typeDonateurRaw.replace({True:'Nouveau', False:'Ancien'})
typeDonateurRaw['pourcentageType'] = typeDonateurs.loc[:,'pourcentageType']
In [10]:
typeDonateurRaw.to_csv('typeDonateur.csv', index=False)
In [11]:
sumDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc='sum')
nbrDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc='count')
meanDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc='mean')
modeDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc=pd.Series.mode)

pivotDonsYearMP = pd.pivot_table(dfMP, index=['entity', 'fiscYear'], values=['amount'], aggfunc=['count', 'sum', 'mean'])
dfPCQ['fiscYear'] = dfPCQ['fiscYear'].astype(str)
nbrDonsCarte = dfPCQ.groupby(['latitude', 'longitude', 'city', 'fiscYear']).count()['amount']
sumDonsCarte = dfPCQ.groupby(['latitude', 'longitude', 'city', 'fiscYear']).sum()['amount']

(231163, 7)

Méthodologie¶

Source des données¶

Les données proviennent du site d'élection Québec. Elles ont été extraitent grâce à un code développer pour ce projet répertoire Github.

Descriptions des données¶

La source de données permet d'obtenir de l'information sur tous les donateurs depuis l'années 2000. Toutefois, nous avons accès au code postal et à la ville pour tous les donateurs depuis 2013 exclusivement. Pour cette raison, nous avons limité notre analyse de 2014 au 17 mars 2022. Une dernière mise à jour des données sera faite avant la publication.

Pour chaque dons, nous avons accès aux informations suivantes :

firstName lastName amount nbrPayment entity fiscYear postalCode city
Émilie A Lachance 100.0 1 P.L.Q./Q.L.P. 2016 H2S2C5 Montréal
Félix A-Papineau 100.0 1 P.L.Q./Q.L.P. 2015 J0W1C0 Ferme-neuve
Miriam Aaron 100.0 1 P.L.Q./Q.L.P. 2013 H3G1L2 Montréal
Miriam Aaron 50.0 1 P.L.Q./Q.L.P. 2014 H3G1L2 Montréal
... ... ... ... ..... ... ... ...

On pivote les données afin d'observer le comportement de vote pour chaque donateur en fonction du temps¶

firstName lastName city postalCode 2015.0 2016.0 2017.0 2018.0 2019.0 2020.0 2021.0 2022.0
Alain Beaulieu Lefebvre J0H 2C0 P.C.Q./C.P.Q.
Alain Mattard Montréal H1R 3A3 P.C.Q./C.P.Q. P.C.Q./C.P.Q. P.C.Q./C.P.Q.
Alain Rochon Cayamant J0X 1Y0 Q.S. P.C.Q./C.P.Q. P.C.Q./C.P.Q.
Alan Wallis Saint-denis-de-brompton J0B 2P0 C.A.Q.- É.F.L. C.A.Q.- É.F.L. C.A.Q.- É.F.L. C.A.Q.- É.F.L. P.C.Q./C.P.Q.

Comment on obtient la séquences de don par années¶

Une fois que l'on pivote les données sur le prenom, nom, code postal et ville, il nous est possible d'avoir accès à tous les donateurs distincts et aux partis auquels ils ont fait le plus de dons dans une année.

Par exemple, si un individu fait 2 dons pour le PQ en 2017 et 1 pour le PLQ, la colonne sera PQ pour ce donateur en 2017.

À égalité, le don effectué au dernier parti est conservé. Par exemple, si une personne donne pour PQ,PLQ,PQ,PLQ en 2018, la valeur PLQ sera inscrite dans la colonne 2018 pour ce donateur.

Le dernier scénario s'est produite 1851 fois sur les 873960 valeurs du tableau. Considérant que nous observons 109245 donateurs unique sur 8 années différentes, il s'agit d'un scénario s'étant produit 0.2% de fois. Il ne s'agit donc pas d'une limite importante et n'influencera que très peu les conclusions de cette analyse.

Définition des différents profils de donateur¶

Avec notre analyse, on aimerait pouvoir détecter si les partis ont attiré des nouveaux donateurs le début du dernier cycle électoral (2019), ou bien s'il s'agissait de donateur qui avait déjà l'habitude de donner au dernier cycle [2015-2018].

On aimerait pouvoir assigner un donateur à un parti X, pour se faire, on regarde le dernier don qu'il a fait et on l'assigne à ce parti.

Ensuite, on détermine s'il s'agit d'un nouveau ou d'un ancien donateur avec les définitions suivantes:

Nouveau donateur : Donateur n'ayant pas donné entre 2015 et 2018 inclusivement et ayant donné au moins une fois entre 2019 et 2022

Ancien donateur : Quelqu'un qui n'est pas un nouveau donateur à nécessairement donné entre 2015 et 2018 ce qui en fait un ancien. Pour les donateurs ayant données au parti X en dernier, on s'intéresse au profil de donateur pour ce parti:

On aimerait également savoir s'il s'agit de personne qui donne fidèle, s'il s'agit d'une personne ayant fait un don unique ou ayant des habidudes de dons variés. On définit pour les nouveaux et les anciens une spécification supplémentaire :

Donateur fidèle: Un donateur ayant donné plus d'un don et tous les dons sont pour le même parti.

Donateurs unique : Un donateur ayant donné qu'une seule fois.

Donateurs volatile : Il s'agit d'un donateur qui n'est pas fièle ou pas unique. On peut imaginer quelqu'un qui a donné plus d'une fois, mais pas toujours pour le même parti entre 2015 et 2022 (Donateur ancien et volatile) ou entre 2019 et 2022 (Donateur nouveau et volatile).

Résultats¶

1 - Combien de donateurs uniques¶

Il y a 109245 donateurs distincts entre 2015 et 2022

In [12]:
dfHistPivot.shape[0]
Out[12]:
109245

Est-ce qu'il s'agit de nouveau donateur?¶

In [13]:
(
    dfHistPivot.nouveau_ancien.value_counts()
    .rename(index={False:'Ancien', True:'Nouveau'})
)
Out[13]:
Ancien     72562
Nouveau    36683
Name: nouveau_ancien, dtype: int64
In [14]:
typeDonateurRaw.groupby(['dernierDon', 'nouveau_ancien']).sum()
Out[14]:
nbrType pourcentageType
dernierDon nouveau_ancien
C.A.Q.- É.F.L. Ancien 12020 51.565852
Nouveau 11290 48.434148
P.C.Q./C.P.Q. Ancien 691 5.174480
Nouveau 12663 94.825520
P.L.Q./Q.L.P. Ancien 20870 84.333455
Nouveau 3877 15.666545
P.Q. Ancien 28417 86.305655
Nouveau 4509 13.694345
Q.S. Ancien 10564 70.861283
Nouveau 4344 29.138717
In [15]:
plotly.offline.init_notebook_mode()

px.bar(typeDonateurRaw.groupby(['dernierDon', 'nouveau_ancien']).sum().reset_index(),
       x='dernierDon',
       y='pourcentageType',
       color='nouveau_ancien',
       labels= {'dernierDon': 'Parti', 'nbrType':'Nombre de donateur distinct'},
       color_discrete_sequence=["Black", "Green"],
       title = "Pourcentage de nouveau donateur et d'ancien donateur pour chaque parti",
       hover_data=['nbrType', 'dernierDon'],
       barmode='group'
      )