1 Giới thiệu

Đây là bài đầu tiên trong series “ggplot2 thực dụng”. Như chúng ta đều biết, ggplot2 là một công cụ đồ họa thống kê rất tuyệt vời trong R. Khác với những software thương mại, ggplot2 không cung cấp những dạng biểu đồ thống kê quy ước, nhưng nó cho phép người dùng tự do diễn đạt ý tưởng của mình thông qua “ngữ pháp đồ thị”.

Tuy nhiên, chính vì tinh thần tự do này, rất hiếm giáo trình ggplot2 thực sự hướng dẫn cho người học cách vận dụng ngữ pháp nói trên để đạt những mục tiêu thực dụng. Đa số giáo trình chỉ mới dừng lại ở việc giải thích cú pháp các functions ggplot2 hoặc bám vào những dạng biểu đồ kinh điển. Ngay cả khi thông thạo ngữ pháp ggplot2, người dùng vẫn bị chi phối bởi thói quen và cách nhìn vấn đề. Thí dụ, khi bạn gặp vấn đề khó, bạn vẫn phải tự suy nghĩ tìm ra giải pháp cho riêng mình.

Một trong những vấn đề này, đó là: Trình bày thông tin khi dataset có quá nhiều cases. Đây là vấn đề “Overplotting”, tức sự chồng lắp của các thành phần hình họa (điểm) khi chúng có tọa độ trùng nhau hay rất gần nhau. Do làm việc trong ngành kỹ thuật y sinh, Nhi thường phải xử lý những dataset với hàng trăm ngàn, thậm chí hàng triệu lines. Những giải pháp mà Nhi sắp chia sẻ sau đây có thể giải quyết hiện tượng chồng lắp và tóm tắt được thông tin về khuynh hướng của dữ liệu.

Trong thí dụ minh họa sau đây, Nhi tạo ra 1 dataset với 2 biến X,Y có phân bố chuẩn, với 99,999 cases.

library(tidyverse)

df=data_frame(X=rnorm(n=99999,mean=20,sd=50),
              Y=rnorm(n=99999,mean=10,sd=40)+X)%>%
  as_tibble()

Đây là một con số khá lớn, nên khi vẽ scatter plot trong ggplot2 biểu đồ sẽ rất rối.

Hình 1: Như bạn thấy: mật độ điểm quá dày và chúng chồng lên nhau một cách hỗn loạn:

p=ggplot(df,aes(X,Y))+ theme_bw()

p+geom_jitter(col="red")

Giải pháp thứ nhất, đó là can thiệp trên chính yếu tố hình họa (điểm), cụ thể là kích thước, độ tương phản, độ trong suốt.

Để tăng độ tương phản và trong suốt, ta có thể dùng những điểm rỗng, tức không có màu nền mà chỉ có màu viền:

Hình 2: Loại bỏ màu nền có thể phân biệt khá nhiều điểm, tuy nhiên với cỡ mẫu quá lớn, cách làm này chưa giải quyết được vấn đề chồng lắp:

p+geom_jitter(col="red",shape=1)

Hình 3:

Cách làm thứ 2, đó là thu nhỏ kích thước mỗi điểm chỉ còn 1 pixel. Biểu đồ sẽ thay đổi theo hướng tăng độ phân giải: TUy nhiên, sự chồng lắp vẫn còn;

p+geom_jitter(col="red",shape=".")

Hình 4: Tiếp theo, ta có thể thử can thiệp cùng lúc lên alpha (độ trong suốt của yếu tố hình họa) và loại bỏ màu nền:

Hiệu ứng của việc giảm alpha là giảm nhiễu, và giữ lại khuynh hướng chính của dữ liệu.

p+geom_jitter(col="red",alpha=0.03,shape=1)

Giải pháp can thiệp lên hình họa dừng lại ở đây, bây giờ ta sẽ thử một cách khác, đó là can thiệp lên yếu tố Thống kê. Cụ thể, ta muốn gom các giá trị gần nhau vào những nhóm đại diện, và biểu diễn tọa độ của các nhóm đại diện này.

Hình 5: geom_bin2d

library(viridis)
## Loading required package: viridisLite
p+geom_bin2d(bins=33,aes(col=..count..),show.legend = F)+
  scale_fill_viridis(begin=0.1,end=0.9,option="A",alpha=0.1)+
  scale_color_viridis(option="B",alpha=0.3)

Hình 6: Hoặc sử dụng geom_hex

p+geom_hex(aes(col=..count..),show.legend = F)+
  scale_fill_viridis(option="A",alpha=0.1,begin=0.1,end=0.9)+
scale_color_viridis(option="B",alpha=0.3)

Hai cách làm trên sẽ gom nhiều điểm lại thành một nhóm, đại diện bằng một ô vuông hay hình lục giác, và tô màu theo mật độ phân bố của các điểm

Ta có thể thay đổi hoàn toàn yếu tố thống kê, thí dụ thể hiện mật độ phân bố 2 chiều thay vì tọa độ giá trị:

Hình 7: 2D Kernel density plot

p+stat_density_2d(aes(fill = ..level..),geom = "polygon",col="black")+
  scale_fill_distiller(palette="Spectral")

Hình 8: Tương tự như trên, Ta có thể sử dụng các nhóm đại diện, nhằm cung cấp cả 2 thông tin về tọa độ và mật độ phân bố.

p+stat_density_2d(aes(fill = ..level..),geom = "polygon",alpha=0.3,show.legend = F)+ 
stat_density_2d(geom = "point", 
                  aes(size = ..density..,fill=..density..),
                  shape=21,n = 18, 
                  contour = FALSE,col="black")+
  scale_fill_distiller(palette="Spectral")

Hình 9: Có bản chất cũng là biểu đồ mật độ phân phối 2 chiều, nhưng có dạng 1 heatmap:

p+stat_density_2d(geom = "raster", 
                  aes(fill = ..density..), 
                  contour = FALSE)+
  scale_x_continuous(expand = c(0,0)) +
  scale_y_continuous(expand = c(0,0)) +
  scale_fill_viridis(option="A",alpha=0.2)

Hình 10: Một heatmap khác đơn giản hơn:

p+stat_density_2d(geom = "raster", 
                  aes(fill = ..density..), 
                  contour = FALSE)+
  scale_x_continuous(expand = c(0,0)) +
  scale_y_continuous(expand = c(0,0)) +
  scale_fill_gradient(low="white",high="red")

Bây giờ ta nhìn vấn đề theo một hướng khác: Giả định nếu ta chỉ quan tâm đến giá trị của 1 biến Y, ta có thể phân nhóm X và sử dụng boxplot, violin plot hay point range như sau:

Hình 11: Phân bố của Y theo X, sau khi phân nhóm X thành 10 đoạn

p+geom_boxplot(aes(group = cut_interval(X, 10),
                   fill=cut_interval(X, 10)),show.legend = F)+
  scale_fill_viridis(option="A",alpha=0.8,discrete = T)

Hình 12: Violin plot

p+geom_violin(aes(group = cut_interval(X, 10),
                   fill=cut_interval(X,10)),show.legend = F)+
  scale_fill_viridis(option="A",alpha=0.8,discrete = T)

Hình 13: Point và error bar

ggplot(df,aes(x=cut_interval(X,10),y=Y))+
               stat_summary(aes(col=cut_interval(X,10)),
                        geom = "pointrange",
                        fun.y = median,
                        fun.ymax = max,
                        fun.ymin=min,
                        size=1)+
                       theme_bw()+
  scale_color_viridis(option="A",discrete = T)+
  scale_x_discrete(breaks=NULL)

Hình 14: Kết hợp scatter dot plot và tóm tắt trung vị của Y, theo 20 phân nhóm của X

ggplot(df,aes(x=cut_interval(X,20),y=Y))+
  geom_jitter(aes(col=Y),alpha=0.05,shape=1)+
  stat_summary(fun.y = "median", 
               geom = "point",
               shape=21,
               size=3,
               fill="white")+
  theme_bw()+
  scale_color_viridis(option="A")+
  scale_x_discrete(breaks=NULL)

Hình 15: Nếu ta đồng thời cắt cả X và Y thành 30 phân nhóm bằng nhau thì kết quả sẽ ra sao ?

ggplot(df,aes(x=cut_interval(X,30),
              y=cut_interval(Y,30)))+
  geom_jitter(aes(col=X),alpha=0.5,shape=".")+
  theme_bw()+
  scale_color_viridis(option="A")+
  scale_x_discrete(breaks=NULL)+
  scale_y_discrete(breaks=NULL)

Hình 16: Cuối cùng, ta có thể vẽ density plot của Y theo từng phân nhóm của X

library(ggridges)

ggplot(df,aes(y=cut_interval(X,15),x=Y))+
  geom_density_ridges(aes(fill=cut_interval(X,15),
                          col=cut_interval(X,15)),
                          scale=4,show.legend = F)+
  theme_bw()+
  scale_y_discrete(breaks=NULL)+
  coord_flip()+
  scale_fill_viridis(option="A",alpha=0.5,discrete = T)+
  scale_color_viridis(option="C",alpha=0.5,discrete = T,begin=1,end=0)
## Picking joint bandwidth of 9.34

Tạm biệt các bạn, hy vọng bài viết giúp các bạn mua vui được vài trống canh.

Hẹn gặp lại các bạn lần tới.

LS0tDQp0aXRsZTogIk92ZXJwbG90dGluZyIgDQphdXRob3I6ICJMw6ogTmfhu41jIEto4bqjIE5oaSINCmRhdGU6ICIyOCBUaMOhbmcgMTIgMjAxNyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJkZWZhdWx0Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmBgYA0KDQohW10ob3ZlcnBsb3QucG5nKQ0KDQojIEdp4bubaSB0aGnhu4d1DQoNCsSQw6J5IGzDoCBiw6BpIMSR4bqndSB0acOqbiB0cm9uZyBzZXJpZXMgImdncGxvdDIgdGjhu7FjIGThu6VuZyIuIE5oxrAgY2jDum5nIHRhIMSR4buBdSBiaeG6v3QsIGdncGxvdDIgbMOgIG3hu5l0IGPDtG5nIGPhu6UgxJHhu5MgaOG7jWEgdGjhu5FuZyBrw6ogcuG6pXQgdHV54buHdCB24budaSB0cm9uZyBSLiBLaMOhYyB24bubaSBuaOG7r25nIHNvZnR3YXJlIHRoxrDGoW5nIG3huqFpLCBnZ3Bsb3QyIGtow7RuZyBjdW5nIGPhuqVwIG5o4buvbmcgZOG6oW5nIGJp4buDdSDEkeG7kyB0aOG7kW5nIGvDqiBxdXkgxrDhu5tjLCBuaMawbmcgbsOzIGNobyBwaMOpcCBuZ8aw4budaSBkw7luZyB04buxIGRvIGRp4buFbiDEkeG6oXQgw70gdMaw4bufbmcgY+G7p2EgbcOsbmggdGjDtG5nIHF1YSAibmfhu68gcGjDoXAgxJHhu5MgdGjhu4siLiANCg0KVHV5IG5oacOqbiwgY2jDrW5oIHbDrCB0aW5oIHRo4bqnbiB04buxIGRvIG7DoHksIHLhuqV0IGhp4bq/bSBnacOhbyB0csOsbmggZ2dwbG90MiB0aOG7sWMgc+G7sSBoxrDhu5tuZyBk4bqrbiBjaG8gbmfGsOG7nWkgaOG7jWMgY8OhY2ggduG6rW4gZOG7pW5nIG5n4buvIHBow6FwIG7Ds2kgdHLDqm4gxJHhu4MgxJHhuqF0IG5o4buvbmcgbeG7pWMgdGnDqnUgdGjhu7FjIGThu6VuZy4gxJBhIHPhu5EgZ2nDoW8gdHLDrG5oIGNo4buJIG3hu5tpIGThu6tuZyBs4bqhaSDhu58gdmnhu4djIGdp4bqjaSB0aMOtY2ggY8O6IHBow6FwIGPDoWMgZnVuY3Rpb25zIGdncGxvdDIgaG/hurdjIGLDoW0gdsOgbyBuaOG7r25nIGThuqFuZyBiaeG7g3UgxJHhu5Mga2luaCDEkWnhu4NuLiBOZ2F5IGPhuqMga2hpIHRow7RuZyB0aOG6oW8gbmfhu68gcGjDoXAgZ2dwbG90MiwgbmfGsOG7nWkgZMO5bmcgduG6q24gYuG7iyBjaGkgcGjhu5FpIGLhu59pIHRow7NpIHF1ZW4gdsOgIGPDoWNoIG5ow6xuIHbhuqVuIMSR4buBLiBUaMOtIGThu6UsIGtoaSBi4bqhbiBn4bq3cCB24bqlbiDEkeG7gSBraMOzLCBi4bqhbiB24bqrbiBwaOG6o2kgdOG7sSBzdXkgbmdoxKkgdMOsbSByYSBnaeG6o2kgcGjDoXAgY2hvIHJpw6puZyBtw6xuaC4gDQoNCk3hu5l0IHRyb25nIG5o4buvbmcgduG6pW4gxJHhu4EgbsOgeSwgxJHDsyBsw6A6IFRyw6xuaCBiw6B5IHRow7RuZyB0aW4ga2hpIGRhdGFzZXQgY8OzIHF1w6Egbmhp4buBdSBjYXNlcy4gxJDDonkgbMOgIHbhuqVuIMSR4buBICJPdmVycGxvdHRpbmciLCB04bupYyBz4buxIGNo4buTbmcgbOG6r3AgY+G7p2EgY8OhYyB0aMOgbmggcGjhuqduIGjDrG5oIGjhu41hICjEkWnhu4NtKSBraGkgY2jDum5nIGPDsyB04buNYSDEkeG7mSB0csO5bmcgbmhhdSBoYXkgcuG6pXQgZ+G6p24gbmhhdS4gRG8gbMOgbSB2aeG7h2MgdHJvbmcgbmfDoG5oIGvhu7kgdGh14bqtdCB5IHNpbmgsIE5oaSB0aMaw4budbmcgcGjhuqNpIHjhu60gbMO9IG5o4buvbmcgZGF0YXNldCB24bubaSBow6BuZyB0csSDbSBuZ8OgbiwgdGjhuq1tIGNow60gaMOgbmcgdHJp4buHdSBsaW5lcy4gTmjhu69uZyBnaeG6o2kgcGjDoXAgbcOgIE5oaSBz4bqvcCBjaGlhIHPhursgc2F1IMSRw6J5IGPDsyB0aOG7gyBnaeG6o2kgcXV54bq/dCBoaeG7h24gdMaw4bujbmcgY2jhu5NuZyBs4bqvcCB2w6AgdMOzbSB04bqvdCDEkcaw4bujYyB0aMO0bmcgdGluIHbhu4Ega2h1eW5oIGjGsOG7m25nIGPhu6dhIGThu68gbGnhu4d1Lg0KDQpUcm9uZyB0aMOtIGThu6UgbWluaCBo4buNYSBzYXUgxJHDonksIE5oaSB04bqhbyByYSAxIGRhdGFzZXQgduG7m2kgMiBiaeG6v24gWCxZIGPDsyBwaMOibiBi4buRIGNodeG6qW4sIHbhu5tpIDk5LDk5OSBjYXNlcy4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KZGY9ZGF0YV9mcmFtZShYPXJub3JtKG49OTk5OTksbWVhbj0yMCxzZD01MCksDQogICAgICAgICAgICAgIFk9cm5vcm0obj05OTk5OSxtZWFuPTEwLHNkPTQwKStYKSU+JQ0KICBhc190aWJibGUoKQ0KYGBgDQoNCsSQw6J5IGzDoCBt4buZdCBjb24gc+G7kSBraMOhIGzhu5tuLCBuw6puIGtoaSB24bq9IHNjYXR0ZXIgcGxvdCB0cm9uZyBnZ3Bsb3QyIGJp4buDdSDEkeG7kyBz4bq9IHLhuqV0IHLhu5FpLiANCg0KSMOsbmggMTogTmjGsCBi4bqhbiB0aOG6pXk6IG3huq10IMSR4buZIMSRaeG7g20gcXXDoSBkw6B5IHbDoCBjaMO6bmcgY2jhu5NuZyBsw6puIG5oYXUgbeG7mXQgY8OhY2ggaOG7l24gbG/huqFuOg0KIA0KYGBge3J9DQpwPWdncGxvdChkZixhZXMoWCxZKSkrIHRoZW1lX2J3KCkNCg0KcCtnZW9tX2ppdHRlcihjb2w9InJlZCIpDQpgYGANCg0KR2nhuqNpIHBow6FwIHRo4bupIG5o4bqldCwgxJHDsyBsw6AgY2FuIHRoaeG7h3AgdHLDqm4gY2jDrW5oIHnhur91IHThu5EgaMOsbmggaOG7jWEgKMSRaeG7g20pLCBj4bulIHRo4buDIGzDoCBrw61jaCB0aMaw4bubYywgxJHhu5kgdMawxqFuZyBwaOG6o24sIMSR4buZIHRyb25nIHN14buRdC4gDQoNCsSQ4buDIHTEg25nIMSR4buZIHTGsMahbmcgcGjhuqNuIHbDoCB0cm9uZyBzdeG7kXQsIHRhIGPDsyB0aOG7gyBkw7luZyBuaOG7r25nIMSRaeG7g20gcuG7l25nLCB04bupYyBraMO0bmcgY8OzIG3DoHUgbuG7gW4gbcOgIGNo4buJIGPDsyBtw6B1IHZp4buBbjoNCg0KSMOsbmggMjogTG/huqFpIGLhu48gbcOgdSBu4buBbiBjw7MgdGjhu4MgcGjDom4gYmnhu4d0IGtow6Egbmhp4buBdSDEkWnhu4NtLCB0dXkgbmhpw6puIHbhu5tpIGPhu6EgbeG6q3UgcXXDoSBs4bubbiwgY8OhY2ggbMOgbSBuw6B5IGNoxrBhIGdp4bqjaSBxdXnhur90IMSRxrDhu6NjIHbhuqVuIMSR4buBIGNo4buTbmcgbOG6r3A6DQoNCmBgYHtyfQ0KcCtnZW9tX2ppdHRlcihjb2w9InJlZCIsc2hhcGU9MSkNCg0KYGBgDQoNCkjDrG5oIDM6IA0KDQpDw6FjaCBsw6BtIHRo4bupIDIsIMSRw7MgbMOgIHRodSBuaOG7jyBrw61jaCB0aMaw4bubYyBt4buXaSDEkWnhu4NtIGNo4buJIGPDsm4gMSBwaXhlbC4gQmnhu4N1IMSR4buTIHPhur0gdGhheSDEkeG7lWkgdGhlbyBoxrDhu5tuZyB0xINuZyDEkeG7mSBwaMOibiBnaeG6o2k6IFRVeSBuaGnDqm4sIHPhu7EgY2jhu5NuZyBs4bqvcCB24bqrbiBjw7JuOw0KDQpgYGB7cn0NCnArZ2VvbV9qaXR0ZXIoY29sPSJyZWQiLHNoYXBlPSIuIikNCg0KYGBgDQoNCkjDrG5oIDQ6IFRp4bq/cCB0aGVvLCB0YSBjw7MgdGjhu4MgdGjhu60gY2FuIHRoaeG7h3AgY8O5bmcgbMO6YyBsw6puIGFscGhhICjEkeG7mSB0cm9uZyBzdeG7kXQgY+G7p2EgeeG6v3UgdOG7kSBow6xuaCBo4buNYSkgdsOgIGxv4bqhaSBi4buPIG3DoHUgbuG7gW46DQoNCkhp4buHdSDhu6luZyBj4bunYSB2aeG7h2MgZ2nhuqNtIGFscGhhIGzDoCBnaeG6o20gbmhp4buFdSwgdsOgIGdp4buvIGzhuqFpIGtodXluaCBoxrDhu5tuZyBjaMOtbmggY+G7p2EgZOG7ryBsaeG7h3UuDQoNCmBgYHtyfQ0KcCtnZW9tX2ppdHRlcihjb2w9InJlZCIsYWxwaGE9MC4wMyxzaGFwZT0xKQ0KDQpgYGANCg0KR2nhuqNpIHBow6FwIGNhbiB0aGnhu4dwIGzDqm4gaMOsbmggaOG7jWEgZOG7q25nIGzhuqFpIOG7nyDEkcOieSwgYsOieSBnaeG7nSB0YSBz4bq9IHRo4butIG3hu5l0IGPDoWNoIGtow6FjLCDEkcOzIGzDoCBjYW4gdGhp4buHcCBsw6puIHnhur91IHThu5EgVGjhu5FuZyBrw6ouIEPhu6UgdGjhu4MsIHRhIG114buRbiBnb20gY8OhYyBnacOhIHRy4buLIGfhuqduIG5oYXUgdsOgbyBuaOG7r25nIG5ow7NtIMSR4bqhaSBkaeG7h24sIHbDoCBiaeG7g3UgZGnhu4VuIHThu41hIMSR4buZIGPhu6dhIGPDoWMgbmjDs20gxJHhuqFpIGRp4buHbiBuw6B5LiANCg0KSMOsbmggNTogZ2VvbV9iaW4yZCANCg0KYGBge3J9DQpsaWJyYXJ5KHZpcmlkaXMpDQoNCnArZ2VvbV9iaW4yZChiaW5zPTMzLGFlcyhjb2w9Li5jb3VudC4uKSxzaG93LmxlZ2VuZCA9IEYpKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMoYmVnaW49MC4xLGVuZD0wLjksb3B0aW9uPSJBIixhbHBoYT0wLjEpKw0KICBzY2FsZV9jb2xvcl92aXJpZGlzKG9wdGlvbj0iQiIsYWxwaGE9MC4zKQ0KDQoNCmBgYA0KDQpIw6xuaCA2OiBIb+G6t2Mgc+G7rSBk4bulbmcgZ2VvbV9oZXgNCg0KYGBge3J9DQpwK2dlb21faGV4KGFlcyhjb2w9Li5jb3VudC4uKSxzaG93LmxlZ2VuZCA9IEYpKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMob3B0aW9uPSJBIixhbHBoYT0wLjEsYmVnaW49MC4xLGVuZD0wLjkpKw0Kc2NhbGVfY29sb3JfdmlyaWRpcyhvcHRpb249IkIiLGFscGhhPTAuMykNCg0KYGBgDQoNCkhhaSBjw6FjaCBsw6BtIHRyw6puIHPhur0gZ29tIG5oaeG7gXUgxJFp4buDbSBs4bqhaSB0aMOgbmggbeG7mXQgbmjDs20sIMSR4bqhaSBkaeG7h24gYuG6sW5nIG3hu5l0IMO0IHZ1w7RuZyBoYXkgaMOsbmggbOG7pWMgZ2nDoWMsIHbDoCB0w7QgbcOgdSB0aGVvIG3huq10IMSR4buZIHBow6JuIGLhu5EgY+G7p2EgY8OhYyDEkWnhu4NtDQoNClRhIGPDsyB0aOG7gyB0aGF5IMSR4buVaSBob8OgbiB0b8OgbiB54bq/dSB04buRIHRo4buRbmcga8OqLCB0aMOtIGThu6UgdGjhu4MgaGnhu4duIG3huq10IMSR4buZIHBow6JuIGLhu5EgMiBjaGnhu4F1IHRoYXkgdsOsIHThu41hIMSR4buZIGdpw6EgdHLhu4s6DQoNCkjDrG5oIDc6IDJEIEtlcm5lbCBkZW5zaXR5IHBsb3QNCg0KYGBge3J9DQpwK3N0YXRfZGVuc2l0eV8yZChhZXMoZmlsbCA9IC4ubGV2ZWwuLiksZ2VvbSA9ICJwb2x5Z29uIixjb2w9ImJsYWNrIikrDQogIHNjYWxlX2ZpbGxfZGlzdGlsbGVyKHBhbGV0dGU9IlNwZWN0cmFsIikNCg0KDQpgYGANCg0KSMOsbmggODogVMawxqFuZyB04buxIG5oxrAgdHLDqm4sIFRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBjw6FjIG5ow7NtIMSR4bqhaSBkaeG7h24sIG5o4bqxbSBjdW5nIGPhuqVwIGPhuqMgMiB0aMO0bmcgdGluIHbhu4EgdOG7jWEgxJHhu5kgdsOgIG3huq10IMSR4buZIHBow6JuIGLhu5EuDQoNCmBgYHtyfQ0KcCtzdGF0X2RlbnNpdHlfMmQoYWVzKGZpbGwgPSAuLmxldmVsLi4pLGdlb20gPSAicG9seWdvbiIsYWxwaGE9MC4zLHNob3cubGVnZW5kID0gRikrIA0Kc3RhdF9kZW5zaXR5XzJkKGdlb20gPSAicG9pbnQiLCANCiAgICAgICAgICAgICAgICAgIGFlcyhzaXplID0gLi5kZW5zaXR5Li4sZmlsbD0uLmRlbnNpdHkuLiksDQogICAgICAgICAgICAgICAgICBzaGFwZT0yMSxuID0gMTgsIA0KICAgICAgICAgICAgICAgICAgY29udG91ciA9IEZBTFNFLGNvbD0iYmxhY2siKSsNCiAgc2NhbGVfZmlsbF9kaXN0aWxsZXIocGFsZXR0ZT0iU3BlY3RyYWwiKQ0KYGBgDQoNCkjDrG5oIDk6IEPDsyBi4bqjbiBjaOG6pXQgY8WpbmcgbMOgIGJp4buDdSDEkeG7kyBt4bqtdCDEkeG7mSBwaMOibiBwaOG7kWkgMiBjaGnhu4F1LCBuaMawbmcgY8OzIGThuqFuZyAxIGhlYXRtYXA6DQoNCmBgYHtyfQ0KcCtzdGF0X2RlbnNpdHlfMmQoZ2VvbSA9ICJyYXN0ZXIiLCANCiAgICAgICAgICAgICAgICAgIGFlcyhmaWxsID0gLi5kZW5zaXR5Li4pLCANCiAgICAgICAgICAgICAgICAgIGNvbnRvdXIgPSBGQUxTRSkrDQogIHNjYWxlX3hfY29udGludW91cyhleHBhbmQgPSBjKDAsMCkpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGV4cGFuZCA9IGMoMCwwKSkgKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMob3B0aW9uPSJBIixhbHBoYT0wLjIpDQoNCg0KYGBgDQoNCkjDrG5oIDEwOiBN4buZdCBoZWF0bWFwIGtow6FjIMSRxqFuIGdp4bqjbiBoxqFuOg0KDQpgYGB7cn0NCnArc3RhdF9kZW5zaXR5XzJkKGdlb20gPSAicmFzdGVyIiwgDQogICAgICAgICAgICAgICAgICBhZXMoZmlsbCA9IC4uZGVuc2l0eS4uKSwgDQogICAgICAgICAgICAgICAgICBjb250b3VyID0gRkFMU0UpKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLDApKSArDQogIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsMCkpICsNCiAgc2NhbGVfZmlsbF9ncmFkaWVudChsb3c9IndoaXRlIixoaWdoPSJyZWQiKQ0KDQoNCmBgYA0KDQpCw6J5IGdp4budIHRhIG5ow6xuIHbhuqVuIMSR4buBIHRoZW8gbeG7mXQgaMaw4bubbmcga2jDoWM6IEdp4bqjIMSR4buLbmggbuG6v3UgdGEgY2jhu4kgcXVhbiB0w6JtIMSR4bq/biBnacOhIHRy4buLIGPhu6dhIDEgYmnhur9uIFksIHRhIGPDsyB0aOG7gyBwaMOibiBuaMOzbSBYIHbDoCBz4butIGThu6VuZyBib3hwbG90LCB2aW9saW4gcGxvdCBoYXkgcG9pbnQgcmFuZ2UgbmjGsCBzYXU6DQoNCkjDrG5oIDExOiBQaMOibiBi4buRIGPhu6dhIFkgdGhlbyBYLCBzYXUga2hpIHBow6JuIG5ow7NtIFggdGjDoG5oIDEwIMSRb+G6oW4NCg0KYGBge3J9DQpwK2dlb21fYm94cGxvdChhZXMoZ3JvdXAgPSBjdXRfaW50ZXJ2YWwoWCwgMTApLA0KICAgICAgICAgICAgICAgICAgIGZpbGw9Y3V0X2ludGVydmFsKFgsIDEwKSksc2hvdy5sZWdlbmQgPSBGKSsNCiAgc2NhbGVfZmlsbF92aXJpZGlzKG9wdGlvbj0iQSIsYWxwaGE9MC44LGRpc2NyZXRlID0gVCkNCg0KDQpgYGANCg0KSMOsbmggMTI6IFZpb2xpbiBwbG90DQoNCmBgYHtyfQ0KcCtnZW9tX3Zpb2xpbihhZXMoZ3JvdXAgPSBjdXRfaW50ZXJ2YWwoWCwgMTApLA0KICAgICAgICAgICAgICAgICAgIGZpbGw9Y3V0X2ludGVydmFsKFgsMTApKSxzaG93LmxlZ2VuZCA9IEYpKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMob3B0aW9uPSJBIixhbHBoYT0wLjgsZGlzY3JldGUgPSBUKQ0KDQpgYGANCg0KSMOsbmggMTM6IFBvaW50IHbDoCBlcnJvciBiYXINCg0KYGBge3J9DQpnZ3Bsb3QoZGYsYWVzKHg9Y3V0X2ludGVydmFsKFgsMTApLHk9WSkpKw0KICAgICAgICAgICAgICAgc3RhdF9zdW1tYXJ5KGFlcyhjb2w9Y3V0X2ludGVydmFsKFgsMTApKSwNCiAgICAgICAgICAgICAgICAgICAgICAgIGdlb20gPSAicG9pbnRyYW5nZSIsDQogICAgICAgICAgICAgICAgICAgICAgICBmdW4ueSA9IG1lZGlhbiwNCiAgICAgICAgICAgICAgICAgICAgICAgIGZ1bi55bWF4ID0gbWF4LA0KICAgICAgICAgICAgICAgICAgICAgICAgZnVuLnltaW49bWluLA0KICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0xKSsNCiAgICAgICAgICAgICAgICAgICAgICAgdGhlbWVfYncoKSsNCiAgc2NhbGVfY29sb3JfdmlyaWRpcyhvcHRpb249IkEiLGRpc2NyZXRlID0gVCkrDQogIHNjYWxlX3hfZGlzY3JldGUoYnJlYWtzPU5VTEwpDQoNCmBgYA0KDQpIw6xuaCAxNDogS+G6v3QgaOG7o3Agc2NhdHRlciBkb3QgcGxvdCB2w6AgdMOzbSB04bqvdCB0cnVuZyB24buLIGPhu6dhIFksIHRoZW8gMjAgcGjDom4gbmjDs20gY+G7p2EgWA0KDQpgYGB7cn0NCmdncGxvdChkZixhZXMoeD1jdXRfaW50ZXJ2YWwoWCwyMCkseT1ZKSkrDQogIGdlb21faml0dGVyKGFlcyhjb2w9WSksYWxwaGE9MC4wNSxzaGFwZT0xKSsNCiAgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gIm1lZGlhbiIsIA0KICAgICAgICAgICAgICAgZ2VvbSA9ICJwb2ludCIsDQogICAgICAgICAgICAgICBzaGFwZT0yMSwNCiAgICAgICAgICAgICAgIHNpemU9MywNCiAgICAgICAgICAgICAgIGZpbGw9IndoaXRlIikrDQogIHRoZW1lX2J3KCkrDQogIHNjYWxlX2NvbG9yX3ZpcmlkaXMob3B0aW9uPSJBIikrDQogIHNjYWxlX3hfZGlzY3JldGUoYnJlYWtzPU5VTEwpDQpgYGANCg0KSMOsbmggMTU6IE7hur91IHRhIMSR4buTbmcgdGjhu51pIGPhuq90IGPhuqMgWCB2w6AgWSB0aMOgbmggMzAgcGjDom4gbmjDs20gYuG6sW5nIG5oYXUgdGjDrCBr4bq/dCBxdeG6oyBz4bq9IHJhIHNhbyA/DQoNCmBgYHtyfQ0KZ2dwbG90KGRmLGFlcyh4PWN1dF9pbnRlcnZhbChYLDMwKSwNCiAgICAgICAgICAgICAgeT1jdXRfaW50ZXJ2YWwoWSwzMCkpKSsNCiAgZ2VvbV9qaXR0ZXIoYWVzKGNvbD1YKSxhbHBoYT0wLjUsc2hhcGU9Ii4iKSsNCiAgdGhlbWVfYncoKSsNCiAgc2NhbGVfY29sb3JfdmlyaWRpcyhvcHRpb249IkEiKSsNCiAgc2NhbGVfeF9kaXNjcmV0ZShicmVha3M9TlVMTCkrDQogIHNjYWxlX3lfZGlzY3JldGUoYnJlYWtzPU5VTEwpDQpgYGANCg0KSMOsbmggMTY6IEN14buRaSBjw7luZywgdGEgY8OzIHRo4buDIHbhur0gZGVuc2l0eSBwbG90IGPhu6dhIFkgdGhlbyB04burbmcgcGjDom4gbmjDs20gY+G7p2EgWA0KDQpgYGB7cn0NCmxpYnJhcnkoZ2dyaWRnZXMpDQoNCmdncGxvdChkZixhZXMoeT1jdXRfaW50ZXJ2YWwoWCwxNSkseD1ZKSkrDQogIGdlb21fZGVuc2l0eV9yaWRnZXMoYWVzKGZpbGw9Y3V0X2ludGVydmFsKFgsMTUpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBjb2w9Y3V0X2ludGVydmFsKFgsMTUpKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9NCxzaG93LmxlZ2VuZCA9IEYpKw0KICB0aGVtZV9idygpKw0KICBzY2FsZV95X2Rpc2NyZXRlKGJyZWFrcz1OVUxMKSsNCiAgY29vcmRfZmxpcCgpKw0KICBzY2FsZV9maWxsX3ZpcmlkaXMob3B0aW9uPSJBIixhbHBoYT0wLjUsZGlzY3JldGUgPSBUKSsNCiAgc2NhbGVfY29sb3JfdmlyaWRpcyhvcHRpb249IkMiLGFscGhhPTAuNSxkaXNjcmV0ZSA9IFQsYmVnaW49MSxlbmQ9MCkNCmBgYA0KDQpU4bqhbSBiaeG7h3QgY8OhYyBi4bqhbiwgaHkgduG7jW5nIGLDoGkgdmnhur90IGdpw7pwIGPDoWMgYuG6oW4gbXVhIHZ1aSDEkcaw4bujYyB2w6BpIHRy4buRbmcgY2FuaC4NCg0KSOG6uW4gZ+G6t3AgbOG6oWkgY8OhYyBi4bqhbiBs4bqnbiB04bubaS4=