# The code was removed by Watson Studio for sharing.
*Open Data and Data Science: Analysis of the Parliamentary Activity of the 14th Legislature of Portugal*
Frederico Muñoz frederico.munoz@pt.ibm.com
👉 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
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.
👉 *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.
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:
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
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.
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/ ).
!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]
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.
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:
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)
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))
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:
## 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:
ini_tree
é do tipo xml.etree.ElementTree.ElementTree
array
) de elementospt_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.
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:
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:
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:
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:
## 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)
É 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
## 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.
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:
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:
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):
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'}
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.
## 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'}
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.
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)
import pandas as pd
ini_df = pd.DataFrame(init_list)
print(ini_df.shape)
ini_df.head()
(2050, 20)
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:
ini_df.loc[0]
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.
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:
O seguinte bloco cria uma nova coluna, L/JKM
, composta da sobreposição dos votos de ambos.
## 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"]]
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:
## 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"]]
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
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:
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.
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:
act_df = pd.DataFrame(act_list)
print(act_df.shape)
act_df.head()
(226, 21)
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.
## 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()
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 |
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.
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.
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).
print(votes.shape)
votes.columns
(2276, 21)
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')
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)
votes_hm = votes[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN','PSD','IL','CDS-PP', 'CH']]
votes_hm.head()
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:
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:
Pode relacionar-se com uma maior produção de propostas a votação.
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.
Podem representar convergências em propostas apresentadas pelo próprio ou em matérias consideradas estratégicas.
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.
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.
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).
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.
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()
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:
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
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.
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.
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:
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.
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
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:
## 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:
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)
## 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:
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.
Já conseguimos determinar as distâncias e os agrupamentos, mas as possibilidades de visualização não se esgotam por aí.
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] $:
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)
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.
affinmat_mm = pd.DataFrame(1-distmat_mm, distmat.index, distmat.columns)
affinmat_mm
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:
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:
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
{'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.
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.
Até agora temos conseguido extrair informação interessante dos dados de votação:
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).
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
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:
## 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:
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()
e para Spectrum Clustering, com um maior número de clusters:
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 Spectrum Scaling clusters (2D)')
for label, x, y in zip(distmat_mm.columns, coords[:, 0], coords[:, 1]):
ax.scatter(x, y, c = "C"+str(sc_dict[label]), s=250)
ax.axis('equal')
ax.annotate(label,xy = (x-0.02, y+0.025))
plt.show()
É interessante reparar que ambos os métodos apresentam resultados compatíveis (os resultados de Spectrum Clustering são essencialmente uma divisão dentro dos grupos identificados por DBSCAN) e ambos fazem visualmente sentido quando consideradas as distâncias do diagrama de MDS.
Usámos MDS a duas dimensões, mas podemos fazê-lo também a 3 dimensões. Esta visualização será, provavelmente, a que mais interesse despertará: não por ser qualitativamente melhor que a anterior (adiciona de facto mais uma dimensão mas é discutível que seja mais compreensível), mas porque quem queira usar os dados apresentados até agora para justificar as suas próprias posições está de certa forma constrangido pela forma directa como as tabelas e os gráficos são exibidos: é sempre possível (e desejável) tecer considerações várias sobre o que foi apresentado mas essas considerações estão limitadas pela bidimensionalidade dos diagramas.
Tal não acontece com a visualização do MDS em 3D: na forma interactiva que apresentamos permite a manipulação (nos vários sentidos da palavra) do cubo de forma a construir seja qual for o "compasso", "quadrante" ou esquema similar que seja desejado: ajustando os vários eixos há sempre forma de se poder "provar" que determinado partido está "no extremo" ou que outro "está próximo", que um é "do centro" e os outros "marginais": tal como estrelas numa constelação pode-se construir artificialmente uma visão a duas dimensões que não reflecte a verdadeira distância.
Nesta espécie de pacote "faça o seu próprio compasso político" cada um passa a poder escolher o hiperplano ideológico que mais lhe convém, com a vantagem de poder ser apresentado como "baseado em dados e análises quantitativas" (mesmo que, ao omitir a terceira dimensão, esteja de facto a representar a informação pior do que a versão a duas dimensões).
Dito isto, a versão interactiva (e até certas visualização fixas de certos ângulos que permitem uma maior noção da terceira dimensão) é útil pois permite uma maior interactividade e adiciona mais uma dimensão, logo potencialmente mais informação.
Para podernos anotar o gráfico definimos algums funções auxiliares (“Python - Matplotlib: Annotating a 3D Scatter Plot - Stack Overflow” 2020)
## From https://stackoverflow.com/questions/10374930/matplotlib-annotating-a-3d-scatter-plot
from mpl_toolkits.mplot3d.proj3d import proj_transform
from matplotlib.text import Annotation
class Annotation3D(Annotation):
'''Annotate the point xyz with text s'''
def __init__(self, s, xyz, *args, **kwargs):
Annotation.__init__(self,s, xy=(0,0), *args, **kwargs)
self._verts3d = xyz
def draw(self, renderer):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj_transform(xs3d, ys3d, zs3d, renderer.M)
self.xy=(xs,ys)
Annotation.draw(self, renderer)
def annotate3D(ax, s, *args, **kwargs):
'''add anotation text s to to Axes3d ax'''
tag = Annotation3D(s, *args, **kwargs)
ax.add_artist(tag)
O processo é semelhante aos anteriores, com a definição de 3 dimensões via o parâmetro n_components
e alguns ajustes na contrução do gráfico:
from sklearn.manifold import MDS
import mpl_toolkits.mplot3d
import random
mds = MDS(n_components=3, dissimilarity='precomputed',random_state=1234, n_init=100, max_iter=1000)
results = mds.fit(distmat.values)
parties = distmat.columns
coords = results.embedding_
sns.set()
sns.set_style("ticks")
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot(111, projection='3d')
fig.suptitle('Portuguese Parliament Voting Records Analysis, 14th Legislature', fontsize=14)
ax.set_title('MDS with Spectrum Scaling clusters (3D)')
for label, x, y, z in zip(parties, coords[:, 0], coords[:, 1], coords[:, 2]):
#print(label,pmds_colors[label])
ax.scatter(x, y, z, c="C"+str(sc_dict[label]),s=250)
annotate3D(ax, s=str(label), xyz=[x,y,z], fontsize=10, xytext=(-3,3),
textcoords='offset points', ha='right',va='bottom')
plt.show()
Existe uma proliferação de recursos e questionários assentes em "quadrantes", "compassos" e outros termos que pretendem posicionar o utilizador em termos ideológicos, com metodologias diferentes (e com resultados diferentes) e de diversos graus de complexidade e sofisticação (Wall, Krouwel, and Kleinnijenhuis 2012). Este trabalho não pretende ser uma "revelação" ou emprestar uma base "científica" aos posicionamentos de cada um através de uma "matematização" do conhecimento enquanto valorizador qualitativo, lembrando Ortega y Gasset e a forma crítica como referia a tendência de todas disciplinas se quererem ao exemplo da física (Ortega y Gasset 1995).
Nesta conclusão iremos consciente e explicitamente abandonar pretensões de completa neutralidade e distanciamento, sem que isso signifique a execução de um determinado programa político pessoal: trata-se tão só do resultado natural da análise dos dados sob um prisma mais qualitativo.
Os resultados obtidos são, de certa forma, razoavelmente modestos em termos de novidades:
Há que ter em conta o facto do Partido Socialista ser o partido de sustentação do Governo, algo que determina de várias formas a forma como as votações ocorrem; de que forma é que este facto influencia o seu posicionamento não é possível quantificar mas, intutitivamente, não será irrelevante. Outro factor (embora não abordado de forma directa neste trabalho) é a aparente maior produção em termos de propostas dos partidos à esquerda, o que leva a m maior número de rejeições do partido que sustenta o Governo e, logo, um maior distânciamento.
À direita, e pondo de lado a já referida inclusão do PS que pode ter as razões apontadas como factor relevante, o PSD aparece com um posicionamento pouco surpreendente no sentido em que se enquadra no "centro-direita" que se esperaria, sem prejuízo do posicionamento mais amplo que em diferentes fases possa considerar adequadas (Lourinho 2020).
A distância entre CDS-PP, CHEGA e IL é bastante pequena (note-se que embora os partidos à esquerda sejam genericamente mais próximos exibem distâncias entre si maiores do que estes três); este resultado será talvez mais interessante para a IL no sentido em ser, como referido anteriormente, quem mais frequentemente recusa ser classificado em termos de "esquerda e direita" (“Iniciativa Liberal Descontente Com Lugar Atribuído a Deputado No Parlamento - TSF” 2020): em termos de votações esta pretensão não tem aparente materialização quantitativa, sempre dentro dos contragimentos explicitamente identificados.
À esquerda, o resultado não nos parece ser grandemente surpreendente: apesar das diferenças em várias votações o PCP e o PEV (este mais próximo dos restantes partidos agrupados à esquerda) aparecem como um bloco diferenciável do BE e Livre/JKM. No caso do PCP e PEV a proximidade não é estranha considerando serem partidos pertencentes a uma mesma coligação eleitoral (Lusa 2020b), mas também no caso do BE e Livre/JKM há que considerar a proximidade histórica existente (ZAP 2019).
A posição do PAN será, aqui, provavelmente a mais interessamente no sentido em que este se classifica como "nem de esquerda ou direita" (“Expresso | A História Do Partido Que Diz Que Não É de Direita Nem de Esquerda: O PAN” 2020): aparece "à esquerda" (estamos sempre aqui a mapear os resultados com uma realidade pre-existente) do Partido Socialista, e embora sendo por pouco não deixa de ser relevante.
Outras leituras podem ser feitas (e dadas as várias dimensões dos dados, leituras aparentemente opostas podem coexistir) tanto com os resultados apresentados como através da alterção da forma de tratamento dos dados.
Em termos de Data Science, este trabalho poderia (e poderá) ter uma continuidade, nomeadamente dentro do que são as possibilidade do Watson Studio, componente da plataforma "IBM Cloud Pak for Data" (“IBM Cloud Pak for Data” 2020): execução diária da extração de dados, validação dos mesmos, actualização dos resultados de forma automática e também a possibilidade de criação de modelos preditivos automaticamente actualizados e testados através de Watson Machine Learning (“Watson Machine Learning - Overview | IBM” 2020); estas possibilidades da plataforma IBM Watson são transversais a qualquer projecto de Data Science, sendo o presente trabalho apenas um exemplo que utiliza uma pequena parte das funcionalidades disponibilizadas. Em concreto, e em diferentes fases de execução, podemos referir as seguintes linhas de trabalho:
Para finalizar, e considerando sempre a necessidade de analise crítica dos dados apresentados e a sua correcção, recordamos quando um (excelente) professor (de uma matéria bastante relevante para a análise efectuada) utilizou, numa das suas aulas, uma citação que se mantém fundamental: "Se não temo o erro, é porque estou sempre disposto a corrigi-lo" (“Frase do matemático português Bento de Jesus Caraça”, SPM 2020). Também para este trabalho estamos necessariamente abertos a correcções e sugestões fundamentadas.
MUÑOZ, Frederico Serrano - Dados abertos e Ciência de Dados: Análise da Actividade Parlamentar da XIV Legislatura do Parlamento Português [Online] WWW: : https://dataplatform.cloud.ibm.com/analytics/notebooks/v2/0b23f01c-e55a-4fe4-a2ca-57f2e478bf3e/view?access_token=4cc13fba3f3967530650b11a2956ed27c5cec9042113237f97cb0e4c58d3905f
BibTeX
@report{munozDadosAbertosCiencia2020,
title = {Dados abertos e Ciência de Dados: Análise da Actividade Parlamentar da XIV Legislatura do Parlamento Português},
author = {Muñoz, Frederico Serrano},
date = {2020},
url = {https://dataplatform.cloud.ibm.com/analytics/notebooks/v2/0b23f01c-e55a-4fe4-a2ca-57f2e478bf3e/view?access_token=4cc13fba3f3967530650b11a2956ed27c5cec9042113237f97cb0e4c58d3905f},
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. A detailed explanation of how an euclidean distance matrix is obtained and how clustering and MDS work using test datasets is also included},
langid = {por, en},
type = {Jupyter Notebook}
}
Este bloco de notas pode ser executado na plataforma IBM Data Platform através da criação de um novo Notebook (https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/creating-notebooks.html?audience=wdp) e importação do URL (https://github.com/fsmunoz/pt-act-parlamentar/raw/master/Actividade%20Parlamentar%20da%20XIV%20Legislatura.ipynb).
É também possível utilizar o ambiente de execução Binder; a forma mais directa é através da integração disponibilizada no visualizador do projecto Jupyter e que para este projecto tem a seguinte ligação:
No canto superior direito o ícone do Binder (três círculos) permite iniciar um ambiente dedicado onde a interacção passa a ser possível.
Há uma diferença entre a primeira votação e a primeira iniciativa (e, em termos gerais, entre iniciativas e votações): existem iniciativas que por razões várias não têm votações associadas. Assim, note-se os detalhes da primeira iniciativa e como o tema é diferente do apresentados para a primeira iniciativa com votação mostrada anteriormente:
for c in ini_tree.find("pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut"):
print("{0:15}: {1}".format(c.tag,c.text))
iniNr : 28 iniTipo : A iniDescTipo : Apreciação Parlamentar iniLeg : XIV iniSel : 1 dataInicioleg : 2019-10-25 dataFimleg : 2020-09-14 iniTitulo : Decreto-Lei n.º 29/2020, de 29 de junho, que "Cria um programa de apoio ao emparcelamento rural simples, designado «Emparcelar para Ordenar»" iniTextoSubst : NAO iniLinkTexto : http://app.parlamento.pt/webutils/docs/doc.pdf?path=6148523063446f764c324679595842774f6a63334e7a637664326c756157357059326c6864476c3259584d7657456c574c33526c6548527663793977595841794f433159535659755a47396a&fich=pap28-XIV.doc&Inline=true iniEventos : None iniAutorDeputados: None iniAutorOutros : None iniId : 45156 iniAutorGruposParlamentares: None
Esta iniciativa não tem votações associadas pelo que não irá ser incluída na tabela que lista as votações, mas deveria ser incluída numa tabela com objectivos diferentes do presente trabalho, focada por exemplo nas iniciativas parlamentares; aproveitemo-la, porém, para demonstrar como extrair a informação da autoria da iniciativa: os campos onde aparece None
são quase sempre campos que contém informação com vários níveis e que é preciso extrair de forma especial. Para a autoria existe a secção iniAutorGruposParlamentares
que contém um objecto pt_gov_ar_objectos_AutoresGruposParlamentaresOut
com o nome do grupo parlamentar como conteúdo de GP
:
for c in ini_tree.find("pt_gov_ar_objectos_iniciativas_DetalhePesquisaIniciativasOut"):
if c.tag == "iniAutorGruposParlamentares":
print("{0:30}: {1}".format(c.tag,c.find('pt_gov_ar_objectos_AutoresGruposParlamentaresOut/GP').text))
else:
print("{0:30}: {1}".format(c.tag,c.text))
iniNr : 28 iniTipo : A iniDescTipo : Apreciação Parlamentar iniLeg : XIV iniSel : 1 dataInicioleg : 2019-10-25 dataFimleg : 2020-09-14 iniTitulo : Decreto-Lei n.º 29/2020, de 29 de junho, que "Cria um programa de apoio ao emparcelamento rural simples, designado «Emparcelar para Ordenar»" iniTextoSubst : NAO iniLinkTexto : http://app.parlamento.pt/webutils/docs/doc.pdf?path=6148523063446f764c324679595842774f6a63334e7a637664326c756157357059326c6864476c3259584d7657456c574c33526c6548527663793977595841794f433159535659755a47396a&fich=pap28-XIV.doc&Inline=true iniEventos : None iniAutorDeputados : None iniAutorOutros : None iniId : 45156 iniAutorGruposParlamentares : PCP
A autoria das iniciativas onde se integram as votações pode ser registada, mas é importante ter claro que esta autoria é independente da autoria da matéria em votação: uma iniciativa de um partido pode integrar votações propostas por outros. Esta situação não impacta a possiblidade de análise a que nos propomos mas abre áreas de exploração adicional para o futuro. Tendo em conta o objectivo deste trabalho optou-se por utilizar apenas a informação das votações de forma a tornar o código mais simples e focado, deixando para um segundo trabalho (em curso) uma contextualização mais ampla.
Começemos pela primeira usando a votação 86004
que representa o primeira caso ao ser uma votação nominal, uma das cinco relativas aos projectos relativos à eutanásia :
from bs4 import BeautifulSoup
def parse_voting1(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 = {}
for k, v in d.items():
for p in v:
## Sometimes there are empty entries ("boo, , bar, baz"), bypass them
if p != ' ':
votes[p.strip()] = k
return votes
for v in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='86004']"):
if v.tag == "detalhe":
vote_86004 = parse_voting1(v.text)
print("\t{0:15}: {1}".format(v.tag,parse_voting1(v.text)))
else:
print("\t{0:15}: {1}".format(v.tag,v.text))
id : 86004 resultado : Aprovado reuniao : 32 tipoReuniao : RP detalhe : {'Bruno Aragão (PS)': 'A Favor', 'Ana Maria Silva (PS)': 'A Favor', 'Joana Bento (PS)': 'A Favor', 'João Gouveia (PS)': 'A Favor', 'Ana Passos (PS)': 'A Favor', 'Francisco Pereira Oliveira (PS)': 'A Favor', 'Sara Velez (PS)': 'A Favor', 'Rita Borges Madeira (PS)': 'A Favor', 'Diogo Leão (PS)': 'A Favor', 'João Miguel Nicolau (PS)': 'A Favor', 'Alexandra Tavares de Moura (PS)': 'A Favor', 'Fernando Anastácio (PS)': 'A Favor', 'Fernando Paulo Ferreira (PS)': 'A Favor', 'Vera Braz (PS)': 'A Favor', 'Paulo Marques (PS)': 'A Favor', 'Eduardo Barroco de Melo (PS)': 'A Favor', 'Manuel dos Santos Afonso (PS)': 'A Favor', 'Mara Coelho (PS)': 'A Favor', 'Sofia Araújo (PS)': 'A Favor', 'Fernando José (PS)': 'A Favor', 'Clarisse Campos (PS)': 'A Favor', 'Cláudia Santos (PS)': 'A Favor', 'Filipe Neto Brandão (PS)': 'A Favor', 'Porfírio Silva (PS)': 'A Favor', 'Susana Correia (PS)': 'A Favor', 'Hugo Oliveira (PS)': 'A Favor', 'Joana Sá Pereira (PS)': 'A Favor', 'Telma Guerreiro (PS)': 'A Favor', 'Sónia Fertuzinhos (PS)': 'A Favor', 'Maria Begonha (PS)': 'A Favor', 'Hugo Pires (PS)': 'A Favor', 'Palmira Maciel (PS)': 'A Favor', 'Luís Soares (PS)': 'A Favor', 'Nuno Sá (PS)': 'A Favor', 'Pedro Coimbra (PS)': 'A Favor', 'Cristina Jesus (PS)': 'A Favor', 'Tiago Estevão Martins (PS)': 'A Favor', 'Luís Capoulas Santos (PS)': 'A Favor', 'Norberto Patinho (PS)': 'A Favor', 'Maria Joaquina Matos (PS)': 'A Favor', 'Luís Graça (PS)': 'A Favor', 'Santinho Pacheco (PS)': 'A Favor', 'Elza Pais (PS)': 'A Favor', 'João Paulo Pedrosa (PS)': 'A Favor', 'Edite Estrela (PS)': 'A Favor', 'Eduardo Ferro Rodrigues (PS)': 'A Favor', 'Maria da Luz Rosinha (PS)': 'A Favor', 'Marcos Perestrello (PS)': 'A Favor', 'Sérgio Sousa Pinto (PS)': 'A Favor', 'Pedro Delgado Alves (PS)': 'A Favor', 'Jorge Lacão (PS)': 'A Favor', 'Isabel Alves Moreira (PS)': 'A Favor', 'Ricardo Leão (PS)': 'A Favor', 'Miguel Matos (PS)': 'A Favor', 'Luís Moreira Testa (PS)': 'A Favor', 'Alexandre Quintanilha (PS)': 'A Favor', 'Rosário Gambôa (PS)': 'A Favor', 'Cristina Mendes da Silva (PS)': 'A Favor', 'Tiago Barbosa Ribeiro (PS)': 'A Favor', 'Isabel Oneto (PS)': 'A Favor', 'Bacelar de Vasconcelos (PS)': 'A Favor', 'Joana Lima (PS)': 'A Favor', 'Constança Urbano de Sousa (PS)': 'A Favor', 'José Magalhães (PS)': 'A Favor', 'Hugo Carvalho (PS)': 'A Favor', 'Carla Sousa (PS)': 'A Favor', 'António Gameiro (PS)': 'A Favor', 'Hugo Costa (PS)': 'A Favor', 'Eurídice Pereira (PS)': 'A Favor', 'Catarina Marcelino (PS)': 'A Favor', 'Maria Antónia de Almeida Santos (PS)': 'A Favor', 'Filipe Pacheco (PS)': 'A Favor', 'Marina Gonçalves (PS)': 'A Favor', 'Anabela Rodrigues (PS)': 'A Favor', 'Francisco Rocha (PS)': 'A Favor', 'José Rui Cruz (PS)': 'A Favor', 'João Azevedo Castro (PS)': 'A Favor', 'Olavo Câmara (PS)': 'A Favor', 'Paulo Pisco (PS)': 'A Favor', 'André Coelho Lima (PSD)': 'A Favor', 'António Maló de Abreu (PSD)': 'A Favor', 'Cristóvão Norte (PSD)': 'A Favor', 'Isabel Meireles (PSD)': 'A Favor', 'Lina Lopes (PSD)': 'A Favor', 'Hugo Martins de Carvalho (PSD)': 'A Favor', 'Rui Rio (PSD)': 'A Favor', 'Catarina Rocha Ferreira (PSD)': 'A Favor', 'Sofia Matos (PSD)': 'A Favor', 'Duarte Marques (PSD)': 'A Favor', 'Moisés Ferreira (BE)': 'A Favor', 'Nelson Peralta (BE)': 'A Favor', 'José Maria Cardoso (BE)': 'A Favor', 'Alexandra Vieira (BE)': 'A Favor', 'José Manuel Pureza (BE)': 'A Favor', 'João Vasconcelos (BE)': 'A Favor', 'Ricardo Vicente (BE)': 'A Favor', 'Mariana Mortágua (BE)': 'A Favor', 'Pedro Filipe Soares (BE)': 'A Favor', 'Beatriz Gomes Dias (BE)': 'A Favor', 'Jorge Costa (BE)': 'A Favor', 'Isabel Pires (BE)': 'A Favor', 'Catarina Martins (BE)': 'A Favor', 'José Moura Soeiro (BE)': 'A Favor', 'Luís Monteiro (BE)': 'A Favor', 'Maria Manuel Rola (BE)': 'A Favor', 'Fabíola Cardoso (BE)': 'A Favor', 'Joana Mortágua (BE)': 'A Favor', 'Sandra Cunha (BE)': 'A Favor', 'André Silva (PAN)': 'A Favor', 'Inês de Sousa Real (PAN)': 'A Favor', 'Bebiana Cunha (PAN)': 'A Favor', 'Cristina Rodrigues (PAN)': 'A Favor', 'Joacine Katar Moreira (Ninsc)': 'A Favor', 'João Cotrim de Figueiredo (IL)': 'A Favor', 'Célia Paz (PS)': 'Contra', 'Cristina Sousa (PS)': 'Contra', 'Maria da Graça Reis (PS)': 'Contra', 'Paulo Neves (PSD)': 'Contra', 'Bruno Dias (PCP)': 'Contra', 'João Gonçalves Pereira (CDS-PP)': 'Contra', 'Raul Miguel Castro (PS)': 'Contra', 'Pedro Cegonho (PS)': 'Contra', 'Romualda Fernandes (PS)': 'Contra', 'Ana Paula Vitorino (PS)': 'Contra', 'José Luís Carneiro (PS)': 'Contra', 'Ascenso Simões (PS)': 'Contra', 'António Topa (PSD)': 'Contra', 'Helga Correia (PSD)': 'Contra', 'Bruno Coimbra (PSD)': 'Contra', 'André Neves (PSD)': 'Contra', 'Carla Madureira (PSD)': 'Contra', 'Firmino Marques (PSD)': 'Contra', 'Clara Marques Mendes (PSD)': 'Contra', 'Carlos Eduardo Reis (PSD)': 'Contra', 'Jorge Paulo Oliveira (PSD)': 'Contra', 'Maria Gabriela Fonseca (PSD)': 'Contra', 'Emídio Guerreiro (PSD)': 'Contra', 'Isabel Lopes (PSD)': 'Contra', 'Cláudia André (PSD)': 'Contra', 'Paulo Leitão (PSD)': 'Contra', 'Rui Cristina (PSD)': 'Contra', 'Ofélia Ramos (PSD)': 'Contra', 'Carlos Peixoto (PSD)': 'Contra', 'Hugo Patrício Oliveira (PSD)': 'Contra', 'Olga Silvestre (PSD)': 'Contra', 'João Gomes Marques (PSD)': 'Contra', 'Filipa Roseta (PSD)': 'Contra', 'José Silvano (PSD)': 'Contra', 'Luís Marques Guedes (PSD)': 'Contra', 'Sandra Pereira (PSD)': 'Contra', 'Ricardo Baptista Leite (PSD)': 'Contra', 'Carlos Silva (PSD)': 'Contra', 'Alexandre Poço (PSD)': 'Contra', 'Alberto Machado (PSD)': 'Contra', 'José Cancela Moura (PSD)': 'Contra', 'Maria Germana Rocha (PSD)': 'Contra', 'Afonso Oliveira (PSD)': 'Contra', 'Álvaro Almeida (PSD)': 'Contra', 'Alberto Fonseca (PSD)': 'Contra', 'Paulo Rios de Oliveira (PSD)': 'Contra', 'Carla Barros (PSD)': 'Contra', 'Hugo Carneiro (PSD)': 'Contra', 'António Cunha (PSD)': 'Contra', 'Márcia Passos (PSD)': 'Contra', 'Isaura Morais (PSD)': 'Contra', 'João Moura (PSD)': 'Contra', 'Nuno Miguel Carvalho (PSD)': 'Contra', 'Fernando Negrão (PSD)': 'Contra', 'Fernanda Velez (PSD)': 'Contra', 'Jorge Salgueiro Mendes (PSD)': 'Contra', 'Eduardo Teixeira (PSD)': 'Contra', 'Luís Leite Ramos (PSD)': 'Contra', 'Cláudia Bento (PSD)': 'Contra', 'Artur Soveral Andrade (PSD)': 'Contra', 'Fernando Ruas (PSD)': 'Contra', 'Pedro Alves (PSD)': 'Contra', 'Carla Borges (PSD)': 'Contra', 'António Lima Costa (PSD)': 'Contra', 'Paulo Moniz (PSD)': 'Contra', 'António Ventura (PSD)': 'Contra', 'Sérgio Marques (PSD)': 'Contra', 'Sara Madruga da Costa (PSD)': 'Contra', 'Carlos Alberto Gonçalves (PSD)': 'Contra', 'José Cesário (PSD)': 'Contra', 'João Dias (PCP)': 'Contra', 'João Oliveira (PCP)': 'Contra', 'Jerónimo de Sousa (PCP)': 'Contra', 'Alma Rivera (PCP)': 'Contra', 'Duarte Alves (PCP)': 'Contra', 'Diana Ferreira (PCP)': 'Contra', 'Ana Mesquita (PCP)': 'Contra', 'António Filipe (PCP)': 'Contra', 'Paula Santos (PCP)': 'Contra', 'João Pinho de Almeida (CDS-PP)': 'Contra', 'Telmo Correia (CDS-PP)': 'Contra', 'Ana Rita Bessa (CDS-PP)': 'Contra', 'Cecília Meireles (CDS-PP)': 'Contra', 'André Ventura (CH)': 'Contra', 'Carlos Brás (PS)': 'Abstenção', 'José Manuel Carpinteira (PS)': 'Abstenção', 'Paulo Porto (PS)': 'Abstenção', 'Pedro do Carmo (PS)': 'Abstenção', 'Joaquim Barreto (PS)': 'Abstenção', 'Jorge Gomes (PS)': 'Abstenção', 'Hortense Martins (PS)': 'Abstenção', 'Nuno Fazenda (PS)': 'Abstenção', 'João Ataíde (PS)': 'Abstenção', 'Ricardo Pinheiro (PS)': 'Abstenção', 'João Paulo Correia (PS)': 'Abstenção', 'Pedro Sousa (PS)': 'Abstenção', 'Ana Catarina Mendonça Mendes (PS)': 'Abstenção', 'André Pinotes Batista (PS)': 'Abstenção', 'João Azevedo (PS)': 'Abstenção', 'Isabel Rodrigues (PS)': 'Abstenção', 'Lara Martinho (PS)': 'Abstenção', 'Marta Freitas (PS)': 'Abstenção', 'Ana Miguel dos Santos (PSD)': 'Abstenção', 'Rui Silva (PSD)': 'Abstenção', 'Adão Silva (PSD)': 'Abstenção', 'Pedro Pinto (PSD)': 'Abstenção', 'Mariana Silva (PEV)': 'Abstenção', 'José Luís Ferreira (PEV)': 'Abstenção'}
A divisão dos votos por partido para esta votação em específico é a seguinte para esta votação:
import pandas as pd
import re
## Initialise an empty dict
vote_e = {}
## Iteract through the existing dict with the nominal voting results
for k, v in vote_86004.items():
print(k,v)
## Erase the name of the MP and keep the party only
nk = re.sub(r".*\((.+?)\).*", r"\1", k)
## If it's the first entry for a key, create it
if nk not in vote_e:
vote_e[nk] = [0,0,0]
## Add to a specific index in a list
if v == "A Favor":
vote_e[nk][0] += 1
elif v == "Abstenção":
vote_e[nk][1] += 1
elif v == "Contra":
vote_e[nk][2] += 1
## Convert dict to pandas dataframe, add a total columns and display it
vote_e_df = pd.DataFrame.from_dict(vote_e, orient='index', columns=['A Favor', 'Abstenção', 'Contra'])
vote_e_df['Total'] = vote_e_df.sum(axis=1)
vote_e_df
Bruno Aragão (PS) A Favor Ana Maria Silva (PS) A Favor Joana Bento (PS) A Favor João Gouveia (PS) A Favor Ana Passos (PS) A Favor Francisco Pereira Oliveira (PS) A Favor Sara Velez (PS) A Favor Rita Borges Madeira (PS) A Favor Diogo Leão (PS) A Favor João Miguel Nicolau (PS) A Favor Alexandra Tavares de Moura (PS) A Favor Fernando Anastácio (PS) A Favor Fernando Paulo Ferreira (PS) A Favor Vera Braz (PS) A Favor Paulo Marques (PS) A Favor Eduardo Barroco de Melo (PS) A Favor Manuel dos Santos Afonso (PS) A Favor Mara Coelho (PS) A Favor Sofia Araújo (PS) A Favor Fernando José (PS) A Favor Clarisse Campos (PS) A Favor Cláudia Santos (PS) A Favor Filipe Neto Brandão (PS) A Favor Porfírio Silva (PS) A Favor Susana Correia (PS) A Favor Hugo Oliveira (PS) A Favor Joana Sá Pereira (PS) A Favor Telma Guerreiro (PS) A Favor Sónia Fertuzinhos (PS) A Favor Maria Begonha (PS) A Favor Hugo Pires (PS) A Favor Palmira Maciel (PS) A Favor Luís Soares (PS) A Favor Nuno Sá (PS) A Favor Pedro Coimbra (PS) A Favor Cristina Jesus (PS) A Favor Tiago Estevão Martins (PS) A Favor Luís Capoulas Santos (PS) A Favor Norberto Patinho (PS) A Favor Maria Joaquina Matos (PS) A Favor Luís Graça (PS) A Favor Santinho Pacheco (PS) A Favor Elza Pais (PS) A Favor João Paulo Pedrosa (PS) A Favor Edite Estrela (PS) A Favor Eduardo Ferro Rodrigues (PS) A Favor Maria da Luz Rosinha (PS) A Favor Marcos Perestrello (PS) A Favor Sérgio Sousa Pinto (PS) A Favor Pedro Delgado Alves (PS) A Favor Jorge Lacão (PS) A Favor Isabel Alves Moreira (PS) A Favor Ricardo Leão (PS) A Favor Miguel Matos (PS) A Favor Luís Moreira Testa (PS) A Favor Alexandre Quintanilha (PS) A Favor Rosário Gambôa (PS) A Favor Cristina Mendes da Silva (PS) A Favor Tiago Barbosa Ribeiro (PS) A Favor Isabel Oneto (PS) A Favor Bacelar de Vasconcelos (PS) A Favor Joana Lima (PS) A Favor Constança Urbano de Sousa (PS) A Favor José Magalhães (PS) A Favor Hugo Carvalho (PS) A Favor Carla Sousa (PS) A Favor António Gameiro (PS) A Favor Hugo Costa (PS) A Favor Eurídice Pereira (PS) A Favor Catarina Marcelino (PS) A Favor Maria Antónia de Almeida Santos (PS) A Favor Filipe Pacheco (PS) A Favor Marina Gonçalves (PS) A Favor Anabela Rodrigues (PS) A Favor Francisco Rocha (PS) A Favor José Rui Cruz (PS) A Favor João Azevedo Castro (PS) A Favor Olavo Câmara (PS) A Favor Paulo Pisco (PS) A Favor André Coelho Lima (PSD) A Favor António Maló de Abreu (PSD) A Favor Cristóvão Norte (PSD) A Favor Isabel Meireles (PSD) A Favor Lina Lopes (PSD) A Favor Hugo Martins de Carvalho (PSD) A Favor Rui Rio (PSD) A Favor Catarina Rocha Ferreira (PSD) A Favor Sofia Matos (PSD) A Favor Duarte Marques (PSD) A Favor Moisés Ferreira (BE) A Favor Nelson Peralta (BE) A Favor José Maria Cardoso (BE) A Favor Alexandra Vieira (BE) A Favor José Manuel Pureza (BE) A Favor João Vasconcelos (BE) A Favor Ricardo Vicente (BE) A Favor Mariana Mortágua (BE) A Favor Pedro Filipe Soares (BE) A Favor Beatriz Gomes Dias (BE) A Favor Jorge Costa (BE) A Favor Isabel Pires (BE) A Favor Catarina Martins (BE) A Favor José Moura Soeiro (BE) A Favor Luís Monteiro (BE) A Favor Maria Manuel Rola (BE) A Favor Fabíola Cardoso (BE) A Favor Joana Mortágua (BE) A Favor Sandra Cunha (BE) A Favor André Silva (PAN) A Favor Inês de Sousa Real (PAN) A Favor Bebiana Cunha (PAN) A Favor Cristina Rodrigues (PAN) A Favor Joacine Katar Moreira (Ninsc) A Favor João Cotrim de Figueiredo (IL) A Favor Célia Paz (PS) Contra Cristina Sousa (PS) Contra Maria da Graça Reis (PS) Contra Paulo Neves (PSD) Contra Bruno Dias (PCP) Contra João Gonçalves Pereira (CDS-PP) Contra Raul Miguel Castro (PS) Contra Pedro Cegonho (PS) Contra Romualda Fernandes (PS) Contra Ana Paula Vitorino (PS) Contra José Luís Carneiro (PS) Contra Ascenso Simões (PS) Contra António Topa (PSD) Contra Helga Correia (PSD) Contra Bruno Coimbra (PSD) Contra André Neves (PSD) Contra Carla Madureira (PSD) Contra Firmino Marques (PSD) Contra Clara Marques Mendes (PSD) Contra Carlos Eduardo Reis (PSD) Contra Jorge Paulo Oliveira (PSD) Contra Maria Gabriela Fonseca (PSD) Contra Emídio Guerreiro (PSD) Contra Isabel Lopes (PSD) Contra Cláudia André (PSD) Contra Paulo Leitão (PSD) Contra Rui Cristina (PSD) Contra Ofélia Ramos (PSD) Contra Carlos Peixoto (PSD) Contra Hugo Patrício Oliveira (PSD) Contra Olga Silvestre (PSD) Contra João Gomes Marques (PSD) Contra Filipa Roseta (PSD) Contra José Silvano (PSD) Contra Luís Marques Guedes (PSD) Contra Sandra Pereira (PSD) Contra Ricardo Baptista Leite (PSD) Contra Carlos Silva (PSD) Contra Alexandre Poço (PSD) Contra Alberto Machado (PSD) Contra José Cancela Moura (PSD) Contra Maria Germana Rocha (PSD) Contra Afonso Oliveira (PSD) Contra Álvaro Almeida (PSD) Contra Alberto Fonseca (PSD) Contra Paulo Rios de Oliveira (PSD) Contra Carla Barros (PSD) Contra Hugo Carneiro (PSD) Contra António Cunha (PSD) Contra Márcia Passos (PSD) Contra Isaura Morais (PSD) Contra João Moura (PSD) Contra Nuno Miguel Carvalho (PSD) Contra Fernando Negrão (PSD) Contra Fernanda Velez (PSD) Contra Jorge Salgueiro Mendes (PSD) Contra Eduardo Teixeira (PSD) Contra Luís Leite Ramos (PSD) Contra Cláudia Bento (PSD) Contra Artur Soveral Andrade (PSD) Contra Fernando Ruas (PSD) Contra Pedro Alves (PSD) Contra Carla Borges (PSD) Contra António Lima Costa (PSD) Contra Paulo Moniz (PSD) Contra António Ventura (PSD) Contra Sérgio Marques (PSD) Contra Sara Madruga da Costa (PSD) Contra Carlos Alberto Gonçalves (PSD) Contra José Cesário (PSD) Contra João Dias (PCP) Contra João Oliveira (PCP) Contra Jerónimo de Sousa (PCP) Contra Alma Rivera (PCP) Contra Duarte Alves (PCP) Contra Diana Ferreira (PCP) Contra Ana Mesquita (PCP) Contra António Filipe (PCP) Contra Paula Santos (PCP) Contra João Pinho de Almeida (CDS-PP) Contra Telmo Correia (CDS-PP) Contra Ana Rita Bessa (CDS-PP) Contra Cecília Meireles (CDS-PP) Contra André Ventura (CH) Contra Carlos Brás (PS) Abstenção José Manuel Carpinteira (PS) Abstenção Paulo Porto (PS) Abstenção Pedro do Carmo (PS) Abstenção Joaquim Barreto (PS) Abstenção Jorge Gomes (PS) Abstenção Hortense Martins (PS) Abstenção Nuno Fazenda (PS) Abstenção João Ataíde (PS) Abstenção Ricardo Pinheiro (PS) Abstenção João Paulo Correia (PS) Abstenção Pedro Sousa (PS) Abstenção Ana Catarina Mendonça Mendes (PS) Abstenção André Pinotes Batista (PS) Abstenção João Azevedo (PS) Abstenção Isabel Rodrigues (PS) Abstenção Lara Martinho (PS) Abstenção Marta Freitas (PS) Abstenção Ana Miguel dos Santos (PSD) Abstenção Rui Silva (PSD) Abstenção Adão Silva (PSD) Abstenção Pedro Pinto (PSD) Abstenção Mariana Silva (PEV) Abstenção José Luís Ferreira (PEV) Abstenção
A Favor | Abstenção | Contra | Total | |
---|---|---|---|---|
PS | 79 | 18 | 9 | 106 |
PSD | 10 | 4 | 59 | 73 |
BE | 19 | 0 | 0 | 19 |
PAN | 4 | 0 | 0 | 4 |
Ninsc | 1 | 0 | 0 | 1 |
IL | 1 | 0 | 0 | 1 |
PCP | 0 | 0 | 10 | 10 |
CDS-PP | 0 | 0 | 5 | 5 |
CH | 0 | 0 | 1 | 1 |
PEV | 0 | 2 | 0 | 2 |
Um gráfico de barras torna claro que a divisão ocorreu apenas nos dois maiores grupos parlamentares.
## Plot a stacked bar plot using the dataframe
import seaborn as sns
legend_colors = ["#79DE79", "#FCFC99","#FB6962"]
sns.set(style="whitegrid")
## Plot sorted by total number of votes
vote_e_df.sort_values('Total', ascending=False).drop('Total',axis=1).plot(kind="bar", stacked=True, color=sns.color_palette(legend_colors))
<matplotlib.axes._subplots.AxesSubplot at 0x7fa90e70b7f0>
O segundo caso diz respeito a votações onde a posição partidária é indicada e apenas as excepções incluídas; nestes casos existem uma determinada posição maioritária à qual se registam os desvios. Não existe forma, na verdade, de determinar se estamos perante uma posição mais ou menos "oficial" que a determinada pelo caso anterior, até porque isso implicaria saber se foi relaxada ou não a disciplina de voto, algo que não é possível obter dos dados em si.
Também estes casos são excepcionais e quando existem são quase sempre com um número reduzido de excepções; atentemos à votação 87724
, a primeira que apresenta uma situação deste tipo:
for v in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='87724']"):
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 : 87724 resultado : Aprovado descricao : Texto Final apresentado pela Comissão de Saúde relativo às Apreciações Parlamentares n.ºs 12/XIV/1.ª (BE); 13/XIV/1.ª (PCP) e 20/XIV/1.ª (PSD) reuniao : 68 tipoReuniao : RP detalhe : {'PSD': 'A Favor', 'BE': 'A Favor', 'PCP': 'A Favor', 'CDS-PP': 'A Favor', 'PAN': 'A Favor', 'PEV': 'A Favor', 'CH': 'A Favor', 'IL': 'A Favor', 'Cristina Rodrigues (Ninsc)': 'A Favor', 'Joacine Katar Moreira (Ninsc)': 'A Favor', 'PS': 'Contra'}
Para além das deputadas não-inscritas temos a indicação do sentido de votos dos partidos e também a de dois deputados do PS que votaram de forma diferente do seu grupo parlamentar.
Como considerar esta situação? Em coerência com a opção anterior consideramos a posição indicada pelo partido como sendo aquela que determina o seu posicionamento, não considerando as excepções. A justificação é semelhante à anterior embora neste caso tenha menos impacto: acontece sobretudo nos grupos parlamentares de maior dimensão (PS e PSD) e, como referido, de forma muito esporádica e com um reduzido número de deputados com votações desviantes.
A votação 87946 apresenta-nos o terceiro caso, uma variante adicional quando o número de votos em sentido divergente é maior:
for v in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='87946']"):
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 : 87946 resultado : Rejeitado reuniao : 75 tipoReuniao : RP ausencias : None detalhe : {'BE': 'A Favor', 'PAN': 'A Favor', 'PEV': 'A Favor', 'IL': 'A Favor', 'Cristina Rodrigues (Ninsc)': 'A Favor', 'PS': 'Contra', 'PSD': 'Contra', 'PCP': 'Contra', 'CDS-PP': 'Contra', 'CH': 'Contra', 'Joacine Katar Moreira (Ninsc)': 'Ausência'}
Aqui temos um indicador numérico como prefixo e que indica quantidade de deputados de um determinado grupo parlamentar (neste caso do PS) que votaram de forma divergente. Também aqui usamos o mesmo princípio e retemos a votação indicada como sendo a do partido e por isso descartam-se estas indicações.
Para obter informação adicional das iniciativas onde se incluem as votações podem-se usar várias abordagens, desde o processamento de todas as iniciativas de forma recursiva à procura a partir do identificador da votação; o seguinte bloco exemplifica a segunda aproximação para a primeira votação encontrada:
import re
import collections
root = ini_tree
init_list = []
init_dict = collections.OrderedDict()
def get_initiative_desc (vid, tree):
"""
Gets the initiative title from a voting id
"""
for c in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut/[id='"+ vid +"']/../../../.."):
if c.tag == "iniTitulo":
return c.text
for c in ini_tree.find(".//pt_gov_ar_objectos_VotacaoOut"):
if c.tag == "id":
init_dict["iniDesc"] = get_initiative_desc(c.text, ini_tree)
init_dict[c.tag] = c.text
init_list.append(init_dict)
ini_df_extra = pd.DataFrame(init_list)
print(ini_df_extra.shape)
ini_df_extra
(1, 7)
iniDesc | id | resultado | descricao | reuniao | tipoReuniao | detalhe | |
---|---|---|---|---|---|---|---|
0 | Decreto-Lei n.º 27/2020 de 17 de junho (Altera... | 88184 | Aprovado | Texto Final apresentado pela Comissão de Admin... | 76 | RP | A Favor: <I>PS</I>, <I>PSD</I><BR>Contra: <I>B... |
Esta abordagem não é usada para as Iniciativas mas será aplicada às Actividades, essencialmente porque neste último caso há uma relação mais directa entre a matéria a votação e a actividade parlamentar (de tal forma que a votação não inclui uma descrição própria).
Existem dois casos que precisam ser analisados de forma individual.
Começando por Joacine Katar Moreira, a deputada eleita pelo Livre desvinculou-se pouco tempo após a eleição (“Joacine Katar Moreira demite-se do Livre - DN” 2020) e sendo deputada única a situação criada em termos de registo de votação é de complementaridade: ou há registo de votação do Livre ou há registo de votação da deputada Joacine Katar Moreira.
print("Livre: ", ini_df["L"].dropna().shape[0])
print("JKM: ", ini_df["Joacine Katar Moreira (Ninsc)"].dropna().shape[0])
ini_df[["L","Joacine Katar Moreira (Ninsc)"]].head()
Livre: 138 JKM: 1865
L | Joacine Katar Moreira (Ninsc) | |
---|---|---|
0 | NaN | Contra |
1 | NaN | Contra |
2 | NaN | Contra |
3 | NaN | A Favor |
4 | NaN | A Favor |
Um caso diferente é o da deputada Cristina Rodrigues, eleita por Setúbal pelo PAN e que se desvinculou já perto do final da legislatura (SAPO 2020): não só porque o número de votações efectadas enquanto deputada não-inscrita é bastante menor, como o facto de não ser deputada única faz com que existam votações onde a deputada e o seu anterior grupo parlamentar ambos votam, e como se pode verificar na tabela seguinte , várias vezes de forma diferente:
print("PAN: ", ini_df["PAN"].dropna().shape[0])
print("CR: ", ini_df["Cristina Rodrigues (Ninsc)"].dropna().shape[0])
ini_df[["PAN","Cristina Rodrigues (Ninsc)"]].dropna()[ini_df["PAN"] != ini_df["Cristina Rodrigues (Ninsc)"]]
PAN: 2038 CR: 730
/opt/conda/envs/Python36/lib/python3.6/site-packages/ipykernel/__main__.py:3: UserWarning: Boolean Series key will be reindexed to match DataFrame index. app.launch_new_instance()
PAN | Cristina Rodrigues (Ninsc) | |
---|---|---|
105 | Abstenção | A Favor |
108 | A Favor | Abstenção |
109 | A Favor | Abstenção |
115 | A Favor | Contra |
134 | A Favor | Abstenção |
137 | Abstenção | A Favor |
139 | Abstenção | A Favor |
146 | Abstenção | A Favor |
150 | Contra | Abstenção |
166 | Contra | Abstenção |
167 | Abstenção | A Favor |
171 | Contra | Abstenção |
178 | Abstenção | A Favor |
191 | Contra | A Favor |
201 | A Favor | Abstenção |
213 | A Favor | Abstenção |
221 | Contra | Abstenção |
222 | Contra | Abstenção |
223 | Contra | Abstenção |
267 | Contra | A Favor |
303 | A Favor | Contra |
304 | A Favor | Contra |
305 | A Favor | Contra |
306 | A Favor | Abstenção |
308 | A Favor | Abstenção |
349 | Contra | A Favor |
490 | Abstenção | A Favor |
505 | Abstenção | A Favor |
513 | Abstenção | A Favor |
518 | Contra | A Favor |
... | ... | ... |
1353 | A Favor | Abstenção |
1354 | A Favor | Abstenção |
1355 | A Favor | Abstenção |
1356 | A Favor | Abstenção |
1362 | Contra | Abstenção |
1364 | A Favor | Abstenção |
1365 | A Favor | Abstenção |
1366 | Contra | Abstenção |
1367 | Contra | Abstenção |
1368 | Contra | Abstenção |
1369 | Contra | Abstenção |
1373 | Contra | Abstenção |
1376 | Abstenção | A Favor |
1377 | Abstenção | A Favor |
1378 | Abstenção | A Favor |
1398 | A Favor | Abstenção |
1399 | Contra | Abstenção |
1405 | Abstenção | A Favor |
1426 | A Favor | Abstenção |
1928 | A Favor | Abstenção |
1936 | A Favor | Abstenção |
1965 | Abstenção | A Favor |
1968 | Contra | A Favor |
1975 | Abstenção | A Favor |
1983 | Contra | A Favor |
2003 | Abstenção | Contra |
2007 | Abstenção | A Favor |
2009 | Abstenção | A Favor |
2010 | Abstenção | A Favor |
2031 | Contra | Abstenção |
107 rows × 2 columns
Existem várias formas de se obter o resultado deste processamento; a que apresentamos utiliza PixieDust
(“Welcome to PixieDust — PixieDust Documentation” 2020), uma biblioteca que permite trabalhar directamente com os dados sem se ter neessidade de modificar o código, incluíndo visualizações, filtragem e e também descarregar em vários formatos.
from pixiedust.display import *
display(votes)
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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Abstenção | Contra | Contra | Abstenção | Contra | nan | nan | nan | Abstenção | Abstenção | Contra | Contra | A Favor | Abstenção | I | nan | Texto Final apresentado pela Comissão de Orçamento e Finanças relativo à Proposta de Lei n.º 33/XIV/1.ª (GOV) | 87794 | Aprovado | 71 | nan |
Abstenção | A Favor | A Favor | A Favor | A Favor | Contra | nan | Contra | A Favor | A Favor | Contra | Contra | Contra | A Favor | I | nan | Ponto 2 | 88272 | Rejeitado | 76 | nan |
A Favor | Abstenção | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | nan | 87599 | Rejeitado | 65 | nan |
A Favor | Abstenção | Abstenção | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Abstenção | I | nan | Corpo do artigo 2.º | 86986 | Aprovado | 51 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento oral, apresentado pelo PS, solicitando a dispensa de redação final e do prazo para apresentação de reclamações contra inexatidões relativamente à Proposta de Lei n.º 18/XIV/1.ª (GOV), à Proposta de Lei n.º 20/XIV/1.ª (GOV), à Proposta de Lei n.º 21/XIV/1.ª (GOV), ao Projeto de Lei n.º 285/XIV/1.ª (PCP) e ao Projeto de Lei n.º 292/XIV/1.ª (PCP) | 86400 | Aprovado | 44 | unanime |
Abstenção | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | Abstenção | A Favor | I | nan | nan | 87405 | Aprovado | 60 | nan |
A Favor | A Favor | Abstenção | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | Texto de Substituição apresentado pela Comissão de Defesa Nacional relativo à Proposta de Lei n.º 3/XIV/1.ª (GOV) e aos Projetos de Lei n.ºs 27/XIV/1.ª (CDS-PP), 57/XIV/1.ª (PAN), 121/XIV/1.ª (PCP), 180/XIV/1.ª (BE) e 193/XIV/1.ª (PSD) | 88337 | Aprovado | 76 | nan |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Votação da assunção pelo Plenário das votações indiciárias realizadas na especialidade em sede de Comissão - Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo ao Projeto de Lei n.º 117/XIV/1.ª (PAN) e Projeto de Lei n.º 118/XIV/1.ª (PCP) | 88180 | Aprovado | 76 | unanime |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | nan | 86052 | Aprovado | 35 | unanime |
Contra | A Favor | Contra | nan | A Favor | Contra | nan | Contra | Abstenção | Abstenção | Contra | Contra | Contra | Contra | I | nan | Proposta de alteração do CDS-PP, de aditamento de um n.º 6 ao artigo 2.º da proposta de lei | 86703 | Rejeitado | 45 | nan |
A Favor | A Favor | Ausência | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | CH | Requerimento, apresentado pelo PCP solicitando a baixa à Comissão Competente, sem votação, por 60 dias, do Projeto de Lei n.º 200/XIV/1.ª (PCP) | 86130 | Aprovado | 38 | unanime |
Abstenção | Abstenção | nan | nan | A Favor | Abstenção | nan | Abstenção | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | nan | 85945 | Aprovado | 29 | nan |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | nan | De pesar pela morte em serviço dos bombeiros Filipe André Azinheiro Pedrosa e José Augusto Dias Fernandes | 88157 | Aprovado | 76 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PSD, de avocação pelo Plenário da votação na especialidade de artigos do Texto de Substituição apresentado pela Comissão de Economia, Inovação, Obras Públicas e Habitação relativo à Proposta de Lei n.º 41/XIV/1.ª (GOV) - Requerimento, apresentado pelo PS, de avocação pelo Plenário da votação na especialidade do artigo 70.º do Decreto-Lei n.º 18/2008, de 29 de janeiro, constante no artigo 19.º do Texto de Substituição apresentado pela Comissão de Economia, Inovação, Obras Públicas e Habitação relativo à Proposta de Lei n.º 41/XIV/1.ª (GOV) | 88967 | Aprovado | 15 | unanime |
A Favor | A Favor | Abstenção | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | A Favor | I | nan | nan | 85605 | Aprovado | 17 | nan |
A Favor | Contra | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do PCP - n.º 3 do artigo 24.º | 86781 | Rejeitado | 45 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Texto Final apresentado pela Comissão de Ambiente, Energia e Ordenamento do Território relativo aos Projetos de Resolução n.ºs 52/XIV/1.ª (BE); 58/XIV/1.ª (PEV) e 130/XIV/1.ª (PSD). | 85869 | Aprovado | 26 | unanime |
A Favor | Abstenção | Abstenção | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Votação da proposta apresentada pelo PCP, de aditamento de um novo n.º 2 ao artigo 21.º do Decreto-Lei n.º 10-A/2020, de 13 de março | 86231 | Rejeitado | 42 | nan |
A Favor | Abstenção | Abstenção | nan | Contra | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Abstenção | I | nan | nan | 85370 | Rejeitado | 7 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PEV solicitando a baixa à Comissão de Administração Pública, Modernização Administrativa, Descentralização e Poder Local, sem votação, por 30 dias, dos Projetos de Lei n.º 398/XIV/1.ª (PEV) e 399/XIV/1.ª (PEV) | 87750 | Aprovado | 60 | unanime |
A Favor | Contra | Abstenção | nan | Contra | A Favor | nan | A Favor | Contra | Contra | A Favor | A Favor | Contra | Contra | I | nan | Proposta apresentada pelo PCP, de eliminação do artigo 4.º da proposta de lei | 86371 | Rejeitado | 44 | nan |
Abstenção | Abstenção | Abstenção | nan | A Favor | Abstenção | nan | Abstenção | A Favor | A Favor | Abstenção | Contra | Contra | Contra | I | nan | Proposta de aditamento do PAN de um artigo 2.º-A ao texto final | 87289 | Rejeitado | 57 | nan |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo BE, de avocação pelo Plenário da votação na especialidade das propostas de alteração relativas ao Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo ao Projeto de Lei n.º 117/XIV/1.ª (PAN) e Projeto de Lei n.º 118/XIV/1.ª (PCP) | 88193 | Aprovado | 76 | unanime |
A Favor | Abstenção | Ausência | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | CH | nan | 88883 | Aprovado | 12 | nan |
Abstenção | Abstenção | Abstenção | nan | A Favor | Abstenção | nan | Abstenção | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Proposta apresentada pelo PSD, de aditamento de um artigo 6.º à proposta de lei | 86361 | Aprovado | 44 | nan |
A Favor | Contra | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do PCP - n.º 2 ao artigo 21.º | 86754 | Rejeitado | 45 | nan |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | nan | De pesar pela morte de Dame Vera Lynn | 87668 | Aprovado | 68 | unanime |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | A Favor | A Favor | A | nan | De congratulação pelo reconhecimento internacional ao setor do Turismo em Portugal | 85581 | Aprovado | 17 | nan |
Contra | A Favor | Abstenção | A Favor | A Favor | Ausência | nan | Ausência | A Favor | A Favor | Contra | Contra | Contra | Abstenção | I | JOACINE KATAR MOREIRA (Ninsc) | nan | 87966 | Rejeitado | 75 | nan |
Abstenção | A Favor | Ausência | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | CH | Requerimento oral, apresentado pelo PS, solicitando a dispensa de redação final e do prazo para apresentação de reclamações contra inexatidões relativamente do Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo aos Projetos de Lei n.ºs 505/XIV/1.ª (PSD) e 549/XIV/2.ª (PS); e do Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo ao Projeto de Lei n.º 547/XIV/2.ª (PS) | 89075 | Aprovado | 17 | nan |
A Favor | Abstenção | nan | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | nan | 85924 | Rejeitado | 29 | nan |
Abstenção | Abstenção | Abstenção | nan | A Favor | Abstenção | nan | Abstenção | A Favor | A Favor | Contra | Contra | A Favor | Abstenção | I | nan | N.º 5 do artigo 5.º da proposta de lei | 86407 | Aprovado | 44 | nan |
A Favor | Abstenção | nan | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Abstenção | I | nan | nan | 86136 | Rejeitado | 38 | nan |
A Favor | Contra | Contra | nan | A Favor | A Favor | nan | A Favor | Abstenção | Abstenção | Contra | Contra | Contra | Abstenção | I | nan | Proposta de alteração da NINSC, de substituição do n.º 2 do artigo 4.º da proposta de lei | 86529 | Rejeitado | 45 | nan |
A Favor | A Favor | Abstenção | nan | Abstenção | Abstenção | nan | Abstenção | Contra | Contra | A Favor | A Favor | Contra | A Favor | I | nan | Proposta de alteração do PSD, de eliminação do n.º 8 do artigo 2.º | 86934 | Aprovado | 49 | nan |
A Favor | Contra | Abstenção | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do PAN - n.º 3 do artigo 26.º | 86797 | Rejeitado | 45 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PCP a requerer a votação na generalidade, especialidade e votação final global do Projeto de Lei n.º 285/XIV/1.ª (PCP) | 86323 | Aprovado | 44 | unanime |
A Favor | A Favor | Ausência | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | CH | Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo à Proposta de Lei n.º 2/XIV/1.ª (GOV) | 86171 | Aprovado | 38 | unanime |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PAN solicitando a baixa à Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias, sem votação, por 60 dias, do Projeto de Lei n.º 52/XIV/1.ª (PAN) | 85592 | Aprovado | 17 | unanime |
A Favor | Abstenção | Contra | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Proposta de alteração do PS, de aditamento de um artigo 5.º-D à proposta de lei | 86732 | Aprovado | 45 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | A Favor | I | nan | Texto Final apresentado pela Comissão de Educação, Ciência, Juventude e Desporto relativo aos Projeto de Resolução n.ºs 105/XIV/1.ª (BE), 173/XIV/1.ª (PCP) e 207/XIV/1.ª (PAN) | 87442 | Aprovado | 60 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Artigo 4.º da proposta de lei | 86448 | Aprovado | 44 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do PCP – Artigo 8.º do texto de substituição | 88313 | Rejeitado | 76 | nan |
A Favor | Contra | A Favor | nan | Abstenção | nan | A Favor | A Favor | A Favor | A Favor | Contra | Abstenção | Contra | Contra | I | nan | nan | 85720 | Rejeitado | 20 | nan |
A Favor | A Favor | A Favor | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | Abstenção | A Favor | A Favor | I | nan | nan | 87403 | Aprovado | 60 | nan |
A Favor | Contra | Ausência | A Favor | A Favor | A Favor | nan | A Favor | Abstenção | A Favor | Contra | A Favor | A Favor | Contra | I | CH | nan | 89133 | Aprovado | 17 | nan |
A Favor | Contra | Abstenção | nan | Contra | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do PCP - n.º 7 ao artigo 23.º | 86773 | Rejeitado | 45 | nan |
A Favor | A Favor | Contra | A Favor | Contra | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | nan | 88312 | Aprovado | 76 | nan |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | nan | De preocupação pela Inexistência de conclusões relativamente ao desaparecimento do empresário Américo Sebastião, em Moçambique | 85454 | Aprovado | 13 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Texto de Substituição apresentado pela Comissão de Orçamento e Finanças relativo aos Projetos de Lei n.ºs 410/XIV/1.ª (CDS-PP) e 441/XIV/1.ª (PS) - Votação da assunção pelo Plenário das votações indiciárias realizadas na especialidade em sede de Comissão | 88226 | Aprovado | 76 | unanime |
A Favor | Abstenção | nan | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta de alteração do BE – aditamento de um n.º 5 ao artigo 11.º da Lei n.º 4-C/2020, de 6 de abril | 87206 | Rejeitado | 55 | nan |
A Favor | A Favor | Ausência | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | CH | De pesar pelo falecimento de Fernando Tavares Loureiro | 85904 | Aprovado | 29 | unanime |
A Favor | Contra | Contra | nan | Contra | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta apresentada pela Ninsc, de emenda do n.º 5 do artigo 5.º da proposta de lei | 86397 | Rejeitado | 44 | nan |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo BE solicitando a baixa à Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias, sem votação, por 60 dias, do Projeto de Lei n.º 114/XIV/1.ª (BE) | 85596 | Aprovado | 17 | unanime |
A Favor | Contra | Abstenção | A Favor | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | nan | 87682 | Rejeitado | 68 | nan |
Abstenção | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo ao Projeto de Lei n.º 99/XIV/1.ª (PSD) | 87312 | Aprovado | 57 | nan |
A Favor | Contra | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Proposta apresentada pelo BE, de aditamento de um n.º 6 ao artigo 5.º da proposta de lei | 86410 | Rejeitado | 44 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Texto Final apresentado pela Comissão de Ambiente, Energia e Ordenamento do Território relativo aos Projetos de Resolução n.ºs 52/XIV/1.ª (BE); 58/XIV/1.ª (PEV) e 130/XIV/1.ª (PSD). | 85870 | Aprovado | 26 | unanime |
A Favor | Contra | Abstenção | nan | Contra | A Favor | nan | A Favor | Abstenção | Abstenção | A Favor | A Favor | Contra | Contra | I | nan | nan | 86636 | Rejeitado | 45 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | A Favor | I | nan | Texto Final apresentado pela Comissão de Educação, Ciência, Juventude e Desporto relativo aos Projeto de Resolução n.ºs 105/XIV/1.ª (BE), 173/XIV/1.ª (PCP) e 207/XIV/1.ª (PAN) | 87445 | Aprovado | 60 | nan |
A Favor | A Favor | Ausência | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | CH | Requerimento, apresentado pela PS solicitando a baixa à Comissão de Trabalho e Segurança Social, sem votação, por 30 dias, da Proposta de Lei n.º 57/XIV/2.ª (GOV) e da Proposta de Lei n.º 59/XIV/2.ª (GOV) | 89126 | Aprovado | 17 | unanime |
A Favor | Contra | A Favor | nan | Contra | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | nan | 85717 | Rejeitado | 20 | nan |
Contra | Contra | Contra | Contra | Contra | Contra | nan | Contra | Contra | Contra | Contra | Contra | A Favor | A Favor | I | nan | Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo ao Projeto de Lei n.º 459/XIV/1.ª (PSD) | 88395 | Aprovado | 76 | nan |
A Favor | A Favor | A Favor | A Favor | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Abstenção | A Favor | I | nan | Texto de Substituição apresentado pela Comissão de Trabalho e Segurança Social relativo aos Projetos de Resolução n.ºs 393/XIV/1.ª (BE), 403/XIV/1.ª (PSD) e 413/XIV/1.ª (CDS-PP) | 88207 | Aprovado | 76 | nan |
A Favor | Abstenção | Contra | nan | Abstenção | Abstenção | nan | Abstenção | Abstenção | Abstenção | A Favor | A Favor | Contra | Contra | I | nan | nan | 85990 | Rejeitado | 32 | nan |
Contra | A Favor | A Favor | Contra | Abstenção | Contra | nan | Contra | Contra | Contra | Contra | Contra | Contra | Contra | I | nan | nan | 87786 | Rejeitado | 70 | nan |
A Favor | Contra | Abstenção | nan | Contra | Contra | nan | Contra | A Favor | A Favor | Abstenção | Abstenção | Contra | Contra | I | nan | Proposta apresentada pelo PAN, de emenda do n.º 3 do artigo 5.º da proposta de lei | 86386 | Rejeitado | 44 | nan |
A Favor | Contra | Abstenção | nan | Contra | Abstenção | nan | Abstenção | Contra | Contra | A Favor | A Favor | Contra | Contra | I | nan | Proposta apresentada pelo PEV, de emenda ao artigo 8.º da proposta de lei | 86427 | Rejeitado | 44 | nan |
Abstenção | Abstenção | A Favor | nan | A Favor | Contra | nan | Contra | A Favor | A Favor | Contra | Contra | Contra | Contra | I | nan | Votação da proposta apresentada pelo CH, de emenda do corpo do artigo 6.º da proposta de lei | 86267 | Rejeitado | 42 | nan |
Abstenção | Abstenção | A Favor | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Abstenção | I | nan | Proposta de alteração do PAN – aditamento de um n.º 2 ao artigo 8.º-A da Lei n.º 1-A/2020, de 19 de março, constante do artigo 3.º | 86988 | Rejeitado | 51 | nan |
Contra | Abstenção | nan | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | Contra | Contra | I | nan | Ponto 2 | 85939 | Rejeitado | 29 | nan |
Abstenção | A Favor | Abstenção | nan | Abstenção | Abstenção | nan | Abstenção | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | nan | 86827 | Aprovado | 46 | nan |
A Favor | Contra | Contra | nan | Contra | A Favor | nan | A Favor | Contra | Contra | A Favor | A Favor | Contra | Contra | I | nan | Proposta apresentada pelo PCP, de emenda das alíneas e) e f) do n.º 3 do artigo 2.º da proposta de lei | 86414 | Rejeitado | 44 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PCP solicitando a baixa à Comissão de Trabalho e Segurança Social, sem votação, por 30 dias, do Projeto de Lei n.º 427/XIV/1.ª (PCP) | 87479 | Aprovado | 62 | unanime |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | Contra | A Favor | I | nan | nan | 87487 | Aprovado | 62 | nan |
A Favor | Abstenção | Abstenção | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | nan | N.º 2 do artigo 4.º da proposta de lei | 86533 | Aprovado | 45 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento, apresentado pelo PCP a requerer a votação na generalidade, especialidade e votação final global do Projeto de Lei n.º 292/XIV/1.ª (PCP) | 86331 | Aprovado | 44 | unanime |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | nan | De saudação pelo 1.º de Dezembro de 1640 | 85559 | Aprovado | 17 | unanime |
A Favor | Contra | Contra | nan | Contra | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Abstenção | Contra | A | nan | De condenação da repressão contra as manifestações populares no Chile | 85349 | Rejeitado | 7 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimento oral, apresentado pelo PS, solicitando a dispensa de redação final e do prazo para apresentação de reclamações contra inexatidões relativamente à Proposta de Lei n.º 22/XIV/1.ª (GOV), à Proposta de Lei n.º 23/XIV/1.ª (GOV), à Apreciação Parlamentar n.º 9/XIV/1.ª (PCP); à Apreciação Parlamentar n.º 10/XIV/1.ª (BE); ao Projeto de Lei n.º 258/XIV/1.ª (PEV); ao Projeto de Lei n.º 265/XIV/1.ª (PEV); ao Projeto de Lei n.º 269/XIV/1.ª (PEV); ao Projeto de Lei n.º 282/XIV/1.ª (BE); ao Projeto de Lei n.º 284/XIV/1.ª (BE); ao Projeto de Lei n.º 297/XIV/1.ª (PCP); ao Projeto de Lei n.º 309/XIV/1.ª (PAN); ao Projeto de Lei n.º 326/XIV/1.ª (PAN) e ao Projeto de Lei n.º 328/XIV/1.ª (BE) | 86695 | Aprovado | 45 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Texto Final apresentado pela Comissão de Ambiente, Energia e Ordenamento do Território relativo aos Projetos de Resolução n.ºs 264/XIV/1.ª (BE); 287/XIV/1.ª (PAN); 320/XIV/1.ª (PCP); 410/XIV/1.ª (PSD) e 474/XIV/1.ª (PS). | 87729 | Aprovado | 68 | unanime |
A Favor | Contra | Contra | Abstenção | Contra | A Favor | nan | A Favor | Contra | Abstenção | A Favor | A Favor | Contra | Contra | I | nan | nan | 88540 | Rejeitado | 3 | nan |
A Favor | A Favor | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | I | nan | Artigo 2.º | 86657 | Aprovado | 45 | nan |
A Favor | A Favor | A Favor | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | I | nan | Votação da proposta apresentada pelo PAN, de aditamento do artigo 7.º-H | 86305 | Rejeitado | 42 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | Contra | Contra | A Favor | A Favor | A Favor | A Favor | I | nan | Proposta de alteração do PSD, de emenda do n.º 3 do artigo 4.º da proposta de lei | 86537 | Aprovado | 45 | nan |
nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | I | nan | Texto Final apresentado pela Comissão de Assuntos Constitucionais, Direitos, Liberdades e Garantias relativo aos Projetos de Regimento n.ºs 1/XIV/1.ª (IL), 2/XIV/1.ª (CH); 3/XIV/1.ª (PS); 4/XIV/1.ª (PSD); 5/XIV/1.ª (CDS-PP); 6/XIV/1.ª (PAN); 7/XIV/1.ª (IL) e 8/XIV/1.ª (PSD) | 88373 | Aprovado | 76 | nan |
A Favor | A Favor | A Favor | nan | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | nan | 87581 | Aprovado | 65 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | nan | Requerimentos, apresentados pelo BE e PAN, de avocação pelo Plenário da votação na especialidade das propostas de alteração relativas aos Projetos de Regimento n.ºs 1/XIV/1.ª (IL); 2/XIV/1.ª (CH); 3/XIV/1.ª (PS); 4/XIV/1.ª (PSD); 5/XIV/1.ª (CDS-PP); 6/XIV/1.ª (PAN); 7/XIV/1.ª (IL) e 8/XIV/1.ª (PSD) | 88380 | Aprovado | 76 | unanime |
A Favor | Contra | Contra | A Favor | Abstenção | Contra | nan | Contra | A Favor | A Favor | Contra | Contra | Contra | Contra | I | nan | nan | 88780 | Rejeitado | 9 | nan |
Contra | Contra | Contra | nan | Contra | Contra | nan | Contra | A Favor | A Favor | Contra | Contra | Contra | Contra | I | nan | Proposta de alteração do PAN, de aditamento de um novo n.º 1 ao artigo 4.º da proposta de lei | 86716 | Rejeitado | 45 | nan |
A Favor | Abstenção | nan | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | A Favor | I | nan | Ponto 1 | 85938 | Aprovado | 29 | nan |
A Favor | A Favor | A Favor | nan | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | A | nan | De pesar pelo assassinato de quatro cidadãos portugueses na Venezuela | 85338 | Aprovado | 7 | unanime |
A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | nan | A Favor | A Favor | A Favor | Abstenção | Abstenção | Abstenção | A Favor | I | nan | nan | 88286 | Aprovado | 76 | nan |
Contra | Abstenção | A Favor | A Favor | A Favor | Ausência | nan | Ausência | A Favor | A Favor | A Favor | A Favor | A Favor | A Favor | I | JOACINE KATAR MOREIRA (Ninsc) | nan | 87954 | Aprovado | 75 | nan |
Contra | Contra | Contra | Contra | Contra | Contra | nan | Contra | Contra | Contra | Contra | Contra | A Favor | A Favor | I | nan | • Avocação requerida pelo BE e PAN - Proposta de alteração do PS – Artigo 224.º do Regimento da Assembleia da República n.º 1/2007, de 20 de agosto, constante do artigo 2.º do texto final | 88278 | Aprovado | 76 | nan |
A Favor | Abstenção | Contra | nan | Contra | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | Restantes normas do artigo 4.º | 86680 | Rejeitado | 45 | nan |
Abstenção | A Favor | Ausência | A Favor | A Favor | Abstenção | nan | Abstenção | Contra | A Favor | Abstenção | Abstenção | A Favor | A Favor | I | CH | Proposta do PS, de emenda do artigo 8.º | 89099 | Aprovado | 17 | nan |
A Favor | Contra | Abstenção | nan | Abstenção | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Contra | Contra | I | nan | nan | 86603 | Rejeitado | 45 | nan |
A Favor | Abstenção | A Favor | nan | Contra | A Favor | nan | A Favor | A Favor | A Favor | A Favor | A Favor | Abstenção | Abstenção | I | nan | nan | 86026 | Aprovado | 34 | nan |
Abstenção | Abstenção | A Favor | nan | Contra | Abstenção | nan | Abstenção | A Favor | A Favor | Abstenção | A Favor | Abstenção | A Favor | I | nan | nan | 86041 | Aprovado | 34 | nan |
A matriz de distância é feita através da utilização da função pdist
; como já mencionado a distância euclidiana entre dois pontos $p$ e $q$ é $ d\left( p,q\right) = \sqrt {\sum _{i=1}^{n} \left( q_{i}-p_{i}\right)^2 }$, para $n$ dimensões . De forma a facilitar a intuição de como esta distância é aplicada ao registo de votações vamos, passo a passo, analisar um caso simplificado (o que nos serve também de forma de validação indirecta do processo).
Vamos criar um cenário com três partidos, com nomes que reflectem o seu perfil de votação:
Começemos por uma única votação, onde cada um dos partidos segue a sua linha programática:
v1=[[1],[0],[-1]]
v1_df = pd.DataFrame(v1, columns=["v1"], index=["F","A", "C"])
v1_df
v1 | |
---|---|
F | 1 |
A | 0 |
C | -1 |
Com uma única votação o número de dimensões é de $n = 1$ e é bastante intuitivo que a distância entre eles é a norma:$ d\left( x,y\right) = | x - y |$; entre 1 e 0 a distância é 1, entre 1 e -1 a distância é 2, etc. Para $ n = 1 $ a distância euclidiana é equivalente à norma, pois $ \sqrt{\left( q - p\right)^2} = | q - p | $, como pode ser observado quando medidas a distância entre eles:
import math
## We could also sue the array directly, e.g.
## print("Distance from" , a[0][0] , "and" , a[1][0],"=",math.sqrt((a[0][0]-a[1][0]) ** 2))
print("Distance from", v1_df.loc["F"].name, "and", v1_df.loc["A"].name, "=",
math.sqrt((v1_df.loc["F"]["v1"]-v1_df.loc["A"]["v1"]) ** 2))
print("Distance from", v1_df.loc["F"].name, "and", v1_df.loc["C"].name, "=",
math.sqrt((v1_df.loc["F"]["v1"]-v1_df.loc["C"]["v1"]) ** 2))
print("Distance from", v1_df.loc["A"].name, "and", v1_df.loc["C"].name, "=",
math.sqrt((v1_df.loc["A"]["v1"]-v1_df.loc["C"]["v1"]) ** 2))
Distance from F and A = 1.0 Distance from F and C = 2.0 Distance from A and C = 1.0
Temos a distância entre os três possíveis pares: a distância entre F e A é idêntica à distância entre A e F. Isto é importante porque ajuda a explicar a diferença entre a forma "condensada" e forma "quadrada". A função pdist
retorna a distância entre os vários pares na forma condensada:
pdist(v1_df)
array([1., 2., 1.])
Estes são os mesmos valores que obtivemos de forma manual, e é isso que a função faz: calcula uma determinada distância (euclidiana, neste caso e por omissão) entre todos os pares possíveis, sem que seja necessário especificarmos todas as combinações possíveis.
Este formato condensado não é o que permite uma leitura mais imediata, e para tal existe a função squareform
que apresenta os mesmos resultados mas numa matriz simétrica:
squareform(pdist(v1_df))
array([[0., 1., 2.], [1., 0., 1.], [2., 1., 0.]])
Em formato tabular torna-se ainda mais claro... e isto é exactmante a matriz de distância usada para o mapa térmico
v1_distmat=pd.DataFrame(squareform(pdist(v1)), columns=v1_df.index, index=v1_df.index)
v1_distmat
F | A | C | |
---|---|---|---|
F | 0.0 | 1.0 | 2.0 |
A | 1.0 | 0.0 | 1.0 |
C | 2.0 | 1.0 | 0.0 |
Geometricamente temos pontos numa recta: uma análise das distâncias entre os três partidos com base no histórico de votação seria simples de visualizar com base na posição desses pontos:
fig, ax = plt.subplots()
ax.axhline(y=0,c="gold",zorder=-1)
for x in v1_df["v1"]:
ax.scatter(x,y=0)
plt.show()
Com base na matriz de distância contruimos o mapa térmico de forma muito simples:
plt.figure(figsize=(4,4))
sns.heatmap(
v1_distmat,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
annot = True,
square =True,
)
plt.show()
...e o agrupamento com base nessa matriz:
v1_distmat_link = hc.linkage(pdist(v1_df))
sns.clustermap(
v1_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v1_distmat_link,
col_linkage=v1_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7faa07a027b8>
Usando a mesma abordagem (agora sem necessidade de explicações adicionais) adicionamos mais uma votação, sempre em linha com o perfil fixo de votos de cada um: cada partido passa a ter dois votos, logo temos duas dimensões:
v2=[[1,1],[0,0],[-1,-1]]
v2_df = pd.DataFrame(v2, columns=["v1","v2"], index=["F","A", "C"])
v2_df
v1 | v2 | |
---|---|---|
F | 1 | 1 |
A | 0 | 0 |
C | -1 | -1 |
A distância euclidiana é agora feita de forma mais genérica: a raíz quadrada da soma do quadrado das diferenças: para a primeira diferença isto significa, passo a passo e para $q=F$ e $p=A$:
$ d\left(q,p\right) = \sqrt {\sum _{i=1}^{n} \left( q_{i}-p_{i}\right)^2 } = \sqrt{\left( q_{1}-p_{1}\right)^2 + \left( q_{2}-p_{2}\right)^2 } = \sqrt{\left( 1 - 0\right)^2 + \left( 1 - 0\right)^2} = \sqrt{ 1^2 + 1^2 } = \sqrt{1+1} = \sqrt{2} \approx 1.4142135623730951 $
E de facto:
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v2_df.loc["F"],v2_df.loc["A"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v2_df.loc["F"],v2_df.loc["C"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v2_df.loc["A"],v2_df.loc["C"]))))
1.4142135623730951 2.8284271247461903 1.4142135623730951
Tal como antes é idêntico ao resultado de pdist
, tanto na sua forma condensada como quadrada:
print("pdist:\n", pdist(v2),"\n")
print("squareform:\n",squareform(pdist(v2)))
v2_distmat=pd.DataFrame(squareform(pdist(v2)), columns=v2_df.index, index=v2_df.index)
v2_distmat
pdist: [1.41421356 2.82842712 1.41421356] squareform: [[0. 1.41421356 2.82842712] [1.41421356 0. 1.41421356] [2.82842712 1.41421356 0. ]]
F | A | C | |
---|---|---|---|
F | 0.000000 | 1.414214 | 2.828427 |
A | 1.414214 | 0.000000 | 1.414214 |
C | 2.828427 | 1.414214 | 0.000000 |
Com duas votações temos $ n = 2 $ e conseguimos ver os pontos num espaço cartesiano em $ \mathbb{R}^2 $ (um plano).
fig, ax = plt.subplots()
for x,y in zip(v2_df["v1"], v2_df["v2"]):
ax.scatter(x,y)
fig.show()
A matriz de distância seria neste caso, como no anterior, desnecessária (em $ \mathbb{R}^2 $ as distâncias entre os partidos são óbvias por facilmente visualizáveis); a forma de a construir é idêntica:
plt.figure(figsize=(4,4))
sns.heatmap(
v2_distmat,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
annot = True,
square =True,
)
plt.show()
... e o mapa térmico correspondente:
v2_distmat_link = hc.linkage(pdist(v2_df))
sns.clustermap(
v2_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v2_distmat_link,
col_linkage=v2_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7faa074e52b0>
Este é o último caso onde a visualização pode ser feita de forma directa. Consideremos:
v3=[[1,1,1],[0,0,0],[-1,-1,-1]]
v3_df = pd.DataFrame(v3, columns=["v1","v2", "v3"], index=["F","A", "C"])
v3_df
v1 | v2 | v3 | |
---|---|---|---|
F | 1 | 1 | 1 |
A | 0 | 0 | 0 |
C | -1 | -1 | -1 |
A distância é calculada da mesma forma:
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v3_df.loc["F"],v3_df.loc["A"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v3_df.loc["F"],v3_df.loc["C"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v3_df.loc["A"],v3_df.loc["C"]))))
1.7320508075688772 3.4641016151377544 1.7320508075688772
... bem como a matriz de distância:
print("pdist:\n", pdist(v3),"\n")
print("squareform:\n",squareform(pdist(v3)))
v3_distmat=pd.DataFrame(squareform(pdist(v3)), columns=v3_df.index, index=v3_df.index)
v3_distmat
pdist: [1.73205081 3.46410162 1.73205081] squareform: [[0. 1.73205081 3.46410162] [1.73205081 0. 1.73205081] [3.46410162 1.73205081 0. ]]
F | A | C | |
---|---|---|---|
F | 0.000000 | 1.732051 | 3.464102 |
A | 1.732051 | 0.000000 | 1.732051 |
C | 3.464102 | 1.732051 | 0.000000 |
Estamos agora em $ \mathbb{R}^3 $, e para visualizar podemos usar uma projecção tridimensional:
fig, ax = plt.subplots()
ax = fig.add_subplot(111,projection='3d')
for x,y,z in zip(v3_df["v1"], v3_df["v2"], v3_df["v3"]):
ax.scatter(x,y,z)
fig.show()
plt.figure(figsize=(4,4))
sns.heatmap(
v3_distmat,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
annot = True,
square =True,
)
plt.show()
v3_distmat_link = hc.linkage(pdist(v3_df))
sns.clustermap(
v3_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v3_distmat_link,
col_linkage=v3_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7fa9e16cbbe0>
A partir daqui coloca-se a questão fundamental: a impossibilidade de visualizar de forma directa a distância para além das três dimensões. A distância existe e segue exactamente os mesmo passos, simplesmente não é passível de visualização, razão pela qual é necessário (agora sim) depender de formas que "reduzam" as dimensões e as tornem visualizáveis.
Até agora os agrupamentos têm sido sempre iguais pois a distância é sempre linear; vamos neste último caso assumir que o Partido Abstencionista teve uma mudança de posição e passou a votar por vezes a favor e contra, embora mais contra que a favor:
v4=[[1,1,1,1,1,1,1,1,1,1],[0,0,0,1,0,-1,0,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]]
v4_df = pd.DataFrame(v4, columns=["v1","v2", "v3","v4","v5","v6","v7","v8","v9","v10"], index=["F","A", "C"])
v4_df
v1 | v2 | v3 | v4 | v5 | v6 | v7 | v8 | v9 | v10 | |
---|---|---|---|---|---|---|---|---|---|---|
F | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
A | 0 | 0 | 0 | 1 | 0 | -1 | 0 | -1 | -1 | -1 |
C | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
A distância calculdada "manualmente":
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v4_df.loc["F"],v4_df.loc["A"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v4_df.loc["F"],v4_df.loc["C"]))))
print(math.sqrt(sum((px - qx) ** 2.0 for px, qx in zip(v4_df.loc["A"],v4_df.loc["C"]))))
4.58257569495584 6.324555320336759 3.0
... e o cálculo via pdist
e a matriz de distância:
print("pdist:\n", pdist(v4),"\n")
print("squareform:\n",squareform(pdist(v4)))
v4_distmat=pd.DataFrame(squareform(pdist(v4)), columns=v4_df.index, index=v4_df.index)
v4_distmat
pdist: [4.58257569 6.32455532 3. ] squareform: [[0. 4.58257569 6.32455532] [4.58257569 0. 3. ] [6.32455532 3. 0. ]]
F | A | C | |
---|---|---|---|
F | 0.000000 | 4.582576 | 6.324555 |
A | 4.582576 | 0.000000 | 3.000000 |
C | 6.324555 | 3.000000 | 0.000000 |
O mapa térmico:
plt.figure(figsize=(4,4))
sns.heatmap(
v4_distmat,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
annot = True,
square =True,
)
plt.show()
... e o dendograma, já apresentando um agrupamento com base na maior aproximação com base no registo de votação:
v4_distmat_link = hc.linkage(pdist(v4_df), method="ward")
sns.clustermap(
v4_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v4_distmat_link,
col_linkage=v4_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7fa8df99e828>
Da mesma forma que podemos especificar diferentes métricas de distância, podemos também especificar diferentes métodos de agregação, que com base na distância entre os vários pontos vão determinar de que forma podem ser agrupados: de que forma se determina a ligação entre eles.
O valor por omissão usado pela função linkage
o da ligação simple
, determinada pela distância mínima entre os pontos mais próximos $ \min \,\{\,d(a,b):a\in A,\,b\in B\,\} $; uma alternativa bastante comum e que determina os cluster com base na distância média é o average
(também conhecido por UPGMA), e outra é o método de Ward [14WardMethod].
Podemos ver a diferença entre os três com o seguinte exemplo; adicionamos um partido ("B") normalmente se abstem mas vota duas vezes de forma diferente, ambas em sentido oposto ao partido "A".
v5=[[1,1,1,1,1,1,1,1,1,1],[0,0,0,1,0,-1,0,-1,-1,-1],[0,0,0,-1,0,0,0,0,0,1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]]
v5_df = pd.DataFrame(v5, columns=["v1","v2", "v3","v4","v5","v6","v7","v8","v9","v10"], index=["F","A","B" ,"C"])
v5_distmat=pd.DataFrame(squareform(pdist(v5)), columns=v5_df.index, index=v5_df.index)
v5_distmat
F | A | B | C | |
---|---|---|---|---|
F | 0.000000 | 4.582576 | 3.464102 | 6.324555 |
A | 4.582576 | 0.000000 | 3.316625 | 3.000000 |
B | 3.464102 | 3.316625 | 0.000000 | 3.464102 |
C | 6.324555 | 3.000000 | 3.464102 | 0.000000 |
Usando o método simples obtemos o seguinte dendograma:
v5_distmat_link = hc.linkage(pdist(v5_df))
sns.clustermap(
v5_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v5_distmat_link,
col_linkage=v5_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7fa8df7d0128>
Usando UPGMA não há alterações (neste caso muito simples, porque o mais provável é existirem quando aplicadas a dados reais com maior número de observações e dimensões); os agrupamentos tendem a ser feitos de forma recursiva, por separação de elementos individuais até à identificação de um par final:
v5_distmat_link = hc.linkage(pdist(v5_df), method="average")
sns.clustermap(
v5_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v5_distmat_link,
col_linkage=v5_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7fa8df6cdac8>
Com o método de Ward o resultado não é muito diferente mas tende a agrupar em clusters de número e dimensões iguais e é por isso uma escolha muito popular (Morse 1980); neste caso cria dois grupos de dois elementos, agrupando o novo partido pela consideração que faz das destâncias entre todos:
v5_distmat_link = hc.linkage(pdist(v5_df), method="ward")
sns.clustermap(
v5_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
row_linkage=v5_distmat_link,
col_linkage=v5_distmat_link,
figsize=(4,4)
)
<seaborn.matrix.ClusterGrid at 0x7fa8df5d0ba8>
Consideremos uma votação como a seguinte, onde os 4 partidos têm um perfil de votação simples que torna simples adivinhar os agrupamentos: entre o que vota sempre a favor e o que vota sempre contra temos partidos que metade das vezes se abstêm e a outra metade votam de forma diferente; a matriz de distância correspondente:
v6=[[1,1,1,1,1,1,1,1,1,1],[1,0,1,0,1,0,1,0,1,0],[0,-1,0,-1,0,-1,0,-1,0,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]]
v6_df = pd.DataFrame(v6, columns=["v1","v2", "v3","v4","v5","v6","v7","v8","v9","v10"], index=["F","A","B" ,"C"])
v6_distmat=pd.DataFrame(squareform(pdist(v6)), columns=v6_df.index, index=v6_df.index)
v6_distmat
F | A | B | C | |
---|---|---|---|---|
F | 0.000000 | 2.236068 | 5.000000 | 6.324555 |
A | 2.236068 | 0.000000 | 3.162278 | 5.000000 |
B | 5.000000 | 3.162278 | 0.000000 | 2.236068 |
C | 6.324555 | 5.000000 | 2.236068 | 0.000000 |
Com estas distâncias conseguimos, através de MDS, reduzir o número de dimensões de forma a podermos visualizar o resultado:
from sklearn.manifold import MDS
import random
mds = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)
results = mds.fit(v6_distmat.values)
coords = results.embedding_
fig, ax = plt.subplots(figsize=(8,8))
for label, x, y in zip(v6_distmat.columns, coords[:, 0], coords[:, 1]):
ax.scatter(x, y, s=250)
ax.axis('equal')
ax.annotate(
label,
xy = (x+0.1, y),
)
plt.show()
Observe-se que não temos agrupamentos: MDS não é um método de clustering mas sim de redução das dimensões de forma a manter as distâncias relativas entre os vários pontos, o que nos permite identificar visualmente possíveis grupos (neste caso parece claro que $\{C,B\}$ e $\{A,F\}$ são grupos que se destacam pela proximidade edos seus elementos e distância entre si).
Este método é, como referido, uma das formas de se poder proceder à identificação automática de clusters, tendo como base uma matriz de afinidade
v6_distmat_mm=((v6_distmat-v6_distmat.min().min())/(v6_distmat.max().max()-v6_distmat.min().min()))*1
pd.DataFrame(1-v6_distmat_mm, v6_distmat.index, v6_distmat.columns)
F | A | B | C | |
---|---|---|---|---|
F | 1.000000 | 0.646447 | 0.209431 | 0.000000 |
A | 0.646447 | 1.000000 | 0.500000 | 0.209431 |
B | 0.209431 | 0.500000 | 1.000000 | 0.646447 |
C | 0.000000 | 0.209431 | 0.646447 | 1.000000 |
Um parâmetros fundamental é o número de clusters a identificar; neste caso indicamos dois:
v6_sc = SpectralClustering(2, affinity="precomputed",random_state=2020).fit_predict(1 - v6_distmat_mm)
v6_sc_dict = dict(zip(v6_distmat,v6_sc))
print(v6_sc_dict)
{'F': 1, 'A': 1, 'B': 0, 'C': 0}
O MDS correspondente:
fig, ax = plt.subplots(figsize=(6,6))
for label, x, y in zip(v6_distmat.columns, coords[:, 0], coords[:, 1]):
ax.scatter(x, y, c = "C"+str(v6_sc_dict[label]), s=250)
ax.axis('equal')
ax.annotate(
label,xy = (x+0.3, y),)
Este método dispensa a inicialização com o número de grupos :
from sklearn.cluster import DBSCAN
dbscan_labels = DBSCAN(min_samples=2,eps=0.7).fit((1 - v6_distmat_mm))
dbscan_labels.labels_
v6_dbscan_dict = dict(zip(v6_distmat_mm,dbscan_labels.labels_))
v6_dbscan_dict
{'F': 0, 'A': 0, 'B': 1, 'C': 1}
Neste caso específico o resultado é idêntico:
fig, ax = plt.subplots(figsize=(6,6))
for label, x, y in zip(v6_distmat.columns, coords[:, 0], coords[:, 1]):
ax.scatter(x, y, c = "C"+str(v6_dbscan_dict[label]), s=250)
ax.axis('equal')
ax.annotate(
label,xy = (x+0.3, y),)
Como referido optámos por não incluir a deputação não-inscrita Cristina Rodrigues na análise principal, essencialmente pela mais reduzida quantidade de votações. Fazemos aqui, contudo, duas análises de detalhes de forma a permitir uma leitura específica desta situação.
Neste caso assumimos que os votos da deputada são os mesmos do PAN antes da sua saída (condensamos todos os passos necessário para a construção da matriz de distância e dendograma num único bloco, seguindo-se as restantes análises sem explicações adicionais por redundantes):
cr_votes_hm = votes[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN','PAN/CR','PSD','IL','CDS-PP', 'CH']]
cr_votes_hmn = cr_votes_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0]).fillna(0)
## Transpose the dataframe used for the heatmap
cr_votes_t = cr_votes_hmn.transpose()
## Determine the Eucledian pairwise distance
## ("euclidean" is actually the default option)
cr_pwdist = pdist(cr_votes_t, metric='euclidean')
## Create a square dataframe with the pairwise distances:
## the distance matrix
cr_distmat = pd.DataFrame(
squareform(cr_pwdist), # pass a symmetric distance matrix
columns = cr_votes_t.index,
index = cr_votes_t.index
)
## Perform hierarchical linkage on the distance matrix using Ward's method.
cr_distmat_link = hc.linkage(cr_pwdist, method="ward", optimal_ordering=True )
#plt.figure(figsize=(8,8))
sns.clustermap(
cr_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
#standard_scale=1,
row_linkage=cr_distmat_link,
col_linkage=cr_distmat_link,
figsize=(8,8)
)
plt.show()
Aplicando Spectrum Clustering aos dados obtemos a seguinte classificação de clusters:
cr_distmat_mm=((cr_distmat-cr_distmat.min().min())/(cr_distmat.max().max()-cr_distmat.min().min()))*1
cr_affinmat_mm = pd.DataFrame(1-cr_distmat_mm, cr_distmat.index, cr_distmat.columns)
cr_sc = SpectralClustering(5, affinity="precomputed",random_state=2020).fit_predict(cr_affinmat_mm)
cr_sc_dict = dict(zip(cr_distmat,cr_sc))
cr_sc_dict
{'BE': 4, 'PCP': 3, 'PEV': 3, 'L/JKM': 4, 'PS': 1, 'PAN': 0, 'PAN/CR': 0, 'PSD': 1, 'IL': 2, 'CDS-PP': 2, 'CH': 2}
O gráfico de MDS com a inclusão da deputada Cristina Rodrigues é o seguinte:
cr_mds = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)
cr_results = mds.fit(cr_distmat_mm.values)
cr_coords = cr_results.embedding_
sns.set()
sns.set_style("ticks")
fig, ax = plt.subplots(figsize=(6,6))
#fig.suptitle('Portuguese Parliament Voting Records Analysis, 14th Legislature\nMDS with Spectrum Clustering clusters, inc. Cristina Rodrigues (2D)', fontsize=14)
#plt.title('MDS with Spectrum Clustering clusters, inc. Cristina Rodrigues (2D)')
for label, x, y in zip(cr_distmat_mm.columns, cr_coords[:, 0], cr_coords[:, 1]):
ax.scatter(x, y, c = "C"+str(cr_sc_dict[label]), s=100)
ax.axis('equal')
if label=="PAN/CR":
ax.annotate(label,xy = (x-0.10, y+0.020))
else:
ax.annotate(label,xy = (x-0.02, y+0.020))
plt.show()
Como podemos observar as diferenças são bastante pequenas, o que tendo em conta o (relativamente) pequeno número de votações feitas de forma individual não é de estranhar, em especial quando existem também muitos casos onde são idênticos, e onde são diferentes normalmente a diferença é a mínima (i.e. são raras situações de votos A Favor/Contra, que têm maior distância):
cr_pan_comp = votes[[ 'PAN','PAN/CR','Cristina Rodrigues (Ninsc)']]
cr_pan_comp = cr_pan_comp.dropna().replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0])
voting_palette = ["#FB6962","#FCFC99","#79DE79", "black"]
cr_pan_diff = cr_pan_comp[["PAN","Cristina Rodrigues (Ninsc)"]].dropna()[cr_pan_comp["PAN"] != cr_pan_comp["Cristina Rodrigues (Ninsc)"]]
cr_pan_same= cr_pan_comp[["PAN","Cristina Rodrigues (Ninsc)"]].dropna()[cr_pan_comp["PAN"] == cr_pan_comp["Cristina Rodrigues (Ninsc)"]]
print("Voted the same:", cr_pan_same.shape[0])
print("Voted differently:",cr_pan_diff.shape[0])
fig, ax = plt.subplots(figsize=(6,6))
sns.set(color_codes=True)
## Build a heatmap of the vote when they diverged
#fig.suptitle('Portuguese Parliament Voting Records Analysis, 14th Legislature', fontsize=14)
#ax.set_title('PAN and Cristina Rodrigues voting record (only where they diverge)')
sns.heatmap(cr_pan_diff.astype(float) , cbar=False,yticklabels = True, cmap=sns.color_palette(palette=["#FB6962","#FCFC99","#79DE79"]), linewidth=1)
plt.show()
Voted the same: 647 Voted differently: 108
A combinação de votos em Actividades e Iniciativas resulta na matriz de distância que será usada para as restantes análises; recuperando o clustermap para referência:
## Display the heatmap of the distance matrix
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot()
## 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()
<Figure size 576x576 with 0 Axes>
e também o MDS correspondente:
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
## 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()
Uma questão que pode ser colocada é se existem diferenças entre as votações em Iniciativas e Actividades: as Actividades são em menor número (pelo que o seu peso será sempre menor quando somado às iniciativas) mas inclui votos sobre temáticas que podem ter uma dimensão diferente: votos de pesar, votos de condenação e outro tipo de actividades que têm desde logo uma possível dimensão internacional mais vincada e são formas de demonstração de posições ideológicas sobre acontecimentos nacionais e internacionais.
Refira-se que todas as votações são votações políticas: o votar contra ou a favor de um voto de pesar ou condenação demonstra uma avaliação política sobre a pessoa, o acontecimento e/ou o próprio texto em votação, texto esse que inclui ele próprio considerações políticas e ideológicas. Existem muitas votações por unanimidade nos casos de votos de pesar e congratulação, mas existem também muitos outros com votações divididas, não sendo incomum votações sobre o mesmo tema com votações opostas, dada a forma como um mesmo acontecimento é descrito e apresentado.
O seguinte bloco apresentea o clustermap para cada uma das fontes de votação:
votes_ini_hm = ini_df[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN','PSD','IL','CDS-PP', 'CH']]
votes_ini_hmn = votes_ini_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0]).fillna(0)
votes_act_hm = act_df[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN','PSD','IL','CDS-PP', 'CH']]
votes_act_hmn = votes_act_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0]).fillna(0)
## Transpose the dataframe used for the heatmap
votes_ini_t = votes_ini_hmn.transpose()
votes_act_t = votes_act_hmn.transpose()
## Determine the Eucledian pairwise distance
## ("euclidean" is actually the default option)
pwdist_ini = pdist(votes_ini_t, metric='euclidean')
pwdist_act = pdist(votes_act_t, metric='euclidean')
## Create the distance matrix
distmat_ini = pd.DataFrame(
squareform(pwdist_ini), # pass a symmetric distance matrix
columns = votes_ini_t.index,
index = votes_ini_t.index
)
distmat_act = pd.DataFrame(
squareform(pwdist_act), # pass a symmetric distance matrix
columns = votes_act_t.index,
index = votes_act_t.index
)
## Display the heatmap of the distance matrix
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot()
## Perform hierarchical linkage on the distance matrix using Ward's method.
distmat_ini_link = hc.linkage(pwdist_ini, method="ward", optimal_ordering=True )
distmat_act_link = hc.linkage(pwdist_act, method="ward", optimal_ordering=True )
sns.clustermap(
distmat_ini,
annot = True,
cmap=sns.color_palette("Purples_r"),
linewidth=1,
#standard_scale=1,
row_linkage=distmat_ini_link,
col_linkage=distmat_ini_link,
figsize=(8,8)).fig.suptitle('Portuguese Parliament 14th Legislature, Clustermap (Initiatives Only)')
sns.clustermap(
distmat_act,
annot = True,
cmap=sns.color_palette("Blues_r"),
linewidth=1,
#standard_scale=1,
row_linkage=distmat_act_link,
col_linkage=distmat_act_link,
figsize=(8,8)).fig.suptitle('Portuguese Parliament 14th Legislature, Clustermap (Activities Only)')
plt.show()
<Figure size 576x576 with 0 Axes>
Existem diferenças, em particular na análise que apenas inclui as Atividades: em particular o PS aparence agora agrupado "à esquerda" (separando-se antes do PAN, sendo o restante semelhante), e isto implica a rearrumação "à direita", com a criação de dois grupos (CDS-PP e CH, PSD e IL).
Estas diferenças podem também ser vistas no MDS de cada tipo de votação:
from sklearn.manifold import MDS
import numpy as np
mds_act = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)
mds_ini = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)
distmat_act_mm=((distmat_act-distmat_act.min().min())/(distmat_act.max().max()-distmat_act.min().min()))*1
distmat_ini_mm=((distmat_ini-distmat_ini.min().min())/(distmat_ini.max().max()-distmat_ini.min().min()))*1
## We use the normalised distance matrix but results would
## be similar with the original one, just with a different scale/axis
results_act = mds_act.fit(distmat_act_mm.values)
coords_act = results_act.embedding_
results_ini = mds_ini.fit(distmat_ini_mm.values)
coords_ini= results_ini.embedding_
## Graphic options
sns.set()
sns.set_style("ticks")
fig, ax = plt.subplots(figsize=(8,8))
plt.title('Portuguese Parliament Voting Records Analysis, 14th Legislature (Initiatives)', fontsize=14)
for label, x, y in zip(distmat_ini_mm.columns, coords_ini[:, 0], coords_ini[:, 1]):
ax.scatter(x, y, s=250)
ax.axis('equal')
ax.annotate(label,xy = (x-0.02, y+0.025))
plt.show()
fig, ax = plt.subplots(figsize=(8,8))
plt.title('Portuguese Parliament Voting Records Analysis, 14th Legislature (Activities)', fontsize=14)
for label, x, y in zip(distmat_act_mm.columns, coords_act[:, 0], coords_act[:, 1]):
ax.scatter(x, y, s=250)
ax.axis('equal')
ax.annotate(label,xy = (x-0.02, y+0.025))
plt.show()
Se o "mapa" das Iniciativas é muito semelhante ao global, já o das Actividades apresenta diferenças (como seria de esperar tendo em conta o mapa de distâncias e dendograma), e quando comparado com o global:
Se reduzirmos a análise apenas às votações onde a deputada Cristina Rodrigues participou como deputda não inscrita temos o seguinte resultado, que não altera de forma substancial o que já foi apresentado:
cr_only_votes_hm = votes[['BE', 'PCP', 'PEV', 'L/JKM', 'PS', 'PAN',"Cristina Rodrigues (Ninsc)",'PSD','IL','CDS-PP', 'CH']]
cr_only_votes_hmn = cr_only_votes_hm.replace(["A Favor", "Contra", "Abstenção", "Ausência"], [1,-1,0,0]).dropna()
## Transpose the dataframe used for the heatmap
cr_only_votes_t = cr_only_votes_hmn.transpose()
## Determine the Eucledian pairwise distance
## ("euclidean" is actually the default option)
cr_only_pwdist = pdist(cr_only_votes_t, metric='euclidean')
## Create a square dataframe with the pairwise distances:
## the distance matrix
cr_only_distmat = pd.DataFrame(
squareform(cr_only_pwdist), # pass a symmetric distance matrix
columns = cr_only_votes_t.index,
index = cr_only_votes_t.index
)
cr_only_distmat_link = hc.linkage(cr_only_pwdist, method="ward", optimal_ordering=True )
## Normalise
cr_only_distmat_mm=((cr_only_distmat-cr_only_distmat.min().min())/(cr_only_distmat.max().max()-cr_only_distmat.min().min()))*1
## SC
cr_only_affinmat_mm = pd.DataFrame(1-cr_only_distmat_mm, cr_only_distmat.index, cr_only_distmat.columns)
cr_only_sc = SpectralClustering(5, affinity="precomputed",random_state=2020).fit_predict(cr_only_affinmat_mm)
cr_only_sc_dict = dict(zip(cr_only_distmat,cr_only_sc))
cr_only_sc_dict
## MDS
cr_only_mds = MDS(n_components=2, dissimilarity='precomputed',random_state=2020, n_init=100, max_iter=1000)
cr_only_results = mds.fit(cr_only_distmat_mm.values)
cr_only_coords = cr_only_results.embedding_
## Plot it
sns.set()
sns.set_style("ticks")
fig, ax = plt.subplots(figsize=(6,6))
fig.suptitle('Portuguese Parliament Voting Records Analysis, 14th Legislature', fontsize=14)
ax.set_title('MDS with Spectrum Clustering clusters, inc. Cristina Rodrigues (2D)')
for label, x, y in zip(cr_only_distmat_mm.columns, cr_only_coords[:, 0], cr_only_coords[:, 1]):
ax.scatter(x, y, c = "C"+str(cr_only_sc_dict[label]), s=100)
#ax.scatter(x, y, s=100)
ax.axis('equal')
if label=="PAN/CR":
ax.annotate(label,xy = (x-0.10, y+0.020))
else:
ax.annotate(label,xy = (x-0.02, y+0.020))
plt.show()
... e a respectiva matriz de distância:
sns.clustermap(
cr_only_distmat,
annot = True,
cmap=sns.color_palette("Reds_r"),
linewidth=1,
#standard_scale=1,
row_linkage=cr_only_distmat_link,
col_linkage=cr_only_distmat_link,
figsize=(8,8)
)
plt.show()
*2020, Frederico Muñoz*