In [1]:
# The code was removed by Watson Studio for sharing.

Dados Abertos e Ciência de Dados: Análise da Actividade Parlamentar da XIV Legislatura Portuguesa

Open Data and Data Science: Analysis of the Parliamentary Activity of the 14th Legislature of Portugal


Frederico Mu√Īoz [email protected]


ūüĎČ IBM Data Platform URL: https://dataplatform.cloud.ibm.com/analytics/notebooks/v2/0b23f01c-e55a-4fe4-a2ca-57f2e478bf3e/view?access_token=4cc13fba3f3967530650b11a2956ed27c5cec9042113237f97cb0e4c58d3905f

ūüĎČ Jupyter Viewer: https://nbviewer.jupyter.org/github/fsmunoz/pt-act-parlamentar/raw/master/Actividade%20Parlamentar%20da%20XIV%20Legislatura.ipynb

ūüĎČ How to cite this notebook


Abstract: The application of Data Science methods and analysis to voting records is a well-established practice; in this paper we describe the process of obtaining, cleaning, exploring and choosing adequate analytic methods for the purpose of determining the distance between parties in the Portuguese Parliament. Through the use of institutional Open Data resources we discuss the different approaches possible in terms of clustering and dimension reduction, using DBSCAN, Spectrum Clustering and Multidimensional Scaling to augment the information produced by distance and affinity matrices, and discuss the results of these methods in the available data in a time where the concepts of Left and Right are often hotly debated. A detailed explanation of how an euclidean distance matrix is obtained and how clustering and MDS work using test datasets is also included.


Introdução

ūüĎČ This notebook is currently only fully available in Portuguese; while a future translation is likely in the meantime one can make good use of the fact that the actual code blocks are in English to at least follow through those parts

O posicionamento absoluto e relativo dos v√°rios partidos pol√≠ticos no Parlamento portugu√™s tem sido motivo de interesse redobrado nos √ļltimos anos. A elei√ß√£o de deputados de partidos sem anterior presen√ßa parlamentar tem alimentado o debate cujas implica√ß√Ķes ideol√≥gicas foram v√≠siveis de forma bastante pr√°tica na problem√°tica em torno da escolha de lugares: partidos desagradados com o lugar atribu√≠do (‚ÄúIniciativa Liberal Descontente Com Lugar Atribu√≠do a Deputado No Parlamento - TSF‚ÄĚ 2020), dificuldades gerais em termos de arruma√ß√£o dos deputados (Renascen√ßa 2019), quest√Ķes de ordem mais ou menos pr√°tica em torno de acessos (Almeida 2019), enfim, v√°rias dimens√Ķes para uma quest√£o que acaba por revelar a import√Ęncia simb√≥lica do posicionamento absoluto e relativo de cada partido no hemiciclo.

Esta questão não é particularmente nova (Lourenço 2020), colocando-se em maior ou em menor grau com a entrada de novos partidos e a consequente necessidade de tomada de posição por parte do recém-chegado partido e a harmonização (possível) com os restantes, sendo que a sua posterior actividade parlamentar (nas suas diversas vertentes) poderá ou não alinhar-se com a sua auto-identificação (reflectida ou não nos lugares no hemiciclo).

O ponto de partida para esta an√°lise foi precisamente tentar descobrir se exclusivamente com base na actividade parlamentar, e em concreto no registo de vota√ß√Ķes, √© poss√≠vel estabelecer rela√ß√Ķes de proximidade e dist√Ęncia que permitam um agrupamento que n√£o dependa de classifica√ß√Ķes a priori, e se sim, de que forma estes agrupamentos confirmam ou divergem da percep√ß√£o existente?

A utiliza√ß√£o de dados abertos disponibilizados pelo Parlamento torna esta an√°lise substancialmente mais simples, embora n√£o sem a necessidade de tratamento e valida√ß√£o dos dados; de um ponto de vista pr√°tico este bloco de notas demonstra como aceder e transformar os dados de uma forma que pode ser √ļtil para outras an√°lises. No cen√°rio nacional refer√™ncia para a iniciativa http://hemiciclo.pt que, em linha com iniciativas europeias semelhantes, fornecesse um interface para um maior escrutinio da actividade parlamentar e um conjunto alargado de indicadores directos e indirectos do maior interesse (Sapage 2020). O presente trabalho tem alguns pontos de contacto com esta iniciativa, dentro dos limites que o seu objectivo pedag√≥gico estabelece.

A combina√ß√£o de dados abertos com um bloco de notas Jupyter permite que o leitor tenha visibilidade dos v√°rios passos e transforma√ß√Ķes (Randles et al. 2017), o que pode por vezes apresentar uma excessiva complexidade para quem n√£o tenha familiaridade com programa√ß√£o; tent√°mos obviar esta limita√ß√£o atrav√©s da descri√ß√£o das v√°rias ac√ß√Ķes de forma a que se possa seguir a l√≥gica e fruir dos resultados. Esta transpar√™ncia assume uma dimens√£o adicional tendo em conta a tem√°tica que nos proposmos analisar, embora seja importante de forma tranversal (sobre a import√Ęncia da repetibilidade, rastreabilidade, acesso e o papel de blocos Jupyter no contexto de open science ver, entre outros, exemplos em ecologia (Powers and Hampton 2019) astronomia (Wofford et al. 2019)).

A plataforma utilizada para o desenvolvimento deste trabalho é a IBM Watson Data Platform (https://dataplatform.cloud.ibm.com/), que permite a utilização de notebooks Jupyter no contexto de uma gestão integral do processo de Data Science.

ūüĎČ NB: a visualiza√ß√£o do conte√ļdo deste bloco de notas ter√° varia√ß√Ķes conforme a forma que estiver a ser manipulado; na sua vers√£o "est√°tica" os diagramas e tabelas ser√£o tamb√©m eles est√°ticos. √Č, contudo, feito para poder ser instanciado e executado de forma interactiva, sendo a forma mais r√°pida a cria√ß√£o de uma conta gratuita na IBM Watson Data Platform (com acesso ao Watson Studio) e importa√ß√£o deste trabalho, ou a utiliza√ß√£o de Binder como descrito na sec√ß√£o Execu√ß√£o do bloco de notas.

Metodologia

Com base nos dados disponibilizados pela Assembleia da Rep√ļblica em formato XML (‚ÄúDados Abertos‚ÄĚ 2020) s√£o criadas dataframes (tabelas de duas dimens√Ķes) com base no processamento e selec√ß√£o de informa√ß√£o relativa aos padr√Ķes de vota√ß√£o de cada partido (e/ou deputados n√£o-inscritos).

S√£o fundamentalmente feitas as seguintes an√°lises:

  1. Vista geral das vota√ß√Ķes de cada partido, visualizado atrav√©s de um heatmap
  2. Matriz de dist√Ęncia euclidiana entre todos os partidos e visualiza√ß√£o de clustering hier√°rquico atrav√©s de um dendograma e m√©todo de Ward.
  3. Identificação de grupos (clustering) por DBSCAN e Spectral Clustering, com criação de matriz de afinidade
  4. Redu√ß√£o das dimens√Ķes e visualiza√ß√£o das dist√Ęncias e agrupamentos num espa√ßo cartesiano a duas e tr√™s dimens√Ķes atrav√©s de Multidimensional Scaling (MDS)

A utilidade deste tipo de an√°lise em ci√™ncia pol√≠tica √© reconhecida (Figueiredo Filho et al. 2014) e tem sido aplicada a v√°rios registos de vota√ß√Ķes; a an√°lise presente tem como principal diferen√ßa o ser efectuado sobre as vota√ß√Ķes de partidos e n√£o, como √© mais comum na bibliografia consultada, a deputados individuais.

O tratamento pr√©vio dos dados em formato XML √© feito de forma a seleccionar as vota√ß√Ķes de cada partido (ou deputado n√£o inscrito), num processo com alguma complexidade que √© por isso detalhado em sec√ß√£o pr√≥pria do Ap√™ndice

A informa√ß√£o √© obtida a partir das listas publicadas de vota√ß√Ķes relativas a

  • Actividades
  • Iniciativas

Os dados utilizados s√£o um subconjuntos dos disponibilizados, sendo que qualquer erro ou omiss√£o nos dados originais ir√° ter imediato reflexo nos resultados das an√°lises.

Este trabalho n√£o tem como objectivo principal apenas mostrar os resultados finais, mas tamb√©m (ou fundamentalmente) todo o processo comum em Data Science para que se chegue at√© eles; √© por isso pleno de blocos de c√≥digo que tent√°mos contextualizar com descri√ß√Ķes que tornem a sua compreens√£o dispens√°vel para a quem n√£o interessem esss detalhes, ao mesmo tempo que desvi√°mos para sec√ß√Ķes do Ap√™ndice discuss√Ķes mais extensas de v√°rios passos. Requer, ainda assim, algum esfor√ßo em termos de seguir o caminho (por vezes pejado de desvios, atalhos e retrocessos) at√© aos diversos resultados apresentados.

ūüĎČ NB: O processo de tratamento de dados n√£o √© indiferente para o resultado final: s√£o feitas escolhas a v√°rios n√≠veis (desde a selec√ß√£o dos dados considerados importantes aos algoritmos escolhidos) que t√™m impacto nos resultados, nem que seja por omiss√£o. Mais do que evit√°-lo (o que n√£o seria poss√≠vel), opt√°mos por identificar de forma clara as escolhas feitas e explicar as raz√Ķes que levaram √† sua escolha: cada leitor poder√° assim determinar a razoabilidade de cada uma e, sobretudo, ensaiar novas formas que considere mais adequadas.

Instalação de pré-requisitos

Como primeiro passo definimos alguns valores que dever√£o ser utilizados pelo bloco de notas Jupyter para exibir tabelas e diagramas e algumas bibliotecas essenciais que s√£o importantes para poder executar o bloco de notas de forma independente (nomeadamente em ambientes Jupyter exteriores ao IBM Watson Data Platform como o Binder https://mybinder.org/ ).

In [1]:
!pip install -q  itables matplotlib pandas bs4 html5lib lxml seaborn sklearn pixiedust

%matplotlib notebook

from itables import show
import itables.options as opt

opt.maxColumns=100
opt.maxRows=2000
opt.lengthMenu = [10, 20, 50, 100, 200, 500]

Obtenção, limpeza e tratamento dos dados

Esta fase é fundamental para toda a restante análise: é onde obtemos os dados e os transformamos em informação num formato que pode ser facilmente manipulado.

Obtenção do ficheiro

Na página do Parlamento podem ser obtidos, para cada legislatura, ficheiros em formato JSON ou XML com a informação relativa a cada "área temática". Para a XIV Legislatura estamos interessados em duas dessas áreas:

  • Iniciativas: "...Da lista de tipo de Iniciativas entradas, constam Projetos e Propostas de Lei, Projetos e Propostas de Resolu√ß√£o, Aprecia√ß√Ķes Parlamentares, Inqu√©ritos Parlamentares, Projetos de Revis√£o Constitucional, Projetos de Delibera√ß√£o, Projetos de Regimento, Ratifica√ß√Ķes, sendo as mais usuais, projetos de lei que s√£o apresentados por Deputados, Grupos Parlamentares ou um grupo de cidad√£os, e as propostas de lei apresentadas pelo Governo e pelas Assembleias Legislativas das Regi√Ķes Aut√≥nomas."
  • Actividades: "...Consideram-se Atividades Parlamentares as seguintes atividades: Aprecia√ß√£o de relat√≥rios entregues por entidades externas, Audi√™ncias, Audi√ß√Ķes, Cerim√≥nias, Conta Geral do Estado, Debates, Declara√ß√Ķes Pol√≠ticas, Defesa Nacional, Desloca√ß√Ķes no √Ęmbito das Comiss√Ķes, Desloca√ß√Ķes do Presidente da Rep√ļblica, Elei√ß√£o e composi√ß√£o para √≥rg√£os externos, Eventos no √Ęmbito de Comiss√Ķes, Grandes Op√ß√Ķes do Conceito Estrat√©gico da Defesa Nacional, Interpela√ß√Ķes ao Governo, Mo√ß√Ķes, Or√ßamento e Conta de Ger√™ncia da AR, Orienta√ß√£o da Pol√≠tica Or√ßamental, Perguntas ao Governo, Programa de Estabilidade e Crescimento, Programa do Governo, Relat√≥rios de Seguran√ßa Interna, Seguran√ßa Interna, Votos."

Estas √°reas concentram a maioria (se n√£o a totalidade) das vota√ß√Ķes dos partidos, algo que tendo em conta o objectivo √© fundamental: para a an√°lise proposta s√≥ interessam eventos onde seja poss√≠vel extra√≠r a informa√ß√£o da vota√ß√£o, e dentro destes (como veremos) os que sejam directamente imput√°veis a partidos.

Comen√ßando pelas iniciativas parlamentares, definimos o URL da vers√£o em XML do ficheiro; para o processamento deste formato usaremos Element.Tree, uma das op√ß√Ķes em Python para este fim que vem inclu√≠da de base, pelo que o seguinte bloco descarrega o ficheiro e converte-o numa "√°rvore" (onde os "ramos" e as "folhas" s√£o subdivis√Ķes da informa√ß√£o dado o formato hier√°rquico)

In [2]:
from urllib.request import urlopen
import xml.etree.ElementTree as ET

ini_url = 'http://app.parlamento.pt/webutils/docs/doc.xml?path=6148523063446f764c324679626d56304c3239775a57356b595852684c3052685a47397a51574a6c636e52766379394a626d6c6a6157463061585a68637939595356596c4d6a424d5a57647063327868644856795953394a626d6c6a6157463061585a686331684a566935346257773d&fich=IniciativasXIV.xml&Inline=true'
ini_tree = ET.parse(urlopen(ini_url))

Formato XML via Element.Tree, autoria das iniciativas e transformações preliminares

Esta √°rvore pode ser visualizada abrindo o ficheiro original num editor de c√≥digo que o formate de forma adequada (nomeadamente atrav√©s de indenta√ß√£o dos blocos); trata-se de um ficheiro relativamente grande (~30 MB) e tem um conte√ļdo estruturado da forma habitual para o formato como pode ser observado na seguinte extrac√ß√£o das primeiras linhas:

In [3]:
## Import BeautifulSoup for the pretty-priting
from bs4 import BeautifulSoup

## Get a string with the XML from the root down
xmlstr = ET.tostring(ini_tree.getroot(), encoding='utf8', method='xml')

## Print the first 300 charaters; note that extra tags will be added to make the output "valid"
print(BeautifulSoup(xmlstr[0:300], "xml").prettify())
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut>
 <pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut>
  <iniNr>
   28
  </iniNr>
  <iniTipo>
   A
  </iniTipo>
  <iniDescTipo>
   Apreciação Parlamentar
  </iniDescTipo>
  <iniLeg>
   XIV
  </iniLeg>
  <iniSel>
   1
  </iniSel>
  <data/>
 </pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut>
</ArrayOfPt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut>

O código final é o resultado final de um processo de estudo do formato (com tentativas, avanços e recuos) mas, em termos gerais, o seguinte bloco diz-nos que:

  • A vari√°vel ini_tree √© do tipo xml.etree.ElementTree.ElementTree
  • Na ra√≠z da √°rvore est√° um conjunto (array) de elementos
  • Esses elementos t√™m a etiqueta pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut

Por sua vez estes √ļltimos elementos s√£o constituidos por mais "ramos" e "folhas" que cont√©m informa√ß√£o sobre a iniciativa: quem a prop√īs, a fases, as vota√ß√Ķes e os seus resultados, etc.

In [4]:
print(type(ini_tree))
print(ini_tree.findall("."))
counter = 0
for initiative in ini_tree.findall('.')[0]:
    ## Only provide an example of the first lines
    if counter < 5:
        print(initiative)
    counter += 1
<class 'xml.etree.ElementTree.ElementTree'>
[<Element 'ArrayOfPt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa087dbc28>]
<Element 'pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa087dbb38>
<Element 'pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa08808e58>
<Element 'pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa08310b38>
<Element 'pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa08315868>
<Element 'pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut' at 0x7faa0831ad68>

O total de iniciativas é-nos dados pelo valor acumulado no bloco anterior:

In [5]:
print("Total de iniciativas contidas na √°rvore: ", counter)
Total de iniciativas contidas na √°rvore:  1466

Dada a import√Ęncia para o objectivo proposto √© √ļtil descrever o elemento pt_gov_ar_objectos_VotacaoOut que cont√©m a informa√ß√£o sobre vota√ß√Ķes; podemos come√ßar por contar as entradas na √°rvore:

In [6]:
counter = 0
for c in ini_tree.findall(".//pt_gov_ar_objectos_VotacaoOut"):
    print('.', end='')
    counter += 1
.........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

Tal como anteriormente o acumulador indica-nos o total, neste caso de vota√ß√Ķes:

In [7]:
print("Total de vota√ß√Ķes contidas na √°rvore: ", counter)
Total de vota√ß√Ķes contidas na √°rvore:  2057

O seguinte bloco encontra apenas a primeira ocorrência, para simplificar a discussão dos dados apenas a um caso:

In [8]:
## Get the first occurence of a "VotacaoOut" entry
for c in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut"):
    print("{0:15}: {1}".format(c.tag,c.text))
id             : 88184
resultado      : Aprovado
descricao      : Texto Final apresentado pela Comiss√£o de Administra√ß√£o P√ļblica, Moderniza√ß√£o Administrativa, Descentraliza√ß√£o e Poder Local relativo √†s Aprecia√ß√Ķes Parlamentares n.¬ļs 21/XIV/1.¬™ (PSD); 22/XIV/1.¬™ (BE); e 23/XIV/1.¬™ (PCP)
reuniao        : 76
tipoReuniao    : RP
detalhe        : A Favor: <I>PS</I>, <I>PSD</I><BR>Contra: <I>BE</I>, <I>PCP</I>, <I>CDS-PP</I>, <I>PAN</I>, <I>PEV</I>, <I>CH</I>, Cristina Rodrigues (Ninsc), Joacine Katar Moreira (Ninsc)<BR>Abstenção: <I>IL</I>

Note-se para j√° que existe informa√ß√£o que n√£o est√° presente (nomeadamente a autoria); a autoria da iniciativa pode ser determinada facilmente mas n√£o se relaciona de forma directa com a autoria da mat√©ria em vota√ß√£o, e existem iniciativas sem vota√ß√Ķes associadas (cf. Iniciativas e vota√ß√Ķes)

Votações: de descrição textual a informação estruturada

√Č importante notar que o o resultado da vota√ß√£o est√° em "texto livre", o que nos remete para a diferen√ßa entre dados estruturados e n√£o-estruturados [DataStructureData2018]; de forma simples significa que antes de podermos utilizar os dados temos de os transformar pois o que temos no campo detalhe n√£o pode ser usado sem ser convertido numa tabela com o par partido/vota√ß√£o

In [9]:
## Find the first instance of VotacaoOut
for c in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut"):
        print("\t{0:15}: {1}".format(c.tag, c.text))
	id             : 88184
	resultado      : Aprovado
	descricao      : Texto Final apresentado pela Comiss√£o de Administra√ß√£o P√ļblica, Moderniza√ß√£o Administrativa, Descentraliza√ß√£o e Poder Local relativo √†s Aprecia√ß√Ķes Parlamentares n.¬ļs 21/XIV/1.¬™ (PSD); 22/XIV/1.¬™ (BE); e 23/XIV/1.¬™ (PCP)
	reuniao        : 76
	tipoReuniao    : RP
	detalhe        : A Favor: <I>PS</I>, <I>PSD</I><BR>Contra: <I>BE</I>, <I>PCP</I>, <I>CDS-PP</I>, <I>PAN</I>, <I>PEV</I>, <I>CH</I>, Cristina Rodrigues (Ninsc), Joacine Katar Moreira (Ninsc)<BR>Abstenção: <I>IL</I>

O texto contido no detalhe tem informa√ß√£o descritiva que cont√©m formata√ß√£o HTML. √Č necess√°rio processar o texto de forma a poder criar uma tabela que associe a cada partido (ou deputado n√£o-inscrito) o seu sentido de voto. Este processamento seria simples se existisse sempre uma √ļnica forma de descrever as vota√ß√Ķes, o que n√£o acontece.

Partidos e deputados na determinação do sentido de voto

O tratamento e transforma√ß√£o de dados √© um processo que por vezes parece estar sempre inacabado; seja 80% do tempo gasto (‚ÄúCleaning Big Data: Most Time-Consuming, Least Enjoyable Data Science Task, Survey Says‚ÄĚ 2020) ou menos (Dodds 2020) tem sempre uma import√Ęncia fundamental. At√© agora cobrimos j√° v√°rios aspectos que nos colocam muito pr√≥ximos da utiliza√ß√£o dos dados de forma estruturada, mas ainda restam detalhes fundamentais.

Um ponto importante a relembrar: a metodologia adoptada e os objectivos da an√°lise prendem-se com o posionamento relativo de partidos pol√≠ticos, por via dos seus grupos parlamentares ou deputados individuais, e n√£o com o perfil de vota√ß√£o de cada deputado. O sistema parlamentar portugu√™s assenta na representatividade por via de elei√ß√£o de deputados eleitos por partidos mas os deputados votam de forma individual. Com varia√ß√Ķes dependendo dos partidos. A chamada "disciplina partid√°ria" √© a regra, por raz√Ķes que n√£o s√£o √ļnicas do sistema pol√≠tico portugu√™s (Jackson 1968).

Esta realidade est√° presente na forma como os votos s√£o apresentados por partidos nos registos oficiais mas apresenta tamb√©m excep√ß√Ķes importantes:

  1. Vota√ß√Ķes nominais onde todos os votos s√£o descritos de forma individual.
  2. Vota√ß√Ķes onde existem votos de deputados diferentes do seu grupo sendo apresentados de forma nominal.
  3. Vota√ß√Ķes onde existem votos de deputados diferentes do seu grupo*, apresentados como um n√ļmero total de deputados do grupo com esse sentido divergente.
  4. Vota√ß√Ķes onde os votos s√£o indicados sem excep√ß√Ķes, sendo este o caso mais comum e tamb√©m mais simples de cobrir.

Nos tr√™s primeiros casos n√£o √© imediata a posi√ß√£o de cada partido, e uma op√ß√£o seria excluir estas vota√ß√Ķes completamente. Outra op√ß√£o, e a que escolhemos, foi o de determinar o sentido de voto com base na maioria dos votos.

O reduzido n√ļmero de vota√ß√Ķes por este m√©todo tornam esta decis√£o de impacto reduzido, mas n√£o deixa de existir: olhando para as vota√ß√Ķes relativas √† eutan√°sia √© conhecida a forma diferenciada como foi votada. A sua elimina√ß√£o iria, por exemplo, omitir dados que afastam a IL do CDS-PP e CHEGA, √† direita, e o PCP do PEV e BE, √† esquerda. O reduzido n√ļmero de casos n√£o deve remover a necessidade de se assumir a possibilidade do impacto ser relevante quando mais √† frente verificarmos que a dist√Ęncia relativa de partidos (em particular √† direita) √© muito semelhante, o que torna qualquer observa√ß√£o adicional pass√≠vel de impactar o agrupamento e particularmnte o dendograma, raz√£o pela qual consider√°mos adequada a decis√£o de extrapolar a decis√£o da maioria dos deputados de um partido como indicador da posi√ß√£o do partido.

Os detalhes do processamento para cada tipo de votação é discutido em detalhe no apêndice Processamento da descrição de votos, com os respectivos exemplos. O seguinte bloco de código é o resultado final do tratamento dos vários casos:

In [10]:
from bs4 import BeautifulSoup
import re

## Iteract through the existing dict
def party_from_votes (votes):
    """
    Determines the position of a party based on the majority position by summing all the individual votes.
    Argument is a dictionary returned by parse_voting()
    Returns a dictionary with the majority position of each party
    """
    party_vote = {}
    for k, v in votes.items():
        ## Erase the name of the MP and keep the party only
        ## only when it's not from the "Ninsc" group - 
        ## these need to be differentiated by name
        if re.match(".*\(Ninsc\)" , k) is None:
            nk = re.sub(r".*\((.+?)\).*", r"\1", k)
        else:
            nk = k
        ## If it's the first entry for a key, create it
        if nk not in party_vote:
            party_vote[nk] = [0,0,0]
        ## Add to a specific index in a list
        if v == "A Favor":
            party_vote[nk][0] += 1
        elif v == "Abstenção":
            party_vote[nk][1] += 1
        elif v == "Contra":
            party_vote[nk][2] += 1
    for k,v in party_vote.items():
        party_vote[k]=["A Favor", "Abstenção", "Contra"][v.index(max(v))]
    return party_vote

def parse_voting(v_str):
    """Parses the voting details in a string and returns a dict.
    
    Keyword arguments:
    
    v_str: a string with the description of the voting behaviour.
    """
    ## Split by the HTML line break and put it in a dict
    d = dict(x.split(':') for x in v_str.split('<BR>'))
    ## Remove the HTML tags
    for k, v in d.items():
        ctext = BeautifulSoup(v, "lxml")
        d[k] = ctext.get_text().strip().split(",")
    ## Invert the dict to get a 1-to-1 mapping
    ## and trim it
    votes = {}
    if len(v_str) < 1000:    # Naive approach but realistically speaking... works well enough.
        for k, v in d.items():
            for p in v:
                if (p != ' ' and                                       # Bypass empty entries
                    re.match("[0-9]+", p.strip()) is None and           # Bypass quantified divergent voting patterns
                    (re.match(".*\w +\(.+\)", p.strip()) is None or     # Bypass individual votes...
                     re.match(".*\(Ninsc\)" , p.strip()) is not None)): # ... except when coming from "Ninsc"
                        #print("|"+ p.strip() + "|" + ":\t" + k)
                        votes[p.strip()] = k
    else:  # This is a nominal vote since the size of the string is greater than 1000
        for k, v in d.items():
            for p in v:
                if p != ' ':
                    votes[p.strip()] = k
        ## Call the auxiliary function to produce the party position based on the majority votes
        votes = party_from_votes(votes)
    return votes

O resultado da vota√ß√£o nominal relativa √† eutan√°sia que foi dada como primeiro exemplo √© simplificado e apresenta a vota√ß√£o maiorit√°ria de cada partido como aquela que ser√° considerada (um tratamento semelhante, por simples omiss√£o, √© dado √†s restantes varia√ß√Ķes):

In [11]:
for v in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='86004']"):
    if v.tag == "detalhe":
        print("\t{0:15}: {1}".format(v.tag,parse_voting(v.text)))
    else:
        print("\t{0:15}: {1}".format(v.tag,v.text))
	id             : 86004
	resultado      : Aprovado
	reuniao        : 32
	tipoReuniao    : RP
	detalhe        : {'PS': 'A Favor', 'PSD': 'Contra', 'BE': 'A Favor', 'PAN': 'A Favor', 'Joacine Katar Moreira (Ninsc)': 'A Favor', 'IL': 'A Favor', 'PCP': 'Contra', 'CDS-PP': 'Contra', 'CH': 'Contra', 'PEV': 'Abstenção'}

Deputados não-inscritos

Existe um tratamento especial no c√≥digo para os deputados n√£o-inscritos de forma a evitar que nestes casos seja removido o nome; recentemente foram aprovadas altera√ß√Ķes que permitem a diferencia√ß√£o de forma mais clara entre as duas deputadas n√£o-inscritas (Lusa 2020a) mas que n√£o se aplicam retroactivamente aos dados existentes.

Esta situação leva a que seja necessário diferenciar pelo nome, caso contrário na seguinte votação seriam contabilizados votos para um grupo/partido "Ninsc", o que não faria sentido.

In [12]:
## Example of vote where two MPs which are not registered with a party would
## be grouped under a generic "Unregistered" group
for v in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='88184']"):
    if v.tag == "detalhe":
        print("\t{0:15}: {1}".format(v.tag,parse_voting(v.text)))
    else:
        print("\t{0:15}: {1}".format(v.tag,v.text))
	id             : 88184
	resultado      : Aprovado
	descricao      : Texto Final apresentado pela Comiss√£o de Administra√ß√£o P√ļblica, Moderniza√ß√£o Administrativa, Descentraliza√ß√£o e Poder Local relativo √†s Aprecia√ß√Ķes Parlamentares n.¬ļs 21/XIV/1.¬™ (PSD); 22/XIV/1.¬™ (BE); e 23/XIV/1.¬™ (PCP)
	reuniao        : 76
	tipoReuniao    : RP
	detalhe        : {'PS': 'A Favor', 'PSD': 'A Favor', 'BE': 'Contra', 'PCP': 'Contra', 'CDS-PP': 'Contra', 'PAN': 'Contra', 'PEV': 'Contra', 'CH': 'Contra', 'Cristina Rodrigues (Ninsc)': 'Contra', 'Joacine Katar Moreira (Ninsc)': 'Contra', 'IL': 'Abstenção'}

Criação do dataframe

Ap√≥s estes v√°rios desvios (fundamentais para que se consiga produzir o resultado necess√°rio) voltamos √† representa√ß√£o em √°rvore que resulta da interpreta√ß√£o do formato XML da vota√ß√£o e com o aux√≠lio das v√°rias fun√ß√Ķes √° entretanto definidas criamos um modelo tabular de todas as vota√ß√Ķes.

In [13]:
import collections

root = ini_tree

counter=0

## We will build a dataframe from a list of dicts
## Inspired by the approach of Chris Moffitt here https://pbpython.com/pandas-list-dict.html
init_list = []

for voting in ini_tree.findall(".//pt_gov_ar_objectos_VotacaoOut"):
    votep = voting.find('./detalhe')
    if votep is not None:
        init_dict = collections.OrderedDict()
        counter +=1                 
        init_dict['id'] = voting.find('id').text
        ## Add the "I" for Type to mark this as coming from "Iniciativas"
        init_dict['Tipo'] = "I"
        for c in voting:
            if c.tag == "detalhe":
                for party, vote in parse_voting(c.text).items():
                    init_dict[party] = vote 
            elif c.tag == "descricao":
                    init_dict[c.tag] = c.text
            elif c.tag == "ausencias":
                    init_dict[c.tag] = c.find("string").text
            else:
                    init_dict[c.tag] = c.text
        init_list.append(init_dict)
    ## Provide progression feedback
    print('.', end='')
        
print(counter)
.........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................2050

O processamento criou uma lista de dicion√°rios que convertemos numa dataframe Pandas. Note-se como v√°rias vota√ß√Ķes n√£o t√™m o campo descricao: obter a informa√ß√£o do t√≠tulo da iniciativa seria uma op√ß√£o possivel para permitir adicionar contexto a estes casos, mas como referido, n√£o sendo tal necess√°rio para os objectivos imediatos deste trabalho, optou-se por simplificar o c√≥digo e omitir essa informa√ß√£o (ver Informa√ß√£o adicional da iniciativa para um exemplo)

In [14]:
import pandas as pd

ini_df = pd.DataFrame(init_list)
print(ini_df.shape)
ini_df.head()
(2050, 20)
Out[14]:
id Tipo resultado descricao reuniao tipoReuniao PS PSD BE PCP CDS-PP PAN PEV CH Cristina Rodrigues (Ninsc) Joacine Katar Moreira (Ninsc) IL unanime L ausencias
0 88184 I Aprovado Texto Final apresentado pela Comissão de Admin... 76 RP A Favor A Favor Contra Contra Contra Contra Contra Contra Contra Contra Abstenção NaN NaN NaN
1 88183 I Aprovado Texto Final apresentado pela Comissão de Admin... 76 RP A Favor A Favor Contra Contra Contra Contra Contra Contra Contra Contra Abstenção NaN NaN NaN
2 88181 I Aprovado Texto Final apresentado pela Comissão de Admin... 76 RP A Favor A Favor Contra Contra Contra Contra Contra Contra Contra Contra Abstenção NaN NaN NaN
3 87725 I Aprovado Texto Final apresentado pela Comiss√£o de Sa√ļde... 68 RP Contra A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor NaN NaN NaN
4 88346 I Aprovado Texto Final apresentado pela Comiss√£o de Assun... 76 RP Contra A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor A Favor NaN NaN NaN

A tabela cont√©m 1723 linhas (o n√ļmero de vota√ß√Ķes que tinhamos j√° observado) e 20 colunas; cada "caso" equivale a uma vota√ß√£o com os dados respectivos indicados pelas colunas, como pode ser observado tomando o primeiro caso como exemplo:

In [15]:
ini_df.loc[0]
Out[15]:
id                                                                           88184
Tipo                                                                             I
resultado                                                                 Aprovado
descricao                        Texto Final apresentado pela Comiss√£o de Admin...
reuniao                                                                         76
tipoReuniao                                                                     RP
PS                                                                         A Favor
PSD                                                                        A Favor
BE                                                                          Contra
PCP                                                                         Contra
CDS-PP                                                                      Contra
PAN                                                                         Contra
PEV                                                                         Contra
CH                                                                          Contra
Cristina Rodrigues (Ninsc)                                                  Contra
Joacine Katar Moreira (Ninsc)                                               Contra
IL                                                                       Abstenção
unanime                                                                        NaN
L                                                                              NaN
ausencias                                                                      NaN
Name: 0, dtype: object

Nem todos os campos est√£o preenchidos em todas as vota√ß√Ķes, o que √© normal visto nem todos os campos serem obrigat√≥rios.

Não-inscritos e a necessidade de processamento adicional

Já anteriormente o tratamento dos "não inscritos" levou à necessidade de pré-processamento; agora que já temos a estrutura de dados num dataframe é também necessário proceder a alguns passos de pós-processamento que surgem após uma leitura supercicial da tabela acima.

Existem dois casos que precisam ser analisados de forma individual; na secção Processamento adicional dos não-inscritos do apêndice detalhamos a informação que levou à decisão tomada e que é a seguinte:

  • Agrupar os dados do Livre e da deputada Joacine Katar Moreira, por ser deputada √ļnica e assim existir uma continuidade em termos do registo de vota√ß√£o.
  • N√£o includir a deputada Cristina Rodrigues na compara√ß√£o global dado o limitado n√ļmero de vota√ß√Ķes.

O seguinte bloco cria uma nova coluna, L/JKM, composta da sobreposição dos votos de ambos.

In [16]:
## Copy Livre voting record to new aggregate columns...
ini_df["L/JKM"] = ini_df["L"]
## ... and fill the NAs with JKM voting record.
ini_df["L/JKM"] = ini_df["L/JKM"].fillna(ini_df["Joacine Katar Moreira (Ninsc)"])
ini_df[["descricao","L","Joacine Katar Moreira (Ninsc)","L/JKM"]]
Out[16]:
descricao L Joacine Katar Moreira (Ninsc) L/JKM
0 Texto Final apresentado pela Comiss√£o de Admin... NaN Contra Contra
1 Texto Final apresentado pela Comiss√£o de Admin... NaN Contra Contra
2 Texto Final apresentado pela Comiss√£o de Admin... NaN Contra Contra
3 Texto Final apresentado pela Comiss√£o de Sa√ļde... NaN A Favor A Favor
4 Texto Final apresentado pela Comiss√£o de Assun... NaN A Favor A Favor
5 Texto Final apresentado pela Comiss√£o de Sa√ļde... NaN A Favor A Favor
6 Texto Final apresentado pela Comiss√£o de Sa√ļde... NaN A Favor A Favor
7 Texto Final apresentado pela Comiss√£o de Assun... NaN A Favor A Favor
8 Proposta de altera√ß√£o do BE - al√≠nea a) do n.¬ļ... NaN A Favor A Favor
9 Proposta de altera√ß√£o do BE - n.¬ļ 2 do artigo 4.¬ļ NaN A Favor A Favor
10 Proposta de altera√ß√£o do BE - al√≠nea c) do n.¬ļ... NaN A Favor A Favor
11 Proposta de alteração do PAN - alínea c) do n.... NaN A Favor A Favor
12 Proposta de altera√ß√£o do BE - n.¬ļ 4 ao artigo 4.¬ļ NaN A Favor A Favor
13 Proposta de altera√ß√£o do PAN - artigo 5.¬ļ-A NaN A Favor A Favor
14 Proposta de altera√ß√£o do PAN - artigo 5.¬ļ-B NaN A Favor A Favor
15 Proposta de altera√ß√£o do BE - artigo 6.¬ļ-A NaN A Favor A Favor
16 Proposta de altera√ß√£o do BE - artigo 13.¬ļ-A NaN A Favor A Favor
17 NaN NaN A Favor A Favor
18 Requerimento oral, apresentado pelo PS, solici... NaN A Favor A Favor
19 Proposta de altera√ß√£o do PCP - n.¬ļ 7 ao artigo... NaN A Favor A Favor
20 Proposta de altera√ß√£o do PCP - n.¬ļ 8 ao artigo... NaN A Favor A Favor
21 Proposta de altera√ß√£o do PCP - n.¬ļ 4 do artigo... NaN A Favor A Favor
22 Proposta de altera√ß√£o do PCP - n.¬ļ 1 do artigo... NaN A Favor A Favor
23 Proposta de altera√ß√£o do PAN - n.¬ļ 1 do artig... NaN Contra Contra
24 Proposta de altera√ß√£o do PAN - n.¬ļ 2 do artigo... NaN Contra Contra
25 Proposta de altera√ß√£o do PAN - n.¬ļ 3 do artig... NaN Contra Contra
26 Proposta de altera√ß√£o do PAN - n.¬ļ 4 do artigo... NaN Contra Contra
27 Proposta de altera√ß√£o do PCP - n.¬ļ 5 ao artigo... NaN A Favor A Favor
28 Proposta de altera√ß√£o do PCP - n.¬ļ 6 ao artigo... NaN A Favor A Favor
29 Proposta de altera√ß√£o do PCP - n.¬ļ 8 ao artigo... NaN A Favor A Favor
... ... ... ... ...
2020 NaN NaN A Favor A Favor
2021 NaN NaN A Favor A Favor
2022 NaN NaN A Favor A Favor
2023 NaN NaN A Favor A Favor
2024 NaN NaN A Favor A Favor
2025 NaN NaN A Favor A Favor
2026 NaN NaN A Favor A Favor
2027 NaN NaN A Favor A Favor
2028 NaN NaN A Favor A Favor
2029 NaN NaN A Favor A Favor
2030 NaN NaN A Favor A Favor
2031 NaN NaN Abstenção Abstenção
2032 NaN NaN A Favor A Favor
2033 NaN NaN A Favor A Favor
2034 NaN NaN A Favor A Favor
2035 NaN NaN A Favor A Favor
2036 Requerimento, apresentado pela PS solicitando ... NaN A Favor A Favor
2037 NaN NaN Abstenção Abstenção
2038 NaN NaN Abstenção Abstenção
2039 NaN NaN Abstenção Abstenção
2040 Requerimento oral, apresentado pelo PS, solici... NaN A Favor A Favor
2041 Requerimento, apresentado pela PS solicitando ... NaN A Favor A Favor
2042 NaN NaN A Favor A Favor
2043 Proposta do PSD, de substitui√ß√£o do artigo 1.¬ļ... NaN A Favor A Favor
2044 Proposta do PSD, de substitui√ß√£o do artigo 2.¬ļ... NaN A Favor A Favor
2045 Proposta do PSD, de aditamento de um novo arti... NaN A Favor A Favor
2046 Artigo 3.¬ļ da proposta de lei NaN A Favor A Favor
2047 NaN NaN A Favor A Favor
2048 NaN NaN A Favor A Favor
2049 NaN NaN Abstenção Abstenção

2050 rows √ó 4 columns

Processo semelhante aplicamos à deputada Cristina Rodrigues para podermos fazer uma análise específica:

In [17]:
## Copy PAN voting record to new aggregate columns...
ini_df["PAN/CR"] = ini_df["PAN"]
## ... and update/replace with CR voting where it exists
ini_df["PAN/CR"].update(ini_df["Cristina Rodrigues (Ninsc)"])
ini_df[["descricao","PAN","Cristina Rodrigues (Ninsc)","PAN/CR"]]
Out[17]:
descricao PAN Cristina Rodrigues (Ninsc) PAN/CR
0 Texto Final apresentado pela Comiss√£o de Admin... Contra Contra Contra
1 Texto Final apresentado pela Comiss√£o de Admin... Contra Contra Contra
2 Texto Final apresentado pela Comiss√£o de Admin... Contra Contra Contra
3 Texto Final apresentado pela Comiss√£o de Sa√ļde... A Favor A Favor A Favor
4 Texto Final apresentado pela Comiss√£o de Assun... A Favor A Favor A Favor
5 Texto Final apresentado pela Comiss√£o de Sa√ļde... A Favor A Favor A Favor
6 Texto Final apresentado pela Comiss√£o de Sa√ļde... A Favor A Favor A Favor
7 Texto Final apresentado pela Comiss√£o de Assun... A Favor A Favor A Favor
8 Proposta de altera√ß√£o do BE - al√≠nea a) do n.¬ļ... A Favor NaN A Favor
9 Proposta de altera√ß√£o do BE - n.¬ļ 2 do artigo 4.¬ļ A Favor NaN A Favor
10 Proposta de altera√ß√£o do BE - al√≠nea c) do n.¬ļ... A Favor NaN A Favor
11 Proposta de alteração do PAN - alínea c) do n.... A Favor NaN A Favor
12 Proposta de altera√ß√£o do BE - n.¬ļ 4 ao artigo 4.¬ļ A Favor NaN A Favor
13 Proposta de altera√ß√£o do PAN - artigo 5.¬ļ-A A Favor NaN A Favor
14 Proposta de altera√ß√£o do PAN - artigo 5.¬ļ-B A Favor NaN A Favor
15 Proposta de altera√ß√£o do BE - artigo 6.¬ļ-A A Favor NaN A Favor
16 Proposta de altera√ß√£o do BE - artigo 13.¬ļ-A A Favor NaN A Favor
17 NaN A Favor NaN A Favor
18 Requerimento oral, apresentado pelo PS, solici... A Favor NaN A Favor
19 Proposta de altera√ß√£o do PCP - n.¬ļ 7 ao artigo... A Favor NaN A Favor
20 Proposta de altera√ß√£o do PCP - n.¬ļ 8 ao artigo... A Favor NaN A Favor
21 Proposta de altera√ß√£o do PCP - n.¬ļ 4 do artigo... A Favor NaN A Favor
22 Proposta de altera√ß√£o do PCP - n.¬ļ 1 do artigo... A Favor NaN A Favor
23 Proposta de altera√ß√£o do PAN - n.¬ļ 1 do artig... A Favor NaN A Favor
24 Proposta de altera√ß√£o do PAN - n.¬ļ 2 do artigo... A Favor NaN A Favor
25 Proposta de altera√ß√£o do PAN - n.¬ļ 3 do artig... A Favor NaN A Favor
26 Proposta de altera√ß√£o do PAN - n.¬ļ 4 do artigo... A Favor NaN A Favor
27 Proposta de altera√ß√£o do PCP - n.¬ļ 5 ao artigo... A Favor NaN A Favor
28 Proposta de altera√ß√£o do PCP - n.¬ļ 6 ao artigo... A Favor NaN A Favor
29 Proposta de altera√ß√£o do PCP - n.¬ļ 8 ao artigo... A Favor NaN A Favor
... ... ... ... ...
2020 NaN A Favor A Favor A Favor
2021 NaN A Favor A Favor A Favor
2022 NaN A Favor A Favor A Favor
2023 NaN A Favor A Favor A Favor
2024 NaN A Favor A Favor A Favor
2025 NaN Abstenção Abstenção Abstenção
2026 NaN A Favor A Favor A Favor
2027 NaN A Favor A Favor A Favor
2028 NaN A Favor A Favor A Favor
2029 NaN A Favor A Favor A Favor
2030 NaN A Favor A Favor A Favor
2031 NaN Contra Abstenção Abstenção
2032 NaN A Favor A Favor A Favor
2033 NaN A Favor A Favor A Favor
2034 NaN A Favor A Favor A Favor
2035 NaN A Favor A Favor A Favor
2036 Requerimento, apresentado pela PS solicitando ... A Favor A Favor A Favor
2037 NaN Abstenção Abstenção Abstenção
2038 NaN Abstenção Abstenção Abstenção
2039 NaN Abstenção Abstenção Abstenção
2040 Requerimento oral, apresentado pelo PS, solici... A Favor A Favor A Favor
2041 Requerimento, apresentado pela PS solicitando ... A Favor A Favor A Favor
2042 NaN A Favor A Favor A Favor
2043 Proposta do PSD, de substitui√ß√£o do artigo 1.¬ļ... A Favor A Favor A Favor
2044 Proposta do PSD, de substitui√ß√£o do artigo 2.¬ļ... A Favor A Favor A Favor
2045 Proposta do PSD, de aditamento de um novo arti... A Favor A Favor A Favor
2046 Artigo 3.¬ļ da proposta de lei A Favor A Favor A Favor
2047 NaN A Favor A Favor A Favor
2048 NaN A Favor A Favor A Favor
2049 NaN A Favor A Favor A Favor

2050 rows √ó 4 columns

Processamento de Actividades

At√© agora trabalh√°mos sobre iniciativas; existe uma fonte adicional de informa√ß√£o (contida num ficheiro separado e com uma estrutura semelhante mas nao id√™ntica) relativa √†s Actividades Parlamentares, onde tamb√©m se inclu√©m vota√ß√Ķes.

Toda a discuss√£o e c√≥digo de processamento feito pode ser aplicado pelo que n√£o iremos repetir as considera√ß√Ķes j√° feitas; o processo de obten√ß√£o do XML √© id√™ntico:

In [18]:
act_url = 'http://app.parlamento.pt/webutils/docs/doc.xml?path=6148523063446f764c324679626d56304c3239775a57356b595852684c3052685a47397a51574a6c636e52766379394264476c32615752685a47567a4c31684a566955794d45786c5a326c7a6247463064584a684c30463061585a705a47466b5a584e595356597565473173&fich=AtividadesXIV.xml&Inline=true'
act_tree = ET.parse(urlopen(act_url))

O processamento também o é, com a diferença de que adicionamos um campo descricao que é na verdade obtido a partir do assunto da actividade.

In [19]:
import re
import collections

root = act_tree

counter=0

## We will build a dataframe from a list of dicts
## Inspired by the approach of Chris Moffitt here https://pbpython.com/pandas-list-dict.html
act_list = []

def get_toplevel_desc (vid, tree):
    """
    Gets the top-level title from a voting id
    """
    for c in tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='"+ vid +"']/../.."):
        if c.tag == "assunto":
            return c.text

for voting in act_tree.findall(".//pt_gov_ar_objectos_VotacaoOut"):
    act_dict = collections.OrderedDict()
    counter +=1
    votep = voting.find('./detalhe')
    if votep is not None:
        act_dict['id'] = voting.find('id').text
        ## Add the "A" for Type to mark this as coming from "Iniciativas"
        act_dict['Tipo'] = "A"
        for c in voting:
            if c.tag == "id":
                act_dict['descricao'] = get_toplevel_desc(c.text, act_tree)
            if c.tag == "detalhe":
                for party, vote in parse_voting(c.text).items():
                    act_dict[party] = vote 
            elif c.tag == "ausencias":
                    act_dict[c.tag] = c.find("string").text
            else:
                    act_dict[c.tag] = c.text
        act_list.append(act_dict)
    ## Provide progression feedback
    print('.', end='')

print(counter)
.......................................................................................................................................................................................................................................................247

Criamos também um dataframe com base na informação recolhida:

In [20]:
act_df = pd.DataFrame(act_list)
print(act_df.shape)

act_df.head()
(226, 21)
Out[20]:
id Tipo descricao resultado reuniao unanime PS PSD BE PCP ... PAN PEV CH IL Cristina Rodrigues (Ninsc) Joacine Katar Moreira (Ninsc) data ausencias publicacao L
0 88959 A De condenação pela destruição de anta na Herda... Aprovado 15 unanime A Favor A Favor A Favor A Favor ... A Favor A Favor A Favor A Favor A Favor A Favor 2020-10-16 NaN NaN NaN
1 88957 A De pesar pelo falecimento de Fernando Alberto ... Aprovado 15 unanime A Favor A Favor A Favor A Favor ... A Favor A Favor A Favor A Favor A Favor A Favor 2020-10-16 NaN NaN NaN
2 88956 A De pesar pelo falecimento de Augusto Boucinha Aprovado 15 unanime A Favor A Favor A Favor A Favor ... A Favor A Favor A Favor A Favor A Favor A Favor 2020-10-16 NaN NaN NaN
3 88954 A De pesar pelo falecimento de Augusto Cymbron Aprovado 15 unanime A Favor A Favor A Favor A Favor ... A Favor A Favor A Favor A Favor A Favor A Favor 2020-10-16 NaN NaN NaN
4 88863 A De pesar pela morte de Quino Aprovado 12 unanime A Favor A Favor A Favor A Favor ... A Favor A Favor Ausência A Favor A Favor A Favor 2020-10-09 CH NaN NaN

5 rows √ó 21 columns

√Č necess√°rio aplicar o mesmo princ√≠pio relativo √† agrega√ß√£o de votos do Livre e Joacine Katar Moreira, PAN e Cristina Rodrigues.

In [21]:
## Copy Livre voting record to new aggregate columns...
act_df["L/JKM"] = act_df["L"]
## ... and fill the NAs with JKM voting record.
act_df["L/JKM"] = act_df["L/JKM"].fillna(act_df["Joacine Katar Moreira (Ninsc)"])

## Copy PAN voting record to new aggregate columns...
act_df["PAN/CR"] = act_df["PAN"]
## ... and update/replace with CR voting where it exists
act_df["PAN/CR"].update(act_df["Cristina Rodrigues (Ninsc)"])
act_df[["descricao","PAN","Cristina Rodrigues (Ninsc)","PAN/CR"]].head()
Out[21]:
descricao PAN Cristina Rodrigues (Ninsc) PAN/CR
0 De condenação pela destruição de anta na Herda... A Favor A Favor A Favor
1 De pesar pelo falecimento de Fernando Alberto ... A Favor A Favor A Favor
2 De pesar pelo falecimento de Augusto Boucinha A Favor A Favor A Favor
3 De pesar pelo falecimento de Augusto Cymbron A Favor A Favor A Favor
4 De pesar pela morte de Quino A Favor A Favor A Favor

Agregação das votações

Neste momento temos dois dataframes, um para as Iniciativas e outro para as Actividades; durante o processo de construção de ambos foi adicionada uma coluna Tipo que permite identificar, se necessário, a origem da votação.

No presente trabalho consideramos todas as vota√ß√Ķes de forma id√™ntica pelo que nos resta construir um dataframe √ļnico que seja a jun√ß√£o dos existentes, tendo em considera√ß√£o que as colunas de ambos n√£o s√£o exactamente as mesmas.

In [22]:
print(ini_df.sort_index(axis=1).columns)
print(act_df.sort_index(axis=1).columns)
Index(['BE', 'CDS-PP', 'CH', 'Cristina Rodrigues (Ninsc)', 'IL',
       'Joacine Katar Moreira (Ninsc)', 'L', 'L/JKM', 'PAN', 'PAN/CR', 'PCP',
       'PEV', 'PS', 'PSD', 'Tipo', 'ausencias', 'descricao', 'id', 'resultado',
       'reuniao', 'tipoReuniao', 'unanime'],
      dtype='object')
Index(['BE', 'CDS-PP', 'CH', 'Cristina Rodrigues (Ninsc)', 'IL',
       'Joacine Katar Moreira (Ninsc)', 'L', 'L/JKM', 'PAN', 'PAN/CR', 'PCP',
       'PEV', 'PS', 'PSD', 'Tipo', 'ausencias', 'data', 'descricao', 'id',
       'publicacao', 'resultado', 'reuniao', 'unanime'],
      dtype='object')

A concatenação dos dados é feita através da remoção das colunas diferentes.

In [23]:
votes = pd.concat([ini_df.drop(["tipoReuniao"],axis=1),act_df.drop(["data","publicacao"],axis=1)], sort=True)

Este √ļltimo passo de processamento d√° origem √† tabela final que ir√° ser usada para as v√°rias an√°lises posteriores; o seu formato formato tabular pode servir de base para an√°lises com outras ferramentas (ver Convers√£o do dataframe em CSV).

In [24]:
print(votes.shape)
votes.columns
(2276, 21)
Out[24]:
Index(['BE', 'CDS-PP', 'CH', 'Cristina Rodrigues (Ninsc)', 'IL',
       'Joacine Katar Moreira (Ninsc)', 'L', 'L/JKM', 'PAN', 'PAN/CR', 'PCP',
       'PEV', 'PS', 'PSD', 'Tipo', 'ausencias', 'descricao', 'id', 'resultado',
       'reuniao', 'unanime'],
      dtype='object')

Exploração e visualização dos dados

Tendo a nossa fonte de dados devidamente constru√≠da podemos fazer algumas an√°lises explorat√≥rias com base em visualiza√ß√Ķes.

Para come√ßar seria interessante podermos ter uma representa√ß√£o de alto-n√≠vel que apresentasse as vota√ß√Ķes de forma global. Para este fim um mapa t√©rmico (heatmap) √© uma op√ß√£o.

Come√ßamos por criar um novo dataframe que contenha apenas o sentido de voto dos partidos que vamos analisar, removendo os campos adicionais. Esta ac√ß√£o poderia ser feita tamb√©m por remo√ß√£o de colunas: optamos pela especifica√ß√£o dos campos tamb√©m porque nos permite colocar os v√°rios partidos em "ordem", consoante a sua posi√ß√£o no parlamento (‚ÄúEst√£o Distribu√≠das (para J√°) as Cadeiras No Parlamento - DN‚ÄĚ 2020), o que facilita a localiza√ß√£o e interpreta√ß√£o dos dados (e inclusivamente a maior ou menor pertin√™ncia dessa dimens√£o)

In [25]:
votes_hm = votes[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN','PSD','IL','CDS-PP', 'CH']]
votes_hm.head()
Out[25]:
BE PCP PEV L/JKM PS PAN PSD IL CDS-PP CH
0 Contra Contra Contra Contra A Favor Contra A Favor Abstenção Contra Contra
1 Contra Contra Contra Contra A Favor Contra A Favor Abstenção Contra Contra
2 Contra Contra Contra Contra A Favor Contra A Favor Abstenção Contra Contra
3 A Favor A Favor A Favor A Favor Contra A Favor A Favor A Favor A Favor A Favor
4 A Favor A Favor A Favor A Favor Contra A Favor A Favor A Favor A Favor A Favor

Este mapa apresenta os votos seguindo o seguinte esquema de cores:

  • A Favor: verde
  • Absten√ß√£o: amarelo
  • Contra: vermelho
  • Aus√™ncia/NA: preto
In [26]:
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns

votes_hmn = votes_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,2]).fillna(0)

voting_palette = ["#FB6962","#FCFC99","#79DE79", "black"]

fig = plt.figure(figsize=(8,8))
sns.heatmap(votes_hmn,
            square=False,
            yticklabels = False,
            cbar=False,
            cmap=sns.color_palette(voting_palette),
           )
plt.show()

Existem alguns pontos interessantes que podem ser observados desde j√°. Uma an√°lise da origem das iniciativas e actividades a vota√ß√£o poder√° confirmar ou desmentir estas observa√ß√Ķes:

  1. Uma maior tendência para votos "A Favor" à esquerda

Pode relacionar-se com uma maior produção de propostas a votação.

  1. Uma notória diferenciação do PS na maior quantidade de votos Contra

Enquanto partido que suporta o Governo, esta tendência pode relacionar-se com a anterior e representar o voto contra de propostas de outros partidos.

  1. Algum acompanhamento do PSD nos votos contra do PS

Podem representar convergências em propostas apresentadas pelo próprio ou em matérias consideradas estratégicas.

  1. Uma maior tend√™ncia para absten√ß√Ķes √† direita

Mais uma vez pode estar relacionada com a autoria das propostas ou representar uma maior abertura a outras propostas ao usar mais a Abstenção do que a votação contra.

  1. Ausências visíveis no Livre/Joacine Katar Moreira e Chega

S√£o dois deputados √ļnicos pelo que √© compreens√≠vel quando comparado com grupos parlamentares, embora se note que a IL tem tamb√©m um deputado √ļnico e n√£o regista comportamente id√™ntico.

  1. O PAN parece estar "deslocado"

Observando o alinhamento de cores o PAN estaria mais próximo de qualquer dos partidos de ambos os lados dos seus vizinhos imediatos (PS e PSD), sendo que o comportamente em termos de abstenção o parece aproximar mais do lado esquerdo (L/JKM).

  1. Aparente maior heterogeneidade à direita e homogeneidade à esquerda

Pode ser artefacto visual dado pelo maior n√ļmero de absten√ß√Ķes mas parece existir uma maior varia√ß√£o crom√°tica √† direita do que √† esquerda.

Estes s√£o apenas algumas das impress√Ķes que uma explora√ß√£o visual inicial proporciona; para algumas delas podemos encontrar novas visualiza√ß√Ķes que forne√ßam informa√ß√£o adicional. O seguinte diagrama de barras mostra o sentido de voto de cada partido ordenado em ordem descrecente de votos contra.

In [27]:
from matplotlib.colors import ListedColormap

votes_against = votes_hm.apply(pd.Series.value_counts).sort_values(by='Contra', ascending=False, axis=1).T
votes_against.plot(kind='bar', stacked=True, colormap=ListedColormap(sns.color_palette("RdBu_r", 4)),linewidth=0)
plt.legend(loc='center left', bbox_to_anchor=(0.7, 0.15))
plt.show()

Quem vota com quem: identificação de votações idênticas

Com estes dados podemos tentar obter uma resposta mais clara do que o "mapa térmico" anterior nos apresenta como sendo semelhanças e diferenças no registo de votação.

Uma das quest√Ķes que se coloca (e normalmente coloca-se com maior √™nfase sempre que h√° uma vota√ß√£o que √© apontada como sendo "at√≠pica", com base na percep√ß√£o geral do que √© o comportamente de voto habitual de cada partido) √© saber "quem vota com quem". Estes dados podem ser obtidos atrav√©s da identifica√ß√£o, para cada partido, da quantidade de vota√ß√Ķes onde cada outro votou da mesma forma e cria√ß√£o de uma tabela com os resultados:

In [28]:
import numpy as np
pv_list = []
print("Total voting instances: ", votes_hm.shape[0])

## Not necessarily the most straightforard way (check .crosstab or .pivot_table, possibly with pandas.melt and/or groupby)
## but follows the same approach as before in using a list of dicts
for party in votes_hm.columns:
    pv_dict = collections.OrderedDict()
    for column in votes_hmn:
        pv_dict[column]=votes_hmn[votes_hmn[party] == votes_hmn[column]].shape[0]
    pv_list.append(pv_dict)

pv = pd.DataFrame(pv_list,index=votes_hm.columns)
pv
Total voting instances:  2276
Out[28]:
BE PCP PEV L/JKM PS PAN PSD IL CDS-PP CH
BE 2276 1833 1882 1884 1107 1710 1010 1109 1013 1008
PCP 1833 2276 2162 1679 1079 1461 984 1082 1009 990
PEV 1882 2162 2276 1730 1073 1532 970 1097 973 973
L/JKM 1884 1679 1730 2276 1006 1613 930 1080 941 938
PS 1107 1079 1073 1006 2276 1123 1535 1122 1175 830
PAN 1710 1461 1532 1613 1123 2276 1141 1277 1115 1073
PSD 1010 984 970 930 1535 1141 2276 1331 1573 1211
IL 1109 1082 1097 1080 1122 1277 1331 2276 1423 1328
CDS-PP 1013 1009 973 941 1175 1115 1573 1423 2276 1396
CH 1008 990 973 938 830 1073 1211 1328 1396 2276

Com estes dados podemos usar um "mapa t√©rmico" (heatmap), uma visualiza√ß√£o especialmente adequada para os casos onde temos valores que em magnitude em duas dimens√Ķes; esta visualiza√ß√£o vai fundamentalmente facilitar a interpreta√ß√£o das proximidades e dist√Ęncias ao combinar a informa√ß√£o num√©rica j√° obtida com uma categoriza√ß√£ crom√°tica que depende do valor: neste caso, quantas mais vota√ß√Ķes com vota√ß√Ķes iguais, mais escuro.

In [30]:
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot()

sns.heatmap(
    pv,
    cmap=sns.color_palette("mako_r"),
    linewidth=1,
    annot = True,
    square =True,
    fmt="d",
    cbar_kws={"shrink": 0.8})
plt.title('Portuguese Parliament 14th Legislature, identical voting count')

plt.show()

Esta visualização já nos fornece pistas mais concretas em termos de quem votam mais vezes com quem, mas tem uma limitação que não deve ser ignorada: a valorização do sentido de voto não está a ser tido em conta, para a tabela acima é exactamente igual que um partido vote a Favor e que o outro vote Contra ou se Abstenha. Esta forma binária de considerar "proximidade" descarta informação relevante ao não considerar que, mesmo nos casos onde existem diferentes sentidos de voto, existem diferenças importantes.

O ideal seria, portanto, conseguir determinar n√£o s√≥ a quantidade de vezes que os partidos votam da mesma forma, mas tamb√©m quantificar de forma diferente quando n√£o o fazem. Isto para todos os partidos, e para todas as vota√ß√Ķes. E √© precisamente isso que iremos fazer atrav√©s da cria√ß√£o de uma matriz de dist√Ęncias assente na dist√Ęncia num√©rica entre os sentidos de voto.

Matriz de distância entre os partidos

Com base nos hist√≥rico de vota√ß√Ķes de cada partido produzimos uma matriz de dist√Ęncias entre eles; uma matriz de dist√Ęncias √© uma matriz quadradra $n\times n$ (onde n √© o n√ļmero de partidos) e onde a dist√Ęncia entre p e q √© o valor de $ d_{pq} $.

$ \begin{equation} D= \begin{bmatrix} d_{11} & d_{12} & \cdots & d_{1 n} \\ d_{21} & d_{22} & \cdots & d_{2 n} \\ \vdots & \vdots & \ddots & \vdots \\ d_{31} & d_{32} & \cdots & d_{n n} \end{bmatrix}_{\ n\times n} \end{equation} $

A dist√Ęncia √© obtida atrav√©s da compara√ß√£o de todas as observa√ß√Ķes de cada par usando uma determinada m√©trica de dist√Ęncia, sendo a dist√Ęncia euclideana bastante comum em termos gerais e tamb√©m dentro de estudos sobre o mesmo dom√≠nio tem√°tico (Krilavińćius and ŇĹilinskas 2008): cada elemento da matriz representa $ d\left( p,q\right) = \sqrt {\sum _{i=1}^{n} \left( q_{i}-p_{i}\right)^2 }$, equivalente, para dois pontos $P,Q $ , √† mais gen√©rica dist√Ęncia de Minkowski $ D\left(P,Q\right)=\left(\sum _{i=1}^{n}|x_{i}-y_{i}|^{p}\right)^{\frac {1}{p}} $ para $ p = 1$, mas note-se que a diagonal da matrix ir√° representar a dist√Ęncia entre um partido e ele pr√≥prio, logo $ d_{11} = d_{22} = \dots = d_{nn} = 0 $.

Na sec√ß√£o Dist√Ęncias e matrizes coloc√°mos uma discuss√£o mais detalhada (mas passo-a-passo e destinada a quem n√£o tenha necessariamente presente a matem√°tica utilizada) sobre dist√Ęncias, clustering e como s√£o calculdadas, para quem tenha interesse numa compreens√£o mais quantitativa da mat√©ria.

A convers√£o de votos em representa√ß√Ķes n√ļmericas pode ser feita de v√°rias formas (Hix, Noury, and Roland 2006); adoptamos a abordagem de Krilavińćius & ŇĹilinskas (2008) no j√° citado trabalho relativo √†s vota√ß√Ķes no parlamento lituano por nos parecer apropriada √† realidade portuguesa:

  • A favor: 1
  • Contra: -1
  • Absten√ß√£o: 0
  • Aus√™ncia: 0

Este ponto é (mais um) dos que de forma relativamente opaca - pois raramente os detalhes têm a mesma projecção que os resultado finais - podem influenciar os resultados; cremos que em particular a equiparação entre abstenção e ausência merece alguma reflexão: considerámos que uma ausência em determinada votação tem um peso equivalente à abstenção, embora uma de forma passiva e outra activa.

Para obtermos a matriz de dist√Ęncia usamos a fun√ß√£o pdist e constru√≠mos um dataframe que √© uma matriz sim√©trica das dist√Ęncias entre os partidos.

In [31]:
from scipy.spatial.distance import squareform
from scipy.spatial.distance import pdist
import scipy.spatial as sp, scipy.cluster.hierarchy as hc
from itables import show

votes_hmn = votes_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0]).fillna(0)

## Transpose the dataframe used for the heatmap
votes_t = votes_hmn.transpose()

## Determine the Eucledian pairwise distance
## ("euclidean" is actually the default option)
pwdist = pdist(votes_t, metric='euclidean')

## Create a square dataframe with the pairwise distances: the distance matrix
distmat = pd.DataFrame(
    squareform(pwdist), # pass a symmetric distance matrix
    columns = votes_t.index,
    index = votes_t.index
)
#show(distmat, scrollY="200px", scrollCollapse=True, paging=False)

## Normalise by scaling between 0-1, using dataframe max value to keep the symmetry.
## This is essentially a cosmetic step to 
#distmat=((distmat-distmat.min().min())/(distmat.max().max()-distmat.min().min()))*1
distmat
Out[31]:
BE PCP PEV L/JKM PS PAN PSD IL CDS-PP CH
BE 0.000000 27.422618 24.698178 22.090722 61.180062 33.166248 59.874870 50.734604 54.881691 47.927028
PCP 27.422618 0.000000 13.416408 29.698485 58.094750 40.570926 56.964901 51.088159 52.744668 47.085029
PEV 24.698178 13.416408 0.000000 27.495454 59.067758 38.574603 58.129167 51.029403 54.092513 47.717921
L/JKM 22.090722 29.698485 27.495454 0.000000 60.621778 32.984845 58.506410 48.826222 53.684262 46.593991
PS 61.180062 58.094750 59.067758 60.621778 0.000000 60.605280 42.000000 52.981129 49.020404 54.899909
PAN 33.166248 40.570926 38.574603 32.984845 60.605280 0.000000 55.163394 45.166359 50.734604 44.799554
PSD 59.874870 56.964901 58.129167 58.506410 42.000000 55.163394 0.000000 43.023250 34.132096 42.213742
IL 50.734604 51.088159 51.029403 48.826222 52.981129 45.166359 43.023250 0.000000 35.637059 36.482873
CDS-PP 54.881691 52.744668 54.092513 53.684262 49.020404 50.734604 34.132096 35.637059 0.000000 33.181320
CH 47.927028 47.085029 47.717921 46.593991 54.899909 44.799554 42.213742 36.482873 33.181320 0.000000

O mapa t√©rmico √© constru√≠do directamente a partir da matriz de dist√Ęncia, fornencendo uma forma bastante mais intuitiva de visualizar as dist√Ęncias:

In [32]:
## Display the heatmap of the distance matrix

fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot()

sns.heatmap(
    distmat,
    cmap=sns.color_palette("Reds_r"),
    linewidth=1,
    annot = True,
    square =True,
    cbar_kws={"shrink": 0.8})
plt.title('Portuguese Parliament 14th Legislature, Distance Matrix')

plt.show()

A ordem dos √≠ndice √© a que foi determinada anteriormente e baseada na localiza√ß√£o dos partidos e deputados no hemiciclo; deixando considera√ß√Ķes adicionais para depois note-se que duas das impress√Ķes iniciais obtidas pela observa√ß√£o do mapa t√©rmico das vota√ß√Ķes) parecem confirmar-se:

  1. O PAN parece de facto deslocado (restando saber para qual dos lados).
  2. O quadrante superior esquerdo exibe dist√Ęncias menores que o inferior direito, demonstrando uma maior proximidade entre si dos partidos √† esquerda do que √† direita.

Com base nas dist√Ęncias procedemos ao agrupamento usando o m√©todo de Ward (Carvalho et al. 2009) de forma a obtermos uma √°rvore de proximidade hier√°rquica: um dendograma, neste caso associado ao mapa t√©rmica com as linhas e colunas devidamente reordenadas de forma a permitirem visualizar em simult√Ęneo as dist√Ęncias e o agrupamentos (ver Dist√Ęncias e matrizes para uma explica√ß√£o mais aprofundanda do tema)

In [33]:
## Perform hierarchical linkage on the distance matrix using Ward's method.
distmat_link = hc.linkage(pwdist, method="ward", optimal_ordering=True )

sns.clustermap(
    distmat,
    annot = True,
    cmap=sns.color_palette("Reds_r"),
    linewidth=1,
    #standard_scale=1,
    row_linkage=distmat_link,
    col_linkage=distmat_link,
    figsize=(8,8)).fig.suptitle('Portuguese Parliament 14th Legislature, Clustermap')

plt.show()

Uma visualiza√ß√£o apenas do dendograma √© tamb√©m poss√≠vel; a altura das linhas e os pontos de deriva√ß√£o n√£o s√£o aleat√≥rios, representam as dist√Ęncias determinadas pelo m√©todo de agrupamento:

In [34]:
from scipy.cluster.hierarchy import dendrogram
fig = plt.figure(figsize=(8,5))
dendrogram(distmat_link, labels=votes_hmn.columns)

plt.title("Portuguese Parliament 14th Legislature, Dendogram")
plt.show()

ūüĎČ Estamos a trabalhar sobre o total de vota√ß√Ķes, inclu√≠ndo Actividade e Iniciativas; uma quest√£o que se pode colocar √© se existem diferen√ßas, algo que discutimos em Diferen√ßas entre Actividades e Iniciativas.

Identificar grupos e reduzir dimensões

J√° conseguimos determinar as dist√Ęncias e os agrupamentos, mas as possibilidades de visualiza√ß√£o n√£o se esgotam por a√≠.

Clustering de observações: DBSCAN e Spectrum Scaling

Uma forma diferente de determinar agrupamentos é através de métodos de clustering, que procuram determinar agrupamentos de pontos com base em mecanismos específicos de cada um dos algoritmos.

Vamos demonstrar dois deles, e como passo preliminar vamos transformar a nossa matriz de dist√Ęncias: ao contr√°rio do dendograma anterior estes m√©todos utilizam uma matriz de afinidade, onde valores mais altos significam uma maior semelhan√ßa (e, consequentemente, para uma matriz sim√©trica a diagonal passa de 0 para 1).

Como passo preliminar normalizamos as dist√Ęncias no intervalo $ [0,1] $:

In [35]:
import numpy as np

distmat_mm=((distmat-distmat.min().min())/(distmat.max().max()-distmat.min().min()))*1
pd.DataFrame(distmat_mm, distmat.index, distmat.columns)
Out[35]:
BE PCP PEV L/JKM PS PAN PSD IL CDS-PP CH
BE 0.000000 0.448228 0.403697 0.361077 1.000000 0.542109 0.978666 0.829267 0.897052 0.783377
PCP 0.448228 0.000000 0.219294 0.485428 0.949570 0.663140 0.931102 0.835046 0.862122 0.769614
PEV 0.403697 0.219294 0.000000 0.449419 0.965474 0.630509 0.950133 0.834086 0.884153 0.779959
L/JKM 0.361077 0.485428 0.449419 0.000000 0.990875 0.539144 0.956299 0.798074 0.877480 0.761588
PS 1.000000 0.949570 0.965474 0.990875 0.000000 0.990605 0.686498 0.865987 0.801248 0.897350
PAN 0.542109 0.663140 0.630509 0.539144 0.990605 0.000000 0.901656 0.738253 0.829267 0.732257
PSD 0.978666 0.931102 0.950133 0.956299 0.686498 0.901656 0.000000 0.703223 0.557896 0.689992
IL 0.829267 0.835046 0.834086 0.798074 0.865987 0.738253 0.703223 0.000000 0.582495 0.596320
CDS-PP 0.897052 0.862122 0.884153 0.877480 0.801248 0.829267 0.557896 0.582495 0.000000 0.542355
CH 0.783377 0.769614 0.779959 0.761588 0.897350 0.732257 0.689992 0.596320 0.542355 0.000000

... e subtraimos √† unidade todos os valores da matriz, uma forma simples de obter a matriz de afinidade (mas n√£o a √ļnica, nem a que garantidamente resulta sempre melhor). O resultado √© uma matriz cujos valores indicam a semelhan√ßa entre os pares, ao inv√©s da dist√Ęncia.

In [36]:
affinmat_mm = pd.DataFrame(1-distmat_mm, distmat.index, distmat.columns)
affinmat_mm 
Out[36]:
BE PCP PEV L/JKM PS PAN PSD IL CDS-PP CH
BE 1.000000 0.551772 0.596303 0.638923 0.000000 0.457891 0.021334 0.170733 0.102948 0.216623
PCP 0.551772 1.000000 0.780706 0.514572 0.050430 0.336860 0.068898 0.164954 0.137878 0.230386
PEV 0.596303 0.780706 1.000000 0.550581 0.034526 0.369491 0.049867 0.165914 0.115847 0.220041
L/JKM 0.638923 0.514572 0.550581 1.000000 0.009125 0.460856 0.043701 0.201926 0.122520 0.238412
PS 0.000000 0.050430 0.034526 0.009125 1.000000 0.009395 0.313502 0.134013 0.198752 0.102650
PAN 0.457891 0.336860 0.369491 0.460856 0.009395 1.000000 0.098344 0.261747 0.170733 0.267743
PSD 0.021334 0.068898 0.049867 0.043701 0.313502 0.098344 1.000000 0.296777 0.442104 0.310008
IL 0.170733 0.164954 0.165914 0.201926 0.134013 0.261747 0.296777 1.000000 0.417505 0.403680
CDS-PP 0.102948 0.137878 0.115847 0.122520 0.198752 0.170733 0.442104 0.417505 1.000000 0.457645
CH 0.216623 0.230386 0.220041 0.238412 0.102650 0.267743 0.310008 0.403680 0.457645 1.000000

A matriz de afinididade pode agora ser visualizada de forma semelhante ao mapa t√©rmico de dist√Ęncia:

In [37]:
sns.set(style="white")

## Make the top triangle
mask = np.triu(np.ones_like(affinmat_mm, dtype=np.bool))
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot()
plt.title('Portuguese Parliament 14th Legislature, Affinity Matrix')

## Display the heatmap of the affinity matrix, masking the top triangle

sns.heatmap(
    affinmat_mm,
    cmap=sns.color_palette("Greens"),
    linewidth=1,
    annot = False,
    square =True,
    cbar_kws={"shrink": .8},
    mask=mask,linewidths=.5)

plt.show()

Existem v√°rios m√©todos de proceder √† identifica√ß√£o de clusters; um deles √© o DBSCAN (Density-Based Spatial Clustering of Applications with Noise), um algoritmo que, entre outras caracter√≠sticas, n√£o necessita de ser inicializado com um n√ļmero pr√©-determinado de grupos, procedendo √† sua identifica√ß√£o atrav√©s da densidade dos pontos (‚ÄúDBSCAN: Macroscopic Investigation Python‚ÄĚ 2018).

Note-se que estes m√©todos s√£o normalmente utilizados com um maior n√ļmero de observa√ß√Ķes; seria, por exemplo, um m√©todo que atrav√©s do registo dos votos individuais de cada deputado identificaria os partidos. Aqui temos um n√ļmero reduzido de observa√ß√Ķes (pois estamos a utilizar uma matriz de semelhan√ßa entre $10\times10$, o que tamb√©m explica o reduzido n√ļmero de clusters identificados - dois, alinhados com a primeira divis√£o do dendograma:

In [38]:
from sklearn.cluster import DBSCAN

dbscan_labels = DBSCAN(eps=1.1).fit(affinmat_mm)
dbscan_labels.labels_
dbscan_dict = dict(zip(distmat_mm,dbscan_labels.labels_))
dbscan_dict
Out[38]:
{'BE': 0,
 'PCP': 0,
 'PEV': 0,
 'L/JKM': 0,
 'PS': 1,
 'PAN': 0,
 'PSD': 1,
 'IL': 1,
 'CDS-PP': 1,
 'CH': 1}

Outra abordagem para efectuar a identifica√ß√£o de grupos passa pela utiliza√ß√£o de Spectral Clustering, uma forma de clustering que utiliza os valores-pr√≥prios e vectores-pr√≥prios de matrizes como forma de determina√ß√£o dos grupos. Este m√©todo necessita que seja determinado a priori o n√ļmero de clusters; com base na an√°lise anterior (dendograma) observ√°mos que temos 4 grupos constituidos por 2 partidos, e nesse sentido usar 4 permite-nos aferir se a distribui√ß√£o dos restantes est√° alinhada com a separa√ß√£o proposta pelo modelo anterior.

In [39]:
from sklearn.cluster import SpectralClustering
sc = SpectralClustering(4, affinity="precomputed",random_state=2020).fit_predict(affinmat_mm)
sc_dict = dict(zip(distmat,sc))

print(sc_dict)
{'BE': 1, 'PCP': 3, 'PEV': 3, 'L/JKM': 1, 'PS': 0, 'PAN': 1, 'PSD': 0, 'IL': 2, 'CDS-PP': 2, 'CH': 2}

O agrupamento est√° em linha com o que observ√°mos no dendograma - mas tal n√£o era garantido ou necess√°rio pois diferentes formas de agrega√ß√£o podem ter resultados diferentes que devem ser fonte de an√°lise e podem fornecer dados adicionais interessantes. A utiliza√ß√£o de Spectrum Scaling permite-nos aqui determinar um n√ļmero de grupos maior.

Multidimensional scaling

Até agora temos conseguido extrair informação interessante dos dados de votação:

  1. O mapa térmico de votação permite-nos uma primeira visão do comportamente de todos os partidos.
  2. A matriz de dist√Ęncias fornece-nos uma forma de comparar as dist√Ęncias entre os diferentes partidos atrav√©s de um mapa t√©rmico.
  3. O dendograma identifica de forma hier√°rquica agrupamentos.
  4. Através de DBSCAN e Spectrum Clustering identificamos "blocos" com base na matriz de afinidade.

N√£o temos ainda uma forma de visualizar a dist√Ęncia relativa de cada partido em rela√ß√£o aos outros com base nas dist√Ęncias/semelhan√ßas: temos algo pr√≥ximo com base no dendograma mas existem outras formas de visualiza√ß√£o interessantes.

Uma das formas √© o multidimensional scaling que permite visualizar a dist√Ęncia ao projectar em 2 ou 3 dimens√Ķes (tamb√©m conhecidas como dimens√Ķes visualizavies) conjuntos multidimensionais, mantendo a dist√Ęncia relativa (‚ÄúGraphical Representation of Proximity Measures for Multidimensional Data ¬ę‚ÄĮThe Mathematica Journal‚ÄĚ 2020).

Como √© habitual temos em Python, atrav√©s da biblioteca scikit-learn (que j√° us√°mos para DBSCAN e Spectrum Clustering), uma implementa√ß√£o que podemos usar sem grande dificuldade (‚Äú2.2. Manifold Learning ‚ÄĒ Scikit-Learn 0.23.2 Documentation‚ÄĚ 2020).

In [40]:
from sklearn.manifold import MDS

mds = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)

## We use the normalised distance matrix but results would
## be similar with the original one, just with a different scale/axis
results = mds.fit(distmat_mm.values)
coords = results.embedding_
coords
Out[40]:
array([[ 0.32947197,  0.36768563],
       [-0.00198288,  0.42950115],
       [ 0.10197222,  0.44174205],
       [ 0.39394375,  0.21823126],
       [-0.68817252,  0.15327307],
       [ 0.49485406, -0.08254548],
       [-0.5243696 , -0.28781799],
       [ 0.15152887, -0.51072806],
       [-0.23299015, -0.4774982 ],
       [-0.02425571, -0.25184343]])

O resultado é um um conjunto de pontos definidos por coordenadas, neste caso 2 ($x,y$) dado termos especificado que queriamos reduzir para n_components = 2. Com estas coordenadas podemos facilmente criar um gráfico de dispersão:

In [41]:
## Graphic options
sns.set()
sns.set_style("ticks")

fig, ax = plt.subplots(figsize=(8,8))

plt.title('Portuguese Parliament Voting Records Analysis, 14th Legislature', fontsize=14)

for label, x, y in zip(distmat_mm.columns, coords[:, 0], coords[:, 1]):
    ax.scatter(x, y, s=250)
    ax.axis('equal')
    ax.annotate(label,xy = (x-0.02, y+0.025))
plt.show()

Atrav√©s de MDS podemos agora ter uma ideia mais clara do que, visualmente, representam as dist√Ęncias que calcul√°mos, reduzidas para duas dimens√Ķes. Isto d√°-nos a possibilidade de definirmos agrupamentos da forma que consideremos mais adequada.

Em todo o caso, e visto que temos os resultados de DBSCAN e de Spectrum Clustering, podemos adicionar também esta informação, complementando o MDS com a informação anterior ao definirmos as cores dos marcadores de cada partido consoante o grupo previamente identificado.

Para o caso de DBSCAN:

In [42]:
from sklearn.manifold import MDS
import random

sns.set()
sns.set_style("ticks")


fig, ax = plt.subplots(figsize=(8,8))

fig.suptitle('Portuguese Parliament Voting Records Analysis, 14th Legislature', fontsize=14)
ax.set_title('MDS with DBSCAN clusters (2D)')

for label, x, y in zip(distmat_mm.columns, coords[:, 0], coords[:, 1]):
    ax.scatter(x, y, c = "C"+str(dbscan_dict[label]), s=250)
    ax.axis('equal')
    ax.annotate(label,xy = (x-0.02, y+0.025))

plt.show()