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:

autoplot(gafa_stock)

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:

  1. Sử dụng hàm filter() lấy ra các thông tin cho mã cổ phiếu AAPL từ gafa_stock.
  2. 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:

x <- 10

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:

10 -> 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:

y
## [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:

x %>% 
  sin() %>% 
  tan()
## [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=