Giới thiệu.

Từ những khách hàng tiêu dùng lớn cho đến những khách hàng rời bỏ doanh nghiệp, tất cả những khách hàng đều có nhu cầu và mong muốn đa dạng. Doanh nghiệp muốn khách hàng chi tiêu nhiều hơn từ những chiến dịch tiếp thị chương trình, sản phẩm mới tới khách hàng theo những cách khác nhau. Tuy nhiên, câu hỏi đặt ra là làm thế nào để đưa ra được các chiến dịch tiếp thị phù hợp với những nhóm khách hàng đang có nhu cầu để từ đó tăng tỷ lệ phản hồi từ khách hàng và từ đó tăng doanh số bán hàng. Bài toán đặt ra là làm thế nào để có thể phân khúc khách hàng một cách tương đối chính xác dựa trên hành vi giao dịch lịch sử của khách hàng, thuật toán RFM sẽ giúp chúng ta giải quyết vấn đề này một cách nhanh chóng và hiệu quả.

RFM

Phân tích RFM là một kĩ thuật phân khúc khách hàng dựa trên hành vi giao dịch của khách hàng trong quá khứ, RFM bao gồm 3 chỉ số chính:

  • Recency (R): Thời gian giao dịch cuối cùng.
  • Frequency (F): Tổng số lần giao dịch chi tiêu.
  • Monetary value (M): Tổng só tiền giao dịch chi tiêu.

Lợi ích của phân tích RFM:

  • Tăng tỷ lệ giữ chân khách hàng.
  • Tăng tốc độ phản hồi từ khách hàng.
  • Tăng tỷ doanh thu từ khách hàng.

Phân khúc khách hàng.

Để thực hiện phân tích RFM, giả sử chúng ta chia khách hàng thành các nhóm khác nhau theo phân phối (phân vị) cho tần suất chi tiêu, lần chi tiêu cuối cùng, và tổng số tiền chi tiêu.

Giả sử, mỗi nhóm biến chúng ta chi thành các mức phân vị khách nhau, nếu:

  • 3 nhóm: 3x3x3 = 27 phân khúc.
  • 4 nhóm: 4x4x4 = 64 phân khúc.
  • 5 nhóm: 5x5x5 = 125 phân khúc.

Khi số nhóm nhiều lên, việc quả n lý các phân khúc là điều khó khăn cho doanh nghiệp. Chúng ta xét ví dụ hành vi chiêu tiêu của khách hàng A: - Khách hàng nằm trong nhóm người mua gần đây nhất (R = 1) - Khách hàng nằm trong nhóm người mua với số lượng nhiều nhất (F = 1) - Khách hàng nằm trong nhóm người chi tiêu tiêu nhiều nhất (M = 1)

Từ đây, ta xác định được khách hàng A này thuộc phân khúc RFM (111) là nhóm khách hàng tốt nhất, dưới đây là một bảng miêu tả các phân khúc chính đối với RFM (tham khảo):

Phân khúc RFM Mô ta ̉ Chương trình MKT
Best Customer 111 Những khách hàng đã mua gần đây nhất, thường xuyên nhất và chi tiêu nhiều nhất Sản phẩm mới, chương trình khách hàng thân thiết
Loyal Customer X1X Khách hàng mua gần đây nhất Sử dụng R và M để phân khúc tiếp theo
Big Spenders XX1 Khách hàng chi tiêu nhiều nhất Tiếp thị cho họ những sản phẩm đắt nhất của doanh nghiệp
Almost Lost 311 Không được mua trong một thời gian, nhưng mua thường xuyên và chi tiêu nhiều nhất Ưu đãi về giá để họ tiếp tục sử dụng
Lost Customers 411 Không được mua trong một thời gian, nhưng mua thường xuyên và chi tiêu nhiều nhất Ưu đãi về giá để họ tiếp tục
Lost Cheap Customers 444 Lần mua cuối cùng từ rất lâu, số tiền giao dịch ít và số lần giao dịch ít Không nên tập trung vào nhóm khách này

Cách thực hiện phân tích RFM.

# Bước 1: Connect R với SQL server
# install.packages('RODBC')
library(RODBC)
library(dplyr)
library(ggplot2)
library(extrafont)
my_font <- "OfficinaSansITC"



# test <- odbcConnect('test', 
#                     uid = 'sa', # User SQL server
#                     pwd = 'Dangvu257@') # # Password SQL server
# 
# # Kéo 1 bảng dữ liệu từ SQL vào R...
# df <- sqlQuery(test,"select * From RFM_MODEL..FCT_RFM_MODEL
#                      where 1 = 1
#                      and MONTH_ID = '20180331'
#                      and CUSTOMER_GENDER in ('MALE','FEMALE')
#                ")

df1 <- readRDS(file = 'D:\\VUDT\\8.TRAINING\\Chart\\RFM_DATE.RDD')

# Pareto chart
df <- 
  df1 %>% 
  group_by(CUSTOMER_SEGMENTATION) %>% 
  summarise(mpg_sum = sum(AMOUNT_TOT)) %>% 
  ungroup() %>% 
  mutate(mpg_sum = round(mpg_sum/1e9)) %>% 
  arrange(mpg_sum)  %>% 
  as.data.frame() %>% 
  mutate(mpg_cumsum = cumsum(mpg_sum),
         mpg_total = sum(mpg_sum)) %>% 
  mutate(mpg_percent = mpg_cumsum/mpg_total) %>% 
  mutate(mpg_int = seq(1:NROW(.)))

# Chart
nr <- nrow(df)
N <- sum(df$mpg_sum)

df_ticks <- data.frame(xtick0 = rep(nr +.55, 11), xtick1 = rep(nr +.59, 11),
                       ytick = seq(0, N, N/10))
y2 <- paste0(seq(0, 100, 10), '%')

ggplot(df, aes(x = reorder(CUSTOMER_SEGMENTATION,mpg_cumsum), y = mpg_sum)) +
  geom_bar(stat="identity", fill = "#23576E", width = 0.5) +
  geom_line(aes(x = mpg_int, y = mpg_cumsum), color = "#8C3F4D") +
  geom_point(aes(x = reorder(CUSTOMER_SEGMENTATION,mpg_cumsum), y = mpg_cumsum), color = "#8C3F4D", pch = 19) +
  scale_y_continuous(breaks=seq(0, N, N/10), limits=c(-.02 * N, N * 1.02)) +
  scale_x_discrete(breaks = df$CUSTOMER_SEGMENTATION) +
  guides(fill = FALSE, color = FALSE) +
  annotate("rect", xmin = nr + .55, xmax = nr + 1, ymin = -.02 * N, ymax = N * 1.02,
           fill = "#EFF2F4") +
  annotate("text", x = nr + .8, y = seq(0, N, N/10), label = y2, size = 3, family="serif") +
  # geom_segment(x = nr + .55, xend = nr + .55, y = -.02 * N, yend = N * 1.02, color = "grey50") +
  # geom_segment(data = df_ticks, aes(x = xtick0, y = ytick, xend = xtick1, yend = ytick)) +
  labs(title = "T\u00D4\u0309NG DOANH S\u00D4\u0301 GIAO DI\u0323CH\n THEO T\u01AF\u0300NG PH\u00C2N KHU\u0301C", 
       y = "", 
       x = "",
       subtitle = "Tha\u0301ng ba\u0301o ca\u0301o: 31-03-2018") +
  theme_bw() +
  theme(text = element_text(family = 'serif')) +
  theme(axis.title = element_text(family = my_font,size = 10)) +
  theme(panel.background = element_blank(), 
        panel.grid = element_blank(),
        axis.ticks = element_blank(),
        # axis.text.x = element_blank(),
        axis.text.x = element_text(size = 10, face = 'bold',angle = 90, vjust = -0.0),
        panel.border = element_blank(),
        plot.margin = unit(c(1,4,1,4), "cm"),
        axis.title = element_text(family = my_font,size = 16),
        text = element_text(family = 'serif')) +
  theme(title = element_text(size = 10),
        plot.subtitle=element_text(size = 12, hjust=0.77, face = 'italic', color="black")) +
  theme(plot.title = element_text(hjust = 0.5,face = 'bold')) +
  theme(plot.background = element_rect(fill = "#EFF2F4", color = NA)) + 
  theme(panel.background = element_rect(fill = "#EFF2F4", color = NA)) +
  theme(legend.background = element_rect(fill = "#EFF2F4", color = NA))

PCD để thực hiện phân tích RFM trên SQL


USE [RFM_MODEL]
GO
/****** Object:  StoredProcedure [dbo].[PCD_DIM_COHORT_TOTAL]    Script Date: 5/27/2019 11:50:55 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      <VU DANG,>
-- Create date: <2019/05/17,,>
-- Description: <COHORT TOTAL,,>
-- Ref: https://rstudio-pubs-static.s3.amazonaws.com/365184_904c4/*
369586e49fc8fa08adcae1d559d.html

-- =============================================
ALTER PROCEDURE [dbo].[PCD_DIM_COHORT_TOTAL] 
    -- Add the parameters for the stored procedure here
    @DATE VARCHAR(8)
AS
BEGIN
    SET NOCOUNT ON;

    -- Insert statements for procedure here

-- STEP 1: TẠO DANH SÁCH KHÁCH HÀNG BAN ĐẦU --- THÁNG 01/01/2017 - 30/11/2017 (REPORT_MONTH 2017/12)
DROP TABLE IF EXISTS #step1_customer_original

SELECT DISTINCT CUSTOMER_CARD
INTO #step1_customer_original
FROM DATA_RAW..card_transaction_sb_201701_201803
WHERE 1 = 1
AND DAYID BETWEEN  DATEADD(m, DATEDIFF(m, 0, DATEADD(month, -11, @DATE)), 0)  AND EOMONTH(DATEADD(month, -11, @DATE))
--AND DAYID BETWEEN  DATEADD(month, -12, @DATE) AND DATEADD(month, -11, @DATE)

-- BƯỚC 2: TẠO COHORT TRONG 12 THÁNG

DROP TABLE IF EXISTS #STEP2_CUSTOMER_COHORT ;

SELECT 
    CONVERT(VARCHAR(6),DAYID,112) AS COHORT_ID,
    COUNT(DISTINCT CUSTOMER_CARD) AS NUMBER,
    CONVERT(VARCHAR(6),@DATE,112) MONTH_ID

INTO #STEP2_CUSTOMER_COHORT 
FROM DATA_RAW..card_transaction_sb_201701_201803 A
WHERE 1 = 1
AND DAYID BETWEEN  DATEADD(m, DATEDIFF(m, 0, DATEADD(month, -11, @DATE)), 0) AND @DATE

--AND DAYID BETWEEN  DATEADD(month, -12, @DATE) AND @DATE
--AND YEAR(DAYID) = 2017
AND CUSTOMER_CARD IN (SELECT CUSTOMER_CARD from #step1_customer_original)
GROUP BY CONVERT(VARCHAR(6),DAYID,112)
ORDER BY COUNT(DISTINCT CUSTOMER_CARD) DESC;

/*
DROP TABLE IF EXISTS RFM_MODEL..DIM_COHORT 

CREATE TABLE RFM_MODEL..DIM_COHORT (
    MONTH_ID VARCHAR(8),
    COHORT_ID VARCHAR(6),
    RETENTION FLOAT,
    RETENTION_RATE FLOAT
    )
    */
DELETE FROM RFM_MODEL..DIM_COHORT WHERE MONTH_ID =  @DATE

INSERT INTO RFM_MODEL..DIM_COHORT (
    MONTH_ID,
    COHORT_ID,
    RETENTION
    )

SELECT  
    @DATE MONTH_ID,
    COHORT_ID,
    NUMBER
FROM #STEP2_CUSTOMER_COHORT 

-- TÍNH RETENTION_RATE

UPDATE RFM_MODEL..DIM_COHORT 
SET RETENTION_RATE = ROUND(A.RETENTION/B.RETENTION_MAX,2)
FROM RFM_MODEL..DIM_COHORT  A, (SELECT MAX(RETENTION) RETENTION_MAX FROM RFM_MODEL..DIM_COHORT WHERE MONTH_ID = @DATE ) B
WHERE A.MONTH_ID = @DATE
END

*/
LS0tDQp0aXRsZTogIlJGTSAtIFBow6JuIGtow7pjIGtow6FjaCBow6BuZyINCmF1dGhvcjogIlJDaGFydCINCmRhdGU6ICJNYXkgMjcsIDIwMTkiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHB5Z21lbnRzDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLHdhcm5pbmcgPSBGLGVycm9yID0gRixtZXNzYWdlID0gRikNCmBgYA0KDQojIEdp4bubaSB0aGnhu4d1Lg0KICANCiAgVOG7qyBuaOG7r25nIGtow6FjaCBow6BuZyB0acOqdSBkw7luZyBs4bubbiBjaG8gxJHhur9uIG5o4buvbmcga2jDoWNoIGjDoG5nIHLhu51pIGLhu48gZG9hbmggbmdoaeG7h3AsIHThuqV0IGPhuqMgbmjhu69uZyBraMOhY2ggaMOgbmcgxJHhu4F1IGPDsyBuaHUgY+G6p3UgdsOgIG1vbmcgbXXhu5FuIMSRYSBk4bqhbmcuIERvYW5oIG5naGnhu4dwIG114buRbiBraMOhY2ggaMOgbmcgY2hpIHRpw6p1IG5oaeG7gXUgaMahbiB04burIG5o4buvbmcgY2hp4bq/biBk4buLY2ggdGnhur9wIHRo4buLIGNoxrDGoW5nIHRyw6xuaCwgc+G6o24gcGjhuqltIG3hu5tpIHThu5tpIGtow6FjaCBow6BuZyB0aGVvIG5o4buvbmcgY8OhY2gga2jDoWMgbmhhdS4gVHV5IG5oacOqbiwgY8OidSBo4buPaSDEkeG6t3QgcmEgbMOgIGzDoG0gdGjhur8gbsOgbyDEkeG7gyDEkcawYSByYSDEkcaw4bujYyBjw6FjIGNoaeG6v24gZOG7i2NoIHRp4bq/cCB0aOG7iyBwaMO5IGjhu6NwIHbhu5tpIG5o4buvbmcgbmjDs20ga2jDoWNoIGjDoG5nIMSRYW5nIGPDsyBuaHUgY+G6p3UgxJHhu4MgdOG7qyDEkcOzIHTEg25nIHThu7cgbOG7hyBwaOG6o24gaOG7k2kgdOG7qyBraMOhY2ggaMOgbmcgdsOgIHThu6sgxJHDsyB0xINuZyBkb2FuaCBz4buRIGLDoW4gaMOgbmcuIELDoGkgdG/DoW4gxJHhurd0IHJhIGzDoCBsw6BtIHRo4bq/IG7DoG8gxJHhu4MgY8OzIHRo4buDIHBow6JuIGtow7pjIGtow6FjaCBow6BuZyBt4buZdCBjw6FjaCB0xrDGoW5nIMSR4buRaSBjaMOtbmggeMOhYyBk4buxYSB0csOqbiBow6BuaCB2aSBnaWFvIGThu4tjaCBs4buLY2ggc+G7rSBj4bunYSBraMOhY2ggaMOgbmcsIHRodeG6rXQgdG/DoW4gUkZNIHPhur0gZ2nDunAgY2jDum5nIHRhIGdp4bqjaSBxdXnhur90IHbhuqVuIMSR4buBIG7DoHkgbeG7mXQgY8OhY2ggbmhhbmggY2jDs25nIHbDoCBoaeG7h3UgcXXhuqMuDQogIA0KIyBSRk0gDQoNCiAgUGjDom4gdMOtY2ggUkZNIGzDoCBt4buZdCBrxKkgdGh14bqtdCBwaMOibiBraMO6YyBraMOhY2ggaMOgbmcgZOG7sWEgdHLDqm4gaMOgbmggdmkgZ2lhbyBk4buLY2ggY+G7p2Ega2jDoWNoIGjDoG5nIHRyb25nIHF1w6Ega2jhu6ksIFJGTSBiYW8gZ+G7k20gMyBjaOG7iSBz4buRIGNow61uaDoNCiAgICANCiAgLSAqKlJlY2VuY3kgKFIpOioqIFRo4budaSBnaWFuIGdpYW8gZOG7i2NoIGN14buRaSBjw7luZy4NCiAgLSAqKkZyZXF1ZW5jeSAoRik6KiogVOG7lW5nIHPhu5EgbOG6p24gZ2lhbyBk4buLY2ggY2hpIHRpw6p1Lg0KICAtICoqTW9uZXRhcnkgdmFsdWUgKE0pOioqIFThu5VuZyBzw7MgdGnhu4FuIGdpYW8gZOG7i2NoIGNoaSB0acOqdS4NCiAgDQogIEzhu6NpIMOtY2ggY+G7p2EgcGjDom4gdMOtY2ggUkZNOg0KICANCiAgLSBUxINuZyB04bu3IGzhu4cgZ2nhu68gY2jDom4ga2jDoWNoIGjDoG5nLg0KICAtIFTEg25nIHThu5FjIMSR4buZIHBo4bqjbiBo4buTaSB04burIGtow6FjaCBow6BuZy4NCiAgLSBUxINuZyB04bu3IGRvYW5oIHRodSB04burIGtow6FjaCBow6BuZy4NCg0KDQojIFBow6JuIGtow7pjIGtow6FjaCBow6BuZy4NCg0KICDEkOG7gyB0aOG7sWMgaGnhu4duIHBow6JuIHTDrWNoIFJGTSwgZ2nhuqMgc+G7rSBjaMO6bmcgdGEgY2hpYSBraMOhY2ggaMOgbmcgdGjDoG5oIGPDoWMgbmjDs20ga2jDoWMgbmhhdSB0aGVvIHBow6JuIHBo4buRaSAocGjDom4gduG7iykgY2hvIHThuqduIHN14bqldCBjaGkgdGnDqnUsIGzhuqduIGNoaSB0acOqdSBjdeG7kWkgY8O5bmcsIHbDoCB04buVbmcgc+G7kSB0aeG7gW4gY2hpIHRpw6p1Lg0KICANCiAgR2nhuqMgc+G7rSwgbeG7l2kgbmjDs20gYmnhur9uIGNow7puZyB0YSBjaGkgdGjDoG5oIGPDoWMgbeG7qWMgcGjDom4gduG7iyBraMOhY2ggbmhhdSwgbuG6v3U6DQogIA0KICAtIDMgbmjDs206IDN4M3gzID0gMjcgcGjDom4ga2jDumMuDQogIC0gNCBuaMOzbTogNHg0eDQgPSA2NCBwaMOibiBraMO6Yy4NCiAgLSA1IG5ow7NtOiA1eDV4NSA9IDEyNSBwaMOibiBraMO6Yy4NCiAgDQogIEtoaSBz4buRIG5ow7NtIG5oaeG7gXUgbMOqbiwgdmnhu4djIHF14bqjIG4gbMO9IGPDoWMgcGjDom4ga2jDumMgbMOgIMSRaeG7gXUga2jDsyBraMSDbiBjaG8gZG9hbmggbmdoaeG7h3AuICBDaMO6bmcgdGEgeMOpdCB2w60gZOG7pSBow6BuaCB2aSBjaGnDqnUgdGnDqnUgY+G7p2Ega2jDoWNoIGjDoG5nIEE6DQogIC0gS2jDoWNoIGjDoG5nIG7hurFtIHRyb25nIG5ow7NtIG5nxrDhu51pIG11YSBn4bqnbiDEkcOieSBuaOG6pXQgKFIgPSAxKQ0KICAtIEtow6FjaCBow6BuZyBu4bqxbSB0cm9uZyBuaMOzbSBuZ8aw4budaSBtdWEgduG7m2kgc+G7kSBsxrDhu6NuZyBuaGnhu4F1IG5o4bqldCAoRiA9IDEpDQogIC0gS2jDoWNoIGjDoG5nIG7hurFtIHRyb25nIG5ow7NtIG5nxrDhu51pIGNoaSB0acOqdSB0acOqdSBuaGnhu4F1IG5o4bqldCAoTSA9IDEpDQogIA0KICBU4burIMSRw6J5LCB0YSB4w6FjIMSR4buLbmggxJHGsOG7o2Mga2jDoWNoIGjDoG5nIEEgbsOgeSB0aHXhu5ljIHBow6JuIGtow7pjIFJGTSAoMTExKSBsw6AgbmjDs20ga2jDoWNoIGjDoG5nIHThu5F0IG5o4bqldCwgZMaw4bubaSDEkcOieSBsw6AgbeG7mXQgYuG6o25nIG1pw6p1IHThuqMgY8OhYyBwaMOibiBraMO6YyBjaMOtbmggxJHhu5FpIHbhu5tpIFJGTSAodGhhbSBraOG6o28pOg0KICANCiAgfFBow6JuIGtow7pjICAgICAgICAgfFJGTSAgICAgICAgIHxNw7QgdGEgICAgICAgICAgICAgICDMiXxDaMawxqFuZyB0csOsbmggTUtUfA0KICB8Oi0tLS0tLS0tOnw6LTp8Oi0tLS18Oi0tLS0tLS0tLS0tLS0tLXwNCiAgfEJlc3QgQ3VzdG9tZXJ8MTExfE5o4buvbmcga2jDoWNoIGjDoG5nIMSRw6MgbXVhIGfhuqduIMSRw6J5IG5o4bqldCwgdGjGsOG7nW5nIHh1ecOqbiBuaOG6pXQgdsOgIGNoaSB0acOqdSBuaGnhu4F1IG5o4bqldHxT4bqjbiBwaOG6qW0gbeG7m2ksIGNoxrDGoW5nIHRyw6xuaCBraMOhY2ggaMOgbmcgdGjDom4gdGhp4bq/dHwNCiAgfExveWFsIEN1c3RvbWVyfFgxWHxLaMOhY2ggaMOgbmcgbXVhIGfhuqduIMSRw6J5IG5o4bqldHxT4butIGThu6VuZyBSIHbDoCBNIMSR4buDIHBow6JuIGtow7pjIHRp4bq/cCB0aGVvfA0KICB8QmlnIFNwZW5kZXJzfFhYMXxLaMOhY2ggaMOgbmcgY2hpIHRpw6p1IG5oaeG7gXUgbmjhuqV0fFRp4bq/cCB0aOG7iyBjaG8gaOG7jSBuaOG7r25nIHPhuqNuIHBo4bqpbSDEkeG6r3QgbmjhuqV0IGPhu6dhIGRvYW5oIG5naGnhu4dwfA0KICB8QWxtb3N0IExvc3R8MzExfEtow7RuZyDEkcaw4bujYyBtdWEgdHJvbmcgbeG7mXQgdGjhu51pIGdpYW4sIG5oxrBuZyBtdWEgdGjGsOG7nW5nIHh1ecOqbiB2w6AgY2hpIHRpw6p1IG5oaeG7gXUgbmjhuqV0fMavdSDEkcOjaSB24buBIGdpw6EgxJHhu4MgaOG7jSB0aeG6v3AgdOG7pWMgc+G7rSBk4bulbmd8DQogIHxMb3N0IEN1c3RvbWVyc3w0MTF8S2jDtG5nIMSRxrDhu6NjIG11YSB0cm9uZyBt4buZdCB0aOG7nWkgZ2lhbiwgbmjGsG5nIG11YSB0aMaw4budbmcgeHV5w6puIHbDoCBjaGkgdGnDqnUgbmhp4buBdSBuaOG6pXR8xq91IMSRw6NpIHbhu4EgZ2nDoSDEkeG7gyBo4buNIHRp4bq/cCB04bulY3wNCiAgfExvc3QgQ2hlYXAgQ3VzdG9tZXJzfDQ0NHxM4bqnbiBtdWEgY3Xhu5FpIGPDuW5nIHThu6sgcuG6pXQgbMOidSwgc+G7kSB0aeG7gW4gZ2lhbyBk4buLY2ggw610IHbDoCBz4buRIGzhuqduIGdpYW8gZOG7i2NoIMOtdHxLaMO0bmcgbsOqbiB04bqtcCB0cnVuZyB2w6BvIG5ow7NtIGtow6FjaCBuw6B5fA0KICANCg0KIyBDw6FjaCB0aOG7sWMgaGnhu4duIHBow6JuIHTDrWNoIFJGTS4NCg0KICAtIEThu68gbGnhu4d1OiBMw6AgdGjDtG5nIHRpbiBnaWFvIGThu4tjaCBjaGkgdGnDqnUgcXVhIHRo4bq7IHTDrW4gZOG7pW5nLg0KICAtIFRo4budaSBnaWFuOiAxIG7Eg20uDQogIC0gVGjhu51pIMSRaeG7g20gcGjDom4gdMOtY2g6IDMxLzAzLzIwMTguDQogIC0gS+G6v3QgcXXhuqMgcGjDom4gdMOtY2ggUkZNOiDEkMaw4bujYyB4w6J5IGThu7FuZyB0csOqbiBTUUwgc2VydmVyLg0KICAtIExpbmsgaMaw4bubbmcgZOG6q24gcGjDom4gdMOtY2ggUkZNIHPhu60gZOG7pW5nIFI6IGh0dHBzOi8vcnB1YnMuY29tL0x1ZG92aWNiZW5pc3RhbnQvQ3VzdG9tZXJfQW5hbHlzaXM/ZmJjbGlkPUl3QVIxZFhPQ19fRXBSejFqSEo3aHJaTFIyVGU3RGZ5ZzJsd1ZEaFZLcHhDWUp6MmdqZVR4RzU5TFh5dHcNCiAgDQpgYGB7cn0NCiMgQsaw4bubYyAxOiBDb25uZWN0IFIgduG7m2kgU1FMIHNlcnZlcg0KIyBpbnN0YWxsLnBhY2thZ2VzKCdST0RCQycpDQpsaWJyYXJ5KFJPREJDKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoZXh0cmFmb250KQ0KbXlfZm9udCA8LSAiT2ZmaWNpbmFTYW5zSVRDIg0KDQoNCg0KIyB0ZXN0IDwtIG9kYmNDb25uZWN0KCd0ZXN0JywgDQojICAgICAgICAgICAgICAgICAgICAgdWlkID0gJ3NhJywgIyBVc2VyIFNRTCBzZXJ2ZXINCiMgICAgICAgICAgICAgICAgICAgICBwd2QgPSAnRGFuZ3Z1MjU3QCcpICMgIyBQYXNzd29yZCBTUUwgc2VydmVyDQojIA0KIyAjIEvDqW8gMSBi4bqjbmcgZOG7ryBsaeG7h3UgdOG7qyBTUUwgdsOgbyBSLi4uDQojIGRmIDwtIHNxbFF1ZXJ5KHRlc3QsInNlbGVjdCAqIEZyb20gUkZNX01PREVMLi5GQ1RfUkZNX01PREVMDQojICAgICAgICAgICAgICAgICAgICAgIHdoZXJlIDEgPSAxDQojICAgICAgICAgICAgICAgICAgICAgIGFuZCBNT05USF9JRCA9ICcyMDE4MDMzMScNCiMgICAgICAgICAgICAgICAgICAgICAgYW5kIENVU1RPTUVSX0dFTkRFUiBpbiAoJ01BTEUnLCdGRU1BTEUnKQ0KIyAgICAgICAgICAgICAgICAiKQ0KDQpkZjEgPC0gcmVhZFJEUyhmaWxlID0gJ0Q6XFxWVURUXFw4LlRSQUlOSU5HXFxDaGFydFxcUkZNX0RBVEUuUkREJykNCg0KIyBQYXJldG8gY2hhcnQNCmRmIDwtIA0KICBkZjEgJT4lIA0KICBncm91cF9ieShDVVNUT01FUl9TRUdNRU5UQVRJT04pICU+JSANCiAgc3VtbWFyaXNlKG1wZ19zdW0gPSBzdW0oQU1PVU5UX1RPVCkpICU+JSANCiAgdW5ncm91cCgpICU+JSANCiAgbXV0YXRlKG1wZ19zdW0gPSByb3VuZChtcGdfc3VtLzFlOSkpICU+JSANCiAgYXJyYW5nZShtcGdfc3VtKSAgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICBtdXRhdGUobXBnX2N1bXN1bSA9IGN1bXN1bShtcGdfc3VtKSwNCiAgICAgICAgIG1wZ190b3RhbCA9IHN1bShtcGdfc3VtKSkgJT4lIA0KICBtdXRhdGUobXBnX3BlcmNlbnQgPSBtcGdfY3Vtc3VtL21wZ190b3RhbCkgJT4lIA0KICBtdXRhdGUobXBnX2ludCA9IHNlcSgxOk5ST1coLikpKQ0KDQojIENoYXJ0DQpuciA8LSBucm93KGRmKQ0KTiA8LSBzdW0oZGYkbXBnX3N1bSkNCg0KZGZfdGlja3MgPC0gZGF0YS5mcmFtZSh4dGljazAgPSByZXAobnIgKy41NSwgMTEpLCB4dGljazEgPSByZXAobnIgKy41OSwgMTEpLA0KICAgICAgICAgICAgICAgICAgICAgICB5dGljayA9IHNlcSgwLCBOLCBOLzEwKSkNCnkyIDwtIHBhc3RlMChzZXEoMCwgMTAwLCAxMCksICclJykNCg0KZ2dwbG90KGRmLCBhZXMoeCA9IHJlb3JkZXIoQ1VTVE9NRVJfU0VHTUVOVEFUSU9OLG1wZ19jdW1zdW0pLCB5ID0gbXBnX3N1bSkpICsNCiAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBmaWxsID0gIiMyMzU3NkUiLCB3aWR0aCA9IDAuNSkgKw0KICBnZW9tX2xpbmUoYWVzKHggPSBtcGdfaW50LCB5ID0gbXBnX2N1bXN1bSksIGNvbG9yID0gIiM4QzNGNEQiKSArDQogIGdlb21fcG9pbnQoYWVzKHggPSByZW9yZGVyKENVU1RPTUVSX1NFR01FTlRBVElPTixtcGdfY3Vtc3VtKSwgeSA9IG1wZ19jdW1zdW0pLCBjb2xvciA9ICIjOEMzRjREIiwgcGNoID0gMTkpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcz1zZXEoMCwgTiwgTi8xMCksIGxpbWl0cz1jKC0uMDIgKiBOLCBOICogMS4wMikpICsNCiAgc2NhbGVfeF9kaXNjcmV0ZShicmVha3MgPSBkZiRDVVNUT01FUl9TRUdNRU5UQVRJT04pICsNCiAgZ3VpZGVzKGZpbGwgPSBGQUxTRSwgY29sb3IgPSBGQUxTRSkgKw0KICBhbm5vdGF0ZSgicmVjdCIsIHhtaW4gPSBuciArIC41NSwgeG1heCA9IG5yICsgMSwgeW1pbiA9IC0uMDIgKiBOLCB5bWF4ID0gTiAqIDEuMDIsDQogICAgICAgICAgIGZpbGwgPSAiI0VGRjJGNCIpICsNCiAgYW5ub3RhdGUoInRleHQiLCB4ID0gbnIgKyAuOCwgeSA9IHNlcSgwLCBOLCBOLzEwKSwgbGFiZWwgPSB5Miwgc2l6ZSA9IDMsIGZhbWlseT0ic2VyaWYiKSArDQogICMgZ2VvbV9zZWdtZW50KHggPSBuciArIC41NSwgeGVuZCA9IG5yICsgLjU1LCB5ID0gLS4wMiAqIE4sIHllbmQgPSBOICogMS4wMiwgY29sb3IgPSAiZ3JleTUwIikgKw0KICAjIGdlb21fc2VnbWVudChkYXRhID0gZGZfdGlja3MsIGFlcyh4ID0geHRpY2swLCB5ID0geXRpY2ssIHhlbmQgPSB4dGljazEsIHllbmQgPSB5dGljaykpICsNCiAgbGFicyh0aXRsZSA9ICJUXHUwMEQ0XHUwMzA5TkcgRE9BTkggU1x1MDBENFx1MDMwMSBHSUFPIERJXHUwMzIzQ0hcbiBUSEVPIFRcdTAxQUZcdTAzMDBORyBQSFx1MDBDMk4gS0hVXHUwMzAxQyIsIA0KICAgICAgIHkgPSAiIiwgDQogICAgICAgeCA9ICIiLA0KICAgICAgIHN1YnRpdGxlID0gIlRoYVx1MDMwMW5nIGJhXHUwMzAxbyBjYVx1MDMwMW86IDMxLTAzLTIwMTgiKSArDQogIHRoZW1lX2J3KCkgKw0KICB0aGVtZSh0ZXh0ID0gZWxlbWVudF90ZXh0KGZhbWlseSA9ICdzZXJpZicpKSArDQogIHRoZW1lKGF4aXMudGl0bGUgPSBlbGVtZW50X3RleHQoZmFtaWx5ID0gbXlfZm9udCxzaXplID0gMTApKSArDQogIHRoZW1lKHBhbmVsLmJhY2tncm91bmQgPSBlbGVtZW50X2JsYW5rKCksIA0KICAgICAgICBwYW5lbC5ncmlkID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBheGlzLnRpY2tzID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICAjIGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChzaXplID0gMTAsIGZhY2UgPSAnYm9sZCcsYW5nbGUgPSA5MCwgdmp1c3QgPSAtMC4wKSwNCiAgICAgICAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBwbG90Lm1hcmdpbiA9IHVuaXQoYygxLDQsMSw0KSwgImNtIiksDQogICAgICAgIGF4aXMudGl0bGUgPSBlbGVtZW50X3RleHQoZmFtaWx5ID0gbXlfZm9udCxzaXplID0gMTYpLA0KICAgICAgICB0ZXh0ID0gZWxlbWVudF90ZXh0KGZhbWlseSA9ICdzZXJpZicpKSArDQogIHRoZW1lKHRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSAxMCksDQogICAgICAgIHBsb3Quc3VidGl0bGU9ZWxlbWVudF90ZXh0KHNpemUgPSAxMiwgaGp1c3Q9MC43NywgZmFjZSA9ICdpdGFsaWMnLCBjb2xvcj0iYmxhY2siKSkgKw0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41LGZhY2UgPSAnYm9sZCcpKSArDQogIHRoZW1lKHBsb3QuYmFja2dyb3VuZCA9IGVsZW1lbnRfcmVjdChmaWxsID0gIiNFRkYyRjQiLCBjb2xvciA9IE5BKSkgKyANCiAgdGhlbWUocGFuZWwuYmFja2dyb3VuZCA9IGVsZW1lbnRfcmVjdChmaWxsID0gIiNFRkYyRjQiLCBjb2xvciA9IE5BKSkgKw0KICB0aGVtZShsZWdlbmQuYmFja2dyb3VuZCA9IGVsZW1lbnRfcmVjdChmaWxsID0gIiNFRkYyRjQiLCBjb2xvciA9IE5BKSkNCg0KDQpgYGANCg0KDQoNCiMgUENEIMSR4buDIHRo4buxYyBoaeG7h24gcGjDom4gdMOtY2ggUkZNIHRyw6puIFNRTA0KDQpgYGB7U1FMfQ0KDQpVU0UgW1JGTV9NT0RFTF0NCkdPDQovKioqKioqIE9iamVjdDogIFN0b3JlZFByb2NlZHVyZSBbZGJvXS5bUENEX0RJTV9DT0hPUlRfVE9UQUxdICAgIFNjcmlwdCBEYXRlOiA1LzI3LzIwMTkgMTE6NTA6NTUgQU0gKioqKioqLw0KU0VUIEFOU0lfTlVMTFMgT04NCkdPDQpTRVQgUVVPVEVEX0lERU5USUZJRVIgT04NCkdPDQotLSA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCi0tIEF1dGhvcjoJCTxWVSBEQU5HLD4NCi0tIENyZWF0ZSBkYXRlOiA8MjAxOS8wNS8xNywsPg0KLS0gRGVzY3JpcHRpb246CTxDT0hPUlQgVE9UQUwsLD4NCi0tIFJlZjogaHR0cHM6Ly9yc3R1ZGlvLXB1YnMtc3RhdGljLnMzLmFtYXpvbmF3cy5jb20vMzY1MTg0XzkwNGM0LyoNCjM2OTU4NmU0OWZjOGZhMDhhZGNhZTFkNTU5ZC5odG1sDQoNCi0tID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KQUxURVIgUFJPQ0VEVVJFIFtkYm9dLltQQ0RfRElNX0NPSE9SVF9UT1RBTF0gDQoJLS0gQWRkIHRoZSBwYXJhbWV0ZXJzIGZvciB0aGUgc3RvcmVkIHByb2NlZHVyZSBoZXJlDQoJQERBVEUgVkFSQ0hBUig4KQ0KQVMNCkJFR0lODQoJU0VUIE5PQ09VTlQgT047DQoNCiAgICAtLSBJbnNlcnQgc3RhdGVtZW50cyBmb3IgcHJvY2VkdXJlIGhlcmUNCg0KLS0gU1RFUCAxOiBU4bqgTyBEQU5IIFPDgUNIIEtIw4FDSCBIw4BORyBCQU4gxJDhuqZVIC0tLSBUSMOBTkcgMDEvMDEvMjAxNyAtIDMwLzExLzIwMTcgKFJFUE9SVF9NT05USCAyMDE3LzEyKQ0KRFJPUCBUQUJMRSBJRiBFWElTVFMgI3N0ZXAxX2N1c3RvbWVyX29yaWdpbmFsDQoNClNFTEVDVCBESVNUSU5DVCBDVVNUT01FUl9DQVJEDQpJTlRPICNzdGVwMV9jdXN0b21lcl9vcmlnaW5hbA0KRlJPTSBEQVRBX1JBVy4uY2FyZF90cmFuc2FjdGlvbl9zYl8yMDE3MDFfMjAxODAzDQpXSEVSRSAxID0gMQ0KQU5EIERBWUlEIEJFVFdFRU4gIERBVEVBREQobSwgREFURURJRkYobSwgMCwgREFURUFERChtb250aCwgLTExLCBAREFURSkpLCAwKSAgQU5EIEVPTU9OVEgoREFURUFERChtb250aCwgLTExLCBAREFURSkpDQotLUFORCBEQVlJRCBCRVRXRUVOICBEQVRFQUREKG1vbnRoLCAtMTIsIEBEQVRFKSBBTkQgREFURUFERChtb250aCwgLTExLCBAREFURSkNCg0KLS0gQsav4buaQyAyOiBU4bqgTyBDT0hPUlQgVFJPTkcgMTIgVEjDgU5HDQoNCkRST1AgVEFCTEUgSUYgRVhJU1RTICNTVEVQMl9DVVNUT01FUl9DT0hPUlQJOw0KDQpTRUxFQ1QgDQoJQ09OVkVSVChWQVJDSEFSKDYpLERBWUlELDExMikgQVMgQ09IT1JUX0lELA0KICAgIENPVU5UKERJU1RJTkNUIENVU1RPTUVSX0NBUkQpIEFTIE5VTUJFUiwNCglDT05WRVJUKFZBUkNIQVIoNiksQERBVEUsMTEyKSBNT05USF9JRA0KDQpJTlRPICNTVEVQMl9DVVNUT01FUl9DT0hPUlQJDQpGUk9NIERBVEFfUkFXLi5jYXJkX3RyYW5zYWN0aW9uX3NiXzIwMTcwMV8yMDE4MDMgQQ0KV0hFUkUgMSA9IDENCkFORCBEQVlJRCBCRVRXRUVOICBEQVRFQUREKG0sIERBVEVESUZGKG0sIDAsIERBVEVBREQobW9udGgsIC0xMSwgQERBVEUpKSwgMCkgQU5EIEBEQVRFDQoNCi0tQU5EIERBWUlEIEJFVFdFRU4gIERBVEVBREQobW9udGgsIC0xMiwgQERBVEUpIEFORCBAREFURQ0KLS1BTkQgWUVBUihEQVlJRCkgPSAyMDE3DQpBTkQgQ1VTVE9NRVJfQ0FSRCBJTiAoU0VMRUNUIENVU1RPTUVSX0NBUkQgZnJvbSAjc3RlcDFfY3VzdG9tZXJfb3JpZ2luYWwpDQpHUk9VUCBCWSBDT05WRVJUKFZBUkNIQVIoNiksREFZSUQsMTEyKQ0KT1JERVIgQlkgQ09VTlQoRElTVElOQ1QgQ1VTVE9NRVJfQ0FSRCkgREVTQzsNCg0KLyoNCkRST1AgVEFCTEUgSUYgRVhJU1RTIFJGTV9NT0RFTC4uRElNX0NPSE9SVCANCg0KQ1JFQVRFIFRBQkxFIFJGTV9NT0RFTC4uRElNX0NPSE9SVCAoDQoJTU9OVEhfSUQgVkFSQ0hBUig4KSwNCglDT0hPUlRfSUQgVkFSQ0hBUig2KSwNCglSRVRFTlRJT04gRkxPQVQsDQoJUkVURU5USU9OX1JBVEUgRkxPQVQNCgkpDQoJKi8NCkRFTEVURSBGUk9NIFJGTV9NT0RFTC4uRElNX0NPSE9SVCBXSEVSRSBNT05USF9JRCA9IAlAREFURQ0KDQpJTlNFUlQgSU5UTyBSRk1fTU9ERUwuLkRJTV9DT0hPUlQgKA0KCU1PTlRIX0lELA0KCUNPSE9SVF9JRCwNCglSRVRFTlRJT04NCgkpDQoNClNFTEVDVCAgDQoJQERBVEUgTU9OVEhfSUQsDQoJQ09IT1JUX0lELA0KCU5VTUJFUg0KRlJPTSAjU1RFUDJfQ1VTVE9NRVJfQ09IT1JUCQ0KDQotLSBUw41OSCBSRVRFTlRJT05fUkFURQ0KDQpVUERBVEUgUkZNX01PREVMLi5ESU1fQ09IT1JUIA0KU0VUIFJFVEVOVElPTl9SQVRFID0gUk9VTkQoQS5SRVRFTlRJT04vQi5SRVRFTlRJT05fTUFYLDIpDQpGUk9NIFJGTV9NT0RFTC4uRElNX0NPSE9SVCAgQSwgKFNFTEVDVCBNQVgoUkVURU5USU9OKSBSRVRFTlRJT05fTUFYIEZST00gUkZNX01PREVMLi5ESU1fQ09IT1JUIFdIRVJFIE1PTlRIX0lEID0gQERBVEUgKSBCDQpXSEVSRSBBLk1PTlRIX0lEID0gQERBVEUNCkVORA0KDQoqLw0KYGBgDQoNCg==