Exercise 2.1

Question. Explore the following four time series: Bricks from aus_production, Lynx from pelt, Close from gafa_stock, Demand from vic_elec.

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.

Exercise 2.2

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.

Exercise 2.3

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.

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.

Exercise 2.4

Question. The USgas package contains data on the demand for natural gas in the US.

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.

Exercise 2.5

Question. Download tourism.xlsx from the book website and read it into R using readxl::read_excel().

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.

Exercise 2.6

Question. The aus_arrivals data set comprises quarterly international arrivals to Australia from Japan, New Zealand, UK and the US.

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.

Exercise 2.7

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.

Exercise 2.8

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.

Total Private Employed (us_employment)

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)

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.

Hare (pelt)

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 Cost (PBS)

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.

Barrels (us_gasoline)

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.

Exercise 2.9

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:

  1. If the time plot has a strong trend, the ACF bars start high and decay slowly, and they stay positive for many lags. Trend means today is close to yesterday, and still fairly close to last month.
  2. If the time plot has clear seasonality, the ACF has bumps at the seasonal lag and its multiples. Monthly data shows peaks at lag 12 and 24, quarterly data at lag 4 and 8.
  3. If the time plot has both trend and seasonality, the ACF decays slowly and has waves riding on top of the decay.
  4. If the time plot looks like random noise with no pattern, almost all the ACF bars sit inside the blue dashed lines, which means they are not different from zero.

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.

Exercise 2.10

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.

Exercise 2.11

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.