Domingo, 1 de Junho de 2008

Cálculo da variância com linguagem funcional

Paradigma funcional A implementação de cálculos básicos de estatística em linguagens funcionais é tremendamente mais simples do que usando linguagens imperativas, como a tradicional C, muito usada devido a sua abstração matemática¹.

Escolhi como exemplo o cálculo da variância, que determina o grau de dispersão dos elementos de um conjunto ou vetor.

Para o cálculo da variância é preciso calcular a média dos elementos do vetor.

Média


Em quase todos os textos que li sobre variância, covariância e outras operações estastísticas, é aconselhado usar a média quadrada dos elementos, mas meu professor de estatística diz pra usar a média aritmética.

Então vamos implementar as duas.

Média aritmética


Média aritmética consiste no somatório de todos os elementos dividido pelo número de elementos.

Em C a implementação é:
double arithmetic_mean(double *v, int len) {
double sum = 0;
int i;
for (i = 0; i < len; ++i)
sum += v[i];
return sum / len;
}


Em Common Lisp o mesmo algoritmo é implementado assim:
(defun arithmetic-mean (a-list)
(/ (reduce '+ a-list) (length a-list)))


Desconsiderando a implementação em duas linhas contra seis ou sete em C, podemos citar algumas vantagens:
  1. Em C é preciso informar o tamanho do vetor, enquanto em Common Lisp não – em C++ usando STL também não seria necessário.
  2. Em C usamos diversar variáveis que mudam de estado em ciclos. Em Common Lisp usamos funções e retornos, numa relação 1:1 com a Matemática – por exemplo, Σx vira (reduce '+ x).
  3. Realmente… duas linhas em Lisp contra seis ou sete em C. =)


Média quadrada


Média quadrada ou RMS consiste na raiz quadrado da razão do somatório dos quadrados dos elementos pela quantidade:
double root_mean_square(double *v, int len) {
double sum = 0;
int i;
for (int i = 0; i < len; ++i)
sum += v[i] * v[i];
return sqrt(sum / len);
}


Observação: para usar sqrt() é preciso incluir o cabeçalho math.h.

Agora vamos fazer a mesma coisa em Common Lisp:
(defun root-mean-square (a-list)
(sqrt
(/
(reduce '+ (mapcar (lambda (e) (* e e)) a-list))
(length a-list))))


[update 2008-06-14]O código acima foi alterado segundo a sugestão do Pedro – veja comentários abaixo.[/update]


Esse ficou maiorzinho, no entanto mantém uma relação muito mais próxima com a expressão matemática do que C. No caso o form loop criou um novo vetor contendo os quadrados dos elementos, que foi reduzido por reduce e '+ – Σ.

Escolhendo uma média


Se você escolher a média aritmética, faça:
#define mean(v, len) arithmetic_mean(v, len)


Em Lisp:
(defun mean (a-list)
(arithmetic-mean a-list))


E se escolher média quadrada:
#define mean(v, len) root_mean_square(v, len)

(defun mean (a-list)
(root-mean-square a-list))


Numerador da variância


Há dois tipos de variância: populacional e da amostra. O cálculo é similar, mudando apenas o denominador. Portanto vamos calcular primeiro o numerador, idêntico para os dois tipos de variância.

Em C, o cálculo do numerador é:
double varnum(double *v, int len) {
double mean_x = mean(v, len);
double sum = 0;
int i;
for (i = 0; i < len; ++i)
sum += pow(v[i] - mean_x, 2.);
return sum;
}


Observação: para usar pow() é preciso incluir o cabeçalho math.h.

Agora em Common Lisp:
(defun var-num (a-list)
(reduce #'+
(mapcar (lambda (e) (expt (- e (mean a-list)) 2)) a-list)))


[update 2008-06-14]O código acima foi alterado segundo a sugestão do Pedro – veja comentários abaixo.[/update]


É claro, para quem está acostumado a ver códigos e mais códigos em linguagens derivadas de C o código em Lisp pode parecer confuso, mas se você for tentar explicar a um matemático sem conhecimento de C, verá que programação funcional se encaixa melhor à Matemática pura.

Variância populacional


A variância populacional consiste na razão do numerador calculado pela quantidade de elementos e é usada quando o vetor representa todos os elementos do universo desejado.
double populational_variance(double *v, int len) {
return varnum(v, len) / len;
}


Em Common Lisp:
(defun populational-variance (a-list)
(/ (var-num a-list) (length a-list)))


Variância da amostra


A variância da amostra consiste na razão do numerador calculado pela quantidade de elementos menos um e é usada quando o vetor representa apenas uma amostragem do universo desejado.
double sample_variance(double *v, int len) {
return varnum(v, len) / (len - 1);
}


Em Common Lisp:
(defun sample-variance (a-list)
(/ (var-num a-list) (-(length a-list) 1)))


Conclusão


Para operações estatísticas ou puramente matemáticas, linguagens funcionais podem ser uma alternativa mais eficiente do que linguagens imperativas.

[update 2008-06-07]Versão em Ruby pelo Tiago no Programando sem cafeína![/update]
[update 2008-06-10]Versão em Python pelo LKRaider no LKVenia! =)[/update]


[]'s
Cacilhas, La Batalema

¹Abstração matemática: quis dizer abstração de alto nível em busca de uma representação mais matemática.

8 comentários:

Tiago Albineli Motta disse...

Fiz uma implementação desse cálculo em Ruby. Creio que o uso fica ainda mais simplificado:

array.variancia_populacional

array.variancia_da_amostra

Tá lá no meu blog, depois dá uma olhada:
http://programandosemcafeina.blogspot.com/2008/06/clculo-da-varincia-com-ruby.html

Danilo Oliveira disse...

Não conheço Lisp, mas conheço Haskell.

Em Haskell sai mais ou menos assim:

variancia :: [Double] -> Double

variancia xs = ( sum ( map ( \ x -> (x - media) ^ 2 ) xs ) ) / len
where
len = (fromIntegral $ length xs)
media = (sum xs) / len


A única coisa que ficou de obscura foi o fromIntegral, sem aquilo não funciona (não sei bem porque).

Tipo assim, se você digitar no hugs

>(sum [1,2,3,4.1]) / (length [1,2,3,4.1])

Dá o seguinte erro:
ERROR - Cannot infer instance
*** Instance : Fractional Int
*** Expression : sum [1,2,3,4.1] / length [1,2,3,4.1]


Com o fromIntegral esse erro não aconteçe e a média é calculada normalmente.

LKRaider disse...

Publiquei uma versão das funções implementadas em Python no meu blog:

http://lkraider.eipper.com.br

La Batalema Pitonisto disse...

Só de curiosidade, a covarância é tão fácil de implementar quanto a variância.

Em C teríamos a função:

double covarnum(double *vx, double *vy, int len) {
    double mean_x = mean(vx, len);
    double mean_y = mean(vy, len);
    double sum = 0;
    int i;
    for (i = 0; i < len; ++i)
        sum += (vx[i] - mean_x) * (vy[i] - mean_y);
    return sum;
}

Com as alterações equivalentes para covariâncias populacional e amostral.

Em Common Lisp, imaginando que o vetor traga os dados no formato (cons x y):

(defun covar-num (a-list)
    (let (mean-x mean-y)
        (setq mean-x (mean (for e in a-list collect (car e))))
        (setq mean-y (mean (for e in a-list collect (cdr e))))
        (reduce '+
            (loop
                for e in a-list
                collect (* (- (car e) mean-x) (- (cdr e) mean-y))))))

Não pensei direito – nem testei – para escrever isso, então pode haver erros.

Danilo Oliveira disse...

Já descobri o motivo daquele problema de Haskell:

sum aplicado a lista de floats retorna float, length aplicado a qualquer lista retorna int. O Haskell deixa operar com um literal int e outro float, mas quando são duas funções retornando valores desses tipos dá erro.
Ex:
func1 :: Int -> Int
func1 x = 2 * x

func2 :: Float -> Float
func2 x = 2 * x

No Hugs:

Main> (func1 2) * (func2 2)
ERROR - Type error in application
*** Expression : func1 2 * func2 2
*** Term : func1 2
*** Type : Int
*** Does not match : Float

O fromIntegral resolve o problema:

Main> (fromIntegral $ func1 2 ) * (func2 2)
16.0

O dollar ($), é um açúcar sintático para o resto da expressão entre parênteses
(fromIntegral $ func1 2 ) equivale à (fromIntegral ( func1 2 ) )

Haskell é legal pra caramba, mas de vez em quando é meio chatinha ...

O exemplo que eu dei acima pode ser implementado de forma bem parecida usando Python.

Pra programar funcional em Python tem algumas dicas:

* usar funções de alta ordem, isto é , funções que são passadas como parâmetro a outra função, ou retornadas como resultado de uma função

* usar os módulos (operator, itertools, functional) e funções (map, filter, reduce, etc) que servem para esse propósito

* não usar comandos com efeito colateral. É só fazer o seguinte, você pode criar variáveis e inicializá-las, mas nunca modifique-as depois

Com isso já da pra programar funcional em Python, só que não de uma forma tão declarativa como Haskell (por exemplo, avaliar uma função usando casamento de padrões) e também sem avaliação preguiçosa.

Quem quiser saber mais:

http://www.norvig.com/python-lisp.html

http://www.ibm.com/developerworks/library/l-prog.html

La Batalema Pitonisto disse...

Valeu pelas dicas galera! ;-)

[]'s
Cacilhas, La Batalema

Pedro disse...

sse foi um post bem legal, parabens! Algumas sugestões: as funções var-num e root-mean-square poderiam ser simplificadas, reduzidas e ter um estilo mais funcional com o uso do mapcar no lugar do loop:

(defun var-num (a-list)
(reduce #'+ (mapcar (lambda (e) (expt (- e (mean a-list)) 2)) a-list)))

(defun root-mean-square (a-list)
(sqrt (/ (reduce #'+ (mapcar (lambda (e) (* e e)) a-list))
(length a-list))))

A implementação de covar-num em um dos comentários esta em um estilo desnecessáriamente imperativo, para usar um estilo funcional poderiamos ter:

(defun covar-num (a-list)
(let ((mean-x (mean (mapcar #'first a-list)))
(mean-y (mean (mapcar #'second a-list))))
(reduce #'+ (mapcar (lambda (e) (* (- (first e) mean-x)
(- (second e) mean-y)))
a-list))))

Pedro Kroger

La Batalema Pitonisto disse...

Valeu Pedro!

Realmente Lisp não é uma linguagem que eu domine bem e esses detalhes muitas vezes me escapam. =)

No final de semana vou alterar o artigo e acrescentar sua sugestáo.

[]'s
Cacilhas, La Batalema