Elaborado por Juan Javier Santos Ochoa (@jjsantoso)

Introducción

Hace poco me tocó trabajar con los datos de una encuesta de INEGI y usé Python para hacer el análisis descriptivo. Quienes trabajamos con datos del INEGI hemos visto que es usual que los archivos de datos abiertos vengan en varias carpetas que contienen tanto los datos como los metadatos e información adicional sobre la encuesta. Por ejemplo, si descargamos los datos para la Encuesta Nacional de Seguridad Pública Urbana (ENSU) veremos que vienen 3 carpetas, cada una corresponde a una sección de la encuesta:

Note: Los datos se pueden descargar directamente de la página de INEGI, pero aquí dejo una copia de los que yo usé para este tutorial. Descargar datos
  • conjunto_de_datos_VIV_ENSU_12_2020: Cuestionario sociodemográfico sección I y II
  • conjunto_de_datos_CS_ENSU_12_2020: Cuestionario sociodemográfico sección III
  • conjunto_de_datos_CB_ENSU_12_2020: Cuestionario principal de la encuesta sección I, II, III y IV

Si vemos al interior de uno de estos módulos, la estructura incluye las siguientes carpetas:

  • Catálogos: tiene los catálogos para cada variable en el cuestionario
  • Conjunto de datos: tiene los datos principales de la encuesta
  • Diccionario de datos: el nombre e información de cada variable
  • Metadatos: tiene información de la encuesta.
  • Modelo entidad relación: es un diagrama que muestra cómo se relacionan los diferentes conjuntos de datos.

Si echamos un vistazo rápido a los datos en Excel (conjunto_de_datos/conjunto_de_datos_CB_ENSU_12_2020.csv) veremos que la mayor parte de las variables viene codificada. Solo con este archivo no podemos saber qué es cada columna y cuáles es el significado de sus valores. Nos hace falta el diccionario de variables y los catálogos para poder interpretarlas.

En el archivo diccionario_de_datos/diccionario_de_datos_CB_ENSU_12_2020.csv tenemos cuál es el texto de cada pregunta. De ahí sabemos que, por ejemplo, la pregunta BP1_1 es "Percepción de seguridad en la ciudad". Estos valores se conocen como etiquetas de las variables.

Por otro lado, dentro de la carpeta catalogos viene un archivo csv por cada variable del conjunto de datos.

Este archivo nos dice cómo debemos transformar los valores numéricos de las categorías por sus valores de texto. Por ejemplo, si abrimos el archivo "BP1_1.csv" su contenido nos muestra que para la variable BP1_1 debemos interpretar que un 1 corresponde a la categorías "seguro?", el 2 corresponde a "inseguro?" y el 9 a "No sabe/No responde". Estos valores se conocen como etiquetas de los valores.

Note: Las etiquetas de valores tiene sentido para variables categóricas, es decir las que tienen pocos valores nominales. Para variables numéricas o puramente de texto no es necesario usar etiquetas de valores.

Es evidente que para manejar una encuesta es fundamental conocer las etiquetas de las variables y sus valores. Sería mucho más fácil si estas etiquetas estuvieran incluidas en el mismo archivo junto con los datos, pero como están en formato .csv no es posible guadar esa información en un solo archivo y por tanto, termina repartida en muchos. Entonces, nuestro objetivo es integrar el diccionario y el catálogo a los datos para que sea más fácil hacer nuestro análisis. Queremos que en las tablas o gráficas que hagamos, las variables categóricas aparezcan como texto, en lugar de los valores numéricos que asignó INEGI. De igual forma, nos gustaría que en lugar de aparecer el nombre de la variable como en la base de datos, aparezca su descripción. Para lograr esto usaremos objetos tipo diccionario nativos de Python y dataframes de Pandas.

Tip: Otros formatos de datos, como por ejemplo los archivos .dta de Stata o .sav de SPSS sí permiten guardar esas etiquetas junto con los datos, sin embargo, esos no son formatos de datos abiertos que sean fácilmente accesible. En algunos casos, como en la ENOE, INEGI también publica archivos .dta y .sav.

Datos

Primero, vamos a importar las librerías necesarias.

import glob
import sys
import pandas as pd

print('Python', sys.version)
print(pd.__name__, pd.__version__)
Python 3.8.5 (default, Sep  3 2020, 21:29:08) [MSC v.1916 64 bit (AMD64)]
pandas 1.1.3

Para ilustrar, vamos a seleccionar los datos de la carpeta conjunto_de_datos_CB_ENSU_12_2020 (Cuestionario principal de la encuesta sección I, II, III y IV).

datos = pd.read_csv('conjunto_de_datos_CB_ENSU_12_2020/conjunto_de_datos/conjunto_de_datos_CB_ENSU_12_2020.csv')
datos.head()
ID_VIV ID_PER UPM VIV_SEL R_SEL CVE_ENT NOM_ENT CVE_MUN NOM_MUN LOC ... BP4_1_5 BP4_1_6 BP4_1_7 BP4_1_8 BP4_1_9 FAC_SEL DOMINIO EST UPM_DIS EST_DIS
0 100188.049 0100188.049.03\r 100188 1 3 1 Aguascalientes\r 1 Aguascalientes\r 1 ... 2 2 2 2 2 4046 U\r 3 10 1390
1 100188.072 0100188.072.06\r 100188 2 6 1 Aguascalientes\r 1 Aguascalientes\r 1 ... 2 2 2 2 2 6069 U\r 3 10 1390
2 100188.093 0100188.093.03\r 100188 3 3 1 Aguascalientes\r 1 Aguascalientes\r 1 ... 2 2 2 2 2 3035 U\r 3 10 1390
3 100188.111 0100188.111.01\r 100188 5 1 1 Aguascalientes\r 1 Aguascalientes\r 1 ... 2 2 2 2 2 3035 U\r 3 10 1390
4 100295.009 0100295.009.01\r 100295 1 1 1 Aguascalientes\r 1 Aguascalientes\r 1 ... 2 2 2 2 2 1704 U\r 4 20 1400

5 rows × 176 columns

datos.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22283 entries, 0 to 22282
Columns: 176 entries, ID_VIV to EST_DIS
dtypes: float64(1), int64(84), object(91)
memory usage: 29.9+ MB

Necesitamos el diccionario de variables y los catálogos. Para el diccionario de variables cargamos el archivo conjunto_de_datos_CB_ENSU_12_2020/diccionario_de_datos/diccionario_de_datos_CB_ENSU_12_2020.csv

preguntas = pd.read_csv('conjunto_de_datos_CB_ENSU_12_2020/diccionario_de_datos/diccionario_de_datos_CB_ENSU_12_2020.csv', encoding='latin1')
preguntas.head(10)
NOMBRE_CAMPO NEMONICO TIPO LONGITUD RANGO_CLAVES
0 Identificador de vivienda seleccionada ID_VIV Alfanumérico 11 0100001.001,…,3299999.999\r
1 Identificador del informante seleccionado ID_PER Alfanumérico 14 0100001.001.01,..., 3299999.999.30\r
2 Unidad Primaria de Muestreo UPM Numérico 7 0100001…3299999\r
3 Vivienda seleccionada VIV_SEL Numérico 2 01…09\r
4 Número de renglón de la persona seleccionada R_SEL Numérico 2 01…30\r
5 Clave Entidad CVE_ENT Numérico 2 01\r
6 Clave Entidad CVE_ENT Numérico 2 02\r
7 Clave Entidad CVE_ENT Numérico 2 03\r
8 Clave Entidad CVE_ENT Numérico 2 04\r
9 Clave Entidad CVE_ENT Numérico 2 05\r

Este nos muestra cómo aparece cada variable en el archivo de datos ("NEMONICO") y cuál es su descripción ("NOMBRE_CAMPO"), además contiene el tipo y rango de valores que puede tener cada variable. Por esta razón, el nombre de cada variable puede estar repetido varias veces. Lo que haremos es eliminar los duplicados y quedarnos con los valores únicos, para después crear un objeto diccionario que tenga como llaves el "NEMONICO" y como valores el "NOMBRE_CAMPO". A continuación se ve el proceso y el resultado:

dicc_preguntas = preguntas.drop_duplicates(subset=['NEMONICO']).set_index('NEMONICO')['NOMBRE_CAMPO'].to_dict()
print(str(dicc_preguntas)[:500])
{'ID_VIV': 'Identificador de vivienda seleccionada', 'ID_PER': 'Identificador del informante seleccionado', 'UPM': 'Unidad Primaria de Muestreo ', 'VIV_SEL': 'Vivienda seleccionada', 'R_SEL': 'Número de renglón de la persona seleccionada', 'CVE_ENT': 'Clave Entidad', 'NOM_ENT': 'Nombre de la Entidad', 'CVE_MUN': 'Clave Municipio', 'NOM_MUN': 'Nombre del Municipio', 'LOC': 'Localidad', 'CD': 'Ciudad', 'NOM_CD': 'Nombre de la Ciudad', 'PER': 'Periodo de la entrevista', 'R_DEF': 'Resultado definiti

Para el catálogo tenemos que trabajar un poco más porque la información está en muchos archivos. Primero vamos a generar una lista de todos los archivos en la carpeta catalogo.

dir_catalogos = 'conjunto_de_datos_CB_ENSU_12_2020/catalogos/'
archivos_catalogo_respuestas = glob.glob1(dir_catalogos, '*.csv')
archivos_catalogo_respuestas[:10]
['BP1_1.csv',
 'BP1_10_1.csv',
 'BP1_10_2.csv',
 'BP1_10_3.csv',
 'BP1_10_4.csv',
 'BP1_10_5.csv',
 'BP1_2_01.csv',
 'BP1_2_02.csv',
 'BP1_2_03.csv',
 'BP1_2_04.csv']

A continuación vamos a leer cada uno de los archivos individuales y generar un diccionario por comprensión que contenga como llaves el "NEMONICO" y los valores serán otro diccionario que tiene la relación de los valores numéricos y de texto para los valores de cada variable.

dicc_respuestas = {
    f[:-4]: pd.read_csv(f'{dir_catalogos}/{f}', encoding='latin1', index_col=f[:-4])['descrip'].to_dict() for f in archivos_catalogo_respuestas
}
print(str(dicc_respuestas)[:500])
{'BP1_1': {1: 'seguro?', 2: 'inseguro?', 9: 'No sabe / no responde'}, 'BP1_10_1': {1: 'Mucha confianza', 2: 'Algo de confianza', 3: 'Algo de desconfianza', 4: 'Mucha desconfianza', 9: 'No sabe / no responde'}, 'BP1_10_2': {1: 'Mucha confianza', 2: 'Algo de confianza', 3: 'Algo de desconfianza', 4: 'Mucha desconfianza', 9: 'No sabe / no responde'}, 'BP1_10_3': {1: 'Mucha confianza', 2: 'Algo de confianza', 3: 'Algo de desconfianza', 4: 'Mucha desconfianza', 9: 'No sabe / no responde'}, 'BP1_10_4'

Ahora tenemos dos diccionarios, uno con las etiquetas de las variables (dicc_preguntas) y otro con las etiquetas de los valores de cada pregunta dicc_respuestas. En ambos diccionarios la llave principal es el némonico de la pregunta, por tanto si queremos saber cuáles son las etiquetas de variables y valores solo tenemos que indexar los diccionarios con el nemónico correspondiente, por ejemplo para "SEX" y "BP1_1" obtenemos:

print('Etiqueta de variable:', dicc_preguntas['SEX'], '\nEtiqueta de valores',dicc_respuestas['SEX'])
Etiqueta de variable: Sexo 
Etiqueta de valores {1: 'Hombre', 2: 'Mujer'}
print('Etiqueta de variable:', dicc_preguntas['BP1_1'], '\nEtiqueta de valores',dicc_respuestas['BP1_1'])
Etiqueta de variable: Percepción de seguridad en la ciudad 
Etiqueta de valores {1: 'seguro?', 2: 'inseguro?', 9: 'No sabe / no responde'}

Uso de las etiquetas

Ya que tenemos las etiquetas, veamos cómo podemos usarlas para interpretar mejor en nuestros análisis. Hagamos una tabulación cruzada para ver la distribución de respuestas de las variables "SEX" y "BP1_1"

Warning: En estos ejemplos no estamos considerando que cada observación tiene una ponderación distinta (variable FAC_SEL) por tanto los resultados no son estimaciones válidas. En una próxima entrada veremos cómo integrar las ponderaciones.
pd.crosstab(datos['SEX'], datos['BP1_1'])
BP1_1 1 2 9
SEX
1 4268 5849 33
2 3703 8379 51

El índice del dataframe y los nombres de las columnas contienen los valores numéricos de las categorías. Vamos a reemplazarlos por sus valores de texto renombrándolo con el método .rename()

pd.crosstab(datos['SEX'], datos['BP1_1'])\
    .rename(index=dicc_respuestas['SEX'],
            columns=dicc_respuestas['BP1_1'])
BP1_1 seguro? inseguro? No sabe / no responde
SEX
Hombre 4268 5849 33
Mujer 3703 8379 51

Ahora es mucho más fácil de entender esta tabla.

Vamos a hacer otra tabla similar a la anterior, pero en este caso desagregando por más variables y usando el método groupby:

vars_by = ['SEX', 'BP1_1', 'BP1_5_1']
grouped = datos.groupby(vars_by).agg(N=('ID_PER', 'count'))
grouped.head(15)
N
SEX BP1_1 BP1_5_1
1 1 1 1255
2 2821
3 190
9 2
2 1 3587
2 2010
3 249
9 3
9 1 14
2 16
3 3
2 1 1 1463
2 1995
3 243
9 2

Nuevamente reemplazamos los valores de las categorías usando el método .rename(). Hacemos un loop para reemplazar las categorías de cada variable. Como tenemos varios niveles en el índice, especificamos la opción level para que use solo el catálogo con la variable que le corresponde.

for v in vars_by:
    grouped.rename(index=dicc_respuestas[v], level=v, inplace=True)

grouped
N
SEX BP1_1 BP1_5_1
Hombre seguro? 1255
No 2821
No aplica 190
No sabe / no responde 2
inseguro? 3587
No 2010
No aplica 249
No sabe / no responde 3
No sabe / no responde 14
No 16
No aplica 3
Mujer seguro? 1463
No 1995
No aplica 243
No sabe / no responde 2
inseguro? 5783
No 2192
No aplica 400
No sabe / no responde 4
No sabe / no responde 19
No 23
No aplica 7
No sabe / no responde 2

Para que sea más fácil de ver, reestructuramos la tabla usando .unstack().

grouped.unstack('BP1_1')
N
BP1_1 No sabe / no responde inseguro? seguro?
SEX BP1_5_1
Hombre No 16.0 2010.0 2821.0
No aplica 3.0 249.0 190.0
No sabe / no responde NaN 3.0 2.0
14.0 3587.0 1255.0
Mujer No 23.0 2192.0 1995.0
No aplica 7.0 400.0 243.0
No sabe / no responde 2.0 4.0 2.0
19.0 5783.0 1463.0

Ya integramps las etiquetas de los valores, ahora faltan las etiquetas de las variables. Para eso usaremos el método .rename_axis()

cuadro = grouped.unstack('BP1_1')\
    .rename_axis(index=dicc_preguntas, columns=dicc_preguntas)

cuadro
N
Percepción de seguridad en la ciudad No sabe / no responde inseguro? seguro?
Sexo Cambiar sus hábitos respecto a llevar cosas de valor por temor a sufrir algún delito
Hombre No 16.0 2010.0 2821.0
No aplica 3.0 249.0 190.0
No sabe / no responde NaN 3.0 2.0
14.0 3587.0 1255.0
Mujer No 23.0 2192.0 1995.0
No aplica 7.0 400.0 243.0
No sabe / no responde 2.0 4.0 2.0
19.0 5783.0 1463.0

Nuestro cuadro ya es entendible, podemos exportarlo a Excel y verificar que tenemos las etiquetas:

cuadro.to_excel('reporte.xlsx')

Si usas Stata...(o incluso si no)

Como dije antes, el formato .dta permite guardar las etiquetas de variables y valores junto con los datos. Los DataFrames de Pandas traen de forma nativa el método .to_stata() para exportar la información a este formato. Para que Stata reconozca correctamnete las etiquetas de valores es necesario que en pandas las variables sean de tipo categoria. A continuación, vamos a seleccionar un subconjunto de variables, reemplazaremos sus valores numéricos por las categorías de texto y convertiremos la variable en tipo categórica:

vars_export = ['SEX', 'BP1_1', 'BP1_2_01', 'BP1_2_02', 'BP1_2_03', 'BP1_2_04', 'BP1_2_05', 'BP1_2_06', 'BP1_2_07', 'BP1_2_08', 'BP1_2_09', 'BP1_2_10', 'BP1_2_11', 'BP1_2_12']
datos_stata = datos[vars_export].apply(lambda s: s.map(dicc_respuestas[s.name])).astype('category')
datos_stata
SEX BP1_1 BP1_2_01 BP1_2_02 BP1_2_03 BP1_2_04 BP1_2_05 BP1_2_06 BP1_2_07 BP1_2_08 BP1_2_09 BP1_2_10 BP1_2_11 BP1_2_12
0 Hombre seguro? Seguro(a) No aplica Seguro(a) No aplica No aplica No aplica No aplica No aplica No aplica No aplica No aplica No aplica
1 Hombre inseguro? Seguro(a) Inseguro(a) Seguro(a) No aplica Seguro(a) Seguro(a) Inseguro(a) Inseguro(a) Inseguro(a) Seguro(a) Inseguro(a) Seguro(a)
2 Hombre inseguro? Seguro(a) Seguro(a) Inseguro(a) No aplica No aplica Seguro(a) Inseguro(a) Inseguro(a) No aplica Inseguro(a) Inseguro(a) Inseguro(a)
3 Mujer inseguro? Inseguro(a) No aplica Inseguro(a) No aplica Inseguro(a) No aplica No aplica No aplica No aplica Inseguro(a) No aplica No aplica
4 Hombre seguro? Seguro(a) Inseguro(a) Seguro(a) No aplica No aplica No aplica Inseguro(a) No aplica No aplica Inseguro(a) Inseguro(a) Inseguro(a)
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
22278 Hombre inseguro? Seguro(a) Seguro(a) Seguro(a) No aplica Inseguro(a) Seguro(a) Inseguro(a) Inseguro(a) Seguro(a) Seguro(a) Inseguro(a) Seguro(a)
22279 Hombre inseguro? Seguro(a) Seguro(a) Inseguro(a) No aplica No aplica Seguro(a) Inseguro(a) Inseguro(a) No aplica Seguro(a) Seguro(a) No aplica
22280 Hombre seguro? Seguro(a) No aplica Seguro(a) No aplica Seguro(a) Seguro(a) Seguro(a) Seguro(a) No aplica Seguro(a) Seguro(a) No aplica
22281 Mujer inseguro? Seguro(a) Inseguro(a) Inseguro(a) No aplica Inseguro(a) Inseguro(a) Inseguro(a) Inseguro(a) No aplica Inseguro(a) Inseguro(a) Inseguro(a)
22282 Mujer seguro? Seguro(a) Seguro(a) Inseguro(a) No aplica Inseguro(a) Seguro(a) Inseguro(a) Seguro(a) Seguro(a) Seguro(a) Seguro(a) Inseguro(a)

22283 rows × 14 columns

Este dataframe lo exportaremos a Stata, junto con el diccionario que contiene las etiquetas de variables, especificando en la opción variable_labels.

datos_stata.to_stata('datos_stata.dta', write_index=False, variable_labels=dicc_preguntas)

Al abrir el arcivo en Stata podemos ver que efectivamente se guardaron las etiquetas de los valores y de las variables:

Hasta donde entiendo, esta sería una forma más fácil de etiquetar datos provenientes de INEGI para usuarios de Stata que haciendo el procedimiento entero en Stata. Igualmente, para los que no son usuarios de Stata, pero sí de Python o R y quieren conservar las variables categóricas etiquetadas este es un buen formato.

datos_2 = pd.read_stata('datos_stata.dta')
datos_2.dtypes
SEX         category
BP1_1       category
BP1_2_01    category
BP1_2_02    category
BP1_2_03    category
BP1_2_04    category
BP1_2_05    category
BP1_2_06    category
BP1_2_07    category
BP1_2_08    category
BP1_2_09    category
BP1_2_10    category
BP1_2_11    category
BP1_2_12    category
dtype: object