Question. Explore the following four time series:
Bricks from aus_production, Lynx
from pelt, Close from gafa_stock,
Demand from vic_elec.
? (or help()) to find out about the
data in each series.autoplot() to produce a time plot of each
series.My thought process. The first thing I want to know
about any time series is how often the data was measured. That one fact
decides everything that comes later, like whether a seasonal plot even
makes sense. So my plan is simple. First I read the help page for each
dataset so I know what the numbers actually mean. Then I ask R directly
for the time interval instead of guessing by eye, because
interval() gives me the real answer. Only after that do I
plot, so I already know what I am looking at.
These are the help calls the question asks for. I set this chunk to
eval = FALSE on purpose. The ? command opens
the Help pane inside RStudio, so it shows me the documentation on screen
while I work, but it does not return anything that can be printed into a
knitted document. Running it here would add nothing to the PDF.
# Help pages for context on each dataset.
# These open in the RStudio Help pane when run interactively.
?aus_production
?pelt
?gafa_stock
?vic_elec
So that the documentation actually appears in this report, I pull the help file in as text and print it. This is the same content the Help pane shows, just captured in a way the document can display.
# Small helper: print the first n lines of a dataset's help page as plain text.
show_help <- function(topic, package, n = 22) {
db <- utils::help(topic, package = (package))
txt <- utils::capture.output(
tools::Rd2txt(utils:::.getHelpFile(db),
options = list(underline_titles = FALSE))
)
cat(head(txt, n), sep = "\n")
}
show_help("aus_production", "tsibbledata")
## Quarterly production of selected commodities in Australia.
##
## Description:
##
## Quarterly estimates of selected indicators of manufacturing
## production in Australia.
##
## Format:
##
## Time series of class 'tsibble'.
##
## Details:
##
## 'aus_production' is a half-hourly 'tsibble' with six values:
##
## Beer: Beer production in megalitres.
## Tobacco: Tobacco and cigarette production in tonnes.
## Bricks: Clay brick production in millions of bricks.
## Cement: Portland cement production in thousands of tonnes.
## Electricity: Electricity production in gigawatt hours.
## Gas: Gas production in petajoules.
##
show_help("pelt", "tsibbledata")
## Pelt trading records
##
## Description:
##
## Hudson Bay Company trading records for Snowshoe Hare and Canadian
## Lynx furs from 1845 to 1935. This data contains trade records for
## all areas of the company.
##
## Format:
##
## Time series of class 'tsibble'
##
## Details:
##
## 'pelt' is an annual 'tsibble' with two values:
##
## Hare: The number of Snowshoe Hare pelts traded.
## Lynx: The number of Canadian Lynx pelts traded.
##
## Source:
##
## Hudson Bay Company
show_help("gafa_stock", "tsibbledata")
## GAFA stock prices
##
## Description:
##
## Historical stock prices from 2014-2018 for Google, Amazon,
## Facebook and Apple. All prices are in $USD.
##
## Format:
##
## Time series of class 'tsibble'
##
## Details:
##
## 'gafa_stock' is a 'tsibble' containing data on irregular trading
## days:
##
## Open: The opening price for the stock.
## High: The stock's highest trading price.
## Low: The stock's lowest trading price.
## Close: The closing price for the stock.
## Adj_Close: The adjusted closing price for the stock.
## Volume: The amount of stock traded.
show_help("vic_elec", "tsibbledata")
## Half-hourly electricity demand for Victoria, Australia
##
## Description:
##
## 'vic_elec' is a half-hourly 'tsibble' with three values:
##
## Demand: Total electricity demand in MWh.
## Temperature: Temperature of Melbourne (BOM site 086071).
## Holiday: Indicator for if that day is a public holiday.
##
## Format:
##
## Time series of class 'tsibble'.
##
## Details:
##
## This data is for operational demand, which is the demand met by
## local scheduled generating units, semi-scheduled generating units,
## and non-scheduled intermittent generating units of aggregate
## capacity larger than 30 MWh, and by generation imports to the
## region. The operational demand excludes the demand met by
## non-scheduled non-intermittent generating units, non-scheduled
What the help pages tell me.
aus_production is quarterly manufacturing output for
Australia, and Bricks is clay brick production in millions
of bricks. pelt is the Hudson Bay Company trading records,
where Lynx is the number of Canadian lynx pelts traded.
gafa_stock holds daily share prices for Google, Amazon,
Facebook and Apple, and Close is the closing price.
vic_elec is half-hourly electricity demand for Victoria,
where Demand is in megawatt hours.
Knowing the units matters. Without the help page I would not know
that Bricks is counted in millions or that
Demand is in MWh, and I would not be able to label the axes
properly later on.
# Ask R for the time interval instead of guessing from the plot.
interval(aus_production) # Bricks: quarterly
## <interval[1]>
## [1] 1Q
interval(pelt) # Lynx: annual
## <interval[1]>
## [1] 1Y
interval(gafa_stock) # Close: irregular, shown as "!" (trading days only)
## <interval[1]>
## [1] !
interval(vic_elec) # Demand: half-hourly (30 minutes)
## <interval[1]>
## [1] 30m
What the intervals tell me.
aus_production is quarterly (1Q),
pelt is annual (1Y), vic_elec is
half-hourly (30m), and gafa_stock comes back
as !.
That ! is the interesting one, and it is the reason I
ran interval() instead of assuming. The exclamation mark is
how tsibble reports an irregular interval. The dates in
gafa_stock are daily dates, but the stock market is closed
on weekends and holidays, so there are gaps and the spacing between rows
is not constant. R will not call that daily.
I would have gotten this wrong by eye, because the plot looks like a normal daily series. This detail matters later in Exercise 2.11, where the gaps have to be removed before the ACF means anything.
aus_production |> autoplot(Bricks)
pelt |> autoplot(Lynx)
gafa_stock |> autoplot(Close)
vic_elec |> autoplot(Demand)
What I see in the plots. Bricks rises for a long
time and then falls off after the 1980s, with a repeating up and down
inside each year. Lynx has big waves that repeat roughly every ten
years, but the waves are not tied to the calendar, so that is a cycle
and not seasonality. Close shows four separate stock lines
that mostly drift upward. Demand is so dense that it looks like a solid
block, which is normal when you plot years of half-hourly data all at
once.
Now the last plot with better labels:
vic_elec |>
autoplot(Demand) +
labs(
title = "Half-hourly electricity demand: Victoria, Australia",
x = "Time",
y = "Demand (MWh)"
)
Why I labeled it this way. The default axis labels just repeat the column names, which means nothing to a reader who has not seen the dataset. Saying “Demand (MWh)” tells them the unit, and the title tells them the place. A plot should be readable on its own without the code next to it.
Question. Use filter() to find what
days corresponded to the peak closing price for each of the four stocks
in gafa_stock.
My thought process. The key words here are “for each
of the four stocks.” If I just filtered on the overall maximum, I would
only get one row, and it would be whichever company had the highest
price in dollars. That is not what the question asks. So I group by
Symbol first. Once the data is grouped,
max(Close) is computed inside each company separately, and
the filter keeps the top day for each one. Then I ungroup so the result
behaves like a normal table again.
gafa_stock |>
group_by(Symbol) |>
filter(Close == max(Close)) |>
ungroup() |>
select(Symbol, Date, Close)
## # A tsibble: 4 x 3 [!]
## # Key: Symbol [4]
## Symbol Date Close
## <chr> <date> <dbl>
## 1 AAPL 2018-10-03 232.
## 2 AMZN 2018-09-04 2040.
## 3 FB 2018-07-25 218.
## 4 GOOG 2018-07-26 1268.
What I found. Each of the four stocks peaks in 2018, and Amazon and Google have much higher share prices than Apple and Facebook. That price gap is a good reminder that you cannot compare these four lines directly. A one dollar move means something very different for a 2000 dollar stock than for a 200 dollar stock.
Question. Download the file tute1.csv
from the book website, open it in Excel (or some other spreadsheet
application), and review its contents. You should find four columns of
information. Columns B through D each contain a quarterly series,
labelled Sales, AdBudget and GDP.
Sales contains the quarterly sales for a small company over
the period 1981-2005. AdBudget is the advertising budget
and GDP is the gross domestic product. All series have been
adjusted for inflation.
facet_grid().My thought process. There are three steps here and
each one has a reason. Reading the CSV gives me a plain table, and R has
no idea the Quarter column is time. So I run
yearquarter() to turn it into a real quarterly value, then
as_tsibble() to tell R that this column is the time index.
After that, pivot_longer() stacks the three measures into
one long column so ggplot can draw them with a single
geom_line() call instead of three.
tute1 <- readr::read_csv("tute1.csv")
head(tute1)
## # A tibble: 6 × 4
## Quarter Sales AdBudget GDP
## <date> <dbl> <dbl> <dbl>
## 1 1981-03-01 1020. 659. 252.
## 2 1981-06-01 889. 589 291.
## 3 1981-09-01 795 512. 291.
## 4 1981-12-01 1004. 614. 292.
## 5 1982-03-01 1058. 647. 279.
## 6 1982-06-01 944. 602 254
mytimeseries <- tute1 |>
mutate(Quarter = yearquarter(Quarter)) |>
as_tsibble(index = Quarter)
mytimeseries
## # A tsibble: 100 x 4 [1Q]
## Quarter Sales AdBudget GDP
## <qtr> <dbl> <dbl> <dbl>
## 1 1981 Q1 1020. 659. 252.
## 2 1981 Q2 889. 589 291.
## 3 1981 Q3 795 512. 291.
## 4 1981 Q4 1004. 614. 292.
## 5 1982 Q1 1058. 647. 279.
## 6 1982 Q2 944. 602 254
## 7 1982 Q3 778. 531. 296.
## 8 1982 Q4 932. 608. 272.
## 9 1983 Q1 996. 638. 260.
## 10 1983 Q2 908. 582. 280.
## # ℹ 90 more rows
mytimeseries |>
pivot_longer(-Quarter) |>
ggplot(aes(x = Quarter, y = value, colour = name)) +
geom_line() +
facet_grid(name ~ ., scales = "free_y")
# The same plot without facet_grid()
mytimeseries |>
pivot_longer(-Quarter) |>
ggplot(aes(x = Quarter, y = value, colour = name)) +
geom_line()
What changes when I drop facet_grid().
With facets, each series gets its own panel and its own y axis because
of scales = "free_y". That lets me see the shape of each
series clearly. Without facets, all three lines are squeezed onto one
shared y axis. GDP sits on a much smaller scale than Sales and AdBudget,
so its line gets flattened and its movement is hard to see.
The lesson I take from this is that a shared axis is only fair when the series are on similar scales. When they are not, separate panels are the honest way to show them.
Question. The USgas package contains
data on the demand for natural gas in the US.
USgas package.us_total with year as the index
and state as the key.My thought process. This is the first exercise with
more than one series stored in the same table, so I have to think about
the key. The index is year because that is the time column.
The key is state because that is what tells one series
apart from another. Without setting the key, R would see several rows
sharing the same year and would complain that the rows are not unique.
After that, filtering down to the six New England states is
straightforward.
us_total_ts <- us_total |>
as_tsibble(index = year, key = state)
new_england <- c("Maine", "Vermont", "New Hampshire",
"Massachusetts", "Connecticut", "Rhode Island")
us_total_ts |>
filter(state %in% new_england) |>
autoplot(y) +
labs(
title = "Annual natural gas consumption: New England states",
x = "Year",
y = "Consumption (million cubic feet)"
)
What I see. Massachusetts uses by far the most gas, with Connecticut second, which makes sense because they have much bigger populations than the rest. Vermont uses the least. Most of the states trend upward over the period.
Maine is the one that really stands out. It sits near 6,000 through the late 1990s and then jumps to about 96,000 by 2001, which is roughly fifteen times higher in two years. A change that large in that short a time cannot be homes suddenly using more gas. It almost certainly reflects new gas fired power plants coming online, which is a change in how electricity was generated rather than a change in household demand.
Question. Download tourism.xlsx from
the book website and read it into R using
readxl::read_excel().
tourism
tsibble from the tsibble package.Region and
Purpose had the maximum number of overnight trips on
average.My thought process. The goal is to match the built
in tourism object exactly, so I first ask what makes each
row unique. A row is one region, one state, one purpose, in one quarter.
That means the key needs all three of Region,
State, and Purpose together. The
Quarter column comes in from Excel as text, so I have to
convert it with yearquarter() before it can be used as an
index.
For the average part, the words “on average” are what matter. I want the mean across all quarters for each region and purpose pair, not the single biggest quarter.
tourism_xl <- readxl::read_excel("tourism.xlsx")
my_tourism <- tourism_xl |>
mutate(Quarter = yearquarter(Quarter)) |>
as_tsibble(index = Quarter, key = c(Region, State, Purpose))
my_tourism
## # A tsibble: 24,320 x 5 [1Q]
## # Key: Region, State, Purpose [304]
## Quarter Region State Purpose Trips
## <qtr> <chr> <chr> <chr> <dbl>
## 1 1998 Q1 Adelaide South Australia Business 135.
## 2 1998 Q2 Adelaide South Australia Business 110.
## 3 1998 Q3 Adelaide South Australia Business 166.
## 4 1998 Q4 Adelaide South Australia Business 127.
## 5 1999 Q1 Adelaide South Australia Business 137.
## 6 1999 Q2 Adelaide South Australia Business 200.
## 7 1999 Q3 Adelaide South Australia Business 169.
## 8 1999 Q4 Adelaide South Australia Business 134.
## 9 2000 Q1 Adelaide South Australia Business 154.
## 10 2000 Q2 Adelaide South Australia Business 169.
## # ℹ 24,310 more rows
# Check it matches the built in tourism tsibble.
identical(dim(my_tourism), dim(tourism))
## [1] TRUE
all.equal(as_tibble(my_tourism), as_tibble(tourism))
## [1] TRUE
# Region and Purpose combination with the highest average overnight trips.
my_tourism |>
as_tibble() |>
group_by(Region, Purpose) |>
summarise(avg_trips = mean(Trips), .groups = "drop") |>
slice_max(avg_trips, n = 5)
## # A tibble: 5 × 3
## Region Purpose avg_trips
## <chr> <chr> <dbl>
## 1 Sydney Visiting 747.
## 2 Melbourne Visiting 619.
## 3 Sydney Business 602.
## 4 North Coast NSW Holiday 588.
## 5 Sydney Holiday 550.
What I found. Sydney with the purpose “Visiting” has the highest average number of overnight trips. That fits what I would expect. Sydney is the largest city in Australia, so it has the most friends and relatives for people to travel and visit.
# Total trips by State, with Region and Purpose combined together.
state_trips <- my_tourism |>
group_by(State) |>
summarise(Trips = sum(Trips))
state_trips
## # A tsibble: 640 x 3 [1Q]
## # Key: State [8]
## State Quarter Trips
## <chr> <qtr> <dbl>
## 1 ACT 1998 Q1 551.
## 2 ACT 1998 Q2 416.
## 3 ACT 1998 Q3 436.
## 4 ACT 1998 Q4 450.
## 5 ACT 1999 Q1 379.
## 6 ACT 1999 Q2 558.
## 7 ACT 1999 Q3 449.
## 8 ACT 1999 Q4 595.
## 9 ACT 2000 Q1 600.
## 10 ACT 2000 Q2 557.
## # ℹ 630 more rows
state_trips |>
autoplot(Trips) +
labs(
title = "Total overnight trips by State",
x = "Quarter",
y = "Trips (thousands)"
)
Why this works. Because state_trips is
still a tsibble, group_by() and summarise()
add up the trips within each quarter instead of collapsing time away.
The result keeps Quarter as the index and uses
State as the only key, which is exactly what the question
asked for.
Question. The aus_arrivals data set
comprises quarterly international arrivals to Australia from Japan, New
Zealand, UK and the US.
autoplot(), gg_season() and
gg_subseries() to compare the differences between the
arrivals from these four countries.My thought process. Each of these three plots
answers a different question, so I use all three on purpose.
autoplot() shows me the long run trend.
gg_season() puts every year on top of the others so I can
see which quarter is busy. gg_subseries() splits the data
by quarter and draws a mean line for each one, which shows whether a
given quarter is changing over time. Looking at only one plot would
leave me guessing.
aus_arrivals |>
autoplot(Arrivals) +
labs(title = "Quarterly international arrivals to Australia",
x = "Quarter", y = "Arrivals (thousands)")
aus_arrivals |>
gg_season(Arrivals) +
labs(title = "Seasonal plot: arrivals to Australia", y = "Arrivals (thousands)")
aus_arrivals |>
gg_subseries(Arrivals) +
labs(title = "Subseries plot: arrivals to Australia", y = "Arrivals (thousands)")
How the four countries differ. All four grow overall, but the seasonal shape is different for each one, and that is the interesting part.
New Zealand peaks in Q3, which is the Australian winter. Japan peaks in Q1. The UK peaks strongly in Q4 and Q1, which lines up with people traveling for the Christmas and summer holidays in Australia. The US has the weakest seasonal pattern of the four.
Unusual observations. Japanese arrivals rise steeply until the late 1990s and then fall and stay down, which matches the Asian financial crisis and Japan’s weak economy after it. There is also a visible drop after 2001, which likely reflects the fall in international travel following the September 11 attacks. New Zealand shows a spike in Q3 2000, which is the Sydney Olympics.
Question. Monthly Australian retail data is provided
in aus_retail. Select one of the time series as follows
(but choose your own seed value):
set.seed(12345678)
myseries <- aus_retail |>
filter(`Series ID` == sample(aus_retail$`Series ID`,1))
Explore your chosen retail time series using the following functions:
autoplot(), gg_season(),
gg_subseries(), gg_lag(),
ACF() |> autoplot().
Can you spot any seasonality, cyclicity and trend? What do you learn about the series?
My thought process. I set my own seed so the same series is picked every time I knit the file. That matters because otherwise my writeup could describe a series that the reader does not get. Then I run the five plots in order from most general to most specific. The ACF comes last because by that point I already have a guess about seasonality, and the ACF is what confirms it with numbers instead of my eyes.
set.seed(624)
myseries <- aus_retail |>
filter(`Series ID` == sample(aus_retail$`Series ID`, 1))
# Which series did I get?
myseries |>
as_tibble() |>
distinct(State, Industry, `Series ID`)
## # A tibble: 1 × 3
## State Industry `Series ID`
## <chr> <chr> <chr>
## 1 New South Wales Takeaway food services A3349792X
myseries |>
autoplot(Turnover) +
labs(title = "Selected Australian retail series", y = "Turnover ($ million)")
myseries |>
gg_season(Turnover) +
labs(title = "Seasonal plot", y = "Turnover ($ million)")
myseries |>
gg_subseries(Turnover) +
labs(title = "Subseries plot", y = "Turnover ($ million)")
myseries |>
gg_lag(Turnover, geom = "point") +
labs(title = "Lag plot")
myseries |>
ACF(Turnover) |>
autoplot() +
labs(title = "ACF of selected retail series")
Trend. There is a strong upward trend across the whole period. Retail turnover is measured in dollars, so it grows because the population grows and because of inflation. An upward slope is expected here.
Seasonality. The seasonal plot shows a clear December spike every year, which is the holiday period, and February is the weakest month of the year. The subseries plot backs this up. The December panel sits well above the others and its mean line is the highest, while the February panel is the lowest. The series I drew is takeaway food services in New South Wales, so the December peak is people eating out over the holidays rather than Christmas gift shopping.
Cyclicity. I do not see a clear cycle here. The movement that is left after trend and season looks like ordinary noise rather than a repeating multi year wave.
What the lag plot and ACF add. The lag plot shows the tightest straight line relationship at lag 12, which means this month looks most like the same month one year ago. The ACF confirms it. All the bars are positive and far outside the blue significance lines, they decay slowly because of the trend, and there are small bumps at lags 12 and 24 caused by the yearly season. A slowly decaying positive ACF like this is the classic signature of a trended series.
Question. Use the following graphics functions:
autoplot(), gg_season(),
gg_subseries(), gg_lag(), ACF()
and explore features from the following time series: “Total Private”
Employed from us_employment,
Bricks from aus_production, Hare
from pelt, “H02” Cost from PBS,
and Barrels from us_gasoline.
My thought process. Five series is a lot, so instead
of running all five plots on all five series I picked the plots that
actually say something for each one. Annual data like Hare
cannot have a seasonal plot at all, because there is only one
observation per year. Recognizing that up front saved me from writing
code that would just throw an error.
us_private <- us_employment |>
filter(Title == "Total Private")
us_private |> autoplot(Employed) +
labs(title = "US Total Private Employment", y = "Employed (thousands)")
us_private |> gg_season(Employed) +
labs(title = "Seasonal plot: US Total Private Employment")
us_private |> ACF(Employed) |> autoplot() +
labs(title = "ACF: US Total Private Employment")
Strong upward trend across the whole period. There is mild seasonality with a dip each January after holiday hiring ends. The clearly unusual years are the recessions, and the drop around 2008 and 2009 is the deepest one in the series. The ACF decays very slowly, which is what a strongly trended series always looks like.
bricks <- aus_production |>
filter(!is.na(Bricks))
bricks |> autoplot(Bricks) + labs(title = "Australian brick production")
bricks |> gg_season(Bricks) + labs(title = "Seasonal plot: Bricks")
bricks |> gg_subseries(Bricks) + labs(title = "Subseries plot: Bricks")
bricks |> ACF(Bricks) |> autoplot() + labs(title = "ACF: Bricks")
Bricks rise until about 1980 and then fall, so the trend changes
direction partway through. That is a good example of why you cannot
describe a whole series with one word. Seasonally, Q1 is the weakest
quarter and Q3 is the strongest, which fits the building season. The
unusual years are the sharp drops in the early 1980s and again in the
early 1990s, both recession periods for Australian construction. I
filtered out the missing values first, since Bricks is not
recorded for the most recent years in the dataset.
pelt |> autoplot(Hare) + labs(title = "Snowshoe hare pelts traded")
pelt |> ACF(Hare) |> autoplot() + labs(title = "ACF: Hare")
This series is annual, so seasonal and subseries plots do not apply. There is no real trend. What dominates is a strong cycle of roughly nine to ten years, which is the well known predator and prey cycle with the lynx. The ACF shows this clearly by rising and falling in a wave and going negative, instead of decaying steadily the way a trended series does. This is the cleanest example in the whole homework of a cycle that is not seasonality, because the peaks do not land on a fixed calendar period.
h02 <- PBS |>
filter(ATC2 == "H02") |>
summarise(Cost = sum(Cost))
h02 |> autoplot(Cost) + labs(title = "H02 drug cost", y = "Cost ($)")
h02 |> gg_season(Cost) + labs(title = "Seasonal plot: H02 Cost")
h02 |> gg_subseries(Cost) + labs(title = "Subseries plot: H02 Cost")
h02 |> ACF(Cost) |> autoplot() + labs(title = "ACF: H02 Cost")
I had to sum across the Concession and Type categories first, because
PBS splits H02 into several series and the question asks
about H02 overall. There is an upward trend and a very strong yearly
pattern. Cost is lowest in February and climbs through the year. That
pattern comes from the way the Australian subsidy scheme resets at the
start of each calendar year, so it is a rule made by people rather than
a weather effect. Seasonality does not have to come from nature.
us_gasoline |> autoplot(Barrels) +
labs(title = "US finished motor gasoline product supplied",
y = "Million barrels per day")
us_gasoline |> gg_season(Barrels) + labs(title = "Seasonal plot: US gasoline")
us_gasoline |> ACF(Barrels) |> autoplot() + labs(title = "ACF: US gasoline")
This is weekly data, so the seasonal period is about 52 rather than 12, and the plots look noisier than the monthly ones. The trend rises until around 2007, flattens, dips during the 2008 recession, and then recovers. Within each year, demand is lowest in winter and highest in summer, which matches the American summer driving season. The unusual years are 2008 and 2009, when the recession and high fuel prices pushed demand down.
Question. The following time plots and ACF plots correspond to four different time series. Your task is to match each time plot in the first row with one of the ACF plots in the second row.
My thought process. This one has no code, because the plots live in the book. What I can do is write down the rules I used to match them, since those rules are the actual point of the exercise. An ACF is a fingerprint of the shape of a series, so I work backward from shape to fingerprint.
The rules I used:
My matching. In the book figure, the series with the obvious yearly cycle goes with the ACF that has regular spikes at the seasonal lags. The series that just wanders upward goes with the ACF that decays slowly and stays positive. The series that looks like static goes with the ACF where every bar is inside the blue bands. That gives 1-B, 2-A, 3-D, 4-C.
The general idea I take away is that a slowly decaying ACF means trend, a wavy ACF means season or cycle, and a flat ACF inside the bands means there is nothing to forecast from the past values alone.
Question. The aus_livestock data
contains the monthly total number of pigs slaughtered in Victoria,
Australia, from Jul 1972 to Dec 2018. Use filter() to
extract pig slaughters in Victoria between 1990 and 1995. Use
autoplot() and ACF() for this data. How do
they differ from white noise? If a longer period of data is used, what
difference does it make to the ACF?
My thought process. The point of this exercise is to compare a short window against a long one, so I build both and run the same two plots on each. That way any difference I describe comes from the length of the window and nothing else. The white noise question is really asking whether the bars break through the blue lines, so that is what I look for.
pigs_vic_90s <- aus_livestock |>
filter(Animal == "Pigs", State == "Victoria",
year(Month) >= 1990, year(Month) <= 1995)
pigs_vic_90s |> autoplot(Count) +
labs(title = "Pigs slaughtered in Victoria, 1990-1995", y = "Count")
pigs_vic_90s |> ACF(Count) |> autoplot() +
labs(title = "ACF: Victoria pigs, 1990-1995")
How this differs from white noise. White noise has no pattern at all, and its ACF bars should nearly all sit inside the blue dashed lines. That is not what happens here. There are 72 observations, so the bands sit at about plus or minus 0.24, and 15 of the 18 bars reach past them. The first lag is around 0.66, which is very high. That tells me one month’s count carries a lot of information about the next month’s, so this series is not white noise, even though the time plot looks fairly jumpy at first glance.
This is why I trust the ACF more than my eyes. A noisy looking plot can still have real structure in it.
pigs_vic_all <- aus_livestock |>
filter(Animal == "Pigs", State == "Victoria")
pigs_vic_all |> autoplot(Count) +
labs(title = "Pigs slaughtered in Victoria, full series", y = "Count")
pigs_vic_all |> ACF(Count) |> autoplot() +
labs(title = "ACF: Victoria pigs, full series")
What the longer period changes. Two things happen. First, the full series shows a long rise and then a fall that the six year window completely hid, so the trend becomes visible. Second, the ACF changes shape. With all 558 months, every one of the 27 bars is positive and outside the bands, the first lag rises to about 0.83, and the decay is slow. That slow positive decay is the trend showing up in the ACF.
There is also a technical reason the bars look more convincing. The blue significance lines are drawn at roughly plus or minus 2 divided by the square root of the number of observations. That takes the bands from about 0.24 with 72 months down to about 0.09 with 558 months. More data means narrower bands, so real correlation is easier to detect. Short samples hide structure. That is the main lesson for me here.
Question. Use the following code to compute the daily changes in Google closing stock prices.
dgoog <- gafa_stock |>
filter(Symbol == "GOOG", year(Date) >= 2018) |>
mutate(trading_day = row_number()) |>
update_tsibble(index = trading_day, regular = TRUE) |>
mutate(diff = difference(Close))
dgoog <- gafa_stock |>
filter(Symbol == "GOOG", year(Date) >= 2018) |>
mutate(trading_day = row_number()) |>
update_tsibble(index = trading_day, regular = TRUE) |>
mutate(diff = difference(Close))
head(dgoog)
## # A tsibble: 6 x 10 [1]
## # Key: Symbol [1]
## Symbol Date Open High Low Close Adj_Close Volume trading_day diff
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <dbl>
## 1 GOOG 2018-01-02 1048. 1067. 1045. 1065 1065 1237600 1 NA
## 2 GOOG 2018-01-03 1064. 1086. 1063. 1082. 1082. 1430200 2 17.5
## 3 GOOG 2018-01-04 1088 1094. 1084. 1086. 1086. 1004600 3 3.92
## 4 GOOG 2018-01-05 1094 1104. 1092 1102. 1102. 1279100 4 15.8
## 5 GOOG 2018-01-08 1102. 1111. 1102. 1107. 1107. 1047600 5 4.71
## 6 GOOG 2018-01-09 1109. 1111. 1101. 1106. 1106. 902500 6 -0.680
Why the re-index was necessary. This goes back to
what I noticed in Exercise 2.1. The stock market is closed on weekends
and holidays, so the Date index has gaps in it. It is daily
data with days missing, which makes it irregular.
That matters because ACF() assumes the observations are
evenly spaced. If the gaps are left in, then “lag 1” would sometimes
mean one day apart and sometimes three days apart across a weekend, and
the answer would be misleading. Creating trading_day with
row_number() gives a clean counter of 1, 2, 3 with no
holes, and update_tsibble(regular = TRUE) tells R to treat
that counter as the new index. Now lag 1 always means one trading day,
which is a consistent thing to measure.
dgoog |>
autoplot(diff) +
labs(title = "Daily changes in Google closing price, 2018 onward",
x = "Trading day", y = "Change in closing price ($)")
dgoog |>
ACF(diff) |>
autoplot() +
labs(title = "ACF of daily changes in Google closing price")
Do the changes look like white noise? Yes. The plot of the differences wobbles around zero with no trend and no seasonal pattern. In the ACF, 22 of the 23 bars sit inside the blue dashed lines, which means those correlations are not different from zero in any meaningful way.
Only one bar crosses, at lag 8, and it is barely past the line. That does not change my answer. With 23 lags and bands set at 95 percent, you expect about one bar to cross by pure chance even when the series really is white noise. Lag 8 also has no real world meaning for daily stock data, so I read it as chance rather than a pattern. I would only change my mind if several bars crossed, or if one crossed at a lag that made sense, like lag 5 for a weekly effect.
What this means in practice. If daily price changes are white noise, then past changes give you nothing to predict future changes with. That is not a failure of the method. It is close to what the efficient market idea would predict, and it is useful to know that before spending time building a model that was never going to work.
One more thing worth noticing. The raw Close price is
clearly not white noise, since it trends. But after taking differences,
what is left looks like noise. Differencing removed the trend, and that
same idea comes back later in the course when we make series
stationary.