0% acharam este documento útil (0 voto)
92 visualizações165 páginas

Análise Prática de Séries Temporais - Livro

O documento aborda a história e a aplicação de séries temporais, destacando a importância de dados históricos em áreas como medicina e estatísticas vitais. Ele também explora técnicas de manipulação de dados, como 'data wrangling', para análise de séries temporais, utilizando exemplos práticos em Python e R. Além disso, discute métodos de imputação de dados ausentes e visualização de resultados.

Enviado por

luizctsilva
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
92 visualizações165 páginas

Análise Prática de Séries Temporais - Livro

O documento aborda a história e a aplicação de séries temporais, destacando a importância de dados históricos em áreas como medicina e estatísticas vitais. Ele também explora técnicas de manipulação de dados, como 'data wrangling', para análise de séries temporais, utilizando exemplos práticos em Python e R. Além disso, discute métodos de imputação de dados ausentes e visualização de resultados.

Enviado por

luizctsilva
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 165

Caderno Colorido

Rio de Janeiro, 2021


CAPÍTULO 1
Séries Temporais: Panorama e Breve História

Figura 1-1. As tabelas atuariais de John Graunt foram um dos primeiros resultados do
pensamento no estilo de série temporal aplicado a questões médicas. Fonte: Wikipedia
(https://perma.cc/2FHJ-67SB).

1
Figura 1-2. Um registro inicial de ECG do artigo original de 1877 de Augustus D. Walder,
MD, “A Demonstration on Man of Electromotive Changes Accompanying the Heart’s Beat”
[Uma Demonstração Individual das Mudanças Eletromotivas que Acompanham as Batidas
do Coração, em tradução livre] (https://perma.cc/ZGB8-3C95). Os ECGs mais antigos eram
difíceis de construir e usar, por isso levaram mais algumas décadas para que se tornassem
uma ferramenta operacional para os clínicos gerais.

Figura 1-3. Primeiro registro humano de um EEG, de 1924. Fonte: Wikipedia


(https://oreil.ly/P_M4U).

Figura 1-4. O governo federal dos EUA financia muitas agências governamentais e
organizações sem fins lucrativos que registram estatísticas vitais e elaboram indicadores
econômicos. Fonte: Agência Nacional de Pesquisa Econômica dos Estados Unidos (https://
www.nber.org/cycles/cyclesmain.html).

2 | Capítulo 1: Séries Temporais: Panorama e Breve História


CAPÍTULO 2
Encontrando Dados e Usando o Data Wrangling
com Séries Temporais

Figura 2-1. O UCI Machine Learning Repository inclui uma lista com explicações detalhadas
de conjuntos de dados de séries temporais.

3
Figura 2-2. O conjunto de dados de absenteísmo no trabalho [absenteeism at work data set]
é o primeiro na lista de conjuntos de dados de séries temporais no UCI Machine Learning
Repository.

Figura 2-3. As primeiras linhas de um arquivo do conjunto de dados de linguagem de sinais


australiana [Australian sign language data set]. Como você pode ver, este é um formato de
arquivo de dados largo.

Figura 2-4. Gráficos de um ator e uma atriz realizando movimentos de yoga repetidamente.
Plotamos duas séries temporais de amostra por ator. Não há rótulos de tempo explícitos no
eixo x. Em vez da unidade de tempo, o que importa é se os pontos de dados do eixo x estão
espaçados uniformemente, como são apresentados aqui.

4 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


Figura 2-5. Um espectro de amostra do vinho amostrado no conjunto de dados de vinho
UCI. Os picos na curva indicam regiões de comprimento de onda que têm taxas de absorção
extremamente altas. Os comprimentos de onda são espaçados uniformemente ao longo do
eixo x, enquanto o eixo y indica a taxa de absorção, também em escala linear. Podemos usar
a análise de série temporal para comparar curvas entre si, como feito antes.

## python
>>> YearJoined.groupby('memberId').count().
groupby('memberStats').count()

1000

## python
>>> emails[emails.EmailsOpened < 1]

Empty DataFrame
Columns: [EmailsOpened, member, week]
Index: []

## python
>>> emails[emails.member == 998]
EmailsOpened member week
25464 1 998 2017-12-04
25465 3 998 2017-12-11
25466 3 998 2017-12-18
25467 3 998 2018-01-01
25468 3 998 2018-01-08
25469 2 998 2018-01-15
25470 3 998 2018-01-22
25471 2 998 2018-01-29
25472 3 998 2018-02-05
25473 3 998 2018-02-12
25474 3 998 2018-02-19
25475 2 998 2018-02-26
25476 2 998 2018-03-05

## python
>>> (max(emails[emails.member == 998].week) -
min(emails[emails.member == 998].week)).days/7
25.0

Análise Prática de Séries Temporais | 5


## python
>>> emails[emails.member == 998].shape
(24, 3)

## python
>>> complete_idx = pd.MultiIndex.from_product((set(emails.week),
set(emails.member)))

## python
>>> all_email = emails.set_index(['week', 'member']).
reindex(complete_idx, fill_value = 0).
reset_index()
>>> all_email.columns = ['week', 'member', 'EmailsOpened']

## python
>>> all_email[all_email.member == 998].sort_values('week')
week member EmailsOpened
2015-02-09 998 0
2015-02-16 998 0
2015-02-23 998 0
2015-03-02 998 0
2015-03-09 998 0

## python
>>> cutoff_dates = emails.groupby('member').week.
agg(['min', 'max']).reset_index)
>>> cutoff_dates = cutoff_dates.reset_index()

## python
>>>python
## for _, row in cutoff_dates.iterrows():
>>> for
member
_, row in=cutoff_dates.iterrows():
row['member']
>>> start_date
member = row['min']
row['member']
>>> end_date
start_date = row['max']
row['min']
>>> all_email.drop(
end_date = row['max']
all_email[all_email.member == member]
>>> all_email.drop(
[all_email.week < start_date].index,
all_email[all_email.member inplace=True)
== member]
>>> all_email.drop(all_email[all_email.member
[all_email.week < start_date].index, ==inplace=True)
member]
[all_email.week > end_date].index, inplace=True)
>>> all_email.drop(all_email[all_email.member == member]
[all_email.week > end_date].index, inplace=True)

## python
>>> donations.timestamp = pd.to_datetime(donations.timestamp)
>>> donations.set_index('timestamp', inplace = True)
>>> agg_don = donations.groupby('member').apply(
lambda df: df.amount.resample("W-MON").sum().dropna())

6 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


## python
>>> for member, member_email in all_email.groupby('member'):
>>> member_donations = agg_donations[agg_donations.member
== member]

>>> member_donations.set_index('timestamp', inplace = True)


>>> member_email.set_index ('week', inplace = True)

>>> member_email = all_email[all_email.member == member]


>>> member_email.sort_values('week').set_index('week')

>>> df = pd.merge(member_email, member_donations, how = 'left',


left_index = True,
right_index = True)
>>> df.fillna(0)

>>> df['member'] = df.member_x


>>> merged_df = merged_df.append(df.reset_index()
[['member', 'week', 'emailsOpened', 'amount']])

## python
>>> df = merged_df[merged_df.member == 998]
>>> df['target'] = df.amount.shift(1)
>>> df = df.fillna(0)
>>> df

amount emailsOpened member week target


0 1 998 2017-12-04 0
0 3 998 2017-12-11 0
0 3 998 2017-12-18 0
0 0 998 2017-12-25 0
0 3 998 2018-01-01 0
50 3 998 2018-01-08 0
0 2 998 2018-01-15 50

## R
> require(zoo) ## O zoo fornece os recursos para séries temporais.
> require(data.table) ## O data.table é um dataframe de alto desempenho.

> unemp <- fread("UNRATE.csv")


> unemp[, DATE := as.Date(DATE)]
> setkey(unemp, DATE)

> ## Gera um conjunto de dados onde os dados estão aleatoriamente ausentes.


> rand.unemp.idx <- sample(1:nrow(unemp), .1*nrow(unemp))
> rand.unemp <- unemp[-rand.unemp.idx]

> ## Gera um conjunto de dados onde os dados possuem maior probabilidade


> ## de ausência quando o desemprego é alto.
> high.unemp.idx <- which(unemp$UNRATE > 8)
> num.to.select <- .2 * length(high.unemp.idx)
> high.unemp.idx <- sample(high.unemp.idx,)
> bias.unemp <- unemp[-high.unemp.idx]

Análise Prática de Séries Temporais | 7


## R
> all.dates <- seq(from = unemp$DATE[1], to = tail(unemp$DATE, 1),
by = "months")
> rand.unemp = rand.unemp[J(all.dates), roll=0]
> bias.unemp = bias.unemp[J(all.dates), roll=0]
> rand.unemp[, rpt := is.na(UNRATE)]
## Aqui, rotulamos os dados ausentes para plotá-los mais facilmente.

Rolling Joins
## R
> ## Temos um pequeno data.table das datas de doação.
> donations <- data.table(
> amt = c(99, 100, 5, 15, 11, 1200),
> dt = as.Date(c("2019-2-27", "2019-3-2", "2019-6-13",
> "2019-8-1", "2019-8-31", "2019-9-15"))
> )

> ## Temos também informações sobre as datas


> ## de cada campanha publicitária.
> publicity <- data.table(
> identifier = c("q4q42", "4299hj", "bbg2"),
> dt = as.Date(c("2019-1-1",
> "2019-4-1",
> "2019-7-1")))

> ## Definimos a chave primária em cada data.table.


> setkey(publicity, "dt")
> setkey(donations, "dt")

> ## Queremos rotular cada doação de acordo com a campanha publicitária


> ## que mais recentemente a precedeu.
> ### Podemos ver isso facilmente com roll = TRUE
> publicity[donations, roll = TRUE]

## R
> publicity[donations, roll = TRUE]
identifier dt amt
1: q4q42 2019-02-27 99
2: q4q42 2019-03-02 100
3: 4299hj 2019-06-13 5
4: bbg2 2019-08-01 15
5: bbg2 2019-08-31 11
6: bbg2 2019-09-15 1200

## R
> rand.unemp[, impute.ff := na.locf(UNRATE, na.rm = FALSE)]
> bias.unemp[, impute.ff := na.locf(UNRATE, na.rm = FALSE)]
>
> ## Para plotar um gráfico de amostra que mostra as partes achatadas.
> unemp[350:400, plot (DATE, UNRATE,
col = 1, lwd = 2, type = 'b')]
> rand.unemp[350:400, lines(DATE, impute.ff,
col = 2, lwd = 2, lty = 2)]
> rand.unemp[350:400][rpt == TRUE, points(DATE, impute.ff,
col = 2, pch = 6, cex = 2)]

8 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


Figura 2-6. Série temporal original plotada com uma linha sólida e a série temporal com
valores preenchidos pelo método foward fill para pontos aleatoriamente ausentes plotados
com uma linha tracejada. Os valores preenchidos com o foward fill são marcados por meio
de triângulos com vértices para baixo.

Figura 2-7. Plotando a taxa de desemprego real versus uma série que usou o foward fill. Esse
gráfico mostra que o forward fill não distorceu sistematicamente os dados.

## R
> ## média móvel sem lookahead
> rand.unemp[, impute.rm.nolookahead := rollapply(c(NA, NA, UNRATE), 3,
> function(x) {
> if (!is.na(x[3])) x[3] else mean(x, na.rm = TRUE)
> })]
> bias.unemp[, impute.rm.nolookahead := rollapply(c(NA, NA, UNRATE), 3,
> function(x) {
> if (!is.na(x[3])) x[3] else mean(x, na.rm = TRUE)
> })]

Análise Prática de Séries Temporais | 9


## R
> ## média móvel com lookahead
> rand.unemp[, complete.rm := rollapply(c(NA, UNRATE, NA), 3,
> function(x) {
> if (!is.na(x[2]))
> x[2]
> else
> mean(x, na.rm = TRUE)
> })]

Figura 2-8. A linha pontilhada mostra a


imputação da média móvel sem lookahead,
enquanto a linha tracejada mostra a
imputação da média móvel com lookahead.
Da mesma forma, os quadrados mostram os
pontos imputados sem lookahead, ao passo
que os triângulos com vértices para baixo
mostram a média móvel com um lookahead.
## R
> ## Interpolação linear
> rand.unemp[, impute.li := na.approx(UNRATE)]
> bias.unemp[, impute.li := na.approx(UNRATE)]
>
> ## Interpolação polinomial
> rand.unemp[, impute.sp := na.spline(UNRATE)]
> bias.unemp[, impute.sp := na.spline(UNRATE)]
>
> use.idx = 90:120
> unemp[use.idx, plot(DATE, UNRATE, col = 1, type = 'b')]
> rand.unemp[use.idx, lines(DATE, impute.li, col = 2, lwd = 2, lty = 2)]
> rand.unemp[use.idx, lines(DATE, impute.sp, col = 3, lwd = 2, lty = 3)]

10 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


Figura 2-9. A linha tracejada mostra a interpolação linear, enquanto a linha pontilhada
mostra a interpolação por splines.

## R
> sort(rand.unemp[ , lapply(.SD, function(x) mean((x - unemp$UNRATE)^2,
> na.rm = TRUE)),
> .SDcols = c("impute.ff", "impute.rm.lookahead",
> "impute.rm.nolookahead", "impute.li",
> "impute.sp")])
impute.li impute.rm.lookahead impute.sp impute.ff impute.rm.nolookahead
0.0017 0.0019 0.0021 0.0056 0.0080

> sort(bias.unemp[ , lapply(.SD, function(x) mean((x - unemp$UNRATE)^2,


> na.rm = TRUE)),
> .SDcols = c("impute.ff", "impute.rm.lookahead",
> "impute.rm.nolookahead", "impute.li",
> "impute.sp")])
impute.sp impute.li impute.rm.lookahead impute.rm.nolookahead impute.ff
0.0012 0.0013 0.0017 0.0030 0.0052

Análise Prática de Séries Temporais | 11


## R
> unemp[seq.int(from = 1, to = nrow(unemp), by = 12)]
DATE UNRATE
1948-01-01 3.4
1949-01-01 4.3
1950-01-01 6.5
1951-01-01 3.7
1952-01-01 3.2
1953-01-01 2.9
1954-01-01 4.9
1955-01-01 4.9
1956-01-01 4.0
1957-01-01 4.2

## R
> unemp[, mean(UNRATE), by = format(DATE, "%Y")]
format V1
1948 3.75
1949 6.05
1950 5.21
1951 3.28
1952 3.03
1953 2.93
1954 5.59
1955 4.37
1956 4.13
1957 4.30

## R
> all.dates <- seq(from = unemp$DATE[1], to = tail(unemp$DATE, 1),
> by = "months")
> rand.unemp = rand.unemp[J(all.dates), roll=0]

## R
> daily.unemployment = unemp[J(all.dates), roll = 31]
> daily.unemployment
DATE UNRATE
1948-01-01 3.4
1948-01-02 3.4
1948-01-03 3.4
1948-01-04 3.4
1948-01-05 3.4

## python
>>> air
Date Passengers
0 1949-01 112
1 1949-02 118
2 1949-03 132
3 1949-04 129
4 1949-05 121
5 1949-06 135
6 1949-07 148
7 1949-08 148
8 1949-09 136
9 1949-10 119
10 1949-11 104

12 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


## python
>>> air['Smooth.5'] = pd.ewma(air, alpha = .5).Passengers
>>> air['Smooth.9'] = pd.ewma(air, alpha = .9).Passengers

## python
>>> air
Date Passengers Smooth.5 Smooth.9
0 1949-01 112 112.000000 112.000000
1 1949-02 118 116.000000 117.454545
2 1949-03 132 125.142857 130.558559
3 1949-04 129 127.200000 129.155716
4 1949-05 121 124.000000 121.815498
5 1949-06 135 129.587302 133.681562
6 1949-07 148 138.866142 146.568157
7 1949-08 148 143.450980 147.856816
8 1949-09 136 139.718200 137.185682
9 1949-10 119 129.348974 120.818568
10 1949-11 104 116.668295 105.681857

Figura 2-10. O
aumento da média
e da variância dos
dados são aparentes
em um gráfico de
dispersão, mas não
vemos uma tendência
sazonal óbvia.

Análise Prática de Séries Temporais | 13


Figura 2-11. Um
gráfico linear
mostra claramente a
sazonalidade.
## R
> plot(stl(AirPassengers, "periodic"))

Figura 2-12. Uma decomposição


da série temporal original em
um componente sazonal, uma
tendência e os resíduos. Preste
atenção a cada eixo y do gráfico,
pois eles são muito diferentes.
Repare que esse é o motivo das
barras cinzas no lado direito de
cada gráfico. Elas apresentam o
mesmo tamanho absoluto (em
unidades do eixo y), de modo que
sua exibição relativamente diferente
é um lembrete visual das diferentes
escalas do eixo y em diferentes
componentes.

14 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


## python
>>> datetime.datetime.utcnow()
datetime.datetime(2018, 5, 31, 14, 49, 43, 187680)

>>> datetime.datetime.now()
datetime.datetime(2018, 5, 31, 10, 49, 59, 984947)
>>> # Como podemos ver, minha máquina não retorna UTC,
>>> # embora não haja fuso horário anexado.

>>> datetime.datetime.now(datetime.timezone.utc)
datetime.datetime(2018, 5, 31, 14, 51, 35, 601355,
tzinfo=datetime.timezone.utc)

## python
>>> western = pytz.timezone('US/Pacific')
>>> western.zone
'US/Pacific'

## python
>>> ## A API oferece suporte a duas maneiras de construir um horário
>>> ## consciente do fuso horário, seja por meio de 'localize' ou
>>> ## para converter um fuso horário de um local em outro.
>>> # Aqui, usei o localize
>>> loc_dt = western.localize(datetime.datetime(2018, 5, 15, 12, 34, 0))
datetime.datetime(2018, 5, 15, 12, 34,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)

## python
>>> london_tz = pytz.timezone('Europe/London')
>>> london_dt = loc_dt.astimezone(london_tz)

>>> london_dt
datetime.datetime(2018, 5, 15, 20, 34,
tzinfo=<DstTzInfo 'Europe/London' BST+1:00:00 DST>)

>>> f = '%Y-%m-%d %H:%M:%S %Z%z'


>>> datetime.datetime(2018, 5, 12, 12, 15, 0,
tzinfo = london_tz).strftime(f)
'2018-05-12 12:15:00 LMT-0001'

>>> ## Como destacado na documentação, o pytz usando o tzinfo do inicializador


>>> ## datetime.datetime nem sempre leva ao resultado desejado,
>>> ## como no exemplo de Londres.

>>> ## De acordo com a documentação do pytz, esse método leva aos resultados
>>> ## desejados em fusos horários sem horário de verão.

Análise Prática de Séries Temporais | 15


## python
>>> # Em geral, você quer armazenar dados em UTC
>>> # e converter apenas ao gerar saída legível para usuários.
>>> # você também pode calcular a data com fusos horários.
>>> event1 = datetime.datetime(2018, 5, 12, 12, 15, 0,
tzinfo = london_tz)
>>> event2 = datetime.datetime(2018, 5, 13, 9, 15, 0,
tzinfo = western)
>>> event2 - event1
>>> ## Isso resultará no time delta errado,
>>> ## porque os fusos horários não foram rotulados corretamente.

>>> event1 = london_tz.localize(


datetime.datetime(2018, 5, 12, 12, 15, 0))
>>> event2 = western.localize(
datetime.datetime(2018, 5, 13, 9, 15, 0))
>>> event2 - event1

>>> event1 = london_tz.localize(


(datetime.datetime(2018, 5, 12, 12, 15, 0))).
astimezone(datetime.timezone.utc)
>>> event2 = western.localize(
datetime.datetime(2018, 5, 13, 9, 15, 0)).
astimezone(datetime.timezone.utc)
>>> event2 - event1

## python
## Dê uma olhada no pytz.common_timezones
>>> pytz.common_timezones
(long output...)

## Ou países específicos
>>> pytz.country_timezones('RU')
['Europe/Kaliningrad', 'Europe/Moscow', 'Europe/Simferopol',
'Europe/Volgograd', 'Europe/Kirov', 'Europe/Astrakhan',
'Europe/Saratov', 'Europe/Ulyanovsk', 'Europe/Samara',
'Asia/Yekaterinburg', 'Asia/Omsk', 'Asia/Novosibirsk',
'Asia/Barnaul', 'Asia/Tomsk', 'Asia/Novokuznetsk',
'Asia/Krasnoyarsk', 'Asia/Irkutsk', 'Asia/Chita',
'Asia/Yakutsk', 'Asia/Khandyga', 'Asia/Vladivostok',
'Asia/Ust-Nera', 'Asia/Magadan', 'Asia/Sakhalin',
'Asia/Srednekolymsk', 'Asia/Kamchatka', 'Asia/Anadyr']
>>>
>>> pytz.country_timezones('fr')
>>> ['Europe/Paris']

16 | Capítulo 2: Encontrando Dados e Usando o Data Wrangling com Séries Temporais


## python
>>> ## Fusos horários
>>> ambig_time = western.localize(
datetime.datetime(2002, 10, 27, 1, 30, 00)).
astimezone(datetime.timezone.utc)
>>> ambig_time_earlier = ambig_time - datetime.timedelta(hours=1)
>>> ambig_time_later = ambig_time + datetime.timedelta(hours=1)
>>> ambig_time_earlier.astimezone(western)
>>> ambig_time.astimezone(western)
>>> ambig_time_later.astimezone(western)

>>> #Resultados nesta saída


datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
datetime.datetime(2002, 10, 27, 2, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
>>> # Veja que os dois últimos timestamps são idênticos, nada bom!

>>> ## Nesse caso, você precisa usar is_dst para indicar


>>> ## se o horário de verão está em vigor.

>>> ambig_time = western.localize(


datetime.datetime(2002, 10, 27, 1, 30, 00), is_dst = True).
astimezone(datetime.timezone.utc)
>>> ambig_time_earlier = ambig_time - datetime.timedelta(hours=1)
>>> ambig_time_later = ambig_time + datetime.timedelta(hours=1)
>>> ambig_time_earlier.astimezone(western)
>>> ambig_time.astimezone(western)
>>> ambig_time_later.astimezone(western)

datetime.datetime(2002, 10, 27, 0, 30,


tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
## Veja que agora não temos o mesmo tempo acontecendo duas vezes.
## Pode parecer assim até que você verifique o deslocamento do UTC.

Análise Prática de Séries Temporais | 17


CAPÍTULO 3
Análise Exploratória de Dados
para Séries Temporais

## R
> head(EuStockMarkets)
DAX SMI CAC FTSE
[1,] 1628.75 1678.1 1772.8 2443.6
[2,] 1613.63 1688.5 1750.5 2460.2

[3,] 1606.51 1678.6 1718.0 2448.2


[4,] 1621.04 1684.1 1708.1 2470.4
[5,] 1618.16 1686.6 1723.1 2484.7
[6,] 1610.61 1671.6 1714.3 2466.8

## R
> plot(EuStockMarkets)
Figura 3-1. Um gráfico simples dos dados das séries temporais.

## R
> class(EuStockMarkets)
[1] "mts" "ts" "matrix"

## R
> frequency(EuStockMarkets)
[1] 260

## R
> start(EuStockMarkets)
[1] 1991 130
> end(EuStockMarkets)
1] 1998 169

## R
> window(EuStockMarkets, start = 1997, end = 1998)
Time Series:
Start = c(1997, 1)
End = c(1998, 1)
Frequency = 260 DAX SMI CAC FTSE
1997.000 2844.09 3869.8 2289.6 4092.5
1997.004 2844.09 3869.8 2289.6 4092.5
1997.008 2844.09 3869.8 2303.8 4092.5
...
1997.988 4162.92 6115.1 2894.5 5168.3
1997.992 4055.35 5989.9 2822.9 5020.2
1997.996 4125.54 6049.3 2869.7 5018.2
1998.000 4132.79 6044.7 2858.1 5049.8

Análise Prática de Séries Temporais | 19


## R
> hist( EuStockMarkets[, "SMI"], 30)
> hist(diff(EuStockMarkets[, "SMI"], 30))

Figura 3-2. O histograma dos dados não transformados (parte superior) é bastante amplo
e não mostra uma distribuição normal. Isso é esperado, visto que os dados subjacentes
apresentam uma tendência. Diferenciamos os dados para remover a tendência e isso
transforma os dados até uma forma de distribuição mais normal (parte inferior).

## R
> plot( EuStockMarkets[, "SMI"], EuStockMarkets[, "DAX"])
> plot(diff(EuStockMarkets[, "SMI"]), diff(EuStockMarkets[, "DAX"]))

20 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-3. Esses gráficos de dispersão simples de dois índices de ações sugerem fortes
correlações. No entanto, existem razões para desconfiarmos deles.

## R
> plot(lag(diff(EuStockMarkets[, "SMI"]), 1),
diff(EuStockMarkets[, "DAX"]))

Figura 3-4. A correlação entre as ações desaparece assim que inserimos um lag de tempo,
indicando que o SMI [Índice de Bolsa de Valores Suíço] não parece predizer o DAX [Índice
de Ações Alemão].

Análise Prática de Séries Temporais | 21


Figura 3-5. O conjunto de dados dos passageiros das companhias aéreas oferece um exemplo
claro de uma série temporal não estacionária. Com o passar do tempo, tanto a média quanto
a variância dos dados estão mudando. Vemos também evidências de sazonalidade refletindo
intrinsecamente um processo não estacionário.

## R
> ## Calcula uma média móvel usando a base R.
> ## Função de filtro.
> x <- rnorm(n = 100, mean = 0, sd = 10) + 1:100
> mn <- function(n) rep(1/n, n)

> plot(x, type = 'l', lwd = 1)


> lines(filter(x, mn( 5)), col = 2, lwd = 3, lty = 2)
> lines(filter(x, mn(50)), col = 3, lwd = 3, lty = 3)

22 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-6. Duas curvas exploratórias geradas por meio da suavização de média móvel.
Podemos usá-las para procurar uma tendência em dados com ruído ou para decidir que
tipos de desvios do comportamento linear são interessantes para investigar versus quais são
provavelmente um ruído.

## R
> ## Você também pode escrever funções customizadas
> require(zoo)

> f1 <- rollapply(zoo(x), 20, function(w) min(w),


> align = "left", partial = TRUE)
> f2 <- rollapply(zoo(x), 20, function(w) min(w),
> align = "right", partial = TRUE)

> plot (x, lwd = 1, type = 'l')


> lines(f1, col = 2, lwd = 3, lty = 2)
> lines(f2, col = 3, lwd = 3, lty = 3)

Análise Prática de Séries Temporais | 23


Figura 3-7. O mínimo possível de janela rolante alinhada à esquerda (traços longos) ou à
direita (traços curtos). O alinhamento à esquerda prevê eventos no futuro, enquanto o direito
analisa apenas eventos no passado. É importante observar isso para evitar o lookahead.
No entanto, às vezes um alinhamento à esquerda pode ser útil para fazer perguntas
exploratórias, como “se eu soubesse disso com antecedência, me ajudaria?”. Não raro, nem
um lookahead informa muita coisa, o que significa que uma determinada variável não é
informativa. Quando se usa uma medição que não ajuda em nada para saber o futuro, ela
não é uma medição que serve para alguma coisa em nenhum tempo de uma série temporal.

## R
> # expanding windows/janela de expansão.
> plot(x, type = 'l', lwd = 1)
> lines(cummax(x), col = 2, lwd = 3, lty = 2) # max
> lines(cumsum(x)/1:length(x), col = 3, lwd = 3, lty = 3) # mean

Figura 3-8. Uma janela de expansão “max” (traços longos) e uma janela de expansão “mean”
(traços curtos). O uso de uma janela de expansão significa que o max sempre reflete o máximo
global até aquele tempo, tornando-a uma função monotônica. Graças à extensa “memória”

24 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


da janela de expansão, sua média é menor do que a média móvel (Figura 3-7), porque a
tendência subjacente é menos proeminente na média de expansão. Agora, se isso é bom, ruim
ou imparcial, depende de nossas suposições e nossos conhecimentos do sistema subjacente.

## R
> plot(x, type = 'l', lwd = 1)
> lines(rollapply(zoo(x), seq_along(x), function(w) max(w),
> partial = TRUE, align = "right"),
> col = 2, lwd = 3, lty = 2)
> lines(rollapply(zoo(x), seq_along(x), function(w) mean(w),
> partial = TRUE, align = "right"),
> col = 3, lwd = 3, lty = 3)

## R
> x <- 1:100
> y <- sin(x × pi /3)
> plot(y, type = "b")
> acf(y)

Figura 3-9. Gráfico de uma função seno e sua ACF.

## R
> cor(y, shift(y, 1), use = "pairwise.complete.obs")
[1] 0.5000015
> cor(y, shift(y, 2), use = "pairwise.complete.obs")
[1] -0.5003747

Análise Prática de Séries Temporais | 25


## R
> y <- sin(x × pi /3)
> plot(y[1:30], type = "b")
> pacf(y)

Figura 3-10. Plotagem e PACF de um processo sazonal sem ruído.

## R
> y1 <- sin(x × pi /3)
> plot(y1, type = "b")
> acf (y1)
> pacf(y1)

> y2 <- sin(x × pi /10)


> plot(y2, type = "b")
> acf (y2)
> pacf(y2)

26 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-11. Gráficos de duas funções senoidais, com suas funções ACF e PACF,
respectivamente.

## R
> y <- y1 + y2
> plot(y, type = "b")
> acf (y)
> pacf(y)

Análise Prática de Séries Temporais | 27


Figura 3-12. Gráfico e funções ACF e PACF para a soma das duas séries senoidais.

## R
> noise1 <- rnorm(100, sd = 0.05)
> noise2 <- rnorm(100, sd = 0.05)

> y1 <- y1 + noise1


> y2 <- y2 + noise2
> y <- y1 + y2

> plot(y1, type = 'b')


> acf (y1)
> pacf(y1)

> plot(y2, type = 'b')


> acf (y2)
> pacf(y2)

> plot(y, type = 'b')


> acf (y)
> pacf(y)

28 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-13. Gráficos de funções ACF e PACF de dois processos senoidais ruidosos e suas
respectivas somas.

Análise Prática de Séries Temporais | 29


Figura 3-14. Gráficos e funções ACF e PACF relacionados à soma com muito ruído de dois
processos senoidais.

30 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Dados Não Estacionários
## R
> x <- 1:100
> plot(x)
> acf (x)
> pacf(x)

Figura 3-15. Gráficos e funções ACF e PACF de um processo de tendência linear.

Análise Prática de Séries Temporais | 31


Figura 3-16. Gráficos das funções ACF e PACF dos dados do AirPassengers. Veja que aqui
os lags não são números unitários, pois são expressos como frações de um ano. Isso ocorre
porque o conjunto de dados AirPassengers assume a forma de um objeto ts, que tem uma
frequência integrada usada para plotagem (e para outras finalidades).

Figura 3-17. Algumas correlações espúrias podem parecer bastante convincentes. Esse gráfico
foi retirado do site de Tyler Vigen (https://perma.cc/6UYH-FPBX) de correlações espúrias
[Número de morte por afogamento em piscinas correlacionado com a aparição de
Nicolas Cage em filmes].

32 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


## R
> require(timevis)
> donations <- fread("donations.csv")
> d <- donations[, .(min(timestamp), max(timestamp)), user]
> names(d) <- c("content", "start", "end")
> d <- d[start != end]
> timevis(d[sample(1:nrow(d), 20)])

Figura 3-18. Esse gráfico de Gantt de uma amostra aleatória de dados pode nos dar uma
ideia da distribuição das faixas de períodos de tempo “ativos” para os usuários/doadores.

## R
> t(matrix(AirPassengers, nrow = 12, ncol = 12))
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
[1,] 112 118 132 129 121 135 148 148 136 119 104 118
[2,] 115 126 141 135 125 149 170 170 158 133 114 140
[3,] 145 150 178 163 172 178 199 199 184 162 146 166
[4,] 171 180 193 181 183 218 230 242 209 191 172 194
[5,] 196 196 236 235 229 243 264 272 237 211 180 201
[6,] 204 188 235 227 234 264 302 293 259 229 203 229
[7,] 242 233 267 269 270 315 364 347 312 274 237 278
[8,] 284 277 317 313 318 374 413 405 355 306 271 306
[9,] 315 301 356 348 355 422 465 467 404 347 305 336
[10,] 340 318 362 348 363 435 491 505 404 359 310 337
[11,] 360 342 406 396 420 472 548 559 463 407 362 405
[12,] 417 391 419 461 472 535 622 606 508 461 390 432

Análise Prática de Séries Temporais | 33


## R
> colors <- c("green", "red", "pink", "blue",
> "yellow","lightsalmon", "black", "gray",
> "cyan", "lightblue", "maroon", "purple")
> matplot(matrix(AirPassengers, nrow = 12, ncol = 12),
> type = 'l', col = colors, lty = 1, lwd = 2.5,
> xaxt = "n", ylab = "Passenger Count")
> legend("topleft", legend = 1949:1960, lty = 1, lwd = 2.5,
> col = colors)
> axis(1, at = 1:12, labels = c("Jan", "Feb", "Mar", "Apr",
> "May", "Jun", "Jul", "Aug",
> "Sep", "Oct", "Nov", "Dec"))

Figura 3-19. Contagem mês a mês por ano1.

1 Visite o GitHub (https://github.com/PracticalTimeSeriesAnalysis/BookRepo) para visualizar a figura original


ou faça um gráfico você mesmo para obter uma aparência mais detalhada.

34 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-20. Geramos um gráfico sazonal semelhante e com maior facilidade ao usar
a função seasonplot().

## R
> months <- c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
> "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

> matplot(t(matrix(AirPassengers, nrow = 12, ncol = 12)),


> type = 'l', col = colors, lty = 1, lwd = 2.5)
> legend("left", legend = months,
> col = colors, lty = 1, lwd = 2.5)

Análise Prática de Séries Temporais | 35


Figura 3-21. Curvas mensais de séries temporais ano a ano2.

## R
> monthplot(AirPassengers)

Figura 3-22. Ao usar a função monthplot(), podemos ver como o desempenho mensal muda
com o passar dos anos.

2 Visite o GitHub (https://github.com/PracticalTimeSeriesAnalysis/BookRepo) para visualizar a figura original


ou faça um gráfico você mesmo para obter uma aparência mais detalhada.

36 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


## R
> hist2d <- function(data, nbins.y, xlabels) {
> ## Fazemos ybins com espaçamento uniforme
> ## para incluir pontos mínimos e máximos.
> ymin = min(data)
> ymax = max(data) × 1.0001
> ## Saída lazy para evitar a preocupação com inclusão/exclusão.

> ybins = seq(from = ymin, to = ymax, length.out = nbins.y + 1 )

> Faz uma matriz zero do tamanho apropriado.


> hist.matrix = matrix(0, nrow = nbins.y, ncol = ncol(data))

> ## Os dados vêm em forma de matriz,


> ## em que cada linha representa um ponto de dados.
> for (i in 1:nrow(data)) {
> ts = findInterval(data[i, ], ybins)
> for (j in 1:ncol(data)) {
> hist.matrix[ts[j], j] = hist.matrix[ts[j], j] + 1
> }
> }
> hist.matrix
> }

## R
> h = hist2d(t(matrix(AirPassengers, nrow = 12, ncol = 12)), 5, months)
> image(1:ncol(h), 1:nrow(h), t(h), col = heat.colors(5),
> axes = FALSE, xlab = "Time", ylab = "Passenger Count")

Figura 3-23. Mapa de calor construído a partir de nosso histograma 2D manual dos dados
do AirPassenger.

Análise Prática de Séries Temporais | 37


## R
> require(data.table)

> words <- fread(url.str)


> w1 <- words[V1 == 1]

> h = hist2d(w1, 25, 1:ncol(w1))

> colors <- gray.colors(20, start = 1, end = .5)


> par(mfrow = c(1, 2))
> image(1:ncol(h), 1:nrow(h), t(h),
> col = colors, axes = FALSE, xlab = "Time", ylab = "Projection Value")
> image(1:ncol(h), 1:nrow(h), t(log(h)),
> col = colors, axes = FALSE, xlab = "Time", ylab = "Projection Value")

Figura 3-24. Histograma bidimensional de uma métrica de áudio para uma única palavra.
O gráfico à esquerda tem uma escala de contagem linear, enquanto o gráfico à direita tem
uma escala de contagem de transformação log.

## R
> w1 <- words[V1 == 1]

> ## Uso da função melt nos dados para os pares de coordenadas


> ## emparelhadas esperadas pela maioria das implementações de histograma 2D.
> names(w1) <- c("type", 1:270)
> w1 <- melt(w1, id.vars = "type")

> w1 <- w1[, -1]


> names(w1) <- c("Time point", "Value")

> plot(hexbin(w1))

38 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


Figura 3-25. Uma visualização alternativa de histograma 2D dos mesmos dados.

## R
> require(plotly)
> require(data.table)

> months = 1:12


> ap = data.table(matrix(AirPassengers, nrow = 12, ncol = 12))
> names(ap) = as.character(1949:1960)
> ap[, month := months]
> ap = melt(ap, id.vars = 'month')
> names(ap) = c("month", "year", "count")

> p <- plot_ly(ap, x = ~month, y = ~year, z = ~count,


> color = ~as.factor(month)) %>%
> add_markers() %>%
> layout(scene = list(xaxis = list(title = 'Month'),
> yaxis = list(title = 'Year'),
> zaxis = list(title = 'PassengerCount')))

Análise Prática de Séries Temporais | 39


Figura 3-26. Um gráfico de dispersão 3D dos dados do AirPassenger. Essa perspectiva destaca
a sazonalidade .
3

Figura 3-27. Outra perspectiva sobre os mesmos dados ilustra com mais clareza a tendência
crescente de um ano para o outro. Eu recomendo muito que execute o código em seu próprio
computador, no qual poderá rotacioná-lo você mesmo4.

3 Visite o GitHub (https://github.com/PracticalTimeSeriesAnalysis/BookRepo) para visualizar a figura original


ou faça um gráfico você mesmo para obter uma aparência mais detalhada.
4 Visite o GitHub (https://github.com/PracticalTimeSeriesAnalysis/BookRepo) para visualizar a figura original
ou faça um gráfico você mesmo para obter uma aparência mais detalhada.

40 | Capítulo 3: Análise Exploratória de Dados para Séries Temporais


## R
> file.location <- 'https://raw.githubusercontent.com/plotly/datasets/master/\
_3d-line-plot.csv'
> data <- read.csv(file.location)
> p <- plot_ly(data, x = ~x1, y = ~y1, z = ~z1,
> type = 'scatter3d', mode = 'lines',
> line = list(color = '#1f77b4', width = 1))

Figura 3-28. Uma perspectiva em um passeio aleatório bidimensional ao longo do tempo.

Figura 3-29. Essa perspectiva do mesmo passeio aleatório é mais reveladora. Novamente, o
incentivo a testar esse código!

Análise Prática de Séries Temporais | 41


CAPÍTULO 4
Simulando Dados de Séries Temporais

## python
>>> ## membership status
>>> years = ['2014', '2015', '2016', '2017', '2018']
>>> memberStatus = ['bronze', 'silver', 'gold', 'inactive']

>>> memberYears = np.random.choice(years, 1000,


>>> p = [0.1, 0.1, 0.15, 0.30, 0.35])
>>> memberStats = np.random.choice(memberStatus, 1000,
>>> p = [0.5, 0.3, 0.1, 0.1])

>>> yearJoined = pd.DataFrame({'yearJoined': memberYears,


>>> 'memberStats': memberStats})
## python
>>> NUM_EMAILS_SENT_WEEKLY = 3

>>> ## Definimos várias funções para diferentes padrões.


>>> def never_opens(period_rng):
>>> return []

>>> def constant_open_rate(period_rng):


>>> n, p = NUM_EMAILS_SENT_WEEKLY, np.random.uniform(0, 1)
>>> num_opened = np.random.binomial(n, p, len(period_rng))
>>> return num_opened

>>> def increasing_open_rate(period_rng):


>>> return open_rate_with_factor_change(period_rng,
>>> np.random.uniform(1.01,
>>> 1.30))

>>> def decreasing_open_rate(period_rng):


>>> return open_rate_with_factor_change(period_rng,
>>> np.random.uniform(0.5,
>>> 0.99))

>>> def open_rate_with_factor_change(period_rng, fac):


>>> if len(period_rng) < 1 :
>>> return []
>>> times = np.random.randint(0, len(period_rng),
>>> int(0.1 * len(period_rng)))
>>> num_opened = np.zeros(len(period_rng))
>>> for prd in range(0, len(period_rng), 2):
>>> try:
>>> n, p = NUM_EMAILS_SENT_WEEKLY, np.random.uniform(0,
>>> 1)
>>> num_opened[prd:(prd + 2)] = np.random.binomial(n, p,
>>> 2)
>>> p = max(min(1, p * fac), 0)
>>> except:
>>> num_opened[prd] = np.random.binomial(n, p, 1)
>>> for t in range(len(times)):
>>> num_opened[times[t]] = 0
>>> return num_opened

Análise Prática de Séries Temporais | 43


## python
>>> ## Comportamento de doação.
>>> def produce_donations(period_rng, member_behavior, num_emails,
>>> use_id, member_join_year):
>>> donation_amounts = np.array([0, 25, 50, 75, 100, 250, 500,
>>> 1000, 1500, 2000])
>>> member_has = np.random.choice(donation_amounts)
>>> email_fraction = num_emails /
>>> (NUM_EMAILS_SENT_WEEKLY * len(period_rng))
>>> member_gives = member_has * email_fraction
>>> member_gives_idx = np.where(member_gives
>>> >= donation_amounts)[0][-1]
>>> member_gives_idx = max(min(member_gives_idx,
>>> len(donation_amounts) - 2),
>>> 1)
>>> num_times_gave = np.random.poisson(2) *
>>> (2018 - member_join_year)
>>> times = np.random.randint(0, len(period_rng), num_times_gave)
>>> dons = pd.DataFrame({'member' : [],
>>> 'amount' : [],
>>> 'timestamp': []})

>>> for n in range(num_times_gave):


>>> donation = donation_amounts[member_gives_idx
>>> + np.random.binomial(1, .3)]
>>> ts = str(period_rng[times[n]].start_time
>>> + random_weekly_time_delta())
>>> dons = dons.append(pd.DataFrame(
>>> {'member' : [use_id],
>>> 'amount' : [donation],
>>> 'timestamp': [ts]}))
>>>
>>> if dons.shape[0] > 0:
>>> dons = dons[dons.amount != 0]
>>> ## Não relatamos a ausência de evento de doação,
>>> ## pois isso não seria registrado em um banco de dados do mundo real.
>>>
>>> return dons

## python
>>> def random_weekly_time_delta():
>>> days_of_week = [d for d in range(7)]
>>> hours_of_day = [h for h in range(11, 23)]
>>> minute_of_hour = [m for m in range(60)]
>>> second_of_minute = [s for s in range(60)]
>>> return pd.Timedelta(str(np.random.choice(days_of_week))
>>> + " days" ) +
>>> pd.Timedelta(str(np.random.choice(hours_of_day))
>>> + " hours" ) +
>>> pd.Timedelta(str(np.random.choice(minute_of_hour))
>>> + " minutes") +
>>> pd.Timedelta(str(np.random.choice(second_of_minute))
>>> + " seconds")

44 | Capítulo 4: Simulando Dados de Séries Temporais


## python
>>> behaviors = [never_opens,
>>> constant_open_rate,
>>> increasing_open_rate,
>>> decreasing_open_rate]
>>> member_behaviors = np.random.choice(behaviors, 1000,
>>> [0.2, 0.5, 0.1, 0.2])

>>> rng = pd.period_range('2015-02-14', '2018-06-01', freq = 'W')


>>> emails = pd.DataFrame({'member' : [],
>>> 'week' : [],
>>> 'emailsOpened': []})
>>> donations = pd.DataFrame({'member' : [],
>>> 'amount' : [],
>>> 'timestamp': []})

>>> for idx in range(yearJoined.shape[0]):


>>> ## Gera aleatoriamente a data em que um membro teria se afiliado.
>>> join_date = pd.Timestamp(yearJoined.iloc[idx].yearJoined) +
>>> pd.Timedelta(str(np.random.randint(0, 365)) +
>>> ' days')
>>> join_date = min(join_date, pd.Timestamp('2018-06-01'))
>>>
>>> ## Membro não deve ter timestamps vigentes antes de se afiliar.
>>> member_rng = rng[rng > join_date]
>>>
>>> if len(member_rng) < 1:
>>> continue
>>>
>>> info = member_behaviors[idx](member_rng)
>>> if len(info) == len(member_rng):
>>> emails = emails.append(pd.DataFrame(
>>> {'member': [idx] * len(info),
>>> 'week': [str(r.start_time) for r in member_rng],
>>> 'emailsOpened': info}))
>>> donations = donations.append(
>>> produce_donations(member_rng, member_behaviors[idx],
>>> sum(info), idx, join_date.year))

## python
>>> df.set_index(pd.to_datetime(df.timestamp), inplace = True)
>>> df.sort_index(inplace = True)
>>> df.groupby(pd.Grouper(freq='M')).amount.sum().plot()

Análise Prática de Séries Temporais | 45


Figura 4-1. Soma total das doações recebidas para cada mês do conjunto de dados.

## python
>>> import numpy as np

>>> def taxi_id_number(num_taxis):


>>> arr = np.arange(num_taxis)
>>> np.random.shuffle(arr)
>>> for i in range(num_taxis):
>>> yield arr[i]

## python
>>> ids = taxi_id_number(10)
>>> print(next(ids))
>>> print(next(ids))
>>> print(next(ids))

7
2
5

## python
>>> def shift_info():
>>> start_times_and_freqs = [(0, 8), (8, 30), (16, 15)]
>>> indices = np.arange(len(start_times_and_freqs))
>>> while True:
>>> idx = np.random.choice(indices, p = [0.25, 0.5, 0.25])
>>> start = start_times_and_freqs[idx]
>>> yield (start[0], start[0] + 7.5, start[1])

46 | Capítulo 4: Simulando Dados de Séries Temporais


## python
>>> def taxi_process(taxi_id_generator, shift_info_generator):
>>> taxi_id = next(taxi_id_generator)
>>> shift_start, shift_end, shift_mean_trips =
>>> next(shift_info_generator)
>>> actual_trips = round(np.random.normal(loc = shift_mean_trips,
>>> scale = 2))
>>> average_trip_time = 6.5 / shift_mean_trips * 60
>>> # Converte tempo médio de viagem em minutos.
>>> between_events_time = 1.0 / (shift_mean_trips - 1) * 60
>>> # Esta é uma cidade eficiente na qual os táxis raramente ficam sem rodar.
>>> time = shift_start
>>> yield TimePoint(taxi_id, 'start shift', time)
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> for i in range(actual_trips):
>>> yield TimePoint(taxi_id, 'pick up ', time)
>>> deltaT = np.random.poisson(average_trip_time) / 60
>>> time += deltaT
>>> yield TimePoint(taxi_id, 'drop off ', time)
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> yield TimePoint(taxi_id, 'end shift ', time)

## python
>>> from dataclasses import dataclass

>>> @dataclass
>>> class TimePoint:
>>> taxi_id: int
>>> name: str
>>> time: float

>>> def __lt__(self, other):


>>> return self.time < other.time

## python
>>> import queue

>>> class Simulator:


>>> def __init__(self, num_taxis):
>>> self._time_points = queue.PriorityQueue()
>>> taxi_id_generator = taxi_id_number(num_taxis)
>>> shift_info_generator = shift_info()
>>> self._taxis = [taxi_process(taxi_id_generator,

Análise Prática de Séries Temporais | 47


>>> shift_info_generator) for
>>> i in range(num_taxis)]
>>> self._prepare_run()

>>> def _prepare_run(self):


>>> for t in self._taxis:
>>> while True:
>>> try:
>>> e = next(t)
>>> self._time_points.put(e)
>>> except:
>>> break

>>> def run(self):


>>> sim_time = 0
>>> while sim_time < 24:
>>> if self._time_points.empty():
>>> break
>>> p = self._time_points.get()
>>> sim_time = p.time
>>> print(p)

## python
>>> sim = Simulator(1000)
>>> sim.run()

id: 0539 name: drop off time: 23:58


id: 0318 name: pick up time: 23:58
id: 0759 name: end shift time: 23:58
id: 0977 name: pick up time: 23:58
id: 0693 name: end shift time: 23:59
id: 0085 name: end shift time: 23:59
id: 0351 name: end shift time: 23:59
id: 0036 name: end shift time: 23:59
id: 0314 name: drop off time: 23:59

## python
>>> ### CONFIGURAÇÃO
>>> ## Layout de física.
>>> N = 5 # width of lattice
>>> M = 5 # height of lattice
>>> ## Definições de temperatura.
>>> temperature = 0.5
>>> BETA = 1 / temperature

>>> def initRandState(N, M):


>>> block = np.random.choice([-1, 1], size = (N, M))
>>> return block

48 | Capítulo 4: Simulando Dados de Séries Temporais


## python
>>> def costForCenterState(state, i, j, n, m):
>>> centerS = state[i, j]
>>> neighbors = [((i + 1) % n, j), ((i - 1) % n, j),
>>> (i, (j + 1) % m), (i, (j - 1) % m)]
>>> ## Observe o % n porque impomos condições de limite periódicas.
>>> ## Ignore isso se não fizer sentido — é apenas restrição física
>>> ## no sistema, dizendo que o sistema 2D é como
>>> ## a superfície de um donut.
>>> interactionE = [state[x, y] * centerS for (x, y) in neighbors]
>>> return np.sum(interactionE)

## python
>>> def magnetizationForState(state):
>>> return np.sum(state)

## python
>>> def mcmcAdjust(state):
>>> n = state.shape[0]
>>> m = state.shape[1]
>>> x, y = np.random.randint(0, n), np.random.randint(0, m)
>>> centerS = state[x, y]
>>> cost = costForCenterState(state, x, y, n, m)
>>> if cost < 0:
>>> centerS *= -1
>>> elif np.random.random() < np.exp(-cost * BETA):
>>> centerS *= -1
>>> state[x, y] = centerS
>>> return state

## python
>>> def runState(state, n_steps, snapsteps = None):
>>> if snapsteps is None:
>>> snapsteps = np.linspace(0, n_steps, num = round(n_steps / (M * N * 100)),
>>> dtype = np.int32)
>>> saved_states = []
>>> sp = 0
>>> magnet_hist = []
>>> for i in range(n_steps):
>>> state = mcmcAdjust(state)
>>> magnet_hist.append(magnetizationForState(state))
>>> if sp < len(snapsteps) and i == snapsteps[sp]:
>>> saved_states.append(np.copy(state))
>>> sp += 1
>>> return state, saved_states, magnet_hist

## python
>>> ### EXECUTA A SIMULAÇÃO
>>> init_state = initRandState(N, M)
>>> print(init_state)
>>> final_state = runState(np.copy(init_state), 1000)

Análise Prática de Séries Temporais | 49


Figura 4-2. Estado inicial de um material ferromagnético simulado 5 × 5, inicializado
com cada estado selecionado aleatoriamente, rotacionado para cima ou para baixo com
probabilidade igual.

Figura 4-3. Estado final de baixa temperatura em nossa simulação executada, conforme
visto em 1.000 intervalos de tempo.

50 | Capítulo 4: Simulando Dados de Séries Temporais


## python
>>> Coletamos cada série temporal como um elemento separado na lista de resultados.
>>> results = []
>>> for i in range(100):
>>> init_state = initRandState(N, M)
>>> final_state, states, magnet_hist = runState(init_state, 1000)
>>> results.append(magnet_hist)
>>>
>>> ## Plotamos cada curva com um pouco de transparência,
>>> ## de modo que possamos ver as curvas que se sobrepõem.
>>> for mh in results:
>>> plt.plot(mh,'r', alpha=0.2)

Figura 4-4. Cem simulações independentes de possíveis formas pelas quais o sistema poderia
entrar em um estado magnetizado a temperatura baixa, mesmo quando cada site de lattice
original foi inicializado aleatoriamente.

Análise Prática de Séries Temporais | 51


CAPÍTULO 5
Armazenando Dados Temporais

Figura 5-1. Um recurso comum de um banco de dados desenvolvido para uso de séries
temporais é que a taxa de inserção de dados é constante em relação ao tamanho do banco
de dados. Uma vantagem surpreendente em comparação a uma solução de armazenamento
SQL tradicional.
CAPÍTULO 6
Modelos Estatísticos para Séries Temporais

Figura 6-1. Número diário de ordens bancárias (2).

53
Figura 6-2. PACF das séries temporais das ordens não transformadas exemplificadas
na Figura 6-1.

## R
> fit <- ar(demand[["Banking orders (2)"]], method = "mle")
> fit

Call:
ar(x = demand[["Banking orders (2)"]], method = "mle")
Coefficients:
1 2 3
-0.1360 -0.2014 -0.3175

## R
> est <- arima(x = demand[["Banking orders (2)"]],
> order = c(3, 0, 0))
> est

Call:
arima(x = demand[["Banking orders (2)"]], order = c(3, 0, 0))

Coefficients:
ar1 ar2 ar3 intercept
-0.1358 -0.2013 -0.3176 79075.350
s.e. 0.1299 0.1289 0.1296 2981.125

sigma^2 estimated as 1.414e+09: log likelihood = -717.42,


aic = 1444.83

54 | Capítulo 6: Modelos Estatísticos para Séries Temporais


## R
> est.1 <- arima(x = demand[["Banking orders (2)"]],
> order = c(3, 0, 0),
> fixed = c(0, NA, NA, NA))
> est.1

Call:
arima(x = demand[["Banking orders (2)"]],
order = c(3, 0, 0),
fixed = c(0, NA, NA, NA))

Coefficients:
ar1 ar2 ar3 intercept
0 -0.1831 -0.3031 79190.705
s.e. 0 0.1289 0.1298 3345.253

sigma^2 estimated as 1.44e+09: log likelihood = -717.96,


aic = 1443.91

## R
> fixed <- c(0, NA, NA, NA)

## R
> acf(est.1$residuals)

Figura 6-3. ACF dos resíduos do modelo AR (3) que ajustamos ao forçar o parâmetro
lag – 1 em 0.

Análise Prática de Séries Temporais | 55


## R
> Box.test(est.1$residuals, lag = 10, type = "Ljung", fitdf = 3)

Box-Ljung test

data: est.1$residuals
X-squared = 9.3261, df = 7, p-value = 0.2301

## R
> require(forecast)
> plot(demand[["Banking orders (2)"]], type = 'l')
> lines(fitted(est.1), col = 3, lwd = 2) ## use o pacote forecast

Figura 6-4. Aqui vemos a série temporal original na linha sólida e, na linha tracejada, a série
temporal ajustada.

56 | Capítulo 6: Modelos Estatísticos para Séries Temporais


Figura 6-5. As séries e as predições diferenciadas estão fortemente correlacionadas,
sugerindo que o modelo identificou uma relação subjacente.

## R
> fitted(est.1, h = 3)

## R
> var(fitted(est.1, h = 3), na.rm = TRUE)

[1] 174,870,141
> var(fitted(est.1, h = 5), na.rm = TRUE)
[1] 32,323,722
> var(fitted(est.1, h = 10), na.rm = TRUE)
[1] 1,013,396
> var(fitted(est.1, h = 20), na.rm = TRUE)
[1] 1,176
> var(fitted(est.1, h = 30), na.rm = TRUE)
[1] 3.5

Análise Prática de Séries Temporais | 57


Figura 6-6. Gráficos de previsões para o futuro. Os valores distribuídos pelo eixo y
se estreitam cada vez mais à medida que predizemos mais no futuro e, aparentemente,
o modelo fornece uma predição constante do valor médio do processo. Os horizontes preditos
aumentam de cima para baixo, exibindo 3, 10 e 30 intervalos de tempo, respectivamente.

## R
> acf(demand[["Banking orders (2)"]])

Figura 6-7. Usamos a ACF da série temporal de demanda para determinar a ordem do
modelo MA.

58 | Capítulo 6: Modelos Estatísticos para Séries Temporais


## R
> ma.est = arima(x = demand[["Banking orders (2)"]],
order = c(0, 0, 9),
fixed = c(0, 0, NA, rep(0, 5), NA, NA))
> ma.est
Call:
arima(x = demand[["Banking orders (2)"]], order = c(0, 0, 9),

fixed = c(0, 0, NA, rep(0, 5), NA, NA))

Coefficients:
ma1 ma2 ma3 ma4 ma5 ma6 ma7 ma8 ma9 intercept
0 0 -0.4725 0 0 0 0 0 -0.0120 79689.81
s.e. 0 0 0.1459 0 0 0 0 0 0.1444 2674.60

sigma^2 estimated as 1.4e+09: log likelihood = -717.31,


aic = 1442.61

## R
> Box.test(ma.est$residuals, lag = 10, type = "Ljung", fitdf = 3)

Box-Ljung test

data: ma.est$residuals
X-squared = 7.6516, df = 7, p-value = 0.3643

## R
> fitted(ma.est, h=1)

Time Series:
Start = 1
End = 60
Frequency = 1
[1] 90116.64 80626.91 74090.45 38321.61 74734.77 101153.20 65930.90
[8] 106351.80 104138.05 86938.99 102868.16 80502.02 81466.01 77619.15
[15] 100984.93 81463.10 61622.54 79660.81 88563.91 65370.99 104679.89
[22] 48047.39 73070.29 115034.16 80034.03 70052.29 70728.85 90437.86
[29] 80684.44 91533.59 101668.18 42273.27 93055.40 68187.65 75863.50
[36] 40195.15 82368.91 90605.60 69924.83 54032.55 90866.20 85839.41
[43] 64932.70 43030.64 85575.32 76561.14 82047.95 95683.35 66553.13
[50] 89532.20 85102.64 80937.97 93926.74 47468.84 75223.67 100887.60
[57] 92059.32 84459.85 67112.16 80917.23

## R
> fitted(ma.est, h=10)

Time Series:
Start = 1
End = 60
Frequency = 1
[1] NA NA NA NA NA NA NA NA
[9] NA NA 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[17] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[25] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[33] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[41] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81

Análise Prática de Séries Temporais | 59


[49] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[57] 79689.81 79689.81 79689.81 79689.81

## R
> require(forecast)
> set.seed(1017)
> ## ordem do modelo arima escondida de propósito
> y = arima.sim(n = 1000, list(ar = ###, ma = ###))

Figure 6-8. Gráfico de nossa série temporal.

Figura 6-9. ACF e PACF de nossa série temporal.

60 | Capítulo 6: Modelos Estatísticos para Séries Temporais


## R
> ar1.ma1.model = Arima(y, order = c(1, 0, 1))
> par(mfrow = c(2,1))
> acf(ar1.ma1.model$residuals)
> pacf(ar1.ma1.model$residuals)

Figura 6-10. ACF e PACF dos resíduos de um modelo ARIMA (1, 0, 1).

## R
> ar2.ma1.model = Arima(y, order = c(2, 0, 1))
> plot(y, type = 'l')
> lines(ar2.ma1.model$fitted, col = 2)
> plot(y, ar2.ma1.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.ma1.model$residuals)
> pacf(ar2.ma1.model$residuals)

Análise Prática de Séries Temporais | 61


Figura 6-11. ACF e PACF dos resíduos de um modelo ARIMA (2, 0, 1).

## R
> ar2.ma2.model = Arima(y, order = c(2, 0, 2))
> plot(y, type = 'l')
> lines(ar2.ma2.model$fitted, col = 2)
> plot(y, ar2.ma2.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.ma2.model$residuals)
> pacf(ar2.ma2.model$residuals)
>
> ar2.d1.ma2.model = Arima(y, order = c(2, 1, 2))
> plot(y, type = 'l')
> lines(ar2.d1.ma2.model$fitted, col = 2)
> plot(y, ar2.d1.ma2.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.d1.ma2.model$residuals)
> pacf(ar2.d1.ma2.model$residuals)

## R
> cor(y, ar1.ma1.model$fitted)
[1] 0.3018926
> cor(y, ar2.ma1.model$fitted)
[1] 0.4683598
> cor(y, ar2.ma2.model$fitted)
[1] 0.4684905
> cor(y, ar2.d1.ma2.model$fitted)
[1] 0.4688166

## R
## original coefficients
> y = arima.sim(n = 1000, list(ar = c(0.8, -0.4), ma = c(-0.7)))
> ar2.ma1.model$coef
ar1 ar2 ma1 intercept
0.785028320 -0.462287054 -0.612708282 -0.005227573

62 | Capítulo 6: Modelos Estatísticos para Séries Temporais


## R
> est = auto.arima(demand[["Banking orders (2)"]],
stepwise = FALSE, ## É bem lento,
## mas nos permite uma pesquisa mais completa.
max.p = 3, max.q = 9)
> est
Series: demand[["Banking orders (2)"]]
ARIMA(0,0,3) with non-zero mean

Coefficients:
ma1 ma2 ma3 mean
-0.0645 -0.1144 -0.4796 79914.783
s.e. 0.1327 0.1150 0.1915 1897.407

sigma^2 estimated as 1.467e+09: log likelihood=-716.71


AIC=1443.42 AICc=1444.53 BIC=1453.89

## R
> auto.model = auto.arima(y)
> auto.model
Series: y
ARIMA(2,0,1) with zero mean

Coefficients:
ar1 ar2 ma1
0.7847 -0.4622 -0.6123

s.e. 0.0487 0.0285 0.0522

sigma^2 estimated as 1.019: log likelihood=-1427.21


AIC=2862.41 AICc=2862.45 BIC=2882.04

## R
> VARselect(demand[, 11:12, with = FALSE], lag.max=4,
+ type="const")
$selection
AIC(n) HQ(n) SC(n) FPE(n)
3 3 1 3

$criteria
1 2 3 4
AIC(n) 3.975854e+01 3.967373e+01 3.957496e+01 3.968281e+01
HQ(n) 3.984267e+01 3.981395e+01 3.977126e+01 3.993521e+01
SC(n) 3.997554e+01 4.003540e+01 4.008130e+01 4.033382e+01
FPE(n) 1.849280e+17 1.700189e+17 1.542863e+17 1.723729e+17

## R
> est.var <- VAR(demand[, 11:12, with = FALSE], p=3, type="const")
> est.var

> par(mfrow = c(2, 1))


> plot(demand$`Banking orders (2)`, type = "l")
> lines(fitted(est.var)[, 1], col = 2)
> plot(demand$`Banking orders (3)`,
> type = "l")
> lines(fitted(est.var)[, 2], col = 2)

Análise Prática de Séries Temporais | 63


> par(mfrow = c(2, 1))
> acf(demand$`Banking orders (2)` - fitted(est.var)[, 1])
> acf(demand$`Banking orders (3)` -
> fitted(est.var)[, 2])

Figura 6-12. Na parte superior, vemos os valores reais (linha contínua) versus os valores
preditos (linha tracejada) para as ordens bancárias (2) e, na parte inferior, vemos o mesmo
para as ordens bancárias (3). Curiosamente, o gráfico superior se parece mais com uma
previsão comum, em que a previsão é um pouco “lenta” na mudança em relação aos
dados reais, ao passo que no gráfico inferior vemos que a previsão predisse mudanças um
pouco antes de acontecerem. Isso sugere que as ordens bancárias (2) “lideram” as ordens
bancárias (3), o que significa que as ordens bancárias (2) são úteis na predição de ordens do
controlador de tráfego, mas não o contrário, ou pelo menos não em tal grau.

64 | Capítulo 6: Modelos Estatísticos para Séries Temporais


Figura 6-13. Plotamos a função de autocorrelação dos resíduos para cada série temporal.
Repare que ambas as séries têm uma autocorrelação significativa dos erros no lag 3, que não
pode ser totalmente contabilizada no modelo.

## R
> serial.test(est.var, lags.pt = 8, type="PT.asymptotic")

Portmanteau Test (asymptotic)

data: Residuals of VAR object est.var


Chi-squared = 20.463, df = 20, p-value = 0.4293

Análise Prática de Séries Temporais | 65


CAPÍTULO 7
Modelos de Espaço de Estados
para Séries Temporais

## R
## Foguete levará 100 intervalos de tempo.
ts.length <- 100

## A aceleração conduzirá o movimento.


a <- rep(0.5, ts.length)

## Posição e velocidade começam em 0.


x <- rep(0, ts.length)
v <- rep(0, ts.length)
for (ts in 2:ts.length) {

x[ts] <- v[ts - 1] * 2 + x[ts - 1] + 1/2 * a[ts-1] ^ 2


x[ts] <- x[ts] + rnorm(1, sd = 20) ## componente estocástico
v[ts] <- v[ts - 1] + 2 * a[ts-1]
}

## R
par(mfrow = c(3, 1))
plot(x, main = "Position", type = 'l')
plot(v, main = "Velocity", type = 'l')
plot(acceleration, main = "Acceleration", type = 'l')

## R
z <- x + rnorm(ts.length, sd = 300)
plot (x, ylim = range(c(x, z)))
lines(z)
Figura 7-1. A posição, a velocidade e a aceleração do nosso foguete.

Figura 7-2. A verdadeira posição (pontos) versus nossa medição ruidosa (linha). Repare
que a posição x não retrata uma parábola perfeita devido ao ruído que inserimos na
equação de transição de estado.

Análise Prática de Séries Temporais | 67


## R
kalman.motion <- function(z, Q, R, A, H) {
dimState = dim(Q)[1]

xhatminus <- array(rep(0, ts.length * dimState),


c(ts.length, dimState))
xhat <- array(rep(0, ts.length * dimState),
c(ts.length, dimState))

Pminus <- array(rep(0, ts.length * dimState * dimState),


c(ts.length, dimState, dimState))
P <- array(rep(0, ts.length * dimState * dimState),
c(ts.length, dimState, dimState))

K <- array(rep(0, ts.length * dimState),


c(ts.length, dimState)) # Kalman gain

# Estimativas iniciais = começando em 0 para todas as métricas.


xhat[1, ] <- rep(0, dimState)
P[1, , ] <- diag(dimState)

for (k in 2:ts.length) {
# Atualização do tempo.
xhatminus[k, ] <- A %*% matrix(xhat[k-1, ])
Pminus[k, , ] <- A %*% P[k-1, , ] %*% t(A) + Q

K[k, ] <- Pminus[k, , ] %*% H %*%


solve( t(H) %*% Pminus[k, , ] %*% H + R )
xhat[k, ] <- xhatminus[k, ] + K[k, ] %*%
(z[k]- t(H) %*% xhatminus[k, ])
P[k, , ] <- (diag(dimState)-K[k,] %*% t(H)) %*% Pminus[k, , ]
}

## Retornamos a previsão e o valor suavizado.


return(list(xhat = xhat, xhatminus = xhatminus))
}

## R
## Parâmetros de ruído:
R <- 10^2 ## Variância de medição — este valor deve ser definido de acordo com
## os limites físicos conhecidos da ferramenta de medição.
## Nós o definimos de acordo com o ruído que adicionamos a x
## para produzir x na geração de dados acima da variância do processo,
Q <- 10 ## geralmente considerado um hiperparâmetro
## a ser ajustado a fim de maximizar o desempenho.

## Parâmetros dinâmicos:

A <- matrix(1) ## x_t = A * x_t-1 (Como o x anterior afeta o x posterior)


H <- matrix(1) ## y_t = H * x_t (Traduzindo estado para medição)

## Executa os dados por meio do método de filtragem de Kalman


xhat <- kalman.motion(z, diag(1) * Q, R, A, H)[[1]]

68 | Capítulo 7: Modelos de Espaço de Estados para Séries Temporais


Measured
Actual
Filtered
Forecast

Figura 7-3. Muitas grandezas relacionadas: a posição medida (linha sólida); a posição
real/verdadeira (linha tracejada); a estimativa de filtragem da posição (ou seja, a
melhor estimativa para a posição no tempo t com a medição no tempo t incorporada,
representada pela linha pontilhada); e a previsão da posição (ou seja, a melhor
estimativa para a posição no tempo t incorporada somente à dinâmica conhecida
somada às medições até o tempo t – 1, não incluindo o tempo t. Grandeza representada
pela linha mista).

... q(t-1) q(t) q(t+1) ...

y(t-1) y(t) y(t+1)

Figura 7-4. O processo de um HMM. Os estados reais do sistema em um determinado


tempo são representados por x(t), ao passo que os dados observáveis em um determinado
tempo são representados por y(t). Apenas x(t) é relevante para y(t). Em outras palavras,
x(t − 1) não oferece nenhuma informação adicional para predizer y(t) se conhecermos
x(t). Da mesma forma, apenas x(t) tem alguma relação para predizer x(t + 1) e não há
nenhuma informação adicional de x(t – 1). Este é o aspecto Markov do sistema.

Análise Prática de Séries Temporais | 69


Figura 7-5. Essa série temporal foi simulada com quatro estados. Porém não fica
claro, a partir de uma observação visual, que existem quatro estados nem onde
um estado termina e outro começa.

Figura 7-6. O algoritmo de Viterbi busca todos os caminhos possíveis para explicar uma
dada série de tempo observada, em que um caminho indica qual estado foi ocupado em
cada intervalo de tempo.

70 | Capítulo 7: Modelos de Espaço de Estados para Séries Temporais


## R
## Veja que, neste caso, escolhemos definir uma seed.
## Caso defina a mesma seed, nossos números devem ser correspondentes
set.seed(123)

## Define os parâmetros para a distribuição de cada um


## dos quatro estados de mercado que queremos representar.
bull_mu <- 0.1
bull_sd <- 0.1

neutral_mu <- 0.02


neutral_sd <- 0.08

bear_mu <- -0.03


bear_sd <- 0.2

panic_mu <- -0.1


panic_sd <- 0.3

## Coleta esses parâmetros em vetores para fácil indexação.


mus <- c(bull_mu, neutral_mu, bear_mu, panic_mu)
sds <- c(bull_sd, neutral_sd, bear_sd, panic_sd)

## Define algumas constantes para representar a série temporal que geraremos.


NUM.PERIODS <- 10
SMALLEST.PERIOD <- 20
LONGEST.PERIOD <- 40

## Determina estocasticamente uma série de contagens de dias,


## cada contagem de dias indica uma “execução” ou um estado do mercado
days <- sample(SMALLEST.PERIOD:LONGEST.PERIOD, NUM.PERIODS,
replace = TRUE)

## Para cada número de dias no vetor days,


## geramos uma série temporal para essa série de dias em um determinado estado do mercado
## e a adicionamos à nossa série temporal geral.
returns <- numeric()
true.mean <- numeric()
for (d in days) {
idx = sample(1:4, 1, prob = c(0.2, 0.6, 0.18, 0.02))
returns <- c(returns, rnorm(d, mean = mus[idx], sd = sds[idx]))
true.mean <- c(true.mean, rep(mus[idx], d))
}

## R
> table(true.mean)
true.mean
-0.03 0.02 0.1
155 103 58

## R
require(depmixS4)
hmm.model <- depmix(returns ~ 1, family = gaussian(),
nstates = 4, data=data.frame(returns=returns))
model.fit <- fit(hmm.model)
post_probs <- posterior(model.fit)

Análise Prática de Séries Temporais | 71


## R
plot(returns, type = 'l', lwd = 3, col = 1,
yaxt = "n", xaxt = "n", xlab = "", ylab = "",
ylim = c(-0.6, 0.6))

lapply(0:(length(returns) - 1, function (i) {


## Adiciona um retângulo de fundo da cor apropriada
## para indicar o estado durante um determinado intervalo de tempo.
rect(i,-0.6,(i + 1),0.6,
col = rgb(0.0,0.0,0.0,alpha=(0.2 * post_probs$state[i + 1])),
border = NA)
}

Figura 7-7. Nesse gráfico, o segundo plano indica um estado distinto e a linha preta
sólida representa os valores reais. Os tipos de objetos de linha branca vertical são, na
verdade, porções muito estreitas que representam os tempos, nos quais se estima que o
processo esteja no que acaba sendo o mais raro dos quatro estados.

bull_mu <- 0.1


bull_sd <- 0.1

neutral_mu <- 0.02


neutral_sd <- 0.08

bear_mu <- -0.03


bear_sd <- 0.2

panic_mu <- -0.1


panic_sd <- 0.3

72 | Capítulo 7: Modelos de Espaço de Estados para Séries Temporais


> attr(model.fit, "response")
[[1]]
[[1]][[1]] <- Coincidentemente, tem uma média próxima ao modelo pânico,
mas esse modelo não gerou nenhum dado na amostra que fosse adequado.
Em vez disso, o algoritmo atribuiu este quarto estado a valores mais negativos.

Model of type gaussian (identity), formula: returns ~ 1


Coefficients:
(Intercept)

-0.09190191
sd 0.03165587

[[2]]
[[2]][[1]] <- Poderia ser consistente com o modelo bear market.
Model of type gaussian (identity), formula: returns ~ 1
Coefficients:
(Intercept)
-0.05140387
sd 0.2002024

[[3]]
[[3]][[1]] <- Poderia ser consistente com o modelo bull market.
Model of type gaussian (identity), formula: returns ~ 1
Coefficients:
(Intercept)
0.0853683
sd 0.07115133

[[4]]
[[4]][[1]] <- Poderia ser um pouco consistente com o modelo neutro.
Model of type gaussian (identity), formula: returns ~ 1
Coefficients:
(Intercept)
-0.0006163519
sd 0.0496334

## R
elec = fread("electric.csv")

require(bsts)
n = colnames(elec)[9]
par(mfrow = c(2, 1))
plot(elec[[n]][1:4000])
plot(elec[[n]][1:96)
## Conforme analisado anteriormente no livro,
## a escala temporal apropriada é a chave para a compreensão dos dados ts.

Análise Prática de Séries Temporais | 73


Figura 7-8. No gráfico superior, ajustamos a série completa (duas mil medições
consecutivas de hora em hora). No gráfico inferior, temos um subconjunto menor e mais
compreensível dos dados. Quando analisamos o gráfico de uma forma que evidencia os
padrões diários, ele faz mais sentido.

## R
ss <- AddLocalLinearTrend(list(), elec[[n]])
ss <- AddSeasonal(ss, elec[[n]], nseasons = 24, season.duration = 1 )
ss <- AddSeasonal(ss, elec[[n]], nseasons = 7, season.duration = 24)

## R
model1 <- bsts(elec[[n]],
state.specification = ss,
niter = 100)
plot(model1, xlim = c(1800, 1900)

## R
plot(model1, "seasonal", nseasons = 7, season.duration = 24)

74 | Capítulo 7: Modelos de Espaço de Estados para Séries Temporais


Figura 7-9. Os dias das sazonalidades da semana demonstram que há uma diferença em
dias da semana distintos. Demonstra também que as distribuições dos parâmetros
do dia da semana são estáveis ao longo do tempo para cada dia da semana.

Figura 7-10. Distribuições de contribuições da tendência dos dados, bem como dos
componentes sazonais diários e do dia da semana. Ao somar esses três componentes,
você obterá o valor da predição.

••
pred <- predict(model1, horizon = 24, quantiles = c(0.05, 0.95))
plot(pred, plot.original = 72)

Análise Prática de Séries Temporais | 75


Figura 7-11. As últimas 72 horas em nossos dados combinadas com uma previsão
para as próximas 24 horas, bem como limites de quantis de 5% e 95% nas previsões.
Observe que a distribuição da previsão se espalha mais à medida que fazemos previsões
mais no futuro.

76 | Capítulo 7: Modelos de Espaço de Estados para Séries Temporais


CAPÍTULO 8
Gerando e Selecionando Características
para uma Série Temporal

Periodogramas

77
Figura 8-2. Exemplos de características de séries temporais que os profissionais da saúde
usam para ler dados de séries temporais de um EKG.

Figura 8-3. Na série temporal de índice glicêmico de um dia, podemos ver que existem
quatro características que a maioria dos profissionais de saúde identificaria em um
dia típico, e uma delas não está relacionada à alimentação, mas é conhecida como o
“ fenômeno do amanhecer” [Dawn Phenomenon] ou “ fenômeno do alvorecer”. É o tipo
de dado que desejaríamos identificar em uma ou várias características.

## python
>> from tsfresh.examples.robot_execution_failures import
download_robot_execution_failures,
load_robot_execution_failures
>> download_robot_execution_failures()
>> timeseries, y = load_robot_execution_failures()

78 | Capítulo 8: Gerando e Selecionando Características para uma Série Temporal


## python
>> from tsfresh import extract_features
>> extracted_features = extract_features(timeseries,
column_id = "id",
column_sort = "time")

## python
>> extracted_features.columns
Index(['F_x__abs_energy', 'F_x__absolute_sum_of_changes',
'F_x__agg_autocorrelation__f_agg_"mean"',
'F_x__agg_autocorrelation__f_agg_"median"',
'F_x__agg_autocorrelation__f_agg_"var"',
'F_x__agg_linear_trend__f_agg_"max"__chunk_len_10__attr_
"intercept"',
'F_x__agg_linear_trend__f_agg_"max"__chunk_len_10__attr_
"rvalue"',
'F_x__agg_linear_trend__f_agg_"max"__chunk_len_10__attr_
"slope"',
'F_x__agg_linear_trend__f_agg_"max"__chunk_len_10__attr_
"stderr"',
'F_x__agg_linear_trend__f_agg_"max"__chunk_len_50__attr_
"intercept"',
...
'T_z__time_reversal_asymmetry_statistic__lag_1',
'T_z__time_reversal_asymmetry_statistic__lag_2',
'T_z__time_reversal_asymmetry_statistic__lag_3',
'T_z__value_count__value_-inf', 'T_z__value_count__value_0',
'T_z__value_count__value_1', 'T_z__value_count__value_inf',

'T_z__value_count__value_nan', 'T_z__variance',
'T_z__variance_larger_than_standard_deviation'],
dtype='object', name='variable', length=4764)

## R
>> x_idx = random.sample(range(len(features_filtered.columns)), 10)
>> selX = features_filtered.iloc[:, x_idx].values
>> unselected_features = list(set(extracted_features.columns)
.difference(set(features_filtered.columns)))
>> unselected_features = random.sample(unselected_features, 10)
>> unsel_x_idx = [idx for (idx, val) in enumerate(
extracted_features.columns) if val in unselected_features]
>> unselX = extracted_features.iloc[:, unsel_x_idx].values
>> mixed_X = np.hstack([selX, unselX])

>> svc = SVC(kernel="linear", C=1)


>> rfe = RFE(estimator=svc, n_features_to_select=1, step=1)
>> rfe.fit(mixed_X, y)
>> rfe.ranking_
array([ 9, 12, 8, 1, 2, 3, 6, 4, 10, 11,
16, 5, 15, 14, 7, 13, 17, 18, 19, 20])

Análise Prática de Séries Temporais | 79


CAPÍTULO 9
Aprendizado de Máquina
para Séries Temporais

## python
>>> from cesium import datasets
>>> eeg = datasets.fetch_andrzejak()

## python
>>> plt.subplot(3, 1, 1)
>>> plt.plot(eeg["measurements"][0])
>>> plt.legend(eeg['classes'][0])
>>> plt.subplot(3, 1, 2)

>>> plt.plot(eeg["measurements"][300])
>>> plt.legend(eeg['classes'][300])
>>> plt.subplot(3, 1, 3)
>>> plt.plot(eeg["measurements"][450])
>>> plt.legend(eeg['classes'][450])
Figura 9-1. Plotamos três amostras selecionadas aleatoriamente do conjunto de dados EEG.
As amostras são independentes e não medições contemporâneas de diferentes partes do
mesmo cérebro. Cada uma é uma medição de série temporal independente feita com um
paciente diferente em um momento distinto.

## python
>>> from cesium import featurize.featurize_time_series as ft
>>> features_to_use = ["amplitude",
>>> "percent_beyond_1_std",
>>> "percent_close_to_median",
>>> "skew",
>>> "max_slope"]
>>> fset_cesium = ft(times = eeg["times"],
>>> values = eeg["measurements"],
>>> errors = None,
>>> features_to_use = features_to_use,
>>> scheduler = None)

Figura 9-2. Valores numéricos das características que geramos para as primeiras amostras
em nosso conjunto de dados.
Análise Prática de Séries Temporais | 81
## python
>>> np.std(eeg_small["measurements"][0])
40.411
>>> np.mean(eeg_small["measurements"][0])
-4.132
>>> sample_ts = eeg_small["measurements"][0]
>>> sz = len(sample_ts)
>>> ll = -4.13 - 40.4
>>> ul = -4.13 + 40.4
>>> quals = [i for i in range(sz) if sample_ts[i] < ll or
sample_ts[i] > ul ]
>>> len(quals)/len(ser)
0.327 ## Compatível com a característica gerada na Figura 9-2

Figura 9-3. Nesta árvore de


regressão simples, uma série de
ramificações lógicas é utilizada
para se chegar à predição de
peso de um ser humano. Trata-
se de um modelo bruto, mas
ilustra a abordagem não linear
e variável que até um modelo
de árvore simples pode aplicar a
um problema de regressão.
## python
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
fset_cesium.values, eeg["classes"], random_state=21)

## python
>>> from sklearn.ensemble import RandomForestClassifier
>>> rf_clf = RandomForestClassifier(n_estimators = 10,
>>> max_depth = 3,
>>> random_state = 21)
>>> rf_clf.fit(X_train, y_train)

## python
>>> rf_clf.score(X_test, y_test)
0.616

## python
>>> import xgboost as xgb
>>> xgb_clf = xgb.XGBClassifier(n_estimators = 10,

>>> max_depth = 3,
>>> random_state = 21)
>>> xgb_clf.fit(X_train, y_train)
>>> xgb_clf.score(X_test, y_test)
0.648

82 | Capítulo 9: Aprendizado de Máquina para Séries Temporais


## python
>>> start = time.time()
>>> xgb_clf.fit(X_train, y_train)
>>> end = time.time()
>>> end - start
0.0189

## Random Forest
>>> start = time.time()
>>> rf_clf.fit(X_train, y_train)
>>> end = time.time()
>>> end - start
0.027

## python
## python
## Testa
>>> ##
>>> Testa oo mesmo
mesmo número
número de
de árvores
árvores (10),
(10), porém
porém com
com menos
menos complexidade
complexidade
## (max_depth
>>> ##
>>> (max_depth == 2)
2)
>>>
>>>
>>> ##
>>> ## XGBoost
XGBoost
>>>
>>> xgb_clf
xgb_clf == xgb.XGBClassifier(n_estimators
xgb.XGBClassifier(n_estimators == 10,
10,
max_depth
max_depth == 2,
2,
random_state = 21)
random_state = 21)
>>>
>>> xgb_clf.fit(X_train,
xgb_clf.fit(X_train, y_train)
y_train)
>>> xgb_clf.score(X_test, y_test)
>>> xgb_clf.score(X_test, y_test)
0.616
0.616

>>> ##
>>> Random Forest
## Random Forest
>>> rf_clf
>>> rf_clf == RandomForestClassifier(n_estimators
RandomForestClassifier(n_estimators == 10,
10,
max_depth
max_depth == 2,
2,
random_state
random_state == 21)
21)
>>>
>>> rf_clf.fit(X_train,
rf_clf.fit(X_train, y_train)
y_train)
>>> rf_clf.score(X_test, y_test)
>>> rf_clf.score(X_test, y_test)
0.544
0.544

Isso vale até mesmo quando reduzimos ainda mais a complexidade da árvore:
>>> ## Testa o mesmo número de árvores (10), porém com menos complexidade
>>> ## (max_depth = 1)

>>> ## XGBoost
>>> xgb_clf = xgb.XGBClassifier(n_estimators = 10,
max_depth = 1,
random_state = 21)
>>> xgb_clf.fit(X_train, y_train)
>>> xgb_clf.score(X_test, y_test)
0.632

>>> ## Random Forest


>>> rf_clf = RandomForestClassifier(n_estimators = 10,
max_depth = 1,
random_state = 21)
>>> rf_clf.fit(X_train, y_train)
>>> rf_clf.score(X_test, y_test)
0.376

Análise Prática de Séries Temporais | 83


Figura 9-4. Os perfis de projeção de três palavras distintas (12, 23 e 9) são bem diferentes
uns dos outros. Já vimos também algumas características que poderiam distinguir
essas palavras umas das outras: a localização temporal (eixo x) do maior pico ou
mesmo do segundo maior pico, o número de picos locais, o intervalo geral de valores e a
convexidade média das curvas.

## python
>>> plt.subplot(3, 2, 1)
>>> plt.plot(words.iloc[1, 1:-1])
>>> plt.title("Word = " + str(words.word[1]), fontweight = 'bold')
>>> plt.subplot(3, 2, 2)
>>> plt.hist(words.iloc[1, 1:-1], 10)
>>> plt.subplot(3, 2, 3)
>>> plt.plot(words.iloc[3, 1:-1])
>>> plt.title("Word = " + str(words.word[3]), fontweight = 'bold')
>>> plt.subplot(3, 2, 4)
>>> plt.hist(words.iloc[3, 1:-1], 10)
>>> plt.subplot(3, 2, 5)
>>> plt.plot(words.iloc[5, 1:-1])
>>> plt.title("Word = " + str(words.word[11]), fontweight = 'bold')
>>> plt.subplot(3, 2, 6)
>>> plt.hist(words.iloc[5, 1:-1], 10)

84 | Capítulo 9: Aprendizado de Máquina para Séries Temporais


Figura 9-5. Outra maneira de medir as classes para fazer um brainstorming de
características úteis. O histograma de exemplos de classes individuais sinaliza que os
atributos desse histograma, como o total de picos locais, a assimetria e a curtose seriam
úteis e possivelmente bons substitutos para alguns dos atributos das curvas da série
temporal, óbvias ao olho humano, mas nada fáceis de identificar com o código.

## python
>>> x = np.array([])
>>> y = np.array([])
>>>
>>> w = 12
>>> selected_words = words[words.word == w]

>>> selected_words.shape
>>>
>>> for idx, row in selected_words.iterrows():
>>> y = np.hstack([y, row[1:271]])
>>> x = np.hstack([x, np.array(range(270))])
>>>
>>> fig, ax = plt.subplots()

Análise Prática de Séries Temporais | 85


Figura 9-6. Um histograma 2D das projeções de palavra 1D para palavra = 12. O eixo y
é o valor em um determinado intervalo de tempo, e o eixo x representa os 270 intervalos
de tempo para cada amostra de série temporal/projeção de palavra.

Figura 9-7. Um histograma 2D das projeções de palavra 1D para palavra = 23. O eixo y é
o valor em um determinado intervalo de tempo e o eixo x representa os 270 intervalos de
tempo para cada amostra de série temporal/projeção de palavra.

86 | Capítulo 9: Aprendizado de Máquina para Séries Temporais


from cesium import featurize.featurize_time as ft

## python
>>> word_vals = words.iloc[:, 1:271]
>>> times = []
>>> word_values = []
>>> for idx, row in word_vals.iterrows():
>>> word_values.append(row.values)
>>> times.append(np.array([i for i in range(row.values.shape[0])]))
>>>
>>> features_to_use = ['amplitude',
>>> 'percent_beyond_1_std',
>>> 'percent_close_to_median']
>>> featurized_words = ft(times = times,
>>> values = word_values,
>>> errors = None,
>>> features_to_use = features_to_use,
>>> scheduler = None)

## python
>>> ## Cria algumas características derivadas do histograma
>>> times = []
>>> hist_values = []
>>> for idx, row in words_features.iterrows():
>>> hist_values.append(np.histogram(row.values,
>>> bins=10,
>>> range=(-2.5, 5.0))[0] + .0001)
>>> ## 0s cause downstream problems
>>> times.append(np.array([i for i in range(9)]))
>>>
>>> features_to_use = ["amplitude",
>>> "percent_close_to_median",
>>> "skew"
>>> ]
>>>
>>> featurized_hists = ft(times = times,
>>> values = hist_values,
>>> errors = None,
>>> features_to_use = features_to_use,
>>> scheduler = None)

## python
>>> features = pd.concat([featurized_words.reset_index(drop=True),
>>> featurized_hists],
>>> axis=1)

Análise Prática de Séries Temporais | 87


Figura 9-8. Funcionamento do dynamic time warping. Cada ponto em uma série
temporal é mapeado em um ponto na série temporal oposta, apesar de não se exigir
nenhum mapeamento ponto a ponto. Mas isso gera consequências: (1) As séries
temporais não precisam ter o mesmo comprimento ou a mesma escala temporal.
O importante é o formato. (2) O tempo nem sempre avança durante o processo de ajuste
e talvez não se mova no mesmo ritmo para cada série temporal. Quando digo não
se mover no tempo significa progredir ao longo da curva na direção do eixo x.
Fonte: Wikipédia (https://perma.cc/F9ER-RTDS).

## python
>>> def distDTW(ts1, ts2):
>>> ## Aqui é a configuração
>>> DTW={}
>>> for i in range(len(ts1)):
>>> DTW[(i, -1)] = np.inf
>>> for i in range(len(ts2)):
>>> DTW[(-1, i)] = np.inf
>>> DTW[(-1, -1)] = 0
>>>
>>> ## Aqui, é onde realmente calculamos o ideal,
>>> ## um passo de cada vez.
>>> for i in range(len(ts1)):
>>> for j in range(len(ts2)):
>>> dist = (ts1[i] - ts2[j])**2
>>> DTW[(i, j)] = dist + min(DTW[(i-1, j)],
>>> DTW[(i, j-1)],
>>> DTW[(i-1, j-1)])
>>> ## Este é um exemplo de programação dinâmica
>>>
>>> ## Quando encontramos o caminho completo,
>>> ## retornamos a distância associada.
>>> return sqrt(DTW[len(ts1)-1, len(ts2)-1])

## python
>>> from sklearn import preprocessing
>>> feature_values = preprocessing.scale(features.values)

88 | Capítulo 9: Aprendizado de Máquina para Séries Temporais


## python
>>> from sklearn.cluster import AgglomerativeClustering
>>> feature_clustering = AgglomerativeClustering(n_clusters = 50,
>>> linkage = 'ward')
>>> feature_clustering.fit(feature_values)
>>> words['feature_labels'] = feature_clustering.fit_predict(p)

## python
>>> from sklearn.metrics.cluster import homogeneity_score
>>> homogeneity_score(words.word, words.feature_labels)
0.508

## python
>>> p = pairwise_distances(X, metric = distDTW)
>>> ## Isso leva tempo para ser calculado então vale a pena salvar
>>> with open("pairwise_word_distances.npy", "wb") as f:
np.save(f, p)

## python
>>> from sklearn.cluster import AgglomerativeClustering
>>> dtw_clustering = AgglomerativeClustering(linkage = 'average',
>>> n_clusters = 50,
>>> affinity = 'precomputed')
>>> words['dtw_labels'] = dtw_clustering.fit_predict(p)

## python
>>> from sklearn.metrics.cluster import homogeneity_score,
>>> completeness_score
>>> homogeneity_score(words.word, words.dtw_labels)
0.828
>>> completeness_score(words.word, words.dtw_labels)
0.923

Análise Prática de Séries Temporais | 89


CAPÍTULO 10
Aprendizado Profundo para Séries Temporais

Figura 10-1. Uma simples rede feedforward.

## python
>>> import mxnet as mx
>>> fc1 = mx.gluon.nn.Dense(120, activation='relu')

## python
>>> ## Cria uma rede em vez de uma camada independente.
>>> from mx.gluon import nn
>>> net = nn.Sequential()
>>> net.add(nn.Dense(120, activation='relu'),
>>> nn.Dense(1))
>>> net.initialize(init=init.Xavier())
>>>
>>> ## Define a perda que queremos.
>>> L2Loss = gluon.loss.L2Loss()
>>>
>>> trainer = gluon.Train(net.collect_params(), 'sgd',
>>> {'learning_rate': 0.01})
## python
>>> for data,target in train_data:
>>> ## Calcula o gradiente.
>>> with autograd.record():
>>> out = net(data)
>>> loss = L2Loss(output, data)
>>> loss.backward()
>>> ## Aplica o gradiente para atualizar os parâmetros.
>>> trainer.step(batch_size)

## python
>>> net.save_parameters('model.params')

## R
> elec = fread("electricity.txt")
> elec
V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18
1: 14 69 234 415 215 1056 29 840 226 265 179 148 112 171 229 1001 49 162
2: 18 92 312 556 292 1363 29 1102 271 340 235 192 143 213 301 1223 64 216
3: 21 96 312 560 272 1240 29 1025 270 300 221 171 132 185 261 1172 61 197
4: 20 92 312 443 213 845 24 833 179 211 170 149 116 151 209 813 40 173
5: 22 91 312 346 190 647 16 733 186 179 142 170 99 136 148 688 29 144

## R
> ncol(elec)
[1] 321
> nrow(elec)
[1] 26304

## R
> elec[125:148, plot(V4, type = 'l', col = 1, ylim = c(0, 1000))]
> elec[125:148, lines(V14, type = 'l', col = 2)]
> elec[125:148, lines(V114, type = 'l', col = 3)]

## R
> elec[1:168, plot(V4, type = 'l', col = 1, ylim = c(0, 1000))]
> elec[1:168, lines(V14, type = 'l', col = 2)]
> elec[1:168, lines(V114, type = 'l', col = 3)]

Análise Prática de Séries Temporais | 91


Figura 10-2. Vinte e quatro horas amostradas a partir do conjunto de dados para 3
locais diferentes, em relação aos 321 locais disponíveis no conjunto de dados. Embora
não tenhamos noção de a que horas locais esses índices correspondem, vemos um padrão
diário coerente.

Figura 10-3. Um ciclo completo de sete dias dos dados amostrados para os mesmos três
locais, conforme ilustrado no gráfico diário. Nossa sensação de um padrão diário é
corroborada com essa visão mais ampla dos dados, indicando que um grande pico por
dia junto com algumas características menores parece consistente no comportamento.

## R
> elec.diff[1:168, plot( V4, type = 'l', col = 1, ylim = c(-350, 350))]
> elec.diff[1:168, lines(V14, type = 'l', col = 2)]
> elec.diff[1:168, lines(V114, type = 'l', col = 3)]

92 | Capítulo 10: Aprendizado Profundo para Séries Temporais


Figura 10-4. Uma amostra de uma semana das mesmas séries temporais de energia
elétrica diferenciadas dos mesmos três locais, representando a mudança de hora em
hora. Embora essa série ainda apresente um padrão, assim como a série original, os
componentes imprevisíveis da série se tornam mais aparentes, pois representam uma
porção maior do valor da série diferenciada do que da série original.

## python
>>> from math import floor
>>>
>>> ## Para arquivamento.
>>> import os
>>> import argparse
>>>
>>> ## Módulo de aprendizado profundo.
>>> import mxnet as mx
>>>
>>> ## Processamento de dados.
>>> import numpy as np
>>> import pandas as pd
>>>
>>> ## Relatórios customizados.
>>> import perf

## python
>>> ## Alguns hiperparâmetros que não ajustaremos por meio de entradas de linha de comando
>>> DATA_SEGMENTS = { 'tr': 0.6, 'va': 0.2, 'tst': 0.2}

Análise Prática de Séries Temporais | 93


>>> THRESHOLD_EPOCHS = 5
>>> COR_THRESHOLD = 0.0005
>>>
>>> ## Define o parser.
>>> parser = argparse.ArgumentParser()
>>>
>>> ## DATA SHAPING
>>> parser.add_argument('--win', type=int, default=24*7)
>>> parser.add_argument('--h', type=int, default=3)
>>>
>>> ## ESPECIFICAÇÕES DO MODELO
>>> parser.add_argument('--model', type=str, default='rnn_model')
>>> ## Componentes da CNN
>>> parser.add_argument('--sz-filt', type=str, default=8)
>>> parser.add_argument('--n-filt', type=int, default=10)
>>> ## Componentes da RNN
>>> parser.add_argument('--rnn-units', type=int, default=10)
>>>
>>> ## DETALHES DO TREINAMENTO
>>> parser.add_argument('--batch-n', type=int, default=1024)
>>> parser.add_argument('--lr', type=float, default=0.0001)
>>> parser.add_argument('--drop', type=float, default=0.2)
>>> parser.add_argument('--n-epochs', type=int, default=30)
>>>
>>> ## REPOSITÓRIO DE TRABALHO
>>> parser.add_argument('--data-dir', type=str, default='../data')
>>> parser.add_argument('--save-dir', type=str, default=None)

--drop=0.2 --win=96 --batch-n=128 --lr=0.001 --n-epochs=25


--data-dir=/data/elec --save-dir=/archive/results
--model=model_will_vary

## python
>>> ################################
>>> ## PREPARAÇÃO DOS DADOS ##
>>> ################################
>>>
>>> def prepare_iters(data_dir, win, h, model, batch_n):
>>> X, Y = prepared_data(data_dir, win, h, model)
>>>
>>> n_tr = int(Y.shape[0] * DATA_SEGMENTS['tr'])
>>> n_va = int(Y.shape[0] * DATA_SEGMENTS['va'])
>>>
>>> X_tr, X_valid, X_test = X[ : n_tr],
>>> X[n_tr : n_tr + n_va],
>>> X[n_tr + n_va : ]
>>> Y_tr, Y_valid, Y_test = Y[ : n_tr],

94 | Capítulo 10: Aprendizado Profundo para Séries Temporais


>>> Y[n_tr : n_tr + n_va],
>>> Y[n_tr + n_va : ]
>>>
>>> iter_tr = mx.io.NDArrayIter(data = X_tr,
>>> label = Y_tr,
>>> batch_size = batch_n)
>>> iter_val = mx.io.NDArrayIter(data = X_valid,
>>> label = Y_valid,
>>> batch_size = batch_n)
>>> iter_test = mx.io.NDArrayIter(data = X_test,
>>> label = Y_test,
>>> batch_size = batch_n)
>>>
>>> return (iter_tr, iter_val, iter_test)

## python
>>> def prepared_data(data_dir, win, h, model_name):
>>> df = pd.read_csv(os.path.join(data_dir, 'electricity.diff.txt'),
>>> sep=',', header=0)
>>> x = df.as_matrix()
>>> ## Normaliza os dados. Isso cria um lookahead, já que
>>> ## normalizamos baseados nos valores medidos no conjunto
>>> ## de dados. Em um pipeline menos básico, calcularíamos
>>> ## isso como uma média móvel para evitar o lookahead.
>>> x = (x - np.mean(x, axis = 0)) / (np.std(x, axis = 0))
>>>
>>> if model_name == 'fc_model': ## NC data format

>>> ## Fornece o primeiro e o segundo lookbacks em uma entrada simples.


>>> X = np.hstack([x[1:-1], x[:-h]])
>>> Y = x[h:]
>>> return (X, Y)
>>> else: ## TNC data format
>>> # Pré-aloca X e Y
>>> # X shape = num examples * time win * num channels (NTC)
>>> X = np.zeros((x.shape[0] - win - h, win, x.shape[1]))
>>> Y = np.zeros((x.shape[0] - win - h, x.shape[1]))
>>>
>>> for i in range(win, x.shape[0] - h):
>>> ## O valor alvo/rótulo está h passos à frente.
>>> Y[i-win] = x[i + h - 1 , :]
>>> ## Os dados de entrada são os passos anteriores do win
>>> X[i-win] = x[(i - win) : i , :]
>>>
>>> return (X, Y)

## python
>>> x = (x - np.mean(x, axis = 0)) / (np.std(x, axis = 0))

## python
>>> if model_name == 'fc_model': ## NC data format
>>> ## Fornece o primeiro e o segundo lookbacks em uma entrada achatada.
>>> X = np.hstack([x[1:-h], x[0:-(h+1)]])
>>> Y = x[(h+1):]

Análise Prática de Séries Temporais | 95


## python
(Pdb) X[0, 1:10] == x[1, 1:10]
array([ True, True, True, True, True, True, True, True, True])
(Pdb) X[0, 322:331] == x[0, 1:10]
array([ True, True, True, True, True, True, True, True, True])

(Pdb) Y[0, 1:10] == x[4, 1:10]


array([ True, True, True, True, True, True, True, True, True])

## python
>>> # Pré-aloca X e Y.
>>> # X shape = num examples * time win * num channels (NTC)
>>> X = np.zeros((x.shape[0] - win - h, win, x.shape[1]))
>>> Y = np.zeros((x.shape[0] - win - h, x.shape[1]))
>>>
>>> for i in range(win, x.shape[0] - h):
>>> ## O valor alvo/rótulo está h passos à frente.
>>> Y[i-win] = x[i + h - 1 , :]
>>> ## Os dados de entrada são os passos anteriores do win.
>>> X[i-win] = x[(i - win) : i , :]

## python
(Pdb) Y[0, 1:10] == x[98, 1:10]
array([ True, True, True, True, True, True, True, True, True])
(Pdb) X.shape
(26204, 96, 321)
(Pdb) X[0, :, 1] == x[0:96, 1]
array([ True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,

True, True, True, True, True, True, True, True, True,


True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True])

96 | Capítulo 10: Aprendizado Profundo para Séries Temporais


## python
>>> def train(symbol, iter_train, valid_iter, iter_test,
>>> data_names, label_names,
>>> save_dir):
>>> ## Salva as informações/resultados de treinamento.
>>> if not os.path.exists(args.save_dir):
>>> os.makedirs(args.save_dir)
>>> printFile = open(os.path.join(args.save_dir, 'log.txt'), 'w')
>>> def print_to_file(msg):
>>> print(msg)
>>> print(msg, file = printFile, flush = True)
>>> ## Cabeçalho dos resultados de arquivamento.
>>> print_to_file('Epoch Training Cor Validation Cor')
>>>
>>> ## Armazena os valores de épocas anteriores para definir um threshold de melhoria.
>>> ## Finaliza mais cedo se o progresso for lento.
>>> buf = RingBuffer(THRESHOLD_EPOCHS)
>>> old_val = None
>>>
>>> ## Boilerplate do mxnet
>>> ## O padrão é 1 gpu cujo índice é 0.
>>> devs = [mx.gpu(0)]
>>> module = mx.mod.Module(symbol,
>>> data_names=data_names,
>>> label_names=label_names,
>>> context=devs)
>>> module.bind(data_shapes=iter_train.provide_data,
>>> label_shapes=iter_train.provide_label)
>>> module.init_params(mx.initializer.Uniform(0.1))
>>> module.init_optimizer(optimizer='adam',
>>> optimizer_params={'learning_rate':
>>> args.lr})
>>>
>>> ## Treinamento.
>>> for epoch in range( args.n_epochs):
>>> iter_train.reset()
>>> iter_val.reset()
>>> for batch in iter_train:
>>> # Calcula as predições.
>>> module.forward(batch, is_train=True)
>>> # Calcula os gradientes.
>>> module.backward()
>>> # Atualiza os parâmetros.
>>> module.update()
>>>
>>> ## Resultados do treinamento.
>>> train_pred = module.predict(iter_train).asnumpy()
>>> train_label = iter_train.label[0][1].asnumpy()
>>> train_perf = perf.write_eval(train_pred, train_label,
>>> save_dir, 'train', epoch)
>>>
>>> ## Resultados de validação.
>>> val_pred = module.predict(iter_val).asnumpy()
>>> val_label = iter_val.label[0][1].asnumpy()
>>> val_perf = perf.write_eval(val_pred, val_label,
>>> save_dir, 'valid', epoch)
>>>
>>> print_to_file('%d %f %f ' %
>>> (epoch, train_perf['COR'], val_perf['COR']))
>>>
>>> # Se ainda não tiver medidas de melhoria, pule.
>>> if epoch > 0: Análise Prática de Séries Temporais | 97
>>> buf.append(val_perf['COR'] - old_val)
>>> # Se tiver medidas de melhoria, verifique-as.
>>> if epoch > 2:
>>> vals = buf.get()
>>> args.lr})
>>>
>>> ## Treinamento.
>>> for epoch in range( args.n_epochs):
>>> iter_train.reset()
>>> iter_val.reset()
>>> for batch in iter_train:
>>> # Calcula as predições.
>>> module.forward(batch, is_train=True)
>>> # Calcula os gradientes.
>>> module.backward()
>>> # Atualiza os parâmetros.
>>> module.update()
>>>
>>> ## Resultados do treinamento.
>>> train_pred = module.predict(iter_train).asnumpy()
>>> train_label = iter_train.label[0][1].asnumpy()
>>> train_perf = perf.write_eval(train_pred, train_label,
>>> save_dir, 'train', epoch)
>>>
>>> ## Resultados de validação.
>>> val_pred = module.predict(iter_val).asnumpy()
>>> val_label = iter_val.label[0][1].asnumpy()
>>> val_perf = perf.write_eval(val_pred, val_label,
>>> save_dir, 'valid', epoch)
>>>
>>> print_to_file('%d %f %f ' %
>>> (epoch, train_perf['COR'], val_perf['COR']))
>>>
>>> # Se ainda não tiver medidas de melhoria, pule.
>>> if epoch > 0:
>>> buf.append(val_perf['COR'] - old_val)
>>> # Se tiver medidas de melhoria, verifique-as.
>>> if epoch > 2:
>>> vals = buf.get()
>>> vals = [v for v in vals if v != 0]
>>> if sum([v < COR_THRESHOLD for v in vals]) == len(vals):
>>> print_to_file('EARLY EXIT')
>>> break
>>> old_val = val_perf['COR']
>>>
>>> ## Treinamento
>>> test_pred = module.predict(iter_test).asnumpy()
>>> test_label = iter_test.label[0][1].asnumpy()
>>> test_perf = perf.write_eval(test_pred, test_label,
>>> save_dir, 'tst', epoch)
>>> print_to_file('TESTING PERFORMANCE')
>>> print_to_file(test_perf)

## python
>>> ## Boilerplate do mxnet.
>>> ## O padrão é 1 gpu cujo índice é 0
>>> devs = [mx.gpu(0)]
>>> module = mx.mod.Module(symbol,
>>> data_names=data_names,
>>> label_names=label_names,
>>> context=devs)
>>> module.bind(data_shapes=iter_train.provide_data,
>>> label_shapes=iter_train.provide_label)
>>> module.init_params(mx.initializer.Uniform(0.1))
>>> module.init_optimizer(optimizer='adam',
>>> optimizer_params={'learning_rate':
>>> args.lr})

98 | Capítulo 10: Aprendizado Profundo para Séries Temporais


## python
>>> for epoch in range( args.n_epochs):
>>> iter_train.reset()
>>> iter_val.reset()
>>> for batch in iter_train:
>>> module.forward(batch, is_train=True) # Calcula as predições.
>>> module.backward() # Calcula os gradientes.
>>> module.update() # Atualiza os parâmetros.

## python
>>> ## Resultados do treinamento.
>>> train_pred = module.predict(iter_train).asnumpy()
>>> train_label = iter_train.label[0][1].asnumpy()
>>> train_perf = evaluate_and_write(train_pred, train_label,
>>> save_dir, 'train', epoch)
>>>
>>> ## Resultados da validação.
>>> val_pred = module.predict(iter_val).asnumpy()
>>> val_label = iter_val.label[0][1].asnumpy()
>>> val_perf = evaluate_and_write(val_pred, val_label,
>>> save_dir, 'valid', epoch)
##
## python
python
>>>
>>> ##
if Resultados
epoch > 0: do treinamento.
>>>
>>> train_pred = module.predict(iter_train).asnumpy()
buf.append(val_perf['COR'] - old_val)
>>>
>>> train_label
if epoch > 2:= iter_train.label[0][1].asnumpy()
>>>
>>> train_perf = evaluate_and_write(train_pred, train_label,
vals = buf.get()
>>>
>>> vals = [v for v in vals if v save_dir,
!= 0] 'train', epoch)
>>>
>>> if sum([v < COR_THRESHOLD for v in vals]) == len(vals):
>>>
>>> ## Resultados da validação. EXIT')
print_to_file('EARLY
>>>
>>> val_predbreak
= module.predict(iter_val).asnumpy()
>>>
>>> val_label
old_val = =val_perf['COR']
iter_val.label[0][1].asnumpy()
>>> val_perf = evaluate_and_write(val_pred, val_label,
>>>
## python save_dir, 'valid', epoch)
>>> def evaluate_and_write(pred, label, save_dir, mode, epoch):
>>> if not os.path.exists(save_dir):
>>> os.makedirs(save_dir)
>>>
>>> pred_df = pd.DataFrame(pred)
>>> label_df = pd.DataFrame(label)
>>> pred_df.to_csv( os.path.join(save_dir, '%s_pred%d.csv'
>>> % (mode, epoch)))
>>> label_df.to_csv(os.path.join(save_dir, '%s_label%d.csv'
>>> % (mode, epoch)))
>>>
>>> return { 'COR': COR(label,pred) }

Análise Prática de Séries Temporais | 99


## python
>>> def COR(label, pred):
>>> label_demeaned = label - label.mean(0)
>>> label_sumsquares = np.sum(np.square(label_demeaned), 0)
>>>
>>> pred_demeaned = pred - pred.mean(0)
>>> pred_sumsquares = np.sum(np.square(pred_demeaned), 0)
>>>
>>> cor_coef = np.diagonal(np.dot(label_demeaned.T, pred_demeaned)) /
>>> np.sqrt(label_sumsquares * pred_sumsquares)
>>>
>>> return np.nanmean(cor_coef)

## python
>>> if __name__ == '__main__':

>>> # Parser dos argumentos de linha de comando.


>>> args = parser.parse_args()
>>> # Parser dos argumentos de linha de comando.
>>> # Cria
args iteradores de dados.
= parser.parse_args()
>>> iter_train, iter_val, iter_test = prepare_iters(
>>> args.data_dir,
# Cria args.win,
iteradores de dados. args.h,
>>> args.model,
iter_train, args.batch_n)
iter_val, iter_test = prepare_iters(
>>> args.data_dir, args.win, args.h,
>>> ## Prepara os símbolos.
args.model, args.batch_n)
>>> input_feature_shape = iter_train.provide_data[0][1]
>>> ## Prepara os símbolos.
>>> X = mx.sym.Variable(iter_train.provide_data[0].name
input_feature_shape = iter_train.provide_data[0][1] )
>>> Y = mx.sym.Variable(iter_train.provide_label[0].name)
>>> X = mx.sym.Variable(iter_train.provide_data[0].name )
>>> #Y Define o modelo.
= mx.sym.Variable(iter_train.provide_label[0].name)
>>> model_dict = {
>>> 'fc_model'
# Define o modelo. : fc_model,
>>> 'rnn_model'
model_dict = { : rnn_model,
>>> 'cnn_model'
'fc_model' cnn_model,
: fc_model,
>>> 'simple_lstnet_model' : rnn_model,
'rnn_model' simple_lstnet_model
>>> }
'cnn_model' : cnn_model,
>>> model = model_dict[args.model]
'simple_lstnet_model' : simple_lstnet_model
>>> }
>>> symbol,
model data_names, label_names = model(iter_train,
= model_dict[args.model]
>>> input_feature_shape,
>>> X, Y,
symbol, data_names, label_names = model(iter_train,
>>> args.win, args.sz_filt,
input_feature_shape,
>>> args.n_filt,
X, Y, args.drop)
>>> args.win, args.sz_filt,
>>> ## Treina. args.n_filt, args.drop)
>>> train(symbol, iter_train, iter_val, iter_test,
>>> data_names, label_names, args.save_dir)
## Treina.
>>> train(symbol, iter_train, iter_val, iter_test,
>>> data_names, label_names, args.save_dir)

## python
def print_to_file(msg):

print(msg, file = printFile, flush = True)esfa


print_to_file(args)

100 | Capítulo 10: Aprendizado Profundo para Séries Temporais


## python
>>> if model_name == 'fc_model':
>>> ## Fornece o primeiro e o segundo lookbacks em uma entrada achatada.
>>> X = np.hstack([x[1:-1], x[:-2]])
>>> Y = x[2:]
>>> return (X, Y)

## python
>>> def fc_model(iter_train, window, filter_size, num_filter, dropout):
>>> X = mx.sym.Variable(iter_train.provide_data[0].name)
>>> Y = mx.sym.Variable(iter_train.provide_label[0].name)
>>>
>>> output = mx.sym.FullyConnected(data=X, num_hidden = 20)
>>> output = mx.sym.Activation(output, act_type = 'relu')
>>> output = mx.sym.FullyConnected(data=output, num_hidden = 10)
>>> output = mx.sym.Activation(output, act_type = 'relu')
>>> output = mx.sym.FullyConnected(data = output, num_hidden = 321)
>>>
>>> loss_grad = mx.sym.LinearRegressionOutput(data = output,
>>> label = Y)
>>> return loss_grad, [v.name for v in iter_train.provide_data],
>>> [v.name for v in iter_train.provide_label]

Figura 10-5. A função tanh exibe comportamento não linear em valores pequenos antes
de se tornar funcionalmente constante, com uma derivada zero, em valores mais altos.

Figura 10-6. Apesar de a função ReLU ser fácil de ser calcular, ela introduz
uma não linearidade.

Análise Prática de Séries Temporais | 101


Figura 10-7. Um “mecanismo de atenção feedforward”.

Figura 10-8. Uma rede convolucional. Muitas janelas bidimensionais do tamanho


do kernel especificado deslizam pela imagem original, gerando muitos mapas de
características [feature maps] a partir dos pesos treináveis aplicados à imagem. Não
raro, eles são combinados e, depois, pós-processados com uma função de ativação. O
processo é repetido diversas vezes em várias camadas com o objetivo de reduzir muitas
características em um intervalo menor de valores, resultando, por exemplo, em um
sistema de classificação.

## python
>>> def cnn_model(iter_train, input_feature_shape, X, Y,
>>> win, sz_filt, n_filter, drop):
>>> conv_input = mx.sym.reshape(data=X, shape=(0, 1, win, -1))
>>> ## A convolução espera entrada 4d (N x channel x height x width)
>>> ## em nosso caso canal = 1 (semelhante a uma imagem em preto e branco.)
>>> ## height = time e width = canais recortam os locais de energia elétrica.
>>>
>>> cnn_output = mx.sym.Convolution(data=conv_input,

102 | Capítulo 10: Aprendizado Profundo para Séries Temporais


>>> kernel=(sz_filt,
>>> input_feature_shape[2]),
>>> num_filter=n_filter)
>>> cnn_output = mx.sym.Activation(data=cnn_output, act_type='relu')
>>> cnn_output = mx.sym.reshape(mx.sym.transpose(data=cnn_output,
>>> axes=(0, 2, 1, 3)),
>>> shape=(0, 0, 0))
>>> cnn_output = mx.sym.Dropout(cnn_output, p=drop)
>>>
>>> output = mx.sym.FullyConnected(data=cnn_output,
>>> num_hidden=input_feature_shape[2])
>>> loss_grad = mx.sym.LinearRegressionOutput(data=output, label=Y)
>>> return (loss_grad,
>>> [v.name for v in iter_train.provide_data],
>>> [v.name for v in iter_train.provide_label])

0 0.330701 0.292515
1 0.389125 0.349906
2 0.443271 0.388266
3 0.491140 0.442201
4 0.478684 0.410715
5 0.612608 0.564204
6 0.581578 0.543928
7 0.633367 0.596467
8 0.662014 0.586691
9 0.699139 0.600454
10 0.692562 0.623640
11 0.717497 0.650300
12 0.710350 0.644042
13 0.715771 0.651708
14 0.717952 0.651409
15 0.712251 0.655117
16 0.708909 0.645550
17 0.696493 0.650402
18 0.695321 0.634691
19 0.672669 0.620604
20 0.662301 0.597580
21 0.680593 0.631812
22 0.670143 0.623459
23 0.684297 0.633189
24 0.660073 0.604098

TESTING PERFORMANCE
{'COR': 0.5561901}

Análise Prática de Séries Temporais | 103


Figura 10-9. Este gráfico representa um componente importante da arquitetura WaveNet
(https://perma.cc/Z4KZ-ZXBQ). Aqui, vemos como as redes neurais convolucionais têm
modificações de arquitetura adequadas para séries temporais.

Figura 10-10. Uma visualização agradável e transparente de quatro tipos de séries


temporais, da esquerda para a direita: (1) ruído branco, (2) séries harmônicas/sazonais
com duas frequências, (3) dados caóticos com uma tendência e (4) um processo
autorregressivo. Fonte: Wikipédia (https://perma.cc/4BV2-57T4), fornecida por Norbert
Marwan em 2006.

104 | Capítulo 10: Aprendizado Profundo para Séries Temporais


Figura 10-11. Como uma arquitetura de rede neural recorrente é desenrolada uma vez
para cada intervalo de tempo quando aplicada aos dados.

## python
>>> ## Esse código foi escrito para funcionar com pesos exportados
## python
>>> ## da
EsseTensorFlow:
código foi escrito para funcionar com pesos exportados
>>> ## https://www.tensorflow.org/api_docs/python/tf/contrib/cudnn_rnn/CudnnGRU
da TensorFlow:
>>> ## Mas pode ser facilmente reaproveitado para acomodar outros pesos:
https://www.tensorflow.org/api_docs/python/tf/contrib/cudnn_rnn/CudnnGRU
>>> def calc_gru(X,
## Mas pode ser weights, num_inputs,
facilmente num_features):
reaproveitado para acomodar outros pesos:
>>> def Us = weights[:(3*num_features*num_inputs)]
calc_gru(X, weights, num_inputs, num_features):
>>> Us = np.reshape(Us, [3, num_features, num_inputs])
weights[:(3*num_features*num_inputs)]
>>> Us = np.reshape(Us, [3, num_features, num_inputs])
>>> Ws = weights[(3*num_features*num_inputs):(3*num_features*num_features +
>>> 3*num_features*num_inputs)]
Ws = weights[(3*num_features*num_inputs):(3*num_features*num_features +
>>> Ws = np.reshape(Ws, [3, num_features, num_features])
3*num_features*num_inputs)]
>>> Ws = np.reshape(Ws, [3, num_features, num_features])
>>> Bs = weights[(-6 * num_features) :]
>>> Bs = weights[(-6
np.reshape(Bs, [6, num_features])
* num_features) :]
>>> s ==np.zeros([129,
Bs np.reshape(Bs,num_features])
[6, num_features])
>>> sh = np.zeros([129, num_features])
>>> h = np.zeros([129, num_features])
>>> for t in range(X.shape[0]):
>>> for tz in
= sigmoid(np.matmul(Us[0,
range(X.shape[0]): :, :], X[t, :]) +
>>> np.matmul(Ws[0, :, :], s[t, :]) + :,
z = sigmoid(np.matmul(Us[0, Bs[0,
:],:] + Bs[3,
X[t, :]) +:])
r = sigmoid(np.matmul(Us[1,
>>> np.matmul(Ws[0, :, :],:]X[t,
:, :], s[t, :]) + Bs[0, :]) +:])
+ Bs[3,
>>> np.matmul(Ws[1, :, :], s[t, :]) + :,
r = sigmoid(np.matmul(Us[1, Bs[1,
:],:] + Bs[4,
X[t, :]) +:])
h[t+1, :] :,
>>> np.matmul(Ws[1, = np.tanh(np.matmul(Us[2,
:], s[t, :]) + Bs[1, :]:, :], X[t,
+ Bs[4, :]):]) +
>>> Bs[2, h[t+1,
:] + :] = np.tanh(np.matmul(Us[2, :, :], X[t, :]) +
>>> Bs[2,
r*(np.matmul(Ws[2,
:] + :, :], s[t, :]) + Bs[5, :]))
>>> r*(np.matmul(Ws[2,
s[t+1, :] = (1
:,-:],
z)*h[t
s[t,+:])
1, +:]Bs[5,
+ z*s[t,
:])):]
>>> s[t+1, :] = (1 - z)*h[t + 1, :] + z*s[t, :]
>>> return h, s
>>> return h, s

Análise Prática de Séries Temporais | 105


## python
>>> def rnn_model(iter_train, window, filter_size, num_filter,
>>> dropout):
>>> input_feature_shape = iter_train.provide_data[0][1]
>>> X = mx.sym.Variable(iter_train.provide_data[0].name)
>>> Y = mx.sym.Variable(iter_train.provide_label[0].name)
>>>
>>> rnn_cells = mx.rnn.SequentialRNNCell()
>>> rnn_cells.add(mx.rnn.GRUCell(num_hidden=args.rnn_units))
>>> rnn_cells.add(mx.rnn.DropoutCell(dropout))
>>> outputs, _ = rnn_cells.unroll(length=window, inputs=X,
>>> merge_outputs=False)
>>>

>>> output = mx.sym.FullyConnected(data=outputs[-1],


>>> num_hidden =
input_feature_shape[2])
>>> loss_grad = mx.sym.LinearRegressionOutput(data = output,
>>> label = Y)
>>>
>>> return loss_grad, [v.name for v in iter_train.provide_data],
>>> [v.name for v in iter_train.provide_label]

Epoch Training Cor Validation Cor


0 0.072042 0.069731
1 0.182215 0.172532
2 0.297282 0.286091
3 0.371913 0.362091
4 0.409293 0.400009
5 0.433166 0.422921
6 0.449039 0.438942
7 0.453482 0.443348
8 0.451456 0.444014
9 0.454096 0.448437
10 0.457957 0.452124
11 0.457557 0.452186
12 0.463094 0.455822
13 0.469880 0.461116
14 0.474144 0.464173
15 0.474631 0.464381
16 0.475872 0.466868
17 0.476915 0.468521
18 0.484525 0.477189
19 0.487937 0.483717
20 0.487227 0.485799
21 0.479950 0.478439
22 0.460862 0.455787
23 0.430904 0.427170
24 0.385353 0.387026

TESTING PERFORMANCE
{'COR': 0.36212805}

106 | Capítulo 10: Aprendizado Profundo para Séries Temporais


Figura 10-12. Um autoencoder, também conhecido como modelo seq2seq, é muito
popular para processamento e modelagem de linguagem, além de também ter
demonstrado sucesso considerável na análise de séries temporais.

Figura 10-13. Na arquitetura LSTNet modificada, podemos ver que há um componente


autorregressivo (parte inferior da imagem) em paralelo a uma arquitetura de rede
neural. A arquitetura da rede neural coloca um elemento convolucional e um elemento
recorrente em ordem consecutiva, operando nas mesmas entradas, um após o outro.

Análise Prática de Séries Temporais | 107


## python
>>> ## Deve ser 4d ou 5d para usar o padding.
>>> conv_input = mx.sym.reshape(data=X, shape=(0, 1, win, -1))
>>>
>>> ## Elemento convolucional.
>>> ## Adicionamos o padding no final do tempo win
>>> cnn_output = mx.sym.pad(data=conv_input,
>>> mode="constant",
>>> constant_value=0,
>>> pad_width=(0, 0,
>>> 0, 0,
>>> 0, sz_filt - 1,
>>> 0, 0))
>>> cnn_output = mx.sym.Convolution(data=cnn_output,
>>> kernel=(sz_filt,
>>> input_feature_shape[2]),
>>> num_filter=n_filter)
>>> cnn_output = mx.sym.Activation(data=cnn_output, act_type='relu')
>>> cnn_output = mx.sym.reshape(mx.sym.transpose(data=cnn_output,
>>> axes=(0, 2, 1, 3)),
>>> shape=(0, 0, 0))
>>> cnn_output = mx.sym.Dropout(cnn_output, p=drop)

## python
>>> ## Elemento recorrente
>>> stacked_rnn_cells = mx.rnn.SequentialRNNCell()
>>> stacked_rnn_cells.add(mx.rnn.GRUCell(num_hidden=args.rnn_units))
>>> outputs, _ = stacked_rnn_cells.unroll(length=win,
>>> inputs=cnn_output,
>>> merge_outputs=False)
>>>
>>> rnn_output = outputs[-1]
>>> n_outputs = input_feature_shape[2]
>>> cnn_rnn_model = mx.sym.FullyConnected(data=rnn_output,
>>> num_hidden=n_outputs)

## python
>>> ## Elemento ar
>>> ar_outputs = []
>>> for i in list(range(input_feature_shape[2])):
>>> ar_series = mx.sym.slice_axis(data=X,
>>> axis=2,
>>> begin=i,
>>> end=i+1)
>>> fc_ar = mx.sym.FullyConnected(data=ar_series, num_hidden=1)
>>> ar_outputs.append(fc_ar)
>>> ar_model = mx.sym.concat(*ar_outputs, dim=1)

108 | Capítulo 10: Aprendizado Profundo para Séries Temporais


## python
>>> def simple_lstnet_model(iter_train, input_feature_shape, X, Y,
>>> win, sz_filt, n_filter, drop):
>>> ## Deve ser 4d ou 5d para usar o padding.
>>> conv_input = mx.sym.reshape(data=X, shape=(0, 1, win, -1))
>>>
>>> ## Elemento convolucional.
>>> ## Adicionamos o padding no final do tempo win.
>>> cnn_output = mx.sym.pad(data=conv_input,
>>> mode="constant",
>>> constant_value=0,
>>> pad_width=(0, 0,
>>> 0, 0,
>>> 0, sz_filt - 1,
>>> 0, 0))
>>> cnn_output = mx.sym.Convolution(data = cnn_output,
>>> kernel = (sz_filt,
>>> input_feature_shape[2]),
>>> num_filter = n_filter)
>>> cnn_output = mx.sym.Activation(data = cnn_output,
>>> act_type = 'relu')
>>> cnn_output = mx.sym.reshape(mx.sym.transpose(data = cnn_output,
>>> axes = (0, 2, 1, 3)),
>>> shape=(0, 0, 0))
>>> cnn_output = mx.sym.Dropout(cnn_output, p = drop)
>>>
>>> ## Elemento recorrente.
>>> stacked_rnn_cells = mx.rnn.SequentialRNNCell()
>>> stacked_rnn_cells.add(mx.rnn.GRUCell(num_hidden = args.rnn_units))
>>> outputs, _ = stacked_rnn_cells.unroll(length = win,
>>> inputs = cnn_output,
>>> merge_outputs = False)
>>> rnn_output = outputs[-1]
>>> n_outputs = input_feature_shape[2]
>>> cnn_rnn_model = mx.sym.FullyConnected(data=rnn_output,
>>> num_hidden=n_outputs)
>>> ## Elemento ar.
>>> ar_outputs = []
>>> for i in list(range(input_feature_shape[2])):
>>> ar_series = mx.sym.slice_axis(data=X,
>>> axis=2,
>>> begin=i,
>>> end=i+1)
>>> fc_ar = mx.sym.FullyConnected(data = ar_series,
num_hidden = 1)
>>> ar_outputs.append(fc_ar)
>>> ar_model = mx.sym.concat(*ar_outputs, dim=1)
>>>
>>> output = cnn_rnn_model + ar_model
>>> loss_grad = mx.sym.LinearRegressionOutput(data=output, label=Y)
>>> return (loss_grad,
>>> [v.name for v in iter_train.provide_data],
>>> [v.name for v in iter_train.provide_label])

Análise Prática de Séries Temporais | 109


Epoch Training Cor Validation Cor
0 0.256770 0.234937
1 0.434099 0.407904
2 0.533922 0.506611
3 0.591801 0.564167
4 0.630204 0.602560
5 0.657628 0.629978
6 0.678421 0.650730
7 0.694862 0.667147
8 0.708346 0.680659
9 0.719600 0.691968
10 0.729215 0.701734
11 0.737400 0.709933
12 0.744532 0.717168
13 0.750767 0.723566
14 0.756166 0.729052
15 0.760954 0.733959
16 0.765159 0.738307
17 0.768900 0.742223
18 0.772208 0.745687
19 0.775171 0.748792
20 0.777806 0.751554
21 0.780167 0.754034
22 0.782299 0.756265
23 0.784197 0.758194
24 0.785910 0.760000

TESTING PERFORMANCE
{'COR': 0.7622162}

110 | Capítulo 10: Aprendizado Profundo para Séries Temporais


CAPÍTULO 11
Medição de Erros

Figura 11-1. O padrão de excelência para avaliar o desempenho de um modelo de série


temporal, treinamento roll-forward, validação e janelas de teste.

111
## R
> require(forecast)
>
> phi <- 0.7
> time_steps <- 24
> N <- 1000
> sigma_error <- 1
>
> sd_series <- sigma_error^2 / (1 - phi^2)
> starts <- rnorm(N, sd = sqrt(sd_series))
> estimates <- numeric(N)
> res <- numeric(time_steps)
>
> for (i in 1:N) {
> errs = rnorm(time_steps, sd = sigma_error)
> res[1] <- starts[i] + errs[1]
>
> for (t in 2:time_steps) {
> res[t] <- phi * tail(res, 1) + errs[t]
> }
> estimates <- c(estimates, arima(res, c(1, 0, 0))$coef[1])
> }
>
> hist(estimates,
> main = "Estimated Phi for AR(1) when ts is AR(1)",
> breaks = 50)

Figura 11-2. A distribuição das estimativas [estimates] para ϕ.

112 | Capítulo 11: Medição de Erros


## R
> summary(estimates1)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.3436 0.4909 0.6224 0.5919 0.7204 0.9331

## R
> ## Agora vamos assumir que temos um verdadeiro processo AR (2),
> ## e como isso é mais complicado, vamos mudar para arima.sim
> phi_1 <- 0.7
> phi_2 <- -0.2
>
> estimates <- numeric(N)
> for (i in 1:N) {
> res <- arima.sim(list(order = c(2,0,0),
> ar = c(phi_1, phi_2)),

> n = time_steps)
> estimates[i] <- arima(res, c(1, 0, 0))$coef[1]
> }
>
> hist(estimates,
> main = "Estimated Phi for AR(1) when ts is AR(2)",
> breaks = 50)

Figura 11-3. A distribuição estimada do coeficiente do lag 1 para um modelo AR (1) se


ajusta a um processo para o qual a verdadeira representação é um processo AR (2).

## R
> summary(estimates)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.5252 0.4766 0.6143 0.5846 0.7215 0.9468

Análise Prática de Séries Temporais | 113


CAPÍTULO 12
Considerações de Desempenho em
Ajustes e Disponibilização de Modelos de
Séries Temporais

## python
>>> def array_to_ts(arr):
>>> idx = 0
>>> while idx + 6 <= arr.shape[0]:
>>> yield arr[idx:(idx+6)]
CAPÍTULO 13
Aplicações na Área de Assistência Médica

## R
> flu = fread("train.csv")
> flu[, flu.rate := as.numeric(TauxGrippe)]
> head(flu)
Id week region_code region_name TauxGrippe flu.rate
1: 3235 201352 42 ALSACE 7 7
2: 3236 201352 72 AQUITAINE 0 0
3: 3237 201352 83 AUVERGNE 88 88
4: 3238 201352 25 BASSE-NORMANDIE 15 15
5: 3239 201352 26 BOURGOGNE 0 0
6: 3240 201352 53 BRETAGNE 67 67

## R
> nrow(flu[is.na(flu.rate)]) / nrow(flu)
[1] 0.01393243
> unique(flu[is.na(flu.rate)]$region_name)
[1] "HAUTE-NORMANDIE" "NORD-PAS-DE-CALAIS" "PICARDIE"
[4] "LIMOUSIN" "FRANCHE-COMTE" "CENTRE"
[7] "AUVERGNE" "BASSE-NORMANDIE" "BOURGOGNE"
[10] "CHAMPAGNE-ARDENNE" "LANGUEDOC-ROUSSILLON" "PAYS-DE-LA-LOIRE"
[13] "CORSE"

## R
> flu[, year := as.numeric(substr(week, 1, 4))]
> flu[, wk := as.numeric(substr(week, 5, 6))]
> ## O estilo não é ótimo, temos colunas de 2 semanas.

## R
> flu[, date:= as.Date(paste0(as.character(flu$week), "1"), "%Y%U%u")]

115
## R
## Vamos focar Paris.
> paris.flu = flu[region_name == "ILE-DE-FRANCE"]
> paris.flu = paris.flu[order(date, decreasing = FALSE)]

> paris.flu[, .(week, date, flu.rate)]


week date flu.rate
1: 200401 2004-01-05 66
2: 200402 2004-01-12 74
3: 200403 2004-01-19 88
4: 200404 2004-01-26 26
5: 200405 2004-02-02 17
---
518: 201350 2013-12-16 13
519: 201351 2013-12-23 49
520: 201352 2013-12-30 24
521: 200953 <NA> 145
522: 200453 <NA> 56

## R
> paris.flu[, .N, year]
year N
1: 2004 53
2: 2005 52
...
9: 2012 52
10: 2013 52

> paris.flu[, .N, wk]


wk N
1: 1 10
2: 2 10
3: 3 10
...
51: 51 10
52: 52 10
53: 53 2
wk N

## R
> paris.flu[, plot(date, flu.rate,
> type = "l", xlab = "Date",
> ylab = "Flu rate")]

## R
> paris.flu <- paris.flu[week != 53]

116 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-1. Ao plotar a série temporal do índice da gripe [flu rate], podemos ver a
sazonalidade do índice da gripe em Paris.

## R
> acf(paris.flu$flu.rate, )
> acf(diff(paris.flu$flu.rate, 52))

Figura 13-2. No primeiro gráfico, plotamos a função de autocorrelação para o índice da gripe
de Paris e, no segundo gráfico, plotamos o índice da gripe de Paris diferenciado. Analisamos
apenas um intervalo limitado de valores de lag.

Análise Prática de Séries Temporais | 117


## R
> acf(paris.flu$flu.rate, lag.max = 104)
> acf(diff(paris.flu$flu.rate, 52), lag.max = 104)

Figura 13-3. No primeiro gráfico, plotamos a função de autocorrelação para o índice da


gripe de Paris e, no segundo gráfico, plotamos o índice da gripe de Paris diferenciado. Agora
examinamos um leque mais amplo de valores de lag.

## R
> plot(diff(diff(paris.flu$flu.rate, 52), 52))
> plot(diff(diff(paris.flu$flu.rate, 52), 1))

118 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-4. Gráfico de duas versões de diferenciação de nossa série para ter uma ideia do
comportamento sazonal nos dados.

## R
> par(mfrow = c(2, 1))
> acf (diff(diff(paris.flu$flu.rate, 52), 1), lag.max = 104)
> pacf(diff(diff(paris.flu$flu.rate, 52), 1), lag.max = 104)

Análise Prática de Séries Temporais | 119


Figura 13-5. Gráfico da função de autocorrelação parcial das séries diferenciadas
que selecionamos.

## R
> ## Ajuste do arima.
> ## Vamos estimar 2 semanas à frente.
> first.fit.size <- 104
> h <- 2
> n <- nrow(paris.flu) - h - first.fit.size

120 | Capítulo 13: Aplicações na Área de Assistência Médica


>
> ## gObtém as dimensões padrão para ajustes que produziremos
> ## e as informações relacionadas, como os coeficientes.
> first.fit <- arima(paris.flu$flu.rate[1:first.fit.size], order = c(2, 1, 0),
> seasonal = list(order = c(0,1,0), period = 52))
> first.order <- arimaorder(first.fit)
>
> ## Pré-alocar espaço para armazenar nossas predições e coeficientes.
> fit.preds <- array(0, dim = c(n, h))
> fit.coefs <- array(0, dim = c(n, length(first.fit$coef)))
>
> ## Após ajuste inicial, avançamos o ajuste uma semana de cada vez,
> ## cada vez reajustando o modelo
> ## e salvando os novos coeficientes e a nova previsão.
> ## Atenção! Este loop demora um pouco para rodar.
> for (i in (first.fit.size + 1):(nrow(paris.flu) - h)) {
> ## predict for an increasingly large window
> data.to.fit = paris.flu[1:i]
> fit = arima(data.to.fit$flu.rate, order = first.order[1:3],
> seasonal = first.order[4:6])
> fit.preds[i - first.fit.size, ] <- forecast(fit, h = 2)$mean
> fit.coefs[i - first.fit.size, ] <- fit$coef
> }

## R
> ylim <- range(paris.flu$flu.rate[300:400],
> fit.preds[, h][(300-h):(400-h)])
> par(mfrow = c(1, 1))
> plot(paris.flu$date[300:400], paris.flu$flu.rate[300:400],
> ylim = ylim, cex = 0.8,
> main = "Actual and predicted flu with SARIMA (2, 1, 0), (0, 1, 0)",
> xlab = "Date", ylab = "Flu rate")
> lines(paris.flu$date[300:400], fit.preds[, h][(300-h):(400-h)],
> col = 2, type = "l",
> lty = 2, lwd = 2)

Análise Prática de Séries Temporais | 121


Figura 13-6. Os índices da gripe [flu rate] (pontos) acompanhados de nossas previsões
SARIMA (linha pontilhada). As predições desse modelo simples podem auxiliar no
planejamento da saúde pública.

## R
> ## Pré-aloca os vetores para manter coeficientes e ajustes.
> fit.preds <- array(0, dim = c(n, h))
> fit.coefs <- array(0, dim = c(n, 100))
>
> ## Regressores exógenos.
> ## que são componentes da série de Fourier ajustados aos dados
> flu.ts <- ts(log(paris.flu$flu.rate + 1) + 0.0001,
> frequency = 52)
> ## Adiciona pequenos deslocamentos,
> ## pois os valores O pequenos causam problemas numéricos no ajuste.
> exog.regressors <- fourier(flu.ts, K = 2)
> exog.colnames <- colnames(exog.regressors)

122 | Capítulo 13: Aplicações na Área de Assistência Médica


>
> ## Ajusta o modelo novamente a cada semana
> ## com a janela de expansão dos dados de treinamento.
> for (i in (first.fit.size + 1):(nrow(paris.flu) - h)) {
> data.to.fit <- ts(flu.ts[1:i], frequency = 52)
> exogs.for.fit <- exog.regressors[1:i,]
> exogs.for.predict <- exog.regressors[(i + 1):(i + h),]
>
> fit <- auto.arima(data.to.fit,
> xreg = exogs.for.fit,
> seasonal = FALSE)
>
> fit.preds[i - first.fit.size, ] <- forecast(fit, h = h,
> xreg = exogs.for.predict)$mean
> fit.coefs[i - first.fit.size, 1:length(fit$coef)] = fit$coef
> }

## R
> flu.ts = ts(log(paris.flu$flu.rate + 1) + 0.0001, ## add epsilon
> frequency = 52)

## R
> exog.regressors <- fourier(flu.ts, K = 2)
> exog.colnames <- colnames(exog.regressors)

## R
> fit <- auto.arima(data.to.fit,
> xreg = exogs.for.fit,
> seasonal = FALSE)

## R
> fit.preds[i - first.fit.size, ] <- forecast(fit, h = 2h,
> xreg = exogs.for.predict)$mean

## R
> ylim = range(paris.flu$flu.rate)
> plot(paris.flu$date[300:400], paris.flu$flu.rate[300:400],
> ylim = ylim, cex = 0.8,
> main = "Actual and predicted flu with ARIMA +
> harmonic regressors",
> xlab = "Date", ylab = "Flu rate")
> lines(paris.flu$date[300:400], exp(fit.preds[, h][(300-h):(400-h)]),
> col = 2, type = 'l',
> lty = 2, lwd = 2)

Análise Prática de Séries Temporais | 123


Figura 13-7. Gráfico dos índices reais de gripe (pontos) em comparação com nossas predições
do modelo de regressão harmônica dinâmica ARIMA (linha tracejada).

## R
> plot(test$flu.rate)
> which(test$flu.rate > 400)
Error in which(res$flu.rate > 400) : object 'res' not found
> which(test$flu.rate > 400)
[1] 1 59 108 109 110 111 112 113
> abline(v = 59)
> which.max(test$flu.rate)
[1] 109
> abline(v = 109)

124 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-8. Gráfico que contempla apenas os valores de teste para a gripe e seus locais de
pico aparentes.

## R
> files <- list.files(full.names = TRUE)
> files <- grep("entries", files, value = TRUE)
> dt <- data.table()
> for (f in files) {
> dt <- rbindlist(list(dt, fread(f)))
> }
>
> ## Remove o na das colunas
> dt <- dt[!is.na(sgv)]

## R
> dt[, timestamp := as.POSIXct(date)]
> ## Funciona para mim porque meu computador está no horário EST
> ## (horário da Costa Leste dos EUA), mas pode não funcionar para você.
>
> ## Forma adequada.
> dt[, timestamp := force_tz(as.POSIXct(date), "EST")]
>
> ## Ordem cronológica.
> dt = dt[order(timestamp, decreasing = FALSE)]

Análise Prática de Séries Temporais | 125


## R
> head(dt[, .(date, sgv)])
date sgv
1: 2015-02-18 06:30:09 162
2: 2015-02-18 06:30:09 162
3: 2015-02-18 06:30:09 162
4: 2015-02-18 06:30:09 162
5: 2015-02-18 06:35:09 154
6: 2015-02-18 06:35:09 154

## R
> dt <- dt[!duplicated(dt)]

## R
> nrow(dt)
[1] 24861
> length(unique(dt$timestamp))
[1] 23273
> ## Ainda temos dados duplicados, tanto quanto timestamps,
> ## por isso os deletaremos.

## R
> ## Podemos identificar um exemplo usando dt[, .N, timestamp][order(N)]
> ## analisando a maioria dos dados repetidos.
> dt[date == "2015-03-10 06:27:19"]
device date dateString sgv direction type
1: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 66 Flat sgv
2: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 70 Flat sgv
3: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 66 Flat sgv
4: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 70 Flat sgv
5: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 66 Flat sgv
6: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 70 Flat sgv
7: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 66 Flat sgv
8: dexcom 2015-03-10 06:27:19 Tue Mar 10 06:27:18 EDT 2015 70 Flat sgv
## Um pouco mais de inspeção sugere que isso não é muito importante.

## R
> dt <- unique(dt, by=c("timestamp"))

126 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-9. O domínio de tempo integral e o intervalo de valor dos dados disponíveis neste
gráfico ingênuo da série temporal. Infelizmente, os dados são tão dispersos no tempo e
incoerentes que o gráfico não nos fornece uma boa compreensão do comportamento
do sistema.

## R
> ## Vamos dar uma olhada em algumas janelas de tempo mais curtas nos dados.
> start.date <- as.Date("2015-01-01")
> stop.date <- as.Date("2015-04-01")

> dt[between(timestamp, start.date, stop.date),


> plot(timestamp, sgv, cex = .5)]

Análise Prática de Séries Temporais | 127


Figura 13-10. Focar um segmento específico da série temporal ajuda, porém é um segmento
muito compacto para termos uma noção de qualquer tipo de dinâmica da série temporal.
Devemos ampliar ainda mais o eixo do tempo.

## R
> ## Analise também um único dia para ver com o que se parecer
> ## Se tivéssemos mais tempo, provavelmente deveríamos fazer histogramas 2D de dias
> ## e ver como isso funciona.
> par(mfrow = c(2, 1))
> start.date = as.Date("2015-07-04")
> stop.date = as.Date("2015-07-05")
> dt[between(timestamp, start.date, stop.date),
> plot(timestamp, sgv, cex = .5)]
>
> start.date = as.Date("2015-07-05")
> stop.date = as.Date("2015-07-06")
> dt[between(timestamp, start.date, stop.date),
> plot(timestamp, sgv, cex = .5)]
> ## Se tivéssemos mais dias, poderíamos fazer algumas análises dos “day shapes",
> ## mas aqui parece que não temos dados suficientes.

128 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-11. Gráfico dos dados de dois dias específicos em julho. Podemos ver o surgimento
de uma espécie de padrão diário. Por fim, estamos examinando os dados em uma escala
temporal em que o olho humano pode entender o que está ocorrendo.

## R
> ## Devemos considerar uma característica para o mês do ano.
> ## Talvez haja efeitos sazonais.
> dt[, month := strftime(timestamp, "%m")]
>
> dt[, local.hour := as.numeric(strftime(timestamp, "%H"))]
> ## Percebemos que algumas horas do dia estão excessivamente representadas
> ## porque essa pessoa não está usando o dispositivo de forma consistente
> ## ou talvez o dispositivo tenha problemas de registro ou funcionalidade.
> ## Como é um pico estranho à meia-noite,
> ## isso parece sugerir problemas com o dispositivo em vez do usuário
> ## (quem liga e desliga o dispositivo à meia-noite?).
>
> hist(dt$local.hour)

## R
> ## Os valores do índice glicêmico tendem a depender da hora do dia.
> plot(dt[, mean(sgv), local.hour][order(local.hour)], type = 'l',
> ylab = "Mean blood glucose per local hour",
> xlab = "Local hour")

Análise Prática de Séries Temporais | 129


Figura 13-12. A partir do histograma de hora local, podemos observar que os horários em
que o CGM está coletando e informando os dados não são aleatórios. A grande concentração
por volta da meia-noite também sugere uma irregularidade nos relatórios ou no
funcionamento do dispositivo, que provavelmente não se deve ao comportamento do usuário
(é pouco provável que o usuário esteja manipulando o dispositivo a essa hora).

Figura 13-13. A média do índice glicêmico no sangue varia bastante conforme a hora do dia.

130 | Capítulo 13: Aplicações na Área de Assistência Médica


## R
> nth.pos = function(x, pos) {
> names(sort(-table(x)))[pos]
> ## Temos esse trecho de código graças ao grupo de usuários r
> }
> dt[, nth.pos(direction, 1), local.hour][order(local.hour)]
local.hour V1
1: 0 Flat
2: 1 Flat
3: 2 Flat
...
21: 20 Flat
22: 21 Flat
23: 22 Flat
24: 23 Flat
local.hour V1

## R
> dt[, nth.pos(direction, 2), local.hour][order(local.hour)]
local.hour V1
1: 0 FortyFiveUp
2: 1 FortyFiveUp
3: 2 FortyFiveUp
4: 3 FortyFiveDown
5: 4 FortyFiveDown
6: 5 FortyFiveUp
7: 6 FortyFiveDown
8: 7 FortyFiveDown
9: 8 FortyFiveDown
10: 9 FortyFiveUp
11: 10 FortyFiveUp
12: 11 FortyFiveDown
13: 12 FortyFiveDown
14: 13 FortyFiveDown
15: 14 FortyFiveDown
16: 15 FortyFiveDown
17: 16 FortyFiveUp
18: 17 FortyFiveUp
19: 18 FortyFiveDown
20: 19 FortyFiveUp
21: 20 FortyFiveUp
22: 21 FortyFiveUp
23: 22 FortyFiveUp
24: 23 FortyFiveDown
local.hour V1

## R
> ## PRIMEIRO, não faça isso da maneira errada!
> ## Nota: cuidado ao calcular as diferenças de tempo ingenuamente!
> as.numeric(dt$timestamp[10] - dt$timestamp[1])
[1] 40
> as.numeric(dt$timestamp[1000] - dt$timestamp[1])
[1] 11.69274
> dt$timestamp[1000] - dt$timestamp[1]
Time difference of 11.69274 days

Análise Prática de Séries Temporais | 131


## R
> dt[, delta.t := as.numeric(difftime(timestamp, shift(timestamp, 6),
> units = 'mins'))]
> dt[, valid.sd := !is.na(delta.t) & delta.t < 31]
> dt[, .N, valid.sd]
valid.sd N
1: FALSE 1838
2: TRUE 21435

## R
> dt[, sd.window := 0]
> for (i in 7:nrow(dt)) {
> dt[i, ]$sd.window = sd(dt[(i-6):i]$sgv)
> }
> ## Imputaremos os dados ausentes para os casos sd inválidos
> ## preenchendo a média da população (alerta de LOOKAHEAD, mas estamos cientes).
> imputed.val = mean(dt[valid.sd == TRUE]$sd.window)
> dt[valid.sd == FALSE, sd.window := imputed.val]

## R
> ## Agora também precisamos preencher nosso valor y,
> ## o alvo real.
> ## Isso também exigirá que verifiquemos a validade quando calculamos sd
> ## por causa dos dados atemporais contínuos no mesmo data.table.
> ## Vamos tentar prever 30 minutos à frente
> ## (previsões mais curtas são mais fáceis).
>
> ## udamos para 6 porque estamos amostrando a cada 5 minutos.
> dt[, pred.delta.t := as.numeric(difftime(shift(timestamp, 6,
> type = "lead"),
> timestamp,
> units = 'mins'))]
> dt[, pred.valid := !is.na(pred.delta.t) & pred.delta.t < 31]
>
> dt[, target := 0]
> for (i in 1:nrow(dt)) {
> dt[i, ]$target = dt[i + 6]$sgv
> }

132 | Capítulo 13: Aplicações na Área de Assistência Médica


## R
> ## Agora devemos verificar o nosso trabalho.
> i = 300
> dt[i + (-12:10), .(timestamp, sgv, target, pred.valid)]
timestamp sgv target pred.valid
1: 2015-02-19 16:15:05 146 158 TRUE
2: 2015-02-19 16:20:05 150 158 TRUE
3: 2015-02-19 16:25:05 154 151 FALSE
4: 2015-02-19 16:30:05 157 146 FALSE
5: 2015-02-19 16:35:05 160 144 FALSE
6: 2015-02-19 16:40:05 161 143 FALSE
7: 2015-02-19 16:45:05 158 144 FALSE
8: 2015-02-19 16:50:05 158 145 FALSE
9: 2015-02-19 17:00:05 151 149 TRUE
10: 2015-02-19 17:05:05 146 153 TRUE
11: 2015-02-19 17:10:05 144 154 TRUE
12: 2015-02-19 17:15:05 143 155 TRUE
13: 2015-02-19 17:20:05 144 157 TRUE
14: 2015-02-19 17:25:05 145 158 TRUE
15: 2015-02-19 17:30:05 149 159 TRUE
16: 2015-02-19 17:35:05 153 161 TRUE
17: 2015-02-19 17:40:05 154 164 TRUE
18: 2015-02-19 17:45:05 155 166 TRUE
19: 2015-02-19 17:50:05 157 168 TRUE
20: 2015-02-19 17:55:05 158 170 TRUE
21: 2015-02-19 18:00:04 159 172 TRUE
22: 2015-02-19 18:05:04 161 153 FALSE
23: 2015-02-19 18:10:04 164 149 FALSE
timestamp sgv target pred.valid

## R
> ## Vamos dividir o dia em três partes, em vez de
> ## segmentos de 24 horas. Fazemos isso com base na
> ## noção de um dia "típico".
> dt[, day.q.1 := between(local.hour, 5, 10.99)]
> dt[, day.q.2 := between(local.hour, 11, 16.99)]
> dt[, day.q.3 := between(local.hour, 17, 22.99)]
> dt[, day.q.4 := !day.q.1 & !day.q.2 & !day.q.3]

## R
> ## Vamos definir um rótulo a "winter/not winter" em vez de
> ## um rótulo mensal. Esta decisão é parcialmente baseada na
> ## distribuição temporal de nossos dados.
> dt[, is.winter := as.numeric(month) < 4]

## R
> ## Precisamos também usar a codificação na características
> ## direction e limpar um pouco os dados.
> dt[direction == "NOT COMPUTABLE", direction := "NOT_COMPUTABLE"]
> dir.names = character()
> for (nm in unique(dt$direction)) {
> new.col = paste0("dir_", nm)
> dir.names = c(dir.names, new.col)
> dt[, eval(parse(text = paste0(new.col, " :=
(direction == '", nm, "')")))]
> }

Análise Prática de Séries Temporais | 133


## R
> ## Precisamos definir dados de treinamento e teste para testá-los.
> ## Não queremos que todos os testes cheguem ao final do período de teste,
> ## e como hipotetizamos que alguns comportamentos podem ser sazonais,
> ## testaremos os dados de teste e finais de ambas as “temporadas”.
> winter.data <- dt[is.winter == TRUE]
> train.row.cutoff <- round(nrow(winter.data) * .9)
> train.winter <- winter.data[1:train.row.cutoff]
> test.winter <- winter.data[(train.row.cutoff + 1): nrow(winter.data)]
>
> spring.data <- dt[is.winter == FALSE]
> train.row.cutoff <- round(nrow(spring.data) * .9)
> train.spring <- spring.data[1:train.row.cutoff]
> test.spring <- spring.data[(train.row.cutoff + 1): nrow(spring.data)]
>
> train.data <- rbindlist(list(train.winter, train.spring))
> test.data <- rbindlist(list(test.winter, test.spring))
>
> ## Agora inclua apenas as colunas que devemos
> ## usar valores categóricos: valid.sd, day.q.1, day.q.2, day.q.3, is.winter
> ## mais todos os nomes das colunas 'dir_'.
> col.names <- c(dir.names, "sgv", "sd.window", "valid.sd",
> "day.q.1", "day.q.2", "day.q.3", "is.winter")
>
> train.X <- train.data[, col.names, with = FALSE]
> train.Y <- train.data$target
>
> test.X <- test.data[, col.names, with = FALSE]
> test.Y <- test.data$target

## R
> model <- xgboost(data = as.matrix(train.X), label = train.Y,
> max.depth = 2,
> eta = 1,
> nthread = 2,
> nrounds = 250,
> objective = "reg:linear")
> y.pred <- predict(model, as.matrix(test.X))

## R
> ## Agora, vamos considerar um dia específico.
> test.data[date < as.Date("2015-03-17")]
> par(mfrow = c(1, 1))
> i <- 1
> j <- 102
> ylim <- range(test.Y[i:j], y.pred[i:j])
> plot(test.data$timestamp[i:j], test.Y[i:j], ylim = ylim)
> points(test.data$timestamp[i:j], y.pred[i:j], cex = .5, col = 2)

134 | Capítulo 13: Aplicações na Área de Assistência Médica


Figura 13-14. Em larga escala, nossas predições aparentemente são boas, mas, por ora, é
difícil analisá-las se não as ampliarmos. Não é assim que alguém usando a previsão
a testaria.

Figura 13-15. Analisar dias específicos de cada vez oferece uma perspectiva melhor sobre o
funcionamento do nosso algoritmo de predição.

Análise Prática de Séries Temporais | 135


## R
> par(mfrow = c(2, 1))
> high.idx = which(test.Y > 300)
> plot(test.Y[high.idx], y.pred[high.idx], xlim = c(200, 400),
> ylim = c(200, 400))
> cor(test.Y[high.idx], y.pred[high.idx])
[1] 0.3304997
>
> low.idx = which((test.Y < 60 & test.Y > 10))
> plot(test.Y[low.idx], y.pred[low.idx], xlim = c(0, 200),
> ylim = c(0, 200))
> cor(test.Y[low.idx], y.pred[low.idx])
[1] 0.08747175

Figura 13-16. Gráfico das predições e dos valores reais comparados individualmente em cada
extremidade. Na parte superior, representamos os valores comparados com os valores altos
dos índices glicêmicos medidos no sangue e, na parte inferior, comparamos os valores baixos.

136 | Capítulo 13: Aplicações na Área de Assistência Médica


CAPÍTULO 14
Aplicações na Área Financeira

## python
>>> df = pd.read_csv("sp500.csv")
>>> df.head()
>>> df.tail()
>>> df.index = df.Date
>>> df.Close.plot()

Figura 14-1. Dados brutos do início e do final do arquivo CSV. Observe que os valores
mudam visivelmente de 1990 a 2019 — nenhuma surpresa para quem conhece a história
dos mercados financeiros dos Estados Unidos.

137
Figura 14-2. A cotação diária de fechamento do S&P 500 não é uma série
temporal estacionária.

## python
>>> ## Escolhe três semanas (seg-sex) de anos diferentes.
>>> ## Escala a cotação de fechamento de cada dia pelas médias
>>> ## das cotações de fechamento da semana. 1990.
>>> vals = df['1990-05-07':'1990-05-11'].Close.values
>>> mean_val = np.mean(vals)
>>> plt.plot([1, 2, 3, 4, 5], vals/mean_val)
>>> plt.xticks([1, 2, 3, 4, 5],
>>> labels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'])
>>>
>>> ## 2000
>>> vals = df['2000-05-08':'2000-05-12'].Close.values
>>> mean_val = np.mean(vals)
>>> plt.plot([1, 2, 3, 4, 5], vals/mean_val)
>>> plt.xticks([1, 2, 3, 4, 5],
>>> labels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'])
>>>
>>> ## 2018
>>> vals = df['2018-05-07':'2018-05-11'].Close.values
>>> mean_val = np.mean(vals)
>>> plt.plot([1, 2, 3, 4, 5], vals/mean_val)
>>> plt.xticks([1, 2, 3, 4, 5],
>>> labels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'])

138 | Capítulo 14: Aplicações na Área Financeira


Figura 14-3. Cotações de fechamento médias escalonadas para uma semana em maio de
1990, 2000 e 2018.

## python
>>> df['Return'] = df.Close - df.Open
>>> df.Return.plot()

Análise Prática de Séries Temporais | 139


Figura 14-4. Os retornos diários [daily returns] demonstram uma média próxima de
zero ao longo do tempo, porém sua variância muda acentuadamente em diferentes
períodos de tempo. Esse comportamento inspirou modelos como o GARCH, analisado
brevemente no Capítulo 6

## python
>>> df['DailyVolatility'] = df.High - df.Low
>>> df.DailyVolatility.plot()

Figura 14-6. A volatilidade diária é uma série temporal de valor positivo (por definição)
que mostra graus de variância visivelmente diferentes em diferentes pontos da série
temporal S&P 500.

## python
>>> ewdf = df.ewm(halflife = 10).mean()
>>> ewdf.DailyVolatility.plot()

140 | Capítulo 14: Aplicações na Área Financeira


Figura 14-7. O gráfico da média móvel exponencialmente ponderada da volatilidade
diária é mais suavizado do que o gráfico dos valores brutos, porém ainda nos mostra
uma série temporal não estacionária.

## python
>>> ## Calcula a variância móvel exponencialmente ponderada.
>>> vewdf = df.ewm(halflife = 10).var()
>>>
>>> ## Escalona penalizando e normalizando.
>>> scaled = df.DailyVolatility - ewdf.DailyVolatility
>>> scaled = scaled / vewdf.DailyVolatility**0.5
>>> scaled.plot()

Figura 14-8. A transformação dos dados com a média ponderada exponencialmente e


variância resulta em uma série temporal mais uniforme, com valores comparáveis ao
longo do período de 1990 a 2019.

Análise Prática de Séries Temporais | 141


## python
>>> df['ScaledVolatility'] = ((df.DailyVolatility -
>>> ewdf.DailyVolatility)
>>> / vewdf.DailyVolatility**0.5 )
>>> df['ScaledReturn'] = ((df.Return - ewdf.Return)
>>> / vewdf.Return**0.5 )
>>> df['ScaledVolume'] = ((df.Volume - ewdf.Volume)
>>> / vewdf.Volume**0.5 )

## python
>>> df = df.dropna()

## python
>>> ## Divide nossos dados em componentes de treinamento e teste.
>>> train_df = df[:7000]
>>> test_df = df[7000:]
>>>
>>> ## Constrói nossas variáveis de pipeline a partir de dados de treinamento,
>>> ## tomando apenas valores de interesse de data frames maiores.
>>> horizon = 10
>>> X = train_df[:(7000 - horizon)][["ScaledVolatility", "ScaledReturn",
"ScaledVolume"]].values
>>> Y = train_df[horizon:]["ScaledReturn"].values

## python
>>> X = np.expand_dims(X, axis = 1)

## python
>>> X = np.split(X, X.shape[0]/10, axis = 0)
>>> X = np.concatenate(X, axis = 1)
>>> X.shape
(10, 699, 3)

## python
>>> ## Parâmetros de arquitetura.
>>> NUM_HIDDEN = 4
>>> NUM_LAYERS = 2
>>>
>>> ## Parâmetros de formatação de dados.
>>> BATCH_SIZE = 64
>>> WINDOW_SIZE = 20
>>>
>>> ## Parâmetros de treinamento.
>>> LEARNING_RATE = 1e-2
>>> EPOCHS = 30

## python
>>> Xinp = tf.placeholder(dtype = tf.float32,
>>> shape = [WINDOW_SIZE, None, 3])
>>> Yinp = tf.placeholder(dtype = tf.float32, shape = [None])

142 | Capítulo 14: Aplicações na Área Financeira


## python
>>> with tf.variable_scope("scope1", reuse=tf.AUTO_REUSE):
>>> cells = [tf.nn.rnn_cell.LSTMCell(num_units=NUM_HIDDEN)
>>> for n in range(NUM_LAYERS)]
>>> stacked_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(cells)
>>> rnn_output, states = tf.nn.dynamic_rnn(stacked_rnn_cell,
>>> Xinp,
>>> dtype=tf.float32)
>>> W = tf.get_variable("W_fc", [NUM_HIDDEN, 1],
>>> initializer =
>>> tf.random_uniform_initializer(-.2, .2))
>>>
>>> ## Veja que não temos viés porque esperamos retorno zero médio.
>>> output = tf.squeeze(tf.matmul(rnn_output[-1, :, :], W))
>>>
>>> loss = tf.nn.l2_loss(output - Yinp)
>>> opt = tf.train.GradientDescentOptimizer(LEARNING_RATE)
>>> train_step = opt.minimize(loss)

Análise Prática de Séries Temporais | 143


## python
>>> ## Para cada época.
>>> y_hat_dict = {}
>>> Y_dict = {}
>>>
>>> in_sample_Y_dict = {}
>>> in_sample_y_hat_dict = {}
>>>
>>> for ep in range(EPOCHS):
>>> epoch_training_loss = 0.0
>>> for i in range(WINDOW_SIZE):
>>> X = train_df[:(7000 - WINDOW_SIZE)][["ScaledVolatility",
>>> "ScaledReturn",
>>> "ScaledVolume"]].values
>>> Y = train_df[WINDOW_SIZE:]["ScaledReturn"].values
>>>
>>> ## Torna-o divisível pelo tamanho da janela.
>>> num_to_unpack = math.floor(X.shape[0] / WINDOW_SIZE)
>>> start_idx = X.shape[0] - num_to_unpack * WINDOW_SIZE
>>> X = X[start_idx:]
>>> Y = Y[start_idx:]
>>>
>>> X = X[i:-(WINDOW_SIZE-i)]
>>> Y = Y[i:-(WINDOW_SIZE-i)]
>>>
>>> X = np.expand_dims(X, axis = 1)
>>> X = np.split(X, X.shape[0]/WINDOW_SIZE, axis = 0)
>>> X = np.concatenate(X, axis = 1)
>>> Y = Y[::WINDOW_SIZE]
>>> ## TREINAMENTO
>>> ## Agora coloca em batch e executa uma sessão
>>> for j in range(math.ceil(Y.shape[0] / BATCH_SIZE)):
>>> ll = BATCH_SIZE * j
>>> ul = BATCH_SIZE * (j + 1)
>>>
>>> if ul > X.shape[1]:
>>> ul = X.shape[1] - 1
>>> ll = X.shape[1]- BATCH_SIZE
>>>
>>> training_loss, _, y_hat = sess.run([loss, train_step,
>>> output],
>>> feed_dict = {
>>> Xinp: X[:, ll:ul, :],
>>> Yinp: Y[ll:ul]
>>> })
>>> epoch_training_loss += training_loss
>>>
>>> in_sample_Y_dict[ep] = Y[ll:ul]
>>> ## Repare que isso nos renderá apenas a última parte
>>> ## dos dados treinados.
>>> in_sample_y_hat_dict[ep] = y_hat
>>>

144 | Capítulo 14: Aplicações na Área Financeira


>>> ## TESTE
>>> X = test_df[:(test_df.shape[0] - WINDOW_SIZE)]
>>> [["ScaledVolatility", "ScaledReturn",
>>> "ScaledVolume"]].values
>>> Y = test_df[WINDOW_SIZE:]["ScaledReturn"].values
>>> num_to_unpack = math.floor(X.shape[0] / WINDOW_SIZE)
>>> start_idx = X.shape[0] - num_to_unpack * WINDOW_SIZE
>>> ## Melhor descartar no início do que no final do período
>>> ## de treinamento quando deve ser deletado.
>>> X = X[start_idx:]
>>> Y = Y[start_idx:]
>>>
>>> X = np.expand_dims(X, axis = 1)
>>> X = np.split(X, X.shape[0]/WINDOW_SIZE, axis = 0)
>>> X = np.concatenate(X, axis = 1)
>>> Y = Y[::WINDOW_SIZE]
>>> testing_loss, y_hat = sess.run([loss, output],
>>> feed_dict = { Xinp: X, Yinp: Y })
>>> ## Não é o ideal.
>>> ## Deveríamos realmente ter uma perda de validação além do teste.
>>>
>>> print("Epoch: %d Training loss: %0.2f
>>> Testing loss %0.2f:" %
>>> (ep, epoch_training_loss, testing_loss))
>>> Y_dict[ep] = Y
>>>
Epoch: 0y_hat_dict[ep] = y_hat
Training loss: 2670.27 Testing loss 526.937:
Epoch: 1 Training loss: 2669.72 Testing loss 526.908:
Epoch: 2 Training loss: 2669.53 Testing loss 526.889:
Epoch: 3 Training loss: 2669.42 Testing loss 526.874:
Epoch: 4 Training loss: 2669.34 Testing loss 526.862:
Epoch: 5 Training loss: 2669.27 Testing loss 526.853:
Epoch: 6 Training loss: 2669.21 Testing loss 526.845:
Epoch: 7 Training loss: 2669.15 Testing loss 526.839:
Epoch: 8 Training loss: 2669.09 Testing loss 526.834:
Epoch: 9 Training loss: 2669.03 Testing loss 526.829:
Epoch: 10 Training loss: 2668.97 Testing loss 526.824:
Epoch: 11 Training loss: 2668.92 Testing loss 526.819:
Epoch: 12 Training loss: 2668.86 Testing loss 526.814:
Epoch: 13 Training loss: 2668.80 Testing loss 526.808:
Epoch: 14 Training loss: 2668.73 Testing loss 526.802:
Epoch: 15 Training loss: 2668.66 Testing loss 526.797:
Epoch: 16 Training loss: 2668.58 Testing loss 526.792:
Epoch: 17 Training loss: 2668.49 Testing loss 526.788:
Epoch: 18 Training loss: 2668.39 Testing loss 526.786:
Epoch: 19 Training loss: 2668.28 Testing loss 526.784:
Epoch: 20 Training loss: 2668.17 Testing loss 526.783:
Epoch: 21 Training loss: 2668.04 Testing loss 526.781:
Epoch: 22 Training loss: 2667.91 Testing loss 526.778:
Epoch: 23 Training loss: 2667.77 Testing loss 526.773:
Epoch: 24 Training loss: 2667.62 Testing loss 526.768:
Epoch: 25 Training loss: 2667.47 Testing loss 526.762:
Epoch: 26 Training loss: 2667.31 Testing loss 526.755:
Epoch: 27 Training loss: 2667.15 Testing loss 526.748:
Epoch: 28 Training loss: 2666.98 Testing loss 526.741:
Epoch: 29 Training loss: 2666.80 Testing loss 526.734:

## python
>>> plt.plot(test_y_dict[MAX_EPOCH])
>>> plt.plot(test_y_hat_dict[MAX_EPOCH], 'r--')
>>> plt.show()

Análise Prática de Séries Temporais | 145


Figura 14-9. Os valores de retorno reais para uma subseção do período de teste (linha
contínua) são plotados e comparados com as previsões da rede neural (linha tracejada).
A escala da previsão é tão diferente dos dados reais que fica difícil avaliar o modelo.

## python
>>> pearsonr(test_y_dict[MAX_EPOCH], test_y_hat_dict[MAX_EPOCH])
(0.03595786881773419, 0.20105107068949668)

## python
>>> plt.plot(test_y_dict[MAX_EPOCH][:100])
>>> plt.plot(test_y_hat_dict[MAX_EPOCH][:100] * 50, 'r--')
>>> plt.show()

Figura 14-10. Uma melhor noção de como as predições do modelo (linha tracejada) se
compara aos dados reais (linha contínua). No entanto, com uma correlação tão baixa,
talvez tendamos a ver um padrão que não existe. Por isso, as métricas quantitativas nos
ajudarão mais do que as avaliações visuais para dados financeiros ruidosos.

146 | Capítulo 14: Aplicações na Área Financeira


CAPÍTULO 15
Usos Governamentais de Séries Temporais

Figura 15-1. Dois dos quatro gráficos na página principal do site de dados abertos
de Singapura (acessado na primavera de 2019) são visualizações de séries temporais
utilizadas para mostrar informações importantes sobre o país.

147
## Linha de comando do Linux.
$ head 311.csv

Unique Key,Created Date,Closed Date,Agency,Agency Name,Complaint Type,Descripto


27863591,04/17/2014 12:00:00 AM,04/28/2014 12:00:00 AM,DOHMH,Department of Heal
27863592,04/17/2014 12:00:00 AM,04/22/2014 12:00:00 AM,DOHMH,Department of Heal
27863595,04/17/2014 10:23:00 AM,0417/2014 12:00:00 PM,DSNY,Queens East 12,Derel
27863602,04/17/2014 05:01:00 PM,04/17/2014 05:01:00 PM,DSNY,BCC - Queens East,D
27863603,04/17/2014 12:00:00 AM,04/23/2014 12:00:00 AM,HPD,Department of Housin
27863604,04/17/2014 12:00:00 AM,04/22/2014 12:00:00 AM,HPD,Department of Housin
27863605,04/17/2014 12:00:00 AM,04/21/2014 12:00:00 AM,HPD,Department of Housin

## Linha de comando do Linux.


$ wc -l 311.csv
19811967 311.csv

## R
> df = fread("311.csv", skip = 0, nrows = 10)
> colnames(df)
[1] "Unique Key" "Created Date"
[3] "Closed Date" "Agency"
[5] "Agency Name" "Complaint Type"
[7] "Descriptor" "Location Type"
[9] "Incident Zip" "Incident Address"
[11] "Street Name" "Cross Street 1"
[13] "Cross Street 2" "Intersection Street 1"
[15] "Intersection Street 2" "Address Type"
[17] "City" "Landmark"
[19] "Facility Type" "Status"
[21] "Due Date" "Resolution Description"
[23] "Resolution Action Updated Date" "Community Board"
[25] "BBL" "Borough"
[27] "X Coordinate (State Plane)" "Y Coordinate (State Plane)"
[29] "Open Data Channel Type" "Park Facility Name"
[31] "Park Borough" "Vehicle Type"
[33] "Taxi Company Borough" "Taxi Pick Up Location"
[35] "Bridge Highway Name" "Bridge Highway Direction"
[37] "Road Ramp" "Bridge Highway Segment"
[39] "Latitude" "Longitude"
[41] "Location"

"Created Date"
"Closed Date"
"Due Date"
"Resolution Action Updated Date"

## R
> df$CreatedDate = df[, CreatedDate := as.POSIXct(CreatedDate,
> format = "%m/%d/%Y %I:%M:%S %p")

## R
> summary(as.numeric(df$ClosedDate - df$CreatedDate,
> units = "days"))
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
-75.958 1.000 4.631 13.128 12.994 469.737 113

148 | Capítulo 15: Usos Governamentais de Séries Temporais


## R
> range(df$CreatedDate)
[1] "2014-03-13 12:56:24 EDT" "2019-02-06 23:35:00 EST"

Figura 15-2. Ainda que a maioria das datas nas primeiras 10 mil linhas de código
aparentemente se encaixem em 2014, elas também avançam até 2019 — e com
frequência!

## Linha de comando do Linux.


$ head -n 1000 311.csv | tail -n 999 > test.csv

## Linha de comando do Linux.


$ sort --field-separator=',' --key=2,3 test.csv > testsorted.csv

1: 02/02/2019 10:00:27 AM 02/06/2019 07:44:37 AM


2: 03/27/2014 07:38:15 AM 04/01/2014 12:00:00 AM
3: 03/27/2014 11:07:31 AM 03/28/2014 01:09:00 PM
4: 03/28/2014 06:35:13 AM 03/28/2014 10:47:00 PM
5: 03/28/2014 08:31:38 AM 03/28/2014 10:37:00 AM
---
995: 07/03/2017 12:40:04 PM 07/03/2017 03:40:02 PM
996: 07/04/2017 01:30:35 AM 07/04/2017 02:50:22 AM
997: 09/03/2014 03:32:57 PM 09/04/2014 04:30:30 PM
998: 09/05/2014 11:17:53 AM 09/08/2014 03:37:25 PM
999: 11/06/2018 07:15:28 PM 11/06/2018 08:17:52 PM

Análise Prática de Séries Temporais | 149


Figura 15-3. O portal NYC Open Data faz a classificação por meio de uma interface
web que parece estar disponível para qualquer coluna neste grande conjunto de dados.
É um recurso gratuito impressionante, quando você pensa sobre o poder computacional
necessário para a classificação.

## R
> ## Lê apenas as colunas de interesse.
> df = fread("311.tsv", select = c("Created Date", "Closed Date"))
>
> ## Usa o paradigma 'set' recomendado do data.table para definir os nomes das colunas.
> setnames(df, gsub(" ", "", colnames(df)))
>
> ## Elimina as linhas com um campo de data em branco.
> df = df[nchar(CreatedDate) > 1 & nchar(ClosedDate) > 1]
>
> ## Converte as colunas de string de data para POSIXct.
> fmt.str = "%m/%d/%Y %I:%M:%S %p"
> df[, CreatedDate := as.POSIXct(CreatedDate, format = fmt.str)]
> df[, ClosedDate := as.POSIXct(ClosedDate, format = fmt.str)]
>
> ## Ordenamento em ordem cronológica do CreatedDate.
> setorder(df, CreatedDate)
>
> ## Calcula o número de dias entre a criação e o encerramento
> ## da reclamação na linha direta 311.
> df[, LagTime := as.numeric(difftime(ClosedDate, CreatedDate,
> units = "days"))]

150 | Capítulo 15: Usos Governamentais de Séries Temporais


## R
> summary(df$LagTime)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
-42943.4 0.1 2.0 2.0 7.9 368961.1 609835

> nrow(df[LagTime < 0]) / nrow(df)


[1] 0.01362189

> nrow(df[LagTime > 1000]) / nrow(df)


[1] 0.0009169934

> df = df[LagTime < 1000]


> df = df[LagTime > 0]

> df.new = df[seq(1, nrow(df), 2), ]


> write.csv(df.new[order(ClosedDate)], "abridged.df.csv")

Figura 15-4. A estrutura computacional de nossa estimativa de quantis online usando o


algoritmo P-square. Mantemos uma série de marcadores indicando onde achamos que
os quantis estão e quais são as contagens cumulativas para todos os pontos de dados
menores ou iguais a cada um desses quantis.

Análise Prática de Séries Temporais | 151


## python
>>> ## Importações e um lambda.
>>> import bisect
>>> import math
>>>
>>> sign = lambda x: (1, -1)[x < 0]
>>>
>>> ## beginning of class definition
>>> class PQuantile:
>>> def __init__(self, b, discount_factor):
>>> ## Inicialização
>>> self.num_obs = 0 ## self-explanatory
>>> ## Contagens por quantil.
>>> self.n = [i for i in range(self.b+1)]
>>> self.q = [] ## the estimated quantile values
>>>
>>> ## b é o número de quantis,
>>> ## incluindo os quantis 0 e 100.
>>> ## (valores mínimo e máximo).
>>> self.b = b
>>> ## O fator de desconto define como ajustamos as contagens anteriores
>>> ## quando novos dados estão disponíveis.
>>> self.discount_factor = discount_factor

## python
>>> def next_obs(self, x):
>>> if self.num_obs < (self.b + 1):
>>> self.q.append(x)
>>> self.q.sort()
>>> self.num_obs = self.num_obs + 1
>>> else:
>>> self.next_obs2(x)
>>> self.next_obs = self.next_obs2

152 | Capítulo 15: Usos Governamentais de Séries Temporais


## python
>>> def next_obs2(self, x):
>>> ## Descontando o número de observações.
>>> if self.num_obs > self.b * 10:
>>> corrected_obs = max(self.discount_factor * self.num_obs,
>>> self.b)
>>> self.num_obs = corrected_obs + 1
>>> self.n = [math.ceil(nn * self.discount_factor)
>>> for nn in self.n]
>>>
>>> for i in range(len(self.n) - 1):
>>> if self.n[i + 1] - self.n[i] == 0:
>>> self.n[i+1] = self.n[i + 1] + 1
>>> elif self.n[i + 1] < self.n[1]:
>>> ## in practice this doesn't seem to happen
>>> self.n[i + 1] = self.n[i] - self.n[1 + 1] + 1
>>> else:
>>> self.num_obs = self.num_obs + 1
>>>
>>> k = bisect.bisect_left(self.q, x)
>>> if k is 0:
>>> self.q[0] = x
>>> elif k is self.b+1 :
>>> self.q[-1] = x
>>> k = self.b
>>> if k is not 0:
>>> k = k - 1
>>>
>>> self.n[(k+1):(self.b+1)] = [self.n[i] + 1
>>> for i in range((k+1),
>>> (self.b+1))]
>>> for i in range(1, self.b):
>>> np = (i)*(self.num_obs - 1 )/(self.b)
>>> d = np - self.n[i]
>>> if (d >= 1 and (self.n[i+1] - self.n[i]) > 1):
>>> self._update_val(i, d)
>>> elif (d <= -1 and (self.n[i-1] - self.n[i]) < -1):
>>> self._update_val(i, d)

Análise Prática de Séries Temporais | 153


## python
>>> ## Atualização geral.
>>> ## Como você pode ver, self.q e self.n são atualizados
>>> ## conforme a posição de um quantil é deslocada.
>>> def _update_val(self, i, d):
>>> d = sign(d)
>>> qp = self._adjust_parabolic(i, d)
>>> if self.q[i] < qp < self.q[i+1]:
>>> self.q[i] = qp
>>> else:
>>> self.q[i] = self._adjust_linear(i, d)
>>> self.n[i] = self.n[i] + d
>>>
>>> ## Este é o método de atualização principal.
>>> def _adjust_parabolic(self, i, d):
>>> new_val = self.q[i]
>>> m1 = d/(self.n[i+1] - self.n[i-1])
>>> s1 = (self.n[i] - self.n[i-1] + d) *
>>> (self.q[i+1] - self.q[i]) /
>>> (self.n[i+1] - self.n[i])
>>> s2 = (self.n[i+1] - self.n[i] - d) *
>>>
>>> ## Backup do ajuste linear
>>> ## quando as condições parabólicas não são atendidas.
>>> def _adjust_linear(self, i, d):
>>> new_val = self.q[i]
>>> new_val = new_val + d * (self.q[i + d] - self.q[i]) /
>>> (self.n[i+d] - self.n[i])
>>> return new_val

## python
>>> class PQuantile:
>>> ## INICIALIZAÇÃO.
>>> def __init__(self, b, discount_factor):
>>> self.num_obs = 0
>>> self.b = b
>>> self.discount_factor = discount_factor
>>> self.n = [i for i in range(self.b+1)]
>>> self.q = []
>>>
>>> ## DATA INTAKE
>>> def next_obs(self, x):
>>> if self.num_obs < (self.b + 1):
>>> self.q.append(x)
>>> self.q.sort()
>>> self.num_obs = self.num_obs + 1

154 | Capítulo 15: Usos Governamentais de Séries Temporais


>>> else:
>>> self.next_obs2(x)
>>> self.next_obs = self.next_obs2
>>>
>>> def next_obs2(self, x):
>>> ## Descontando o número de observações.
>>> if self.num_obs > self.b * 10:
>>> corrected_obs = max(self.discount_factor
>>> * self.num_obs,
>>> self.b)
>>> self.num_obs = corrected_obs + 1
>>> self.n = [math.ceil(nn * self.discount_factor)
>>> for nn in self.n]
>>>
>>> for i in range(len(self.n) - 1):
>>> if self.n[i + 1] - self.n[i] == 0:
>>> self.n[i+1] = self.n[i + 1] + 1
>>> elif self.n[i + 1] < self.n[1]:
>>> ## Na prática, isso não parece acontecer.
>>> self.n[i + 1] = self.n[i] - self.n[1 + 1] + 1
>>> else:
>>> self.num_obs = self.num_obs + 1
>>>
>>> k = bisect.bisect_left(self.q, x)
>>> if k is 0:
>>> self.q[0] = x
>>> elif k is self.b+1 :
>>> self.q[-1] = x
>>> k = self.b
>>> if k is not 0:
>>> k = k - 1
>>>
>>> self.n[(k+1):(self.b+1)] = [self.n[i] + 1
>>> for i in range((k+1),
>>> (self.b+1))]
>>> for i in range(1, self.b):
>>> np = (i)*(self.num_obs - 1 )/(self.b)
>>> d = np - self.n[i]
>>> if (d >= 1 and (self.n[i+1] - self.n[i]) > 1):
>>> self._update_val(i, d)
>>> elif (d <= -1 and (self.n[i-1] - self.n[i]) < -1):
>>> self._update_val(i, d)
>>>
>>> ## AJUSTES DO HISTOGRAMA.
>>> def _update_val(self, i, d):
>>> d = sign(d)
>>> qp = self._adjust_parabolic(i, d)
>>> if self.q[i] < qp < self.q[i+1]:
>>> self.q[i] = qp
>>> else:
>>> self.q[i] = self._adjust_linear(i, d)
>>> self.n[i] = self.n[i] + d
>>>
>>> def _adjust_parabolic(self, i, d):
>>> new_val = self.q[i]
>>> m1 = d/(self.n[i+1] - self.n[i-1])

Análise Prática de Séries Temporais | 155


>>> s1 = (self.n[i] - self.n[i-1] + d) *
>>> (self.q[i+1] - self.q[i]) /
>>> (self.n[i+1] - self.n[i])
>>> s2 = (self.n[i+1] - self.n[i] - d) *
>>> (self.q[i] - self.q[i-1]) /
>>> (self.n[i] - self.n[i-1])
>>> new_val = new_val + m1 * (s1 + s2)
>>> return new_val
>>>
>>> def _adjust_linear(self, i, d):
>>> new_val = self.q[i]
>>> new_val = new_val + d * (self.q[i + d] - self.q[i]) /
>>> (self.n[i+d] - self.n[i])
>>> return new_val

## python
>>> qt = PQuantile(10, 0.3)
>>> qt_ests = []
>>>
>>> for _ in range(100):
>>> b.next_obs(uniform())
>>> if len(b.q) > 10:
>>> qt_ests.append(qt.q[4])
>>> for _ in range(100):
>>> b.next_obs(uniform(low = 0.9))
>>> qt_ests.append(qt.q[4])
>>>
>>> plt.plot(qt_ests)

Figura 15-5. Quando descontamos intensamente as medidas mais antigas


(multiplicando por um fator de desconto menor), vemos que a distribuição
subjacente mudou.

156 | Capítulo 15: Usos Governamentais de Séries Temporais


## python
>>> qt = PQuantile(10, 0.8)
>>> qt_ests = []
>>>
>>> for _ in range(100):
>>> b.next_obs(uniform())
>>> if len(b.q) > 10:
>>> qt_ests.append(qt.q[4])
>>> for _ in range(100):
>>> b.next_obs(uniform(low = 0.9))
>>> qt_ests.append(qt.q[4])
>>>
>>> plt.plot(qt_ests)

Figura 15-6. Quando descontamos menos as medições mais antigas (multiplicando por
um fator de desconto maior), nossa estimativa de quantil demora mais para reconhecer
que a distribuição subjacente mudou.

## python
>>> import numpy
>>> nrows = 1000000
>>> qt_est1 = np.zeros(nrows)
>>> qt_est2 = np.zeros(nrows)
>>> qt_est3 = np.zeros(nrows)
>>> qt_est4 = np.zeros(nrows)
>>> qt_est5 = np.zeros(nrows)
>>> qt_est6 = np.zeros(nrows)
>>> qt_est7 = np.zeros(nrows)
>>> qt_est8 = np.zeros(nrows)
>>> qt_est9 = np.zeros(nrows)
>>> for idx, val in enumerate(df.LagTime[:nrows]):
>>> qt.next_obs(val)
>>> if len(qt.q) > 10:
>>> qt_est1[idx] = qt.q[1]

Análise Prática de Séries Temporais | 157


>>> qt_est2[idx] = qt.q[2]
>>> qt_est3[idx] = qt.q[3]
>>> qt_est4[idx] = qt.q[4]
>>> qt_est5[idx] = qt.q[5]
>>> qt_est6[idx] = qt.q[6]
>>> qt_est7[idx] = qt.q[7]
>>> qt_est8[idx] = qt.q[8]
>>> qt_est9[idx] = qt.q[9]
>>>
>>> plot(qt_est9, color = 'red')
>>> plt.plot(qt_est7, color = 'pink')
>>> plt.plot(qt_est5, color = 'blue')
>>> plt.plot(qt_est3, color = 'gray')
>>> plt.plot(qt_est2, color = 'orange'

Figura 15-7. Os valores dos percentis 90, 70, 50, 30 e 20 ao longo do tempo para as
primeiras 100 mil linhas no conjunto de dados quando classificados por data de
encerramento.

158 | Capítulo 15: Usos Governamentais de Séries Temporais


CAPÍTULO 16
Pacotes de Séries Temporais

Pacote Prophet Open Source do Facebook


## R
> library(pageviews)
> df_wiki = article_pageviews(project = "en.wikipedia",
> article = "Facebook",
> start = as.Date('2015-11-01'),
> end = as.Date("2018-11-02"),
> user_type = c("user"),
> platform = c("mobile-web"))
> colnames(df_wiki)
[1] "project" "language" "article" "access" "agent" "granularity"
[7] "date" "views"

## R
> ## Dividimos os dados de acordo com o que precisamos
> ## e fornecemos a eles os nomes de coluna esperados.
> df = df_wiki[, c("date", "views")]
> colnames(df) = c("ds", "y")

> ## Usamos também a transformação logarítmica,


> ## porque os dados têm mudanças tão extremas nos valores
> ## que grandes valores diminuem a variação normal intradiária.
> df$y = log(df$y)

> ## Criamos um data frame 'futuro' que inclui as datas futuras


> ## que gostaríamos de prever,
> ## preveremos 365 dias à frente de nossos dados
> m = prophet(df)
> future <- make_future_dataframe(m, periods = 365)
> tail(future)
ds
1458 2019-10-28
1459 2019-10-29
1460 2019-10-30
1461 2019-10-31
1462 2019-11-01
1463 2019-11-02

> ## Geramos a previsão para as datas de interesse.


> forecast <- predict(m, future)
> tail(forecast[c('ds', 'yhat', 'yhat_lower', 'yhat_upper')])
ds yhat yhat_lower yhat_upper
1458 2019-10-28 9.119005 8.318483 9.959014 159
1459 2019-10-29 9.090555 8.283542 9.982579
1460 2019-10-30 9.064916 8.251723 9.908362
1461 2019-10-31 9.066713 8.254401 9.923814
1462 2019-11-01 9.015019 8.166530 9.883218
1458 2019-10-28
1459 2019-10-29
1460 2019-10-30
1461 2019-10-31
1462 2019-11-01
1463 2019-11-02

> ## Geramos a previsão para as datas de interesse.


> forecast <- predict(m, future)
> tail(forecast[c('ds', 'yhat', 'yhat_lower', 'yhat_upper')])
ds yhat yhat_lower yhat_upper
1458 2019-10-28 9.119005 8.318483 9.959014
1459 2019-10-29 9.090555 8.283542 9.982579
1460 2019-10-30 9.064916 8.251723 9.908362
1461 2019-10-31 9.066713 8.254401 9.923814
1462 2019-11-01 9.015019 8.166530 9.883218
1463 2019-11-02 9.008619 8.195123 9.862962
> ## Agora temos predições para o nosso valor de interesse.

> ## Por último, plotamos um gráfico para ver como o pacote se saiu qualitativamente.
> plot(df$ds, df$y, col = 1, type = 'l', xlim = range(forecast$ds),
> main = "Actual and predicted Wikipedia pageviews of 'Facebook'")
> points(forecast$ds, forecast$yhat, type = 'l', col = 2)

Figura 16-1. Gráfico dos dados de contagem de páginas da Wikipédia (linha contínua) e
as previsões do Prophet para esses dados (linha tracejada espessa).

## R
> prophet_plot_components(m, forecast)

160 | Capítulo 16: Pacotes de Séries Temporais


Figura 16-2. Predição desagregada em uma tendência, com componentes semanais e
anuais. A partir disso, a predição é gerada pela soma dos componentes. Repare que
diferentes componentes são formados de maneira distinta. Os dados de tendência
têm uma forma linear, ao passo que os dados anuais são curvos devido ao ajuste
da série de Fourier subjacente (leia mais na postagem do blog do Facebook
mencionada anteriormente).

## R
> library(AnomalyDetection)
> data(raw_data)
> head(raw_data)
timestamp count
1 1980-09-25 14:01:00 182.478
2 1980-09-25 14:02:00 176.231
3 1980-09-25 14:03:00 183.917
4 1980-09-25 14:04:00 177.798
5 1980-09-25 14:05:00 165.469
6 1980-09-25 14:06:00 181.878

## R
> ## Detecta uma alta porcentagem de anomalias em qualquer direção.
> general_anoms = AnomalyDetectionTs(raw_data, max_anoms=0.05,
> direction='both')
>
> ## Detecta uma porcentagem menor de anomalias apenas na direção pos.
> high_anoms = AnomalyDetectionTs(raw_data, max_anoms=0.01,
> direction='pos')

Análise Prática de Séries Temporais | 161


Figura 16-3. As anomalias relatadas pela função AnomalyDetectionTs() do Twitter com
configurações muito inclusivas (parte superior) e com configurações mais limitadas
(parte inferior).

Figura 16-4. As mesmas anomalias identificadas, agora com foco nas anomalias que
ocorreram em um cluster no mesmo dia.

162 | Capítulo 16: Pacotes de Séries Temporais


CAPÍTULO 17
Previsões sobre como Prever

Previsão como Serviço

Figura 17-1. Um exemplo de pipeline de previsão de séries temporais como serviço.


Esses serviços estão sendo oferecidos por uma variedade de pequenas startups e gigantes
tecnológicos, principalmente pela Amazon, que lançou um pacote de produtos adaptados
para prever dados de séries temporais automaticamente e em escala por meio de um pacote
de produtos orientados ao aprendizado profundo e modelos estatísticos.

163

Você também pode gostar