1  Introdução ao Python

O programa Python foi escolhido para ministrar este curso por uma série de razões. Além de ser um programa livre, no sentido de possuir livre distribuição e código fonte aberto, pode ser utilizado nas plataformas Windows e Linux. Além do mais, o Python possui grande versatilidade no sentido de possuir inúmeros pacotes já prontos e nos possibilitar criar novas rotinas e funções. O PyPi é o repositório oficial do Python onde todos os pacotes são armazenados. Você pode pensar nele como um Github para os pacotes do Python. O Python foi criado pelo holandês Guido van Rossum para ser uma linguagem de programação simples e legível, além de ser muito produtiva. O Python evoluiu e se tornou em uma linguagem muito atrativa e uma das principais escolhas para aplicações de desenvolvimento web, análise de dados e inteligência artificial, entre outras. Por ser genuinamente um programa orientado por objeto nos possibilita programar com muita eficiência e versatilidade, embora apresente algumas mudanças em sua implementação em relação a outras linguagens orientadas por objetos. Outro aspecto que é bastante atrativo no Python refere-se ao fato de o mesmo receber contribuições de pesquisadores de todo o mundo na forma de pacotes. Essa é uma característica que faz com que haja grande desenvolvimento do programa em relativamente curtos espaços de tempo e que nos possibilita encontrar soluções para quase todos os problemas com os quais nos deparamos em situações reais. Para os problemas que não conseguimos encontrar soluções, o ambiente de programação Python nos possibilita criar nossas próprias soluções.

Nestas notas de aulas pretendemos apresentar os conceitos básicos da estatística computacional de uma forma bastante simples. Inicialmente obteremos nossas próprias soluções para um determinado método ou técnica e em um segundo momento mostraremos que podemos ter a mesma solução pronta do Python quando esta estiver disponível. Particularmente neste capítulo vamos apresentar algumas características do ambiente e da linguagem para implementarmos nossas soluções. Nosso curso não pretende dar soluções avançadas e de eficiência máxima para os problemas que abordaremos, mas propiciar aos alunos um primeiro contato com a linguagem Python e com os problemas básicos da estatística computacional.

A desvantagem é que o Python não é um programa fácil de aprender. Alguns esforços iniciais são necessários até que consigamos obter algum benefício. Não temos a intenção de apresentar neste curso os recursos do Python para análises de modelos lineares de posto completo ou incompleto, de modelos não-lineares, de modelos lineares generalizados ou de gráficos. Eventualmente poderemos utilizar algumas destas funções como um passo intermediário da solução do problema que estaremos focando. Este material será construído com uma breve e simplificada abordagem teórica do tópico e associará exemplificações práticas dos recursos de programação Python para resolver algum problema formulado, em casos particulares da teoria estudada.

Este material é apenas uma primeira versão que deverá ter muitos defeitos. Assim, o leitor que encontrá-los ou tiver uma melhor solução para o problema poderá contribuir enviando um e-mail para .

Visite minha homepage https://des.ufla.br/~danielff/.

1.1 Introdução aos Comandos e Objetos do Python

No Python os objetos podem ser de diferentes tipos ou estruturas, tais como os números (int, float e complexos), boolean, string, list, tuple, set, dictionary, functions (objeto que encapsula códigos), dataframes e muitos outros. Vamos descrever de forma sucinta e gradativa alguns destes objetos e comandos básicos do Python.

As instruções do Python podem ser escritas em um editor de texto e digitadas no terminal do programa. Para usarmos o Python, inicialmente instalamos uma distribuição do Programa. Recomendamos a versão atual (no momento do lançamento do livro) que pode ser baixada no site https://www.python.org/ do link https://www.python.org/ftp/python/3.13.0/python-3.13.0-amd64.exe. Para digitarmos os códigos, recomendamos que seja baixado o program livre Positron no site https://posit.co/ usando o link https://github.com/posit-dev/positron/releases/download/2024.11.0-140/Positron-2024.11.0-140-Setup.exe.

Veja um print do Positron:

Positron: para códigos em Python ou R

Uma vez instalado, podemos digitar os códigos e com Ctrl Enter executamos os códigos linha por linha ou um bloco de linhas marcadas. É importante instalarmos algumas bibliotecas básicas, caso elas já não estejam instaladas. A seguir, temos um código Python para esse propósito.

pip install numpy
pip install sympy
pip install PIL
pip install jupyter
pip install matplotlib

Em seguida devemos importar as libraries que precisarmos. No script a seguir consideramos a importação de todas as libraries. A library math não precisa ser instalada, pois já vem com as distribuições do Python.

import math
import numpy
import sympy
import PIL
import jupyter
import matplotlib

1.1.1 Operações Aritméticas Básicas

Podemos usar o Python como uma calculadora, temos o seguinte programa, em que cada linha física do editor tem um comando Python específico. Podemos separar em uma mesma linha vários comandos com ponto e vírgula.

# Programa ilustrativo de operações elementares
# em Python
1 + 2 + 3     # soma dos 3 primeiros inteiros
3**10 - 1 + 8
6 / 5 + 0.5**4
6
59056
1.2625

Podemos observar que o símbolo # é usado para inserirmos comentários no código Python e o operador / faz divisão usando operadores reais. Para divisão de inteiros, podemos usar //, assim, 6 // 5 retorna \(1\) e 6 / 5 retorna 1,2. O resto da divisão por inteiro é obtido pelo operador %. Assim, 6 % 5 retorna 1. Temos também que o operador ** é a função potência, ou seja, por exemplo, \(3^{10}\) é 3**10 em Python.

O Python também pode realizar operações com números complexos, que no caso, são representados por \(a+bj\), em que \(a\) é a parte real do número e \(bj\), a parte imaginária, sendo \(j\) \(=\) \(\sqrt{-1}\). O \(j\) é representado por \(i\) nos livros de matemática e de outras áreas. O programa a seguir ilustra uma operação com números complexos dada por \((6-4i)^2\). Assim, temos

(6-4j)**2
(20-48j)

Apresentamos a seguir um script que faz uso da library math, cujo primeiro comando foi para importá-la, o penúltimo para obter o valor de \(\pi\) e o último comando calculou \(\sqrt{2}\). Também fizemos mais uma operação com números complexos.

import math
2 + 4 + 5.6
2 / 3 - 4
(3-4j)*(3+4j)
math.pi
math.sqrt(2)
11.6
-3.3333333333333335
(25+0j)
3.141592653589793
1.4142135623730951

As bibliotecas numpy e sympy são para diversos cálculos matemáticos, sendo que a última efetua cálculos simbólicos.

import sympy
import numpy
numpy.set_printoptions(legacy='1.25')
sympy.sin(sympy.pi/5)
numpy.sin(numpy.pi/5)
type(1.5 + 2.1j) # tipo do objeto

\(\displaystyle \sqrt{\frac{5}{8} - \frac{\sqrt{5}}{8}}\)

0.5877852522924731
complex

Podemos realizar uma operação matemática básica como algumas das anteriormente apresentadas ou até mesmo, mais complexa e armazenar o valor em uma variável, digamos x. Essa variável pode ser usada para outras operações matemáticas e até simbólicas, se usarmo o sympy. Veja Knuth (1984) para discussão sobre programação simbólica. O script a seguir ilustra alguns casos deste procedimento.

x = sympy.symbols('y')
z = (1+x**2)**2
sympy.simplify(z)
y = 2.3 + 6.7**2
r = y**2 + 1 / 2**3
print('r =', r, 'y = ', y, 'e', 'z =', z)

\(\displaystyle \left(y^{2} + 1\right)^{2}\)

r = 2227.0211 y =  47.19 e z = (y**2 + 1)**2

Assim, atribuímos dados às variáveis Python. O Python diferencia maiúsculas de minúsculas e nomes como X e x são diferentes. No Python as variáveis são ponteiros (pointers). Comandos como \(x = 2\) cria um objeto \(x\) e atribuí (armazena) o valor \(2\) nele em outras linguagens, mas no Python, há um objeto inteiro \(2\) e \(x\) é um ponteiro, apontando para ele. Veja as consequências disso a seguir, sendo que o comando \n realiza uma quebra de linha. Não há maiores implicações em objetos escalares como este, mas quando se trata, por exemplo, de listas, nosso próximo objeto, a questão já é bem diferente.

x = z = b = 1
b = 7
print('x is pointing to', x,
      '\nz is pointing to', z, '\nb is pointing to', b)
x is pointing to 1 
z is pointing to 1 
b is pointing to 7

Para lidarmos com funções de números complexos a library cmath deve ser importada e as funções trigonométricas de números complexos podem ser usadas com base nesta biblioteca e não na library math, que é designada para números reais (float). Veja o script ilustrativo a seguir.

import cmath
(cmath.cos(0.1 - 0.4j) + cmath.sin(0.2 + 0.6j))**2.5
(1.0143106793360148+2.4164569923716543j)

1.2 Variáveis Booleanas

Variáveis booleanas em Python recebem os valores True e False apenas. Várias operações de comparações como ==, >=, <=, & (and), | (or) e ! (negação - not) podem ser usadas para este tipo de objeto, as variáveis booleanas. Veja um simples exemplo disso. Posteriormente, voltaremos a falar destes operadores de variáveis booleanas.

x = True
x
y = False
y
x != y # ou
z =  not(y)
x == z
True
False
True
True

1.3 Strings

As strings (variáveis texto) são um importante tipo de objeto Python. Uma vez que temos um objeto definido, os métodos e funções estão disponíveis para serem usados. As strings são denotadas por str em Python. Veja alguns exemplos, em que as strings foram atribuídas ou não a objetos (variáveis).

'Esta é uma string'
mensagem = 'Universidade Federal '
type(mensagem)
mensagem
UFLA = mensagem + 'de Lavras, MG.'
UFLA
'Esta é uma string'
str
'Universidade Federal '
'Universidade Federal de Lavras, MG.'

Alguns métodos que podemos usar com as strings são ilustrados a seguir, entre muitos outros, mostrando o poder da linguagem orientada por objetos.

UFLA.capitalize()
UFLA.lower()
UFLA.upper()
UFLA
'Universidade federal de lavras, mg.'
'universidade federal de lavras, mg.'
'UNIVERSIDADE FEDERAL DE LAVRAS, MG.'
'Universidade Federal de Lavras, MG.'

Estes métodos atuam no objeto, mas, como ficou claro no exemplo anterior, não mudam o conteúdo do objeto. Podemos realizar operações com strings, como ilustrado a seguir. O método str.format possibilita formatar strings, como, por exemplo, incluir substrings nos campos marcados com {}.

UFLA * 2
nome = 'Nome: {}, Sobrenome: {}'
nome.format('Daniel', 'Furtado Ferreira')
'Universidade Federal de Lavras, MG.Universidade Federal de Lavras, MG.'
'Nome: Daniel, Sobrenome: Furtado Ferreira'

Podemos usar o Python para interagir com o usuário, solicitando a entrada de dados (strings no caso) com o comando input. Veja os exemplos a seguir.

nome = input('entre com seu primeiro nome: ')
print(nome + ' foi aprovado!')
x = int(input('entre com um valor inteiro: ')) 
  # transforma o str em inteiro: int
x # se o número de entrada não for int, resulta em erro

Se o usuário entrar com Daniel, o resultado será Daniel foi aprovado!. Esta versão de Markdown ainda não suporta interatividade com o usuário. Portanto, o comando input não foi avaliado na saída deste script. No segundo comando, se o usuário entrar com um número não inteiro, haverá uma mensagem de erro do Python. Existem opções para lidar com erros deste tipo e de outras causas também.

1.4 Listas, Tuplas, Conjuntos e Dicionários

Vamos abordar cada um destes objetos separadamente. Vamos começar pelas listas.

1.4.1 Listas

As listas, lists são os primeiros blocos de construção para lidarmos como manipulação de dados. As listas são vetores cujo primeiro elemento inicia-se no 0, mas cujos elementos de cada célula pode ser diferentes tipos mistos, desde inteiros, booleanos, reais, complexos, strings, caracteres, conjuntos, tuplas e outras listas. As listas fazem parte do quarteto list, tuple, set e dictionary. A biblioteca numpy fornece ferramentas adicionais para lidarmos com grande coleções de dados.

x = [1, 2, 3, 4]
x
y = [1, 'Estat', 3.5, 4+5j]
y
type(x)
type(y)
y[3]
[1, 2, 3, 4]
[1, 'Estat', 3.5, (4+5j)]
list
list
(4+5j)

A variável x é uma lista de inteiros com \(4\) elementos, que são indexados por \(0\), \(1\), \(2\), \(3\). Assim, \(x[1]\) aponta para o valor \(2\) e \(x[0]\) para o valor \(1\). A variável y também é uma lista com \(4\) elementos de diferentes tipo, sendo \(y[0]\) um inteiro, \(y[1]\) uma string \(y[2]\) um float e \(y[3]\), um número complexo. Para criar a lista, simplesmente utilizamos as chaves [], com cada elemento da lista separado por uma vírgula. É possível criar uma lista com elementos com valores repetidos e eles serão identificados como sendo diferentes, pois a lista respeita as ordens de entradas dos valores e preserva a ordem. Podemos verificar se um elemento pertence a lista com o comando in, como mostra o script a seguir, entre outros exemplos.

[2, 7] == [7, 2]
[5, 7] == [5, 7, 7]
x
2 in x
7 in x
y
4+5j in y
'Daniel' in y
False
False
[1, 2, 3, 4]
True
False
[1, 'Estat', 3.5, (4+5j)]
True
False

Podemos, como foi feito com as strings realizar algumas operações aritméticas com as listas, como mostra o exemplo do seguinte script.

x+y
x+[[0,1],'teste',[1,0]]
y*2
y[0] # primeiro elemento da lista
y[-1] # último elemento da lista
# numeração -1,-2,-3,-4 para o índice
# acessa as posições, 3,2,1,0, 
# respectivamente da lista y
[1, 2, 3, 4, 1, 'Estat', 3.5, (4+5j)]
[1, 2, 3, 4, [0, 1], 'teste', [1, 0]]
[1, 'Estat', 3.5, (4+5j), 1, 'Estat', 3.5, (4+5j)]
1
(4+5j)

Vejamos agora o problema dos ponteiros, por meio do exemplo do script apresentado na sequência.

x = z = b = [1,2,3]
b[1] = 7
print('x is pointing to', x,
      '\nz is pointing to', z, '\nb is pointing to', b)
# todos os objetos foram alterados e não só b
# pois eles apontam para a mesma lista [1,2,3]
x is pointing to [1, 7, 3] 
z is pointing to [1, 7, 3] 
b is pointing to [1, 7, 3]

Observamos que se x, z e b apontarem para o mesmo objeto, então se alterarmos o valor b[1] de 2 para 7, então todos os três objetos serão alterados na posição 1, que corresponde ao segundo valor da lista, pois ela se inicia na posição 0. Entretanto, se em vez de b[1] = 7 tivéssemos usado a atribuição b = [7,9], então os vetores x e z não seriam alterados, com a nova atribuição do vetor b. Nos exemplos anteriores, vimos também que os elementos de uma lista são acessados pelo seu índice que varia de 0 a n-1, sendo no seu tamanho. Assim, a lista x=[1,2,3,4] tem seus elementos x[0] igual a 1, x[1] igual a 2, x[2]igual a 3 e x[3] igual a 4. Também podemos variar o índice de -1 a -n, sendo que -1 significa a última posição da lista, ou seja, a posição n-1, -2 corresponde a posição n-2 e assim por diante até -n, que corresponde a posição 0 da lista.

As listas são objetos e como tais podemos utilizar alguns métodos associados a eles. As listas são mutáveis e dinâmicas (podemos alterar seus elementos), são ordenadas (cada elemento da lista possui uma ordem definida na sua criação) e permitem elementos repetidos. Para usarmos um método ou uma função deveremos considerar a diferença entre eles. Embora todos métodos sejam funções em Python, nem toda função é um método. As funções recebem os objetos como entradas e não os modifica e os métodos agem nos objetos. A seguir apresentamos uma relação de alguns métodos ou funções associados às listas:

  • sort(): ordena a lista em ordem crescente.

  • append(): adiciona um elemento ao final da lista.

  • extend: adiciona múltiplos elementos à lista.

  • index(): usado para encontrar o índice de um elemento na lista.

  • max(list): retorna o valor máximo de uma lista.

  • min(list): retorna o valor mínimo de uma lista.

  • list(tuple): transforma uma tuple numa lista.

  • len(list): retorna o tamanho da lista (número de elementos).

  • filter(fun,list): filtra uma lista usando uma função fun Python.

Vamos ilustrar alguns destes métodos com exemplos particulares. Vamos considerar uma lista e aplicarmos o método sort() para ordenarmos os seus valores. Neste exemplo a seguir, vamos ver a diferença de um método e de uma função, observando como o método modifica o objeto que o chamou. Neste caso, a chamada de um método é dada pelo nome da lista (objeto) seguida de um ponto e do nome do método: lista.met().

x = [7.4, 5.8, 9.3, 3.2]
x # objeto x original
x.sort()
x # objeto x modificado pelo método sort() ordenado
[7.4, 5.8, 9.3, 3.2]
[3.2, 5.8, 7.4, 9.3]

Para o método append temos o seguinte script, que acrescentou o mês de Abril ao final de uma lista com os três primeiros meses do ano.

mes = ['Janeiro', 'Fevereiro', 'Março']
mes.append('Abril')
print(mes)
['Janeiro', 'Fevereiro', 'Março', 'Abril']

O método extend é aplicado na lista x anterior e acrescenta mais dois elementos ao final da mesma.

x
x.extend([1.6, 11.6])
x
x.sort() # ordena x, pois não estava mais em ordem
x
[3.2, 5.8, 7.4, 9.3]
[3.2, 5.8, 7.4, 9.3, 1.6, 11.6]
[1.6, 3.2, 5.8, 7.4, 9.3, 11.6]

O index() é um método para encontra o índice de um elemento na lista. Se o elemento procurado não estiver na lista, o Python mostrará uma mensagem de erro.

i = x.index(7.4)
i # lembre-se que a lista começa no 0 e não no 1
3

Já as funções len, max() e min() atuam no objeto, passado como entrada da função, mas não o modificam. Veja o exemplo na lista x dos exemplos anteriores o efeito destas duas funções.

len(x)     # tamanho da lista x
len(mes)   # tamanho da lista mes
b = max(x)
a = min(x)
a
b
mes
x # x e mes não modificados
6
4
1.6
11.6
['Janeiro', 'Fevereiro', 'Março', 'Abril']
[1.6, 3.2, 5.8, 7.4, 9.3, 11.6]

Para ilustrar a uso da função filter() vamos considerar uma função que retorna True ou False para uma certa condição de interesse. Por exemplo, se quiséssemos saber quais números dos seis elementos da lista x possui resto da divisão por 2 menor que 1,5. Esse resultado é obtido com a comparação a % 2 <= 1.5, que irá retornar verdadeiro ou falso para o número representado por a. Só que devemos fazer isso para todos os elementos da lista x ou de outra lista qualquer. Devemos criar uma função para receber cada elemento da lista e verificar a condição, retornando True ou False e passar pela função filter() para realizar a iteração nos elementos da lista x ou na lista de interesse. Vamos criar um primeira função no exemplo a seguir e em seguida aplicar a função filter(). O tipo de objeto retornado desta função é filter, logo, tem de ser transformado em lista antes de imprimir.

def resto(a):
    if ((a % 2) <= 1.5):
        return True
    else:
        return False
# aplicar a função filter 
x_filtrado = filter(resto, x) 
print(list(x_filtrado))
[3.2, 7.4, 9.3]

Podemos usar a função list(objeto) para construir uma lista a partir deste objeto, como ocorreu com o objeto x_filtradodo script anterior. Então a função list() é um construtor de listas. Observe que o operador % retorna o resto da divisão por inteiro, que no caso, foi por 2. A função resto retorna verdadeiro ou falso, de acordo com a condição do resto da divisão de a por 2. Para definir uma função necessariamente usamos o comando def seguido pelo nome da função (resto) com o argumento (a). Depois vem o corpo da função, que deve ter indentação obrigatória. Não há separação com {} ou [] ou outros caracteres para separar o corpo da função. Esta separação é feita apenas com uso das indentações apropriadas. Falaremos posteriormente de função com mais detalhes.

Podemos aproveitar algumas funções prontas das listas para calcularmos algumas quantidades de interesse, como, por exemplo, a soma dos seus elementos. Para isso, poderíamos usar uma estrutura de repetição, como o for que veremos posteriormente e na medida que o loop se adianta, vamos atualizando a soma. Porém podemos usar a função sum(). Um loop for é executado como bytecode Python interpretado, enquanto a função sum() é escrita puramente na linguagem C, portanto, bem mais rápida e eficiente. Veja o código a seguir para ilustrarmos a soma de todos os valores do vetor x. Falaremos das estruturas condicionais e de repetições posteriormente.

soma = sum(x)
soma
# média
soma / len(x)
38.9
6.483333333333333

Outros métodos como o count() (conta o número de ocorrências de um dado valor), reverse() (ordena a lista em ordem reversa a ordem original), clear() (limpa todos os dados da lista), copy() (copia todos os dados da lista), insert() (insere um elemento em uma posição específica da lista) e pop() (remove um elemento em uma posição específica) como apresentado no scrip a seguir.

L = [1, 2, 3, 5, 7, 2, 4.5]
L.count(2)
L.count(2.1)
L1 = L.copy()
L.reverse()
L
L.clear()
L
L1
L1.insert(1, 3) # insere o valor 3 na posição 1
L1
L1.pop(1) # retira o elemento 3 da posição 1
L1
2
0
[4.5, 2, 7, 5, 3, 2, 1]
[]
[1, 2, 3, 5, 7, 2, 4.5]
[1, 3, 2, 3, 5, 7, 2, 4.5]
3
[1, 2, 3, 5, 7, 2, 4.5]

Podemos construir uma matriz, haja vista que Python não possui um objeto matricial, usando uma lista. Se criarmos uma lista de ncomponentes, com cada um dos componentes tendo m componentes, teremos uma matriz \(n \times m\). Vejamos no script a seguir a construção da seguinte matriz:

\[ A = \left[ \begin{array}{cc} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{array} \right]. \qquad(1.1)\]

O script correspondente a matriz definida em (Equation 1.1) é:

A = [[1,4],[2,5],[3,6]]
print('A = ',A)
A =  [[1, 4], [2, 5], [3, 6]]

Vamos apresentar também alguns detalhes extras para acessarmos os valores de uma lista. Podemos acessar o valor de uma lista x indicando a posição do elemento que queremos acessar da seguinte forma: x[i], em que i representa um valor inteiro entre 0 e len(x), digamos n. Para acessarmos um subconjunto de uma lista L de tamanho n, podemos usar o seguinte comando L[m:s] que irá acessar os elementos da posição m até a posição s-1 e não s. Isso corresponde aos elementos m+1, m+2, ..., s. Assim não devemos confundir o elemento com sua posição, pois o vetor inicia-se em 0 (elemento 1) e não em 1. Se os índices são negativos, então a lista será acessada de trás para frente, sendo que -1 corresponde a posição n-1, -2 a posição n-2 e assim sucessivamente até -n, que corresponde a posição 0. Veja alguns exemplos no script a seguir.

L = [1, 2, 3, 4, 5, 6, 7] 
L
[1, 2, 3, 4, 5, 6, 7]

Acessando posições em particulares, para impressão ou para atribuição:

L[0]
L[1] = 8
L
1
[1, 8, 3, 4, 5, 6, 7]

Acessando, posições com os índices negativos.

L[-1]  # posição n-1
L[-len(L)] # posição 0
7
1

Para blocos de elementos temos que o comando L{m:s:r] acessa os elementos nas posições m, m+1+r, m+1+2r, ... até a (s-1)-ésima posição ou até a posição mais próxima de s-1 possível. Muito cuidado deve ser tomado, pois o limite, superior não indica onde o subconjunto termina entre os índices válidos de uma lista e sim,que ela termina na posição destacada subtraída de 1.

L[1:4]
L[1:23] # passa do limite len(L)
L[0:5:2]
L[-1:-8:-1]
L[4:]
L[:6]
[8, 3, 4]
[8, 3, 4, 5, 6, 7]
[1, 3, 5]
[7, 6, 5, 4, 3, 8, 1]
[5, 6, 7]
[1, 8, 3, 4, 5, 6]

Se omitirmos os limites inferior ou superior da sequência, então a lista selecionada será iniciada no índice 0 (valor inicial) ou terminará no último índice (valor final da lista), como nos dois últimos exemplos apresentados.

1.4.2 Tuplas

As tuplas são objetos Python muito parecidos com as listas. Vários métodos e funções que se aplicam às listas também se aplicam às tuplas. Ao contrário das listas, as tuplas são objetos imutáveis, ou seja, uma vez criadas elas não podem ser modificadas. Assim, se criarmos uma tupla por t = (1,2,3) não poderemos atribuir valor, por exemplo, deste jeito t[1] = 9. Elas podem conter mais de um valor idêntico e são ordenadas, como as listas. A forma de criar a tupla em relação à lista é o uso dos parênteses no lugar dos colchetes.

t = (1, 2, 'DFF', 3)
'DFF' in t
t[2]
t[0]
t[:3]
len(t)
True
'DFF'
1
(1, 2, 'DFF')
4

Os métodos count() e index podem ser usados nas tuplas, como ilustrado a seguir. O método index() tem a seguinte sintaxe, sendo que os dois últimos argumentos são opcionais: tuple.index(element, start, end).

t.count(1) # número de ocorrência de 1
t.index('DFF') # índice da posição de 'DFF'
1
2

A tupla pode ter qualquer tipo como sendo seus elementos, incluindo uma tupla ou uma lista.

t1 = ((1,2),'r', [2,3,4])
t1
print('Componente lista da tupla ',t1[2])
print('Elemento 0 do componente lista da tupla ',t1[2][0])
t1[2].append(5)
print('Modificando o componente lista da tupla ',t1)
t2 = [1,2,3]
sum(t2)
((1, 2), 'r', [2, 3, 4])
Componente lista da tupla  [2, 3, 4]
Elemento 0 do componente lista da tupla  2
Modificando o componente lista da tupla  ((1, 2), 'r', [2, 3, 4, 5])
6

A questão é: por que devemos usar tuplas, se elas não podem ser modificadas? A resposta para isso vem do fato de que a manipulação de dados via tuplas que são imutáveis é muito mais rápida do que nas listas. Apesar da tupla apontar para a mesma identificação da memória, fomos capazes de modificar um de seus elementos, que era a lista na sua segunda posição. Isso não mudou a identificação na memória para a qual a tupla t1 apontava.

Existem muitos métodos ou funções que funcionam com as tuplas como o len(t) e o count() como ilustrado a seguir.

t = (1,1,2,3,3,3,4,5)
t.count(3)
len(t)
3
8

A função any(t) retorna True se há algum item True na tupla e retorna False, caso contrário. Neste caso, a tupla ou qualquer outro tipo apropriado poderá ter elementos 0 e 1 ou booleanos. Pode ser aplicada nas listas, conjuntos e nos dicionários.

t = (True, False, False,False,True)
any(t)
True

Podemos usar ainda as funções min(), max(), sum()e sorted(), como ilustrado a seguir. A função sorted() ordena a tupla e retorna uma lista ordenada como resultado. Veja que o método lista.sort() altera o objeto lista e não pode ser aplicado na tupla, pelo fato de a tupla ser imutável.

t = (2.3,4.5,1.2,1.1,9.7,5.3)
min(t)
max(t)
sum(t)
sorted(t)
t
1.1
9.7
24.099999999999998
[1.1, 1.2, 2.3, 4.5, 5.3, 9.7]
(2.3, 4.5, 1.2, 1.1, 9.7, 5.3)

1.4.3 Conjuntos

Os conjuntos set em Python tem uma conotação muito próxima com a definição de conjuntos da matemática. Esses são conjuntos que a ordem ou duplicação de seus elementos não mudam o conjunto. Assim, são imutáveis, não ordenados e não pode ter mais de um elemento idêntico em suas ocorrências. Podemos usar o construtor (função) set() para criar um conjunto ou usarmos as chaves{} para digitar seus elementos separados por vírgula.

phi = set()
print('Conjunto vazio: ', phi)
# precisa ser uma lista ou tupla de argumento
A = set(['A','D','B','C','E'])
A
B = {1,2,3.4,5,6,6}
'A' in A
B # repare que o elemento 
  #repetido 6 aparece 1 vez apenas
B[0]
Conjunto vazio:  set()
{'A', 'B', 'C', 'D', 'E'}
True
{1, 2, 3.4, 5, 6}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[35], line 10
      8 B # repare que o elemento 
      9   #repetido 6 aparece 1 vez apenas
---> 10 B[0]

TypeError: 'set' object is not subscriptable

Não podemos acessar um elemento de um conjunto por B[0], por exemplo, o que ocasiona um erro, como pode ser visto no resultado do script anterior. A ordem não é importante. Vejamos a comparação do conjunto A anterior com o novo conjunto C criado a seguir.

C = set(['A','B','C','D','E'])
C
A == C
{'A', 'B', 'C', 'D', 'E'}
True

Algumas operações matemáticas com conjuntos estão disponíveis em Python, como união, interseção, diferença (\(A^c \cap B\)) e diferença simétrica \(((A^c \cap B) \cup (A \cap B^c))\), como ilustrados no exemplo a seguir.

A = {1,2,3,4,5,6}
B = {4,5,7,8,9,10}
A.union(B)
A.intersection(B)
A.difference(B) # esta em A, mas não em B
B.difference(A) # está em B, mas não em A
A.symmetric_difference(B) # está só em A ou só em B
A & B # intersecção
A | B # união
A - B # diferença
B - A # diferença
A^B   # diferença simétrica
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
{4, 5}
{1, 2, 3, 6}
{7, 8, 9, 10}
{1, 2, 3, 6, 7, 8, 9, 10}
{4, 5}
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
{1, 2, 3, 6}
{7, 8, 9, 10}
{1, 2, 3, 6, 7, 8, 9, 10}

1.4.4 Dicionários

Os dicionários dictionary() são objetos mutáveis e iteráveis. Os seus elementos vem sempre aos pares, sendo o primeiro valor uma chave e o segundo elemento, o valor da chave. Tanto a chave e seu valor são objetos Python. A chave é imutável, mas seus valores associados são objetos mutáveis ou não.

D = {1: [1,2,3.4,5], 2: 3.7, 3: {1,2,3}}
D[1]
D[2]
type(D[3])
type(D[1])
[1, 2, 3.4, 5]
3.7
set
list

Temos um dicionário, com as chaves 1, 2 e 3. Para a chave 1 temos uma lista como seu valor, para a chave 2, temos um valor float e para a chave 3, criamos um objeto do tipo conjunto. A seguir, acrescentamos uma chave, nomeada 4 com um valor booleano associado. O método keys() recupera as chaves do objeto dicionário D, transforma numa lista e imprime a lista e seu primeiro elemento C[0].

D[4] = True
print(D)
C = list(D.keys())
print(C)
C[0]
{1: [1, 2, 3.4, 5], 2: 3.7, 3: {1, 2, 3}, 4: True}
[1, 2, 3, 4]
1

Do mesmo modo,podemos obter os valores das chaves facilmente, como ilustrado a seguir. Posteriormente, veremos como poderemos utilizar uma estrutura de repetição for para percorrer os elementos do dicionário e armazenar os valores em uma lista ou processá-los um a um.

D.values()
list(D.values())
D[C[0]] # acessando o valor com chave 1, C[0]
C[0] in D # verificando se chave 1 pertence a D
5 in D    # verificar se a chave 5 pertence a D
dict_values([[1, 2, 3.4, 5], 3.7, {1, 2, 3}, True])
[[1, 2, 3.4, 5], 3.7, {1, 2, 3}, True]
[1, 2, 3.4, 5]
True
False

Podemos deletar o conteúdo de uma chave, usando a função del ou usando o método pop(). E podemos atualizar o dicionário, criando novas chaves e valores, com o método update.

del D[3] # elimina a chave 3
D
D.pop(4) # elimina a chave 4
D
D.update({5: 'sou novo', 6:'eu também'})
D
{1: [1, 2, 3.4, 5], 2: 3.7, 4: True}
True
{1: [1, 2, 3.4, 5], 2: 3.7}
{1: [1, 2, 3.4, 5], 2: 3.7, 5: 'sou novo', 6: 'eu também'}

Podemos criar um objeto dicionário, criando duas tuplas, digamos c e v, com as chaves e com os valores das chaves (de mesmo tamanho). Em seguida emparelhamos os elementos com o comando zip(c, v). Finalmente, usamos a função dict para criar o dicionário dos valores emparelhados.

c = (1,2,3,4) # chaves
v = ([1,2],True,4.5,('r','s'))
y = dict(zip(c,v))
y
y.get(5,'Chave não existe') 
# get() não gera erro em chave inexistente
# mas, y[5]  geraria erro
y.get(1)
y[1]# como 1 existe, é equivalente ao get()
{1: [1, 2], 2: True, 3: 4.5, 4: ('r', 's')}
'Chave não existe'
[1, 2]
[1, 2]

Assim, tendo acesso a um valor da lista, podemos usar os métodos e funções apropriadas para lidarmos com eles. Mais detalhes destes objetos, aparecerão oportunamente, quando avançarmos em mais características da programação em Python.

1.5 Matrizes e Arranjos

As matrizes em Python, como dissemos e mostramos anteriormente, podem ser criadas pelas listas. Assim, vamos criar a seguir uma matriz \(2 \times 2\) usando list para ilustrarmos o procedimento de criação. Se a dimensão for uma só, as listas são arranjos de uma dimensão, conhecidas por vetores.

A = [[4,1],[1,1]] # matriz 2 x 2
print('A = ',A)
A[1][1]           # retorna o valor A[2,2]
A[1][1] = 2       # altera o seu valor
A
A =  [[4, 1], [1, 1]]
1
[[4, 1], [1, 2]]

Para lidarmos com funções vetoriais (arrays) ou matriciais, podemos, entre outras possibilidades usar a biblioteca (pacote) numpy. Nosso primeiro passo é importar o pacote numpy com o apelido, np, que é o mais usado, para facilitar a chamada de seus métodos e funções. Isso só pode ser feito, se já tivermos instalado o pacote numpy.

del numpy    # eliminar a última importação
import numpy as np

Em seguida, criamos uma matriz com o uso da função array(). Vamos usar nossa lista A anterior, para fazer isso.

B = np.array(A)
B
array([[4, 1],
       [1, 2]])

Podemos criar também a partir de tuplas, em vez de listas, a matriz numpy. Além disso, existem funções próprias do pacote para criarmos matrizes, como, por exemplo, a matriz de zeros \(2 \times 4\) a seguir. Também existem funções para criarmos arrays (vetores) unidimensionais, como o método arange() e o linspace(). O primeiro cria um vetor indo de n até o máximo m (inteiros) sem incluí-lo de \(1\) em \(1\), ou do mínimo n até o máximo m (excluindo o máximo) de \(r\) em \(r\): arange(n,m) ou arange(n,m,r). Já o linspace(n,m,s) inicia em n, finaliza em m, mas com passo igual a (m - n) / (s - 1).

C = np.zeros((2,4))
print(C)
np.arange(2,7) # vetor com elementos 2,3,..,6
np.arange(2,7,0.5) # de 2 até 7, de 0.5 em 0,5 (exceto o 7)
np.linspace(2,6,6)
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]]
array([2, 3, 4, 5, 6])
array([2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5])
array([2. , 2.8, 3.6, 4.4, 5.2, 6. ])

Vamos ilustrar alguns cálculos simples com vetores.

x = np.arange(2,3,0.2)
y = np.arange(3,6,0.7)
print('', x,'\n', y)
x + y # adição dos vetores
x * y # produto elementwise 
y ** x # potenciação elementwise
 [2.  2.2 2.4 2.6 2.8] 
 [3.  3.7 4.4 5.1 5.8]
array([5. , 5.9, 6.8, 7.7, 8.6])
array([ 6.  ,  8.14, 10.56, 13.26, 16.24])
array([  9.        ,  17.78458735,  35.01760371,  69.13253113,
       137.27719037])

A multiplicação de matrizes por sua vez pode ser feita com a função np.dot, que tem uma versão na forma de método também, não modificando o objeto que o acionou.

C = np.array([[2,1],[1,2]])
B
C
np.dot(B,C)
B.dot(C)
B
array([[4, 1],
       [1, 2]])
array([[2, 1],
       [1, 2]])
array([[9, 6],
       [4, 5]])
array([[9, 6],
       [4, 5]])
array([[4, 1],
       [1, 2]])

As inversas podem ser obtidas com a função np.linalg.inv() e as inversas generalizadas de Moore-Penrose pelo método numpy.linalg.pinv(). O determinante pode ser obtido por np.linalg.det() e os autovalores e autovetores (decomposição espectral) por np.linalg.eig(). É importante observar que os autovalores do np.linalg.eig() não necessariamente estará ordenado do maior para o menor, como é convencionalmente adotado em diferentes outros programas. Este método é bem geral e pode ser usado em matrizes reais ou complexas quadradas. Alternativamente, para matrizes simétricas o numpy possui o método np.linalg.eigh(), que retorna os autovalores em ordem crescente. Veja o exemplo a seguir.

np.linalg.inv(B)
np.linalg.pinv(B) # igual a inversa (posto completo)
np.linalg.det(B)
L, P = np.linalg.eig(B)
print('Autovalores: ',L,'\nAutovetores: ',P)
L1, P1 = np.linalg.eigh(B)
print('Autovalores: ',L1,'\nAutovetores: ',P1)
array([[ 0.28571429, -0.14285714],
       [-0.14285714,  0.57142857]])
array([[ 0.28571429, -0.14285714],
       [-0.14285714,  0.57142857]])
6.999999999999999
Autovalores:  [4.41421356 1.58578644] 
Autovetores:  [[ 0.92387953 -0.38268343]
 [ 0.38268343  0.92387953]]
Autovalores:  [1.58578644 4.41421356] 
Autovetores:  [[ 0.38268343 -0.92387953]
 [-0.92387953 -0.38268343]]

Também podemos usar a biblioteca scipy com os métodos sp.linalg.eig() e sp.linalg.eigh(), que fazem exatamente como os seus similares da biblioteca numpy. Para isto devemos importar, depois de instalada, a biblioteca usando import scipy as sp.

Para matrizes n x m, podemos usar uma decomposição matricial muito útil para a estatística, em métodos como componentes principais, AMMI, biplot, entre outros. Esse método é chamado de decomposição do valor singular. Nele obtemos a decomposição de uma matriz \(\mathbf{A}\) (m x n) da seguinte forma: \(\mathbf{A}\) \(=\) \(\mathbf{U} \mathbf{\Lambda} \mathbf{V}^\top\), em que o método np.linalg.svd() retorna \(\mathbf{U}\) uma matriz \(m \times k\) ortonormal por colunas, \(\mathbf{V}\) uma matriz \(n \times k\) ortonormal por coluna e o vetor correspondente à diagonal de \(\mathbf{\Lambda}\), que é uma matriz diagonal \(k \times k\) de elementos reais positivos, se usarmos a opção full_matrices=False. As matrizes \(\mathbf{U}\) e \(\mathbf{V}\) são as matrizes dos vetores singulares e a matriz \(\mathbf{\Lambda}\) é a matriz dos valores singulares, sendo \(k\) o posto de \(\mathbf{A}\) que é \(k=\min(n,m)\). O script a seguir ilustra a obtenção da decomposição singular de uma matriz \(3 \times 2\).

A = np.array([[1,4],[2,7],[9, 3]])
U,L,Vt = np.linalg.svd(A, full_matrices=False)
print('Vetores singulares à esquerda: ',U,
'\nVetor dos valores singulares: ',L,
'\nVetores singulares à direita (transposto): ',Vt)
len(L) # posto de A
np.diag(L) # constrói a matriz Lambda
Vetores singulares à esquerda:  [[-0.30248631 -0.39963983]
 [-0.54614812 -0.67137377]
 [-0.78116852  0.62413562]] 
Vetor dos valores singulares:  [11.19813546  5.88232625] 
Vetores singulares à direita (transposto):  [[-0.75238412 -0.65872463]
 [ 0.65872463 -0.75238412]]
2
array([[11.19813546,  0.        ],
       [ 0.        ,  5.88232625]])

Para verificarmos que a decomposição realmente é adequada, temos o seguinte script:

np.dot(np.dot(U,np.diag(L)),Vt)
U.dot(np.diag(L)).dot(Vt) # alternativo
U @ np.diag(L) @ Vt       # alternativo
array([[1., 4.],
       [2., 7.],
       [9., 3.]])
array([[1., 4.],
       [2., 7.],
       [9., 3.]])
array([[1., 4.],
       [2., 7.],
       [9., 3.]])

Muitas outras funções existem no pacote numpy para lidarmos ou operarmos matrizes. Se necessitarmos de alguma outra função para alguma operação matricial, vamos apresentá-la nestas ocasiões.

1.6 Arquivos de Dados

Usaremos pouco esta estrutura de dados neste material, embora vamos apresentar a biblioteca pandas para lidarmos com os DataFrames. Iremos apresentar a leitura de um arquivo particular com duas variáveis X1 e X2, gravado como arquivo texto separado por espaço entre as colunas (variáveis). O caminho onde este arquivo se encontra em meu computador é: g:/Meu Drive/daniel/Cursos/Estatistica computacional/Apostila/ e seu nome é dados.txt. Devemos inicialmente instalar a biblioteca pandas com o comando: pip install pandas. Posteriormente, carregamos o pacote pandas, como apresentado no script a seguir. Para mudar o diretório, precisamos importar o pacote os e usar os.getcwd() para obter o diretório de trabalho atual e para alterá-lo os.chdir('[path]').

import pandas as pd
import os
apath = os.getcwd() # caminho do projeto
os.chdir('g:/Meu Drive/daniel/Cursos/Estatistica computacional/Apostila/')
os.getcwd()
'g:\\Meu Drive\\daniel\\Cursos\\Estatistica computacional\\Apostila'

Para lermos um arquivo deste diretório em um objeto DataFrame podemos usar a função pd.read_csv('dados.txt',r'\s+'), cujo símbolo r'\s+' significa que o arquivo é separado por espaços, podendo variar o número de espaços entre colunas de registro (linha) para registro, ou seja, se o número de espaços não está padronizado entre as colunas para as diferentes linhas do arquivo de dados.

dados = pd.read_csv('dados.txt',sep=r'\s+')
dados
X1 X2
0 13.4 14
1 14.6 15
2 13.5 19
3 15.0 23
4 14.6 17
5 14.0 20
6 16.4 21
7 14.8 16
8 15.2 27
9 15.5 34
10 15.2 26
11 16.9 28
12 14.8 24
13 16.2 26
14 14.7 23
15 14.7 9
16 16.5 18
17 15.4 28
18 15.1 17
19 14.2 14

Os DataFrames são estruturas de dados tabular, sendo que cada coluna possui constitui de uma sequência de valores do mesmo tipo (booleano, float, strings, etc.), em que as diferentes colunas podem ser e potencialmente são de diferentes tipos. Os DataFrames possuem uma coluna adicional chamada index que no exemplo anterior, do objeto dados, variou de 0 a 19, pois nosso DataFrame possui 20 linhas e duas variáveis X1 e X2, que no arquivo dados.txt estavam identificadas na primeira linha física do arquivo que foi lido pelo método read_csv(). O index mapeia as linhas do DataFrame com os labels mencionados.

Vamos mostrar como construir um DataFrame diretamente a partir de um objeto dict para o construtor DataFrame() do pandas. Vamos criar um DataFrame de um delineamento inteiramente casualizado, com 2tratamentos e 3 repetições de cada um, com os respectivas produtividades avaliadas nas 6 parcelas experimentais.

dic = {'rep': [1,2,3,1,2,3],
        'trat': [1,1,1,2,2,2],
        'prod': [3.4,2.3,5.6,5.7,6.3,7.1]}
arqd = pd.DataFrame(dic)
arqd
rep trat prod
0 1 1 3.4
1 2 1 2.3
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1

Podemos acrescentar uma nova coluna em nosso DataFrame, ou eliminarmos uma já existente, criado um novo DataFrame para receber o resultado, como mostrado a seguir, com a opção columns, para qualquer ordem das chaves (nomes das colunas).

arqd
arqd1 = pd.DataFrame(arqd,columns=['trat','prod'])
arqd1 # selecionando 2 variáveis no DataFrame
arqd['alt'] = [1.5,1.3,1.4,1.2,1.1,1.6] #criando variável altura
arqd
rep trat prod
0 1 1 3.4
1 2 1 2.3
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1
trat prod
0 1 3.4
1 1 2.3
2 1 5.6
3 2 5.7
4 2 6.3
5 2 7.1
rep trat prod alt
0 1 1 3.4 1.5
1 2 1 2.3 1.3
2 3 1 5.6 1.4
3 1 2 5.7 1.2
4 2 2 6.3 1.1
5 3 2 7.1 1.6

Para acessarmos as chaves (colunas), os índices e os valores do DataFrame podemos usar os seguintes códigos ilustrativos. Observamos que a instrução arqd.prod para acessar a coluna prod, resulta em erro, pois prod é uma palavra reservada (produto). Devemos usar a alternativa anterior para esta chave.

arqd.columns
arqd.index
arqd.values
arqd['prod'][0] # produção do índice 0
arqd['prod'] # toda a coluna de produção
arqd.prod # cuidado, pois prod é palavra reservada: erro
arqd.rep
Index(['rep', 'trat', 'prod', 'alt'], dtype='object')
RangeIndex(start=0, stop=6, step=1)
array([[1. , 1. , 3.4, 1.5],
       [2. , 1. , 2.3, 1.3],
       [3. , 1. , 5.6, 1.4],
       [1. , 2. , 5.7, 1.2],
       [2. , 2. , 6.3, 1.1],
       [3. , 2. , 7.1, 1.6]])
3.4
0    3.4
1    2.3
2    5.6
3    5.7
4    6.3
5    7.1
Name: prod, dtype: float64
<bound method DataFrame.prod of    rep  trat  prod  alt
0    1     1   3.4  1.5
1    2     1   2.3  1.3
2    3     1   5.6  1.4
3    1     2   5.7  1.2
4    2     2   6.3  1.1
5    3     2   7.1  1.6>
0    1
1    2
2    3
3    1
4    2
5    3
Name: rep, dtype: int64

Para extrairmos uma linha inteira usamos o método loc do DataFrame associado ao índice da linha (registro), como ilustrado no scripta seguir.

L  = arqd.loc[1] # segunda linha do DataFrame
print(L)
L2 = arqd.loc[[0,5]] # as linhas 1 e 6 de arqd
L2                   # com os índices 0 e 5
rep     2.0
trat    1.0
prod    2.3
alt     1.3
Name: 1, dtype: float64
rep trat prod alt
0 1 1 3.4 1.5
5 3 2 7.1 1.6

Para selecionar um bloco de registros (linhas) indo de inicio ao fim (excluindo o limite final) usamos arqd[inicio:fim]. Como ilustrado a seguir, onde extraímos do índice 0 até o índice 3, ou seja, as três primeiras linhas com os índices 0, 1 e 2 do arqd.

arqd[0:3]
rep trat prod alt
0 1 1 3.4 1.5
1 2 1 2.3 1.3
2 3 1 5.6 1.4

Valores perdidos podem fazer parte do DataFrame e neste caso, eles assumem o valor NaN, do inglês not a number. Podemos deletar uma coluna com o comando del, da seguinte forma.

del arqd['alt']
arqd
rep trat prod
0 1 1 3.4
1 2 1 2.3
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1

Podemos filtrar impondo condições específicas ao DataFrame. Por exemplo, se estivermos interessado no DataFrame resultante dos elementos em que a produção é maior ou igual a 5, teremos o seguinte resultado. O resultado filtrado mostra os registros nas mesmas posições originais e o seu DataFrame original arqd permanece inalterado.

arqd[arqd['prod'] >= 5.0]
arqd
rep trat prod
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1
rep trat prod
0 1 1 3.4
1 2 1 2.3
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1

Para gravarmos um DataFrame podemos escolher o diretório (pasta) e o nome do arquivo e gravarmos em um arquivo csv (arquivo separado por vírgula) com o comando arqd.to_csv('nome.csv,index=False,header=True) para não salvar o índice e salvar o cabeçalho. Vamos recuperar em nosso código, o path original com o comando os.chdir(apath), em que apath foi obtido quando iniciamos o assunto sobre DataFrame e refere-se ao diretório deste projeto. Escolhemos o nome dic.csv para o arquivo. Podemos ler o arquivo novamente, conforme mostramos nos primeiros passos da abordagem dos DataFrames. Depois de gravado, repetimos sua leitura e o colocamos no objeto dic, em que devemos atentar para o separador de colunas, que neste caso é a vírgula.

os.chdir(apath)
arqd.to_csv('dic.csv',index=False,header=True)
dic = pd.read_csv('dic.csv',sep=r',')
dic
rep trat prod
0 1 1 3.4
1 2 1 2.3
2 3 1 5.6
3 1 2 5.7
4 2 2 6.3
5 3 2 7.1

Em futuras edições, mostraremos mais detalhes dos DataFrames. Nos capítulos posteriores, caso venhamos a precisar de um DataFrame e de algumas de suas propriedades, então iremos adicionar os conteúdos necessários nesta ocasião. Falaremos agora das estruturas condicionais e as estruturas de repetições.

1.7 Estruturas de Controle de Programação

O Python é um ambiente de programação em que programas contêm os módulos, os módulos contém instruções, as instruções contém comandos e as expressões criam e processam os objetos. Estas instruções são as atribuições tipo a=b, a chamada de métodos e funções como print(a), if/elif/else para seleção de ações, o for/else para realizar iterações, o while/else para loopsem geral, break e continuepara controle de loops, def para definições de funções, yeld para gerador de funções, entre outros. As instruções são organizadas em expressões em grupos de comandos, que diferentemente de outras linguagens são organizados pelas indentações (recuo do parágrafo). Assim, o corpo ou grupo de comando de uma instrução específica, ficará reunida se elas tiverem indentadas em relação a instrução principal e com o mesmo nível de indentação. Em outra linguagens os grupos de comandos são reunidos pelas chaves: {grupos de comandos}. O fim de uma linha termina a instrução, que também pode ser finalizada por um ponto e vírgula. Uma instrução pode continuar em uma nova linha se a linha terminar com o \ ou com o uso dos parênteses.

O nome das variáveis em Python em Python devem iniciar com underscore ou letra, seguido por números ou letras ou underscore. O python diferencia as maiúsculas das minúsculas, assim Y é diferente de y. Os nomes devem evitar as palavras reservadas do Python, como False, None, True, class, and, if, elif, yield, while, break, global, nor, try, return, break,in, etc. Por convenção, as classes em Python começam com maiúsculo e os módulos e nomes de variáveis por minúsculo.

As estruturas condicionais, if/elif/else são estruturas em Python para selecionar ações. Esta instrução pode conter outras instruções do mesmo tipo ou diferentes instruções em seus grupos de comando. O formato geral é dado por

if condição1:
  instruções1
elif condição2: # opcional elifs
  instruções2
else:           # opcional else
    instruções3

A instrução elif significa else if e é opcional. Se a condição1 for verificada, é executado o bloco denominado instruções1, que estão indentados em relação ao if. Caso a condição seja falsa, é testada a condição2 e se ela for verdadeira, são executados as instruções denotadas por instruções2, que pode ser uma simples instrução ou várias instruções, indentadas em relação ao elif. Finalmente, se a condição2 for falsa são executadas as instruções3. Cada linha das instruções if, elif ou else são seguidas por um ponto e vírgula. Veja o exemplo simples a seguir.

x = 5
if x > 6:
  print('Recebe mais que 6 salários.')
  print('Você está entre os 20% mais ricos!')
else:
  print('Você recebe 6 salários ou menos.')
  print('Você representa 80% da população!')
Você recebe 6 salários ou menos.
Você representa 80% da população!

Para um modelo probabilístico temos o seguinte modelo para a função de distribuição:

\[\begin{align*} F_X(x) =& \left\{ \begin{array}{ll} 0 & \textrm{se } x < 0\\ x^2 & \textrm{se } 0\le x \le 1\\ 1 & \textrm{se } x > 1. \end{array} \right. \end{align*}\]

Para este modelo, temos o seguinte script, no qual decidimos qual parte do programa rodar, conforme os valores de x são atribuídos.

x = 0.8
if x < 0:
  F = 0
elif 0 <= x <= 1:
  F = x**2
else:
  F = 1
F
0.6400000000000001

As estruturas de repetição do Python são o for e o while/else. Existe ainda um terceiro tipo de procedimento em Python para realizarmos iterações. A estrutura geral de um código Python para o for è apresentado no scripta seguir.

for i in:
  instruçoes1
else:            # opcional instrução else
  instruções2

Os objetos do comando for são os objetos iteradores ou iteráveis, que são aqueles que contém um número contáveis de valores. As listas, tuplas, dicionários e conjuntos ão todos objetos iteráveis. Vamos ilustrar com um simples exemplo a seguir, para calcularmos a soma, o produtório e a média de uma lista de valores.

x = [2.3, 4.1, 1.5, 2.3, 4.7]
soma = 0
prod = 1
n = len(x)
for y in x:
  soma = soma  + y
  prod = prod * y
media = soma / n
print('A soma é: ',soma)
print('O produtório é: ',prod)
print('A média é: ',media)
A soma é:  14.899999999999999
O produtório é:  152.90744999999995
A média é:  2.9799999999999995

Para realizarmos iterações em um dicionário, temos o seguinte exemplo:

D = {1: 1.3, 2: 3.1, 3: 1.7}
for i in D: # iterar nas chaves
  print(i, '=', D[i])
print('Agora iterando nas chaves e valores:')  
for (i, valor) in D.items():
  print(i, '=', valor)  # iterar em chave e valor
1 = 1.3
2 = 3.1
3 = 1.7
Agora iterando nas chaves e valores:
1 = 1.3
2 = 3.1
3 = 1.7

Finalmente, um exemplo em um conjunto:

A = {1,2,3,4,5}
for i in A:
  print('elemento: ',i)
elemento:  1
elemento:  2
elemento:  3
elemento:  4
elemento:  5

Podemos criar um sequência de valores com o comando range(n) que vai de 0 a 6. Assim, também podemos usar o for nessa sequência, como ilustrado no exemplo a seguir.

x = [2.3, 4.1, 1.5, 2.3, 4.7]
soma = 0
n = len(x)
for i in range(n):
  soma = soma  + x[i]
media = soma / n
print('A soma é: ',soma)
print('A média é: ',media)
A soma é:  14.899999999999999
A média é:  2.9799999999999995

O while é uma outra estrutura de repetição, em que o bloco de comandos indentados irão ser executados até que uma condição seja satisfeita. A estrutura geral é dada a seguir.

while condição: 
  instruçoes1
else:            # opcional instrução else
  instruções2

O exemplo a seguir, ilustra o cálculo do total e da média de uma lista.

x = [2.3, 4.1, 1.5, 2.3, 4.7]
soma = 0
n = len(x)
i = 0
while i < n:
  soma = soma  + x[i]
  i = i + 1
media = soma / n
print('A soma é: ',soma)
print('A média é: ',media)
A soma é:  14.899999999999999
A média é:  2.9799999999999995

Podemos usar os comandos break e continue dentro do while (ou do for). O break é usado após uma segunda condição ser verificada no bloco de comandos do interior da estrutura de repetição e pula a execução do programa para a primeira linha de instrução após o bloco do loop, ou seja, encerra o loop. O continue executa a primeira linha testando a condição primária do while ou tomando o próximo valor do iterador no for, ou seja, vai para o início do loop. Veja o exemplo a seguir.

y = 35 # experimente outro número inteiro > 1
x = y // 2
while x > 1:
  if y % x == 0:
    print(y, 'tem fator ', x)
    break
  x = x - 1
else:
  print(y,' é primo')
35 tem fator  7

Podemos utilizar a função filter, como já ilustramos anteriormente neste material, para realizarmos iterações em nosso código. Não daremos mais detalhes disso, por enquanto.

1.8 Funções

As funções em todas as linguagens é uma poderosa ferramenta de programação, que nos permite quebrar um grande problema em pequenas tarefas (as funções), facilitando assim a resolução do problema como um todo. Dizemos que é a estratégia de dividir para conquistar. As funções em geral recebem um objeto e o processa de acordo com as regras definidas em seu bloco de comando. Desta forma a linguagem ganha grande poder, conveniência e elegância. O aprendizado em escrever funções úteis é uma das muitas maneiras de fazer com que o uso do Python seja confortável e produtivo. A sintaxe geral de uma função é dada por:

def nome(arg1, arg2,...,argn):
  instruções

As instruções significam um bloco de comandos (indentados) e podem ou não ter o comando de return objeto, que pode acontecer em qualquer parte do bloco de comandos. A função pode não ter este comando de return, se ela modificar um arquivo apenas gravando um novo resultado ou se imprimir uma mensagem quando chamada. Os argumentos ou parâmetros são passado para a função e a sua chamada deve obedecer estritamente a ordem em que eles aparecem, a menos que a chamada seja com chave, ou seja, do tipo `arg1 = 2.3, por exemplo. Neste caso, os argumentos podem ser colocados em qualquer ordem. Os argumentos de uma função podem conter valores default, ou seja, na declaração do nome da função podemos ter algo do tipo: def nome(x, theta = 0.5). O argumento theta=0.5 pode ser omitido na chamada da função, que será atribuído seu valor 0,5 padrão.

Vamos apresentar uma função simples para testar a hipótese \(H_0: \mu=\mu_0\) a partir de uma amostra simples de uma distribuição normal. Dois argumentos serão utilizados: o vetor (lista) de dados \(x\) de tamanho \(n\) e o valor real hipotético \(\mu_0\). A função calculará o valor da estatística \(t_c\) do teste por:

\[\begin{align}\label{ecp01:eq:tstudent} t_c = & \dfrac{\bar{X}-\mu_0}{\frac{S}{\sqrt{n}}}. \end{align}\]

A função resultante, em Python, é apresentada a seguir. Neste exemplo uma amostra de tamanho \(n = 8\) foi utilizada para obter o valor da estatística para testar a hipótese \(H_0: \mu=3,0\) (se a amostra era proveniente do povo na’vu). Podemos observar que o resultado final da função é igual ao do último comando executado, ou seja o valor da estatística e do valor-\(p\), por meio de um objeto do tipo dicionário. Esta função utiliza no seu escopo três funções do Python (funções básicas do numpy), ainda não apresentadas. As duas primeiras, var() e mean() retornam a variância e a média do vetor utilizado como argumento, respectivamente, e a terceira, pt(), retorna a probabilidade acumulada da distribuição \(t\) de Student para o primeiro argumento da função com \(\nu\) graus de liberdade, que é o seu segundo argumento.

import scipy as sp # para calcular probabilidade da t
def t_test(x, mu0):
  n = len(x)
  s2 = np.var(x,ddof=1) # ddof=1, divisor n-1 para s2
  xb = np.mean(x)
  t = {'tc':0,'p.val':0}
  t['tc'] = (xb-mu0) / (s2 / n)**0.5
  t['p.val'] = 2*(1-sp.stats.t.cdf(abs(t['tc']),n-1))
  return t
y = [1.76,1.81,1.74,1.71,1.79,1.75]
t = t_test(y, 3.0) # altura de avatares
print('tc = ',t['tc'])
print('p.val = ',t['p.val'])
tc =  -84.89699641330068
p.val =  4.297311617662558e-09

Podemos reescreve esta função para colocarmos um valor default para o argumento mu0. Se escolhêssemos o valor 0 e em alguma chamada da função, esse argumento fosse omitido, seria feito o teste da hipótese \(H_0: \mu=0\) por padrão.

def t_test(x, mu0 = 0):
  n = len(x)
  s2 = np.var(x,ddof=1) # ddof=1, divisor n-1 para s2
  xb = np.mean(x)
  t = {'tc':0,'p.val':0,'S2': s2, 'xbar': xb}
  t['tc'] = (xb-mu0) / (s2 / n)**0.5
  t['p.val'] = 2*(1-sp.stats.t.cdf(abs(t['tc']),n-1))
  return t
y = [1.76,1.81,1.74,1.71,1.79,1.75]
t = t_test(y) # teste H0: mu0=0
print('tc = ',t['tc'])
print('p.val = ',t['p.val'])
print(list(t.items())[2:4])
tc =  120.49896265113645
p.val =  7.465636997494585e-10
[('S2', 0.001280000000000002), ('xbar', 1.76)]

Vamos realizar um test para a correlação em populações normais bivariadas. Assim, dado o par de variáveis vetoriais x e y tomados em n indivíduos, temos a hipótese nula

\[\begin{align*} H_0:&\, \rho=0 \end{align*}\]

e a estatística do teste sob \(H_0\) dada por

\[\begin{align*} t_c =& \dfrac{r\sqrt{n-2}}{\sqrt{1-r^2}}, \end{align*}\]

em que \(r\) é o coeficiente de correlação amostral entre \(X\) e \(Y\); e \(n\) é o tamanho da amostra. Sob \(H_0\), essa estatística segue a distribuição \(t\) de Student com \(\nu=n-2\) graus de liberdade.

A função deve receber os vetores \(\mathbf{x}\) e \(\mathbf{y}\) e retornar o resultado do teste: estatística e valor-\(p\). Pode-se utilizar a função cor do Python scipy para obter a correlação entre \(\mathbf{x}\) e \(\mathbf{y}\).

def cor_test(x, y):
  n = len(x)
  if n != len(y):
    print('Listas devem ter o mesmo tamanho!')
    return
  r = np.corrcoef(x,y)[0,1]
  t = {'tc':0,'p.val':0,'r': r}
  t['tc'] = r * (n-2)**0.5 / (1 - r**2)**0.5
  t['p.val'] =2*(1-sp.stats.t.cdf(abs(t['tc']),n-2))
  return t
x = [1, 2, 3.1, 4.2]
y = [2.1, 3.9, 6.1, 8.3]
t = cor_test(x, y) 
print('tc = ',t['tc'])
print('p.val = ',t['p.val'])
print('r = ',t['r'])
tc =  58.469836352468235
p.val =  0.0002923787083537466
r =  0.9997076212916461

Finalmente, vamos obter uma função para obtermos potências reais de matrizes quadradas simétricas positivas definidas. Para isso vamos escrever uma função para obter potências reais de uma matriz \(\mathbf{A}\) simétrica e positiva definida:

\[\begin{align*} \mathbf{A}=& \mathbf{P}\mathbf{\Lambda} \mathbf{P}^\top, \end{align*}\]

sendo a potência de ordem \(\alpha\in \mathbb{R}\) dada por

\[\begin{align*} \mathbf{A}^\alpha=& \mathbf{P} \mathbf{\Lambda}^\alpha \mathbf{P}^\top. \end{align*}\]

Deve receber \(\mathbf{A}\) e retornar \(\mathbf{A}^\alpha\). O script a seguir ilustra uma função para obtermos estas potências matriciais. Observe que não é uma potência elemento a elemento. Também devemos observar que não é importante que os autovalores estejam ordenados. Mas é óbvio que os autovetores associados a cada autovalor deve estar corretamente associado e preservado e isso é feito pela biblioteca numpy por meio da função eig().

def mat_power(A, alpha = 0.5):
  e_val, e_vec = np.linalg.eig(A)
  if  any(e_val) < 0:
    print('Matriz não é positiva definida!')
    return
  Ap = e_vec.dot(np.diag(e_val**alpha)).dot(np.transpose(e_vec))
  return Ap
A = [[4,1],[1,2]]
print('A = ',A)  
mat_power(A) # raiz quadrada
A3 = mat_power(A, 1/3) # raiz cúbica
A3
A3.dot(A3).dot(A3) # verificando
A =  [[4, 1], [1, 2]]
array([[1.97773553, 0.29759397],
       [0.29759397, 1.38254759]])
array([[1.57094963, 0.16768037],
       [0.16768037, 1.23558888]])
array([[4., 1.],
       [1., 2.]])

1.9 Estatística Computacional

Os métodos de computação intensiva têm desempenhado um papel cada vez mais importante para resolver problemas de diferentes áreas da ciência. Vamos apresentar algoritmos para gerar realizações de variáveis aleatórias de diversas distribuições de probabilidade, para realizar operações matriciais, para realizar inferências utilizando métodos de permutação e bootstrap, etc. Assim, buscamos realizar uma divisão deste material em uma seção básica e em outra aplicada. As técnicas computacionais são denominadas de estatística computacional se forem usadas para realizarmos inferências, para gerarmos realizações de variáveis aleatórias ou para compararmos métodos e técnicas estatísticas.

Vamos explorar métodos de geração de realizações de variáveis aleatórias de diversos modelos probabilísticos, para manipularmos matrizes, para obtermos quadraturas de funções de distribuição de diversos modelos probabilísticos e de funções especiais na estatística e finalmente vamos apresentar os métodos de computação intensiva para realizarmos inferências em diferentes situações reais. Temos a intenção de criar algoritmos em linguagem Python e posteriormente, quando existirem, apresentar os comandos para acessarmos os mesmos algoritmos já implementados.

Vamos apresentar os métodos de bootstrap e Monte Carlo, os testes de permutação e o procedimento jackknife para realizarmos inferências nas mais diferentes situações reais. Assim, este curso tem basicamente duas intenções: possibilitar ao aluno realizar suas próprias simulações e permitir que realizem suas inferências de interesse em situações em que seria altamente complexo o uso da inferência clássica.

Seja na inferência frequentista ou na inferência Bayesiana, os métodos de simulação de números aleatórios de diferentes modelos probabilísticos assumem grande importância. Para utilizarmos de uma forma mais eficiente a estatística computacional, um conhecimento mínimo de simulação de realizações de variáveis aleatórias é uma necessidade que não deve ser ignorada. Vamos dar grande ênfase a este assunto, sem descuidar dos demais. Apresentaremos neste material diversos algoritmos desenvolvidos e adaptados para a linguagem Python.

Simular é a arte de construir modelos segundo Naylor et al. (1971), com o objetivo de imitar o funcionamento de um sistema real, para averiguarmos o que aconteceria se fossem feitas alterações no seu funcionamento (Dachs (1988)). Este tipo de procedimento pode ter um custo baixo, evitar prejuízos por não utilizarmos procedimentos inadequados e otimizar a decisão e o funcionamento do sistema real.

Precauções contra erros devem ser tomadas quando realizamos algum tipo de simulação. Podemos enumerar:

  1. escolha inadequada das distribuições;

  2. simplificação inadequada da realidade; e

  3. erros de implementação.

Devemos fazer o sistema simulado operar nas condições do sistema real e verificar por meio de alguns testes se os resultados estão de acordo com o que se observa no sistema real. A este processo denominamos de validação. A simulação é uma técnica que usamos para a solução de problemas. Se a solução alcançada for mais rápida, com eficiência igual ou superior, de menor custo e de fácil interpretação em relação a outro método qualquer, o uso de simulação é justificável.

1.10 Exercícios

  1. Criar no Python os vetores \(\mathbf{a}^\top=[4, 2, 1, 5]\) e \(\mathbf{b}^\top=[6, 3, 8, 9]\) e concatená-los formando um único vetor. Obter o vetor \(\mathbf{c}=2\mathbf{a} - \mathbf{b}\) e o vetor \(\mathbf{d}\) \(=\) \(\mathbf{b}^\top \mathbf{a}\). Criar uma sequência cujo valor inicial é igual a \(2\) e o valor final é \(30\) e cujo passo é igual a \(2\). Replicar cada valor da sequência \(4\) vezes de duas formas diferentes (valores replicados ficam agregados e a sequência toda se replica sem que os valores iguais fiquem agregados).

  2. Selecionar o subvetor de \(\mathbf{x}^\top=[4, 3, 5, 7, 9, 10]\) cujos elementos são menores ou iguais a \(7\).

  3. Criar a matriz

    \[\begin{align*} \mathbf{A}= \left[\begin{array}{cc} 10 & 1 \\ 1 & 2 \end{array} \right] \end{align*}\]

    e determinar os autovalores e a decomposição espectral de \(\mathbf{A}\).

  4. Construir uma função para verificar quantos elementos de um vetor de dimensão \(n\) são menores ou iguais a uma constante \(k\), real. Utilize as estruturas de repetições for e while para realizar tal tarefa (cada uma destas estruturas deverá ser implementada em uma diferente função). Existe algum procedimento mais eficiente para gerarmos tal função sem utilizar estruturas de repetições? Se sim, implementá-lo.

  5. Implementar uma função R para realizar o teste \(t\) de Student para duas amostras independentes. Considerar os casos de variâncias heterogêneas e homogêneas. Utilizar uma estrutura condicional para aplicar o teste apropriado, caso as variâncias sejam heterogêneas ou homogêneas. A decisão deve ser baseada em um teste de homogeneidade de variâncias. Para realizar tal tarefa implementar uma função específica assumindo normalidade das amostras aleatórias.

  6. Criar uma função para obter a inversa de Moore-Penrose de uma matriz qualquer \(n\) \(\times\) \(m\), baseado na decomposição do valor singular, função svd do np.linalg.svd. Seja para isso uma matriz \(\mathbf{A}\), cuja decomposição do valor singular é \(\mathbf{A}\) \(=\) \(\mathbf{U}\mathbf{D}\mathbf{V}^\top\), em que \(\mathbf{D}\) é a matriz diagonal dos valores singulares e \(\mathbf{U}\) e \(\mathbf{V}\) são os vetores singulares correspondentes. A inversa de Moore-Penrose de \(\mathbf{A}\) é definida por \(\mathbf{A}^+\) \(=\) \(\mathbf{V}\mathbf{D}^{-1}\mathbf{U}^\top\).