Reabramos la conexión a la base de datos. Ya que está en memoria, y al cerrar la conexión se ha borrado, tendremos que volver a hacer el proceso de antes. En una base de datos habitual los datos son persistentes.
cis <- readRDS('./assets/clean-data.RDS')
con <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(con, "cis", cis)
WHERE
Armados con lo que hemos visto antes, podemos empezar ahora a utilizar SQL para entender mejor los datos. Por ejemplo, podemos hacer selecciones de los datos para echar un vistazo a la composición de los datos por ideología:
dbFetch(dbSendQuery(con, "SELECT age, gender FROM cis WHERE ideology == 10;"))
Podemos por ejemplo mirar a la distribución en los extremos ideológicos
dbFetch(dbSendQuery(con, "SELECT COUNT(*) FROM cis WHERE ideology == 1;"))
dbFetch(dbSendQuery(con, "SELECT COUNT(*) FROM cis WHERE ideology == 10;"))
dbFetch(dbSendQuery(con, "SELECT COUNT(*) FROM cis WHERE ideology == 1 OR ideology == 10;"))
NULL
Una cosa importante es que RBDMS tiene el concepto de NULL
que representa varios tipos de situaciones, pero que en este caso nos ayuda a representar los valores perdidos.
dbFetch(dbSendQuery(con, "SELECT COUNT(*) FROM cis WHERE ideology IS NULL;"))
ORDER BY
Podemos ordenar los valores a lo largo de una variable. Por ejemplo, podemos ver la distribución de los casos usando como orden la ideología:
res <- dbSendQuery(con, "SELECT age, gender, ideology FROM cis ORDER BY ideology;")
dbFetch(res)
Como vemos, los valores NULL
aparecen al principio, que quizás es algo que prefiramos no ver en este caso
res <- dbSendQuery(con, "SELECT age, ideology FROM cis ORDER BY ideology WHERE ideology is not NULL;")
dbFetch(res)
SQL es muy flexible pero tiene preferencias sobre el orden de las cláusulas. Lo que es útil es que al evaluar la búsqueda, SQL intenta ser útil dándonos una pista de qué ha ido mal.
res <- dbSendQuery(con, "SELECT age, ideology FROM cis WHERE ideology is not NULL ORDER BY ideology;")
dbFetch(res)
Es útil que podamos también ejecutar el orden inverso:
res <- dbSendQuery(con, "SELECT age, ideology FROM cis ORDER BY ideology DESC;")
dbFetch(res)
Y que podamos ordenar los datos por más de una variable. En este caso, ordenamos ideología y edad usando diferente órdenes.
res <- dbSendQuery(con, "SELECT age, ideology FROM cis ORDER BY ideology DESC, age;")
dbFetch(res)
GROUP BY y HAVING
Ahora podemos empezar a hacer análisis más interesantes. Por ejemplo, podemos intentar contar el número de votantes de cada partido. Para ello queremos agrupar los datos por partido y después aplicar la función COUNT
a esos resultados.
dbFetch(dbSendQuery(con, "SELECT COUNT(*) FROM cis GROUP BY (pid);"))
¿Qué nos falta aquí? Saber el partido al que se refiere la búsqueda
dbFetch(dbSendQuery(con, "SELECT pid, COUNT(*) FROM cis GROUP BY (pid);"))
Igual que antes, podemos asociar un resultado (una columna en este caso) a un nombre usando un alias:
dbFetch(dbSendQuery(con, "SELECT pid, COUNT(*) as count FROM cis GROUP BY (pid);"))
Veamos ahora por ejemplo la distribución ideológica de cada partido mirando para ello la ideología declarada de los votantes de ese partido. Lo que queremos hacer es agrupar los datos por pid
y después, para cada grupo, calcular la media de la ideología.
dbFetch(dbSendQuery(con, "SELECT pid, AVG(ideology) as mean FROM cis GROUP BY (pid);"))
Podemos también agruppar los resultados usando más de una variable. Por ejemplo, podemos calcular la distribución ideológica usando pid
pero también recuerdo de voto, lo que nos permite ver si los que se identifican con un partido pero no han votado por él en el pasado tienen una ideología diferente de los que son consistentes.
dbFetch(dbSendQuery(con, "SELECT pid, pastvote, AVG(ideology)
FROM cis
WHERE pid IS NOT NULL
AND pastvote IS NOT NULL
GROUP BY pid, pastvote;"))
HAVING
La cláusula HAVING
es poco intuitiva al principio pero es muy útil. Imaginemos que nos interesa calcular los resultados pero únicamente para aquellos grupos para los que tenemos suficientes datos como para que la media de ideología sea una estimación “razonable”. En este caso, queremos calcular la tabla anterior y después de calcularla queremos limitarla a las filas para las que el COUNT
de ideología sea mayor que, por ejemplo, 30. WHERE
no nos será útil en este caso. Y podríamos resolver este problema usando una subquery (que no veremos) pero por lo general la solución más eficiente cuando queremos aplicar transformaciones/funciones a grupos, es usar HAVING.
dbFetch(dbSendQuery(con, "SELECT pid, pastvote, AVG(ideology)
FROM cis
WHERE pid IS NOT NULL
AND pastvote IS NOT NULL
GROUP BY pid, pastvote
HAVING COUNT(ideology) > 30;"))
JOIN
Ya hemos visto que uno de los valores de las bases de datos relacionales es el hecho de que nos permiten establecer relaciones entre tablas. Una de las utilidades más prácticas que SQL nos permite es realizar un JOIN
, es decir, juntar variables tablas usando columnas comunes. Es lo mismo que hemos visto usando merge
en R
. Es un tema que se escapa del contenido básico pero es útil quizás ver un JOIN
Empezaremos por cargar en SQLite
otra tabla con la que poder hacer JOIN
. Por ejemplo, en los datos CIS que hemos usado hasta ahora tenemos una distribución de quienes han contestado a las preguntas de la entrevista. Pero podemos intentar poner estos datos en el contexto de, por ejemplo, datos contextuales sobre la región de la que los votantes vienen. Para ello podemos poner en la base de datos una tabla que capture información a nivel regional.
frame <- read.csv('./assets/ccaa.csv')
dbWriteTable(con, "frame", frame)
dbListTables(con)
Listemos los datos:
dbFetch(dbSendQuery(con, "SELECT * FROM frame;"))
Ahora haremos un JOIN
de las dos tablas de tal forma que, a cada entrevistado, le asignemos los datos de renta de la comunidad autónoma en la que vive usando los datos de 2018.
res <- dbFetch(dbSendQuery(con,
"SELECT *
FROM cis
JOIN frame
ON cis.ccaa = frame.ccaa"))
head(res)
Pero quizás es más interesante ejecutar operaciones directamente en la base de datos y sobre el JOIN
que acabamos de hacer
res <- dbFetch(dbSendQuery(con,
"SELECT cis.pid, cis.ccaa, frame.renta, AVG(ideology) as mean_ideology
FROM cis
JOIN frame
ON cis.ccaa = frame.ccaa
WHERE pid IS NOT NULL
AND pastvote IS NOT NULL
GROUP BY pid
HAVING COUNT(ideology) > 30
AND renta > AVG(renta);"))
print(res)
dbplyr
Lo que hemos visto hasta ahora es muy parecido a dplyr
que es el conjuto de verbos para la gestión de matrices de datos que vimos en la introducción a R
. No es una sorpresa. La idea detrás de dplyr
es la de incorporar a R
un lenguaje similar a SQL. De hecho, una extensión de dplyr
nos permite usar los mismos verbos pero con una base de datos como backend. Es decir, podríamos ejecutar la mayoría de las instrucciones anteriores pero usando dbplyr
. Para ello, lo único que tenemos hacer es establecer una conexión y declarar en qué tabla queremos trabajar.
library(dbplyr)
cis_db <- tbl(con, "cis")
Podríamos usar la misma query de antes directamente desde R
:
res <- cis_db %>%
group_by(pid, pastvote) %>%
filter(!is.na(pid) & !is.na(pastvote)) %>%
summarize(mean_ideology=mean(ideology, na.rm=TRUE))
res
Para ejecutar la búsqueda y crear una copia usaremos collect
res %>% collect()
Podemos ver el statement en SQL
res %>% show_query()
LS0tIAp0aXRsZTogIkludHJvZHVjY2nDs24gYSBTUUwgKHkgSUkpIgpkYXRlOiAiYHIgZm9ybWF0KFN5cy50aW1lKCksICclQiAlZCwgJVknKWAiCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0UsIGNhY2hlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZXZhbCA9IEZBTFNFKQprbml0cjo6b3B0c19jaHVuayRzZXQoZmlnLnBhdGggPSAnLi9hc3NldHMvJykKYGBgCgpSZWFicmFtb3MgbGEgY29uZXhpw7NuIGEgbGEgYmFzZSBkZSBkYXRvcy4gWWEgcXVlIGVzdMOhIGVuIG1lbW9yaWEsIHkgYWwKY2VycmFyIGxhIGNvbmV4acOzbiBzZSBoYSBib3JyYWRvLCB0ZW5kcmVtb3MgcXVlIHZvbHZlciBhIGhhY2VyIGVsCnByb2Nlc28gZGUgYW50ZXMuIEVuIHVuYSBiYXNlIGRlIGRhdG9zIGhhYml0dWFsIGxvcyBkYXRvcyBzb24KcGVyc2lzdGVudGVzLgoKYGBge3J9CmNpcyA8LSByZWFkUkRTKCcuL2Fzc2V0cy9jbGVhbi1kYXRhLlJEUycpCmNvbiA8LSBkYkNvbm5lY3QoUlNRTGl0ZTo6U1FMaXRlKCksICI6bWVtb3J5OiIpCmRiV3JpdGVUYWJsZShjb24sICJjaXMiLCBjaXMpCmBgYAoKIyBXSEVSRQoKQXJtYWRvcyBjb24gbG8gcXVlIGhlbW9zIHZpc3RvIGFudGVzLCBwb2RlbW9zIGVtcGV6YXIgYWhvcmEgYSB1dGlsaXphcgpTUUwgcGFyYSBlbnRlbmRlciBtZWpvciBsb3MgZGF0b3MuIFBvciBlamVtcGxvLCBwb2RlbW9zIGhhY2VyCnNlbGVjY2lvbmVzIGRlIGxvcyBkYXRvcyBwYXJhIGVjaGFyIHVuIHZpc3Rhem8gYSBsYSBjb21wb3NpY2nDs24gZGUgbG9zCmRhdG9zIHBvciBpZGVvbG9nw61hOgoKYGBge3J9CmRiRmV0Y2goZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgZ2VuZGVyIEZST00gY2lzIFdIRVJFIGlkZW9sb2d5ID09IDEwOyIpKQpgYGAKClBvZGVtb3MgcG9yIGVqZW1wbG8gbWlyYXIgYSBsYSBkaXN0cmlidWNpw7NuIGVuIGxvcyBleHRyZW1vcwppZGVvbMOzZ2ljb3MKCmBgYHtyfQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCBDT1VOVCgqKSBGUk9NIGNpcyBXSEVSRSBpZGVvbG9neSA9PSAxOyIpKQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCBDT1VOVCgqKSBGUk9NIGNpcyBXSEVSRSBpZGVvbG9neSA9PSAxMDsiKSkKZGJGZXRjaChkYlNlbmRRdWVyeShjb24sICJTRUxFQ1QgQ09VTlQoKikgRlJPTSBjaXMgV0hFUkUgaWRlb2xvZ3kgPT0gMSBPUiBpZGVvbG9neSA9PSAxMDsiKSkKYGBgCgojIE5VTEwKClVuYSBjb3NhIGltcG9ydGFudGUgZXMgcXVlIFJCRE1TIHRpZW5lIGVsIGNvbmNlcHRvIGRlIGBOVUxMYCBxdWUKcmVwcmVzZW50YSB2YXJpb3MgdGlwb3MgZGUgc2l0dWFjaW9uZXMsIHBlcm8gcXVlIGVuIGVzdGUgY2FzbyBub3MKYXl1ZGEgYSByZXByZXNlbnRhciBsb3MgdmFsb3JlcyBwZXJkaWRvcy4gCgpgYGB7cn0KZGJGZXRjaChkYlNlbmRRdWVyeShjb24sICJTRUxFQ1QgQ09VTlQoKikgRlJPTSBjaXMgV0hFUkUgaWRlb2xvZ3kgSVMgTlVMTDsiKSkKYGBgCgoKIyBPUkRFUiBCWQoKUG9kZW1vcyBvcmRlbmFyIGxvcyB2YWxvcmVzIGEgbG8gbGFyZ28gZGUgdW5hIHZhcmlhYmxlLiBQb3IgZWplbXBsbywKcG9kZW1vcyB2ZXIgbGEgZGlzdHJpYnVjacOzbiBkZSBsb3MgY2Fzb3MgdXNhbmRvIGNvbW8gb3JkZW4gbGEKaWRlb2xvZ8OtYToKCmBgYHtyfQpyZXMgPC0gZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgZ2VuZGVyLCBpZGVvbG9neSBGUk9NIGNpcyBPUkRFUiBCWSBpZGVvbG9neTsiKQpkYkZldGNoKHJlcykKYGBgCgpDb21vIHZlbW9zLCBsb3MgdmFsb3JlcyBgTlVMTGAgYXBhcmVjZW4gYWwgcHJpbmNpcGlvLCBxdWUgcXVpesOhcyBlcwphbGdvIHF1ZSBwcmVmaXJhbW9zIG5vIHZlciBlbiBlc3RlIGNhc28KCmBgYHtyfQpyZXMgPC0gZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgaWRlb2xvZ3kgRlJPTSBjaXMgT1JERVIgQlkgaWRlb2xvZ3kgV0hFUkUgaWRlb2xvZ3kgaXMgbm90IE5VTEw7IikKZGJGZXRjaChyZXMpCmBgYAoKU1FMIGVzIG11eSBmbGV4aWJsZSBwZXJvIHRpZW5lIHByZWZlcmVuY2lhcyBzb2JyZSBlbCBvcmRlbiBkZSBsYXMKY2zDoXVzdWxhcy4gTG8gcXVlIGVzIMO6dGlsIGVzIHF1ZSBhbCBldmFsdWFyIGxhIGLDunNxdWVkYSwgU1FMIGludGVudGEKc2VyIMO6dGlsIGTDoW5kb25vcyB1bmEgcGlzdGEgZGUgcXXDqSBoYSBpZG8gbWFsLiAKCmBgYHtyfQpyZXMgPC0gZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgaWRlb2xvZ3kgRlJPTSBjaXMgV0hFUkUgaWRlb2xvZ3kgaXMgbm90IE5VTEwgT1JERVIgQlkgaWRlb2xvZ3k7IikKZGJGZXRjaChyZXMpCmBgYAoKRXMgw7p0aWwgcXVlIHBvZGFtb3MgdGFtYmnDqW4gZWplY3V0YXIgZWwgb3JkZW4gaW52ZXJzbzoKCmBgYHtyfQpyZXMgPC0gZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgaWRlb2xvZ3kgRlJPTSBjaXMgT1JERVIgQlkgaWRlb2xvZ3kgREVTQzsiKQpkYkZldGNoKHJlcykKYGBgCgpZIHF1ZSBwb2RhbW9zIG9yZGVuYXIgbG9zIGRhdG9zIHBvciBtw6FzIGRlIHVuYSB2YXJpYWJsZS4gRW4gZXN0ZSBjYXNvLApvcmRlbmFtb3MgaWRlb2xvZ8OtYSB5IGVkYWQgdXNhbmRvIGRpZmVyZW50ZSDDs3JkZW5lcy4KCmBgYHtyfQpyZXMgPC0gZGJTZW5kUXVlcnkoY29uLCAiU0VMRUNUIGFnZSwgaWRlb2xvZ3kgRlJPTSBjaXMgT1JERVIgQlkgaWRlb2xvZ3kgREVTQywgYWdlOyIpCmRiRmV0Y2gocmVzKQpgYGAKCiMgR1JPVVAgQlkgeSBIQVZJTkcKCkFob3JhIHBvZGVtb3MgZW1wZXphciBhIGhhY2VyIGFuw6FsaXNpcyBtw6FzIGludGVyZXNhbnRlcy4gUG9yIGVqZW1wbG8sCnBvZGVtb3MgaW50ZW50YXIgY29udGFyIGVsIG7Dum1lcm8gZGUgdm90YW50ZXMgZGUgY2FkYSBwYXJ0aWRvLiBQYXJhCmVsbG8gcXVlcmVtb3MgYWdydXBhciBsb3MgZGF0b3MgcG9yIHBhcnRpZG8geSBkZXNwdcOpcyBhcGxpY2FyIGxhCmZ1bmNpw7NuIGBDT1VOVGAgYSBlc29zIHJlc3VsdGFkb3MuCgpgYGB7cn0KZGJGZXRjaChkYlNlbmRRdWVyeShjb24sICJTRUxFQ1QgQ09VTlQoKikgRlJPTSBjaXMgR1JPVVAgQlkgKHBpZCk7IikpCmBgYAoKwr9RdcOpIG5vcyBmYWx0YSBhcXXDrT8gU2FiZXIgZWwgcGFydGlkbyBhbCBxdWUgc2UgcmVmaWVyZSBsYSBiw7pzcXVlZGEKCmBgYHtyfQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCBwaWQsIENPVU5UKCopIEZST00gY2lzIEdST1VQIEJZIChwaWQpOyIpKQpgYGAKCklndWFsIHF1ZSBhbnRlcywgcG9kZW1vcyBhc29jaWFyIHVuIHJlc3VsdGFkbyAodW5hIGNvbHVtbmEgZW4gZXN0ZQpjYXNvKSBhIHVuIG5vbWJyZSB1c2FuZG8gdW4gYWxpYXM6CgpgYGB7cn0KZGJGZXRjaChkYlNlbmRRdWVyeShjb24sICJTRUxFQ1QgcGlkLCBDT1VOVCgqKSBhcyBjb3VudCBGUk9NIGNpcyBHUk9VUCBCWSAocGlkKTsiKSkKYGBgCgpWZWFtb3MgYWhvcmEgcG9yIGVqZW1wbG8gbGEgZGlzdHJpYnVjacOzbiBpZGVvbMOzZ2ljYSBkZSBjYWRhIHBhcnRpZG8KbWlyYW5kbyBwYXJhIGVsbG8gbGEgaWRlb2xvZ8OtYSBkZWNsYXJhZGEgZGUgbG9zIHZvdGFudGVzIGRlIGVzZQpwYXJ0aWRvLiBMbyBxdWUgcXVlcmVtb3MgaGFjZXIgZXMgYWdydXBhciBsb3MgZGF0b3MgcG9yIGBwaWRgIHkKZGVzcHXDqXMsIHBhcmEgY2FkYSBncnVwbywgY2FsY3VsYXIgbGEgbWVkaWEgZGUgbGEgaWRlb2xvZ8OtYS4gCgpgYGB7cn0KZGJGZXRjaChkYlNlbmRRdWVyeShjb24sICJTRUxFQ1QgcGlkLCBBVkcoaWRlb2xvZ3kpIGFzIG1lYW4gRlJPTSBjaXMgR1JPVVAgQlkgKHBpZCk7IikpCmBgYAoKUG9kZW1vcyB0YW1iacOpbiBhZ3J1cHBhciBsb3MgcmVzdWx0YWRvcyB1c2FuZG8gbcOhcyBkZSB1bmEgdmFyaWFibGUuClBvciBlamVtcGxvLCBwb2RlbW9zIGNhbGN1bGFyIGxhIGRpc3RyaWJ1Y2nDs24gaWRlb2zDs2dpY2EgdXNhbmRvIGBwaWRgCnBlcm8gdGFtYmnDqW4gcmVjdWVyZG8gZGUgdm90bywgbG8gcXVlIG5vcyBwZXJtaXRlIHZlciBzaSBsb3MgcXVlIHNlCmlkZW50aWZpY2FuIGNvbiB1biBwYXJ0aWRvIHBlcm8gbm8gaGFuIHZvdGFkbyBwb3Igw6lsIGVuIGVsIHBhc2Fkbwp0aWVuZW4gdW5hIGlkZW9sb2fDrWEgZGlmZXJlbnRlIGRlIGxvcyBxdWUgc29uIGNvbnNpc3RlbnRlcy4KCmBgYHtyfQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCBwaWQsIHBhc3R2b3RlLCBBVkcoaWRlb2xvZ3kpIAogICAgICAgICAgICAgICAgICAgICAgICAgIEZST00gY2lzIAogICAgICAgICAgICAgICAgICAgICAgICAgIFdIRVJFIHBpZCBJUyBOT1QgTlVMTAogICAgICAgICAgICAgICAgICAgICAgICAgIEFORCBwYXN0dm90ZSBJUyBOT1QgTlVMTAogICAgICAgICAgICAgICAgICAgICAgICAgIEdST1VQIEJZIHBpZCwgcGFzdHZvdGU7IikpCmBgYAoKIyMgSEFWSU5HCgpMYSBjbMOhdXN1bGEgYEhBVklOR2AgZXMgcG9jbyBpbnR1aXRpdmEgYWwgcHJpbmNpcGlvIHBlcm8gZXMgbXV5IMO6dGlsLgpJbWFnaW5lbW9zIHF1ZSBub3MgaW50ZXJlc2EgY2FsY3VsYXIgbG9zIHJlc3VsdGFkb3MgcGVybyDDum5pY2FtZW50ZQpwYXJhIGFxdWVsbG9zIGdydXBvcyBwYXJhIGxvcyBxdWUgdGVuZW1vcyBzdWZpY2llbnRlcyBkYXRvcyBjb21vIHBhcmEKcXVlIGxhIG1lZGlhIGRlIGlkZW9sb2fDrWEgc2VhIHVuYSBlc3RpbWFjacOzbiAicmF6b25hYmxlIi4gRW4gZXN0ZQpjYXNvLCBxdWVyZW1vcyBjYWxjdWxhciBsYSB0YWJsYSBhbnRlcmlvciB5IGRlc3B1w6lzIGRlIGNhbGN1bGFybGEKcXVlcmVtb3MgbGltaXRhcmxhIGEgbGFzIGZpbGFzIHBhcmEgbGFzIHF1ZSBlbCBgQ09VTlRgIGRlIGlkZW9sb2fDrWEKc2VhIG1heW9yIHF1ZSwgcG9yIGVqZW1wbG8sIDMwLiBgV0hFUkVgIG5vIG5vcyBzZXLDoSDDunRpbCBlbiBlc3RlIGNhc28uClkgcG9kcsOtYW1vcyByZXNvbHZlciBlc3RlIHByb2JsZW1hIHVzYW5kbyB1bmEgc3VicXVlcnkgKHF1ZSBubwp2ZXJlbW9zKSBwZXJvIHBvciBsbyBnZW5lcmFsIGxhIHNvbHVjacOzbiBtw6FzIGVmaWNpZW50ZSBjdWFuZG8gcXVlcmVtb3MKYXBsaWNhciB0cmFuc2Zvcm1hY2lvbmVzL2Z1bmNpb25lcyBhIGdydXBvcywgZXMgdXNhciBgSEFWSU5HLmAKCmBgYHtyfQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCBwaWQsIHBhc3R2b3RlLCBBVkcoaWRlb2xvZ3kpIAogICAgICAgICAgICAgICAgICAgICAgICAgIEZST00gY2lzIAogICAgICAgICAgICAgICAgICAgICAgICAgIFdIRVJFIHBpZCBJUyBOT1QgTlVMTAogICAgICAgICAgICAgICAgICAgICAgICAgIEFORCBwYXN0dm90ZSBJUyBOT1QgTlVMTAogICAgICAgICAgICAgICAgICAgICAgICAgIEdST1VQIEJZIHBpZCwgcGFzdHZvdGUKICAgICAgICAgICAgICAgICAgICAgICAgICBIQVZJTkcgQ09VTlQoaWRlb2xvZ3kpID4gMzA7IikpCmBgYAoKIyBKT0lOCgpZYSBoZW1vcyB2aXN0byBxdWUgdW5vIGRlIGxvcyB2YWxvcmVzIGRlIGxhcyBiYXNlcyBkZSBkYXRvcwpyZWxhY2lvbmFsZXMgZXMgZWwgaGVjaG8gZGUgcXVlIG5vcyBwZXJtaXRlbiBlc3RhYmxlY2VyIHJlbGFjaW9uZXMKZW50cmUgdGFibGFzLiBVbmEgZGUgbGFzIHV0aWxpZGFkZXMgbcOhcyBwcsOhY3RpY2FzIHF1ZSBTUUwgbm9zIHBlcm1pdGUKZXMgcmVhbGl6YXIgdW4gYEpPSU5gLCBlcyBkZWNpciwganVudGFyIHZhcmlhYmxlcyB0YWJsYXMgdXNhbmRvCmNvbHVtbmFzIGNvbXVuZXMuIEVzIGxvIG1pc21vIHF1ZSBoZW1vcyB2aXN0byB1c2FuZG8gYG1lcmdlYCBlbiBgUmAuCkVzIHVuIHRlbWEgcXVlIHNlIGVzY2FwYSBkZWwgY29udGVuaWRvIGLDoXNpY28gcGVybyBlcyDDunRpbCBxdWl6w6FzIHZlcgp1biBgSk9JTmAKCkVtcGV6YXJlbW9zIHBvciBjYXJnYXIgZW4gYFNRTGl0ZWAgb3RyYSB0YWJsYSBjb24gbGEgcXVlIHBvZGVyIGhhY2VyCmBKT0lOYC4gUG9yIGVqZW1wbG8sIGVuIGxvcyBkYXRvcyBDSVMgcXVlIGhlbW9zIHVzYWRvIGhhc3RhIGFob3JhCnRlbmVtb3MgdW5hIGRpc3RyaWJ1Y2nDs24gZGUgcXVpZW5lcyBoYW4gY29udGVzdGFkbyBhIGxhcyBwcmVndW50YXMKZGUgbGEgZW50cmV2aXN0YS4gUGVybyBwb2RlbW9zIGludGVudGFyIHBvbmVyIGVzdG9zIGRhdG9zIGVuIGVsCmNvbnRleHRvIGRlLCBwb3IgZWplbXBsbywgZGF0b3MgY29udGV4dHVhbGVzIHNvYnJlIGxhIHJlZ2nDs24gZGUgbGEgcXVlCmxvcyB2b3RhbnRlcyB2aWVuZW4uIFBhcmEgZWxsbyBwb2RlbW9zIHBvbmVyIGVuIGxhIGJhc2UgZGUgZGF0b3MgdW5hCnRhYmxhIHF1ZSBjYXB0dXJlIGluZm9ybWFjacOzbiBhIG5pdmVsIHJlZ2lvbmFsLgoKYGBge3J9CmZyYW1lIDwtIHJlYWQuY3N2KCcuL2Fzc2V0cy9jY2FhLmNzdicpCmRiV3JpdGVUYWJsZShjb24sICJmcmFtZSIsIGZyYW1lKQpkYkxpc3RUYWJsZXMoY29uKQpgYGAKCkxpc3RlbW9zIGxvcyBkYXRvczoKCmBgYHtyfQpkYkZldGNoKGRiU2VuZFF1ZXJ5KGNvbiwgIlNFTEVDVCAqIEZST00gZnJhbWU7IikpCmBgYAoKQWhvcmEgaGFyZW1vcyB1biBgSk9JTmAgZGUgbGFzIGRvcyB0YWJsYXMgZGUgdGFsIGZvcm1hIHF1ZSwgYSBjYWRhCmVudHJldmlzdGFkbywgbGUgYXNpZ25lbW9zIGxvcyBkYXRvcyBkZSByZW50YSBkZSBsYSBjb211bmlkYWQgYXV0w7Nub21hCmVuIGxhIHF1ZSB2aXZlIHVzYW5kbyBsb3MgZGF0b3MgZGUgMjAxOC4KCmBgYHtyfQpyZXMgPC0gZGJGZXRjaChkYlNlbmRRdWVyeShjb24sCiJTRUxFQ1QgKgogRlJPTSBjaXMKIEpPSU4gZnJhbWUKIE9OIGNpcy5jY2FhID0gZnJhbWUuY2NhYSIpKQpoZWFkKHJlcykKYGBgCgpQZXJvIHF1aXrDoXMgZXMgbcOhcyBpbnRlcmVzYW50ZSBlamVjdXRhciBvcGVyYWNpb25lcyBkaXJlY3RhbWVudGUgZW4gbGEKYmFzZSBkZSBkYXRvcyB5IHNvYnJlIGVsIGBKT0lOYCBxdWUgYWNhYmFtb3MgZGUgaGFjZXIKCmBgYHtyfQpyZXMgPC0gZGJGZXRjaChkYlNlbmRRdWVyeShjb24sCiJTRUxFQ1QgY2lzLnBpZCwgY2lzLmNjYWEsIGZyYW1lLnJlbnRhLCBBVkcoaWRlb2xvZ3kpIGFzIG1lYW5faWRlb2xvZ3kKIEZST00gY2lzCiBKT0lOIGZyYW1lCiBPTiBjaXMuY2NhYSA9IGZyYW1lLmNjYWEKIFdIRVJFIHBpZCBJUyBOT1QgTlVMTAogQU5EIHBhc3R2b3RlIElTIE5PVCBOVUxMCiBHUk9VUCBCWSBwaWQKIEhBVklORyBDT1VOVChpZGVvbG9neSkgPiAzMAogQU5EIHJlbnRhID4gQVZHKHJlbnRhKTsiKSkKcHJpbnQocmVzKQpgYGAKCiMgZGJwbHlyCgpMbyBxdWUgaGVtb3MgdmlzdG8gaGFzdGEgYWhvcmEgZXMgbXV5IHBhcmVjaWRvIGEgYGRwbHlyYCBxdWUgZXMgZWwKY29uanV0byBkZSB2ZXJib3MgcGFyYSBsYSBnZXN0acOzbiBkZSBtYXRyaWNlcyBkZSBkYXRvcyBxdWUgdmltb3MgZW4gbGEKaW50cm9kdWNjacOzbiBhIGBSYC4gTm8gZXMgdW5hIHNvcnByZXNhLiBMYSBpZGVhIGRldHLDoXMgZGUgYGRwbHlyYCBlcwpsYSBkZSBpbmNvcnBvcmFyIGEgYFJgIHVuIGxlbmd1YWplIHNpbWlsYXIgYSBTUUwuIERlIGhlY2hvLCB1bmEKZXh0ZW5zacOzbiBkZSBgZHBseXJgIG5vcyBwZXJtaXRlIHVzYXIgbG9zIG1pc21vcyB2ZXJib3MgcGVybyBjb24gdW5hCmJhc2UgZGUgZGF0b3MgY29tbyBfYmFja2VuZF8uIEVzIGRlY2lyLCBwb2Ryw61hbW9zIGVqZWN1dGFyIGxhIG1heW9yw61hCmRlIGxhcyBpbnN0cnVjY2lvbmVzIGFudGVyaW9yZXMgcGVybyB1c2FuZG8gYGRicGx5cmAuIFBhcmEgZWxsbywgbG8Kw7puaWNvIHF1ZSB0ZW5lbW9zIGhhY2VyIGVzIGVzdGFibGVjZXIgdW5hIGNvbmV4acOzbiB5IGRlY2xhcmFyIGVuIHF1w6kKdGFibGEgcXVlcmVtb3MgdHJhYmFqYXIuCgpgYGB7cn0KbGlicmFyeShkYnBseXIpCmNpc19kYiA8LSB0YmwoY29uLCAiY2lzIikKYGBgCgpQb2Ryw61hbW9zIHVzYXIgbGEgbWlzbWEgcXVlcnkgZGUgYW50ZXMgZGlyZWN0YW1lbnRlIGRlc2RlIGBSYDoKCmBgYHtyfQpyZXMgPC0gY2lzX2RiICU+JQogICAgZ3JvdXBfYnkocGlkLCBwYXN0dm90ZSkgJT4lCiAgICBmaWx0ZXIoIWlzLm5hKHBpZCkgJiAhaXMubmEocGFzdHZvdGUpKSAlPiUKICAgIHN1bW1hcml6ZShtZWFuX2lkZW9sb2d5PW1lYW4oaWRlb2xvZ3ksIG5hLnJtPVRSVUUpKQpyZXMKYGBgCgpQYXJhIGVqZWN1dGFyIGxhIGLDunNxdWVkYSB5IGNyZWFyIHVuYSBjb3BpYSB1c2FyZW1vcyBgY29sbGVjdGAKCmBgYHtyfQpyZXMgJT4lIGNvbGxlY3QoKSAgICAKYGBgCgpQb2RlbW9zIHZlciBlbCBzdGF0ZW1lbnQgZW4gU1FMIAoKYGBge3J9CnJlcyAlPiUgc2hvd19xdWVyeSgpICAgIApgYGAK