Autocorrelación de delitos en CABA - 2022

Joaquín Lovizio Ramos

2024-10-28

Introducción

En este trabajo intentaremos analizar el comportamiento geo-espacial de delitos denunciados en CABA durante 2022. Se trabajará con el dataset provisto por el portal de datos abiertos del GCBA (disponible aquí y un dataset de radios censales. El análisis es exploratorio e intentará detectar zonas con alto volumen de delitos y zonas con comportamientos particulares.

Importo librerías

# !pip install mapclassify folium pointpats contextily pygeos h3 pysal
import pandas as pd
import numpy as np
import geopandas as gpd
import os

import matplotlib.pyplot as plt
from matplotlib.patches import Polygon as PolygonM #noten como usamos otro alias
from matplotlib.patches import Patch
from shapely.geometry import Polygon
from splot.esda import plot_moran_bv_simulation, plot_moran_bv, moran_scatterplot


import seaborn as sns
import contextily as ctx

import libpysal as lps

import esda
from esda.moran import Moran_BV, Moran_Local_BV

Cargo data

data = pd.read_csv('./delitos/delitos_2022.csv', delimiter=",", encoding="Latin1")
print(data.shape)
## (112961, 16)
print(data.dtypes)
## Unnamed: 0      int64
## id.mapa         int64
## anio            int64
## mes            object
## dia            object
## fecha          object
## franja          int64
## tipo           object
## subtipo        object
## uso_arma       object
## uso_moto       object
## barrio         object
## comuna        float64
## latitud       float64
## longitud      float64
## cantidad        int64
## dtype: object
data.head()
##    Unnamed: 0  id.mapa  anio        mes  ... comuna    latitud   longitud cantidad
## 0           1        1  2022    OCTUBRE  ...   15.0 -34.584136 -58.454704        1
## 1           2        2  2022    OCTUBRE  ...    4.0 -34.645043 -58.373194        1
## 2           3        3  2022  NOVIEMBRE  ...   15.0 -34.589982 -58.446471        1
## 3           4        5  2022  NOVIEMBRE  ...    2.0 -34.596748 -58.413609        1
## 4           5        6  2022       MAYO  ...    9.0 -34.640978 -58.480254        1
## 
## [5 rows x 16 columns]
# Leemos los datos por radio censal
radios = gpd.read_file('./radios/radios.gpkg')
print(radios.shape)
## (3554, 17)
print(radios.dtypes)
## ID               int64
## CO_FRAC_RA      object
## COMUNA           int32
## FRACCION         int64
## RADIO            int32
## TOTAL_POB        int64
## T_VARON          int64
## T_MUJER          int64
## T_VIVIENDA     float64
## V_PARTICUL       int64
## V_COLECTIV     float64
## T_HOGAR        float64
## H_CON_NBI      float64
## H_SIN_NBI      float64
## univComP       float64
## univComp_1     float64
## geometry      geometry
## dtype: object
radios.head()
##    ID CO_FRAC_RA  ...  univComp_1                                           geometry
## 0   1      1_1_1  ...         7.0  MULTIPOLYGON (((6374084.199 6171829.069, 63740...
## 1   2     1_12_1  ...        27.0  MULTIPOLYGON (((6372815.703 6170430.589, 63728...
## 2   3    1_12_10  ...        29.0  MULTIPOLYGON (((6373471.888 6170345.81, 637359...
## 3   4    1_12_11  ...        32.0  MULTIPOLYGON (((6374523.865 6170322.842, 63745...
## 4   5     1_12_2  ...        39.0  MULTIPOLYGON (((6372943.061 6170441.384, 63730...
## 
## [5 rows x 17 columns]

Transformo df a geo

Para trabajar con estadística espacial, debemos pasar nuestro dataframe a geodataframe, es decir, debemos tener al menos una variable con datos georreferenciados. Vamos a construir esta variable a partir de las variables latitud y longitud y le daremos un sistema de coordenadas al dataframe.

def convertir_a_float(valor):
  #elimino todos los puntos del string y luego aplico un punto después de los dos primeros números,luego convierto a float
  valor = str(valor).replace('.', '')  # elimino todos los puntos
  if len(valor) > 3:
    valor = valor[:3] + '.' + valor[2:]  # inserto un punto después de los dos primeros números
  try:
    return float(valor)
  except ValueError:
    return None

# aplico la función a las columnas de latitud y longitud
data['latitud2'] = data['latitud'].apply(convertir_a_float)
data['longitud2'] = data['longitud'].apply(convertir_a_float)

# elimino filas con valores no válidos en latitud o longitud
data = data.dropna(subset=['latitud', 'longitud'])
data_geo = gpd.GeoDataFrame(data, geometry = gpd.GeoSeries.from_xy(x = data.longitud, y = data.latitud, crs = 4326))
print(data_geo.dtypes)
## Unnamed: 0       int64
## id.mapa          int64
## anio             int64
## mes             object
## dia             object
## fecha           object
## franja           int64
## tipo            object
## subtipo         object
## uso_arma        object
## uso_moto        object
## barrio          object
## comuna         float64
## latitud        float64
## longitud       float64
## cantidad         int64
## latitud2       float64
## longitud2      float64
## geometry      geometry
## dtype: object
data.head()
##    Unnamed: 0  id.mapa  anio  ... cantidad   latitud2  longitud2
## 0           1        1  2022  ...        1 -34.458414 -58.845470
## 1           2        2  2022  ...        1 -34.464504 -58.837319
## 2           3        3  2022  ...        1 -34.458998 -58.844647
## 3           4        5  2022  ...        1 -34.459675 -58.841361
## 4           5        6  2022  ...        1 -34.464098 -58.848025
## 
## [5 rows x 18 columns]

Exploro

Veamos primero la densidad poblacional en CABA por radio censal

radios['densidad'] = (radios.TOTAL_POB / (radios.geometry.area / 1000000)).round().astype(int)
radios.explore(column = 'densidad', scheme = 'boxplot', tiles = 'CartoDB positron', cmap = 'PuOr', legend = True)
Make this Notebook Trusted to load map: File -> Trust Notebook

Ahora veamos la distribución espacial del volumen de delitos denunciados. Para esto vamos a crear un mapa de calor

f, ax = plt.subplots(1, figsize=(9, 9))
# Generamos un "heatmap" 
sns.kdeplot(
    x="longitud",
    y="latitud",
    data=data[1:50000], #Uso una porción de datos porque es algo muy costoso de computar
    n_levels=20, #pueden ajustar este parametro
    fill=True,
    alpha=0.55,
    cmap="coolwarm",
)
ctx.add_basemap(
    ax, source=ctx.providers.CartoDB.Positron, crs="EPSG:4326"
)
ax.set_axis_off()
plt.show() 

En prinicpio podemos observar que hay cierta coincidencia entre la densidad porblacional y el volumen de delitos denunciados. Esta obervación tiene lógica a priori, ya que donde más personas se encuentran, mayor probabilidad de delitos puede haber (lo mismo se puede pensar para otro tipo de unidad de análisis, donde mayor concentración de personas haya, mayor cantidad de casos de una variable podremos encontrar). Sin embargo, en el mapa de calor, también obervamos algunos focos en zonas con menor densidad poblacional, como Liniers o Constitución. Indagaremos más sobre este comportamiento.

Procesamiento

Join espacial

Primero crearemos un join espacial entre ambos dataframes, a fin de ubicar los puntos que representan a los delitos dentro de cada polígono de los radios censales. Para esto primero debo analizar si el sistema de coordenadas de cada dataframe coincide

print(data_geo.crs)
## EPSG:4326
print(radios.crs)
## PROJCS["POSGAR_2007_Argentina_6",GEOGCS["GCS_POSGAR 2007",DATUM["Posiciones_Geodesicas_Argentinas_2007",SPHEROID["GRS 1980",6378137,298.257222101],AUTHORITY["EPSG","1062"]],PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",-90],PARAMETER["central_meridian",-57],PARAMETER["scale_factor",1],PARAMETER["false_easting",6500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]

Normalizo el sistema de coordenadas y procedo con el join espacial

radios = radios.to_crs(epsg=4326)
# realizo el join espacial
data_geo_radios = gpd.sjoin(data_geo, radios, how="left", predicate="within")

# verifico el resultado
print(data_geo_radios.head())
##    Unnamed: 0  id.mapa  anio  ... univComP univComp_1 densidad
## 0           1        1  2022  ...   7.0833       51.0  19632.0
## 1           2        2  2022  ...   9.1916       83.0  23777.0
## 2           3        3  2022  ...  12.4855      108.0  27084.0
## 3           4        5  2022  ...  15.8756      194.0  45067.0
## 4           5        6  2022  ...   5.5604       64.0  16813.0
## 
## [5 rows x 37 columns]

Ahora haremos un conteo de delitos por cada radio censal y sumaremos esa información como una variable más al dataframe de radios censales

# contar filas por CO_FRAC_RA
conteo_por_fraccion = data_geo_radios.groupby('CO_FRAC_RA')['CO_FRAC_RA'].count()

# agregar la columna de conteo al df radios
radios = radios.merge(conteo_por_fraccion.rename('conteo_delitos'), left_on='CO_FRAC_RA', right_index=True, how='left')

# verificaamos el resultado
print(radios.head())
##    ID CO_FRAC_RA  ...  densidad  conteo_delitos
## 0   1      1_1_1  ...       187           696.0
## 1   2     1_12_1  ...     18360            72.0
## 2   3    1_12_10  ...      6667            62.0
## 3   4    1_12_11  ...      1441           263.0
## 4   5     1_12_2  ...     12465            36.0
## 
## [5 rows x 19 columns]
# removemos filas con valores nulos o NA para no entorpecer el posterior análisis
radios = radios.dropna(subset=['conteo_delitos'])

# verificaamos el resultado
print(radios.head())
##    ID CO_FRAC_RA  ...  densidad  conteo_delitos
## 0   1      1_1_1  ...       187           696.0
## 1   2     1_12_1  ...     18360            72.0
## 2   3    1_12_10  ...      6667            62.0
## 3   4    1_12_11  ...      1441           263.0
## 4   5     1_12_2  ...     12465            36.0
## 
## [5 rows x 19 columns]

MORAN

Vamos a intentar analizar la relación entre los radios censales y los delitos denunciados en cada uno, aplicando una técnica de autocorrelación espacial y el indicador I de Moran. Primero construiremos una matriz de adyacencia de polígonos con un formato Queen (reina).

# Construimos la matriz
wq =  lps.weights.Queen.from_dataframe(radios,ids ='CO_FRAC_RA')
## E:\Programas\Anaconda\Lib\site-packages\libpysal\weights\contiguity.py:347: UserWarning: The weights matrix is not fully connected: 
##  There are 2 disconnected components.
##   W.__init__(self, neighbors, ids=ids, **kw)
wq.transform = 'r'

Ahora crearemos el indicador y evaluaremos si existe una estructura espacial de nuestros datos.

#calcular estadistico global para saber si existe una estructura espacial
y = radios.conteo_delitos
np.random.seed(12345)
mi = esda.moran.Moran(y, wq)
print('Moran Global: ', mi.I, 'p-value: ',mi.p_sim)
## Moran Global:  0.26091980771631124 p-value:  0.001
sns.kdeplot(mi.sim, fill=True)
plt.vlines(mi.I, 0, 1, color='r')
plt.vlines(mi.EI, 0,1)
plt.xlabel("Moran's I")
plt.show();

Si bien el indicador quedó con un valor bajo, el p-value indica que existe correlación entre los polígonos por lo que seguiremos con el análisis planteado.

# Calculamos un Moran local
li = esda.moran.Moran_Local(y, wq)
fig, ax = moran_scatterplot(li, p=0.05)
ax.set_xlabel('Delitos')
ax.set_ylabel('Lag espacial de Delitos')
plt.show();

rojo = '#d7191c'
celeste = '#abd9e9'
azul = '#2c7bb6'
naranja = '#fdae61'

legend_elements = [
                   Patch(facecolor=rojo, edgecolor='w',label='Alto - Bajo'),
                   Patch(facecolor=naranja, edgecolor='w',label='Hotspot'),
                   Patch(facecolor=celeste, edgecolor='w',label='Bajo - Alto'),
                   Patch(facecolor=azul, edgecolor='w',label='Coldspot')
                   ]
# Este análisis devuelve una clasificación de cada celda en los 4 clusters

sig = li.p_sim < 0.05
radios['cuadrante'] = sig * li.q
radios['color'] = radios['cuadrante'].replace({1:rojo, 2:celeste, 3:azul, 4:naranja})
radios.loc[radios.cuadrante == 0,'color'] = np.nan

radios['cuadrante'] = radios['cuadrante'].replace({1:'hotspot',2:'bajo_alto',3:'coldspot',4:'alto_bajo'})

radios.cuadrante.value_counts(normalize = True)
## cuadrante
## 0            0.781047
## coldspot     0.104668
## hotspot      0.066761
## bajo_alto    0.039604
## alto_bajo    0.007921
## Name: proportion, dtype: float64

Mapeamos los resultados del Moran local

import folium

# Crear un mapa base centrado en la ubicación deseada
m = folium.Map(location=[-34.60, -58.44], zoom_start=12)  # Coordenadas de ejemplo para CABA

# Añadir capa base
folium.TileLayer('CartoDB.Positron').add_to(m)

# Agregar capas según cuadrantes, respetando el color asignado
## <folium.raster_layers.TileLayer object at 0x00000000932AB910>
for cuadrante, color in radios[['cuadrante', 'color']].dropna().drop_duplicates().values:
    subset = radios[radios['cuadrante'] == cuadrante].dropna()

    # Crear una capa para cada cuadrante con el color correspondiente y un label
    folium.GeoJson(
        subset,
        name=f"{cuadrante}",  # Nombre de la capa basado en el cuadrante
        style_function=lambda feature, col=color: {'color': col, 'weight': 2, 'fillOpacity': 0.6},
        tooltip=folium.features.GeoJsonTooltip(
            fields=["cuadrante", "COMUNA", "TOTAL_POB", "densidad", "conteo_delitos"],  # Ajusta los nombres de las variables que quieras mostrar en el label
            aliases=["Cuadrante", "Comuna", "Habitantes", "Densidad Poblacional", "Total delitos denunciados"],  # Ajusta los alias según cómo quieras que se vean en el tooltip
            localize=True
        )
    ).add_to(m)


# Añadir el control de capas al mapa después de añadir las capas
## <folium.features.GeoJson object at 0x0000000093542D90>
## <folium.features.GeoJson object at 0x00000000939122D0>
## <folium.features.GeoJson object at 0x000000009EEDDCD0>
## <folium.features.GeoJson object at 0x00000000932A8D90>
folium.LayerControl(collapsed=False).add_to(m)  # `collapsed=False` para intentar expandir el control

# Mostrar el mapa
## <folium.map.LayerControl object at 0x000000009EEDC650>
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Conclusión

Finalmente, si observamos los radios que quedaron categorizados con el valor alto_bajo (es decir, polígonos con alta cantidad de delitos y rodeado de polígonos con baja cantidad de delitos) podemos ver que en general se encuentran en zonas residenciales, de baja densidad poblacional pero mucho movimiento o circulación de personas, como ser el caso del polígono de Barrancas de Belgrano o la estación de Villa del Parque, ambos radios con altos niveles de circulación y tránsito en una zona residencial. Esta obervación podría sugerir un futuro análisis comparativo de radios censales teniendo en cuenta la densidad poblacional y la circulación o tránisto de personas, aplicando un análisis Moran bivariado.

LS0tDQp0aXRsZTogIkF1dG9jb3JyZWxhY2nDs24gZGUgZGVsaXRvcyBlbiBDQUJBIC0gMjAyMiINCmF1dGhvcjogIkpvYXF1w61uIExvdml6aW8gUmFtb3MiDQpkYXRlOiAiMjAyNC0xMC0yOCINCm91dHB1dDoNCiAgcm1kZm9ybWF0czo6ZG93bmN1dGU6DQogICAgbGlnaHRib3g6IFRSVUUNCiAgICBoaWdobGlnaHQ6IHRhbmdvDQogICAgdG9jOiAzDQogICAgbnVtYmVyLXNlY3Rpb25zOiBUUlVFDQogICAgY29kZS1mb2xkaW5nOiBzaG93ICNvY3VsdGEgZWwgY29kaWdvDQogICAgY29kZV9kb3dubG9hZDogVFJVRSAjIHBhcmEgZGVzY2FyZ2FyIGVsIHJtZA0KLS0tDQoNCiMgSW50cm9kdWNjacOzbg0KDQpFbiBlc3RlIHRyYWJham8gaW50ZW50YXJlbW9zIGFuYWxpemFyIGVsIGNvbXBvcnRhbWllbnRvIGdlby1lc3BhY2lhbCBkZSBkZWxpdG9zIGRlbnVuY2lhZG9zIGVuIENBQkEgZHVyYW50ZSAyMDIyLiBTZSB0cmFiYWphcsOhIGNvbiBlbCBkYXRhc2V0IHByb3Zpc3RvIHBvciBlbCBwb3J0YWwgZGUgZGF0b3MgYWJpZXJ0b3MgZGVsIEdDQkEgKGRpc3BvbmlibGUgW2FxdcOtXShodHRwczovL2RhdGEuYnVlbm9zYWlyZXMuZ29iLmFyL2RhdGFzZXQvZGVsaXRvcy9yZXNvdXJjZS8zZmJjMzgwOC0xNGM3LTQ1NTktOGJhNS1mNjhlOTE5ZmVlNDApIHkgdW4gZGF0YXNldCBkZSByYWRpb3MgY2Vuc2FsZXMuIEVsIGFuw6FsaXNpcyBlcyBleHBsb3JhdG9yaW8gZSBpbnRlbnRhcsOhIGRldGVjdGFyIHpvbmFzIGNvbiBhbHRvIHZvbHVtZW4gZGUgZGVsaXRvcyB5IHpvbmFzIGNvbiBjb21wb3J0YW1pZW50b3MgcGFydGljdWxhcmVzLg0KDQojIEltcG9ydG8gbGlicmVyw61hcw0KDQpgYGB7cHl0aG9ufQ0KIyAhcGlwIGluc3RhbGwgbWFwY2xhc3NpZnkgZm9saXVtIHBvaW50cGF0cyBjb250ZXh0aWx5IHB5Z2VvcyBoMyBweXNhbA0KYGBgDQoNCmBgYHtweXRob259DQppbXBvcnQgcGFuZGFzIGFzIHBkDQppbXBvcnQgbnVtcHkgYXMgbnANCmltcG9ydCBnZW9wYW5kYXMgYXMgZ3BkDQppbXBvcnQgb3MNCg0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KZnJvbSBtYXRwbG90bGliLnBhdGNoZXMgaW1wb3J0IFBvbHlnb24gYXMgUG9seWdvbk0gI25vdGVuIGNvbW8gdXNhbW9zIG90cm8gYWxpYXMNCmZyb20gbWF0cGxvdGxpYi5wYXRjaGVzIGltcG9ydCBQYXRjaA0KZnJvbSBzaGFwZWx5Lmdlb21ldHJ5IGltcG9ydCBQb2x5Z29uDQpmcm9tIHNwbG90LmVzZGEgaW1wb3J0IHBsb3RfbW9yYW5fYnZfc2ltdWxhdGlvbiwgcGxvdF9tb3Jhbl9idiwgbW9yYW5fc2NhdHRlcnBsb3QNCg0KDQppbXBvcnQgc2VhYm9ybiBhcyBzbnMNCmltcG9ydCBjb250ZXh0aWx5IGFzIGN0eA0KDQppbXBvcnQgbGlicHlzYWwgYXMgbHBzDQoNCmltcG9ydCBlc2RhDQpmcm9tIGVzZGEubW9yYW4gaW1wb3J0IE1vcmFuX0JWLCBNb3Jhbl9Mb2NhbF9CVg0KYGBgDQoNCiMgQ2FyZ28gZGF0YQ0KDQpgYGB7cHl0aG9ufQ0KZGF0YSA9IHBkLnJlYWRfY3N2KCcuL2RlbGl0b3MvZGVsaXRvc18yMDIyLmNzdicsIGRlbGltaXRlcj0iLCIsIGVuY29kaW5nPSJMYXRpbjEiKQ0KYGBgDQoNCmBgYHtweXRob259DQpwcmludChkYXRhLnNoYXBlKQ0KcHJpbnQoZGF0YS5kdHlwZXMpDQpkYXRhLmhlYWQoKQ0KYGBgDQoNCmBgYHtweXRob259DQojIExlZW1vcyBsb3MgZGF0b3MgcG9yIHJhZGlvIGNlbnNhbA0KcmFkaW9zID0gZ3BkLnJlYWRfZmlsZSgnLi9yYWRpb3MvcmFkaW9zLmdwa2cnKQ0KYGBgDQoNCmBgYHtweXRob259DQpwcmludChyYWRpb3Muc2hhcGUpDQpwcmludChyYWRpb3MuZHR5cGVzKQ0KcmFkaW9zLmhlYWQoKQ0KYGBgDQoNCiMgVHJhbnNmb3JtbyBkZiBhIGdlbw0KDQpQYXJhIHRyYWJhamFyIGNvbiBlc3RhZMOtc3RpY2EgZXNwYWNpYWwsIGRlYmVtb3MgcGFzYXIgbnVlc3RybyBkYXRhZnJhbWUgYSBnZW9kYXRhZnJhbWUsIGVzIGRlY2lyLCBkZWJlbW9zIHRlbmVyIGFsIG1lbm9zIHVuYSB2YXJpYWJsZSBjb24gZGF0b3MgZ2VvcnJlZmVyZW5jaWFkb3MuIFZhbW9zIGEgY29uc3RydWlyIGVzdGEgdmFyaWFibGUgYSBwYXJ0aXIgZGUgbGFzIHZhcmlhYmxlcyBsYXRpdHVkIHkgbG9uZ2l0dWQgeSBsZSBkYXJlbW9zIHVuIHNpc3RlbWEgZGUgY29vcmRlbmFkYXMgYWwgZGF0YWZyYW1lLg0KDQpgYGB7cHl0aG9ufQ0KZGVmIGNvbnZlcnRpcl9hX2Zsb2F0KHZhbG9yKToNCiAgI2VsaW1pbm8gdG9kb3MgbG9zIHB1bnRvcyBkZWwgc3RyaW5nIHkgbHVlZ28gYXBsaWNvIHVuIHB1bnRvIGRlc3B1w6lzIGRlIGxvcyBkb3MgcHJpbWVyb3MgbsO6bWVyb3MsbHVlZ28gY29udmllcnRvIGEgZmxvYXQNCiAgdmFsb3IgPSBzdHIodmFsb3IpLnJlcGxhY2UoJy4nLCAnJykgICMgZWxpbWlubyB0b2RvcyBsb3MgcHVudG9zDQogIGlmIGxlbih2YWxvcikgPiAzOg0KICAgIHZhbG9yID0gdmFsb3JbOjNdICsgJy4nICsgdmFsb3JbMjpdICAjIGluc2VydG8gdW4gcHVudG8gZGVzcHXDqXMgZGUgbG9zIGRvcyBwcmltZXJvcyBuw7ptZXJvcw0KICB0cnk6DQogICAgcmV0dXJuIGZsb2F0KHZhbG9yKQ0KICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICByZXR1cm4gTm9uZQ0KDQojIGFwbGljbyBsYSBmdW5jacOzbiBhIGxhcyBjb2x1bW5hcyBkZSBsYXRpdHVkIHkgbG9uZ2l0dWQNCmRhdGFbJ2xhdGl0dWQyJ10gPSBkYXRhWydsYXRpdHVkJ10uYXBwbHkoY29udmVydGlyX2FfZmxvYXQpDQpkYXRhWydsb25naXR1ZDInXSA9IGRhdGFbJ2xvbmdpdHVkJ10uYXBwbHkoY29udmVydGlyX2FfZmxvYXQpDQoNCiMgZWxpbWlubyBmaWxhcyBjb24gdmFsb3JlcyBubyB2w6FsaWRvcyBlbiBsYXRpdHVkIG8gbG9uZ2l0dWQNCmRhdGEgPSBkYXRhLmRyb3BuYShzdWJzZXQ9WydsYXRpdHVkJywgJ2xvbmdpdHVkJ10pDQpgYGANCg0KYGBge3B5dGhvbn0NCmRhdGFfZ2VvID0gZ3BkLkdlb0RhdGFGcmFtZShkYXRhLCBnZW9tZXRyeSA9IGdwZC5HZW9TZXJpZXMuZnJvbV94eSh4ID0gZGF0YS5sb25naXR1ZCwgeSA9IGRhdGEubGF0aXR1ZCwgY3JzID0gNDMyNikpDQpgYGANCg0KYGBge3B5dGhvbn0NCnByaW50KGRhdGFfZ2VvLmR0eXBlcykNCmRhdGEuaGVhZCgpDQpgYGANCg0KIyBFeHBsb3JvDQoNClZlYW1vcyBwcmltZXJvIGxhIGRlbnNpZGFkIHBvYmxhY2lvbmFsIGVuIENBQkEgcG9yIHJhZGlvIGNlbnNhbA0KDQpgYGB7cHl0aG9ufQ0KcmFkaW9zWydkZW5zaWRhZCddID0gKHJhZGlvcy5UT1RBTF9QT0IgLyAocmFkaW9zLmdlb21ldHJ5LmFyZWEgLyAxMDAwMDAwKSkucm91bmQoKS5hc3R5cGUoaW50KQ0KYGBgDQoNCmBgYHtweXRob259DQpyYWRpb3MuZXhwbG9yZShjb2x1bW4gPSAnZGVuc2lkYWQnLCBzY2hlbWUgPSAnYm94cGxvdCcsIHRpbGVzID0gJ0NhcnRvREIgcG9zaXRyb24nLCBjbWFwID0gJ1B1T3InLCBsZWdlbmQgPSBUcnVlKQ0KYGBgDQoNCkFob3JhIHZlYW1vcyBsYSBkaXN0cmlidWNpw7NuIGVzcGFjaWFsIGRlbCB2b2x1bWVuIGRlIGRlbGl0b3MgZGVudW5jaWFkb3MuIFBhcmEgZXN0byB2YW1vcyBhIGNyZWFyIHVuIG1hcGEgZGUgY2Fsb3INCg0KYGBge3B5dGhvbn0NCmYsIGF4ID0gcGx0LnN1YnBsb3RzKDEsIGZpZ3NpemU9KDksIDkpKQ0KIyBHZW5lcmFtb3MgdW4gImhlYXRtYXAiIA0Kc25zLmtkZXBsb3QoDQogICAgeD0ibG9uZ2l0dWQiLA0KICAgIHk9ImxhdGl0dWQiLA0KICAgIGRhdGE9ZGF0YVsxOjUwMDAwXSwgI1VzbyB1bmEgcG9yY2nDs24gZGUgZGF0b3MgcG9ycXVlIGVzIGFsZ28gbXV5IGNvc3Rvc28gZGUgY29tcHV0YXINCiAgICBuX2xldmVscz0yMCwgI3B1ZWRlbiBhanVzdGFyIGVzdGUgcGFyYW1ldHJvDQogICAgZmlsbD1UcnVlLA0KICAgIGFscGhhPTAuNTUsDQogICAgY21hcD0iY29vbHdhcm0iLA0KKQ0KY3R4LmFkZF9iYXNlbWFwKA0KICAgIGF4LCBzb3VyY2U9Y3R4LnByb3ZpZGVycy5DYXJ0b0RCLlBvc2l0cm9uLCBjcnM9IkVQU0c6NDMyNiINCikNCmF4LnNldF9heGlzX29mZigpDQpwbHQuc2hvdygpIA0KYGBgDQoNCkVuIHByaW5pY3BpbyBwb2RlbW9zIG9ic2VydmFyIHF1ZSBoYXkgY2llcnRhIGNvaW5jaWRlbmNpYSBlbnRyZSBsYSBkZW5zaWRhZCBwb3JibGFjaW9uYWwgeSBlbCB2b2x1bWVuIGRlIGRlbGl0b3MgZGVudW5jaWFkb3MuIEVzdGEgb2JlcnZhY2nDs24gdGllbmUgbMOzZ2ljYSBhIHByaW9yaSwgeWEgcXVlIGRvbmRlIG3DoXMgcGVyc29uYXMgc2UgZW5jdWVudHJhbiwgbWF5b3IgcHJvYmFiaWxpZGFkIGRlIGRlbGl0b3MgcHVlZGUgaGFiZXIgKGxvIG1pc21vIHNlIHB1ZWRlIHBlbnNhciBwYXJhIG90cm8gdGlwbyBkZSB1bmlkYWQgZGUgYW7DoWxpc2lzLCBkb25kZSBtYXlvciBjb25jZW50cmFjacOzbiBkZSBwZXJzb25hcyBoYXlhLCBtYXlvciBjYW50aWRhZCBkZSBjYXNvcyBkZSB1bmEgdmFyaWFibGUgcG9kcmVtb3MgZW5jb250cmFyKS4gU2luIGVtYmFyZ28sIGVuIGVsIG1hcGEgZGUgY2Fsb3IsIHRhbWJpw6luIG9iZXJ2YW1vcyBhbGd1bm9zIGZvY29zIGVuIHpvbmFzIGNvbiBtZW5vciBkZW5zaWRhZCBwb2JsYWNpb25hbCwgY29tbyBMaW5pZXJzIG8gQ29uc3RpdHVjacOzbi4gSW5kYWdhcmVtb3MgbcOhcyBzb2JyZSBlc3RlIGNvbXBvcnRhbWllbnRvLg0KDQojIFByb2Nlc2FtaWVudG8NCg0KIyMgSm9pbiBlc3BhY2lhbA0KDQpQcmltZXJvIGNyZWFyZW1vcyB1biBqb2luIGVzcGFjaWFsIGVudHJlIGFtYm9zIGRhdGFmcmFtZXMsIGEgZmluIGRlIHViaWNhciBsb3MgcHVudG9zIHF1ZSByZXByZXNlbnRhbiBhIGxvcyBkZWxpdG9zIGRlbnRybyBkZSBjYWRhIHBvbMOtZ29ubyBkZSBsb3MgcmFkaW9zIGNlbnNhbGVzLiBQYXJhIGVzdG8gcHJpbWVybyBkZWJvIGFuYWxpemFyIHNpIGVsIHNpc3RlbWEgZGUgY29vcmRlbmFkYXMgZGUgY2FkYSBkYXRhZnJhbWUgY29pbmNpZGUNCg0KYGBge3B5dGhvbn0NCnByaW50KGRhdGFfZ2VvLmNycykNCnByaW50KHJhZGlvcy5jcnMpDQpgYGANCg0KTm9ybWFsaXpvIGVsIHNpc3RlbWEgZGUgY29vcmRlbmFkYXMgeSBwcm9jZWRvIGNvbiBlbCBqb2luIGVzcGFjaWFsDQoNCmBgYHtweXRob259DQpyYWRpb3MgPSByYWRpb3MudG9fY3JzKGVwc2c9NDMyNikNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyByZWFsaXpvIGVsIGpvaW4gZXNwYWNpYWwNCmRhdGFfZ2VvX3JhZGlvcyA9IGdwZC5zam9pbihkYXRhX2dlbywgcmFkaW9zLCBob3c9ImxlZnQiLCBwcmVkaWNhdGU9IndpdGhpbiIpDQoNCiMgdmVyaWZpY28gZWwgcmVzdWx0YWRvDQpwcmludChkYXRhX2dlb19yYWRpb3MuaGVhZCgpKQ0KYGBgDQoNCkFob3JhIGhhcmVtb3MgdW4gY29udGVvIGRlIGRlbGl0b3MgcG9yIGNhZGEgcmFkaW8gY2Vuc2FsIHkgc3VtYXJlbW9zIGVzYSBpbmZvcm1hY2nDs24gY29tbyB1bmEgdmFyaWFibGUgbcOhcyBhbCBkYXRhZnJhbWUgZGUgcmFkaW9zIGNlbnNhbGVzDQoNCmBgYHtweXRob259DQojIGNvbnRhciBmaWxhcyBwb3IgQ09fRlJBQ19SQQ0KY29udGVvX3Bvcl9mcmFjY2lvbiA9IGRhdGFfZ2VvX3JhZGlvcy5ncm91cGJ5KCdDT19GUkFDX1JBJylbJ0NPX0ZSQUNfUkEnXS5jb3VudCgpDQoNCiMgYWdyZWdhciBsYSBjb2x1bW5hIGRlIGNvbnRlbyBhbCBkZiByYWRpb3MNCnJhZGlvcyA9IHJhZGlvcy5tZXJnZShjb250ZW9fcG9yX2ZyYWNjaW9uLnJlbmFtZSgnY29udGVvX2RlbGl0b3MnKSwgbGVmdF9vbj0nQ09fRlJBQ19SQScsIHJpZ2h0X2luZGV4PVRydWUsIGhvdz0nbGVmdCcpDQoNCiMgdmVyaWZpY2FhbW9zIGVsIHJlc3VsdGFkbw0KcHJpbnQocmFkaW9zLmhlYWQoKSkNCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyByZW1vdmVtb3MgZmlsYXMgY29uIHZhbG9yZXMgbnVsb3MgbyBOQSBwYXJhIG5vIGVudG9ycGVjZXIgZWwgcG9zdGVyaW9yIGFuw6FsaXNpcw0KcmFkaW9zID0gcmFkaW9zLmRyb3BuYShzdWJzZXQ9Wydjb250ZW9fZGVsaXRvcyddKQ0KDQojIHZlcmlmaWNhYW1vcyBlbCByZXN1bHRhZG8NCnByaW50KHJhZGlvcy5oZWFkKCkpDQpgYGANCg0KIyMgTU9SQU4NCg0KVmFtb3MgYSBpbnRlbnRhciBhbmFsaXphciBsYSByZWxhY2nDs24gZW50cmUgbG9zIHJhZGlvcyBjZW5zYWxlcyB5IGxvcyBkZWxpdG9zIGRlbnVuY2lhZG9zIGVuIGNhZGEgdW5vLCBhcGxpY2FuZG8gdW5hIHTDqWNuaWNhIGRlIGF1dG9jb3JyZWxhY2nDs24gZXNwYWNpYWwgeSBlbCBpbmRpY2Fkb3IgSSBkZSBNb3Jhbi4gUHJpbWVybyBjb25zdHJ1aXJlbW9zIHVuYSBtYXRyaXogZGUgYWR5YWNlbmNpYSBkZSBwb2zDrWdvbm9zIGNvbiB1biBmb3JtYXRvIFF1ZWVuIChyZWluYSkuDQoNCmBgYHtweXRob259DQojIENvbnN0cnVpbW9zIGxhIG1hdHJpeg0Kd3EgPSAgbHBzLndlaWdodHMuUXVlZW4uZnJvbV9kYXRhZnJhbWUocmFkaW9zLGlkcyA9J0NPX0ZSQUNfUkEnKQ0Kd3EudHJhbnNmb3JtID0gJ3InDQpgYGANCg0KQWhvcmEgY3JlYXJlbW9zIGVsIGluZGljYWRvciB5IGV2YWx1YXJlbW9zIHNpIGV4aXN0ZSB1bmEgZXN0cnVjdHVyYSBlc3BhY2lhbCBkZSBudWVzdHJvcyBkYXRvcy4NCg0KYGBge3B5dGhvbn0NCiNjYWxjdWxhciBlc3RhZGlzdGljbyBnbG9iYWwgcGFyYSBzYWJlciBzaSBleGlzdGUgdW5hIGVzdHJ1Y3R1cmEgZXNwYWNpYWwNCnkgPSByYWRpb3MuY29udGVvX2RlbGl0b3MNCm5wLnJhbmRvbS5zZWVkKDEyMzQ1KQ0KbWkgPSBlc2RhLm1vcmFuLk1vcmFuKHksIHdxKQ0KcHJpbnQoJ01vcmFuIEdsb2JhbDogJywgbWkuSSwgJ3AtdmFsdWU6ICcsbWkucF9zaW0pDQoNCnNucy5rZGVwbG90KG1pLnNpbSwgZmlsbD1UcnVlKQ0KcGx0LnZsaW5lcyhtaS5JLCAwLCAxLCBjb2xvcj0ncicpDQpwbHQudmxpbmVzKG1pLkVJLCAwLDEpDQpwbHQueGxhYmVsKCJNb3JhbidzIEkiKQ0KcGx0LnNob3coKTsNCmBgYA0KDQpTaSBiaWVuIGVsIGluZGljYWRvciBxdWVkw7MgY29uIHVuIHZhbG9yIGJham8sIGVsIHAtdmFsdWUgaW5kaWNhIHF1ZSBleGlzdGUgY29ycmVsYWNpw7NuIGVudHJlIGxvcyBwb2zDrWdvbm9zIHBvciBsbyBxdWUgc2VndWlyZW1vcyBjb24gZWwgYW7DoWxpc2lzIHBsYW50ZWFkby4NCg0KYGBge3B5dGhvbn0NCiMgQ2FsY3VsYW1vcyB1biBNb3JhbiBsb2NhbA0KbGkgPSBlc2RhLm1vcmFuLk1vcmFuX0xvY2FsKHksIHdxKQ0KYGBgDQoNCmBgYHtweXRob259DQpmaWcsIGF4ID0gbW9yYW5fc2NhdHRlcnBsb3QobGksIHA9MC4wNSkNCmF4LnNldF94bGFiZWwoJ0RlbGl0b3MnKQ0KYXguc2V0X3lsYWJlbCgnTGFnIGVzcGFjaWFsIGRlIERlbGl0b3MnKQ0KcGx0LnNob3coKTsNCmBgYA0KDQpgYGB7cHl0aG9ufQ0Kcm9qbyA9ICcjZDcxOTFjJw0KY2VsZXN0ZSA9ICcjYWJkOWU5Jw0KYXp1bCA9ICcjMmM3YmI2Jw0KbmFyYW5qYSA9ICcjZmRhZTYxJw0KDQpsZWdlbmRfZWxlbWVudHMgPSBbDQogICAgICAgICAgICAgICAgICAgUGF0Y2goZmFjZWNvbG9yPXJvam8sIGVkZ2Vjb2xvcj0ndycsbGFiZWw9J0FsdG8gLSBCYWpvJyksDQogICAgICAgICAgICAgICAgICAgUGF0Y2goZmFjZWNvbG9yPW5hcmFuamEsIGVkZ2Vjb2xvcj0ndycsbGFiZWw9J0hvdHNwb3QnKSwNCiAgICAgICAgICAgICAgICAgICBQYXRjaChmYWNlY29sb3I9Y2VsZXN0ZSwgZWRnZWNvbG9yPSd3JyxsYWJlbD0nQmFqbyAtIEFsdG8nKSwNCiAgICAgICAgICAgICAgICAgICBQYXRjaChmYWNlY29sb3I9YXp1bCwgZWRnZWNvbG9yPSd3JyxsYWJlbD0nQ29sZHNwb3QnKQ0KICAgICAgICAgICAgICAgICAgIF0NCmBgYA0KDQpgYGB7cHl0aG9ufQ0KIyBFc3RlIGFuw6FsaXNpcyBkZXZ1ZWx2ZSB1bmEgY2xhc2lmaWNhY2nDs24gZGUgY2FkYSBjZWxkYSBlbiBsb3MgNCBjbHVzdGVycw0KDQpzaWcgPSBsaS5wX3NpbSA8IDAuMDUNCnJhZGlvc1snY3VhZHJhbnRlJ10gPSBzaWcgKiBsaS5xDQpyYWRpb3NbJ2NvbG9yJ10gPSByYWRpb3NbJ2N1YWRyYW50ZSddLnJlcGxhY2UoezE6cm9qbywgMjpjZWxlc3RlLCAzOmF6dWwsIDQ6bmFyYW5qYX0pDQpyYWRpb3MubG9jW3JhZGlvcy5jdWFkcmFudGUgPT0gMCwnY29sb3InXSA9IG5wLm5hbg0KDQpyYWRpb3NbJ2N1YWRyYW50ZSddID0gcmFkaW9zWydjdWFkcmFudGUnXS5yZXBsYWNlKHsxOidob3RzcG90JywyOidiYWpvX2FsdG8nLDM6J2NvbGRzcG90Jyw0OidhbHRvX2Jham8nfSkNCg0KcmFkaW9zLmN1YWRyYW50ZS52YWx1ZV9jb3VudHMobm9ybWFsaXplID0gVHJ1ZSkNCmBgYA0KDQpNYXBlYW1vcyBsb3MgcmVzdWx0YWRvcyBkZWwgTW9yYW4gbG9jYWwNCg0KYGBge3B5dGhvbiwgZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFfQ0KaW1wb3J0IGZvbGl1bQ0KDQojIENyZWFyIHVuIG1hcGEgYmFzZSBjZW50cmFkbyBlbiBsYSB1YmljYWNpw7NuIGRlc2VhZGENCm0gPSBmb2xpdW0uTWFwKGxvY2F0aW9uPVstMzQuNjAsIC01OC40NF0sIHpvb21fc3RhcnQ9MTIpICAjIENvb3JkZW5hZGFzIGRlIGVqZW1wbG8gcGFyYSBDQUJBDQoNCiMgQcOxYWRpciBjYXBhIGJhc2UNCmZvbGl1bS5UaWxlTGF5ZXIoJ0NhcnRvREIuUG9zaXRyb24nKS5hZGRfdG8obSkNCg0KIyBBZ3JlZ2FyIGNhcGFzIHNlZ8O6biBjdWFkcmFudGVzLCByZXNwZXRhbmRvIGVsIGNvbG9yIGFzaWduYWRvDQpmb3IgY3VhZHJhbnRlLCBjb2xvciBpbiByYWRpb3NbWydjdWFkcmFudGUnLCAnY29sb3InXV0uZHJvcG5hKCkuZHJvcF9kdXBsaWNhdGVzKCkudmFsdWVzOg0KICAgIHN1YnNldCA9IHJhZGlvc1tyYWRpb3NbJ2N1YWRyYW50ZSddID09IGN1YWRyYW50ZV0uZHJvcG5hKCkNCg0KICAgICMgQ3JlYXIgdW5hIGNhcGEgcGFyYSBjYWRhIGN1YWRyYW50ZSBjb24gZWwgY29sb3IgY29ycmVzcG9uZGllbnRlIHkgdW4gbGFiZWwNCiAgICBmb2xpdW0uR2VvSnNvbigNCiAgICAgICAgc3Vic2V0LA0KICAgICAgICBuYW1lPWYie2N1YWRyYW50ZX0iLCAgIyBOb21icmUgZGUgbGEgY2FwYSBiYXNhZG8gZW4gZWwgY3VhZHJhbnRlDQogICAgICAgIHN0eWxlX2Z1bmN0aW9uPWxhbWJkYSBmZWF0dXJlLCBjb2w9Y29sb3I6IHsnY29sb3InOiBjb2wsICd3ZWlnaHQnOiAyLCAnZmlsbE9wYWNpdHknOiAwLjZ9LA0KICAgICAgICB0b29sdGlwPWZvbGl1bS5mZWF0dXJlcy5HZW9Kc29uVG9vbHRpcCgNCiAgICAgICAgICAgIGZpZWxkcz1bImN1YWRyYW50ZSIsICJDT01VTkEiLCAiVE9UQUxfUE9CIiwgImRlbnNpZGFkIiwgImNvbnRlb19kZWxpdG9zIl0sICAjIEFqdXN0YSBsb3Mgbm9tYnJlcyBkZSBsYXMgdmFyaWFibGVzIHF1ZSBxdWllcmFzIG1vc3RyYXIgZW4gZWwgbGFiZWwNCiAgICAgICAgICAgIGFsaWFzZXM9WyJDdWFkcmFudGUiLCAiQ29tdW5hIiwgIkhhYml0YW50ZXMiLCAiRGVuc2lkYWQgUG9ibGFjaW9uYWwiLCAiVG90YWwgZGVsaXRvcyBkZW51bmNpYWRvcyJdLCAgIyBBanVzdGEgbG9zIGFsaWFzIHNlZ8O6biBjw7NtbyBxdWllcmFzIHF1ZSBzZSB2ZWFuIGVuIGVsIHRvb2x0aXANCiAgICAgICAgICAgIGxvY2FsaXplPVRydWUNCiAgICAgICAgKQ0KICAgICkuYWRkX3RvKG0pDQoNCg0KIyBBw7FhZGlyIGVsIGNvbnRyb2wgZGUgY2FwYXMgYWwgbWFwYSBkZXNwdcOpcyBkZSBhw7FhZGlyIGxhcyBjYXBhcw0KZm9saXVtLkxheWVyQ29udHJvbChjb2xsYXBzZWQ9RmFsc2UpLmFkZF90byhtKSAgIyBgY29sbGFwc2VkPUZhbHNlYCBwYXJhIGludGVudGFyIGV4cGFuZGlyIGVsIGNvbnRyb2wNCg0KIyBNb3N0cmFyIGVsIG1hcGENCm0NCmBgYA0KDQojIENvbmNsdXNpw7NuDQoNCkZpbmFsbWVudGUsIHNpIG9ic2VydmFtb3MgbG9zIHJhZGlvcyBxdWUgcXVlZGFyb24gY2F0ZWdvcml6YWRvcyBjb24gZWwgdmFsb3IgKiphbHRvX2Jham8qKiAoZXMgZGVjaXIsIHBvbMOtZ29ub3MgY29uIGFsdGEgY2FudGlkYWQgZGUgZGVsaXRvcyB5IHJvZGVhZG8gZGUgcG9sw61nb25vcyBjb24gYmFqYSBjYW50aWRhZCBkZSBkZWxpdG9zKSBwb2RlbW9zIHZlciBxdWUgZW4gZ2VuZXJhbCBzZSBlbmN1ZW50cmFuIGVuICoqem9uYXMgcmVzaWRlbmNpYWxlcyoqLCBkZSAqKmJhamEgZGVuc2lkYWQgcG9ibGFjaW9uYWwqKiBwZXJvIG11Y2hvICoqbW92aW1pZW50byBvIGNpcmN1bGFjacOzbiBkZSBwZXJzb25hcyoqLCBjb21vIHNlciBlbCBjYXNvIGRlbCBwb2zDrWdvbm8gZGUgQmFycmFuY2FzIGRlIEJlbGdyYW5vIG8gbGEgZXN0YWNpw7NuIGRlIFZpbGxhIGRlbCBQYXJxdWUsIGFtYm9zIHJhZGlvcyBjb24gYWx0b3Mgbml2ZWxlcyBkZSBjaXJjdWxhY2nDs24geSB0csOhbnNpdG8gZW4gdW5hIHpvbmEgcmVzaWRlbmNpYWwuIEVzdGEgb2JlcnZhY2nDs24gcG9kcsOtYSBzdWdlcmlyIHVuIGZ1dHVybyBhbsOhbGlzaXMgY29tcGFyYXRpdm8gZGUgcmFkaW9zIGNlbnNhbGVzIHRlbmllbmRvIGVuIGN1ZW50YSBsYSBkZW5zaWRhZCBwb2JsYWNpb25hbCB5IGxhIGNpcmN1bGFjacOzbiBvIHRyw6FuaXN0byBkZSBwZXJzb25hcywgYXBsaWNhbmRvIHVuIGFuw6FsaXNpcyBNb3JhbiBiaXZhcmlhZG8uDQo=