In this notebook, we try to apply an unsupervised learning algorithm to votation profile of every people in order to detect clusters, and observe whether they match with the political partites. To do so, we first create a network with people as nodes, and connect each node to their k (e.g 3) nearest neighbours based on the matrix distance computed previously. The ML algorithm is a spectral clustering algorithm which uses the adjacency matrix of this network.


In [1]:
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
import sklearn
import sklearn.ensemble
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans
import csv

Collect the data


In [2]:
path = '../../datas/nlp_results/'
voting_df = pd.read_csv(path+'voting_with_topics.csv')
print('Entries in the DataFrame',voting_df.shape)

#Dropping the useless column
voting_df = voting_df.drop('Unnamed: 0',1)

#Putting numerical values into the columns that should have numerical values
#print(voting_df.columns.values)

num_cols = ['Decision', ' armée', ' asile / immigration', ' assurances', ' budget', ' dunno', ' entreprise/ finance',
           ' environnement', ' famille / enfants', ' imposition', ' politique internationale', ' retraite  ']
voting_df[num_cols] = voting_df[num_cols].apply(pd.to_numeric)

#Inserting the full name at the second position
voting_df.insert(2,'Name', voting_df['FirstName'] + ' ' + voting_df['LastName'])

voting_df = voting_df.drop_duplicates(['Name'], keep = 'last')
voting_df = voting_df.set_index(['Name'])
voting_df.head(3)


Entries in the DataFrame (1713854, 39)
Out[2]:
BillTitle BusinessNumber BusinessShortNumber BusinessTitle Canton CantonID CantonName Decision DecisionText FirstName ... asile / immigration assurances budget dunno entreprise/ finance environnement famille / enfants imposition politique internationale retraite
Name
Didier Berberat NaN 20073681 7.3681 Simplifier les réglementations de tous les dép... NE 13 Neuenburg 2 Nein Didier ... 0.045455 0.045455 0.045457 0.045459 0.045455 0.045455 0.545439 0.045458 0.045456 0.045455
Attilio Bignasca Arrêté fédéral concernant la ratification des ... 20010083 1.0830 Convention alpine. Protocoles de mise en oeuvre TI 21 Tessin 1 Ja Attilio ... 0.620930 0.007576 0.310887 0.007576 0.007576 0.007576 0.007576 0.007576 0.007576 0.007576
Jasmin Hutter-Hutter Arrêté fédéral concernant la ratification des ... 20010083 1.0830 Convention alpine. Protocoles de mise en oeuvre SG 16 St. Gallen 5 Hat nicht teilgenommen Jasmin ... 0.620930 0.007576 0.310887 0.007576 0.007576 0.007576 0.007576 0.007576 0.007576 0.007576

3 rows × 38 columns


In [3]:
profileMatrixFile = 'profileMatrix.csv'
profileMatrix = pd.read_csv(profileMatrixFile, index_col = 0)
profileArray = profileMatrix.values
print(profileArray.shape)
profileMatrix.head()


(358, 3470)
Out[3]:
Arrêté fédéral concernant la contribution de la Suisse en faveur de la Bulgarie et de la Roumanie au titre de la réduction des disparités économiques et sociales dans l'Union européenne élargie Réduction des disparités économiques et sociales dans l'UE. Contribution de la Suisse en faveur de la Roumanie et de la Bulgarie Renforcement du Traité sur la non-prolifération des armes nucléaires Une zone exempte d'armes nucléaires au coeur de l'Europe Boycott de la liste des terroristes établie par l'ONU Ratification du Protocole de l'ONU sur les armes à feu et mise en oeuvre de l'instrument Thalmann Accompagner la construction du nouvel Etat du Kosovo Renonciation à des projets d'aide au développement menés par l'Etat Meilleure protection juridique pour les défenseurs de l'environnement La Suisse doit oeuvrer pour préserver l'unité de la Macédoine Promouvoir l'Observatoire du Conseil de l'Europe pour le respect des droits de l'homme en Palestine et en Israël ... Améliorer le taux de réussite aux examens de fin d'apprentissage Lutte contre les prix élevés en Suisse. Présenter une version élaguée de la révision de la loi sur les cartels Droit international par la Suisse. Appliquer les règles adoptées pour la Crimée annexée aux territoires occupés de Palestine Simplifier la répartition et le contrôle des aides financières destinées aux associations de consommateurs Analyser l'efficacité des mesures prises pour renforcer la sécurité de l'approvisionnement Préciser les bases légales qui régissent l'allocation d'aides financières aux associations de consommateurs Ne pas défavoriser les étables à stabulation entravée Dettes envers l'assurance-chômage. Que les chefs des entreprises en faillite passent à la caisse Dépistage du cancer Arrêté fédéral relatif à l’initiative populaire «Réparation de l’injustice faite aux enfants placés de force et aux victimes de mesures de coercition prises à des fins d’assistance (initiative sur la réparation)» Réparation de l’injustice faite aux enfants placés de force et aux victimes de mesures de coercition prises à des fins d’assistance (Initiative sur la réparation). Initiative populaire et contre-projet indirect
Chiara Simoneschi-Cortesi 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ... -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0
Pascale Bruderer Wyss 0.0 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ... -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0
Guy Parmelin 1.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0 1.0 1.0 ... -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0
Jean-René Germanier 0.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0 ... -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0
Edi Engelberger 0.0 1.0 1.0 1.0 1.0 1.0 0.0 1.0 0.0 1.0 ... -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0

5 rows × 3470 columns


In [24]:
distanceMatrixFile = 'distanceMatrix.csv'
distances = pd.read_csv(distanceMatrixFile, index_col = 0)
distances = distances.replace(-0.001, 0)
distancesArray = distances.values
print(distancesArray.shape)
distances.head()


(358, 358)
Out[24]:
Chiara Simoneschi-Cortesi Pascale Bruderer Wyss Guy Parmelin Jean-René Germanier Edi Engelberger Andreas Brönnimann Jakob Büchler Eric Voruz Edith Graf-Litscher Jacques Neirynck ... Jacques Nicolet Marco Chiesa Thomas Ammann Andrea Gmür-Schönenberger Jean-Luc Addor Manfred Bühler Tim Guldimann Christoph Eymann Werner Salzmann Magdalena Martullo-Blocher
Chiara Simoneschi-Cortesi 0.000000 0.458336 0.609746 0.440706 0.518711 0.626783 0.505388 0.499799 0.492113 0.439109 ... 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000
Pascale Bruderer Wyss 0.458336 0.000000 0.682856 0.497788 0.590861 0.688270 0.609910 0.379554 0.363349 0.471073 ... 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000
Guy Parmelin 0.609746 0.682856 0.000000 0.536323 0.526200 0.315147 0.508120 0.780699 0.799681 0.666520 ... 0.369855 0.379299 0.578710 0.562172 0.423374 0.414934 0.658758 0.527543 0.417766 0.420579
Jean-René Germanier 0.440706 0.497788 0.536323 0.000000 0.420912 0.544931 0.463815 0.613330 0.614832 0.512094 ... 0.549063 0.575224 0.428746 0.461774 0.562296 0.549063 0.618347 0.500000 0.575224 0.568796
Edi Engelberger 0.518711 0.590861 0.526200 0.420912 0.000000 0.543272 0.437048 0.695664 0.671299 0.582310 ... 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000

5 rows × 358 columns

Build adjacency matrix

We compute for each row the k entries with lowest distance, and put 1 for each of them, and 0 elsewhere.


In [78]:
k = 4 # number of nearest neighbours that we take into account in the adjacency matrix
for i in distances:
    d = distances.loc[i]
    np.sort(d)
    threshold = d[k-1]
    for j in distances:
        if distances.loc[i][j] > threshold:
            distances.loc[i][j] = 0
        else:
            distances.loc[i][j] = 1

distances.head()


Out[78]:
Chiara Simoneschi-Cortesi Pascale Bruderer Wyss Guy Parmelin Jean-René Germanier Edi Engelberger Andreas Brönnimann Jakob Büchler Eric Voruz Edith Graf-Litscher Jacques Neirynck ... Jacques Nicolet Marco Chiesa Thomas Ammann Andrea Gmür-Schönenberger Jean-Luc Addor Manfred Bühler Tim Guldimann Christoph Eymann Werner Salzmann Magdalena Martullo-Blocher
Chiara Simoneschi-Cortesi 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Pascale Bruderer Wyss 1.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 1.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Guy Parmelin 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
Jean-René Germanier 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Edi Engelberger 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0

5 rows × 358 columns

Spectral Clustering

We now apply the clustering algorithm to the adjacency matrix. This matrix is likely not to be symmetric, but the algorithm will symmetrize it, which does make sense in this case.


In [113]:
nbClust = 4
clusterDist = sklearn.cluster.spectral_clustering(affinity = distances.values, n_clusters = nbClust)
clusterDist


/home/paul/anaconda2/lib/python3.5/site-packages/sklearn/utils/validation.py:640: UserWarning: Array is not symmetric, and will be converted to symmetric by average with its transpose.
  warnings.warn("Array is not symmetric, and will be converted "
Out[113]:
array([1, 1, 0, 2, 2, 0, 2, 1, 1, 1, 0, 2, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
       0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 2, 1, 0, 1, 0, 1, 0,
       1, 0, 1, 0, 1, 0, 1, 2, 1, 0, 1, 0, 1, 0, 1, 3, 1, 3, 1, 2, 1, 2, 1,
       2, 1, 2, 1, 2, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 2,
       1, 2, 1, 3, 1, 0, 1, 1, 1, 3, 1, 2, 1, 3, 1, 3, 2, 0, 2, 0, 2, 0, 2,
       0, 2, 0, 2, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 3,
       1, 3, 1, 3, 1, 3, 1, 2, 1, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 0, 0, 2,
       0, 2, 0, 2, 0, 0, 1, 0, 1, 2, 1, 2, 1, 3, 1, 0, 2, 0, 2, 2, 2, 2, 2,
       2, 2, 2, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 2, 1, 0, 1, 1, 1, 0, 2, 1,
       1, 1, 1, 0, 2, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 2, 2, 2, 3, 1, 0, 3,
       0, 2, 3, 1, 1, 1, 1, 2, 1, 1, 2, 3, 1, 2, 2, 1, 1, 0, 2, 0, 1, 0, 2,
       2, 1, 2, 2, 1, 0, 3, 0, 1, 2, 2, 1, 2, 0, 0, 1, 3, 1, 1, 3, 2, 1, 2,
       2, 1, 1, 3, 0, 3, 0, 2, 2, 2, 0, 1, 2, 0, 2, 3, 3, 3, 2, 0, 2, 3, 2,
       1, 0, 1, 2, 3, 0, 0, 3, 3, 0, 0, 1, 1, 1, 0, 2, 0, 0, 2, 0, 2, 0, 0,
       2, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 3, 3, 3, 3, 3, 3, 3, 3,
       3, 2, 2, 0, 0, 2, 2, 0, 0, 1, 3, 0, 0], dtype=int32)

Analysis of the clustering

We would like to observe whether the obtained clustering spearates well the different political partites. To do so, we compute for each cluster the percentage of people in each partite.


In [121]:
ratio_df = pd.DataFrame(index = voting_df.ParlGroupName.unique())
ratio_df['ratio'] = 0
np.array(ratio_df.index)


Out[121]:
array(['Groupe socialiste', 'Groupe des Paysans, Artisans et Bourgeois',
       'Groupe conservateur-catholique', 'Groupe radical-démocratique',
       'Groupe écologiste', 'Non inscrit', 'Groupe BD',
       "Groupe vert'libéral"], dtype=object)

In [122]:
def ratioPartite(cluster, clusterDist):
    # Compute the partites distribution for all people within this cluster
    people = distances.index[clusterDist == cluster]
    size = len(people)
    ratio_df = pd.DataFrame(index = voting_df.ParlGroupName.unique())
    ratio_df['ratio'] = 1.0
    for group in np.array(ratio_df.index):
        print(group)
        peopleGroup = [p for p in people[voting_df.loc[people].ParlGroupName == group]]
        print(len(peopleGroup) / float(size))
        ratio_df.set_value(group, 'ratio', len(peopleGroup) / float(size))
    return ratio_df

In [126]:
ratio_df = pd.DataFrame(index = voting_df.ParlGroupName.unique(), columns = range(nbClust))
ratio_df[0] = range(8)
ratio_df


Out[126]:
0 1 2 3
Groupe socialiste 0 NaN NaN NaN
Groupe des Paysans, Artisans et Bourgeois 1 NaN NaN NaN
Groupe conservateur-catholique 2 NaN NaN NaN
Groupe radical-démocratique 3 NaN NaN NaN
Groupe écologiste 4 NaN NaN NaN
Non inscrit 5 NaN NaN NaN
Groupe BD 6 NaN NaN NaN
Groupe vert'libéral 7 NaN NaN NaN

In [125]:
ratio_df = pd.DataFrame(index = voting_df.ParlGroupName.unique(), columns = range(nbClust))
for cluster in range(nbClust):
    ratio = ratioPartite(cluster, clusterDist)
    ratio_df[cluster] = ratio.values
    
ratio_df


Groupe socialiste
0.0
Groupe des Paysans, Artisans et Bourgeois
0.9
Groupe conservateur-catholique
0.008333333333333333
Groupe radical-démocratique
0.09166666666666666
Groupe écologiste
0.0
Non inscrit
0.0
Groupe BD
0.0
Groupe vert'libéral
0.0
Groupe socialiste
0.5811965811965812
Groupe des Paysans, Artisans et Bourgeois
0.0
Groupe conservateur-catholique
0.06837606837606838
Groupe radical-démocratique
0.008547008547008548
Groupe écologiste
0.2222222222222222
Non inscrit
0.017094017094017096
Groupe BD
0.0
Groupe vert'libéral
0.10256410256410256
Groupe socialiste
0.047619047619047616
Groupe des Paysans, Artisans et Bourgeois
0.023809523809523808
Groupe conservateur-catholique
0.5595238095238095
Groupe radical-démocratique
0.15476190476190477
Groupe écologiste
0.05952380952380952
Non inscrit
0.0
Groupe BD
0.15476190476190477
Groupe vert'libéral
0.0
Groupe socialiste
0.0
Groupe des Paysans, Artisans et Bourgeois
0.0
Groupe conservateur-catholique
0.0
Groupe radical-démocratique
1.0
Groupe écologiste
0.0
Non inscrit
0.0
Groupe BD
0.0
Groupe vert'libéral
0.0
Out[125]:
0 1 2 3
Groupe socialiste 0.000000 0.581197 0.047619 0.0
Groupe des Paysans, Artisans et Bourgeois 0.900000 0.000000 0.023810 0.0
Groupe conservateur-catholique 0.008333 0.068376 0.559524 0.0
Groupe radical-démocratique 0.091667 0.008547 0.154762 1.0
Groupe écologiste 0.000000 0.222222 0.059524 0.0
Non inscrit 0.000000 0.017094 0.000000 0.0
Groupe BD 0.000000 0.000000 0.154762 0.0
Groupe vert'libéral 0.000000 0.102564 0.000000 0.0

We observe that when we cluster people in 4 clusters, each partites are well separated :

  • cluster 0 : Groupe des Paysans, Artisans et Bourgeois
  • cluster 1 : Groupe socialiste, Groupe écologiste, Groupe vert'libéral
  • cluster 2 : Groupe conservateur-catholique, Groupe BD
  • cluster 3 : Groupe radical-démocratique

Note that we could also separate the data in 3 clusters. In this case, we observe that clusters 2 and 3 are merged together.