
Introduction
Đây là tài liệu hướng dẫn thực hành R cho khóa học Business Forecasting tại Monash University dành riêng cho H.L. Giảng viên của khóa học này là Prof George Athanasopoulos - đồng tác giả của Forecasting: Principles and Practice.
tsibble objects
Kiểu tổ chức dữ liệu cho time series được sử dụng trong giáo trình Forecasting: Principles and Practice và cả khóa học Business Forecasting là tsibble. Tất cả các dữ liệu đính kèm thư viện fpp3 đều ở dạng này và được gọi là tsibble object. Chúng ta lấy ví dụ bộ dữ liệu gafa_stock:
# Clear R environment:
rm(list = ls())
# Load fpp3 package:
library(fpp3)
# Load gafa_stock data set:
data("gafa_stock")
Sau khi load bộ dữ liệu này chúng ta có thể xem qua:
# Show some observations:
head(gafa_stock)
## # A tsibble: 6 x 8 [!]
## # Key: Symbol [1]
## Symbol Date Open High Low Close Adj_Close Volume
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 AAPL 2014-01-02 79.4 79.6 78.9 79.0 67.0 58671200
## 2 AAPL 2014-01-03 79.0 79.1 77.2 77.3 65.5 98116900
## 3 AAPL 2014-01-06 76.8 78.1 76.2 77.7 65.9 103152700
## 4 AAPL 2014-01-07 77.8 78.0 76.8 77.1 65.4 79302300
## 5 AAPL 2014-01-08 77.0 77.9 77.0 77.6 65.8 64632400
## 6 AAPL 2014-01-09 78.1 78.1 76.5 76.6 65.0 69787200
Nếu dữ liệu time series ở dạng tsibble thì chúng ta có thể sử dụng tất cả các hàm và mô hình của fpp3. Ví dụ, để vẽ đồ thị chỉ cần sử dụng một lệnh đơn giản như sau:

Như vậy chỉ bằng lệnh autoplot()
chúng ta có thể tạo ra plot cho đồng thời cả 4 mã cổ phiếu.
Chúng ta có thể áp dụng các hàm của thư viện dplyr cho tsibble:
# Use mutate() for creating a new column:
gafa_new <- gafa_stock %>% mutate(open_close = Open / Close)
head(gafa_new)
## # A tsibble: 6 x 9 [!]
## # Key: Symbol [1]
## Symbol Date Open High Low Close Adj_Close Volume open_close
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 AAPL 2014-01-02 79.4 79.6 78.9 79.0 67.0 58671200 1.00
## 2 AAPL 2014-01-03 79.0 79.1 77.2 77.3 65.5 98116900 1.02
## 3 AAPL 2014-01-06 76.8 78.1 76.2 77.7 65.9 103152700 0.988
## 4 AAPL 2014-01-07 77.8 78.0 76.8 77.1 65.4 79302300 1.01
## 5 AAPL 2014-01-08 77.0 77.9 77.0 77.6 65.8 64632400 0.991
## 6 AAPL 2014-01-09 78.1 78.1 76.5 76.6 65.0 69787200 1.02
Như vậy chúng ta đã tạo thêm cột biến có tên open_close bằng được tạo ra bằng cách lấy giá mở cửa chia cho giá đóng cửa.
Bài tập dành cho bạn:
- Sử dụng hàm
filter()
lấy ra các thông tin cho mã cổ phiếu AAPL từ gafa_stock.
- Sử dụng hàm
select()
lấy ra dữ liệu chỉ gồm hai cột Date và Open từ gafa_stock.
Để sử dụng các hàm của fpp3 điều trước tiên cần làm là convert data đó về tsibble object bằng hàm as_tsibble()
. Một ví dụ là câu hỏi trong assignment có liên quan đến dữ liệu về nhiệt đột ở Melbourne Airport. Trước hết lấy dữ liệu về nhiệt độ, ví dụ, của tháng 8 - 2021 tại đây rồi load bộ dữ liệu này:
# Load data:
tem_airport <- read.csv("C:/Users/Admin/Documents/IDCJDW3049.202108.csv", skip = 4)
Chỉ giữ lại cột thứ 2 và 4 của tem_airport bằng hàm select()
:
# Select some columns:
mini_data <- tem_airport %>% select(Date, Maximum.temperature...C.)
# Show data:
head(mini_data)
## Date Maximum.temperature...C.
## 1 2021-08-1 14.8
## 2 2021-08-2 14.7
## 3 2021-08-3 13.6
## 4 2021-08-4 14.1
## 5 2021-08-5 15.5
## 6 2021-08-6 13.9
# Data type:
str(mini_data)
## 'data.frame': 31 obs. of 2 variables:
## $ Date : chr "2021-08-1" "2021-08-2" "2021-08-3" "2021-08-4" ...
## $ Maximum.temperature...C.: num 14.8 14.7 13.6 14.1 15.5 13.9 14.8 15.2 15.2 17.8 ...
Cột biến Date là text. Chúng ta cần convert cột biến này về date-time như sau bằng hàm ymd()
của thư viện lubridate:
library(lubridate)
mini_data_ymd <- mini_data %>% mutate(Date = ymd(Date))
Đến lúc này chúng ta có thể sử dụng hàm as_tsibble()
:
data_tsibble <- as_tsibble(mini_data_ymd, index = Date)
Assignment Convention
Đến đây cần nói rõ cho bạn rõ về quy ước sử dụng kí hiệu gán (Assignment). Kí hiệu gán thường thấy nhất là <-. Chẳng hạn như đoạn R codes sau:
data_tsibble <- as_tsibble(mini_data_ymd, index = Date)
Tương tự ta có thể gán giá trị 10 cho cái gọi là object có tên là x như sau:
R cho phép sử dụng một kí hiệu gán theo style khác trông cứ như là “ngược lại”. Chẳng hạn đoạn R codes ở trên có thể được “trình bày” theo cách khác như sau:
as_tsibble(mini_data_ymd, index = Date) -> data_tsibble
Tương tự như vậy là gán 10 cho x:
Hai cách thức viết/trình bày R codes như trên có giá trị như nhau. Bạn cần nắm rõ cái này để đọc R codes của người khác và viết codes cho mình.
Pipe Operator
R sử dụng một số toán tử mà quan trọng nhất trong số đó là toán tử pipe (pipe operator), có kí hiệu là %>%. Bạn đã thấy toán tử này ở trên.
Để làm rõ vai trò của toán tử này chúng ta xét bài toán sau. Xét hàm hợp y = tan(sin(x)). Để tính giá trị của y khi x = 1 với ngôn ngữ R thì chúng ta có thể làm từng bước sau:
# Stage 1:
x <- 1
# Stage 2:
trung_gian <- sin(x)
# Stage 3:
y <- tan(trung_gian)
Đến đây chúng ta có giá trị cuối cùng của y là 1.11894:
## [1] 1.11894
Cách làm trên chúng ta phải tạo ra object trung gian có tên trung_gian. Với toán tử Pipe chúng ta không cần tạo ra trung gian này mà vẫn có kết quả cuối cùng 1.11894 như sau:
## [1] 1.11894
Chú ý rằng với toán tử Pipe chúng ta có thể viết codes theo một cách thức khác như sau:
x %>% sin() -> trung_gian
trung_gian %>% tan() -> y
Hoặc như sau cũng được:
trung_gian <- x %>% sin()
y <- trung_gian %>% tan()
The forecaster’s toolbox
Mục này tương ứng với Chapter 5 của Forecasting: Principles and Practice, 3E. Để minh họa, hãy trở lại với quesion 1 của Assignment 1 là dự báo giá đóng cửa của Google. Như có thể thấy, tại thời điểm nhận được Assignment 1 là 11-03-2022 nhưng lại yêu cầu dự báo là ngày 25-03-2022. Tức là dự báo trước 15 ngày.
Để giải quyết quesion 1 trước hết chúng ta, ví dụ, download dữ liệu của Google tại thời điểm 11-03-2022 và hất ngược về phía trước 1 năm chẳng hạn. Chúng ta có thể sử dụng hàm tq_get()
của thư viện tidyquant để lấy dữ liệu cần thiết như sau:
# Load tidyquant package:
library(tidyquant)
# Load data for Google:
tq_get(x = "GOOGL", from = "2021-03-11", to = "2022-03-11") -> google_price
# Show some observations:
google_price %>% head()
## # A tibble: 6 x 8
## symbol date open high low close volume adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 GOOGL 2021-03-11 2058. 2111. 2056. 2101. 1385100 2101.
## 2 GOOGL 2021-03-12 2076. 2078. 2032. 2050 1690900 2050
## 3 GOOGL 2021-03-15 2045. 2055. 2028. 2054. 1308400 2054.
## 4 GOOGL 2021-03-16 2066. 2114. 2059. 2084. 1595000 2084.
## 5 GOOGL 2021-03-17 2068. 2099 2044. 2082. 1319100 2082.
## 6 GOOGL 2021-03-18 2048. 2069. 2019. 2021. 1585000 2021.
Dữ liệu Google ở trên đang ở dạng tibble (có thể gọi tên khác là data frame). Kiểu tổ chức dữ liệu này không phải là tsibble do vậy ta không thể áp dụng trực tiếp các hàm của fpp3, như autoplot()
, cho object này được. Để sử dụng được các hàm của fpp3, chúng ta trước hết phải convert google_price về dạng tsibble như sau:
# Convert to tsibble:
as_tsibble(google_price, index = date) -> google_price_ts
Đến đây có thể sử dụng bất kì hàm nào của fpp3, ví dụ, autoplot()
để hình ảnh hóa dữ liệu:
# Plot for close price:
google_price_ts %>%
autoplot(close) +
labs(y = "Closing Price (in USD)",
x = "",
title = "Figure 1: Closing price for Google")

Chú ý rằng để sử dụng được các method, các hàm của fpp3 thì time series nếu theo ngày, là phải liên tiếp nhau. Tuy nhiên yêu cầu này không được thỏa mãn. Ví dụ:
google_price_ts %>% head()
## # A tsibble: 6 x 8 [1D]
## symbol date open high low close volume adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 GOOGL 2021-03-11 2058. 2111. 2056. 2101. 1385100 2101.
## 2 GOOGL 2021-03-12 2076. 2078. 2032. 2050 1690900 2050
## 3 GOOGL 2021-03-15 2045. 2055. 2028. 2054. 1308400 2054.
## 4 GOOGL 2021-03-16 2066. 2114. 2059. 2084. 1595000 2084.
## 5 GOOGL 2021-03-17 2068. 2099 2044. 2082. 1319100 2082.
## 6 GOOGL 2021-03-18 2048. 2069. 2019. 2021. 1585000 2021.
Có thể thấy ngày 2021-03-12 rồi “nhảy cóc” lên ngày 2021-03-15 luôn. Để xử lí vấn đề này chúng ta sử dụng hàm fill_gaps()
của hệ sinh thái fpp3:
google_price_ts %>% fill_gaps() -> google_price_ts_filled
Hãy so sánh bằng cách quan sát kĩ kết quả dưới đây:
google_price_ts_filled %>% head()
## # A tsibble: 6 x 8 [1D]
## symbol date open high low close volume adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 GOOGL 2021-03-11 2058. 2111. 2056. 2101. 1385100 2101.
## 2 GOOGL 2021-03-12 2076. 2078. 2032. 2050 1690900 2050
## 3 <NA> 2021-03-13 NA NA NA NA NA NA
## 4 <NA> 2021-03-14 NA NA NA NA NA NA
## 5 GOOGL 2021-03-15 2045. 2055. 2028. 2054. 1308400 2054.
## 6 GOOGL 2021-03-16 2066. 2114. 2059. 2084. 1595000 2084.
Có thể thấy 2021-03-13 và 2021-03-14 là missing data - tức là không có quan sát cho hai ngày này. Đến đây chúng ta có thể sử dụng tsibble này cho dự báo.
Để dự báo, chúng ta có thể vận dụng ví dụ Australian quarterly beer production được trình bày trong textbook. Cụ thể, có thể sử dụng method có tên Snaive để dự báo. Trước hết fit method này trên dữ liệu ở dạng tsibble ở trên:
# Fit the method:
close_fit <- google_price_ts_filled %>%
model(SNAIVE = SNAIVE(close))
Một cách trình bày khác mà ở đó không muốn tạo ra object trung gian google_price_ts_filled thì R codes sẽ như sau:
# Fit the method:
close_fit <- google_price_ts %>%
fill_gaps() %>%
model(SNAIVE = SNAIVE(close))
Chúng ta có thể sử dụng method này để dự báo, ví dụ, cho 30 ngày sắp tới với khoảng tin cậy 80%:
close_fit %>%
forecast(h = 30) %>%
hilo(80) -> df_forecasts
Nếu 30 mà chưa đủ cover 2022-03-25 thì bạn có thể thay bằng, ví dụ, 1001. Thực tế thì h = 30 ở đoạn R codes trên là đã lớn hơn trước 15 ngày rồi. Lúc này chúng ta có thể chỉ ra kết quả dự báo tại 2022-03-25 như sau:
df_forecasts %>%
filter(date == ymd("2022-03-25"))
## # A tsibble: 1 x 5 [1D]
## # Key: .model [1]
## .model date close .mean `80%`
## <chr> <date> <dist> <dbl> <hilo>
## 1 SNAIVE 2022-03-25 N(2638, 22952) 2638. [2443.974, 2832.286]80
Những câu hỏi khác của Assignment làm khá tương tự. Chỉ khác nhau tương đối lớn ở khâu xử lí số liệu để convert về tsibble mà thôi.
(còn nữa…)
LS0tDQp0aXRsZTogIlIgR3VpZGUgZm9yIEJ1c2luZXNzIEZvcmVjYXN0aW5nIENvdXJzZSINCmF1dGhvcjogJ0F1dGhvcjogTmd1eWVuIENoaSBEdW5nJw0Kc3VidGl0bGU6ICJSIEJhc2ljIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBjYWNoZSA9IFRSVUUpDQoNCmBgYA0KDQohW10oQzpcVXNlcnNcXEFkbWluXFxEb2N1bWVudHNcXGV0Zi5wbmcpDQoNCiMgSW50cm9kdWN0aW9uDQoNCsSQw6J5IGzDoCB0w6BpIGxp4buHdSBoxrDhu5tuZyBk4bqrbiB0aOG7sWMgaMOgbmggUiBjaG8ga2jDs2EgaOG7jWMgQnVzaW5lc3MgRm9yZWNhc3RpbmcgdOG6oWkgTW9uYXNoIFVuaXZlcnNpdHkgZMOgbmggcmnDqm5nIGNobyBILkwuIEdp4bqjbmcgdmnDqm4gY+G7p2Ega2jDs2EgaOG7jWMgbsOgeSBsw6AgUHJvZiBHZW9yZ2UgQXRoYW5hc29wb3Vsb3MgLSDEkeG7k25nIHTDoWMgZ2nhuqMgY+G7p2EgW0ZvcmVjYXN0aW5nOiBQcmluY2lwbGVzIGFuZCBQcmFjdGljZV0oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9Gb3JlY2FzdGluZy1QcmluY2lwbGVzLVByYWN0aWNlLVJvYi1IeW5kbWFuL2RwLzA5ODc1MDcxMzMvcmVmPXNyXzFfMT9jcmlkPTNLVUswT0ZNNk5NUUEma2V5d29yZHM9R2VvcmdlK0F0aGFuYXNvcG91bG9zJnFpZD0xNjQ2NzIzOTYyJnNwcmVmaXg9Z2VvcmdlK2F0aGFuYXNvcG91bG9zJTJDYXBzJTJDMjg2JnNyPTgtMSkuIA0KDQoNCiMgdHNpYmJsZSBvYmplY3RzDQoNCktp4buDdSB04buVIGNo4bupYyBk4buvIGxp4buHdSBjaG8gdGltZSBzZXJpZXMgxJHGsOG7o2Mgc+G7rSBk4bulbmcgdHJvbmcgZ2nDoW8gdHLDrG5oICpGb3JlY2FzdGluZzogUHJpbmNpcGxlcyBhbmQgUHJhY3RpY2UqIHbDoCBj4bqjIGtow7NhIGjhu41jIEJ1c2luZXNzIEZvcmVjYXN0aW5nIGzDoCB0c2liYmxlLiBU4bqldCBj4bqjIGPDoWMgZOG7ryBsaeG7h3UgxJHDrW5oIGvDqG0gdGjGsCB2aeG7h24gW2ZwcDNdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9mcHAzL2luZGV4Lmh0bWwpIMSR4buBdSDhu58gZOG6oW5nIG7DoHkgdsOgIMSRxrDhu6NjIGfhu41pIGzDoCB0c2liYmxlIG9iamVjdC4gQ2jDum5nIHRhIGzhuqV5IHbDrSBk4bulIGLhu5kgZOG7ryBsaeG7h3UgKmdhZmFfc3RvY2sqOiANCg0KDQpgYGB7cn0NCiMgQ2xlYXIgUiBlbnZpcm9ubWVudDogDQpybShsaXN0ID0gbHMoKSkNCg0KIyBMb2FkIGZwcDMgcGFja2FnZTogDQpsaWJyYXJ5KGZwcDMpDQoNCiMgTG9hZCBnYWZhX3N0b2NrIGRhdGEgc2V0OiANCmRhdGEoImdhZmFfc3RvY2siKQ0KDQpgYGANCg0KU2F1IGtoaSBsb2FkIGLhu5kgZOG7ryBsaeG7h3UgbsOgeSBjaMO6bmcgdGEgY8OzIHRo4buDIHhlbSBxdWE6IA0KDQpgYGB7cn0NCiMgU2hvdyBzb21lIG9ic2VydmF0aW9uczogDQpoZWFkKGdhZmFfc3RvY2spDQpgYGANCg0KDQpO4bq/dSBk4buvIGxp4buHdSB0aW1lIHNlcmllcyDhu58gZOG6oW5nIHRzaWJibGUgdGjDrCBjaMO6bmcgdGEgY8OzIHRo4buDIHPhu60gZOG7pW5nIHThuqV0IGPhuqMgY8OhYyBow6BtIHbDoCBtw7QgaMOsbmggY+G7p2EgZnBwMy4gVsOtIGThu6UsIMSR4buDIHbhur0gxJHhu5MgdGjhu4sgY2jhu4kgY+G6p24gc+G7rSBk4bulbmcgbeG7mXQgbOG7h25oIMSRxqFuIGdp4bqjbiBuaMawIHNhdTogDQoNCmBgYHtyfQ0KYXV0b3Bsb3QoZ2FmYV9zdG9jaykNCmBgYA0KDQpOaMawIHbhuq15IGNo4buJIGLhurFuZyBs4buHbmggYGF1dG9wbG90KClgIGNow7puZyB0YSBjw7MgdGjhu4MgdOG6oW8gcmEgcGxvdCBjaG8gxJHhu5NuZyB0aOG7nWkgY+G6oyA0IG3DoyBj4buVIHBoaeG6v3UuIA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIMOhcCBk4bulbmcgY8OhYyBow6BtIGPhu6dhIHRoxrAgdmnhu4duICpkcGx5ciogY2hvIHRzaWJibGU6IA0KDQoNCmBgYHtyfQ0KIyBVc2UgbXV0YXRlKCkgZm9yIGNyZWF0aW5nIGEgbmV3IGNvbHVtbjogDQoNCmdhZmFfbmV3IDwtIGdhZmFfc3RvY2sgJT4lIG11dGF0ZShvcGVuX2Nsb3NlID0gT3BlbiAvIENsb3NlKQ0KDQpoZWFkKGdhZmFfbmV3KQ0KYGBgDQoNCk5oxrAgduG6rXkgY2jDum5nIHRhIMSRw6MgdOG6oW8gdGjDqm0gY+G7mXQgYmnhur9uIGPDsyB0w6puIG9wZW5fY2xvc2UgYuG6sW5nIMSRxrDhu6NjIHThuqFvIHJhIGLhurFuZyBjw6FjaCBs4bqleSBnacOhIG3hu58gY+G7rWEgY2hpYSBjaG8gZ2nDoSDEkcOzbmcgY+G7rWEuIA0KDQoqKkLDoGkgdOG6rXAgZMOgbmggY2hvIGLhuqFuOioqDQoNCjEuIFPhu60gZOG7pW5nIGjDoG0gYGZpbHRlcigpYCBs4bqleSByYSBjw6FjIHRow7RuZyB0aW4gY2hvIG3DoyBj4buVIHBoaeG6v3UgQUFQTCB04burIGdhZmFfc3RvY2suIA0KMi4gU+G7rSBk4bulbmcgaMOgbSBgc2VsZWN0KClgIGzhuqV5IHJhIGThu68gbGnhu4d1IGNo4buJIGfhu5NtIGhhaSBj4buZdCBEYXRlIHbDoCBPcGVuIHThu6sgZ2FmYV9zdG9jay4gDQoNCg0KxJDhu4Mgc+G7rSBk4bulbmcgY8OhYyBow6BtIGPhu6dhIGZwcDMgxJFp4buBdSB0csaw4bubYyB0acOqbiBj4bqnbiBsw6BtIGzDoCAqKmNvbnZlcnQgZGF0YSDEkcOzIHbhu4EgdHNpYmJsZSBvYmplY3QqKiBi4bqxbmcgaMOgbSBgYXNfdHNpYmJsZSgpYC4gTeG7mXQgdsOtIGThu6UgbMOgIGPDonUgaOG7j2kgdHJvbmcgYXNzaWdubWVudCBjw7MgbGnDqm4gcXVhbiDEkeG6v24gZOG7ryBsaeG7h3UgduG7gSBuaGnhu4d0IMSR4buZdCDhu58gTWVsYm91cm5lIEFpcnBvcnQuIFRyxrDhu5tjIGjhur90IGzhuqV5IGThu68gbGnhu4d1IHbhu4Egbmhp4buHdCDEkeG7mSwgdsOtIGThu6UsIGPhu6dhIHRow6FuZyA4IC0gMjAyMSBbdOG6oWkgxJHDonldKGh0dHA6Ly93d3cuYm9tLmdvdi5hdS9jbGltYXRlL2R3by8yMDIxMDgvdGV4dC9JRENKRFczMDQ5LjIwMjEwOC5jc3YpIHLhu5NpIGxvYWQgYuG7mSBk4buvIGxp4buHdSBuw6B5OiANCg0KDQpgYGB7cn0NCiMgTG9hZCBkYXRhOiANCnRlbV9haXJwb3J0IDwtIHJlYWQuY3N2KCJDOi9Vc2Vycy9BZG1pbi9Eb2N1bWVudHMvSURDSkRXMzA0OS4yMDIxMDguY3N2Iiwgc2tpcCA9IDQpDQpgYGANCg0KDQpDaOG7iSBnaeG7ryBs4bqhaSBj4buZdCB0aOG7qSAyIHbDoCA0IGPhu6dhIHRlbV9haXJwb3J0IGLhurFuZyBow6BtIGBzZWxlY3QoKWA6IA0KDQpgYGB7cn0NCiMgU2VsZWN0IHNvbWUgY29sdW1uczogDQptaW5pX2RhdGEgPC0gdGVtX2FpcnBvcnQgJT4lIHNlbGVjdChEYXRlLCBNYXhpbXVtLnRlbXBlcmF0dXJlLi4uQy4pDQoNCiMgU2hvdyBkYXRhOiANCmhlYWQobWluaV9kYXRhKQ0KDQojIERhdGEgdHlwZTogDQpzdHIobWluaV9kYXRhKQ0KYGBgDQoNCkPhu5l0IGJp4bq/biBEYXRlIGzDoCB0ZXh0LiBDaMO6bmcgdGEgY+G6p24gY29udmVydCBj4buZdCBiaeG6v24gbsOgeSB24buBIGRhdGUtdGltZSBuaMawIHNhdSBi4bqxbmcgaMOgbSBgeW1kKClgIGPhu6dhIHRoxrAgdmnhu4duIGx1YnJpZGF0ZTogDQoNCmBgYHtyfQ0KbGlicmFyeShsdWJyaWRhdGUpDQoNCm1pbmlfZGF0YV95bWQgPC0gbWluaV9kYXRhICU+JSBtdXRhdGUoRGF0ZSA9IHltZChEYXRlKSkNCmBgYA0KDQrEkOG6v24gbMO6YyBuw6B5IGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgaMOgbSBgYXNfdHNpYmJsZSgpYDogDQoNCmBgYHtyfQ0KZGF0YV90c2liYmxlIDwtIGFzX3RzaWJibGUobWluaV9kYXRhX3ltZCwgaW5kZXggPSBEYXRlKQ0KYGBgDQoNCiMgQXNzaWdubWVudCBDb252ZW50aW9uDQoNCsSQ4bq/biDEkcOieSBj4bqnbiBuw7NpIHLDtSBjaG8gYuG6oW4gcsO1IHbhu4EgcXV5IMaw4bubYyBz4butIGThu6VuZyBrw60gaGnhu4d1IGfDoW4gKEFzc2lnbm1lbnQpLiBLw60gaGnhu4d1IGfDoW4gdGjGsOG7nW5nIHRo4bqleSBuaOG6pXQgbMOgICoqPC0qKi4gQ2jhurNuZyBo4bqhbiBuaMawIMSRb+G6oW4gUiBjb2RlcyBzYXU6IA0KDQpgYGB7cn0NCmRhdGFfdHNpYmJsZSA8LSBhc190c2liYmxlKG1pbmlfZGF0YV95bWQsIGluZGV4ID0gRGF0ZSkNCmBgYA0KDQpUxrDGoW5nIHThu7EgdGEgY8OzIHRo4buDIGfDoW4gZ2nDoSB0cuG7iyAxMCBjaG8gY8OhaSBn4buNaSBsw6Agb2JqZWN0IGPDsyB0w6puIGzDoCB4IG5oxrAgc2F1OiANCg0KYGBge3J9DQp4IDwtIDEwDQpgYGANCg0KUiBjaG8gcGjDqXAgc+G7rSBk4bulbmcgbeG7mXQga8OtIGhp4buHdSBnw6FuIHRoZW8gc3R5bGUga2jDoWMgdHLDtG5nIGPhu6kgbmjGsCBsw6AgIm5nxrDhu6NjIGzhuqFpIi4gQ2jhurNuZyBo4bqhbiDEkW/huqFuIFIgY29kZXMg4bufIHRyw6puIGPDsyB0aOG7gyDEkcaw4bujYyAidHLDrG5oIGLDoHkiIHRoZW8gY8OhY2gga2jDoWMgbmjGsCBzYXU6IA0KDQpgYGB7cn0NCmFzX3RzaWJibGUobWluaV9kYXRhX3ltZCwgaW5kZXggPSBEYXRlKSAtPiBkYXRhX3RzaWJibGUNCmBgYA0KDQpUxrDGoW5nIHThu7EgbmjGsCB24bqteSBsw6AgZ8OhbiAxMCBjaG8geDogDQoNCmBgYHtyfQ0KMTAgLT4geA0KYGBgDQoNCkhhaSBjw6FjaCB0aOG7qWMgdmnhur90L3Ryw6xuaCBiw6B5IFIgY29kZXMgbmjGsCB0csOqbiBjw7MgZ2nDoSB0cuG7iyBuaMawIG5oYXUuIELhuqFuIGPhuqduIG7huq9tIHLDtSBjw6FpIG7DoHkgxJHhu4MgxJHhu41jIFIgY29kZXMgY+G7p2EgbmfGsOG7nWkga2jDoWMgdsOgIHZp4bq/dCBjb2RlcyBjaG8gbcOsbmguIA0KDQoNCiMgUGlwZSBPcGVyYXRvcg0KDQoNClIgc+G7rSBk4bulbmcgbeG7mXQgc+G7kSB0b8OhbiB04butIG3DoCBxdWFuIHRy4buNbmcgbmjhuqV0IHRyb25nIHPhu5EgxJHDsyBsw6AgdG/DoW4gdOG7rSBwaXBlIChbcGlwZSBvcGVyYXRvcl0oaHR0cHM6Ly91Yy1yLmdpdGh1Yi5pby9waXBlKSksIGPDsyBrw60gaGnhu4d1IGzDoCAqKiU+JSoqLiBC4bqhbiDEkcOjIHRo4bqleSB0b8OhbiB04butIG7DoHkg4bufIHRyw6puLiANCg0KDQrEkOG7gyBsw6BtIHLDtSB2YWkgdHLDsiBj4bunYSB0b8OhbiB04butIG7DoHkgY2jDum5nIHRhIHjDqXQgYsOgaSB0b8OhbiBzYXUuIFjDqXQgaMOgbSBo4bujcCB5ID0gdGFuKHNpbih4KSkuIMSQ4buDIHTDrW5oIGdpw6EgdHLhu4sgY+G7p2EgeSBraGkgeCA9IDEgduG7m2kgbmfDtG4gbmfhu68gUiB0aMOsIGNow7puZyB0YSBjw7MgdGjhu4MgbMOgbSB04burbmcgYsaw4bubYyBzYXU6IA0KDQpgYGB7cn0NCiMgU3RhZ2UgMTogDQp4IDwtIDENCg0KIyBTdGFnZSAyOiANCnRydW5nX2dpYW4gPC0gc2luKHgpDQoNCiMgU3RhZ2UgMzogDQp5IDwtIHRhbih0cnVuZ19naWFuKQ0KDQpgYGANCg0KxJDhur9uIMSRw6J5IGNow7puZyB0YSBjw7MgZ2nDoSB0cuG7iyBjdeG7kWkgY8O5bmcgY+G7p2EgeSBsw6AgMS4xMTg5NDogDQoNCmBgYHtyfQ0KeQ0KYGBgDQoNCkPDoWNoIGzDoG0gdHLDqm4gY2jDum5nIHRhIHBo4bqjaSB04bqhbyByYSBvYmplY3QgdHJ1bmcgZ2lhbiBjw7MgdMOqbiB0cnVuZ19naWFuLiBW4bubaSB0b8OhbiB04butIFBpcGUgY2jDum5nIHRhIGtow7RuZyBj4bqnbiB04bqhbyByYSB0cnVuZyBnaWFuIG7DoHkgbcOgIHbhuqtuIGPDsyBr4bq/dCBxdeG6oyBjdeG7kWkgY8O5bmcgMS4xMTg5NCBuaMawIHNhdTogDQoNCg0KYGBge3J9DQp4ICU+JSANCiAgc2luKCkgJT4lIA0KICB0YW4oKQ0KYGBgDQoNCkNow7ogw70gcuG6sW5nIHbhu5tpIHRvw6FuIHThu60gUGlwZSBjaMO6bmcgdGEgY8OzIHRo4buDIHZp4bq/dCBjb2RlcyB0aGVvIG3hu5l0IGPDoWNoIHRo4bupYyBraMOhYyBuaMawIHNhdTogDQoNCmBgYHtyfQ0KeCAlPiUgc2luKCkgLT4gdHJ1bmdfZ2lhbg0KDQp0cnVuZ19naWFuICU+JSB0YW4oKSAtPiB5DQpgYGANCg0KSG/hurdjIG5oxrAgc2F1IGPFqW5nIMSRxrDhu6NjOiANCg0KYGBge3J9DQp0cnVuZ19naWFuIDwtIHggJT4lIHNpbigpDQoNCnkgPC0gdHJ1bmdfZ2lhbiAlPiUgdGFuKCkNCmBgYA0KDQojIFRoZSBmb3JlY2FzdGVy4oCZcyB0b29sYm94DQoNCk3hu6VjIG7DoHkgdMawxqFuZyDhu6luZyB24bubaSBDaGFwdGVyIDUgY+G7p2EgRm9yZWNhc3Rpbmc6IFByaW5jaXBsZXMgYW5kIFByYWN0aWNlLCAzRS4gxJDhu4MgbWluaCBo4buNYSwgaMOjeSB0cuG7nyBs4bqhaSB24bubaSBxdWVzaW9uIDEgY+G7p2EgQXNzaWdubWVudCAxIGzDoCBk4buxIGLDoW8gZ2nDoSDEkcOzbmcgY+G7rWEgY+G7p2EgR29vZ2xlLiBOaMawIGPDsyB0aOG7gyB0aOG6pXksIHThuqFpIHRo4budaSDEkWnhu4NtIG5o4bqtbiDEkcaw4bujYyBBc3NpZ25tZW50IDEgbMOgIDExLTAzLTIwMjIgbmjGsG5nIGzhuqFpIHnDqnUgY+G6p3UgZOG7sSBiw6FvIGzDoCBuZ8OgeSAyNS0wMy0yMDIyLiBU4bupYyBsw6AgKmThu7EgYsOhbyB0csaw4bubYyAxNSBuZ8OgeSouIA0KDQrEkOG7gyBnaeG6o2kgcXV54bq/dCBxdWVzaW9uIDEgdHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhLCB2w60gZOG7pSwgZG93bmxvYWQgZOG7ryBsaeG7h3UgY+G7p2EgR29vZ2xlIHThuqFpIHRo4budaSDEkWnhu4NtIDExLTAzLTIwMjIgdsOgIGjhuqV0IG5nxrDhu6NjIHbhu4EgcGjDrWEgdHLGsOG7m2MgMSBuxINtIGNo4bqzbmcgaOG6oW4uIENow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgaMOgbSBgdHFfZ2V0KClgIGPhu6dhIHRoxrAgdmnhu4duIFt0aWR5cXVhbnRdKGh0dHBzOi8vYnVzaW5lc3Mtc2NpZW5jZS5naXRodWIuaW8vdGlkeXF1YW50LykgxJHhu4MgbOG6pXkgZOG7ryBsaeG7h3UgY+G6p24gdGhp4bq/dCBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBMb2FkIHRpZHlxdWFudCBwYWNrYWdlOiANCmxpYnJhcnkodGlkeXF1YW50KQ0KDQojIExvYWQgZGF0YSBmb3IgR29vZ2xlOiANCnRxX2dldCh4ID0gIkdPT0dMIiwgZnJvbSA9ICIyMDIxLTAzLTExIiwgdG8gPSAiMjAyMi0wMy0xMSIpIC0+IGdvb2dsZV9wcmljZQ0KDQojIFNob3cgc29tZSBvYnNlcnZhdGlvbnM6IA0KDQpnb29nbGVfcHJpY2UgJT4lIGhlYWQoKQ0KYGBgDQoNCkThu68gbGnhu4d1IEdvb2dsZSDhu58gdHLDqm4gxJFhbmcg4bufIGThuqFuZyB0aWJibGUgKGPDsyB0aOG7gyBn4buNaSB0w6puIGtow6FjIGzDoCBkYXRhIGZyYW1lKS4gS2nhu4N1IHThu5UgY2jhu6ljIGThu68gbGnhu4d1IG7DoHkga2jDtG5nIHBo4bqjaSBsw6AgdHNpYmJsZSBkbyB24bqteSB0YSBraMO0bmcgdGjhu4Mgw6FwIGThu6VuZyB0cuG7sWMgdGnhur9wIGPDoWMgaMOgbSBj4bunYSBmcHAzLCBuaMawIGBhdXRvcGxvdCgpYCwgY2hvIG9iamVjdCBuw6B5IMSRxrDhu6NjLiDEkOG7gyBz4butIGThu6VuZyDEkcaw4bujYyBjw6FjIGjDoG0gY+G7p2EgZnBwMywgY2jDum5nIHRhIHRyxrDhu5tjIGjhur90IHBo4bqjaSBjb252ZXJ0IGdvb2dsZV9wcmljZSB24buBIGThuqFuZyB0c2liYmxlIG5oxrAgc2F1OiANCg0KYGBge3J9DQojIENvbnZlcnQgdG8gdHNpYmJsZTogDQphc190c2liYmxlKGdvb2dsZV9wcmljZSwgaW5kZXggPSBkYXRlKSAtPiBnb29nbGVfcHJpY2VfdHMNCmBgYA0KDQrEkOG6v24gxJHDonkgY8OzIHRo4buDIHPhu60gZOG7pW5nIGLhuqV0IGvDrCBow6BtIG7DoG8gY+G7p2EgZnBwMywgdsOtIGThu6UsIGBhdXRvcGxvdCgpYCDEkeG7gyBow6xuaCDhuqNuaCBow7NhIGThu68gbGnhu4d1OiANCg0KYGBge3J9DQojIFBsb3QgZm9yIGNsb3NlIHByaWNlOiANCmdvb2dsZV9wcmljZV90cyAlPiUNCiAgYXV0b3Bsb3QoY2xvc2UpICsNCiAgbGFicyh5ID0gIkNsb3NpbmcgUHJpY2UgKGluIFVTRCkiLCANCiAgICAgICB4ID0gIiIsIA0KICAgICAgIHRpdGxlID0gIkZpZ3VyZSAxOiBDbG9zaW5nIHByaWNlIGZvciBHb29nbGUiKQ0KYGBgDQoNCkNow7ogw70gcuG6sW5nIMSR4buDIHPhu60gZOG7pW5nIMSRxrDhu6NjIGPDoWMgbWV0aG9kLCBjw6FjIGjDoG0gY+G7p2EgZnBwMyB0aMOsIHRpbWUgc2VyaWVzIG7hur91IHRoZW8gbmfDoHksIGzDoCBwaOG6o2kgbGnDqm4gdGnhur9wIG5oYXUuIFR1eSBuaGnDqm4gecOqdSBj4bqndSBuw6B5IGtow7RuZyDEkcaw4bujYyB0aOG7j2EgbcOjbi4gVsOtIGThu6U6IA0KDQpgYGB7cn0NCmdvb2dsZV9wcmljZV90cyAlPiUgaGVhZCgpDQpgYGANCg0KQ8OzIHRo4buDIHRo4bqleSBuZ8OgeSAyMDIxLTAzLTEyIHLhu5NpICJuaOG6o3kgY8OzYyIgbMOqbiBuZ8OgeSAyMDIxLTAzLTE1IGx1w7RuLiDEkOG7gyB44butIGzDrSB24bqlbiDEkeG7gSBuw6B5IGNow7puZyB0YSBz4butIGThu6VuZyBow6BtIGBmaWxsX2dhcHMoKWAgY+G7p2EgaOG7hyBzaW5oIHRow6FpIGZwcDM6IA0KDQpgYGB7cn0NCmdvb2dsZV9wcmljZV90cyAlPiUgZmlsbF9nYXBzKCkgLT4gZ29vZ2xlX3ByaWNlX3RzX2ZpbGxlZCANCmBgYA0KDQpIw6N5IHNvIHPDoW5oIGLhurFuZyBjw6FjaCBxdWFuIHPDoXQga8SpIGvhur90IHF14bqjIGTGsOG7m2kgxJHDonk6IA0KDQpgYGB7cn0NCmdvb2dsZV9wcmljZV90c19maWxsZWQgJT4lIGhlYWQoKQ0KYGBgDQoNCkPDsyB0aOG7gyB0aOG6pXkgMjAyMS0wMy0xMyB2w6AgMjAyMS0wMy0xNCBsw6AgbWlzc2luZyBkYXRhIC0gdOG7qWMgbMOgIGtow7RuZyBjw7MgcXVhbiBzw6F0IGNobyBoYWkgbmfDoHkgbsOgeS4gxJDhur9uIMSRw6J5IGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgdHNpYmJsZSBuw6B5IGNobyBk4buxIGLDoW8uIA0KDQrEkOG7gyBk4buxIGLDoW8sIGNow7puZyB0YSBjw7MgdGjhu4MgduG6rW4gZOG7pW5nIHbDrSBk4bulICpBdXN0cmFsaWFuIHF1YXJ0ZXJseSBiZWVyIHByb2R1Y3Rpb24qIMSRxrDhu6NjIHRyw6xuaCBiw6B5IHRyb25nIHRleHRib29rLiBD4bulIHRo4buDLCBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgbWV0aG9kIGPDsyB0w6puIFNuYWl2ZSDEkeG7gyBk4buxIGLDoW8uIFRyxrDhu5tjIGjhur90IGZpdCBtZXRob2QgbsOgeSB0csOqbiBk4buvIGxp4buHdSDhu58gZOG6oW5nIHRzaWJibGUg4bufIHRyw6puOiANCg0KYGBge3J9DQojIEZpdCB0aGUgbWV0aG9kOg0KY2xvc2VfZml0IDwtIGdvb2dsZV9wcmljZV90c19maWxsZWQgJT4lDQogIG1vZGVsKFNOQUlWRSA9IFNOQUlWRShjbG9zZSkpDQpgYGANCg0KTeG7mXQgY8OhY2ggdHLDrG5oIGLDoHkga2jDoWMgbcOgIOG7nyDEkcOzIGtow7RuZyBtdeG7kW4gdOG6oW8gcmEgb2JqZWN0IHRydW5nIGdpYW4gZ29vZ2xlX3ByaWNlX3RzX2ZpbGxlZCB0aMOsIFIgY29kZXMgc+G6vSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBGaXQgdGhlIG1ldGhvZDogDQpjbG9zZV9maXQgPC0gZ29vZ2xlX3ByaWNlX3RzICU+JQ0KICBmaWxsX2dhcHMoKSAlPiUgDQogIG1vZGVsKFNOQUlWRSA9IFNOQUlWRShjbG9zZSkpDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBtZXRob2QgbsOgeSDEkeG7gyBk4buxIGLDoW8sIHbDrSBk4bulLCBjaG8gMzAgbmfDoHkgc+G6r3AgdOG7m2kgduG7m2kga2hv4bqjbmcgdGluIGPhuq15IDgwJTogDQoNCmBgYHtyfQ0KY2xvc2VfZml0ICU+JSANCiAgZm9yZWNhc3QoaCA9IDMwKSAlPiUgDQogIGhpbG8oODApIC0+IGRmX2ZvcmVjYXN0cw0KYGBgDQoNCk7hur91IDMwIG3DoCBjaMawYSDEkeG7pyBjb3ZlciAyMDIyLTAzLTI1IHRow6wgYuG6oW4gY8OzIHRo4buDIHRoYXkgYuG6sW5nLCB2w60gZOG7pSwgMTAwMS4gVGjhu7FjIHThur8gdGjDrCBoID0gMzAg4bufIMSRb+G6oW4gUiBjb2RlcyB0csOqbiBsw6AgxJHDoyBs4bubbiBoxqFuICp0csaw4bubYyAxNSBuZ8OgeSogcuG7k2kuIEzDumMgbsOgeSBjaMO6bmcgdGEgY8OzIHRo4buDIGNo4buJIHJhIGvhur90IHF14bqjIGThu7EgYsOhbyB04bqhaSAyMDIyLTAzLTI1IG5oxrAgc2F1OiANCg0KDQpgYGB7cn0NCmRmX2ZvcmVjYXN0cyAlPiUgDQogIGZpbHRlcihkYXRlID09IHltZCgiMjAyMi0wMy0yNSIpKQ0KYGBgDQoNCk5o4buvbmcgY8OidSBo4buPaSBraMOhYyBj4bunYSBBc3NpZ25tZW50IGzDoG0ga2jDoSB0xrDGoW5nIHThu7EuIENo4buJIGtow6FjIG5oYXUgdMawxqFuZyDEkeG7kWkgbOG7m24g4bufIGtow6J1IHjhu60gbMOtIHPhu5EgbGnhu4d1IMSR4buDIGNvbnZlcnQgduG7gSB0c2liYmxlIG3DoCB0aMO0aS4gDQoNCihjw7JuIG7hu69hLi4uKQ0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQo=