Motivations and Objectives

Dự án này được thực hiện nhằm xây dựng và phát triển một mô hình dự báo hành vi mua sắm của khách hàng dựa trên dữ liệu lịch sử về giao dịch. Bộ dữ liệu thô được cung cấp bao gồm các thông tin sau:

  • customer serial number (csn) là mã số của khách hàng.
  • transaction_info là thông tin cụ thể về giỏ hàng hóa đã mua của khách hàng trong đó: (1) article là mã hàng hóa, (2) salesquantity là số lượng mua, và (3) price là giá hàng.

Data Processing

Trước hết đọc dữ liệu thô và xem qua:

csn date transaction_info
Y2NgaWJoYw== 2018-03-02 [{‘article’: ‘10020163’, ‘salesquantity’: 2.0, ‘price’: 18250.0}]
Y2NgamRpZA== 2018-02-08 [{‘article’: ‘10225636’, ‘salesquantity’: 1.1420000000000001, ‘price’: 45900.18}, {‘article’: ‘10014829’, ‘salesquantity’: 1.0, ‘price’: 12000.0}, {‘article’: ‘10020649’, ‘salesquantity’: 1.0, ‘price’: 8200.0}, {‘article’: ‘10008535’, ‘salesquantity’: 1.0, ‘price’: 65400.0}, {‘article’: ‘10008553’, ‘salesquantity’: 1.0, ‘price’: 4000.0}, {‘article’: ‘10008611’, ‘salesquantity’: 2.0, ‘price’: 7000.0}, {‘article’: ‘10008895’, ‘salesquantity’: 1.0, ‘price’: 27700.0}, {‘article’: ‘10009067’, ‘salesquantity’: 1.0, ‘price’: 38000.0}, {‘article’: ‘10009156’, ‘salesquantity’: 1.0, ‘price’: 14500.0}, {‘article’: ‘10010004’, ‘salesquantity’: 1.0, ‘price’: 16000.0}, {‘article’: ‘10010264’, ‘salesquantity’: 3.0, ‘price’: 12333.33}, {‘article’: ‘10010266’, ‘salesquantity’: 1.0, ‘price’: 46400.0}, {‘article’: ‘10020822’, ‘salesquantity’: 2.0, ‘price’: 14900.0}, {‘article’: ‘10234347’, ‘salesquantity’: 1.0, ‘price’: 8500.0}, {‘article’: ‘10322779’, ‘salesquantity’: 1.0, ‘price’: 6700.0}, {‘article’: ‘10322780’, ‘salesquantity’: 1.0, ‘price’: 6700.0}]

Như vậy khách hàng có mã Y2NgaWJoYw== vào ngày 2018-03-02 chỉ mua một Item có mã là 10020163, số lượng mua là 2 và giá bán (cho một sản phẩm) là 18250. Với khách hàng có mã là Y2NgamRpZA== vào ngày 2018-02-08 thì người này mua nhiều hơn một sản phẩm và một một sản phẩm trong giỏ hàng được ngăn cách nhau bởi }, {. Đáng chú ý là với hàng hóa có mã 10225636 thì lượng mua là 1.1420000000000001.

Lượng mua là một số nguyên dương. Các chuỗi cửa hàng, nếu có bán nửa cân mực khô chẳng hạn, thì nó sẽ đóng thành gói và hệ thống lưu thông tin sẽ ghi nhận là “bán ra 1 gói mực”. Do vậy có thể suy luận rằng 1.1420000000000001 là một lỗi của hệ thống cơ sở dữ liệu.

Dựa trên những phân tích dữ liệu thô ở trên chúng ta viết hàm để extract ra các thông tin về một giao dịch như sau:

Hàm trên trả ra kết quả là một Data Frame. Ví dụ với giao dịch thứ 1 và thứ 8:

CustomerID InvoiceDate StockCode Quantity UnitPrice
Y2NgaWJoYw== 2018-03-02 10020163 2 18250
CustomerID InvoiceDate StockCode Quantity UnitPrice
Y2NgamRpZA== 2018-02-08 10225636 NA 459001.8
Y2NgamRpZA== 2018-02-08 10014829 1 12000.0
Y2NgamRpZA== 2018-02-08 10020649 1 8200.0
Y2NgamRpZA== 2018-02-08 10008535 1 65400.0
Y2NgamRpZA== 2018-02-08 10008553 1 4000.0
Y2NgamRpZA== 2018-02-08 10008611 2 7000.0
Y2NgamRpZA== 2018-02-08 10008895 1 27700.0
Y2NgamRpZA== 2018-02-08 10009067 1 38000.0
Y2NgamRpZA== 2018-02-08 10009156 1 14500.0
Y2NgamRpZA== 2018-02-08 10010004 1 16000.0
Y2NgamRpZA== 2018-02-08 10010264 3 123333.3
Y2NgamRpZA== 2018-02-08 10010266 1 46400.0
Y2NgamRpZA== 2018-02-08 10020822 2 14900.0
Y2NgamRpZA== 2018-02-08 10234347 1 8500.0
Y2NgamRpZA== 2018-02-08 10322779 1 6700.0
Y2NgamRpZA== 2018-02-08 10322780 1 6700.0

Giao dịch thứ 8 có Missing Data về lượng hàng mua cho mã 10225636 mà nguyên nhân bắt nguồn từ việc dữ liệu gốc ghi nhận số lượng của mã hàng này là 1.1420000000000001. Sử dụng hàm đã có để xử lí dữ liệu:

Với các thông tin extract được chúng ta có thể xem qua:

CustomerID InvoiceDate StockCode Quantity UnitPrice
Y2NgaWJoYw== 2018-03-02 10020163 2 18250
Y2NgaWJoYw== 2018-03-04 10026562 3 13000
Y2NgaWJoYw== 2018-03-04 10320883 2 43000
Y2NgaWlpYA== 2018-02-27 10013531 1 17800
Y2NgaWlpYA== 2018-02-27 10015613 1 5600
Y2NgaWlpYA== 2018-02-27 10320578 1 5600

Đánh giá mức độ thiếu của dữ liệu:

CustomerID InvoiceDate StockCode Quantity UnitPrice
0 0 0 8.975835 0

Có gần 9% dữ liệu là Missing ở cột biến Quantity. Như đã phân tích ở trên chúng ta có thể giả định rằng nguyên nhân đến từ việc nghi nhận không phù hợp của hệ thống cơ sở dữ liệu. Việc xử lí các dữ liệu trống này bằng các phương pháp thông thường (như thay thế bằng mean hay median) là không phù hợp. Nếu được cung cấp chi tiết hơn những thông tin về dữ liệu thô, về cách thức hệ thống ghi nhận cơ sở dữ liệu thì chúng ta có thể có cách xử lí Missing Data này phù hợp hơn.

Chúng ta cũng giả định rằng các Missing thuộc kiểu MAR (Missing at Random) và do vậy việc loại các Missing không ảnh hưởng đến việc xây dựng mô hình vì việc này là tương đương với việc mẫu quan sát được lấy ít hơn 9% so với ban đầu:

CustomerID time_ymd StockCode Quantity UnitPrice w_day time_mon
Y2NgaWJoYw== 2018-03-02 10020163 2 18250 Fri Mar
Y2NgaWJoYw== 2018-03-04 10026562 3 13000 Sun Mar
Y2NgaWJoYw== 2018-03-04 10320883 2 43000 Sun Mar
Y2NgaWlpYA== 2018-02-27 10013531 1 17800 Tue Feb
Y2NgaWlpYA== 2018-02-27 10015613 1 5600 Tue Feb
Y2NgaWlpYA== 2018-02-27 10320578 1 5600 Tue Feb

Dữ liệu được làm sạch ở trên giờ có thể được sử dụng cho phân tích và xây dựng mô hình dự báo hành vi mua sắm của khách hàng.

Exploratory Data Analysis

Bước phân tích khám phá dữ liệu này được thực hiện nhằm tìm kiếm một số insights có thể là quan trọng cho việc thiết kế - xây dựng mô hình dự báo hành vi của khách hàng sau này. Ví dụ chúng ta có thể thấy có một thời điểm đâu đó giữa Feb và Mar sản lượng bán ra tăng đột ngột. Đây có thể là kết quả của việc Rainbow Store thực hiện các chiến dịch khuyến mãi kích cầu mua sắm:

Tương tự là doanh thu của Rainbow Store:

Sản lượng bán ra của Rainbow Store cao nhất ở Jun:

Tuy nhiên doanh thu thì có xu hướng giảm nhẹ qua các tháng ngoại trừ tháng May có doanh thu tăng lên:

Chúng ta cũng có thể thấy rằng doanh thu đến từ các Item có phân bố không đều: có một số mặt hàng mang lại doanh chu rất cao cho Rainbow Store:

Chúng ta có thể list ra danh sách các mặt hàng mà mang lại 80% doanh thu cho Rainbow Store:

## [1] 0.00843729
StockCode sales money_percent cum_money ID
10053361 456429446194 0.0652207 0.0652207 1
10069664 205408427811 0.0293515 0.0945721 2
10053663 205385103453 0.0293481 0.1239203 3
10053664 171035994195 0.0244399 0.1483602 4
10054850 169014480527 0.0241510 0.1725112 5
10054908 160322716792 0.0229090 0.1954202 6
10053716 127189715878 0.0181745 0.2135947 7
10054786 121005187343 0.0172908 0.2308856 8
10071533 115900281750 0.0165614 0.2474469 9
10053662 113814602744 0.0162633 0.2637103 10
10053676 103754862514 0.0148259 0.2785361 11
10053675 91333175078 0.0130509 0.2915870 12
10056225 86556505303 0.0123683 0.3039553 13
10055071 75922618487 0.0108488 0.3148042 14
10225636 75731762010 0.0108216 0.3256257 15
10086797 75372024326 0.0107701 0.3363959 16
10060637 70981630306 0.0101428 0.3465387 17
10053322 60778901472 0.0086849 0.3552236 18
10053749 58415519916 0.0083472 0.3635707 19
10053725 54927315183 0.0078487 0.3714195 20
10214403 53473614068 0.0076410 0.3790605 21
10054606 53233512314 0.0076067 0.3866672 22
10054870 53034097530 0.0075782 0.3942454 23
10053722 51666524058 0.0073828 0.4016282 24
10315134 49791909717 0.0071149 0.4087431 25
10053679 48567479158 0.0069400 0.4156831 26
10053660 48167635063 0.0068828 0.4225659 27
10053750 45031083030 0.0064346 0.4290006 28
10054905 44812042983 0.0064033 0.4354039 29
10053818 43440477674 0.0062073 0.4416113 30
10053440 42888239004 0.0061284 0.4477397 31
10053791 42561702119 0.0060818 0.4538215 32
10054858 40212396987 0.0057461 0.4595675 33
10236531 39472051754 0.0056403 0.4652078 34
10053687 38454572733 0.0054949 0.4707027 35
10054968 38366512033 0.0054823 0.4761850 36
10309060 37245336644 0.0053221 0.4815071 37
10053654 35899158023 0.0051297 0.4866369 38
10071527 35310428757 0.0050456 0.4916825 39
10236565 35238100724 0.0050353 0.4967178 40
10053669 34929598436 0.0049912 0.5017090 41
10053774 33872599335 0.0048402 0.5065492 42
10053645 33818836965 0.0048325 0.5113816 43
10053673 33762682646 0.0048245 0.5162061 44
10054928 33504466921 0.0047876 0.5209937 45
10053666 33094988950 0.0047290 0.5257227 46
10053674 33058858137 0.0047239 0.5304466 47
10053727 32799955580 0.0046869 0.5351335 48
10054848 32602625324 0.0046587 0.5397922 49
10231115 32329051607 0.0046196 0.5444118 50
10054624 31325548780 0.0044762 0.5488880 51
10053672 29554609249 0.0042232 0.5531111 52
10053362 29389927798 0.0041996 0.5573108 53
10071650 29387541738 0.0041993 0.5615100 54
10071517 27934205907 0.0039916 0.5655017 55
10071744 27735310899 0.0039632 0.5694648 56
10071540 27638460863 0.0039493 0.5734142 57
10053661 27584568037 0.0039416 0.5773558 58
10054789 25801155583 0.0036868 0.5810427 59
10054693 25763123172 0.0036814 0.5847240 60
10054881 25539209303 0.0036494 0.5883734 61
10054819 24962582033 0.0035670 0.5919404 62
10053352 24910574121 0.0035596 0.5954999 63
10307816 24465327451 0.0034959 0.5989959 64
10054625 23908682903 0.0034164 0.6024123 65
10055065 23381002657 0.0033410 0.6057532 66
10054793 23056925646 0.0032947 0.6090479 67
10055064 22792608200 0.0032569 0.6123048 68
10054871 22726467185 0.0032475 0.6155523 69
10308124 22013371013 0.0031456 0.6186978 70
10316269 21843804960 0.0031213 0.6218192 71
10054842 21741004839 0.0031066 0.6249258 72
10053740 21411330932 0.0030595 0.6279854 73
10053695 21209627842 0.0030307 0.6310161 74
10054708 21156634542 0.0030231 0.6340392 75
10060672 20914890578 0.0029886 0.6370278 76
10054892 20608601274 0.0029448 0.6399726 77
10237550 20512493474 0.0029311 0.6429037 78
10071516 20454613484 0.0029228 0.6458266 79
10053360 20358839329 0.0029091 0.6487357 80
10053769 20093110419 0.0028712 0.6516069 81
10054816 19908528619 0.0028448 0.6544517 82
10054697 19489637197 0.0027849 0.6572366 83
10054854 19413304450 0.0027740 0.6600106 84
10053820 19383359398 0.0027698 0.6627804 85
10316132 18586743031 0.0026559 0.6654363 86
10053779 18536200367 0.0026487 0.6680850 87
10054806 18262788249 0.0026096 0.6706946 88
10053686 18059175147 0.0025805 0.6732751 89
10054783 17473368085 0.0024968 0.6757720 90
10229621 17373627212 0.0024826 0.6782545 91
10071487 17308563037 0.0024733 0.6807278 92
10053377 16794599550 0.0023998 0.6831277 93
10056226 16773813161 0.0023969 0.6855245 94
10405705 16585495873 0.0023700 0.6878945 95
10404849 16450680138 0.0023507 0.6902452 96
10237432 15976029914 0.0022829 0.6925280 97
10053808 15640213722 0.0022349 0.6947629 98
10053325 15577383047 0.0022259 0.6969888 99
10053324 15317538489 0.0021888 0.6991776 100
10404850 15288687737 0.0021846 0.7013622 101
10071541 15277586199 0.0021831 0.7035453 102
10307829 15041879313 0.0021494 0.7056947 103
10085558 14796156915 0.0021143 0.7078090 104
10053308 14764309645 0.0021097 0.7099187 105
10054846 14692924528 0.0020995 0.7120182 106
10404878 14634134375 0.0020911 0.7141093 107
10054912 14619633243 0.0020890 0.7161984 108
10053942 14334870386 0.0020484 0.7182467 109
10053368 14299338336 0.0020433 0.7202900 110
10403736 14057404846 0.0020087 0.7222987 111
10237429 13576721190 0.0019400 0.7242387 112
10054700 13511655588 0.0019307 0.7261694 113
10054788 12865270351 0.0018384 0.7280078 114
10308125 12565635314 0.0017955 0.7298033 115
10053736 12521273631 0.0017892 0.7315926 116
10054830 12403602373 0.0017724 0.7333649 117
10083059 12296104218 0.0017570 0.7351220 118
10053954 12290995037 0.0017563 0.7368783 119
10053704 12026371030 0.0017185 0.7385968 120
10053374 11962966462 0.0017094 0.7403062 121
10054992 11946260885 0.0017070 0.7420132 122
10053670 11735120035 0.0016769 0.7436901 123
10308204 11643806709 0.0016638 0.7453539 124
10053307 11616420019 0.0016599 0.7470138 125
10053718 11559786791 0.0016518 0.7486656 126
10054729 11477426572 0.0016400 0.7503057 127
10054629 11415859242 0.0016312 0.7519369 128
10053642 11155605979 0.0015941 0.7535310 129
10054773 11124836657 0.0015897 0.7551207 130
10060630 11094463720 0.0015853 0.7567060 131
10316105 10945774400 0.0015641 0.7582701 132
10054807 10636599682 0.0015199 0.7597900 133
10054669 10572550361 0.0015107 0.7613007 134
10053461 10413181860 0.0014880 0.7627887 135
10053409 10331265495 0.0014763 0.7642649 136
10054695 10311556876 0.0014735 0.7657384 137
10053549 10307089575 0.0014728 0.7672112 138
10053351 10201644874 0.0014577 0.7686689 139
10053667 9932125807 0.0014192 0.7700882 140
10054667 9799785996 0.0014003 0.7714885 141
10053724 9743546846 0.0013923 0.7728808 142
10308269 9740962357 0.0013919 0.7742727 143
10231118 9688134566 0.0013844 0.7756571 144
10053950 9683930933 0.0013838 0.7770408 145
10308228 9570067804 0.0013675 0.7784083 146
10053685 9331376827 0.0013334 0.7797417 147
10053476 9288517330 0.0013273 0.7810690 148
10402904 9272117776 0.0013249 0.7823939 149
10054977 9201937245 0.0013149 0.7837088 150
10308235 9169815293 0.0013103 0.7850191 151
10054698 9117734907 0.0013029 0.7863220 152
10054838 8931201807 0.0012762 0.7875982 153
10307820 8906175536 0.0012726 0.7888708 154
10307827 8898140436 0.0012715 0.7901423 155
10054817 8834638382 0.0012624 0.7914047 156
10054835 8731081666 0.0012476 0.7926523 157
10054859 8606838398 0.0012299 0.7938822 158
10053331 8569222248 0.0012245 0.7951067 159
10404061 8506058868 0.0012155 0.7963221 160
10054344 8464015802 0.0012095 0.7975316 161
10071512 8454481011 0.0012081 0.7987397 162
10407045 8394225783 0.0011995 0.7999391 163

Như vậy 163 Items chủ lực này mang lại 80% doanh thu nên Rainbow Store nên tập trung công tác hậu cần - kho bãi - dự trữ (Logistic) cho nhóm hàng hóa này. Ví dụ, riêng mã hàng 10053361 đã tạo ra 6.52% doanh thu cho cửa hàng.

Approach to Modelling

Người thực hiện dự án này đề xuất sử dụng các Features có tên là R, F và M (có thể gọi là biến đầu vào Inputs) mô tả hành vi tiêu dùng của khách hàng với các định nghĩa như sau:

  • R (Recency) là khoảng thời gian gần đây nhất mà khách hàng mua hàng hoặc sử dụng dịch vụ (hoặc mua hàng).
  • F (Frequency) là tần suất mua hàng/sử dụng dịch vụ.
  • M (Monetary) là số tiền mà khách hàng mua hàng hóa/dịch vụ.

F và M là những inputs được tính trong một khoảng thời gian khảo sát nhất định (1 năm, một tháng, hoặc 1 quý hoặc là 5 tháng như tình huống của Project này). Riêng R thì phụ thuộc vào mốc thời gian lựa chọn của người làm mô hình và không ảnh hưởng đến kết quả của mô hình. Dưới đây là R codes cho tính toán R, F và M đồng thời tạo ra một cột biến mới có tên BuyNextMonth với mô tả cụ thể như sau: nếu một mã khách hàng, ví dụ, có thực hiện mua hàng trong 2 tháng Feb + Mar và cũng có thực hiện giao dịch trong tháng kế tiếp là Apr thì khách hàng này sẽ được ghi nhận là Yes ở cột biến BuyNextMonth và nếu khách hàng này không có bất kì giao dịch nào ở tháng Apr thì được nghi nhận là No.

Chúng ta sử dụng RFM extract ra từ hai tháng Feb và Mar là Inputs:

CustomerID freq money recency BuyNextMonth
aGdgaGRm 21 14427449 20 No
aGdgbmdq 9 44511900 51 Yes
aGdgbWNm 6 99979199 46 Yes
aGdgcGNs 96 942962495 0 Yes
aGdgcGRp 59 352569255 33 Yes
aGdhb2Jn 2 244802756 1 No

Chúng ta có thể sử dụng nhiều mô hình phân loại để dự báo hành hành vi của khách hàng kể cả cách tiếp cận của thống kê truyền thống là Logistic cho đến các cách tiếp cận hiện đại hơn của Machine Learning. Để lựa chọn một mô hình có chất lượng dự báo tốt nhất mà predicts which customers make at least 1 purchase in a given month thì hướng tiếp cận đề xuất như sau:

  • Huấn luyện 15 mô hình phân loại trên cùng 15 mẫu validation data rồi tính toán trung bình Sensitivity tương ứng.
  • Sensitivity (hay còn gọi là Recall) được lựa chọn làm tiêu chí đánh giá và lựa chọn mô hình vì tiêu chí này cho biến tỉ lệ dự báo đúng những khách hàng sẽ mua hàng trong tháng tới. Chi tiết và giải thích chi tiết về tiêu chí này được trình bày ở đâyở đây.
  • Mô hình nào có Sensitivity sẽ được lựa chọn cho dự báo hành vi tiêu dùng của khách hàng tháng kế tiếp.

Bước này đóng vai trò là thăm dò - tìm kiếm sơ bộ mô hình tốt nhất có thể. Dưới đây là R codes cho việc huấn luyện và so sánh chất lượng dự báo của mô hình dựa trên Sensitivity:

## Iter   TrainDeviance   ValidDeviance   StepSize   Improve
##      1        1.3482             nan     0.1000    0.0190
##      2        1.3161             nan     0.1000    0.0157
##      3        1.2844             nan     0.1000    0.0159
##      4        1.2559             nan     0.1000    0.0135
##      5        1.2352             nan     0.1000    0.0103
##      6        1.2164             nan     0.1000    0.0092
##      7        1.2014             nan     0.1000    0.0077
##      8        1.1869             nan     0.1000    0.0070
##      9        1.1754             nan     0.1000    0.0054
##     10        1.1640             nan     0.1000    0.0058
##     20        1.1036             nan     0.1000    0.0017
##     40        1.0735             nan     0.1000    0.0002
##     50        1.0704             nan     0.1000    0.0001
## 
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
## 
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
## 
## # weights:  26
## initial  value 9433.194597 
## iter  10 value 7597.875242
## iter  20 value 7203.250534
## iter  30 value 7111.234719
## iter  40 value 7108.289764
## iter  50 value 7106.505261
## iter  60 value 7106.263299
## iter  70 value 7105.117909
## iter  80 value 7103.227087
## iter  90 value 7102.075021
## iter 100 value 7101.667148
## final  value 7101.667148 
## stopped after 100 iterations
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
##    user  system elapsed 
##  254.92    7.66 2558.61
# Khai thác các kết quả của việc huấn luyện 15 mô hình: 
list_of_results <- lapply(my_models, function(x) {model_list1[[x]]$resample})

# Chuyển hóa về data frame: 

total_df <- do.call("bind_rows", list_of_results)
total_df %>% mutate(Model = lapply(my_models, function(x) {rep(x, number*repeats)}) %>% unlist()) -> total_df

# Tính trung bình của Sensitivity, AUC, và Specificity: 
total_df %>% 
  dplyr::select(-Resample) %>% 
  group_by(Model) %>% 
  summarise(avg_auc = mean(ROC), avg_sen = mean(Sens), avg_spec = mean(Spec)) %>% 
  ungroup() -> df_results


# Hàm hình ảnh hóa chất lượng phân loại của các mô hình: 


my_bar <- function(metric_name) {
  
  metric_name <- noquote(metric_name)
  my_colors <- c("#3E606F")
  
  df_results %>% 
    dplyr::select(Model, metric_name) -> df
  
  names(df) <- c("Model", "value")
  
  df %>% 
    arrange(value) %>%
    mutate(Model = factor(Model, levels = Model)) %>%
    mutate(label = paste0(round(100*value, 1), "%")) -> m
  
  m %>% 
    ggplot(aes(Model, value)) +
    geom_col(fill = my_colors, color = my_colors) +
    coord_flip() +
    geom_text(data = m, aes(label = label), hjust = 1.1, color = "white", size = 4) + 
    theme_ft_rc() + 
    theme(panel.grid = element_blank()) + 
    theme(axis.text.x = element_blank()) + 
    theme(axis.text.y = element_text(color = "white", size = 12)) + 
    scale_y_discrete(expand = c(0.01, 0)) + 
    labs(x = NULL, y = NULL)
}

# Chất lượng phân loại của các mô hình theo chiều giảm dần của các chỉ số: 
gridExtra::grid.arrange(my_bar("avg_auc") + labs(title = "AUC/ROC"), 
                        my_bar("avg_sen") + labs(title = "Sensitivity"), 
                        my_bar("avg_spec") + labs(title = "Specificity"), 
                        nrow = 1, padding = unit(99, "line"))

Kết quả trên chỉ ra rằng Neural Network (nnet) là mô hình ML có Sensitivity lớn nhất: trung bình là 81.6%. Tuy nhiên nếu lưu ý thêm tiêu chuẩn diện tích nằm dưới đường cong ROC thì GBM (Gradient Boosting Machines) lại là mô hình toàn diện hơn cả.

Do vậy mô hình kiến nghị được lựa chọn để dự báo một khách hàng “có mua ít nhất một item trong tháng tới hay không?” sẽ là GBM. Dưới đây là R codes huấn luyện GBM trên train data rồi đánh giá lại chất lượng dự báo của mô hình trên test data:

## Iter   TrainDeviance   ValidDeviance   StepSize   Improve
##      1        1.3391             nan     0.1000    0.0232
##      2        1.3013             nan     0.1000    0.0189
##      3        1.2705             nan     0.1000    0.0154
##      4        1.2424             nan     0.1000    0.0133
##      5        1.2201             nan     0.1000    0.0111
##      6        1.2010             nan     0.1000    0.0094
##      7        1.1826             nan     0.1000    0.0088
##      8        1.1678             nan     0.1000    0.0075
##      9        1.1545             nan     0.1000    0.0065
##     10        1.1426             nan     0.1000    0.0055
##     20        1.0835             nan     0.1000    0.0012
##     40        1.0583             nan     0.1000   -0.0001
##     50        1.0549             nan     0.1000   -0.0001
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   No  Yes
##        No  1085  477
##        Yes  256  833
##                                          
##                Accuracy : 0.7235         
##                  95% CI : (0.706, 0.7405)
##     No Information Rate : 0.5058         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.4458         
##                                          
##  Mcnemar's Test P-Value : 4.441e-16      
##                                          
##             Sensitivity : 0.6359         
##             Specificity : 0.8091         
##          Pos Pred Value : 0.7649         
##          Neg Pred Value : 0.6946         
##              Prevalence : 0.4942         
##          Detection Rate : 0.3142         
##    Detection Prevalence : 0.4108         
##       Balanced Accuracy : 0.7225         
##                                          
##        'Positive' Class : Yes            
## 

Thực tế có 1310 khách mua hàng trong tháng Apr và GBM dự báo đúng sẽ có 833 người (trong tổng số 1310) sẽ mua hàng. Nói cách khác Sensitivity là 63.59% (bằng 833 / (833 + 477)) như ta có thể thấy ở ma trận nhầm lẫn. Chỉ số đánh giá chất lượng khác của mô hình chúng ta quan tâm có thể là Accuracy = 72.23%.

Chúng ta cũng có thể minh họa ROC và tính diện tích nằm dưới đường cong AUC:

Trong hầu hết các ứng dụng thì ROC/AUC trên 65% là mô hình có thể sử dụng được. Trong tình huống của chúng ta thì GBM có ROC/AUC = 77.60% trên test data cũng là một kết quả khá cao.

Discussion Problem

Dưới đây là một số vấn đề chưa được giải quyết:

  • Thứ nhất, những kết quả mà chúng ta thu được từ việc sử dụng mô hình GBM trên test data thì đó mới chỉ là GBM mặc định chưa tinh chỉnh tham số. Bằng việc tinh chỉnh tham số tối ưu cho GBM chúng ta có thể đạt kết quả chính xác hơn khi dự báo có hay không một khách hàng sẽ mua hàng trong tháng kế tiếp. Tuy nhiên trong giới hạn thời gian 8h thì việc tinh chỉnh GBM là việc không khả thì vì thời gian training có thể rất lớn và đòi hỏi cấu hình phần cứng máy tính tương đối cao. Giải pháp khác là lựa chọn dịch vụ tính toán đám mây của Amazon. Riêng việc huấn luyện 15 mô hình ở trên cũng đã mất gần 2h.

  • Thứ hai, Assignment này không đưa ra một mục tiêu cụ thể nào cho việc xây dựng mô hình. Các mô hình ML có thể được tinh chỉnh (và lựa chọn) theo một tiêu chí nào đó như Accuracy, Recall hay ROC/AUC nhưng Assignment này thì không nêu rõ cụ thể. Do vậy, người xây dựng mô hình này tự đặt ra mục tiêu của việc xây dựng mô hình, ví dụ, là phải cover được ít nhất 70% khách hàng sẽ mua hàng trong tháng tới. Hiện tại GBM mặc định mới chỉ cover được 63.59% khách hàng sẽ mua hàng trong tháng tới. Mục tiêu này có thể đạt được bằng một trong hai cách: (1) tinh chỉnh/tìm kiếm tham số tối ưu cho GBM, hơạc (2) thay đổi ngưỡng xác suất khi phân loại vì ma trận nhầm lẫn mặc định sử dụng ngưỡng 0.5 cho phân loại. Vấn đề này đã được trình bày và giải quyết ở mục 3.3.

  • Việc tinh chỉnh các mô hình ML có thể rất mất thời gian. Chiến lược tinh chỉnh kiểu Grid Search sẽ liệt kê ra tất cả các sự kết hợp có thể có của các tham số rồi tìm kiếm một sự kết hợp cụ thể nào đó của tham số sao cho lượng phân loại của mô hình là cao nhất. Như vậy nếu chúng ta lựa chọn chỉ 5 tham số (con số này vẫn còn ít so với các tham số có thể tinh chỉnh) và mỗi một tham số chọn 10 ứng viên và sử dụng 5 folds cho Cross-Validation thì tổng số các mô hình mà máy tính sẽ phải chạy là 5×10^5 = 500.000 mô hình (nếu lựa chọn refit = False). Để cắt giảm thời gian huấn luyện chúng ta có thể thực hiện tinh chỉnh tham số theo kiểu hên - xui bằng cách chỉ chọn ngẫu nhiên, chẳng hạn, 50000 (tức là 10% của 500.000) sự kết hợp khác nhau của tham số để tinh chỉnh mô hình. Cách thức tinh chỉnh hên xui này gọi là Random Search. Chiến lược tinh chỉnh này là chúng ta có thể cắt giảm đáng kể thời gian tinh chỉnh với các giá phải trả là chúng ta có thể “bỏ sót” tham số tốt nhất của mô hình ML. Tuy vậy trong một số tình huống thì tinh chỉnh theo Random Seach có thể vẫn là không hiệu quả và chúng ta cần một chiến lược tinh chỉnh hiệu quả hơn là Bayesian Optimization có thể được thực hiện trên cả R lẫn Python. Nếu sử dụng Python thì Bayesian Optimization có thể được thực hiện như sau.

  • Về mặt kĩ thuật, có thể sử dụng thư viện h2o để huấn luyện và tinh chỉnh GBM. Việc sử dụng h2o cho huấn luyện và tinh chỉnh GBM (cũng như các mô hình ML nói chung) có một vài ưu thế: (1) chạy trên cả R và Python với cú pháp tương tự nhau, (2) được viết với lõi là ngôn ngữ Java nên triển khai ứng dụng ở dạng web service là rất tiện lợi, (3) thời gian training mô hình tương đối nhanh.

Tất cả những vấn đề này cần thời gian (cũng như phần cứng máy tính tương đối mạnh) và không thể giải quyết được trong khoảng thời gian là 8h.

Applications in Bussines Context

Giả sử mục tiêu được hạ xuống là mô hình cover được ít nhất 60% khách hàng sẽ mua hàng trong tháng tới (tức là tháng Apr) thì GBM mặc định của chúng ta thỏa mãn. Nếu vậy chúng ta sẽ gửi promotional e-mails cho 833 khách hàng có danh sách dưới đây (chỉ liệt kê 6 khách hàng đầu):

CustomerID BuyNextMonth_Predicted
aGdobmNl Yes
aGdobmZj Yes
aGhkaWo= Yes
aGhkcWpo Yes
aGhpa2dk Yes
aGNgaGNl Yes

Còn nếu chúng ta muốn dự báo những khách hàng nào sẽ mua hàng trong tháng 5 (May) dựa trên dữ liệu giao dịch của hai tháng trước đó là Mar và Apr thì trước hết chúng ta training mô hình GBM (mặc định) trên dữ liệu của hai tháng Mar + Apr:

## Iter   TrainDeviance   ValidDeviance   StepSize   Improve
##      1        1.3408             nan     0.1000    0.0201
##      2        1.3068             nan     0.1000    0.0171
##      3        1.2785             nan     0.1000    0.0141
##      4        1.2534             nan     0.1000    0.0118
##      5        1.2326             nan     0.1000    0.0100
##      6        1.2136             nan     0.1000    0.0093
##      7        1.1982             nan     0.1000    0.0076
##      8        1.1842             nan     0.1000    0.0064
##      9        1.1712             nan     0.1000    0.0062
##     10        1.1614             nan     0.1000    0.0044
##     20        1.1075             nan     0.1000    0.0008
##     40        1.0860             nan     0.1000   -0.0001
##     50        1.0824             nan     0.1000   -0.0001

Chất lượng dự báo của mô hình thể hiện qua ma trận nhầm lẫn:

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   No  Yes
##        No  1167  469
##        Yes  235  780
##                                           
##                Accuracy : 0.7344          
##                  95% CI : (0.7172, 0.7512)
##     No Information Rate : 0.5289          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.4616          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
##                                           
##             Sensitivity : 0.6245          
##             Specificity : 0.8324          
##          Pos Pred Value : 0.7685          
##          Neg Pred Value : 0.7133          
##              Prevalence : 0.4711          
##          Detection Rate : 0.2942          
##    Detection Prevalence : 0.3829          
##       Balanced Accuracy : 0.7284          
##                                           
##        'Positive' Class : Yes             
## 

GBM lúc này sẽ cover đúng 62.45% khách hàng sẽ mua hàng vào tháng May kế tiếp và dưới đây là danh sách 780 khách hàng đó:

CustomerID BuyNextMonth_Predicted
aGdobmZj Yes
aGdpbmFp Yes
aGhna2Rk Yes
aGhoaWlp Yes
aGlga2k= Yes
aGNhaWVm Yes
LS0tDQp0aXRsZTogJ1ByZWRpY3QgQ3VzdG9tZXIgUHVyY2hhc2UnDQphdXRob3I6ICdBdXRob3I6IE5ndXllbiBDaGkgRHVuZycNCnN1YnRpdGxlOiAiTWluaSBQcm9qZWN0IGZvciBWaW5JRCdzIFJlY3J1aXRtZW50Ig0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgI2NvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgZmlnLndpZHRoID0gMTAsIGZpZy5oZWlnaHQgPSA2KQ0KYGBgDQoNCiFbXShDOlxcVXNlcnNcXEFETUlOXFxEZXNrdG9wXFxzYWxsaW5nLmpwZykNCg0KDQojIE1vdGl2YXRpb25zIGFuZCBPYmplY3RpdmVzDQoNCkThu7Egw6FuIG7DoHkgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBuaOG6sW0geMOieSBk4buxbmcgdsOgIHBow6F0IHRyaeG7g24gbeG7mXQgbcO0IGjDrG5oIGThu7EgYsOhbyBow6BuaCB2aSBtdWEgc+G6r20gY+G7p2Ega2jDoWNoIGjDoG5nIGThu7FhIHRyw6puIGThu68gbGnhu4d1IGzhu4tjaCBz4butIHbhu4EgZ2lhbyBk4buLY2guIELhu5kgZOG7ryBsaeG7h3UgdGjDtCDEkcaw4bujYyBjdW5nIGPhuqVwIGJhbyBn4buTbSBjw6FjIHRow7RuZyB0aW4gc2F1OiANCg0KLSAqKmN1c3RvbWVyIHNlcmlhbCBudW1iZXIgKGNzbikqKiBsw6AgbcOjIHPhu5EgY+G7p2Ega2jDoWNoIGjDoG5nLiANCi0gKip0cmFuc2FjdGlvbl9pbmZvKiogbMOgIHRow7RuZyB0aW4gY+G7pSB0aOG7gyB24buBIGdp4buPIGjDoG5nIGjDs2EgxJHDoyBtdWEgY+G7p2Ega2jDoWNoIGjDoG5nIHRyb25nIMSRw7M6ICgxKSAqYXJ0aWNsZSogbMOgIG3DoyBow6BuZyBow7NhLCAoMikgKnNhbGVzcXVhbnRpdHkqIGzDoCBz4buRIGzGsOG7o25nIG11YSwgdsOgICgzKSAqcHJpY2UqIGzDoCBnacOhIGjDoG5nLiANCg0KIyBEYXRhIFByb2Nlc3NpbmcNCg0KVHLGsOG7m2MgaOG6v3QgxJHhu41jIGThu68gbGnhu4d1IHRow7QgdsOgIHhlbSBxdWE6IA0KDQpgYGB7cn0NCiMgxJDhu41jIGThu68gbGnhu4d1OiANCnJtKGxpc3QgPSBscygpKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGtuaXRyKQ0KZGZfdHJhbiA8LSByZWFkX2NzdigiQzpcXFVzZXJzXFxBRE1JTlxcRGVza3RvcFxcVmluSURSZWNydWl0Q2hhbGxlbmdlXFxWaW5JRFJlY3J1aXRDaGFsbGVuZ2VfTUxUcmFja19EYXRhU2V0LmNzdiIpDQoNCiMgWGVtIHF1YSBt4buZdCBz4buRIGdpYW8gZOG7i2NoOiANCmRmX3RyYW4gJT4lIA0KICBzbGljZShjKDEsIDgpKSAlPiUgDQogIGthYmxlKCkNCg0KYGBgDQoNCk5oxrAgduG6rXkga2jDoWNoIGjDoG5nIGPDsyBtw6MgKipZMk5nYVdKb1l3PT0qKiB2w6BvIG5nw6B5IDIwMTgtMDMtMDIgY2jhu4kgbXVhIG3hu5l0IEl0ZW0gY8OzIG3DoyBsw6AgMTAwMjAxNjMsIHPhu5EgbMaw4bujbmcgbXVhIGzDoCAyIHbDoCBnacOhIGLDoW4gKGNobyBt4buZdCBz4bqjbiBwaOG6qW0pIGzDoCAxODI1MC4gVuG7m2kga2jDoWNoIGjDoG5nIGPDsyBtw6MgbMOgICoqWTJOZ2FtUnBaQT09KiogdsOgbyBuZ8OgeSAyMDE4LTAyLTA4IHRow6wgbmfGsOG7nWkgbsOgeSBtdWEgbmhp4buBdSBoxqFuIG3hu5l0IHPhuqNuIHBo4bqpbSB2w6AgbeG7mXQgbeG7mXQgc+G6o24gcGjhuqltIHRyb25nIGdp4buPIGjDoG5nIMSRxrDhu6NjIG5nxINuIGPDoWNoIG5oYXUgYuG7n2kgKip9LCB7KiouIMSQw6FuZyBjaMO6IMO9IGzDoCB24bubaSBow6BuZyBow7NhIGPDsyBtw6MgMTAyMjU2MzYgdGjDrCBsxrDhu6NuZyBtdWEgbMOgIDEuMTQyMDAwMDAwMDAwMDAwMS4gDQoNCkzGsOG7o25nIG11YSBsw6AgbeG7mXQgc+G7kSBuZ3V5w6puIGTGsMahbmcuIEPDoWMgY2h14buXaSBj4butYSBow6BuZywgbuG6v3UgY8OzIGLDoW4gbuG7rWEgY8OibiBt4buxYyBraMO0IGNo4bqzbmcgaOG6oW4sIHRow6wgbsOzIHPhur0gxJHDs25nIHRow6BuaCBnw7NpIHbDoCBo4buHIHRo4buRbmcgbMawdSB0aMO0bmcgdGluIHPhur0gZ2hpIG5o4bqtbiBsw6AgImLDoW4gcmEgMSBnw7NpIG3hu7FjIi4gRG8gduG6rXkgY8OzIHRo4buDIHN1eSBsdeG6rW4gcuG6sW5nIDEuMTQyMDAwMDAwMDAwMDAwMSBsw6AgbeG7mXQgbOG7l2kgY+G7p2EgaOG7hyB0aOG7kW5nIGPGoSBz4bufIGThu68gbGnhu4d1LiANCg0KROG7sWEgdHLDqm4gbmjhu69uZyBwaMOibiB0w61jaCBk4buvIGxp4buHdSB0aMO0IOG7nyB0csOqbiBjaMO6bmcgdGEgdmnhur90IGjDoG0gxJHhu4MgZXh0cmFjdCByYSBjw6FjIHRow7RuZyB0aW4gduG7gSBt4buZdCBnaWFvIGThu4tjaCBuaMawIHNhdTogDQoNCmBgYHtyfQ0KDQpuIDwtIG5yb3coZGZfdHJhbikgIyBT4buRIGzGsOG7o25nIGPDoWMgZ2lhbyBk4buLY2guIA0KdHJhbl9pbmYgPC0gZGZfdHJhbiR0cmFuc2FjdGlvbl9pbmZvICMgVGjDtG5nIHRpbiBjaHVuZyB24buBIGdpYW8gZOG7i2NoIGPhu6dhIGtow6FjaCBow6BuZy4gDQp0cmFuX2RhdGUgPC0gZGZfdHJhbiRkYXRlICMgVGjhu51pIMSRaeG7g20gdGjhu7FjIGhp4buHbiBnaWFvIGThu4tjaC4gDQpjdXNJRCA9IGRmX3RyYW4kY3NuICMgTcOjIGtow6FjaCBow6BuZy4gDQoNCiMgVmnhur90IGjDoG0gRXh0cmFjdCBjw6FjIHRow7RuZyB0aW4gduG7gTogKDEpIE3DoyBow6BuZywgKDIpIHPhu5EgbMaw4bujbmcsIHbDoCAoMykgZ2nDoTogDQoNCmV4dHJhY3RfdHJhbnNhY3Rpb25JbmZvIDwtIGZ1bmN0aW9uKGspIHsNCiAgc3RyX3NwbGl0KHRyYW5faW5mW2tdLCAiXFx9XFwsIiwgc2ltcGxpZnkgPSBUUlVFKSAlPiUgDQogICAgc3RyX3NwbGl0KCI6Iiwgc2ltcGxpZnkgPSBUUlVFKSAlPiUgDQogICAgZGF0YS5mcmFtZSgpICU+JSANCiAgICBzZWxlY3QoLVgxKSAlPiUgDQogICAgbXV0YXRlX2FsbChmdW5jdGlvbih4KSB7c3RyX3JlcGxhY2VfYWxsKHgsICJbXjAtOV0iLCAiIil9KSAlPiUgDQogICAgbXV0YXRlKFN0b2NrQ29kZSA9IGFzLmNoYXJhY3RlcihYMiksIFF1YW50aXR5ID0gYXMuaW50ZWdlcihYMykgLyAxMCwgVW5pdFByaWNlID0gYXMubnVtZXJpYyhYNCkgLyAxMCkgJT4lIA0KICAgIHNlbGVjdCgtY29udGFpbnMoIlgiKSkgJT4lIA0KICAgIG11dGF0ZShJbnZvaWNlRGF0ZSA9IHRyYW5fZGF0ZVtrXSwgQ3VzdG9tZXJJRCA9IGN1c0lEW2tdKSAlPiUgDQogICAgc2VsZWN0KEN1c3RvbWVySUQsIEludm9pY2VEYXRlLCBldmVyeXRoaW5nKCkpICU+JSANCiAgICByZXR1cm4oKQ0KfQ0KDQpgYGANCg0KSMOgbSB0csOqbiB0cuG6oyByYSBr4bq/dCBxdeG6oyBsw6AgbeG7mXQgRGF0YSBGcmFtZS4gVsOtIGThu6UgduG7m2kgZ2lhbyBk4buLY2ggdGjhu6kgMSB2w6AgdGjhu6kgODogDQoNCg0KYGBge3J9DQojIEdpYW8gZOG7i2NoIHRo4bupIDE6IA0KZXh0cmFjdF90cmFuc2FjdGlvbkluZm8oMSkgJT4lIGthYmxlKCkNCg0KIyBHaWFvIGThu4tjaCB0aOG7qSA4OiANCmV4dHJhY3RfdHJhbnNhY3Rpb25JbmZvKDgpICU+JSBrYWJsZSgpDQoNCmBgYA0KDQoNCkdpYW8gZOG7i2NoIHRo4bupIDggY8OzIE1pc3NpbmcgRGF0YSB24buBIGzGsOG7o25nIGjDoG5nIG11YSBjaG8gbcOjIDEwMjI1NjM2IG3DoCBuZ3V5w6puIG5ow6JuIGLhuq90IG5ndeG7k24gdOG7qyB2aeG7h2MgZOG7ryBsaeG7h3UgZ+G7kWMgZ2hpIG5o4bqtbiBz4buRIGzGsOG7o25nIGPhu6dhIG3DoyBow6BuZyBuw6B5IGzDoCAxLjE0MjAwMDAwMDAwMDAwMDEuIFPhu60gZOG7pW5nIGjDoG0gxJHDoyBjw7MgxJHhu4MgeOG7rSBsw60gZOG7ryBsaeG7h3U6IA0KDQoNCmBgYHtyLCBldmFsPUZBTFNFfQ0KbGFwcGx5KDE6biwgZXh0cmFjdF90cmFuc2FjdGlvbkluZm8pIC0+IGRhdGFfbGlzdA0KZG8uY2FsbCgiYmluZF9yb3dzIiwgZGF0YV9saXN0KSAtPiBkYXRhX2RmDQpzYXZlKGRhdGFfZGYsIGZpbGUgPSAiZGF0YV9kZi5SRGF0YSIpICMgTMawdSBs4bqhaSBk4buvIGxp4buHdS4gDQpgYGANCg0KVuG7m2kgY8OhYyB0aMO0bmcgdGluIGV4dHJhY3QgxJHGsOG7o2MgY2jDum5nIHRhIGPDsyB0aOG7gyB4ZW0gcXVhOiANCg0KYGBge3J9DQpsb2FkKCJDOlxcVXNlcnNcXEFETUlOXFxEZXNrdG9wXFxkYXRhX2RmLlJEYXRhIikgIyBMb2FkIGThu68gbGnhu4d1IMSRw6MgbMawdS4gDQphbGxfb2JqIDwtIGxzKCkNCnJtKGxpc3QgPSBhbGxfb2JqWyFzdHJfZGV0ZWN0KGFsbF9vYmosICJkYXRhX2RmIildKSAjIFjDs2EgY8OhYyBvYmplY3RzIHRo4burYS4gDQoNCiMgWGVtIHF1YSBk4buvIGxp4buHdTogDQpkYXRhX2RmICU+JSANCiAgaGVhZCgpICU+JQ0KICBrYWJsZSgpDQpgYGANCg0KxJDDoW5oIGdpw6EgbeG7qWMgxJHhu5kgdGhp4bq/dSBj4bunYSBk4buvIGxp4buHdTogDQoNCg0KYGBge3J9DQojIEjDoG0gxJHDoW5oIGdpw6EgZOG7ryBsaeG7h3UgdHLhu5FuZzogDQpuYV9yYXRlIDwtIGZ1bmN0aW9uKHgpIHsxMDAqc3VtKGlzLm5hKHgpKSAvIGxlbmd0aCh4KX0NCg0KIyBT4butIGThu6VuZyBow6BtOiANCmRhdGFfZGYgJT4lDQogIHN1bW1hcmlzZV9hbGwobmFfcmF0ZSkgJT4lIA0KICBrYWJsZSgpDQpgYGANCg0KDQpDw7MgZ+G6p24gOSUgZOG7ryBsaeG7h3UgbMOgIE1pc3Npbmcg4bufIGPhu5l0IGJp4bq/biBRdWFudGl0eS4gTmjGsCDEkcOjIHBow6JuIHTDrWNoIOG7nyB0csOqbiBjaMO6bmcgdGEgY8OzIHRo4buDIGdp4bqjIMSR4buLbmggcuG6sW5nIG5ndXnDqm4gbmjDom4gxJHhur9uIHThu6sgdmnhu4djIG5naGkgbmjhuq1uIGtow7RuZyBwaMO5IGjhu6NwIGPhu6dhIGjhu4cgdGjhu5FuZyBjxqEgc+G7nyBk4buvIGxp4buHdS4gVmnhu4djIHjhu60gbMOtIGPDoWMgZOG7ryBsaeG7h3UgdHLhu5FuZyBuw6B5IGLhurFuZyBjw6FjIHBoxrDGoW5nIHBow6FwIHRow7RuZyB0aMaw4budbmcgKG5oxrAgdGhheSB0aOG6vyBi4bqxbmcgbWVhbiBoYXkgbWVkaWFuKSBsw6Aga2jDtG5nIHBow7kgaOG7o3AuIE7hur91IMSRxrDhu6NjIGN1bmcgY+G6pXAgY2hpIHRp4bq/dCBoxqFuIG5o4buvbmcgdGjDtG5nIHRpbiB24buBIGThu68gbGnhu4d1IHRow7QsIHbhu4EgY8OhY2ggdGjhu6ljIGjhu4cgdGjhu5FuZyBnaGkgbmjhuq1uIGPGoSBz4bufIGThu68gbGnhu4d1IHRow6wgY2jDum5nIHRhIGPDsyB0aOG7gyBjw7MgY8OhY2ggeOG7rSBsw60gTWlzc2luZyBEYXRhIG7DoHkgcGjDuSBo4bujcCBoxqFuLiANCg0KQ2jDum5nIHRhIGPFqW5nIGdp4bqjIMSR4buLbmggcuG6sW5nIGPDoWMgTWlzc2luZyB0aHXhu5ljIGtp4buDdSBNQVIgKE1pc3NpbmcgYXQgUmFuZG9tKSB2w6AgZG8gduG6rXkgdmnhu4djIGxv4bqhaSBjw6FjIE1pc3Npbmcga2jDtG5nIOG6o25oIGjGsOG7n25nIMSR4bq/biB2aeG7h2MgeMOieSBk4buxbmcgbcO0IGjDrG5oIHbDrCB2aeG7h2MgbsOgeSBsw6AgdMawxqFuZyDEkcawxqFuZyB24bubaSB2aeG7h2MgbeG6q3UgcXVhbiBzw6F0IMSRxrDhu6NjIGzhuqV5IMOtdCBoxqFuIDklIHNvIHbhu5tpIGJhbiDEkeG6p3U6IA0KDQoNCg0KYGBge3J9DQojIExv4bqhaSBjw6FjIE1pc3Npbmc6IA0KZGF0YV9kZiAlPiUgZmlsdGVyKCFpcy5uYShRdWFudGl0eSkpIC0+IG15X2RmDQoNCiMgVOG6oW8gdGjDqm0gbeG7mXQgc+G7kSBiaeG6v24gc+G7kSBtw6AgY8OzIHRo4buDIHPhu60gZOG7pW5nIHNhdSBuw6B5OiANCg0KbGlicmFyeShsdWJyaWRhdGUpDQoNCm15X2RmICU+JSANCiAgcmVuYW1lKHRpbWVfeW1kID0gSW52b2ljZURhdGUpICU+JSANCiAgbXV0YXRlKHdfZGF5ID0gd2RheSh0aW1lX3ltZCwgbGFiZWwgPSBUUlVFLCBhYmJyID0gVFJVRSksIA0KICAgICAgICAgdGltZV9tb24gPSBtb250aCh0aW1lX3ltZCwgbGFiZWwgPSBUUlVFLCBhYmJyID0gVFJVRSkpIC0+IG15X2RmDQoNCiMgWGVtIHF1YTogDQpteV9kZiAlPiUgDQogIGhlYWQoKSAlPiUgDQogIGthYmxlKCkNCmBgYA0KDQpE4buvIGxp4buHdSDEkcaw4bujYyBsw6BtIHPhuqFjaCDhu58gdHLDqm4gZ2nhu50gY8OzIHRo4buDIMSRxrDhu6NjIHPhu60gZOG7pW5nIGNobyBwaMOibiB0w61jaCB2w6AgeMOieSBk4buxbmcgbcO0IGjDrG5oIGThu7EgYsOhbyBow6BuaCB2aSBtdWEgc+G6r20gY+G7p2Ega2jDoWNoIGjDoG5nLiANCg0KIyBFeHBsb3JhdG9yeSBEYXRhIEFuYWx5c2lzDQoNCkLGsOG7m2MgcGjDom4gdMOtY2gga2jDoW0gcGjDoSBk4buvIGxp4buHdSBuw6B5IMSRxrDhu6NjIHRo4buxYyBoaeG7h24gbmjhurFtIHTDrG0ga2nhur9tIG3hu5l0IHPhu5EgaW5zaWdodHMgY8OzIHRo4buDIGzDoCBxdWFuIHRy4buNbmcgY2hvIHZp4buHYyB0aGnhur90IGvhur8gLSB4w6J5IGThu7FuZyBtw7QgaMOsbmggZOG7sSBiw6FvIGjDoG5oIHZpIGPhu6dhIGtow6FjaCBow6BuZyBzYXUgbsOgeS4gVsOtIGThu6UgY2jDum5nIHRhIGPDsyB0aOG7gyB0aOG6pXkgY8OzIG3hu5l0IHRo4budaSDEkWnhu4NtIMSRw6J1IMSRw7MgZ2nhu69hIEZlYiB2w6AgTWFyIHPhuqNuIGzGsOG7o25nIGLDoW4gcmEgdMSDbmcgxJHhu5l0IG5n4buZdC4gxJDDonkgY8OzIHRo4buDIGzDoCBr4bq/dCBxdeG6oyBj4bunYSB2aeG7h2MgUmFpbmJvdyBTdG9yZSB0aOG7sWMgaGnhu4duIGPDoWMgY2hp4bq/biBk4buLY2gga2h1eeG6v24gbcOjaSBrw61jaCBj4bqndSBtdWEgc+G6r206IA0KDQoNCmBgYHtyfQ0KbGlicmFyeShocmJydGhlbWVzKQ0KdGhlbWVfc2V0KHRoZW1lX21vZGVybl9yYygpKQ0KDQpteV9kZiAlPiUgDQogIGdyb3VwX2J5KHRpbWVfeW1kKSAlPiUgDQogIHN1bW1hcmlzZShzYWxlcyA9IHN1bShRdWFudGl0eSkpICU+JSANCiAgdW5ncm91cCgpIC0+IHNhbGVzX2J5VGltZV9obQ0KDQpzYWxlc19ieVRpbWVfaG0gJT4lIA0KICBtdXRhdGUoc2FsZXMgPSBzYWxlcyAvIDEwMDApICU+JSANCiAgZ2dwbG90KGFlcyh0aW1lX3ltZCwgc2FsZXMpKSArIA0KICBnZW9tX2xpbmUoKSArIA0KICBnZW9tX3BvaW50KGNvbG9yID0gImZpcmVicmljayIpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAsIDMwMCkpICsgDQogIGxhYnModGl0bGUgPSAiRmlndXJlIDE6IERhaWx5IFF1YW50aXR5IFNhbGVzIChUaG91c2FuZHMpIiwgeCA9ICJUaW1lIiwgeSA9ICJRdWFudGl0eSIpDQpgYGANCg0KVMawxqFuZyB04buxIGzDoCBkb2FuaCB0aHUgY+G7p2EgUmFpbmJvdyBTdG9yZTogDQoNCg0KYGBge3J9DQpteV9kZiAlPiUgDQogIG11dGF0ZShtb25leSA9IFF1YW50aXR5KlVuaXRQcmljZSkgLT4gbXlfZGYNCg0KbXlfZGYgJT4lIA0KICBncm91cF9ieSh0aW1lX3ltZCkgJT4lIA0KICBzdW1tYXJpc2UobW9uZXlTYWxlcyA9IHN1bShtb25leSkpICU+JSANCiAgdW5ncm91cCgpIC0+IHNhbGVzX2J5VGltZV9tb25leQ0KDQpzYWxlc19ieVRpbWVfbW9uZXkgJT4lIA0KICBtdXRhdGUobW9uZXlTYWxlcyA9IG1vbmV5U2FsZXMgLyAxMDAwMDAwMDAwKSAlPiUgDQogIGdncGxvdChhZXModGltZV95bWQsIG1vbmV5U2FsZXMpKSArIA0KICBnZW9tX2xpbmUoKSArIA0KICBnZW9tX3BvaW50KGNvbG9yID0gImZpcmVicmljayIpICsgDQogIGxhYnModGl0bGUgPSAiRmlndXJlIDI6IERhaWx5IE1vbmV0YXJ5IFNhbGVzIChCaWxsaW9ucykiLCB4ID0gIlRpbWUiLCB5ID0gIiIpDQoNCmBgYA0KDQoNClPhuqNuIGzGsOG7o25nIGLDoW4gcmEgY+G7p2EgUmFpbmJvdyBTdG9yZSBjYW8gbmjhuqV0IOG7nyBKdW46IA0KDQpgYGB7cn0NCm15X2RmICU+JSANCiAgZ3JvdXBfYnkodGltZV9tb24pICU+JSANCiAgc3VtbWFyaXNlX2VhY2goZnVucyhzdW0pLCBRdWFudGl0eSkgJT4lIA0KICBtdXRhdGUoUXVhbnRpdHkgPSBRdWFudGl0eSAvIDEwMDApICU+JSANCiAgZ2dwbG90KGFlcyh0aW1lX21vbiwgUXVhbnRpdHkpKSArIA0KICBnZW9tX2NvbCgpICsgDQogIHRoZW1lKHBhbmVsLmdyaWQubWFqb3IueCA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgbGFicyh0aXRsZSA9ICJGaWd1cmUgMzogUXVhbnRpdHkgU2FsZXMgaW4gVGhvdXNhbmRzIGJ5IE1vbnRoIiwgeCA9ICJUaW1lIiwgeSA9ICJRdWFubGl0eSIpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDAsIDQwMDApKQ0KDQpgYGANCg0KVHV5IG5oacOqbiBkb2FuaCB0aHUgdGjDrCBjw7MgeHUgaMaw4bubbmcgZ2nhuqNtIG5o4bq5IHF1YSBjw6FjIHRow6FuZyBuZ2/huqFpIHRy4burIHRow6FuZyBNYXkgY8OzIGRvYW5oIHRodSB0xINuZyBsw6puOiANCg0KYGBge3J9DQpteV9kZiAlPiUgDQogIGdyb3VwX2J5KHRpbWVfbW9uKSAlPiUgDQogIHN1bW1hcmlzZV9lYWNoKGZ1bnMoc3VtKSwgbW9uZXkpICU+JSANCiAgbXV0YXRlKG1vbmV5ID0gbW9uZXkgLyAxMDAwMDAwMDAwKSAlPiUgDQogIGdncGxvdChhZXModGltZV9tb24sIG1vbmV5KSkgKyANCiAgZ2VvbV9jb2woKSArIA0KICB0aGVtZShwYW5lbC5ncmlkLm1ham9yLnggPSBlbGVtZW50X2JsYW5rKCkpICsgDQogIGxhYnModGl0bGUgPSAiRmlndXJlIDQ6IE1vbmV0YXJ5IFNhbGVzIGluIEJpbGxpb25zIGJ5IE1vbnRoIiwgeCA9ICJUaW1lIiwgeSA9ICIiKQ0KYGBgDQoNCkNow7puZyB0YSBjxaluZyBjw7MgdGjhu4MgdGjhuqV5IHLhurFuZyBkb2FuaCB0aHUgxJHhur9uIHThu6sgY8OhYyBJdGVtIGPDsyBwaMOibiBi4buRIGtow7RuZyDEkeG7gXU6IGPDsyBt4buZdCBz4buRIG3hurd0IGjDoG5nIG1hbmcgbOG6oWkgZG9hbmggY2h1IHLhuqV0IGNhbyBjaG8gUmFpbmJvdyBTdG9yZTogDQoNCg0KYGBge3J9DQpteV9kZiAlPiUgDQogIGdyb3VwX2J5KFN0b2NrQ29kZSkgJT4lIA0KICBzdW1tYXJpc2Uoc2FsZXMgPSBzdW0obW9uZXkpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGFycmFuZ2UoLXNhbGVzKSAlPiUgDQogIG11dGF0ZShTdG9ja0NvZGUgPSBmYWN0b3IoU3RvY2tDb2RlLCBsZXZlbHMgPSBTdG9ja0NvZGUpKSAlPiUgDQogIG11dGF0ZSh0b3RhbCA9IHN1bShzYWxlcykpICU+JSANCiAgbXV0YXRlKG1vbmV5X3BlcmNlbnQgPSBzYWxlcyAvIHRvdGFsKSAlPiUgDQogIG11dGF0ZShjdW1fbW9uZXkgPSBjdW1zdW0obW9uZXlfcGVyY2VudCkpIC0+IG1vbmV5U2FsZXNfSXRlbQ0KDQoNCg0KbW9uZXlTYWxlc19JdGVtICU+JSANCiAgbXV0YXRlKHNhbGVzID0gc2FsZXMgLyAxMDAwMDAwMDAwKSAlPiUgDQogIGdncGxvdChhZXMoU3RvY2tDb2RlLCBzYWxlcykpICsgDQogIGdlb21fY29sKCkgKyANCiAgdGhlbWUocGFuZWwuZ3JpZC5tYWpvci54ID0gZWxlbWVudF9ibGFuaygpKSArIA0KICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgbGFicyh0aXRsZSA9ICJGaWd1cmUgNTogTW9uZXkgU2FsZXMgYnkgUHJvZHVjdCAoQmlsbGlvbnMpIiwgeCA9ICIiLCB5ID0gIiIpDQoNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGxpc3QgcmEgZGFuaCBzw6FjaCBjw6FjIG3hurd0IGjDoG5nIG3DoCBtYW5nIGzhuqFpIDgwJSBkb2FuaCB0aHUgY2hvIFJhaW5ib3cgU3RvcmU6IA0KDQpgYGB7cn0NCm1vbmV5U2FsZXNfSXRlbSAlPiUgDQogIGZpbHRlcihjdW1fbW9uZXkgPD0gMC44KSAtPiB0b3A4MF9zYWxlcw0KDQp0b3A4MF9pdGVtcyA8LSB0b3A4MF9zYWxlcyAlPiUgbnJvdygpDQp0b3A4MF9pdGVtcyAvIG5yb3cobW9uZXlTYWxlc19JdGVtKQ0KDQoNCm1vbmV5U2FsZXNfSXRlbSAlPiUgDQogIHNlbGVjdCgtdG90YWwpICU+JSANCiAgZmlsdGVyKFN0b2NrQ29kZSAlaW4lIHRvcDgwX3NhbGVzJFN0b2NrQ29kZSkgJT4lIA0KICBtdXRhdGUoSUQgPSAxOnRvcDgwX2l0ZW1zKSAlPiUgDQogIGthYmxlKCkNCmBgYA0KDQpOaMawIHbhuq15IDE2MyBJdGVtcyBjaOG7pyBs4buxYyBuw6B5IG1hbmcgbOG6oWkgODAlIGRvYW5oIHRodSBuw6puIFJhaW5ib3cgU3RvcmUgbsOqbiB04bqtcCB0cnVuZyBjw7RuZyB0w6FjIGjhuq11IGPhuqduIC0ga2hvIGLDo2kgLSBk4buxIHRy4buvIChMb2dpc3RpYykgY2hvIG5ow7NtIGjDoG5nIGjDs2EgbsOgeS4gVsOtIGThu6UsIHJpw6puZyBtw6MgaMOgbmcgMTAwNTMzNjEgxJHDoyB04bqhbyByYSA2LjUyJSBkb2FuaCB0aHUgY2hvIGPhu61hIGjDoG5nLiANCg0KDQojIEFwcHJvYWNoIHRvIE1vZGVsbGluZw0KDQpOZ8aw4budaSB0aOG7sWMgaGnhu4duIGThu7Egw6FuIG7DoHkgxJHhu4EgeHXhuqV0IHPhu60gZOG7pW5nIGPDoWMgRmVhdHVyZXMgY8OzIHTDqm4gbMOgIFIsIEYgdsOgIE0gKGPDsyB0aOG7gyBn4buNaSBsw6AgYmnhur9uIMSR4bqndSB2w6BvIElucHV0cykgbcO0IHThuqMgaMOgbmggdmkgdGnDqnUgZMO5bmcgY+G7p2Ega2jDoWNoIGjDoG5nIHbhu5tpIGPDoWMgxJHhu4tuaCBuZ2jEqWEgbmjGsCBzYXU6IA0KDQoNCi0gKipSIChSZWNlbmN5KSoqIGzDoCBraG/huqNuZyB0aOG7nWkgZ2lhbiBn4bqnbiDEkcOieSBuaOG6pXQgbcOgIGtow6FjaCBow6BuZyBtdWEgaMOgbmcgaG/hurdjIHPhu60gZOG7pW5nIGThu4tjaCB24bulIChob+G6t2MgbXVhIGjDoG5nKS4NCi0gKipGIChGcmVxdWVuY3kpKiogIGzDoCB04bqnbiBzdeG6pXQgbXVhIGjDoG5nL3Phu60gZOG7pW5nIGThu4tjaCB24bulLg0KLSAqKk0gKE1vbmV0YXJ5KSoqIGzDoCBz4buRIHRp4buBbiBtw6Aga2jDoWNoIGjDoG5nIG11YSBow6BuZyBow7NhL2Thu4tjaCB24bulLg0KDQpGIHbDoCBNIGzDoCBuaOG7r25nIGlucHV0cyDEkcaw4bujYyB0w61uaCB0cm9uZyBt4buZdCBraG/huqNuZyB0aOG7nWkgZ2lhbiBraOG6o28gc8OhdCBuaOG6pXQgxJHhu4tuaCAoMSBuxINtLCBt4buZdCB0aMOhbmcsIGhv4bq3YyAxIHF1w70gaG/hurdjIGzDoCA1IHRow6FuZyBuaMawIHTDrG5oIGh14buRbmcgY+G7p2EgUHJvamVjdCBuw6B5KS4gUmnDqm5nIFIgdGjDrCBwaOG7pSB0aHXhu5ljIHbDoG8gbeG7kWMgdGjhu51pIGdpYW4gbOG7sWEgY2jhu41uIGPhu6dhIG5nxrDhu51pIGzDoG0gbcO0IGjDrG5oIHbDoCBraMO0bmcg4bqjbmggaMaw4bufbmcgxJHhur9uIGvhur90IHF14bqjIGPhu6dhIG3DtCBow6xuaC4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlcyBjaG8gdMOtbmggdG/DoW4gUiwgRiB2w6AgTSDEkeG7k25nIHRo4budaSB04bqhbyByYSBt4buZdCBj4buZdCBiaeG6v24gbeG7m2kgY8OzIHTDqm4gKipCdXlOZXh0TW9udGgqKiB24bubaSBtw7QgdOG6oyBj4bulIHRo4buDIG5oxrAgc2F1OiAqbuG6v3UgbeG7mXQgbcOjIGtow6FjaCBow6BuZywgdsOtIGThu6UsIGPDsyB0aOG7sWMgaGnhu4duIG11YSBow6BuZyB0cm9uZyAyIHRow6FuZyBGZWIgKyBNYXIgdsOgIGPFqW5nIGPDsyB0aOG7sWMgaGnhu4duIGdpYW8gZOG7i2NoIHRyb25nIHRow6FuZyBr4bq/IHRp4bq/cCBsw6AgQXByIHRow6wga2jDoWNoIGjDoG5nIG7DoHkgc+G6vSDEkcaw4bujYyBnaGkgbmjhuq1uIGzDoCBZZXMg4bufIGPhu5l0IGJp4bq/biBCdXlOZXh0TW9udGggdsOgIG7hur91IGtow6FjaCBow6BuZyBuw6B5IGtow7RuZyBjw7MgYuG6pXQga8OsIGdpYW8gZOG7i2NoIG7DoG8g4bufIHRow6FuZyBBcHIgdGjDrCDEkcaw4bujYyBuZ2hpIG5o4bqtbiBsw6AgTm8qLiANCg0KDQoNCmBgYHtyfQ0KDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgICBFeHRyYWN0IGPDoWMgRmVhdHVyZXMgKFJGTSBJbnB1dHMpIHbDoCBTcGxpdCBEYXRhDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0KIyBIw6BtIHRo4buxYyBoaeG7h24gZXh0cmFjdCByYSBjw6FjIGlucHV0cyBSRk06IA0KDQpwcm9jZXNpbmdfUkZNZGF0YSA8LSBmdW5jdGlvbihtb250aF90cmFpbmluZywgbW9udGhfbG9va2JhY2spIHsNCiAgDQogICMgRGF0YSBz4butIGThu6VuZyDEkeG7gyBodeG6pW4gbHV54buHbiBtw7QgaMOsbmg6IA0KICB0cmFpbl9kZiA8LSBteV9kZiAlPiUgDQogICAgZmlsdGVyKHRpbWVfbW9uICVpbiUgbW9udGhfdHJhaW5pbmcpDQogIA0KICAjIERhdGEgZMO5bmcgxJHhu4Mga2nhu4NtIHRyYSBraMOhY2ggaMOgbmcgc+G6vSBtdWEg4bufIHRow6FuZyBr4bq/IHRp4bq/cCBoYXkga2jDtG5nOiANCiAgbG9va2JhY2tfZGYgPC0gbXlfZGYgJT4lIA0KICAgIGZpbHRlcih0aW1lX21vbiA9PSBtb250aF9sb29rYmFjaykNCiAgDQogICMgQ8OhYyBraMOhY2ggaMOgbmcgdHJvbmcgMiB0aMOhbmcgbGnDqm4gdGnhur9wOiANCiAgY3VzdG9tZXJfMm1vbnRocyA8LSB0cmFpbl9kZiRDdXN0b21lcklEICU+JSB1bmlxdWUoKQ0KICANCiAgIyBDw6FjIGtow6FjaCBow6BuZyBjw7MgbeG6t3Qg4bufIHRow6FuZyBr4bq/IHRp4bq/cDogDQogIGN1c3RvbWVyX25leHRfbW9udGggPC0gbG9va2JhY2tfZGYkQ3VzdG9tZXJJRCAlPiUgdW5pcXVlKCkNCiAgDQogICMgVMOtbmggTTogDQogIHRyYWluX2RmICU+JSANCiAgICBncm91cF9ieShDdXN0b21lcklEKSAlPiUgDQogICAgc3VtbWFyaXNlKG1vbmV5ID0gc3VtKG1vbmV5KSkgJT4lIA0KICAgIHVuZ3JvdXAoKSAtPiBkZl90cmFpbl9tDQogIA0KICAjIFTDrW5oIEY6IA0KICB0cmFpbl9kZiAlPiUgDQogICAgZ3JvdXBfYnkoQ3VzdG9tZXJJRCkgJT4lIA0KICAgIGNvdW50KCkgJT4lIA0KICAgIHVuZ3JvdXAoKSAlPiUgDQogICAgcmVuYW1lKGZyZXEgPSBuKSAtPiBkZl90cmFpbl9mDQogIA0KICAjIFTDrW5oIFI6IA0KICANCiAgbm93X3RpbWUgPC0gbWF4KHRyYWluX2RmJHRpbWVfeW1kKQ0KICB5IDwtIGFzLm51bWVyaWMobm93X3RpbWUgLSB0cmFpbl9kZiR0aW1lX3ltZCkNCiAgDQogIHRyYWluX2RmICU+JSANCiAgICBtdXRhdGUocmVjZW5jeSA9IHkpICU+JSANCiAgICBncm91cF9ieShDdXN0b21lcklEKSAlPiUgDQogICAgc3VtbWFyaXNlKHJlY2VuY3kgPSBtaW4ocmVjZW5jeSkpICU+JSANCiAgICB1bmdyb3VwKCkgLT4gZGZfdHJhaW5fcg0KICANCiAgIyBKb2luIFJGTTogDQogIGRmX21vZGVsbGluZyA8LSBkZl90cmFpbl9mICU+JSANCiAgICBmdWxsX2pvaW4oZGZfdHJhaW5fbSwgYnkgPSAiQ3VzdG9tZXJJRCIpICU+JSANCiAgICBmdWxsX2pvaW4oZGZfdHJhaW5fciwgYnkgPSAiQ3VzdG9tZXJJRCIpICU+JSANCiAgICBtdXRhdGUoQnV5TmV4dE1vbnRoID0gY2FzZV93aGVuKEN1c3RvbWVySUQgJWluJSBjdXN0b21lcl9uZXh0X21vbnRoIH4gIlllcyIsIFRSVUUgfiAiTm8iKSkgJT4lIA0KICAgIG11dGF0ZShCdXlOZXh0TW9udGggPSBhcy5mYWN0b3IoQnV5TmV4dE1vbnRoKSkNCiAgDQogIHJldHVybihkZl9tb2RlbGxpbmcgKQ0KICANCiAgfQ0KDQpgYGANCg0KQ2jDum5nIHRhIHPhu60gZOG7pW5nIFJGTSBleHRyYWN0IHJhIHThu6sgaGFpIHRow6FuZyBGZWIgdsOgIE1hciBsw6AgSW5wdXRzOiANCg0KDQpgYGB7cn0NCiMgU+G7rSBk4bulbmcgaMOgbSDEkeG7gyBleHRyYWN0IHJhIFJGTTogDQptb250aF90cmFpbmluZzIzIDwtIGMoIkZlYiIsICJNYXIiKQ0KcHJvY2VzaW5nX1JGTWRhdGEobW9udGhfdHJhaW5pbmcgPSBtb250aF90cmFpbmluZzIzLCBtb250aF9sb29rYmFjayA9IGMoIkFwciIpKSAtPiBkZl9tb2RlbGxpbmcNCg0KIyBYZW0gcXVhIGThu68gbGnhu4d1OiANCmRmX21vZGVsbGluZyAlPiUgDQogIGhlYWQoKSAlPiUgDQogIGthYmxlKCkNCmBgYA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHPhu60gZOG7pW5nIG5oaeG7gXUgbcO0IGjDrG5oIHBow6JuIGxv4bqhaSDEkeG7gyBk4buxIGLDoW8gaMOgbmggaMOgbmggdmkgY+G7p2Ega2jDoWNoIGjDoG5nIGvhu4MgY+G6oyBjw6FjaCB0aeG6v3AgY+G6rW4gY+G7p2EgdGjhu5FuZyBrw6ogdHJ1eeG7gW4gdGjhu5FuZyBsw6AgTG9naXN0aWMgY2hvIMSR4bq/biBjw6FjIGPDoWNoIHRp4bq/cCBj4bqtbiBoaeG7h24gxJHhuqFpIGjGoW4gY+G7p2EgTWFjaGluZSBMZWFybmluZy4gxJDhu4MgbOG7sWEgY2jhu41uIG3hu5l0IG3DtCBow6xuaCBjw7MgY2jhuqV0IGzGsOG7o25nIGThu7EgYsOhbyB04buRdCBuaOG6pXQgbcOgICoqcHJlZGljdHMgd2hpY2ggY3VzdG9tZXJzIG1ha2UgYXQgbGVhc3QgMSBwdXJjaGFzZSBpbiBhIGdpdmVuIG1vbnRoKiogdGjDrCBoxrDhu5tuZyB0aeG6v3AgY+G6rW4gxJHhu4EgeHXhuqV0IG5oxrAgc2F1OiANCg0KLSBIdeG6pW4gbHV54buHbiAxNSBtw7QgaMOsbmggcGjDom4gbG/huqFpIHRyw6puIGPDuW5nIDE1IG3huqt1IHZhbGlkYXRpb24gZGF0YSBy4buTaSB0w61uaCB0b8OhbiB0cnVuZyBiw6xuaCBTZW5zaXRpdml0eSB0xrDGoW5nIOG7qW5nLiANCi0gU2Vuc2l0aXZpdHkgKGhheSBjw7JuIGfhu41pIGzDoCBSZWNhbGwpIMSRxrDhu6NjIGzhu7FhIGNo4buNbiBsw6BtIHRpw6p1IGNow60gxJHDoW5oIGdpw6EgdsOgIGzhu7FhIGNo4buNbiBtw7QgaMOsbmggdsOsIHRpw6p1IGNow60gbsOgeSBjaG8gYmnhur9uICp04buJIGzhu4cgZOG7sSBiw6FvIMSRw7puZyBuaOG7r25nIGtow6FjaCBow6BuZyBz4bq9IG11YSBow6BuZyB0cm9uZyB0aMOhbmcgdOG7m2kqLiBDaGkgdGnhur90IHbDoCBnaeG6o2kgdGjDrWNoIGNoaSB0aeG6v3QgduG7gSB0acOqdSBjaMOtICBuw6B5IMSRxrDhu6NjIHRyw6xuaCBiw6B5IFvhu58gxJHDonldKGh0dHBzOi8vcnB1YnMuY29tL2NoaWR1bmdrdC80NDc5ODkpIHbDoCBb4bufIMSRw6J5XShodHRwczovL2dpdGh1Yi5jb20vQ2hpRHVuZ05ndXllbi9DaGFwdGVyM19UaWV1X0NoaV9EYW5oX0dpYS9ibG9iL21hc3Rlci9DaGFwdGVyM19QZXJmb3JtYW5jZV9NZXRyaWNzLmlweW5iKS4gDQotIE3DtCBow6xuaCBuw6BvIGPDsyBTZW5zaXRpdml0eSBz4bq9IMSRxrDhu6NjIGzhu7FhIGNo4buNbiBjaG8gZOG7sSBiw6FvIGjDoG5oIHZpIHRpw6p1IGTDuW5nIGPhu6dhIGtow6FjaCBow6BuZyB0aMOhbmcga+G6vyB0aeG6v3AuIA0KDQpCxrDhu5tjIG7DoHkgxJHDs25nIHZhaSB0csOyIGzDoCB0aMSDbSBkw7IgLSB0w6xtIGtp4bq/bSBzxqEgYuG7mSBtw7QgaMOsbmggdOG7kXQgbmjhuqV0IGPDsyB0aOG7gy4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlcyBjaG8gdmnhu4djIGh14bqlbiBsdXnhu4duIHbDoCBzbyBzw6FuaCBjaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIG3DtCBow6xuaCBk4buxYSB0csOqbiBTZW5zaXRpdml0eTogDQoNCmBgYHtyfQ0KIyBTY2FsaW5nIDAtMSBjaG8gZOG7ryBsaeG7h3U6IA0KZGZfZm9yTUwgPC0gZGZfbW9kZWxsaW5nICU+JSANCiAgc2VsZWN0KC0gQ3VzdG9tZXJJRCkgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkgeyh4IC0gbWluKHgpKSAvIChtYXgoeCkgLSBtaW4oeCkpfSkNCg0KIyBDaGlhIGThu68gbGnhu4d1IHRoZW8gdOG7iSBs4buHIDgwIC0gMjA6DQoNCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgxKQ0KaWQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihkZl9mb3JNTCRCdXlOZXh0TW9udGgsIHAgPSAwLjgsIGxpc3QgPSBGQUxTRSkNCmRmX3RyYWluIDwtIGRmX2Zvck1MW2lkLCBdDQpkZl90ZXN0IDwtIGRmX2Zvck1MWy1pZCwgXQ0KDQojIFRoaeG6v3QgbOG6rXAgY8OhYyDEkWnhu4F1IGtp4buHbiB0aW5oIGNo4buJbmggdsOgIHNvIHPDoW5oOiANCg0Kc2V0LnNlZWQoMSkNCm51bWJlciA8LSA1DQpyZXBlYXRzIDwtIDMNCg0KY29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IG51bWJlciAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IHJlcGVhdHMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icyA9IFRSVUUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZVByZWRpY3Rpb25zID0gImZpbmFsIiwgDQogICAgICAgICAgICAgICAgICAgICAgICBpbmRleCA9IGNyZWF0ZVJlc2FtcGxlKGRmX2Zvck1MJEJ1eU5leHRNb250aCwgcmVwZWF0cypudW1iZXIpLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSwgDQogICAgICAgICAgICAgICAgICAgICAgICBhbGxvd1BhcmFsbGVsID0gVFJVRSkNCg0KIyBT4butIGThu6VuZyB0w61uaCB0b8OhbiBzb25nIHNvbmcgbmjhurFtIHRp4bq/dCBraeG7h20gdGjhu51pIGdpYW4gdHJhaW5pbmc6IA0KbGlicmFyeShkb1BhcmFsbGVsKQ0KcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzID0gZGV0ZWN0Q29yZXMoKSAtIDEpDQoNCiMgMTUgbW9kZWxzIMSRxrDhu6NjIGzhu7FhIGNo4buNbjogDQoNCm15X21vZGVscyA8LSBjKCJhZGFib29zdCIsICJ4Z2JUcmVlIiwgInN2bVJhZGlhbCIsIA0KICAgICAgICAgICAgICAgImtubiIsICAiZ2JtIiwgIkM1LjAiLCAicmFuZ2VyIiwNCiAgICAgICAgICAgICAgICJyZiIsICJubmV0IiwgImdsbSIsICJsZGEiLCAidHJlZWJhZyIsIA0KICAgICAgICAgICAgICAgImJhZ0ZEQSIsICJnbG1ib29zdCIsICJjZm9yZXN0IikNCg0KIyBIdeG6pW4gbHV54buHbiBtw7QgaMOsbmg6IA0KDQpsaWJyYXJ5KGNhcmV0RW5zZW1ibGUpDQpzZXQuc2VlZCgxKQ0Kc3lzdGVtLnRpbWUobW9kZWxfbGlzdDEgPC0gY2FyZXRMaXN0KEJ1eU5leHRNb250aCB+LiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGRmX2Zvck1MLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0cmljID0gIlJPQyIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZExpc3QgPSBteV9tb2RlbHMpKQ0KDQoNCiMgS2hhaSB0aMOhYyBjw6FjIGvhur90IHF14bqjIGPhu6dhIHZp4buHYyBodeG6pW4gbHV54buHbiAxNSBtw7QgaMOsbmg6IA0KbGlzdF9vZl9yZXN1bHRzIDwtIGxhcHBseShteV9tb2RlbHMsIGZ1bmN0aW9uKHgpIHttb2RlbF9saXN0MVtbeF1dJHJlc2FtcGxlfSkNCg0KIyBDaHV54buDbiBow7NhIHbhu4EgZGF0YSBmcmFtZTogDQoNCnRvdGFsX2RmIDwtIGRvLmNhbGwoImJpbmRfcm93cyIsIGxpc3Rfb2ZfcmVzdWx0cykNCnRvdGFsX2RmICU+JSBtdXRhdGUoTW9kZWwgPSBsYXBwbHkobXlfbW9kZWxzLCBmdW5jdGlvbih4KSB7cmVwKHgsIG51bWJlcipyZXBlYXRzKX0pICU+JSB1bmxpc3QoKSkgLT4gdG90YWxfZGYNCg0KIyBUw61uaCB0cnVuZyBiw6xuaCBj4bunYSBTZW5zaXRpdml0eSwgQVVDLCB2w6AgU3BlY2lmaWNpdHk6IA0KdG90YWxfZGYgJT4lIA0KICBkcGx5cjo6c2VsZWN0KC1SZXNhbXBsZSkgJT4lIA0KICBncm91cF9ieShNb2RlbCkgJT4lIA0KICBzdW1tYXJpc2UoYXZnX2F1YyA9IG1lYW4oUk9DKSwgYXZnX3NlbiA9IG1lYW4oU2VucyksIGF2Z19zcGVjID0gbWVhbihTcGVjKSkgJT4lIA0KICB1bmdyb3VwKCkgLT4gZGZfcmVzdWx0cw0KDQoNCiMgSMOgbSBow6xuaCDhuqNuaCBow7NhIGNo4bqldCBsxrDhu6NuZyBwaMOibiBsb+G6oWkgY+G7p2EgY8OhYyBtw7QgaMOsbmg6IA0KDQoNCm15X2JhciA8LSBmdW5jdGlvbihtZXRyaWNfbmFtZSkgew0KICANCiAgbWV0cmljX25hbWUgPC0gbm9xdW90ZShtZXRyaWNfbmFtZSkNCiAgbXlfY29sb3JzIDwtIGMoIiMzRTYwNkYiKQ0KICANCiAgZGZfcmVzdWx0cyAlPiUgDQogICAgZHBseXI6OnNlbGVjdChNb2RlbCwgbWV0cmljX25hbWUpIC0+IGRmDQogIA0KICBuYW1lcyhkZikgPC0gYygiTW9kZWwiLCAidmFsdWUiKQ0KICANCiAgZGYgJT4lIA0KICAgIGFycmFuZ2UodmFsdWUpICU+JQ0KICAgIG11dGF0ZShNb2RlbCA9IGZhY3RvcihNb2RlbCwgbGV2ZWxzID0gTW9kZWwpKSAlPiUNCiAgICBtdXRhdGUobGFiZWwgPSBwYXN0ZTAocm91bmQoMTAwKnZhbHVlLCAxKSwgIiUiKSkgLT4gbQ0KICANCiAgbSAlPiUgDQogICAgZ2dwbG90KGFlcyhNb2RlbCwgdmFsdWUpKSArDQogICAgZ2VvbV9jb2woZmlsbCA9IG15X2NvbG9ycywgY29sb3IgPSBteV9jb2xvcnMpICsNCiAgICBjb29yZF9mbGlwKCkgKw0KICAgIGdlb21fdGV4dChkYXRhID0gbSwgYWVzKGxhYmVsID0gbGFiZWwpLCBoanVzdCA9IDEuMSwgY29sb3IgPSAid2hpdGUiLCBzaXplID0gNCkgKyANCiAgICB0aGVtZV9mdF9yYygpICsgDQogICAgdGhlbWUocGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgICB0aGVtZShheGlzLnRleHQueSA9IGVsZW1lbnRfdGV4dChjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSAxMikpICsgDQogICAgc2NhbGVfeV9kaXNjcmV0ZShleHBhbmQgPSBjKDAuMDEsIDApKSArIA0KICAgIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMKQ0KfQ0KDQojIENo4bqldCBsxrDhu6NuZyBwaMOibiBsb+G6oWkgY+G7p2EgY8OhYyBtw7QgaMOsbmggdGhlbyBjaGnhu4F1IGdp4bqjbSBk4bqnbiBj4bunYSBjw6FjIGNo4buJIHPhu5E6IA0KZ3JpZEV4dHJhOjpncmlkLmFycmFuZ2UobXlfYmFyKCJhdmdfYXVjIikgKyBsYWJzKHRpdGxlID0gIkFVQy9ST0MiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICBteV9iYXIoImF2Z19zZW4iKSArIGxhYnModGl0bGUgPSAiU2Vuc2l0aXZpdHkiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICBteV9iYXIoImF2Z19zcGVjIikgKyBsYWJzKHRpdGxlID0gIlNwZWNpZmljaXR5IiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgbnJvdyA9IDEsIHBhZGRpbmcgPSB1bml0KDk5LCAibGluZSIpKQ0KYGBgDQoNCg0KS+G6v3QgcXXhuqMgdHLDqm4gY2jhu4kgcmEgcuG6sW5nIE5ldXJhbCBOZXR3b3JrIChubmV0KSBsw6AgbcO0IGjDrG5oIE1MIGPDsyBTZW5zaXRpdml0eSBs4bubbiBuaOG6pXQ6IHRydW5nIGLDrG5oIGzDoCA4MS42JS4gVHV5IG5oacOqbiBu4bq/dSBsxrB1IMO9IHRow6ptIHRpw6p1IGNodeG6qW4gZGnhu4duIHTDrWNoIG7hurFtIGTGsOG7m2kgxJHGsOG7nW5nIGNvbmcgUk9DIHRow6wgR0JNIChHcmFkaWVudCBCb29zdGluZyBNYWNoaW5lcykgbOG6oWkgbMOgIG3DtCBow6xuaCB0b8OgbiBkaeG7h24gaMahbiBj4bqjLiANCg0KRG8gduG6rXkgbcO0IGjDrG5oIGtp4bq/biBuZ2jhu4sgxJHGsOG7o2MgbOG7sWEgY2jhu41uIMSR4buDIGThu7EgYsOhbyBt4buZdCBraMOhY2ggaMOgbmcgImPDsyBtdWEgw610IG5o4bqldCBt4buZdCBpdGVtIHRyb25nIHRow6FuZyB04bubaSBoYXkga2jDtG5nPyIgc+G6vSBsw6AgR0JNLiBExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzIGh14bqlbiBsdXnhu4duIEdCTSB0csOqbiB0cmFpbiBkYXRhIHLhu5NpIMSRw6FuaCBnacOhIGzhuqFpIGNo4bqldCBsxrDhu6NuZyBk4buxIGLDoW8gY+G7p2EgbcO0IGjDrG5oIHRyw6puIHRlc3QgZGF0YTogDQoNCg0KYGBge3J9DQojIFRoaeG6v3QgbOG6rXAgY8OhYyDEkWnhu4F1IGtp4buHbiB0aW5oIGNo4buJbmg6IA0Kc2V0LnNlZWQoMSkNCmNvbnRyb2xfbWwgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSBudW1iZXIgLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSByZXBlYXRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzUHJvYnMgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhbGxvd1BhcmFsbGVsID0gVFJVRSkNCg0KIyBIdeG6pW4gbHV54buHbiBHQk06IA0Kc2V0LnNlZWQoMjkpDQpnYm1fZGVmYXVsdCA8LSB0cmFpbihCdXlOZXh0TW9udGggfi4sIA0KICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gImdibSIsDQogICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW4sDQogICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjb250cm9sX21sLA0KICAgICAgICAgICAgICAgICAgICAgbWV0cmljID0gIlJPQyIpDQoNCg0KIyBDaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIEdCTTogDQpwcmVkIDwtIHByZWRpY3QoZ2JtX2RlZmF1bHQsIGRmX3Rlc3QpDQphY3R1YWwgPC0gZGZfdGVzdCRCdXlOZXh0TW9udGgNCmNvbmZ1c2lvbk1hdHJpeChwcmVkLCBhY3R1YWwsIHBvc2l0aXZlID0gIlllcyIpDQoNCmBgYA0KDQpUaOG7sWMgdOG6vyBjw7MgMTMxMCBraMOhY2ggbXVhIGjDoG5nIHRyb25nIHRow6FuZyBBcHIgdsOgIEdCTSBk4buxIGLDoW8gxJHDum5nIHPhur0gY8OzIDgzMyBuZ8aw4budaSAodHJvbmcgdOG7lW5nIHPhu5EgMTMxMCkgc+G6vSBtdWEgaMOgbmcuIE7Ds2kgY8OhY2gga2jDoWMgU2Vuc2l0aXZpdHkgbMOgIDYzLjU5JSAoYuG6sW5nIDgzMyAvICg4MzMgKyA0NzcpKSBuaMawIHRhIGPDsyB0aOG7gyB0aOG6pXkg4bufIG1hIHRy4bqtbiBuaOG6p20gbOG6q24uIENo4buJIHPhu5EgxJHDoW5oIGdpw6EgY2jhuqV0IGzGsOG7o25nIGtow6FjIGPhu6dhIG3DtCBow6xuaCBjaMO6bmcgdGEgcXVhbiB0w6JtIGPDsyB0aOG7gyBsw6AgQWNjdXJhY3kgPSA3Mi4yMyUuIA0KDQpDaMO6bmcgdGEgY8WpbmcgY8OzIHRo4buDIG1pbmggaOG7jWEgUk9DIHbDoCB0w61uaCBkaeG7h24gdMOtY2ggbuG6sW0gZMaw4bubaSDEkcaw4budbmcgY29uZyBBVUM6IA0KDQpgYGB7cn0NCiMgSMOgbSB0w61uaCBBVUMvUk9DOiANCg0KYXVjX2Zvcl90ZXN0IDwtIGZ1bmN0aW9uKHBkX3NlbGVjdGVkKSB7DQogIHJldHVybihwUk9DOjpyb2MoYWN0dWFsLCBwZF9zZWxlY3RlZCkpDQp9DQoNCg0KIyBIw6BtIGjDrG5oIOG6o25oIGjDs2EgQVVDL1JPQyBjdXJ2ZToNCm15X1JPQ19jdXJ2ZSA8LSBmdW5jdGlvbihhdWNfb2JqZWN0KSB7DQogIA0KICBzZW5fc3BlY19kZiA8LSBkYXRhX2ZyYW1lKFRQUiA9IGF1Y19vYmplY3Qkc2Vuc2l0aXZpdGllcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgRlBSID0gMSAtIGF1Y19vYmplY3Qkc3BlY2lmaWNpdGllcykNCiAgDQogIHNlbl9zcGVjX2RmICU+JSANCiAgICBnZ3Bsb3QoYWVzKHggPSBGUFIsIHltaW4gPSAwLCB5bWF4ID0gVFBSKSkrDQogICAgZ2VvbV9wb2x5Z29uKGFlcyh5ID0gVFBSKSwgZmlsbCA9ICJyZWQiLCBhbHBoYSA9IDAuMykrDQogICAgZ2VvbV9wYXRoKGFlcyh5ID0gVFBSKSwgY29sID0gImZpcmVicmljayIsIHNpemUgPSAxLjIpICsNCiAgICBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yID0gImdyYXkzNyIsIHNpemUgPSAxLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArDQogICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKyANCiAgICBzY2FsZV94X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArIA0KICAgIHRoZW1lX2J3KCkgKw0KICAgIGNvb3JkX2VxdWFsKCkgJT4lIA0KICAgIHJldHVybigpDQp9DQoNCg0KIyBYw6FjIHN14bqldCBk4buxIGLDoW8gY2hvIHPhu7Ega2nhu4duICJraMOhY2ggaMOgbmcgc+G6vSBtdWEgaMOgbmcgdHJvbmcgdGjDoW5nIGvhur8gdGnhur9wIjogDQoNCnBkX3ByZWQgPC0gcHJlZGljdChnYm1fZGVmYXVsdCwgZGZfdGVzdCwgdHlwZSA9ICJwcm9iIikgJT4lIHB1bGwoWWVzKSANCm15X2F1YyA8LSBhdWNfZm9yX3Rlc3QocGRfcHJlZCkNCg0KIyBT4butIGThu6VuZyBow6BtOiANCm15X2F1YyAlPiUgDQogIG15X1JPQ19jdXJ2ZSgpICsgDQogIGxhYnMoeCA9ICJGUFIgKDEgLSBTcGVjaWZpY2l0eSkiLCANCiAgICAgICB5ID0gIlRQUiAoU2Vuc2l0aXZpdHkpIiwgDQogICAgICAgdGl0bGUgPSAiRmlndXJlIDM6IE1vZGVsIFBlcmZvcm1hbmNlIEJhc2VkIG9uIFRlc3QgRGF0YSIsIA0KICAgICAgIHN1YnRpdGxlID0gcGFzdGUwKCJBVUMgVmFsdWUgZm9yIEdCTSBNb2RlbDogIiwgbXlfYXVjJGF1YyAlPiUgcm91bmQoMykpKQ0KYGBgDQoNClRyb25nIGjhuqd1IGjhur90IGPDoWMg4bupbmcgZOG7pW5nIHRow6wgUk9DL0FVQyB0csOqbiA2NSUgbMOgIG3DtCBow6xuaCBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgxJHGsOG7o2MuIFRyb25nIHTDrG5oIGh14buRbmcgY+G7p2EgY2jDum5nIHRhIHRow6wgR0JNIGPDsyBST0MvQVVDID0gNzcuNjAlIHRyw6puIHRlc3QgZGF0YSBjxaluZyBsw6AgbeG7mXQga+G6v3QgcXXhuqMga2jDoSBjYW8uIA0KDQojIERpc2N1c3Npb24gUHJvYmxlbQ0KDQpExrDhu5tpIMSRw6J5IGzDoCBt4buZdCBz4buRIHbhuqVuIMSR4buBIGNoxrBhIMSRxrDhu6NjIGdp4bqjaSBxdXnhur90OiANCg0KLSBUaOG7qSBuaOG6pXQsIG5o4buvbmcga+G6v3QgcXXhuqMgbcOgIGNow7puZyB0YSB0aHUgxJHGsOG7o2MgdOG7qyB2aeG7h2Mgc+G7rSBk4bulbmcgbcO0IGjDrG5oIEdCTSB0csOqbiB0ZXN0IGRhdGEgdGjDrCDEkcOzIG3hu5tpIGNo4buJIGzDoCBHQk0gbeG6t2MgxJHhu4tuaCBjaMawYSB0aW5oIGNo4buJbmggdGhhbSBz4buRLiBC4bqxbmcgdmnhu4djIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdOG7kWkgxrB1IGNobyBHQk0gY2jDum5nIHRhIGPDsyB0aOG7gyDEkeG6oXQga+G6v3QgcXXhuqMgY2jDrW5oIHjDoWMgaMahbiBraGkgZOG7sSBiw6FvIGPDsyBoYXkga2jDtG5nIG3hu5l0IGtow6FjaCBow6BuZyBz4bq9IG11YSBow6BuZyB0cm9uZyB0aMOhbmcga+G6vyB0aeG6v3AuIFR1eSBuaGnDqm4gdHJvbmcgZ2nhu5tpIGjhuqFuIHRo4budaSBnaWFuIDhoIHRow6wgdmnhu4djIHRpbmggY2jhu4luaCBHQk0gbMOgIHZp4buHYyBraMO0bmcga2jhuqMgdGjDrCB2w6wgdGjhu51pIGdpYW4gdHJhaW5pbmcgY8OzIHRo4buDIHLhuqV0IGzhu5tuIHbDoCDEkcOyaSBo4buPaSBj4bqldSBow6xuaCBwaOG6p24gY+G7qW5nIG3DoXkgdMOtbmggdMawxqFuZyDEkeG7kWkgY2FvLiBHaeG6o2kgcGjDoXAga2jDoWMgbMOgIGzhu7FhIGNo4buNbiBk4buLY2ggduG7pSB0w61uaCB0b8OhbiDEkcOhbSBtw6J5IGPhu6dhIEFtYXpvbi4gUmnDqm5nIHZp4buHYyBodeG6pW4gbHV54buHbiAxNSBtw7QgaMOsbmgg4bufIHRyw6puIGPFqW5nIMSRw6MgbeG6pXQgZ+G6p24gMmguIA0KDQotIFRo4bupIGhhaSwgQXNzaWdubWVudCBuw6B5IGtow7RuZyDEkcawYSByYSBt4buZdCBt4bulYyB0acOqdSBj4bulIHRo4buDIG7DoG8gY2hvIHZp4buHYyB4w6J5IGThu7FuZyBtw7QgaMOsbmguIEPDoWMgbcO0IGjDrG5oIE1MIGPDsyB0aOG7gyDEkcaw4bujYyB0aW5oIGNo4buJbmggKHbDoCBs4buxYSBjaOG7jW4pIHRoZW8gbeG7mXQgdGnDqnUgY2jDrSBuw6BvIMSRw7MgbmjGsCBBY2N1cmFjeSwgUmVjYWxsIGhheSBST0MvQVVDIG5oxrBuZyBBc3NpZ25tZW50IG7DoHkgdGjDrCBraMO0bmcgbsOqdSByw7UgY+G7pSB0aOG7gy4gRG8gduG6rXksIG5nxrDhu51pIHjDonkgZOG7sW5nIG3DtCBow6xuaCBuw6B5IHThu7EgxJHhurd0IHJhIG3hu6VjIHRpw6p1IGPhu6dhIHZp4buHYyB4w6J5IGThu7FuZyBtw7QgaMOsbmgsIHbDrSBk4bulLCBsw6AgKipwaOG6o2kgY292ZXIgxJHGsOG7o2Mgw610IG5o4bqldCA3MCUga2jDoWNoIGjDoG5nIHPhur0gbXVhIGjDoG5nIHRyb25nIHRow6FuZyB04bubaSoqLiBIaeG7h24gdOG6oWkgR0JNIG3hurdjIMSR4buLbmggbeG7m2kgY2jhu4kgY292ZXIgxJHGsOG7o2MgNjMuNTklIGtow6FjaCBow6BuZyBz4bq9IG11YSBow6BuZyB0cm9uZyB0aMOhbmcgdOG7m2kuIE3hu6VjIHRpw6p1IG7DoHkgY8OzIHRo4buDIMSR4bqhdCDEkcaw4bujYyBi4bqxbmcgbeG7mXQgdHJvbmcgaGFpIGPDoWNoOiAoMSkgdGluaCBjaOG7iW5oL3TDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUgY2hvIEdCTSwgaMah4bqhYyAoMikgdGhheSDEkeG7lWkgbmfGsOG7oW5nIHjDoWMgc3XhuqV0IGtoaSBwaMOibiBsb+G6oWkgdsOsIG1hIHRy4bqtbiBuaOG6p20gbOG6q24gbeG6t2MgxJHhu4tuaCBz4butIGThu6VuZyBuZ8aw4buhbmcgMC41IGNobyBwaMOibiBsb+G6oWkuIFbhuqVuIMSR4buBIG7DoHkgxJHDoyDEkcaw4bujYyB0csOsbmggYsOgeSB2w6AgZ2nhuqNpIHF1eeG6v3Qg4bufIFtt4bulYyAzLjNdKGh0dHBzOi8vZ2l0aHViLmNvbS9DaGlEdW5nTmd1eWVuL0NoYXB0ZXIzX1RpZXVfQ2hpX0RhbmhfR2lhL2Jsb2IvbWFzdGVyL0NoYXB0ZXIzX1BlcmZvcm1hbmNlX01ldHJpY3MuaXB5bmIpLiANCg0KLSBWaeG7h2MgdGluaCBjaOG7iW5oIGPDoWMgbcO0IGjDrG5oIE1MIGPDsyB0aOG7gyBy4bqldCBt4bqldCB0aOG7nWkgZ2lhbi4gQ2hp4bq/biBsxrDhu6NjIHRpbmggY2jhu4luaCBraeG7g3UgR3JpZCBTZWFyY2ggc+G6vSBsaeG7h3Qga8OqIHJhIHThuqV0IGPhuqMgY8OhYyBz4buxIGvhur90IGjhu6NwIGPDsyB0aOG7gyBjw7MgY+G7p2EgY8OhYyB0aGFtIHPhu5EgcuG7k2kgdMOsbSBraeG6v20gbeG7mXQgc+G7sSBr4bq/dCBo4bujcCBj4bulIHRo4buDIG7DoG8gxJHDsyBj4bunYSB0aGFtIHPhu5Egc2FvIGNobyBsxrDhu6NuZyBwaMOibiBsb+G6oWkgY+G7p2EgbcO0IGjDrG5oIGzDoCBjYW8gbmjhuqV0LiBOaMawIHbhuq15IG7hur91IGNow7puZyB0YSBs4buxYSBjaOG7jW4gY2jhu4kgNSB0aGFtIHPhu5EgKGNvbiBz4buRIG7DoHkgduG6q24gY8OybiDDrXQgc28gduG7m2kgY8OhYyB0aGFtIHPhu5EgY8OzIHRo4buDIHRpbmggY2jhu4luaCkgdsOgIG3hu5dpIG3hu5l0IHRoYW0gc+G7kSBjaOG7jW4gMTAg4bupbmcgdmnDqm4gdsOgIHPhu60gZOG7pW5nIDUgZm9sZHMgY2hvIENyb3NzLVZhbGlkYXRpb24gdGjDrCB04buVbmcgc+G7kSBjw6FjIG3DtCBow6xuaCBtw6AgbcOheSB0w61uaCBz4bq9IHBo4bqjaSBjaOG6oXkgbMOgIDXDlzEwXjUgPSA1MDAuMDAwIG3DtCBow6xuaCAobuG6v3UgbOG7sWEgY2jhu41uIHJlZml0ID0gRmFsc2UpLiDEkOG7gyBj4bqvdCBnaeG6o20gdGjhu51pIGdpYW4gaHXhuqVuIGx1eeG7h24gY2jDum5nIHRhIGPDsyB0aOG7gyB0aOG7sWMgaGnhu4duIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdGhlbyBraeG7g3UgaMOqbiAtIHh1aSBi4bqxbmcgY8OhY2ggY2jhu4kgY2jhu41uIG5n4bqrdSBuaGnDqm4sIGNo4bqzbmcgaOG6oW4sIDUwMDAwICh04bupYyBsw6AgMTAlIGPhu6dhIDUwMC4wMDApIHPhu7Ega+G6v3QgaOG7o3Aga2jDoWMgbmhhdSBj4bunYSB0aGFtIHPhu5EgxJHhu4MgdGluaCBjaOG7iW5oIG3DtCBow6xuaC4gQ8OhY2ggdGjhu6ljIHRpbmggY2jhu4luaCBow6puIHh1aSBuw6B5IGfhu41pIGzDoCBSYW5kb20gU2VhcmNoLiBDaGnhur9uIGzGsOG7o2MgdGluaCBjaOG7iW5oIG7DoHkgbMOgIGNow7puZyB0YSBjw7MgdGjhu4MgY+G6r3QgZ2nhuqNtIMSRw6FuZyBr4buDIHRo4budaSBnaWFuIHRpbmggY2jhu4luaCB24bubaSBjw6FjIGdpw6EgcGjhuqNpIHRy4bqjIGzDoCBjaMO6bmcgdGEgY8OzIHRo4buDICJi4buPIHPDs3QiIHRoYW0gc+G7kSB04buRdCBuaOG6pXQgY+G7p2EgbcO0IGjDrG5oIE1MLiBUdXkgduG6rXkgdHJvbmcgbeG7mXQgc+G7kSB0w6xuaCBodeG7kW5nIHRow6wgdGluaCBjaOG7iW5oIHRoZW8gUmFuZG9tIFNlYWNoIGPDsyB0aOG7gyB24bqrbiBsw6Aga2jDtG5nIGhp4buHdSBxdeG6oyB2w6AgY2jDum5nIHRhIGPhuqduIG3hu5l0IGNoaeG6v24gbMaw4bujYyB0aW5oIGNo4buJbmggaGnhu4d1IHF14bqjIGjGoW4gbMOgIEJheWVzaWFuIE9wdGltaXphdGlvbiBjw7MgdGjhu4MgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiB0csOqbiBj4bqjIFIgbOG6q24gUHl0aG9uLiBO4bq/dSBz4butIGThu6VuZyBQeXRob24gdGjDrCBCYXllc2lhbiBPcHRpbWl6YXRpb24gY8OzIHRo4buDIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gW25oxrAgc2F1XShodHRwczovL2dpdGh1Yi5jb20vQ2hpRHVuZ05ndXllbi9DaGFwdGVyNl9CYXllc2lhbl9PcHRpbWl6YXRpb24tUmFuZG9tRm9yZXN0LS9ibG9iL21hc3Rlci9DaGFwdGVyNl9CYXllc2lhbl9PcHRpbWl6YXRpb25fRm9yX1R1cm5pbmdfUGFyYW1ldGVycy5pcHluYikuIA0KDQoNCi0gVuG7gSBt4bq3dCBrxKkgdGh14bqtdCwgY8OzIHRo4buDIHPhu60gZOG7pW5nIHRoxrAgdmnhu4duICoqaDJvKiogxJHhu4MgaHXhuqVuIGx1eeG7h24gdsOgIHRpbmggY2jhu4luaCBHQk0uIFZp4buHYyBz4butIGThu6VuZyBoMm8gY2hvIGh14bqlbiBsdXnhu4duIHbDoCB0aW5oIGNo4buJbmggR0JNIChjxaluZyBuaMawIGPDoWMgbcO0IGjDrG5oIE1MIG7Ds2kgY2h1bmcpIGPDsyBt4buZdCB2w6BpIMawdSB0aOG6vzogKDEpIGNo4bqheSB0csOqbiBj4bqjIFIgdsOgIFB5dGhvbiB24bubaSBjw7ogcGjDoXAgdMawxqFuZyB04buxIG5oYXUsICgyKSDEkcaw4bujYyB2aeG6v3QgduG7m2kgbMO1aSBsw6AgbmfDtG4gbmfhu68gSmF2YSBuw6puIHRyaeG7g24ga2hhaSDhu6luZyBk4bulbmcg4bufIGThuqFuZyB3ZWIgc2VydmljZSBsw6AgcuG6pXQgdGnhu4duIGzhu6NpLCAoMykgdGjhu51pIGdpYW4gdHJhaW5pbmcgbcO0IGjDrG5oIHTGsMahbmcgxJHhu5FpIG5oYW5oLiANCg0KDQpU4bqldCBj4bqjIG5o4buvbmcgduG6pW4gxJHhu4EgbsOgeSBj4bqnbiB0aOG7nWkgZ2lhbiAoY8WpbmcgbmjGsCBwaOG6p24gY+G7qW5nIG3DoXkgdMOtbmggdMawxqFuZyDEkeG7kWkgbeG6oW5oKSB2w6Aga2jDtG5nIHRo4buDIGdp4bqjaSBxdXnhur90IMSRxrDhu6NjIHRyb25nIGtob+G6o25nIHRo4budaSBnaWFuIGzDoCA4aC4gDQoNCiMgQXBwbGljYXRpb25zIGluIEJ1c3NpbmVzIENvbnRleHQNCg0KR2nhuqMgc+G7rSBt4bulYyB0acOqdSDEkcaw4bujYyBo4bqhIHh14buRbmcgbMOgICoqbcO0IGjDrG5oIGNvdmVyIMSRxrDhu6NjIMOtdCBuaOG6pXQgNjAlIGtow6FjaCBow6BuZyBz4bq9IG11YSBow6BuZyB0cm9uZyB0aMOhbmcgdOG7m2kgKHThu6ljIGzDoCB0aMOhbmcgQXByKSoqIHRow6wgR0JNIG3hurdjIMSR4buLbmggY+G7p2EgY2jDum5nIHRhIHRo4buPYSBtw6NuLiBO4bq/dSB24bqteSBjaMO6bmcgdGEgc+G6vSBn4butaSBwcm9tb3Rpb25hbCBlLW1haWxzIGNobyA4MzMga2jDoWNoIGjDoG5nIGPDsyBkYW5oIHPDoWNoIGTGsOG7m2kgxJHDonkgKGNo4buJIGxp4buHdCBrw6ogNiBraMOhY2ggaMOgbmcgxJHhuqd1KTogDQoNCg0KYGBge3J9DQpkZl9tb2RlbGxpbmdbLWlkLCBdICU+JSANCiAgbXV0YXRlKEJ1eU5leHRNb250aF9QcmVkaWN0ZWQgPSBhY3R1YWwpICU+JSANCiAgZmlsdGVyKEJ1eU5leHRNb250aF9QcmVkaWN0ZWQgPT0gIlllcyIpICU+JSANCiAgc2VsZWN0KEN1c3RvbWVySUQsIEJ1eU5leHRNb250aF9QcmVkaWN0ZWQpICU+JSANCiAgaGVhZCgpICU+JSANCiAga2FibGUoKQ0KYGBgDQoNCkPDsm4gbuG6v3UgY2jDum5nIHRhIG114buRbiBk4buxIGLDoW8gbmjhu69uZyBraMOhY2ggaMOgbmcgbsOgbyBz4bq9IG11YSBow6BuZyB0cm9uZyB0aMOhbmcgNSAoTWF5KSBk4buxYSB0csOqbiBk4buvIGxp4buHdSBnaWFvIGThu4tjaCBj4bunYSBoYWkgdGjDoW5nIHRyxrDhu5tjIMSRw7MgbMOgIE1hciB2w6AgQXByIHRow6wgdHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhIHRyYWluaW5nIG3DtCBow6xuaCBHQk0gKG3hurdjIMSR4buLbmgpIHRyw6puIGThu68gbGnhu4d1IGPhu6dhIGhhaSB0aMOhbmcgTWFyICsgQXByOiANCg0KDQpgYGB7cn0NCm1vbnRoX3RyYWluaW5nMzQgPC0gYygiTWFyIiwgIkFwciIpDQpwcm9jZXNpbmdfUkZNZGF0YShtb250aF90cmFpbmluZyA9IG1vbnRoX3RyYWluaW5nMjMsIG1vbnRoX2xvb2tiYWNrID0gYygiTWF5IikpIC0+IGRmX21vZGVsbGluZw0KDQpkZl9mb3JNTCA8LSBkZl9tb2RlbGxpbmcgJT4lIA0KICBzZWxlY3QoLSBDdXN0b21lcklEKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7KHggLSBtaW4oeCkpIC8gKG1heCh4KSAtIG1pbih4KSl9KQ0KDQpzZXQuc2VlZCgxKQ0KaWQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihkZl9mb3JNTCRCdXlOZXh0TW9udGgsIHAgPSAwLjgsIGxpc3QgPSBGQUxTRSkNCmRmX3RyYWluIDwtIGRmX2Zvck1MW2lkLCBdDQpkZl90ZXN0IDwtIGRmX2Zvck1MWy1pZCwgXQ0KDQpzZXQuc2VlZCgyOSkNCmdibV9kZWZhdWx0IDwtIHRyYWluKEJ1eU5leHRNb250aCB+LiwgDQogICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiZ2JtIiwNCiAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkZl90cmFpbiwNCiAgICAgICAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2xfbWwsDQogICAgICAgICAgICAgICAgICAgICBtZXRyaWMgPSAiUk9DIikNCg0KYGBgDQoNCkNo4bqldCBsxrDhu6NuZyBk4buxIGLDoW8gY+G7p2EgbcO0IGjDrG5oIHRo4buDIGhp4buHbiBxdWEgbWEgdHLhuq1uIG5o4bqnbSBs4bqrbjogDQoNCmBgYHtyfQ0KIyBDaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIEdCTTogDQpwcmVkIDwtIHByZWRpY3QoZ2JtX2RlZmF1bHQsIGRmX3Rlc3QpDQphY3R1YWwgPC0gZGZfdGVzdCRCdXlOZXh0TW9udGgNCmNvbmZ1c2lvbk1hdHJpeChwcmVkLCBhY3R1YWwsIHBvc2l0aXZlID0gIlllcyIpDQpgYGANCg0KR0JNIGzDumMgbsOgeSBz4bq9IGNvdmVyIMSRw7puZyA2Mi40NSUga2jDoWNoIGjDoG5nIHPhur0gbXVhIGjDoG5nIHbDoG8gdGjDoW5nIE1heSBr4bq/IHRp4bq/cCB2w6AgZMaw4bubaSDEkcOieSBsw6AgZGFuaCBzw6FjaCA3ODAga2jDoWNoIGjDoG5nIMSRw7M6IA0KDQpgYGB7cn0NCmRmX21vZGVsbGluZ1staWQsIF0gJT4lIA0KICBtdXRhdGUoQnV5TmV4dE1vbnRoX1ByZWRpY3RlZCA9IGFjdHVhbCkgJT4lIA0KICBmaWx0ZXIoQnV5TmV4dE1vbnRoX1ByZWRpY3RlZCA9PSAiWWVzIikgJT4lIA0KICBzZWxlY3QoQ3VzdG9tZXJJRCwgQnV5TmV4dE1vbnRoX1ByZWRpY3RlZCkgJT4lIA0KICBoZWFkKCkgJT4lIA0KICBrYWJsZSgpDQpgYGANCg0KDQoNCg0K