The Tidyverts family

Tidyverts is a family of R packages for time series analysis and forecasting. It is the work of Rob Hyndman, professor of statistics at Monash University, and his team. The family is intended to be the next-generation replacement for the very popular forecast package, and is currently under active development. The purpose of this document is to provide a quick overview of the capabilities and features of Tidyverts.

Data manipulation and summaries

Tidyverts is designed to integrate with the Tidyverse, the de facto standard for data analytics workflows in R (the similarity in names is something of an in-joke). In particular, it defines a new data structure called a tsibble, which is an extended version of a tibble for working with time series. You can use all the standard dplyr/purrr verbs with tsibbles.

Let’s look at how to do some simple summaries. We’ll use the aus_retail dataset, which contains retail turnover statistics by (Australian) state and industry, going back to 1982. This is part of the tsibbledata package, which contains several example time series datasets.

library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(tsibbledata)
library(tsibble)
library(feasts)
Loading required package: fabletools
library(fable)

slice(aus_retail, 1:6)

A tsibble includes two attributes to hold the time series information in the data:

In the aus_retail dataset, the index is Month, with a range from April 1982 to December 2018. The keys are State and Industry, meaning that the data contains one time series for each combination of these variables.

key_vars(aus_retail)
[1] "State"    "Industry"
State

Industry
index_var(aus_retail)
[1] "Month"
Month
range(aus_retail$Month)
<yearmonth[2]>
[1] "1982 Apr" "2018 Dec"

One thing to be aware of when summarising a tsibble with group_by/summarise is that the index variable is automatically included in the groups. This lets you easily aggregate lower-level time series into higher-level ones. However, if you want to aggregate over the index itself, you have to convert to a tibble first. Let’s look at the total turnover by industry and by state.

aus_retail %>%
    as_tibble() %>%
    group_by(Industry) %>%
    summarise(Turnover=sum(Turnover))
`summarise()` ungrouping output (override with `.groups` argument)
aus_retail %>%
    as_tibble() %>%
    group_by(State) %>%
    summarise(Turnover=sum(Turnover))
`summarise()` ungrouping output (override with `.groups` argument)

There aren’t any surprises here, if you’re familiar with Australian geography. NSW and Victoria are the largest states for retail turnover, closely followed by Queensland. The Northern Territory has the lowest turnover, with Tasmania second-lowest. Canberra (Australian Capital Territory) has higher turnover than Tasmania despite having a slightly smaller population; this is probably because it has higher average income, being the national capital.

Other methods provided for grouping and summarising tsibbles include index_by, which lets you manipulate how the index variable is treated as a grouping variable; and group_by_key, which automatically groups by the key variables.

Plotting the data

Let’s have a look at the trends in the data over time. Tidyverts defines a ggplot::autoplot method for tsibbles, which is intended to give you a reasonable plot for a time series. When given a dataset with keys, it will produce a separate time plot for each key combination. For this dataset, there are about \(8 \times 20 = 160\) combinations, which is too many to show on a single plot; instead, let’s look at the state-level aggregation.

library(ggplot2)

aus_retail %>%
    group_by(State) %>%  # we don't need to include Month in the groups
    summarise(Turnover=sum(Turnover)) %>%
    autoplot(Turnover) +
        theme(legend.position="bottom") +
        scale_color_brewer(palette="Dark2")

Observations about this plot:

Repeating the plot on the log scale confirms the multiplicative nature of the data, and also highlights differences between states in their trends over the period.

aus_retail %>%
    group_by(State) %>%
    summarise(Turnover=sum(Turnover)) %>%
    autoplot(Turnover) +
        theme(legend.position="bottom") +
        scale_color_brewer(palette="Dark2") +
        scale_y_log10() +
        annotation_logticks()

We can also look at the corresponding plots for some individual industries, with a suitably defined helper function.

industry_autologplot <- function(industry)
{
    aus_retail %>%
        filter(Industry == industry) %>%
        select(-Industry) %>%
        autoplot(Turnover) +
            theme(legend.position="bottom") +
            scale_color_brewer(palette="Dark2") +
            scale_y_log10() +
            annotation_logticks() +
            ggtitle(industry)
}

industry_autologplot("Food retailing")

industry_autologplot("Cafes, restaurants and takeaway food services")

industry_autologplot("Clothing, footwear and personal accessory retailing")

industry_autologplot("Electrical and electronic goods retailing")

The Tidyverts framework defines a number of other functions to help with exploratory analysis, in the feasts package. For plotting, the gg_subseries function produces a seasonal subseries plot, which facets the time series by each season in the seasonal period:

aus_retail %>%
    filter(Industry == "Food retailing", State == "New South Wales") %>%
    gg_season(Turnover, facet_period="10 years") +
        theme(axis.text.x=element_blank(), legend.position="bottom") +
        scale_y_log10() +
        annotation_logticks() +
        ggtitle("Food retailing/New South Wales")

The gg_season function produces a plot where the x-axis shows data from within each season. This plot type allows the underlying seasonal pattern to be seen more clearly, and can be useful in identifying years in which the pattern changes. Here, the blue and black lines are the average turnover and trend over time, within each month.

aus_retail %>%
    filter(Industry == "Food retailing", State == "New South Wales") %>%
    gg_subseries(Turnover) +
        theme(axis.text.x=element_blank()) +
        scale_y_log10() +
        annotation_logticks() +
        ggtitle("Food retailing/New South Wales")

For this dataset though, these plots are probably less helpful because of the large number of years.

LS0tCnRpdGxlOiAiVXNpbmcgVGlkeXZlcnRzIHdpdGggdGhlIEF1c3RyYWxpYW4gcmV0YWlsIGRhdGE6IGludHJvZHVjdGlvbiBhbmQgZXhwbG9yYXRpb24iCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCiMjIFRoZSBUaWR5dmVydHMgZmFtaWx5CgpbKipUaWR5dmVydHMqKl0oaHR0cHM6Ly90aWR5dmVydHMub3JnKSBpcyBhIGZhbWlseSBvZiBSIHBhY2thZ2VzIGZvciB0aW1lIHNlcmllcyBhbmFseXNpcyBhbmQgZm9yZWNhc3RpbmcuIEl0IGlzIHRoZSB3b3JrIG9mIFJvYiBIeW5kbWFuLCBwcm9mZXNzb3Igb2Ygc3RhdGlzdGljcyBhdCBNb25hc2ggVW5pdmVyc2l0eSwgYW5kIGhpcyB0ZWFtLiBUaGUgZmFtaWx5IGlzIGludGVuZGVkIHRvIGJlIHRoZSBuZXh0LWdlbmVyYXRpb24gcmVwbGFjZW1lbnQgZm9yIHRoZSB2ZXJ5IHBvcHVsYXIgYGZvcmVjYXN0YCBwYWNrYWdlLCBhbmQgaXMgY3VycmVudGx5IHVuZGVyIGFjdGl2ZSBkZXZlbG9wbWVudC4gVGhlIHB1cnBvc2Ugb2YgdGhpcyBkb2N1bWVudCBpcyB0byBwcm92aWRlIGEgcXVpY2sgb3ZlcnZpZXcgb2YgdGhlIGNhcGFiaWxpdGllcyBhbmQgZmVhdHVyZXMgb2YgVGlkeXZlcnRzLgoKIyMgRGF0YSBtYW5pcHVsYXRpb24gYW5kIHN1bW1hcmllcwoKVGlkeXZlcnRzIGlzIGRlc2lnbmVkIHRvIGludGVncmF0ZSB3aXRoIHRoZSBbVGlkeXZlcnNlXShodHRwczovL3RpZHl2ZXJzZS5vcmcpLCB0aGUgZGUgZmFjdG8gc3RhbmRhcmQgZm9yIGRhdGEgYW5hbHl0aWNzIHdvcmtmbG93cyBpbiBSICh0aGUgc2ltaWxhcml0eSBpbiBuYW1lcyBpcyBzb21ldGhpbmcgb2YgYW4gaW4tam9rZSkuIEluIHBhcnRpY3VsYXIsIGl0IGRlZmluZXMgYSBuZXcgZGF0YSBzdHJ1Y3R1cmUgY2FsbGVkIGEgX3RzaWJibGVfLCB3aGljaCBpcyBhbiBleHRlbmRlZCB2ZXJzaW9uIG9mIGEgdGliYmxlIGZvciB3b3JraW5nIHdpdGggdGltZSBzZXJpZXMuIFlvdSBjYW4gdXNlIGFsbCB0aGUgc3RhbmRhcmQgZHBseXIvcHVycnIgdmVyYnMgd2l0aCB0c2liYmxlcy4KCkxldCdzIGxvb2sgYXQgaG93IHRvIGRvIHNvbWUgc2ltcGxlIHN1bW1hcmllcy4gV2UnbGwgdXNlIHRoZSBgYXVzX3JldGFpbGAgZGF0YXNldCwgd2hpY2ggY29udGFpbnMgcmV0YWlsIHR1cm5vdmVyIHN0YXRpc3RpY3MgYnkgKEF1c3RyYWxpYW4pIHN0YXRlIGFuZCBpbmR1c3RyeSwgZ29pbmcgYmFjayB0byAxOTgyLiBUaGlzIGlzIHBhcnQgb2YgdGhlIHRzaWJibGVkYXRhIHBhY2thZ2UsIHdoaWNoIGNvbnRhaW5zIHNldmVyYWwgZXhhbXBsZSB0aW1lIHNlcmllcyBkYXRhc2V0cy4KCmBgYHtyfQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KHRzaWJibGVkYXRhKQpsaWJyYXJ5KHRzaWJibGUpCmxpYnJhcnkoZmVhc3RzKQpsaWJyYXJ5KGZhYmxlKQoKc2xpY2UoYXVzX3JldGFpbCwgMTo2KQpgYGAKCkEgdHNpYmJsZSBpbmNsdWRlcyB0d28gYXR0cmlidXRlcyB0byBob2xkIHRoZSB0aW1lIHNlcmllcyBpbmZvcm1hdGlvbiBpbiB0aGUgZGF0YToKCi0gQW4gX2luZGV4Xywgd2hpY2ggaXMgdGhlIHRpbWUgdmFyaWFibGUKLSBPcHRpb25hbGx5LCBvbmUgb3IgbW9yZSBfa2V5c18sIHdoaWNoIGlkZW50aWZ5IHNlcGFyYXRlIHRpbWUgc2VyaWVzIHdpdGhpbiB0aGUgZGF0YXNldAoKSW4gdGhlIGBhdXNfcmV0YWlsYCBkYXRhc2V0LCB0aGUgaW5kZXggaXMgYE1vbnRoYCwgd2l0aCBhIHJhbmdlIGZyb20gQXByaWwgMTk4MiB0byBEZWNlbWJlciAyMDE4LiBUaGUga2V5cyBhcmUgYFN0YXRlYCBhbmQgYEluZHVzdHJ5YCwgbWVhbmluZyB0aGF0IHRoZSBkYXRhIGNvbnRhaW5zIG9uZSB0aW1lIHNlcmllcyBmb3IgZWFjaCBjb21iaW5hdGlvbiBvZiB0aGVzZSB2YXJpYWJsZXMuCgpgYGB7cn0Ka2V5X3ZhcnMoYXVzX3JldGFpbCkKCmluZGV4X3ZhcihhdXNfcmV0YWlsKQoKcmFuZ2UoYXVzX3JldGFpbCRNb250aCkKYGBgCgpPbmUgdGhpbmcgdG8gYmUgYXdhcmUgb2Ygd2hlbiBzdW1tYXJpc2luZyBhIHRzaWJibGUgd2l0aCBgZ3JvdXBfYnkvc3VtbWFyaXNlYCBpcyB0aGF0IHRoZSBpbmRleCB2YXJpYWJsZSBpcyBhdXRvbWF0aWNhbGx5IGluY2x1ZGVkIGluIHRoZSBncm91cHMuIFRoaXMgbGV0cyB5b3UgZWFzaWx5IGFnZ3JlZ2F0ZSBsb3dlci1sZXZlbCB0aW1lIHNlcmllcyBpbnRvIGhpZ2hlci1sZXZlbCBvbmVzLiBIb3dldmVyLCBpZiB5b3Ugd2FudCB0byBhZ2dyZWdhdGUgb3ZlciB0aGUgaW5kZXggaXRzZWxmLCB5b3UgaGF2ZSB0byBjb252ZXJ0IHRvIGEgX3RpYmJsZV8gZmlyc3QuIExldCdzIGxvb2sgYXQgdGhlIHRvdGFsIHR1cm5vdmVyIGJ5IGluZHVzdHJ5IGFuZCBieSBzdGF0ZS4gCgpgYGB7cn0KYXVzX3JldGFpbCAlPiUKICAgIGFzX3RpYmJsZSgpICU+JQogICAgZ3JvdXBfYnkoSW5kdXN0cnkpICU+JQogICAgc3VtbWFyaXNlKFR1cm5vdmVyPXN1bShUdXJub3ZlcikpCgphdXNfcmV0YWlsICU+JQogICAgYXNfdGliYmxlKCkgJT4lCiAgICBncm91cF9ieShTdGF0ZSkgJT4lCiAgICBzdW1tYXJpc2UoVHVybm92ZXI9c3VtKFR1cm5vdmVyKSkKYGBgCgpUaGVyZSBhcmVuJ3QgYW55IHN1cnByaXNlcyBoZXJlLCBpZiB5b3UncmUgZmFtaWxpYXIgd2l0aCBBdXN0cmFsaWFuIGdlb2dyYXBoeS4gTlNXIGFuZCBWaWN0b3JpYSBhcmUgdGhlIGxhcmdlc3Qgc3RhdGVzIGZvciByZXRhaWwgdHVybm92ZXIsIGNsb3NlbHkgZm9sbG93ZWQgYnkgUXVlZW5zbGFuZC4gVGhlIE5vcnRoZXJuIFRlcnJpdG9yeSBoYXMgdGhlIGxvd2VzdCB0dXJub3Zlciwgd2l0aCBUYXNtYW5pYSBzZWNvbmQtbG93ZXN0LiBDYW5iZXJyYSAoQXVzdHJhbGlhbiBDYXBpdGFsIFRlcnJpdG9yeSkgaGFzIGhpZ2hlciB0dXJub3ZlciB0aGFuIFRhc21hbmlhIGRlc3BpdGUgaGF2aW5nIGEgc2xpZ2h0bHkgc21hbGxlciBwb3B1bGF0aW9uOyB0aGlzIGlzIHByb2JhYmx5IGJlY2F1c2UgaXQgaGFzIGhpZ2hlciBhdmVyYWdlIGluY29tZSwgYmVpbmcgdGhlIG5hdGlvbmFsIGNhcGl0YWwuCgpPdGhlciBtZXRob2RzIHByb3ZpZGVkIGZvciBncm91cGluZyBhbmQgc3VtbWFyaXNpbmcgdHNpYmJsZXMgaW5jbHVkZSBgaW5kZXhfYnlgLCB3aGljaCBsZXRzIHlvdSBtYW5pcHVsYXRlIGhvdyB0aGUgaW5kZXggdmFyaWFibGUgaXMgdHJlYXRlZCBhcyBhIGdyb3VwaW5nIHZhcmlhYmxlOyBhbmQgYGdyb3VwX2J5X2tleWAsIHdoaWNoIGF1dG9tYXRpY2FsbHkgZ3JvdXBzIGJ5IHRoZSBrZXkgdmFyaWFibGVzLgoKIyMgUGxvdHRpbmcgdGhlIGRhdGEKCkxldCdzIGhhdmUgYSBsb29rIGF0IHRoZSB0cmVuZHMgaW4gdGhlIGRhdGEgb3ZlciB0aW1lLiBUaWR5dmVydHMgZGVmaW5lcyBhIGBnZ3Bsb3Q6OmF1dG9wbG90YCBtZXRob2QgZm9yIHRzaWJibGVzLCB3aGljaCBpcyBpbnRlbmRlZCB0byBnaXZlIHlvdSBhIHJlYXNvbmFibGUgcGxvdCBmb3IgYSB0aW1lIHNlcmllcy4gV2hlbiBnaXZlbiBhIGRhdGFzZXQgd2l0aCBrZXlzLCBpdCB3aWxsIHByb2R1Y2UgYSBzZXBhcmF0ZSB0aW1lIHBsb3QgZm9yIGVhY2gga2V5IGNvbWJpbmF0aW9uLiBGb3IgdGhpcyBkYXRhc2V0LCB0aGVyZSBhcmUgYWJvdXQgJDggXHRpbWVzIDIwID0gMTYwJCBjb21iaW5hdGlvbnMsIHdoaWNoIGlzIHRvbyBtYW55IHRvIHNob3cgb24gYSBzaW5nbGUgcGxvdDsgaW5zdGVhZCwgbGV0J3MgbG9vayBhdCB0aGUgc3RhdGUtbGV2ZWwgYWdncmVnYXRpb24uCgpgYGB7cn0KbGlicmFyeShnZ3Bsb3QyKQoKYXVzX3JldGFpbCAlPiUKICAgIGdyb3VwX2J5KFN0YXRlKSAlPiUgICMgd2UgZG9uJ3QgbmVlZCB0byBpbmNsdWRlIE1vbnRoIGluIHRoZSBncm91cHMKICAgIHN1bW1hcmlzZShUdXJub3Zlcj1zdW0oVHVybm92ZXIpKSAlPiUKICAgIGF1dG9wbG90KFR1cm5vdmVyKSArCiAgICAgICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uPSJib3R0b20iKSArCiAgICAgICAgc2NhbGVfY29sb3JfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikKYGBgCgpPYnNlcnZhdGlvbnMgYWJvdXQgdGhpcyBwbG90OgoKLSBUaGVyZSBpcyBhIHZlcnkgb2J2aW91cyBpbmNyZWFzaW5nIHRyZW5kIG92ZXIgdGltZSwgd2hpY2ggaXMgdG8gYmUgZXhwZWN0ZWQgZ2l2ZW4gZ2VuZXJhbCBlY29ub21pYyBncm93dGguCi0gVGhlcmUgaXMgYWxzbyB2ZXJ5IHN0cm9uZyBzZWFzb25hbGl0eSwgd2l0aCBhIHNwaWtlIGluIERlY2VtYmVyIHJlZmxlY3RpbmcgQ2hyaXN0bWFzIHNwZW5kaW5nLgotIEJvdGggdHJlbmQgYW5kIHNlYXNvbmFsaXR5IGFwcGVhciB0byBiZSBtdWx0aXBsaWNhdGl2ZS4KClJlcGVhdGluZyB0aGUgcGxvdCBvbiB0aGUgbG9nIHNjYWxlIGNvbmZpcm1zIHRoZSBtdWx0aXBsaWNhdGl2ZSBuYXR1cmUgb2YgdGhlIGRhdGEsIGFuZCBhbHNvIGhpZ2hsaWdodHMgZGlmZmVyZW5jZXMgYmV0d2VlbiBzdGF0ZXMgaW4gdGhlaXIgdHJlbmRzIG92ZXIgdGhlIHBlcmlvZC4KCmBgYHtyfQphdXNfcmV0YWlsICU+JQogICAgZ3JvdXBfYnkoU3RhdGUpICU+JQogICAgc3VtbWFyaXNlKFR1cm5vdmVyPXN1bShUdXJub3ZlcikpICU+JQogICAgYXV0b3Bsb3QoVHVybm92ZXIpICsKICAgICAgICB0aGVtZShsZWdlbmQucG9zaXRpb249ImJvdHRvbSIpICsKICAgICAgICBzY2FsZV9jb2xvcl9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArCiAgICAgICAgc2NhbGVfeV9sb2cxMCgpICsKICAgICAgICBhbm5vdGF0aW9uX2xvZ3RpY2tzKCkKYGBgCgpXZSBjYW4gYWxzbyBsb29rIGF0IHRoZSBjb3JyZXNwb25kaW5nIHBsb3RzIGZvciBzb21lIGluZGl2aWR1YWwgaW5kdXN0cmllcywgd2l0aCBhIHN1aXRhYmx5IGRlZmluZWQgaGVscGVyIGZ1bmN0aW9uLgoKYGBge3J9CmluZHVzdHJ5X2F1dG9sb2dwbG90IDwtIGZ1bmN0aW9uKGluZHVzdHJ5KQp7CiAgICBhdXNfcmV0YWlsICU+JQogICAgICAgIGZpbHRlcihJbmR1c3RyeSA9PSBpbmR1c3RyeSkgJT4lCiAgICAgICAgc2VsZWN0KC1JbmR1c3RyeSkgJT4lCiAgICAgICAgYXV0b3Bsb3QoVHVybm92ZXIpICsKICAgICAgICAgICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uPSJib3R0b20iKSArCiAgICAgICAgICAgIHNjYWxlX2NvbG9yX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsKICAgICAgICAgICAgc2NhbGVfeV9sb2cxMCgpICsKICAgICAgICAgICAgYW5ub3RhdGlvbl9sb2d0aWNrcygpICsKICAgICAgICAgICAgZ2d0aXRsZShpbmR1c3RyeSkKfQoKaW5kdXN0cnlfYXV0b2xvZ3Bsb3QoIkZvb2QgcmV0YWlsaW5nIikKaW5kdXN0cnlfYXV0b2xvZ3Bsb3QoIkNhZmVzLCByZXN0YXVyYW50cyBhbmQgdGFrZWF3YXkgZm9vZCBzZXJ2aWNlcyIpCmluZHVzdHJ5X2F1dG9sb2dwbG90KCJDbG90aGluZywgZm9vdHdlYXIgYW5kIHBlcnNvbmFsIGFjY2Vzc29yeSByZXRhaWxpbmciKQppbmR1c3RyeV9hdXRvbG9ncGxvdCgiRWxlY3RyaWNhbCBhbmQgZWxlY3Ryb25pYyBnb29kcyByZXRhaWxpbmciKQpgYGAKClRoZSBUaWR5dmVydHMgZnJhbWV3b3JrIGRlZmluZXMgYSBudW1iZXIgb2Ygb3RoZXIgZnVuY3Rpb25zIHRvIGhlbHAgd2l0aCBleHBsb3JhdG9yeSBhbmFseXNpcywgaW4gdGhlIGZlYXN0cyBwYWNrYWdlLiBGb3IgcGxvdHRpbmcsIHRoZSBgZ2dfc3Vic2VyaWVzYCBmdW5jdGlvbiBwcm9kdWNlcyBhIHNlYXNvbmFsIHN1YnNlcmllcyBwbG90LCB3aGljaCBmYWNldHMgdGhlIHRpbWUgc2VyaWVzIGJ5IGVhY2ggc2Vhc29uIGluIHRoZSBzZWFzb25hbCBwZXJpb2Q6CgpgYGB7cn0KYXVzX3JldGFpbCAlPiUKICAgIGZpbHRlcihJbmR1c3RyeSA9PSAiRm9vZCByZXRhaWxpbmciLCBTdGF0ZSA9PSAiTmV3IFNvdXRoIFdhbGVzIikgJT4lCiAgICBnZ19zZWFzb24oVHVybm92ZXIsIGZhY2V0X3BlcmlvZD0iMTAgeWVhcnMiKSArCiAgICAgICAgdGhlbWUoYXhpcy50ZXh0Lng9ZWxlbWVudF9ibGFuaygpLCBsZWdlbmQucG9zaXRpb249ImJvdHRvbSIpICsKICAgICAgICBzY2FsZV95X2xvZzEwKCkgKwogICAgICAgIGFubm90YXRpb25fbG9ndGlja3MoKSArCiAgICAgICAgZ2d0aXRsZSgiRm9vZCByZXRhaWxpbmcvTmV3IFNvdXRoIFdhbGVzIikKYGBgCgpUaGUgYGdnX3NlYXNvbmAgZnVuY3Rpb24gcHJvZHVjZXMgYSBwbG90IHdoZXJlIHRoZSB4LWF4aXMgc2hvd3MgZGF0YSBmcm9tIHdpdGhpbiBlYWNoIHNlYXNvbi4gVGhpcyBwbG90IHR5cGUgYWxsb3dzIHRoZSB1bmRlcmx5aW5nIHNlYXNvbmFsIHBhdHRlcm4gdG8gYmUgc2VlbiBtb3JlIGNsZWFybHksIGFuZCBjYW4gYmUgdXNlZnVsIGluIGlkZW50aWZ5aW5nIHllYXJzIGluIHdoaWNoIHRoZSBwYXR0ZXJuIGNoYW5nZXMuIEhlcmUsIHRoZSBibHVlIGFuZCBibGFjayBsaW5lcyBhcmUgdGhlIGF2ZXJhZ2UgdHVybm92ZXIgYW5kIHRyZW5kIG92ZXIgdGltZSwgd2l0aGluIGVhY2ggbW9udGguCgpgYGB7cn0KYXVzX3JldGFpbCAlPiUKICAgIGZpbHRlcihJbmR1c3RyeSA9PSAiRm9vZCByZXRhaWxpbmciLCBTdGF0ZSA9PSAiTmV3IFNvdXRoIFdhbGVzIikgJT4lCiAgICBnZ19zdWJzZXJpZXMoVHVybm92ZXIpICsKICAgICAgICB0aGVtZShheGlzLnRleHQueD1lbGVtZW50X2JsYW5rKCkpICsKICAgICAgICBzY2FsZV95X2xvZzEwKCkgKwogICAgICAgIGFubm90YXRpb25fbG9ndGlja3MoKSArCiAgICAgICAgZ2d0aXRsZSgiRm9vZCByZXRhaWxpbmcvTmV3IFNvdXRoIFdhbGVzIikKYGBgCgpGb3IgdGhpcyBkYXRhc2V0IHRob3VnaCwgdGhlc2UgcGxvdHMgYXJlIHByb2JhYmx5IGxlc3MgaGVscGZ1bCBiZWNhdXNlIG9mIHRoZSBsYXJnZSBudW1iZXIgb2YgeWVhcnMuCgo=