domingo, 22 de dezembro de 2013

Barras paralelas

Numa das últimas aulas estivemos a preparar o último teste. Um dos exemplos procurava passar por alguns dos aspectos essenciais: dicionários e ficheiros. O problema consistia em escrever um programa que realizasse quatro tarefas:

1- simular o lançamento de dois dados guardando o resultado da sua soma numa lista,

2- guardar os resultados num ficheiro, com um resultado por linha;

3- construir um dicionário de frequências em que as chaves fossem os números e os valores o número de vezes que ocorreram;

5- apresentar um gráfico de barras com o histograma das frequências. O gráfico seria feito com a ajuda do módulo turtle.

Os três primeiros sub-problemas eram revisitações de algo que já tínhamos feito. A nossa ideia era consolidar os conceitos e aproveitar para mais um exemplo de decomposição de problemas em sub-problemas e das quesdtões que se levantam.

Cada um deles podia ser resolvido de modo simples. Comecemos pelo problema da geração.
import random

def gera_1(n):
    lista = list()
    for i in range(n):
        lista.append(random.randint(1,6)+random.randint(1,6))
    return lista

def gera_2(n):
    return [random.randint(1,6) + random.randint(1,6) for i in range(n)]

def dado():
    return random.randint(1,6)

def gera_3(n):
    return [dado()+dado() for i in range(n)]
A primeira solução é a mais básica. A outras duas usam listas por compreensão, sendo que a última abstrai o problema de lançar o dado. Guardar os dados gerados num ficheiro também é trivial.
def guarda(ficheiro,dados):
    with open(ficheiro,'w') as f_out:
        for dado in dados:
            f_out.write(str(dado)+'\n')
Aqui usámos o gestor de contexto with. Finalmente ir buscar os dados ao ficheiro e construir um dicionário de frequências.
def freq(ficheiro):
    with open(ficheiro) as f_in:
        dados_str = f_in.read().split()
        dados_num = [eval(dado) for dado in dados_str]
        # dicio frequências
        dicio = dict()
        for dado in dados_num:
            dicio[dado] = dicio.get(dado,0) + 1
    return dicio
Estas três soluções têm que ser integradas num único programa. Só precisamos ter em atenção a interface.
def main(n, ficheiro):
    # gerar
    dados = gera_2(n)
    # guardar
    guarda(ficheiro, dados)
    # frequência
    frequencia = freq(ficheiro)
Agora fica a questão das barras e do uso do turtle. Admitamos que queremos fazer um gráfico como o da figura:
Vamos tentar decompor tudo em sub-problemas mais simples: os eixos, as barras e os valores. Vamos começar pelas barras e fazer algo como no caso do lançamento dos dados: começar pela questão de visualizar uma barra.
import turtle

def barra(pos,altura,cor, espessura):
    # posiciona
    turtle.penup()
    turtle.goto(pos)
    turtle.pendown()
    turtle.setheading(90)
    # atributos
    turtle.color(cor)
    turtle.width(espessura)
    # Desenha
    turtle.forward(altura)
A ideia desta solução é trivial: posicionar a tartaruga, colocá-la a apontar para norte e avançar um valor igual à altura da barra.

Desenhar um gráfico de barras pode ser agora feito chamando repetidas vezes a função barra.
def grafico_barras(pos_inicial,lista_barras, cor, espessura,delta):
    pos = pos_inicial
    for altura in lista_barras:
        barra(pos,altura,cor, espessura)
        # posiciona)
        pos = (turtle.xcor()+ delta,0)
    turtle.hideturtle()
O parâmetro delta controla o afastamento entre as barras. Passemos aos eixos. Pode ser feito de diferentes maneiras. Um exemplo:
def eixos(pos_x, pos_y, tam_x, tam_y):
    # posiciona 
    turtle.penup()
    turtle.goto(pos_x, pos_y)
    turtle.pendown()
    # eixo x
    turtle.forward(tam_x)
    turtle.write('X', font=('Arial',8,'bold'))
    # posiciona
    turtle.penup()
    turtle.goto(pos_x, pos_y)
    turtle.pendown()
    # eixo y
    turtle.setheading(90)
    turtle.forward(tam_y)
  
    turtle.write('Y', font=('Arial',8,'bold'))
    turtle.hideturtle()
Usamos a função write para escrever o nome dos eixos. Notar como se controla a escrita. É evidente que podemos ter o desenho dos eixos e das barras tudo junto:
def grafico_barras_eixos(pos_inicial,lista_barras, cor, espessura,delta):
    eixos(pos_inicial[0] -delta, pos_inicial[1], len(lista_barras) * (espessura + delta), max(lista_barras) + delta)
    pos = pos_inicial
    for altura in lista_barras:
        barra(pos,altura,cor, espessura)
        # posiciona)
        pos = (turtle.xcor()+ delta,0)
    turtle.hideturtle()
Notar como se controla a dimensão dos eixos! Falta acrescentar os números. Mas isso pode ser feito com uma pequena alteração no programa que desenha as barras.
def grafico_barras_eixos(pos_inicial,lista_barras, cor, espessura,delta):
    eixos(pos_inicial[0] -delta, pos_inicial[1], len(lista_barras) * (espessura + delta), max(lista_barras) + delta)
    pos = pos_inicial
    for altura in lista_barras:
        barra(pos,altura,cor, espessura)
        turtle.pencolor('black')
        turtle.write(altura, font=('Arial',8,'bold'))
        # posiciona)
        pos = (turtle.xcor()+ delta,0)
    turtle.hideturtle()
Podemos juntar esta solução ao resto do programa, definindo um novo main.
def main(n, ficheiro):
    # gerar
    dados = gera_2(n)
    print(dados)
    # guardar
    guarda(ficheiro, dados)
    # frequência
    frequencia = freq(ficheiro)
    print(frequencia)
    # gráfico
    valores = [frequencia.get(chave,0) for chave in range(1,13)]
    grafico_barras_eixos((0,0),valores,'red',5,15)
Se executarmos o programa, no entanto, o resultado não é muito famoso. O problema está no facto de os valores serem muito pequenos. Uma solução simples é definir um factor de escala que dependerá do número de lançamentos de dados. Interessa-nos uma alteração que aumente muito os valores pequenos de n e pouco os valores grandes. Uma função candidata a tal é a função logaritmo!
def main(n, ficheiro):
    # gerar
    dados = gera_2(n)
    print(dados)
    # guardar
    guarda(ficheiro, dados)
    # frequência
    frequencia = freq(ficheiro)
    print(frequencia)
    # gráfico
    escala = int(math.log(n,10)) # <--- 
    valores = [escala * frequencia.get(chave,0) for chave in range(1,13)]
    grafico_barras_eixos((0,0),valores,'red',5,15)
    turtle.exitonclick()
Mas podemos promover outra pequena alteração para poder colocar a identificação dos números ao longo dos eixos dos XX.
A mudança principal envolve a função barra.
def barra(pos,altura,cor, espessura,num):
    if altura:
        # posiciona
        turtle.penup()
        turtle.goto(pos[0],pos[1]-20)
        turtle.write(num)
        turtle.goto(pos)
        turtle.pendown()
        turtle.setheading(90)
        # atributos
        turtle.color(cor)
        turtle.width(espessura)       
        # Desenha
        turtle.forward(altura)
        turtle.pencolor('black')
        turtle.write(altura, font=('Arial',8,'bold'))  
Notar que passámos a fazer aqui toda a parte que interssa saber da barra: valor de x, de y e o desenho.

Para quem gosta de tudo muito direitinho aqui fica o programa final completo.
import random
import operator
import math

def gera_2(n):
    return [random.randint(1,6) + random.randint(1,6) for i in range(n)]

# Guardar
def guarda(ficheiro,dados):
    with open(ficheiro,'w') as f_out:
        for dado in dados:
            f_out.write(str(dado)+'\n')
    
# Frequência
def freq(ficheiro):
    with open(ficheiro) as f_in:
        dados_str = f_in.read().split()
        dados_num = [eval(dado) for dado in dados_str]
        # dicio frequências
        dicio = dict()
        for dado in dados_num:
            dicio[dado] = dicio.get(dado,0) + 1
    return dicio

# Ver
import turtle

def barra(pos,altura,cor, espessura,num):
    if altura:
        # posiciona
        turtle.penup()
        turtle.goto(pos[0],pos[1]-20)
        turtle.write(num)
        turtle.goto(pos)
        turtle.pendown()
        turtle.setheading(90)
        # atributos
        turtle.color(cor)
        turtle.width(espessura)       
        # Desenha
        turtle.forward(altura)
        turtle.pencolor('black')
        turtle.write(altura, font=('Arial',8,'bold'))        

def grafico_barras_eixos(pos_inicial,lista_barras, cor, espessura,delta):
    eixos(pos_inicial[0] - delta, pos_inicial[1], len(lista_barras) * (espessura + delta), max(lista_barras) + delta)
    pos = pos_inicial
    for num,altura in enumerate(lista_barras):
        barra(pos,altura,cor, espessura,num)
        # posiciona
        pos = (turtle.xcor()+ delta,pos_inicial[1])
    turtle.hideturtle()

def eixos(pos_x, pos_y, tam_x, tam_y):
    # posiciona 
    turtle.penup()
    turtle.goto(pos_x, pos_y)
    turtle.pendown()
    # eixo x
    turtle.forward(tam_x)
    turtle.write('X', font=('Arial',8,'bold'))
    # posiciona
    turtle.penup()
    turtle.goto(pos_x, pos_y)
    turtle.pendown()
    # eixo y
    turtle.setheading(90)
    turtle.forward(tam_y)
    turtle.write('Y', font=('Arial',8,'bold'))
    turtle.hideturtle()
    
# MAIN ----------------
def main(n, ficheiro):
    # gerar
    dados = gera_2(n)
    print(dados)
    # guardar
    guarda(ficheiro, dados)
    # frequência
    frequencia = freq(ficheiro)
    # gráfico
    escala = int(math.log(n,10)) # <-- 
    valores = [escala * frequencia.get(chave,0) for chave in range(1,13)]
    grafico_barras_eixos((0,0),valores,'red',5,15)
    turtle.exitonclick()
    
    
    

if __name__ == '__main__':
    main(1000,'/Users/ernestojfcosta/tmp/tudo.txt')
Para quem gosta de experiências, tente executar o programa para diferentes valores crescentes de n. Os gráficos de barras que são desenhados vão convergir para uma forma de distribuição conhecida em probabilidades: a distribuição normal. é uma boa altura para revisitar a Lei dos Grandes Números.

Boas Festas!

Sem comentários:

Enviar um comentário