El siguiente ejercicio presenta un caso de aplicación de machine learning, el empleo de aprendizaje estadístico automatizado para identificar patrones en grandes volúmenes de datos. El machine learning (de aquí en más ML) es utilizado en infinidad de campos debido a su creciente facilidad de uso y capacidad -en ciertos contextos- para predecir resultados con alta precisión.

El ejercicio fue adaptado de un tutorial realizado por Cam Nugent. Utiliza como fuente datos un dataset que contiene el valor mediano de las viviendas en California, EEUU, de acuerdo a un censo de 1990. ¡Es casi información arqueológica!

Habitantes de California en los ’90 con sus vestimentas nativas

A pesar de no contener datos de factura reciente, el dataset ha aparecido en varios textos introductorios debido a que no contiene información sensible, es de dominio público, y sus variables son auto-explicativas. Además, su tamaño resulta adecuado: suficiente para no ser considerado “de juguete”, pero no tan grande como para ser engorroso.

Cada fila representa un conjunto de manzanas, o “Census block groups”. Por comodidad, nosotros les llamaremos distritos. El objetivo del ejercicio es predecir el valor medio de las viviendas de cada distrito en función de sus otros atributos conocidos: cuánta gente vive allí, donde esta localizado, si queda cerca del mar, la antigüedad de sus casas, etc.

Allá vamos.

Paso 0: Cargar paquetes

Además de las funciones de R “base”, vamos a usar las del paquete tidyverse para procesar y visualizar nuestros datos, y las de randomForest, para aplicar el algoritmo de ML homónimo, que es relativamente simple y a la vez efectivo.

#install.packages("tidyverse")
library(tidyverse)
#install.packages("randomForest")
library(randomForest)

Paso 1: Cargar los datos

Descargamos el dataset del siguiente modo:

vivienda <- read_csv("http://bitsandbricks.github.io/data/housing.csv")

Paso 2: Examinar los datos

Echamos un vistazo a las primeras filas,

head(vivienda)
## # A tibble: 6 x 10
##   longitud latitud mediana_antigue… total_habitacio… total_dormitori…
##      <dbl>   <dbl>            <dbl>            <dbl>            <dbl>
## 1    -122.    37.9               41              880              129
## 2    -122.    37.9               21             7099             1106
## 3    -122.    37.8               52             1467              190
## 4    -122.    37.8               52             1274              235
## 5    -122.    37.8               52             1627              280
## 6    -122.    37.8               52              919              213
## # … with 5 more variables: poblacion <dbl>, hogares <dbl>,
## #   mediana_ingresos <dbl>, mediana_valor_vivienda <dbl>,
## #   proximidad_oceano <chr>

Y extraemos un resumen del contenido

summary(vivienda)
##     longitud         latitud      mediana_antiguedad_viviendas
##  Min.   :-124.3   Min.   :32.54   Min.   : 1.00               
##  1st Qu.:-121.8   1st Qu.:33.93   1st Qu.:18.00               
##  Median :-118.5   Median :34.26   Median :29.00               
##  Mean   :-119.6   Mean   :35.63   Mean   :28.64               
##  3rd Qu.:-118.0   3rd Qu.:37.71   3rd Qu.:37.00               
##  Max.   :-114.3   Max.   :41.95   Max.   :52.00               
##                                                               
##  total_habitaciones total_dormitorios   poblacion        hogares      
##  Min.   :    2      Min.   :   1.0    Min.   :    3   Min.   :   1.0  
##  1st Qu.: 1448      1st Qu.: 296.0    1st Qu.:  787   1st Qu.: 280.0  
##  Median : 2127      Median : 435.0    Median : 1166   Median : 409.0  
##  Mean   : 2636      Mean   : 537.9    Mean   : 1425   Mean   : 499.5  
##  3rd Qu.: 3148      3rd Qu.: 647.0    3rd Qu.: 1725   3rd Qu.: 605.0  
##  Max.   :39320      Max.   :6445.0    Max.   :35682   Max.   :6082.0  
##                     NA's   :207                                       
##  mediana_ingresos  mediana_valor_vivienda proximidad_oceano 
##  Min.   : 0.4999   Min.   : 14999         Length:20640      
##  1st Qu.: 2.5634   1st Qu.:119600         Class :character  
##  Median : 3.5348   Median :179700         Mode  :character  
##  Mean   : 3.8707   Mean   :206856                           
##  3rd Qu.: 4.7432   3rd Qu.:264725                           
##  Max.   :15.0001   Max.   :500001                           
## 

Obsérvese que la variable “total_dormitorios” exhibe 207 datos faltantes, representados con NA. Luego volveremos a éste tema.

Y espiemos la distribución de algunas variables, como

ingresos:

ggplot() +
    geom_histogram(data = vivienda, aes(x = mediana_ingresos))

antigüedad de las viviendas:

ggplot() +
    geom_histogram(data = vivienda, aes(x = mediana_antiguedad_viviendas))

población:

ggplot() +
    geom_histogram(data = vivienda, aes(x = poblacion))

… o proximidad al océano (dado que se trata de una variable categórica en lugar de continua, usamos un gráfico de barras en lugar de un histograma):

ggplot() +
    geom_bar(data = vivienda, aes(x = proximidad_oceano))

Paso 3: Limpiar los datos

Imputar valores faltantes

Es habitual que los algoritmos empleados para ML no acepten datos faltantes. Es por eso que la limpieza básica de un dataset casi siempre incluye la imputación de datos no disponibles, evitando descartar por incompletas filas que contienen información valiosa en sus campos si disponibles.

Habíamos notado que la variable “total_dormitorios” tiene faltantes en 207 filas. Para esos casos, usaremos una de las técnicas de imputación mas naif que hay: donde haya un valor desconocido, lo reemplazaremos por la mediana de los valores generales.

vivienda <- vivienda %>% 
    mutate(total_dormitorios = ifelse(is.na(total_dormitorios),
                                       median(total_dormitorios, na.rm = TRUE),
                                       total_dormitorios))
    
summary(vivienda)
##     longitud         latitud      mediana_antiguedad_viviendas
##  Min.   :-124.3   Min.   :32.54   Min.   : 1.00               
##  1st Qu.:-121.8   1st Qu.:33.93   1st Qu.:18.00               
##  Median :-118.5   Median :34.26   Median :29.00               
##  Mean   :-119.6   Mean   :35.63   Mean   :28.64               
##  3rd Qu.:-118.0   3rd Qu.:37.71   3rd Qu.:37.00               
##  Max.   :-114.3   Max.   :41.95   Max.   :52.00               
##  total_habitaciones total_dormitorios   poblacion        hogares      
##  Min.   :    2      Min.   :   1.0    Min.   :    3   Min.   :   1.0  
##  1st Qu.: 1448      1st Qu.: 297.0    1st Qu.:  787   1st Qu.: 280.0  
##  Median : 2127      Median : 435.0    Median : 1166   Median : 409.0  
##  Mean   : 2636      Mean   : 536.8    Mean   : 1425   Mean   : 499.5  
##  3rd Qu.: 3148      3rd Qu.: 643.2    3rd Qu.: 1725   3rd Qu.: 605.0  
##  Max.   :39320      Max.   :6445.0    Max.   :35682   Max.   :6082.0  
##  mediana_ingresos  mediana_valor_vivienda proximidad_oceano 
##  Min.   : 0.4999   Min.   : 14999         Length:20640      
##  1st Qu.: 2.5634   1st Qu.:119600         Class :character  
##  Median : 3.5348   Median :179700         Mode  :character  
##  Mean   : 3.8707   Mean   :206856                           
##  3rd Qu.: 4.7432   3rd Qu.:264725                           
##  Max.   :15.0001   Max.   :500001

Nos hemos librado de los NA.

Normalizar variables

También deberíamos evaluar si algunas variables requieren ser normalizadas. En este caso las candidatas son “total_habitaciones” y “total_dormitorios”, ya que es de esperarse que sean mayores donde hay más casas -naturalmente- pero eso no nos indica si las casas tienden a ser más o menos grandes que la media. Lo que haremos entonces es dividir la cantidad total de habitaciones por la cantidad de viviendas en cada distrito, y nos quedaremos con esas métricas en lugar de las originales:

vivienda <- vivienda %>% 
    mutate(promedio_habitaciones = total_habitaciones/hogares,
           promedio_dormitorios = total_dormitorios/hogares) %>%
    select(-total_habitaciones, -total_dormitorios)

head(vivienda)
## # A tibble: 6 x 10
##   longitud latitud mediana_antigue… poblacion hogares mediana_ingresos
##      <dbl>   <dbl>            <dbl>     <dbl>   <dbl>            <dbl>
## 1    -122.    37.9               41       322     126             8.33
## 2    -122.    37.9               21      2401    1138             8.30
## 3    -122.    37.8               52       496     177             7.26
## 4    -122.    37.8               52       558     219             5.64
## 5    -122.    37.8               52       565     259             3.85
## 6    -122.    37.8               52       413     193             4.04
## # … with 4 more variables: mediana_valor_vivienda <dbl>,
## #   proximidad_oceano <chr>, promedio_habitaciones <dbl>,
## #   promedio_dormitorios <dbl>

Del mismo modo sería mejor tener densidad de población en lugar de población total, pero sin saber el área que ocupa cada distrito no vamos a poder obtener la densidad.

Codificar variables categóricas

Ahora nos encargaremos de las variables categóricas, aquí representadas por “proximidad_oceano”. Rara vez es posible utilizar columnas categóricas en modelos estadísticos, pero por suerte podemos recurrir a la alternativa de reemplazar una columna de datos categóricos por una serie de variables binarias, o “dummy”.

Es decir, en lugar de…

caso proximidad_oceano
A CERCANO A BAHIA
B ALEJADO DEL OCEANO
C CERCANO A OCEANO

… tendríamos algo así como:

caso CERCANO A BAHIA ALEJADO DEL OCEANO ALEJADO DEL OCEANO
A 1 0 0
B 0 1 0
C 0 0 1

Como buen lenguaje creado por y para practicantes del análisis estadístico, R trae una función específica para realizar ésta tarea, model.matrix(). Se usa así:

matriz_categorias_oceano <- model.matrix(data = vivienda, ~ proximidad_oceano - 1)

y el resultado es, ni más ni menos, una matriz de variables binarias que representan las categorías originales:

head(matriz_categorias_oceano)
##   proximidad_oceanoALEJADO_DEL_OCEANO proximidad_oceanoCERCANO_A_BAHIA
## 1                                   0                                1
## 2                                   0                                1
## 3                                   0                                1
## 4                                   0                                1
## 5                                   0                                1
## 6                                   0                                1
##   proximidad_oceanoCERCANO_A_OCEANO proximidad_oceanoISLA
## 1                                 0                     0
## 2                                 0                     0
## 3                                 0                     0
## 4                                 0                     0
## 5                                 0                     0
## 6                                 0                     0
##   proximidad_oceanoMENOS_1H_VIAJE
## 1                               0
## 2                               0
## 3                               0
## 4                               0
## 5                               0
## 6                               0

Pronto agregaremos la matriz a nuestro dataframe, pero antes terminemos con otros ajustes pendientes.

Unificar la escala de las variables numéricas

Éste paso siempre es necesario cuando estamos trabajando con variables que utilizan distintas unidades de medida. Aquí tenemos personas, hogares, habitaciones, dormitorios, años de antigüedad… de todo. Muchos algoritmos asumen que todas las variables tienen escalas comparables, lo cual genera problemas con las que alcanzan valores relativamente muy altos (como población, que llegar a decenas de miles) versus las que tienen rangos mucho menores (como antigüedad en años mediana, que “sólo” llega a 52). Si las dejásemos así, varias de las técnicas habituales del ML adjudicarían mucho más peso a las variables con números grandes, “despreciando” a las que por su naturaleza se mueven en rango más reducidos.

En todo caso, no importa lo disimiles que sean las unidades de medida, la solución es simple: convertimos todas las variables a la famosa “distribución Z”, o función de estandarización, que convierte variables a una escala sin unidad de medida, que expresa cada valor como la cantidad de desvíos estándar que lo alejan de la media. Expresar todas las variables numéricas en forma de “z scores”, o “valores z”, las hace directamente comparables entre sí.

En R disponemos de la función scale(), que obtiene los z-scores. Tomaremos entonces nuestro dataframe y usaremos mutate_all() para aplicar una función a todas las columnas restantes de un tirón. Eso si, quitando antes ciertas variables: las variables categóricas (que no tiene sentido pasar a z-scores porque no son variables numéricas), y la variable que estamos intentando predecir, ya que su escala no afecta los modelos y podemos dejarla en su formato original fácil de interpretar.

vivienda <- vivienda %>% 
    select(-proximidad_oceano, -mediana_valor_vivienda) %>% 
    mutate_all(funs(scale)) %>% 
    mutate(mediana_valor_vivienda = vivienda$mediana_valor_vivienda)

Y obsérvese que scale() mediante, ahora todas las variables tienen promedio igual a 0, y se mueven en el mismo rango

summary(vivienda)
##      longitud.V1           latitud.V1      mediana_antiguedad_viviendas.V1
##  Min.   :-2.3859345   Min.   :-1.4475329   Min.   :-2.1961273             
##  1st Qu.:-1.1131820   1st Qu.:-0.7967694   1st Qu.:-0.8453727             
##  Median : 0.5389006   Median :-0.6422715   Median : 0.0286450             
##  Mean   : 0.0000000   Mean   : 0.0000000   Mean   : 0.0000000             
##  3rd Qu.: 0.7784775   3rd Qu.: 0.9729330   3rd Qu.: 0.6642943             
##  Max.   : 2.6252165   Max.   : 2.9579960   Max.   : 1.8561366             
##     poblacion.V1         hogares.V1      mediana_ingresos.V1
##  Min.   :-1.256092   Min.   :-1.303952   Min.   :-1.774256  
##  1st Qu.:-0.563795   1st Qu.:-0.574216   1st Qu.:-0.688102  
##  Median :-0.229126   Median :-0.236810   Median :-0.176791  
##  Mean   : 0.000000   Mean   : 0.000000   Mean   : 0.000000  
##  3rd Qu.: 0.264489   3rd Qu.: 0.275836   3rd Qu.: 0.459295  
##  Max.   :30.249597   Max.   :14.601166   Max.   : 5.858144  
##  promedio_habitaciones.V1 promedio_dormitorios.V1 mediana_valor_vivienda
##  Min.   :-1.85227         Min.   :-1.88710        Min.   : 14999        
##  1st Qu.:-0.39944         1st Qu.:-0.18373        1st Qu.:119600        
##  Median :-0.08078         Median :-0.10026        Median :179700        
##  Mean   : 0.00000         Mean   : 0.00000        Mean   :206856        
##  3rd Qu.: 0.25196         3rd Qu.:-0.00172        3rd Qu.:264725        
##  Max.   :55.16190         Max.   :63.49837        Max.   :500001

… y sin que esto haya cambiado la forma de las distribuciones. Compárense ahora con las que examinamos al inicio,

ingresos:

ggplot() +
    geom_histogram(data = vivienda, aes(x = mediana_ingresos))

antigüedad de las viviendas:

ggplot() +
    geom_histogram(data = vivienda, aes(x = mediana_antiguedad_viviendas))

población:

ggplot() +
    geom_histogram(data = vivienda, aes(x = poblacion))

¡las formas son iguales! no hemos hemos perdido “información” respecto a que tan típico o extremo es cada valor, y hemos ganado la posibilidad de comparar en forma directa todas las variables: si un distrito tiene un valor cercano a cero en población, y -digamos- más de 4 en ingresos, sabemos automáticamente que su población es parecida a la media de todos los distritos, pero sus ingresos son altísimos.

Consolidar todas las variables generadas ad-hoc en un sólo dataframe

Nos ha quedado por un lado un dataframe de variables numéricas estandarizadas, y por otro una matriz que representa la pertenencia de cada distrito a su categoría respecto a “proximidad al océano”.

Primero convertimos la matriz en dataframe (paso simple ya éstas estructura de datos son muy similares entre si), y luego unimos las columnas de ambos con la función cbind():

matriz_categorias_oceano <- as.data.frame(matriz_categorias_oceano)

vivienda <- vivienda %>% 
    cbind(matriz_categorias_oceano)
head(vivienda)
##    longitud  latitud mediana_antiguedad_viviendas  poblacion    hogares
## 1 -1.327803 1.052523                    0.9821189 -0.9744050 -0.9770092
## 2 -1.322812 1.043159                   -0.6070042  0.8614180  1.6699206
## 3 -1.332794 1.038478                    1.8561366 -0.8207575 -0.8436165
## 4 -1.337785 1.038478                    1.8561366 -0.7660095 -0.7337637
## 5 -1.337785 1.038478                    1.8561366 -0.7598283 -0.6291419
## 6 -1.337785 1.038478                    1.8561366 -0.8940491 -0.8017678
##   mediana_ingresos promedio_habitaciones promedio_dormitorios
## 1       2.34470896             0.6285442         -0.148510661
## 2       2.33218146             0.3270334         -0.248535936
## 3       1.78265622             1.1555925         -0.052900657
## 4       0.93294491             0.1569623         -0.053646030
## 5      -0.01288068             0.3447024         -0.038194658
## 6       0.08744452            -0.2697231          0.005232996
##   mediana_valor_vivienda proximidad_oceanoALEJADO_DEL_OCEANO
## 1                 452600                                   0
## 2                 358500                                   0
## 3                 352100                                   0
## 4                 341300                                   0
## 5                 342200                                   0
## 6                 269700                                   0
##   proximidad_oceanoCERCANO_A_BAHIA proximidad_oceanoCERCANO_A_OCEANO
## 1                                1                                 0
## 2                                1                                 0
## 3                                1                                 0
## 4                                1                                 0
## 5                                1                                 0
## 6                                1                                 0
##   proximidad_oceanoISLA proximidad_oceanoMENOS_1H_VIAJE
## 1                     0                               0
## 2                     0                               0
## 3                     0                               0
## 4                     0                               0
## 5                     0                               0
## 6                     0                               0

Ya tenemos tenemos los datos limpios y en orden.

Paso 4: Crear sets de entrenamiento y de testeo

Para poder evaluar la calidad de un modelo predictivo, es práctica común dividir los datos disponibles en dos porciones. Una parte será utilizada para “entrenar” el modelo de ML, es decir se le permitirá al algoritmo acceder a esos datos para establecer la forma en que cada variable predictora incide en la que se quiere predecir. El resto será preservado y utilizado para “tomarle examen” al modelo: se le mostraran sólo las variables predictoras de esos datos, pidiendo al modelo una predicción del valor a estimar para cada una. Por último, contrastando aciertos y errores, se podrá establecer el grado de precisión del modelo.

Incluso podríamos tener varios modelos distintos, obtenidos con distintas técnicas de ML. No es difícil, ya que una vez que los datos han sido obtenidos y preparados, nada impide usarlos como insumo de distintos algoritmos. En ese caso, se puede comparar la performance de los distintos modelos evaluando cual acierta mejor con la data de testeo.

Definamos entonces cuales filas van al set de entrenamiento, y cuáles al de testeo, eligiéndolas al azar. De acuerdo a distintas recetas, a veces se separa el 90% de los datos para entrenamiento y el resto para testeo, otras veces es mitad y mitad… ya que siempre es más o menos arbitrario, aquí usaremos el 80% para entrenar, y el 20% para testear.

#definimos a mano la "semilla" de aleatorización para obtener resultados reproducibles
set.seed(1810)

Tomamos al azar el 80% de las posiciones entre 1 y la cantidad total de filas de nuestro dataset

seleccion <- sample(1:nrow(vivienda), size = nrow(vivienda) * 0.8)

entrenamiento <- vivienda %>% 
    filter(row_number() %in% seleccion)

# el testeo es el set opuesto - aquellas filas cuya posición no está entre las seleccionadas
# el operador ! convierte una proposición en negativa 

testeo <- vivienda %>% 
    filter(!(row_number() %in% seleccion))

Ahora si, por fin, apliquemos un poco de machine learning.

Paso 5: Entrenar y testear un modelo

Random Forest, una implementación de árboles de decisión como los ilustrados en “Una introducción visual al machine learning”:

modelo_RF <- randomForest(data = entrenamiento, mediana_valor_vivienda ~ .,
                         ntree = 500,
                         importance = TRUE)

# el parámetro "importance": Define si el modelo estimará la importancia relativa de cada predictor en la calidad de la predicción -es decir, cuales variables son más importantes para predecir

# resultados:
modelo_RF
## 
## Call:
##  randomForest(formula = mediana_valor_vivienda ~ ., data = entrenamiento,      ntree = 500, importance = TRUE) 
##                Type of random forest: regression
##                      Number of trees: 500
## No. of variables tried at each split: 4
## 
##           Mean of squared residuals: 2457430763
##                     % Var explained: 81.57

Según dice allí, el modelo puede explicar más del 80% de la varianza de valores encontrada entre los distritos californianos en base a las variables predictoras que empleamos.

¿Qué tiene dentro el modelo?

summary(modelo_RF)
##                 Length Class  Mode     
## call                5  -none- call     
## type                1  -none- character
## predicted       16512  -none- numeric  
## mse               500  -none- numeric  
## rsq               500  -none- numeric  
## oob.times       16512  -none- numeric  
## importance         26  -none- numeric  
## importanceSD       13  -none- numeric  
## localImportance     0  -none- NULL     
## proximity           0  -none- NULL     
## ntree               1  -none- numeric  
## mtry                1  -none- numeric  
## forest             11  -none- list     
## coefs               0  -none- NULL     
## y               16512  -none- numeric  
## test                0  -none- NULL     
## inbag               0  -none- NULL     
## terms               3  terms  call

De todo! Por ejemplo, “type” nos emite confirmar qué tipo de análisis realizó: Fue de regresión en este caso, peor podría haber sido otro, como clasificación (cuando se predice un atributo categórico en lugar de una variable continua):

modelo_RF$type
## [1] "regression"

O “importance”, que contiene un ranking con la importancia relativa de cada predictor, es decir cuáles son los que más ayudan a estimar el valor a predecir:

modelo_RF$importance
##                                        %IncMSE IncNodePurity
## longitud                            6562596634  2.507078e+13
## latitud                             5307845606  2.205102e+13
## mediana_antiguedad_viviendas        1126272280  9.996510e+12
## poblacion                           1047763842  7.549644e+12
## hogares                             1172196457  7.995806e+12
## mediana_ingresos                    8630114201  7.465523e+13
## promedio_habitaciones               1867232899  2.031336e+13
## promedio_dormitorios                 429802972  7.674806e+12
## proximidad_oceanoALEJADO_DEL_OCEANO 3906233140  3.043369e+13
## proximidad_oceanoCERCANO_A_BAHIA     391425489  1.329375e+12
## proximidad_oceanoCERCANO_A_OCEANO    479630868  2.170891e+12
## proximidad_oceanoISLA                   258890  4.775854e+10
## proximidad_oceanoMENOS_1H_VIAJE     1649914818  4.476848e+12

La columna “%IncMSE” representa el porcentaje de error promedio, la magnitud en la que el valor predicho por el modelo difiere del valor observado, cuando cada predictor se retira del modelo (es decir, cuanto peor sería la predicción si no se usara). Por eso los números mayores están asociados a los predictores de más peso, que en este caso son “mediana_ingresos”, y luego longitud y latitud. Además de encontrar la correlación esperable entre nivel de ingresos de una población y el valor de sus viviendas, nuestro modelo ha encontrado que la ubicación es la clave del valor de la propiedad… y sin saber nada de geografía ni urbanismo.

En “predicted” tenemos la mediana del valor de la vivienda predicha para cada distrito:

head(modelo_RF$predicted)
##        1        2        3        4        5        6 
## 424089.7 402434.9 421956.7 332392.9 234152.2 223307.9

Aprovechando que dentro del modelo, “y” contiene los valores observados, evaluemos en forma gráfica cuánto se aproximan las predicciones de cada distrito al valor real (el observado) :

ggplot() +
    geom_point(aes(x = modelo_RF$predicted, y = modelo_RF$y), alpha = 0.3) 

Se ajusta bastante bien. Luego veremos una manera de cuantificar la precisión del modelo.

Midiendo la performance del modelo contra datos que no conoce

Veamos ahora como se comporta nuestro modelo cuando debe predecir valores de distritos que no se han utilizado para el entrenamiento, los que reservamos para el set de testeo.

predicciones_test <- predict(modelo_RF, newdata = testeo)

head(predicciones_test)
##        1        2        3        4        5        6 
## 241927.4 228608.7 136310.6 128209.6 161684.6 234114.8

En un gráfico:

ggplot() +
    geom_point(aes(x = predicciones_test, y = testeo$mediana_valor_vivienda), alpha = 0.3) 

Luce incluso mejor que el ajuste con los datos conocidos.

Comparando performance

Es práctico obtener un sólo número, un indicador simple que nos diga que tan bien predice el modelo, y así poder comparar distintos modelos entre si (o distintos datasets contra el mismo modelo) utilizando esa medida. En estadística es común el uso del RMSE como indicador de grado de ajuste, o “Root Mean Square Error” - la raíz cuadrada de la media de los errores al cuadrado.

El modelo incluye el MSE (o sea la suma de los errores al cuadrado) que surge de comparar predicciones con valores observados. Y en el caso de un random forest, que intenta muchos árboles distintos, varios MSEs resultantes: 500 en nuestro caso, uno por cada árbol trazado.

Tomamos la media de todos los MSE para obtener un valor general, y luego tomamos la raíz cuadrada para obtener el RMSE:

RMSE <- modelo_RF$mse %>% 
    mean() %>%
    sqrt()

RMSE
## [1] 50506.37

Eso significa que la diferencia promedio entre valor esperado y valor hallado para cada distrito fue de 5.050636810^{4} dólares.

Y en comparación, ¿qué tan bueno resultó el modelo cuando se aplicó a datos que no conocía?

RMSE_test <- sqrt(mean((predicciones_test - testeo$mediana_valor_vivienda)^2))

RMSE_test
## [1] 46875.33

Con un valor medio de error de 4.687532910^{4} dólares, el modelo ha funcionado muy bien con datos desconocidos, incluso mejorando levemente su performance respecto al set de training.

Esto indica que no sufre de “overfitting”, la condición de estar excesivamente ajustado a los datos con los que fue entrenado. Por eso el modelo no pierde precisión cuando lidia con datos nuevos.

Como despedida, volvamos al examen visual. Representamos en un gráfico cada valor predicho y cada valor observado para los datos de entrenamiento:

ggplot() +
    geom_point(aes(x = 1:length(predicciones_test), y = predicciones_test), 
               color = "salmon",
               alpha = .5,
               size = .5) +
    geom_point(aes(x = 1:nrow(testeo), y = testeo$mediana_valor_vivienda), 
               color = "lightblue",
               alpha = .5,
               size = .5) +
    labs(x = "valores predichos",
         y = "valores observados") +
    theme_minimal()

LS0tCnRpdGxlOiAiSW50cm9kdWNjacOzbiBhIE1hY2hpbmUgTGVhcm5pbmcgY29uIFIgKHkgVGlkeXZlcnNlKSIKYXV0aG9yOiAiQW50b25pbyBWw6F6cXVleiBCcnVzdCIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlCiAgcGRmX2RvY3VtZW50OiBkZWZhdWx0Ci0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmBgYAoKRWwgc2lndWllbnRlIGVqZXJjaWNpbyBwcmVzZW50YSB1biBjYXNvIGRlIGFwbGljYWNpw7NuIGRlIF9tYWNoaW5lIGxlYXJuaW5nXywgZWwgZW1wbGVvIGRlIGFwcmVuZGl6YWplIGVzdGFkw61zdGljbyBhdXRvbWF0aXphZG8gcGFyYSBpZGVudGlmaWNhciBwYXRyb25lcyBlbiBncmFuZGVzIHZvbMO6bWVuZXMgZGUgZGF0b3MuIEVsIG1hY2hpbmUgbGVhcm5pbmcgKGRlIGFxdcOtIGVuIG3DoXMgTUwpIGVzIHV0aWxpemFkbyBlbiBpbmZpbmlkYWQgZGUgY2FtcG9zIGRlYmlkbyBhIHN1IGNyZWNpZW50ZSBmYWNpbGlkYWQgZGUgdXNvIHkgY2FwYWNpZGFkIC1lbiBjaWVydG9zIGNvbnRleHRvcy0gcGFyYSBwcmVkZWNpciByZXN1bHRhZG9zIGNvbiBhbHRhIHByZWNpc2nDs24uCgpFbCBlamVyY2ljaW8gZnVlIGFkYXB0YWRvIGRlIHVuIFt0dXRvcmlhbCByZWFsaXphZG8gcG9yIENhbSBOdWdlbnRdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vY2FtbnVnZW50L2ludHJvZHVjdGlvbi10by1tYWNoaW5lLWxlYXJuaW5nLWluLXItdHV0b3JpYWwpLiBVdGlsaXphIGNvbW8gZnVlbnRlIGRhdG9zIHVuIFtkYXRhc2V0XShodHRwczovL3d3dy5rYWdnbGUuY29tL2NhbW51Z2VudC9jYWxpZm9ybmlhLWhvdXNpbmctcHJpY2VzL3ZlcnNpb24vMSkgcXVlIGNvbnRpZW5lIGVsIHZhbG9yIG1lZGlhbm8gZGUgbGFzIHZpdmllbmRhcyBlbiBDYWxpZm9ybmlhLCBFRVVVLCBkZSBhY3VlcmRvIGEgdW4gY2Vuc28gZGUgMTk5MC4gwqFFcyBjYXNpIGluZm9ybWFjacOzbiBhcnF1ZW9sw7NnaWNhIQoKIVtdKGh0dHBzOi8vYml0c2FuZGJyaWNrcy5naXRodWIuaW8vY2llbmNpYV9kZV9kYXRvc19nZW50ZV9zb2NpYWJsZS9pbWFnZW5lcy85MDIxMC5qcGcpCgpfSGFiaXRhbnRlcyBkZSBDYWxpZm9ybmlhIGVuIGxvcyAnOTAgY29uIHN1cyB2ZXN0aW1lbnRhcyBuYXRpdmFzXwoKCkEgcGVzYXIgZGUgbm8gY29udGVuZXIgZGF0b3MgZGUgZmFjdHVyYSByZWNpZW50ZSwgZWwgZGF0YXNldCBoYSBhcGFyZWNpZG8gZW4gdmFyaW9zIHRleHRvcyBpbnRyb2R1Y3RvcmlvcyBkZWJpZG8gYSBxdWUgbm8gY29udGllbmUgaW5mb3JtYWNpw7NuIHNlbnNpYmxlLCBlcyBkZSBkb21pbmlvIHDDumJsaWNvLCB5IHN1cyB2YXJpYWJsZXMgc29uIGF1dG8tZXhwbGljYXRpdmFzLiBBZGVtw6FzLCBzdSB0YW1hw7FvIHJlc3VsdGEgYWRlY3VhZG86IHN1ZmljaWVudGUgcGFyYSBubyBzZXIgY29uc2lkZXJhZG8gImRlIGp1Z3VldGUiLCBwZXJvIG5vIHRhbiBncmFuZGUgY29tbyBwYXJhIHNlciBlbmdvcnJvc28uCgpDYWRhIGZpbGEgcmVwcmVzZW50YSB1biBjb25qdW50byBkZSBtYW56YW5hcywgbyAiQ2Vuc3VzIGJsb2NrIGdyb3VwcyIuIFBvciBjb21vZGlkYWQsIG5vc290cm9zIGxlcyBsbGFtYXJlbW9zIGRpc3RyaXRvcy4gRWwgb2JqZXRpdm8gZGVsIGVqZXJjaWNpbyBlcyBwcmVkZWNpciBlbCB2YWxvciBtZWRpbyBkZSBsYXMgdml2aWVuZGFzIGRlIGNhZGEgZGlzdHJpdG8gZW4gZnVuY2nDs24gZGUgc3VzIG90cm9zIGF0cmlidXRvcyBjb25vY2lkb3M6IGN1w6FudGEgZ2VudGUgdml2ZSBhbGzDrSwgZG9uZGUgZXN0YSBsb2NhbGl6YWRvLCBzaSBxdWVkYSBjZXJjYSBkZWwgbWFyLCBsYSBhbnRpZ8O8ZWRhZCBkZSBzdXMgY2FzYXMsIGV0Yy4gCgpBbGzDoSB2YW1vcy4KCiMjIFBhc28gMDogQ2FyZ2FyIHBhcXVldGVzCgpBZGVtw6FzIGRlIGxhcyBmdW5jaW9uZXMgZGUgUiAiYmFzZSIsIHZhbW9zIGEgdXNhciBsYXMgZGVsIHBhcXVldGUgYHRpZHl2ZXJzZWAgcGFyYSBwcm9jZXNhciB5IHZpc3VhbGl6YXIgbnVlc3Ryb3MgZGF0b3MsIHkgbGFzIGRlIGByYW5kb21Gb3Jlc3RgLCBwYXJhIGFwbGljYXIgZWwgYWxnb3JpdG1vIGRlIE1MIGhvbcOzbmltbywgcXVlIGVzIHJlbGF0aXZhbWVudGUgc2ltcGxlIHkgYSBsYSB2ZXogZWZlY3Rpdm8uIAoKYGBge3J9CiNpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiKQpsaWJyYXJ5KHRpZHl2ZXJzZSkKI2luc3RhbGwucGFja2FnZXMoInJhbmRvbUZvcmVzdCIpCmxpYnJhcnkocmFuZG9tRm9yZXN0KQpgYGAKCgojIyBQYXNvIDE6IENhcmdhciBsb3MgZGF0b3MKCgpEZXNjYXJnYW1vcyBlbCBkYXRhc2V0IGRlbCBzaWd1aWVudGUgbW9kbzoKCmBgYHtyfQp2aXZpZW5kYSA8LSByZWFkX2NzdigiaHR0cDovL2JpdHNhbmRicmlja3MuZ2l0aHViLmlvL2RhdGEvaG91c2luZy5jc3YiKQpgYGAKCgojIyBQYXNvIDI6IEV4YW1pbmFyIGxvcyBkYXRvcwoKRWNoYW1vcyB1biB2aXN0YXpvIGEgbGFzIHByaW1lcmFzIGZpbGFzLAoKYGBge3J9CmhlYWQodml2aWVuZGEpCmBgYAoKWSBleHRyYWVtb3MgdW4gcmVzdW1lbiBkZWwgY29udGVuaWRvCgpgYGB7cn0Kc3VtbWFyeSh2aXZpZW5kYSkKYGBgCgpPYnPDqXJ2ZXNlIHF1ZSBsYSB2YXJpYWJsZSAidG90YWxfZG9ybWl0b3Jpb3MiIGV4aGliZSAyMDcgZGF0b3MgZmFsdGFudGVzLCByZXByZXNlbnRhZG9zIGNvbiBgTkFgLiBMdWVnbyB2b2x2ZXJlbW9zIGEgw6lzdGUgdGVtYS4KClkgZXNwaWVtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBhbGd1bmFzIHZhcmlhYmxlcywgY29tbwoKaW5ncmVzb3M6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IG1lZGlhbmFfaW5ncmVzb3MpKQpgYGAKCmFudGlnw7xlZGFkIGRlIGxhcyB2aXZpZW5kYXM6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IG1lZGlhbmFfYW50aWd1ZWRhZF92aXZpZW5kYXMpKQpgYGAKCnBvYmxhY2nDs246CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IHBvYmxhY2lvbikpCmBgYAoKLi4uIG8gcHJveGltaWRhZCBhbCBvY8OpYW5vIChkYWRvIHF1ZSBzZSB0cmF0YSBkZSB1bmEgdmFyaWFibGUgY2F0ZWfDs3JpY2EgZW4gbHVnYXIgZGUgY29udGludWEsIHVzYW1vcyB1biBncsOhZmljbyBkZSBiYXJyYXMgZW4gbHVnYXIgZGUgdW4gaGlzdG9ncmFtYSk6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9iYXIoZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IHByb3hpbWlkYWRfb2NlYW5vKSkKYGBgCgojIyBQYXNvIDM6IExpbXBpYXIgbG9zIGRhdG9zCgoKIyMjIEltcHV0YXIgdmFsb3JlcyBmYWx0YW50ZXMKCkVzIGhhYml0dWFsIHF1ZSBsb3MgYWxnb3JpdG1vcyBlbXBsZWFkb3MgcGFyYSBNTCBubyBhY2VwdGVuIGRhdG9zIGZhbHRhbnRlcy4gRXMgcG9yIGVzbyBxdWUgbGEgbGltcGllemEgYsOhc2ljYSBkZSB1biBkYXRhc2V0IGNhc2kgc2llbXByZSBpbmNsdXllIGxhIGltcHV0YWNpw7NuIGRlIGRhdG9zIG5vIGRpc3BvbmlibGVzLCBldml0YW5kbyBkZXNjYXJ0YXIgcG9yIGluY29tcGxldGFzIGZpbGFzIHF1ZSBjb250aWVuZW4gaW5mb3JtYWNpw7NuIHZhbGlvc2EgZW4gc3VzIGNhbXBvcyBzaSBkaXNwb25pYmxlcy4KCkhhYsOtYW1vcyBub3RhZG8gcXVlIGxhIHZhcmlhYmxlICJ0b3RhbF9kb3JtaXRvcmlvcyIgdGllbmUgZmFsdGFudGVzIGVuIDIwNyBmaWxhcy4gUGFyYSBlc29zIGNhc29zLCB1c2FyZW1vcyB1bmEgZGUgbGFzIHTDqWNuaWNhcyBkZSBpbXB1dGFjacOzbiBtYXMgX25haWZfIHF1ZSBoYXk6IGRvbmRlIGhheWEgdW4gdmFsb3IgZGVzY29ub2NpZG8sIGxvIHJlZW1wbGF6YXJlbW9zIHBvciBsYSBtZWRpYW5hIGRlIGxvcyB2YWxvcmVzIGdlbmVyYWxlcy4gCgpgYGB7cn0Kdml2aWVuZGEgPC0gdml2aWVuZGEgJT4lIAogICAgbXV0YXRlKHRvdGFsX2Rvcm1pdG9yaW9zID0gaWZlbHNlKGlzLm5hKHRvdGFsX2Rvcm1pdG9yaW9zKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVkaWFuKHRvdGFsX2Rvcm1pdG9yaW9zLCBuYS5ybSA9IFRSVUUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b3RhbF9kb3JtaXRvcmlvcykpCiAgICAKc3VtbWFyeSh2aXZpZW5kYSkKYGBgCgpOb3MgaGVtb3MgbGlicmFkbyBkZSBsb3MgYE5BYC4KCgojIyMgTm9ybWFsaXphciB2YXJpYWJsZXMKClRhbWJpw6luIGRlYmVyw61hbW9zIGV2YWx1YXIgc2kgYWxndW5hcyB2YXJpYWJsZXMgcmVxdWllcmVuIHNlciBub3JtYWxpemFkYXMuIEVuIGVzdGUgY2FzbyBsYXMgY2FuZGlkYXRhcyBzb24gInRvdGFsX2hhYml0YWNpb25lcyIgeSAidG90YWxfZG9ybWl0b3Jpb3MiLCB5YSBxdWUgZXMgZGUgZXNwZXJhcnNlIHF1ZSBzZWFuIG1heW9yZXMgZG9uZGUgaGF5IG3DoXMgY2FzYXMgLW5hdHVyYWxtZW50ZS0gcGVybyBlc28gbm8gbm9zIGluZGljYSBzaSBsYXMgY2FzYXMgdGllbmRlbiBhIHNlciBtw6FzIG8gbWVub3MgZ3JhbmRlcyBxdWUgbGEgbWVkaWEuIExvIHF1ZSBoYXJlbW9zIGVudG9uY2VzIGVzIGRpdmlkaXIgbGEgY2FudGlkYWQgdG90YWwgZGUgaGFiaXRhY2lvbmVzIHBvciBsYSBjYW50aWRhZCBkZSB2aXZpZW5kYXMgZW4gY2FkYSBkaXN0cml0bywgeSBub3MgcXVlZGFyZW1vcyBjb24gZXNhcyBtw6l0cmljYXMgZW4gbHVnYXIgZGUgbGFzIG9yaWdpbmFsZXM6CgpgYGB7cn0Kdml2aWVuZGEgPC0gdml2aWVuZGEgJT4lIAogICAgbXV0YXRlKHByb21lZGlvX2hhYml0YWNpb25lcyA9IHRvdGFsX2hhYml0YWNpb25lcy9ob2dhcmVzLAogICAgICAgICAgIHByb21lZGlvX2Rvcm1pdG9yaW9zID0gdG90YWxfZG9ybWl0b3Jpb3MvaG9nYXJlcykgJT4lCiAgICBzZWxlY3QoLXRvdGFsX2hhYml0YWNpb25lcywgLXRvdGFsX2Rvcm1pdG9yaW9zKQoKaGVhZCh2aXZpZW5kYSkKYGBgCgpEZWwgbWlzbW8gbW9kbyBzZXLDrWEgbWVqb3IgdGVuZXIgZGVuc2lkYWQgZGUgcG9ibGFjacOzbiBlbiBsdWdhciBkZSBwb2JsYWNpw7NuIHRvdGFsLCBwZXJvIHNpbiBzYWJlciBlbCDDoXJlYSBxdWUgb2N1cGEgY2FkYSBkaXN0cml0byBubyB2YW1vcyBhIHBvZGVyIG9idGVuZXIgbGEgZGVuc2lkYWQuIAoKCiMjIyBDb2RpZmljYXIgdmFyaWFibGVzIGNhdGVnw7NyaWNhcwoKQWhvcmEgbm9zIGVuY2FyZ2FyZW1vcyBkZSBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcywgYXF1w60gcmVwcmVzZW50YWRhcyBwb3IgInByb3hpbWlkYWRfb2NlYW5vIi4gUmFyYSB2ZXogZXMgcG9zaWJsZSB1dGlsaXphciBjb2x1bW5hcyBjYXRlZ8OzcmljYXMgZW4gbW9kZWxvcyBlc3RhZMOtc3RpY29zLCBwZXJvIHBvciBzdWVydGUgcG9kZW1vcyByZWN1cnJpciBhIGxhIGFsdGVybmF0aXZhIGRlIHJlZW1wbGF6YXIgdW5hIGNvbHVtbmEgZGUgZGF0b3MgY2F0ZWfDs3JpY29zIHBvciB1bmEgc2VyaWUgZGUgdmFyaWFibGVzIGJpbmFyaWFzLCBvICJkdW1teSIuIAoKRXMgZGVjaXIsIGVuIGx1Z2FyIGRlLi4uCgoKfCBjYXNvIHwgcHJveGltaWRhZF9vY2Vhbm8gIHwKfC0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLS0tLXwKfCBBICAgIHwgQ0VSQ0FOTyBBIEJBSElBICAgIHwKfCBCICAgIHwgQUxFSkFETyBERUwgT0NFQU5PIHwKfCBDICAgIHwgQ0VSQ0FOTyBBIE9DRUFOTyAgIHwKCgouLi4gdGVuZHLDrWFtb3MgYWxnbyBhc8OtIGNvbW86CgoKfCBjYXNvIHwgQ0VSQ0FOTyBBIEJBSElBIHwgQUxFSkFETyBERUwgT0NFQU5PIHwgQUxFSkFETyBERUwgT0NFQU5PIHwKfC0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLS0tLXwtLS0tLS0tLS0tLS0tLS0tLS0tLXwKfCBBICAgIHwgMSAgICAgICAgICAgICAgIHwgMCAgICAgICAgICAgICAgICAgIHwgMCAgICAgICAgICAgICAgICAgIHwKfCBCICAgIHwgMCAgICAgICAgICAgICAgIHwgMSAgICAgICAgICAgICAgICAgIHwgMCAgICAgICAgICAgICAgICAgIHwKfCBDICAgIHwgMCAgICAgICAgICAgICAgIHwgMCAgICAgICAgICAgICAgICAgIHwgMSAgICAgICAgICAgICAgICAgIHwKCgoKQ29tbyBidWVuIGxlbmd1YWplIGNyZWFkbyBwb3IgeSBwYXJhIHByYWN0aWNhbnRlcyBkZWwgYW7DoWxpc2lzIGVzdGFkw61zdGljbywgYFJgIHRyYWUgdW5hIGZ1bmNpw7NuIGVzcGVjw61maWNhIHBhcmEgcmVhbGl6YXIgw6lzdGEgdGFyZWEsIGBtb2RlbC5tYXRyaXgoKWAuIFNlIHVzYSBhc8OtOgoKCmBgYHtyfQptYXRyaXpfY2F0ZWdvcmlhc19vY2Vhbm8gPC0gbW9kZWwubWF0cml4KGRhdGEgPSB2aXZpZW5kYSwgfiBwcm94aW1pZGFkX29jZWFubyAtIDEpCmBgYAoKeSBlbCByZXN1bHRhZG8gZXMsIG5pIG3DoXMgbmkgbWVub3MsIHVuYSBtYXRyaXogZGUgdmFyaWFibGVzIGJpbmFyaWFzIHF1ZSByZXByZXNlbnRhbiBsYXMgY2F0ZWdvcsOtYXMgb3JpZ2luYWxlczoKCmBgYHtyfQpoZWFkKG1hdHJpel9jYXRlZ29yaWFzX29jZWFubykKYGBgCgpQcm9udG8gYWdyZWdhcmVtb3MgbGEgbWF0cml6IGEgbnVlc3RybyBkYXRhZnJhbWUsIHBlcm8gYW50ZXMgdGVybWluZW1vcyBjb24gb3Ryb3MgYWp1c3RlcyBwZW5kaWVudGVzLgoKIyMjIFVuaWZpY2FyIGxhIGVzY2FsYSBkZSBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMKCsOJc3RlIHBhc28gc2llbXByZSBlcyBuZWNlc2FyaW8gY3VhbmRvIGVzdGFtb3MgdHJhYmFqYW5kbyBjb24gdmFyaWFibGVzIHF1ZSB1dGlsaXphbiBkaXN0aW50YXMgdW5pZGFkZXMgZGUgbWVkaWRhLiBBcXXDrSB0ZW5lbW9zIHBlcnNvbmFzLCBob2dhcmVzLCBoYWJpdGFjaW9uZXMsIGRvcm1pdG9yaW9zLCBhw7FvcyBkZSBhbnRpZ8O8ZWRhZC4uLiBkZSB0b2RvLiBNdWNob3MgYWxnb3JpdG1vcyBhc3VtZW4gcXVlIHRvZGFzIGxhcyB2YXJpYWJsZXMgdGllbmVuIGVzY2FsYXMgY29tcGFyYWJsZXMsIGxvIGN1YWwgZ2VuZXJhIHByb2JsZW1hcyBjb24gbGFzIHF1ZSBhbGNhbnphbiB2YWxvcmVzIHJlbGF0aXZhbWVudGUgbXV5IGFsdG9zIChjb21vIHBvYmxhY2nDs24sIHF1ZSBsbGVnYXIgYSBkZWNlbmFzIGRlIG1pbGVzKSB2ZXJzdXMgbGFzIHF1ZSB0aWVuZW4gcmFuZ29zIG11Y2hvIG1lbm9yZXMgKGNvbW8gYW50aWfDvGVkYWQgZW4gYcOxb3MgbWVkaWFuYSwgcXVlICJzw7NsbyIgbGxlZ2EgYSA1MikuIFNpIGxhcyBkZWrDoXNlbW9zIGFzw60sIHZhcmlhcyBkZSBsYXMgdMOpY25pY2FzIGhhYml0dWFsZXMgZGVsIE1MIGFkanVkaWNhcsOtYW4gbXVjaG8gbcOhcyBwZXNvIGEgbGFzIHZhcmlhYmxlcyBjb24gbsO6bWVyb3MgZ3JhbmRlcywgImRlc3ByZWNpYW5kbyIgYSBsYXMgcXVlIHBvciBzdSBuYXR1cmFsZXphIHNlIG11ZXZlbiBlbiByYW5nbyBtw6FzIHJlZHVjaWRvcy4KCkVuIHRvZG8gY2Fzbywgbm8gaW1wb3J0YSBsbyBkaXNpbWlsZXMgcXVlIHNlYW4gbGFzIHVuaWRhZGVzIGRlIG1lZGlkYSwgbGEgc29sdWNpw7NuIGVzIHNpbXBsZTogY29udmVydGltb3MgdG9kYXMgbGFzIHZhcmlhYmxlcyBhIGxhIGZhbW9zYSAiZGlzdHJpYnVjacOzbiBaIiwgbyBmdW5jacOzbiBkZSBlc3RhbmRhcml6YWNpw7NuLCBxdWUgY29udmllcnRlIHZhcmlhYmxlcyBhIHVuYSBlc2NhbGEgc2luIHVuaWRhZCBkZSBtZWRpZGEsIHF1ZSBleHByZXNhIGNhZGEgdmFsb3IgY29tbyBsYSBjYW50aWRhZCBkZSBkZXN2w61vcyBlc3TDoW5kYXIgcXVlIGxvIGFsZWphbiBkZSBsYSBtZWRpYS4gRXhwcmVzYXIgdG9kYXMgbGFzIHZhcmlhYmxlcyBudW3DqXJpY2FzIGVuIGZvcm1hIGRlICJ6IHNjb3JlcyIsIG8gInZhbG9yZXMgeiIsIGxhcyBoYWNlIGRpcmVjdGFtZW50ZSBjb21wYXJhYmxlcyBlbnRyZSBzw60uIAoKRW4gYFJgIGRpc3BvbmVtb3MgZGUgbGEgZnVuY2nDs24gYHNjYWxlKClgLCBxdWUgb2J0aWVuZSBsb3Mgei1zY29yZXMuIFRvbWFyZW1vcyBlbnRvbmNlcyBudWVzdHJvIGRhdGFmcmFtZSB5IHVzYXJlbW9zIGBtdXRhdGVfYWxsKClgIHBhcmEgYXBsaWNhciB1bmEgZnVuY2nDs24gYSB0b2RhcyBsYXMgY29sdW1uYXMgcmVzdGFudGVzIGRlIHVuIHRpcsOzbi4gRXNvIHNpLCBxdWl0YW5kbyBhbnRlcyBjaWVydGFzIHZhcmlhYmxlczogbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgKHF1ZSBubyB0aWVuZSBzZW50aWRvIHBhc2FyIGEgei1zY29yZXMgcG9ycXVlIG5vIHNvbiB2YXJpYWJsZXMgbnVtw6lyaWNhcyksIHkgbGEgdmFyaWFibGUgcXVlIGVzdGFtb3MgaW50ZW50YW5kbyBwcmVkZWNpciwgeWEgcXVlIHN1IGVzY2FsYSBubyBhZmVjdGEgbG9zIG1vZGVsb3MgeSBwb2RlbW9zIGRlamFybGEgZW4gc3UgZm9ybWF0byBvcmlnaW5hbCBmw6FjaWwgZGUgaW50ZXJwcmV0YXIuCgpgYGB7cn0Kdml2aWVuZGEgPC0gdml2aWVuZGEgJT4lIAogICAgc2VsZWN0KC1wcm94aW1pZGFkX29jZWFubywgLW1lZGlhbmFfdmFsb3Jfdml2aWVuZGEpICU+JSAKICAgIG11dGF0ZV9hbGwoZnVucyhzY2FsZSkpICU+JSAKICAgIG11dGF0ZShtZWRpYW5hX3ZhbG9yX3ZpdmllbmRhID0gdml2aWVuZGEkbWVkaWFuYV92YWxvcl92aXZpZW5kYSkKCmBgYAoKWSBvYnPDqXJ2ZXNlIHF1ZSBgc2NhbGUoKWAgbWVkaWFudGUsIGFob3JhIHRvZGFzIGxhcyB2YXJpYWJsZXMgdGllbmVuIHByb21lZGlvIGlndWFsIGEgMCwgeSBzZSBtdWV2ZW4gZW4gZWwgbWlzbW8gcmFuZ28KCmBgYHtyfQpzdW1tYXJ5KHZpdmllbmRhKQpgYGAKCi4uLiB5IHNpbiBxdWUgZXN0byBoYXlhIGNhbWJpYWRvIGxhIGZvcm1hIGRlIGxhcyBkaXN0cmlidWNpb25lcy4gQ29tcMOhcmVuc2UgYWhvcmEgY29uIGxhcyBxdWUgZXhhbWluYW1vcyBhbCBpbmljaW8sIAoKaW5ncmVzb3M6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IG1lZGlhbmFfaW5ncmVzb3MpKQpgYGAKCmFudGlnw7xlZGFkIGRlIGxhcyB2aXZpZW5kYXM6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IG1lZGlhbmFfYW50aWd1ZWRhZF92aXZpZW5kYXMpKQpgYGAKCnBvYmxhY2nDs246CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9oaXN0b2dyYW0oZGF0YSA9IHZpdmllbmRhLCBhZXMoeCA9IHBvYmxhY2lvbikpCmBgYAoKwqFsYXMgZm9ybWFzIHNvbiBpZ3VhbGVzISBubyBoZW1vcyBoZW1vcyBwZXJkaWRvICJpbmZvcm1hY2nDs24iIHJlc3BlY3RvIGEgcXVlIHRhbiB0w61waWNvIG8gZXh0cmVtbyBlcyBjYWRhIHZhbG9yLCB5IGhlbW9zIGdhbmFkbyBsYSBwb3NpYmlsaWRhZCBkZSBjb21wYXJhciBlbiBmb3JtYSBkaXJlY3RhIHRvZGFzIGxhcyB2YXJpYWJsZXM6IHNpIHVuIGRpc3RyaXRvIHRpZW5lIHVuIHZhbG9yIGNlcmNhbm8gYSBjZXJvIGVuIHBvYmxhY2nDs24sIHkgIC1kaWdhbW9zLSBtw6FzIGRlIDQgZW4gaW5ncmVzb3MsIHNhYmVtb3MgYXV0b23DoXRpY2FtZW50ZSBxdWUgc3UgcG9ibGFjacOzbiBlcyBwYXJlY2lkYSBhIGxhIG1lZGlhIGRlIHRvZG9zIGxvcyBkaXN0cml0b3MsIHBlcm8gc3VzIGluZ3Jlc29zIHNvbiBhbHTDrXNpbW9zLgoKCiMjIyBDb25zb2xpZGFyIHRvZGFzIGxhcyB2YXJpYWJsZXMgZ2VuZXJhZGFzIGFkLWhvYyBlbiB1biBzw7NsbyBkYXRhZnJhbWUgCgpOb3MgaGEgcXVlZGFkbyBwb3IgdW4gbGFkbyB1biBkYXRhZnJhbWUgZGUgdmFyaWFibGVzIG51bcOpcmljYXMgZXN0YW5kYXJpemFkYXMsIHkgcG9yIG90cm8gdW5hIG1hdHJpeiBxdWUgcmVwcmVzZW50YSBsYSBwZXJ0ZW5lbmNpYSBkZSBjYWRhIGRpc3RyaXRvIGEgc3UgY2F0ZWdvcsOtYSByZXNwZWN0byBhICJwcm94aW1pZGFkIGFsIG9jw6lhbm8iLiAKClByaW1lcm8gY29udmVydGltb3MgbGEgbWF0cml6IGVuIGRhdGFmcmFtZSAocGFzbyBzaW1wbGUgeWEgw6lzdGFzIGVzdHJ1Y3R1cmEgZGUgZGF0b3Mgc29uIG11eSBzaW1pbGFyZXMgZW50cmUgc2kpLCB5IGx1ZWdvIHVuaW1vcyBsYXMgY29sdW1uYXMgZGUgYW1ib3MgY29uIGxhIGZ1bmNpw7NuIGBjYmluZCgpYDoKCmBgYHtyfQptYXRyaXpfY2F0ZWdvcmlhc19vY2Vhbm8gPC0gYXMuZGF0YS5mcmFtZShtYXRyaXpfY2F0ZWdvcmlhc19vY2Vhbm8pCgp2aXZpZW5kYSA8LSB2aXZpZW5kYSAlPiUgCiAgICBjYmluZChtYXRyaXpfY2F0ZWdvcmlhc19vY2Vhbm8pCmBgYAoKYGBge3J9CmhlYWQodml2aWVuZGEpCmBgYAoKWWEgdGVuZW1vcyB0ZW5lbW9zIGxvcyBkYXRvcyBsaW1waW9zIHkgZW4gb3JkZW4uIAoKCiMjIFBhc28gNDogQ3JlYXIgc2V0cyBkZSBlbnRyZW5hbWllbnRvIHkgZGUgdGVzdGVvCgpQYXJhIHBvZGVyIGV2YWx1YXIgbGEgY2FsaWRhZCBkZSB1biBtb2RlbG8gcHJlZGljdGl2bywgZXMgcHLDoWN0aWNhIGNvbcO6biBkaXZpZGlyIGxvcyBkYXRvcyBkaXNwb25pYmxlcyBlbiBkb3MgcG9yY2lvbmVzLiBVbmEgcGFydGUgc2Vyw6EgdXRpbGl6YWRhIHBhcmEgImVudHJlbmFyIiBlbCBtb2RlbG8gZGUgTUwsIGVzIGRlY2lyIHNlIGxlIHBlcm1pdGlyw6EgYWwgYWxnb3JpdG1vIGFjY2VkZXIgYSBlc29zIGRhdG9zIHBhcmEgZXN0YWJsZWNlciBsYSBmb3JtYSBlbiBxdWUgY2FkYSB2YXJpYWJsZSBwcmVkaWN0b3JhIGluY2lkZSBlbiBsYSBxdWUgc2UgcXVpZXJlIHByZWRlY2lyLiBFbCByZXN0byBzZXLDoSBwcmVzZXJ2YWRvIHkgdXRpbGl6YWRvIHBhcmEgInRvbWFybGUgZXhhbWVuIiBhbCBtb2RlbG86IHNlIGxlIG1vc3RyYXJhbiBzw7NsbyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzIGRlIGVzb3MgZGF0b3MsIHBpZGllbmRvIGFsIG1vZGVsbyB1bmEgcHJlZGljY2nDs24gZGVsIHZhbG9yIGEgZXN0aW1hciBwYXJhIGNhZGEgdW5hLiBQb3Igw7psdGltbywgY29udHJhc3RhbmRvIGFjaWVydG9zIHkgZXJyb3Jlcywgc2UgcG9kcsOhIGVzdGFibGVjZXIgZWwgZ3JhZG8gZGUgcHJlY2lzacOzbiBkZWwgbW9kZWxvLiAKCkluY2x1c28gcG9kcsOtYW1vcyB0ZW5lciB2YXJpb3MgbW9kZWxvcyBkaXN0aW50b3MsIG9idGVuaWRvcyBjb24gZGlzdGludGFzIHTDqWNuaWNhcyBkZSBNTC4gTm8gZXMgZGlmw61jaWwsIHlhIHF1ZSB1bmEgdmV6IHF1ZSBsb3MgZGF0b3MgaGFuIHNpZG8gb2J0ZW5pZG9zIHkgcHJlcGFyYWRvcywgbmFkYSBpbXBpZGUgdXNhcmxvcyBjb21vIGluc3VtbyBkZSBkaXN0aW50b3MgYWxnb3JpdG1vcy4gRW4gZXNlIGNhc28sIHNlIHB1ZWRlIGNvbXBhcmFyIGxhIHBlcmZvcm1hbmNlIGRlIGxvcyBkaXN0aW50b3MgbW9kZWxvcyBldmFsdWFuZG8gY3VhbCBhY2llcnRhIG1lam9yIGNvbiBsYSBkYXRhIGRlIHRlc3Rlby4KCkRlZmluYW1vcyBlbnRvbmNlcyBjdWFsZXMgZmlsYXMgdmFuIGFsIHNldCBkZSBlbnRyZW5hbWllbnRvLCB5IGN1w6FsZXMgYWwgZGUgdGVzdGVvLCBlbGlnacOpbmRvbGFzIGFsIGF6YXIuIERlIGFjdWVyZG8gYSBkaXN0aW50YXMgcmVjZXRhcywgYSB2ZWNlcyBzZSBzZXBhcmEgZWwgOTAlIGRlIGxvcyBkYXRvcyBwYXJhIGVudHJlbmFtaWVudG8geSBlbCByZXN0byBwYXJhIHRlc3Rlbywgb3RyYXMgdmVjZXMgZXMgbWl0YWQgeSBtaXRhZC4uLiB5YSBxdWUgc2llbXByZSBlcyBtw6FzIG8gbWVub3MgYXJiaXRyYXJpbywgYXF1w60gdXNhcmVtb3MgZWwgODAlIHBhcmEgZW50cmVuYXIsIHkgZWwgMjAlIHBhcmEgdGVzdGVhci4KCmBgYHtyfQojZGVmaW5pbW9zIGEgbWFubyBsYSAic2VtaWxsYSIgZGUgYWxlYXRvcml6YWNpw7NuIHBhcmEgb2J0ZW5lciByZXN1bHRhZG9zIHJlcHJvZHVjaWJsZXMKc2V0LnNlZWQoMTgxMCkKYGBgCgpUb21hbW9zIGFsIGF6YXIgZWwgODAlIGRlIGxhcyBwb3NpY2lvbmVzIGVudHJlIDEgeSBsYSBjYW50aWRhZCB0b3RhbCBkZSBmaWxhcyBkZSBudWVzdHJvIGRhdGFzZXQKCmBgYHtyfQpzZWxlY2Npb24gPC0gc2FtcGxlKDE6bnJvdyh2aXZpZW5kYSksIHNpemUgPSBucm93KHZpdmllbmRhKSAqIDAuOCkKCmVudHJlbmFtaWVudG8gPC0gdml2aWVuZGEgJT4lIAogICAgZmlsdGVyKHJvd19udW1iZXIoKSAlaW4lIHNlbGVjY2lvbikKCiMgZWwgdGVzdGVvIGVzIGVsIHNldCBvcHVlc3RvIC0gYXF1ZWxsYXMgZmlsYXMgY3V5YSBwb3NpY2nDs24gbm8gZXN0w6EgZW50cmUgbGFzIHNlbGVjY2lvbmFkYXMKIyBlbCBvcGVyYWRvciAhIGNvbnZpZXJ0ZSB1bmEgcHJvcG9zaWNpw7NuIGVuIG5lZ2F0aXZhIAoKdGVzdGVvIDwtIHZpdmllbmRhICU+JSAKICAgIGZpbHRlcighKHJvd19udW1iZXIoKSAlaW4lIHNlbGVjY2lvbikpCgpgYGAKCkFob3JhIHNpLCBwb3IgZmluLCBhcGxpcXVlbW9zIHVuIHBvY28gZGUgbWFjaGluZSBsZWFybmluZy4KCgojIyMgUGFzbyA1OiBFbnRyZW5hciB5IHRlc3RlYXIgdW4gbW9kZWxvCgpSYW5kb20gRm9yZXN0LCB1bmEgaW1wbGVtZW50YWNpw7NuIGRlIMOhcmJvbGVzIGRlIGRlY2lzacOzbiBjb21vIGxvcyBpbHVzdHJhZG9zIGVuIFsiVW5hIGludHJvZHVjY2nDs24gdmlzdWFsIGFsIG1hY2hpbmUgbGVhcm5pbmciXShodHRwOi8vd3d3LnIyZDMudXMvdW5hLWludHJvZHVjY2lvbi12aXN1YWwtYWwtbWFjaGluZS1sZWFybmluZy0xLyk6CgpgYGB7cn0KbW9kZWxvX1JGIDwtIHJhbmRvbUZvcmVzdChkYXRhID0gZW50cmVuYW1pZW50bywgbWVkaWFuYV92YWxvcl92aXZpZW5kYSB+IC4sCiAgICAgICAgICAgICAgICAgICAgICAgICBudHJlZSA9IDUwMCwKICAgICAgICAgICAgICAgICAgICAgICAgIGltcG9ydGFuY2UgPSBUUlVFKQoKIyBlbCBwYXLDoW1ldHJvICJpbXBvcnRhbmNlIjogRGVmaW5lIHNpIGVsIG1vZGVsbyBlc3RpbWFyw6EgbGEgaW1wb3J0YW5jaWEgcmVsYXRpdmEgZGUgY2FkYSBwcmVkaWN0b3IgZW4gbGEgY2FsaWRhZCBkZSBsYSBwcmVkaWNjacOzbiAtZXMgZGVjaXIsIGN1YWxlcyB2YXJpYWJsZXMgc29uIG3DoXMgaW1wb3J0YW50ZXMgcGFyYSBwcmVkZWNpcgoKIyByZXN1bHRhZG9zOgptb2RlbG9fUkYKYGBgCgpTZWfDum4gZGljZSBhbGzDrSwgZWwgbW9kZWxvIHB1ZWRlIGV4cGxpY2FyIG3DoXMgZGVsIDgwJSBkZSBsYSB2YXJpYW56YSBkZSB2YWxvcmVzIGVuY29udHJhZGEgZW50cmUgbG9zIGRpc3RyaXRvcyBjYWxpZm9ybmlhbm9zIGVuIGJhc2UgYSBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzIHF1ZSBlbXBsZWFtb3MuCgoKwr9RdcOpIHRpZW5lIGRlbnRybyBlbCBtb2RlbG8/CgpgYGB7cn0Kc3VtbWFyeShtb2RlbG9fUkYpCmBgYAoKRGUgdG9kbyEgUG9yIGVqZW1wbG8sICJ0eXBlIiBub3MgZW1pdGUgY29uZmlybWFyIHF1w6kgdGlwbyBkZSBhbsOhbGlzaXMgcmVhbGl6w7M6IEZ1ZSBkZSByZWdyZXNpw7NuIGVuIGVzdGUgY2FzbywgcGVvciBwb2Ryw61hIGhhYmVyIHNpZG8gb3RybywgY29tbyBjbGFzaWZpY2FjacOzbiAoY3VhbmRvIHNlIHByZWRpY2UgdW4gYXRyaWJ1dG8gY2F0ZWfDs3JpY28gZW4gbHVnYXIgZGUgdW5hIHZhcmlhYmxlIGNvbnRpbnVhKToKCmBgYHtyfQptb2RlbG9fUkYkdHlwZQpgYGAKCk8gImltcG9ydGFuY2UiLCBxdWUgY29udGllbmUgdW4gcmFua2luZyBjb24gbGEgaW1wb3J0YW5jaWEgcmVsYXRpdmEgZGUgY2FkYSBwcmVkaWN0b3IsIGVzIGRlY2lyIGN1w6FsZXMgc29uIGxvcyBxdWUgbcOhcyBheXVkYW4gYSBlc3RpbWFyIGVsIHZhbG9yIGEgcHJlZGVjaXI6CgpgYGB7cn0KbW9kZWxvX1JGJGltcG9ydGFuY2UKYGBgCgpMYSBjb2x1bW5hICIlSW5jTVNFIiByZXByZXNlbnRhIGVsIHBvcmNlbnRhamUgZGUgZXJyb3IgcHJvbWVkaW8sIGxhIG1hZ25pdHVkIGVuIGxhIHF1ZSBlbCB2YWxvciBwcmVkaWNobyBwb3IgZWwgbW9kZWxvIGRpZmllcmUgZGVsIHZhbG9yIG9ic2VydmFkbywgY3VhbmRvIGNhZGEgcHJlZGljdG9yIHNlIHJldGlyYSBkZWwgbW9kZWxvIChlcyBkZWNpciwgY3VhbnRvIHBlb3Igc2Vyw61hIGxhIHByZWRpY2Npw7NuIHNpIG5vIHNlIHVzYXJhKS4gUG9yIGVzbyBsb3MgbsO6bWVyb3MgbWF5b3JlcyBlc3TDoW4gYXNvY2lhZG9zIGEgbG9zIHByZWRpY3RvcmVzIGRlIG3DoXMgcGVzbywgcXVlIGVuIGVzdGUgY2FzbyBzb24gIm1lZGlhbmFfaW5ncmVzb3MiLCB5IGx1ZWdvIGxvbmdpdHVkIHkgbGF0aXR1ZC4gQWRlbcOhcyBkZSBlbmNvbnRyYXIgbGEgY29ycmVsYWNpw7NuIGVzcGVyYWJsZSBlbnRyZSBuaXZlbCBkZSBpbmdyZXNvcyBkZSB1bmEgcG9ibGFjacOzbiB5IGVsIHZhbG9yIGRlIHN1cyB2aXZpZW5kYXMsIG51ZXN0cm8gbW9kZWxvIGhhIGVuY29udHJhZG8gcXVlIGxhIHViaWNhY2nDs24gZXMgbGEgY2xhdmUgZGVsIHZhbG9yIGRlIGxhIHByb3BpZWRhZC4uLiB5IHNpbiBzYWJlciBuYWRhIGRlIGdlb2dyYWbDrWEgbmkgdXJiYW5pc21vLgoKCkVuICJwcmVkaWN0ZWQiIHRlbmVtb3MgbGEgbWVkaWFuYSBkZWwgdmFsb3IgZGUgbGEgdml2aWVuZGEgcHJlZGljaGEgcGFyYSBjYWRhIGRpc3RyaXRvOgoKYGBge3J9CmhlYWQobW9kZWxvX1JGJHByZWRpY3RlZCkKYGBgCgoKQXByb3ZlY2hhbmRvIHF1ZSBkZW50cm8gZGVsIG1vZGVsbywgInkiIGNvbnRpZW5lIGxvcyB2YWxvcmVzIG9ic2VydmFkb3MsIGV2YWx1ZW1vcyBlbiBmb3JtYSBncsOhZmljYSBjdcOhbnRvIHNlIGFwcm94aW1hbiBsYXMgcHJlZGljY2lvbmVzIGRlIGNhZGEgZGlzdHJpdG8gYWwgdmFsb3IgcmVhbCAoZWwgb2JzZXJ2YWRvKSA6CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9wb2ludChhZXMoeCA9IG1vZGVsb19SRiRwcmVkaWN0ZWQsIHkgPSBtb2RlbG9fUkYkeSksIGFscGhhID0gMC4zKSAKYGBgCgpTZSBhanVzdGEgYmFzdGFudGUgYmllbi4gTHVlZ28gdmVyZW1vcyB1bmEgbWFuZXJhIGRlIGN1YW50aWZpY2FyIGxhIHByZWNpc2nDs24gZGVsIG1vZGVsby4gCgoKIyMjIE1pZGllbmRvIGxhIHBlcmZvcm1hbmNlIGRlbCBtb2RlbG8gY29udHJhIGRhdG9zIHF1ZSBubyBjb25vY2UKClZlYW1vcyBhaG9yYSBjb21vIHNlIGNvbXBvcnRhIG51ZXN0cm8gbW9kZWxvIGN1YW5kbyBkZWJlIHByZWRlY2lyIHZhbG9yZXMgZGUgZGlzdHJpdG9zIHF1ZSBubyBzZSBoYW4gdXRpbGl6YWRvIHBhcmEgZWwgZW50cmVuYW1pZW50bywgbG9zIHF1ZSByZXNlcnZhbW9zIHBhcmEgZWwgc2V0IGRlIHRlc3Rlby4KCmBgYHtyfQpwcmVkaWNjaW9uZXNfdGVzdCA8LSBwcmVkaWN0KG1vZGVsb19SRiwgbmV3ZGF0YSA9IHRlc3RlbykKCmhlYWQocHJlZGljY2lvbmVzX3Rlc3QpCmBgYAoKRW4gdW4gZ3LDoWZpY286CgpgYGB7cn0KZ2dwbG90KCkgKwogICAgZ2VvbV9wb2ludChhZXMoeCA9IHByZWRpY2Npb25lc190ZXN0LCB5ID0gdGVzdGVvJG1lZGlhbmFfdmFsb3Jfdml2aWVuZGEpLCBhbHBoYSA9IDAuMykgCmBgYAoKTHVjZSBpbmNsdXNvIG1lam9yIHF1ZSBlbCBhanVzdGUgY29uIGxvcyBkYXRvcyBjb25vY2lkb3MuCgojIyMgQ29tcGFyYW5kbyBwZXJmb3JtYW5jZQoKRXMgcHLDoWN0aWNvIG9idGVuZXIgdW4gc8OzbG8gbsO6bWVybywgdW4gaW5kaWNhZG9yIHNpbXBsZSBxdWUgbm9zIGRpZ2EgcXVlIHRhbiBiaWVuIHByZWRpY2UgZWwgbW9kZWxvLCB5IGFzw60gcG9kZXIgY29tcGFyYXIgZGlzdGludG9zIG1vZGVsb3MgZW50cmUgc2kgKG8gZGlzdGludG9zIGRhdGFzZXRzIGNvbnRyYSBlbCBtaXNtbyBtb2RlbG8pIHV0aWxpemFuZG8gZXNhIG1lZGlkYS4gRW4gZXN0YWTDrXN0aWNhIGVzIGNvbcO6biBlbCB1c28gZGVsIFJNU0UgY29tbyBpbmRpY2Fkb3IgZGUgZ3JhZG8gZGUgYWp1c3RlLCBvICJSb290IE1lYW4gU3F1YXJlIEVycm9yIiAtIGxhIHJhw616IGN1YWRyYWRhIGRlIGxhIG1lZGlhIGRlIGxvcyBlcnJvcmVzIGFsIGN1YWRyYWRvLgoKRWwgbW9kZWxvIGluY2x1eWUgZWwgTVNFIChvIHNlYSBsYSBzdW1hIGRlIGxvcyBlcnJvcmVzIGFsIGN1YWRyYWRvKSBxdWUgc3VyZ2UgZGUgY29tcGFyYXIgcHJlZGljY2lvbmVzIGNvbiB2YWxvcmVzIG9ic2VydmFkb3MuIFkgZW4gZWwgY2FzbyBkZSB1biByYW5kb20gZm9yZXN0LCBxdWUgaW50ZW50YSBtdWNob3Mgw6FyYm9sZXMgZGlzdGludG9zLCB2YXJpb3MgTVNFcyByZXN1bHRhbnRlczogNTAwIGVuIG51ZXN0cm8gY2FzbywgdW5vIHBvciBjYWRhIMOhcmJvbCB0cmF6YWRvLiAKClRvbWFtb3MgbGEgbWVkaWEgZGUgdG9kb3MgbG9zIE1TRSBwYXJhIG9idGVuZXIgdW4gdmFsb3IgZ2VuZXJhbCwgeSBsdWVnbyB0b21hbW9zIGxhIHJhw616IGN1YWRyYWRhIHBhcmEgb2J0ZW5lciBlbCBSTVNFOiAKCmBgYHtyfQpSTVNFIDwtIG1vZGVsb19SRiRtc2UgJT4lIAogICAgbWVhbigpICU+JQogICAgc3FydCgpCgpSTVNFCmBgYAoKRXNvIHNpZ25pZmljYSBxdWUgbGEgZGlmZXJlbmNpYSBwcm9tZWRpbyBlbnRyZSB2YWxvciBlc3BlcmFkbyB5IHZhbG9yIGhhbGxhZG8gcGFyYSBjYWRhIGRpc3RyaXRvIGZ1ZSBkZSBgciBSTVNFYCBkw7NsYXJlcy4KClkgZW4gY29tcGFyYWNpw7NuLCDCv3F1w6kgdGFuIGJ1ZW5vIHJlc3VsdMOzIGVsIG1vZGVsbyBjdWFuZG8gc2UgYXBsaWPDsyBhIGRhdG9zIHF1ZSBubyBjb25vY8OtYT8KCgpgYGB7cn0KUk1TRV90ZXN0IDwtIHNxcnQobWVhbigocHJlZGljY2lvbmVzX3Rlc3QgLSB0ZXN0ZW8kbWVkaWFuYV92YWxvcl92aXZpZW5kYSleMikpCgpSTVNFX3Rlc3QKYGBgCgpDb24gdW4gdmFsb3IgbWVkaW8gZGUgZXJyb3IgZGUgYHIgUk1TRV90ZXN0YCBkw7NsYXJlcywgZWwgbW9kZWxvIGhhIGZ1bmNpb25hZG8gbXV5IGJpZW4gY29uIGRhdG9zIGRlc2Nvbm9jaWRvcywgaW5jbHVzbyBtZWpvcmFuZG8gbGV2ZW1lbnRlIHN1IHBlcmZvcm1hbmNlIHJlc3BlY3RvIGFsIHNldCBkZSBfdHJhaW5pbmdfLiAKCkVzdG8gaW5kaWNhIHF1ZSBubyBzdWZyZSBkZSAib3ZlcmZpdHRpbmciLCBsYSBjb25kaWNpw7NuIGRlIGVzdGFyIGV4Y2VzaXZhbWVudGUgYWp1c3RhZG8gYSBsb3MgZGF0b3MgY29uIGxvcyBxdWUgZnVlIGVudHJlbmFkby4gUG9yIGVzbyBlbCBtb2RlbG8gbm8gcGllcmRlIHByZWNpc2nDs24gY3VhbmRvIGxpZGlhIGNvbiBkYXRvcyBudWV2b3MuCgpDb21vIGRlc3BlZGlkYSwgdm9sdmFtb3MgYWwgZXhhbWVuIHZpc3VhbC4gUmVwcmVzZW50YW1vcyBlbiB1biBncsOhZmljbyBjYWRhIHZhbG9yIHByZWRpY2hvIHkgY2FkYSB2YWxvciBvYnNlcnZhZG8gcGFyYSBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50bzoKCmBgYHtyfQpnZ3Bsb3QoKSArCiAgICBnZW9tX3BvaW50KGFlcyh4ID0gMTpsZW5ndGgocHJlZGljY2lvbmVzX3Rlc3QpLCB5ID0gcHJlZGljY2lvbmVzX3Rlc3QpLCAKICAgICAgICAgICAgICAgY29sb3IgPSAic2FsbW9uIiwKICAgICAgICAgICAgICAgYWxwaGEgPSAuNSwKICAgICAgICAgICAgICAgc2l6ZSA9IC41KSArCiAgICBnZW9tX3BvaW50KGFlcyh4ID0gMTpucm93KHRlc3RlbyksIHkgPSB0ZXN0ZW8kbWVkaWFuYV92YWxvcl92aXZpZW5kYSksIAogICAgICAgICAgICAgICBjb2xvciA9ICJsaWdodGJsdWUiLAogICAgICAgICAgICAgICBhbHBoYSA9IC41LAogICAgICAgICAgICAgICBzaXplID0gLjUpICsKICAgIGxhYnMoeCA9ICJ2YWxvcmVzIHByZWRpY2hvcyIsCiAgICAgICAgIHkgPSAidmFsb3JlcyBvYnNlcnZhZG9zIikgKwogICAgdGhlbWVfbWluaW1hbCgpCmBgYAo=