# Pré-processamento de dados e validação

Oi pessoal!

Nessa aula, vamos falar um pouco sobre pré-processamento de dados e validação de modelos! Vamos aprender:
- Como processar seus dados para entegá-los da melhor forma para seu modelo.
- A prever quem vive e quem morre em um acidente marítimo.
- Métricas de avaliação.
- Estratégias de validação.
- Como submeter em uma competição do Kaggle.

Nós vamos passar por diversas técnicas e colocá-las em prática em um dos datasets mais clássicos: Sobreviventes do Titanic!

Espero que vocês gostem, não esqueçam de deixar o like e adicionar o notebook aos seus favoritos.

### *IMPORTANTE BAIXAR OS DADOS*

Para baixar os dados você vai precisar se cadastrar no Kaggle - explico no próximo tópico o que é esse site.

Os dados podem ser baixados em: https://www.kaggle.com/c/titanic/data
Nesse link você também encontra uma descrição sobre cada uma das features do dataset.

Três arquivos serão fornecidos:

- Dados de treino: conjunto de dados dos quais sabemos a resposta.
- Dados de teste: conjunto de dados para o qual faremos nossas previsões.
- Exemplo de submissão: Arquivo que nos mostra o formato de uma submissão para essa competição.

## O que é o Kaggle?

O Kaggle é a maior comunidade de Ciência de Dados do mundo. Nele, você encontra diversos datasets, estudos de pessoas do mundo todo e ainda pode participar de competições organizadas pelo site - muitas delas valendo dinheiro.

Nosso objetivo é que, ao fim dessa aula, você faça sua primeira submissão em uma competição do Kaggle. A competição é a do [Titanic](https://www.kaggle.com/c/titanic/overview), e ela foi criada apenas para fins educacionais.

## Importando os dados

Vamos começar importando os dados que acabamos de baixar e dando uma primeira olhada neles. Nós vamos assumir que vocês ja sabem Pandas mas, qualquer dúvida que surgir, não deixem de dar uma olhada na aula específica do assunto, pesquisar no Google (a documentação dessas bibliotecas é bem feita) ou perguntar pra gente!

```python
import numpy as np
import pandas as pd
```

```python
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

combine = [train, test]
```

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Name
      Sex
      Age
      SibSp
      Parch
      Ticket
      Fare
      Cabin
      Embarked

      0
      1
      0
      3
      Braund, Mr. Owen Harris
      male
      22.0
      1
      0
      A/5 21171
      7.2500
      NaN
      S

      1
      2
      1
      1
      Cumings, Mrs. John Bradley (Florence Briggs Th...
      female
      38.0
      1
      0
      PC 17599
      71.2833
      C85
      C

      2
      3
      1
      3
      Heikkinen, Miss. Laina
      female
      26.0
      0
      0
      STON/O2. 3101282
      7.9250
      NaN
      S

      3
      4
      1
      1
      Futrelle, Mrs. Jacques Heath (Lily May Peel)
      female
      35.0
      1
      0
      113803
      53.1000
      C123
      S

      4
      5
      0
      3
      Allen, Mr. William Henry
      male
      35.0
      0
      0
      373450
      8.0500
      NaN
      S

## Tipos de variáveis

Vamos começar entendendo melhor sobre o nosso material de estudo: os dados. Um dataset pode ser visto como um conjunto de *data objects*, normalmente chamados de instâncias, observações, vetores, linhas ou exemplos. Esses são cada uma das linhas do nosso conjunto de dados.

Cada um desses *data objects* é definido por um conjunto de varíaveis - ou *features*. Que expressam, de diferentes maneiras, as características da nossa instância. Ao descrever uma sala de aula, por exemplo, podemos ter as seguintes features: localização, capacidade, quadro negro ou branco, quantidade de tomadas, número de portas, se possui um ar condicionado. A ideia é que essas variáveis sejam capazes de descrever as caracteristicas básicas das salas.

Existem diferentes tipos de variáveis, eles são:

- Numéricas
    - Discretas (representam contagens) e.g. número de cadeiras em uma sala
    - Contínuas (assumem valores em um intervalo contínuo) e.g preço de uma passagem
- Categóricas
    - Ordinais (a ordem importa) e.g. tamanho de roupa (P < M < G)
    - Nominais (não existe ordem entre os valores) e.g. cor da roupa

Vamos identificar agora os tipos de variáveis que temos no dataset do Titanic. Todo *DataFrame* possui um atributo chamado **dtypes**, que nos mostra o tipo de cada variável.

```python
train.dtypes
```

    PassengerId      int64
    Survived         int64
    Pclass           int64
    Name            object
    Sex             object
    Age            float64
    SibSp            int64
    Parch            int64
    Ticket          object
    Fare           float64
    Cabin           object
    Embarked        object
    dtype: object

Agora nós sabemos de que tipo cada variável é. O tipo *object* indica que a variável é uma string, que trataremos como variáveis categóricas.

É importante perceber que nem toda variável númerica é **realmente** numérica. Pegue a variável *Pclass*, por exemplo, seus valores são: 1a, 2a ou 3a classe. Esses valores já foram processados utilizando uma das técnicas que veremos nesse notebook e, por isso, ela agora é tratada como um número pelo código, embora permaneça sendo uma feature categórica.

Para entender de verdade o tipo de cada variável, o melhor que podemos fazer é ler a documentação que é normalmente fornecida com os dados. Abaixo, separaremos as variáveis nos tipos que mencionamos anteriormente.

**Quais variáveis são numéricas?**
Contínuas: Age, Fare.
 Discretas: SibSp, Parch.

**Quais variáveis são categóricas?**
Nominais: Survived, Sex, and Embarked.
 Ordinais: Pclass.

## Valores faltantes

Antes de começarmos a criar nossas features - ou fazer qualquer coisa - é bom sempre dar uma olhada no nosso conjunto de dados. Podemos ver se nenhum dado está faltando, verificar se os dados fazem sentido, se estão consistentes e coisas do tipo.

Além disso, é uma boa ideia fazer perguntas e buscar as respostas no seu dataset, assim como criar gráficos que te façam entender melhor seus dados e que forneçam insights sobre eles. Essa etapa do processo é chamada de Análise Exploratória de Dados (EDA).

Como o foco da aula é no pré-processamento, vamos apenas tratar dos valores faltantes pois eles podem nos trazer alguns problemas mais pra frente. Utilizando os comandos mostrados abaixo, você pode descobrir quantos valores estão faltando em cada coluna do seu dataset.

```python
train.isna().sum()
```

    PassengerId      0
    Survived         0
    Pclass           0
    Name             0
    Sex              0
    Age            177
    SibSp            0
    Parch            0
    Ticket           0
    Fare             0
    Cabin          687
    Embarked         2
    dtype: int64

```python
test.isna().sum()
```

    PassengerId      0
    Pclass           0
    Name             0
    Sex              0
    Age             86
    SibSp            0
    Parch            0
    Ticket           0
    Fare             1
    Cabin          327
    Embarked         0
    dtype: int64

Existem diversas formas de preencher os *missing values*, por exemplo:
 - Média ou mediana de toda a coluna.
 - Agrupar os dados por outras features e pegar média ou mediana depois. Por exemplo, digamos que queremos preencher valores vazios de idade. É possível que pessoas de diferentes classes tenham médias de idades diferentes, então, na hora de preencher podemos olhar para a média ou mediana da idade daqueles que estão na mesma classe que nosso exemplo com idade vazia está.
 - Preencher com 0.
 - Excluir as linhas que contém valores nulos.

Todas essas formas são válidas e tem seus prós e contras. Por motivo de simplicidade, vamos preencher os valores faltantes em Age e Fare com a mediana da coluna e em Embarked com o valor mais frequente. Não vamos preencher Cabin pois não usaremos essa feature.

```python
embarked_mode = train['Embarked'].mode()[0]
train['Embarked'] = train['Embarked'].fillna(embarked_mode)
test['Embarked'] = test['Embarked'].fillna(embarked_mode)

train_median_fare = train['Fare'].dropna().median()
train['Fare'] = train['Fare'].fillna(train_median_fare)
test['Fare'] = test['Fare'].fillna(train_median_fare)

train_median_age = train['Age'].dropna().median()
train['Age'] = train['Age'].fillna(train_median_age)
test['Age'] = test['Age'].fillna(train_median_age)
```

## Feature engineering

Protinho, agora podemos começar a criar algumas features básicas.

Uma das partes mais importantes do pré-processamento de dados é a criação de features que ajudem seu modelo a encontrar padrões mais facilmente. Nessa parte do notebook, nós vamos criar algumas novas variáveis e analisar como elas se relacionam com a sobrevivência dos passageiros.

As features criadas aqui foram retiradas de: https://www.kaggle.com/startupsci/titanic-data-science-solutions

### Título do passageiro

Se pensarmos bem, o nome de um indivíduo não tem relação alguma com suas chances de sobrevivência. Podemos então retirar essa coluna?
Bom, talvez. Vamos dar uma olhada em alguns dos nomes.

```python
train['Name'].head()
```

    0                              Braund, Mr. Owen Harris
    1    Cumings, Mrs. John Bradley (Florence Briggs Th...
    2                               Heikkinen, Miss. Laina
    3         Futrelle, Mrs. Jacques Heath (Lily May Peel)
    4                             Allen, Mr. William Henry
    Name: Name, dtype: object

Os nomes em nossos conjuntos de dados são únicos, mas os títulos dos indivíduos (Mr., Mrs., Miss) se repetem. Vamos isolar os títulos de cada nome para analisarmos essa fato. A tabela abaixo mostra, para cada título, quantos homens e quantas mulheres o possuem.

O método *Series.str.extract()* extrai texto se baseando na regex utilizada.

```python
for df in combine:
    df['Title'] = df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)

pd.crosstab(train['Title'], train['Sex'])
```

      Sex
      female
      male

      Title

      Capt
      0
      1

      Col
      0
      2

      Countess
      1
      0

      Don
      0
      1

      Dr
      1
      6

      Jonkheer
      0
      1

      Lady
      1
      0

      Major
      0
      2

      Master
      0
      40

      Miss
      182
      0

      Mlle
      2
      0

      Mme
      1
      0

      Mr
      0
      517

      Mrs
      125
      0

      Ms
      1
      0

      Rev
      0
      6

      Sir
      0
      1

Antes de ver a relação dos títulos com sobrevivência, vamos agrupá-los. Talvez seja uma boa ideia deixar todos os mais raros em um único grupo, então vamos fazer isso.

```python
for df in combine:
    df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col',
                                       'Don', 'Dr', 'Major', 'Rev', 'Sir',
                                       'Jonkheer', 'Dona'], 'Rare')

    df['Title'] = df['Title'].replace('Mlle', 'Miss')
    df['Title'] = df['Title'].replace('Ms', 'Miss')
    df['Title'] = df['Title'].replace('Mme', 'Mrs')

train[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()
```

      Title
      Survived

      0
      Master
      0.575000

      1
      Miss
      0.702703

      2
      Mr
      0.156673

      3
      Mrs
      0.793651

      4
      Rare
      0.347826

Muito interessante. O título de um passageiro realmente influencia sua chance de sobrevivência. Podemos manter essa feature que criamos e excluir a feature *Name*.

```python
train = train.drop(['Name'], axis=1)
test = test.drop(['Name'], axis=1)

combine = [train, test]
```

### O passageiro está sozinho?

Uma pessoa que está sozinha tem mais chances de sobreviver? Abaixo criaremos uma feature que indica o tamanho da família do passageiro que está a bordo, e uma outra feature para indicar se um indivíduo está sozinho ou não.

```python
for df in combine:
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
    df['IsAlone'] = 0
    df.loc[df['FamilySize'] == 1, 'IsAlone'] = 1

train[['IsAlone', 'Survived']].groupby(['IsAlone'], as_index=False).mean()
```

      IsAlone
      Survived

      0
      0
      0.505650

      1
      1
      0.303538

Aparentemente não. Você consegue imaginar um motivo para isso?

Vou excluir as features SibSp e Parch pois acho que as features que criamos já nos dão bastante informação. Você pode escolher mantê-las se preferir ou até mesmo testar posteriormente de que forma seu modelo foi melhor.

```python
train = train.drop(['SibSp', 'Parch'], axis=1)
test = test.drop(['SibSp', 'Parch'], axis=1)

combine = [train, test]
```

### Como ficaram nossos datasets?

Vamos dar uma olhada nos nossos dois datasets (treino e teste) após a criação das features.

```python
print(train.shape, test.shape)
```

    (891, 12) (418, 11)

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Sex
      Age
      Ticket
      Fare
      Cabin
      Embarked
      Title
      FamilySize
      IsAlone

      0
      1
      0
      3
      male
      22.0
      A/5 21171
      7.2500
      NaN
      S
      Mr
      2
      0

      1
      2
      1
      1
      female
      38.0
      PC 17599
      71.2833
      C85
      C
      Mrs
      2
      0

      2
      3
      1
      3
      female
      26.0
      STON/O2. 3101282
      7.9250
      NaN
      S
      Miss
      1
      1

      3
      4
      1
      1
      female
      35.0
      113803
      53.1000
      C123
      S
      Mrs
      2
      0

      4
      5
      0
      3
      male
      35.0
      373450
      8.0500
      NaN
      S
      Mr
      1
      1

```python
test.head()
```

      PassengerId
      Pclass
      Sex
      Age
      Ticket
      Fare
      Cabin
      Embarked
      Title
      FamilySize
      IsAlone

      0
      892
      3
      male
      34.5
      330911
      7.8292
      NaN
      Q
      Mr
      1
      1

      1
      893
      3
      female
      47.0
      363272
      7.0000
      NaN
      S
      Mrs
      2
      0

      2
      894
      2
      male
      62.0
      240276
      9.6875
      NaN
      Q
      Mr
      1
      1

      3
      895
      3
      male
      27.0
      315154
      8.6625
      NaN
      S
      Mr
      1
      1

      4
      896
      3
      female
      22.0
      3101298
      12.2875
      NaN
      S
      Mrs
      3
      0

Algumas dessas variáveis não nos dão muita informação, então podemos excluí-las. São elas *Ticket* e *Cabin*.

```python
train = train.drop(['Ticket', 'Cabin'], axis=1)
test = test.drop(['Ticket', 'Cabin'], axis=1)

combine = [train, test]
```

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Sex
      Age
      Fare
      Embarked
      Title
      FamilySize
      IsAlone

      0
      1
      0
      3
      male
      22.0
      7.2500
      S
      Mr
      2
      0

      1
      2
      1
      1
      female
      38.0
      71.2833
      C
      Mrs
      2
      0

      2
      3
      1
      3
      female
      26.0
      7.9250
      S
      Miss
      1
      1

      3
      4
      1
      1
      female
      35.0
      53.1000
      S
      Mrs
      2
      0

      4
      5
      0
      3
      male
      35.0
      8.0500
      S
      Mr
      1
      1

## Feature encoding

O modelo que usaremos só aceita variáveis numéricas, por isso, precisaremos transformar nossos dados de alguma forma. Existem várias maneiras de fazer isso e nós vamos dar uma olhada em duas delas.

### Label encoding

Método que mapeia cada categoria em um número. É mais utilizado com métodos de árvore.

OBS: O For não é necessário aqui pois estamos encodando apenas uma variável, mas preferi deixar uma forma mais geral.

```python
from sklearn.preprocessing import LabelEncoder
```

```python
label_encoder_columns = ['Embarked']

for column in label_encoder_columns:
    # cria um encoder
    label_encoder = LabelEncoder()

    # cria um dicionario para o encoder
    label_encoder.fit(train[column])

    # aplica as transformações nos datasets
    train[column] = label_encoder.transform(train[column])
    test[column] = label_encoder.transform(test[column])
```

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Sex
      Age
      Fare
      Embarked
      Title
      FamilySize
      IsAlone

      0
      1
      0
      3
      male
      22.0
      7.2500
      2
      Mr
      2
      0

      1
      2
      1
      1
      female
      38.0
      71.2833
      0
      Mrs
      2
      0

      2
      3
      1
      3
      female
      26.0
      7.9250
      2
      Miss
      1
      1

      3
      4
      1
      1
      female
      35.0
      53.1000
      2
      Mrs
      2
      0

      4
      5
      0
      3
      male
      35.0
      8.0500
      2
      Mr
      1
      1

Como podemos ver, *Embarked* agora possui apenas valores numéricos. Caso seja necessário voltar aos valores iniciais em algum ponto do código, basta usar o método *LabelEncoder.inverse_transform()*. Abaixo temos um exemplo de como isso funciona.

```python
label_encoder.inverse_transform(train['Embarked'])[0:5]
```

    array(['S', 'C', 'S', 'S', 'S'], dtype=object)

### One-hot encoding

É um método que cria, para cada categoria, uma coluna com valores binários indicando a presença ou não da categoria na instância.

- Normalmente utilizado com métodos lineares, kNN e redes neurais.
- É necessário tomar cuidado caso existam muitos valores diferentes de categoria.

Evita que o modelo pense algo como: Mrs. > Miss > Mr. Deixa mais clara para o modelo a independência das categorias. Vamos aplicar em algumas colunas apenas para exemplificar. Para fazer isso podemos usar a função *pd.get_dummies()*

#### Exemplo de retorno da função

```python
pd.get_dummies(train['Title'])
```

      Master
      Miss
      Mr
      Mrs
      Rare

      0
      0
      0
      1
      0
      0

      1
      0
      0
      0
      1
      0

      2
      0
      1
      0
      0
      0

      3
      0
      0
      0
      1
      0

      4
      0
      0
      1
      0
      0

      ...
      ...
      ...
      ...
      ...
      ...

      886
      0
      0
      0
      0
      1

      887
      0
      1
      0
      0
      0

      888
      0
      1
      0
      0
      0

      889
      0
      0
      1
      0
      0

      890
      0
      0
      1
      0
      0

891 rows × 5 columns

#### Aplicação

```python
one_hot_columns = ['Sex', 'Title']

one_hot = pd.get_dummies(train[one_hot_columns])
train = pd.concat([train, one_hot], axis=1).drop(one_hot_columns, axis=1)

one_hot = pd.get_dummies(test[one_hot_columns])
test = pd.concat([test, one_hot], axis=1).drop(one_hot_columns, axis=1)
```

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Age
      Fare
      Embarked
      FamilySize
      IsAlone
      Sex_female
      Sex_male
      Title_Master
      Title_Miss
      Title_Mr
      Title_Mrs
      Title_Rare

      0
      1
      0
      3
      22.0
      7.2500
      2
      2
      0
      0
      1
      0
      0
      1
      0
      0

      1
      2
      1
      1
      38.0
      71.2833
      0
      2
      0
      1
      0
      0
      0
      0
      1
      0

      2
      3
      1
      3
      26.0
      7.9250
      2
      1
      1
      1
      0
      0
      1
      0
      0
      0

      3
      4
      1
      1
      35.0
      53.1000
      2
      2
      0
      1
      0
      0
      0
      0
      1
      0

      4
      5
      0
      3
      35.0
      8.0500
      2
      1
      1
      0
      1
      0
      0
      1
      0
      0

## Feature scaling

Alguns algorítmos são sensíveis a escala dos dados e, por essa razão, precisamos colocar todos os dados na mesma escala para que esses modelos funcionem da melhor forma possível. Vamos dar uma olhada em dois dos métodos mais comuns para fazer isso.

É importante saber que uma Árvores de Decisão não é um desses modelos sensíveis, então só aplicaremos esses métodos aqui para exemplificação.

Como queremos aplicar essas tranformações nos nossos dados de treino e teste, é necessário que o Scaler tenha uma visão geral dos dados na hora de se adequar a eles, por isso, criaremos um dataset que concatena train e test.

```python
df_concat = pd.concat([train.drop(['Survived'], axis=1), test], axis=0)
```

### Normalização

Faz com que todos os valores fiquem no intervalo [0, 1]:

$
\begin{align}
{x_{i}}' = \frac{x_{i} - min(x)}{max(x) - min(x)}
\end{align}
$

Podemos aplicar na coluna *Fare*.

```python
from sklearn.preprocessing import MinMaxScaler
```

```python
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(df_concat[['Fare']])

train[['Fare']] = scaler.transform(train[['Fare']])
test[['Fare']] = scaler.transform(test[['Fare']])
```

### Padronização

Faz com que os valores tenham média 0 e desvio padrão 1:

$
\begin{align}
{x_{i}}' = \frac{x_{i} -\mu}{\sigma}
\end{align}
$

Vamos utilizar a padronização na feature *Age* para exemplificar.

Explicação (em inglês, infelizmente) da importância de normalizar os padronizar os dados: https://humansofdata.atlan.com/2018/12/data-standardization/

```python
from sklearn.preprocessing import StandardScaler
```

```python
scaler = StandardScaler()
scaler.fit(df_concat[['Age']])

train[['Age']] = scaler.transform(train[['Age']])
test[['Age']] = scaler.transform(test[['Age']])
```

## Resultado final

Vamos dar uma olhada final nos nossos dados antes se seguirmos em frente!

```python
train.head()
```

      PassengerId
      Survived
      Pclass
      Age
      Fare
      Embarked
      FamilySize
      IsAlone
      Sex_female
      Sex_male
      Title_Master
      Title_Miss
      Title_Mr
      Title_Mrs
      Title_Rare

      0
      1
      0
      3
      -0.581628
      0.014151
      2
      2
      0
      0
      1
      0
      0
      1
      0
      0

      1
      2
      1
      1
      0.658652
      0.139136
      0
      2
      0
      1
      0
      0
      0
      0
      1
      0

      2
      3
      1
      3
      -0.271558
      0.015469
      2
      1
      1
      1
      0
      0
      1
      0
      0
      0

      3
      4
      1
      1
      0.426099
      0.103644
      2
      2
      0
      1
      0
      0
      0
      0
      1
      0

      4
      5
      0
      3
      0.426099
      0.015713
      2
      1
      1
      0
      1
      0
      0
      1
      0
      0

```python
test.head()
```

      PassengerId
      Pclass
      Age
      Fare
      Embarked
      FamilySize
      IsAlone
      Sex_female
      Sex_male
      Title_Master
      Title_Miss
      Title_Mr
      Title_Mrs
      Title_Rare

      0
      892
      3
      0.387341
      0.015282
      1
      1
      1
      0
      1
      0
      0
      1
      0
      0

      1
      893
      3
      1.356310
      0.013663
      2
      2
      0
      1
      0
      0
      0
      0
      1
      0

      2
      894
      2
      2.519072
      0.018909
      1
      1
      1
      0
      1
      0
      0
      1
      0
      0

      3
      895
      3
      -0.194041
      0.016908
      2
      1
      1
      0
      1
      0
      0
      1
      0
      0

      4
      896
      3
      -0.581628
      0.023984
      2
      3
      0
      1
      0
      0
      0
      0
      1
      0

## Métricas de avaliação

Antes de começarmos a utilizar modelos para prever o futuro dos nossos passageiros, precisamos estudar um pouco sobre como avaliar o modelo que criarmos.

Métricas de avaliação são super importantes pois nos dão um valor que representa o quão bem nosso modelo está se saindo.

A métrica utilizada depende se estamos tratando de um problema de classificação ou regressão. Vamos ensinar algumas para cada um dos casos:

- Classificação
    - Acurácia
    - Precision e Recall
    - F-measure
    - AUC-ROC Curve
- Regressão
    - Erro quadrático médio (MSE)
    - R²

### Matriz de confusão

É uma medida de performance para modelos de classificação. É uma tabela com a combinação de valores previstos pelo modelo e valores reais:

Vamos entender o que cada uma das siglas representa fazendo uma analogia com um teste de alguma doença:

- TP (True Positive): Seu modelo previu positivo e estava correto. Esse caso seria equivalente a você **estar** doente e receber um **teste positivo**.

- TN (True Negative): Seu modelo previu negativo e estava correto. Esse caso é equivalente a você **não estar** doente e receber um **teste negativo**.

- FP (False Positive): Seu modelo previu positivo e estava incorreto: Esse caso é equivalente a você **não estar** doente e receber um **teste positivo**.

- FN (False Negative): Seu modelo previu negativo e estava incorreto. Esse caso é equivalente a você **estar** doente e receber um **teste negativo**.

Nossas previsões corretas ficam todas na diagonal principal da matrix.

É importante perceber que a matrix de confusão pode ser usada para problemas de classficação de mais de 2 classes. Abaixo, temos um exemplo da matrix de confusão sendo usada em um problema de classificação de 3 classes:

### Precision e Recall

**Precision**

Qual a proporção de previsões positivas realmente corretas?

$
\begin{align}
Precision = \frac{TP}{TP+FP}
\end{align}
$

Um modelo sem falso positivo tem uma precisão de 1.0.

**Recall**

Qual a proporção de observações realmente positivas foi identificada corretamente?

$
\begin{align}
Recall = \frac{TP}{TP+FN}
\end{align}
$

Um modelo sem falso negativo tem recall de 1.0.

### Acurácia

Indica a porcentagem de predições corretas que o modelo realizou.
Pode ser expressa da seguinte forma:

$
\begin{align}
Accuracy = \frac{TP+TN}{Total}
\end{align}
$

Pode enganar caso o número de ocorrências de cada classe esteja desbalanceado.

### F-measure

É uma medida de acurácia que leva em conta precision e recall ao mesmo tempo. Ela é dada por uma média harmônica das duas medidas:

$
\begin{align}
F_{1} = \frac{1}{recall^{-1} + precision^{-1}} = 2 \cdot \frac{recall \cdot precision}{recall+precision}
\end{align}
$

### AUC (Area under the curve)

Área abaixo de qual curva? Da curva **ROC** (Receiver operating characteristic).

E como funciona essa curva?

- Essa curva indica a relação entre as taxas de falsos positivos e verdadeiros positivos
- Nosso modelo terá como saída a probabilidade de classificar os dados como da classe 1
- Vamos supor que se P(Y=1) for > 0.5, dizemos que aquela instância é da classe 1, caso contrário classe 0.
- Isso irá nos dar uma taxa de falsos positivos e uma taxa de verdadeiros positivos e isso é um ponto na curva ROC.
- Agora escolhemos outros thresholds - diferentes de 0.5. Cada um desses gera um novo ponto na curva ROC.

Essa metrica não é sensível a distribuição das classes e, por essa razão, é comumente utilizada como métrica de diversas competições.

É importante notar que um **classificador aleatório** possui uma ROC-AUC de 0.5.

### Mean Squared Error

Método mais simples para avaliação de modelos de regressão. Basicamente, para cada instância, calculamos o quadrado da diferença entre o que deveríamos prever e o que previmos e então tiramos a média:

$
\begin{align}
MSE = \frac{1}{N} \sum_{i=0}^{N}(f(x_{i}) - y_{i})^{2}
\end{align}
$

### R² Score

Outro método para avaliar modelos de regressão. É também chamado de coeficiente de determinação e tem todos seus valores no intervalo [0,1].

$
\begin{align}
SStot = \sum_{i=0}(y_{i} - \bar{y}_{i})^{2}
\end{align}
$

$
\begin{align}
SSres = \sum_{i=0}(y_{i} - \hat{y}_{i})^{2}
\end{align}
$

$
\begin{align}
R^{2} = 1 - \frac{SSres}{SStot}
\end{align}
$

## Estratégias de validação

Com nossa métrica escolhida em mãos, chegou a hora de criar e validar nosso modelo!

Vamos aprender agora dois métodos de validação e, depois de validar nosso modelo, faremos uma submissão no Kaggle para ver o quanto nossa validação se aproxima do nosso resultado real no conjunto de teste.

Antes disso, vamos só preparar nossos dados para os processos.

```python
train = train.drop(['PassengerId'], axis=1)

pass_ids = test['PassengerId']
test = test.drop(['PassengerId'], axis=1)
```

```python
X = train.drop(['Survived'], axis=1)
y = train['Survived']
```

```python
X.shape
```

    (891, 13)

```python
test.shape
```

    (418, 13)

Perceba que agora temos o X e o test com as mesmas dimensões, isso vai facilitar as coisas para nós.

### Hold Out

Nessa estratégia, nós dividiremos nosso conjunto de dados em dois conjuntos. Usaremos um deles para treinar nosso modelo (conjunto de treino) e o outro para calcular a métrica de avaliação (conjunto de validação).

Nós sabemos a resposta correta para todo o nosso conjunto de dados mas vamos fingir que não sabemos para uma parte dele, usar nosso modelo pra prever essas respostas e depois comparar as predições com as respostas verdadeiras.

```python
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import confusion_matrix
```

```python
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
```

```python
model = DecisionTreeClassifier()
model.fit(X_train, y_train)

preds = model.predict(X_val)
print(f'Accuracy = {accuracy_score(y_val, preds)}')

# matriz de confusão do nosso modelo
confusion_matrix(y_val, preds)
```

    Accuracy = 0.7932960893854749

    array([[93, 19],
           [18, 49]])

### k-Fold Cross Validation

Nessa estratégia vamos dividir nossos dados em K conjuntos. Em cada iteração, utilizaremos K-1 desses conjuntos para treinamento e um deles para validação (variando qual conjunto é utilizado para validação em cada iteração). Depois disso, calculamos a média das iterações.

```python
from sklearn.model_selection import KFold
```

```python
NFOLDS = 5
folds = KFold(n_splits=NFOLDS)

columns = X.columns
score = 0

# criando os folds
splits = folds.split(X, y)

# para cada fold, pegaremos os index de treino e index de validação
for fold_n, (train_index, valid_index) in enumerate(splits):
    X_train, X_valid = X[columns].iloc[train_index], X[columns].iloc[valid_index]
    y_train, y_valid = y.iloc[train_index], y.iloc[valid_index]

    model = DecisionTreeClassifier(max_depth=3)
    model.fit(X_train, y_train)

    y_pred_valid = model.predict(X_valid)
    print(f"Fold {fold_n + 1} | Accuracy: {accuracy_score(y_valid, y_pred_valid)}")

    score += accuracy_score(y_valid, y_pred_valid)

print(f"\nMean accuracy = {score/NFOLDS}")
```

    Fold 1 | Accuracy: 0.8435754189944135
    Fold 2 | Accuracy: 0.8089887640449438
    Fold 3 | Accuracy: 0.8258426966292135
    Fold 4 | Accuracy: 0.7696629213483146
    Fold 5 | Accuracy: 0.8651685393258427

    Mean accuracy = 0.8226476680685456

A ideia é utilizar a validação para escolher o melhor modelo e seus respectivos parâmetros.

## Prevendo para o teste e submetendo no Kaggle

Chegou a hora de usar o modelo que criamos e prever a resposta para os dados que não temos resposta!

```python
# treinando o modelo com todos os dados
model = DecisionTreeClassifier(max_depth=3)
model.fit(X, y)
```

    DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='gini',
                           max_depth=3, max_features=None, max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, presort='deprecated',
                           random_state=None, splitter='best')

```python
# usando o arquivo de exemplo para
sub = pd.read_csv('gender_submission.csv')
```

```python
test_preds = model.predict(test)
sub['Survived'] = test_preds
```

```python
sub.head()
```

      PassengerId
      Survived

      0
      892
      0

      1
      893
      1

      2
      894
      0

      3
      895
      0

      4
      896
      1

```python
sub.to_csv('submission.csv', index=False)
```

Uma vez que você tem seu arquivo de predições, basta seguir o tutorial [nessa página](https://www.kaggle.com/c/titanic/overview) para aprender a submeter no site! E pronto!

### *Parabéns!!!*

Infelizmente, você vai perceber que nossa pontuação ainda não foi muito alta. Na verdade, ela foi só um pouco melhor que a submissão fornecida como exemplo. Mas tudo bem, nós ainda podemos melhorar muito! E isso nos leva ao nosso penúltimo tópico...

## É hora de ser criativo e botar a mão na massa!

Crie um novo notebook e faça as coisas do seu jeito! Esse notebook serviu apenas para te ensinar algumas técnicas, mas existe muito mais coisas para fazer. Crie novas features, processe os dados de forma diferente, teste e aprenda.

Uma boa fonte de aprendizado é a [sessão de Notebooks](https://www.kaggle.com/c/titanic/notebooks) dessa competição no Kaggle. Lá, competidores vão mostrar toda a sua solução e você pode aprender muito com eles. Não deixe de dar uma olhada.

## Considerações finais

Essa aula abordou diversos tópicos de pré-processamento dos dados e validação dos modelos, levando a uma solução, não ótima, mas completa do dataset do Titanic. É importante ter sempre em mente que cada tipo de modelo prefere os dados de uma certa forma quando realizando um pré-processamento. As técnicas ensinadas aqui podem te ajudar a processar os dados pra vários modelos.

Espero que essa aula tenha sido útil pra você. Não deixe de nos dar seu feedback, nós também estamos apredendo. Além disso, se você tiver qualquer dúvida, é só chamar no Telegram ou no Slack!

Obrigado pela atenção!


## Custom time imput

In this session we want to demonstrate another feature how to use the TTM and our software, where we want to give special attention to the source term $S(x,t)$ in the coupled heat diffusion equation.

\begin{align}
C_E \cdot\rho\cdot\partial_t T_E &= \partial_x(k_E(T_E)\partial_x T_E(x,t)) + G(T_L-T_E) + S(x,t) \\ \nonumber
C_L\cdot\rho\cdot\partial_t T_L &= \partial_x(k_L(T_L)\partial_x T_L(x,t)) + G(T_E-T_L)
\end{align}

* We consider, in $S(x,t)$ the time grid of a THz pulse, from data obtained in the lab and map it on the required time grid of the simulation.
* The spacial decay of the injected energy is computed via Lambert Beer´s law, i.e. exponential decay according to the optical penetration depth.
* The workflow of this session is as follows:
  * Load the data from the `.txt` file [THz-data](https://github.com/udcm-su/heat-diffusion-1D/blob/master/Examples/THzPulse.txt) and pass it on to the `source()` class. (Here the pulse is a THz in time but in principle it can be anything.)
  * Choose an extra refined time grid around $t_0$ of the pulse to correctly capture the dynamics in time
  * Define a material stack with `addLayer(...)` (here [SRO|STO]).
  * Run the simulation and visualize the output dynamics.

Not required but makes physical units more obvious: [SI-units](https://pypi.org/project/numericalunits/)

```python
from NTMpy import NTMpy as ntm
from matplotlib import pyplot as plt
import numpy as np
import numericalunits as u
u.reset_units('SI')
```

```python
#SRO- Parameters
length_SRO = 40*u.nm
n_SRO      = 0.5j #Approximation
k_SRO1 = 6*u.W/(u.m*u.K)
k_SRO2 = 1*u.W/(u.m*u.K)
C_SRO1    = lambda Te: 0.112*Te
C_SRO2  = 450
rho_SRO = 6500*u.kg/(u.m**3)
G_SRO   = 5e17*u.W/(u.m**3*u.K)
#STO- Parameters
length_STO = 80*u.nm
n_STO      = 0.5j #Approximation
k_STO1     = 12*u.W/(u.m*u.K)
k_STO2     = 1*u.W/(u.m*u.K)
C_STO1     = lambda Te: 0.025*Te
C_STO2     = 730
rho_STO    = 5100*u.kg/(u.m**3)
G_STO      = 5e17*u.W/(u.m**3*u.K)
```

```python
# Load data from the lab
labData = np.loadtxt('C:/Users/lUKAS/Documents/UDCM/THzPulse/THzPulse.txt')
ttime   = labData[:,0]*u.ps
ampl    = labData[:,1]
#Define a source
s                           = ntm.source()
s.spaceprofile              = "LB"
s.timeprofile               = "Custom"
s.fluence                   = 5*u.mJ/u.cm**2    #Fluence of the laser
s.t0                        = 1.5*u.ps          #Approximately the time of THz peak
s.lambda_vac                = 299800            #1THz in nm
s.loadData                  = [ttime,ampl]      #implementing the Data from the lab
s.adjusted_grid             = 'on'              #Injecting more points in the time grid
s.dt0                       = 3*u.ps            #t0-dt0 and t0+dt0 get more points
s.extra_points              = 600               #Amount of extra points
```

```python
#Two temperatures are considered, electron and lattice
sim = ntm.simulation(2,s)
#add parameters for both layers and both systems
#(lengt, refractive_index, conductivity [electron, lattice], heatCapacity [electron, lattice], density, linear Coupling)
#SRO Layer
sim.addLayer(length_SRO,n_SRO,[k_SRO1,k_SRO2],[C_SRO1,C_SRO2],rho_SRO,G_SRO)
#STO Layer
sim.addLayer(length_STO,n_STO,[k_STO1,k_STO2],[C_STO1,C_STO2],rho_STO,G_STO)
sim.final_time = 12*u.ps
#To get the raw output
[spacegrid,timegrid,Tempmap] = sim.run()
#Create a visual object where the simulation gets passed on
v = ntm.visual(sim)
```

    -----------------------------------------------------------
     No specific time constant has been indicated.
     The stability region has been calculated and an appropriate timestep has been chosen.
     Timestep = 2.79e-14 s
    -----------------------------------------------------------
    -----------------------------------------------------------
    A refined Grid around t0 has been applied
    -----------------------------------------------------------

    100%|██████████████████████████████████████████████████████████████████████████████| 867/867 [00:00<00:00, 2642.69it/s]

    -----------------------------------------------------------
    Heat diffusion in a coupled electron-lattice system has been simulated
    Eleapsed time in E.E.- loop: 0.34369754791259766
    -----------------------------------------------------------
    ------------------------------------------------------------
    The simulation object of the 2 temerature system has been passed on to the visual class.
    ------------------------------------------------------------
    -----------------------------------------------------------
    A refined Grid around t0 has been applied
    -----------------------------------------------------------

    100%|██████████████████████████████████████████████████████████████████████████████| 867/867 [00:00<00:00, 2283.25it/s]

    -----------------------------------------------------------
    Heat diffusion in a coupled electron-lattice system has been simulated
    Eleapsed time in E.E.- loop: 0.39531373977661133
    -----------------------------------------------------------

```python
#Output of v.source is the full matrix of the source(x,t)
so = v.source()
#Contour plots to depict heat map in space and time
v.contour('1')# Electronic system
v.contour('2')# Lattice system
#Time dynamics of Temperature, averaged in space
[tt,avTemp_vec] = v.average()
#To show the refined time grid
v.timegrid()
```

In the plot, where the data gets averaged in space and we can see the dynamics in time, the characteristic shape of a THz- pulse can be seen.

Note however, that formally the wavelength $\lambda_{vac}$ has still be provided to the source input `s.lambda_vac = number` (in nm).

This is because the decay of the pulse intensity, or how much of the energy gets locally absorbed, with respect to space, is calculated via the refractive index.

In the example given, the refractive index is not correct, but an assumption, since this data is not easy to obtain in the THz region.


# Standalone Fishbone-Moncrief C Code

We start with the NRPy+ expressions generated in the [Tutorial-FishboneMoncriefID](Tutorial-FishboneMoncriefID.ipynb), and output them to the C file "FishboneMoncriefID/FMstandalone.h".

Further, $\Gamma = \alpha u^0$ is given by (as shown [here](Tutorial-u0_smallb_Poynting-Cartesian.ipynb)):
$$
\Gamma = \alpha u^0 = \sqrt{\frac{1}{1 - \gamma_{ij}v^i_{(n)}v^j_{(n)}}}.
$$

```python
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
from outputC import lhrh         # NRPy+: Core C code output module
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import FishboneMoncriefID.FishboneMoncriefID as fmid

# Step 1: Set up the Fishbone-Moncrief initial data. This sets all the ID gridfunctions.
fmid.FishboneMoncriefID("Spherical")

gammaDD = ixp.zerorank2()

DIM = 3
for i in range(DIM):
    for j in range(DIM):
        if i<=j:
            gammaDD[i][j] = fmid.IDgammaDD[i][j]
        else:
            gammaDD[i][j] = fmid.IDgammaDD[j][i]

# gamma_{ij} v^i_{(n)} v^j_{(n)}
Gammacontraction = sp.sympify(0)
for i in range(DIM):
    for j in range(DIM):
        Gammacontraction += gammaDD[i][j] * fmid.IDValencia3velocityU[i] * fmid.IDValencia3velocityU[j]

Gammafactor = sp.sqrt(1 / (1 - Gammacontraction))

# -={ F-M quantities: Generate C code from expressions and output to file }=-
FishboneMoncrief_to_print = [\
                     lhrh(lhs="alpha",rhs=fmid.IDalpha),\
                     lhrh(lhs="betaU0",rhs=fmid.IDbetaU[0]),\
                     lhrh(lhs="betaU1",rhs=fmid.IDbetaU[1]),\
                     lhrh(lhs="betaU2",rhs=fmid.IDbetaU[2]),\
                     lhrh(lhs="Gammafactor",rhs=Gammafactor),\
                     lhrh(lhs="Gamma_times_ValenciavU0",rhs=Gammafactor*fmid.IDValencia3velocityU[0]),\
                     lhrh(lhs="Gamma_times_ValenciavU1",rhs=Gammafactor*fmid.IDValencia3velocityU[1]),\
                     lhrh(lhs="Gamma_times_ValenciavU2",rhs=Gammafactor*fmid.IDValencia3velocityU[2]),\
                     lhrh(lhs="uKS4U1",rhs=fmid.uKS4U[1]),\
                     lhrh(lhs="uKS4U2",rhs=fmid.uKS4U[2]),\
                     lhrh(lhs="uKS4U3",rhs=fmid.uKS4U[3]),\
                     lhrh(lhs="uBL4U1",rhs=fmid.uBL4U[1]),\
                     lhrh(lhs="uBL4U2",rhs=fmid.uBL4U[2]),\
                     lhrh(lhs="uBL4U3",rhs=fmid.uBL4U[3])
                     ]
# print(fmid.uKS4U[3])
fin.FD_outputC("FishboneMoncriefID/FM_standalone.h",FishboneMoncrief_to_print,params="outCverbose=False,CSE_enable=False")
```

    Wrote to file "FishboneMoncriefID/FM_standalone.h"

```python
%%writefile FishboneMoncriefID/FM_standalone.c

#include "stdio.h"
#include "stdlib.h"
#include "math.h"

const double a = 0.9375;
const double M = 1.0;
const double r_at_max_density = 12.0;
const double r_in = 6.0;

int main(int argc, const char *argv[]) {

    // Step 0a: Read command-line input, error out if nonconformant
    double xx0,xx1,xx2;
/*
    if(argc != 4) {
        printf("Error: Expected three command-line arguments: ./FM_standalone r theta phi\n");
        exit(1);
    }
    xx0 = strtod(argv[1],NULL);
    xx1 = strtod(argv[2],NULL);
    xx2 = strtod(argv[3],NULL);
*/

//    printf("# Output: r,th,ph, alpha, betaU0, betaU1, betaU2, Gamma, Gamma*vValenciaU0, Gamma*vValenciaU1, Gamma*vValenciaU2\n");
    for(double xx0=1.6;xx0<50.0;xx0+=0.2) {
        xx1 = 1.56463634120e0; //M_PI/2.0;
        xx2 = 0.0;
        double alpha,betaU0,betaU1,betaU2,Gammafactor,Gamma_times_ValenciavU0,Gamma_times_ValenciavU1,Gamma_times_ValenciavU2;
        double uKS4U1,uKS4U2,uKS4U3,uBL4U1,uBL4U2,uBL4U3;
#include "FM_standalone.h"
        if(xx0 < r_in) {
            Gammafactor = 1.0;
            Gamma_times_ValenciavU0 = Gamma_times_ValenciavU1 = Gamma_times_ValenciavU2 = 0.0;
            uKS4U1 = uKS4U2 = uKS4U3 = 0.0;
            uBL4U1 = uBL4U2 = uBL4U3 = 0.0;
        }
        printf("%e %e %e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e %.15e\n",
               xx0,xx1,xx2,
               alpha,betaU0,betaU1,betaU2,
               Gammafactor,
               Gamma_times_ValenciavU0, // util1(1) in FMtorus.f90; util(1,i,j,k) near the write statement
               Gamma_times_ValenciavU1, // util1(3) in FMtorus.f90.
               Gamma_times_ValenciavU2, // util1(2) in FMtorus.f90.
               uKS4U1,uKS4U2,uKS4U3,
               uBL4U1,uBL4U2,uBL4U3);
    }
    return 0;
}
```

    Writing FishboneMoncriefID/FM_standalone.c

```python
!gcc -O2 FishboneMoncriefID/FM_standalone.c -o FM_standalone -lm
```

```python
!./FM_standalone > out.txt
```

```python
%matplotlib inline

import matplotlib.pyplot as plt
import mpmath as mp
import csv

# Download torus_cuts.csv:
URL     = "http://astro.phys.wvu.edu/zetienne/torus_cuts.csv"
outfile = "torus_cuts.csv"
try:
    with open(outfile,"w") as file:
        file.write(urllib.request.urlopen(URL).read().decode("utf-8"))
except:
    try:
        with open(outfile,"w") as file:
            file.write(urllib.urlopen(URL).read().decode("utf-8"))
    except:
        # If all else fails, hope wget does the job
        !wget -O $outfile $URL

def file_reader(filename,list_of_cols,delim=" "):
    with open(filename) as file:
        reader = csv.reader(file, delimiter=delim)
        data  = list(zip(*reader))
#         print(data)
        # data is a tuple of strings. Tuples are immutable, and we need to perform math on
        #   the data, so here we convert tuple to lists of floats:
#        data_output = [[sp.sympify(0) for i in range(len(list_of_cols))] for j in range(len(data[0]))]
        data_output = [[sp.sympify(0) for i in range(len(data[0]))] for j in range(len(list_of_cols))]
        for i in range(len(data[0])):
            for j in range(len(list_of_cols)):
#                 print(i,j,data[list_of_cols[j]][i])
                data_output[j][i] = float(data[list_of_cols[j]][i])
        return data_output

NRPy_data_output = file_reader('out.txt',       [0,7,8,9,10])
std_data_output  = file_reader('torus_cuts.csv',[0,4,1,3,2])

ylabels = ['Lorentz Gamma_{KS}=G','G*v^r_{KS,Val.}','G*v^{\\theta}_{KS,Val.}','G*v^{\phi}_{KS,Val.}']

for i in range(len(ylabels)):
    # https://matplotlib.org/gallery/text_labels_and_annotations/legend.html#sphx-glr-gallery-text-labels-and-annotations-legend-py
    fig, ax = plt.subplots()
    plt.title("NRPy's FM solve with FMtorus.f90: "+ylabels[i])
    plt.xlabel("r/M")
    plt.ylabel(ylabels[i])
    ax.plot(NRPy_data_output[0], NRPy_data_output[i+1], 'k--', label='NRPyFMSolve')
    ax.plot(std_data_output[0],   std_data_output[i+1], 'k-',  label='FMtorus.f90')
    legend = ax.legend(loc='upper right', shadow=True, fontsize='x-large')
    legend.get_frame().set_facecolor('C1')
    plt.show()
```


# Optimization of a State-to-State Transfer in a Two-Level-System

```python
# NBVAL_IGNORE_OUTPUT
%load_ext watermark
import qutip
import numpy as np
import scipy
import matplotlib
import matplotlib.pylab as plt
import krotov
%watermark -v --iversions
pi = np.pi
sqrt = np.sqrt
basis = qutip.basis
tensor = qutip.tensor
coherent = qutip.coherent
```

    The watermark extension is already loaded. To reload it, use:
      %reload_ext watermark
    krotov           0.3.0
    numpy            1.15.4
    scipy            1.2.0
    matplotlib.pylab 1.15.4
    matplotlib       3.0.2
    qutip            4.3.1
    CPython 3.7.2
    IPython 7.2.0

$\newcommand{tr}[0]{\operatorname{tr}}
\newcommand{diag}[0]{\operatorname{diag}}
\newcommand{abs}[0]{\operatorname{abs}}
\newcommand{pop}[0]{\operatorname{pop}}
\newcommand{aux}[0]{\text{aux}}
\newcommand{opt}[0]{\text{opt}}
\newcommand{tgt}[0]{\text{tgt}}
\newcommand{init}[0]{\text{init}}
\newcommand{lab}[0]{\text{lab}}
\newcommand{rwa}[0]{\text{rwa}}
\newcommand{bra}[1]{\langle#1\vert}
\newcommand{ket}[1]{\vert#1\rangle}
\newcommand{Bra}[1]{\left\langle#1\right\vert}
\newcommand{Ket}[1]{\left\vert#1\right\rangle}
\newcommand{Braket}[2]{\left\langle #1\vphantom{#2} \mid
#2\vphantom{#1}\right\rangle}
\newcommand{op}[1]{\hat{#1}}
\newcommand{Op}[1]{\hat{#1}}
\newcommand{dd}[0]{\,\text{d}}
\newcommand{Liouville}[0]{\mathcal{L}}
\newcommand{DynMap}[0]{\mathcal{E}}
\newcommand{identity}[0]{\mathbf{1}}
\newcommand{Norm}[1]{\lVert#1\rVert}
\newcommand{Abs}[1]{\left\vert#1\right\vert}
\newcommand{avg}[1]{\langle#1\rangle}
\newcommand{Avg}[1]{\left\langle#1\right\rangle}
\newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
\newcommand{Re}[0]{\operatorname{Re}}
\newcommand{Im}[0]{\operatorname{Im}}$
The purpose of this example is not to solve an especially interesting physical
problem but to give a rather simple example of how the package can be used in
order to solve an optimization problem.

## Define the Hamiltonian

In the
following the Hamiltonian, guess field and
states are defined.

The Hamiltonian
$\op{H}_{0} = - \omega \op{\sigma}_{z}$
represents a
simple qubit with energy
level splitting $\omega$ in the basis
$\{\ket{0},\ket{1}\}$. The control
field
$\epsilon(t)$ is assumed to couple via
the
Hamiltonian $\op{H}_{1}(t) =
\epsilon(t) \op{\sigma}_{x}$ to the qubit,
i.e., the control
field effectively
drives
transitions between both qubit
states. For now, we initialize the control
field as constant.

```python
N = 12
α = 2
```

# Plotting functions

```python
def plot_population(result):
    fig, ax = plt.subplots()
    ax.plot(result.times, result.expect[0], label='0')
    ax.plot(result.times, result.expect[1], label='1')
    ax.legend()
    ax.set_xlabel('time')
    ax.set_ylabel('population')
    plt.show(fig)

def plot_system(ψ):
    bl = qutip.Bloch()
    bl.add_states(ψ.ptrace(0))
    bl.show()
    qutip.visualization.plot_wigner_fock_distribution(ψ.ptrace(1))
def plot_resonator(ψ):
    fig, ax = plt.subplots(1,len(ψ), figsize=(3*len(ψ),3))
    for (ϕ, axis) in zip(ψ, ax):
        qutip.visualization.plot_wigner(ϕ.ptrace(1), fig=fig, ax=axis, alpha_max = 2*α)
        axis.axis_equal = True

def plot_cardinal(ψ):
    bl = qutip.Bloch()
    bl.vector_color = ['r','g','b','g','b','r']
    [bl.add_states(ϕ.ptrace(0), 'vector') for ϕ in ψ]
    bl.show()

def plot_all(dyn, ψ):
    ψ_i = [g.states[0] for g in dyn]
    ψ_f = [g.states[-1] for g in dyn]
    ψ_t = [ϕ[1] for ϕ in ψ]
    plot_cardinal(ψ_i)
    plot_resonator(ψ_i)
    plot_cardinal(ψ_t)
    plot_resonator(ψ_t)
    plot_cardinal(ψ_f)
    plot_resonator(ψ_f)
```

```python
def cat(N,α,P = 1):
    return (coherent(N,α) + P*coherent(N,-α)).unit()
def fid(result, target):
    return (np.abs((result.states[-1].dag()*target).full())**2)[0][0]
```

```python
Ri = qutip.operators.identity(N)
Si = qutip.operators.identity(2)

σ_z = qutip.operators.sigmaz()
σ_y = qutip.operators.sigmay()
σ_x = qutip.operators.sigmax()
a = qutip.operators.destroy(N)

σ_z = qutip.tensor(σ_z,Ri)
σ_y = qutip.tensor(σ_y,Ri)
σ_x = qutip.tensor(σ_x,Ri)
b = qutip.tensor(qutip.operators.destroy(2),Ri)
a = qutip.tensor(Si,a)
I = qutip.tensor(Si,Ri)

cat_0 = cat(N,α)
cat_1 = cat(N,α*1j)

def hamiltonian(ω=1.0, ampl0=0.2):
    """Two-level-system Hamiltonian

    Args:
        ω (float): energy separation of the qubit levels
        ampl0 (float): constant amplitude of the driving field
    """

    K_r   = 2*pi*0.45e-3   # Kerr res
    K_q   = 2*pi*297e-3    # Kerr qubit 200-300 MHz
    K_r   = 10
    K_q   = 1
    ω_r = 2.0 * 2 * pi      # resonator frequency
    ω_q = 3.0 * 2 * pi      # qubit frequency
    χ = 0.025 * 2 * pi   # parameter in the dispersive hamiltonian

    delta = abs(ω_r - ω_q)    # detuning
    g = sqrt(delta * χ)  # coupling strength that is consistent with chi

    #H_occ = w_r*a.dag()*a + w_q*b.dag()*b
    H_occ = -ω_q/2.0 * σ_z  + ω_r* a.dag()*a
    use_dispersive = True
    use_kerr = True
    if use_dispersive:
        #H_coup = - chi_qr * a.dag()*a * b.dag()*b
        H_coup =  χ * (a.dag() * a + I/2) * σ_z
    else:
        #H_coup = g * (a.dag() * b + a * b.dag())
        H_coup = g * σ_x *(a.dag() + a)
    if use_kerr:
        H_kerr = - K_r/2 * a.dag()**2 * a**2 - K_q/2 * b.dag()**2 * b**2
    else:
        H_kerr = tensor(qubit.operators.qzero(2), qubit.operators.qzero(N))

    H_d = H_occ + H_coup + H_kerr

    H_qr = (b + b.dag())
    H_qi = 1j*(b.dag() - b)
    H_rr = (a + a.dag())
    H_ri = 1j*(a.dag() - a)

    ϵ_qr = lambda t, args: ampl0*np.cos(ω_q*t)
    ϵ_qi = lambda t, args: ampl0*np.sin(ω_q*t)
    ϵ_rr = lambda t, args: ampl0*np.cos(ω_r*t)
    ϵ_ri = lambda t, args: ampl0*np.sin(ω_r*t)
    return [H_d, [H_qr, ϵ_qr], [H_qi, ϵ_qi], [H_rr, ϵ_rr], [H_ri, ϵ_ri]]

def coeffs_to_state(c,init = True):
    if init:
        ψ = tensor((c[0]*basis(2,0) + c[1]*basis(2,1)).unit() , (basis(N,0)))
    else:
        ψ = tensor((basis(2,0)) , (c[0]*cat_0 + c[1]*cat_1).unit())
    return ψ

def states(coeffs):
    return [[coeffs_to_state(c,True),coeffs_to_state(c,False)] for c in coeffs]
H = hamiltonian()
coeffs = [(1,0), (1,-1), (1,1j), (1,1), (1,-1j), (0,1)]
ψ = states(coeffs)
```

The projectors $\op{P}_0 = \ket{0}\bra{0}$ and $\op{P}_1 = \ket{1}\bra{1}$ are
introduced since they allow for calculating the
population in the respective
states later on.

```python
def proj(ψ):
    return ψ * ψ.dag()
```

## Define the optimization target

First we define the time grid of the
dynamics, i.e., by taking the following
values as an example, we define the
initial state to be at time $t=0$ and
consider a total propagation time of
$T=5$. The entire time grid is divided into
$n_{t}=500$ equidistant time steps.

```python
T = 20
steps = 1000
tlist = np.linspace(0, T, steps)
```

Next, we define the optimization targets, which is technically a list of
objectives, but here it has just one entry defining a simple state-to-state
transfer
from initial state $\ket{\Psi_{\init}} = \ket{0}$ to the target state
$\ket{\Psi_{\tgt}} = \ket{1}$, which we want to reach at final time $T$. Note
that we also have to pass the Hamiltonian $\op{H}(t)$ that determines the
dynamics of
the system to the optimization objective.

```python
objectives = [krotov.Objective(initial_state=ϕ[0], target=ϕ[1], H=H) for ϕ in ψ]
```

In addition, we have to define and assign a shape function $S(t)$ for the update
in each control iteration to each
control field that will be updated. This shape
usually takes care of
experimental limits such as the necessity of finite ramps
at the beginning and
end of the control field or other conceivable limitations
for field shapes: wherever $S(t)$ is zero, the optimization will not change the
value of the control from the original guess.

```python
def S(t):
    """Shape function for the field update"""
    return krotov.shapes.flattop(t, t_start=0, t_stop=T, t_rise=0.06*T, t_fall=0.06*T, func='sinsq')
```

At this point, we also change the initial control field $\epsilon_{0}(t)$ from a
constant to a shaped pulse that switches on smoothly from zero and again
switches off at the final time $T$. We re-use the shape function $S(t)$ that we
defined for the updates for this purpose (although generally, $S(t)$ for the
updates has nothing to with the shape of the control field).

```python
def shape_field(ϵ):
    """Applies the shape function S(t) to the guess field"""
    ϵ_shaped = lambda t, args: ϵ(t, args)*S(t)
    return ϵ_shaped

for H_i in H[1:]:
    H_i[1] = shape_field(H_i[1])

```

Having defined the shape function $S(t)$ and having shaped the guess field, we
now tell the optimization to also use $S(t)$ as the update-shape for
$\epsilon_0(t)$. In addition, we have to choose `lambda_a` for each control
field. It controls the update magnitude of the respective field in each
iteration.

```python
opt_lambda = [.5]
pulse_options = {H_i[1]: dict(lambda_a=opt_lambda[0], shape=S) for H_i in H[1:]}
```

It is convenient to introduce the function `print_fidelity`, which can be passed
to the optimization procedure and will be called after each iteration and thus
provides additional feedback about the optimization progress.

```python
def print_fidelity(**args):
    F_re = np.average(np.array(args['tau_vals']).real)
    print("    F = {}".format(F_re))
    #return F_re
```

## Simulate dynamics of the guess field

Before heading towards the optimization
procedure, we first simulate the
dynamics under the guess field
$\epsilon_{0}(t)$.

```python
def plot_pulse(pulse, tlist):
    fig, ax = plt.subplots(figsize=(15,4))
    if callable(pulse):
        pulse = np.array([pulse(t, args=None) for t in tlist])
    ax.plot(tlist, pulse)
    ax.set_xlabel('Time')
    ax.set_ylabel('Pulse amplitude')
    plt.show(fig)
```

The following plot shows the guess field $\epsilon_{0}(t)$, which is, as chosen
above, just a constant field (with a smooth switch-on and switch-off)

```python
for H_i in H[1:]:
    plot_pulse(H_i[1], tlist)
```

The next line solves the equation of motion for the defined objective, which
contains the initial state $\ket{\Psi_{\init}}$ and the Hamiltonian $\op{H}(t)$
defining its evolution.

```python
guess_dynamics = [ob.mesolve(tlist, progress_bar=True) for ob in objectives]
```

    10.0%. Run time:   0.24s. Est. time left: 00:00:00:02
    20.0%. Run time:   0.46s. Est. time left: 00:00:00:01
    30.0%. Run time:   0.75s. Est. time left: 00:00:00:01
    40.0%. Run time:   1.08s. Est. time left: 00:00:00:01
    50.0%. Run time:   1.31s. Est. time left: 00:00:00:01
    60.0%. Run time:   1.56s. Est. time left: 00:00:00:01
    70.0%. Run time:   1.89s. Est. time left: 00:00:00:00
    80.0%. Run time:   2.16s. Est. time left: 00:00:00:00
    90.0%. Run time:   2.47s. Est. time left: 00:00:00:00
    Total run time:   2.82s
    10.0%. Run time:   0.28s. Est. time left: 00:00:00:02
    20.0%. Run time:   0.78s. Est. time left: 00:00:00:03
    30.0%. Run time:   1.42s. Est. time left: 00:00:00:03
    40.0%. Run time:   1.97s. Est. time left: 00:00:00:02
    50.0%. Run time:   2.43s. Est. time left: 00:00:00:02
    60.0%. Run time:   2.91s. Est. time left: 00:00:00:01
    70.0%. Run time:   3.34s. Est. time left: 00:00:00:01
    80.0%. Run time:   3.89s. Est. time left: 00:00:00:00
    90.0%. Run time:   4.44s. Est. time left: 00:00:00:00
    Total run time:   5.08s
    10.0%. Run time:   0.27s. Est. time left: 00:00:00:02
    20.0%. Run time:   0.73s. Est. time left: 00:00:00:02
    30.0%. Run time:   1.20s. Est. time left: 00:00:00:02
    40.0%. Run time:   1.62s. Est. time left: 00:00:00:02
    50.0%. Run time:   2.06s. Est. time left: 00:00:00:02
    60.0%. Run time:   2.55s. Est. time left: 00:00:00:01
    70.0%. Run time:   3.02s. Est. time left: 00:00:00:01
    80.0%. Run time:   3.53s. Est. time left: 00:00:00:00
    90.0%. Run time:   3.98s. Est. time left: 00:00:00:00
    Total run time:   4.52s
    10.0%. Run time:   0.24s. Est. time left: 00:00:00:02
    20.0%. Run time:   0.64s. Est. time left: 00:00:00:02
    30.0%. Run time:   1.18s. Est. time left: 00:00:00:02
    40.0%. Run time:   1.66s. Est. time left: 00:00:00:02
    50.0%. Run time:   2.09s. Est. time left: 00:00:00:02
    60.0%. Run time:   2.55s. Est. time left: 00:00:00:01
    70.0%. Run time:   2.99s. Est. time left: 00:00:00:01
    80.0%. Run time:   3.40s. Est. time left: 00:00:00:00
    90.0%. Run time:   3.83s. Est. time left: 00:00:00:00
    Total run time:   4.37s
    10.0%. Run time:   0.29s. Est. time left: 00:00:00:02
    20.0%. Run time:   0.73s. Est. time left: 00:00:00:02
    30.0%. Run time:   1.21s. Est. time left: 00:00:00:02
    40.0%. Run time:   1.69s. Est. time left: 00:00:00:02
    50.0%. Run time:   2.12s. Est. time left: 00:00:00:02
    60.0%. Run time:   2.55s. Est. time left: 00:00:00:01
    70.0%. Run time:   2.96s. Est. time left: 00:00:00:01
    80.0%. Run time:   3.37s. Est. time left: 00:00:00:00
    90.0%. Run time:   3.81s. Est. time left: 00:00:00:00
    Total run time:   4.40s
    10.0%. Run time:   0.22s. Est. time left: 00:00:00:01
    20.0%. Run time:   0.48s. Est. time left: 00:00:00:01
    30.0%. Run time:   0.70s. Est. time left: 00:00:00:01
    40.0%. Run time:   0.92s. Est. time left: 00:00:00:01
    50.0%. Run time:   1.14s. Est. time left: 00:00:00:01
    60.0%. Run time:   1.37s. Est. time left: 00:00:00:00
    70.0%. Run time:   1.58s. Est. time left: 00:00:00:00
    80.0%. Run time:   1.85s. Est. time left: 00:00:00:00
    90.0%. Run time:   2.08s. Est. time left: 00:00:00:00
    Total run time:   2.32s

The plot of the population dynamics shows that the guess field does not transfer
the initial state $\ket{\Psi_{\init}} = \ket{0}$ to the desired target state
$\ket{\Psi_{\tgt}} = \ket{1}$.

```python
plot_all(guess_dynamics, ψ)
```

## Optimize

In the following we optimize the guess field $\epsilon_{0}(t)$ such
that the intended state-to-state transfer $\ket{\Psi_{\init}} \rightarrow
\ket{\Psi_{\tgt}}$ is solved.

The cell below carries out the optimization. It
requires, besides the
previously
defined optimization `objectives`, information
about the
optimization functional
$F$ (via `chi_constructor`) and the
propagation method that should be used. In
addition, the number of total
iterations is required and, as an option, we pass
an info-hook that after each
iteration combines a complete printout of the state
of the optimization with the
`print_fidelity` function defined above.

Here, we
choose $F = F_{re}$ with
\begin{equation}
F_{re}
=
\Re\Braket{\Psi(T)}{\Psi_{\tgt}}
\end{equation}

with
$\ket{\Psi(T)}$ the
forward propagated state of $\ket{\Psi_{\init}}$.

```python
# Reset results
oct_result = None
```

```python
iters = 10
if oct_result is not None:
    iters = oct_result.iters[-1] + iters

oct_result = krotov.optimize_pulses(
    objectives,
    pulse_options=pulse_options,
    tlist=tlist,
    propagator=krotov.propagators.expm,
    chi_constructor=krotov.functionals.chis_re,
    info_hook=krotov.info_hooks.chain(
        krotov.info_hooks.print_table(J_T=krotov.functionals.F_ss),
        print_fidelity
    ),
    check_convergence=krotov.convergence.Or(
        krotov.convergence.value_below(1e-3, name='J_T'),
        krotov.convergence.delta_below(1e-5),
        #krotov.convergence.check_monotonic_error,
    ),
    iter_stop=iters,
    continue_from = oct_result
)
```

      iter.        J_T   ∑∫gₐ(t)dt          J       ΔJ_T         ΔJ  secs
          0   8.63e-01    0.00e+00   8.63e-01        n/a        n/a    24
        F = 0.9278502468677411
         11   8.91e-01    1.57e-02   9.07e-01   2.84e-02   4.41e-02    58 **
        F = 0.9434210714137601
         12   9.06e-01    7.82e-03   9.13e-01   1.45e-02   2.23e-02    53 **
        F = 0.9511976883671558
         13   9.14e-01    4.33e-03   9.18e-01   8.04e-03   1.24e-02    53 **
        F = 0.9555267758081483
         14   9.19e-01    2.68e-03   9.21e-01   4.99e-03   7.67e-03    53 **
        F = 0.9582163420552078
         15   9.22e-01    1.74e-03   9.24e-01   3.26e-03   5.00e-03    55 **
        F = 0.9599627865037025
         16   9.24e-01    1.15e-03   9.25e-01   2.18e-03   3.33e-03    58 **
        F = 0.9611222714810728
         17   9.26e-01    7.85e-04   9.26e-01   1.49e-03   2.27e-03    53 **
        F = 0.9619121240358566
         18   9.27e-01    5.52e-04   9.27e-01   1.05e-03   1.61e-03    52 **
        F = 0.9624691040196324
         19   9.27e-01    4.04e-04   9.28e-01   7.76e-04   1.18e-03    52 **
        F = 0.9628777967301221

```python
oct_result
```

## Simulate dynamics of the optimized field

Having obtained the optimized
control field, we can now
plot it and calculate the
population dynamics under
this field.

```python
[plot_pulse(c, tlist) for c in oct_result.optimized_controls]
```

In contrast to the dynamics under the guess field, the optimized field indeed
drives the initial state $\ket{\Psi_{\init}} = \ket{0}$ to the desired target
state $\ket{\Psi_{\tgt}} = \ket{1}$.

```python
opt_dynamics = [ob.mesolve(tlist, progress_bar=True) for ob in oct_result.optimized_objectives]
```

```python
#print(fid(opt_dynamics, ψ[0][1]))
plot_all(opt_dynamics, ψ)
```

```python

```

Archive/Other tests/Cat state encoding tests.ipynb


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Class 9: Introduction to Business Cycle Data

The *business cycle* is the fluctuation of many macroeconomic quantities that last for about 1.5 to 8 years. Colloquially, the term refers to the alternating periods of expansion and contraction in the macroeconomy. Business cycle fluctuations are costly because they are associated with misallocations of capital and labor. Recessions are particularly painful for workers that become unemployed and for the families of workers who become unemployed. The costs of the business cycle have driven research into understanding the cause of the cycle.

The collective set of theories to explain the cycle is called *business cycle theory* and eventually we'll study and critique two competing theoretical perspectives. However, before we approach the theory, we should first uncover some empirical facts about the business cycle that models should be able to explain.

In this lecture, we will:

1. Visualize the difference between the trend and cyclical components of GDP, consumption, investent, and hours.
2. Compute the log-deviations from trend of GDP, consumption, investent, and hours.
3. Compute summary statistics about the business cycle that models of the cycle should be able to explain.

## Data

The file `rbc_data_actual_trend.csv`, available at https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/rbc_data_actual_trend.csv, contains actual and trend data for real GDP per capita, real consumption per capita, real investment per capita, real physical capital per capita, TFP, and hours per capita at quarterly frequency. The GDP, consumption, investment, and capital data are in terms of 2012 dollars. Hours is measured as an index with the value in October 2012 set to 100. All of the data are *real* quantities. That is, there are no *nominal* quantities like money or inflation or a nominal interest rate. The reason is that the first theory that we will encounter is called *real business cycle* or RBC theory and, in that theory, there is no place for nominal quantities. RBC theory seeks to explain fluctuations in real quantities as being primarily due to TFP shocks; i.e., shocks to the production function.

```python
# Read business_cycle_data_actual_trend.csv into a Pandas DataFrame with the first column set as the index and parse_dates=True
df = pd.read_csv('https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/rbc_data_actual_trend.csv',index_col=0,parse_dates=True)

# Print the last five rows of the data
df.tail()
```

      gdp
      gdp_trend
      consumption
      consumption_trend
      investment
      investment_trend
      hours
      hours_trend
      capital
      capital_trend
      tfp
      tfp_trend

      2017-07-01
      70.962825
      70.917497
      48.396808
      48.310246
      12.367761
      12.448772
      104.125352
      104.335782
      81.231703
      80.970914
      26.390793
      26.369009

      2017-10-01
      71.202896
      71.165850
      48.753914
      48.505150
      12.355945
      12.525952
      104.740753
      104.601773
      81.540436
      81.257996
      26.343841
      26.384892

      2018-01-01
      71.316755
      71.416054
      48.679327
      48.700981
      12.610681
      12.603944
      104.926059
      104.868649
      81.705191
      81.547025
      26.337055
      26.401009

      2018-04-01
      71.901724
      71.667788
      48.877348
      48.897710
      12.608979
      12.682658
      105.232911
      105.136299
      82.080416
      81.837588
      26.460262
      26.417303

      2018-07-01
      72.326256
      71.920661
      49.191966
      49.095294
      12.991636
      12.762006
      105.431597
      105.404649
      82.413648
      82.129363
      26.546207
      26.433677

```python
# Construct a 2x2 grid of plots of GDP, consumption, investment, and hours. CELL PROVIDED
base_year = '2012'
fig = plt.figure(figsize=(12,8))

ax1 = fig.add_subplot(2,2,1)
ax1.plot(df.gdp,'-',lw=3,alpha = 0.7)
ax1.grid()
ax1.set_title('GDP per capita')
ax1.set_ylabel('Thousands of '+base_year+' dollars')

ax2 = fig.add_subplot(2,2,2)
ax2.plot(df.consumption,'-',lw=3,alpha = 0.7)
ax2.grid()
ax2.set_title('Consumption per capita')
ax2.set_ylabel('Thousands of '+base_year+' dollars')

ax3 = fig.add_subplot(2,2,3)
ax3.plot(df.investment,'-',lw=3,alpha = 0.7)
ax3.grid()
ax3.set_title('Investment per capita')
ax3.set_ylabel('Thousands of '+base_year+' dollars')

ax4 = fig.add_subplot(2,2,4)
ax4.plot(df.hours,'-',lw=3,alpha = 0.7)
ax4.grid()
ax4.set_title('Hours per capita')
ax4.set_ylabel('Index ('+base_year+'=100)')

fig.tight_layout()
```

## Cycles and Trends

Business cycle theory is tested against data. However, data on the business cycle data is not readily available and must be constructed. A time series $X_t$ can be decomposed into a trend component $X_t^{trend}$ and a cyclical component $X_t^{cycle}$ such that:

\begin{align}
X_t & = X_t^{trend} + X_t^{cycle}. \tag{1}
\end{align}

In equation (1), $X_t^{trend}$ is the long-run value about which $X_t$ fluctuates. $X^{cycle}_t$ is the amount by which $X_t$ excedes its trend. The process for decomposing a series into trend and cyclical components is called *filtering* and is more technical than we want to get into. We'll take for granted that such procedures exist.

Often times, it's useful to express the cyclical component of a time series as the difference between the (natural) log of the series and the log of the trend:

\begin{align}
\hat{x}_t & = \log\left(X_t\right) - \log\left(X_t^{trend}\right) \approx\frac{X_t-X_t^{trend}}{X_t^{trend}}
\end{align}

The log-deviation from trend is approximately equal to the percent deviation of the series from trend (divided by 100).

### GDP

```python
# Construct a plot of real GDP with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.gdp,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.gdp_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('GDP per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Create a new column called gdp_cycle equal to the difference between acual and trend GDP
df['gdp_cycle'] = df['gdp'] - df['gdp_trend']

# Create a new column called gdp_cycle_dev equal to the log difference between actual GDP and trend GDP:
df['gdp_cycle_dev'] = np.log(df['gdp']) - np.log(df['gdp_trend'])

# Plot the log deviation of GDP from its trend (times 100)
plt.plot(df.gdp_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('GDP: Percent deviation from trend')
plt.grid()
```

### Consumption, Investment, and Hours

```python
# Create three new columns called cons_cycle, invest_cycle, and hours_cycle equal to the cyclical components of the
# respective series
df['cons_cycle'] = df['consumption'] - df['consumption_trend']
df['invest_cycle'] = df['investment'] - df['investment_trend']
df['hours_cycle'] = df['hours'] - df['hours_trend']

# Create a new column called cons_cycle_dev, invest_cycle_dev, and hours_cycle_dev equal to the log difference between
# the actual and trend values of the respective series:
df['cons_cycle_dev'] = np.log(df['consumption']) - np.log(df['consumption_trend'])
df['invest_cycle_dev'] = np.log(df['investment']) - np.log(df['investment_trend'])
df['hours_cycle_dev'] = np.log(df['hours']) - np.log(df['hours_trend'])

```

```python
# Construct a plot of consumption with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.consumption,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.consumption_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('Consumption per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of consumption from its trend (times 100)
plt.plot(df.cons_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Consumption: Percent deviation from trend')
plt.grid()
```

```python
# Construct a plot of investment with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.investment,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.investment_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('Investment per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of investment from its trend (times 100)
plt.plot(df.invest_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Investment: Percent deviation from trend')
plt.grid()
```

```python
# Construct a plot of hours with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.hours,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.hours_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Index: ('+base_year+'=100)')
plt.title('Hours per capita')
plt.legend(loc='lower left',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of hours from its trend (times 100)
plt.plot(df.hours_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Hours: Percent deviation from trend')
plt.grid()
```

## US Business Cycle Statistics

```python
# Create a new variable called df_cycle that is a DataFrame with columns columns gdp_cycle_dev, cons_cycle_dev,
# invest_cycle_dev, and hours_cycle_dev from df.
df_cycle = df[['gdp_cycle_dev', 'cons_cycle_dev', 'invest_cycle_dev', 'hours_cycle_dev']]

# Print the first five rows of df_cycle
print(df_cycle.head())
```

                gdp_cycle_dev  cons_cycle_dev  invest_cycle_dev  hours_cycle_dev
    1948-01-01       0.021966        0.007885          0.050324         0.027210
    1948-04-01       0.025403        0.011468          0.096154         0.023054
    1948-07-01       0.017087       -0.000568          0.109654         0.025447
    1948-10-01       0.005779       -0.007582          0.074770         0.012991
    1949-01-01      -0.020577       -0.017215         -0.103118        -0.007194

```python
# Use the DataFrame method .mean() to find the average values of the gdp_cycle_dev, cons_cycle_dev, invest_cycle_dev,
# and hours_cycle_dev columns
df_cycle.mean()
```

    gdp_cycle_dev      -3.256602e-14
    cons_cycle_dev     -7.624368e-14
    invest_cycle_dev   -4.837042e-14
    hours_cycle_dev    -1.256004e-14
    dtype: float64

```python
# Use the DataFrame method .std() to find the standard deviations of the gdp_cycle_dev, cons_cycle_dev, invest_cycle_dev,
# and hours_cycle_dev columns
df_cycle.std()
```

    gdp_cycle_dev       0.016202
    cons_cycle_dev      0.011571
    invest_cycle_dev    0.074918
    hours_cycle_dev     0.018924
    dtype: float64

```python
# Use the DataFrame method .corr() to find the coefficients of correlation among the gdp_cycle_dev, cons_cycle_dev,
# invest_cycle_dev, and hours_cycle_dev columns
df_cycle.corr()
```

      gdp_cycle_dev
      cons_cycle_dev
      invest_cycle_dev
      hours_cycle_dev

      gdp_cycle_dev
      1.000000
      0.794574
      0.847777
      0.875156

      cons_cycle_dev
      0.794574
      1.000000
      0.681763
      0.705705

      invest_cycle_dev
      0.847777
      0.681763
      1.000000
      0.789560

      hours_cycle_dev
      0.875156
      0.705705
      0.789560
      1.000000

**Questions**

1. Which quantity varies the most over the business cycle?
2. Which quantity varies the least over the business cycle?
3. Which quantity is most correlated with GDP over the business cycle?

**Answers**

1. Investment fluctuates the most over the business cycle.
2. Consumption fluctuates the least over the business cycle.
3. Hours is the quantity that is most correlated with GDP over the cycle. Since the capital stock changes slowly over time, large fluctuations due primarily to large fluctuations in employment.

```python
# Plot the cyclical components of GDP, consumption, investment, and hours (all times 100) on the same set of axes
fig = plt.figure(figsize=(12,4))
ax = fig.add_subplot(1,1,1)
(df_cycle*100).plot(ax = ax,legend=False,lw=3,alpha=0.7)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
ax.set_ylabel('Percent')
ax.set_title('US Business cycle data')
ax.grid()
```