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:
- 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.
- 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). - 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:
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
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.
Publiquei uma versão das funções implementadas em Python no meu blog:
http://lkraider.eipper.com.br
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.
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
Valeu pelas dicas galera! ;-)
[]'s
Cacilhas, La Batalema
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
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
Postar um comentário