<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Lab 1
Step1:  (1b) Indentações 
No Python a indentação faz o papel das chaves para determinar o bloco de comandos de uma função, condicional ou laço de repetição. O início de um bloco é definido pelo caractere "
Step2:  (1c) Funções 
As funções no Python podem receber quantas entradas necessárias e retornar múltiplas saídas. A declaração da função é precedida pela palavra-chave def.
Step3:  (1d) Tipos Especiais 
Além dos tipos básicos de variáveis o Python possui os tipos lista (list), tupla (tuple) e dicionários (dict).
As listas e tuplas são agregadores de valores e podem agrupar valores não necessariamente dos mesmos tipos. A diferença entre os dois é que listas são mutáveis (podem ser alterados) e tuplas são imutáveis. Elas são indexadas a partir do índice 0.
Os dicionários são arranjos associativos que permite associar uma chave (de qualquer tipo) a um valor (também de qualquer tipo).
Step4:  (1e) Iteradores 
Para iterar por uma lista, tupla ou dicionário podemos utilizar a palavra-chave in. Em conjunto com a instrução for ela percorre cada elemento iterativamente. Essa palavra-chave também pode ser utilizada para verificar se um elemento está contido na lista.
Step5:  (1f) Geradores e List Comprehension 
O Python permite uma sintaxe mais enxuta (e otimizada) para gerar uma nova lista de acordo com alguma regra específica
Step6:  Quando precisamos trabalhar com listas muito grandes, mas sem a necessidade de acessar os elementos aleatóriamente, podemos utilizar os geradores.
Um gerador define a instrução para gerar uma sequência, calculando cada elemento da sequência conforme requisitado.
Step7:  (1g) Arquivos 
A leitura de arquivos é feita pelo comando open() que gera um apontador para arquivo, podemos utilizar o laço for para ler cada linha do arquivo iterativamente como uma string.
Step8:  Parte 2
Step9:  (2b) Multiplicação elemento-a-elemento e produto interno 
A multiplicação elemento-a-elemento é calculada como
Step10:  (2c) Multiplicação de Matriz 
A multiplicação de matriz é definida por
Step11:  (2d) Slices 
Nos vetores e matrizes do Numpy podemos selecionar sub-conjuntos de valores durante a indexação. Ex.
Step12:  Parte 3
Step13:  As funções lambdas tem restrições em relação a expressão computada. Essa expressão não pode conter print ou incremento +=, por exemplo.
Além disso, os parâmetros de entrada podem ser de qualquer tipo, incluindo tuplas e listas.
Step14:  (3b) Lógica Funcional 
No paradigma funcional trabalhamos com os conceitos de dados imutáveis, ou seja, não existe o conceito de variáveis, uma vez que um valor é designado a um nome, esse valor não pode mudar.
Step15:  (3c) Funções de Alta Ordem 
Desse modo, o uso de laços (for, while) é desencorajado e substituídos pela recursividade e funções de alta ordem. Uma função de alta ordem é uma função que recebe uma ou mais funções como parâmetro e retorna uma função.
Step16:  Esse tipo de função ajuda a criar um código declarativo, em que o próprio código auto-explica o que está sendo feito
Um exemplo interessante é a construção de uma função que retorna outra função.
Step20:  (3d) Map, Reduce, Filter 
Essas três funções são utilizadas para transformação de listas no paradigma funcional. Essas funções recebem como parâmetro uma função f e uma lista l.
Map
Step21:  Para reduzir o tamanho do código e facilitar a leitura, podemos compor as funções em sequência
<ASSISTANT_TASK:>
Python Code:
x = 10          # x é um inteiro
print type(x)
x = 1.3         # x é um ponto flutuante
print type(x)
x = "Ola"       # x é uma string
print type(x)
x = [1, 5, 10]  # x é uma lista
print type(x)
Explanation: Lab 1: Revisão de Conceitos Básicos
Nesse notebook revisaremos os conceitos básicos da linguagem de programação Python, Algebra Linear, o uso da biblioteca NumPy e conceitos de Programação Funcional.
A primeira parte abordará as diferenças de sintaxe da linguagem Python em relação ao Java e ao C/C++ e formas de otimizar o código. A segunda parte mostrará, os conceitos básicos da biblioteca Numpy e como utilizá-la para aplicar conceitos de Algebra Linear. Finalmente, a terceira parte mostrará os conceitos de Programação Funcional como Expressões Lambda e Funções de Ordem Alta.
Para navegar pelo notebook execute cada uma das células utilizando a tecla de atalho SHIFT-ENTER, isso executará as intruções da célula In mostrando o resultado na célula Out correspondente. Todas as variáveis criadas em uma célula podem ser acessadas em todas as células subsequentes.
As células-exercícios iniciam com o comentário # EXERCICIO e os códigos a serem completados estão marcados pelos comentários &lt;COMPLETAR&gt;.
 Nesse notebook: 
Parte 1: Python
Parte 2: NumPy
Parte 3: Programação Funcional
Parte 1: Python
 (1a) Declarações de Variáveis 
O tipo de variáveis no Python é definido dinâmicamente pelo interpretador. Não existe necessidade em declarar.
End of explanation
x = 10
for i in range(20):
    # Início da repetição For
    x = x + 1
    if x%2 == 0:
        # Instrução se condição verdadeira
        x = x + 1
    else:
        # Instrução se a condição for falsa
        x = x + 2
    # Fim do bloco de repetição For
print x # Isso está fora do for! Boa Ideia!
Explanation: (1b) Indentações 
No Python a indentação faz o papel das chaves para determinar o bloco de comandos de uma função, condicional ou laço de repetição. O início de um bloco é definido pelo caractere ":".
End of explanation
def Soma(x,y):
    return x+y
def Mult(x,y):
    return x*y
def SomaMult(x,y):
    return x+y, x*y  # múltiplas saídas separadas por vírgula
print Soma(10,2), Mult(10,2), SomaMult(10,2)
# O retorno de múltiplas saídas podem ser atribuídas diretamente para múltiplas variáveis
w,z = SomaMult(10,2)
print w, z
Explanation: (1c) Funções 
As funções no Python podem receber quantas entradas necessárias e retornar múltiplas saídas. A declaração da função é precedida pela palavra-chave def.
End of explanation
lista = [1, 2, True, "palavra"]
tupla = (1, 2, True, "palavra")
lista[1] = 3.0
print lista
tupla[1] = 3.0 # Vai dar erro!
# range(n) gera uma lista de valores entre 0 e n-1
# len(lista) retorna o tamanho de uma lista
def DobraValores(lista):
    for i in range(len(lista)):
        lista[i] = lista[i]*2
    return lista
lista = [1,2,3,4]
lista2 = DobraValores(lista)
print lista, lista2  # As listas são passadas como referência para as funções
dicionario = { "Ana":12, "Joao":13, "Jose":17 }  # declaração inicial do dicionário, pode ser {} para dic. vazio
print dicionario["Ana"]  # acesso ao elemento pela chave entre colchetes
dicionario["Maria"] = 11  # podemos alterar ou inserir um novo elemento
print dicionario
print "As chaves do dicionário são: ", dicionario.keys()
print "Os valores do dicionário são: ", dicionario.values()
Explanation: (1d) Tipos Especiais 
Além dos tipos básicos de variáveis o Python possui os tipos lista (list), tupla (tuple) e dicionários (dict).
As listas e tuplas são agregadores de valores e podem agrupar valores não necessariamente dos mesmos tipos. A diferença entre os dois é que listas são mutáveis (podem ser alterados) e tuplas são imutáveis. Elas são indexadas a partir do índice 0.
Os dicionários são arranjos associativos que permite associar uma chave (de qualquer tipo) a um valor (também de qualquer tipo).
End of explanation
lista = range(10)  # gera a lista [0,..,9]
print 8 in lista, 12 in lista
for x in lista:
    print x
Explanation: (1e) Iteradores 
Para iterar por uma lista, tupla ou dicionário podemos utilizar a palavra-chave in. Em conjunto com a instrução for ela percorre cada elemento iterativamente. Essa palavra-chave também pode ser utilizada para verificar se um elemento está contido na lista.
End of explanation
# Jeito tradicional, mas não otimizado
listaOriginal = [1,2,3,4,5,6,7,8,9]
listaQuadrada = []
for x in listaOriginal:
    listaQuadrada.append(x*x)
print listaQuadrada
# Através do List Comprehension
listaQuadrada = [ x*x for x in listaOriginal ]
print listaQuadrada
Explanation: (1f) Geradores e List Comprehension 
O Python permite uma sintaxe mais enxuta (e otimizada) para gerar uma nova lista de acordo com alguma regra específica:
[ funcao(x) for x in listaGeradora ]
End of explanation
listaQuadrada = ( x*x for x in listaOriginal )
print listaQuadrada # os elementos ainda não foram calculados
for x in listaQuadrada:
    print x # a cada iteração apenas o próximo elemento é calculado, a lista não existe na memória
Explanation: Quando precisamos trabalhar com listas muito grandes, mas sem a necessidade de acessar os elementos aleatóriamente, podemos utilizar os geradores.
Um gerador define a instrução para gerar uma sequência, calculando cada elemento da sequência conforme requisitado.
End of explanation
import os.path
caminho = os.path.join('Data','Aula01')  # garante o uso correto de / ou \\ para diretórios
arquivo = os.path.join(caminho,'exemplo.txt')
f = open(arquivo)
for linha in f:
    print linha
f.close()    
Explanation: (1g) Arquivos 
A leitura de arquivos é feita pelo comando open() que gera um apontador para arquivo, podemos utilizar o laço for para ler cada linha do arquivo iterativamente como uma string.
End of explanation
# Como convenção importaremos a biblioteca numpy como np
import numpy as np
# EXERCICIO
# Crie uma array numpy com os valores 1, 2, 3
arraySimples = <COMPLETAR>
# Faça o produto escalar multiplicando a array por 5
vezesCinco = <COMPLETAR>
print arraySimples
print vezesCinco
# TESTE do exercício (2a)
assert np.all(vezesCinco == [5, 10, 15]), 'valor incorreto para vezesCinco'
print "Correto!"
Explanation: Parte 2: Numpy
NumPy é uma biblioteca do Python para trabalhar com arrays. Essa biblioteca provê abstrações para utilizar arrays como vetores e matrizes. Ela é otimizada para ser rápida e eficiente em relação ao uso de memória. O tipo básico do NumPy é o ndarray, que é uma array multidimensional de tamanho fixo que contém elementos de um tipo apenas.
 (2a) Multiplicação por Escalar 
Para esse exercício, crie uma ndarray contendo os elementos [1, 2, 3] e multiplique essa array por 5. Use o comando np.array() para criar a array. Note que um dos possíveis parâmetros para essa função é ums lista do Python. A multiplicação escalar pode ser feita utilizando o operador *.
Note que se você criar uma array partindo de uma lista do Python, você obterá uma array unidimensional, que é equivalente a um vetor.
End of explanation
# EXERCICIO
# A função np.arange(inicio,fim,passo)  cria uma lista iniciando em inicio, terminando antes do fim seguindo passo
u = np.arange(0, 5, .5)   # np.array([0,0.5,1.0,...,4.5])
v = np.arange(5, 10, .5)
elementoAelemento = <COMPLETAR>
prodInterno = <COMPLETAR>
print 'u: {0}'.format(u)
print 'v: {0}'.format(v)
print '\nelementoAelemento\n{0}'.format(elementoAelemento)
print '\nprodInterno\n{0}'.format(prodInterno)
# TESTE do exercício (2b)
assert np.all(elementoAelemento == [ 0., 2.75, 6., 9.75, 14., 18.75, 24., 29.75, 36., 42.75]), "Valores incorretos para elementoAelemento"
print "Primeiro teste OK"
assert prodInterno==183.75, "Valor incorreto para prodInterno"
print "Segundo teste OK"
Explanation: (2b) Multiplicação elemento-a-elemento e produto interno 
A multiplicação elemento-a-elemento é calculada como: $$ \mathbf{x} \odot \mathbf{y} =  \begin{bmatrix} x_1 y_1 \\  x_2 y_2 \\ \vdots \\ x_n y_n \end{bmatrix} $$
E o do produto interno de dois vetores de mesmo tamanho $ n $: $$ \mathbf{w} \cdot \mathbf{x} = \sum_{i=1}^n w_i x_i $$
Em alguns livros você também vê $ \mathbf{w} \cdot \mathbf{x} $ escrito como  $ \mathbf{w}^\top \mathbf{x} $
O tipo Numpy Array suporta essas duas operações, ao utilizar o operador * para multiplicar dois vetores ou matrizes, ele executará a multiplicação elemento-a-elemento. Para realizar o produto interno você pode utilizar tanto a função np.dot() ou ndarray.dot(). Ex.: dados os vetores $x$ e $y$ pode realizar a operação como np.dot(x,y) ou x.dot(y).
End of explanation
# EXERCICIO
from numpy.linalg import inv  # agora podemos utilizar o comando inv() sem preceder com np.linalg
# Criar uma matriz com listas de listas
A = np.matrix([[1,2,3,4],[5,6,7,8]])
print 'A:\n{0}'.format(A)
# Imprima a matriz transposta
print '\nA transposta:\n{0}'.format(<COMPLETAR>)
# Multiplique A por sua Transposta
AAt = <COMPLETAR>
print '\nAAt:\n{0}'.format(AAt)
# Inverta AAt com o comando inv()
AAtInv = <COMPLETAR>
print '\nAAtInv:\n{0}'.format(AAtInv)
# Mostre que a matriz vezes sua inversa é a identidade
# .round(n) arredonda os valores para n casas decimais
print '\nAAtInv * AAt:\n{0}'.format((<COMPLETAR>).round(4))
# TESTE do exercício (2c)
assert np.all(AAt == np.matrix([[30, 70], [70, 174]])), "Valores incorretos para AAt"
print "Primeiro teste OK"
assert np.allclose(AAtInv, np.matrix([[0.54375, -0.21875], [-0.21875, 0.09375]])), "Valor incorreto para AAtInv"
print "Segundo teste OK"
Explanation: (2c) Multiplicação de Matriz 
A multiplicação de matriz é definida por:
$$ [\mathbf{X} \mathbf{Y}]{i,j} = \sum{r=1}^n \mathbf{X}{i,r} \mathbf{Y}{r,j} $$
Note que o número de colunas da primeira matriz deve ser o mesmo do número de linhas da segunda matriz, representada por  $ n $
No Numpy utilizamo np.matrix() quando queremos criar uma matriz a partir de listas do Python. Com esse tipo podemos utilizar o operador * para multiplicação de matrizes, np.multiply() para multiplicação elemento-a-elemento, np.matrix.transpose() ou .T para calcular a transposta e np.linalg.inv() para calcular a inversa de uma matriz quadrada.
End of explanation
# EXERCICIO
atributos = np.array([1, 2, 3, 4])
print 'atributos:\n{0}'.format(atributos)
# Crie uma array com os 3 últimos elementos de atributos
ultTres = <COMPLETAR>
print '\nÚltimos três:\n{0}'.format(ultTres)
# TEST do exercício (2d)
assert np.all(ultTres == [2, 3, 4]), "Valores incorretos para ultTres"
print "Teste OK"
Explanation: (2d) Slices 
Nos vetores e matrizes do Numpy podemos selecionar sub-conjuntos de valores durante a indexação. Ex.:
v[:10] seleciona os 10 primeiros elementos
v[2:]  seleciona os elementos da terceira posição em diante
v[-5:] retorna os 5 últimos elementos
v[:-5] retorna os elementos do começo até o ultimo-5
v[1:3] retorna os elementos 1 e 2
End of explanation
# EXERCICIO
# Lembre-se que: "lambda x, y: x + y" cria uma função que adiciona dois valores
mult10 = <COMPLETAR>
print mult10(5)
# Note that the function still shows its name as <lambda>
print '\n', mult10
assert mult10(10)==100, "Função incorreta"
print "Teste OK"
Explanation: Parte 3: Programação Funcional 
 (3a) Funções Anônimas (Lambda) 
Uma função/expressão lambda é utilizada para definir funções simples com apenas uma instrução. Para isso basta usar a instrução lambda seguido da lista de parâmetros de entrada, precedidos por : e a expressão a ser executada. Por exempo, lambda x, y: x + y é uma função anônima que calcula a soma de dois valores.
Expressões lambda geram uma função quando interpretadas pelo Python. Elas são úteis quando precisamos aplicar uma função simples em diversos elementos de uma lista.
Para saber mais sobre Lambdas: Lambda Functions, Lambda Tutorial, and Python Functions.
No exercício abaixo crie uma função lambda que multiplique um valor por 10, atribua a variável designada.
End of explanation
p1 = (1,3)
p2 = (3,7)
euclidiana2D = lambda (x0,y0), (x1,y1): np.sqrt(((x0-x1)**2) + ((y0-y1)**2))  # sqrt é a raíz quadrada
print euclidiana2D(p1,p2)
Explanation: As funções lambdas tem restrições em relação a expressão computada. Essa expressão não pode conter print ou incremento +=, por exemplo.
Além disso, os parâmetros de entrada podem ser de qualquer tipo, incluindo tuplas e listas.
End of explanation
# Lógica não-funcional
a = 0
def inc():
    global a
    a = a + 1
# Lógica funcional
def incFn(a):
    return a+1
Explanation: (3b) Lógica Funcional 
No paradigma funcional trabalhamos com os conceitos de dados imutáveis, ou seja, não existe o conceito de variáveis, uma vez que um valor é designado a um nome, esse valor não pode mudar.
End of explanation
# Função para somar 3 valores
def Soma3(a,b,c):
    return a+b+c
# Função que soma apenas dois valores
def Soma2(a,b):
    return a+b
# Soma 3 poderia ser criado a partir de Soma2:
Soma3Fn = lambda a,b,c: Soma2(Soma2(a,b),c)
Explanation: (3c) Funções de Alta Ordem 
Desse modo, o uso de laços (for, while) é desencorajado e substituídos pela recursividade e funções de alta ordem. Uma função de alta ordem é uma função que recebe uma ou mais funções como parâmetro e retorna uma função.
End of explanation
# Cria uma função que calcula a eq. do segundo grau no formato ax^2 + bx + c
def Eq2grau(a,b,c):
    def f(x):
        return a*x**2 + b*x + c
    return f
f = Eq2grau(10,2,1)
print f(10)
# EXERCICIO
# Escreva uma função Soma(x) que retorna uma função que recebe um valor y e soma ao x.
def Soma(x):
    <COMPLETAR>
Soma2 = lambda a,b: Soma(a)(b)
Soma3 = lambda a,b,c: Soma(Soma(a)(b))(c)
print Soma2(1,3), Soma3(1,2,3)
assert Soma3(1,2,3)==6, "Erro na função"
print "Ok"
Explanation: Esse tipo de função ajuda a criar um código declarativo, em que o próprio código auto-explica o que está sendo feito
Um exemplo interessante é a construção de uma função que retorna outra função.
End of explanation
class FuncionalW(object):
    def __init__(self, data):
        self.data = data
    def map(self, function):
        Call `map` on the items in `data` using the provided `function`
        return FuncionalW(map(function, self.data))
    def reduce(self, function):
        Call `reduce` on the items in `data` using the provided `function`
        return reduce(function, self.data)
    def filter(self, function):
        Call `filter` on the items in `data` using the provided `function`
        return FuncionalW(filter(function, self.data))
    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)
    def __getattr__(self, name):  return getattr(self.data, name)
    def __getitem__(self, k):  return self.data.__getitem__(k)
    def __repr__(self):  return 'FuncionalW({0})'.format(repr(self.data))
    def __str__(self):  return 'FuncionalW({0})'.format(str(self.data))
# Exemplo de Map
# Criaremos uma lista
lista = FuncionalW(range(10))
# Criar uma função a ser aplicada nessa lista
f = lambda x: x*x
# Programação Imperativa
resultado1 = FuncionalW([])
for x in lista:
    resultado1.append(f(x))
print "Resultado: {}".format(resultado1)    
# Funcional
print "Resultado usando Map: {}".format(lista.map(f))
# Exemplo de Reduce
# Criaremos uma lista
lista = FuncionalW(range(1,10))
# Criar uma função a ser aplicada nessa lista
f = lambda x,y: x*y
# Programação Imperativa
produtoria = 1
for x in lista:
    produtoria = f(produtoria,x)
print "Resultado: {}".format(produtoria)    
# Funcional
print "Resultado usando Reduce: {}".format(lista.reduce(f))
# EXERCICIO
dataset = FuncionalW(range(10))
# Multiplique cada elemento por 5
mapResult = <COMPLETAR>
# Filtre eliminando os elementos ímpares
# No Python "x % 2" é o resultado do resto da divisão de x por 2
filterResult = <COMPLETAR>
# Some os elementos
reduceResult = <COMPLETAR>
print 'mapResult: {0}'.format(mapResult)
print '\nfilterResult: {0}'.format(filterResult)
print '\nreduceResult: {0}'.format(reduceResult)
assert mapResult == FuncionalW([0, 5, 10, 15, 20, 25, 30, 35, 40, 45]),"Valor incorreto para mapResult"
print "Teste 1 OK"
assert filterResult == FuncionalW([0, 2, 4, 6, 8]), "Valor incorreto para filterResult"
print "Teste 2 OK"
assert reduceResult == 45, "Valor incorreto para reduceResult"
print "Teste 3 OK"
Explanation: (3d) Map, Reduce, Filter 
Essas três funções são utilizadas para transformação de listas no paradigma funcional. Essas funções recebem como parâmetro uma função f e uma lista l.
Map: aplica a função em cada elemento da lista, gerando uma nova lista
Reduce: aplica a função cumulativamente em pares de elementos da lista, retornando um único valor agregado ao final
Filter: gera uma nova lista contendo os elementos da lista l em que a aplicação de f returna True
Para os próximos exercícios vamos utilizar a classe FuncionalW para criar uma sintaxe parecida com a que utilizaremos com o Spark.
End of explanation
dataset = FuncionalW(range(10))
Soma = (dataset
            .map(lambda x: x*5)
            .filter(lambda x: x%2==0)
            .reduce(lambda x,y: x+y)
            )
print Soma
# EXERCICIO
# split() divide a string em palavras
Texto = FuncionalW("Esse texto tem varias palavras cada linha tem palavras escritas Esse texto esta escrito".split())
# Vamos fazer uma contagem da palavra 'palavras' no texto
# Crie uma função lambda que recebe duas entradas e retorna se são iguais ou não
Igual = <COMPLETAR>
# Crie uma função lambda que utiliza a função Igual para detectar se a entrada é igual a palavra 'palavras'
DetectaPalavra = <COMPLETAR>
# 1) Filtre as palavras iguais a 'palavras'
# 2) Mapeie todos os elementos para o valor 1
# 3) Reduza para a somatória
contagem = (Texto
            .<COMPLETAR>
            .<COMPLETAR>
            .<COMPLETAR>
            )
print "Existem {} ocorrências de 'palavras'".format(contagem)
Explanation: Para reduzir o tamanho do código e facilitar a leitura, podemos compor as funções em sequência
End of explanation
<END_TASK>