sexta-feira, 4 de junho de 2010

O coeficiente de Gini em SQL

O coeficiente de Gini é uma medida de desigualdade usado na economia para avaliar a distribuição de renda de diferentes sociedades.

Ele é um número entre 0 e 1, sendo 1 a desigualdade completa (uma única pessoa tem toda a riqueza) e 0 a igualdade total (todos ganham a mesma coisa). O número também é interpretado como a metade da diferença média normalizada. Ou seja, se a média é R$1.000 e o coeficiente de Gini for 0,6, então a diferença média entre rendas vai ser R$1.000*0,6*2=R$1.200. Ou seja, neste caso, a diferença média é maior que a média; esta seria uma sociedade bastante desigual. Países considerados avançados, como os da Escandinávia têm coeficientes entre 0,2 e 0,3. Países como Brasil, Uruguai, Estados Unidos e China, têm coeficientes entre 0,4 e 0,5. Atualmente, o país mais desigual do mundo é a Namíbia, com 0,707.

Na falta de dados sobre renda, usei uma lista de produtos. Mesmo assim, vou supor estar usando uma tabela PESSOAS com as seguintes colunas:
  • ID - um identificador único para casa pessoa;
  • SALARIO - o salário.

Usando a definição supracitada, escrevi o seguinte no Oracle:

select (diff/media)/2 from (
select avg(abs(p1.salario-p2.salario)) diff
from pessoas p1, pessoas p2
where p1.id<>p2.id
),(
select avg(salario) media
from pessoas
)

Temos dois cross-joins. O primeiro é terrível, porque multiplica todos os registros da tabela por ela mesma para calular as diferenças. Como eu tinha 14.354 registros, o cross-join resultante disso tinha 206.022.962 linhas. Nada bom. Se eu quisesse calcular o coeficiente de Gini do Brasil, teria que usar uma tabela de uns 190 milhões de registros e isso não ia funcionar. O segundo é inócuo, porque o último select produz apenas uma linha.

Por sorte, um economista chamado Angus Deanton encontrou uma maneira melhor de calcular isto. Na fórmula abaixo, N é o número total de indivíduos, u é a média dos rendimentos, X é o rendimento de cada um e P é a classificação de cada um (sendo 1 o mais rico e N o mais pobre).


Isso é muito mais eficiente, porque agora não é mais preciso comparar cada um a todos os demais.

A consulta resultante usa a função analítica ROW_NUMBER() para classificar os salários:

select ((n+1)/(n-1))-(2/(n*(n-1)*media))*sum(pc*salario) from (
select salario, row_number() over (order by salario desc) pc
from pessoas
), (
select avg(salario) media, count(1) n
from pessoas
) group by n, media

A tabela abaixo compara os resultados:

SELECTTempoResultado
167,94s0,55617007994480478319584630635542608397
20,03s0,5561700799448047831958463063554260839701

O segundo, além de ser 2.264 vezes mais rápido, ainda produziu mais duas casas de precisão que, por outro lado, não têm nenhuma utilidade.

2 comentários:

Claudio disse...

Awesome! Que tal liberar o dataset (sanitizado) para comparacao?

Estive brincando c/ o MySQL e um dataset de umas 22K linhas. A primeira abordagem me tomou 121s, a segunda 0.06, mas com um hack para compensar a ausencia de ROW_NUMBER() no MySQL ...

[]s
~heckler

forinti disse...

Interessante como a proporção entre os tempos manteve-se. Tem tanta coisa que pode variar, mesmo usando o mesmo banco.

Por outro lado, não entendo o que alguém tão inteligente esteja fazendo com MySQL. Postgresql tem Window Functions, que são sua versão das Analytic Functions do Oracle.

Tsk, tsk. No cookie for you.