Teoría

lm() es la función en R para ajustar modelos lineales.
Es el modelo estadístico más básico que existe y más fácil de interpretar.
Para interpretarlo se usa la medida R-Cuadrada, que significa qué tan cerca están los datos de la regresión. Va de 0 a 1, donde 1 es que el modelo explica toda la variabilidad.

Contexto

Una inmobiliaria cuenta con registros de casas vendidas, con sus características de superficie, ubicación, estacionamiento y distancias a servicios. Se desea generar un modelo predictivo del precio.

Instalar paquetes y llamar librerías

# install.packages("corrplot")
library(corrplot)   # sirve para dibujar el mapa de correlaciones

Importar la base de datos

# file.choose()
df <- read.csv("/Users/santiagojaramillo/Library/Mobile Documents/com~apple~CloudDocs/Tec/Semestre 7/Inteligencia Artificial con Impacto Empresarial (Gpo 601)/HousePriceData.csv")

Entender la base de datos

summary(df)   # mínimos, máximos y promedios de cada columna
##   Observation      Dist_Taxi      Dist_Market    Dist_Hospital  
##  Min.   :  1.0   Min.   :  146   Min.   : 1666   Min.   : 3227  
##  1st Qu.:237.0   1st Qu.: 6477   1st Qu.: 9367   1st Qu.:11302  
##  Median :469.0   Median : 8228   Median :11149   Median :13189  
##  Mean   :468.4   Mean   : 8235   Mean   :11022   Mean   :13091  
##  3rd Qu.:700.0   3rd Qu.: 9939   3rd Qu.:12675   3rd Qu.:14855  
##  Max.   :932.0   Max.   :20662   Max.   :20945   Max.   :23294  
##                                                                 
##      Carpet         Builtup           Parking      City_Category
##  Min.   :  775   Min.   :  932   Length   :905   Length   :905  
##  1st Qu.: 1317   1st Qu.: 1579   N.unique :  4   N.unique :  3  
##  Median : 1478   Median : 1774   N.blank  :  0   N.blank  :  0  
##  Mean   : 1511   Mean   : 1794   Min.nchar:  4   Min.nchar:  5  
##  3rd Qu.: 1654   3rd Qu.: 1985   Max.nchar: 12   Max.nchar:  5  
##  Max.   :24300   Max.   :12730                                  
##  NAs    :7                                                      
##     Rainfall       House_Price       
##  Min.   :-110.0   Min.   :  1492000  
##  1st Qu.: 600.0   1st Qu.:  4623000  
##  Median : 780.0   Median :  5860000  
##  Mean   : 786.9   Mean   :  6083992  
##  3rd Qu.: 970.0   3rd Qu.:  7200000  
##  Max.   :1560.0   Max.   :150000000  
## 
str(df)       # qué tipo de dato es cada columna (número o texto)
## 'data.frame':    905 obs. of  10 variables:
##  $ Observation  : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ Dist_Taxi    : int  9796 8294 11001 8301 10510 6665 13153 5882 7495 8233 ...
##  $ Dist_Market  : int  5250 8186 14399 11188 12629 5142 11869 9948 11589 7067 ...
##  $ Dist_Hospital: int  10703 12694 16991 12289 13921 9972 17811 13315 13370 11400 ...
##  $ Carpet       : int  1659 1461 1340 1451 1770 1442 1542 1261 1090 1030 ...
##  $ Builtup      : int  1961 1752 1609 1748 2111 1733 1858 1507 1321 1235 ...
##  $ Parking      : chr  "Open" "Not Provided" "Not Provided" "Covered" ...
##  $ City_Category: chr  "CAT B" "CAT B" "CAT A" "CAT B" ...
##  $ Rainfall     : int  530 210 720 620 450 760 1030 1020 680 1130 ...
##  $ House_Price  : int  6649000 3982000 5401000 5373000 4662000 4526000 7224000 3772000 4631000 4415000 ...
head(df)      # primeras 6 filas
##   Observation Dist_Taxi Dist_Market Dist_Hospital Carpet Builtup      Parking
## 1           1      9796        5250         10703   1659    1961         Open
## 2           2      8294        8186         12694   1461    1752 Not Provided
## 3           3     11001       14399         16991   1340    1609 Not Provided
## 4           4      8301       11188         12289   1451    1748      Covered
## 5           5     10510       12629         13921   1770    2111 Not Provided
## 6           6      6665        5142          9972   1442    1733         Open
##   City_Category Rainfall House_Price
## 1         CAT B      530     6649000
## 2         CAT B      210     3982000
## 3         CAT A      720     5401000
## 4         CAT B      620     5373000
## 5         CAT B      450     4662000
## 6         CAT B      760     4526000
tail(df)      # últimas 6 filas
##     Observation Dist_Taxi Dist_Market Dist_Hospital Carpet Builtup      Parking
## 900         927     10915       17486         15964   1549    1851 Not Provided
## 901         928     12176        8518         15673   1582    1910      Covered
## 902         929      7214        8717         10553   1387    1663         Open
## 903         930      7423       11708         13220   1200    1436         Open
## 904         931     15082       14700         19617   1299    1560         Open
## 905         932      9297       12537         14418   1174    1429      Covered
##     City_Category Rainfall House_Price
## 900         CAT C     1220     7062000
## 901         CAT C     1080     6639000
## 902         CAT A      850     8208000
## 903         CAT A     1060     7644000
## 904         CAT B      770     9661000
## 905         CAT C     1110     5434000

Lo que vemos: son 905 casas y 10 columnas. House_Price es lo que queremos predecir. Parking y City_Category son texto (categorías), el resto son números.

# La correlación solo se puede calcular entre números, por eso primero
# separamos las columnas numéricas.
numericas <- df[sapply(df, is.numeric)]
correlacion <- cor(numericas, use = "complete.obs")
corrplot(correlacion)

Cómo leer el mapa: los círculos azules grandes significan que dos variables suben juntas. Aquí se ve que Carpet y Builtup (los dos tamaños de la casa) están muy relacionados entre sí, y ambos con el precio. Las distancias y la lluvia casi no tienen color, o sea que no parecen influir en el precio.

Limpiar la base de datos

colSums(is.na(df))   # cuenta cuántos datos faltan en cada columna
##   Observation     Dist_Taxi   Dist_Market Dist_Hospital        Carpet 
##             0             0             0             0             7 
##       Builtup       Parking City_Category      Rainfall   House_Price 
##             0             0             0             0             0
df <- na.omit(df)    # borra las filas que tienen datos faltantes

Recomendación: aquí faltaban 7 datos en Carpet. Al borrarlos quedamos con 898 casas de las 905 originales. Como es menos del 1%, no afecta el modelo. Si hubiera faltado mucha información sí habría que pensar en rellenar los datos en lugar de borrarlos.

Generar el modelo

# El punto (.) significa "usa todas las demás columnas para predecir el precio"
regresion <- lm(House_Price ~ ., data = df)
summary(regresion)
## 
## Call:
## lm(formula = House_Price ~ ., data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -3694478  -799483   -54242   780191  4539531 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          5.331e+06  3.781e+05  14.097  < 2e-16 ***
## Observation          4.070e+02  1.524e+02   2.670  0.00773 ** 
## Dist_Taxi            2.777e+01  2.685e+01   1.034  0.30127    
## Dist_Market          1.444e+01  2.083e+01   0.693  0.48839    
## Dist_Hospital        4.921e+01  3.011e+01   1.634  0.10257    
## Carpet               9.894e+03  1.423e+02  69.508  < 2e-16 ***
## Builtup             -7.544e+03  2.407e+02 -31.344  < 2e-16 ***
## ParkingNo Parking   -6.156e+05  1.388e+05  -4.435 1.04e-05 ***
## ParkingNot Provided -4.975e+05  1.236e+05  -4.027 6.15e-05 ***
## ParkingOpen         -2.575e+05  1.127e+05  -2.285  0.02253 *  
## City_CategoryCAT B  -1.874e+06  9.613e+04 -19.494  < 2e-16 ***
## City_CategoryCAT C  -2.897e+06  1.059e+05 -27.367  < 2e-16 ***
## Rainfall            -9.559e+01  1.543e+02  -0.620  0.53564    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1224000 on 885 degrees of freedom
## Multiple R-squared:  0.9433, Adjusted R-squared:  0.9426 
## F-statistic:  1228 on 12 and 885 DF,  p-value: < 2.2e-16

Cómo leer esto: hay que fijarse en las estrellas del lado derecho. Entre más estrellas, más importante es esa variable para explicar el precio.

  • Con estrellas (sí sirven): Carpet, Builtup, Parking y City_Category.
  • Sin estrellas (no sirven): Dist_Taxi, Dist_Market, Dist_Hospital y Rainfall.

La R-cuadrada es 0.9433, o sea que este modelo explica el 94% de por qué una casa cuesta lo que cuesta. Está muy bien.

Recomendación: Observation aparece con estrellas, pero no hay que usarla. Es solo el número de fila (1, 2, 3…), no una característica real de la casa. Que salga significativa es pura coincidencia.

Ajustar el modelo

# Quitamos las variables que no servían y nos quedamos solo con las importantes
regresion2 <- lm(House_Price ~ Carpet + Builtup + Parking +
                   City_Category, data = df)
summary(regresion2)
## 
## Call:
## lm(formula = House_Price ~ Carpet + Builtup + Parking + City_Category, 
##     data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -3430967  -802513   -46915   775070  4263090 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          6477702.2   272077.2  23.808  < 2e-16 ***
## Carpet                  9968.8      143.3  69.584  < 2e-16 ***
## Builtup                -7611.6      243.3 -31.279  < 2e-16 ***
## ParkingNo Parking    -536301.8   139943.6  -3.832 0.000136 ***
## ParkingNot Provided  -457380.4   125020.1  -3.658 0.000269 ***
## ParkingOpen          -236576.3   114356.8  -2.069 0.038857 *  
## City_CategoryCAT B  -1918518.1    97007.4 -19.777  < 2e-16 ***
## City_CategoryCAT C  -2908756.6   107359.6 -27.094  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1243000 on 890 degrees of freedom
## Multiple R-squared:  0.9412, Adjusted R-squared:  0.9407 
## F-statistic:  2034 on 7 and 890 DF,  p-value: < 2.2e-16

Resultado: la R-cuadrada bajó apenas de 0.9433 a 0.9412, casi nada. Esto quiere decir que las 4 variables que quitamos de verdad no aportaban.

Recomendación: siempre conviene quedarse con el modelo más simple cuando explica prácticamente lo mismo. Es más fácil de entender y de usar.

Qué significan los números:

  • Carpet: cada metro extra de superficie sube el precio unos $9,969.
  • Builtup: sale en negativo porque ya está contada la superficie en Carpet; las dos miden casi lo mismo y el modelo las compensa entre sí.
  • City_Category: aquí está la diferencia más grande. Comparada con CAT A, una casa igual en CAT B cuesta $1.9 millones menos y en CAT C $2.9 millones menos.
  • Parking: no tener estacionamiento baja el precio unos $536,000 contra tenerlo cubierto.

Generar Predicciones

# Inventamos una casa y le pedimos al modelo que calcule su precio.
# Dejamos todo igual y solo cambiamos la ciudad, para comparar.
datos_nuevos <- data.frame(
  Carpet = 1500,
  Builtup = 1800,
  Parking = "Covered",
  City_Category = c("CAT A", "CAT B", "CAT C")
)

predict(regresion2, newdata = datos_nuevos)
##       1       2       3 
## 7729938 5811420 4821181

Interpretación: la misma casa (1500 de superficie, 1800 de construcción y estacionamiento cubierto) valdría $7.7 millones en CAT A, $5.8 millones en CAT B y $4.8 millones en CAT C.

Conclusión y recomendación: para esta inmobiliaria, lo que más define el precio es la zona de la ciudad y el tamaño de la casa. Qué tan cerca esté del taxi, del mercado o del hospital no cambia el precio, así que no vale la pena usarlo como argumento de venta.

LS0tCnRpdGxlOiAiUmVncmVzacOzbiBMaW5lYWwgLSBQcmVjaW8gZGUgY2FzYXMiCmF1dGhvcjogIlNhbnRpYWdvIEphcmFtaWxsbyBOYXZhIgpkYXRlOiAiMjAyNi0wOC0yMCIKb3V0cHV0OgogICAgaHRtbF9kb2N1bWVudDoKICAgICAgdG9jOiBUUlVFCiAgICAgIHRvY19mbG9hdDogVFJVRQogICAgICBjb2RlX2Rvd25sb2FkOiBUUlVFCiAgICAgIHRoZW1lOiBjb3NtbwotLS0KCiFbXShyZWdyZXNpb25fbGluZWFsLmdpZikKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPlRlb3LDrWE8L3NwYW4+CgoqKmxtKCkqKiBlcyBsYSBmdW5jacOzbiBlbiBSIHBhcmEgYWp1c3RhciBtb2RlbG9zIGxpbmVhbGVzLiAgCkVzIGVsIG1vZGVsbyBlc3RhZMOtc3RpY28gbcOhcyBiw6FzaWNvIHF1ZSBleGlzdGUgeSBtw6FzIGbDoWNpbCBkZSBpbnRlcnByZXRhci4gIApQYXJhIGludGVycHJldGFybG8gc2UgdXNhIGxhIG1lZGlkYSBSLUN1YWRyYWRhLCBxdWUgc2lnbmlmaWNhIHF1w6kgdGFuIGNlcmNhIGVzdMOhbiBsb3MgZGF0b3MgZGUgbGEgcmVncmVzacOzbi4gVmEgZGUgMCBhIDEsIGRvbmRlIDEgZXMgcXVlIGVsIG1vZGVsbyBleHBsaWNhIHRvZGEgbGEgdmFyaWFiaWxpZGFkLgoKIyA8c3BhbiBzdHlsZT0iY29sb3I6Ymx1ZSI+Q29udGV4dG88L3NwYW4+CgpVbmEgaW5tb2JpbGlhcmlhIGN1ZW50YSBjb24gcmVnaXN0cm9zIGRlIGNhc2FzIHZlbmRpZGFzLCBjb24gc3VzIGNhcmFjdGVyw61zdGljYXMgZGUgc3VwZXJmaWNpZSwgdWJpY2FjacOzbiwgZXN0YWNpb25hbWllbnRvIHkgZGlzdGFuY2lhcyBhIHNlcnZpY2lvcy4gU2UgZGVzZWEgZ2VuZXJhciB1biBtb2RlbG8gcHJlZGljdGl2byBkZWwgcHJlY2lvLgoKIyA8c3BhbiBzdHlsZT0iY29sb3I6Ymx1ZSI+SW5zdGFsYXIgcGFxdWV0ZXMgeSBsbGFtYXIgbGlicmVyw61hczwvc3Bhbj4KCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojIGluc3RhbGwucGFja2FnZXMoImNvcnJwbG90IikKbGlicmFyeShjb3JycGxvdCkgICAjIHNpcnZlIHBhcmEgZGlidWphciBlbCBtYXBhIGRlIGNvcnJlbGFjaW9uZXMKYGBgCgojIDxzcGFuIHN0eWxlPSJjb2xvcjpibHVlIj5JbXBvcnRhciBsYSBiYXNlIGRlIGRhdG9zPC9zcGFuPgoKYGBge3J9CiMgZmlsZS5jaG9vc2UoKQpkZiA8LSByZWFkLmNzdigiL1VzZXJzL3NhbnRpYWdvamFyYW1pbGxvL0xpYnJhcnkvTW9iaWxlIERvY3VtZW50cy9jb21+YXBwbGV+Q2xvdWREb2NzL1RlYy9TZW1lc3RyZSA3L0ludGVsaWdlbmNpYSBBcnRpZmljaWFsIGNvbiBJbXBhY3RvIEVtcHJlc2FyaWFsIChHcG8gNjAxKS9Ib3VzZVByaWNlRGF0YS5jc3YiKQpgYGAKCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPkVudGVuZGVyIGxhIGJhc2UgZGUgZGF0b3M8L3NwYW4+CgpgYGB7cn0Kc3VtbWFyeShkZikgICAjIG3DrW5pbW9zLCBtw6F4aW1vcyB5IHByb21lZGlvcyBkZSBjYWRhIGNvbHVtbmEKc3RyKGRmKSAgICAgICAjIHF1w6kgdGlwbyBkZSBkYXRvIGVzIGNhZGEgY29sdW1uYSAobsO6bWVybyBvIHRleHRvKQpoZWFkKGRmKSAgICAgICMgcHJpbWVyYXMgNiBmaWxhcwp0YWlsKGRmKSAgICAgICMgw7psdGltYXMgNiBmaWxhcwpgYGAKCioqTG8gcXVlIHZlbW9zOioqIHNvbiA5MDUgY2FzYXMgeSAxMCBjb2x1bW5hcy4gYEhvdXNlX1ByaWNlYCBlcyBsbyBxdWUgcXVlcmVtb3MgcHJlZGVjaXIuCmBQYXJraW5nYCB5IGBDaXR5X0NhdGVnb3J5YCBzb24gdGV4dG8gKGNhdGVnb3LDrWFzKSwgZWwgcmVzdG8gc29uIG7Dum1lcm9zLgoKYGBge3J9CiMgTGEgY29ycmVsYWNpw7NuIHNvbG8gc2UgcHVlZGUgY2FsY3VsYXIgZW50cmUgbsO6bWVyb3MsIHBvciBlc28gcHJpbWVybwojIHNlcGFyYW1vcyBsYXMgY29sdW1uYXMgbnVtw6lyaWNhcy4KbnVtZXJpY2FzIDwtIGRmW3NhcHBseShkZiwgaXMubnVtZXJpYyldCmNvcnJlbGFjaW9uIDwtIGNvcihudW1lcmljYXMsIHVzZSA9ICJjb21wbGV0ZS5vYnMiKQpjb3JycGxvdChjb3JyZWxhY2lvbikKYGBgCgoqKkPDs21vIGxlZXIgZWwgbWFwYToqKiBsb3MgY8OtcmN1bG9zIGF6dWxlcyBncmFuZGVzIHNpZ25pZmljYW4gcXVlIGRvcyB2YXJpYWJsZXMgc3ViZW4ganVudGFzLgpBcXXDrSBzZSB2ZSBxdWUgYENhcnBldGAgeSBgQnVpbHR1cGAgKGxvcyBkb3MgdGFtYcOxb3MgZGUgbGEgY2FzYSkgZXN0w6FuIG11eSByZWxhY2lvbmFkb3MgZW50cmUgc8OtLAp5IGFtYm9zIGNvbiBlbCBwcmVjaW8uIExhcyBkaXN0YW5jaWFzIHkgbGEgbGx1dmlhIGNhc2kgbm8gdGllbmVuIGNvbG9yLCBvIHNlYSBxdWUKbm8gcGFyZWNlbiBpbmZsdWlyIGVuIGVsIHByZWNpby4KCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPkxpbXBpYXIgbGEgYmFzZSBkZSBkYXRvczwvc3Bhbj4KCmBgYHtyfQpjb2xTdW1zKGlzLm5hKGRmKSkgICAjIGN1ZW50YSBjdcOhbnRvcyBkYXRvcyBmYWx0YW4gZW4gY2FkYSBjb2x1bW5hCmRmIDwtIG5hLm9taXQoZGYpICAgICMgYm9ycmEgbGFzIGZpbGFzIHF1ZSB0aWVuZW4gZGF0b3MgZmFsdGFudGVzCmBgYAoKKipSZWNvbWVuZGFjacOzbjoqKiBhcXXDrSBmYWx0YWJhbiA3IGRhdG9zIGVuIGBDYXJwZXRgLiBBbCBib3JyYXJsb3MgcXVlZGFtb3MgY29uIDg5OCBjYXNhcwpkZSBsYXMgOTA1IG9yaWdpbmFsZXMuIENvbW8gZXMgbWVub3MgZGVsIDElLCBubyBhZmVjdGEgZWwgbW9kZWxvLiBTaSBodWJpZXJhIGZhbHRhZG8KbXVjaGEgaW5mb3JtYWNpw7NuIHPDrSBoYWJyw61hIHF1ZSBwZW5zYXIgZW4gcmVsbGVuYXIgbG9zIGRhdG9zIGVuIGx1Z2FyIGRlIGJvcnJhcmxvcy4KCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPkdlbmVyYXIgZWwgbW9kZWxvPC9zcGFuPgoKYGBge3J9CiMgRWwgcHVudG8gKC4pIHNpZ25pZmljYSAidXNhIHRvZGFzIGxhcyBkZW3DoXMgY29sdW1uYXMgcGFyYSBwcmVkZWNpciBlbCBwcmVjaW8iCnJlZ3Jlc2lvbiA8LSBsbShIb3VzZV9QcmljZSB+IC4sIGRhdGEgPSBkZikKc3VtbWFyeShyZWdyZXNpb24pCmBgYAoKKipDw7NtbyBsZWVyIGVzdG86KiogaGF5IHF1ZSBmaWphcnNlIGVuIGxhcyBlc3RyZWxsYXMgZGVsIGxhZG8gZGVyZWNoby4gRW50cmUgbcOhcyBlc3RyZWxsYXMsCm3DoXMgaW1wb3J0YW50ZSBlcyBlc2EgdmFyaWFibGUgcGFyYSBleHBsaWNhciBlbCBwcmVjaW8uCgotIENvbiBlc3RyZWxsYXMgKHPDrSBzaXJ2ZW4pOiBgQ2FycGV0YCwgYEJ1aWx0dXBgLCBgUGFya2luZ2AgeSBgQ2l0eV9DYXRlZ29yeWAuCi0gU2luIGVzdHJlbGxhcyAobm8gc2lydmVuKTogYERpc3RfVGF4aWAsIGBEaXN0X01hcmtldGAsIGBEaXN0X0hvc3BpdGFsYCB5IGBSYWluZmFsbGAuCgpMYSBSLWN1YWRyYWRhIGVzICoqMC45NDMzKiosIG8gc2VhIHF1ZSBlc3RlIG1vZGVsbyBleHBsaWNhIGVsIDk0JSBkZSBwb3IgcXXDqSB1bmEgY2FzYQpjdWVzdGEgbG8gcXVlIGN1ZXN0YS4gRXN0w6EgbXV5IGJpZW4uCgoqKlJlY29tZW5kYWNpw7NuOioqIGBPYnNlcnZhdGlvbmAgYXBhcmVjZSBjb24gZXN0cmVsbGFzLCBwZXJvIG5vIGhheSBxdWUgdXNhcmxhLiBFcyBzb2xvIGVsCm7Dum1lcm8gZGUgZmlsYSAoMSwgMiwgMy4uLiksIG5vIHVuYSBjYXJhY3RlcsOtc3RpY2EgcmVhbCBkZSBsYSBjYXNhLiBRdWUgc2FsZ2Egc2lnbmlmaWNhdGl2YQplcyBwdXJhIGNvaW5jaWRlbmNpYS4KCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPkFqdXN0YXIgZWwgbW9kZWxvPC9zcGFuPgoKYGBge3J9CiMgUXVpdGFtb3MgbGFzIHZhcmlhYmxlcyBxdWUgbm8gc2VydsOtYW4geSBub3MgcXVlZGFtb3Mgc29sbyBjb24gbGFzIGltcG9ydGFudGVzCnJlZ3Jlc2lvbjIgPC0gbG0oSG91c2VfUHJpY2UgfiBDYXJwZXQgKyBCdWlsdHVwICsgUGFya2luZyArCiAgICAgICAgICAgICAgICAgICBDaXR5X0NhdGVnb3J5LCBkYXRhID0gZGYpCnN1bW1hcnkocmVncmVzaW9uMikKYGBgCgoqKlJlc3VsdGFkbzoqKiBsYSBSLWN1YWRyYWRhIGJhasOzIGFwZW5hcyBkZSAwLjk0MzMgYSAqKjAuOTQxMioqLCBjYXNpIG5hZGEuIEVzdG8gcXVpZXJlIGRlY2lyCnF1ZSBsYXMgNCB2YXJpYWJsZXMgcXVlIHF1aXRhbW9zIGRlIHZlcmRhZCBubyBhcG9ydGFiYW4uCgoqKlJlY29tZW5kYWNpw7NuOioqIHNpZW1wcmUgY29udmllbmUgcXVlZGFyc2UgY29uIGVsIG1vZGVsbyBtw6FzIHNpbXBsZSBjdWFuZG8gZXhwbGljYQpwcsOhY3RpY2FtZW50ZSBsbyBtaXNtby4gRXMgbcOhcyBmw6FjaWwgZGUgZW50ZW5kZXIgeSBkZSB1c2FyLgoKKipRdcOpIHNpZ25pZmljYW4gbG9zIG7Dum1lcm9zOioqCgotIGBDYXJwZXRgOiBjYWRhIG1ldHJvIGV4dHJhIGRlIHN1cGVyZmljaWUgc3ViZSBlbCBwcmVjaW8gdW5vcyAqKiQ5LDk2OSoqLgotIGBCdWlsdHVwYDogc2FsZSBlbiBuZWdhdGl2byBwb3JxdWUgeWEgZXN0w6EgY29udGFkYSBsYSBzdXBlcmZpY2llIGVuIGBDYXJwZXRgOwogIGxhcyBkb3MgbWlkZW4gY2FzaSBsbyBtaXNtbyB5IGVsIG1vZGVsbyBsYXMgY29tcGVuc2EgZW50cmUgc8OtLgotIGBDaXR5X0NhdGVnb3J5YDogYXF1w60gZXN0w6EgbGEgZGlmZXJlbmNpYSBtw6FzIGdyYW5kZS4gQ29tcGFyYWRhIGNvbiBDQVQgQSwKICB1bmEgY2FzYSBpZ3VhbCBlbiBDQVQgQiBjdWVzdGEgKiokMS45IG1pbGxvbmVzIG1lbm9zKiogeSBlbiBDQVQgQyAqKiQyLjkgbWlsbG9uZXMgbWVub3MqKi4KLSBgUGFya2luZ2A6IG5vIHRlbmVyIGVzdGFjaW9uYW1pZW50byBiYWphIGVsIHByZWNpbyB1bm9zICoqJDUzNiwwMDAqKiBjb250cmEgdGVuZXJsbyBjdWJpZXJ0by4KCiMgPHNwYW4gc3R5bGU9ImNvbG9yOmJsdWUiPkdlbmVyYXIgUHJlZGljY2lvbmVzPC9zcGFuPgoKYGBge3J9CiMgSW52ZW50YW1vcyB1bmEgY2FzYSB5IGxlIHBlZGltb3MgYWwgbW9kZWxvIHF1ZSBjYWxjdWxlIHN1IHByZWNpby4KIyBEZWphbW9zIHRvZG8gaWd1YWwgeSBzb2xvIGNhbWJpYW1vcyBsYSBjaXVkYWQsIHBhcmEgY29tcGFyYXIuCmRhdG9zX251ZXZvcyA8LSBkYXRhLmZyYW1lKAogIENhcnBldCA9IDE1MDAsCiAgQnVpbHR1cCA9IDE4MDAsCiAgUGFya2luZyA9ICJDb3ZlcmVkIiwKICBDaXR5X0NhdGVnb3J5ID0gYygiQ0FUIEEiLCAiQ0FUIEIiLCAiQ0FUIEMiKQopCgpwcmVkaWN0KHJlZ3Jlc2lvbjIsIG5ld2RhdGEgPSBkYXRvc19udWV2b3MpCmBgYAoKKipJbnRlcnByZXRhY2nDs246KiogbGEgbWlzbWEgY2FzYSAoMTUwMCBkZSBzdXBlcmZpY2llLCAxODAwIGRlIGNvbnN0cnVjY2nDs24geSBlc3RhY2lvbmFtaWVudG8KY3ViaWVydG8pIHZhbGRyw61hICoqJDcuNyBtaWxsb25lcyoqIGVuIENBVCBBLCAqKiQ1LjggbWlsbG9uZXMqKiBlbiBDQVQgQiB5ICoqJDQuOCBtaWxsb25lcyoqCmVuIENBVCBDLgoKKipDb25jbHVzacOzbiB5IHJlY29tZW5kYWNpw7NuOioqIHBhcmEgZXN0YSBpbm1vYmlsaWFyaWEsIGxvIHF1ZSBtw6FzIGRlZmluZSBlbCBwcmVjaW8gZXMKKipsYSB6b25hIGRlIGxhIGNpdWRhZCB5IGVsIHRhbWHDsW8gZGUgbGEgY2FzYSoqLiBRdcOpIHRhbiBjZXJjYSBlc3TDqSBkZWwgdGF4aSwgZGVsIG1lcmNhZG8KbyBkZWwgaG9zcGl0YWwgbm8gY2FtYmlhIGVsIHByZWNpbywgYXPDrSBxdWUgbm8gdmFsZSBsYSBwZW5hIHVzYXJsbyBjb21vIGFyZ3VtZW50byBkZSB2ZW50YS4K