O Campeonato Brasileiro está ficando mais injusto?

Uma análise exploratória usando o Coeficiente de Gini

Todos os dados e a análise completa pode ser encontrada no repositório brasileirao-gini. Lá tem todas as instruções para executar passo a passo a análise.

Introdução

Uma das coisas boas da era digital é que com o uso do WhatsApp podemos ter as nossas mesas redondas virtuais com os nossos amigos não importa o quanto estamos longe estamos deles. E dentro de um dessas resenhas virtuais estavamos discutindo o ótimo trabalho do treinador do Flamengo na atual temporada (i.e. aspectos relativos de como ele mudou positivamente o time).

Porém, apareceu um tópico relevante durante o debate que foi a hipótese que na medida em que entra mais dinheiro no campeonato e nos times maiores o Brasileirão vai ficando cada vez mais injusto com um conjunto de times ganhando muitos jogos e os times pequenos ficarem relegados a meros coadjuvantes dentro da liga.

Em outras palavras, isso significa que o campeonato sempre é disputado pelos mesmos clubes com poderio econômico e por causa dessa disparidade a competitividade poderia estar diminuindo ao longo do tempo.

Isso levantou a questão dentro do nosso grupo que foi: O brasileirão está ficando mais injusto ao longo do tempo?

E é essa pergunta que eu vou tentar responder no final desse post.

Como checar se há uma desigualdade estrutural no Campeonato Brasileiro?

E para responder inicialmente essa pregunta eu vou usar o Coeficiente de Gini que é uma métrica para medir a dispersão estatística que inicialmente foi criada para medir a distribuição de renda e riqueza entre países e é amplamente usada em economia como um importante indicador de monitoramento dessas questões.

O Coeficiente de Gini é usado como medida relativa para benchmark e monitoramento de desigualdade e pobreza através de diversos países e serve como base de analise e desenvolvimento de politicas publicas como você pode conferir nos trabalhos de SobottkaMoreira e autores e pelo Instituto de Pesquisa Econômica Aplicada o IPEA no trabalho de Barros e autores.

Como este post não é para falar em profundidade sobre este indicador eu sugiro a leitura do trabalho original de Conrrado Gini chamado Variabilità e Mutabilità ou este trabalho sobre como esse indicador foi usado de parâmetro para alguns algumas analises de bem estar social no Brasil.

Isto é, eu vou medir de forma simples a variância da distribuição dos pontos dentro de cada uma das edições do Campeonato Brasileiro (que eu vou chamar daqui em diante de Brasileirão) para verificar se há uma desigualdade latente estrutural dentro dos campeonatos e ao longo dos anos.

Algumas considerações prévias sobre algumas limitações sobre a forma de medir essa desigualdade

De acordo com o que coloquei anteriormente podemos fazer de maneira bem simples a mensuração de fatores importantes como desigualdade econômica através da renda e/ou riqueza usando o Coeficiente de Gini, e aqui eu vou usar uma alegoria simples de como eu vou aplicar isso nos dados do Brasileirão.

Se esse índice serve de alguma maneira medir a desigualdade entre países considerando a sua riqueza ou mesmo a sua renda, trazendo para o mundo do futebol podemos usar como representação dos dados os pontos ganhos de um time dentro de uma temporada como se fosse a renda e aplicar o Coeficiente de Gini regular não somente dentro de um ano especifico da liga mas também monitorar essa desigualdade ao longo do tempo.

Para isso eu vou usar a base de dados de todos os resultados do Brasileirão desde 2003 até 2018 extraídos da Wikipédia. E aqui eu tenho que fazer duas considerações que são:

São algumas limitações importantes que devem ser consideradas, dado que o número de pontos em disputa foi modificado e um ajuste é necessário.

Cabe ressaltar que o Coeficiente de Gini e da análise em si possuem inúmeras limitações que devem ser entendidas como:

  • Não levar o “Fator Tradição” em consideração a um efeito de produtividade dos clubes ao longo do tempo, i.e., um clube grande e antigo carrega uma estrutura financeira/econômica/institucional maior do que os clubes mais novos, e isso pode ser visto na distribuição dos campeões na era dos pontos corridos (Em economia isso seria similar ao efeito de transição de produtividade de uma base instalada produtiva ao longo dos anos);
  • Diferenças econômicas das regiões em que os clubes têm as suas bases;
  • O Coeficiente de Gini olha somente o resultado final sem levar em considerações fatores conjunturais que podem influenciar estes mesmos resultados como gestão, e momento econômico do time, e outros eventos como Olimpíada e Copa do Mundo;
  • O conceito da natureza da geração dos pontos (o que em economia seria a renda) podem ter dinâmicas muito diferentes dado que o índice não captura se os pontos foram gerados através de 3 empates (1 ponto multiplicado por 3) ou por uma vitória e duas derrotas (3 pontos);
  • Analisar a geração de pontos em si ao longo do tempo pode ser bem complicado e não representaria uma comparação plausível. Por exemplo: O Flamengo campeão brasileiro de 2009 seria na melhor das possibilidades um mero quinto colocado em 2014;
  • O índice em si por tratar somente do resultado final, não mostra a transitividade desses pontos ao longo do campeonato. Explico: Se um time nas rodadas finais do campeonato não tem mais nenhum tipo de chance de ir para uma boa competição(Copa Libertadores), de ser rebaixado para divisões inferiores, ou mesmo já conquistou o titulo pode acontecer desses times entrarem com menos disposição para ganhar os jogos. Incentivos financeiros entre os times também podem ocorrerEste artigo da Investopedia fala um pouco sobre esse efeito;

Algumas outras limitações do Coeficiente de Gini podem ser encontradas no trabalho de Tsai, no Working Paper de Osberg, no site do HSRC ou para quem quiser uma critica mais vocal tem esse ensaio do Craig Wright.

Para calcular o Coeficiente de Gini eu usei o código da Olivia Guest apenas para fins de simplicidade, mas qualquer software pode ser usado uma vez que os dados estão disponíveis no repositório.

Como o código têm muito boilerplate code de coisas que eu já fiz no passado e o meu foco é a analise em si, eu não vou comentar o código inteiro.

Vamos dar uma olhada inicial apenas para ver se os dados foram carregados corretamente.

# Imports, 538 theme in the charts and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%pylab inline
plt.style.use('fivethirtyeight')
df_brasileirao = pd.read_csv('dataset-2003-2018.csv', delimiter=';')
print(f'Number of Records: {df_brasileirao.shape[0]} – Number of Columns: {df_brasileirao.shape[1]}')
# Loading Check
df_brasileirao.head(5)
view raw imports.py hosted with ❤ by GitHub

Aparentemente tudo OK com os dados, então eu vou realizar o calculo do Coeficiente de Gini usando todas as edições do Brasileirão.

Ranking de desigualdade entre todas as edições do Brasileirão usando o Coeficiente de Gini

Para quem quiser calcular, basta usar essas duas funções:

# Gini function as PyGini package
def gini(arr, eps=1e-8):
'''
Reference: PyGini (I owe you a beer @o_guest)
https://github.com/mckib2/pygini/blob/master/pygini/gini.py
Calculate the Gini coefficient of a numpy array.
Notes
—–
Based on bottom eq on [2]_.
References
———-
.. [2]_ http://www.statsdirect.com/help/
default.htm#nonparametric_methods/gini.htm
'''
# All values are treated equally, arrays must be 1d and > 0:
arr = np.abs(arr).flatten() + eps
# Values must be sorted:
arr = np.sort(arr)
# Index per array element:
index = np.arange(1, arr.shape[0]+1)
# Number of array elements:
N = arr.shape[0]
# Gini coefficient:
return(np.sum((2*index N 1)*arr))/(N*np.sum(arr))
def get_gini_df(df):
"""Generate DF with Gini Index
Parameters
———-
df : Pandas Dataframe
Dataframe with Brasileirão data
Returns
——-
gini_df : Pandas Dataframe
Returns a Pandas Dataframe with the year, team and gini index
"""
gini_per_year = []
for year in df['year'].unique():
championship_index = gini(np.array(df[df['year'] == year]['points']))
champion = (df[(df['year'] == year) & (df['position'] == 1)]['team'])
gini_per_year.append((year, champion.values[0], round(championship_index, 4)))
gini_df = pd.DataFrame(gini_per_year)
gini_df.columns = ['year', 'team', 'gini']
# Indexing the date field for graph it smoothly
gini_df.set_index('year', inplace=True)
return gini_df
gini_df = get_gini_df(df_brasileirao)
gini_df.sort_values(by=['gini'], ascending=True)
view raw gini-calculation.py hosted with ❤ by GitHub

Usando os dados que foram carregados e o código acima, eu realizei o cálculo do Coeficiente de Gini e obtive a seguinte tabela:

Coeficiente de Gini Calculado com os respectivos campeões de cada edição

Considerando o Coeficiente de Gini como principal métrica de ordenação, podemos ver que a edição do Brasileirão de 2017, 2005 (ambas com o Corinthians campeão) e de 2009 (Flamengo campeão) foram as que tiveram mais igualdade dentro da era dos pontos corridos.

Por outro lado, as edições de 2018 (Palmeiras campeão), 2014 (Cruzeiro campeão) e 2012 (Fluminense campeão) foram as mais desiguais no que se refere a distribuição final dos pontos. Um fato curioso é que se pegarmos as 5 temporadas mais desiguais, veremos o Palmeiras e o Fluminense com 2 títulos cada (respectivamente 2018, 2016 e 2012, 2010).

Essas informações apontam que Corinthians e Flamengo tendem a ganhar as temporadas com a menor distribuição de pontos finais, e quando Palmeiras e Fluminense ganham são geralmente temporadas mais desiguais da perspectiva de distribuição dos pontos no final. Aliaá isso seria uma boa hipótese inicial para ser testada com mais dados.

Vamos olhar os dois extremos que são as temporadas de 2017 (mais igual) e 2018 (mais desigual).

Brasileirão 2017
Brasileirão 2018

Olhando a distribuição dos pontos aqui, podemos ver que a diferença de pontos entre os campeões foi de 8 pontos (80-72) e considerando a distancia do campeão com o quinto colocado nos dois campeonatos, se em 2017 temos uma distância de 15 pontos (72-57) em 2018 temos 17 pontos (80-63), ou seja muito parecida.

Entretanto, sabendo que o campeão sempre têm um pouco de margem considerando apenas esses extremos, se considerarmos apenas a distância entre o vice-campeão e o quinto colocado em 2017 temos apenas 6 pontos (63-57) enquanto em 2018 essa distância vai para 9 pontos (72-63).

Realizando o mesmo exercício entre o campeão e o pior time do campeonato em 2017 temos uma diferença de 36 pontos (72-36) enquanto em 2018 chegamos a expressiva marca de 57 pontos de diferença (80-23). Isso mostra que mesmo entre os piores times ao longo desses dois campeonatos temos a expressiva diferença de 13 pontos (36 (Atlético Goianiense/2017 – 23 (Paraná). Vamos ficar atentos a essas informações, pois eu vou voltar aqui posteriormente.

Agora eu vou gerar o gráfico de como ficou esse Coeficiente de Gini ao longo do tempo.

def get_graph_ts(df, column, title, label):
"""Generate graph of a Time Series
Parameters
———-
df : Pandas Dataframe
Dataframe with Brasileirão data
column : string
Column with the metric to be ploted
title : string
Graph title to be displayed
label : string
Name of the series that will be placed
as legend
Returns
——-
"""
plt.figure(figsize=(20,10))
plt.title(title)
plt.xlabel('Years')
plt.ylabel('Gini Index')
plt.plot(df[column], label=label)
plt.xticks(gini_df.index)
get_graph_ts(gini_df,
'gini',
'Gini Index in Brasileirão',
'Gini Index',
)
view raw graph-gini.py hosted with ❤ by GitHub
Coeficiente de Gini no Brasileirão ao longo do tempo

Em uma primeira análise podemos perceber algumas curiosidades:

  • Ao que parece temos sim uma tendência não tão clara de crescimento da desigualdade, mas com vales e picos bem distintos e considerando que o formato de 20 times têm apenas 12 temporadas completas isso tem que ser levado com cautela;
  • Algo surpreendente é que os vales costumam acontecer nos anos ímpares e os picos nos anos pares. Isso talvez seja explicado por algum efeito externo, tal como as Olimpíadas e Copa do Mundo que ocorrem em anos pares. E uma hipótese bem fraca, mas ainda sim é uma hipótese. (Nota do Autor: Se algueém tiver uma explicação razoável pode me mandar um comentário que eu coloco aqui e dou o crédito);
  • Ao que parece depois de 2011 houve uma subida mais consistente e manutenção desse aumento da desigualdade com o Coeficiente de Gini voltando para baixo de 0.115 apenas em 2017, ou seja, uma janela de 6 anos acima do patamar de 0.115;
  • E falando em 2017, essa temporada ao que parece foi uma total quebra em relação a essa desigualdade, dado que considerando o segundo, terceiro e quarto colocados terminarem com a diferença de apenas 1 ponto3 times ficaram com 43 pontos, com dois deles sendo rebaixados para a segunda divisão.

Entretanto, uma coisa que eu considerei foi que talvez tenha algum efeito que eu chamo de “Última temporada na Série A” ou “Já estou rebaixado mesmo, não tem mais o que fazer” em relação a essa (des)igualdade, dado que sempre saem/entram 4 times por ano (i.e. 20% dos times são trocados todos os anos). Para remover esse potencial efeito, eu vou considerar uma média móvel considerando os 3 últimos campeonatos. Ou seja, sempre haverá a combinação de a) dois anos desiguais e um ano igual e b) dois anos iguais com um ano desigual.

# Some graphs with rolling average
date_range = [2003, 2004, 2005, 2006, 2007,
2008, 2009, 2010, 2011, 2012,
2013, 2014, 2015, 2016, 2017,
2018]
def get_graph_ts_rolling_average(ts, title, window, date_range=date_range):
"""Generate graph of a Time Series with a simple rolling average
Parameters
———-
ts : Pandas Dataframe column
Dataframe column with a metric to be ploted
title : Pandas Dataframe
Graph title to be displayed
window : int
Rolling back window to be considered in the average
date_range : Array
Array to be used in the ploting. Matplotlib has a
very bad way to deal with that, so I need to use this
workaround to place all years properly
Returns
——-
"""
plt.figure(figsize=(20,10))
plt.plot(date_range, ts.rolling(window=window, center=False).mean(), label='gini');
plt.title(f'{title}{window}')
plt.xlabel('Years')
plt.ylabel('Gini Index')
plt.xticks(gini_df.index)
plt.legend()
get_graph_ts_rolling_average(gini_df['gini'],
'Rolling Average in Gini Index in Brasileirão – Rolling Average window=',
3,
)
view raw rolling-gini.py hosted with ❤ by GitHub
Coeficiente de Gini quando consideramos uma janela de 3 edições para computar a média

Realizando essa suavização a grosso modo, podemos ver o efeito desse aumento da desigualdade mais claro, quase que de forma linear de 2009 até 2016 sendo quebrado apenas pela famigerada temporada de 2017, e com a temporada de 2018 sofrendo um efeito claro com esse ajuste.

Anteriormente eu falei sobre essa disparidade dos pontos entre os primeiros colocados, entre os últimos colocados e os campeões em relação aos piores times da liga. Podemos perceber que sempre nestes casos extremos temos o campeão vencendo com uma pequena folga, e com os times subsequentes com algum tipo de disputa em ordens diferentes de magnitude.

Para validar esse ponto de que essa desigualdade está aumentando de uma maneira um pouco mais robusta, eu vou remover os nossos outliers desses campeonatos, ou resumindo: Eu vou tirar o campeão e o pior time da temporada da análise.

(Nota do Autor: Eu sei que existem inúmeras abordagens de como se fazer isso da maneira correta, com recursos quase que infinitos na internet de técnicas (1234 e inclusive com testes estatísticos muito robustos de como se fazer isso da maneira correta, caso seja necessário. Por questões de simplicidade e para reforçar o meu ponto, eu estou removendo dado que dentro dessas situações extremas como eu tenho como resultados a) o campeão com uma leve vantagem no final e b) o pior time da temporada sendo muito pior mesmo, a ideia é ver a igualdade dos outros times no campeonato. Lembrem-se que eu quero ver de maneira geral (des)igualdade da liga e tirar o efeito dos supercampeões e dos vergonhosos sacos de pancada da temporada).)

Dito isto, vou remover esses outliers e calcular novamente o Coeficiente de Gini.

def get_brasileirao_no_outliers(df):
"""Generate a DF removing the champion and the worst team of the championship
Parameters
———-
df : Pandas Dataframe
Dataframe with Brasileirão data
Returns
——-
df_concat : Pandas Dataframe
Returns a Pandas Dataframe without the outliers
"""
df_concat = pd.DataFrame()
for year in df['year'].unique():
pos_min = df[df['year'] == year]['position'].min()
pos_max = df[df['year'] == year]['position'].max()
df_filtered = df[(df['year'] == year) \
& (~df['position'].isin([pos_min, pos_max]))]
df_concat = df_concat.append(df_filtered)
return df_concat
def get_gini(df):
"""Generate a DF with the year and the following Gini Index calculated
Parameters
———-
df : Pandas Dataframe
Dataframe with Brasileirão data
Returns
——-
gini_df : Pandas Dataframe
Returns a Pandas Dataframe with the year, and gini index
"""
gini_per_year = []
for year in df['year'].unique():
championship_index = gini(np.array(df[df['year'] == year]['points']))
gini_per_year.append((year, round(championship_index, 4)))
gini_df = pd.DataFrame(gini_per_year)
gini_df.columns = ['year', 'gini']
# Indexing the date field for graph it smoothly
gini_df.set_index('year', inplace=True)
return gini_df
# Outlier removal
df_brasileirao_no_outliers = get_brasileirao_no_outliers(df_brasileirao)
df_brasileirao_no_outliers_gini = get_gini(df_brasileirao_no_outliers)
df_brasileirao_no_outliers_gini.sort_values(by=['gini'], ascending=True)
Gini Index com o campeão e o último colocado do campeonato removidos

Agora temos algumas mudanças significativas no nosso painel que são:

  • Se antes tínhamos as temporadas de 2017, 2005 e 2009 como as mais iguais, agora tiramos a temporada de 2009 vencida pelo Flamengo, e colocamos a de 2007 vencida pelo São Paulo;
  • Pelo lado das temporadas mais desiguais, no caso tínhamos a ordem de desigualdade pelas temporadas de 2018, 2014, e 2012, agora temos a nova ordem em relação as edições de 2014 (Cruzeiro), 2012 (Fluminense) e 2018 (Palmeiras)

Vamos checar como ficou a tabela das edições mais extremas sem os campeões em 2007 e 2014.

Brasileirão 2007
Brasileirão 2014

Olhando as duas tabelas finais em que removemos o campeão e o pior time, logo de cara podemos reparar que enquanto na temporada de 2014 temos dois vales de diferenças de 7 pontos (4° e 5° e 7° e 8°), no campeonato de 2007 a maior diferença foi de 4 pontos (15° e 16°) e fazendo um pequeno exercício de imaginação podemos dizer que o Paraná que teve 41 pontos em 2007 não teria perigo nenhum de ir para a segunda divisão, se tivesse o mesmo número de pontos no campeonato de 2014. Como eu coloquei anteriormente essa análise de transpor um time ao longo do tempo não é muito valida, porém, mencionei isso apenas para ressaltar a importância da distribuição dos pontos ao invés do número absoluto de pontos no resultado final.

Vamos gerar o gráfico apenas para verificar se a tendência do aumento da desigualdade permanece ou não.

Gini Index ao longo do tempo, removendo o campeão e o pior time da temporada
Gini Index ao longo do tempo, removendo o campeão e o pior time da temporada, considerando a média das 3 últimas edições

Olhando o Coeficiente de Gini removendo o campeão e o pior time, podemos ver que ainda temos a tendência de aumento da desigualdade dentro da liga, seja usando a métrica de forma regular ou aplicando uma média móvel com uma janela de 3 temporadas ao longo do tempo.

Fatos

Ao longo dos dados apresentados nessa análise eu cheguei aos seguintes fatos:

  • Temos uma tendência crescente na desigualdade em relação ao número de pontos entre os times dentro do campeonato brasileiro;
  • Esse crescimento começa de forma mais substancial em 2010;
  • As temporadas mais recentes (2018 e 2017) são respectivamente as temporadas com a maior e a menor desigualdade;
  • Essa tendência do aumento da desigualdade ocorre mesmo removendo o campeão e o pior time da temporada, dado que o campeão nos casos extremos possui uma pequena vantagem em relação ao segundo colocado, e o pior time termina com uma pontuação virtualmente impossível de reverter em uma ocasião de última rodada;
  • Nas temporadas que há uma desigualdade maior existe vales de pontos entre blocos de times, no caso que vimos esses vales foram de 7 pontos (2 vitorias + 1 empate);
  • Nas temporadas mais iguais esses vales de blocos times provavelmente são menores ou em alguns casos inexistentes, no caso observado havia somente um bloco de 4 pontos (1 vitória + 1 empate) e excluindo este fato não havia nenhuma distância maior do que 1 vitória por toda a tabela entre posições imediatamente superiores.

Conclusão e considerações para o futuro

Se tivermos que responder a nossa pergunta principal que foi “O brasileirão está ficando mais injusto ao longo do tempo?” a resposta correta seria:

“Sim. Com o uso do Coeficiente de Gini como métrica para mensurar se há uma desigualdade estrutural mostrou que existem sim elementos latentes dessa desigualdade”.

O leitor mais atento pode repaar que uma coisa que eu tomei muito cuidado aqui foi para não realizar afirmações relativas à competitividade, afirmações relativas às condições financeiras dos clubes, incremento de premiações ou ausência de incentivos para os piores colocados, etc.; dado que estes aspectos são difíceis de mensurar e há pouquíssimos dados disponíveis de maneira confiável para a análise, mas empiricamente talvez podemos realizar algumas afirmações nessas direções.

Em relação a análises futuras existem muitas hipóteses que podem ser testadas como um potencial problema fundamental de competitividade devido ao fato de muitos clubes que não representam uma elite (i.e. muitos times fracos na liga principal) e inclusive existem pautas relativas ao aumento do número dos clubes rebaixados, hipóteses que elencam fatores importantes como a disparidade financeira como um dos potenciais fatores de existir poucos supertimes, há uma hipótese que ganha tração também que é a respeito das cotas financeiras de direitos de exibição de jogos na televisão que compõe grande parte da receita desses clubes que a contar do ano que vem vai punir pesadamente os times que forem rebaixados.

São muitas hipóteses em discussão e muitos aspectos que poderiam ser a razão para o aumento dessa desigualdade. Há um grande número de aspectos que podem ser estudados e no final desse post tem alguns links para referências em outras ligas.

Todos os dados e a análise completa pode ser encontrada no repositório brasileirao-gini. Lá tem todas as instruções para executar passo a passo a análise.

Referências e links úteis

Inequality in the Premier League – Çınar Baymul

An Analysis Of Parity Levels In Soccer – Harvard Sports

Which Sports League has the Most Parity? – Harvard Sports

Major League Soccer and the Effect of Egalitarianism – Harvard Sports

The Gini Coefficient as a Measure of League Competitiveness and Title Uncertainty – Australia Sports Betting

Mourão, P. R., & Teixeira, J. S. (2015). Gini playing soccer. Applied Economics, 47(49), 5229-5246

How “fair” are European soccer leagues? Gini index applied to points distribution of 5 soccer leagues between 2000 and 2015 – r/soccer

Footballomics: Estimating League Disparity Performance with a Point-Rank Gini Index – Christoforos Nikolaou

O Campeonato Brasileiro está ficando mais injusto?

Ranking Metrics for Information Retrieval

Just a small compilation that I found in Github:

"""Information Retrieval metrics
Useful Resources:
http://www.cs.utexas.edu/~mooney/ir-course/slides/Evaluation.ppt
http://www.nii.ac.jp/TechReports/05-014E.pdf
http://www.stanford.edu/class/cs276/handouts/EvaluationNew-handout-6-per.pdf
http://hal.archives-ouvertes.fr/docs/00/72/67/60/PDF/07-busa-fekete.pdf
Learning to Rank for Information Retrieval (Tie-Yan Liu)
"""
import numpy as np
def mean_reciprocal_rank(rs):
"""Score is reciprocal of the rank of the first relevant item
First element is 'rank 1'. Relevance is binary (nonzero is relevant).
Example from http://en.wikipedia.org/wiki/Mean_reciprocal_rank
>>> rs = [[0, 0, 1], [0, 1, 0], [1, 0, 0]]
>>> mean_reciprocal_rank(rs)
0.61111111111111105
>>> rs = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]])
>>> mean_reciprocal_rank(rs)
0.5
>>> rs = [[0, 0, 0, 1], [1, 0, 0], [1, 0, 0]]
>>> mean_reciprocal_rank(rs)
0.75
Args:
rs: Iterator of relevance scores (list or numpy) in rank order
(first element is the first item)
Returns:
Mean reciprocal rank
"""
rs = (np.asarray(r).nonzero()[0] for r in rs)
return np.mean([1. / (r[0] + 1) if r.size else 0. for r in rs])
def r_precision(r):
"""Score is precision after all relevant documents have been retrieved
Relevance is binary (nonzero is relevant).
>>> r = [0, 0, 1]
>>> r_precision(r)
0.33333333333333331
>>> r = [0, 1, 0]
>>> r_precision(r)
0.5
>>> r = [1, 0, 0]
>>> r_precision(r)
1.0
Args:
r: Relevance scores (list or numpy) in rank order
(first element is the first item)
Returns:
R Precision
"""
r = np.asarray(r) != 0
z = r.nonzero()[0]
if not z.size:
return 0.
return np.mean(r[:z[1] + 1])
def precision_at_k(r, k):
"""Score is precision @ k
Relevance is binary (nonzero is relevant).
>>> r = [0, 0, 1]
>>> precision_at_k(r, 1)
0.0
>>> precision_at_k(r, 2)
0.0
>>> precision_at_k(r, 3)
0.33333333333333331
>>> precision_at_k(r, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: Relevance score length < k
Args:
r: Relevance scores (list or numpy) in rank order
(first element is the first item)
Returns:
Precision @ k
Raises:
ValueError: len(r) must be >= k
"""
assert k >= 1
r = np.asarray(r)[:k] != 0
if r.size != k:
raise ValueError('Relevance score length < k')
return np.mean(r)
def average_precision(r):
"""Score is average precision (area under PR curve)
Relevance is binary (nonzero is relevant).
>>> r = [1, 1, 0, 1, 0, 1, 0, 0, 0, 1]
>>> delta_r = 1. / sum(r)
>>> sum([sum(r[:x + 1]) / (x + 1.) * delta_r for x, y in enumerate(r) if y])
0.7833333333333333
>>> average_precision(r)
0.78333333333333333
Args:
r: Relevance scores (list or numpy) in rank order
(first element is the first item)
Returns:
Average precision
"""
r = np.asarray(r) != 0
out = [precision_at_k(r, k + 1) for k in range(r.size) if r[k]]
if not out:
return 0.
return np.mean(out)
def mean_average_precision(rs):
"""Score is mean average precision
Relevance is binary (nonzero is relevant).
>>> rs = [[1, 1, 0, 1, 0, 1, 0, 0, 0, 1]]
>>> mean_average_precision(rs)
0.78333333333333333
>>> rs = [[1, 1, 0, 1, 0, 1, 0, 0, 0, 1], [0]]
>>> mean_average_precision(rs)
0.39166666666666666
Args:
rs: Iterator of relevance scores (list or numpy) in rank order
(first element is the first item)
Returns:
Mean average precision
"""
return np.mean([average_precision(r) for r in rs])
def dcg_at_k(r, k, method=0):
"""Score is discounted cumulative gain (dcg)
Relevance is positive real values. Can use binary
as the previous methods.
Example from
http://www.stanford.edu/class/cs276/handouts/EvaluationNew-handout-6-per.pdf
>>> r = [3, 2, 3, 0, 0, 1, 2, 2, 3, 0]
>>> dcg_at_k(r, 1)
3.0
>>> dcg_at_k(r, 1, method=1)
3.0
>>> dcg_at_k(r, 2)
5.0
>>> dcg_at_k(r, 2, method=1)
4.2618595071429155
>>> dcg_at_k(r, 10)
9.6051177391888114
>>> dcg_at_k(r, 11)
9.6051177391888114
Args:
r: Relevance scores (list or numpy) in rank order
(first element is the first item)
k: Number of results to consider
method: If 0 then weights are [1.0, 1.0, 0.6309, 0.5, 0.4307, …]
If 1 then weights are [1.0, 0.6309, 0.5, 0.4307, …]
Returns:
Discounted cumulative gain
"""
r = np.asfarray(r)[:k]
if r.size:
if method == 0:
return r[0] + np.sum(r[1:] / np.log2(np.arange(2, r.size + 1)))
elif method == 1:
return np.sum(r / np.log2(np.arange(2, r.size + 2)))
else:
raise ValueError('method must be 0 or 1.')
return 0.
def ndcg_at_k(r, k, method=0):
"""Score is normalized discounted cumulative gain (ndcg)
Relevance is positive real values. Can use binary
as the previous methods.
Example from
http://www.stanford.edu/class/cs276/handouts/EvaluationNew-handout-6-per.pdf
>>> r = [3, 2, 3, 0, 0, 1, 2, 2, 3, 0]
>>> ndcg_at_k(r, 1)
1.0
>>> r = [2, 1, 2, 0]
>>> ndcg_at_k(r, 4)
0.9203032077642922
>>> ndcg_at_k(r, 4, method=1)
0.96519546960144276
>>> ndcg_at_k([0], 1)
0.0
>>> ndcg_at_k([1], 2)
1.0
Args:
r: Relevance scores (list or numpy) in rank order
(first element is the first item)
k: Number of results to consider
method: If 0 then weights are [1.0, 1.0, 0.6309, 0.5, 0.4307, …]
If 1 then weights are [1.0, 0.6309, 0.5, 0.4307, …]
Returns:
Normalized discounted cumulative gain
"""
dcg_max = dcg_at_k(sorted(r, reverse=True), k, method)
if not dcg_max:
return 0.
return dcg_at_k(r, k, method) / dcg_max
if __name__ == "__main__":
import doctest
doctest.testmod()
view raw rank_metrics.py hosted with ❤ by GitHub
Ranking Metrics for Information Retrieval

Queridos Cientistas de Dados brasileiros: Por favor, entendam por que Word Cloud é horrível para Análise de textos e tentem TF-IDF + Word N-Grams

Eu sei que o título ficou meio bait, então relevem pois aqui o tom sempre será de moderação.

Eu ja cometi esse erro no passado (essa thread mostra isso bem que como uma fênix burra e teimosa eu continuo fazendo isso), e confesso que quando vemos aquelas nuvens de palavras temos a sensação que somos os melhores analistas do mundo, pois podemos explicar de maneira direta e simples para as pessoas mais leigas no assunto. Afinal quem não gostaria de ser conhecido por ser um excelente storyteller, não é mesmo?

Apesar disso, nessas linhas mal escritas eu vou tentar fazer o meu ponto do por que Word Cloud é um instrumento péssimo para análise de textos/discursos que além de ser falho por usar apenas frequência absoluta das palavras esse método dá muita margem para o que eu chamo de “saltos interpretativos” que mais parecem delírios da cabeça de quem está analisando do que análise propriamente dita.

O problema…

Vira e mexe sempre quando chegam as eleições, ou mesmo algum tipo de pronunciamento de alguém importante via discurso a primeira coisa que vemos é a famosa Word Cloud (i.e. ou Nuvem de Palavras ou Tag Cloud).

Eu mesmo adoro usar Word Cloud em alguns discursos presidenciais como podem ser vistos na minha conta no Github em que eu usei esse método em todos os discursos presidenciais que eu consegui encontrar no Itamaraty, os quais vocês podem ver as vergonhosas imagens abaixo: 

Word Cloud dos discursos do ex-presidente FHC
Word Cloud dos discursos do ex-presidente Lula
Word Cloud dos discursos do ex-presidente Dilma
Word Cloud dos discursos do ex-presidente Temer
Word Cloud dos discursos do presidente Bolsonaro

(PS: eu sei que o código está medonho, mas o dia-dia está massacrante e eu não tive tempo de tirar do ar ainda, mas se alguém quiser continuar só dar o fork e “do it yourself”)

Alguns exemplos do péssimo uso recorrente de Word Cloud podem ser vistos na BBC em relação aos discursos dos presidentes brasileiros na ONU, na Revista Época que fez a mesma coisa, na Folha de São Paulo que fez uma nuvem de palavras com os discursos da ex-presidente da republica Dilma Rousseff e no Estado de Minas que fez o mesmo em relação ao discurso de diplomação do presidente da republica Jair Bolsonaro.

Sempre bom quando temos o Jornalismo de Dados (Data Journalism) (Nota do Autor [1]: Desde o inicio dos tempos eu sempre pensei que todo o jornalismo sempre era baseado em dados, mas isso fica para outro post. Vivendo e aprendendo.) usando ferramentas quantitativas para análise desses dados. Porem, o recurso de Word Cloud diz muito pouco dentro de um contexto geral e da espaço para “saltos interpretativos” que fariam inveja ao Tarot e a Astrologia em termos de picaretagem subjetividade.

Um exemplo claro desses saltos interpretativos pode ser visto neste artigo do Linkedin chamado “Nuvem de palavras: nossos candidatos por eles mesmos” no qual o autor colocou mais aspectos pessoais do que ele achava do que ater-se unicamente ao que foi dito de fato.

No blog do Tarcízio Silva no post “O que se esconde por trás de uma novem de palavras” ele fala de alguns recursos de análise que são interessantes do ponto de vista de análise do discurso, contudo, quando falamos de linguística computacionalrecuperação de informação a Word Cloud revela muito pouco chegando a ser até mesmo ser enganosa dependendo do caso e eu vou mostrar isso mais adiante. 

Em NLP Semântica e Sequência não são tudo, mas são 99%

Entretanto, quando falamos de Natural Processing Language (NLP) o uso de Word Cloud mostra bem pouco dado que como esse método é baseado apenas na combinação Frequência de Palavras + Remoção de Stopwords, e com isso dois principais aspectos da análise de texto são perdidos que são: 

a) A Semântica em relação ao que está sendo dito e como isso se relaciona no corpus como um todo e não baseado em interpretação livre (e.g. A palavra “manga” pode ter diferentes significados se usados em diferentes contextos como em frutas ou de roupas); e

b) A sequência no qual sem ela perde-se a chance de entender a probabilidade das palavras aparecerem dentro de um encadeamento lógico em relação ao que está sendo colocado (e.g. Se tivermos as palavras “bebe”, “mata”, “fome” podemos gerar as sequencias “bebê, mata, fome” ou “fome, mata, bebê” o que são fatos absolutamente diferentes). 

Penso que esses dois exemplos simples fizeram o meu ponto de que o Word Cloud por si só não diz muita coisa e para piorar deixa muito espaço para “saltos interpretativos”.

Vamos ver adiante uma forma simples de fazer uma análise textual usando duas técnicas que são TF-IDF e o Word N-Grams. 

TF-IDF + Word N-Grams = Melhor interpretação = Melhores insights 

Basicamente quando usamos Term Frequency–Inverse Document Frequency (TF-IDF) ao invés de realizarmos simplesmente uma análise da frequência das palavras, estamos colocando um determinado nível de semântica na análise do texto dado que, se ao mesmo tempo consideramos a frequência das palavras (TF), também consideramos a frequência inversa do documento (IDF) em que essa combinação entre TF e IDF vai medir a relevância da palavra de acordo com o grau de informação que ela trás para o texto como um todo.

Ou seja, o TF-IDF faz essa relação de ponderação de ocorrência relativa e grau de informação das palavras dentro de um corpus. Esse gráfico do post “Process Text using TFIDF in Python” do Shivangi Sareen mostra bem o que seria o TF-IDF em relação a um corpus.

Fonte: Process Text using TF-IDF in Python

Essa questão da coocorrência relativa de palavras vai ficar mais clara no exemplo prático que será falado depois. 

Ao passo que o Word Cloud não se preocupa com a sequencia o Word N-Grams vem suprir esse ponto dado que não somente as palavras importam, mas também a probabilidade da sequencia dessas palavras dentro de um determinado contexto linguístico e/ou encadeamento semântico. (Nota do Autor [2]: eu sei que vão ter pessoas me cornetando pelo fato de ser n-Grams apenas, mas este conceito de Word n-Grams eu estou emprestando do Facebook FastText que tem um ótimo paper chamado “Bag of Tricks for Efficient Text Classification” que fala sobre essa implementação de usar sequências locais de palavras (i.e.que vão nos dar pequenos contextos locais concatenados) ao invés de letras para classificação de textos, mas isso é papo para outro post. A propósito eu escrevi sobre o FastText aqui a algum tempo.). 

Esse post não tem nenhuma pretensão de ser uma referência em TF-IDF/Word-NGrams mas eu recomendo fortemente o que eu considero a bíblia do NLP que é o livro “Neural Network Methods for Natural Language Processing” do Yoav Goldberg.

Dito isto, vamos para um exemplo prático de o porquê usar TF-IDF e Word N-Grams mostram muito mais do que Word Cloud e como você pode usar em sua análise de textos.

Exemplo prático: Análise de textos de alguns autores do Instituto Mises Brasil 

A algum tempo eu venho acompanhando a política brasileira da perspectiva de ensaístas de vertentes ligadas ao libertarianismoanarcocapitalismosecessãoautopropriedade e assuntos correlatos; e um fato que me chamou bastante a atenção foi a mudança editorial que vem acontecendo de forma lenta em um dos principais Think tanks liberais do Brasil que é o Instituto Mises Brasil (IMB). (Nota do Autor [3]: Eu penso que a editoria ficou ruim ao longo do tempo e o instituto virou apenas uma parodia sem graça voltada ao desenvolvimentismo, financismo bancário com a redução do foco na liberdade.)

Minha hipótese é que devido à diferenças editoriais sobre assuntos ligados à secessão houve uma ruptura entre o Hélio Beltrão e os Chiocca Brothers (Christiano e Fernando) com a consequente mudança de diretoria editorial no IMB e com a fundação do Instituto Rothbard. Os detalhes podem ser encontrados aqui

Eu estou trabalhando em alguns posts adicionais sobre isso, mas para apenas mostrar o meu ponto sobre Word Cloud eu vou usar como exemplo alguns textos de um autor do IMB chamado Fernando Ulrich.

Olhando rapidamente a linha editorial do Fernando Ulrich podemos ver que ele fala majoritariamente sobre 3 assuntos: Moeda, Bitcoin e Sistema Bancário. E ate mesmo um leitor que começou deste autor ontem sabe disto.

Contudo, gerando uma simples Word Cloud com os textos deste autor temos o seguinte resultado:

Word Cloud de todos os posts do Fernando Ulrich

Podemos ver que as palavras que aparecem mais são: Mercado, ano, Governo, Economia, e com Bitcoin, Moeda e Dinheiro com uma frequência menor (Nota do Autor [4]: Eu intencionalmente não usei nenhum tipo de steeming ou lemmatization pois eu queria ter mais interpretabilidade nessa análise). 

Entretanto, vamos olhar agora os mesmos textos usando TF-IDF:

TF-IDF com os textos do Fernando Ulrich

Vejam que usando as top 30 palavras com o maior TF-IDF score podemos ver que as palavras BitcoinBancosBacen, e Moeda aparecem com um TF-IDF score muito superior do que as palavras do Word Cloud (lembrando: Mercado, ano, Governo, Economia); e como leitor dos artigos do Fernando Ulrich fica claro que ele fala sobre esses assuntos com uma frequência muito maior, fato este que pode ser visto nos vídeos dele em seu excelente canal no Youtube

Agora dentro da perspectiva de mapear as sequencias locais dos textos usando Word NGrams, temos o seguinte resultado com os textos do IMB que o Fernando Ulrich escreveu:

Word N-Grams dos textos do Fernando Ulrich (n=3)

Podemos ver aqui que os temas convergem para assuntos como bancos centrais, sistema bancário, e questões relativas ao sistema de reservas fracionadas, o que converge também com as temáticas do seu canal do Youtube e que todo leitor mais antigo desse autor sabe. 

Conclusão

Como vimos acima, quando vamos realizar a análise de textos de forma a dar uma interpretação sobre o que foi dito e/ou a temática do que está sendo falado, o uso de Word Cloud definitivamente não é o melhor e que a combinação TF-IDF + Word N-Grams geram resultados que representam muito melhor a realidade devido ao fato que essas trazem um nível maior mais semântica e encadeamento das palavras que são na pratica aspectos que representam melhor a realidade do que a frequência propriamente dita.

Notas Finais

Nota Final [1]: Amiguinhos intolerantes dos extremos de ambos os espectros, segurem a emoção nos comentários pois eu tenho um pipeline bem grande de coisas que eu estou fazendo nesse sentido para desagradar muita gente ainda. 

Mesmo em 2019 esse tipo de aviso é importante: Analisar os textos das pessoas não significa estar em linha com o que elas falam ou mesmo algum tipo de endosso das opiniões delas.

Acreditem ou não, existe uma serie de pessoas que têm um modelo mental suficientemente robusto para conseguir ler desde a biografia bilionário que é usado em Fanfic empresarial de palco até conseguir entender a luta de pessoas que mesmo sendo 50,94% de uma população dado o seu grupo racial, essas pessoas não têm um único representante de peso na sociedade como um CEO de multinacional ou alguém de relevância poder executivo e judiciário

Nota Final [2]: Eu sei que tem código envolvido e eu vou compartilhar até o final da próxima semana (ao menos o database eu solto). Eu não compartilhei ainda dado que eu tenho que colocar em um nível mínimo de legibilidade para que as pessoas não pensem que eu chicoteei um macaco até ele conseguir digitar todo o script. 

Nota Final [3]: Tem um monte de erro gramatical, semântico e lógico nesse post e todos eles são meus e eu vou ajustar um dia. Aos poucos eu vou corrigir dado além do fato de que eu fui alfabetizado no Brasil no ensino publico durante os anos 90, tenho como fatores agravantes de que eu estou digitando esse texto em um teclado austríaco (ba dum tss) com um corretor ortográfico em… Alemão. Então, relevem.

Queridos Cientistas de Dados brasileiros: Por favor, entendam por que Word Cloud é horrível para Análise de textos e tentem TF-IDF + Word N-Grams