#!/usr/bin/env python
# coding: utf-8

# **This notebook is for a Portuguese speaking audiance as part of a training session. Soon I will post it in English. **
#
# >"Então você não se lembra de um mundo sem robôs.  Houve um tempo quando a humanidade enfrentou o universo sozinha e sem amigo. Agora ela tem criaturas para ajudá-la; criaturas mais fortes que si mesma, mais confiáveis,  mais úteis e absolutamente devotas. A humanidade não mais está sozinha. Você já pensou nisso desta forma?"

# I, Robot - Issac Asimov, 1950
#
#
# # Uma breve história dos algoritmos que aprendem
#
#

# **Bem-vindos ao Laboratório Introdutório de Machine Learning!**
#

#
# Esse é um dos livros que vamos  usar como referência [Python Machine Learning - Second Edition,
# Raschka & Mirjalili, September-2017](https://www.packtpub.com/big-data-and-business-intelligence/python-machine-learning-second-edition)
#
#
# O primeiro passo para iniciar nossos estudos  é compreender que **Machine Learning (ML)** é um sub-campo de pesquisa da **Inteleligência Artificial (IA)** e,  portanto, não é necessariamente seu sinônimo como erroneamente sugerem alguns desavisados por ai.  ** Deep Learning (DL)** é um dos tópicos de **Redes Neurais (NN's)** que por sua vez são uma das sub-áreas de **ML**.  Não cometa o erro de confundir indistintamente Deep Learning com Machine Learning.
# ![](https://blogs.nvidia.com/wp-content/uploads/2016/07/Deep_Learning_Icons_R5_PNG.jpg.png)
#
#
#
# # Peceptron
# Algoritmos de aprendizagem não são um tema novo.  A definição de neurônio artificial, o **perceptron**, foi estabelecida no final da década de 50 (The Perceptron: A Perceiving and Recognizing Automaton, F. Rosenblatt, Cornell Aeronautical Laboratory, 1957) e pode ser resumida na função abaixo:
#
# ![](https://www.dropbox.com/s/s0uvoloszvkg83x/00-Perceptron.jpg?dl=1)
#
# De forma simplificada, a saída de um neurônio artificial é igual a soma do produto das entradas ***x*** pelos pesos ***w*** aplicados a cada entrada.   As entradas ***x*** de um neurônio artificial equivalem aos dentritos de um neurônio biológico e a soma **$\sum_{j=0}^m x_{j}w_{j}$** é o estímulo resultante no axônio, definido por um limiar interno do neurônio (**threshold**) que vai determinar a sua "sensibilidade" ou quando será ativado ou não. Em ML preferencialmente utilizaremos a notação matricial  **$w^Tx$** onde o produto é dado pela transposta de *w* por *x*. A utilização de matrizes permite maior eficiência computacional e simplificação dos códigos de ML.
#
#

# Aqui temos a representação gráfica do perceptron:
#
# ![](https://www.dropbox.com/s/yxvrkm7kk1r991e/01-Perceptron.jpg?dl=1)

# ![](https://www.sololearn.com/avatars/b2b6905b-4e53-412a-bcb8-22bfef2bcec5.jpg)
#
# # Quando as máquinas aprendem...
#
# A aprendizagem se traduz em encontrar pesos que aplicados aos valores de entrada resultem em um determinado valor de saída esperado.  Ainda analisando o gráfico do perceptron acima, vale notar que por questões de convenção e cálculo a entrada **$x_{0} $** é fixada com o valor ***1*** e seu o peso **$w_{0} $**  é chamado de **bias**.   Em uma rede neural de apenas uma entrada teríamos a seguinte equação equivalente z =  $w_{0}$ + $w_{1}x_{1}$.  Se voltarmos às aulas de matemática fundamental veremos que essa é exatamente uma **equação reduzida da reta **, onde $w_{0}$ define a "altura" da reta e  $w_{1}$ define sua inclinação no gráfico.
#
# ![](https://www.dropbox.com/s/cdai4n28jp5m5wp/simple_regression.png?dl=1)
#
#
# O que os algoritmos de ML fazem é buscar de forma automática a equação que melhor representa o conjunto verdade (**y**) para um conjunto de observações ou amostras de entradas.  Uma forma de encontrar a melhor equação é através do cálculo sucessivo da diferença entre os valores gerados pela equação "aprendida" (**ŷ**) e os valores reais observados (**y**). Essa diferença chamamos de **Erro** ou **Perda**. As funções de perda  ou **loss functions** são um importante elemento na construção de algoritmos inteligentes. Em outras palavras, podemos afirmar que a aprendizagem de máquina é essencialmente uma tarefa de otimização de funções.  Atualmente os principais frameworks de ML implementam diversos algoritmos de otimização, sendo o Gradiente Descendente Estocástico ([SGD](http://ruder.io/optimizing-gradient-descent)) um dos mais populares.
#
# O processo de ajustar pesos através de algoritmos de otimização de função é denominado **fit (treino)**, e cada rodada de ajustes é chamada de **Epoch (Época)**. O ajuste geralmente é feito usando um determinado número de amostras por vez que chamamos de **Batch (Lote)** .
#
# No gráfico acima temos um problema onde os valores de solução podem ser linearmente correlacionadas com as amostras de entrada. Neste caso um algoritmo de ** Regressão ** poderia ser aplicado, mas existem varios tipos de algoritmos de ML e cada um vai funcionar melhor em determinados cenários.  Daí o teorema  **No Free Lunch**  em Machine Learning de David H. Wolpert, que nos recorda de que nenhum algoritmo de ML é universalmente melhor que todos os outros em todos cenários (The Lack of A Priori Distinctions Between Learning Algorithms, Wolpert and David H, 1996).
#
#
#
# # Rolling in the Deep
#
# As redes profundas conhecidas como Deep Learning(DL) ganharam maior destaque a partir de 2012 com a vitória de um time universitário canadense em uma competição de classificação de Imagens, a [ImageNet](http://www.image-net.org/).
#
# Mas a vitória deste time canadense está intimamente relacionada com avanços da década de 80, sendo um de seus expoentes o cientista de computação e psicologia cognitiva **Geoffrey Hinton** da Universidade de Toronto. Hinton é conhecido por temas como **Propagação Reversa (Backpropagation)**, **Máquina de Boltzman (Boltzmann Machine** e **Deep Learning**.
#
# Embora o termo Deep Learning já havia sido aplicado a redes neurais artificiais por Igor Aizenberg em 2000,  foi uma plublicação de Geoffrey Hinton e Ruslan Salakhutdinov em 2006 que chamou mais atenção ao mostrar como redes neurais poderiam ser pré-treinadas uma camada por vez, e então fazer ajustes finos por meio de Backpropagation.  Esse avanço contribuiu fortemente para a viabilidade das redes DL como hoje conhecemos.
#
# Em 2012, Hinton e seus dois alunos Alex Krizhevsky e Ilya Sutskever entraram na competição ImageNet e ao fazerem uso de redes densas convolucionais (CNN's) e técnicas avançadas para reduzir overfitting (ajuste excessivo aos dados de treino que resulta em baixa generalização) conseguiram atingir um incrível patamar de erro de 16% contra os 25% alcançados até então com algoritmos classificadores existentes. Hinton e seus alunos criaram uma empresa que seria adquirida posteriormente pelo Google.
#
# Abaixo o gráfico da arquitetura de sua rede **[AlexNet](https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf)** . Esta rede foi treinada por cerca de 5 a 6 dias usando um dataset de milhões de imagens classificadas milhares de classes. A equipe da AlexNet além da arquitetura inovadora utilizou duas placas de video GTX 580 (GPU) para poder suportar a alta demanda de processamento desse tipo de rede. O poder de manipulação de matrizes de uma GPU é muito bemvindo com algoritmos de ML, já que no final das contas toda informação e aprendizagem resultam em matrizes de dados e pesos.
#
#
# ![](https://image.slidesharecdn.com/dlcvd2l4imagenet-160802094728/95/deep-learning-for-computer-vision-imagenet-challenge-upc-2016-7-638.jpg?cb=1470131387)
#
# Nos anos seguintes empresas como Nvidia, Google, Microsoft, Baidu, Amazon, IBM, Ubber, Facebook e Tesla  entrariam de forma ainda mais agressiva na corrida tecnológica por plataformas de inteligência artificial mudando o nível do jogo para uma aposta de trilhões de dólares,  e criando com o apoio das diversas comunidades de código aberto os frameworks poderosos que estão hoje ao alcance de alguns cliques. Abaixo algum dos principais frameworks da atualidade:
#
# ![](https://www.dropbox.com/s/lv9ooa3ur8pxc33/deep-learning-developer-frameworks-407.png?dl=1)

# # O que é Machine Learning?
#

# A seguir vamos começar a entender um pouco mais sobre como funciona os principais tipos de algoritmos de ML,  quais são as estratégias de treino e etapas para construção destes algorítimos.
#
# Então, uma pergunta importante a fazer é: o que é um algorítimo de aprendizagem de máquina? Uma boa definição, emprestada de [*Deep Learning*](http://www.deeplearningbook.org) (Goodfellow-et-al-2016),  seria ***"um algorítmo de aprendizagem de máquina é um algorítmo capaz de aprender com os dados"* **.
#
# Ok, mas o que significa aprender? Tom Mitchell em seu livro* Machine Learning* (McGraw-Hill, New York. 97) nos ajuda com uma definição bem sucinta: *** “Um programa de computador é dito aprender de uma experiência E em respeito a alguma classe de tarefa T e medida de performance P, se sua performance em tarefas T, como medido por P, melhora com a experiência E".***
#
# Todavia não podemos esquecer que Machine Learning é um campo em construção e muitos dos conceitos que hoje consideramos  verdade serão descartados nos próximos anos. O próprio [Geoffrey Hinton em entrevista com Andrew NG](https://www.youtube.com/watch?v=-eyhCTvrEtE) (outro nome bastante conhecido da galera de ML) diz *"Meu conselho é que leia alguma literatura (*de ML*) mas não leia demais... alguns dizem que você deveria passar vários anos lendo a literatura e só então começar a trabalhar em suas próprias idéias e isso pode ser verdade para alguns pesquisadores, mas para pesquisadores criativos eu penso que o que você quer é estudar uma parte da literatura e, então, notar o que todos estão fazendo errado... aquilo que você sente que não está correto, e ao contrário imaginar um jeito de fazer certo... e quando os outros disserem que não serve, apenas continue... tenho um bom princípio para ajudar as pessoas a continuarem que é: ou suas intuições são boas ou não, se são boas você deveria seguir-las e ao final terá sucesso, se não são boas não importa o que você faça... você deveria confiar nas suas intuições não há razão para não fazê-lo..."* (tradução livre)
#
#
# Portanto, a seguir veremos três grandes grupos de algoritimos de ML, mas utilize essa divisão apenas como ferramenta de compreensão já que alguns algoritmos atuais extravasam essas classificações.
#
#
# # Os três tipos de Machine Learning
#
# Os algoritmos de Machine Learning podem ser agrupados em três tipos principais:
#
#
# ![](https://www.dropbox.com/s/btluyzv2e08djan/02-MLTipos.jpg?dl=1)
#
# ## Supervised Learning
# O principal objetivo na **aprendizagem supervisionada** é "aprender" um modelo com base nos dados de treino rotulados que seja capaz fazer predições a respeito de dados novos ou de dados futuros.
#
# Quando os valores esperados são discretos, como por exemplo um algoritmo capaz de reconhecer se uma imagem é de um gato ou cachorro, dizemos que se trata de uma **Tarefa de Classificação** ou seja buscamos um modelo classificador.  Classificação é uma subcategoria da aprendizagem supervisionada na qual o foco é prever rótulos categóricos de novas instâncias baseado nas observações do passado.
#
# A predição de valores contínuos, como por exemplo o preço de venda de um imóvel, é tratada por outra subcategoria de aprendizagem supervisionada a **Regressão**.
#
#
# ## Reinforcement Learning
# Outro tipo de aprendizagem de máquina é o aprendizado por reforço. Em **reinforcement learning** o objetivo é desenvolver um **agente** que melhora sua performance baseado em sucessivas interações com o ambiente.  Diferentemente das funções de perda (loss functions) das técnicas supervisioanadas, aqui o feedback é dado por um sistema de recompensas que pune ou premia certos resultados (**reward function**) com base em certos estados do ambiente.
#
# Um exemplo popular desta arquitetura de aprendizagem é uma Engine de Xadrez. Nela o agente decide uma série de movimentos de acordo com o estado do tabuleiro, a recompensa pode ser definida com base em diversos resultados como sobrepor uma peça inimiga ou tomar sua rainha, ou mesmo a vitória ou derrota final.
#
#
#
#
# ## Unsupervised Learning
# Na aprendizagem não supervisionada lidamos com dados não rótulados ou com informação cuja estrutura não é exatamente conhecida.
#
# Ao usarmos  técnicas de aprendizagem não supervisionada somos capazes de explorar a estrutura de nossas amostras e extrair informação significativa de como essas amostras se relacionam.  Uma das aplicações práticas deste tipo de aprendizagem é a segmentação (**clustering**) de clientes de acordo com suas preferências ou quaisquer outras características que tenhamos à disposição.
#
# Outro campo de aplicação da aprendizagem não supervisionada é redução de dimensionalidade de dados. A redução de dimensionalidade permite eliminar ruídos e comprimir informação resultando em economia de processamento e armazenamento de dados.

# # Botando a mão na massa!
#
# Agora que vimos em linhas gerais o que são algorítmos de ML, vamos começar com o primeiro passo no desenvolvimento de um sistema de ML:   A preparação e exploração de dados ou **exploratory data analysis (EDA)** termo também emprestado do campo de estatística.
#
# Nos exemplos vou usar um famoso dataset chamado Iris que possui 150 amostras de 3 tipos de flores e os tamanhos de suas pétalas.  Em ML essas características ou dados de entrada denominamos **features**.
#
# *Você deve executar cada célula de código . Use Ctrl + Enter para executar e Shift + Enter para criar uma nova célula*
#
# ## 1 - Bibliotecas

# In[ ]:

#Usamos import para importar as bibliotecas e pacotes que vamos utilizar
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt # library for draring charts

# a magic cell (%) abaixo permite exibir gráficos de forma interna adequadamente
get_ipython().run_line_magic('matplotlib', 'inline')

# Exibe a versão das biblioteca. Em alguns casos é importante em que versão está trabalhando
print("Numpy Version {}".format(np.__version__))
print("Pandas Version {}".format(pd.__version__))

# ## 2 - Caminho do Dataset

# In[ ]:

'''
O dataset que vamos usar foi adicionado automaticamente. Podemos adicionar qualquer dataset
público do Kaggle com o botão "Add a Data Source" ou o seu próprio com "Upload a Dataset"

Os dataset adicionados serão postos no caminho "../input".
Abaixo executamos o comando linux ls através do python para listar os arquivos desta pasta:
'''
from subprocess import check_output
print('Arquivos Iris:')
print(check_output(["ls", "../input/iris"]).decode("utf8"))

# ## 3 - Carregando o Dataset

# In[ ]:

# Existem várias formas de se carregar um dataset para uso em ML as duas mais comuns:
# usar iblioteca numpy ou carregar um data frame do pandas como abaixo
df_iris = pd.read_csv('../input/iris/Iris.csv')

# In[ ]:

# Exibe as primeiras 5 linhas do dataframe
df_iris.head(5)

# ## 4 - Explorando os dados com gráficos do matplotlib
#
# No dataset Iris temos na coluna Species os tipos das flores que vamos analisar. Para isso precisamos transformar as classes de flores em números, para podermos seguir com as análise

# **Exibindo a distribuição das classes**

# In[ ]:

# Verificamos os valores únicos para as espécies
print(df_iris['Species'].unique())

# Adicionamos uma nova coluna no data frame e mapeamos com um valor númerico por classe
# essa coluna é nosso target (a predição que nossa rede vai gerar)
df_iris['y'] = df_iris['Species'].map({'Iris-setosa': 1, 'Iris-versicolor': 2, 'Iris-virginica' : 3})

# Configuramos o gráfico
plt.xlabel('Sepal Length Cm')
plt.ylabel('Petal Length Cm')
#plt.scatter(x,y, c=color)
plt.scatter(df_iris['SepalLengthCm'],df_iris['PetalLengthCm'], c=df_iris['y'])
plt.show() # como usamos a magic cell no inicio, esse comando não é obrigatório.

# Observando o gráfico acima podemos verificar que com apenas duas features do dataset é possível separar as classes (com uma apenas amostra como de exceção).  Esse tipo de feature é muito útil para construirmos nossos modelos de aprendizagem.

# **Usando funções plot e hist do matplotlib para compreender melhor os dados:**

# In[ ]:

# Nesse grafico podemos ver que nosso dataset é bastante balanceado
plt.title('Histograma das Classes - Cada Classe tem 50 ocorrências')
plt.hist(df_iris['y'])
plt.show() # Estou mantendo o comando apenas por questão de estética na saída.

plt.title('Histograma da Propriedade Sepal Length\n Maior número de amostras com valor entre 5 e 7 cm')
plt.hist(df_iris['SepalLengthCm'], bins=6)
plt.show()

# In[ ]:

#Como as amostras estão ordenadas é possível "ver" no gráfico onde começa e termina
#cada grupo de 50.
plt.figure(figsize=(15,10))
plt.title('Exibindo as medidas por amostras')
plt.plot ( df_iris['SepalLengthCm'], c='blue', )
plt.plot ( df_iris['SepalWidthCm'], c= 'red')
plt.plot ( df_iris['PetalLengthCm'], c= 'green')
plt.plot ( df_iris['PetalWidthCm'], c= 'yellow')
plt.show()

# ##  5 - Selecionando um algoritmo de ML
# Embora seja um Dataset bem pequeno, ele é bastande balanceadol.  Vimos que as duas features  SepalLengthCm e PetalLengthCm sozinhas praticamente conseguem definir a separação das três classes mas queremos um classificador que faça o maior acerto possíve por isto vamos usar as 4 features que dispomos (O campo **Id ** será descartado para análises).  A biblioteca sckit-learn é muito útil para preparação de dados e para algoritmos que não envolvam redes neurais.
#
# Com dados estruturados os modelos baseados em Regressão, Decisiona Tree e Random Forests são mais indicados. Mas qui vou somente por questão de didática vamos usar uma rede neural de 3 neurônios de com 1 saída (uma para cada tipo de flor) com 4 entradas (uma para cada feature de entrada), em algorítmos de classificação o número de saídas deve ser igual ao número de classes - se não for uma classificação binária(0 ou 1, true ou false, etc).
#
# Não se preocupe caso não compreenda completamente alguma parte do código, vamos explorar todos detalhes nos próximos Labs, o objetivo aqui é você ver etapas gerais de uma solução completa usando Keras e TensorFlow.

# In[ ]:

#veja que ao importar o keras que é um wrapper, o TensorFlow será exibido como backend
from sklearn.model_selection import train_test_split
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from keras.optimizers import SGD
from keras.utils import np_utils

# ## 6 - Feature Engineering
#
# Nesta fase temos a seleção das features que vão compor nosso Modelo - e seu ajuste para compatibilidade com o formato de entrada do tipo de algorítm ML selecionado.  Nossa coluna **'y'**, por exemplo contém valores de 1 a 3, esses valores serão transformados para 0 a 2 e convertidos em três colunas no formato** One-Hot**( esse tipo de codificação será explicada nos próximos labs). Além disso em muitos casos vamos ter que normalizar os valores de entrada antes entregar para uma rede neural ou algum outro tipo de algoritmo de ML.

# In[ ]:

#Número de classes possíveis
n_classes = len(df_iris['Species'].unique()) # 3 classes

#Fazemos slice do Dataframe e as convertemos em matrizes do NumPy
x_full = np.array(df_iris.iloc[:, 1:5].values) # selecionamos as colunas de features e todas linhas
y_full = np.array(df_iris.iloc[:, 6].values) # selecionamos todas linhas mas apenas a coluna 'y'

# Para algorítimos de classificação com mais de duas classes temos que usar one-hot
# aqui uso uma simples subtração para alterar os valores de todas a linhas y
y_full = np_utils.to_categorical(y_full - 1, n_classes)

print("Vericamos se as matrizes de entrada possuem o formato correto")
print("x_full.shape ={}    y_full.shape ={}".format(x_full.shape, y_full.shape))

# ## 7 - Split do dataset em treino e validação

# In[ ]:

seed = 42 # aqui ficamos o seed randômico, para garantir a reprodução de resultados

# A separação do dataset é uma técnica muito importante para maior eficiência
# da validação da eficácia de um modelo e veremos em maior detalhe nos próximos labs.
X_train, X_val, y_train, y_val = train_test_split(x_full, y_full,
                                                test_size=0.2, random_state=seed)

# A classe train_test_split faz o embaralhamento dos dados antes
print("Novamente validamos os formatos do split:")
print(X_train.shape, y_train.shape); print(X_val.shape, y_val.shape)

# ## 8 - Definindo a arquitetura de nossa Rede Neural

# In[ ]:

# Fixamos o seed para biblioteca randômica do NumPy
np.random.seed(seed)

#Cada implementação de um algorítimo de ML chamamos de modelo
model = Sequential()  # modelo sequencial
model.add(Dense(n_classes, input_shape=(4,))) # cria uma camada com 3 neurônios
model.add(Activation('softmax')) # usamos ativação de threshold conhecida como softmax
model.summary()

# Se olharmos acima notamos a existência de 15 parâmetros treináveis. Cada um dos três neurônios possuem 4 entradas, uma para cada feature, portanto teremos 4 x 3  = 12, ou seja 12 pesos que devem ser treinados. E de onde vem os 15?
#
# Lembra que para cada neurônio vamos ter uma entrada $x_{0}$ igual a 1 e um peso peso $w_{0}$ que será seu **bias**?   Então como temos 3 neurônios teremos 3 biases a serem treinados. Dai 12 pesos + 3 biases, resultando em 15 parâmetros treináveis.

# ## 9 - Compilando e Treinando nosso Modelo (Finalmente!!)

# In[ ]:

import timeit

n_epoch = 500 # Número de Épocas
batch_size = 10 #tamanho do Batch (quantidade de amostras por lote de treino)

#Aqui vamos usar o Gradiente Descendente Estocástico, que é um tipo de otimizador
sgd = SGD(lr= 0.1) # lr é o Learning Rate conceito que vamos ver nos próximos labs.

#Todo modelo precisa ser compilado, veja que no parâmetro loss informamos a função de erro
model.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['accuracy'])

#Inicia contagem do tempo
start = timeit.default_timer()

# Aqui fazemos o fit do modelo e salvamos o resultado de cada epoca em history
history = model.fit(X_train, y_train, batch_size=batch_size, epochs=n_epoch,verbose=0)

#Inicia contagem do tempo
elapsed = timeit.default_timer() - start

print("Rede Treinada em {} épocas durante {:.4f} segundos".format(n_epoch, elapsed))

# ## 10 - Avaliando o quão inteligente é nosso algorítimo

# In[ ]:

_, train_accuracy = model.evaluate(X_train, y_train, verbose=0)
_, val_accuracy = model.evaluate(X_val, y_val, verbose=0)

print('Acurácia no Treino: {:.2f}%'.format(train_accuracy * 100))
print('Acurácia na Validação: {:.2f}%'.format(val_accuracy * 100))

# A acurácia é uma métrica que indica em percentual quantas amostras do todo foram classificadas corretamente.
# ***Acurária = Número de Acertos / Número de Testes***
#
# Em nosso dataset de treino conseguimos 117 acertos em 120 amostras (ou testes); e 30 acertos em 30 amostras no conjunto de teste. Com isso temos uma acurácia de 97,5% no treino e de 100% no dataset de teste.
#
# Uau!!! Um excelente resultado com cerca de 5 segundos de treino e uma rede de somente 3 neurônios e um mínimo ajuste de **hiperparâmetro**, o **Learning Rate** (LR=0.1).
#
# Hiperparâmetros serão tema para um próximo lab. Fiquem a vontade para fazer Fork desse Kernel e testar valores diferentes para número de épocas, batch size e tipo de otimizador.

# ** Matrix de Confusão**
#
# Esta é uma outra forma de visualizar a acurácia de uma rede, geralmente aplicamos somente no dataset de validação.
# Aqui apliquei nos dois para poder exibir onde nosso algoritmo errou.

# In[ ]:

y_hat_train = model.predict_classes(X_train)
pd.crosstab(y_hat_train, np.argmax(y_train, axis=1))

# In[ ]:

y_hat_val = model.predict_classes(X_val)
pd.crosstab(y_hat_val, np.argmax(y_val, axis=1))

# Se olharmos as duas matrizes de confusão veremos que nosso modelo errou apenas 3 amostras das 150. Um ótimo feito para uma rede de penas uma camada densa de 3 neurônios.

# ## 11 - Verificando a curva de aprendizagem de sua rede
# É possível verificar que a partir da época 300 não há grande melhoria (diminuição do erro)

# In[ ]:

# a impressão dos valores de perda a cada época de treinamento
# permite ter valiosos insights sobre como seu modelo se comporta durante o treinamento
plt.figure(figsize=(10,8))
plt.plot(history.history['loss'], label='Erro')
plt.plot(history.history['acc'], label='Acurácia')
plt.legend(loc='upper center')
plt.show()

# ## Tarefas do Lab
#
# Crie um fork deste notebook em sua conta (assim você trabalha em sua própria cópia), e nos quadros abaixo escreva código para carregar o dataset  **House Sales in King County** (kc_house_data.csv) que já está copiado na pasta **../input/housesalesprediction** .  Se necessário crie novas células com Alt + Enter.
#

# ### 1 - Carregar o Dataset House Sales de King County

# In[ ]:

print('Arquivo House Sales:')
print(check_output(["ls", "../input/housesalesprediction"]).decode("utf8"))

# In[ ]:

#Usando pandas crie um dataframe para armazenar o dataset House Sales
#df_house = pd.read_csv...

# ### 2 - Exibir as 20 primeiras linhas e as últimas 5 do data frame df_house

# In[ ]:

#Crie seu código abaixo

# ### 3 - Adicione uma nova coluna no dataframe com o nome 'yearsale' (o campo date possui a data da venda)

# In[ ]:

#

# ### 4 - Criar um gráfico que relacione o ano de cosntrução (yr_built) com o valor da venda (price)
# Aqui é possível utilizar a função plot ou scatter, veja qual funciona melhor.

# In[ ]:

#

# ### 5 - Mostrar o histograma de distribuição das vendas de acordo com o local (zipcode), preço de venda (price) e tamanho das casas (sqft_living)

# In[ ]:

# ### 6 - Avaliando de forma geral o conteúdo deste dataset qual ou quais colunas você acredita que tenha maior impacto sobre o valor da venda do imóvel?  Correlacione essas colunas com a coluna price. Plote gráficos que justifiquem sua resposta.

# In[ ]:

# ### 7 - Em ML recorremos ao conceito estatístico *Outlier*. Dada uma série de dados uma amostra que possua um valor muito destoante do restante é considerado um *outlier*.   Em algumas análises reconhecer outliers pode ser de grande ajuda para entender a natureza dos dados a serem explorados.  Como você faria para identificar a existência de outliers ao verificarmos o valor das vendas deste dataset?  Dica tente usar gráficos scatter e hist.

# In[ ]:

# ### 8 - Usando Python e NumPy calcule o valor médio do square feet (pode utilizar a coluna sqft_living para o cálculo) e crie um gráfico para exibir todas as amostras cujo valor do square feet de venda seja maior que o valor médio.

# In[ ]:

#

#

#

#

#

#


#!/usr/bin/env python
# coding: utf-8

# # Applying statistical modeling and machine learning to perform time-series forecasting.

# Tamara Louie
# PyData LA
# October 2018

# # Time-series data due diligence

# ## Read in my data
# **Please download from http://insideairbnb.com/get-the-data.html the following CSV file (23 MB) under the "Los Angeles" section:**
#
# 	reviews.csv
#
#
# You should have **reviews.csv** stored somewhere locally.
#
# Run the cell below, and click on the button **Choose Files**, to select your **reviews.csv** to upload.
#
# *Note: It will take some time to upload this data, so please start your upload and this upload will run for a couple of minutes.*

# In[6]:

import pandas as pd

uploaded = pd.read_csv("reviews.csv")

for fn in uploaded.keys():
  print('User uploaded file "{name}" with length {length} bytes'.format(
      name=fn, length=len(uploaded[fn])))

# ## Import some relevant packages
# Ideally, one should set up their own virtual environment and determine the versions of each library that they are using.  Here, we will assume that the colaboratory environment has some shared environment with access to common Python libraries and the ability to install other libraries necessary.

# In[7]:

# import packages
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
from IPython.display import display, HTML, display_html
import seaborn as sns
import datetime

# set formatting
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_rows', 100)

# read in CSV file data
df = pd.read_csv('reviews.csv')

# ## Look at my data
# - How many rows are in the dataset?
# - How many columns are in this dataset?
# - What data types are the columns?
# - Is the data complete? Are there nulls? Do we have to infer values?
# - What is the definition of these columns?
# - What are some other caveats to the data?
#

# In[8]:

# look at data
display(df.head())

# look a shape of data
display(df.shape)

# look at data types. Ideally look at all rows. Only look at first five here for minimal output.
display(df.iloc[:5,:5].dtypes)

# see if any columns have nulls. Ideally look at all rows. Only look at first five here for minimal output.
display(df.iloc[:5,:5].isnull().any())

# display descriptive statistics
display(df.describe(percentiles=[0.25,0.5,0.75,0.85,0.95,0.99]))

# ## What are some questions I can answer with this data?
#
# Understand the limitations of your data and what potential questions can be answered by data is important.  These questions can reduce, expand, or modify the scope of your project.
#
# If you defined a scope or goal for your project before digging into the data, this might be a good time to revisit it.
#
# **Data.** We have daily count of reviews for given listing ids for given dates.
#
# **Questions I could try to answer. **
#
# *   Forecast future number of reviews for the Los Angeles area.
# * Forecast the future number of reviews for specific listings in the Los Angeles area.

# ## What techniques may help answer these questions?
# ### Statistical models
# *   **Ignore the time-series aspect completely and model using traditional statistical modeling toolbox.**
#   *   *Examples.* Regression-based models.
# *   **Univariate statistical time-series modeling.**
#   *   *Examples.* Averaging and smoothing models, ARIMA models.
# *   **Slight modifications to univariate statistical time-series modeling.**
#   *    *Examples.* External regressors, multi-variate models.
# *   **Additive or component models.**
#   *  *Examples.* Facebook Prophet package.
# *   **Structural time series modeling.**
#   *    *Examples.* Bayesian structural time series modeling, hierarchical time series modeling.
#
# ### Machine learning models
#
# *   **Ignore the time-series aspect completely and model using traditional machine learning modeling toolbox.**
#   *   *Examples.* Support Vector Machines (SVMs), Random Forest Regression, Gradient-Boosted Decision Trees (GBDTs).
# *   **Hidden markov models (HMMs).**
# *   **Other sequence-based models.**
# *   **Gaussian processes (GPs).**
# *   **Recurrent neural networks (RNNs).**
#
# ### Additional data considerations before choosing a model
# *   Whether or not to incorporate external data
# *   Whether or not to keep as univariate or multivariate (i.e., which features and number of features)
# *   Outlier detection and removal
# *   Missing value imputation

# # Let's analyze some time-series data!
#
# - [Link 1](https://www.analyticsvidhya.com/blog/2016/02/time-series-forecasting-codes-python/)
# - [Link 2](https://content.nexosis.com/blog/methods-of-demand-forecasting-bsts-prophet)

# ## Process my data

# In[9]:

# Rename columns
df = df.rename(columns = {'date': 'ds', 'listing_id': 'ts'})

# Group data by number of listings per date
df_example = df.groupby(by = 'ds').agg({'ts': 'count'})

# Change index to datetime
df_example.index = pd.to_datetime(df_example.index)

# Set frequency of time series
df_example = df_example.asfreq(freq='1D')

# Sort the values
df_example = df_example.sort_index(ascending = True)

# Fill values with 0
df_example = df_example.fillna(value = 0)

# Show the end of the data
display(df_example.tail())

# ## Plot my data
# - There does appear to be an overall increasing trend.
# - There appears to be some differences in the variance over time.
# - There may be some seasonality (i.e., cycles) in the data.
# - Not sure about outliers.

# In[10]:

# Plot time series data
f, ax = plt.subplots(1,1)
ax.plot(df_example['ts'])

# Add title
ax.set_title('Time-series graph for 1 time-series example')

# Rotate x-labels
ax.tick_params(axis = 'x', rotation = 45)

# Show graph
plt.show()
plt.close()

# ## Look at stationarity
# Most time-series models assume that the underlying time-series data is **stationary**.  This assumption gives us some nice statistical properties that allows us to use various models for forecasting.
#
# **Stationarity** is a statistical assumption that a time-series has:
# *   **Constant mean**
# *   **Constant variance**
# *   **Autocovariance does not depend on time**
#
# More simply put, if we are using past data to predict future data, we should assume that the data will follow the same general trends and patterns as in the past.  This general statement holds for most training data and modeling tasks.
#
# **There are some good diagrams and explanations on stationarity [here](https://www.analyticsvidhya.com/blog/2015/12/complete-tutorial-time-series-modeling/) and [here](https://people.duke.edu/~rnau/411diff.htm).**
#
# Sometimes we need to transform the data in order to make it stationary.  However, this  transformation then calls into question if this data is truly stationary and is suited to be modeled using these techniques.
#
# **Looking at our data:**
# - Rolling mean and standard deviation look like they change over time.  There may be some de-trending and removing seasonality involved. Based on **Dickey-Fuller test**, because p = 0.31, we fail to reject the null hypothesis (that the time series is not stationary) at the p = 0.05 level, thus concluding that we fail to reject the null hypothesis that our **time series is not stationary**.

# In[14]:

from statsmodels.tsa.stattools import adfuller
def test_stationarity(df, ts):
    """
    Test stationarity using moving average statistics and Dickey-Fuller test
    Source: https://www.analyticsvidhya.com/blog/2016/02/time-series-forecasting-codes-python/
    """

    # Determing rolling statistics
    rolmean = df[ts].rolling(window = 12, center = False).mean()
    rolstd = df[ts].rolling(window = 12, center = False).std()

    # Plot rolling statistics:
    orig = plt.plot(df[ts],
                    color = 'blue',
                    label = 'Original')
    mean = plt.plot(rolmean,
                    color = 'red',
                    label = 'Rolling Mean')
    std = plt.plot(rolstd,
                   color = 'black',
                   label = 'Rolling Std')
    plt.legend(loc = 'best')
    plt.title('Rolling Mean & Standard Deviation for %s' %(ts))
    plt.xticks(rotation = 45)
    plt.show(block = False)
    plt.close()

    # Perform Dickey-Fuller test:
    # Null Hypothesis (H_0): time series is not stationary
    # Alternate Hypothesis (H_1): time series is stationary
    print('Results of Dickey-Fuller Test:')
    dftest = adfuller(df[ts],
                      autolag='AIC')
    dfoutput = pd.Series(dftest[0:4],
                         index = ['Test Statistic',
                                  'p-value',
                                  '# Lags Used',
                                  'Number of Observations Used'])
    for key, value in dftest[4].items():
        dfoutput['Critical Value (%s)'%key] = value
    print dfoutput

# In[12]:

test_stationarity(df = df_example, ts = 'ts')

# ## Correct for stationarity
#
# It is common for time series data to have to correct for non-stationarity.
#
# 2 common reasons behind non-stationarity are:
#
# 1. **Trend** – mean is not constant over time.
# 2. **Seasonality** – variance is not constant over time.
#
# There are ways to correct for trend and seasonality, to make the time series stationary.

# **What happens if you do not correct for these things?**
#
# Many things can happen, including:
# - Variance can be mis-specified
# - Model fit can be worse.
# - Not leveraging valuable time-dependent nature of the data.
#
# Here are some resources on the pitfalls of using traditional methods for time series analysis.
# [Quora link](https://www.quora.com/Why-cant-you-use-linear-regression-for-time-series-data)
# [Quora link](https://www.quora.com/Data-Science-Can-machine-learning-be-used-for-time-series-analysis)
#

# ## Eliminating trend and seasonality
# *   **Transformation**
#   *   *Examples.* Log, square root, etc.
#   *   We are going to look at log.
# *   **Smoothing**
#   *  *Examples.* Weekly average, monthly average, rolling averages.
#   *   We are going to look at weekly average.
# *   **Differencing**
#   *  *Examples.* First-order differencing.
#   *   We are going to look at first-order differencing.
# *   **Polynomial Fitting**
#   *  *Examples.* Fit a regression model.
# *   **Decomposition**

# ## Transformation, Smoothing, and Differencing
# **Looking at our data:**
# - Applying log transformation, weekly moving average smoothing, and differencing made the data more stationary over time. Based on **Dickey-Fuller test**, because p = < 0.05, we fail to reject the null hypothesis (that the time series is not stationary) at the p = 0.05 level, thus concluding that the **time series is stationary**.

# In[1]:

def plot_transformed_data(df, ts, ts_transform):
  """
  Plot transformed and original time series data
  """
  # Plot time series data
  f, ax = plt.subplots(1,1)
  ax.plot(df[ts])
  ax.plot(df[ts_transform], color = 'red')

  # Add title
  ax.set_title('%s and %s time-series graph' %(ts, ts_transform))

  # Rotate x-labels
  ax.tick_params(axis = 'x', rotation = 45)

  # Add legend
  ax.legend([ts, ts_transform])

  plt.show()
  plt.close()

  return

# In[2]:

# Transformation - log ts
df_example['ts_log'] = df_example['ts'].apply(lambda x: np.log(x))

# Transformation - 7-day moving averages of log ts
df_example['ts_log_moving_avg'] = df_example['ts_log'].rolling(window = 7,
                                                               center = False).mean()

# Transformation - 7-day moving average ts
df_example['ts_moving_avg'] = df_example['ts'].rolling(window = 7,
                                                       center = False).mean()

# Transformation - Difference between logged ts and first-order difference logged ts
# df_example['ts_log_diff'] = df_example['ts_log'] - df_example['ts_log'].shift()
df_example['ts_log_diff'] = df_example['ts_log'].diff()

# Transformation - Difference between ts and moving average ts
df_example['ts_moving_avg_diff'] = df_example['ts'] - df_example['ts_moving_avg']

# Transformation - Difference between logged ts and logged moving average ts
df_example['ts_log_moving_avg_diff'] = df_example['ts_log'] - df_example['ts_log_moving_avg']

# Transformation - Difference between logged ts and logged moving average ts
df_example_transform = df_example.dropna()

# Transformation - Logged exponentially weighted moving averages (EWMA) ts
df_example_transform['ts_log_ewma'] = df_example_transform['ts_log'].ewm(halflife = 7,
                                                                         ignore_na = False,
                                                                         min_periods = 0,
                                                                         adjust = True).mean()

# Transformation - Difference between logged ts and logged EWMA ts
df_example_transform['ts_log_ewma_diff'] = df_example_transform['ts_log'] - df_example_transform['ts_log_ewma']

# Display data
display(df_example_transform.head())

# Plot data
plot_transformed_data(df = df_example,
                      ts = 'ts',
                      ts_transform = 'ts_log')
# Plot data
plot_transformed_data(df = df_example,
                      ts = 'ts_log',
                      ts_transform = 'ts_log_moving_avg')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts',
                      ts_transform = 'ts_moving_avg')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts_log',
                      ts_transform = 'ts_log_diff')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts',
                      ts_transform = 'ts_moving_avg_diff')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts_log',
                      ts_transform = 'ts_log_moving_avg_diff')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts_log',
                      ts_transform = 'ts_log_ewma')

# Plot data
plot_transformed_data(df = df_example_transform,
                      ts = 'ts_log',
                      ts_transform = 'ts_log_ewma_diff')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_moving_avg')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log_moving_avg')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log_diff')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_moving_avg_diff')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log_moving_avg_diff')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log_ewma')

# Perform stationarity test
test_stationarity(df = df_example_transform,
                  ts = 'ts_log_ewma_diff')

# ## Decomposition: trend, seasonality, residuals
# **Looking at our data:**
# - De-trending and de-seasonalizing made the data (i.e., the residuals) more stationary over time. Based on **Dickey-Fuller test**, because p = < 0.05, we fail to reject the null hypothesis (that the time series is not stationary) at the p = 0.05 level, thus concluding that the **time series is stationary**.

# In[23]:

def plot_decomposition(df, ts, trend, seasonal, residual):
  """
  Plot time series data
  """
  f, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2,2, figsize = (15, 5), sharex = True)

  ax1.plot(df[ts], label = 'Original')
  ax1.legend(loc = 'best')
  ax1.tick_params(axis = 'x', rotation = 45)

  ax2.plot(df[trend], label = 'Trend')
  ax2.legend(loc = 'best')
  ax2.tick_params(axis = 'x', rotation = 45)

  ax3.plot(df[seasonal],label = 'Seasonality')
  ax3.legend(loc = 'best')
  ax3.tick_params(axis = 'x', rotation = 45)

  ax4.plot(df[residual], label = 'Residuals')
  ax4.legend(loc = 'best')
  ax4.tick_params(axis = 'x', rotation = 45)
  plt.tight_layout()

  # Show graph
  plt.suptitle('Trend, Seasonal, and Residual Decomposition of %s' %(ts),
               x = 0.5,
               y = 1.05,
               fontsize = 18)
  plt.show()
  plt.close()

  return

# In[24]:

from statsmodels.tsa.seasonal import seasonal_decompose
decomposition = seasonal_decompose(df_example_transform['ts_log'], freq = 365)

df_example_transform.loc[:,'trend'] = decomposition.trend
df_example_transform.loc[:,'seasonal'] = decomposition.seasonal
df_example_transform.loc[:,'residual'] = decomposition.resid

plot_decomposition(df = df_example_transform,
                   ts = 'ts_log',
                   trend = 'trend',
                   seasonal = 'seasonal',
                   residual = 'residual')

test_stationarity(df = df_example_transform.dropna(), ts = 'residual')

# # Let us model some time-series data! Finally! ARIMA models.
#
# We will be doing an example here! We can use ARIMA models when we know there is dependence between values and we can leverage that information to forecast.
#
# **ARIMA = Auto-Regressive Integrated Moving Average**.
# **Assumptions.** The time-series is stationary.
# **Depends on:**
#   **1. Number of AR (Auto-Regressive) terms (p).**
#   **2. Number of I (Integrated or Difference) terms (d).**
#   **3. Number of MA (Moving Average) terms (q).**

# ## ACF and PACF Plots
# **How do we determine p, d, and q?**
# For p and q, we can use ACF and PACF plots (below).
#
# **Autocorrelation Function (ACF).** Correlation between the time series with a lagged version of itself (e.g., correlation of Y(t) with Y(t-1)).
#
# **Partial Autocorrelation Function (PACF).** Additional correlation explained by each successive lagged term.
#
# **How do we interpret ACF and PACF plots?**
# - p – Lag value where the PACF chart crosses the upper confidence interval for the first time.
# - q – Lag value where the ACF chart crosses the upper confidence interval for the first time.
#

# In[25]:

def plot_acf_pacf(df, ts):
  """
  Plot auto-correlation function (ACF) and partial auto-correlation (PACF) plots
  """
  f, (ax1, ax2) = plt.subplots(1,2, figsize = (10, 5))

  #Plot ACF:

  ax1.plot(lag_acf)
  ax1.axhline(y=0,linestyle='--',color='gray')
  ax1.axhline(y=-1.96/np.sqrt(len(df[ts])),linestyle='--',color='gray')
  ax1.axhline(y=1.96/np.sqrt(len(df[ts])),linestyle='--',color='gray')
  ax1.set_title('Autocorrelation Function for %s' %(ts))

  #Plot PACF:
  ax2.plot(lag_pacf)
  ax2.axhline(y=0,linestyle='--',color='gray')
  ax2.axhline(y=-1.96/np.sqrt(len(df[ts])),linestyle='--',color='gray')
  ax2.axhline(y=1.96/np.sqrt(len(df[ts])),linestyle='--',color='gray')
  ax2.set_title('Partial Autocorrelation Function for %s' %(ts))

  plt.tight_layout()
  plt.show()
  plt.close()

  return

# In[26]:

#ACF and PACF plots:
from statsmodels.tsa.stattools import acf, pacf

# determine ACF and PACF
lag_acf = acf(np.array(df_example_transform['ts_log_diff']), nlags = 20)
lag_pacf = pacf(np.array(df_example_transform['ts_log_diff']), nlags = 20)

# plot ACF and PACF
plot_acf_pacf(df = df_example_transform, ts = 'ts_log_diff')

# In[27]:

def run_arima_model(df, ts, p, d, q):
  """
  Run ARIMA model
  """
  from statsmodels.tsa.arima_model import ARIMA

  # fit ARIMA model on time series
  model = ARIMA(df[ts], order=(p, d, q))
  results_ = model.fit(disp=-1)

  # get lengths correct to calculate RSS
  len_results = len(results_.fittedvalues)
  ts_modified = df[ts][-len_results:]

  # calculate root mean square error (RMSE) and residual sum of squares (RSS)
  rss = sum((results_.fittedvalues - ts_modified)**2)
  rmse = np.sqrt(rss / len(df[ts]))

  # plot fit
  plt.plot(df[ts])
  plt.plot(results_.fittedvalues, color = 'red')
  plt.title('For ARIMA model (%i, %i, %i) for ts %s, RSS: %.4f, RMSE: %.4f' %(p, d, q, ts, rss, rmse))

  plt.show()
  plt.close()

  return results_

# In[28]:

# Note: I do the differencing in the transformation of the data 'ts_log_diff'
# AR model with 1st order differencing - ARIMA (1,0,0)
model_AR = run_arima_model(df = df_example_transform,
                           ts = 'ts_log_diff',
                           p = 1,
                           d = 0,
                           q = 0)

# MA model with 1st order differencing - ARIMA (0,0,1)
model_MA = run_arima_model(df = df_example_transform,
                           ts = 'ts_log_diff',
                           p = 0,
                           d = 0,
                           q = 1)

# ARMA model with 1st order differencing - ARIMA (1,0,1)
model_MA = run_arima_model(df = df_example_transform,
                           ts = 'ts_log_diff',
                           p = 1,
                           d = 0,
                           q = 1)

# # Let us model some time-series data! Finally! Facebook Prophet package.
#
# We will be doing an example here! Installing the necessary packages might take a couple of minutes.  In the meantime, I can talk a bit about [Facebook Prophet](https://facebook.github.io/prophet/), a tool that allows folks to forecast using additive or component models relatively easily.  It can also include things like:
# * Day of week effects
# * Day of year effects
# * Holiday effects
# * Trend trajectory
# * Can do MCMC sampling

# In[29]:

get_ipython().system('pip install pystan')
get_ipython().system('pip install fbprophet')
from fbprophet import Prophet
import datetime
from datetime import datetime

# In[30]:

def days_between(d1, d2):
    """Calculate the number of days between two dates.  D1 is start date (inclusive) and d2 is end date (inclusive)"""
    d1 = datetime.strptime(d1, "%Y-%m-%d")
    d2 = datetime.strptime(d2, "%Y-%m-%d")
    return abs((d2 - d1).days + 1)

# In[31]:

# Inputs for query

date_column = 'dt'
metric_column = 'ts'
table = df_example
start_training_date = '2010-07-03'
end_training_date = '2018-09-08'
start_forecasting_date = '2018-09-09'
end_forecasting_date = '2018-12-31'
year_to_estimate = '2018'

# Inputs for forecasting

# future_num_points
# If doing different time intervals, change future_num_points
future_num_points = days_between(start_forecasting_date, end_forecasting_date)

cap = None # 2e6

# growth: default = 'linear'
# Can also choose 'logistic'
growth = 'linear'

# n_changepoints: default = 25, uniformly placed in first 80% of time series
n_changepoints = 25

# changepoint_prior_scale: default = 0.05
# Increasing it will make the trend more flexible
changepoint_prior_scale = 0.05

# changpoints: example = ['2016-01-01']
changepoints = None

# holidays_prior_scale: default = 10
# If you find that the holidays are overfitting, you can adjust their prior scale to smooth them
holidays_prior_scale = 10

# interval_width: default = 0.8
interval_width = 0.8

# mcmc_samples: default = 0
# By default Prophet will only return uncertainty in the trend and observation noise.
# To get uncertainty in seasonality, you must do full Bayesian sampling.
# Replaces typical MAP estimation with MCMC sampling, and takes MUCH LONGER - e.g., 10 minutes instead of 10 seconds.
# If you do full sampling, then you will see the uncertainty in seasonal components when you plot:
mcmc_samples = 0

# holiday: default = None
# thanksgiving = pd.DataFrame({
#   'holiday': 'thanksgiving',
#   'ds': pd.to_datetime(['2014-11-27', '2015-11-26',
#                         '2016-11-24', '2017-11-23']),
#   'lower_window': 0,
#   'upper_window': 4,
# })
# christmas = pd.DataFrame({
#   'holiday': 'christmas',
#   'ds': pd.to_datetime(['2014-12-25', '2015-12-25',
#                         '2016-12-25','2017-12-25']),
#   'lower_window': -1,
#   'upper_window': 0,
# })
# holidays = pd.concat((thanksgiving,christmas))
holidays = None

daily_seasonality = True

# In[32]:

# get relevant data - note: could also try this with ts_log_diff
df_prophet = df_example_transform[['ts']] # can try with ts_log_diff

# reset index
df_prophet = df_prophet.reset_index()

# rename columns
df_prophet = df_prophet.rename(columns = {'ds': 'ds', 'ts': 'y'}) # can try with ts_log_diff

# Change 'ds' type from datetime to date (necessary for FB Prophet)
df_prophet['ds'] = pd.to_datetime(df_prophet['ds'])

# Change 'y' type to numeric (necessary for FB Prophet)
df_prophet['y'] = pd.to_numeric(df_prophet['y'], errors='ignore')

# Remove any outliers
# df.loc[(df_['ds'] > '2016-12-13') & (df_['ds'] < '2016-12-19'), 'y'] = None

# In[33]:

def create_daily_forecast(df,
#                           cap,
                          holidays,
                          growth,
                          n_changepoints = 25,
                          changepoint_prior_scale = 0.05,
                          changepoints = None,
                          holidays_prior_scale = 10,
                          interval_width = 0.8,
                          mcmc_samples = 1,
                          future_num_points = 10,
                          daily_seasonality = True):
  """
  Create forecast
  """

  # Create copy of dataframe
  df_ = df.copy()

  # Add in growth parameter, which can change over time
  #     df_['cap'] = max(df_['y']) if cap is None else cap

  # Create model object and fit to dataframe
  m = Prophet(growth = growth,
              n_changepoints = n_changepoints,
              changepoint_prior_scale = changepoint_prior_scale,
              changepoints = changepoints,
              holidays = holidays,
              holidays_prior_scale = holidays_prior_scale,
              interval_width = interval_width,
              mcmc_samples = mcmc_samples,
              daily_seasonality = daily_seasonality)

  # Fit model with dataframe
  m.fit(df_)

  # Create dataframe for predictions
  future = m.make_future_dataframe(periods = future_num_points)
  #     future['cap'] = max(df_['y']) if cap is None else cap

  # Create predictions
  fcst = m.predict(future)

  # Plot
  m.plot(fcst);
  m.plot_components(fcst)

  return fcst

# In[ ]:

fcst = create_daily_forecast(df_prophet,
#                              cap,
                             holidays,
                             growth,
                             n_changepoints,
                             changepoint_prior_scale,
                             changepoints,
                             holidays_prior_scale,
                             interval_width,
                             mcmc_samples,
                             future_num_points,
                             daily_seasonality)

# In[ ]:

def calculate_mape(y_true, y_pred):
    """ Calculate mean absolute percentage error (MAPE)"""
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100

def calculate_mpe(y_true, y_pred):
    """ Calculate mean percentage error (MPE)"""
    return np.mean((y_true - y_pred) / y_true) * 100

def calculate_mae(y_true, y_pred):
    """ Calculate mean absolute error (MAE)"""
    return np.mean(np.abs(y_true - y_pred)) * 100

def calculate_rmse(y_true, y_pred):
    """ Calculate root mean square error (RMSE)"""
    return np.sqrt(np.mean((y_true - y_pred)**2))

def print_error_metrics(y_true, y_pred):
    print('MAPE: %f'%calculate_mape(y_true, y_pred))
    print('MPE: %f'%calculate_mpe(y_true, y_pred))
    print('MAE: %f'%calculate_mae(y_true, y_pred))
    print('RMSE: %f'%calculate_rmse(y_true, y_pred))
    return

# In[ ]:

print_error_metrics(y_true = df_prophet['y'], y_pred = fcst['yhat'])

# # Let us model some time-series data! Finally! LSTM for regression
#
# We will be going through an example here.
#
# Also, here are some resources on recurrent neural networks (RNN) and Long Short-Term Memory networks (LSTMs):
# * [Link 1](https://machinelearningmastery.com/time-series-prediction-lstm-recurrent-neural-networks-python-keras/)
# * [Link 2](https://blog.statsbot.co/time-series-prediction-using-recurrent-neural-networks-lstms-807fa6ca7f)
# * [Link 3](http://adventuresinmachinelearning.com/recurrent-neural-networks-lstm-tutorial-tensorflow/)

# In[ ]:

def do_lstm_model(df,
                  ts,
                  look_back,
                  epochs,
                  type_ = None,
                  train_fraction = 0.67):
  """
   Create LSTM model
   Source: https://machinelearningmastery.com/time-series-prediction-lstm-recurrent-neural-networks-python-keras/
  """
  # Import packages
  import numpy
  import matplotlib.pyplot as plt
  from pandas import read_csv
  import math
  from keras.models import Sequential
  from keras.layers import Dense
  from keras.layers import LSTM
  from sklearn.preprocessing import MinMaxScaler
  from sklearn.metrics import mean_squared_error

  # Convert an array of values into a dataset matrix
  def create_dataset(dataset, look_back=1):
    """
    Create the dataset
    """
    dataX, dataY = [], []
    for i in range(len(dataset)-look_back-1):
      a = dataset[i:(i+look_back), 0]
      dataX.append(a)
      dataY.append(dataset[i + look_back, 0])
    return numpy.array(dataX), numpy.array(dataY)

  # Fix random seed for reproducibility
  numpy.random.seed(7)

  # Get dataset
  dataset = df[ts].values
  dataset = dataset.astype('float32')

  # Normalize the dataset
  scaler = MinMaxScaler(feature_range=(0, 1))
  dataset = scaler.fit_transform(dataset.reshape(-1, 1))

  # Split into train and test sets
  train_size = int(len(dataset) * train_fraction)
  test_size = len(dataset) - train_size
  train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]

  # Reshape into X=t and Y=t+1
  look_back = look_back
  trainX, trainY = create_dataset(train, look_back)
  testX, testY = create_dataset(test, look_back)

  # Reshape input to be [samples, time steps, features]
  if type_ == 'regression with time steps':
    trainX = numpy.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
    testX = numpy.reshape(testX, (testX.shape[0], testX.shape[1], 1))
  elif type_ == 'stacked with memory between batches':
    trainX = numpy.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
    testX = numpy.reshape(testX, (testX.shape[0], testX.shape[1], 1))
  else:
    trainX = numpy.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
    testX = numpy.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

  # Create and fit the LSTM network
  batch_size = 1
  model = Sequential()

  if type_ == 'regression with time steps':
    model.add(LSTM(4, input_shape=(look_back, 1)))
  elif type_ == 'memory between batches':
    model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
  elif type_ == 'stacked with memory between batches':
    model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True, return_sequences=True))
    model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
  else:
    model.add(LSTM(4, input_shape=(1, look_back)))

  model.add(Dense(1))
  model.compile(loss='mean_squared_error', optimizer='adam')

  if type_ == 'memory between batches' or type_ == 'stacked with memory between batches':
    for i in range(100):
      model.fit(trainX, trainY, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
      model.reset_states()
  else:
    model.fit(trainX,
              trainY,
              epochs = epochs,
              batch_size = 1,
              verbose = 2)

  # Make predictions
  if type_ == 'memory between batches' or type_ == 'stacked with memory between batches':
    trainPredict = model.predict(trainX, batch_size=batch_size)
    testPredict = model.predict(testX, batch_size=batch_size)
  else:
    trainPredict = model.predict(trainX)
    testPredict = model.predict(testX)

  # Invert predictions
  trainPredict = scaler.inverse_transform(trainPredict)
  trainY = scaler.inverse_transform([trainY])
  testPredict = scaler.inverse_transform(testPredict)
  testY = scaler.inverse_transform([testY])

  # Calculate root mean squared error
  trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
  print('Train Score: %.2f RMSE' % (trainScore))
  testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
  print('Test Score: %.2f RMSE' % (testScore))

  # Shift train predictions for plotting
  trainPredictPlot = numpy.empty_like(dataset)
  trainPredictPlot[:, :] = numpy.nan
  trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict

  # Shift test predictions for plotting
  testPredictPlot = numpy.empty_like(dataset)
  testPredictPlot[:, :] = numpy.nan
  testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict

  # Plot baseline and predictions
  plt.plot(scaler.inverse_transform(dataset))
  plt.plot(trainPredictPlot)
  plt.plot(testPredictPlot)
  plt.show()
  plt.close()

  return

# In[ ]:

# LSTM Network for Regression
do_lstm_model(df = df_prophet,
              ts = 'y',
              look_back = 1,
              epochs = 5)

# LSTM for Regression Using the Window Method
do_lstm_model(df = df_prophet,
              ts = 'y',
              look_back = 3,
              epochs = 5)

# LSTM for Regression with Time Steps
do_lstm_model(df = df_prophet,
              ts = 'y',
              look_back = 3,
              epochs = 5,
              type_ = 'regression with time steps')

# # LSTM with Memory Between Batches
# do_lstm_model(df = df_prophet,
#               ts = 'y',
#               look_back = 3,
#               epochs = 5,
#               type_ = 'memory between batches')

# # Stacked LSTMs with Memory Between Batches
# do_lstm_model(df = df_prophet,
#               ts = 'y',
#               look_back = 3,
#               epochs = 5,
#               type_ = 'stacked with memory between batches')

# In[ ]:


from sympy.core.containers import Tuple
from sympy.core.singleton import S
from sympy.core.symbol import Symbol
from sympy.core.sympify import SympifyError

from types import FunctionType

class TableForm:
    r"""
    Create a nice table representation of data.

    Examples
    ========

    >>> from sympy import TableForm
    >>> t = TableForm([[5, 7], [4, 2], [10, 3]])
    >>> print(t)
    5  7
    4  2
    10 3

    You can use the SymPy's printing system to produce tables in any
    format (ascii, latex, html, ...).

    >>> print(t.as_latex())
    \begin{tabular}{l l}
    $5$ & $7$ \\
    $4$ & $2$ \\
    $10$ & $3$ \\
    \end{tabular}

    """

    def __init__(self, data, **kwarg):
        """
        Creates a TableForm.

        Parameters:

            data ...
                            2D data to be put into the table; data can be
                            given as a Matrix

            headings ...
                            gives the labels for rows and columns:

                            Can be a single argument that applies to both
                            dimensions:

                                - None ... no labels
                                - "automatic" ... labels are 1, 2, 3, ...

                            Can be a list of labels for rows and columns:
                            The labels for each dimension can be given
                            as None, "automatic", or [l1, l2, ...] e.g.
                            ["automatic", None] will number the rows

                            [default: None]

            alignments ...
                            alignment of the columns with:

                                - "left" or "<"
                                - "center" or "^"
                                - "right" or ">"

                            When given as a single value, the value is used for
                            all columns. The row headings (if given) will be
                            right justified unless an explicit alignment is
                            given for it and all other columns.

                            [default: "left"]

            formats ...
                            a list of format strings or functions that accept
                            3 arguments (entry, row number, col number) and
                            return a string for the table entry. (If a function
                            returns None then the _print method will be used.)

            wipe_zeros ...
                            Do not show zeros in the table.

                            [default: True]

            pad ...
                            the string to use to indicate a missing value (e.g.
                            elements that are None or those that are missing
                            from the end of a row (i.e. any row that is shorter
                            than the rest is assumed to have missing values).
                            When None, nothing will be shown for values that
                            are missing from the end of a row; values that are
                            None, however, will be shown.

                            [default: None]

        Examples
        ========

        >>> from sympy import TableForm, Symbol
        >>> TableForm([[5, 7], [4, 2], [10, 3]])
        5  7
        4  2
        10 3
        >>> TableForm([list('.'*i) for i in range(1, 4)], headings='automatic')
          | 1 2 3
        ---------
        1 | .
        2 | . .
        3 | . . .
        >>> TableForm([[Symbol('.'*(j if not i%2 else 1)) for i in range(3)]
        ...            for j in range(4)], alignments='rcl')
            .
          . . .
         .. . ..
        ... . ...
        """
        from sympy.matrices.dense import Matrix

        # We only support 2D data. Check the consistency:
        if isinstance(data, Matrix):
            data = data.tolist()
        _h = len(data)

        # fill out any short lines
        pad = kwarg.get('pad', None)
        ok_None = False
        if pad is None:
            pad = " "
            ok_None = True
        pad = Symbol(pad)
        _w = max(len(line) for line in data)
        for i, line in enumerate(data):
            if len(line) != _w:
                line.extend([pad]*(_w - len(line)))
            for j, lj in enumerate(line):
                if lj is None:
                    if not ok_None:
                        lj = pad
                else:
                    try:
                        lj = S(lj)
                    except SympifyError:
                        lj = Symbol(str(lj))
                line[j] = lj
            data[i] = line
        _lines = Tuple(*[Tuple(*d) for d in data])

        headings = kwarg.get("headings", [None, None])
        if headings == "automatic":
            _headings = [range(1, _h + 1), range(1, _w + 1)]
        else:
            h1, h2 = headings
            if h1 == "automatic":
                h1 = range(1, _h + 1)
            if h2 == "automatic":
                h2 = range(1, _w + 1)
            _headings = [h1, h2]

        allow = ('l', 'r', 'c')
        alignments = kwarg.get("alignments", "l")

        def _std_align(a):
            a = a.strip().lower()
            if len(a) > 1:
                return {'left': 'l', 'right': 'r', 'center': 'c'}.get(a, a)
            else:
                return {'<': 'l', '>': 'r', '^': 'c'}.get(a, a)
        std_align = _std_align(alignments)
        if std_align in allow:
            _alignments = [std_align]*_w
        else:
            _alignments = []
            for a in alignments:
                std_align = _std_align(a)
                _alignments.append(std_align)
                if std_align not in ('l', 'r', 'c'):
                    raise ValueError('alignment "%s" unrecognized' %
                        alignments)
        if _headings[0] and len(_alignments) == _w + 1:
            _head_align = _alignments[0]
            _alignments = _alignments[1:]
        else:
            _head_align = 'r'
        if len(_alignments) != _w:
            raise ValueError(
                'wrong number of alignments: expected %s but got %s' %
                (_w, len(_alignments)))

        _column_formats = kwarg.get("formats", [None]*_w)

        _wipe_zeros = kwarg.get("wipe_zeros", True)

        self._w = _w
        self._h = _h
        self._lines = _lines
        self._headings = _headings
        self._head_align = _head_align
        self._alignments = _alignments
        self._column_formats = _column_formats
        self._wipe_zeros = _wipe_zeros

    def __repr__(self):
        from .str import sstr
        return sstr(self, order=None)

    def __str__(self):
        from .str import sstr
        return sstr(self, order=None)

    def as_matrix(self):
        """Returns the data of the table in Matrix form.

        Examples
        ========

        >>> from sympy import TableForm
        >>> t = TableForm([[5, 7], [4, 2], [10, 3]], headings='automatic')
        >>> t
          | 1  2
        --------
        1 | 5  7
        2 | 4  2
        3 | 10 3
        >>> t.as_matrix()
        Matrix([
        [ 5, 7],
        [ 4, 2],
        [10, 3]])
        """
        from sympy.matrices.dense import Matrix
        return Matrix(self._lines)

    def as_str(self):
        # XXX obsolete ?
        return str(self)

    def as_latex(self):
        from .latex import latex
        return latex(self)

    def _sympystr(self, p):
        """
        Returns the string representation of 'self'.

        Examples
        ========

        >>> from sympy import TableForm
        >>> t = TableForm([[5, 7], [4, 2], [10, 3]])
        >>> s = t.as_str()

        """
        column_widths = [0] * self._w
        lines = []
        for line in self._lines:
            new_line = []
            for i in range(self._w):
                # Format the item somehow if needed:
                s = str(line[i])
                if self._wipe_zeros and (s == "0"):
                    s = " "
                w = len(s)
                if w > column_widths[i]:
                    column_widths[i] = w
                new_line.append(s)
            lines.append(new_line)

        # Check heading:
        if self._headings[0]:
            self._headings[0] = [str(x) for x in self._headings[0]]
            _head_width = max([len(x) for x in self._headings[0]])

        if self._headings[1]:
            new_line = []
            for i in range(self._w):
                # Format the item somehow if needed:
                s = str(self._headings[1][i])
                w = len(s)
                if w > column_widths[i]:
                    column_widths[i] = w
                new_line.append(s)
            self._headings[1] = new_line

        format_str = []

        def _align(align, w):
            return '%%%s%ss' % (
                ("-" if align == "l" else ""),
                str(w))
        format_str = [_align(align, w) for align, w in
                      zip(self._alignments, column_widths)]
        if self._headings[0]:
            format_str.insert(0, _align(self._head_align, _head_width))
            format_str.insert(1, '|')
        format_str = ' '.join(format_str) + '\n'

        s = []
        if self._headings[1]:
            d = self._headings[1]
            if self._headings[0]:
                d = [""] + d
            first_line = format_str % tuple(d)
            s.append(first_line)
            s.append("-" * (len(first_line) - 1) + "\n")
        for i, line in enumerate(lines):
            d = [l if self._alignments[j] != 'c' else
                 l.center(column_widths[j]) for j, l in enumerate(line)]
            if self._headings[0]:
                l = self._headings[0][i]
                l = (l if self._head_align != 'c' else
                     l.center(_head_width))
                d = [l] + d
            s.append(format_str % tuple(d))
        return ''.join(s)[:-1]  # don't include trailing newline

    def _latex(self, printer):
        """
        Returns the string representation of 'self'.
        """
        # Check heading:
        if self._headings[1]:
            new_line = []
            for i in range(self._w):
                # Format the item somehow if needed:
                new_line.append(str(self._headings[1][i]))
            self._headings[1] = new_line

        alignments = []
        if self._headings[0]:
            self._headings[0] = [str(x) for x in self._headings[0]]
            alignments = [self._head_align]
        alignments.extend(self._alignments)

        s = r"\begin{tabular}{" + " ".join(alignments) + "}\n"

        if self._headings[1]:
            d = self._headings[1]
            if self._headings[0]:
                d = [""] + d
            first_line = " & ".join(d) + r" \\" + "\n"
            s += first_line
            s += r"\hline" + "\n"
        for i, line in enumerate(self._lines):
            d = []
            for j, x in enumerate(line):
                if self._wipe_zeros and (x in (0, "0")):
                    d.append(" ")
                    continue
                f = self._column_formats[j]
                if f:
                    if isinstance(f, FunctionType):
                        v = f(x, i, j)
                        if v is None:
                            v = printer._print(x)
                    else:
                        v = f % x
                    d.append(v)
                else:
                    v = printer._print(x)
                    d.append("$%s$" % v)
            if self._headings[0]:
                d = [self._headings[0][i]] + d
            s += " & ".join(d) + r" \\" + "\n"
        s += r"\end{tabular}"
        return s


#!/usr/bin/env python
# coding: utf-8

# In[1]:

import numpy as np
import matplotlib.pyplot as plt
import scipy as sc
import pandas as pd

# #  Lecture 7 -  Wells and aquifer
#
# ## Prof. Liedl/Prof. Werth/Prof. Chahar/Prabhas is to add the text contents.
# ## Anne/Sophie is to add the numerical contents
#
# Include ipynb file for:
#
# Theis method.
#
#

# ## Transmissivity ##

# When discussing storage properties in Chapter / Lecture …, we saw that aquifers or single layers may frequently be treated as two-dimensional systems. This is justified because the lateral extension of aquifers is usually much larger than the vertical extension. Thus, vertical variations of storage properties can be replaced by some average value without adversely affecting the quantification of groundwater storage.
#
# Similar things can be done with regard to conductivity properties and this brings us to the geohydraulic parameter of **Transmissivity**. The idea is to neglect vertical variations of _hydraulic conductivity_ ($K$)  and to use vertically averaged values instead. This procedure does not eliminate horizontal variability, so _transmissivity_ may still depend on horizontal coordinates $(x, y)$.
#
# The vertically averaged $K$ value is then multiplied by the water-saturated thickness to obtain transmissivity. The concept of water-saturated thickness (or water-saturated depth) requires to distinguish whether _confined_ or _unconfined_ flow conditions prevail.
#
# In general, water-saturated thickness is the distance from the aquifer bottom to a level up to which all pores are filled with water. For _confined aquifers_, this level is equal to aquifer top and water-saturated thickness is tantamount to aquifer thickness. For _unconfined aquifers_, however, water-saturated thickness corresponds to the distance between aquifer bottom and groundwater level. We will see some illustrations below when we try to quantify _transmissivity_.
#
# The symbol $T$ is mostly used to denote transmissivity which has a dimension of $L^2T^{-1}$.
#
#

# Let us have a closer look at the confined case first. The black cuboid in Fig.[LINK] illustrates that water-saturated thickness extends from aquifer bottom to aquifer top. So, it is equal to _aquifer thickness_ $m$. Transmissivity is calculated by
#
# $$
# T_x = K_x \cdot m  \;\; \text{ and }  \;\; Ty = K_y \cdot m
# $$
#
# Here we allow for horizontal aquifer anisotropy with different hydraulic conductivities in $x-$ and $y-$ direction $(K_x \neq K_y)$.
#
# For horizontally isotropic aquifers $(K_x = K_y = K)$, transmissivity is given by $T = K  \cdot m $.
#
#
#

# In[2]:

print("We already have large text content, equations and so we can add a simple numerical example here")

# Things are a bit more complicated for _unconfined aquifers_. Fig. [LINK] illustrates that water-saturated thickness extends from the aquifer bottom to the groundwater table. It is important to note that _transmissivity_ of unconfined aquifers depends on the vertical position of the groundwater table.
#
# For instance, if the groundwater table is lowered during to a draught period, _transmissivity_ is decreasing. This is fundamentally different from the confined case where the water-saturated thickness is given by aquifer geometry only and is not affected by hydraulic head changes.
#
#

# Computing transmissivity of unconfined aquifers requires to determine the difference of hydraulic head h and the elevation of aquifer bottom $z_{bot}$. Based on this, transmissivity is given by:
#
# $$T_x = K_x \cdot (h – z_{bot})$$
# and
# $$T_y = K_y \cdot (h – z_{bot})$$
#
# As above, we are allowing for horizontal aquifer anisotropy. For an isotropic unconfined aquifer we get
# $$T = K\cdot(h – z_{bot})$$

# In[3]:

print("We already have large text content, equations and so we can add a simple numerical example here")

# Two more remarks appear to be appropriate:
#
# - First, _transmissivity_ may be computed by the given equations even if the aquifer bottom is not horizontal. This case is not covered by the figure above.
# - Second, textbooks frequently present the equation $$ T = K\cdot h $$ for transmissivity of unconfined aquifers. It is to be noted that this equation only holds if two conditions are fulfilled:
#
# - The aquifer bottom must be horizontal and
# - hydraulic head values are expressed with respect to the elevation of aquifer bottom (= reference datum).
#
#
# Finally, we can try to compute transmissivity for isotropic aquifers and check how the result depends on several quantities like aquifer bottom, aquifer top, and hydraulic head.
#

# In[4]:

# A bit more complicated numerical example here. An example from Prof. Liedl - we improve further.

print("Q1. Determine if the aquifer is confined or unconfind and compute it's Transmissivity")

# input

K = 8.5e-5 # m/s, hydraulic conductivity
Ab = 120 # m asl, aquifer bottom elevation
At = 150 # m asl, aquifer top elevation
H  = 139 # m, hydraulic head

#intermediate calculation
A_T = At - Ab # m, Aquifer thickness
S_T = np.minimum(A_T, (H - Ab))

# Results

if H < At:
   print("\n It is a unconfined aquifer")
else:
   print("\n It is a Confined aquifer")

T = K*A_T # m²/s, Transmissivity

print("\nThe transmissivity is {0:0.2e} m\u00b2/s".format(T))

# In[ ]:


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.4'
#       jupytext_version: 1.2.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + {"deletable": true, "editable": true, "cell_type": "markdown"}
#

#

#

#
Computational Seismology

#
Reproducible Papers - Syngine Paper

#

#

#

# + {"deletable": true, "editable": true, "cell_type": "markdown"}
# ---
#
# # Figure 8: Education
#
# This notebook is part of the supplementary materials for the Syngine paper and reproduces figure 8.
#
# This notebook creates the phase relative times figure. Requires matplotlib >= 1.5 and an ObsPy version (>= 1.0) with the syngine client.
#
# ##### Authors:
# * Lion Krischer ([@krischer](https://github.com/krischer))

# + {"deletable": true, "editable": true}
# %matplotlib inline

import obspy
import numpy as np

from obspy.clients.syngine import Client

import itertools

import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
plt.style.use("seaborn-paper")

# + {"deletable": true, "editable": true}
c = Client()

# + {"deletable": true, "editable": true}
# Get seismograms for various strike values.
_d_plane_crossing = []

for strike in np.linspace(85, 95, 11):
    print(strike)
    _d_plane_crossing.append((strike, c.get_waveforms(model="ak135f_5s",
        receiverlatitude=0.0, receiverlongitude=0.0,
        sourcelatitude=0.0, sourcelongitude=30.0, components="Z", units="velocity",
        sourcedepthinmeters=0.0, sourcedoublecouple=[strike, 90.0, 0.0])[0]))

# + {"deletable": true, "editable": true}
max_amp = max(np.abs(_i[1].data).max() for _i in _d_plane_crossing)

for _i, tr in enumerate(_d_plane_crossing):
    plt.plot(tr[1].data / max_amp + _i)
plt.xlim(1300, 2100)
plt.show()

# + {"deletable": true, "editable": true}
_d_depth = []

# Get seismograms for various depths.
for depth in [10, 20, 30, 40, 50, 100, 200]:
    print(depth)
    _d_depth.append((depth, c.get_waveforms(model="ak135f_5s",
        receiverlatitude=0.0, receiverlongitude=0.0,
        sourcelatitude=0.0, sourcelongitude=30.0, components="Z", units="velocity",
        sourcedepthinmeters=depth * 1000, sourcemomenttensor=[1E20, 0, 0, 0, 0, 0])[0]))

# + {"deletable": true, "editable": true}
max_amp = max(np.abs(_i[1].data).max() for _i in _d_depth)

for _i, tr in enumerate(_d_depth):
    plt.plot(tr[1].data / max_amp + _i * 0.5)
plt.xlim(0, 6000)
plt.show()

# + {"deletable": true, "editable": true}
# Plot everything.
plt.figure(figsize=(8, 3))

plt.subplot(121)

max_amp = max(np.abs(_i[1].data).max() for _i in _d_plane_crossing)

ticks = []
labels = []

for _i, tr in enumerate(_d_plane_crossing):
    plt.plot(tr[1].times(), tr[1].data / max_amp * 200.4 + _i, color="0.1")
    ticks.append(_i)
    labels.append("%i" % (90.0 - tr[0]))
plt.xlim(345, 460)
plt.text(360, 10.8, "P Phase", size="small")
plt.text(403, 10.8, "PP Phase", size="small")
plt.ylim(-2, 12)
plt.xlabel("Time since origin [s]")
plt.ylabel("$\Delta_{strike}$ from nodal plane [degree]")
plt.yticks(ticks, labels)

plt.subplot(122)
max_amp = max(np.abs(_i[1].data).max() for _i in _d_depth)

colors = ["0.7", "0.1", "0.4", "0.1", "0.1", "0.1", "0.1"]

ticks = []
labels = []
for _i, tr in enumerate(_d_depth):
    plt.plot(tr[1].times(), tr[1].data / max_amp * 14.0 + _i, color=colors[_i])
    ticks.append(_i)
    labels.append("%i" % (tr[0]))
plt.xlim(0, 1500)
plt.gca().yaxis.tick_right()
plt.xlabel("Time since origin [s]")
plt.gca().yaxis.set_label_position("right")
plt.ylabel("Event depth [km]")
plt.yticks(ticks, labels)
plt.tight_layout()
plt.ylim(-1, 7)
plt.savefig("education.pdf")
plt.show()
# -


# -*- coding: utf-8 -*-
# Copyright (C) 2020-2021 by SCICO Developers
# All rights reserved. BSD 3-clause License.
# This file is part of the SCICO package. Details of the copyright and
# user license can be found in the 'LICENSE' file distributed with the
# package.

"""Random number generation.

This module provides convenient wrappers around several `jax.random
<https://jax.readthedocs.io/en/stable/jax.random.html>`_ routines to handle
the generation and splitting of PRNG keys, as well as the generation of random
:class:`.BlockArray`.

::

   # Calls to scico.random functions always return a PRNG key
   # If no key is passed to the function, a new key is generated
   x, key = scico.random.randn((2,))
   print(x)   # [ 0.19307713 -0.52678305]

   # scico.random functions automatically split the PRNGkey and return
   # an updated key
   y, key = scico.random.randn((2,), key=key)
   print(y) # [ 0.00870693 -0.04888531]

The user is responsible for passing the PRNG key to :mod:`scico.random` functions.
If no key is passed, repeated calls to :mod:`scico.random` functions will return the same
random numbers:

::

   x, key = scico.random.randn((2,))
   print(x)   # [ 0.19307713 -0.52678305]

   # No key passed, will return the same random numbers!
   y, key = scico.random.randn((2,))
   print(y)   # [ 0.19307713 -0.52678305]

If the desired shape is a tuple containing tuples, a :class:`.BlockArray` is returned:

::

   x, key = scico.random.randn( ((1, 1), (2,)), key=key)
   print(x)  # scico.blockarray.BlockArray:
             # DeviceArray([ 1.1378784 , -1.220955  , -0.59153646], dtype=float32)

"""

__author__ = """Luke Pfister <pfister@lanl.gov>"""

import functools
import inspect
import sys
from typing import Optional, Tuple, Union

import numpy as np

import jax

from scico.blockarray import BlockArray, block_sizes
from scico.typing import BlockShape, DType, JaxArray, PRNGKey, Shape
from scico.util import is_nested

def _add_seed(fun):
    """
    Modifies a jax.random function to add a `seed` argument.

    Args:
        fun: function to be modified, e.g., jax.random.normal. Expects `key`
        to be the first argument.

    Returns:
        fun_alt: a version of `fun` supporting an optional `seed` argument that
        is used to create a `jax.random.PRNGKey` that is passed along as the `key`.
        The `key` argument may still be used, but is moved to be second-to-last.
        By default, `seed=0`. The `seed` argument is added last. Other arguments are unchanged.
    """

    # find number of arguments to fun
    num_params = len(inspect.signature(fun).parameters)

    def fun_alt(*args, key=None, seed=None, **kwargs):

        # key and seed may be in *args, look for them
        if len(args) >= num_params:  # they passed all position args including key
            key = args[num_params - 1]
        if len(args) > num_params:  # they passed all position args including key and seed
            seed = args[num_params]

        if key is not None and seed is not None:
            raise ValueError("Key and seed cannot both be specified")

        if key is None:
            if seed is None:
                seed = 0
            key = jax.random.PRNGKey(seed)

        result = fun(key, *args[: num_params - 1], **kwargs)

        key, subkey = jax.random.split(key, 2)
        return result, key

    lines = fun.__doc__.split("\n\n")
    fun_alt.__doc__ = "\n\n".join(
        lines[0:1]
        + [
            f"  Wrapped version of `jax.random.{fun.__name__} <https://jax.readthedocs.io/en/stable/jax.random.html#jax.random.{fun.__name__}>`_. "
            "The SCICO version of this function moves the `key` argument to the end of the argument list, "
            "adds an additional `seed` argument after that, and allows the `shape` argument "
            "to accept a nested list, in which case a `BlockArray` is returned. "
            "Always returns a `(result, key)` tuple.",
            "  Original docstring below.",
        ]
        + lines[1:]
    )

    return fun_alt

def _allow_block_shape(fun):
    """
    Decorates a jax.random function so that the `shape` argument may be a BlockShape.
    """

    # use inspect to find which argument number is `shape`
    shape_ind = list(inspect.signature(fun).parameters.keys()).index("shape")

    @functools.wraps(fun)
    def fun_alt(*args, **kwargs):

        # get the shape argument if it was passed
        if len(args) > shape_ind:
            shape = args[shape_ind]
        elif "shape" in kwargs:
            shape = kwargs["shape"]
        else:  # shape was not passed, call fun as normal
            return fun(*args, **kwargs)

        # if shape is not nested, call fun as normal
        if not is_nested(shape):
            return fun(*args, **kwargs)
        # shape is nested, so make a BlockArray!

        # call the wrapped fun with an shape=(size,)
        subargs = list(args)
        subkwargs = kwargs.copy()
        size = np.sum(block_sizes(shape))

        if len(subargs) > shape_ind:
            subargs[shape_ind] = (size,)
        else:  # shape must be a kwarg if not a positional arg
            subkwargs["shape"] = (size,)

        result_flat = fun(*subargs, **subkwargs)

        return BlockArray.array_from_flattened(result_flat, shape)

    return fun_alt

def _wrap(fun):
    fun_wrapped = _add_seed(_allow_block_shape(fun))
    fun_wrapped.__module__ = __name__  # so it appears in docs
    return fun_wrapped

exceptions = [  # these do not take key and shape
    "PRNGKey",
    "double_sided_maxwell",
    "fold_in",
    "permutation",
    "shuffle",
    "split",
    "weibull_min",
    "threefry_2x32",
]

func_names = [
    t[0] for t in inspect.getmembers(jax.random, inspect.isfunction) if t[0] not in exceptions
]

for name in func_names:
    setattr(sys.modules[__name__], name, _wrap(getattr(jax.random, name)))

def randn(
    shape: Union[Shape, BlockShape],
    dtype: DType = np.float32,
    key: Optional[PRNGKey] = None,
    seed: Optional[int] = None,
) -> Tuple[Union[JaxArray, BlockArray], PRNGKey]:
    """Return an array drawn from the standard normal distribution. Alias for :func:`scico.random.normal`.

    Args:
        shape:  Shape of output array.  If shape is a tuple, a DeviceArray is returned.
            If shape is a tuple of tuples, a :class:`.BlockArray` is returned.
        key:  JAX PRNGKey.  Defaults to None, in which case a new key
              is created using the seed arg.
        seed: Seed for new PRNGKey. Default: 0
        dtype: dtype for returned value.  Default to float32.  If np.complex64,
               generates an array sampled from complex normal distribution.

    Returns:
        tuple: A tuple (x, key) containing:

           - **x** : (DeviceArray):  Generated random array
           - **key** : Updated random PRNGKey.
    """
    return normal(shape, dtype, key, seed)


# coding: utf-8

# ----------------------------------------------------------------------------

# Disciplina Tópicos Especiais II (C318)

# Curso: Fundamentos de Machine Learning

# Professor: Ricardo Augusto

# ----------------------------------------------------------------------------

# Projeto de Machine Learning

# Tema: Predição de despesas médicas por meio de Regressão Linear

# Grupo: Emanuel Massafera Magalhães, Pedro Henrique de Almeida e Thiago Santos da Costa

#%% Formulação e Definição do Problema de ML

# Frame the problem and look at the big picture.

# i) Enquadramento do problema de ML - Aprendizagem Supervisionada

# ii) Trata-se de um problema de regressão (variável de saída: despesas médicas de pacientes dos Estados Unidos)

# iii) Regressão Múltipla: uma vez que o sistema de ML precisa lidar com várias características (features) para gerar uma predição

# iv) Regressão Univariada: uma vez que estamos gerando a predição de um único valor para a variável de saída (um valor por paciente)

# v)  Se estamos fazendo a predição de múltiplos valores para a variável de saída temos uma regressão multivariada

# vi) Não há fluxo contínuo de dados entrando no sistema de ML a ser desenvolvido - com isso não há necessidade de ajuste-rápido aos dados (online learning))

# vii) A quantidade de dados pode ser acomodada na memória (batch learning)

#%% Estrutura do Projeto de ML

# - Estrutura do projeto
#   - Importação de bibiliotecas utilizadas
#   - Importação da base de dados
#   - Manipulações iniciais nos dados
#   - Criação de conjuntos de dados de treino e teste
#   - Investigando Correlações
#   - Preparação dos dados para Modelagem
#   - Limpeza dos dados
#   - Manipulando features categóricas
#   - Feature Scaling - Pipeline de transformação
#   - Criando o modelo de regressão linear
#   - Avaliação de Desempenho - Evaluation
#   - Validação Cruzada
#   - Usando o modelo nos dados de teste

#%% Bibliotecas utilizadas no projeto

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

#%% Importação da base de dados

# Importando dados (arquivo .csv) a partir da URL (fonte)
url = 'https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/insurance.csv'
df  = pd.read_csv(url)

# Salvando no diretório do projeto
df.to_csv('insurance.csv')

# Informações sobre o dataframe (atributo info)
df.info()

# ---------------------------
# Informações sobre o dataset

# 1. age: Número inteiro que indica a idade do beneficiário principal (excluindo acima de 64 anos, uma vez que são geralmente cobertos pelo governo).

# 2. sex: Gênero do segurado, podendo ser homem ou mulher.

# 3. bmi: Numero que indica o Índice de Massa Corporal (IMC) de segurado. É uma medida internacional utilizada para determinar se uma pessoa está no peso ideal. O IMC é calculado como sendo peso (em quilogramas) dividido pela altura (em metros) ao quadrado. Um IMC ideal está dentro do faixa de 18,5 a 24,9.

# 4. children: Número inteiro que indica o número de filhos ou dependentes cobertos pelo plano de seguro do segurado.

# 5. smoker: Indica se o segurado regularmente fuma tabaco ou não.

# 6. region: Indica o local de residência do beneficiário nos EUA, dividido em quatro regiões geográficas: nordeste, sudeste, sudoeste ou noroeste.

# 7. charges: Variável dependente que representa o valor de despesas médicas.

#%% Manipulações iniciais nos dados

# Descrição estatística do dataframe Pandas
df_stats = df.describe()
df_stats

# Extração de informações de estatística descritiva do dataframe:
# Ex: 25% dos pacientes tem idade (mediana) até 27 anos
# Ex: 75% dos pacientes tem idade (mediana) até 51 anos
# Ex: A idade média dos pacientes é de 39 anos
# Ex: O valor médio do bmi é de 30.66 kg/m²
# Ex: O valor médio dos gastos médicos medianos é de 13270 dólares

# ----------------------------------------------------------------------------
# Acessando e Manipulando o dataframe

# Indexação por rótulo - selecionando uma série Pandas a partir do dataframe
df['age']

# Indexação por rótulo - selecionando mais de uma série Pandas a partir do dataframe
df[['age', 'bmi']]

# Usando a notação ponto (.) para acesso à series de um dataframe
df.age

# Verificando a variável categórica - sex
gêneros = df['sex'].value_counts()
gêneros

# Verificando a variável categórica - smoker
fumantes = df['smoker'].value_counts()
fumantes

# Verificando a variável categórica - smoker
regiões = df['region'].value_counts()
regiões

# Visualizando histogramas relacionados com todas as variáveis do dataframe
df.hist(bins=50)
plt.show()

# Visualizando histogramas das variáveis - características - features
df['age'].hist(bins = 50)
plt.xlabel('age')
plt.ylabel('count')

df['bmi'].hist(bins = 50)
plt.xlabel('bmi')
plt.ylabel('count')

df['children'].hist(bins = 50)
plt.xlabel('children')
plt.ylabel('count')

df['charges'].hist(bins=50)
plt.xlabel('charges')
plt.ylabel('count')

# Biblioteca para análise de dados (visualizações estatísticas)
import klib
klib.dist_plot(df['age'])
klib.dist_plot(df['charges'])
klib.dist_plot(df['bmi'])
klib.dist_plot(df['children'])

# ----------------------------------------------------------------------------
# Análise conduzida a partir dos histogramas

# i) A grande maioria dos indivíduos em nossos dados tem despesas médicas
# anuais entre zero e US $ 15.000, embora a cauda da distribuição se estenda
# muito além desses picos. Como a regressão linear assume uma distribuição
# normal para a variável dependente, essa distribuição não é ideal.

# ii) Outro problema em mãos é que os modelos de regressão exigem que todos
# os recursos sejam numéricos, mas temos três features (sex, region e smoker)
# que não são numéricas.

# ----------------------------------------------------------------------------

#%% Criação de conjuntos de dados de treino e teste

# Método de Amostragem Aleatória Simples (Sklearn)
from sklearn.model_selection import train_test_split

# Função do scikit-learn train_test_split
train_set, test_set = train_test_split(df, test_size = 0.2, random_state = 42)

#%% Investigando Correlações

# Criando uma cópia do dataset de treino
insurance = train_set.copy()

# Fazendo a estimativa da matriz de correlação das variáveis do dataframe
correlation_matrix = insurance.corr()
correlation_matrix

klib.corr_plot(insurance)
klib.corr_plot(insurance, target='charges')

# Gráfico usando plt.scatter
x = insurance['age']
y = insurance['charges']
plt.scatter(x,y, alpha = 0.05)
plt.xlabel('age')
plt.ylabel('charges')
plt.title('ScatterPlot - age vs charges')
plt.grid()

x = insurance['bmi']
y = insurance['charges']
plt.scatter(x,y, alpha = 0.05)
plt.xlabel('bmi')
plt.ylabel('charges')
plt.title('ScatterPlot - bmi vs charges')
plt.grid()

# Checando qual é o valor do coeficiente de correlação (Person)
# computado entre cada feature (variável) e a característica/variável charges
correlation_matrix['charges']

# Colocando as correlações em ordem ascendente
correlation_matrix['charges'].sort_values(ascending = False)

# Função scatter_matrix (Pandas): forma alternativa de checagem da matriz de correlação
from pandas.plotting import scatter_matrix

# Selecionando atributos (features) de interesse para uso da scatter_matrix
attributes = ['charges',
              'age',
              'bmi',
              'children']
# Função scatter_matrix: plot na forma de matriz contendo gráficos de dispersão
scatter_matrix(insurance[attributes], figsize=(12, 8))

# Análise: aparentemente, a feature age tem relação estatística
# significativa (alta correlação) com os gastos médicos

#%% Preparação dos Dados para Modelagem

# Criando uma cópia do dataset de treino e removendo a variável target charges
insurance = train_set.drop('charges', axis = 1)

# Criando uma série para a variável de saída (target)
insurance_labels = train_set['charges'].copy()

#%% Limpeza dos Dados (Data Cleaning)

klib.missingval_plot(insurance)

# Não há valores faltantes no dataset

#%% Manipulando features categóricas

# Checando informações do dataset
insurance.info()

insurance_num = insurance.drop(['sex', 'smoker', 'region'], axis=1)
insurance_cat = insurance[['sex', 'smoker', 'region']]
insurance_cat.head(10)

# Aplicando OneHotEncoder para fazer a codificação (encoding) das
# variáveis sex, smoker e region
from sklearn.preprocessing import OneHotEncoder

# Criando encoder
cat_encoder = OneHotEncoder()

# Fazendo o fit (estimação de parâmetros) e transform (transformando os dados)
insurance_cat_1hot = cat_encoder.fit_transform(insurance_cat)
insurance_cat_1hot

# A saída é uma matriz esparsa (sparse matrix) - sparse matrix only stores
# the location of the nonzero elements
# Podemos converter para um array NumPy
insurance_cat_1hot.toarray()

# Lista de atributos das categorias
cat_encoder.categories_

#%% Feature Scaling - Pipeline de transformação

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('std_scaler', StandardScaler()),
    ])

from sklearn.compose import ColumnTransformer
num_attribs = list(insurance_num)
cat_attribs = ['sex', 'smoker', 'region']

full_pipeline = ColumnTransformer([
    ('num', num_pipeline, num_attribs),
    ('cat', OneHotEncoder(), cat_attribs),
    ])

insurance_prepared = full_pipeline.fit_transform(insurance)

#%% Modelagem

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(insurance_prepared, insurance_labels)

#%% Avaliação de Desempenho - Evaluation

# ----------------------------------------------------------------------------
# Avaliação de desempenho com todos os dados (sem separação de treino e teste)

from sklearn.metrics import mean_squared_error
insurance_predictions = lin_reg.predict(insurance_prepared)
lin_mse = mean_squared_error(insurance_labels, insurance_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse

# ----------------------------------------------------------------------------
# Avaliação de desempenho com separação de treino e teste
from sklearn.model_selection import train_test_split

# Matriz de Features X
X = insurance_prepared

# Variáveç Target
y = insurance_labels

# Divisão de treino e teste (sklearn) - train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3,
                                                    random_state=42)

lin_mse = mean_squared_error(insurance_labels, insurance_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse

#%% Validação Cruzada

from sklearn.model_selection import cross_val_score

def display_scores(scores):
    print('Scores:', scores)
    print('Mean:', scores.mean())
    print('Standard deviation:', scores.std())

lin_scores = cross_val_score(lin_reg, insurance_prepared, insurance_labels,
scoring='neg_mean_squared_error', cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)

r2_train = lin_reg.score(insurance_prepared, insurance_labels)
print('R2 no set de treino: %.2f' % r2_train)

#%% Usando o modelo nos dados de teste

insurance_test = test_set.drop('charges', axis = 1)
insurance_test_labels = test_set['charges'].copy()

insurance_test_prepared = full_pipeline.fit_transform(insurance_test)

lin_scores = cross_val_score(lin_reg, insurance_test_prepared, insurance_test_labels,
scoring='neg_mean_squared_error', cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)

r2_test = lin_reg.score(insurance_test_prepared, insurance_test_labels)
print('R2 no set de teste: %.2f' % r2_test)