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

# #Manipulando e limpando dados
# Esta seção marca uma mudança sutil. Até agora, apresentamos ideias e técnicas para prepará-lo com uma caixa de ferramentas de técnicas para lidar com situações do mundo real. Agora vamos começar a usar algumas dessas ferramentas e, ao mesmo tempo, dar algumas idéias sobre como e quando usá-las em seu próprio trabalho com dados.
#
# Os dados do mundo real são confusos. Provavelmente, você precisará combinar várias fontes de dados para obter os dados que realmente deseja. Os dados dessas fontes estarão incompletos. E provavelmente não será formatado exatamente da maneira que você deseja para realizar sua análise. É por esses motivos que a maioria dos cientistas de dados dirá que cerca de 80% de qualquer projeto é gasto apenas para colocar os dados em um formulário pronto para análise.
#
# ## Explorando informações do `DataFrame`
#
# > **Objetivo de aprendizagem:** Ao final desta subseção, você deve estar confortável para encontrar informações gerais sobre os dados armazenados nos DataFrames do pandas.
#
# Depois de carregar seus dados no pandas, é mais provável que eles estejam em um `DataFrame`. No entanto, se o conjunto de dados em seu `DataFrame` tem 60.000 linhas e 400 colunas, como você começa a ter uma noção do que está trabalhando? Felizmente, o pandas fornece algumas ferramentas convenientes para examinar rapidamente informações gerais sobre um `DataFrame`, além das primeiras e últimas linhas.
#
# Para explorar essa funcionalidade, importaremos a biblioteca Python scikit-learn e usaremos um conjunto de dados icônico que todo cientista de dados já viu centenas de vezes: o conjunto de dados * Iris * do biólogo britânico Ronald Fisher usado em seu artigo de 1936 "O uso de múltiplos medições em problemas taxonômicos ":

# In[1]:

import pandas as pd
from sklearn.datasets import load_iris

iris = load_iris()
iris_df = pd.DataFrame(data=iris['data'], columns=iris['feature_names'])

# ### `DataFrame.info`
# Vamos dar uma olhada neste conjunto de dados para ver o que temos:

# In[2]:

iris_df.info()

# A partir disso, sabemos que o conjunto de dados * Iris * tem 150 entradas em quatro colunas. Todos os dados são armazenados como números de ponto flutuante de 64 bits.
#

# ### `DataFrame.head`
# A seguir, vamos ver como são as primeiras linhas do nosso `DataFrame`:
#

# In[3]:

iris_df.head()

# ### `DataFrame.tail`
# O outro lado de DataFrame.head é DataFrame.tail, que retorna as últimas cinco linhas de um DataFrame:
#

# In[4]:

iris_df.tail()

# Na prática, é útil examinar facilmente as primeiras linhas ou as últimas linhas de um DataFrame, principalmente quando você está procurando outliers em conjuntos de dados ordenados.
#
# > Conclusão: mesmo olhando para os metadados sobre as informações em um DataFrame ou os primeiros e os últimos valores em um, você pode ter uma ideia imediata sobre o tamanho, a forma e o conteúdo dos dados com os quais está lidando.

# ## Lidando com dados ausentes (missing data)
#
# > **Objetivo de aprendizagem:** Ao final desta subseção, você deve saber como substituir ou remover valores nulos de DataFrames.
#
# Na maioria das vezes, os conjuntos de dados que você deseja usar (ou deve usar) têm valores ausentes. A maneira como os dados ausentes são tratados traz consigo compensações sutis que podem afetar sua análise final e os resultados do mundo real.
#
# O Pandas lida com valores ausentes de duas maneiras. O primeiro que você viu antes nas seções anteriores: `NaN` ou Not a Number. Na verdade, este é um valor especial que faz parte da especificação de ponto flutuante IEEE e é usado apenas para indicar valores de ponto flutuante ausentes.
#
# Para valores ausentes além de flutuantes, o pandas usa o objeto Python `Nenhum`. Embora possa parecer confuso encontrar dois tipos diferentes de valores que dizem essencialmente a mesma coisa, existem razões programáticas sólidas para essa escolha de design e, na prática, seguir esse caminho permite que os pandas forneçam um bom compromisso para a grande maioria dos casos. Não obstante, `Nenhum` e` NaN` trazem restrições que você precisa estar ciente sobre como eles podem ser usados.

# ### `None`: non-float missing data
# Como `None` vem do Python, ele não pode ser usado em matrizes NumPy e pandas que não são do tipo de dados `'object'`. Lembre-se de que as matrizes NumPy (e as estruturas de dados nos pandas) podem conter apenas um tipo de dados. Isso é o que lhes dá um tremendo poder para dados em grande escala e trabalho computacional, mas também limita sua flexibilidade. Esses arrays precisam fazer upcast para o “menor denominador comum”, o tipo de dados que abrangerá tudo no array. Quando `None` está no array, significa que você está trabalhando com objetos Python.
#
# Para ver isso em ação, considere o seguinte exemplo de array (observe o `dtype` para ele):

# In[5]:

import numpy as np

example1 = np.array([2, None, 6, 8])
example1

# A realidade dos tipos de dados upcast traz dois efeitos colaterais. Primeiro, as operações serão realizadas no nível do código Python interpretado, em vez do código NumPy compilado. Essencialmente, isso significa que quaisquer operações envolvendo `Series` ou` DataFrames` com `None` neles serão mais lentas. Embora você provavelmente não notaria esse impacto no desempenho, para grandes conjuntos de dados ele pode se tornar um problema.
#
# O segundo efeito colateral decorre do primeiro. Porque `None` essencialmente arrasta` Series` ou `DataFrames` de volta ao mundo do Python simples, usando agregações NumPy / pandas como` sum () `ou` min () `em matrizes que contêm um valor ``None`` geralmente produzirá um erro:

# In[6]:

example1.sum()

# ### `NaN`: missing float values
#
# Em contraste com `None`, NumPy (e, portanto, pandas) suporta `NaN` para suas operações e ufuncs vetorizadas rápidas. A má notícia é que qualquer aritmética realizada em `NaN` sempre resulta em `NaN`. Por exemplo:

# In[7]:

np.nan + 1

# In[8]:

np.nan * 0

# A boa notícia: agregações executadas em arrays com `NaN` neles não apresentam erros. A má notícia: os resultados não são uniformemente úteis:
#

# In[9]:

example2 = np.array([2, np.nan, 6, 8])
example2.sum(), example2.min(), example2.max()

# No processo de upcasting de tipos de dados para estabelecer homogeneidade de dados em `Series` e` DataFrames`, o pandas irá alternar voluntariamente os valores ausentes entre `None` e` NaN`. Por causa desse recurso de design, pode ser útil pensar em `None` e` NaN` como dois sabores diferentes de "nulo" em pandas. Na verdade, alguns dos métodos principais que você usará para lidar com os valores ausentes nos pandas refletem essa ideia em seus nomes:
#
# - `isnull ()`: Gera uma máscara booleana indicando valores ausentes
# - `notnull ()`: oposto de `isnull ()`
# - `dropna ()`: Retorna uma versão filtrada dos dados
# - `fillna ()`: retorna uma cópia dos dados com os valores ausentes preenchidos ou imputados
#
# Esses são métodos importantes para dominar e se familiarizar com eles, portanto, vamos examiná-los com alguma profundidade.

# ### Detectando valores nulos
# Ambos `isnull ()` e `notnull ()` são seus métodos principais para detectar dados nulos. Ambos retornam máscaras booleanas sobre seus dados.

# In[10]:

example3 = pd.Series([0, np.nan, '', None])

# In[11]:

example3.isnull()

# Observe atentamente a saída. Alguma coisa disso te surpreende? Embora `0` seja um nulo aritmético, é um número inteiro perfeitamente bom e o pandas o considera como tal. `''` é um pouco mais sutil. Embora o tenhamos usado na Seção 1 para representar um valor de string vazio, ele é, no entanto, um objeto de string e não uma representação de nulo no que diz respeito aos pandas.
#
# Agora, vamos virar isso e usar esses métodos de uma maneira mais parecida com a que você vai usar na prática. Você pode usar máscaras booleanas diretamente como um índice `` Series`` ou `` DataFrame``, que pode ser útil ao tentar trabalhar com valores ausentes (ou presentes) isolados.

# ** Conclusão importante **: os métodos `isnull ()` e `notnull ()` produzem resultados semelhantes quando você os usa em `DataFrame`s: eles mostram os resultados e o índice desses resultados, o que o ajudará enormemente enquanto você luta com seus dados.
#

# ### Eliminando valores nulos
#
# Além de identificar valores ausentes, o pandas fornece um meio conveniente de remover valores nulos de `Series` e` DataFrame`s. (Particularmente em grandes conjuntos de dados, muitas vezes é mais aconselhável simplesmente remover os valores [NA] ausentes de sua análise do que lidar com eles de outras maneiras.) Para ver isso em ação, vamos voltar ao `exemplo3`:

# In[12]:

example3 = example3.dropna()
example3

# Observe que isso deve ser parecido com a saída de `example3 [example3.notnull ()]`. A diferença aqui é que, em vez de apenas indexar os valores mascarados, `dropna` removeu esses valores ausentes do` Series` `example3`.
#
# Como os `DataFrame`s têm duas dimensões, eles oferecem mais opções para descartar dados.

# In[13]:

example4 = pd.DataFrame([[1,      np.nan, 7],
                         [2,      5,      8],
                         [np.nan, 6,      9]])
example4

# (Você notou que os pandas transformam duas das colunas em flutuadores para acomodar os `NaN`s?)
#
# Você não pode descartar um único valor de um `DataFrame`, então você deve descartar linhas ou colunas inteiras. Dependendo do que você está fazendo, você pode querer fazer um ou outro e, portanto, o pandas oferece opções para ambos. Como na ciência de dados, as colunas geralmente representam variáveis e as linhas representam observações, é mais provável que você elimine linhas de dados; a configuração padrão para `dropna ()` é descartar todas as linhas que contenham quaisquer valores nulos:

# In[14]:

example4.dropna()

# Se necessário, você pode eliminar os valores NA das colunas. Use `axis = 1` para fazer isso:
#
#
#

# In[15]:

example4.dropna(axis='columns')

# Observe que isso pode eliminar muitos dados que você pode querer manter, principalmente em conjuntos de dados menores. E se você apenas quiser descartar linhas ou colunas que contenham vários ou até mesmo todos os valores nulos? Você especifica essas configurações em `dropna` com os parâmetros` how` e `thresh`.
#
# Por padrão, `how = 'any'` (se você gostaria de verificar por si mesmo ou ver quais outros parâmetros o método possui, execute` example4.dropna? `Em uma célula de código). Você pode, alternativamente, especificar `how = 'all'` de modo a descartar apenas linhas ou colunas que contenham todos os valores nulos. Vamos expandir nosso exemplo `DataFrame` para ver isso em ação.

# In[16]:

example4[3] = np.nan
example4

# O parâmetro `thresh` oferece um controle mais refinado: você define o número de valores * não nulos * que uma linha ou coluna precisa ter para ser mantida:

# In[17]:

example4.dropna(axis='rows', thresh=3)

#
# Aqui, a primeira e a última linha foram eliminadas, porque contêm apenas dois valores não nulos.
#

# ### Preenchendo valores nulos
#
# Dependendo do seu conjunto de dados, às vezes pode fazer mais sentido preencher valores nulos com valores válidos em vez de descartá-los. Você poderia usar `isnull` para fazer isso no lugar, mas pode ser trabalhoso, principalmente se você tiver muitos valores a preencher. Por ser uma tarefa comum em ciência de dados, o pandas fornece `fillna`, que retorna uma cópia do` Series` ou `DataFrame` com os valores ausentes substituídos por um de sua escolha. Vamos criar outro exemplo `Series` para ver como isso funciona na prática.

# In[18]:

example5 = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))
example5

# Você pode preencher todas as entradas nulas com um único valor, como `0`:
#
#
#

# In[19]:

example5.fillna(0)

# Você pode ser criativo sobre como usar `fillna`. Por exemplo, vamos olhar para `example4` novamente, mas desta vez vamos preencher os valores ausentes com a média de todos os valores no` DataFrame`:

# In[20]:

example4.fillna(example4.mean())

# > ** Takeaway: ** Existem várias maneiras de lidar com valores ausentes em seus conjuntos de dados. A estratégia específica que você usa (removê-los, substituí-los ou mesmo como você os substitui) deve ser ditada pelas particularidades desses dados. Você desenvolverá um senso melhor de como lidar com os valores ausentes quanto mais você manipular e interagir com os conjuntos de dados.

# ## Removendo dados duplicados
#
# > ** Objetivo de aprendizado: ** Ao final desta subseção, você deve estar confortável em identificar e remover valores duplicados de DataFrames.
#
# Além de dados ausentes, você frequentemente encontrará dados duplicados em conjuntos de dados do mundo real. Felizmente, o pandas oferece um meio fácil de detectar e remover entradas duplicadas.

# ### Identificando duplicatas: `duplicated`
#
# Você pode localizar facilmente valores duplicados usando o método `duplicated` em pandas, que retorna uma máscara booleana indicando se uma entrada em um` DataFrame` é uma duplicata de um ealier. Vamos criar outro exemplo de `DataFrame` para ver isso em ação.

# In[21]:

example6 = pd.DataFrame({'letters': ['A','B'] * 2 + ['B'],
                         'numbers': [1, 2, 1, 3, 3]})
example6

# In[22]:

example6.duplicated()

# ### Dropping duplicates: `drop_duplicates`
# `drop_duplicates` simplesmente retorna uma cópia dos dados para os quais todos os valores `duplicados` são` False`:

# In[23]:

example6.drop_duplicates()

# Ambos `duplicated` e` drop_duplicates` consideram todas as colunas, mas você pode especificar que eles examinem apenas um subconjunto de colunas em seu `DataFrame`:

# In[24]:

example6.drop_duplicates(['letters'])

# In[25]:

get_ipython().run_line_magic('pinfo', 'example6.drop_duplicates')

# > ** Conclusão: ** Remover dados duplicados é uma parte essencial de quase todos os projetos de ciência de dados. Dados duplicados podem alterar os resultados de suas análises e fornecer resultados espúrios!

#
# ## Combinando conjuntos de dados: merge e join
#
# > ** Objetivo de aprendizagem: ** Ao final desta subseção, você deve ter um conhecimento geral das várias maneiras de combinar `DataFrame`s.
#
# Suas análises mais interessantes geralmente virão de dados combinados de mais de uma fonte. Por causa disso, o pandas oferece vários métodos de mesclar e unir conjuntos de dados para facilitar esse trabalho necessário:
#  - ** `pandas.merge` ** conecta linhas em` DataFrame`s com base em uma ou mais chaves.
#  - ** `pandas.concat` ** concatena ou“ empilha ”objetos ao longo de um eixo.
#  - O método de instância ** `combine_first` ** permite que você junte os dados sobrepostos para preencher os valores ausentes em um objeto com os valores de outro.
#
# Vamos examinar a fusão de dados primeiro, porque será mais familiar para os participantes do curso que já estão familiarizados com SQL ou outros bancos de dados relacionais.

# ### Categorias de junções (joins)
#
# `merge` realiza vários tipos de junções: * um-para-um *, * muitos-para-um * e * muitos-para-muitos *. Você usa a mesma chamada de função básica para implementar todos eles e examinaremos todos os três (porque você precisará de todos os três como algum ponto em sua pesquisa de dados, dependendo dos dados). Começaremos com junções de um para um porque geralmente são o exemplo mais simples.

# #### Joins um a um
#
# Considere combinar dois `DataFrame`s que contêm informações diferentes sobre os mesmos funcionários em uma empresa:

# In[26]:

df1 = pd.DataFrame({'employee': ['Gary', 'Stu', 'Mary', 'Sue'],
                    'group': ['Accounting', 'Marketing', 'Marketing', 'HR']})
df1

# In[27]:

df2 = pd.DataFrame({'employee': ['Mary', 'Stu', 'Gary', 'Sue'],
                    'hire_date': [2008, 2012, 2017, 2018]})
df2

# Combine essas informações em um único `DataFrame` usando a função` merge`:

# In[28]:

df3 = pd.merge(df1, df2)
df3

# O Pandas fez a junção baseado na coluna `employee` porque era a única coluna comum a` df1` e `df2`. (Observe também que os índices originais de `df1` e` df2` foram descartados por `merge`; este é geralmente o caso com mesclagens, a menos que você conduza por índice, que discutiremos mais tarde.)

# #### Joins muitos para um
# Uma junção de muitos para um é como uma junção de um para um, exceto que uma das duas colunas principais contém entradas duplicadas. O DataFrame resultante de tal junção preservará essas entradas duplicadas conforme apropriado:

# In[29]:

df4 = pd.DataFrame({'group': ['Accounting', 'Marketing', 'HR'],
                    'supervisor': ['Carlos', 'Giada', 'Stephanie']})
df4

# In[30]:

pd.merge(df3, df4)

# O `DataFrame` resultante tem uma coluna adicional para` supervisor`; essa coluna tem uma ocorrência extra de 'Giada' que não ocorreu em `df4` porque mais de um funcionário no` DataFrame` mesclado trabalha no grupo 'Marketing'.
#
# Observe que não especificamos em qual coluna juntar. Quando você não especifica essas informações, `merge` usa os nomes das colunas sobrepostas como as chaves. No entanto, isso pode ser ambíguo; várias colunas podem atender a essa condição. Por esse motivo, é uma boa prática especificar explicitamente em qual chave juntar. Você pode fazer isso com o parâmetro `on`:

# In[31]:

pd.merge(df3, df4, on='group')

# #### Muitos para muitos joins
# O que acontecerá se as colunas-chave em ambos os DataFrames que você está unindo contiverem duplicatas? Isso dá a você uma junção de muitos para muitos:

# In[32]:

df5 = pd.DataFrame({'group': ['Accounting', 'Accounting',
                              'Marketing', 'Marketing', 'HR', 'HR'],
                    'core_skills': ['math', 'spreadsheets', 'writing', 'communication',
                               'spreadsheets', 'organization']})
df5

# In[33]:

pd.merge(df1, df5, on='group')

# Novamente, para evitar ambigüidade quanto a qual coluna unir, é uma boa idéia dizer explicitamente ao `merge` qual usar com o parâmetro` on`.

# #### `left_on` and `right_on` keywords
# E se você precisar mesclar dois conjuntos de dados sem nomes de coluna compartilhados? Por exemplo, e se você estiver usando um conjunto de dados em que o nome do funcionário é rotulado como 'nome' em vez de 'funcionário'? Nesses casos, você precisará usar as palavras-chave `left_on` e `right_on`
#  para especificar os nomes das colunas nas quais unir:

# In[34]:

df6 = pd.DataFrame({'name': ['Gary', 'Stu', 'Mary', 'Sue'],
                    'salary': [70000, 80000, 120000, 90000]})
df6

# In[35]:

pd.merge(df1, df6, left_on="employee", right_on="name")

# ### Concatenação em NumPy
# A concatenação em pandas é construída a partir da funcionalidade de concatenação para matrizes NumPy. Esta é a aparência da concatenação NumPy:
#  - Para matrizes unidimensionais:

# In[36]:

x = [1, 2, 3]
y = [4, 5, 6]
z = [7, 8, 9]
np.concatenate([x, y, z])

#
#  - Para matrizes bidimensionais:

# In[37]:

x = [[1, 2],
     [3, 4]]
np.concatenate([x, x], axis=1)

# Observe que o parâmetro `axis = 1` faz com que a concatenação ocorra ao longo de colunas ao invés de linhas. A concatenação em pandas é semelhante a esta.

# ### Concatenação em pandas
#
# Pandas tem uma função, `pd.concat ()` que pode ser usada para uma concatenação simples de objetos `Series` ou` DataFrame` de maneira semelhante a `np.concatenate ()` com ndarrays.

# In[38]:

ser1 = pd.Series(['a', 'b', 'c'], index=[1, 2, 3])
ser2 = pd.Series(['d', 'e', 'f'], index=[4, 5, 6])
pd.concat([ser1, ser2])

# Ele também concatena objetos de dimensões superiores, como `` DataFrame``s:

# In[39]:

df9 = pd.DataFrame({'A': ['a', 'c'],
                    'B': ['b', 'd']})
df9

# In[40]:

pd.concat([df9, df9])

# Observe que `pd.concat` preservou a indexação, embora isso signifique que ela foi duplicada. Você pode ter os resultados reindexados (e evitar possíveis confusões no caminho) assim:

# In[41]:

pd.concat([df9, df9], ignore_index=True)

# Por padrão, `pd.concat` concatena a linha dentro do` DataFrame` (ou seja, `axis = 0` por padrão). Você pode especificar o eixo ao longo do qual concatenar:

# In[42]:

pd.concat([df9, df9], axis=1)

# ### Concatenação com junções
# Assim como fez com a mesclagem acima, você pode usar junções internas e externas ao concatenar DataFrames com diferentes conjuntos de nomes de coluna.

# In[43]:

df10 = pd.DataFrame({'A': ['a', 'd'],
                     'B': ['b', 'e'],
                     'C': ['c', 'f']})
df10

# In[44]:

df11 = pd.DataFrame({'B': ['u', 'x'],
                     'C': ['v', 'y'],
                     'D': ['w', 'z']})
df11

# In[45]:

pd.concat([df10, df11])

# Como vimos anteriormente, a junção padrão para isso é uma junção externa e as entradas para as quais nenhum dado está disponível são preenchidas com valores `NaN`. Você também pode fazer uma junção interna:

# In[46]:

pd.concat([df10, df11], join='inner')

# Outra opção é especificar diretamente o índice das colunas restantes usando o argumento `join_axes`, que obtém uma lista de objetos de índice. Aqui, especificaremos que as colunas retornadas devem ser as mesmas da primeira entrada (`df10`):

# In[47]:

pd.concat([df10, df11], join_axes=[df10.columns])

# #### `append()`
#
# Como a concatenação direta de array é tão comum, os objetos `` Series`` e `` DataFrame`` têm um método `` append`` que pode realizar a mesma coisa em menos teclas. Por exemplo, em vez de chamar `` pd.concat ([df9, df9]) ``, você pode simplesmente chamar `` df9.append (df9) ``:

# In[48]:

df9.append(df9)

# ** Ponto importante **: Ao contrário dos métodos `append ()` e `extend ()` das listas Python, o método `append ()` no pandas não modifica o objeto original. Em vez disso, ele cria um novo objeto com os dados combinados.
#
# > ** Conclusão: ** uma grande parte do valor que você pode fornecer como cientista de dados vem da conexão de vários conjuntos de dados, muitas vezes díspares, para encontrar novos insights. Aprender como juntar e mesclar dados é, portanto, uma parte essencial do seu conjunto de habilidades.

# ## Estatísticas exploratórias e visualização
#
# > ** Objetivo de aprendizagem: ** Ao final desta subseção, você deve estar familiarizado com algumas das maneiras de explorar visualmente os dados armazenados em `DataFrame`s.
#
# Freqüentemente, ao investigar um novo conjunto de dados, é inestimável obter informações de alto nível sobre o que o conjunto de dados contém. Anteriormente nesta seção, discutimos o uso de métodos como `DataFrame.info`,` DataFrame.head` e `DataFrame.tail` para examinar alguns aspectos de um` DataFrame`. Embora esses métodos sejam essenciais, eles são, por si próprios, muitas vezes insuficientes para obter informações suficientes para saber como abordar um novo conjunto de dados. É aqui que entram as estatísticas exploratórias e as visualizações dos conjuntos de dados.
#
# Para ver o que queremos dizer em termos de obtenção de insights exploratórios (visual e numericamente), vamos nos aprofundar em um dos conjuntos de dados que vêm com a biblioteca scikit-learn, o Boston Housing Dataset:

# In[49]:

from sklearn.datasets import load_boston
boston_dataset = load_boston()
df = pd.DataFrame(boston_dataset.data, columns=boston_dataset.feature_names)
df['MEDV'] = boston_dataset.target

# In[50]:

df.head()

#
# Este conjunto de dados contém informações coletadas do U.S Census Bureau sobre habitação na área de Boston, Massachusetts e foi publicado pela primeira vez em 1978. O conjunto de dados tem 14 colunas:
#  - **CRIM**: Taxa de criminalidade per capita por cidade
#  - **ZN**: Proporção de terrenos residenciais zoneados para lotes com mais de 25.000 pés quadrados
#  - **INDUS**: Proporção de acres de negócios não varejistas por cidade
#  - **CHAS**: variável dummy Charles River (= 1 se o trato limita o rio; 0 caso contrário)
#  - **NOX**: concentração de óxidos nítricos (partes por 10 milhões)
#  - **RM**: Número médio de quartos por habitação
#  - **AGE**: Proporção de unidades ocupadas pelo proprietário construídas antes de 1940
#  - **DIS**: distâncias ponderadas até cinco centros de empregos de Boston
#  - **RAD**: Índice de acessibilidade às rodovias radiais
#  - **TAX**: Taxa de imposto de propriedade de valor total por \$ 10.000
#  - **PTRATIO**: Proporção aluno-professor por cidade
#  - **LSTAT**: Porcentagem da porção de status inferior da população
#  - **MEDV**: valor médio das casas ocupadas pelo proprietário em \$ 1.000

# Um dos primeiros métodos que podemos usar para entender melhor este conjunto de dados é `DataFrame.shape`:

# In[51]:

df.shape

# O conjunto de dados possui 506 linhas e 13 colunas.
#
# Para ter uma ideia melhor do conteúdo de cada coluna, podemos usar `DataFrame.describe`, que retorna o valor máximo, valor mínimo, média e desvio padrão dos valores numéricos em cada coluna, além dos quartis de cada coluna:

# In[52]:

df.describe()

# Como o conjunto de dados pode ter muitas colunas, muitas vezes pode ser útil transpor os resultados de `DataFrame.describe` para melhor usá-los:

# Observe que você também pode examinar estatísticas descritivas específicas para colunas sem ter que invocar `DataFrame.describe`:

# In[53]:

df['MEDV'].mean()

# In[54]:

df['MEDV'].max()

# In[55]:

df['AGE'].median()

# ### Exercicio 1:

# In[56]:

#Encontre o valor máximo em df['AGE'].
df['AGE'].max()

# Outra informação que você frequentemente desejará ver é a relação entre as diferentes colunas. Você faz isso com o método `DataFrame.groupby`. Por exemplo, você pode examinar a MEDV média (valor médio das casas ocupadas pelo proprietário) para cada valor de AGE (proporção de unidades ocupadas pelo proprietário construídas antes de 1940):

# In[57]:

df.groupby(['AGE'])['MEDV'].mean()

# ### Exercício 2:

# In[58]:

# Agora tente encontrar o valor mediano de AGE para cada valor de MEDV.
df.groupby(['MEDV'])['AGE'].mean()

# Você também pode aplicar uma função lambda a cada elemento de uma coluna `DataFrame` usando o método `apply`. Por exemplo, digamos que você queira criar uma nova coluna que sinalize uma linha se mais de 50 por cento das casas ocupadas pelo proprietário forem construídas antes de 1940:

# In[59]:

df['AGE_50'] = df['AGE'].apply(lambda x: x>50)

# Depois de aplicado, você também verá quantos valores retornaram verdadeiros e quantos falsos usando o método `value_counts`:

# In[60]:

df['AGE_50'].value_counts()

# Você também pode examinar os números da instrução groupby que criou anteriormente:

# In[61]:

df.groupby(['AGE_50'])['MEDV'].mean()

# Você também pode agrupar por mais de uma variável, como AGE_50 (aquela que você acabou de criar), CHAS (se uma cidade está no rio Charles) e RAD (um índice que mede o acesso às rodovias radiais da área de Boston) e, em seguida, avalie cada grupo para o preço médio médio de uma casa nesse grupo:

# In[62]:

groupby_twovar=df.groupby(['AGE_50','RAD','CHAS'])['MEDV'].mean()

# In[63]:

groupby_twovar

# Vamos analisar esses resultados com um pouco mais de profundidade. A primeira linha relata que as comunidades com menos da metade das casas construídas antes de 1940, com um índice de acesso à rodovia de 1, e que não estão situadas no rio Charles, têm um preço médio de casa de \$ 24.667 (dólares dos anos 1970); a próxima linha mostra que para comunidades semelhantes à primeira linha, exceto por estarem localizadas no Charles River, o preço médio da casa é de \$ 50.000.
#
# Um insight que surge ao continuar a descer é que, se todo o resto for igual, estar localizado próximo ao rio Charles pode aumentar significativamente o valor do estoque de habitações mais recentes. A história é mais ambígua para comunidades dominadas por casas antigas: a proximidade com o Charles aumenta significativamente os preços das casas em uma comunidade (e presumivelmente mais longe da cidade); para todos os outros, estar situado às margens do rio proporcionou um aumento modesto no valor ou, na verdade, diminuiu os preços médios das residências.
#
# Embora agrupamentos como este possam ser uma ótima maneira de começar a interrogar seus dados, você pode não se importar com o formato 'alto' que eles vêm. Nesse caso, você pode desempilhar os dados em um formato "amplo":

# In[64]:

groupby_twovar.unstack()

# ### Exercício 3:

# In[65]:

# Como você poderia usar groupby para ter uma noção da proporção
#Nº de terrenos residenciais zoneados para lotes com mais de 25.000 pés quadrados,
# a proporção de acres de negócios não varejistas por cidade,
# e a distância das cidades dos centros de emprego em Boston?#

df.groupby(['ZN', 'INDUS'])['DIS'].mean()

# Também é frequentemente valioso saber quantos valores únicos uma coluna contém com o método `nunique`:

# In[66]:

df['CHAS'].nunique()

# Complementarmente, você provavelmente também desejará saber quais são esses valores exclusivos, que é onde o método `unique` ajuda:

# In[67]:

df['CHAS'].unique()

# Você pode usar o método `value_counts` para ver quantos de cada valor único existem em uma coluna:

# In[68]:

df['CHAS'].value_counts()

# Ou você pode facilmente traçar um gráfico de barras para ver visualmente a divisão:

# In[69]:

get_ipython().run_line_magic('matplotlib', 'inline')
df['CHAS'].value_counts().plot(kind='bar')

# Observe que o comando mágico do IPython `% matplotlib inline` permite que você visualize o gráfico inline.
#
# Vamos voltar ao conjunto de dados como um todo por um momento. Duas coisas importantes que você procurará em quase qualquer conjunto de dados são tendências e relacionamentos. Uma relação típica entre as variáveis a explorar é a correlação de Pearson, ou a extensão na qual duas variáveis estão linearmente relacionadas. O método `corr` mostrará isso em formato de tabela para todas as colunas em um` DataFrame`:

# In[70]:

df.corr(method='pearson')

# Suponha que você queira apenas examinar as correlações entre todas as colunas e apenas uma variável. Vamos examinar apenas a correlação entre todas as outras variáveis e a porcentagem de casas ocupadas pelos proprietários construídas antes de 1940 (AGE). Faremos isso acessando a coluna por número de índice:

# In[71]:

corr = df.corr(method='pearson')
corr_with_homevalue = corr.iloc[-1]
corr_with_homevalue[corr_with_homevalue.argsort()[::-1]]

# Com as correlações organizadas em ordem decrescente, é fácil começar a ver alguns padrões. Correlacionar AGE com uma variável que criamos a partir de AGE é uma correlação trivial. No entanto, é interessante notar que a porcentagem do estoque de moradias mais antigas nas comunidades está fortemente correlacionada com a poluição do ar (NOX) e a proporção de acres de negócios não varejistas por cidade (INDUS); pelo menos em 1978 na área metropolitana de Boston, as cidades mais antigas são mais industriais.
#
# Graficamente, podemos ver as correlações usando um mapa de calor da biblioteca Seaborn:

# In[72]:

import seaborn as sns
sns.heatmap(df.corr(),cmap=sns.cubehelix_palette(20, light=0.95, dark=0.15))

# Os histogramas são outra ferramenta valiosa para investigar seus dados. Por exemplo, qual é a distribuição geral dos preços das casas ocupadas pelos proprietários na área de Boston?

# In[73]:

import matplotlib.pyplot as plt
plt.hist(df['MEDV'])

# O tamanho do compartimento padrão para o histograma matplotlib (essencialmente grande de grupos de porcentagens que você inclui em cada barra de histograma, neste caso) é muito grande e pode mascarar detalhes menores. Para obter uma visão mais detalhada da coluna AGE, você pode aumentar manualmente o número de compartimentos no histograma:

# In[74]:

plt.hist(df['MEDV'],bins=50)

#
#
# Seaborn tem uma versão um pouco mais atraente do histograma matplotlib padrão: o gráfico de distribuição. Este é um gráfico de combinação de histograma e estimativa de densidade do kernel (KDE) (essencialmente um histograma suavizado):

# In[75]:

sns.distplot(df['MEDV'])

# Outro gráfico comumente usado é o Seaborn jointplot, que combina histogramas para duas colunas junto com um gráfico de dispersão:
#

# In[76]:

sns.jointplot(df['RM'], df['MEDV'], kind='scatter')

# Infelizmente, muitos dos pontos são impressos uns sobre os outros. Você pode ajudar a resolver isso adicionando alguma mistura alfa, uma figura que define a transparência para os pontos de forma que as concentrações deles se sobrepondo sejam aparentes:

# In[77]:

sns.jointplot(df['RM'], df['MEDV'], kind='scatter', alpha=0.3)

# Outra maneira de ver os padrões em seus dados é com um gráfico bidimensional do KDE. As cores mais escuras aqui representam uma maior concentração de pontos de dados:

# In[78]:

sns.kdeplot(df['RM'], df['MEDV'], shade=True)

# Observe que, embora o gráfico do KDE seja muito bom em mostrar concentrações de pontos de dados, estruturas mais refinadas, como relações lineares (como a relação clara entre o número de quartos nas casas e o preço da casa), são perdidas no gráfico do KDE.
#
# Finalmente, o gráfico de pares no Seaborn permite que você veja gráficos de dispersão e histogramas para várias colunas em uma tabela. Aqui, brincamos com algumas das palavras-chave para produzir um gráfico de par mais sofisticado e fácil de ler que incorpora tanto a combinação alfa quanto as linhas de regressão linear para os gráficos de dispersão.

# In[79]:

sns.pairplot(df[['RM', 'AGE', 'LSTAT', 'DIS', 'MEDV']], kind="reg", plot_kws={'line_kws':{'color':'red'}, 'scatter_kws': {'alpha': 0.1}})

# A visualização é o início da parte realmente legal e divertida da ciência de dados. Portanto, experimente essas ferramentas de visualização e veja o que você pode aprender com os dados!

# > ** Conclusão: ** Uma velha piada diz: “O que um cientista de dados vê quando olha para um conjunto de dados? Um monte de números. ” Há mais do que um pouco de verdade nessa piada. A visualização geralmente é a chave para encontrar padrões e correlações em seus dados. Embora a visualização muitas vezes não possa fornecer resultados precisos, ela pode indicar a direção certa para fazer perguntas melhores e encontrar valor nos dados de maneira eficiente.

# In[ ]:


"""Scientific data tests.

Scientific Machine Learning Benchmark:
A benchmark of regression models in chem- and materials informatics.
(c) Matthias Rupp 2019, Citrine Informatics.
"""

import pytest

import numpy as np

import smlb

##################
#  element_data  #
##################

def test_element_data():
    """Tests chemical elements data."""

    # simple examples
    assert smlb.element_data(1, "abbreviation") == "H"

    # verify proton number
    assert all(smlb.element_data(i, "Z") == i for i in range(1, 119))

    # verify round trip
    abrvs = [smlb.element_data(i, "abbreviation") for i in range(1, 119)]
    np.random.seed(0)
    np.random.shuffle(abrvs)
    assert abrvs == [smlb.element_data(smlb.element_data(a, "Z"), "abbreviation") for a in abrvs]


import numpy as np

def ECDF(data):
    """Compute ECDF for a one-dimensional array of measurements."""
    # Number of data points
    n = len(data)

    # x-data for the ECDF
    x = np.sort(data)

    # y-data for the ECDF
    y = np.arange(1, n+1) / n

    return x, y

def despine(ax):
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)

def despine_traceplot(traceplot):
    for row in traceplot:
        for ax in row:
            despine(ax)


#!/usr/bin/env python
u"""
grace_date.py
Written by Tyler Sutterley (05/2021)

Reads index file from podaac_grace_sync.py or gfz_isdc_grace_ftp.py
Parses dates of each GRACE/GRACE-FO file and assigns the month number
Creates an index of dates for GRACE/GRACE-FO files

INPUTS:
    base_dir: Working data directory for GRACE/GRACE-FO data

OPTIONS:
    PROC: GRACE data processing center (CSR/CNES/JPL/GFZ/GRAZ/COSTG/SWARM)
    DREL: GRACE/GRACE-FO Data Release (RL03 for CNES) (RL06 for CSR/GFZ/JPL)
    DSET: GRACE dataset (GAA/GAB/GAC/GAD/GSM)
        GAA is the non-tidal atmospheric correction
        GAB is the non-tidal oceanic correction
        GAC is the combined non-tidal atmospheric and oceanic correction
        GAD is the GRACE ocean bottom pressure product
        GSM is corrected monthly GRACE/GRACE-FO static field product
    OUTPUT: create index of dates for GRACE/GRACE-FO data
    MODE: permissions mode of output files

OUTPUTS:
    dictionary of GRACE/GRACE-FO files indexed by month

PYTHON DEPENDENCIES:
    numpy: Scientific Computing Tools For Python
        https://numpy.org
        https://numpy.org/doc/stable/user/numpy-for-matlab-users.html
    dateutil: powerful extensions to datetime
        https://dateutil.readthedocs.io/en/stable/
    future: Compatibility layer between Python 2 and Python 3
        https://python-future.org/

PROGRAM DEPENDENCIES:
    time.py: utilities for calculating time operations

UPDATE HISTORY:
    Updated 05/2021: define int/float precision to prevent deprecation warning
    Updated 02/2021: use adjust_months function to fix special months cases
    Updated 12/2020: Add SWARM data compilance
    Updated 12/2020: using utilities from time module
    Updated 11/2020: updated for CNES RL04 & RL05 and GRAZ 2018 (monthly fields)
    Updated 10/2020: use argparse to set command line parameters
    Updated 07/2020: added function docstrings
    Updated 03/2020: for public release
    Updated 11/2018: updated regular expression pattern for RL06 GFZ
    Updated 08/2018: using full release string (RL05 instead of 5)
    Updated 06/2018: using python3 compatible octal and input
    Updated 05/2018: can read new GRACE file format for RL06 and GRACE-FO
    Updated 10/2017: added option OUTPUT to write the text file with GRACE dates
        now will output from the function the GRACE month and file name
        adjusted regular expression for extracting parameters from filename
    Updated 02/2017: added mode to modify the permissions of the output file
    Updated 05-06/2016: using __future__ print function, format date lines
    Updated 01/2016: minor clean up.  using enumerate for loop
    Updated 10/2015: added manual fix for month 161 (centered in 160)
    Updated 05/2015: additional 2 digits to date file to reduce the differences
        in dates if importing from date file or binary data file
    Updated 03/2015: added main definition for running from command line
        further generalization, calculates the Julian date of the GRACE date
    Updated 11/2014: output more decimal points for date
    Updated 09/2014: using regular expressions, general code updates
    Updated 03/2014: printing GRACE month with zero-padding
    Updated 02/2014: minor update to if statements
    Updated 01/2014: updated for CNES RL03 (monthly fields)
    Updated 10/2013: created a directory with both RL04 and RL05 (combining)
        made a slight edit for the drift rates
    Updated 09/2013: changed dating schemes for all products
        for CNES: Solution 1 == 001, versus 10-day from start of 2002
    Updated 05/2013: modified for use with GUI program
    Updated 07/2012: changed some variable names, and saving variables
        start_yr, start_day, end_yr, end_day
    Updated 07/2012: fix missing date for CSR RL05
        One of the months 119 registered as 118 as half of 119 is in 118 due to
        accelerometer issues during 119
    Updated 06/2012: fixes missing dates to be automatic
        Also added options to enter the processing center, the data release and
        the dataset from an external main level program
    Updated 04/2012: changes for RL05 data
"""
from __future__ import print_function

import os
import re
import argparse
import numpy as np
import gravity_toolkit.time

def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775):
    """
    Reads index file from podaac_grace_sync.py or gfz_isdc_grace_ftp.py
    Parses dates of each GRACE/GRACE-FO file and assigns the month number
    Creates an index of dates for GRACE/GRACE-FO files

    Arguments
    ---------
    base_dir: working data directory

    Keyword arguments
    -----------------
    PROC: GRACE data processing center
        CSR: University of Texas Center for Space Research
        GFZ: German Research Centre for Geosciences (GeoForschungsZentrum)
        JPL: Jet Propulsion Laboratory
        CNES: French Centre National D'Etudes Spatiales
        GRAZ: Institute of Geodesy from GRAZ University of Technology
        COSTG: International Combination Service for Time-variable Gravity Fields

        SWARM: gravity data from SWARM satellite
    DREL: GRACE/GRACE-FO data release
    DSET: GRACE/GRACE-FO dataset
        GAA: non-tidal atmospheric correction
        GAB: non-tidal oceanic correction
        GAC: combined non-tidal atmospheric and oceanic correction
        GAD: ocean bottom pressure product
        GSM: corrected monthly static gravity field product
    OUTPUT: create index file of dates for GRACE/GRACE-FO data
    MODE: Permission mode of directories and files

    Returns
    -------
    output_files: dictionary of GRACE/GRACE-FO files indexed by month
    """

    #--  Directory of exact product
    grace_dir = os.path.join(base_dir, PROC, DREL, DSET)
    #-- input index file containing GRACE data filenames
    with open(os.path.join(grace_dir, 'index.txt'),'r') as f:
        input_files = f.read().splitlines()

    #--  number of lines in input_files
    n_files = len(input_files)

    #-- define date variables
    start_yr = np.zeros((n_files))#-- year start date
    end_yr = np.zeros((n_files))#-- year end date
    start_day = np.zeros((n_files))#-- day number start date
    end_day = np.zeros((n_files))#-- day number end date
    mid_day = np.zeros((n_files))#-- mid-month day
    tot_days = np.zeros((n_files))#-- number of days since Jan 2002
    tdec = np.zeros((n_files))#-- tdec is the date in decimal form
    mon = np.zeros((n_files,),dtype=np.int64)#-- GRACE/GRACE-FO month number

    if PROC in ('CSR', 'GFZ', 'JPL', 'CNES', 'COSTG'):
        #-- compile numerical expression operator for parameters from files
        #-- will work with previous releases and releases for GRACE-FO
        #-- UTCSR: The University of Texas at Austin Center for Space Research
        #-- EIGEN: GFZ German Research Center for Geosciences (RL01-RL05)
        #-- GFZOP: GFZ German Research Center for Geosciences (RL06+GRACE-FO)
        #-- JPLEM: NASA Jet Propulsion Laboratory (harmonic solutions)
        #-- JPLMSC: NASA Jet Propulsion Laboratory (mascon solutions)
        #-- GRGS: CNES Groupe de Recherche de Géodésie Spatiale
        regex_pattern = (r'(.*?)-2_(\d+)-(\d+)_(.*?)_({0})_(.*?)_(\d+)(.*?)'
           r'(\.gz|\.gfc|\.txt)?$').format(r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG')
    elif PROC == 'GRAZ':
        # -- GRAZ: Institute of Geodesy from GRAZ University of Technology
        regex_pattern = (r'(.*?)-({0})_(.*?)_(\d+)-(\d+)'
                         r'(\.gz|\.gfc|\.txt)').format(r'Grace_operational|Grace2018')
    elif PROC == 'SWARM':
        # -- SWARM: data from SWARM satellite
        regex_pattern = (r'({0})_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)'
                         r'(\.gz|\.gfc|\.txt)').format(r'SW')
    else:
        raise ValueError("Unknown PROC value:", PROC)

    rx = re.compile(regex_pattern, re.VERBOSE)

    #-- for each data file
    for t, infile in enumerate(input_files):
        #-- extract parameters from input filename
        if PROC in ('CSR', 'GFZ', 'JPL', 'CNES', 'COSTG'):
            PFX,start_date,end_date,AUX,PRC,F1,DRL,F2,SFX = rx.findall(infile).pop()

            #-- find start date, end date and number of days
            start_yr[t] = np.float64(start_date[:4])
            end_yr[t] = np.float64(end_date[:4])
            start_day[t] = np.float64(start_date[4:])
            end_day[t] = np.float64(end_date[4:])

        elif PROC == 'GRAZ' or PROC == 'SWARM':
            if PROC == 'GRAZ':
                PFX,SAT,trunc,year,month,SFX = rx.findall(infile).pop()
                #-- find start year, end year
                start_yr[t] = np.float(year)
                end_yr[t] = np.float(year)
            elif PROC == 'SWARM':
                SAT, tmp, PROD, start_date, end_date, RL, SFX = rx.findall(os.path.basename(infile)).pop()

                start_yr[t] = int(start_date[:4])
                end_yr[t] = int(end_date[:4])
                month = int(start_date[4:6])

            #-- Calculation of total days since start of campaign
            #-- Get information on the current year (day per month and day per year)
            dpm = gravity_toolkit.time.dpm_count(start_yr[t])

            #-- find start day, end day
            start_day[t] = np.sum(dpm[:np.int(month) - 1]) + 1
            end_day[t] = np.sum(dpm[:np.int(month)])

        # -- number of days in the starting year for leap and standard years
        dpy = gravity_toolkit.time.calendar_days(start_yr[t]).sum()
        #-- end date taking into account measurements taken on different years
        end_cyclic = (end_yr[t]-start_yr[t])*dpy + end_day[t]
        #-- calculate mid-month value
        mid_day[t] = np.mean([start_day[t], end_cyclic])

        #-- calculate Modified Julian Day from start_yr and mid_day
        MJD = gravity_toolkit.time.convert_calendar_dates(start_yr[t],
            1.0,mid_day[t],epoch=(1858,11,17,0,0,0))
        #-- convert from Modified Julian Days to calendar dates
        cal_date = gravity_toolkit.time.convert_julian(MJD+2400000.5)

        #-- Calculating the mid-month date in decimal form
        tdec[t] = start_yr[t] + mid_day[t]/dpy

        #-- Calculation of total days since start of campaign
        count = 0
        n_yrs = np.int64(start_yr[t]-2002)
        #-- for each of the GRACE years up to the file year
        for iyr in range(n_yrs):
            #-- year
            year = 2002 + iyr
            #-- add all days from prior years to count
            #-- number of days in year i (if leap year or standard year)
            count += gravity_toolkit.time.calendar_days(year).sum()

        #-- calculating the total number of days since 2002
        tot_days[t] = np.mean([count+start_day[t], count+end_cyclic])

        #-- Calculates the month number (or 10-day number for CNES RL01,RL02)
        if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))):
            mon[t] = np.round(1.0+(tot_days[t]-tot_days[0])/10.0)
        else:
            #-- calculate the GRACE/GRACE-FO month (Apr02 == 004)
            #-- https://grace.jpl.nasa.gov/data/grace-months/
            #-- Notes on special months (e.g. 119, 120) below
            mon[t] = 12*(cal_date['year']-2002) + cal_date['month']

    #-- The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with
    #-- Accelerometer shutoffs make the relation between month number
    #-- and date more complicated as days from other months are used
    #-- For CSR and GFZ: Nov 2011 (119) is centered in Oct 2011 (118)
    #-- For JPL: Dec 2011 (120) is centered in Jan 2012 (121)
    #-- For all: May 2015 (161) is centered in Apr 2015 (160)
    mon = gravity_toolkit.time.adjust_months(mon)

    #-- Output GRACE/GRACE-FO date ascii file
    if OUTPUT:
        date_file = '{0}_{1}_DATES.txt'.format(PROC, DREL)
        fid = open(os.path.join(grace_dir,date_file), 'w')
        #-- date file header information
        args = ('Mid-date','Month','Start_Day','End_Day','Total_Days')
        print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*args),file=fid)

    #-- create python dictionary mapping input file names with GRACE months
    grace_files = {}
    #-- for each data file
    for t, infile in enumerate(input_files):
        #-- add file to python dictionary mapped to GRACE/GRACE-FO month
        grace_files[mon[t]] = os.path.join(grace_dir,infile)
        #-- print to GRACE dates ascii file (NOTE: tot_days will be rounded)
        if OUTPUT:
            print(('{0:13.8f} {1:03d} {2:8.0f} {3:03.0f} {4:8.0f} {5:03.0f} '
                '{6:8.0f}').format(tdec[t],mon[t],start_yr[t],start_day[t],
                end_yr[t],end_day[t],tot_days[t]), file=fid)

    #-- close date file
    #-- set permissions level of output date file
    if OUTPUT:
        fid.close()
        os.chmod(os.path.join(grace_dir, date_file), MODE)

    #-- return the python dictionary that maps GRACE months with GRACE files
    return grace_files

#-- PURPOSE: program that calls grace_date() with set parameters
def main():
    #-- command line parameters
    #-- Read the system arguments listed after the program
    parser = argparse.ArgumentParser(
        description="""Parses dates of each GRACE/GRACE-FO file and
            assigns the month number.
            Creates an index of dates for GRACE/GRACE-FO files.
            """
    )
    #-- working data directory
    parser.add_argument('--directory','-D',
        type=lambda p: os.path.abspath(os.path.expanduser(p)),
        default=os.getcwd(),
        help='Working data directory')
    #-- GRACE/GRACE-FO data processing center
    parser.add_argument('--center','-c',
        metavar='PROC', type=str, nargs='+',
        default=['CSR','GFZ','JPL'],
        choices=['CSR','GFZ','JPL', 'CNES','GRAZ','SWARM', 'COSTG'],
        help='GRACE/GRACE-FO Processing Center')
    #-- GRACE/GRACE-FO data release
    parser.add_argument('--release','-r',
        metavar='DREL', type=str, nargs='+',
        default=['RL06'],
        help='GRACE/GRACE-FO Data Release')
    #-- GRACE/GRACE-FO data product
    parser.add_argument('--product','-p',
        metavar='DSET', type=str.upper, nargs='+',
        default=['GAC','GAD','GSM'],
        choices=['GAA','GAB','GAC','GAD','GSM'],
        help='GRACE/GRACE-FO Level-2 data product')
    #-- output GRACE/GRACE-FO ascii date file
    parser.add_argument('--output','-O',
        default=False, action='store_true',
        help='Overwrite existing data')
    #-- permissions mode of the local directories and files (number in octal)
    parser.add_argument('--mode','-M',
        type=lambda x: int(x,base=8), default=0o775,
        help='permissions mode of output files')
    args,_ = parser.parse_known_args()

    #-- run GRACE/GRACE-FO date program
    for pr in args.center:
        for rl in args.release:
            for ds in args.product:
                grace_date(args.directory, PROC=pr, DREL=rl, DSET=ds,
                    OUTPUT=args.output, MODE=args.mode)

#-- run main program
if __name__ == '__main__':
    main()


# -*- coding: utf-8 -*-

r"""
The :mod:`pyunlocbox.solvers` module implements a solving function (which will
minimize your objective function) as well as common solvers.

Solving
-------

Call :func:`solve` to solve your convex optimization problem using your
instantiated solver and functions objects.

Interface
---------

The :class:`solver` base class defines a common interface to all solvers:

.. autosummary::

    solver.pre
    solver.algo
    solver.post

Solvers
-------

Then, derived classes implement various common solvers.

.. autosummary::

    gradient_descent
    forward_backward
    douglas_rachford
    generalized_forward_backward

**Primal-dual solvers** (based on :class:`primal_dual`)

.. autosummary::

    mlfbf
    projection_based

.. inheritance-diagram:: pyunlocbox.solvers
    :parts: 2

"""

import time

import numpy as np

from pyunlocbox.functions import dummy, _prox_star
from pyunlocbox import acceleration

def solve(functions, x0, solver=None, atol=None, dtol=None, rtol=1e-3,
          xtol=None, maxit=200, verbosity='LOW', inplace=False):
    r"""
    Solve an optimization problem whose objective function is the sum of some
    convex functions.

    This function minimizes the objective function :math:`f(x) =
    \sum\limits_{k=0}^{k=K} f_k(x)`, i.e. solves
    :math:`\operatorname{arg\,min}\limits_x f(x)` for :math:`x \in
    \mathbb{R}^{n \times N}` where :math:`n` is the dimensionality of the data
    and :math:`N` the number of independent problems. It returns a dictionary
    with the found solution and some informations about the algorithm
    execution.

    Parameters
    ----------
    functions : list of objects
        A list of convex functions to minimize. These are objects who must
        implement the :meth:`pyunlocbox.functions.func.eval` method. The
        :meth:`pyunlocbox.functions.func.grad` and / or
        :meth:`pyunlocbox.functions.func.prox` methods are required by some
        solvers. Note also that some solvers can only handle two convex
        functions while others may handle more. Please refer to the
        documentation of the considered solver.
    x0 : array_like
        Starting point of the algorithm, :math:`x_0 \in \mathbb{R}^{n \times
        N}`.
    solver : solver class instance, optional
        The solver algorithm. It is an object who must inherit from
        :class:`pyunlocbox.solvers.solver` and implement the :meth:`_pre`,
        :meth:`_algo` and :meth:`_post` methods. If no solver object are
        provided, a standard one will be chosen given the number of convex
        function objects and their implemented methods.
    atol : float, optional
        The absolute tolerance stopping criterion. The algorithm stops when
        :math:`f(x^t) < atol` where :math:`f(x^t)` is the objective function at
        iteration :math:`t`. Default is None.
    dtol : float, optional
        Stop when the objective function is stable enough, i.e. when
        :math:`\left|f(x^t) - f(x^{t-1})\right| < dtol`. Default is None.
    rtol : float, optional
        The relative tolerance stopping criterion. The algorithm stops when
        :math:`\left|\frac{ f(x^t) - f(x^{t-1}) }{ f(x^t) }\right| < rtol`.
        Default is :math:`10^{-3}`.
    xtol : float, optional
        Stop when the variable is stable enough, i.e. when :math:`\frac{\|x^t -
        x^{t-1}\|_2}{\sqrt{n N}} < xtol`. Note that additional memory will be
        used to store :math:`x^{t-1}`. Default is None.
    maxit : int, optional
        The maximum number of iterations. Default is 200.
    verbosity : {'NONE', 'LOW', 'HIGH', 'ALL'}, optional
        The log level : ``'NONE'`` for no log, ``'LOW'`` for resume at
        convergence, ``'HIGH'`` for info at all solving steps, ``'ALL'`` for
        all possible outputs, including at each steps of the proximal operators
        computation. Default is ``'LOW'``.
    inplace : bool, optional
        If True and x0 is a numpy array, then x0 will be modified in place
        during execution to save memory. It will then contain the solution. Be
        careful to pass data of the type (int, float32, float64) you want your
        computations to use.

    Returns
    -------
    sol : ndarray
        The problem solution.
    solver : str
        The used solver.
    crit : {'ATOL', 'DTOL', 'RTOL', 'XTOL', 'MAXIT'}
        The used stopping criterion. See above for definitions.
    niter : int
        The number of iterations.
    time : float
        The execution time in seconds.
    objective : ndarray
        The successive evaluations of the objective function at each iteration.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers

    Define a problem:

    >>> y = [4, 5, 6, 7]
    >>> f = functions.norm_l2(y=y)

    Solve it:

    >>> x0 = np.zeros(len(y))
    >>> ret = solvers.solve([f], x0, atol=1e-2, verbosity='ALL')
    INFO: Dummy objective function added.
    INFO: Selected solver: forward_backward
    INFO: Forward-backward method
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 1.260000e+02
    Iteration 1 of forward_backward:
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 1.400000e+01
        objective = 1.40e+01
    Iteration 2 of forward_backward:
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 2.963739e-01
        objective = 2.96e-01
    Iteration 3 of forward_backward:
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 7.902529e-02
        objective = 7.90e-02
    Iteration 4 of forward_backward:
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 5.752265e-02
        objective = 5.75e-02
    Iteration 5 of forward_backward:
        dummy evaluation: 0.000000e+00
        norm_l2 evaluation: 5.142032e-03
        objective = 5.14e-03
    Solution found after 5 iterations:
        objective function f(sol) = 5.142032e-03
        stopping criterion: ATOL

    Verify the stopping criterion (should be smaller than atol=1e-2):

    >>> np.linalg.norm(ret['sol'] - y)**2  # doctest:+ELLIPSIS
    0.00514203...

    Show the solution (should be close to y w.r.t. the L2-norm measure):

    >>> ret['sol']
    array([4.02555301, 5.03194126, 6.03832952, 7.04471777])

    Show the used solver:

    >>> ret['solver']
    'forward_backward'

    Show some information about the convergence:

    >>> ret['crit']
    'ATOL'
    >>> ret['niter']
    5
    >>> ret['time']  # doctest:+SKIP
    0.0012578964233398438
    >>> ret['objective']  # doctest:+NORMALIZE_WHITESPACE,+ELLIPSIS
    [[126.0, 0], [13.99999999..., 0], [0.29637392..., 0], [0.07902528..., 0],
    [0.05752265..., 0], [0.00514203..., 0]]

    """
    # to prevent any modification of the input
    if not(inplace):
        x0 = x0.copy()

    if verbosity not in ['NONE', 'LOW', 'HIGH', 'ALL']:
        raise ValueError('Verbosity should be either NONE, LOW, HIGH or ALL.')

    # Add a second dummy convex function if only one function is provided.
    if len(functions) < 1:
        raise ValueError('At least 1 convex function should be provided.')
    elif len(functions) == 1:
        functions.append(dummy())
        if verbosity in ['LOW', 'HIGH', 'ALL']:
            print('INFO: Dummy objective function added.')

    # Choose a solver if none provided.
    if not solver:
        if len(functions) == 2:
            fb0 = 'GRAD' in functions[0].cap(x0) and \
                  'PROX' in functions[1].cap(x0)
            fb1 = 'GRAD' in functions[1].cap(x0) and \
                  'PROX' in functions[0].cap(x0)
            dg0 = 'PROX' in functions[0].cap(x0) and \
                  'PROX' in functions[1].cap(x0)
            if fb0 or fb1:
                solver = forward_backward()  # Need one prox and 1 grad.
            elif dg0:
                solver = douglas_rachford()  # Need two prox.
            else:
                raise ValueError('No suitable solver for the given functions.')
        elif len(functions) > 2:
            solver = generalized_forward_backward()
        if verbosity in ['LOW', 'HIGH', 'ALL']:
            name = solver.__class__.__name__
            print('INFO: Selected solver: {}'.format(name))

    # Set solver and functions verbosity.
    translation = {'ALL': 'HIGH', 'HIGH': 'HIGH', 'LOW': 'LOW', 'NONE': 'NONE'}
    solver.verbosity = translation[verbosity]
    translation = {'ALL': 'HIGH', 'HIGH': 'LOW', 'LOW': 'NONE', 'NONE': 'NONE'}
    functions_verbosity = []
    for f in functions:
        functions_verbosity.append(f.verbosity)
        f.verbosity = translation[verbosity]

    tstart = time.time()
    crit = None
    niter = 0
    rtol_only_zeros = True

    # Solver specific initialization.
    solver.pre(functions, x0)

    # Evaluate the objective function at the begining
    objective = [solver.objective(x0)]

    while not crit:

        niter += 1

        if xtol is not None:
            last_sol = np.array(solver.sol, copy=True)

        if verbosity in ['HIGH', 'ALL']:
            name = solver.__class__.__name__
            print('Iteration {} of {}:'.format(niter, name))

        # Solver iterative algorithm.
        solver.algo(objective, niter)

        objective.append(solver.objective(solver.sol))
        current = np.sum(objective[-1])
        last = np.sum(objective[-2])

        # Verify stopping criteria.
        if atol is not None and current < atol:
            crit = 'ATOL'
        if dtol is not None and np.abs(current - last) < dtol:
            crit = 'DTOL'
        if rtol is not None:
            div = current  # Prevent division by 0.
            if div == 0:
                if verbosity in ['LOW', 'HIGH', 'ALL']:
                    print('WARNING: (rtol) objective function is equal to 0 !')
                if last != 0:
                    div = last
                else:
                    div = 1.0  # Result will be zero anyway.
            else:
                rtol_only_zeros = False
            relative = np.abs((current - last) / div)
            if relative < rtol and not rtol_only_zeros:
                crit = 'RTOL'
        if xtol is not None:
            err = np.linalg.norm(solver.sol - last_sol)
            err /= np.sqrt(last_sol.size)
            if err < xtol:
                crit = 'XTOL'
        if maxit is not None and niter >= maxit:
            crit = 'MAXIT'

        if verbosity in ['HIGH', 'ALL']:
            print('    objective = {:.2e}'.format(current))

    # Restore verbosity for functions. In case they are called outside solve().
    for k, f in enumerate(functions):
        f.verbosity = functions_verbosity[k]

    if verbosity in ['LOW', 'HIGH', 'ALL']:
        print('Solution found after {} iterations:'.format(niter))
        print('    objective function f(sol) = {:e}'.format(current))
        print('    stopping criterion: {}'.format(crit))

    # Returned dictionary.
    result = {'sol':       solver.sol,
              'solver':    solver.__class__.__name__,  # algo for consistency ?
              'crit':      crit,
              'niter':     niter,
              'time':      time.time() - tstart,
              'objective': objective}
    try:
        # Update dictionary for primal-dual solvers
        result['dual_sol'] = solver.dual_sol
    except AttributeError:
        pass

    # Solver specific post-processing (e.g. delete references).
    solver.post()

    return result

class solver(object):
    r"""
    Defines the solver object interface.

    This class defines the interface of a solver object intended to be passed
    to the :func:`pyunlocbox.solvers.solve` solving function. It is intended to
    be a base class for standard solvers which will implement the required
    methods. It can also be instantiated by user code and dynamically modified
    for rapid testing. This class also defines the generic attributes of all
    solver objects.

    Parameters
    ----------
    step : float
        The gradient-descent step-size. This parameter is bounded by 0 and
        :math:`\frac{2}{\beta}` where :math:`\beta` is the Lipschitz constant
        of the gradient of the smooth function (or a sum of smooth functions).
        Default is 1.
    accel : pyunlocbox.acceleration.accel
        User-defined object used to adaptively change the current step size
        and solution while the algorithm is running. Default is a dummy
        object that returns unchanged values.

    """

    def __init__(self, step=1., accel=None):
        if step < 0:
            raise ValueError('Step should be a positive number.')
        self.step = step
        self.accel = acceleration.dummy() if accel is None else accel

    def pre(self, functions, x0):
        """
        Solver-specific pre-processing. See parameters documentation in
        :func:`pyunlocbox.solvers.solve` documentation.

        Notes
        -----
        When preprocessing the functions, the solver should split them into
        two lists:
        * `self.smooth_funs`, for functions involved in gradient steps.
        * `self.non_smooth_funs`, for functions involved proximal steps.
        This way, any method that takes in the solver as argument, such as the
        methods in :class:`pyunlocbox.acceleration.accel`, can have some
        context as to how the solver is using the functions.

        """
        self.sol = np.asarray(x0)
        self.smooth_funs = []
        self.non_smooth_funs = []
        self._pre(functions, self.sol)
        self.accel.pre(functions, self.sol)

    def _pre(self, functions, x0):
        raise NotImplementedError("Class user should define this method.")

    def algo(self, objective, niter):
        """
        Call the solver iterative algorithm and the provided acceleration
        scheme. See parameters documentation in
        :func:`pyunlocbox.solvers.solve`

        Notes
        -----
        The method :meth:`self.accel.update_sol` is called before
        :meth:`self._algo` because the acceleration schemes usually involves
        some sort of averaging of previous solutions, which can add some
        unwanted artifacts on the output solution. With this ordering, we
        guarantee that the output of solver.algo is not corrupted by the
        acceleration scheme.

        Similarly, the method :meth:`self.accel.update_step` is called after
        :meth:`self._algo` to allow the step update procedure to act directly
        on the solution output by the underlying algorithm, and not on the
        intermediate solution output by the acceleration scheme in
        :meth:`self.accel.update_sol`.

        """
        self.sol[:] = self.accel.update_sol(self, objective, niter)
        self.step = self.accel.update_step(self, objective, niter)
        self._algo()

    def _algo(self):
        raise NotImplementedError("Class user should define this method.")

    def post(self):
        """
        Solver-specific post-processing. Mainly used to delete references added
        during initialization so that the garbage collector can free the
        memory. See parameters documentation in
        :func:`pyunlocbox.solvers.solve`.

        """
        self._post()
        self.accel.post()
        del self.sol, self.smooth_funs, self.non_smooth_funs

    def _post(self):
        raise NotImplementedError("Class user should define this method.")

    def objective(self, x):
        """
        Return the objective function at x.

        Necessitate `solver._pre(...)` to be run first.
        """
        return self._objective(x)

    def _objective(self, x):
        obj_smooth = [f.eval(x) for f in self.smooth_funs]
        obj_nonsmooth = [f.eval(x) for f in self.non_smooth_funs]
        return obj_nonsmooth + obj_smooth

class gradient_descent(solver):
    r"""
    Gradient descent algorithm.

    This algorithm solves optimization problems composed of the sum of
    any number of smooth functions.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.solver` base class.

    Notes
    -----
    This algorithm requires each function implement the
    :meth:`pyunlocbox.functions.func.grad` method.

    See :class:`pyunlocbox.acceleration.regularized_nonlinear` for a very
    efficient acceleration scheme for this method.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> dim = 25
    >>> np.random.seed(0)
    >>> xstar = np.random.rand(dim)  # True solution
    >>> x0 = np.random.rand(dim)
    >>> x0 = xstar + 5*(x0 - xstar) / np.linalg.norm(x0 - xstar)
    >>> A = np.random.rand(dim, dim)
    >>> step = 1 / np.linalg.norm(np.dot(A.T, A))
    >>> f = functions.norm_l2(lambda_=0.5, A=A, y=np.dot(A, xstar))
    >>> fd = functions.dummy()
    >>> solver = solvers.gradient_descent(step=step)
    >>> params = {'rtol':0, 'maxit':14000, 'verbosity':'NONE'}
    >>> ret = solvers.solve([f, fd], x0, solver, **params)
    >>> pctdiff = 100 * np.sum((xstar - ret['sol'])**2) / np.sum(xstar**2)
    >>> print('Difference: {0:.1f}%'.format(pctdiff))
    Difference: 1.3%

    """

    def __init__(self, **kwargs):
        super(gradient_descent, self).__init__(**kwargs)

    def _pre(self, functions, x0):

        for f in functions:
            if 'GRAD' in f.cap(x0):
                self.smooth_funs.append(f)
            else:
                raise ValueError('Gradient descent requires each function to '
                                 'implement grad().')

        if self.verbosity == 'HIGH':
            print('INFO: Gradient descent minimizing {} smooth '
                  'functions.'.format(len(self.smooth_funs)))

    def _algo(self):
        grad = np.zeros_like(self.sol)
        for f in self.smooth_funs:
            grad += f.grad(self.sol)
        self.sol[:] -= self.step * grad

    def _post(self):
        pass

class forward_backward(solver):
    r"""
    Forward-backward proximal splitting (FISTA and ISTA) algorithm.

    This algorithm solves convex optimization problems composed of the sum of
    a smooth and a non-smooth function.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.solver` base class.

    Parameters
    ----------
    accel : :class:`pyunlocbox.acceleration.accel`
        Acceleration scheme to use.
        Default is :meth:`pyunlocbox.acceleration.fista`, which corresponds
        to the 'FISTA' solver. Passing :meth:`pyunlocbox.acceleration.dummy`
        instead results in the ISTA solver. Note that while FISTA is much more
        time-efficient, it is less memory-efficient.

    Notes
    -----
    This algorithm requires one function to implement the
    :meth:`pyunlocbox.functions.func.prox` method and the other one to
    implement the :meth:`pyunlocbox.functions.func.grad` method.

    See :cite:`beck2009FISTA` for details about the algorithm.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> y = [4, 5, 6, 7]
    >>> x0 = np.zeros(len(y))
    >>> f1 = functions.norm_l2(y=y)
    >>> f2 = functions.dummy()
    >>> solver = solvers.forward_backward(step=0.5)
    >>> ret = solvers.solve([f1, f2], x0, solver, atol=1e-5)
    Solution found after 15 iterations:
        objective function f(sol) = 4.957288e-07
        stopping criterion: ATOL
    >>> ret['sol']
    array([4.0002509 , 5.00031362, 6.00037635, 7.00043907])

    """

    def __init__(self, accel=acceleration.fista(), **kwargs):
        super(forward_backward, self).__init__(accel=accel, **kwargs)

    def _pre(self, functions, x0):

        if self.verbosity == 'HIGH':
            print('INFO: Forward-backward method')

        if len(functions) != 2:
            raise ValueError('Forward-backward requires two convex functions.')

        if 'PROX' in functions[0].cap(x0) and 'GRAD' in functions[1].cap(x0):
            self.smooth_funs.append(functions[1])
            self.non_smooth_funs.append(functions[0])
        elif 'PROX' in functions[1].cap(x0) and 'GRAD' in functions[0].cap(x0):
            self.smooth_funs.append(functions[0])
            self.non_smooth_funs.append(functions[1])
        else:
            raise ValueError('Forward-backward requires a function to '
                             'implement prox() and the other grad().')

    def _algo(self):
        # Forward step
        x = self.sol - self.step * self.smooth_funs[0].grad(self.sol)
        # Backward step
        self.sol[:] = self.non_smooth_funs[0].prox(x, self.step)

    def _post(self):
        pass

class generalized_forward_backward(solver):
    r"""
    Generalized forward-backward proximal splitting algorithm.

    This algorithm solves convex optimization problems composed of the sum of
    any number of non-smooth (or smooth) functions.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.solver` base class.

    Parameters
    ----------
    lambda_ : float, optional
        A relaxation parameter bounded by 0 and 1. Default is 1.

    Notes
    -----
    This algorithm requires each function to either implement the
    :meth:`pyunlocbox.functions.func.prox` method or the
    :meth:`pyunlocbox.functions.func.grad` method.

    See :cite:`raguet2013generalizedFB` for details about the algorithm.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> y = [0.01, 0.2, 8, 0.3, 0 , 0.03, 7]
    >>> x0 = np.zeros(len(y))
    >>> f1 = functions.norm_l2(y=y)
    >>> f2 = functions.norm_l1()
    >>> solver = solvers.generalized_forward_backward(lambda_=1, step=0.5)
    >>> ret = solvers.solve([f1, f2], x0, solver)
    Solution found after 2 iterations:
        objective function f(sol) = 1.463100e+01
        stopping criterion: RTOL
    >>> ret['sol']
    array([0. , 0. , 7.5, 0. , 0. , 0. , 6.5])

    """

    def __init__(self, lambda_=1, *args, **kwargs):
        super(generalized_forward_backward, self).__init__(*args, **kwargs)
        self.lambda_ = lambda_

    def _pre(self, functions, x0):

        if self.lambda_ <= 0 or self.lambda_ > 1:
            raise ValueError('Lambda is bounded by 0 and 1.')

        self.z = []
        for f in functions:
            if 'GRAD' in f.cap(x0):
                self.smooth_funs.append(f)
            elif 'PROX' in f.cap(x0):
                self.non_smooth_funs.append(f)
                self.z.append(np.array(x0, copy=True))
            else:
                raise ValueError('Generalized forward-backward requires each '
                                 'function to implement prox() or grad().')

        if self.verbosity == 'HIGH':
            print('INFO: Generalized forward-backward minimizing {} smooth '
                  'functions and {} non-smooth functions.'.format(
                      len(self.smooth_funs), len(self.non_smooth_funs)))

    def _algo(self):

        # Smooth functions.
        grad = np.zeros_like(self.sol)
        for f in self.smooth_funs:
            grad += f.grad(self.sol)

        # Non-smooth functions.
        if not self.non_smooth_funs:
            self.sol[:] -= self.step * grad  # Reduces to gradient descent.
        else:
            sol = np.zeros_like(self.sol)
            for i, g in enumerate(self.non_smooth_funs):
                tmp = 2 * self.sol - self.z[i] - self.step * grad
                tmp[:] = g.prox(tmp, self.step * len(self.non_smooth_funs))
                self.z[i] += self.lambda_ * (tmp - self.sol)
                sol += 1. * self.z[i] / len(self.non_smooth_funs)
            self.sol[:] = sol

    def _post(self):
        del self.z

class douglas_rachford(solver):
    r"""
    Douglas-Rachford proximal splitting algorithm.

    This algorithm solves convex optimization problems composed of the sum of
    two non-smooth (or smooth) functions.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.solver` base class.

    Parameters
    ----------
    lambda_ : float, optional
        The update term weight. It should be between 0 and 1. Default is 1.

    Notes
    -----
    This algorithm requires the two functions to implement the
    :meth:`pyunlocbox.functions.func.prox` method.

    See :cite:`combettes2007DR` for details about the algorithm.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> y = [4, 5, 6, 7]
    >>> x0 = np.zeros(len(y))
    >>> f1 = functions.norm_l2(y=y)
    >>> f2 = functions.dummy()
    >>> solver = solvers.douglas_rachford(lambda_=1, step=1)
    >>> ret = solvers.solve([f1, f2], x0, solver, atol=1e-5)
    Solution found after 8 iterations:
        objective function f(sol) = 2.927052e-06
        stopping criterion: ATOL
    >>> ret['sol']
    array([3.99939034, 4.99923792, 5.99908551, 6.99893309])

    """

    def __init__(self, lambda_=1, *args, **kwargs):
        super(douglas_rachford, self).__init__(*args, **kwargs)
        self.lambda_ = lambda_

    def _pre(self, functions, x0):

        if self.lambda_ <= 0 or self.lambda_ > 1:
            raise ValueError('Lambda is bounded by 0 and 1.')

        if len(functions) != 2:
            raise ValueError('Douglas-Rachford requires two convex functions.')

        for f in functions:
            if 'PROX' in f.cap(x0):
                self.non_smooth_funs.append(f)
            else:
                raise ValueError('Douglas-Rachford requires each '
                                 'function to implement prox().')

        self.z = np.array(x0, copy=True)

    def _algo(self):
        tmp = self.non_smooth_funs[0].prox(2 * self.sol - self.z, self.step)
        self.z[:] = self.z + self.lambda_ * (tmp - self.sol)
        self.sol[:] = self.non_smooth_funs[1].prox(self.z, self.step)

    def _post(self):
        del self.z

class primal_dual(solver):
    r"""
    Parent class of all primal-dual algorithms.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.solver` base class.

    Parameters
    ----------
    L : function or ndarray, optional
        The transformation L that maps from the primal variable space to the
        dual variable space. Default is the identity, :math:`L(x)=x`. If `L` is
        an ``ndarray``, it will be converted to the operator form.
    Lt : function or ndarray, optional
        The adjoint operator. If `Lt` is an ``ndarray``, it will be converted
        to the operator form. If `L` is an ``ndarray``, default is the
        transpose of `L`. If `L` is a function, default is `L`,
        :math:`Lt(x)=L(x)`.
    d0: ndarray, optional
        Initialization of the dual variable.

    """

    def __init__(self, L=None, Lt=None, d0=None, *args, **kwargs):
        super(primal_dual, self).__init__(*args, **kwargs)

        if L is None:
            self.L = lambda x: x
        else:
            if callable(L):
                self.L = L
            else:
                # Transform matrix form to operator form.
                self.L = lambda x: L.dot(x)

        if Lt is None:
            if L is None:
                self.Lt = lambda x: x
            elif callable(L):
                self.Lt = L
            else:
                self.Lt = lambda x: L.T.dot(x)
        else:
            if callable(Lt):
                self.Lt = Lt
            else:
                self.Lt = lambda x: Lt.dot(x)

        self.d0 = d0

    def _pre(self, functions, x0):
        # Dual variable.
        if self.d0 is None:
            # The copy is necessary in case `L = lambda x: x`.
            self.dual_sol = self.L(np.asarray(x0).copy())
        else:
            self.dual_sol = self.d0

    def _post(self):
        self.d0 = None
        del self.dual_sol

    def _objective(self, x):
        obj_smooth = [f.eval(x) for f in self.smooth_funs]
        obj_nonsmooth = [self.non_smooth_funs[0].eval(x),
                         self.non_smooth_funs[1].eval(self.L(x))]
        return obj_nonsmooth + obj_smooth

class mlfbf(primal_dual):
    r"""
    Monotone+Lipschitz forward-backward-forward primal-dual algorithm.

    This algorithm solves convex optimization problems with objective of the
    form :math:`f(x) + g(Lx) + h(x)`, where :math:`f` and :math:`g` are proper,
    convex, lower-semicontinuous functions with easy-to-compute proximity
    operators, and :math:`h` has Lipschitz-continuous gradient with constant
    :math:`\beta`.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.primal_dual` base class.

    Notes
    -----
    The order of the functions matters: set :math:`f` first on the list,
    :math:`g` second, and :math:`h` third.

    This algorithm requires the first two functions to implement the
    :meth:`pyunlocbox.functions.func.prox` method, and the third function to
    implement the :meth:`pyunlocbox.functions.func.grad` method.

    The step-size should be in the interval :math:`\left] 0, \frac{1}{\beta +
    \|L\|_{2}}\right[`.

    See :cite:`komodakis2015primaldual`, Algorithm 6, for details.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> y = np.array([294, 390, 361])
    >>> L = np.array([[5, 9, 3], [7, 8, 5], [4, 4, 9], [0, 1, 7]])
    >>> x0 = np.zeros(len(y))
    >>> f = functions.dummy()
    >>> f._prox = lambda x, T: np.maximum(np.zeros(len(x)), x)
    >>> g = functions.norm_l2(lambda_=0.5)
    >>> h = functions.norm_l2(y=y, lambda_=0.5)
    >>> max_step = 1/(1 + np.linalg.norm(L, 2))
    >>> solver = solvers.mlfbf(L=L, step=max_step/2.)
    >>> ret = solvers.solve([f, g, h], x0, solver, maxit=1000, rtol=0)
    Solution found after 1000 iterations:
        objective function f(sol) = 1.839060e+05
        stopping criterion: MAXIT
    >>> ret['sol']
    array([1., 1., 1.])

    """

    def _pre(self, functions, x0):
        super(mlfbf, self)._pre(functions, x0)

        if len(functions) != 3:
            raise ValueError('MLFBF requires 3 convex functions.')

        self.non_smooth_funs.append(functions[0])   # f
        self.non_smooth_funs.append(functions[1])   # g
        self.smooth_funs.append(functions[2])       # h

    def _algo(self):
        # Forward steps (in both primal and dual spaces)
        y1 = self.sol - self.step * (self.smooth_funs[0].grad(self.sol) +
                                     self.Lt(self.dual_sol))
        y2 = self.dual_sol + self.step * self.L(self.sol)

        # Backward steps (in both primal and dual spaces)
        p1 = self.non_smooth_funs[0].prox(y1, self.step)
        p2 = _prox_star(self.non_smooth_funs[1], y2, self.step)

        # Forward steps (in both primal and dual spaces)
        q1 = p1 - self.step * (self.smooth_funs[0].grad(p1) + self.Lt(p2))
        q2 = p2 + self.step * self.L(p1)

        # Update solution (in both primal and dual spaces)
        self.sol[:] = self.sol - y1 + q1
        self.dual_sol[:] = self.dual_sol - y2 + q2

class projection_based(primal_dual):
    r"""
    Projection-based primal-dual algorithm.

    This algorithm solves convex optimization problems with objective of the
    form :math:`f(x) + g(Lx)`, where :math:`f` and :math:`g` are proper,
    convex, lower-semicontinuous functions with easy-to-compute proximity
    operators.

    See generic attributes descriptions of the
    :class:`pyunlocbox.solvers.primal_dual` base class.

    Parameters
    ----------
    lambda_ : float, optional
        The update term weight. It should be between 0 and 2. Default is 1.

    Notes
    -----
    The order of the functions matters: set :math:`f` first on the list, and
    :math:`g` second.

    This algorithm requires the two functions to implement the
    :meth:`pyunlocbox.functions.func.prox` method.

    The step-size should be in the interval :math:`\left] 0, \infty \right[`.

    See :cite:`komodakis2015primaldual`, Algorithm 7, for details.

    Examples
    --------
    >>> import numpy as np
    >>> from pyunlocbox import functions, solvers
    >>> y = np.array([294, 390, 361])
    >>> L = np.array([[5, 9, 3], [7, 8, 5], [4, 4, 9], [0, 1, 7]])
    >>> x0 = np.array([500, 1000, -400])
    >>> f = functions.norm_l1(y=y)
    >>> g = functions.norm_l1()
    >>> solver = solvers.projection_based(L=L, step=1.)
    >>> ret = solvers.solve([f, g], x0, solver, maxit=1000, rtol=None, xtol=.1)
    Solution found after 996 iterations:
        objective function f(sol) = 1.045000e+03
        stopping criterion: XTOL
    >>> ret['sol']
    array([0, 0, 0])

    """

    def __init__(self, lambda_=1., *args, **kwargs):
        super(projection_based, self).__init__(*args, **kwargs)
        self.lambda_ = lambda_

    def _pre(self, functions, x0):
        super(projection_based, self)._pre(functions, x0)

        if self.lambda_ <= 0 or self.lambda_ > 2:
            raise ValueError('Lambda is bounded by 0 and 2.')

        if len(functions) != 2:
            raise ValueError('projection_based requires 2 convex functions.')

        self.non_smooth_funs.append(functions[0])   # f
        self.non_smooth_funs.append(functions[1])   # g

    def _algo(self):
        a = self.non_smooth_funs[0].prox(self.sol - self.step *
                                         self.Lt(self.dual_sol), self.step)
        ell = self.L(self.sol)
        b = self.non_smooth_funs[1].prox(ell + self.step * self.dual_sol,
                                         self.step)
        s = (self.sol - a) / self.step + self.Lt(ell - b) / self.step
        t = b - self.L(a)
        tau = np.sum(s**2) + np.sum(t**2)
        if tau == 0:
            self.sol[:] = a
            self.dual_sol[:] = self.dual_sol + (ell - b) / self.step
        else:
            theta = self.lambda_ * (np.sum((self.sol - a)**2) / self.step +
                                    np.sum((ell - b)**2) / self.step) / tau
            self.sol[:] = self.sol - theta * s
            self.dual_sol[:] = self.dual_sol - theta * t


#!/usr/bin/env python
#
# @license Apache-2.0
#
# Copyright (c) 2018 The Stdlib Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Benchmark numpy.ndarray."""

from __future__ import print_function
import timeit

REPEATS = 3
COUNT = [0]  # use a list to allow modification within nested scopes

def print_version():
    """Print the TAP version."""
    print("TAP version 13")

def print_summary(total, passing):
    """Print the benchmark summary.

    # Arguments

    * `total`: total number of tests
    * `passing`: number of passing tests

    """
    print("#")
    print("1.." + str(total))  # TAP plan
    print("# total " + str(total))
    print("# pass  " + str(passing))
    print("#")
    print("# ok")

def print_results(iterations, elapsed):
    """Print benchmark results.

    # Arguments

    * `iterations`: number of iterations
    * `elapsed`: elapsed time (in seconds)

    # Examples

    ``` python
    python> print_results(100000, 0.131009101868)
    ```
    """
    rate = iterations / elapsed

    print("  ---")
    print("  iterations: " + str(iterations))
    print("  elapsed: " + str(elapsed))
    print("  rate: " + str(rate))
    print("  ...")

def benchmark(name, setup, stmt, iterations):
    """Run a benchmark and print benchmark results.

    # Arguments

    * `name`: benchmark name (suffix)
    * `setup`: benchmark setup
    * `stmt`: statement to benchmark
    * `iterations`: number of iterations

    # Examples

    ``` python
    python> benchmark("::random", "from random import random;", "y = random()", 1000000)
    ```
    """
    t = timeit.Timer(stmt, setup=setup)

    i = 0
    while i < REPEATS:
        print("# python::numpy" + name)
        COUNT[0] += 1
        elapsed = t.timeit(number=iterations)
        print_results(iterations, elapsed)
        print("ok " + str(COUNT[0]) + " benchmark finished")
        i += 1

def main():
    """Run the benchmarks."""
    print_version()

    name = "::instantiation"
    setup = "import numpy as np; x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype='float64'); shape = [3, 2]; strides = [2, 1]; offset = 0; order = 'C';"
    stmt = "y = np.ndarray(buffer=x, shape=shape, strides=strides, offset=offset, order=order)"
    iterations = 100000
    benchmark(name, setup, stmt, iterations)

    name = "::get:data"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.data"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:dtype"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.dtype"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:flags"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.flags"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:length"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.size"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:BYTES_PER_ELEMENT"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.itemsize"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:byteLength"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.nbytes"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:ndims"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.ndim"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:shape"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.shape"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = "::get:strides"
    setup = "import numpy as np; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape);"
    stmt = "z = y.strides"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = ":get"
    setup = "import numpy as np; from math import floor; from random import random; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape, dtype='float64');"
    stmt = "z = y[int(floor(random()*3.0)), 1]"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = ":set"
    setup = "import numpy as np; from math import floor; from random import random; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape, dtype='float64');"
    stmt = "y[int(floor(random()*3.0)), 1] = random()"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = ":iget"
    setup = "import numpy as np; from math import floor; from random import random; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape, dtype='float64');"
    stmt = "z = y.item(int(floor(random()*4.0)))"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    name = ":iset"
    setup = "import numpy as np; from math import floor; from random import random; shape = [3, 2]; y = np.ndarray(buffer=np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]), shape=shape, dtype='float64');"
    stmt = "y.itemset(int(floor(random()*4.0)), random())"
    iterations = 1000000
    benchmark(name, setup, stmt, iterations)

    print_summary(COUNT[0], COUNT[0])

if __name__ == "__main__":
    main()


'''
Created on Apr 7, 2014

@author: dustin
'''
"""
You can use the proper typesetting unicode minus (see
http://en.wikipedia.org/wiki/Plus_sign#Plus_sign) or the ASCII hypen
for minus, which some people prefer.  The matplotlibrc param
axes.unicode_minus controls the default behavior.

The default is to use the unicode minus
"""
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

matplotlib.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots()
ax.plot(10*np.random.randn(100), 10*np.random.randn(100), 'o')
ax.set_title('Using hypen instead of unicode minus')
plt.show()


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

# # Week 1
#
# IPL (Indian Premier League) (cricket)

# # Imports

# In[1]:

# %load /Users/bartev/dev/github-bv/sporty/notebooks/imports.py
# %load /Users/bartev/dev/github-bv/sporty/notebooks/imports.py

## Where am I
get_ipython().system('echo $VIRTUAL_ENV')

from IPython.core.display import display, HTML
display(HTML("<style>.container { width:95% !important; }</style>"))

# magics
get_ipython().run_line_magic('load_ext', 'blackcellmagic')
# start cell with `%%black` to format using `black`

get_ipython().run_line_magic('load_ext', 'autoreload')
# start cell with `%autoreload` to reload module
# https://ipython.org/ipython-doc/stable/config/extensions/autoreload.html

# reload all modules when running
get_ipython().run_line_magic('autoreload', '2')

# In[2]:

# imports

import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
import seaborn as sns

from importlib import reload
from pathlib import Path

import matplotlib as mpl
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')

# https://plotnine.readthedocs.io/en/stable/

import plotnine as p9
from plotnine import ggplot, aes, facet_wrap

# In[30]:

get_ipython().system('pip install xlrd')

# In[32]:

get_ipython().system('pip install openpyxl')

# # Read data

# Need `IPL2018teams` data
#
# * https://www.kaggle.com/manasgarg/ipl

# ## archive
#
# 2008-2017
data_dir = Path('/Users/bartev/dev/github-bv/sporty/data/raw/wk1-IPL/archive')
matches = pd.read_csv(data_dir / 'matches.csv')

deliveries = pd.read_csv(data_dir / 'deliveries.csv')deliveries.head()matches.groupby('season').size()
# ## archive 2
# 2008 - 2019
data_dir2 = Path('/Users/bartev/dev/github-bv/sporty/data/raw/wk1-IPL/archive 2')
matches2 = pd.read_csv(data_dir2 / 'matches.csv')
deliveries2 = pd.read_csv(data_dir2 / 'deliveries.csv')matches2.groupby('season').size()deliveries2
# ## archive-3
#
# I found this data somewhere on Kaggle (didn't keep the link)
#
# More data than `archive 2`?

# In[157]:

data_dir = Path('/Users/bartev/dev/github-bv/sporty/data/raw/wk1-IPL/archive-3')
matches = pd.read_csv(data_dir3 / 'matches.csv')
deliveries = pd.read_csv(data_dir3/ 'deliveries.csv')
teams = pd.read_csv(data_dir3 / 'teams.csv')
teamwise = pd.read_csv(data_dir3 / 'teamwise_home_and_away.csv')
players = pd.read_excel(data_dir3 / 'Players.xlsx')

# In[158]:

matches.groupby('Season').size()

# In[63]:

deliveries.head()

# In[64]:

matches.head(2)

# In[159]:

teamwise

# In[ ]:

ipl18 = matches.query("Season == 'IPL-2018'").assign(
    hwin=lambda x: np.where(x["team1"] == x["winner"], 1, 0),
    awin=lambda x: np.where(x["team2"] == x["winner"], 1, 0),
    #                 htruns=lambda x: np.where(x['team1'] == x[''])
)
ipl18

# In[101]:

# get the match ids for season 2018
match_ids_2018 = ipl18["id"]

# Count runs scored by each team for season 2018
match_team_runs = (
    deliveries[["match_id", "inning", "batting_team", "bowling_team", "total_runs"]]
    .merge(match_ids_2018, left_on="match_id", right_on="id")
    .groupby(["match_id", "batting_team"])
    .agg({"total_runs": sum})
    .reset_index()
    .rename(columns={"match_id": "id"})
)
match_team_runs.head(5)

# In[133]:

ipl_cols = [
    "id",
    "Season",
    "date",
    "team1",
    "team2",
    "winner",
    "win_by_runs",
    "win_by_wickets",
    "hwin",
    "awin",
]

ipl18_exp = (
    ipl18[ipl_cols]
    .rename(columns={"team1": "home_team", "team2": "away_team"})
    .merge(
        match_team_runs, left_on=["id", "home_team"], right_on=["id", "batting_team"]
    )
    .drop(columns="batting_team")
    .rename(columns={"total_runs": "htruns"})
    .merge(
        match_team_runs, left_on=["id", "away_team"], right_on=["id", "batting_team"]
    )
    .drop(columns="batting_team")
    .rename(columns={"total_runs": "atruns"})
    .assign(
        my_hwin=lambda x: np.where(x["home_team"] == x["winner"], 1, 0),
        my_awin=lambda x: np.where(x["away_team"] == x["winner"], 1, 0),
    )
    #     .assign(match=lambda x: x['hwin'] == x['my_hwin'])
    .assign(count=1)
)

# In[134]:

ipl18_exp

# * Ph/Pa : number of games played at home/away

# In[143]:

iplhome = (
    ipl18_exp.groupby("home_team")["count", "hwin", "htruns", "atruns"]
    .sum()
    .reset_index()
    .rename(
        columns={
            "home_team": "team",
            "count": "Ph",
            "htruns": "htrunsh",
            "atruns": "atrunsh",
        }
    )
)
iplhome

# In[144]:

iplaway = (
    ipl18_exp.groupby("away_team")["count", "awin", "htruns", "atruns"]
    .sum()
    .reset_index()
    .rename(
        columns={
            "away_team": "team",
            "count": "Pa",
            "htruns": "htrunsa",
            "atruns": "atrunsa",
        }
    )
)
iplaway

# In[151]:

ipl18_combo = (
    iplhome.merge(iplaway, on="team")
    # aggregate home/away data for wins, games played, and runs
    .assign(
        W=lambda x: x["hwin"] + x["awin"],
        G=lambda x: x["Ph"] + x["Pa"],
        R=lambda x: x["htrunsh"] + x["atrunsa"],
        RA=lambda x: x["atrunsh"] + x["htrunsa"],
    )
    # get win percentage and pythagorean expectation
    .assign(wpc=lambda x: x['W'] / x['G'],
           pyth=lambda x: x['R']**2 / (x['R']**2 + x['RA']**2))
)

ipl18_combo

# In[152]:

sns.relplot(x='pyth', y='wpc', data=ipl18_combo)

# # Run a regression

# In[154]:

pyth_lm = smf.ols(formula = 'wpc ~ pyth', data=ipl18_combo).fit()
pyth_lm.summary()

# Interpretation
#
# * pyth
#     * coef = 2.5
#     * std err = 2.077
#     * t value = 1.235
#     * P-value = 0.263 >> 0.05
#     * P-value = probability that we'd observe the value 2.5 (for the coef) by chance if the true value were 0
#     * p-values > 0.05, so values are not considered statistically significant.
#         We have no confidence in this relationship.
#     * R^2 is low (20%)

# In[ ]: