Uploaded by Adrian Galindo

Big data para científicos sociales. Una introducción

advertisement
Cuadernos
Metodológicos
60
Big data para
científicos
sociales.
Una introducción
José Manuel Robles
J. Tinguaro Rodríguez
Rafael Caballero
Daniel Gómez
La revolución digital ha generado un conjunto de
transformaciones de gran relevancia social, política,
económica y cultural. En el ámbito de la investigación social,
la emergencia de dispositivos que rápida y constantemente
recogen y almacenan nuestras opiniones, hábitos de
consumo, movilidad, etc., ha supuesto un profundo reajuste
en los procesos de análisis, así como en las metodologías y
herramientas utilizadas.
Big data es el término usado para designar este tipo de
datos, así como las técnicas empleadas para extraer
información de ellos. Dichas técnicas y, por lo tanto, sus
resultados han sido durante los últimos años patrimonio
de estadísticos, matemáticos y científicos de datos. Este
manual, por el contrario, pretende ser un instrumento para
que los científicos sociales conozcan el big data y estén
en disposición de formar parte de grupos de investigación
multidisciplinares cuyo objetivo sea conocer mejor el mundo
en el que vivimos a través de las lentes que nos facilitan
estos datos masivos y digitales.
Cuadernos
Metodológicos
60
Big data para
científicos
sociales.
Una introducción
José Manuel Robles
J. Tinguaro Rodríguez
Rafael Caballero
Daniel Gómez
Madrid, 2020
Consejo Editorial de la colección Cuadernos Metodológicos
Director
José Félix Tezanos Tortajada, Presidente del CIS
Consejeros
Francisco Alvira Martín, Universidad Complutense de Madrid; Eva Anduiza Perea, Universitat
Autònoma de Barcelona; Andrés Arias Astray, Universidad Complutense de Madrid; Miguel Ángel
Caínzos López, Universidade Santiago de Compostela; M.ª Angeles Cea D'Ancona, Universidad
Complutense de Madrid; Jesús M. De Miguel Rodríguez, Universitat de Barcelona; Vidal Díaz De
Rada, Universidad Pública de Navarra; Verónica Díaz Moreno, Universidad Nacional de Educación
a Distancia; Modesto Escobar Mercado, Universidad de Salamanca; Javier de Esteban Curiel, Centro
de Investigaciones Sociológicas; Manuel Fernández Esquinas, Consejo Superior de Investigaciones
Científicas; J. Sebastián Fernández Prados, Universidad de Almería; Isabel García Rodríguez,
Instituto de Estudios Sociales Avanzados; Juan Ignacio Martínez Pastor, Universidad Nacional de
Educación a Distancia; Violante Martínez Quintana, Universidad Nacional de Educación a Distancia;
Mónica Méndez Lago, Centro de Investigaciones Sociológicas; Verónica de Miguel Luken,
Universidad de Málaga; Rosa Rodríguez Rodríguez, Universidad Nacional de Educación a Distancia;
Leire Salazar Vález, Universidad Nacional de Educación a Distancia; Juan Salcedo Martínez,
Universidad Europea; Rafael Serrano del Rosal, Instituto de Estudios Sociales Avanzados; Luisa
Carlota Solé i Puig, Universitat Autònoma de Barcelona; Eva Sotomayor Morales, Centro de
Investigaciones Sociológicas
Secretaria
M.ª del Rosario H. Sánchez Morales, Directora del Departamento de Publicaciones y Fomento de la
Investigación, CIS
Big data para científicos sociales. Una introducción / José Manuel Robles, J. Tinguaro
Rodríguez, Rafael Caballero y Daniel Gómez. - Madrid: Centro de Investigaciones
Sociológicas, 2020
(Cuadernos Metodológicos; 60)
1. Datos masivos 2. Ciencias sociales
004.6: 316
Las normas editoriales y las instrucciones para los autores pueden consultarse en:
http://www.cis.es/publicaciones/CM/
Todos los derechos reservados. Prohibida la reproducción total o parcial de esta obra por cualquier
procedimiento (ya sea gráfico, electrónico, óptico, químico, mecánico, fotografía, etc.) y el
almacenamiento o transmisión de sus contenidos en soportes magnéticos, sonoros, visuales o de
cualquier otro tipo sin permiso expreso del editor.
COLECCIÓN «CUADERNOS METODOLÓGICOS», NÚM. 60
Catálogo de Publicaciones de la Administración General del Estado
http://publicacionesoficiales.boe.es
Primera edición, octubre, 2020
© CENTRO DE INVESTIGACIONES SOCIOLÓGICAS
Montalbán, 8. 28014 Madrid
© José Manuel Robles, J. Tinguaro Rodríguez, Rafael Caballero, Daniel Gómez
derechos reservados conforme a la ley
Impreso y hecho en España
Printed and made in Spain
NIPO (papel): 092-20-020-3 / NIPO (electrónico): 092-20-021-9
ISBN (papel): 978-84-7476-843-5 / ISBN (electrónico): 978-84-7476-844-2
Depósito Legal: M-25945-2020
DE
LO RO
E
O
C
XENT
Fotocomposición e impresión: RALI, S.A.
•
100% reciclado
El papel utilizado para la impresión de este libro es 100% reciclado y totalmente libre de
cloro, de acuerdo con los criterios medioambientales de contratación pública.
Índice
INTRODUCCIÓN.........................................................................................
11
1.QUÉ ES EL BIG DATA Y SU ENCAJE CON LA INVESTIGACIÓN
EN CIENCIAS SOCIALES......................................................................
15
1.1. Evolución en el almacenamiento de los datos..............................
1.2. Escalabilidad vertical versus escalabilidad horizontal.................
1.3.El crecimiento del volumen de datos y la investigación sociológica: las V del big data.....................................................................
1.3. Extrayendo información a partir de los datos de forma eficiente...
1.4.Contrapartidas de la escalabilidad horizontal..............................
1.5.Ciencia de datos..............................................................................
1.6. Encaje del fenómeno big data con la investigación sociológica...
15
17
2. LAS FUENTES DE DATOS....................................................................
29
2.1. Introducción....................................................................................
29
18
20
21
22
23
2.1.1. Preparando el entorno en Python.......................................
29
2.2. Carga de ficheros.............................................................................
32
2.2.1. Ficheros CSV........................................................................
33
2.2.1.1. Ejemplo: renta per cápita en la OCDE..................
2.2.1.2. Visualización del resultado....................................
33
37
2.2.2. Ficheros XML.......................................................................
39
2.2.2.1. Ejemplo: información meteorológica....................
2.2.2.2.Extracción de información a partir de ficheros
XML........................................................................
41
2.2.3. Ficheros JSON......................................................................
44
2.3. Datos desde redes sociales: Twitter...............................................
45
2.3.1. Códigos de acceso a Twitter................................................
39
45
6
CUADERNOS METODOLÓGICOS 60
La clase escucha...................................................................
Escucha de palabras clave...................................................
Anatomía de un tweet..........................................................
Lectura y tratamiento de tweets..........................................
46
47
48
50
2.4. Web scraping....................................................................................
52
2.3.2.
2.3.3.
2.3.4.
2.3.5.
2.4.1. HTML....................................................................................
2.4.2. Captura de datos con BeautifulSoup...................................
52
54
2.4.2.1. Descarga del fichero...............................................
2.4.2.2. Extraer la información...........................................
55
57
3. ALMACENAMIENTO DE DATOS.........................................................
61
3.1. Almacenamiento local versus cloud...............................................
3.2. Bases de datos relacionales............................................................
61
62
Preparando la base de datos................................................
Creación de tablas................................................................
Inserción, borrado y eliminación........................................
Consultas básicas en SQL....................................................
Consultas desde múltiples tablas........................................
Agregaciones.........................................................................
63
65
69
72
73
74
3.3. Bases de datos no relacionales: MongoDB....................................
76
3.2.1.
3.2.2.
3.2.3.
3.2.4.
3.2.5.
3.2.6.
3.3.1. Arquitectura cliente-servidor en MongoDB........................
3.3.2. Conceptos básicos en MongoDB.........................................
3.3.3. Carga de datos......................................................................
77
80
81
3.3.3.1. Importando datos ya existentes.............................
3.3.3.2. Añadiendo datos con insert...............................
81
82
3.3.4. Consultas simples.................................................................
86
3.3.4.1.
3.3.4.2.
3.3.4.3.
3.3.4.3.
find, skip, limit y sort....................................
Estructura general de find.....................................
Proyección en find..................................................
Selección en find....................................................
86
90
90
91
3.3.4.3.1. Igualdad.................................................
3.3.4.3.2.
Otros operadores de comparación y
lógicos.....................................................
3.3.4.3.3. Arrays......................................................
3.3.4.3.4. $exists................................................
92
3.3.5. find en Python......................................................................
97
92
94
96
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
3.3.6. Agregaciones.........................................................................
3.3.6.1. El pipeline...............................................................
7
99
99
3.3.6.1.1. $group..................................................
3.3.6.1.2. $match..................................................
3.3.6.1.3. $project..............................................
3.3.6.1.4.Otras etapas: $unwind, $sample,
$out… ..................................................
3.3.6.1.5. $lookup................................................
99
102
103
3.3.6.2. Ejemplo: usuario más mencionado.......................
107
3.3.7. Vistas.....................................................................................
3.3.8. Update y remove...............................................................
107
108
Update total...........................................................
Update parcial.......................................................
Upsert...................................................................
Remove...................................................................
108
110
112
113
4. TRATAMIENTO Y ANÁLISIS COMPUTACIONAL DE DATOS..........
115
4.1. Machine learning o aprendizaje automático..................................
116
3.3.8.1.
3.3.8.2.
3.3.8.3.
3.3.8.4.
4.1.1. Conceptos preliminares.......................................................
4.1.1.1. Aprendizaje y optimización...................................
4.1.1.2. Tipos de aprendizaje..............................................
103
105
120
121
122
4.1.1.2.1. Aprendizaje supervisado.......................
4.1.1.2.2. Aprendizaje no supervisado..................
122
124
4.1.1.3. Evaluación del rendimiento: entrenamiento y test.
125
4.1.1.2.1.Entrenamiento y test, validación cruzada y sus variantes...............................
4.1.1.2.2. Medidas de evaluación..........................
Medidas para problemas de regresión...
Medidas para problemas de clasificación.
127
130
131
133
4.1.1.4. La librería scikit-learn.............................................
141
4.1.2. Algoritmos de aprendizaje automático...............................
147
4.1.2.1.
4.1.2.2.
El algoritmo de los k vecinos más cercanos..........
Ejemplo práctico....................................................
Árboles de decisión.................................................
Ejemplo práctico....................................................
148
151
157
163
8
CUADERNOS METODOLÓGICOS 60
4.1.2.3.
4.1.2.4.
4.1.2.5.
4.1.2.6.
4.1.2.7.
Clasificador bayesiano/naive Bayes.......................
Ejemplo práctico....................................................
Redes neuronales artificiales.................................
Ejemplo práctico....................................................
Máquinas de soporte vectorial...............................
Ejemplo práctico....................................................
Random forest.........................................................
Ejemplo práctico....................................................
El algoritmo de las K medias.................................
Ejemplo práctico....................................................
171
176
181
190
200
207
217
220
229
233
4.2. Análisis de redes sociales................................................................
241
4.2.1. Introducción al concepto de análisis de redes sociales......
241
4.2.1.1.Conceptos básicos. Grafos y dígrafos y generación de redes...........................................................
El concepto de grafo...............................................
Ejemplo práctico....................................................
4.2.1.2. El concepto de dígrafo...........................................
Ejemplo práctico....................................................
4.2.1.3. Estructuras de representación de redes................
Ejemplo práctico....................................................
4.2.1.4. Análisis topológico de una red...............................
Ejemplo práctico....................................................
247
247
248
256
257
258
260
261
264
266
4.2.2. Centralidad en redes sociales..............................................
4.2.2.1.
4.2.2.2.
4.2.2.3.
4.2.2.4.
Centralidad por grado............................................
Centralidad por cercanía geométrica....................
Medidas espectrales de centralidad.......................
El autovector dominante izquierdo.......................
Ejemplo práctico....................................................
Índice de Katz/alpha centrality...............................
Page rank.................................................................
Hub and authorities................................................
Medidas de intermediación basadas en caminos.
267
268
269
269
270
271
272
273
275
4.2.2.4.1.
Betweenness. Centralidad por intermediación....................................................
275
4.2.2.5.Medidas de intermediación basadas en flujo (flow
betweenness)............................................................
Ejemplo práctico....................................................
Centralidad por grado............................................
Centralidad por cercanía........................................
275
276
276
279
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
9
Centralidad por intermediación............................
Medidas de centralidad espectrales.......................
Centralidad por flujo..............................................
280
281
282
4.2.3. Detección de comunidades..................................................
283
4.2.3.1.Complejidad en problemas de detección de comunidades....................................................................
4.2.3.2. Detección de comunidades con Phyton................
287
287
CONCLUSIONES.........................................................................................
291
BIBLIOGRAFÍA...........................................................................................
295
Introducción
Con la proliferación de los dispositivos electrónicos, ha aumentado exponencialmente el volumen de datos recogidos y almacenados. Como consecuencia
de ello, los científicos sociales disponen de unos recursos que, por sus características y volumen, suponen un desafío tanto metodológico como epistemológico. Este desafío es tal, que el mercado ha generado el término «científico
de datos» para referirse al experto capacitado para trabajar con este tipo de
fuentes y dar respuesta a preguntas que, en algunos casos, tienen una naturaleza claramente social o política. Sin embargo, en el ámbito académico, regido
por el método científico, esta labor no la realiza un único investigador, sino un
equipo en el que sus miembros, con distintas especialidades, hacen posible la
recogida, depuración, análisis e interpretación de los datos.
Desde nuestro punto de vista, los científicos sociales interesados en trabajar
con big data estamos llamados a formar equipos en los que convivamos, preferentemente, aunque no exclusivamente, con estadísticos e informáticos, para dar
respuesta a los desafíos a los que hacíamos referencia. Estas tres especialidades
recogen los conocimientos mínimos necesarios para trabajar con big data en
ciencia social. Sin embargo, es habitual que a esta estructura básica de equipo de
investigación se sumen expertos de otras áreas como la física, la matemática, las
ciencias naturales, etc. Sea de una u otra forma, el concepto big data supone, en
primer lugar, una llamada para la interdisciplinaridad que nosotros secundamos.
En segundo lugar, supone un replanteamiento del rol de los científicos
sociales en el marco de este tipo de equipos de investigación. Las ciencias de
la computación han generado herramientas cada vez más especializadas y eficientes para el acceso y tratamiento del big data. De la misma forma, las técnicas estadísticas para el análisis de dichos datos, así como los softwares para
aplicarlas, se han complejizado tremendamente para adaptarse a las nuevas
demandas. En este sentido, la cuestión es si debemos esperar que los cientí­
ficos sociales adquieran las competencias propias de informáticos y estadísticos expertos en big data o debemos centrarnos en otro tipo de aportaciones.
Nuestra propuesta en este libro tiende a apoyar esta segunda idea. Es
decir, de la misma forma que no se le pide a un informático que conozca e
interprete un proceso de acción colectiva a partir de las herramientas de
12
CUADERNOS METODOLÓGICOS 60
análisis expuestas por Olson en La lógica de la acción colectiva, parece poco
realista exigirle a un sociólogo el conocimiento que adquieren los expertos
en informática o estadística a lo largo de años.
¿Significa esto una compartimentación estricta de las funciones de los
miembros de un equipo de investigación que quiere trabajar con big data
en ciencias sociales? Nuestra respuesta es sí y no. Sí, en el sentido de que los
científicos sociales debemos establecer cuál es nuestra aportación en el proceso de diseño de la investigación, la recogida de datos, su depuración y análisis. Sin embargo, nuestra respuesta es también no, ya que consideramos que
debemos estar capacitados para intervenir, sin la necesidad de implementar
la tarea, en cualquier fase del proceso de investigación.
Como señalaremos en el capítulo 1, el uso de big data en ciencias sociales no
solo supone un desafío en términos metodológicos o de encaje de los sociológicos en equipos de investigación multidisciplinares, sino también en epistemológicos. Es decir, no solo estamos ante un cambio en la forma de trabajar y en el
tipo de técnicas que serán utilizadas para dichos trabajos, sino que la aparición
del big data está generando, o al menos así lo consideramos en este libro, la
necesidad de pensar en cuestiones como qué realidad social nos ofrecen los
datos de dispositivos móviles, redes sociales digitales u otras herramientas digitales de recogida de información. De esta forma, y aunque para nada es el objeto
de este trabajo, apostamos por una discusión sobre los fundamentos de nuestra
disciplina que mejor encajan con el desarrollo de estas nuevas metodologías y
sobre cuál de las tradiciones de la sociología está más preparada para sacar el
mejor partido posible de ellas. En esta lógica, también debe ocupar un espacio
destacado la sociología de corte más crítico. Cuestiones como hasta qué punto
cambia la investigación científica por el hecho de que los productores de datos
sean empresas privadas o qué sucede cuando estos sistemas hipertecnologizados recogen y analizan cada vez más esferas de nuestras vidas privadas son
cuestiones que requieren la atención de los expertos en ciencias sociales.
A partir de estas premisas, nuestro libro tiene como objetivo armar a los
científicos sociales con los conocimientos básicos para que puedan ser interlocutores capaces de trabajar en un contexto interdisciplinar como el que aquí
señalamos. Es decir, en un contexto de trabajo cuyos datos son big data. Estas
capacidades mínimas no les permitirán, naturalmente, sustituir el trabajo
realizado por informáticos o estadísticos, pero sí establecer con ellos un sistema de comunicación a través del cual puedan transmitirles la complejidad
de la realidad social y las necesidades de investigación que tenemos los científicos sociales. El colofón natural de este proceso es dar respuesta a las preguntas de investigación formuladas 1. Tarea esta que parece reservada a los
1
Tal y como se mencionará más adelante, la sociología apela a mecanismos para explicar los
fenómenos. La capacidad de establecer dichos mecanismos es, naturalmente, una responsabilidad del sociólogo.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
13
científicos sociales. Por lo tanto, este libro no capacitará al lector para trabajar con el big data de forma autónoma, pero sí para hacerlo de forma coordinada y más eficiente.
Para ello, lo primero es definir qué es el big data y cuáles son sus características. Estas serán desarrolladas en el primer apartado, donde reservaremos
un lugar destacado para discutir las posibilidades y limitaciones que supone
el trabajo con big data para las ciencias sociales. El segundo capítulo de este
libro está dirigido a mostrar cuáles son las características de las fuentes de
datos que se consideran bajo la categoría big data. Una vez hecho esto, en el
capítulo 3, procederemos a explicar las principales técnicas de descarga y
almacenamiento de este tipo de información. El capítulo 4 se centra en el análisis con big data. Específicamente, se tratarán dos técnicas básicas en este
entorno. Estas son el aprendizaje automático (machine learning) y el análisis
de redes sociales (social network analysis). Los capítulos 2, 3 y 4 tienen un
interés eminentemente pedagógico, por lo que se intercalarán explicaciones y
ejercicios prácticos que el lector puede seguir. Estos ejercicios han sido desarrollados haciendo uso del software Python. No obstante, para aquellos lectores más habituados a utilizar el programa R hemos preparado un repositorio
de libre acceso 2 en el que podrán encontrar en R los mismos ejercicios 3. Para
la selección de dichos ejemplos, hemos supuesto que el lector no tiene una
formación previa en este tipo de técnicas. Esto hace que este libro pueda ser
aprovechado por científicos sociales con cualquier nivel de conocimientos
estadísticos e informáticos. Esperamos que el lector disfrute de esta obra que
supone, al menos desde nuestro punto de vista, un primer paso en una línea
que no por emergente está exenta de avances significativos.
Disponible en: https://github.com/bigdatasociales/libro
Tanto R como Python son herramientas muy útiles y utilizadas para el análisis de big data.
Sin embargo, hemos optado por este lenguaje por los siguientes motivos. En primer lugar, porque
se trata de un lenguaje con alto nivel de simplicidad, ya que los programas funcionan con la
menor cantidad de código. En segundo lugar, es altamente compatible con muchas herramientas
útiles para el análisis de big data, como, por ejemplo, Hadoop. En tercer lugar, es un lenguaje relativamente sencillo de aprender y, en cuarto lugar, permite acceder a una amplia gama de paquetes de gran alcance y de herramientas de visualización.
2
3
1
Qué es el big data y su encaje con la
investigación en ciencias sociales
1.1. Evolución en el almacenamiento de los datos
La primera idea que sugiere el término big data es la de una cantidad ingente
de datos, pero ¿de qué número de datos hablamos realmente? ¿Por qué el término se ha hecho tan popular en los últimos tiempos? Para contestar estas
cuestiones vamos a comenzar haciendo un pequeño repaso a la evolución histórica del tratamiento automático de datos.
El almacenamiento y recuperación de datos fue una de las primeras aplicaciones de los por entonces llamados «cerebros electrónicos». El primer
computador comercial con una difusión amplia, el UNIVAC I, fue adquirido
en 1951 por la oficina estadounidense del censo. Pronto, las grandes aseguradoras y los bancos siguieron su ejemplo. Los datos se almacenaban en
grandes cintas, que permitían una veloz consulta, obviamente no según
estándares actuales, pero sí en comparación con la consulta de archivos en
papel empleada hasta entonces.
La posibilidad de manejar cantidades de información hasta entonces
impensables trajo también nuevos problemas técnicos. Por ejemplo, si se
deseaba tener el censo ordenado por apellidos, y también, aparte, por códigos
postales, lo que se hacía era duplicar la información en dos cintas, una por
cada ordenación. El problema surgía cuando, por ejemplo, había que corregir
la dirección de un individuo censado. En ese caso, podría ocurrir que la persona encargada de la corrección, por descuido, modificara solo una de las cintas. El resultado sería que, al consultar posteriormente la información, dicho
individuo aparecería con una dirección diferente según el método de ordenación escogido, esto es, según la cinta consultada.
Cuando estas discrepancias surgen, a menudo resulta complicado determinar cuál es el dato correcto y cuál el erróneo, lo que puede dificultar enormemente su corrección. A largo plazo, las inconsistencias pueden llegar a
16
CUADERNOS METODOLÓGICOS 60
convertir los datos almacenados en algo completamente inútil, afectando de
forma notable a la calidad de la información que se puede recuperar. Hay que
añadir que, en estos primeros tiempos, el almacenamiento y la consulta de
estas rudimentarias bases de datos se realizaban mediante software escrito a
propósito para cada caso, lo que aumentaba la probabilidad de errores en los
programas, que terminaban reflejándose, de nuevo, en los resultados.
Esta situación poco deseable continuó sin avances demasiado significativos
hasta que en 1970 Edgar Frank Codd publicó su artículo «A relational model
of data for large shared data banks». En este artículo, Codd indica que para
evitar inconsistencias hay que intentar eliminar la redundancia de los datos, es
decir, se debe evitar almacenar el mismo dato más de una vez. Con este objetivo en mente, Codd describe un modelo de datos destinado a eliminar, o al
menos minimizar, las redundancias o duplicidades de datos. Además, en la
propuesta de Codd, es el propio sistema el que se encarga de detectar y evitar
buena parte de las duplicidades, avisando cuando, por ejemplo, se intenta
incluir en alguna base de datos a dos individuos con el mismo DNI.
Una ventaja fundamental del modelo de Codd es que permite una visión de
alto nivel que parte de la idea de almacenar datos del mismo tipo en tablas con
unas columnas fijas. Además, la persona que gestiona los datos no se preocupa de cómo se graban estos físicamente, solo de razonar de forma abstracta, lo que facilita el diseño y manejo de las tablas. En el artículo de Codd,
estas tablas se describen formalmente como relaciones matemáticas, lo que
ha dado lugar a que a menudo se denomine a la propuesta de Codd el «modelo
relacional» de las bases de datos.
Para insertar, eliminar, modificar o consultar datos en el modelo relacional
se desarrolló un lenguaje al que se llamó SQL (Structured Query Language).
SQL permite realizar consultas complejas de forma elegante, combinando
diferentes tablas y columnas, y de nuevo permitiendo desechar, o al menos
dejar en segundo plano, consideraciones sobre cómo estos datos están físicamente almacenados.
Aunque Codd trabajaba en IBM, fue otra empresa la que convirtió sus
ideas en un gran éxito. Esta empresa, Oracle, ha sido la dominadora en el
ámbito de las bases de datos durante los últimos casi cincuenta años, gracias
al modelo relacional de Codd, que sigue vigente. Dentro del capítulo dedicado
al almacenamiento de datos, veremos en más detalle este modelo y su lenguaje de consultas asociado, SQL.
Por tanto, durante casi cincuenta años, las bases de datos relacionales han
sido capaces de gestionar perfectamente las grandes cantidades de datos requeridas por la mayor parte de las empresas e instituciones públicas. Si en algún
momento el volumen de datos llegaba a rebasar las capacidades físicas del sistema (por ejemplo, a llenar el disco duro), la solución habitual era comprar un
disco duro mayor, o añadir un nuevo disco dentro del mismo ordenador. Por
supuesto, si se sigue necesitando más y más capacidad de almacenamiento,
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
17
llega un momento en que esto no es posible, bien porque el ordenador simplemente no admite más discos duros o de mayor capacidad, o porque la cantidad
de información es tan grande que también requiere más memoria, un mejor
procesador, etc. En estos casos, no queda (o mejor, no quedaba) otra alternativa que comprar un ordenador mayor, con la consiguiente, y a menudo costosa, migración de programas y datos. Esto se conoce como «escalabilidad
vertical». Por supuesto, pasar, por ejemplo, de la gama de ordenador de sobremesa a sistemas mayores requiere un gran desembolso y, a menudo, precisa la
contratación de personal especializado. Sin embargo, esto no llegó a percibirse
en ningún momento como un problema real del modelo de Codd porque, hasta
comienzos del presente siglo, esta situación afectaba, únicamente, a grandes
empresas u organizaciones gubernamentales.
1.2. Escalabilidad vertical versus escalabilidad horizontal
Sin embargo, con la llegada del siglo xxi este escenario ha cambiado significativamente, debido, principalmente, a tres factores relacionados: Internet, la
telefonía móvil, y más recientemente, la llegada de la llamada «Internet de las
cosas». El primero en notar este cambio fue, cómo no, Google, que durante el
desarrollo de su famoso buscador se encontró con la necesidad de almacenar
nada menos que una copia completa de todas las páginas de Internet del
mundo. Más aún, necesitaba que las búsquedas fueran rápidas, un factor
determinante en el éxito de su nuevo buscador. Esto era un problema porque
no había ordenador capaz de almacenar tanta información. Además, era previsible que la cantidad de información a almacenar siguiera creciendo en los
siguientes años a un ritmo muy alto. Es decir, la escalabilidad vertical simplemente no era una opción.
Este problema llevó a Google, y posteriormente a otras empresas como
Facebook y Twitter, a apostar por una forma alternativa de almacenamiento
de datos, la llamada «escalabilidad horizontal», que ha pasado a constituir
uno de los grandes conceptos detrás del término big data. La idea es sencilla:
en lugar de adquirir ordenadores cada vez mayores y más costosos, lo que
haremos será tener multitud de ordenadores pequeños, más o menos baratos,
conectados entre sí. Los datos se distribuirán entre todos los ordenadores,
creando la ilusión de un ordenador gigante. Si vemos que queda poco espacio
en los discos de todos los ordenadores, simplemente conectamos nuevos ordenadores del mismo tipo, y así sucesivamente. El sistema compuesto por todos
estos ordenadores se conoce como «clúster».
Cuando Google y otras empresas se decidieron por la escalabilidad horizontal, solucionaron un problema de hardware, el del almacenamiento físico,
pero se encontraron con un nuevo problema: el del software. Las bases de
datos relacionales de las que hablábamos más arriba no estaban pensadas
18
CUADERNOS METODOLÓGICOS 60
para funcionar en un sistema distribuido con escalabilidad horizontal. Es
más, el nuevo buscador requería la realización de cómputos complejos en este
entorno compuesto por multitud de ordenadores conectados entre sí. Por ello,
Google decidió diseñar sus propias bases de datos, que ya no estaban basadas
en el sistema relacional. La presencia del modelo relacional y de su lenguaje
de consultas SQL era tan abrumadora que el término que definía a las nuevas
bases de datos se acuñó por negación, pasando a conocerse como «bases de
datos NoSQL». Hablaremos un poco más sobre ellas en el capítulo dedicado
al almacenamiento de datos.
En todo caso, ya podemos acotar qué cantidad de datos se considera merecedora del término big data; hablamos de un volumen de datos que supera
ampliamente la capacidad de almacenamiento de un ordenador de sobremesa
típico. La definición es, sin duda, ambigua, pero nos da una primera idea de a
qué cantidad de datos nos referimos.
Hay que señalar que estos volúmenes de datos, que hace treinta años eran
inimaginables, y hace diez años tan solo existían para las grandes empresas,
son hoy en día comunes. Imaginemos, por ejemplo, los registros de visitantes
de una página web, incluso de un comercio pequeño, o los datos de posición
GPS emitidos por los teléfonos de los conductores de una compañía de camiones, o la monstruosa cantidad de datos generada por sensores que tenemos
hoy en día en nuestras casas, vehículos o en los espacios públicos.
1.3. El crecimiento del volumen de datos y la investigación
sociológica: las V del big data
Aunque sea su rasgo más llamativo, el gran volumen de datos no es la única
característica que define a los entornos big data. Habitualmente se menciona
un mínimo de tres propiedades, conocidas como las V del big data por sus iniciales: volumen, velocidad y variedad.
La velocidad se refiere al ritmo de llegada de los datos, y, en ocasiones,
también al ritmo en el que deben ser recuperados y analizados. Imaginemos
que estamos «escuchando» nuevos tweets sobre un tema que nos interesa
para su posterior análisis. En este caso deberemos ser capaces de grabar los
tweets en streaming, es decir, recibiendo y grabando a la suficiente velocidad
para no perder ningún tweet. Los entornos relacionados con la Internet de las
cosas nos proporcionan casos extremos de necesidad de máxima velocidad de
almacenamiento. Esto es así porque se trata de datos producidos por sensores
a ritmos, en ocasiones, por debajo del milisegundo.
La tercera V es la de variedad, que hace referencia a que los datos nos pueden llegar en formatos distintos. Por ejemplo, imaginemos que estamos recopilando datos de diferentes blogs para conocer opiniones acerca de un determinado
tema. Cada blog tendrá una estructura distinta, unos con imágenes, otros puede
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
19
que con vídeos..., y queremos recopilar toda información. En este caso, las
bases de datos relacionales no son una solución adecuada, porque nos obligan
a pensar a priori en un esquema fijo, en unas «columnas» para cada «fila» de la
tabla, donde la fila representaría aquí un blog y las columnas, sus componentes.
En cambio, las bases de datos NoSQL, pensadas para este tipo de entornos,
están preparadas para almacenar y procesar información heterogénea, sin
necesidad de definir un esquema fijo a priori; filas distintas podrán tener columnas diferentes.
A estas tres V se añaden a menudo otras; entre las más comunes, la veracidad y el valor. La veracidad se refiere más bien a la falta de veracidad. En
entornos big data es habitual la existencia de datos erróneos o imprecisos. Por
ejemplo, los datos de posición obtenidos a partir de la señal de un teléfono
móvil tienen un rango de precisión y puntualmente pueden dar informaciones
completamente erróneas. Igualmente, si estamos recopilando tweets sobre,
por ejemplo, el debate en torno a una campaña electoral, lo que haremos es
seleccionar unas cuantas palabras que definan los temas centrales en dicho
proceso político. Sin embargo, dada la polisemia de alguno de los términos
usados en el debate político, algunos tweets descargados contendrán mensajes sin relación alguna con el tema que nos interesa. Es importante tener una
medida de la cantidad de datos erróneos, porque pueden influir en las conclusiones que obtengamos de ellos.
El último término que comienza con la letra V se refiere al valor de los
datos generados, y merece un comentario más detallado que haremos en el
siguiente apartado. Pero antes debemos comentar que el término big data se
suele aplicar a conjuntos de datos que cumplen alguna o algunas de estas propiedades, haciendo hincapié en que rara vez se dan todas simultáneamente.
Por ejemplo, y aunque parezca paradójico, podemos hablar de un entorno big
data en el que los datos se reciben a alta velocidad, y con cierta variedad en
formato, pero donde, sin embargo, los datos no llegan a alcanzar un gran
volumen. Es decir, veremos a menudo utilizar el término big data en situaciones en las que el volumen de datos no es excesivamente alto, pero sí se cumple
alguna de las otras V.
Este caso es relativamente común en el uso que se hace del término en
ciencias sociales. El tipo de acceso a la información que tienen los científicos
sociales es, en muchos casos, limitado. Esta limitación se debe a que los datos
son recursos privados a los que no siempre se puede acceder para su uso científico 4. Esto ha generado que, por lo general, los científicos sociales usen con
4
Esta circunstancia debería hacernos reflexionar sobre el futuro de la investigación científica
en el marco del big data. La realidad es que estamos pasando de un escenario en el que las principales fuentes de datos eran producidas por instituciones públicas o por los propios científicos a
un entorno en el que los datos son patrimonio privado y, en el mejor de los casos, los científicos
podemos acceder a ellos de forma limitada.
20
CUADERNOS METODOLÓGICOS 60
más frecuencia datos de fuentes abiertas como open data o redes sociales digitales que, con más probabilidad, cumplen con los criterios de velocidad y
variedad, pero menos con el de volumen. Por lo tanto, en este manual usamos
el término big data en este sentido más laxo.
1.3. Extrayendo información a partir de los datos de forma
eficiente
Uno de los grandes éxitos del primer ordenador comercial, al que nos hemos
referido antes, UNIVAC I, fue predecir el resultado de las de las elecciones
americanas de 1954. En efecto, la cadena de televisión CBS anticipó la victoria de Dwight D. Eisenhower con tan solo el 1% de los votos escrutados, lo que
resulta especialmente meritorio si se tiene en cuenta que las encuestas daban
como vencedor al otro candidato. Esto mostró la utilidad de los ordenadores
no solo como almacenes de datos, sino como potentes máquinas capaces de
utilizar estos datos para producir nueva información y realizar predicciones
acertadas. Esta es la última V, la del valor de los datos, que es la que ha dado
lugar al crecimiento del fenómeno big data.
En efecto, disponer de una cantidad ingente de datos permite extraer conclusiones y desarrollar herramientas hasta hace poco inimaginables. Un ejemplo conocido es el reconocimiento automático de escritura caligráfica, de
mensajes de voz, o incluso el reconocimiento facial, que han sido posibles gracias a técnicas como el llamado aprendizaje profundo, basado en redes neuronales multicapa que requieren una cantidad ingente de datos clasificados
manualmente para entrenar el modelo (veremos más sobre las técnicas de clasificación en el capítulo 4).
Dicho esto, el principal inconveniente en los entornos big data surge de la
dificultad para realizar cómputos eficientes con tal cantidad de datos. Este problema es tan importante que puede impedir el uso práctico de estos datos para
análisis relativamente sencillos y comunes en ciencias sociales, como, por ejemplo, el cálculo de la media de los datos disponibles. ¿Para qué queremos almacenar tantos datos si luego vamos a tardar una eternidad en procesarlos? Sin
embargo, la escalabilidad horizontal, a la que hemos hecho referencia antes y
que permite almacenar de manera distribuida grandes cantidades de datos,
también viene en nuestra ayuda cuando se trata de analizar la información.
Por ejemplo, supongamos que queremos obtener la media aritmética de un
cierto conjunto de datos en un clúster big data, donde los datos estarán físicamente repartidos entre los miembros del clúster. El cálculo de la media se
hará de la siguiente forma:
a)Se pide a cada miembro o nodo del clúster que calcule la suma de sus
valores, así como el total de valores que contiene.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
21
b)Estos valores calculados, la suma y la cantidad de valores sumados, se
transmiten a un miembro seleccionado para llevar el cómputo final.
c)Finalmente, este nodo especial calcula la suma de los resultados parciales, y divide entre la suma del número de componentes en cada nodo,
obteniendo el resultado final.
El paso crucial de este proceso es el b). Todos los cálculos de las sumas parciales se hacen en paralelo de manera simultánea. Esto hace que en un sistema big data basado en escalabilidad horizontal se tarde prácticamente lo
mismo en calcular la media de 100 mil valores que la de 100 millones de valores. Más valores significará más nodos en el clúster, pero el cálculo se seguirá
en paralelo, con la misma velocidad que en el caso de tan solo tres nodos. Se
puede argüir que, aunque el paso b) parezca, en efecto, escalable, el paso c)
parece, ciertamente, más complicado cuando el clúster cuenta con un mayor
número de nodos. Sin embargo, este paso tiene un impacto mínimo en la eficiencia total, porque el número total de nodos siempre es, en términos de big
data, un número pequeño, lo que se conoce como small data. Aunque contáramos con un gigantesco clúster de 10 mil ordenadores conectados entre sí;
sumar en el paso c) 10 mil números es algo inmediato para un ordenador
estándar.
Esta técnica de divide y vencerás hace que la escalabilidad horizontal no
solo haya constituido una solución para almacenar ingentes cantidades de
datos, sino que, además, haya permitido analizar estos datos de forma eficiente, posibilitando el procesamiento de estos datos y, con ello, el auge del
fenómeno big data.
1.4. Contrapartidas de la escalabilidad horizontal
Sin embargo, trabajar con un gran volumen de datos también presenta varias
contrapartidas que conviene conocer. En primer lugar, en un clúster aumenta
proporcionalmente la probabilidad de que alguno de sus nodos se estropee.
Si, por ejemplo, asumimos que un ordenador, funcionando ininterrumpidamente, alcanza una vida media de dos años, entonces, en un clúster de veinticuatro ordenadores tendremos una media de una rotura de ordenador al mes
como promedio. El problema no es solo económico, sino de pérdida de datos.
Y hacer copias de seguridad de esa cantidad enorme de información es muy
complicado. Para evitar esto, los sistemas que gestionan clústeres big data,
como Hadoop, copian cada dato a más de un nodo (el estándar es a tres nodos,
pero esto es configurable). De esta forma, aunque un ordenador falle, el dato
seguiría en dos nodos más, y no se habría perdido. Es más, en cuanto el sistema detecte la caída se encargará de añadir una copia adicional para mantener el número de copias inalterado.
22
CUADERNOS METODOLÓGICOS 60
Esta política de copia múltiple, asociada de forma invariable a la escalabilidad horizontal, logra tener el sistema activo de forma permanente, y sin pérdida
de información, pero, a cambio, conlleva algunas contrapartidas. La primera, y
más obvia, es la económica. Si hay que grabar cada dato por triplicado, necesitaremos más ordenadores. Por ejemplo, si queremos almacenar datos que ocupan cuatro veces el tamaño de disco de un equipo de sobremesa, necesitaremos
4×3=12 ordenadores, y no cuatro, como inicialmente podríamos pensar.
En segundo lugar, como ya se ha señalado, tener información duplicada
puede dar lugar a inconsistencias. En la mayor parte de los sistemas big data
estas inconsistencias se evitan asegurando de forma automática que cuando
se modifica un dato el cambio se haga efectivo en todas sus réplicas. Sin
embargo, este cambio puede llevar un tiempo (por ejemplo, por errores de
conexión de algunos de los nodos), y si, durante ese tiempo, consultamos un
nodo con una réplica no actualizada, obtendremos la versión antigua del dato.
Por ello, en los entornos big data se suele hablar de consistencia eventual, que
quiere decir que si somos pacientes alcanzaremos la consistencia. Esto hace
que estos sistemas no sean tan adecuados para entornos en los que haya que
modificar y consultar información de forma casi simultánea.
Finalmente, debemos mencionar que la posibilidad de realizar los cómputos costosos en paralelo no siempre existe, depende de cada operación concreta. Hemos visto que resulta muy sencillo en el caso de la media aritmética,
pero supongamos, por ejemplo, que lo que queremos no es calcular la media,
sino la mediana. No es nada obvio que calcular la mediana de los datos en
cada nodo nos pueda ayudar a calcular la mediana del conjunto de datos total.
Esto ha supuesto que numerosos algoritmos conocidos hayan tenido que
adaptarse a este tipo de entornos. A nosotros, como usuarios de sistemas big
data ya implementados, esto no debe importarnos. Sin embargo, es conveniente conocer esta limitación, para entender, por ejemplo, por qué entre las
estadísticas básicas del sistema Spark, uno de los más conocidos para el procesamiento de big data, se encuentran datos típicos, como la media aritmética, los máximos y mínimos, pero sorprendentemente no aparece la mediana.
Esto no significa que esta medida, básica e inmediata en cualquier sistema
small data, no pueda ser calculada, solo que su cómputo resulta costoso en
tiempo. En particular, el sistema Spark ofrece un método para el cálculo de
cuantiles basado en el algoritmo de Greenwald-Khana, un método aproximado eficiente para este tipo de entornos, pero aun así más costoso sobre
grandes cantidades de datos que otras operaciones fácilmente paralelizables.
1.5. Ciencia de datos
Podríamos definir la ciencia de datos como el conjunto de métodos y procesos basados en técnicas estadísticas y de inteligencia artificial destinados a
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
23
obtener información relevante a partir de un conjunto de datos. Para obtener resultados relevantes se deben combinar tres ramas de conocimiento:
— Matemáticas, y, en particular, estadística: tienen una doble misión.
Por un lado, gran parte de los métodos utilizados en ciencia de datos
son de naturaleza estadística. Conocer sus características y limitaciones resulta vital para escoger el método adecuado. Por otra parte, la
estadística nos permitirá diseñar experimentos que nos posibiliten
evaluar los resultados obtenidos, calcular los márgenes de confianza
de las predicciones, la precisión y exhaustividad en métodos de clasificación, etc.
— Informática: se precisa conocer el software existente, ser capaz de adaptarlo y configurarlo, conocer sus posibilidades y limitaciones.
— El ámbito de aplicación, en nuestro caso, las ciencias sociales. Son los
conocedores del ámbito de aplicación los que serán capaces de plantear
las preguntas adecuadas y de interpretar los resultados obtenidos.
Es difícil encontrar personas que tengan conocimientos profundos de las
tres ramas, por eso en el mundo del big data y las ciencias sociales, e incluso
en el mundo del big data en general, se tiende hacia la formación de equipos
interdisciplinares. Por ello, creemos que la labor del profesional de las ciencias sociales no es tanto dominar a la perfección las herramientas y técnicas
informáticas como conocer sus posibilidades, de manera que sea capaz de
reconocer cuándo un problema puede tener solución en un entorno big data.
Siguiendo este principio, el presente manual no busca dar una visión completamente detallada de todos los aspectos técnicos relacionados con los
entornos big data, lo que nos exigiría llegar a niveles técnicos más propios de
la informática que de las ciencias sociales. Sí pretendemos, en cambio, mostrar qué posibilidades ofrecen estos sistemas, de forma que el profesional de
las ciencias sociales sepa qué técnica es la adecuada y qué posibilidades ofrece
en cada contexto.
1.6. Encaje del fenómeno big data con la investigación
sociológica
La sociología, en general, y las ciencias sociales, en particular, estudian
objetos pluralistas. Esto es, entidades que satisfacen determinadas condiciones pero que, a nivel individual, varían sensiblemente en sus propiedades
(Goldthorpe, 2017). De esta forma, aunque los elementos individuales de los
que se ocupa la sociología, los humanos, puedan presentar una considerable
variabilidad, también presentan, a nivel agregado, regularidades de tipo probabilístico.
24
CUADERNOS METODOLÓGICOS 60
Las ciencias sociales toman estas regularidades como explananda, es decir,
consideran que dichas regularidades son su objeto de análisis. Desde esta
óptica, algunos autores han definido la sociología como una «ciencia de la
población» (ibid.). Es decir, una disciplina que analiza las regularidades que
se producen en el seno de un grupo de individuos en un tiempo y espacio concretos.
La sociología se ha mostrado como una disciplina con una gran capacidad
para visibilizar fenómenos. Es decir, apoyada en la estadística, ha alcanzado
grandes éxitos a la hora de evidenciar regularidades y describirlas de forma
que puedan ser vistas por un observador atento. Un caso paradigmático es, por
ejemplo, la segunda transición demográfica. Bajo este concepto describimos la
tendencia según la cual, tras la II Guerra Mundial, los países desarrollados inician un proceso de caída del nivel de fecundidad por debajo del nivel de reemplazo, de descenso de la mortalidad (tanto infantil como en edad adulta), de
aumento del acceso femenino a la educación y al mercado laboral remunerado, de incremento de la edad de concepción del primer hijo, de hijos fuera
del matrimonio, de la edad media del matrimonio, una mayor pluralidad de
modelos de familia, etc. Este proceso, bien descrito y fundamentado estadísticamente, es una regularidad que ha sido observada en un amplio conjunto de
casos de estudio. Es, podríamos decirlo así, una constante sociológica.
Tomar las regularidades estadísticas como el objeto de la sociología hace
menos viable la costumbre de analizar sucesos singulares como objeto de
investigación. En dichos sucesos, el azar juega un papel muy destacado. Además, los sucesos singulares son difícilmente reproducibles o, en otras palabras, difícilmente identificables en otros contextos y momentos. Sin embargo,
observamos que, cuanto mayor es la población estudiada, más posible es
encontrar regularidades que son descriptibles en términos estadísticos. En
este caso, y usando una expresión poética, se hace posible «domesticar el
azar» que afecta a la actividad humana. Por este y los motivos señalados más
arriba, la sociología se ocupa de fenómenos sociales entendiendo estos como
regularidades estadísticas (sucesos generales).
Sin embargo, la sociología es una disciplina menos firme a la hora de hacer
transparentes dichos fenómenos. Es decir, mientras que se ha mostrado muy
capaz de establecer regularidades suficientemente fundamentadas como para
transformarlas en su objeto de investigación, no se ha demostrado tan capaz
a la hora de explicar dichas regularidades. Así, mientras visibiliza bien los
fenómenos sociales, no consigue dar suficientes razones para que se nos presenten de forma transparente y clara. Esta es, sin lugar a duda, la tarea pendiente de la sociología.
Dicho esto, es importante señalar que, para una correcta explicación en ciencias sociales, no debemos evitar la variabilidad del comportamiento humano. De
hecho, todo lo contrario, si el objeto de investigación (explanandum) son las
regularidades estadísticas, la explicación debe producirse a nivel individual.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
25
La sociología no es, así, ontológicamente individualista, es decir, no piensa
que la sociedad esté formada por átomos que, en forma de individuos, interactúan incondicionadamente. En cambio, la sociología es cada vez más individualista en términos metodológicos. Esto significa que, siguiendo un esquema
lógico, parte de los elementos más simples para explicar los elementos más
complejos. Dicho de otra forma, para explicar un comportamiento colectivo
como la huelga, toma las preferencias, objetivos y oportunidades de los agentes individuales involucrados, como base para dar razones que ayudan a desvelar dicho fenómeno.
Este tipo de abordaje analítico se realiza bajo el supuesto de que los individuos se comportan de forma racional. Es decir, usan las oportunidades de
las que disponen para lograr los objetivos que persiguen en función de sus
preferencias. Nuevamente, con esta afirmación no se defiende la tesis de que
los sujetos son ontológicamente racionales, sino que el sociólogo trabaja
bajo el supuesto de que los individuos se comportarán de esa manera. La
infracción de este supuesto es, no obstante, un motivo de investigación para
la sociología.
Como complemento, la explicación propia de las ciencias sociales analíticas se diferencia de otras formas de explicación científica. El enfoque de las
leyes de cobertura defiende que una explicación se produce cuando un fenómeno puede ser subsumido bajo una ley causal general. Por su parte, la explicación estadística se centra en la capacidad de un modelo estadístico para
alcanzar una alta precisión predictiva. Sin embargo, en ciencias sociales se
suele apelar a mecanismos explicativos. Según Peter Hedström (2010), a través de los mecanismos, «los fenómenos sociales se explican deductivamente
haciendo referencia a una constelación básica de entidades y actividades
organizadas espaciotemporalmente de tal forma que producen con regularidad el tipo de fenómeno que se está tratando de explicar» (p. 212). Estas entidades son normalmente actores y las actividades son las acciones que llevan a
cabo dichos actores.
Siendo así, ¿cuál es el encaje entre big data y ciencias sociales? Para responder adecuadamente a esta pregunta, tenemos que asumir, como hace
Aaron Cicourel (2011), que los métodos que tratan de medir los fenómenos
sociales también implican supuestos teóricos. Así, lo primero que debemos
destacar es que el big data no está diseñado para ofrecer descripciones densas
sobre los fenómenos (Goldthorpe, 2010).
Entendemos por «descripciones densas» un conjunto de datos que nos
informan sobre las motivaciones profundas de los individuos, la relación de
estas con el contexto vital y situacional del sujeto, así como, por ejemplo, la
vinculación de todo ello con los procesos de socialización y aprendizaje del
informante. De hecho, y en muchos casos, algunas fuentes big data como las
redes sociales (especialmente Twitter) no nos ofrecen información sobre
recursos individuales de las personas, como el nivel de estudios o la renta. La
26
CUADERNOS METODOLÓGICOS 60
información que obtenemos a partir de los mensajes que los internautas
comparten a través de las redes sociales digitales o gracias a las herramientas
de geolocalización incorporadas en los teléfonos móviles se refiere a acciones, expresiones o comportamientos individualizados y socialmente no contextualizados. Sabemos, por ejemplo, que el sujeto X se encontraba el día 3
de junio de 2016 en la plaza del Palillero y que, dicho sujeto, ese mismo día y
desde ese mismo lugar, expresó su acuerdo con el mensaje emitido por Y a
través de Twitter en relación con la candidatura C a las elecciones generales
celebradas ese mismo mes. Sin embargo, poco o nada sabemos sobre las
motivaciones profundas de ese sujeto para expresar dicho mensaje o para
estar en esa plaza a esa hora y ese día. No estamos, por lo tanto, ante una descripción densa de la circunstancia narrada.
Naturalmente, y en muchos y variados sentidos, este procedimiento supone
una limitación seria para el análisis de la realidad social. Desconocer, por
ejemplo, en qué medida la posición política de X está relacionada con su
socialización primaria es, sin duda, una laguna profunda. Sin embargo, conscientes de ello y asumiendo esta y otras limitaciones similares, los sociólogos
analíticos están especialmente bien posicionados para sacar el mayor partido
posible a los datos procedentes de entornos big data.
La sociología analítica no aspira, como otros paradigmas de las ciencias
sociales, a una descripción profunda sobre cómo se construyen las preferencias de los sujetos o cómo dichos sujetos llegan a estar políticamente posicionados. Por el contrario, el objetivo de la sociología analítica, basada en el
individualismo metodológico y el principio de racionalidad, es comprender
cómo un conjunto elevado de acciones individuales que se realizan conjuntamente termina generando fenómenos de interés a nivel macro (mecanismos
sociales). En otras palabras, no requiere, para cumplir sus propósitos, de una
descripción densa. Por el contrario, requiere de información sobre el sentido
de las acciones de los sujetos y, de forma complementaria, sobre las oportunidades de las que disponen y qué creencias expresan. Todo ello es información
disponible en las grandes bases de datos y pone en evidencia el apropiado
encaje entre esta forma de entender la sociología y este tipo de bases de datos.
Esta forma de entender la sociología no es, sin embargo, nueva para la disciplina. Todo lo contrario, sigue muy de cerca la definición weberiana expresada por Jean-Marie Vincent, autor en Fundamentos metodológicos de la
sociología (edición en castellano de 1972). En este texto define la sociología
como «aquella disciplina que pretende entender, interpretándola, la acción
social [...]. La acción social, por lo tanto, es una acción en donde el sentido
mentado por el sujeto o sujetos está referido a la conducta de otros, orientándose por ésta en su desarrollo» (pp. 44-45). Es decir, nos centramos en la
acción de los individuos en tanto que sus acciones son la expresión de la
in­teracción con otros. Como veremos en este libro, el big data y las técnicas para
estudiarlos encajan bastante bien con esta forma de entender la sociología.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
27
Por último, es de gran relevancia saber qué tipo de información podemos
esperar del big data. Generar expectativas sobre la capacidad de esta herramienta para responder a todas y cada una de las preguntas sociológicamente
relevantes es, cuanto menos, arriesgado. Preferimos pensar, de partida, un
contexto de investigación menos ambicioso en el que el investigador sea consciente de que las complejas y elaboradas técnicas que le serán presentadas a
continuación le devolverán información centrada en las preferencias reveladas por los sujetos, así como relaciones de estos y otros agentes. Esta circunstancia, que puede ser un importante inconveniente para otras tradiciones de
las ciencias sociales, es el terreno natural para las ciencias sociales analíticas.
2
Las fuentes de datos
2.1. Introducción
La primera fase para el análisis de datos consiste, obviamente, en la obtención de los propios datos. A diferencia de las técnicas tradicionales de recogida de información en ciencias sociales, en los entornos tipo big data la web
se transforma en una fuente constante e inagotable del máximo interés para
la investigación social. Este cambio de atención desde los datos analógicos
a los digitales plantea, como era de esperar, nuevas oportunidades, pero
también nuevos retos 1.
En cualquier caso, el problema sigue siendo el siguiente: ¿cómo capturar
esta información?
2.1.1. Preparando el entorno en Python
Durante este capítulo se van a ver las distintas alternativas para la captura de
datos utilizando como base el lenguaje de programación Python. Este lenguaje presenta como ventaja el disponer de multitud de bibliotecas que ya realizan buena parte de la tarea de forma automática. Se trata, además, de un
lenguaje de programación relativamente sencillo de aprender y que también
es empleado de forma habitual en análisis de datos, por lo que será el lenguaje
1
Un aviso inicial, debemos ser cuidadosos y acceder a los datos siempre dentro de la legislación vigente. Algunos de los contenidos de las páginas pueden estar protegidos por derechos de
autor, o ser propiedad de una empresa que no desea cederlos gratuitamente. Es nuestra responsabilidad ser cuidadosos y consultar las posibilidades que ofrece la página (licencia de uso). Además, muchos sitios web pueden llegar a bloquear nuestra IP si detectan que estamos accediendo
a sus datos sin permiso.
En este sentido, hay que mencionar el impulso que en los últimos años se ha dado al concepto
de datos abiertos, adoptado cada vez más por organizaciones privadas y públicas, que busca proporcionar datos accesibles y reutilizables, sin exigencia de permisos específicos.
30
CUADERNOS METODOLÓGICOS 60
utilizado en el resto del libro. Quizá la forma más sencilla de utilizar Python
es a través de los llamados Jupyter Notebooks, que nos permiten escribir el
código desde el navegador e ir probándolo paso a paso.
Para instalar Python existen muchas posibilidades. En este libro recomendamos la distribución Anaconda, que ya incluye numerosas librerías 2. La ventaja de esta distribución es que ya cuenta con muchas bibliotecas preinstaladas,
así como los mencionados Jupyter Notebooks, el entorno sencillo de programación que vamos a utilizar en este capítulo. Tras la instalación, debemos ir
a un terminal (en el sistema operativo Windows hay que buscar Jupyter Notebook en el menú de inicio), y allí teclear:
jupyter notebook
Se abrirá nuestro navegador (o una nueva pestaña si ya está abierto), cargando una página similar a la figura 2.1.
Figura 2.1.
Creación de un nuevo cuaderno o notebook dentro de Jupyter
Observaremos que un terminal (representado por un círculo) queda bloqueado. No debemos cerrarlo ni escribir nada en él mientras estamos trabajando, porque al hacerlo interrumpiríamos la ejecución de Jupyter. Tal y
2
Se puede descargar desde https://www.anaconda.com/download/
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
31
como señala la figura 2.1, elegiremos New y Python 3 para generar un nuevo
notebook, que es el lugar en el que escribiremos los programas:
Figura 2.2.
Ejecución de una casilla de código en Jupyter
Podemos probar a escribir print("Funciona") en la primera casilla, y a
continuación pulsar el botón play que aparece señalado por un círculo en la
figura 2.2. Al hacerlo, el texto Funciona aparecerá bajo la casilla, e inmediatamente se añadirá una nueva casilla al notebook para continuar. Cada casilla del
notebook puede contener varias instrucciones, incluso un programa entero, y
cada una puede ser seleccionada, simplemente situándonos en ella, y ejecutada de forma individual. En el caso de tratar con datos realmente grandes esto
presenta una gran ventaja, porque al corregir errores y mejorar el programa no
tendremos que volver a cargar los datos, o realizar estos cálculos complejos,
sino solo repetir la ejecución de la parte del código que hemos modificado.
El aprendizaje del lenguaje Python está más allá del objetivo de este libro
(requeriría un libro en sí mismo), pero explicaremos el código que vayamos
utilizando de forma que sea comprensible aún sin un estudio detallado del
lenguaje.
En general, podemos distinguir tres orígenes de datos en la web:
1. Redes sociales.
2. Datos incluidos en páginas web.
3. Ficheros disponibles para descarga en distintos formatos.
La siguiente tabla muestra las diferentes bibliotecas Python asociadas a
cada una de estas posibilidades:
32
CUADERNOS METODOLÓGICOS 60
Tabla 2.1.
Fuentes más comunes de datos y bibliotecas Python asociadas
Fuentes web de datos
Biblioteca Python
Redes sociales
Tweepy (Twitter), facebook-sdk, api-rest
Datos en la página web
BeautifulSoup, Selenium
Ficheros disponibles
Requests, CSV, openpyxl (Excel), tika
Las bibliotecas se importan en Python con la palabra reservada import
seguida del nombre de la biblioteca. De esta forma tan sencilla todas las funcionalidades de la biblioteca ya están disponibles en nuestro programa. Sin
embargo, en ocasiones puede que la biblioteca no se encuentre en nuestro
ordenador, como muestra la figura 2.3, en la que tras escribir import tweety
y pulsar el botón play se obtiene un error:
Figura 2.3.
Error de ejecución originado por la falta de una biblioteca
El error indica que no existe ningún módulo con ese nombre. Para descargar la biblioteca abriremos una nueva consola (Símbolo del Sistema en Windows) y teclearemos pip install tweepy. Tras unos instantes, la biblioteca
se habrá instalado y estará lista para utilizarse. Si ahora volvemos al notebook,
nos colocamos de nuevo sobre la casilla con el código import tweepy y volvemos a pulsar el botón play, no se obtendrá ningún mensaje de error, indicando que la biblioteca se ha incorporado sin problemas al programa.
2.2. Carga de ficheros
Veamos ahora cómo cargar y tratar ficheros de datos en los formatos más
usuales.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
33
2.2.1. Ficheros CSV
Cada vez más sitios web nos ofrecen ficheros listos para descargar en distintos
formatos. En este caso obtener el fichero es tan fácil como situar el ratón
sobre el fichero y proceder a la descarga. Sin embargo, los datos vienen en
diferentes formatos, que debemos conocer y ser capaces de tratar.
El formato CSV (Comma Separated Values) es muy sencillo: se trata de un
fichero de texto plano, donde cada entidad ocupa una fila. A su vez, cada fila
contiene todos los elementos que determinan la entidad separados por un
valor especial, normalmente una coma.
2.2.1.1. Ejemplo: renta per cápita en la OCDE
Por ejemplo, supongamos que queremos estudiar la evolución de la renta
per cápita en diversos países. Así, podemos acudir a la página oficial de la
Organización para la Cooperación y el Desarrollo Económicos (OCDE) 3. Allí
podremos descargar los datos tal y como muestra la figura 2.4.
Figura 2.4.
Datos de renta per cápita en la página de la OCDE
Elegiremos full indicator data y descargaremos el fichero, supongamos que
en la carpeta «C:\datos». Podemos abrirlo, por ejemplo, situando el ratón
sobre el fichero en la carpeta, pulsando el botón derecho y eligiendo Abrir con
y Bloc de Notas.
Veamos las primeras filas del fichero tal y como se nos muestran:
3
Disponible en: https://data.oecd.org/gdp/gross-domestic-product-gdp.htm
34
CUADERNOS METODOLÓGICOS 60
"LOCATION","INDICATOR","SUBJECT","MEASURE","FREQUENCY","TIME","Value","Flag Codes"
"AUS","GDP","TOT","MLN_USD","A","1960",25029.0336,
"AUS","GDP","TOT","MLN_USD","A","1961",25320.6787,
"AUS","GDP","TOT","MLN_USD","A","1962",27905.9382,
En este caso, el fichero tiene la renta per cápita anual de un país en un año
dado. La primera línea es la cabecera del fichero CSV. Es importante porque
nos indica el nombre de las columnas. Además, en esta fila vemos que en este
fichero el separador es la coma. Aunque podemos buscar en la propia página
una descripción de cada columna, a nosotros nos van a interesar sobre todo
tres columnas:
— LOCATION: nombre del país.
— MEASURE: el fichero incluye varias medidas, nosotros consideraremos
la renta per cápita en dólares americanos, que corresponde a cuando
esta columna toma el valor USD_CAP.
— TIME: año considerado.
— Value: renta per cápita del país en ese año.
El resto de las filas contendrán los datos. Por ejemplo, la primera fila de
datos (segunda del fichero) indica que Australia tuvo en 1960 una renta per
cápita de 25.029,0336 dólares. En cada fila tiene que haber tantas comas como
en la cabecera, si no obtendremos un error de formato. Si observamos el fichero
en detalle veremos que todas las filas de cada país vienen consecutivas, ordenadas por año e incluyendo datos desde 1960 hasta 2017, ambos inclusive.
Los ficheros CSV se pueden importar y manejar desde Excel. Sin embargo,
aquí vamos a ver cómo utilizarlos directamente desde Python, lo que nos permitirá programar funciones complejas. Python dispone de muchas bibliotecas para tratar con este formato, y en este ejemplo vamos a utilizar la conocida
como pandas. Se trata de una biblioteca muy potente ideal para el análisis
científico de datos, que ofrece una gran cantidad de posibilidades. Aquí vamos
a ver solo las más sencillas, animando al lector interesado en el análisis científico de datos a explorar pandas en detalle.
Supongamos que queremos encontrar el país cuya renta per cápita difiere
menos de la española desde 1970. Es decir, aquel país cuya media de distancias al cuadrado entre su renta per cápita y la española, año a año, es menor.
Empezamos importando la biblioteca pandas:
import pandas as pd
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
35
Recordamos que si esta línea da error es que la biblioteca no está instalada
y desde un símbolo del sistema debemos teclear pip install pandas. A
continuación, leemos el fichero de datos que hemos descargado en el apartado
anterior:
# fuente: https://data.oecd.org/gdp/gross-domestic-productgdp.htm
fichero = 'c:\datos\DP_LIVE_10072018172850717.csv'
ds = pd.read_CSV(fichero)
Hemos incluido un comentario (línea empezando con #) para indicar la
dirección de la que hemos obtenido el fichero. La ruta y el nombre del fichero
(variable fichero) pueden variar y habrá que adaptarlos a cada caso. Finalmente, leemos el fichero y almacenamos el contenido en la variable ds.
Ahora obtenemos la lista de países que incluye este conjunto de datos:
paises = ds.LOCATION.unique()
La notación ds.LOCATION se refiere a la columna de los países. Como
cada país aparece varias veces y queremos que la lista no tenga repeticiones,
añadimos la llamada a unique() al final.
El siguiente fragmento de código es quizá el más complicado:
esp = ds.loc[
(ds['LOCATION'] == 'ESP') &
(ds['MEASURE'] == 'USD_CAP') &
(ds['TIME'] >=1970)].Value
esp = esp.reset_index(drop=True)
La variable esp tendrá los datos de renta per cápita de España para el año
1970. Para ello el conjunto de datos se filtra a partir de tres condiciones:
— Que en la columna LOCATION se encuentre el valor ESP, es decir, que
sea un valor de España.
— Que en la columna MEASURE aparezca el valor USD_CAP para asegurar
que es un dato de renta per cápita en dólares.
— Que la fecha (TIME) sea posterior a 1970.
36
CUADERNOS METODOLÓGICOS 60
Las condiciones se unen con el operador de conjunción (&) para indicar
que las filas que pasen el filtro deben cumplir las tres condiciones simultáneamente. Finalmente, nos quedamos solo con el valor (columna Value) que
tiene la cantidad que queremos comparar con el resto de países. La última
instrucción:
esp = esp.reset_index(drop=True)
tiene un significado un poco técnico, pero es necesaria, porque al filtrar
pandas no solo incluye el valor que indicamos (columna Value), también
añade un índice, que es el número de fila que ocupaba la fila en el conjunto
de datos original. Esta instrucción asegura que este valor índice es eliminado.
paisM = None
for pais in paises:
if pais!='ESP':
otro = ds.loc[(ds['LOCATION'] == pais) &
(ds['MEASURE'] == 'USD_CAP') &
(ds['TIME'] >=1970)].Value
otro = otro.reset_index(drop=True)
if otro.size == esp.size:
mse = otro.sub(esp).pow(2).mean()
if paisM==None or mse < mejorMse:
paisM=pais
mejorMse=mse
print(paisM,mejorMse)
datosM = otro
Inicialmente declaramos una variable paisM, que será la que tendrá el
nombre del país cuya renta difiere menos de la española de media. El valor
None indica que la variable aún no tiene ningún valor útil, no tenemos ningún
país candidato. Para encontrar este candidato, recorremos la lista de todos los
países incluidos en el conjunto de datos mediante una instrucción for. La
variable pais tendrá en cada momento el nombre del país que estamos considerando.
Lo primero que se hace es comprobar que el país no es España, ya que está
en la lista y no queremos compararlo consigo mismo. A continuación, obtenemos en la variable otro los datos de renta per cápita de este país de forma análoga a como se hizo con España.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
37
La instrucción condicional if con condición otro.size == esp.size
tiene como propósito comprobar si para el país considerado se dispone de
tantos datos como en el caso de España. Este problema, el de los datos incompletos o perdidos, lo encontraremos a menudo en los casos reales y se suelen
utilizar dos métodos:
a)No considerar los datos incompletos.
b)Rellenar los datos que faltan, por ejemplo, con la media de los que sí se
tienen.
La decisión dependerá del caso concreto. En nuestro ejemplo, por simplicidad, tomamos la opción a). Finalmente, la instrucción siguiente calcula el
error medio:
mse = otro.sub(esp).pow(2).mean()
Para ello primero hace la resta de los valores año a año (sub). A continuación se elevan estos valores al cuadrado (pow(2)), y finalmente se obtiene la
media. La siguiente instrucción if comprueba si el valor obtenido es menor
que el más pequeño hasta ahora. Si es así, se apuntan el nombre del país en la
variable paisM y los valores de este país en datosM.
Finalmente, ya fuera del bucle, podemos añadir una última instrucción
para mostrar el resultado:
print(paisM,mejorMse)
Como curiosidad diremos que el país obtenido de esta forma es NZL, es
decir, Nueva Zelanda. Resulta curioso observar que el país con una renta
media más similar a la española, año a año, sea justo el país más alejado de
España de todo el planeta.
2.2.1.2. Visualización del resultado
Para asegurarnos de que el resultado obtenido tiene sentido, podemos dibujar
la gráfica de evolución de la renta per cápita en ambos países (España y Nueva
Zelanda). Recordemos que los datos están, respectivamente, en las variables
esp y datosM. Podemos usar la biblioteca matplotlib tal y como muestra el
siguiente código:
38
CUADERNOS METODOLÓGICOS 60
import matplotlib.pyplot as plt
plt.plot(range(1970,1970+esp.size),esp,dashes=[6, 2], label=
'España')
plt.plot(range(1970,1970+datosM.size),datosM, label='Nueva Zelanda')
plt.legend()
plt.show()
Como vemos se comienza por llamar dos veces a plot, que se encargará
de mostrar la gráfica de ambos países. Esta función recibe primero los valores
en el eje x, en este caso los años de 1970 hasta el último de la serie, después,
los del eje y, y, finalmente, algunos valores de formato. Por ejemplo, dashes=[6, 2], label='España' indica que se mostrará la gráfica con guiones de seis puntos de largo, y que la etiqueta será «España».
Tras los dos plot, solo queda poner las leyendas y mostrar el gráfico
(show). El resultado es el mostrado en el gráfico 2.1.
Gráfico 2.1.
Evolución de la renta per cápita en España y Nueva Zelanda
40.000
35.000
30.000
25.000
20.000
15.000
10.000
5.000
0
1970
1980
1990
España
Fuente: OCDE Data.
2000
Nueva Zelanda
2010
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
39
Podemos probar a incluir las gráficas de otros países y podremos comprobar visualmente que siempre se obtiene una diferencia mayor.
2.2.2. Ficheros XML
El término XML proviene del término en inglés eXtensible Markup Language
(lenguaje de marcado extensible). Se trata de un formato que nos permite
almacenar información anidada. Esto quiere decir que, mientras que en el
formato CSV cada dato de una fila contenía un solo dato atómico (un número,
una cadena de caracteres, una fecha…), en XML tendremos que cada «elemento», que es como se llamará aquí, puede contener datos complejos. Por
ejemplo, el elemento dirección, en lugar de tener directamente la dirección
como una cadena de caracteres, puede estar formado por los subelementos
calle, número y piso. A su vez, calle puede ser una cadena de caracteres,
número, un número entero, pero piso podría estar compuesto, a su vez, por
los elementos número de piso y letra.
En particular, en XML tenemos un elemento principal, que representa todo
el documento. Dentro de este elemento hay otros elementos que tienen distintos
fragmentos de la información, y así sucesivamente. Cada elemento viene delimitado por etiquetas <etiq> … </etiq>, donde «etiq» puede ser cualquier nombre que identifique al elemento. Entre la etiqueta de inicio y la de fin puede
venir información relevante, por ejemplo: <provincia>Madrid</provincia>.
Así, se indicaría que estamos leyendo datos correspondientes a la provincia
de Madrid. A menudo, las etiquetas de inicio incluyen atributos que lo particularizan, por ejemplo: <dia fecha="2018-07-08"> … </dia>.
De esta forma, se indica que el elemento contiene información para un día,
y, en particular, para el día 8 de julio de 2018.
2.2.2.1. Ejemplo: información meteorológica
Por ejemplo, supongamos que queremos trabajar con la información meteorológica de Madrid para los próximos siete días. La página de la Agencia Estatal de Meteorología (AEMET) 4 nos presenta esta información con el aspecto
indicado en la figura 2.5.
Podríamos utilizar la técnica de web scraping, descrita más adelante en
este mismo capítulo, para obtener la información deseada, pero la propia
página nos ofrece una alternativa más sencilla: descargar un fichero XML con
la información para analizarla posteriormente. Si hacemos clic sobre el botón
XML, veremos la información en el formato indicado por la figura 2.6.
4
Disponible en: http://www.aemet.es/es/eltiempo/prediccion/municipios/madrid-id28079
40
CUADERNOS METODOLÓGICOS 60
Figura 2.5.
Predicción metereológica en AEMET
Figura 2.6.
Fichero XML descargado de AEMET
-
<root id="28079" version="1.0" xsi:noNamespaceSchemaLocation="http://www.acmct.es/xsd/localidades.xsd">
+<origen></origen>
<elaborado>2018-07-08T09: 14:0 1</elaborado>
<nombre>Madrid</nombre>
<provincia>Madrid</provincia>
- <prediccion>
+<dia fecha="2018-07-08"></dia>
+<dia fecha="2018-07-09"></dia>
+<dia fecha="2018-07-10"></dia>
+<dia fecha="2018-07-11"></dia>
+<dia fecha="2018-07-12"></dia>
+<dia fecha="2018-07-13"></dia>
+<dia fecha="2018-07-14"></dia>
</prediccion>
</root>
El fichero consta de un elemento root que contiene todos los demás, que
son, en este caso, elementos como origen, que aparece contraído en la imagen, elaborado, nombre o provincia. Finalmente, el elemento predicción contiene la predicción para siete días. Dentro de cada día se muestra una gran
cantidad de información. En este ejemplo nos vamos a concentrar en el elemento hijo del elemento día (llamamos «hijo» de un elemento a cualquiera de
los subelementos que contiene) con nombre temperatura:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
41
<dia fecha=»2018-07-14»>
…
<temperatura>
<maxima>35</maxima>
<minima>21</minima>
…
</temperatura>
…
</dia>
Nuestro objetivo es acceder a esta información y procesarla. Por ejemplo,
podríamos intentar determinar la relación existente entre la temperatura y la
afluencia de público a locales comerciales, cuyo dato aproximado podemos
conocer analizando las emisiones de CO2 registradas en las estaciones
medioambientales más próximas a dichos lugares, y que habitualmente también se encuentran disponibles en la web de los ayuntamientos.
Aquí, debido a limitaciones de espacio, vamos a proponer un ejemplo
mucho más modesto: simplemente mostraremos una gráfica de temperaturas
máximas y mínimas para la semana.
2.2.2.2. Extracción de información a partir de ficheros XML
Existen numerosas bibliotecas en Python para tratar con ficheros XML, como
cElementTree o lxml, pero en este apartado vamos a ver, debido a su sencillez,
untangle. Recordemos que para instalarla debemos escribir desde un símbolo
del sistema (preferentemente abierto como administrador):
pip install untangle
Una vez hecho esto podemos emplear la librería como muestra el siguiente
código:
import untangle
import matplotlib.pyplot as plt
obj = untangle.parse('http://www.aemet.es/xml/municipios/
localidad_28079.xml')
l = obj.root.prediccion.dia
fechas = []
42
CUADERNOS METODOLÓGICOS 60
maximas = []
minimas = []
for dia in l:
fechas.append(dia['fecha'][8:])
maximas.append(int(dia.temperatura.maxima.cdata))
minimas.append(int(dia.temperatura.minima.cdata))
plt.plot(fechas,maximas)
plt.plot(fechas,minimas)
plt.show()
En primer lugar, se carga el fichero XML empleando untangle.parse. El
resultado queda almacenado en la variable obj. Hecho esto, el siguiente paso
es navegar el fichero, es decir la variable obj.
Para acceder al elemento más externo del fichero XML empleamos la notación obj.root. Sabemos, por el apartado anterior, que este elemento principal contiene, a su vez, un elemento prediction. Para acceder a él, añadimos
al «camino» el nombre del elemento, siempre tras un punto que se usa como
separador, obteniendo obj.root.prediction. A su vez, prediction
contiene varios elementos con nombre dia (en concreto, siete). Añadimos
una vez más el elemento dia, separando el nombre por un punto. El resultado, que recolecta las predicciones de todos los días, es el siguiente:
l = obj.root.prediccion.dia
que se lee como: «Dentro del XML, visitar al elemento root. Dentro de root,
buscar el elemento prediction, y dentro de prediction, los elementos
dia». Como hay varios elementos día, el resultado es que la variable l contendrá una lista. El bucle for recorre esta lista al objeto de extraer de cada día la
información relevante, que está formada por tres datos:
— La fecha, de la que nos quedamos solo con el día (la notación [8:] indica
que seleccionamos del octavo caracter en adelante, y justo eso es el día).
— La temperatura máxima.
— La temperatura mínima.
Obsérvese que para obtener la temperatura máxima y mínima nombramos
un elemento que, en realidad, no aparece en el XML, cdata: dia.temperatura.maxima.cdata.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
43
Este elemento «invisible» corresponde, en realidad, al texto asociado al elemento maxima. También es reseñable que para acceder a la fecha usamos la
notación entre corchetes: dia['fecha'].
Esto es así porque fecha no es un hijo de dia, sino un atributo (recordemos que el elemento tiene el aspecto <dia fecha="2018-07-14">). Por
tanto, en untangle separamos por un punto el nombre de los hijos, y accedemos mediante la notación entre corchetes a los atributos.
Cuando finaliza el bucle for, tenemos tres listas con los datos que queremos: fechas y temperaturas máximas y mínimas (nótese que es común no
poner acentos en los nombres de las variables porque en algunos lenguajes y
entornos pueden dar errores). Ahora solo nos queda representar gráficamente
el resultado. Esto lo hacemos empleando de nuevo la biblioteca
import matplotlib.pyplot as plt
que ya empleamos en la sección dedicada a ficheros CSV. Aunque la biblioteca nos ofrece multitud de posibilidades para mostrar datos de forma gráfica,
aquí nos limitamos a dibujar las dos gráficas: la primera, de fechas con respecto a temperaturas máximas, y la segunda, de fechas con respecto a mínimas. El resultado variará según la previsión de la AEMET, pero una posible
salida es la mostrada en el gráfico 2.2.
Gráfico 2.2.
Temperaturas máximas y mínimas en un intervalo de días
36
34
32
30
28
26
24
22
20
08
09
10
11
12
13
14
La línea superior representa las temperaturas máximas y la inferior, las
temperaturas mínimas para los siete días considerados.
44
CUADERNOS METODOLÓGICOS 60
2.2.3. Ficheros JSON
Otro formato muy común es el conocido como JSON. Al igual que XML, sirve
para representar información «anidada» o estructurada. Un archivo en formato JSON contiene en general muchos documentos JSON. Cada documento
es una estructura con la forma
{clave1: valor1, …, claven: valorn }
El documento especifica las claves que indican los nombres de los elementos del documento y sus valores asociados, que pueden ser los siguientes:
— Átomos: números, strings, booleanos, etc.
— Otro documento JSON con la misma forma { … }.
— Arrays de valores, encerrados entre corchetes [ … ].
A continuación se muestra un ejemplo de documento JSON:
{
"nombre":"Bertoldo",
"apellidos":"Pedralbes",
"edad":31,
"cuentas":[{"num":"001", "saldo":2000, "compartida":true},
{"num":"002", "saldo":100, "compartida":false}
],
"contacto":{ "email":"bertoldo@ucm.es",
"telfs":{"fijo":"913421234",
"movil":["5655555","444444"]}
}
}
Este documento, con los datos personales de un individuo, tiene cinco claves: nombre, apellidos, edad, cuentas (que representa sus cuentas bancarias) y contacto. Las tres primeras claves contienen datos atómicos (en
particular, cadenas de caracteres o strings). En cambio, cuentas es de tipo
array, es decir, una lista de elementos ordenados, con tantos elementos como
cuentas bancarias tenga la persona, y contacto es, a su vez, un documento
con los datos de contacto.
Conocer el formato JSON nos será muy útil para la parte dedicada a almacenamiento en MongoDB. De momento, en la sección siguiente, vamos a utilizar este formato para grabar datos recogidos desde Twitter.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
45
2.3. Datos desde redes sociales: Twitter
Las redes sociales constituyen una fuente inagotable de datos de interés para
el estudio de las ciencias sociales. El problema es cómo acceder a estos datos.
Conscientes del valor de los datos recopilados, frecuentemente las empresas
propietarias de las redes venden estos datos, debidamente anonimizados, a
menudo a precios muy elevados.
Sin embargo, estas mismas empresas también nos permiten en ocasiones,
aunque con limitaciones, el acceso gratuito a parte de estos datos mediante una
API adecuada. La palabra API (application program interface) se refiere a un conjunto de funcionalidades que nos permiten acceder a un servicio, en otras palabras, una forma de acceso. En esta sección vamos a limitarnos a la red Twitter,
tanto por su amplia utilización como por la facilidad de acceso que proporciona
con sus API. De todas formas, presentaremos los conceptos de forma general.
Vamos a distinguir entre dos formas de acceso, a menudo correspondientes a dos API.
Acceso offline: denominamos acceso offline al acceso a datos ya publicados
en la red social y almacenados en la base de datos. Esta opción suele estar
muy limitada. Por ejemplo, Twitter nos dejará acceder tan solo a los 3.200
últimos tweets de un usuario mediante su rest api, que es como denomina a su
API offline.
Acceso streaming u online: en esta opción no se accede a la base de datos
que almacena los mensajes de la red social, sino que se descargan los datos
según se publican. Si paramos el programa un momento, todos los mensajes
que se publiquen en ese intervalo se perderán (o solo estarán accesibles a través del acceso offline). En el caso de Twitter, la stream api nos permite acceder
al 0,1% de los tweets publicados en cada segundo de forma gratuita. Teniendo
en cuenta la ratio usual de unos 6.000-8.000 tweets/segundo en esta red, estamos hablando de que podremos descargar alrededor de 60-80 tweets por
segundo. Esto puede ser suficiente para realizar un análisis de numerosos
eventos o noticias que se comentan en esta red social, ya que nos permitirá
recolectar algo más de 200.000 tweets por minuto.
En este apartado vamos a ocuparnos del acceso en streaming a Twitter.
2.3.1. Códigos de acceso a Twitter
Para utilizar la librería tweepy necesitamos disponer de cuatro claves de
acceso o tokens. Estas claves están asociadas, en primer lugar, a una cuenta
de Twitter, y, en segundo lugar, a una aplicación que debemos crear, y permiten a Twitter saber quién está accediendo en cada momento a su API.
Por tanto, el primer paso es disponer de una cuenta de Twitter. A
continuación debemos solicitar una cuenta de desarrollador a Twitter en
46
CUADERNOS METODOLÓGICOS 60
https://developer.twitter.com/en/apply-for-access. Para ello la empresa nos
pedirá datos como qué tipo de información se va a descargar, con qué propósito, nuestros datos personales, etc. Tras un periodo de evaluación, si
nuestra propuesta es aprobada, Twitter nos concederá la cuenta de desarrollador, con la que ya podremos obtener las cuatro claves necesarias. En el
resto del apartado supondremos que hemos asignado estos valores a variables Python, con la forma:
import tweepy
import JSON
CONSUMER_TOKEN = "..."
CONSUMER_SECRET = "..."
ACCESS_TOKEN = "..."
ACCESS_TOKEN_SECRET = "..."
La biblioteca JSON la importamos porque vamos a grabar los tweets en
este formato.
2.3.2. La clase escucha
Esta parte del código es la más compleja por los conceptos avanzados que
implica. Una clase en Python es un contenedor genérico que agrupa código,
en forma de funciones, variables, etc. Estas clases luego se instancian para dar
lugar a variables de tipo objeto. Por poner un ejemplo que explica la diferencia, podemos, por ejemplo, pensar en una clase Coche que contiene como
variables para representar el número de matrícula, marca, etc., de un coche
genérico, y en objetos c1 y c2, cada uno dando unos valores a estas variables
para representar coches concretos.
En nuestro caso tenemos que escribir una clase que definirá el comportamiento de nuestro programa ante la llegada de un tweet nuevo. La clase parte
de una clase ya incluida en la biblioteca tweepy, llamada StreamListener,
y redefine el comportamiento de tres funciones:
class MyStreamListener(tweepy.StreamListener):
def __init__(self, api,fichero):
super().__init__(api)
self.fichero = fichero
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
47
def on_status(self, status):
with open(self.fichero, 'a+') as f:
f.write(JSON.dumps(status._JSON)+"\n")
def on_error(self, status_code):
print(status_code)
Las funciones son las siguientes:
— _init_: es la llamada constructora. Se utiliza para convertir la clase
genérica en un objeto concreto. El parámetro self, que veremos en
todas las funciones, representa el objeto en sí, y se pasa de forma
implícita, no tendremos que hacerlo nosotros. El segundo argumento,
api, contiene la conexión ya establecida con Twitter (ahora veremos
cómo se hace esto). El tercero es el fichero en el que grabaremos los
tweets, y se apunta como variable del objeto para que sea visible en el
resto de los métodos.
—o
n_status: la biblioteca llamará a esta función cada vez que surja un
nuevo tweet. Además del propio objeto (self), incluye todos los datos
del tweet en la variable status. En este caso lo que hacemos es abrir el
fichero de salida para añadir (parámetro a+ de open), de forma que
podamos añadir los tweets según lleguen.
—o
n_error: se llamará cuando se detecte un error, por ejemplo, que el
número de tweets que se producen supera el de los que se pueden descargar. Muestra el código de error.
2.3.3. Escucha de palabras clave
Una vez terminada la clase «escucha» ya podemos añadir (fuera de la
clase) el código que procederá a inicializar la recogida y grabación de los
tweets.
folder = 'C:/datos/'
file= 'tweets.txt'
myStreamListener = MyStreamListener(api,folder+file)
myStream = tweepy.Stream(auth = api.auth,listener=myStreamListener)
myStream.filter(track=terms, stall_warnings=True)
48
CUADERNOS METODOLÓGICOS 60
Las dos primeras líneas definen la carpeta y el fichero donde se grabarán
los tweets. Hemos utilizado la extensión «.txt» en lugar de «.JSON» porque
hace más fácil abrir el fichero con un editor normal, aunque el contenido
serán tweets en formato JSON, uno por línea.
A continuación, se define el array de términos que «escuchar». En este caso
solo hemos puesto una palabra, pero debemos escribir todas las que definan
el evento, entre comillas y separadas por comas.
terms = ['#FelizDomingo']
Las siguientes tres líneas utilizan los tokens definidos anteriormente para
acceder al api de Twitter.
auth = tweepy.OAuthHandler(CONSUMER_TOKEN, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)
Finalmente, iniciamos la escucha y filtramos por las palabras clave:
myStreamListener = MyStreamListener(api,folder+file)
myStream = tweepy.Stream(auth= api.auth,
listener=myStreamListener)
myStream.filter(track=terms, stall_warnings=True)
Primero se construye un objeto myStreamListener de la clase
MyStreamListener. A continuación, se inicializa el flujo (Stream) de
escucha. Finalmente se filtra por los términos. El parámetro sirve para obtener avisos si el cliente se va «quedando atrás» a la hora de recoger tweets.
2.3.4. Anatomía de un tweet
Los tweets, tal y como los devuelve tweepy, contienen mucha información, de
la que solo una pequeña parte será relevante. Para que nos hagamos una idea,
el tamaño medio de cada documento JSON asociado a cada tweet ocupa alrededor de los 7.000 caracteres. La figura 2.7 muestra algunas de las claves contenidas en este documento:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
49
Figura 2.7.
Parte de la estructura de un tweet en formato JSON
Como se puede ver, la primera clave (created_at) indica la fecha de creación del tweet. La segunda y la tercera son dos identificadores internos del
tweet. Seguidamente, la clave text tiene el propio texto del tweet.
Los siguientes valores que comienzan por in_reply… se utilizan cuando el
tweet es una respuesta a otro tweet, y tendrán los valores que permitan acceder a un tweet original. Análogamente, encontraremos valores quoted_status…, que tendrán los valores originales cuando el tweet sea una cita. En
particular, quoted_status solo existe si el tweet es una cita. También existe una
clave retweeted_status que indica cuando el tweet es un retweet de otro tweet.
A continuación, vienen los datos del usuario dentro de la clave user. Esta clave
tiene, a su vez, dentro un documento con muchas claves. En la url https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object.HTML
tenemos una descripción pormenorizada de cada clave. Aquí destacamos:
— id: identificador único del usuario.
— name: el nombre que ha introducido el propio usuario.
— screen_name: el nombre que aparece en sus tweets y que pueden usar
otros usuarios para dirigirse a él precediéndolo por «@».
— location: no es la localización real, ni se detecta automáticamente, sino
que tiene un valor que escribió el usuario manualmente. Podemos encontrar «Móstoles», «Madrid» o «La Galaxia». Sin embargo, es a menudo un
buen indicador de la localización del usuario.
— verified: valdrá true si es una cuenta verificada, false en otro caso. Las
cuentas verificadas suelen corresponder a personas u organizaciones
públicas.
50
CUADERNOS METODOLÓGICOS 60
— followers_count: número de seguidores.
— friends_count: número de personas a las que sigue.
— favourites_count: número de veces que ha dado like hasta el momento de
emisión de este tweet.
— statuses_count: número de tweets emitidos en total, incluyendo retweets.
— created_at: fecha de creación de la cuenta.
— lang: lenguaje elegido por el usuario.
— geo_enabled: indica si el usuario tiene activada la geolocalización. Aunque esté en true, Twitter limita el número de geolocalizaciones que
podemos obtener a un número aleatorio muy pequeño. Si queremos
saber si tenemos la geolocalización del tweet debemos comprobar la
clave coordinates del documento principal.
2.3.5. Lectura y tratamiento de tweets
Vamos a ver cómo leer el fichero generado en el apartado anterior. Como
ejemplo práctico, supongamos que queremos detectar el número de tweets
originales en la conversación. Entendemos como tweets originales aquellos
que no son ni retweets, ni réplicas, ni citas de otros tweets.
Para empezar, importamos la biblioteca JSON, que nos permitirá tratar
este formato, y declaramos la carpeta y el nombre del fichero con los tweets.
import JSON
folder = 'C:/datos/'
file = 'tweets.txt'
A continuación, vamos a crear un diccionario Python que contendrá los
contadores de citas, réplicas y retweets:
keys = ['quoted_status','in_reply_to_status_id','retweeted_
status']
dict = {key: 0 for key in keys}
La variable dict tendrá inicialmente el valor 0 para sus tres claves. Como se
ve, hemos elegido como nombre de cada clave un campo del objeto tweet que
nos permite comprobar si el tweet es de ese tipo. También declaramos una
variable para contar el número total de tweets, por el que dividiremos al final
para obtener la proporción:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
51
all = 0
Ahora, finalmente, abrimos el fichero y procesamos cada línea.
with open(folder+file) as tweets:
for line in tweets:
if len(line)>=2:
tweet = JSON.loads(line)
all +=1
for key in keys:
if key in tweet and tweet[key]!=None:
dict[key] +=1
print("Total tweets: ", all,
"Proporción tweets originales: ",1-sum(dict.values())/all)
La instrucción condicional if se utiliza para descartar posibles líneas
vacías. Una vez convertido el tweet en formato JSON en tweet (inicialmente
es leído como una cadena de caracteres en la variable line) e incrementado
el total de tweets (variable all), se recorren todas las claves en keys con un
bucle for. Si alguna de las claves aparece en el tweet con un valor distinto de
None, es que el tweet tiene esa forma, ya sea la clave la de réplica, cita o
retweet, y se incrementa el correspondiente contador en uno.
Al final, se muestra el resultado de sumar todos los valores correspondientes a otros tweets y dividirlos entre el total. Como esa sería la proporción de
los que no son originales y queremos la de los que sí lo son, mostramos realmente el valor uno menos esa cantidad.
Una suposición implícita a este código, y que se verifica en el caso de los
tweets, es que cada tweet puede ser original, cita, réplica o retweet, pero solo
una de estas cosas, en otro caso el bucle for que incrementa los valores del
diccionario sumaría uno en más de una clave y esto haría que se obtuviera un
valor final erróneo. Una optimización obvia, pero que evitamos para mejorar
la claridad del código, sería cambiar el bucle for por un bucle condicional
while que abandonara la iteración tan pronto como se encontrara que se
sumara uno a cualquiera de las claves.
Ejecutando este código sobre un ejemplo se tiene el resultado:
Total tweets: 7681 Proporción tweets originales: 0,25842989
52
CUADERNOS METODOLÓGICOS 60
La proporción de tweets originales (25,8%) puede parecer muy baja,
pero es habitual en Twitter; la mayoría de los usuarios simplemente repiten tweets, contestan o citan a otros, y muy pocos emiten contenido original. En otros conjuntos de tweets, por ejemplo, aquellos relacionados con
el debate político, la proporción de tweets originales es todavía menor,
alcanzando a menudo el 5%. Por ello, la detección de los usuarios influyentes, tema que veremos posteriormente, cobra especial interés en esta red
social.
2.4. Web scraping
Se denomina web scraping a la aplicación de técnicas que, de forma automática, permiten la extracción de datos e información de una página web. Para
extraer los datos, necesitaremos conocer la estructura de la página web, que
normalmente está compuesta por código HTML, imágenes, scripts y ficheros
de estilo. La descripción detallada de todos estos componentes está más allá
del propósito de este libro, pero vamos a ver los conceptos básicos que nos
permitan realizar nuestro objetivo: hacer web scraping.
2.4.1. HTML
Cuando cargamos una página web en un navegador como Firefox, Explorer o
Chrome, el navegador lo que hace es descargarse un fichero de texto que le da
instrucciones de cómo debe mostrarnos la página. Decimos que este fichero
contiene el código HTML de la página. Veamos un ejemplo de una página
mínima en HTML:
<!DOCTYPE HTML>
<HTML>
<head>
<title>
Un ejemplo sencillo de página
</title>
</head>
<body>
<div id="date"> Fecha 25/03/2035 </div>
<div id="content"> Un poco de texto </div>
</body>
</HTML>
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
53
Al comenzar el fichero, en la primera línea, encontramos el texto <!DOCTYPE HTML> indicando que el fichero contiene un documento HTML, el
estándar de las páginas web. A continuación, encontramos <HTML>, la etiqueta que marca el comienzo del documento, y que se cerrará al final del
documento con </HTML>. En HTML, los elementos, que llamaremos a
menudo tags, tienen una estructura con la forma:
<elemento atributo="valor">Contenido</elemento>
El atributo no es obligatorio, y un mismo elemento puede incluir más
de uno. Dentro del documento HTML, esto es, entre la apertura <HTML> y
el cierre </HTML> del elemento principal, encontramos siempre dos elementos:
<head> … </head>: describe la cabecera del documento, como puede ser
su título, el autor, etc.
<body> … </body>: es el contenido en sí de la página, que es lo que nosotros deseamos examinar.
De manera análoga a como sucedía con los ficheros XML, dentro del elemento body encontraremos, a su vez, otros elementos que serán los que
configuran la página, que, a su vez, pueden contener otros elementos, y así
sucesivamente. Podemos imaginar la apertura de cada elemento como un
paréntesis de apertura, que debe ser cerrado para que todo tenga sentido.
En nuestro pequeño ejemplo el cuerpo solo tiene dos frases, marcadas por
las etiquetas <div> y </div>. Podemos escribir con un editor de textos
este código en un fichero, grabarlo con nombre ejemplo.HTML y hacer doble
clic sobre el fichero resultante. Se abrirá el navegador y veremos la (humilde)
página.
Por tanto, para poder extraer datos de una página, lo primero es analizar
su estructura HTML, y localizar los elementos que incluyen la información
que buscamos. A menudo, estos elementos serán tablas HTML, por lo que
aquí vamos a describir brevemente su estructura, que, además, es una de las
más complejas que podemos encontrar.
Las tablas aparecen entre los tags <table> … </table>, y dentro una
estructura de dos niveles. En el primer nivel encontramos las filas, delimitadas por los tags <tr> … </tr>. A su vez, dentro de cada fila encontramos el
nivel de celda o casilla, y viene delimitado por <td> … </td>, o en el caso de
ser las celdas de cabecera, por <th> … </th>.
Un ejemplo nos ayudará a entender mejor esta estructura:
54
<table>
<tr>
<td>fila
<td>fila
</tr>
<tr>
<td>fila
<td>fila
</tr>
</table>
CUADERNOS METODOLÓGICOS 60
1, columna 1</td>
1, columna 2</td>
2, columna 1</td>
2, columna 2</td>
Los espacios en blanco, saltos de línea, indentaciones, etc., son irrelevantes, no se tienen en cuenta en HTML. Esta tabla se mostrará en la página web
como:
fila 1, columna 1
fila 1, columna 2
fila 2, columna 1
fila 2, columna 2
En ocasiones encontraremos otros elementos, por ejemplo, es habitual que
las filas de la tabla se encuentren dentro de elementos <tbody> … </tbody>,
o que haya al principio un elemento <thead> … </thead> indicando la
estructura de la primera fila (cabecera).
2.4.2. Captura de datos con BeautifulSoup
Supongamos que estamos interesados en la llegada de vuelos desde la ciudad
alemana de Hamburgo hasta Palma de Mallorca: conocer el ritmo al que se
suceden los vuelos, fechas de máxima afluencia, etc. Inicialmente podemos ir
a la página de llegadas del Aeropuerto de Palma de Mallorca:
https://www.aeropuertos.net/aeropuerto-de-palma-de-mallorca-llegadas-de-vuelos/
Allí vemos que se muestra una tabla con las llegadas. Sin embargo, y esto
sucederá a menudo, la tabla corresponde a una dirección web diferente que
está «empotrada» dentro de la página principal. Para ver la dirección «real»,
usaremos las posibilidades de nuestro navegador. En general, para web
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
55
scraping, recomendamos usar Chrome o Firefox en lugar del navegador por
defecto de Windows (Edge/Explorer), porque este último complica mucho
el acceso al código HTML.
— Firefox: movemos el ratón a la tabla, sobre un texto que no contenga un
enlace (por ejemplo, el nombre de una ciudad de origen), pulsamos el
botón derecho y elegimos la opción Este marco, y, a continuación, Mostrar solo este marco. La dirección que aparece en el navegador es la
dirección real que contiene la tabla.
— Chrome: movemos el ratón a la tabla, sobre un texto que no contenga un
enlace (por ejemplo, el nombre de una ciudad de origen), pulsamos el
botón derecho y elegimos la opción Ver fuente del marco. La dirección
real es la que se muestra en la barra de direcciones quitando el prefijo
view-source.
La dirección es ciertamente compleja, porque incluye todos los parámetros que permiten mostrar la información para el aeropuerto indicado y en
la hora indicada. Será algo como:
https://www.flightstats.com/go/weblet?guid=c228b59beca1b817:-64e0c4c7:
1117f1ad394:-3b36&weblet=status&action=AirportFlightStatus&airportCode=
PMI&airportQueryType=1&language=Spanish
2.4.2.1. Descarga del fichero
El primer paso va a ser descargar el fichero HTML a nuestro disco duro para
analizarlo a continuación y extraer la información deseada. Esto lo logra el
siguiente código Python:
import requests
url = "https://www.flighstats.com/go……"
resp = requests.get(url)
print(resp)
path = "C:/bertoldo/datos/"
with open(path+'salida.txt', 'wb') as output:
output.write(resp.content)
Vamos a explicar el significado de este código, línea a línea:
—i
mport requests: cargamos la biblioteca requests, que emplearemos para «bajar» la página a nuestro ordenador.
56
CUADERNOS METODOLÓGICOS 60
—u
rl = "…": declaramos una variable url que contendrá la dirección real
de la página entre comillas. No copiamos en el código la dirección completa por exceso de longitud, en el código real sí debemos incluirla, sustituyendo los puntos suspensivos.
— resp = requests.get(url): aquí se procede a leer la página, mediante
el método get de la biblioteca requests. A este método se le pasa como
parámetro la variable url, o, lo que es lo mismo, la dirección de la página a
descargar. El resultado se guarda en la variable resp.
— print(resp): aquí se muestra el estatus de la descarga. Es un número
que indica si la descarga se ha podido llevar a cabo sin problemas. Para
nuestros efectos, baste con señalar que los que empiezan por 2 indican
que todo ha ido bien, mientras que los que empiezan por valores 4 o 5
indican un error en la descarga. Aquí, si todo ha ido bien, el programa
debe mostrar un valor 200, indicando descarga correcta.
—p
ath = "C:/bertoldo/datos/": declara el lugar donde se grabará
el fichero. Debemos sustituirlo por el nombre de una carpeta de nuestro
ordenador que ya exista. Debemos mantener la barra de dividir «/» para
separar los nombres de carpeta aunque estemos en Windows, que usualmente emplea la barra contraria («\»); en Python, siempre, sin importar
el sistema operativo, utilizaremos «/».
— with open(path+'salida.txt', 'wb') as output: es quizá la
instrucción más complicada de este código, y aunque ya la hemos utilizado anteriormente, conviene explicarla en detalle. La palabra reservada with indica en Python que vamos a usar un recurso (un fichero,
una impresora…). A continuación, se indica lo que hay que hacer con
el recurso, que es abrirlo: open. La llamada a open tiene dos argumentos. El primero, el fichero que deseamos abrir. En este caso indicamos que vamos a abrir el fichero «salida.txt» en el camino indicado
por la variable path, que declaramos anteriormente. El segundo
argumento indica el modo de apertura del fichero. En este caso ‘wb’
indica que lo abrimos para escritura. El fragmento de código as
output indica que en el código que sigue el fichero se mencionará
con el nombre output. Finalmente, merece la pena mencionar los
dos puntos del final. En Python, el símbolo «:» indica que comienza
un bloque, es decir, un grupo de instrucciones, en este caso asociadas
a la apertura del fichero. El bloque se distinguirá por la indentación;
todas las instrucciones de las líneas siguientes que aparezcan indentadas se supondrán parte del bloque, y la primera que no lo haga indicará que el bloque ha terminado.
— output.write(resp.content): como vemos, esta instrucción aparece indentada, indicando que estamos dentro del bloque with. Indica
que se debe escribir en el fichero output (output.write) el contenido de la página HTML que acabamos de descargar.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
57
Si todo funciona correctamente, obtendremos un fichero salida.txt en la
carpeta path. Este fichero contiene la información que buscamos, pero embebida dentro del código HTML. Nuestro objetivo ahora es extraer la información buscada.
2.4.2.2. Extraer la información
Sabemos que la información que deseamos está en el fichero que hemos descargado, que tiene la estructura de un archivo HTML. Para localizarla utilizaremos
la biblioteca BeautifulSoup. El primer paso es saber dónde se encuentra esta
información, es decir, asociada a qué elementos HTLML se encuentra. Una
posibilidad es simplemente inspeccionar el fichero que acabamos de descargar
(lo hemos grabado con extensión .txt para que pueda abrirse en cualquier editor
de texto), y buscar los elementos que rodean la información buscada. Sin
embargo, a menudo los ficheros son demasiado extensos, y su estructura, demasiado compleja, con elementos dentro de elementos, y así sucesivamente.
Afortunadamente, navegadores como Chrome y Firefox nos facilitan la
tarea. Basta con que, de nuevo, nos situemos encima de algún elemento de
texto de la tabla que queremos examinar, pulsemos el botón derecho y escojamos Inspeccionar (o Inspeccionar elemento en Firefox). La pantalla se dividirá
en dos, mostrando el código HTML correspondiente al elemento sobre el que
estamos, tal y como muestra la figura 2.8.
Figura 2.8.
Aspecto de una página en Chrome tras pulsar Inspeccionar
El código HTML nos muestra una estructura tipo tabla como la que hemos
indicado más arriba, con un elemento table, dentro, un elemento tbody
58
CUADERNOS METODOLÓGICOS 60
para el cuerpo, y dentro, las filas de la tabla en elementos tr. A su vez, cada
elemento tr tiene dentro las cinco columnas, cada una con su contenido dentro de un elemento td.
Comenzamos por cargar la página y pasarla al formato requerido por la
biblioteca.
with open(path+'salida.txt', "r") as f:
page = f.read()
from bs4 import BeautifulSoup
soup = BeautifulSoup(page, "html.parser")
Inicialmente cargamos el fichero sobre la variable page. Esto se hace
abriendo para lectura el fichero que hemos escrito antes (recordemos que path
es un cambio dentro de nuestro ordenador a una carpeta que contiene un documento HTML bajo el nombre «salida.txt»). A continuación importamos la
biblioteca, y hacemos que la página se convierta en el formato que maneja
mediante BeautifulSoup(page, "HTML.parser"). El primer argumento
de esta llamada es la propia página que acabamos de cargar, y el segundo indica
que se trata de una página que debe ser analizada como HTML. El resultado, la
página, ya lista para ser «navegada», queda en la variable soup.
A partir de aquí buscamos la tabla:
tabla = soup.find("table",{"class": "tableListingTable"})
print(tabla.prettify())
La primera instrucción utiliza el método find para buscar elementos de
tipo table, que tengan un atributo class con valor tableListingTable.
Seguidamente, se muestra la tabla. El método prettify() sirve para mostrarla en un formato legible. El resultado debe ser la tabla que buscamos.
Hay que notar que el método find encuentra el primer elemento de las
características buscadas, en este caso, la tabla con el atributo class indicado. En nuestro ejemplo esto es suficiente, porque solo hay una tabla con
estas características, pero en otras ocasiones habrá que emplear el método
find_all. Por ejemplo, podemos utilizar este método para obtener todas las
filas de la tabla:
trs = tabla.find_all("tr")
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
59
En este caso, trs será una variable con una lista de filas HTML. Podemos
iterar estas filas, buscando aquellas que correspondan a aviones con origen en
Hamburgo.
for fila in trs:
tds = fila.find_all("td")
if 'Hamburg' in tds[2].get_text():
print(tds[0].get_text().strip(),
tds[1].get_text().strip(),
tds[3].get_text().strip())
El bucle for va seleccionando cada fila. Dentro de la fila buscamos sus celdas, es decir, sus elementos td. Dado que el origen del vuelo es la tercera
columna, buscamos aquellas celdas que contengan Hamburg en la posición 2.
Este es un detalle típico de lenguajes como Python, C, C++ o Java que hay que
tener en cuenta: los valores empiezan a contar desde 0, por eso a la tercera fila
se accede con el índice 2. El método get_text() empleado permite acceder
al texto del elemento HTML. Finalmente, si la fila tiene Hamburg en su tercera
columna, mostramos el texto asociado al número de vuelo, la compañía y la
hora de llegada prevista. Utilizamos el método strip() tras cada llamada a
get_text() para quitar los espacios en blanco que a menudo rodean los
valores y dificultan la legibilidad de la salida.
El código mostrará como salida líneas de la forma:
DE 1542 Condor 6:10 PM
Y si lo ejecutamos a menudo nos permitirá ir registrando, de forma automática, todos los vuelos que llegan desde Hamburgo a Palma de Mallorca.
Para finalizar este apartado debemos señalar que, aunque BeautifulSoup
es una excelente librería, tiene algunas limitaciones que conviene conocer. La
más importante es que no permite interaccionar de forma dinámica con la
página. Un caso típico sería una página que nos requiere el nombre de usuario
y la palabra clave antes de entrar. Otro caso común es que tengamos que
seleccionar información inicial, como la página del CIS, que permite seleccionar estudios para un año dado, y que podemos ver en la figura 2.9.
En este ejemplo, para acceder a los datos debemos hacer clic con el ratón
sobre el año deseado antes de acceder a la página con los datos. Pero BeautifulSoup no nos permite hacer esto. Si queremos automatizar el proceso
60
CUADERNOS METODOLÓGICOS 60
de introducción de datos deberemos utilizar una biblioteca Python diferente,
como Selenium, que permita interaccionar con la página.
Figura 2.9.
Un ejemplo de página que requiere entrada del usuario
3
Almacenamiento de datos
Almacenar los datos en ficheros con formato CSV o JSON, como hemos visto
en el capítulo anterior, o incluso en formato Excel, es una posibilidad sencilla
y que da buenos resultados cuando se trata de unos pocos datos. Sin embargo,
para cantidades de datos de tamaño medio, así como para datos o cálculos
que requieran cierta complejidad, necesitaremos emplear un sistema gestor
de bases de datos.
En este capítulo vamos a discutir distintas posibilidades para el alojamiento de los datos. Comenzamos discutiendo qué criterios debemos manejar
para decantarnos entre mantener los datos en nuestro propio ordenador o
hacerlo en la nube. A continuación, haremos un repaso rápido de las bases de
datos relacionales, las cuales continúan siendo la opción preferida en muchas
situaciones. Finalmente, y ya en el mundo de las bases de datos no relacionales, también conocidas como bases de datos NoSQL, nos centraremos en el
estudio detallado de las posibilidades de MongoDB, sistema capaz de almacenar grandes volúmenes de datos sin apenas perder eficiencia.
3.1. Almacenamiento local versus cloud
Uno de los primeros factores que considerar al tratar datos en el contexto de
big data es si vamos a necesitar o no un clúster de ordenadores para manejar
dichos datos. Para decantarnos por una u otra opción debemos tener en
cuenta dos factores.
El primer factor es, evidentemente, el volumen total de datos que puede ser
almacenado en un solo ordenador. Las posibilidades de los equipos de sobremesa en cuanto almacenamiento permiten alcanzar fácilmente los 16 tera­
bytes, es decir 1,6 billones de bytes. Esta cantidad suele ser suficiente para
almacenar la mayoría de los datos requeridos por los estudios e investigaciones en ciencias sociales. Si el volumen de datos sobrepasa estos límites, aún se
pueden emplear discos externos, por ejemplo, mediante la tecnología network
62
CUADERNOS METODOLÓGICOS 60
attached storage (NAS), que nos permite situar discos externos de gran capacidad como parte de una red. Esta estrategia tiene como ventaja adicional que
los datos resultan accesibles desde cualquier ordenador situado en la red. Sin
embargo, si la cantidad de datos supera también la ofrecida por estas soluciones, o si esperamos que crezca de forma continuada sobrepasando estos límites, entonces sí deberemos recurrir a un clúster de ordenadores, lo que
normalmente implica emplear alojamiento en la nube.
Un segundo factor central es la complejidad de los cómputos. En ocasiones
tendremos que realizar cálculos que conllevan un coste en tiempo muy elevado, pero que pueden ser paralelizados, es decir, que se pueden descomponer en cómputos locales que se ejecutan a la vez. En este caso, repartir los
datos entre diversos ordenadores, que cada uno aplique los cálculos a sus
datos localmente y luego unificarlos puede ser ventajoso.
Por lo tanto, cuando el espacio de nuestros equipos es insuficiente y/o los
tiempos de ejecución de las tareas que estamos realizando son demasiado altos,
debemos plantearnos repartir nuestros datos entre un clúster de ordenadores.
Por supuesto, solo organizaciones de tamaño mediano/grande son capaces de
adquirir, configurar y mantener un clúster de ordenadores para su propio uso.
Cuando esto no es posible, lo habitual es almacenar los datos en «la nube», es
decir, alquilar el uso de un clúster a alguna de las compañías que ofrecen esta
posibilidad, como Amazon, Google o Microsoft. El inconveniente de este tipo
de alojamiento es de carácter económico, especialmente si planeamos tener los
datos alojados durante largo tiempo. Por el contrario, la configuración de la
base de datos en remoto, que hasta hace poco era muy complicada, resulta hoy
en día sumamente sencilla y es, cada vez más, la opción más adoptada.
Aparte de la decisión de dónde alojaremos los datos, deberemos definir qué
tipo de gestor de bases de datos utilizaremos: uno relacional, basado en el lenguaje de consultas SQL, o uno no relacional, también conocido como NoSQL.
Para poder escoger entre uno u otro debemos conocer sus características
principales, a lo que dedicamos el resto del capítulo. Empecemos por ver las
posibilidades de las bases de datos relacionales.
3.2. Bases de datos relacionales
Como hemos mencionado anteriormente, las bases de datos relacionales,
también conocidas como bases de datos SQL en referencia a su lenguaje de
consultas, no son las más adecuadas para el tratamiento de conjuntos de datos
realmente grandes. Esto es debido a la dificultad que encuentran este tipo de
bases de datos para implementar el escalado horizontal, el volumen de big
data. Por tanto, si hemos decidido que nuestros datos son tan voluminosos, o
se espera que lo sean, como para necesitar un clúster de ordenadores, posiblemente prefiramos orientarnos hacia las bases de datos no relaciones.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
63
Otro problema añadido es, como veremos a continuación, que la estructura, o esquema, de cada tabla en SQL debe ser fijada de antemano, lo que
entra en conflicto con el concepto de variedad de big data.
Sin embargo, en el caso de conjuntos de datos relativamente pequeños
(almacenables en un solo ordenador), y con una estructura fija, las bases de
datos relacionales siguen siendo una solución de almacenamiento muy común.
Pensemos que el 90% de las bases de datos que encontramos en empresas hoy
en día siguen siendo de este tipo, lo que prueba que se trata de un paradigma
que ha demostrado su eficacia y continúa plenamente vigente. Las grandes
compañías, como Oracle, ofrecen sus gestores de bases de datos de excelente
rendimiento, aunque a un precio considerable. Sin embargo, si lo que buscamos es iniciarnos en este mundo, tanto estas mismas compañías como otras de
software libre ofrecen versiones gratuitas como MySQL que nos pueden servir
para mantener nuestros datos en casos de uso de tamaño pequeño o mediano.
Por ejemplo, en este capítulo vamos a utilizar PostgreSQL, una base de
datos potente y fácil de aprender. Para instalarla basta con acceder a
https://www.PostgreSQL.org/ y seguir las instrucciones. Al hacerlo nos preguntará por una palabra clave, la del usuario root, o usuario principal, que
debemos anotar para usos posteriores. Puede que también nos pregunte si
queremos instalar software adicional, a lo que podemos contestar que no, ya
que para nuestros propósitos es suficiente con la instalación básica.
Si preferimos no instalar ninguna base de datos de momento y solo pretendemos practicar con los ejemplos de este capítulo, podemos utilizar algunas
de las páginas online que nos permiten probar pequeños ejemplos, como
http://SQLfiddle.com/.
3.2.1. Preparando la base de datos
Una vez instalado PostgreSQL, podemos buscar en nuestro menú de inicio de
Windows pgadmin la aplicación que permite gestionar la base de datos. Una
vez abierta esta aplicación pulsaremos sobre el servidor PostgreSQL para
conectarnos a él. El servidor es el programa que interactúa entre los propios
datos y el cliente, que en este caso somos nosotros. Para activar el cliente, el
programa nos pedirá una palabra clave, la misma que introdujimos durante la
instalación, tal y como muestra la figura 3.1.
Podemos grabar la clave (Save password) si estamos en un ordenador de
uso personal. Una vez hecho esto, ya tenemos acceso al servidor de datos. Nos
aparecerán nuevos símbolos, entre otros, el de bases de datos (Databases).
Conviene que para cada aplicación concreta creemos una nueva base de datos,
evitando la mezcla de datos no relacionados. Para ello nos situamos con el
ratón sobre la palabra Databases, pulsamos el botón derecho del ratón y elegimos Create, como muestra la figura 3.2.
64
CUADERNOS METODOLÓGICOS 60
Figura 3.1.
Palabra clave en PostgreSQL
Figura 3.2.
Creación de una base de datos en PostgreSQL
El mismo efecto podemos lograrlo en el menú superior, eligiendo las opciones Object + Create + Database. En cualquier caso, aparecerá un cuadro de diálogo que nos solicitará el nombre de la base de datos. Supongamos que queremos
almacenar datos de Twitter, y que, por tanto, llamamos a la base de datos Twitter. Como propietario (owner) dejamos el usuario por defecto postgreSQL.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
65
El manejo de usuarios tiene gran flexibilidad en SQL; nos permite crear
usuarios que pueden ver ciertas tablas y otras no, o solo consultar, pero no
modificar, etc. Aunque queda fuera del propósito de este este texto, si utilizamos SQL a menudo o formamos parte de un equipo amplio, puede que en
algún momento merezca la pena profundizar en el tema de la gestión de los
permisos y formas de acceso a la información.
En nuestro ejemplo, simplemente introducimos Twitter como nombre de
la base de datos y pulsamos Save.
3.2.2. Creación de tablas
El primer paso para trabajar en el modelo relacional es diseñar las tablas en
las que almacenaremos los datos, lo que se conoce como «esquema» de la
base de datos. En principio, podemos imaginarnos una tabla como algo similar a una hoja Excel, donde cada columna tiene un nombre y alojará datos de
un tipo concreto. El diseño de tablas equivaldría entonces a decidir cuántas
hojas necesitaremos (cuántas «tablas», en el argot SQL), y qué columnas
(también conocidas como «atributos») tendrá cada una. Para definir una
columna necesitaremos indicar su nombre, su tipo y, en ocasiones, restricciones adicionales como la de no contener valores repetidos.
Pensemos, por ejemplo, que queremos grabar los datos de un tweet. Para ello
podríamos tener una tabla usuarios con columnas userID, screen_name y created_at. El userID corresponde al identificador interno que tiene cada usuario en
Twitter. El atributo o columna screen_name es el nombre con el que se identifica
en Twitter el usuario, el que se muestra en pantalla y emplean otros usuarios
para referirse a él. Finalmente, la fecha de creación del usuario (created_at) será
un valor que indica el momento en que el usuario creó su cuenta en Twitter.
Cada usuario corresponderá entonces con una fila de esta tabla. El conjunto de filas irá cambiando de forma dinámica, según añadamos, borremos
o modifiquemos los datos de los usuarios. Lo que no podrá cambiar es el
número de columnas, su nombre de cabecera, ni su tipo.
Si además queremos añadir el contenido de los tweets de cada usuario,
podemos pensar en cambiar el diseño, antes de empezar a trabajar con la
tabla, añadiendo una columna llamada text. Aunque este nuevo esquema es
válido, presenta inconvenientes serios, debido a que genera datos redundantes. En efecto, si tenemos dos o más tweets del mismo usuario, algo muy normal, tendremos que los valores de las columnas userID, screen_name y
created_at se repitirán de forma innecesaria en cada fila correspondiente a distintos tweets del mismo usuario.
Junto con el gasto de espacio que esto pueda suponer, el modelo relacional
propuesto por Edgar F. Codd (1970) desaconseja la repetición de datos ya que
puede dar lugar a las temidas inconsistencias. Por ejemplo, supongamos que,
66
CUADERNOS METODOLÓGICOS 60
debido a un error, hemos introducido mal la fecha de creación (created_at) y
en su lugar hemos incluido la fecha en la que se emite el tweet. Corregir este
error no supone ningún problema, siempre y cuando conozcamos el dato
correcto. No obstante, debemos ser muy cuidadosos, ya que bastaría con dejar
alguna copia del dato erróneo para generar una inconsistencia muy difícil de
corregir a posteriori. Para evitar esta situación, el modelo relacional sugiere
dividir esta tabla inicial en dos tablas.
Tabla tweets
Tabla usuarios
ID
Text
userID
userID
screen_name
created_at
1
«En ...»
1
1
berto_98
01/10/2012
2
«Yo…»
1
2
herminia
23/07/2015
3
«No…»
2
3
amadeus
01/01/2018
4
«Si …»
3
La primera tabla contiene los datos de los tweets, mientras que la segunda
tabla mantiene los datos de los usuarios. Sigue habiendo una pequeña redundancia, el userID se repetirá para varios tweets del mismo usuario, pero hemos
reducido al mínimo las posibles inconsistencias. Si, como mencionábamos
antes, descubrimos un valor de created_at erróneo, solo deberemos cambiar el
valor en una fila, ya que ahora estos valores no se repiten.
Más aún, la base de datos nos permitirá tratar la única columna que se
repite, UserID, de forma especial mediante la palabra foreign key (clave ajena),
que indica que esta columna sirve de enlace con una segunda tabla (en este
caso, con usuarios). De esta manera, si queremos modificar en cualquier
momento el userID, todos sus «enlaces» se cambiarán automáticamente (o se
obtendrá un error si la opción de cambio automático no ha sido activada). Por
ejemplo, si nos damos cuenta de que el usuario con userID 1 realmente debería
tener un valor 10, podemos modificarlo en la tabla usuarios y será la propia
base de datos la que se encargue de hacer las modificaciones correspondientes
en los tweets del usuario de forma automática.
Existe un último aspecto reseñable antes de pasar a la creación real de las
tablas: las bases de datos relacionales exigen que en cada tabla haya un atributo, o una secuencia de atributos, que permita distinguir de forma unívoca
cada fila. En nuestro caso, estos atributos son ID para la tabla tweets y userID
para la tabla usuarios. En efecto, no puede haber dos tweets con el mismo ID
ni dos usuarios con el mismo userID. Determinar estos atributos, llamados
claves primarias, es fundamental porque son necesarios durante la etapa de la
creación de la tabla.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
67
Volvamos a pgadmin, el programa de PostgreSQL que nos ayudará a crear las
tablas. En la sección anterior ya hemos creado la base de datos Twitter. Esto,
que solo lo haremos una vez, causa la aparición de varios objetos. Entre ellos
uno con nombre schemas, que es donde crearemos las tablas. Para ello nos
situamos sobre el primer schema (con nombre Public), pulsamos el botón derecho y elegimos Create y, a continuación, Table, tal y como muestra la figura 3.3.
Figura 3.3.
Creación de una tabla en PostgreSQL
Los «esquemas» son formas de agrupar tablas dentro de la misma base de
datos, una forma de organización que nos ofrece PostgreSQL. En aplicaciones
complejas nos puede interesar que una misma base de datos contenga distintos esquemas, cada uno, a su vez, con sus tablas. Aquí vamos a utilizar un solo
esquema, el que viene creado por defecto. En el diálogo que se abre elegimos
el nombre de la tabla usuarios, como muestra la figura 3.4.
Como se aprecia en la imagen, solo cambiamos el nombre de la tabla y
dejamos el valor por defecto en el resto de los campos. Sin embargo, en este
caso no pulsamos Save, sino que vamos a la pestaña Columns para añadir las
columnas de la tabla. Si resulta que ya hemos pulsado Save, no importa. Basta
con que, dentro de la base de datos Twitter, busquemos los esquemas, localicemos Public dentro las tablas (en particular, la tabla usuarios), despleguemos los objetos que la componen y, finalmente, nos situemos sobre las
columnas pulsando el botón derecho y eligiendo Create + Column. En ambos
casos llegaremos a una ventana o a un contenido similar a la figura 3.5.
68
CUADERNOS METODOLÓGICOS 60
Figura 3.4.
Datos requeridos para crear una tabla en PostgreSQL
Figura 3.5.
Añadiendo una nueva columna en una tabla en PostgreSQL
Para situarnos, recordemos que hemos creado la base de datos, y a continuación, la tabla. Solo nos falta definir las columnas que forman la tabla. Pulsamos + para indicar que queremos crear una nueva columna. A partir de este
punto podremos añadir las columnas para la tabla usuarios, indicando que
UserID es la clave primaria, esto es, el valor que no se puede repetir.
De manera análoga, podemos crear la tabla tweets. Durante la creación,
además de indicar que el identificador del tweet es la clave primaria, también
indicaremos que en esta tabla UserID es una clave ajena o foreign key, que
referencia a la columna del mismo nombre de la tabla de usuarios.
Como alternativa, también podemos crear las tablas directamente desde la
consola de la base de datos. Este método es útil porque hace la creación independiente de la base de datos particular (MySQL, Postgre, etc.). En nuestro
caso la creación sería la siguiente:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
69
create table usuarios(
userID int primary key,
screen_name varchar(20),
created_at date);
create table tweets(
ID int primary key,
text varchar(240),
userID int,
foreign key (userID)
references usuarios(userID));
En la primera tabla declaramos las tres columnas, userID, screen_name y
created_at, cada una seguida de su tipo: un número entero para el identificador, un texto, también conocido como cadena de caracteres o string, con un
máximo de 20 caracteres para screen_name (en SQL se usa la sintaxis varchar(20)), y, finalmente, tipo fecha (date) para la fecha de creación. Obsérvese que, además, la primera columna lleva al final la restricción indicada por
las palabras clave primary key, mostrando que es la clave primaria y que,
por tanto, el valor de esa columna no podrá repetirse en dos filas distintas.
En el caso de la segunda tabla, además de la declaración de las columnas,
encontramos una línea diferente:
foreign key (userID) references usuarios(userID)
Esta es la restricción que indica que la columna userID de la tabla tweets,
ya declarada, es realmente una clave ajena para la tabla usuarios y, en particular, para su columna userID. Esto hará que la base de datos asegure que
todo tweet se corresponde a un usuario insertado previamente en la tabla
usuarios, asegurando la llamada «integridad referencial» entre tablas.
Como resultado de estas instrucciones dispondremos de las tablas, aunque
aún vacías. Es importante enfatizar, una vez más, que las tablas deben ser creadas, en el modelo relacional, a partir de un formato bien definido (columnas, tipo
de cada columna), ya que, una vez creado, esta estructura quedará fijada y no se
podrá cambiar sin un gran coste. Esta es una importante diferencia con bases de
datos NoSQL como MongoDB, donde, como veremos, no hay una estructura fija.
3.2.3. Inserción, borrado y eliminación
Como hemos mencionado anteriormente, la existencia de la clave ajena entre
tweets y usuarios fuerza a que no podamos insertar un tweet si su usuario no
70
CUADERNOS METODOLÓGICOS 60
existe. Esto determina un orden de inserción concreto, en este caso, obligándonos a comenzar por la tabla usuarios.
insert into usuarios values(1,'berto_98','2012/10/01');
insert into usuarios values(2,'hermina','2015/05/23');
insert into usuarios values(3,'amadeus','2018/01/01');
El resultado son tres nuevas filas insertadas en la tabla usuarios. Como
vemos, la sintaxis general para insertar filas en una tabla es la siguiente:
insert into tabla values (valorColumna1,valorColumna2…)
A la hora de escribir los valores debemos ser cuidadosos con poner valores
que, en efecto, sean del tipo de la columna. Por ejemplo, en la primera inserción
escribimos el entero 1 para el identificador de usuario, mientras que tanto el
screen_name como la fecha van entre comillas simples. También debemos recordar que las claves primarias no se pueden repetir. Por ejemplo, si intentamos:
insert into usuarios values(2,'aniceto','2017/05/23');
la base de datos no insertará nada y nos mostrará un error, avisando de que la
clave primaria 2 ya existe. Igualmente, se debe ser cuidadoso al insertar en la
tabla tweets, no solo para no repetir la clave primaria, sino, en este caso, también para que la clave ajena, el IdUsuario, sí exista. Por ejemplo:
insert into tweets values(1,'@aniceto,no estoy de acuerdo',1);
añade una nueva fila a tweets. El primer valor, 1, es el identificador del tweet.
El segundo valor es el texto del tweet, que debe escribirse entre comillas simples por ser de tipo texto (string). El tercer valor, 1, indica que se trata de un
tweet del usuario 1, que existe porque lo hemos insertado previamente. En
cambio, si intentamos:
insert into tweets values(8,'mi voto está decidido',50);
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
71
obtendremos un error, indicándonos que el usuario 50 no existe y, por tanto,
no podemos insertar un tweet suyo.
El orden forzado por las claves ajenas se invierte cuando se trata de eliminar filas mediante la instrucción SQL delete. Por ejemplo,
delete from usuarios where userID=1;
dará un error, porque, si se eliminara este usuario, sus tweets quedarían
«huérfanos» y se rompería la integridad referencial. Por tanto, primero debemos borrar los tweets del usuario, y luego el usuario propiamente dicho:
delete from tweets where userID=1;
delete from usuarios where userID=1;
Como esta situación se da a menudo, existe una alternativa: al crear la
tabla tweets y declarar userID como clave ajena mediante las palabras reservadas foreign key es posible añadir al final la coletilla on delete cascade. Esto hará que, al borrar un usuario, se eliminen de forma automática
todos sus tweets asociados. Esta es una las decisiones que se deberán tomar
durante el diseño de la base de datos.
Si, en lugar de borrar, lo que se desea es modificar una fila ya existente,
deberemos usar la instrucción update. Por ejemplo, supongamos que hemos
escrito mal el screen_name del usuario 2, que en lugar de «hermina» debe
ser «herminia». Podemos corregir esta fila escribiendo:
update usuarios
set screen_name='herminia'
where screen_name='hermina'
Las palabras update, set y where son palabras reservadas en SQL. Tras
update se especifica la tabla en la que se desean modificar filas, tras set se
indica el cambio que realizar (se pueden poner varios cambios separados por
comas, col1=v1, col2=v2, …) y where indica sobre qué filas se debe
actuar. Si no se incluye where, que es el encargado de filtrar a qué filas afectará la modificación, esta afectará a todas las filas de la tabla.
Al igual que sucedía con delete, deberemos ser cuidadosos a la hora de
modificar la clave primaria en la tabla usuarios. Cambiar, por ejemplo, el
72
CUADERNOS METODOLÓGICOS 60
identificador del primer usuario a 4 dejaría «huérfanas» las filas de la tabla
tweets que tienen userID con valor 1, por lo que el sistema no lo permitirá.
La solución más sencilla es, como en el caso de la eliminación, provocar
que el cambio sea automático, esto es, que al cambiar el identificador de un
usuario se modifiquen de forma automática las claves ajenas en tweets que
tenían ese valor. De nuevo, de forma análoga al caso del borrado, esto se
hará añadiendo la opción on update cascade tras la declaración de la
clave ajena.
3.2.4. Consultas básicas en SQL
Vamos a ver algunos ejemplos de consultas sencillas en este lenguaje. SQL es
un lenguaje muy rico y aquí vemos una breve introducción, que, sin embargo,
esperamos que sea suficiente para ser capaces de codificar la mayor parte de
las consultas que podemos necesitar.
La primera consulta simplemente muestra el contenido completo de una
tabla. Esto se escribe así:
select * from usuarios;
La consulta comienza con la palabra reservada select, por la que comenzarán todas las consultas SQL. El símbolo * indica que queremos que se muestren todas las columnas de la tabla. A continuación, la palabra reservada from
indica que se va a dar el nombre de la tabla o tablas de las que se tomarán
datos, en este caso, usuarios. El resultado será una tabla de la forma siguiente:
userID
screen_name
created_at
1
berto_98
2012-10-01
2
herminia
2015-05-23
3
amadeus
2018-01-01
También podemos mostrar solo algunas columnas, indicando sus nombres
explícitamente tras el select. Por ejemplo:
select userID, text from usuarios;
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
73
mostrará:
userID
screen_name
1
berto_98
2
herminia
3
amadeus
Si, en lugar de mostrar todas las filas, solo deseamos mostrar las que cumplan cierta condición, podemos utilizar la palabra reservada where, que ya
vimos para el caso del borrado y actualización de filas:
select created_at, screen_name
from usuarios
where created_at > '2017/01/01';
created_at
screen_name
2018-01-01
amadeus
La cláusula where admite condiciones complejas combinadas con conectores como AND, OR, NOT, etc.
3.2.5. Consultas desde múltiples tablas
Las consultas SQL son especialmente potentes cuando se utilizan combinando varias tablas. Para hacerlo, es de capital importancia tener en cuenta el
criterio con el que vamos a combinar las filas de las diferentes tablas. De no
especificar tal criterio, SQL mezclaría cada fila de la primera tabla con cada
fila de la segunda, es decir, realizaría el producto cartesiano de ambas tablas,
dando lugar a información sin sentido.
Por ejemplo, supongamos que queremos mostrar para cada tweet su
texto, el identificador del usuario y el screen_name del usuario. El texto está
en la tabla tweets y el screen_name, en la tabla usuarios. Para combinar
ambas, lo lógico es hacerlo utilizando el userID, ya que nos permite, a partir del tweet, obtener la información de su usuario. La consulta sería la
siguiente:
74
CUADERNOS METODOLÓGICOS 60
select T.userID, U.screen_name, T.text
from tweets as T,usuarios as U
where T.userID = U.userID;
El resultado:
userID
screen_name
Text
1
berto_98
@aniceto, no estoy de acuerdo
1
berto_98
Hoy ha sido un gran día!
2
herminia
Pasad #FelizDomingo, que mañana llega #TristeLunes
...
...
...
La principal novedad de esta consulta es la combinación de las dos tablas,
tweets y usuarios, en la cláusula from. Ambas tablas son renombradas de
forma interna (solo para la consulta) como T y U, respectivamente. La cláusula where asegura que cada tweet solo se combina con los datos del usuario
que lo ha emitido y se lee como «considerar solo la combinación de filas de
ambas tablas si tienen el mismo userID».
Esta forma de combinar tablas es tan común que el estándar de SQL ofrece
una forma especial de lograr la combinación, a través de la operación join.
Esto es:
select T.userID, U.screen_name, T.text
from tweets as T join usuarios as U on T.userID = U.userID;
3.2.6. Agregaciones
Las agrupaciones o agregaciones consideran, en lugar de filas, grupos de filas.
Cada grupo esta formado por aquellas filas de la tabla que toman el mismo
valor para cierta expresión especificada en la cláusula group by, como muestra el siguiente ejemplo:
select userID, count(*)
from tweets
group by userID;
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
75
En primer lugar, dentro de la tabla tweets se agrupan las filas con el mismo
identificador de usuario. Por ejemplo, si tweets es de la forma:
userID
Text
1
...
1
...
1
...
2
...
2
...
4
...
se formarán tres grupos: el asociado al identificador 1, con tres filas, el del
identificador 2, con dos filas, y finalmente el asociado al identificador 4, que
consta de una única fila. Para cada uno de estos grupos se aplica la cláusula
select, que en el caso de consultas agregadas solo puede contener o bien
nombres de columnas utilizados para agrupar (en este caso userID), o bien
funciones que tengan sentido al aplicarse a un grupo de filas (en el ejemplo,
count(*), que cuenta el número de filas de cada grupo).
La salida:
userID
count(*)
1
3
2
2
4
1
Las consultas con group by pueden incluir una cláusula adicional, having,
que permite excluir grupos:
select userID, count(*)
from tweets
group by userID
having count(*)>1;
En este caso se excluyen aquellos grupos que tienen un solo elemento, lo
que produce el previsible resultado:
76
CUADERNOS METODOLÓGICOS 60
userID
count(*)
1
3
2
2
Para finalizar, es importante enfatizar que hemos visto una pequeña parte
de SQL, aunque esperamos que suficiente para tener una idea de la potencia de
este lenguaje de consultas relacional y emplearlo en numerosos casos prácticos. Hay que añadir, además, que distintas implementaciones de SQL presentarán diversos «dialectos» de SQL y, por tanto, ofrecerán distintas posibilidades,
que debemos consultar en cada caso.
3.3. Bases de datos no relacionales: MongoDB
MongoDB es una base de datos de las llamadas NoSQL o no relacionales.
Estas bases de datos se dividen, a su vez, en varios tipos: orientadas a grafos,
clave-valor, etc. MongoDB sería de las denominadas orientadas a documento.
El nombre documento sustituye a la noción de fila de las bases de datos
SQL. En particular, en el caso de MongoDB, los documentos tienen formato
JSON, formato que ya vimos en un capítulo anterior. Esto nos permitirá almacenar información compleja sin tener que pensar en cómo representar esta
información en un formato de tablas, lo que sería obligado en el caso de SQL.
Otra característica de MongoDB es que las «colecciones», que es como llamaremos a los conjuntos de documentos en lugar del término «tablas» usado
en SQL, no tienen ningún esquema prefijado. Es decir, un documento puede
contener unas claves (el equivalente a «columnas») mientras que el documento siguiente, dentro de la misma colección, puede tener una estructura
completamente diferente.
Por ejemplo, pensemos en el catálogo de una tienda de ropa, donde cada
tipo de prenda tiene sus propias características. En SQL esto nos forzaría a
crear una tabla para cada tipo de ropa, ya que, como hemos visto, en las bases
de datos relacionales tenemos que definir de antemano la estructura de cada
tabla, en particular, el nombre y el tipo de cada columna. En cambio, en MongoDB podemos simplemente crear una colección catálogo, e ir insertando
documentos JSON con distinta estructura según la prenda concreta. Esta
característica nos recuerda a una de las famosas V que definen el término big
data, la que hace referencia a la variedad en el formato de los datos.
Otra de las V es la de volumen, que se refiere a la posibilidad de almacenar
grandes volúmenes de datos en un entorno escalable. MongoDB también
atiende a este requerimiento, permitiendo almacenar los datos en un clúster
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
77
de ordenadores, donde los datos de una misma colección se encuentran repartidos por todo el clúster. Si la colección sigue creciendo, bastará con añadir
nuevos ordenadores al clúster para aumentar la capacidad de almacenamiento.
Podemos pensar que tal cantidad de datos puede llevar a una disminución
de rendimiento, pero esto no sucede, porque MongoDB también está preparado para satisfacer la tercera V de big data, la de velocidad. La velocidad,
sobre todo de lectura de datos, se logra gracias a que las búsquedas se hacen
en paralelo en todos los ordenadores, proporcionando escalabilidad en cuanto
al tiempo de acceso.
Para instalar MongoDB podemos acceder a https://www.mongodb.com/ y
descargar la versión Community Server, que incluye de forma gratuita todo lo
que necesitamos para comenzar a trabajar con esta potente base de datos.
3.3.1. Arquitectura cliente-servidor en MongoDB
Como ya vimos al hablar de PosgreSQL, casi todas las bases de datos siguen
un modelo de arquitectura conocido como cliente-servidor. El servidor es el
programa que accede directamente a los datos y ofrece este servicio a los
clientes. Por su parte, un cliente es un programa que accede al servidor para
solicitarle datos o pedir que haga modificaciones sobre la base de datos. Así,
varios clientes pueden estar conectados, simultáneamente, al mismo servidor.
En este apartado vamos a considerar dos tipos de clientes: la propia consola que viene por defecto con MongoDB y el cliente Python incluido en la
biblioteca pymongo.
Un error común cuando se utilizan bases de datos, y en particular en
Mongo, es intentar utilizar las bases de datos mediante un cliente sin tener
antes el servidor activo. Para comprobar si el servidor está activo o no, lo más
sencillo en MongodB será acceder el cliente por consola y ver si se logra conectar. Para ello abrimos un terminal (Linux/Mac OS) o un Símbolo de sistema en
Windows e intentamos acceder al cliente tecleando simplemente mongo. Si
obtenemos una respuesta como
MongoDB consola version v4.0.10
connecting to: mongodb://127.0.0.1:27017
…
exception: connect failed
será que el cliente no ha logrado conectar con el servidor. El servidor puede
estar en nuestro propio ordenador, pero también en otro ordenador, al que
78
CUADERNOS METODOLÓGICOS 60
debemos poder tener acceso. Además, dentro de cada ordenador, sea el
nuestro o «local», u otro, disponemos de distintos puertos. Un puerto es un
canal de conexión con el propio ordenador, una vía de acceso. Cada servidor
estará «escuchando», esto es, esperando conexiones a través de un puerto en
particular.
Cuando accedemos al servidor, es decir, nos conectamos a la base de datos,
con MongoDB podemos especificar el nombre del servidor (la máquina en la
que se encuentra) y el número de puerto de ese ordenador a través del que se
accede a la base de datos.
Como todo esto es un poco complicado, MongoDB supone que cuando
escribimos mongo, sin más, estamos accediendo a un servidor situado en
nuestro propio ordenador (que se suele representar internamente por la dirección IP 127.0.0.1), y a través del puerto 27017, que es el puerto por defecto de
escucha del servidor MongoDB si no le especificamos otra cosa.
Toda esta información, un poco técnica, nos permite entender mejor la
frase de error
connecting to: mongodb://127.0.0.1:27017
mostrada en el error de conexión, que nos indica que el cliente está «buscando» el servidor como alojado en la máquina actual (representada por el
número IP 127.0.0.1) y, dentro de ella, en el puerto 27017, que es donde se
busca por defecto si se escribe simplemente mongo. Si en lugar de ese puerto
sabemos que el servidor está accesible a través de otro puerto, digamos el
28000, podemos iniciar el cliente con mongo –port 28000. Igualmente, si el
servidor no está alojado en el servidor local, sino que es un servicio remoto,
por ejemplo, proporcionado por el servicio Atlas, ofrecido por MongoDB para
la creación de clústeres alojados en la nube (en particular, en Amazon AWS),
tendremos que incluir tras la llamada a mongo la cadena de conexión proporcionada por el servicio.
En nuestro caso, si cuando tecleamos simplemente mongo el sistema se
conecta con éxito, significará que ya disponemos de un servidor de datos. Esto
se produce o bien porque nosotros hemos iniciado el servidor con anterioridad o bien porque el servidor está incluido en el servicio de arranque del sistema operativo.
En cualquier caso, no está de más que sepamos arrancar nuestro propio
servidor. El primer paso es disponer de una carpeta de datos vacía. Es allí
donde el servidor alojará los datos que insertemos. La siguiente vez podremos
arrancar el servidor sobre esta misma carpeta y tendremos a nuestra disposición los datos que allí quedaron almacenados. Para iniciar el servidor vamos
a un terminal del sistema operativo y tecleamos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
79
mongod -port 28000 -dbpath C:\datos
donde C:\datos es la carpeta de datos, ya sea vacía inicialmente o con los
datos de la vez anterior. Si todo va bien, tras muchos mensajes iniciales aparecerá waiting for connections on port 28000, lo que indica que el servidor
está listo, en modo local, y esperando conexiones de clientes en el puerto
28000.
Es muy importante indicar que, tras iniciarse el servidor, este terminal
queda bloqueado, dedicándose en exclusiva a actuar de servidor, es decir, de
puente hacia los datos. Podemos minimizar la ventana y dejarla aparte, pero
no interrumpirla con Ctrl-C, ni cerrarla, porque pararíamos el servidor. Hay
opciones que permiten arrancar el servidor sin que quede bloqueado el terminal, pero de momento no conviene usarlas para poder detectar por pantalla
las conexiones, errores, etc., de forma sencilla.
Una vez iniciado el servidor, podremos acceder a él a través del cliente de
consola tecleando desde el terminal:
mongo -port 28000
Tras algunos mensajes de inicialización, llegaremos al prompt de la consola de MongoDB. El puerto debe coincidir con el utilizado al iniciar el servidor. Recordemos que, si no se pone ninguno al iniciar MongoDB, este elegirá
por defecto el 27017.
Ya dentro de la shell de Mongo (lo que indicaremos comenzando las líneas
de código por el símbolo >), podemos preguntar, por ejemplo, por las bases de
datos que ya existen con la instrucción show databases:
> show databases
admin 0.000GB
config 0.000GB
local 0.000GB
test 0.001GB
Aunque nosotros no hemos creado ninguna base de datos, MongoDB ya ha
creado varias automáticamente. Por defecto, la consola de MongoDB nos
situará en una base de datos de nombre test. Si queremos cambiar a otra base
de datos podemos teclear, por ejemplo:
80
CUADERNOS METODOLÓGICOS 60
> use pruebas
Y ya estaremos en la base de datos pruebas. Puede que choque al lector,
especialmente si está habituado a bases de datos relacionales, el hecho de que
accedamos a una base de datos que no hemos creado previamente. En
MongoDB todo va a ser así: cuando accedemos a un objeto que no existe, el
sistema simplemente lo crea.
Una vez seleccionada una base de datos podemos ver qué colecciones están
disponibles tecleando:
> show collections
Si deseamos salir de la consola teclearemos:
> quit()
Una nota final acerca de la consola. Está escrita en el lenguaje de programación JavaScript y admite todas las instrucciones de este lenguaje. Aunque
aquí no vamos a aprovechar esta funcionalidad, no está de más saber que es
bastante común desarrollar scripts, es decir, bloques de código, que combinen
instrucciones de MongoDB con instrucciones JavaScript para hacer tratamientos complejos de la información desde este cliente.
3.3.2. Conceptos básicos en MongoDB
Para trabajar en MongoDB debemos conocer su terminología, que, por otra
parte, es común a otras bases de datos orientadas a documentos como, por
ejemplo, CouchDB. Como hemos dicho ya, en MongoDB el equivalente a las
tablas de bases de datos relacionales serán las colecciones, que agrupan documentos, que serían el correspondiente a filas en el modelo relacional. Los
documentos se escriben en formato JSON, que, como vimos en el capítulo 2,
tiene el siguiente aspecto:
{clave1:valor1, …., claven:valorn}
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
81
Las claves representan las columnas y los valores, el contenido de la celda.
Los valores pueden ser atómicos, numéricos, booleanos o strings, pero también pueden ser arrays o incluso otros documentos JSON. Un ejemplo de
documento JSON:
{_id:1,
nombre:"Berto",
contacto: {mail: "berto@jemail.com",
telfs:[ "45612313", 4511]} }
Este documento tiene tres claves. En primer lugar, la clave _id, que es
numérica. En segundo lugar, nombre, de tipo texto o string. Por último, el contacto, que es, a su vez, un documento JSON con dos claves, el mail de tipo
string y telfs, que es un array.
Para el uso correcto de los documentos JSON en MongoDB es importante tener en cuenta algunas observaciones. La clave _id es obligatoria y
debe ser distinta para todos los documentos de una colección. Si al insertar
el documento no la incluimos, MongoDB la añadirá por su cuenta. Esto
provocará que, al hacer las consultas, nos encontremos con algo como
"_id": ObjectId("5b2b831d61c4b790aa98e968").
Por otra parte, en la consola no hace falta poner las comillas en las claves,
puesto que las añade MongoDB automáticamente. Sí que serán necesarias en
el caso de que las claves contengan espacios o algunos caracteres especiales.
Por último, es importante tener en cuenta que el formato JSON no incluye
en su especificación un formato fecha. MongoDB sí lo hace. Por ejemplo,
ISODate("2012-12-19T06:01:17.171Z") representa una fecha y una
hora en zona horaria Zulu (la Z del final), que corresponde a tiempo coordinado universal (UTC) +0, también llamada hora de Greenwich.
3.3.3. Carga de datos
Para poder realizar nuestro análisis, el primer paso es ser capaz de incorporar
datos a nuestras colecciones. Para ello existen varias estrategias que seguir.
Aquí, vamos a ver dos formas de hacerlo.
3.3.3.1. Importando datos ya existentes
MongoDB incluye dos herramientas para importar y exportar datos: mongoimport y mongoexport. Ambas se usan desde la línea de comandos (no
82
CUADERNOS METODOLÓGICOS 60
desde dentro de la consola de MongoDB) y son una excelente herramienta
para facilitar la comunicación de datos. Por ejemplo, consideremos el fichero
de tweets que hemos descargado en el apartado dedicado a la red social, y
supongamos que queremos incorporarlo a una colección final de la base de
datos tweets; podemos escribir:
mongoimport --db twitter --collection feliz --file tuitsDescargados.txt
Donde tuitsDescargados.txt es el fichero que contiene los tweets.
En el caso de que el fichero sea de tipo CSV, hay que indicarlo explícitamente
con el parámetro.
mongoimport -d twitter -c feliz –-type CSV --headerline
--file tuitsDescargados.txt
En este caso se indica el nombre de la base de datos (twitter) y de la colección (tweets) con los parámetros abreviados -d y -c. Además, se avisa a mongoimport de que la primera fila del fichero contiene la cabecera, esto es, los
nombres de las columnas. Esto es importante porque se utilizarán los nombres
en esta cabecera como claves para los documentos JSON importados. Por eso,
si el fichero CSV no incluye cabecera debemos usar la opción --fields y dar
la lista de nombres entre comas, o --fieldFile seguido del nombre de un
fichero de texto con dichos nombres, uno por cada línea del fichero.
3.3.3.2. Añadiendo datos con insert
La forma más sencilla de añadir un documento a una colección es a través de
la instrucción insert. Por ejemplo, desde la consola, comenzamos por entrar
en MongoDB (omitiremos este paso de ahora en adelante):
mongo twitter -port 28000
La palabra twitter tras la llamada a mongo indica que tras entrar en
MongoDB debe situarse en esa base de datos. Si dicha base de datos no existe,
se creará automáticamente una con dicho nombre. También podríamos omitir
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
83
este argumento y después, ya dentro de la consola, escribir use twitter. En
cuanto al parámetro -port 28000, recordemos que debe ser el puerto donde
está «escuchando» el servidor mongod. Una vez dentro de la consola, podemos
escribir:
> db.tweets.insertOne(
{_id: 1,
usuario: {nick:"bertoldo",seguidores:1320},
texto: "@herminia: hoy, excursión a la sierra con @aniceto!",
menciones: ["herminia", "aniceto"],
RT: false} )
Esto hace que se inserte un nuevo documento en la colección tweets de la
base de datos actual (twitter). El documento representa un tweet, con su identificador, los datos del usuario (nick usado en Twitter y número de seguidores), el texto del tweet, un array con los usuarios mencionados, y un valor RT
indicado si es un retweet.
Si todo va bien, el sistema nos lo indicará, mostrando, además, el _id del
documento. Si en algún momento nos equivocamos y queremos borrar la
colección podemos escribir (atención: esta orden borra toda la colección):
db.tweets.drop()
Ya dijimos que MongoDB es «proactivo», así que no nos pedirá confirmación y borrará la colección completa sin más, por lo que conviene utilizar esta
instrucción con cautela.
Aunque la consola es cómoda y adecuada para hacer pruebas, normalmente querremos integrar el procedimiento de inserción en nuestros programas en Python. Con este fin podemos utilizar la biblioteca pymongo.
Ya dentro de Python, por ejemplo, desde los Jupyter notebooks, comenzamos por cargar la biblioteca y establecer una conexión con el servidor:
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:28000/')
La primera línea importa la clase MongoClient de pymongo, y luego establecemos la conexión, que queda almacenada en la variable client. Esta
84
CUADERNOS METODOLÓGICOS 60
variable hará de puente entre el cliente y el servidor. Siendo así, en el resto del
apartado asumiremos que existe sin declararla de nuevo. La notación localhost:28000 indica que el servidor al que nos conectamos está en el propio
servidor (localhost), y que se accede a él a través del puerto 28000.
Ahora podemos acceder a la base de datos twitter, y dentro de ella a la
colección tweets.
db = client['twitter']
tweets = db['tweets']
Finalmente, procedemos a la inserción del documento. Para facilitar la lectura, primero asignamos el documento a una variable intermedia a la que
ponemos, por ejemplo, el nombre tweet.
tweet = {
'_id':2,
'usuario': {'nick':"herminia",'seguidores':5320},
'texto':"RT:@herminia: hoy,excursión a la sierra con @
aniceto!",
'menciones': ["herminia", "aniceto"],
'RT': True,
'origen': 1 }
insertado = tweets.insert_one(tweet)
print(insertado.inserted_id)
En este caso, el tweet es un retweet (reenvío) del tweet anterior y lo indicamos con la clave RT a valor True. Un detalle de sintaxis es que, en la consola
de MongoDB, los valores booleanos se escriben en minúsculas (true,false),
mientras que en Python se escribe la primera letra en mayúscula (True,
False). En nuestra aplicación hemos decidido que, cuando un tweet sea un
retweet, además de indicarlo en la clave RT, apuntaremos el _id del tweet original en la clave origen.
Tras ejecutar el código anterior tenemos ya dos tweets insertados en la
colección tweets. Para tener un conjunto mayor y utilizarlo en el resto del
capítulo vamos a generar de forma aleatoria 100 tweets al azar desde
Python.
Para ello, y de forma análoga al caso anterior, el programa empieza estableciendo la conexión con el servidor de Mongo, seleccionando la base de
datos (twitter) y la colección (tweets). Además, nos aseguramos de que la colección está inicialmente vacía borrando su contenido (drop):
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
85
from pymongo import MongoClient
import random
import string
client = MongoClient('mongodb://localhost:28000/')
db = client['twitter']
tweets = db['tweets']
tweets.drop()
Además de la biblioteca pymongo para conectar con MongoDB, empleamos random, que usaremos para generar valores aleatorios. Usamos, además, la biblioteca str para las operaciones que permiten generar el texto del
tweet. A continuación, fijamos los nombres y seguidores de cuatro usuarios
inventados y el número de tweets que generar (100):
usuarios = [("bertoldo",1320),("herminia",5320),
("aniceto",123),("melibea",411)]
n = 100
Finalmente, el bucle que genera e inserta los tweets:
for i in range(1,n+1):
tweet = {}
tweet['_id'] = i
tweet['text'] =
''.join(random.choices(string.ascii_uppercase, k=10))
u = {}
u['nick'], u['seguidores'] = random.choice(usuarios)
tweet['usuario'] = u
tweet['RT'] = i>1 and random.choice([False,True])
if tweet['RT'] and i>1:
tweet['origen'] = random.randrange(1, i)
m = random.sample(usuarios,
random.randrange(0, len(usuarios)))
tweet['mentions'] = [nick for nick,_ in m]
tweets.insert_one(tweet)
Para cada tweet se genera un diccionario vacío (tweet={}) que se va
completando, primero con el _id, después con el texto formado como la sucesión de diez caracteres aleatorios en mayúscula y, por último, con los datos
del usuario que se eligen al azar del array usuarios.
86
CUADERNOS METODOLÓGICOS 60
El valor RT, que indica si el tweet es un retweet o no, se elige también al
azar, excepto para el primer tweet, que nunca puede ser retweet. Si RT es true,
es decir, si el tweet es un retweet, añadimos, además, el identificador del tweet
que se está retuiteando, de nuevo elegido al azar. Para ello se añade la clave
origen con el _id de uno cualquiera de los tweets anteriores, simulando que ese
tweet anterior es el que se está reemitiendo.
En el caso del retweet, Twitter hace que el texto sea de la forma «RT: text»,
con text el texto del tweet retuiteado. Nuestra simulación no llega a tanto, y se
limita a añadir «RT:» al texto aleatorio generado para el tweet. Finalmente,
para las menciones se coge una muestra de los usuarios y se añaden sus nicks
a la lista mentions (aunque en nuestra pobre simulación las menciones no
salen realmente en el texto del tweet). Para comprobar que todo ha funcionado correctamente podemos ir a la consola de MongoDB y ejecutar el
siguiente comando, que debe devolver el valor 100:
db.tweet.count()
3.3.4. Consultas simples
Ya conocemos un par de mecanismos sencillos para introducir documentos
en nuestra base de datos. Lo siguiente que tenemos que hacer es ser capaces
de extraer información, es decir, de hacer consultas. En MongoDB se distingue entre las consultas simples, que vamos a ver en esta sección, y las consultas agregadas o de agrupación, que veremos en la sección siguiente.
Las siguientes subsecciones se desarrollan dentro de la consola de MongoDB.
Antes de terminar la sección veremos cómo se adapta la notación para su uso
desde pymongo.
3.3.4.1. find, skip, limit y sort
La forma más simple de ver el contenido de una colección desde dentro de la
consola de MongoDB es simplemente:
> db.tweets.find()
Esto nos mostrará los veinte primeros documentos de la colección tweets:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
{ "_id" : 1, "text" : "GKAXRKDQKV", "usuario" :
"herminia", "seguidores" : 5320 }, "RT" : false,
: [ "herminia", "melibea", "bertoldo" ] }
{ "_id" : 2, "text" : "IWGXXFPHSI", "usuario" :
"bertoldo", "seguidores" : 1320 }, "RT" : false,
: [ "aniceto", "herminia", "bertoldo" ] }
…
87
{ "nick" :
"mentions"
{ "nick" :
"mentions"
Si la colección tiene más de veinte documentos, podemos teclear it para
ver los veinte siguientes, y así sucesivamente. El formato en el que se muestran los documentos no es demasiado intuitivo y, en el caso de JSON, puede
ser muy difícil de entender.
> db.tweets.find().pretty()
{
"_id" : 1,
"text" : "GKAXRKDQKV",
"usuario" : {
"nick" : "herminia",
"seguidores" : 5320
},
"RT" : false,
"mentions" : [
"herminia",
"melibea",
"bertoldo"
]
}
…
La función pretty() «embellece» la salida y la hace más legible. Los documentos se muestran en el mismo orden en el que se insertaron.
Podemos saltarnos los primeros documentos con skip. Por ejemplo, si
queremos ver todos los documentos, pero comenzando a partir del segundo:
> db.tweets.find().skip(1).pretty()
Otra función similar es limit(n), que ordena que se muestren únicamente los n primeros documentos. Por ejemplo, para ver solo los tweets que
ocupan las posiciones 6 y 7 podemos emplear:
88
CUADERNOS METODOLÓGICOS 60
> db.tweets.find().skip(5).limit(2).pretty()
Por defecto, el orden en el que se muestran los documentos es el de inserción. Para mostrarlos en otro orden, lo mejor es emplear la función sort().
Esta función recibe como parámetro un documento JSON con las claves que
se deben usar para la ordenación. Si se quiere una ordenación ascendente, la
función debe ir seguida de +1. Por el contrario, si se desea una ordenación
descendente, se usará –1. Por ejemplo, para mostrar los tweets comenzando
desde el de mayor _id, podríamos escribir:
> db.tweets.find().sort({_id:-1}).pretty()
que mostrará:
{
"_id" : 100,
"text" : "BZIVQDRSDU",
"usuario" : {
"nick" : "aniceto",
"seguidores" : 123
},
"RT" : false,
"mentions" : [
"melibea",
"aniceto"
]
}
…
Supongamos que queremos ordenar por número de seguidores, de mayor
a menor. La clave seguidores aparece dentro de la clave usuario. Para indicar
que queremos ordenar por seguidores, usaremos:
> db.tweets.find().sort({"usuario.seguidores":-1})
{ "_id" : 1, "text" : "GKAXRKDQKV", "usuario" : { "nick" :
"herminia", "seguidores" : 5320 }, "RT" : false, "mentions"
: [ "herminia", "melibea", "bertoldo" ] }
…
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
89
La orden sort también permite que se ordene por varias claves. Por ejemplo, si queremos ordenar primero por el número de seguidores de forma descendente y luego, para los tweets de usuarios con el mismo tweet, por el _id
también de forma descendente, podemos escribir:
> db.tweets.find().sort({"usuario.seguidores":-1, _id:-1})
Cuando se combina con otras funciones como limit o skip, la función
sort siempre se ejecuta en primer lugar. Así, si queremos encontrar el tweet
con mayor _id podemos escribir:
> db.tweets.find().sort({_id:-1}).limit(1)
Pero también:
> db.tweets.find().limit(1).sort({_id:-1})
Un apunte final sobre sort. En grandes colecciones, esta orden puede ser
tremendamente lenta. La mejor forma de acelerar este tipo de consultas es
disponer de un índice. Un índice es una estructura que mantiene una copia
ordenada de una colección según ciertos criterios. En realidad, no se trata de
una copia de la colección como tal, lo que sería costosísimo en términos de
espacio. Tan solo se guarda un «puntero» o señal para cada elemento de la
colección real en el orden elegido.
Si sabemos, por ejemplo, que vamos a repetir la consulta anterior a
menudo, podemos crear un índice para acelerarla con:
> db.tweets.createIndex({"usuario.seguidores":-1, _id:-1})
La instrucción, que únicamente debe ejecutarse una vez, no tiene ningún
efecto aparente. Sin embargo, puede lograr que una consulta que tardaba
horas en realizarse pase a requerir, tras la creación del índice, pocos segundos. A cambio de acelerar las consultas, los índices pueden retrasar ligeramente inserciones, modificaciones y borrados. Esto es así porque ahora cada
vez que, por ejemplo, se inserta un documento, también hay que apuntar su
90
CUADERNOS METODOLÓGICOS 60
lugar correspondiente en el índice. Por ello no debemos crear más índices de
los necesarios y únicamente usarlos para acelerar consultas que realmente lo
precisen.
3.3.4.2. Estructura general de find
La función find, ya mencionada en el apartado anterior, es la base de las consultas simples en Mongo. Su estructura general es la siguiente:
find({filtro},{proyección})
El primer parámetro corresponde al filtro, que indicará qué documentos se
deben mostrar. El segundo, la proyección, indicará qué claves se deben mostrar de cada uno de estos documentos. Como hemos visto en el apartado anterior, ambos son opcionales; si se escribe simplemente find() se mostrarán
todos los documentos y todas sus claves.
3.3.4.3. Proyección en find
Si se incluye solo un argumento en find, MongoDB entiende que se refiere al
filtro, y no a la proyección. Por ello, si queremos incluir solo la proyección, la
selección deberá aparecer, aunque sea como el documento vacío.
> find({},{proyección})
La proyección puede adoptar tres formas:
1.{ }: indica que deben mostrarse todas las claves. En este caso, normalmente nos limitaremos a no incluir este parámetro, lo que tendrá el
mismo efecto.
2.{clave1:1, …, clavek:1 }: indica que solo se muestren las claves clave1…
clavek.
3.{clave1:0, …, clavek:0}: indica que se muestren todas las claves menos las
claves clave1…clavek.
Por ejemplo, para ver todos los datos de cada tweet excepto los datos del
usuario, podemos usar la forma 3:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
91
> db.tweets.find({},{usuario:0})
{
:
{
:
…
"_id" : 1, "text" : "GKAXRKDQKV", "RT" : false, "mentions"
[ "herminia", "melibea", "bertoldo" ] }
"_id" : 2, "text" : "IWGXXFPHSI", "RT" : false, "mentions"
[ "aniceto", "herminia", "bertoldo" ] }
Si solo queremos ver el _id del tweet y el texto, podemos utilizar la forma 2:
> db.tweets.find({},{_id:1, text:1})
{ "_id" : 1, "text" : "GKAXRKDQKV" }
{ "_id" : 2, "text" : "IWGXXFPHSI" }
…
Como se ve en el ejemplo, no se pueden mezclar los unos y los ceros. Solo
hay una excepción: el _id. Esta clave especial siempre se muestra, aunque no
se indique explícitamente en la lista.
> db.tweets.find({},{text:1})
{ "_id" : 1, "text" : "GKAXRKDQKV" }
{ "_id" : 2, "text" : "IWGXXFPHSI" }
…
Por ello, en la forma 2, se admite de forma excepcional el uso de _id:0.
> db.tweets.find({},{text:1, _id:0})
{ "text" : "GKAXRKDQKV" }
{ "text" : "IWGXXFPHSI" }
…
3.3.4.3. Selección en find
Veamos ahora las principales posibilidades del primero argumento, el que
determina la selección o filtro de la orden find.
92
CUADERNOS METODOLÓGICOS 60
3.3.4.3.1. Igualdad
La primera y más básica forma de seleccionar documentos es buscar por valores concretos, es decir, filtrar con criterios de igualdad. Por ejemplo, podemos
querer ver tan solo los textos de tweets que son retweets:
> db.tweets.find({RT:true}, {text:1,_id:0})
{ "text" : "RT: UFBFDYKXUK" }
{ "text" : "RT: XTCDXTNIVN" }
…
El primer argumento selecciona solo los tweets con el indicador RT
tomando el valor true. Mientras, el segundo argumento indica que, de los
documentos seleccionados, solo se debe mostrar el campo text. Podemos, además, refinar el filtrado indicando que solo queremos retweets efectuados por
alguien que se llama Bertoldo:
> db.tweets.find({RT:true,
'usuario.nick':'bertoldo'},{text:1,_id:0})
La coma que separa RT y usuario.nick se entiende como una conjunción.
Haciendo esto se buscan tweets que sean retweets y cuyo nick de usuario
corresponda a Bertoldo. Si lo que deseamos es contar el número de documentos que son retweets realizados por Bertoldo, podemos usar la función count:
> db.tweets.find({RT:true, 'usuario.nick':'bertoldo'}).count()
15
3.3.4.3.2. Otros operadores de comparación y lógicos
La siguiente tabla muestra otros operadores que pueden utilizarse para comparar valores, aparte de la igualdad que acabamos de estudiar:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
93
Tabla 3.1.
Operadores de comparación en MongoDB
Operador
$gt
$gte
$lt
$lte
Selecciona documentos tales que…
Mayor que
Mayor o igual
Menor
Menor o igual
$eq
Igual
$ne
Distinto
$and
Conjunción
$or
Disyunción
$not
Negación
$nor
No se cumple ninguna de las condiciones indicadas
Por ejemplo, si queremos contar el total de tweets no emitidos por Bertoldo, podemos utilizar el operador $ne:
> db.tweets.find({'usuario.nick':{$ne:'bertoldo'}}).count()
71
Otra expresión equivalente se obtiene al restar al total el número de tweets
emitidos por Bertoldo. Como vemos, se obtiene el mismo resultado (71).
> db.tweets.find().count() –
db.tweets.find({'usuario.nick':'bertoldo'}).count()
71
También podemos usar estos operadores para indicar un rango de valores.
Por ejemplo, queremos obtener los tweets de usuarios que tienen entre 1.000
y 2.000 seguidores (ambos números excluidos):
> db.tweets.find({'usuario.seguidores':
{$gt:1000, $lt:2000}})
94
CUADERNOS METODOLÓGICOS 60
{ "_id" : 99, "text" : "BLIBHOBCGN", "usuario" :
"bertoldo", "seguidores" : 1320 }, "RT" : false,
: [ "aniceto" ] }
{ "_id" : 97, "text" : "RT: RMXRNHWGJZ", "usuario"
: "bertoldo", "seguidores" : 1320 }, "RT" : true,
8, "mentions" : [ ] }
…
{ "nick" :
"mentions"
: { "nick"
"origen" :
Como se aprecia en el ejemplo, cuando hay varias condiciones sobre la misma
clave, se agrupan, como en {'usuario.seguidores':{$gt:1000, $lt:
2000}}. Esto es necesario porque un documento JSON de MongoDB no puede
contener la misma clave repetida dos o más veces, es decir, escribir {'usua-
rio.seguidores':{$gt:1000}, 'usuario.seguidores':{$lt:2000}}
es incorrecto y daría lugar a errores o a comportamientos inesperados (por
ejemplo, en la consola solo se tendría en cuenta la segunda condición).
Las condiciones se pueden agrupar usando los operadores $not, $and,
$or y $nor. Por ejemplo, si queremos tweets escritos ya sea por Bertoldo o
por Herminia podemos escribir:
> tweets.find({'$or':[{'usuario.nick':"bertoldo"},
{'usuario.nick':"herminia"}]})
{ "_id" : 1, "text" : "GKAXRKDQKV", "usuario" :
"herminia", "seguidores" : 5320 }, "RT" : false,
: [ "herminia", "melibea", "bertoldo" ] }
{ "_id" : 2, "text" : "IWGXXFPHSI", "usuario" :
"bertoldo", "seguidores" : 1320 }, "RT" : false,
: [ "aniceto", "herminia", "bertoldo" ] }
…
{ "nick" :
"mentions"
{ "nick" :
"mentions"
Podría pensarse que esta consulta es incoherente con lo que hemos dicho
anteriormente, porque la clave usuario.nick aparece dos veces. Sin embargo,
no lo es, porque la clave aparece en dos documentos distintos. Los operadores
$and, $or y $nor llevan en su lado derecho un array de documentos que pueden considerarse independientes entre sí.
3.3.4.3.3. Arrays
Las consultas sobre arrays en MongoDB son muy potentes y flexibles, pero
también generan a menudo confusión. El principio inicial es fácil; si un
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
95
documento incluye por ejemplo a:[1,2,3,4], es lo mismo que si incluyera, a la
vez, a:1, a:2, a:3 y a:4. Por ello, si queremos ver la clave mentions de aquellos
tweets que mencionan a alguien llamado, por ejemplo, Aniceto, nos bastará
con escribir:
> db.tweets.find({mentions:"aniceto"}, {mentions:1})
{ "_id" : 2, "mentions" : [ "aniceto", "herminia", "bertoldo" ] }
{ "_id" : 3, "mentions" : [ "aniceto" ] }
{ "_id" : 5, "mentions" : [ "bertoldo", "herminia", "aniceto" ] }
La clave mentions es un array y la selección mentions: "aniceto"
indica «selecciona aquellos documentos en los que mentions o bien sea “aniceto”, o sea un array que contiene “aniceto” entre sus elementos».
Esto nos permite seleccionar documentos cuyos arrays contienen elementos concretos de forma sencilla. Igualmente podemos preguntar por los tweets
que no mencionan a Aniceto:
>
{
]
{
db.tweets.find({'mentions':{$ne:"aniceto"}}, {mentions:1})
"_id" : 1, "mentions" : [ "herminia", "melibea", "bertoldo"
}
"_id" : 4, "mentions" : [ "bertoldo", "melibea" ] }
Sin embargo, también tiene algunos resultados un tanto desconcertantes.
Consideremos el siguiente ejemplo:
> db.arrays.drop()
> db.arrays.insert({a:[10,20,30,40]})
> db.arrays.find({a:{$gt:20,$lt:30}})
{ "_id" :ObjectId("5b2f8c8080115a9b4011dd8c"), a:[ 10, 20,
30, 40 ] }
La consulta podría estar preguntando si hay un elemento mayor que 20 y
menor que 30. Parece no haber ninguno, pero, sin embargo, la consulta ha
tenido éxito. ¿Qué ha ocurrido? Pues que, en efecto, el array a tiene un valor
mayor que 20 (por ejemplo, 30) y otro menor que 30 (por ejemplo, 20). Es
96
CUADERNOS METODOLÓGICOS 60
decir, cada condición de la selección se cumple para un elemento diferente
del array a.
¿Podemos lograr que se apliquen las dos condiciones al mismo elemento?
Para esto existe un operador especial, $elemMatch:
> db.arrays.find({a:{$elemMatch:{$gt:20,$lt:30}}})
En este caso no obtenemos respuesta, porque ningún elemento del array
está entre 20 y 30.
Existen otros operadores para arrays, como $all, que selecciona documentos con una clave de tipo array que contenga (al menos) todos los elementos especificados en una lista. Contamos igualmente con $in, que busca que
el array del documento tenga al menos un elemento de una lista, o $nin, que
requiere que cierta clave de tipo array no tenga ninguno de los elementos indicados. También se pueden hacer consultas con condiciones sobre la longitud
del array. Por ejemplo, la siguiente, que selecciona los tweets con al menos
tres menciones:
> db.tweets.find({'mentions':{$size:3}}).count()
22
3.3.4.3.4. $exists
Como hemos visto, diferentes documentos de la misma colección pueden
tener claves diferentes. Este operador nos permite seleccionar aquellos documentos que sí tienen una clave concreta. Veamos un ejemplo. Supongamos
que queremos mostrar los tweets ordenados por la clave origen, de menor a
mayor:
> db.tweets.find().sort({origen:1})
{ "_id" : 1, "text" : "GKAXRKDQKV", "usuario" :
"herminia", "seguidores" : 5320 }, "RT" : false,
: [ "herminia", "melibea", "bertoldo" ] }
{ "_id" : 2, "text" : "IWGXXFPHSI", "usuario" :
"bertoldo", "seguidores" : 1320 }, "RT" : false,
: [ "aniceto", "herminia", "bertoldo" ] }
…
{ "nick" :
"mentions"
{ "nick" :
"mentions"
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
97
Observamos que los primeros documentos que se muestran no contienen
la clave origen. La causa es que, cuando una clave no existe, MongoDB la considera como de valor mínimo, y, por tanto, estos tweets aparecerán los primeros. Para evitar que aparezcan documentos que no contienen una clave,
debemos utilizar el operador $exists:
> db.tweets.find({origen:{$exists:1}}).sort({origen:1})
{ "_id" : 3, "text" : "RT: UFBFDYKXUK", "usuario" : { "nick"
: "melibea", "seguidores" : 411 }, "RT" : true, "origen" : 1,
"mentions" : [ "aniceto" ] }
{ "_id" : 29, "text" : "RT: VGMQCGYLKS", "usuario" : { "nick"
: "bertoldo", "seguidores" : 1320 }, "RT" : true, "origen" :
1, "mentions" : [ ] }
El valor 1 tras $exists selecciona solo los documentos que tienen este
campo. Un valor 0 seleccionaría solo los que no lo tienen.
3.3.5. find en Python
Recordemos que, como hemos visto, las bases de datos siguen una arquitectura cliente-servidor. Hasta ahora hemos estado utilizando el cliente por
defecto, que es la consola de MongoDB, que se inicia con el comando mongo.
Sin embargo, lo normal es que queramos escribir programas complejos, por
ejemplo, desde Python, que accedan a la base de datos directamente. Para ello
utilizaremos el cliente proporcionado por la biblioteca de Python llamada
pymongo.
Si lo que se desea es acceder tan solo al primer documento que cumpla los
criterios se suele utilizar find_one:
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:28000/')
db = client['twitter']
tweets = db['tweets']
tweet
=
tweets.find_one({"usuario.nick":'bertoldo'},
{'text':1,'_id':0})
print(tweet)
{'text': 'IWGXXFPHSI'}
98
CUADERNOS METODOLÓGICOS 60
En la consola también se puede usar esta misma función, bajo el nombre
de findOne, para obtener el primer resultado que cumpla la selección.
En muchos ejemplos veremos que se renuncia al uso de la proyección ya
que esta se puede realizar fácilmente desde Python. Por ejemplo, podemos
reemplazar las dos últimas instrucciones por
tweet = tweets.find_one({'usuario.nick': "bertoldo"})
print('text: ',tweet['text'])
text: IWGXXFPHSI
Si en lugar de un tweet queremos tratar todos los que cumplan las condiciones indicadas, usaremos directamente find, que nos devolverá un objeto
de tipo pymongo.collection.Collection que podemos iterar con una instrucción for:
for t in tweets.find({'usuario.nick':"bertoldo",
'mentions': "herminia"}):
print(t['text'])
De esta forma, podemos combinar toda la potencia de un lenguaje como
Python con las posibilidades de las consultas ofrecidas por MongoDB.
Un consejo: siempre que podamos, debemos dejar a la base de datos la
tarea de resolver las consultas, evitando la «tentación» de hacer nosotros
mismos el trabajo en Python. Por ejemplo, en lugar del código anterior,
podríamos pensar en escribir:
for t in tweets.find():
if t['usuario']['nick']=="bertoldo" and
"herminia" in t['mentions']:
print(t['text'])
Esta consulta devuelve el mismo resultado que la anterior, pero presenta
varias desventajas en cuanto a eficiencia:
1.Hace que la colección completa «viaje» hasta el ordenador donde está el
cliente, para hacer a continuación el filtrado.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
99
2. No hará uso de índices, ni de las optimizaciones realizadas de forma
automática por el planificador de MongoDB.
Por tanto, siempre que sea posible, dejemos a MongoDB lo que es de
MongoDB.
3.3.6. Agregaciones
Ya hemos visto cómo escribir una gran variedad de consultas con find. Sin
embargo, no hemos visto aún cómo realizar operaciones de agregación, es
decir, cómo combinar varios documentos agrupándolos según un criterio
determinado.
En MongoDB, esta tarea es realizada por la función aggregate. Realmente aggregate es más que una función de agregación, permite realizar consultas complejas que no son posibles con find, incluso si no implican
agregación.
3.3.6.1. El pipeline
La función aggregate se define mediante una serie de etapas consecutivas.
Cada etapa tiene que realizar un tipo de operación determinado (hay más de
veinticinco tipos). La forma general es la siguiente:
db.tweet.aggregate([etapa1, …, etapan])
La primera etapa toma como entrada la colección a la que se aplica la función aggregate, en este ejemplo, tweet. La segunda etapa toma como
entrada el resultado de la primera etapa, y así sucesivamente. A esta estructura es a la que se conoce como «pipeline de agregación en MongoDB». A continuación, presentamos las etapas principales.
3.3.6.1.1. $group
La etapa «reina», permite agrupar elementos y realizar operaciones sobre
cada uno de los grupos. El valor por el que debemos agrupar será el _id del
documento generado.
Como ejemplo, vamos a contar el número de tweets que ha emitido cada
usuario:
100
CUADERNOS METODOLÓGICOS 60
db.tweets.aggregate(
[
{$group:
{ _id:"$usuario.nick",
num_tweets:{$sum:1}
}
}
]
)
{
{
{
{
"_id"
"_id"
"_id"
"_id"
:
:
:
:
"melibea", "num_tweets" : 18 }
"bertoldo", "num_tweets" : 29 }
"aniceto", "num_tweets" : 28 }
"herminia", "num_tweets" : 25
Esta consulta solo tiene una etapa, de tipo $group. El valor de agrupación
es usuario.nick. Llama la atención que el nombre de la clave venga precedido
del valor $. Esto es necesario siempre que se quiera referenciar una clave en
el lado derecho.
Por tanto, la etapa considera todos los elementos de la colección tweets, y
los agrupa por el nick del usuario. Esto da lugar a cuatro grupos. Ahora, para
cada grupo, se crea el campo num_tweets, sumando uno por cada elemento
del grupo. El resultado es el valor buscado.
El uso de $sum:1 es tan común que a partir de la versión 3.4 MongoDB
incluye una etapa que hace esto sin que se necesite escribirlo explícitamente.
Se llama $sortByCount:
db.tweets.aggregate([
{$sortByCount: "$usuario.nick"}
])
{
{
{
{
"_id"
"_id"
"_id"
"_id"
:
:
:
:
"bertoldo", "count" : 29 }
"aniceto", "count" : 28 }
"herminia", "count" : 25 }
"melibea", "count" : 18 }
En $group el atributo _id puede ser compuesto, lo que permite agrupar
por más de un criterio. Por ejemplo, queremos saber para cada usuario cuántos de sus tweets son originales (RT:false), así como cuántos retweets (RT:true).
Podemos obtener esta información así:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
101
db.tweets.aggregate(
[
{$group:
{ _id:{nick:"$usuario.nick", RT:"$RT"},
num_tweets:{$sum:1}
}
}
]
)
{ "_id" : { "nick" : "aniceto", "RT" : false }, "num_tweets"
: 19 }
{ "_id" : { "nick" : "aniceto", "RT" : true }, "num_tweets"
: 9 }
{ "_id" : { "nick" : "melibea", "RT" : false }, "num_tweets"
: 10 }
{ "_id" : { "nick" : "melibea", "RT" : true }, "num_tweets"
: 8 }
{ "_id" : { "nick" : "bertoldo", "RT" : true }, "num_tweets"
: 15 }
{ "_id" : { "nick" : "bertoldo", "RT" : false }, "num_tweets"
: 14 }
{ "_id" : { "nick" : "herminia", "RT" : true }, "num_tweets"
: 10 }
{ "_id" : { "nick" : "herminia", "RT" : false }, "num_tweets"
: 15 }
Además de $sum, se pueden utilizar otros operadores como $avg (media),
$first (un valor del primer documento del grupo), $last (un valor del
último elemento del grupo), $max (máximo) y $min (mínimo), y dos operadores especiales: $push y $addToSet.
$push genera, para cada elemento del grupo, un elemento de un array.
Para ver un ejemplo, supongamos que para cada usuario queremos agrupar
todos los textos de sus tweets en un solo array.
db.tweets.aggregate(
[
{$group:
{ _id:"$usuario.nick",
textos:{$push:"$text"}
}
}
]
)
102
CUADERNOS METODOLÓGICOS 60
{ "_id" : "melibea", "textos" : [ "RT: UFBFDYKXUK", "BVDZDRGDLP", "BTSVWZSTVX", … ] }
…
$addToSet es similar, con la salvedad de que no repite elementos. Es
decir, considera el array como un conjunto.
Para terminar con esta etapa hay que mencionar el «truco» utilizado de
forma habitual para el caso en el que se quiera considerar toda la colección
como un único grupo. Por ejemplo, supongamos que queremos conocer el
número medio de menciones entre todos los tweets de la colección:
db.tweets.aggregate(
[
{$group:
{ _id:null,
menciones:{$avg:{$size:"$mentions"}}
}
}
]
)
{ "_id" : null, "menciones" : 1.46 }
La idea es que el valor null (en realidad se puede poner cualquier constante, 0, true, o «tururú») se evalúa al mismo valor para todos los documentos,
esto es, a null. De esta forma, todos los documentos pasan a formar un único
grupo y ahora se puede aplicar la media del número de menciones.
3.3.6.1.2. $match
Esta etapa sirve para filtrar documentos de la etapa anterior (o de la colección, si es la primera etapa). Supongamos que queremos ver el total de tweets
por usuario, pero solo estamos interesados en aquellos con más de veinte
tweets. Podemos escribir:
db.tweets.aggregate([
{$sortByCount: "$usuario.nick"},
{$match: {count:{$gt:20}} }
])
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
103
{ "_id" : "bertoldo", "count" : 29 }
{ "_id" : "aniceto", "count" : 28 }
{ "_id" : "herminia", "count" : 25 }
Tal y como hemos visto en el apartado anterior, la primera etapa genera
una salida con cuatro documentos, uno por usuario, y cada usuario, con dos
claves: _id, que contiene el nick del usuario, y count, que tiene el total de documentos asociados a ese _id. Por eso, la segunda etapa selecciona aquellos
documentos que tienen un valor count mayor de veinte.
3.3.6.1.3. $project
Esta etapa se encarga de formatear la salida. A diferencia de la proyección
de find, no solo permite incluir claves que ya existen, sino crear claves nuevas, lo que hace que sea más potente. Por ejemplo, la siguiente instrucción
conserva únicamente el campo usuario y, además, crea un nuevo campo
numMentions, que contendrá el tamaño del campo mentions, que es un
array:
db.tweets.aggregate( [
{
$project: {
usuario: 1,
_id:0,
numMentions: {$size:"$mentions"} }}
])
{ "usuario" :
"numMentions"
{ "usuario" :
"numMentions"
…
{
:
{
:
"nick" : "herminia", "seguidores" : 5320 },
3 }
"nick" : "bertoldo", "seguidores" : 1320 },
3 }
3.3.6.1.4. Otras etapas: $unwind, $sample, $out...
Veamos ahora otras etapas usadas a menudo. En primer lugar, $unwind
«desenrolla» un array, convirtiendo cada uno de sus valores en un documento individual. El resultado se entiende mejor a través de un pequeño
ejemplo.
104
CUADERNOS METODOLÓGICOS 60
db.unwind.drop()
db.unwind.insert({_id:1, a:1, b:[1,2,3]})
db.unwind.insert({_id:2, a:2, b:[4,5]})
db.unwind.aggregate([{$unwind:"$b"}])
{
{
{
{
{
"_id"
"_id"
"_id"
"_id"
"_id"
:
:
:
:
:
1,
1,
1,
2,
2,
"a"
"a"
"a"
"a"
"a"
:
:
:
:
:
1,
1,
1,
2,
2,
"b"
"b"
"b"
"b"
"b"
:
:
:
:
:
1
2
3
4
5
}
}
}
}
}
La utilidad de este operador se aprecia con más claridad cuando se combina con otros, tal y como veremos en la siguiente sección.
Otro operador sencillo, pero a veces muy conveniente, es $sample. Este
operador toma una muestra aleatoria de una colección. Algo muy útil para
análisis en ciencia social. Su sintaxis es muy sencilla:
db.tweets.aggregate( [ { $sample: { size: 2 } } ] )
{ "_id" : 21, "text" : "DTCWGGMCLH", "usuario" :
"bertoldo", "seguidores" : 1320 }, "RT" : false,
: [ "melibea", "bertoldo", "herminia" ] }
{ "_id" : 20, "text" : "RT: LWXLFLEXZT", "usuario"
: "bertoldo", "seguidores" : 1320 }, "RT" : true,
17, "mentions" : [ "herminia" ] }
{ "nick" :
"mentions"
: { "nick"
"origen" :
El parámetro size indica el tamaño de la muestra. Finalmente, hay que
mencionar otra etapa muy sencilla pero casi imprescindible: $out. Este
operador almacena el resultado de las etapas anteriores como una nueva
colección. Siempre debe ser la última etapa del pipeline de agregación. Por
ejemplo:
db.tweets.aggregate( [ { $sample: { size: 3 } },
{ $out: "minitweets" } ] )
crea una nueva colección, minitweets con una muestra de tres documentos
tomados de forma aleatoria de la colección tweets. Como vemos, este pipeline
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
105
consta de dos etapas, y, tal y como hemos indicado, $out es la última, que
corresponde con la segunda en este caso.
Además de las ya vistos, existen otras etapas con significado análogo al de
las funciones equivalentes en find: $sort, $limit y $skip.
3.3.6.1.5. $lookup
En MongoDB la mayoría de las consultas afectan a una sola colección. Si
nuestra base de datos estuviera en el modelo relacional hubiéramos utilizado
dos tablas, una de usuarios y otra de tweets, que se combinarían con operaciones join cuando hiciera falta.
En Mongo se prefiere combinar las dos tablas en una sola colección desde
el principio, y evitar estas operaciones join, que suelen resultar costosas en
bases de datos NoSQL. Sin embargo, en ocasiones no hay más remedio que
combinar dos colecciones en la misma consulta. En estos casos es cuando la
etapa $lookup tiene sentido.
Supongamos que para cada retweet queremos saber el _id del retweet, el
usuario que lo ha emitido y el usuario que envió el tweet original. Para entender lo que debemos hacer consideremos un retweet cualquiera:
db.tweets.findOne({RT:true})
{
"_id" : 3,
"text" : "RT: UFBFDYKXUK",
"usuario" : {
"nick" : "melibea",
"seguidores" : 411
},
"RT" : true,
"origen" : 1,
"mentions" : [
"aniceto"
]
}
Ya tenemos el _id del retweet (3) y el usuario (Melibea). Sin embargo, nos
falta el nombre del usuario que emitió el tweet original. Para encontrarlo
debemos buscar en la colección tweets un documento cuyo _id sea el mismo
que el que indica la clave origen.
La estructura general de $lookup será la siguiente:
106
CUADERNOS METODOLÓGICOS 60
{
$lookup:
{
from: <colección a combinar>,
localField: <clave de los documentos origen>,
foreignField: <clave de los documentos de la colección
"from">,
as: <nombre del campo array generado>
}
}
En nuestro ejemplo la colección from será la propia tweets. El localField será origen, y el foreignField, la clave _id (el del tweet original).
En la clave as debemos dar el nombre de una nueva clave. A esta clave se asociará un array con todos los tweets cuyo _id coincida con el del retweet.
El ejemplo completo:
db.tweets.aggregate([
{ $match: {RT:true } },
{
$lookup:
{
from: "tweets",
localField: "origen",
foreignField: "_id",
as: "tweet_original"
}
},
{ $unwind:"$tweet_original"},
{ $project:{_id:"$_id",emitido:"$usuario.nick",
fuente:"$tweet_original.usuario.nick"}}
])
{
{
{
{
}
…
"_id"
"_id"
"_id"
"_id"
:
:
:
:
3, "emitido" : "melibea", "fuente" : "herminia" }
4, "emitido" : "bertoldo", "fuente" : "melibea" }
9, "emitido" : "herminia", "fuente" : "melibea" }
11, "emitido" : "bertoldo", "fuente" : "herminia"
Para este proceso hemos necesitado cuatro etapas. En la primera, hemos
filtrado por RT a true. En la segunda, se ha añadido la información del tweet
original. Seguidamente, desplegamos el array «tweet_original», que sabemos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
107
que solo contiene un documento. Finalmente usamos $project para formatear la salida.
3.3.6.2. Ejemplo: usuario más mencionado
Queremos saber cuál es el usuario que ha recibido más menciones dentro de
la colección tweets, pero teniendo en cuenta solo tweets originales. La idea
sería agrupar por la clave mentions, pero es un array, así que tendremos primero que desplegar el array usando unwind.
db.tweets.aggregate([
{$match:{"RT":true}},
{$unwind:"$mentions"},
{$sortByCount: "$mentions"},
])
{
{
{
{
"_id"
"_id"
"_id"
"_id"
:
:
:
:
"bertoldo", "count" : 15 }
"herminia", "count" : 14 }
"aniceto", "count" : 13 }
"melibea", "count" : 12 }
La primera etapa ($match) filtra los tweets para quedarnos solo con los
que tienen la clave RT a true. La segunda etapa utiliza $unwind para convertir cada mención, que ahora es un array, en un solo documento. Finalmente,
en la tercera etapa ($sortByCount), contamos el número de menciones y
ordenamos por el resultado.
3.3.7. Vistas
Las vistas nos permiten nombrar una consulta de forma que queda asociada
al nombre elegido, y se ejecuta cada vez que es invocada. Veamos un ejemplo,
tomado del apartado anterior:
db.createView("mencionesOriginales","tweets",
[
{$match:{"RT":true}},
{$unwind:"$mentions"},
{$sortByCount: "$mentions"},
])
108
CUADERNOS METODOLÓGICOS 60
El primer parámetro es el nombre de la vista que crear, el segundo, el
nombre de la colección de partida, y, finalmente, el tercero es un pipeline de
agregación. El resultado es aparentemente similar a la creación de una
nueva colección:
show collections
…
mencionesOriginales
…
Sobre la que se puede hacer find:
db.mencionesOriginales.find()
{ "_id" : "bertoldo", "count" : 15 }
{ "_id" : "herminia", "count" : 14 }
{ "_id" : "aniceto", "count" : 13 }
{ "_id" : "melibea", "count" : 12 }
Sin embargo, debemos recordar que cada vez que se hace find sobre una
vista se ejecuta la consulta asociada. Esto hace que el resultado de hacer find
sobre la vista cambie si se modifica el contenido de la colección de partida
(tweets), y también, por supuesto, que su eficiencia sea menor que la consulta
sobre una colección normal, ya que implica ejecutar el pipeline de agregación
asociado.
3.3.8. Update y remove
Para finalizar, veamos estas dos operaciones que permiten modificar o eliminar documentos ya existentes, respectivamente.
3.3.8.1. Update total
La forma más sencilla de modificar un documento es simplemente reemplazarlo por otro. En este caso update tiene dos argumentos. El primero selecciona el elemento que modificar y el segundo es el documento por el que se
sustituirá. Veamos un ejemplo basado en la siguiente pequeña colección:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
109
use astronomia
db.estelar.insert({_id:1, nombre:"Sirio", tipo:"estrella",
espectro:"A1V"})
db.estelar.insert({_id:2, nombre:"Saturno", tipo:"planeta"})
db.estelar.insert({_id:3, nombre:"Plutón", tipo:"planeta"})
Queremos cambiar el tipo de Plutón a «Planeta Enano». Podemos hacer:
db.estelar.update({_id:3}, {tipo:"planeta enano"})
db.estelar.find({_id:3}
{ "_id" : 3, "tipo" : "planeta enano" }
Tras la modificación se ha perdido la clave nombre. ¿Qué ha ocurrido?
Pues sencillamente que, tal y como hemos dicho, en un update total, debemos proporcionar el documento completo, pero solo hemos proporcionado el
tipo (y MongoDB ha mantenido el _id, que es la única clave que no puede
modificarse). Para no perder datos deberíamos haber escrito lo siguiente:
db.estelar.update({_id:3}, {
nombre:"Plutón",
tipo:"planeta enano"})
Parece entonces que este tipo de updates no son útiles, si nos obligan a
reescribir el documento completo. Sin embargo, sí son interesantes cuando,
en lugar de usar la consola, utilizamos Python. Veamos el mismo ejemplo,
pero a través de pymongo. Empezamos preparando la base de datos:
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:28000/')
db = client['astronomia']
estelar = db['estelar']
estelar.drop()
estelar.insert_many([
{'_id':1,'nombre':"Sirio",'tipo':"estrella", 'espectro':"A1V"},
{'_id':2,'nombre':"Saturno", 'tipo':"planeta"},
{'_id':3,'nombre':"Plutón",'tipo':"planeta"} ] )
110
CUADERNOS METODOLÓGICOS 60
En la preparación de la base de datos hemos utilizado la función insert_
many, que permite insertar un array de documentos en la misma instrucción.
Ahora ya podemos hacer el update total. En el caso de pymongo, este tipo
de operación lleva el muy adecuado nombre de replace, en este caso particular, replace_one:
pluton = estelar.find_one({'_id':3})
pluton['tipo'] = "planeta enano"
estelar.replace_one({'_id':pluton['_id']},pluton)
En este caso, como podemos ver, primero «cargamos» el documento que
modificar mediante find_one, lo modificamos, y lo devolvemos a la base de
datos a través de replace_one. La diferencia con la consola está en que en
ningún momento hemos tenido que escribir el documento entero.
En todo caso, las modificaciones se realizan mejor mediante los updates
parciales, que mostramos a continuación.
3.3.8.2. Update parcial
A diferencia del update total, en el parcial, en lugar de reemplazar el documento completo, se especifica qué cambios queremos realizar.
db.estelar.updateOne( {nombre:"Plutón"},
{$set : { tipo: "planeta enano"}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount"
: 1 }
El operador $set indica que se va a listar una serie de claves y que
estas deben modificarse con los valores que se indican. La respuesta de
MongoDB nos indica que ha encontrado un valor con el filtro requerido
({nombre:"Plutón"}) y que se ha modificado. Podría ser que no se
modificara, por ejemplo, si MongoDB comprueba que ya tiene el valor
indicado. El valor matchedCount nunca valdrá más de 1 en el caso de
updateOne, llamado update_one en pymongo, porque esta función se
detiene al encontrar la primera coincidencia, es decir, como su propio
nombre indica, updateOne modifica un solo documento (o cero si no
encuentra el elemento).
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
111
Si se persigue modificar todos los documentos que cumplan una determinada condición, se debe utilizar updateMany desde la consola (update_
many en pymongo).
db.estelar.updateMany( {}, {$currentDate : { fecha: true}})
{ "acknowledged" : true, "matchedCount" : 3, "modifiedCount"
: 3 }
En este caso se seleccionan todos los documentos (filtro {}, primer argumento de updateMany) y a cada uno de ellos se le añade una clave nueva
fecha con la fecha actual. Para esto, en lugar del operador $set, utilizamos
$currentDate, que añade la fecha actual con el nombre de clave indicado.
Además de $set y $currentDate, hay muchos otros operadores de interés. Por ejemplo $rename, es muy útil para renombrar claves. Si deseamos
que la clave «tipo» pase a llamarse «clase», utilizaremos:
db.estelar.updateMany( {}, { $rename: { "tipo": "clase" } } )
{ "acknowledged" : true, "matchedCount" : 3, "modifiedCount"
: 3 }
También es de interés el operador de modificación $unset, que permite
eliminar claves existentes. En caso de desear eliminar la clave «espectro» del
documento asociado a la estrella «Sirio», podemos escribir:
db.estelar.updateOne( {nombre:"Sirio"},
{ $unset: { "espectro": true } } )
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount"
: 1 }
Puede chocar la utilización del valor true. En realidad, se puede poner
cualquier valor, pero no se puede dejar en blanco porque debemos respetar la
sintaxis de JSON.
Otro grupo de operadores interesante son los aritméticos: $inc, $max,
$min y $mul, que permiten actualizar campos. Para ver su funcionamiento,
supongamos que tenemos una colección de productos con elementos de la
siguiente forma:
112
CUADERNOS METODOLÓGICOS 60
db.productos.insert({_id:"123", cantidad:10, vendido:0})
y que queremos registrar una venta, incrementando el valor de la clave «vendido», y decrementando en 1 la cantidad de valores de este producto que tenemos en el almacén.
db.productos.update(
{ _id: "123" },
{ $inc: { almacen: -1, vendido: 1 } }
)
El operador $inc también es muy interesante para actualizar contadores
de visitas en páginas web.
3.3.8.3. Upsert
Supongamos que queremos asegurarnos de que en nuestro caso el sujeto de
estudio «Bertoldo» ya no aparece en nuestra colección. En ese caso, podemos
escribir:
db.sujetos.updateOne({nombre:'Bertoldo'},{$set:{baja:true}})
{ "acknowledged" : true, "matchedCount" : 0, "modifiedCount"
: 0 }
Puede suceder que, aunque Bertoldo haya decidido darse de baja, no conste
en nuestras bases de datos, y que, por tanto, el update anterior no tenga efectos.
Sin embargo, incluso en este caso queremos apuntar la baja, por ejemplo, para
evitar futuras acciones sobre un sujeto cuyos datos sabemos que son erróneos.
Esta situación, donde queremos modificar el documento si existe y crearlo si
no existe, se conoce en MongoDB, y, en general, en el mundo de las bases de
datos, como upsert, y se indica añadiendo un parámetro adicional a update:
db.sujetos.updateOne({nombre:'Bertoldo'},
{$set:{baja:true}},{upsert:true})
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
113
{
"acknowledged"
"matchedCount"
"modifiedCount"
"upsertedId" :
: true,
: 0,
: 0,
ObjectId("5b312810ceb80d4e45f08445")
}
Mongo nos informa de que ningún documento cumple nuestra selección y de
que ninguno ha sido modificado, pero también nos da el _id del objeto insertado,
indicando que la operación ha resultado en la creación de un nuevo documento.
En ocasiones nos interesará añadir una clave al documento asociado a un
upsert, pero solo en el caso en el que se haya insertado el documento, es
decir, solo si no existía con anterioridad. Esto se logra mediante el operador
$setOnInsert.
En el caso del sujeto al que se da de baja, podemos suponer que todos los
sujetos tienen una clave de permanencia con el número de meses que hace
que fueron incluidos en la base de datos. En el caso de que al darse de baja no
exista, puede interesarnos poner este valor a 0:
db.sujetos.updateOne({nombre:'Bertoldo'},{$set:{baja:true},
$setOnInsert:{permanencia:0}},{upsert:true})
{
"acknowledged" : true,
"matchedCount" : 0,
"modifiedCount" : 0,
"upsertedId" : ObjectId("5b3129f2ceb80d4e45f084ac")
}
Podemos comparar el resultado (asumiendo que Bertoldo no estaba y se ha
producido una inserción):
db.sujetos.find()
{ "_id" : ObjectId("5b3129f2ceb80d4e45f084ac"),
"nombre" : "Bertoldo", "baja" : true, "permanencia" : 0 }
3.3.8.4. Remove
Eliminar documentos de una colección resulta muy sencillo en MongoDB.
Basta con que indiquemos un criterio de selección, y el sistema eliminará
todos los documentos de la colección que deseemos:
114
CUADERNOS METODOLÓGICOS 60
> db.estelar.remove({clase:'planeta enano'})
WriteResult({ "nRemoved" : 2 })
Si solo deseamos eliminar un documento, el primero encontrado que verifique las condiciones, podemos añadir la opción justone:
> db.estelar.remove({clase:'planeta'},{justOne:true})
WriteResult({ "nRemoved" : 1 })
Si utilizamos como selección el documento vacío ({}) borraremos la colección completa. Podemos preguntarnos cuál es la diferencia —si la hay— con
llamar a la función drop(). La diferencia es que remove eliminará los documentos uno a uno, de forma que al final tendremos una colección vacía, en la
que, por ejemplo, los índices asociados seguirán existiendo. En cambio, drop()
eliminará por completo la colección y todos sus objetos asociados.
4
Tratamiento y análisis
computacional de datos
En este capítulo se abordarán las técnicas de corte estadístico y matemáticocomputacional que subyacen en el tratamiento y el análisis de datos en entornos
big data. Estas técnicas de análisis y tratamiento constituyen un complemento
de las técnicas de obtención y almacenamiento de ingentes cantidades de datos
que se han descrito en capítulos anteriores, permitiendo procesar y aprovechar
la información disponible, más allá de su almacenamiento y recuperación, de
cara a la extracción de conocimiento útil y la creación de valor añadido.
En este sentido, las técnicas aquí descritas llevan a cabo, en el entorno del
tratamiento de datos masivos, un papel similar al que juegan las técnicas estadísticas clásicas en el análisis de datos «estándar», esto es, para volúmenes de
datos manejables en un único ordenador (o incluso manualmente): la construcción, mediante herramientas matemáticas y/o computacionales, de modelos explicativos y/o predictivos, que generalicen la información disponible y
extraigan de ella patrones relevantes para la comprensión de la realidad y la
toma de decisiones en el contexto de aplicación del que provienen los datos.
Es importante señalar que gran parte de las técnicas de análisis y tratamiento de información que se describen a continuación no se han desarrollado necesariamente en el contexto de su utilización en entornos big data bajo
el paradigma del escalamiento horizontal. Así, la mayoría de estas técnicas
han sido desarrolladas históricamente en el contexto del análisis y el tratamiento de datos estándar. En este sentido, es perfectamente posible usar estas
técnicas fuera de un entorno big data, o al menos en un entorno big data no
caracterizado por la V de volumen. Sin embargo, una característica importante de las técnicas aquí tratadas es que pueden ser escaladas horizontalmente, esto es, ser implementadas en paralelo por medio de un clúster de
ordenadores, de modo que sus tiempos de cómputo y sus requisitos de memoria sean asequibles en el caso de tratar con conjuntos de datos masivos.
De este modo, el objetivo de este capítulo es proporcionar al científico
social una visión general de un conjunto de metodologías de análisis de datos
116
CUADERNOS METODOLÓGICOS 60
con las que, por su novedad o por su desarrollo en contextos más informáticos
(o menos estadísticos), no suele estar familiarizado, y que tienen cada vez una
mayor relevancia y difusión por sus resultados prácticos, su flexibilidad de
aplicación y su adaptabilidad a entornos de datos masivos. Esto no significa
que las técnicas estadísticas más tradicionalmente empleadas en el contexto
de las ciencias sociales, como la regresión lineal, la logística, el análisis discriminante y/o alguna de sus variantes, no sean aptas para su empleo en entornos big data. Al contrario, estas técnicas más clásicas se han demostrado
perfectamente adaptables a este tipo de entorno. Simplemente, en este manual
nuestro objetivo es hacer hincapié en técnicas quizá menos conocidas en el
campo de las ciencias sociales, pero que están recibiendo una gran atención
en otros campos, como la biología, la ingeniería informática o la física.
La exposición sobre estas técnicas de análisis y tratamiento de datos se
dividirá en dos partes, atendiendo principalmente al tipo de datos con los que
se ha de tratar en cada caso. Así, en primer lugar, se describirán diversas técnicas de aprendizaje automático (machine learning, en inglés) diseñadas para
tratar con conjuntos de datos tradicionales, en el sentido de que se dispone de
un conjunto de variables independientes o explicativas, normalmente numéricas (aunque también pueden ser categóricas), con las que se pretende modelizar una relación de dependencia con una o más variables dependientes o
target, que pueden tener naturaleza numérica (dando lugar a un problema de
regresión, por su analogía con el modelo de regresión lineal tradicional) o
categórica (dando lugar, entonces, a un problema de clasificación, el tipo de
problema que trata la regresión logística).
4.1. Machine learning o aprendizaje automático
El término «aprendizaje automático», traducido del inglés machine learning,
hace referencia a un área de la informática y las ciencias de la computación
que trata de la construcción de modelos y programas informáticos que «aprenden» a resolver problemas potencialmente complejos a partir de ejemplos o
datos, que se toman como input, mediante algún tipo de mecanismo inductivo. En este contexto, por aprendizaje se entiende la capacidad de estos programas de mejorar progresivamente su rendimiento en la solución de
problemas específicos a medida que se les va suministrando un conjunto
mayor de datos o ejemplos de partida.
Originalmente, a finales de la década de los años cincuenta del siglo pasado,
cuando se introdujo el término machine learning, se relacionaba este tipo de
metodología con las entonces incipientes capacidades de la inteligencia artificial para atacar problemas que, siendo relativamente sencillos de entender y/o
resolver por humanos, planteaban serias dificultades para su tratamiento computacional. Un ejemplo arquetípico de estos problemas es el reconocimiento
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
117
óptico de caracteres escritos (OCR, del inglés optical character recognition),
tales como letras, números y texto en general. Merece la pena detenerse un
poco en este ejemplo para entender mejor el cambio de enfoque que supone la
aparición de las técnicas de aprendizaje automático.
Reconocer dígitos escritos a mano es una tarea relativamente sencilla para
casi cualquier persona alfabetizada y con un mínimo de experiencia. Esto no significa que los humanos la realicen de manera totalmente perfecta, especialmente
cuando los dígitos que reconocer están afectados por alguna fuente de ruido,
como, por ejemplo, trazos borrosos, incompletos o provenientes de estilos de
escritura peculiares o muy diferentes al usado por la persona encargada de su
reconocimiento. Obsérvese la figura 4.1, a continuación, en la que se representan varios ejemplos de dígitos del 0 al 9. Supongamos que nuestro objetivo es
reconocer aquellos dígitos que representen un 2. Un observador humano reconocería fácil y correctamente varios de los dígitos presentados como ejemplos de
2. Y, posiblemente, podría cometer también algunos errores, asignando como
casos de 2 algunos dígitos que en realidad son, por ejemplo, un 3, un 6 o un 7.
Sin embargo, y esto es lo importante, a pesar de estas limitaciones y de los posibles casos particulares confusos, la tarea apenas plantea dificultades para una
persona con el mínimo de entrenamiento referido. El observador humano
entiende perfectamente la tarea que realizar y la puede llevar a cabo sin necesidad de herramientas especiales o sofisticadas, simplemente usando su conocimiento de lo que es un 2 y posiblemente algo de intuición en los casos más
confusos. Más que aplicar conscientemente un conjunto extenso de instrucciones para distinguir unos dígitos de otros, nuestro cerebro, fundamentalmente, se
basa en los modelos perceptivos de los diferentes dígitos aprendidos con la experiencia y en la similitud que guardan los dígitos para ser reconocidos con tales
modelos perceptivos.
Figura 4.1.
Ejemplos de dígitos para una tarea de reconocimiento de textos u OCR
118
CUADERNOS METODOLÓGICOS 60
Ahora supongamos que se quiere escribir un programa informático que
permita a un ordenador realizar esta misma tarea. Para ello, el programa debe
contener instrucciones muy precisas para reconocer aquellos dígitos que sean
un 2 y separarlos de aquellos que no lo sean. De algún modo, es necesario traducir al lenguaje informático nuestro modelo perceptivo del dígito 2. Pero
esto resulta una tarea enormemente compleja. Para empezar, porque no podemos observar realmente este modelo perceptivo, o este no proporciona un
conjunto claro de instrucciones y pasos que seguir para llevar a cabo la tarea
con éxito. Este conjunto de instrucciones tendría, además, que ser tremendamente extenso y detallado para que el ordenador pudiese afrontar con ciertas
garantías todas las posibles representaciones particulares que puede tener un
número 2. Este tipo de planteamiento, en el que se intenta explicar de manera
casi matemática al ordenador cómo reconocer lo que es un 2, contrasta fuertemente con la manera en que procedemos los humanos, que, simplemente,
«sabemos» lo que es un 2 porque hemos aprendido a reconocerlo a través de
la experiencia.
Dentro de esta analogía, el enfoque del aprendizaje automático intenta
conseguir que el ordenador sea capaz de reconocer dígitos procediendo de
manera similar a cómo lo hacen los humanos: aprendiendo a partir de ejemplos y la experiencia. En lugar de escribir un programa que detalle un conjunto casi infinito de instrucciones para reconocer todos los posibles casos de
un 2, la idea clave del aprendizaje automático es proporcionar al ordenador
una colección de ejemplos de dígitos con sus correspondientes etiquetas, las
cuales identifican qué dígito particular es cada uno de esos ejemplos, de
manera que, a través de un mecanismo de aprendizaje adecuado —un programa que utilice esos ejemplos y sus respectivas etiquetas para ajustar progresivamente un modelo de representación de los dígitos—, el ordenador
produzca entonces un programa que lleve a cabo el reconocimiento de los
dígitos de manera más o menos efectiva. Esto es, bajo este enfoque, el programa que realiza realmente el reconocimiento de los dígitos no es un input
que haya de ser producido por el programador informático, sino que ese programa es producido por un procedimiento de aprendizaje automático —otro
programa— que simplemente utiliza como input imágenes de dígitos correctamente etiquetadas.
Esta metodología de programación característica del aprendizaje automático, en que el programa objetivo —el que resuelve la tarea específica que se
quiere automatizar— no se escribe directamente, sino que es producido por
otro programa a partir de ejemplos adecuados, supone un salto relevante respecto al paradigma de programación tradicional, que requeriría que un programador humano proveyese directamente al ordenador de un programa que
resolviese la tarea específica. Esta diferencia clave entre los dos paradigmas de
programación se ilustra en la figura 4.2. En el caso de la programación tradicional, el ordenador precisa que se le introduzca un programa que especifique
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
119
cómo convertir los datos de entrada —en el ejemplo del OCR, dígitos para ser
reconocidos— en las respectivas salidas —las etiquetas que identifican cada
dígito—. En el caso del aprendizaje automático, el ordenador requiere un conjunto de datos ya etiquetados —siguiendo el ejemplo del OCR, las etiquetas
especifican qué dígito es realmente cada ejemplo de entrada— y un mecanismo
o algoritmo de aprendizaje, que producirá como output el programa objetivo
que especifica cómo asignar salidas —etiquetas— a nuevos datos de entrada
sin etiquetar —por ejemplo, otras colecciones de dígitos—. El programa objetivo así obtenido puede entonces usarse al modo tradicional —permitiendo
asignar a cada nuevo dato de entrada su correspondiente salida—, con la
importante diferencia de que ese programa no ha tenido que ser escrito por un
programador humano, sino que lo ha producido automáticamente el ordenador (junto con el correspondiente algoritmo de aprendizaje).
Figura 4.2.
Paradigma de programación tradicional vs. aprendizaje automático
Así pues, la característica distintiva de la metodología del aprendizaje
automático es el algoritmo o procedimiento de aprendizaje que se encarga de
generalizar los ejemplos de partida en un programa que resuelve la tarea específica que se está atacando. Antes de analizar con mayor detalle en la próxima
sección algunos de los conceptos clave que intervienen en el aprendizaje automático, es conveniente resaltar otros dos aspectos cruciales de estos procedimientos o algoritmos de aprendizaje.
En primer lugar, el algoritmo de aprendizaje no es necesariamente específico para la tarea que resolver. Esto es, un mismo procedimiento de aprendizaje puede usarse para resolver tareas muy diferentes, siempre que se disponga
de datos adecuadamente etiquetados específicos de cada tarea. De este modo,
el mismo procedimiento de aprendizaje que, por ejemplo, provee un OCR a
120
CUADERNOS METODOLÓGICOS 60
partir de dígitos adecuadamente etiquetados puede ser usado con mínimas
modificaciones para obtener un mecanismo para, e. g., la clasificación de la
polaridad de un tweet en relación con un partido político (Da Silva et al.,
2014), el reconocimiento de spam en emails (Guzella et al., 2009), la detección
de fraudes en transacciones comerciales (Phua et al., 2010), la identificación
automática de objetos astronómicos (Kremer et al., 2017), el diagnóstico
médico (De Bruijne, 2016), y un muy largo etcétera, siempre que se disponga
de ejemplos apropiados de estas tareas. En tanto que no es necesario crear de
la nada un procedimiento de aprendizaje para cada tarea, esta generalidad o
flexibilidad de los algoritmos de aprendizaje desplaza una parte importante
del foco hacia los datos, esto es, los ejemplos que proveen la información del
contexto específico de aplicación (i. e., la tarea práctica que resolver). En otras
palabras, dado un algoritmo de aprendizaje, la clave para poder aplicarlo a
resolver una tarea específica es tener datos o ejemplos adecuados de esa tarea
con que alimentar al algoritmo.
Y, en segundo lugar, de manera parecida a cómo los humanos podemos
reconocer dígitos sin saber cómo nuestro cerebro lo consigue, los procedimientos de aprendizaje y los programas objetivo que proporcionan pueden no
ser interpretables. En otras palabras, el algoritmo de aprendizaje puede comportarse como una caja negra, que ajusta una transformación de inputs (e. g.,
ejemplos que etiquetar) en outputs (e. g., etiquetas de esos ejemplos) sin que
realmente sea posible atribuir un sentido o interpretación práctica a esta
transformación o el modo en que se realiza. De este modo, en tanto podría no
ser posible confiar en un procedimiento de aprendizaje simplemente por la,
digamos, «lógica» o coherencia teórica o práctica de sus operaciones o transformaciones, su rendimiento en la resolución de una tarea dada se tiende a
valorar de una manera empírica, normalmente a través de su eficacia predictiva, la cual se mide en una colección de datos etiquetados no usados en el
ajuste o construcción del programa objetivo. Esto hace de los procedimientos
de aprendizaje automático instrumentos de marcado carácter práctico, que
pueden tener pocas o ninguna hipótesis teórica base que haya de verificarse
(al estilo, por ejemplo, de la normalidad o la homocedasticidad en el análisis
de residuos en la regresión lineal), y que están centrados en ser útiles o eficaces en términos de algún criterio empírico.
4.1.1. Conceptos preliminares
Esta sección está dedicada a describir diversos conceptos necesarios para la
comprensión y el uso de las técnicas de aprendizaje automático. Puede consultarse M. Kubat (2017) para profundizar en algunas de las cuestiones
aquí tratadas y obtener una visión más general del estado actual de esta
materia.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
121
4.1.1.1. Aprendizaje y optimización
Como ya se ha introducido en la discusión previa, el elemento central del aprendizaje automático es la existencia de un programa informático que «aprende» a
resolver una tarea específica a partir de datos adecuados. ¿Qué significa que un
programa aprenda? La definición clásica al respecto es la siguiente: «Un programa informático P se dice que aprende de la experiencia E en relación a una
clase de tareas T y una medida de rendimiento M si el rendimiento de P en las
tareas en T, medido por M, mejora con la experiencia E» (Mitchell, 1997).
Esta definición, aunque algo general y ambigua, tiene la virtud de conectar
el concepto de aprendizaje de un programa informático con el de «optimización» de una medida de rendimiento que mide la eficacia del programa en una
tarea dada. De esta manera, al optimizar esta medida, esto es, al obtener mejores valores de ella modificando ciertas partes del programa, se consigue en
principio un programa más eficaz en la resolución de esa tarea. Este procedimiento de optimización se realiza generalmente sobre una colección de parámetros que caracteriza la manera en que el programa informático transforma
los datos de entrada o inputs en los outputs requeridos 1. Esto lo veremos con
mayor claridad con los ejemplos que presentaremos más adelante.
En este enfoque, la medida de rendimiento que mide la eficacia del programa aprendido para resolver una tarea dada tiene obviamente una importancia central. Como se verá en la sección 4.1.2, al exponer diversos procedimientos
de aprendizaje automático, esta medida es siempre un elemento fundamental
de cada procedimiento específico, y cada procedimiento particular puede proveer su propia medida que optimizar. El mecanismo de optimización que se
emplea para guiar el aprendizaje del programa informático es también de gran
importancia. Dependiendo de las características de cada procedimiento, esta
optimización puede realizarse en un único paso, aplicando una fórmula que
proporcione los valores óptimos de los parámetros con base en los datos introducidos 2, o en pasos sucesivos, mediante algún tipo de procedimiento iterativo
heurístico convergente a un óptimo local o global 3.
Resumiendo las ideas anteriores de una manera más formal, es posible
considerar el programa informático que lleva a cabo un procedimiento de
Esta idea debería ser familiar para cualquiera que haya tratado con el ajuste de modelos
estadísticos: así, por ejemplo, el ajuste de un modelo de regresión lineal para un conjunto de
variables explicativas dado es equivalente a obtener los valores de los parámetros o coeficientes
de regresión asociados a esas variables que minimizan una función de error (típicamente la suma
de los residuos al cuadrado, hablándose en este caso de regresión por mínimos cuadrados).
2
Este es el caso en la regresión lineal por mínimos cuadrados.
3
Este es habitualmente el caso en la regresión logística, en que no suele ser posible obtener
fórmulas cerradas para los parámetros óptimos, de manera que el valor final de los parámetros
del modelo ha de obtenerse mediante algún procedimiento iterativo de optimización, típicamente
un algoritmo de descenso del gradiente.
1
122
CUADERNOS METODOLÓGICOS 60
aprendizaje automático como un mecanismo de optimización que, dado un
conjunto de ejemplos E, ajusta una transformación o programa objetivo:
P : X →Y ,
donde X es el espacio de datos de entrada o inputs e Y es el espacio de datos
de salida u outputs, de manera que P representa al programa informático que
transforma inputs x ∈ X en outputs P (x ) = y ∈ Y , y la optimización busca
obtener la mejor de estas transformaciones P en términos de una medida de
rendimiento M(P,E), que depende normalmente de la forma específica de P,
de los ejemplos de entrada E y de los outputs P(x) en que son transformados
los inputs x en E.
4.1.1.2. Tipos de aprendizaje
Dentro del campo del aprendizaje automático es habitual diferenciar varias
tipologías básicas de aprendizaje, típicamente dependientes de las características de los datos de partida (con los que se alimenta el procedimiento de
aprendizaje) y de los problemas prácticos que resolver. Quizá la distinción
más habitual, y la más importante en términos de los métodos de aprendizaje
presentados en este manual, es la que se da entre aprendizaje supervisado y
no supervisado. En este manual se hará un mayor énfasis en los métodos
supervisados, que cuentan con una mayor difusión y aplicación, y solo se dará
una breve introducción a los no supervisados.
4.1.1.2.1. Aprendizaje supervisado
El aprendizaje supervisado se relaciona con problemas en los que existe (al
menos) una variable objetivo o respuesta que se desea explicar o predecir a partir de un conjunto de otras variables explicativas, contándose con ejemplos que
proporcionan información sobre ambos grupos de variables. Esto es, cada uno
de estos ejemplos presenta un caso, registro o instancia con valores conocidos
de las variables explicativas y de la variable objetivo. En este contexto, la labor
del método de aprendizaje es la de generalizar de manera inductiva la relación
entre variables explicativas y objetivo observada en el conjunto de ejemplos disponible, o, al menos, la de proporcionar un mecanismo que, a partir del conjunto de ejemplos, permita asignar valores de la variable objetivo a nuevas
instancias, en las que solo se conoce el valor de las variables explicativas.
Es importante insistir en que el aspecto clave del contexto supervisado es
que los datos disponibles contienen observaciones de la variable objetivo que se
trata de explicar o predecir. Por ejemplo, en el caso del OCR o reconocimiento
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
123
de dígitos escritos a mano, que se ha usado a modo ilustrativo en la introducción de este capítulo, se ha hecho hincapié en la necesidad de contar con una
colección de dígitos debidamente etiquetados, esto es, en los que la imagen digital de cada dígito viene acompañada de una anotación o etiqueta (la variable
objetivo) que especifica a qué dígito corresponde realmente esa imagen digital.
Esta anotación o etiqueta ha de ser normalmente producida por un humano, y
es por esto que a este tipo de datos se los conoce como datos supervisados,
haciendo referencia al proceso (usualmente costoso) en el que expertos humanos asignan la etiqueta correcta de cada ejemplo disponible.
Así pues, en un contexto supervisado es posible asumir de manera general
que cada dato o ejemplo disponible para el aprendizaje se puede representar
mediante un vector (x ; y ) = (x1 ,..., x n ; y ), en el que xi denota la i-ésima de las n
variables explicativas o atributos disponibles, e y denota la variable respuesta
objetivo o target. Denotemos también por E el conjunto de los N ejemplos de
aprendizaje disponibles, esto es,
E = {(x 1 ; y 1 ),...,(x N ; y N )},
donde x k = (x1k ,..., x nk ) e yk denotan, respectivamente, el vector de inputs y el
valor del target del k-ésimo ejemplo de E. Finalmente, denotemos, respectivamente, por X1,...,Xn e Y los rangos de las variables explicativas y la respuesta
(esto es, el conjunto de valores que estas pueden tomar), de modo que el espacio de inputs X puede asociarse al producto cartesiano X = X 1 × ... × X n de estos
rangos. Entonces, la misión de cualquier procedimiento de aprendizaje supervisado es la de ajustar un programa o transformación
P : X 1 × ... × X n → Y ,
que asigne un valor y ∈ Y de la variable respuesta a cada vector
x = (x1 ,..., x n ) ∈ X 1 × ... × X n de valores de las variables explicativas, generalizando inductivamente de la manera más óptima posible las asignaciones particulares observadas en el conjunto E de ejemplos disponible. Esta
«optimalidad» del programa P obtenido se mide normalmente de forma empírica, comparando las asignaciones yˆ = P (x1 ,..., x n ) producidas por la transformación de los ejemplos en E con los valores y conocidos de la variable objetivo
de estos ejemplos.
Como ya se avanzaba en la introducción de este capítulo, dentro del marco
del aprendizaje supervisado es habitual diferenciar dos tipos de tareas o problemas, en función de la naturaleza de la variable objetivo de que se trata:
cuando esta variable es numérica (y, en especial, cuando es continua), se habla
de problemas «de regresión», mientras que cuando es categórica se habla de
que la tarea asociada es «de clasificación». Así, por ejemplo, dentro de las técnicas estadísticas estándar, la regresión lineal constituye una metodología
124
CUADERNOS METODOLÓGICOS 60
enfocada en problemas de regresión (que deben su nombre precisamente a
esta metodología), mientras que la regresión logística o el análisis discriminante son metodologías diseñadas para tratar problemas de clasificación.
Una diferencia relevante entre ambos tipos de tareas es que, en el caso de
los problemas de regresión, la transformación P recorre el rango de una variable numérica, por lo que los valores transformados yˆ = P (x ) pueden en principio tomar cualquier valor (esto es, cualquier número real), y en particular
valores no observados en el conjunto de ejemplos E. En este sentido, la transformación P aprendida a partir de los ejemplos suele actuar como un interpolador, que intenta reconstruir un hipotético proceso 4 que en particular ha
generado los valores y de los ejemplos en E a partir de los correspondientes
vectores x, pero que, de hecho, podría generar valores y diferentes a los observados para vectores x idénticos o diferentes a los presentes en E.
En los problemas de clasificación, sin embargo, el rango que recorre la transformación P está restringido al conjunto de categorías o clases que puede exhibir
la variable objetivo, que por definición será finito y suele coincidir con las clases
observadas en el conjunto de ejemplos E. Por ello, la transformación P actúa en
este caso dividiendo el espacio de inputs X = X 1 × ... × X n en una colección de
regiones asociadas a las diferentes clases del problema, de modo que nuevos
inputs x = (x1 ,..., x n ) serán asignados a la clase asociada a la región a la que pertenece ese vector x. Las fronteras entre estas regiones se suelen denominar «fronteras de decisión», precisamente porque atravesarlas supone tomar una decisión
diferente sobre la clase a la que será asignado un caso o instancia en estudio 5.
4.1.1.2.2. Aprendizaje no supervisado
Por oposición al aprendizaje supervisado, se habla de aprendizaje no supervisado cuando los ejemplos disponibles no contienen información sobre la/s
4
Entendiendo por proceso una función de los inputs a la que posiblemente se le ha añadido
algún tipo de ruido.
5
Nótese que, por ejemplo, la tarea asociada al problema del OCR o reconocimiento de dígitos es
un ejemplo de clasificación. Esto es así ya que lo que se pretende es que, al realizar el reconocimiento, el escáner (o quizá, mejor dicho, su software) asigne uno de los diez dígitos (del 0 al 9) a cada
imagen digitalizada de un dígito escrito a mano, y no un valor real como 7,2761 o 0,0201. Es decir,
en este contexto los diez dígitos funcionan como clases o categorías, más que como cantidades
numéricas (a pesar de ser dígitos). Los inputs (x1 ,..., x n ) corresponden en este caso a las intensidades
(usualmente en la escala de grises) de cada píxel en que se divide cada imagen digital. Por tanto, el
número de variables explicativas n coincide con el número de píxeles de la imagen. Siguiendo en el
contexto del OCR, la labor de un procedimiento de aprendizaje es entonces la de tomar un conjunto
de ejemplos etiquetados E de este tipo y ajustar con base en ellos un programa (o clasificador) P que
realice el reconocimiento de nuevas instancias de imágenes (x1 ,..., x n ) de manera correcta. De algún
modo, esto significa que diferentes patrones (i. e., regiones) de intensidad en determinados grupos
de píxeles serán asignados a diferentes clases, en este caso, uno de los diez dígitos con que contamos.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
125
variable/s objetivo de interés para el problema que se trata. En este caso,
esta variable objetivo ha de ser construida u obtenida exclusivamente a partir
de las variables explicativas, sin tener conocimiento a priori sobre el valor de
la variable objetivo que corresponde a cada ejemplo. En este contexto, por
tanto, se asume que cada ejemplo puede representarse mediante un vector
(x1 ,..., x n ) de variables explicativas o atributos, para el que ahora no se conoce
el correspondiente valor y de la variable objetivo.
De este modo, mientras que el aprendizaje supervisado se centra en la
inducción de una transformación entre inputs y outputs a partir de ejemplos
que relacionan esos inputs y outputs, el aprendizaje no supervisado se asocia
más bien con la tarea de descubrir una estructura o propiedades útiles de los
datos disponibles. El ejemplo arquetípico de este planteamiento es el análisis
de conglomerados o clustering, enfocado a la búsqueda y construcción de clusters (i. e., grupos) de ejemplos similares con base en la información contenida
en los atributos de esos ejemplos. Otra instancia bien conocida de aprendizaje
no supervisado es el análisis factorial, en que se estudia la relación entre las
diferentes variables explicativas de cara a confeccionar una o varias variables
objetivo numéricas que resuman la información contenida en esos atributos.
Por tanto, en términos formales, un procedimiento de aprendizaje no
supervisado trata nuevamente de construir una transformación
P : X 1 × ... × X n → Y
entre inputs y outputs óptima respecto a algún criterio o medida de calidad,
con la diferencia de que 1) ahora la construcción de esta transformación no
procede de forma inductiva (a partir de ejemplos, relacionando determinados
valores de las variables explicativas con ciertos valores de la variable objetivo)
como en el caso supervisado, sino que se realiza a través del análisis de las
propiedades estructurales de los datos (como correlación, proximidad, similitud, etc.), y 2) el criterio de calidad que guía esta construcción no es empírico,
en el sentido de que ya no es posible comparar las asignaciones yˆ = P (x1 ,..., x n )
con valores y de la variable objetivo conocidos a priori, sino que se refiere a
esas propiedades estructurales con base en las cuales se realiza el aprendizaje.
4.1.1.3. Evaluación del rendimiento: entrenamiento y test
Como se ha mencionado, el aprender a partir de ejemplos un programa
objetivo o transformación P que resuelva un determinado problema práctico significa en buena medida optimizar una medida de rendimiento que
depende de cómo ese programa transforma los inputs x de los ejemplos en
valores P(x) de la variable objetivo. En el caso del aprendizaje supervisado,
126
CUADERNOS METODOLÓGICOS 60
es particularmente posible comparar los valores target conocidos y de los
ejemplos (x ; y ) con los valores P(x) obtenidos al transformar sus inputs x.
De este modo, en una primera aproximación, podríamos pensar que si un
programa P cumple que P(x) = y para todos los ejemplos en E, entonces este
programa resuelve perfectamente la tarea práctica a la que se enfrenta. Sin
embargo, aun siendo una característica deseable que un programa obtenga
un buen rendimiento sobre los ejemplos con los que ese programa ha sido
ajustado, nada garantiza que ese rendimiento se mantenga sobre nuevos
ejemplos que no han sido usados en el ajuste del programa. Y en el fondo,
en la práctica queremos el programa objetivo para la predicción de nuevos
casos de los que no se conoce la variable objetivo, no para que sea perfectamente eficiente en la predicción de ejemplos de los que ya conocemos su
target.
Para entender esto mejor, nótese que en el contexto del OCR es perfectamente posible realizar un programa que memorice cada imagen digital del
conjunto de ejemplos E con su etiqueta asociada, de manera que al presentarle una de estas imágenes el programa la reconozca y responda con la etiqueta correspondiente. Este procedimiento obtendría siempre un 100% de
acierto (i. e., clasificaría correctamente todos los dígitos) sobre el conjunto de
ejemplos E con los que se ha confeccionado el programa. Sin embargo, este
programa sería perfectamente inútil en la práctica, pues sería totalmente
incapaz de reconocer cualquier imagen de un dígito que difiriese aun solo
ligeramente de las imágenes de ejemplo, de modo que su rendimiento real
sería más bien cercano al 0%.
En términos más generales, es importante tener en cuenta dos hechos que
surgen en este contexto. En primer lugar, dado un conjunto de ejemplos E, hay
infinitas transformaciones o programas P tal que P(x) = y para todos los ejemplos (x;y) en E que, sin embargo, producen resultados diferentes para inputs x
que no están en E. Esto es, de algún modo es necesario un criterio externo a E
que permita elegir entre diferentes programas con un ajuste similar de los
ejemplos disponibles. Y, en segundo lugar, existe un riesgo asociado al ajuste
excesivo de un programa al conjunto de ejemplos, el llamado «sobreajuste».
En tanto que al aprender se optimiza una medida de rendimiento que depende
de los parámetros libres del programa objetivo o de su modelo subyacente,
cuando el número de parámetros es suficientemente grande y/o el proceso de
optimización sobrepasa un cierto límite, el programa tiende a ajustar no solo
los patrones generales que muestran los ejemplos en E, sino también el ruido
y el sesgo propio de ese conjunto de ejemplos. Esto puede afectar al programa,
haciéndolo menos eficaz y robusto ante nuevos ejemplos con diferente ruido o
sesgo. En otras palabras, aunque el aprendizaje de un programa objetivo ha de
guiarse por su capacidad de ajustar los ejemplos disponibles E, de nuevo es
necesario un criterio externo a esos ejemplos que permita controlar cuándo
ese aprendizaje pasa a generalizar ruido en lugar de patrones útiles.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
127
4.1.1.2.1. Entrenamiento y test, validación cruzada y sus variantes
Estas consideraciones han llevado a la conclusión de que la evaluación del
rendimiento real de un programa obtenido mediante un procedimiento de
aprendizaje automático ha de realizarse sobre un conjunto de ejemplos independiente o alternativo a E, esto es, que no hayan formado parte de los ejemplos con los que se ha ajustado el programa. Así, se trata de validar el uso
práctico que llevará a cabo el programa objetivo, cuya utilidad dependerá de
su eficacia en la predicción de nuevas instancias potencialmente diferentes a
las usadas para el ajuste.
Con este fin, el procedimiento habitual consiste en dividir el conjunto de
ejemplos inicialmente disponible en dos subconjuntos E y T, llamados, respectivamente, el «conjunto de entrenamiento» y el «conjunto de test», de
modo que el primero se utiliza para entrenar (esto es, ajustar o aprender) el
programa objetivo, y el segundo se utiliza para estimar el rendimiento del programa obtenido a partir de E. Por tanto, una vez aprendido un programa P a
partir de los ejemplos de entrenamiento E, se aplica este programa para predecir la variable objetivo de los ejemplos en T, y se obtiene la medida de rendimiento real de P comparando estas predicciones P(x) con los valores reales
y para cada ejemplo (x;y) en T. Por este motivo es común entender que el proceso de aprendizaje de un programa objetivo consta de al menos dos fases,
una de entrenamiento, en la que se produce propiamente el ajuste del programa a los ejemplos, y una segunda fase de test, en que se evalúa el programa
con ejemplos no vistos durante el entrenamiento. Para partir el conjunto de
ejemplos inicial en los subconjuntos de entrenamiento E y test T se suele proceder seleccionando aleatoriamente del conjunto inicial (mediante muestreo
aleatorio simple, por ejemplo) una proporción de ejemplos dada que formarán el conjunto de test, quedando los casos no seleccionados en el conjunto de
entrenamiento. Es conveniente que el conjunto de entrenamiento contenga al
menos el mismo número de ejemplos que el de test, siendo lo habitual mantener una proporción entre 60% entrenamiento / 40% test y 90% entrenamiento
/ 10% test.
Este marco de aprendizaje y evaluación basados en dos conjuntos de ejemplos independientes suele funcionar de manera correcta cuando el número de
ejemplos inicial es elevado y/o la distribución de los ejemplos en el espacio de
inputs y outputs es relativamente homogénea. Cuando estos supuestos no se
dan (lo que en el caso del segundo supuesto no es nada inusual), puede existir
una variabilidad importante en el resultado del entrenamiento y del test
debida a la selección aleatoria de los respectivos conjuntos E y T. Por ejemplo,
si en el caso del OCR se tiene una muestra inicial formada por, digamos, dos
ejemplos de cada dígito, y al seleccionar aleatoriamente un conjunto de test
con un 10% de casos (es decir, dos de los veinte) los dos ejemplos del dígito 5
van a este conjunto, entonces el conjunto de entrenamiento no contendrá
128
CUADERNOS METODOLÓGICOS 60
ningún ejemplo de este dígito, y el procedimiento de aprendizaje será incapaz
de enseñar al programa objetivo OCR a reconocerlo (de hecho, no tendría ninguna constancia de que esa clase existe), por lo que en la fase de test su error
estimado será del 100% (ya que los dos ejemplos del dígito 5 en T no serán
reconocidos correctamente). Si uno de estos ejemplos del dígito 5 se hubiese
mantenido en el conjunto de entrenamiento, por el contrario, el error estimado en test podría ser incluso del 0% (esto es, ambos ejemplos en T podrían
clasificarse correctamente al contar ahora la muestra de entrenamiento con
ejemplos de ambas clases).
Para aumentar la robustez en la estimación del rendimiento práctico de un
programa, eliminando o, al menos, suavizando el efecto de la selección aleatoria de la muestra de test, el procedimiento más extendido es el conocido
como «validación cruzada». En su versión más elemental, conocida como
validación cruzada con uno fuera (leave-one-out cross-validation), este procedimiento consiste en tomar como conjunto de entrenamiento E todo el conjunto de ejemplos inicial menos un único caso, entrenar entonces el programa
objetivo con el conjunto E así formado y predecir el ejemplo restante, que es,
de esta forma, el único elemento del conjunto de test T. Este procedimiento se
repite N veces, es decir, tantas veces como ejemplos contenga el conjunto inicial, tomando en cada repetición un ejemplo diferente para el conjunto de test
T (o equivalentemente, descartando cada vez un ejemplo diferente en el conjunto de entrenamiento E). Finalmente, se promedia la medida de acierto (o
error) obtenida en las N repeticiones, y este promedio se toma como estimación del rendimiento real del programa.
Aunque este procedimiento de validación cruzada con uno fuera puede
proporcionar estimaciones adecuadas del rendimiento real de un programa,
eliminando efectivamente la dependencia de la aleatorización, tiene, no obstante, el inconveniente de requerir entrenar N veces el programa con prácticamente todo el conjunto de ejemplos disponibles. Por tanto, cuando el tamaño
de este conjunto es elevado (lo cual es la tónica general en entornos de datos
masivos), el tiempo de cómputo necesario para llevarlo a cabo puede ser prohibitivo 6. Una solución intermedia que requiere menores tiempos de cómputo
y, al mismo tiempo, proporciona cierta robustez ante la aleatoriedad en la
selección de los conjuntos de entrenamiento y test es la conocida como «validación cruzada de K iteraciones» (K-fold cross-validation). En este procedimiento de evaluación, el conjunto de ejemplos inicial es dividido aleatoriamente
en K subconjuntos de tamaño similar. Uno de estos subconjuntos es entonces
asignado como conjunto de test T, y los K – 1 restantes se agrupan como conjunto de entrenamiento E. El programa se entrena entonces con E y se evalúa
6
Nótese que el tiempo de computación de la fase de entrenamiento suele ser proporcional al
número N de ejemplos en el conjunto E, por lo que cuando N es grande cada fase de entrenamiento puede requerir por sí misma un tiempo no despreciable.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
129
con T, y se obtiene la medida de error correspondiente. El proceso se repite K
veces, de manera que en cada iteración se selecciona como conjunto de test T
uno de los K subconjuntos, uno diferente cada vez, formando E con los restantes. La medida de rendimiento final se obtiene promediando las medidas de
error de cada iteración. Habitualmente se acepta que para K = 10 se obtiene
un buen equilibrio entre la robustez de la estimación y el tiempo computacional que requiere el procedimiento de evaluación, aunque el uso de K = 5 también está extendido.
Antes de pasar a describir las medidas de evaluación más habituales en el
campo del aprendizaje supervisado, haremos hincapié brevemente en algunas
cuestiones relevantes que complementan lo expuesto sobre el marco entrenamiento-test y el procedimiento de validación cruzada. En primer lugar,
cuando se quiere seleccionar la mejor configuración paramétrica de un mismo
procedimiento de aprendizaje (por ejemplo, modelos de regresión lineal con
diferente número de parámetros o variables explicativas) controlando el
sobreajuste, es habitual ampliar este marco para incluir una tercera fase que
se suele denominar «validación». En este marco entrenamiento-validacióntest, el conjunto de ejemplos inicial se divide ahora en tres subconjuntos o
muestras, uno de entrenamiento, otro de validación y otro de test, de manera
que las diferentes configuraciones paramétricas del programa objetivo se
entrenan con el subconjunto de entrenamiento, se selecciona la mejor de ellas
atendiendo a su rendimiento sobre la muestra de validación, y, finalmente, se
estima el rendimiento real de la configuración seleccionada usando la muestra de test. Esto permite evitar el sesgo que podría introducirse en la estimación del rendimiento si se usara para ello la muestra de validación y no la de
test, en tanto que la primera se está usando ya para controlar el sobreajuste de
los modelos y podría por ello producir una subestimación del error (puesto
que el modelo o configuración seleccionado podría aprender a ajustar el ruido
particular de la muestra de test). Al constituir un subconjunto de ejemplos
independiente de todo el proceso de entrenamiento y selección de modelos, la
tercera muestra de test evita introducir ese sesgo en la estimación del rendimiento real del programa.
En segundo lugar, en el contexto del procedimiento de validación cruzada
con K iteraciones, es conveniente tener en cuenta que el uso del muestreo
aleatorio simple para partir el conjunto inicial de ejemplos en las K muestras
correspondientes puede proporcionar malos resultados, ya que no permite
controlar que estas K muestras tengan distribuciones conjuntas similares de
las variables explicativas y target. En consecuencia, las muestras de entrenamiento y test podrían tener distribuciones diferentes de las variables explicativas, o también diferentes distribuciones condicionadas de la variable objetivo
para inputs similares, lo que puede conducir a una estimación más pobre del
rendimiento del programa. Por ello, no es inusual imponer algún tipo de
muestreo estratificado para garantizar que cada una de las K muestras tenga
130
CUADERNOS METODOLÓGICOS 60
un número similar de ejemplos de cada una de las clases (en un problema de
clasificación) o una media similar de la variable objetivo (en un problema
de regresión). Así, se habla de «validación cruzada con estratificación» (stratified cross-validation) cuando se aplica este procedimiento.
Además, controlar que las diferentes muestras tengan una distribución
conjunta similar de las variables explicativas puede traducirse en mejoras significativas en la estimación de rendimiento. En este sentido, se habla de «validación cruzada con distribuciones equilibradas» (distribution balanced
cross-validation). Este mecanismo puede combinarse con muestreo estratificado para cada clase en problemas de clasificación, dando lugar a la «validación cruzada estratificada con distribuciones equilibradas» (distribution
balanced stratified cross validation). Véase J. L. Moreno-Torres et al. (2012)
para profundizar en estas cuestiones.
Finalmente, conviene tener en cuenta que en los problemas de clasificación no es raro contar con proporciones poco equilibradas de ejemplos de las
distintas clases. Por ejemplo, en la detección de fraudes en transacciones
comerciales, la clase positiva (esto es, los ejemplos en que efectivamente se ha
constatado el fraude) suele ser mucho menos numerosa que la clase negativa
(ejemplos en que no ha existido fraude), en una proporción que puede llegar
a 1:100.000. En consecuencia, la mayoría de procedimientos de aprendizaje
suelen sesgarse hacia las clases más numerosas y tender a obviar las menos
numerosas (que muchas veces son las más relevantes), de manera que el rendimiento global del clasificador puede ser bueno o muy bueno (pues clasifica
correctamente la mayoría de los ejemplos de las clases más abundantes) pero
muy pobre en la detección de las clases minoritarias. Este tipo de problemas
reciben el nombre de «problemas de clasificación no equilibrada» (unbalanced classification). En este contexto, es habitual el uso de procedimientos de
submuestreo de las clases mayoritarias o de sobremuestreo de las minoritarias para garantizar la obtención de muestras de entrenamiento más balanceadas respecto a la composición de clases. Puede consultarse la referencia
Y. Sun et al. (2009) para ampliar la información sobre problemas de clasificación no balanceada y las estrategias más habituales para afrontarlos.
4.1.1.2.2. Medidas de evaluación
En esta sección se exponen algunas de las medidas más extendidas para evaluar el rendimiento práctico de programas de aprendizaje automático, en especial, de aprendizaje supervisado. Estas medidas suelen computarse sobre la
muestra de test (o validación) para estimar el grado de desempeño de un programa P tras su entrenamiento, pero también puede tener interés su cómputo
sobre la muestra de entrenamiento, y algunas veces, de hecho, se utilizan para
guiar el propio proceso de entrenamiento. En un marco de validación cruzada,
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
131
estas medidas se obtienen para cada ciclo de muestras entrenamiento-test,
siendo la medida final el promedio (posiblemente ponderado por el tamaño de
cada muestra) de estas evaluaciones intermedias. En cualquier caso, se supone
la existencia de un conjunto de N ejemplos de tipo supervisado
T = {(x 1 ; y 1 ),...,(x N ; y N )}
donde x k = (x1k ,..., x nk ) e yk denotan, respectivamente, el vector de n inputs y el
valor del target del k-ésimo ejemplo de T. De algún modo u otro, las medidas
que se exponen a continuación se basan en la comparación de las predicciones P(xk) realizadas por el programa, siendo evaluado con los valores observados o reales yk, y en algún tipo de promedio o agregación posterior de estas
comparaciones para todos los ejemplos en T. Por la diferente naturaleza de la
variable objetivo, es natural distinguir entre las medidas usadas para problemas de regresión y de clasificación.
Medidas para problemas de regresión
Recordemos que lo que caracteriza a los problemas de regresión es que la
variable objetivo es de naturaleza numérica, y normalmente continua, por lo
que tiene sentido utilizar operaciones de tipo aritmético para realizar las comparaciones entre los valores reales de este target y los predichos por el programa. De hecho, lo habitual es medir la distancia entre los valores predichos
P(xk) y los observados yk. Una distancia mayor entre ellos significa una mayor
discrepancia entre el valor real y el predicho, y, por tanto, un mayor error del
mecanismo de regresión al predecir ese ejemplo. Diferentes modos de computar esta distancia y de promediarla sobre el conjunto T dan, así, lugar a diferentes medidas de error.
El modo natural de medir la distancia entre dos números a y b es mediante
el valor absoluto de su diferencia, esto es, |a – b|. Aplicado al caso de la comparación entre el valor real del target y su predicción, la diferencia | y k − P (x k ) |
se denomina error absoluto, y mide, por tanto, la distancia entre ambos valores. Al promediar los errores absolutos cometidos sobre todos los ejemplos en
T, se obtiene el error absoluto medio (MAE, por las siglas del inglés mean
absolute error), dado por
MAE =
1
N
N
∑| y
k
− P (x k ) | .
k =1
Aunque el MAE proporciona una manera natural de medir el error promedio cometido al predecir una variable numérica, su aplicación no ha sido
132
CUADERNOS METODOLÓGICOS 60
habitual hasta tiempos recientes en tanto que el uso del valor absoluto creaba
problemas en la optimización, lo que derivó en la aplicación mucho más
extendida del error cuadrático medio (MSE, de mean squared error), dado por
MSE =
1
N
N
∑(y
k
− P (x k ))2 .
k =1
Esta medida utiliza los errores al cuadrado, que son siempre positivos, por
lo que puede identificarse con la varianza del error (que suele tener media 0),
y al no requerir el empleo del valor absoluto constituye una función propicia
para su empleo con métodos de optimización analíticos. De hecho, el clásico
método de ajuste por mínimos cuadrados en regresión lineal emplea implícitamente esta medida, de modo que el modelo obtenido (esto es, las estimaciones de los parámetros o coeficientes asociados a las diferentes variables
explicativas) es el que minimiza el MSE sobre la muestra de entrenamiento.
A diferencia del MAE, la manera en que el MSE mide la distancia entre valor
observado y predicho (usando cuadrados) tiende a dar relativamente mayor
importancia a los errores grandes y una menor a los pequeños, lo que hace más
peligrosa la presencia de valores atípicos u outliers y datos influyentes (ya que
pueden inflar notablemente el error o perturbar significativamente el modelo
para evitar esa inflación del error). Aunque, en menor medida, en tanto que el
valor absoluto da una importancia similar a errores grandes y pequeños, también el MAE sufre de falta de robustez ante la presencia de outliers y datos extremos. En este sentido, el problema no es tanto dependiente de la manera de
medir el error o distancia entre valor predicho y real como de la manera de promediar los errores de los diferentes ejemplos en T. En particular, el promedio o
media aritmética usual es especialmente sensible a la presencia de valores
extremos, de modo que la introducción o la eliminación de un único valor
puede modificar sensiblemente el promedio obtenido. Por ello, es cada vez más
frecuente utilizar mecanismos de agregación promediantes diferentes a la
media, siendo posiblemente la mediana la opción más extendida.
Recuérdese que la mediana m de una colección de números corresponde
al valor del elemento central cuando esta colección se ordena de menor a
mayor, de modo que la mitad de los números sean mayores que m y la otra
mitad, menores. Este procedimiento basado solo en propiedades ordinales y
no aritméticas hace de la mediana un promedio mucho más robusto ante la
presencia de valores extremos. Por ejemplo, la mediana de la colección formada por los valores 1, 2, 3, 4 y 100 es 3, mientras que su media es 22. Si
ahora se elimina el valor 100, la mediana de los cuatro valores restantes es
2,5, al igual que la media. Es decir, la mediana apenas ha fluctuado al eliminar el dato extremo, mientras que la media ha sufrido una variación muy
significativa. Así, utilizando la mediana en lugar de la media en la expresión
para el MAE (o el MSE), es posible obtener una nueva medida más robusta
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
133
ante los errores extremos causados por datos atípicos, conocida como el
error absoluto mediano (MEDAE, de median absolute error), dado por
MEDAE = mediana (| y k − P (x k ) |, k = 1,..., N ).
Como se mencionado, el MEDAE puede interpretarse como una estimación del centro de la distribución de los errores absolutos, de modo que el 50%
de los errores observados serán mayores que el MEDAE y el 50% restante,
menores.
Medidas para problemas de clasificación
A diferencia de la regresión, en un problema de clasificación la variable objetivo es categórica, por lo que deja de tener sentido la aplicación de operaciones aritméticas entre predicciones y valores reales e incluso la noción de
distancia entre ellas. La clave en este contexto es que la categoría o clase predicha sea la misma que la real, lo que corresponde al caso en que el programa
o clasificador entrenado reconoce y asigna correctamente la clase del ejemplo
en cuestión. Para formalizar esta comparación, de tipo lógico, se empleará la
expresión y k == P (x k ), la cual tomará el valor 1 cuando efectivamente la clase
predicha P(xk) sea la misma que la clase real yk, y valdrá 0 en caso contrario.
Así, por ejemplo, se tiene que entonces
N
∑y
k
== P (x k )
k =1
representa el número total de ejemplos de la muestra T correctamente clasificados por el programa P, y la proporción de ejemplos bien clasificados o tasa
de acierto (accuracy) del clasificador se obtendrá como
acierto =
1
N
N
∑y
k
== P (x k ).
k =1
De este modo, la tasa de acierto de un clasificador puede interpretarse
como una estimación de la probabilidad de que el clasificador reconozca
correctamente la clase de un ejemplo. Obviamente, una tasa de acierto mayor
indicará, en general, un mejor rendimiento global del clasificador en la tarea
correspondiente.
Para el caso de la clasificación es posible dar algunas referencias con que
valorar las tasas de acierto obtenidas. En un problema de clasificación con C
134
CUADERNOS METODOLÓGICOS 60
clases diferentes (esto es, en el cual la variable objetivo puede exhibir C categorías distintas), un clasificador que sortease al azar la clase que asignar obtendría en promedio una tasa de acierto del 100/C %. Por ejemplo, en un problema
de clasificación binario, con dos clases (C = 2), este tipo de clasificador aleatorio conseguiría en promedio una tasa de acierto del 50%. En tanto este comportamiento aleatorio implica un clasificador que no ha extraído información
útil de la muestra de entrenamiento, cualquier clasificador que tras su entrenamiento no supere esa cifra o la supere solo marginalmente suele ser considerado como pobre (o, alternativamente, que el problema de clasificación
subyacente es complicado o no posee variables explicativas adecuadas para
predecir la variable objetivo).
Por otro lado, es importante tener en cuenta que la tasa de acierto no proporciona normalmente información sobre el rendimiento local del clasificador en cada clase, lo cual puede crear impresiones engañosas en un contexto
en que la relevancia práctica de reconocer adecuadamente algunas clases sea
mayor que la de otras, o en que la proporción entre unas clases y otras esté
fuertemente desbalanceada. Por ejemplo, supongamos de nuevo un problema
binario en que la proporción entre las dos clases es de 1:99, esto es, un 1% de
los ejemplos provienen de la clase minoritaria (digamos la clase positiva) y un
99%, de la clase mayoritaria (llamémosla negativa). Con esta información, se
podría programar un clasificador que asignase aleatoriamente la clase positiva con probabilidad 1% y la negativa, con probabilidad 99%. En promedio,
este clasificador aleatorio obtendría una tasa de acierto en torno al 98%, pero
solo clasificaría correctamente 1 de cada 100 ejemplos de la clase positiva, lo
cual, de nuevo, ha de ser considerado un rendimiento pobre en tanto que este
clasificador apenas ha extraído información útil de la muestra de entrenamiento (todo lo más, la proporción entre clases).
Estas consideraciones llevan a la necesidad de utilizar medidas alternativas o complementarias a la tasa de acierto para conseguir una imagen más
completa del rendimiento real de un clasificador. De hecho, hay un modo de
resumir y mostrar toda la información relevante, a nivel local y global, sobre
el rendimiento de un clasificador. Esto se logra con la llamada «matriz de confusión», que describe el número de ejemplos provenientes de cada clase que
ha sido clasificado en cada una de las clases. En el caso particular de un problema binario, esta matriz toma la forma de una tabla como la mostrada en la
tabla 4.1.
En esta tabla, VP (verdaderos positivos) representa el número de ejemplos
positivos (es decir, cuya clase real es la positiva) que han sido correctamente
clasificados en la clase positiva; FN (falsos negativos) es el número de ejemplos positivos erróneamente clasificados como negativos; FP (falsos positivos)
es el número de ejemplos negativos clasificados como positivos, y VN (verdaderos negativos), el número de ejemplos negativos clasificados como negativos. En los márgenes derecho e inferior se pueden hacer constar los totales
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
135
Tabla 4.1.
Matriz de confusión
Clase real
Clase predicha
Positiva
Negativa
Positiva
VP
FN
N.º ejemplos positivos
Negativa
FP
VN
N.º ejemplos negativos
N.º predicciones
positivas
N.º predicciones
negativas
N
por fila y columna, respectivamente. Dividiendo cada elemento de la tabla
entre N, el número de ejemplos en la muestra clasificada, se puede obtener
una descripción del rendimiento del clasificador mediante proporciones o frecuencias relativas, en lugar de absolutas. De este modo, un clasificador realizaría una clasificación perfecta cuando FN = FP = 0, VP = n.º ejemplos positivos =
n.º predicciones positivas y VN = n.º ejemplos negativos = n.º predicciones
negativas, esto es, cuando todos los ejemplos positivos son clasificados como
tales y similarmente con los negativos (recíprocamente, cuando todas las predicciones positivas corresponden a ejemplos positivos e idem con las predicciones negativas). Nótese que en este contexto de clasificación binaria (con
dos clases), la tasa de acierto puede obtenerse como
acierto =
VP +VN
.
VP + FN +VN + FP
Para problemas con más de dos clases, sigue siendo perfectamente posible
construir el mismo tipo de matriz, detallando las frecuencias de casos reales y
predichos para cada par de clases, de modo que el comportamiento deseable
es el de agrupar todos o la mayoría de ejemplos en la diagonal de la matriz. En
este contexto de problemas de clasificación con más de dos clases, la terminología positivo/negativo se adapta para hablar de ejemplos positivos de una
determinada clase (todos los ejemplos que pertenecen a esa clase) y negativos
(todos los ejemplos que no pertenecen a esa clase).
A partir de la matriz de confusión es posible derivar varias medidas que
aportan información complementaria a la tasa de acierto, y que se centran en
diversos aspectos del rendimiento de un clasificador. Así, se conoce como
«precisión» (precision) de un clasificador para una clase c a la tasa de predicciones correctas de c respecto al total de predicciones de c (esto es, el cociente
entre el número de ejemplos correctamente clasificados en c y el total de
136
CUADERNOS METODOLÓGICOS 60
ejemplos clasificados en c). En el caso binario, la precisión solo se calcula
para la clase positiva, y viene dada por
precisión =
VP
.
VP + FP
Nótese que la precisión de un clasificador en una clase estima la probabilidad de que, dado un ejemplo que ha sido clasificado en esa clase, esta predicción sea correcta. Esta medida informa, por tanto, de cuán creíbles son las
predicciones de un clasificador respecto a una clase, esto es, su eficacia al
asignar ejemplos a esa clase.
Sin embargo, la precisión no informa de la eficacia del clasificador al detectar ejemplos provenientes de esa clase, esto es, dado un ejemplo de una clase,
con qué probabilidad se clasifica en esa clase. Esta probabilidad es medida
por la sensibilidad (sensitivity) de una clase c, también llamada exhaustividad
(recall), y que se calcula como la tasa de ejemplos realmente provenientes de
la clase c que se clasifican correctamente en esa clase. En el caso binario, esta
medida se calcula solo para la clase positiva, obteniéndose como
sensibilidad =
VP
.
VP + FN
El par precisión/sensibilidad suele utilizarse simultáneamente, en tanto
que ambas medidas informan del rendimiento del clasificador respecto a una
clase c desde dos perspectivas complementarias: una, la precisión, evalúa la
capacidad del clasificador de realizar predicciones correctas de esa clase c
(cuando se predice c, con qué eficacia se hace), mientras la otra, la sensibilidad, evalúa la capacidad del clasificador de reconocer o detectar correctamente los ejemplos provenientes de c (cuando la clase real es c, con qué
eficacia se reconoce). Aun así, no es infrecuente informar del rendimiento global de un clasificador (o evaluarlo) respecto de una clase c utilizando una
única medida derivada de este par precisión/sensibilidad, que se conoce como
F-score de la clase c, dado por la media armónica de la precisión y la sensibilidad, esto es
F =2
precisión ⋅ sensibilidad
.
precisión + sensibilidad
Dentro del contexto de la clasificación binaria, otra medida que se emplea en
ocasiones junto con la sensibilidad es la llamada «especificidad» (specificity),
que se calcula como la tasa de ejemplos correctamente clasificados como negativos respecto al total de ejemplos provenientes de la clase negativa, es decir:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
especificidad =
137
VN
.
VN + FP
Esta medida tiene relevancia en problemas binarios en los que se desea
contar con una medida de rendimiento del clasificador sobre la clase negativa.
Por ejemplo, piénsese en un procedimiento para reconocer automáticamente
si un texto trata sobre un tema de actualidad política. La sensibilidad nos
informa de la eficacia de este procedimiento para detectar textos que traten el
tema de interés (i. e., de la clase positiva). En cambio, la especificidad nos
informa de la eficacia del procedimiento para identificar como tales textos
que no tratan la temática objetivo (i. e., de la clase negativa).
Nótese que un clasificador binario que asignase siempre la clase positiva
(es decir, que la asigne con probabilidad 1) obtendría una sensibilidad de 1
(ya que todos los ejemplos de la clase positiva se clasificarían correctamente)
y una especificidad de 0 (ningún ejemplo de la clase negativa sería identificado correctamente). Por el contrario, un clasificador que asigne la clase
negativa con probabilidad 1 obtendría sensibilidad = 0 (todos los ejemplos
positivos se clasificarían mal) y especificidad = 1 (todos los ejemplos negativos
quedarían bien clasificados). En general, un clasificador binario aleatorio que
asigne la clase positiva con probabilidad p y la clase negativa con probabilidad 1 – p obtendrá en promedio sensibilidad = p y especificidad = 1 – p, y, por
tanto, para este clasificador aleatorio se tiene que para cualquier p en promedio se cumple sensibilidad + especificidad = 1. Esto da una medida del rendimiento esperado de un clasificador aleatorio (es decir, que no aprende de los
ejemplos), lo que marca un límite para el rendimiento aceptable de un clasificador entrenado: cualquier par de valores (sensibilidad, especificidad) cuya
suma sea igual o inferior a 1 indicará un rendimiento igual o peor que el esperado con un clasificador aleatorio.
En relación con estas ideas, es preciso observar que muchos clasificadores
binarios (por ejemplo, la regresión logística) permiten utilizar un umbral «u»
(llamado umbral de discriminación) que controla la exigencia impuesta sobre
la evidencia disponible para poder clasificar un ejemplo como positivo.
Cuando esta exigencia es máxima, digamos que cuando u = 1, todos los ejemplos son clasificados como negativos, y, por tanto, se obtiene sensibilidad = 0
y especificidad = 1. En el caso opuesto, cuando la exigencia es mínima, digamos que con u = 0, todos los ejemplos son clasificados como positivos, y se
tiene sensibilidad = 1 y especificidad = 0. Para valores intermedios, entre 0 y
1, de este umbral de exigencia o discriminación u, el clasificador obtendrá
valores de sensibilidad y especificidad variables, cuya suma puede estar por
encima o por debajo de 1.
De este modo, variando el valor de este umbral u (por ejemplo, empezando
en 0 y terminando en 1 con saltos de longitud 0,1) y calculando para cada
138
CUADERNOS METODOLÓGICOS 60
Figura 4.3.
Curvas ROC
valor el par (sensibilidad, especificidad) del clasificador, es posible obtener la
llamada «curva ROC» (del inglés receptor operating characteristic curve), que
une los puntos o pares obtenidos en el espacio sensibilidad × especificidad 7
(véase la figura 4.3). Un clasificador ideal obtendría siempre sensibilidad = 1
y especificidad = 1 con estos umbrales intermedios. Sin embargo, este comportamiento no es realista, y se suele aceptar que un clasificador resuelve la
tarea perfectamente si su curva ROC pasa por ese punto (sensibilidad, especificidad) = (1,1) para algún umbral u (punto lleno en la esquina superior derecha). Más comúnmente, el rendimiento de un buen clasificador adquiere la
forma de una curva situada sobre la línea diagonal discontinua. Esta línea discontinua representa los pares (sensibilidad, especificidad) tales que sensibilidad + especificidad = 1, esto es, representa la curva ROC esperada de un
clasificador aleatorio. Si la curva ROC de un clasificador se encuentra por
debajo de esta diagonal, su rendimiento será entendido como pobre.
7
De hecho, la curva ROC se suele representar en el espacio sensibilidad x (1 – especificidad),
esto es, invirtiendo los valores obtenidos de especificidad, para que el eje horizontal informe de la
tasa de falsos positivos (en lugar de verdaderos negativos), lo que suele considerarse más coherente con el uso en el eje vertical de la sensibilidad, que es la tasa de verdaderos positivos. Aquí,
por simplicidad, se ha elegido la representación en el espacio sensibilidad x especificidad, esto es,
sin invertir los valores de la especificidad. El único cambio reseñable entre esta y la representación habitual invirtiendo la especificidad es que en la segunda la línea diagonal es ascendente en
lugar de descendente, y el punto ideal es el situado en la esquina superior izquierda, en lugar de
en la derecha. El AUC es el mismo en ambos casos.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
139
A partir de la curva ROC es posible obtener una medida de rendimiento
cada vez más extendida, sobre todo en el contexto de clasificación binaria, llamada AUC o área bajo la curva (del inglés area-under-curve), y que se obtiene,
efectivamente, como el área encerrada entre el eje horizontal y la curva ROC.
Un clasificador ideal obtendría un AUC = 1, mientras que un clasificador que
nunca detecta la clase positiva obtendría AUC = 0. El área que encierra la línea
diagonal asociada al rendimiento de un clasificador aleatorio es AUC = 0,5.
Este valor marca, por tanto, la frontera entre un rendimiento pobre (AUC ≤
0,5) y un rendimiento mejor que el que se obtendría por azar (AUC > 0,5).
Finalmente, se expone una última medida de rendimiento cuyo uso suele
aconsejarse en problemas de clasificación no balanceados, esto es, cuando las
frecuencias relativas de las clases distan considerablemente de un reparto
equilibrado. Esta medida, conocida como kappa de Cohen (o simplemente
kappa o κ), intenta reflejar el grado de acuerdo entre la distribución de clases
reales en el conjunto T y la distribución de clases predichas por el clasificador,
descontando o ajustando este por el grado de acuerdo que se podría obtener
por azar. Esta medida se obtiene como
κ=
po − pe
,
1 − pe
donde po denota el acuerdo o concordancia observado entre clases reales y
predichas, esto es, la tasa de acierto antes introducida, y pe denota la probabilidad hipotética de acuerdo por azar. Si el problema de clasificación tiene
C clases, hay N ejemplos en T, nc denota el número de ejemplos existentes de
la clase c y pc el número de ejemplos clasificados en la clase c, esta última probabilidad pe se calcula como
pe =
1 C
∑ nc ⋅ pc .
N 2 c =1
Esta probabilidad se obtiene, por tanto, bajo el supuesto de que el clasificador realizase las pc predicciones de la clase c de manera aleatoria, con probabilidad pc/N. Cuando el acuerdo es máximo, esto es, cuando acierto = po = 1,
se tiene que κ = 1. Cuando el acuerdo obtenido por el clasificador es el esperado para un clasificador aleatorio, es decir, si acierto = po = pe, entonces se
obtiene κ = 0. Es posible obtener valores negativos de κ, que representan un
nivel de acuerdo inferior al esperable por azar.
Como ejemplo, supongamos un problema binario en que la muestra T contiene N = 1.000 ejemplos, de los cuales 990 pertenecen a clase negativa y solo 10
a la positiva, y que el clasificador ha predicho como se muestra en la matriz de
confusión en la tabla 4.2. En estas condiciones, la tasa de acierto del clasificador
140
CUADERNOS METODOLÓGICOS 60
es acierto = po = 972/1.000 = 0,972. Esto podría hacer creer que el rendimiento del
clasificador es relativamente bueno, ya que acierta la clase correcta un 97% de
las veces. Sin embargo, la probabilidad de acuerdo por azar es
pe =
1
(10 ⋅ 20 + 990 ⋅ 980) = 0,9704.
1.0002
En consecuencia, se obtiene κ = 0,054, lo que señala un rendimiento solo
ligeramente superior al que obtendría un clasificador aleatorio al sortear 20
predicciones positivas y 980 negativas entre los 1.000 ejemplos. Por otro lado,
nótese que la precisión de este clasificador es bastante pobre, pues se tiene
que precisión = 0,05, esto es, la probabilidad de que una predicción positiva
sea correcta es del 5%. Igualmente, su capacidad de detectar la clase positiva es baja, pues se tiene sensibilidad = 0,1, un 10% de probabilidad de clasificar correctamente un ejemplo positivo. El comportamiento respecto a la
clase negativa, en cambio, es aparentemente bastante superior, pues especificidad = 0,9808, luego, la probabilidad de identificar correctamente un ejemplo negativo está en torno al 98%. Sin embargo, esto no constituye un gran
logro en este contexto, ya que un clasificador aleatorio que asignase 980 ejemplos al azar a la clase negativa obtendría en promedio especificidad = 0,9702.
Esto ilustra cómo la medida kappa puede arrojar luz sobre el rendimiento real
de un clasificador en un contexto de clases desbalanceadas.
Tabla 4.2.
Matriz de confusión para ejemplo de cálculo de la kappa de Cohen
Clase real
Clase predicha
Positiva
Negativa
Positiva
1
9
10
Negativa
20
971
990
20
980
1.000
Para concluir, puede ser conveniente enfatizar algunos aspectos de cómo se
realiza la evaluación de un procedimiento de aprendizaje automático cuando
se aplica un procedimiento de validación cruzada. En este contexto, recordemos,
se cuenta con una colección de K conjuntos de entrenamiento y test, que pueden ser denotados respectivamente por E1,...,EK y T1,...,TK, de modo que en cada
ciclo j = 1,...,K de la validación cruzada el programa se entrena con el conjunto
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
141
Ej y se evalúa con el conjunto Tj mediante alguna de las medidas de rendimiento
vistas en esta sección. De este modo, para cada ciclo j se computa la medida de
rendimiento sobre el conjunto de entrenamiento y sobre la muestra de test, que
denotaremos, respectivamente, por ej y tj.
Así pues, una vez obtenidas las k medidas e1,...,eK y t1,...,tK, para concluir el
procedimiento de validación cruzada es necesario calcular el promedio de estas
medidas, lo cual se lleva a cabo habitualmente mediante la media aritmética.
Por tanto, la estimación final t del rendimiento del programa viene dada por la
media de los rendimientos estimados en cada muestra de test, esto es
t=
1
K
K
∑t .
j
j =1
De manera similar, es posible computar la media e de los rendimientos
obtenidos sobre las diferentes muestras de entrenamiento. Estas medias pueden acompañarse de la correspondiente desviación típica para evaluar la estabilidad del programa aprendido ante cambios en la muestra de entrenamiento.
A la hora de comparar entre sí dos o más procedimientos o algoritmos de
aprendizaje diferentes, que proporcionarían programas objetivo P diferentes
al ejecutarse sobre una misma muestra de entrenamiento, suele ser necesario
un marco experimental más amplio, en el que se evalúe el rendimiento de los
programas objetivo proporcionados por cada algoritmo sobre una colección
relativamente amplia de diferentes conjuntos de datos o ejemplos. Las correspondientes medias t obtenidas mediante cada mecanismo de aprendizaje
sobre los diferentes conjuntos de datos considerados pueden tomarse como
muestras independientes, que pueden ser comparadas mediante procedimientos inferenciales no paramétricos de cara a contrastar la posible superioridad de un algoritmo sobre otro. Véanse J. Demsar (2006) y S. García et al.
(2008) para mayor información a este respecto.
4.1.1.4. La librería scikit-learn
Para ilustrar el manejo práctico de los algoritmos de aprendizaje automático
que se exponen en las siguientes secciones se empleará una librería de distribución libre para Python dedicada a esta materia: scikit-learn. En esta librería se
encuentran implementados numerosos algoritmos de aprendizaje automático,
así como diversos procedimientos relacionados y, junto con la característica
sencillez de Python, proporciona un entorno rápido y cómodo para su uso. La
librería scikit-learn viene ya incluida con Anaconda, por lo que si se ha instalado
esta última distribución es posible empezar a usar scikit-learn directamente. En
caso contrario, para instalar scikit-learn es necesario tener instalado Python y
142
CUADERNOS METODOLÓGICOS 60
las librerías NumPy y SciPy, y se recomienda también instalar la librería
Matplotlib para permitir el uso de herramientas gráficas 8.
De manera básica, es posible ver la librería scikit-learn como un conjunto
de módulos, dedicados a áreas o métodos generales dentro del aprendizaje
automático, cada uno de los cuales contiene, a su vez, una colección de funciones que realizan labores concretas dentro del área de su módulo. Así, por
ejemplo, el módulo datasets incorpora funciones para cargar diversos conjuntos de datos en un programa. Una de estas funciones del módulo datasets es
load_iris, que carga el conocido conjunto de datos iris de Fisher. Otra función
de este módulo es load_digits, que carga un conjunto de ejemplos para reconocimiento de dígitos. Para poder usar las funciones que contiene un módulo,
primero es necesario cargar o importar este en nuestro programa. Con estos
conceptos en mente, es posible ya exponer el siguiente ejemplo sobre reconocimiento de dígitos con scikit-learn.
Para ello, lo primero es abrir una consola de Python o un notebook de
Jupyter, como se ha explicado en la sección 2.1.1. Comencemos por introducir
la siguiente sentencia:
from sklearn import svm, datasets, metrics
Con esta sentencia, se importan de la librería sklearn (scikit-learn) los módulos svm, datasets y metrics. El primero es el módulo de máquinas de soporte
vectorial (SVM, de support vector machines), una metodología de aprendizaje
supervisado que se tratará más adelante, en la sección 4.1.2.4. El segundo es el
ya referido módulo para cargar conjuntos de datos. El tercero, metrics, contiene una colección de funciones que calculan diversas medidas de rendimiento, como las vistas en la sección anterior. Ahora ya es posible utilizar las
funciones proporcionadas por estos módulos, como se realiza a continuación:
digitos = datasets.load_digits()
X = digitos.data
y = digitos.target
clasificador = svm.SVC(gamma=0.001)
clasificador.fit(X, y)
La primera sentencia carga el dataset digits en la variable digitos. Este
conjunto de ejemplos viene dado por una colección de imágenes digitales de
8
Véase http://scikit-learn.org/stable/install.HTML
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
143
dígitos, cada una con su etiqueta, que identifica el dígito representado en la
imagen en cuestión. En la figura 4.4 se pueden observar algunos ejemplos de
estas imágenes y sus correspondientes etiquetas. Las dos siguientes sentencias asignan a X los N = 1.797 vectores de inputs de los ejemplos (contenidos
en la parte data del dataset digitos) y a y las N etiquetas de estos ejemplos
(contenidas en la parte target de digitos). La siguiente sentencia crea un clasificador, que en este caso será un support vector classifier (SVC), un caso
particular de máquina de soporte vectorial, y especifica un valor para el
parámetro gamma de este clasificador. Aunque este clasificador esté creado,
aún no ha sido ajustado o entrenado usando los datos de ejemplo. Esto se
realiza en la última sentencia, mediante el método fit (ajustar, en inglés) del
clasificador.
Figura 4.4.
Ejemplos de imágenes de dígitos contenidos en el dataset digits
A continuación, introdúzcanse las siguientes sentencias, en las que se especifica que observados contendrá los target de los ejemplos, predichos, las predicciones realizadas por el clasificador sobre los inputs de entrenamiento X, y
se requiere que la función classification_report del módulo metrics realice la
comparación entre valores observados y predichos para computar diversas
medidas de rendimiento, así como la construcción de la matriz de confusión.
observados = digitos.target
predichos = clasificador.predict(X)
print("Informe de rendimiento del clasificador:\n%s\n"%
(metrics.classification_report(observados, predichos)))
print("Matriz de confusión:\n%s" %
metrics.confusion_matrix(observados, predichos))
Al ejecutar el programa hasta este punto, se obtiene la siguiente salida,
especificando para cada clase o dígito (del 0 al 9, en la primera columna) las
medidas de precisión (precision), exhaustividad (recall), F (f1-score) y soporte
(support, que indica el número de ejemplos provenientes de cada clase), y
dando la matriz de confusión requerida:
144
CUADERNOS METODOLÓGICOS 60
Informe de rendimiento del clasificador:
precision
recall
f1-score
support
0
1.00
1.00
1.00
178
1
1.00
1.00
1.00
182
2
1.00
1.00
1.00
177
3
0.99
1.00
1.00
183
4
1.00
1.00
1.00
181
5
1.00
0.99
1.00
182
6
1.00
1.00
1.00
181
7
1.00
1.00
1.00
179
8
1.00
1.00
1.00
174
9
0.99
0.99
0.99
180
avg / total 1.00 1.00 1.00 1797
Matriz
[[178
[   0
[   0
[   0
[   0
[   0
[   0
[   0
[   0
[   0
de confusión:
  0
  0
  0
182
  0
  0
  0
177
  0
  0
  0
183
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
  1
  0
  0
  0
  0
181
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
181
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
181
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
179
  0
  0
  0
  0
  0
  0
  0
  0
  0
  0
174
  0
  0]
  0]
  0]
  0]
  0]
  1]
  0]
  0]
  0]
179]]
Obsérvese que el rendimiento del clasificador sobre la muestra de entrenamiento es casi perfecto, alcanzando valores de precisión y exhaustividad iguales o muy cercanos a 1, y disponiendo la mayoría de ejemplos clasificados en
la diagonal de la matriz de confusión. Sin embargo, como se explica en la sección anterior, estimar el rendimiento del clasificador en la muestra de entrenamiento puede sobrevalorar su rendimiento real, por lo que a continuación
se dividirá aleatoriamente el conjunto de ejemplos en dos muestras, una para
entrenamiento y otra para test, esta última con un 40% de los casos (test_
size=0.4). Para esto, se importa del módulo sklearn.model_selection las funciones train_test_split y cross_val_score (esta última se usará luego
para realizar validación cruzada), y se ejecutan las siguientes sentencias:
from sklearn.model_selection import train_test_split, cross_
val_score
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X , y, test_size=0.4, random_state=1)
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
145
clasificador.fit(X_entrenamiento, y_entrenamiento)
observados = y_test
predichos = clasificador.predict(X_test)
print
("Informe de rendimiento del clasificador:\n%s\n" %
(metrics.classification_report(observados, predichos)))
print("
Matriz de confusión:\n%s" %
metrics.confusion_matrix(observados, predichos))
De este modo, X_entrenamiento e y_entrenamiento contendrán, respectivamente, los vectores de inputs y target de la muestra de entrenamiento, mientras que X_test e y_test contendrán los de la muestra de test. El clasificador se
vuelve a ajustar, esta vez usando la muestra de entrenamiento, pero a la hora
de evaluar su rendimiento se utiliza la muestra de test. El resultado obtenido
es ahora el siguiente:
Informe de rendimiento del clasificador:
precision
recall
f1-score
support
0
1.00
1.00
1.00
74
1
1.00
0.99
0.99
68
2
0.99
1.00
0.99
68
3
0.99
0.98
0.98
83
4
1.00
1.00
1.00
79
5
0.97
0.97
0.97
65
6
0.99
1.00
0.99
70
7
0.99
0.99
0.99
74
8
0.97
0.98
0.98
62
9
0.96
0.95
0.95
76
avg / total 0.98 0.98 0.98 719
Matriz
[[74
[ 0
[ 0
[ 0
[ 0
[ 0
[ 0
[ 0
[ 0
[ 0
de confusión:
0
0
0
0
67
0
0
0
0
68
0
0
0
1
81
0
0
0
0
79
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
63
0
0
0
2
0
0
0
0
0
1
70
0
0
0
0
0
0
1
0
0
0
73
0
0
0
1
0
0
0
0
0
0
61
1
0]
0]
0]
0]
0]
1]
0]
1]
1]
72]]
146
CUADERNOS METODOLÓGICOS 60
Obsérvese que el clasificador obtiene sobre la muestra de test un rendimiento algo más bajo que en el entrenamiento, con diversos ejemplos que no
se clasifican adecuadamente (e. g., un 9 se clasifica como 3, otro dos 9 como 5,
un 1 como 8, etc.). La tasa de acierto en la muestra de test puede computarse
con la sentencia print(clasificador.score (X_test, y_test)), que
arroja un valor de 0,9847, esto es, se estima que el clasificador será capaz de
predecir correctamente más del 98% de los dígitos.
Para aplicar un marco de validación cruzada con cinco iteraciones (cv = 5),
se puede utilizar el siguiente código, donde se importa la librería de funciones
matemáticas numpy para poder requerir, en la última sentencia, el cálculo de
la media (mean) y desviación típica (std) de los resultados obtenidos en test en
cada iteración de la validación cruzada:
import numpy as np
val_cruzada = cross_val_score(clasificador, X, y, cv=5)
print("Tasas de acierto en test por iteración de validación
cruzada:",
val_cruzada)
print(
"Tasa de acierto media en la validación cruzada: ",
val_cruzada.mean())
print(
"Desviación típica de las tasas de acierto: ",
val_cruzada.std())
La salida mostrada al ejecutar estas sentencias es la siguiente:
Tasas de acierto en test por iteración de validación cruzada:
[0.97527473 0.95027624 0.98328691 0.99159664 0.95774648]
Tasa de acierto media en la validación cruzada:
0.9716361987950688
Desviación típica de las tasas de acierto:
0.015469771092169293
Así pues, la estimación que arroja la validación cruzada es algo más conservadora que la obtenida mediante una única división del conjunto de
ejemplos, estimando ahora la tasa de acierto real del clasificador en algo
más del 97%, y ofreciendo, además, una medida de la dispersión de esta
estimación.
Con este ejemplo, esperamos haber convencido al lector de que scikit-learn
proporciona un entorno relativamente sencillo y cómodo para ejecutar algoritmos de aprendizaje automático y medir su rendimiento, incorporando funciones de fácil acceso y uso y una sintaxis amable, que hace intuitivo el significado
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
147
del código. En cualquier caso, se recomienda encarecidamente que el lector
consulte la web http://scikit-learn.org/stable/documentation.HTML para profundizar en el uso de esta librería y en las posibilidades que permite.
4.1.2. Algoritmos de aprendizaje automático
En esta sección se exponen diversos procedimientos o algoritmos de aprendizaje que toman datos como entrada y producen a partir de ellos un programa
o mecanismo que proporciona una solución a una tarea dada. En este sentido,
se tratarán aquí algoritmos para las tareas de clasificación, regresión y clus­
tering o análisis de conglomerados, posiblemente las que representan categorías más amplias de problemas y cuentan con una mayor aplicación. Los
algoritmos que se exponen aquí son los siguientes:
— Algoritmo de los k vecinos más cercanos.
— Árboles de decisión.
— Clasificador bayesiano
— Redes neuronales artificiales.
— Máquinas de soporte vectorial.
— Bosques aleatorios o random forest.
— Algoritmo de las K medias.
Cada algoritmo se introduce primero desde una perspectiva teórica, describiendo su planteamiento y sus características más relevantes. Para no ahuyentar a los neófitos, se intenta que esta exposición teórica se mantenga
siempre en un registro accesible y no demasiado técnico, aunque sin restarle
profundidad y rigor y sin evitar tratar algunos aspectos formales de los modelos matemáticos y computacionales subyacentes. Una vez expuestos los aspectos principales de cada metodología de aprendizaje, se pasa a ilustrar su
manejo con un ejemplo práctico basado en el uso de Python, y, en particular,
de la librería scikit-learn. Estos ejemplos prácticos intentan ser complementarios entre sí, ilustrando con cada algoritmo mecanismos y posibilidades diferentes de cara a que el lector pueda formarse una perspectiva más amplia del
uso de este software.
Es importante insistir en que estos ejemplos prácticos están pensados para
ilustrar exclusivamente el uso de estos procedimientos de aprendizaje, y no
tanto su aplicación al contexto de las ciencias sociales. En este sentido, los
ejemplos de esta sección utilizan siempre conjuntos de datos que, como el
dataset digits recién visto, vienen integrados con scikit-learn. Esto permite que
el lector pueda ejecutar con la mayor facilidad el código requerido, sin tener
que descargar u obtener datos por otras vías, y centrar la exposición en el uso
y las posibilidades que ofrece esta librería.
148
CUADERNOS METODOLÓGICOS 60
4.1.2.1. El algoritmo de los k vecinos más cercanos
En la sección 4.1.1.3 se propuso un ejemplo de un reconocedor de dígitos que,
dada una muestra de entrenamiento E formada por una colección de imágenes de dígitos escritos a mano junto con sus correspondientes etiquetas, simplemente memoriza las imágenes dadas con sus etiquetas. De este modo, al
presentársele una de las imágenes en E responde asignándole la etiqueta o
clase correcta. Como se mencionaba, el problema de este clasificador es que
no sabe qué hacer cuando se le presenta una imagen de un dígito que no está
en E, ya que solo se le ha enseñado a reconocer los ejemplos de la muestra de
entrenamiento. En este sentido, este procedimiento de aprendizaje basado en
simplemente memorizar la muestra de entrenamiento E genera programas
objetivo pobres en tanto que no provee mecanismos de generalización e inferencia que permitan aplicar el conocimiento o experiencia contenidos en E a
nuevos casos no presentes en esta muestra.
En esta sección se expondrá una de las soluciones más sencillas a este problema de la generalización, que parte de la idea, quizá naive pero efectiva en
cualquier caso, de que si dos cosas se parecen en su descripción, posiblemente
también se comporten de manera parecida o sean la misma cosa. Continuando
el ejemplo anterior, de cara a clasificar una imagen de un dígito que no coincide con ninguno de los ejemplos proporcionados en E, esta concepción sugiere
intentar averiguar cuáles son los ejemplos en E más similares o cercanos a esa
imagen, y asignarle a esta una etiqueta en función de las etiquetas que presenten esos «vecinos» de la imagen que clasificar. Este es, de hecho, el funcionamiento básico del algoritmo de los k vecinos más cercanos o k-NN (del inglés k
nearest neighbours). La base de este algoritmo, aplicable a tareas de regresión
y clasificación, consiste en que, a partir de un conjunto de entrenamiento E,
cuando se presenta una nueva instancia con vector de inputs x = (x1,...,xn) cuya
variable target ha de ser predicha, se han de seleccionar los k ejemplos
(x1;y1),...,(xk;yk) de E cuyos vectores de inputs x 1 = (x11 ,..., x 1n ),..., x k = (x1k ,..., x nk )
sean más similares o próximos a x, y predecir para esta instancia un output o
target relacionado con los target conocidos y1,...,yk de esos k vecinos.
En problemas de regresión, el valor predicho ŷ del target numérico para
una nueva instancia x suele obtenerse mediante la media aritmética de los targets y1,...,yk de los k vecinos seleccionados, esto es
yˆ =
1
N
k
∑y .
i
i =1
En problemas de clasificación, la clase asignada a la nueva instancia x es la
clase más numerosa entre los k vecinos seleccionados. Es decir, cada vecino
vota por una clase (la suya), y la clase con más votos es la que se asigna a x.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
149
Un problema que puede surgir con este procedimiento es que haya empate
entre las clases más votadas. Nótese que cuando k = 1, siempre hay una clase
ganadora, la del único vecino seleccionado. En problemas binarios, con dos
clases, si k es impar también es imposible que surjan empates. Cuando el
número de clases es mayor que 2, los empates pueden surgir con cualquier
elección de k: por ejemplo, en un problema de tres clases con k = 8, dos clases
podrían recibir tres votos cada y la tercera, los dos votos restantes. Son posibles diversas estrategias para resolver estas situaciones de empate, como asignar la instancia a la clase, de entre las que estén empatadas, con el vecino más
cercano, sortear la asignación entre las clases empatadas, seleccionar un
nuevo vecino que rompa el empate o incluso dejar la instancia sin clasificar
(ya que la mayor parte de las veces los empates se dan para instancias situadas
en la frontera de decisión entre dos o más clases, para las que cualquier clasificación puede ser poco confiable).
Un aspecto crucial de la metodología k-NN es el modo en que se mide el
parecido o similitud entre la instancia que predecir y los ejemplos en la muestra de entrenamiento. Esta cuestión suele enfrentarse asumiendo que la similaridad entre dos inputs x 1 = (x11 ,..., x 1n ) y x 2 = (x12 ,..., x n2 ) es equivalente a su
cercanía geométrica en el espacio de inputs X = X 1 × ... × X n, de modo que x1 y
x2 serán tanto más similares cuanto menor sea la distancia entre los vectores
(x11 ,..., x 1n ) y (x12 ,..., x n2 ). Esto reduce la cuestión a determinar cómo medir la distancia entre dos vectores de inputs. Un enfoque general consiste en asumir
que la distancia en el espacio X entre dos inputs x1 y x2 toma la forma
d X (x 1 , x 2 ) =
n
∑d (x
i
1
i
, x i2 )2 ,
i =1
donde d i (x 1i , x i2 ) denota la distancia entre los valores x 1i y x i2 de la i-ésima
variable explicativa de ambas instancias. Así, la cuestión se reduce ahora a
determinar cómo medir la distancia entre dos valores de cada una de las diferentes variables explicativas. Para una variable numérica, la opción habitual
es usar el valor absoluto de la diferencia entre x 1i y x i2, esto es, d i (x 1i , x i2 ) =| x 1i − x i2 |.
Para variables explicativas categóricas, lo habitual es usar una comparación
lógica entre las dos categorías x 1i y x i2, de modo que d i (x 1i , x i2 ) = 0 si x 1i = x i2,
esto es, si ambas categorías son la misma, y d i (x 1i , x i2 ) = 1 cuando x 1i ≠ x i2. Si se
entiende que algunos pares de categorías son más similares entre sí que otros
(por ejemplo, invierno y verano podrían considerarse categorías menos similares que invierno y otoño), es posible introducir valores intermedios entre 0 y
1 para representarlo (por ejemplo, podría especificarse que d(invierno,otoño)
= 0,5 y d(invierno,verano) = 1).
Aunque este enfoque proporciona una manera efectiva de calcular distancias
entre una instancia que predecir y los ejemplos de la muestra de entrenamiento,
150
CUADERNOS METODOLÓGICOS 60
por sí solo puede dar lugar a comportamientos indeseables. Por ejemplo, supongamos que se cuenta con dos variables explicativas, X1 = precio de un viaje en
euros y X2 = estación del año en que se realiza. Claramente, la primera es una
variable numérica continua y la segunda es una variable categórica, y es totalmente factible que las diferencias entre los valores de X1 de dos instancias sean
del orden de centenares o miles de euros, mientras que, siguiendo lo anteriormente dicho, las distancias entre categorías de X2 serán a lo sumo 1. En esta
situación, la distancia global dX estará completamente dominada por la primera
variable, haciendo irrelevante cualquier diferencia en la segunda.
En general, este desequilibrio se presenta frecuentemente cuando las diversas variables explicativas están medidas en unidades diferentes. Por tanto, el
procedimiento para la medición de las distancias debe acompañarse siempre
de algún tipo de escalamiento o normalización de los datos, especialmente de
los atributos numéricos, que provea una escala común en la que las diferencias de valores de las distintas variables tengan una magnitud similar. Esta
escala común suele ser el intervalo [0,1]. De las múltiples maneras de realizar
esta normalización, quizá la más simple (y posiblemente por ello una de las
más extendidas) es la que procede primero identificando los valores máximo
y mínimo de la variable Xi que normalizar, y luego reemplazando cada valor
xi de esta variable mediante la expresión
xi =
x i − min( X i )
.
max( X i ) − min( X i )
Por ejemplo, si los valores de partida del atributo Xi son 2, –4, 6 y 3, entonces el mínimo es mín(Xi) = –4, y restando este a los valores anteriores se
obtiene 6, 0, 10 y 7. Es decir, el nuevo mínimo es 0, y el nuevo máximo es
máx(Xi) – mín(Xi) = 6 – (–4) = 10, por lo que al dividir entre esta cantidad se
obtienen los valores normalizados 0,2, 0, 1 y 0,7.
Una vez normalizados los vectores de atributos de los ejemplos de la muestra de entrenamiento E y de la instancia que predecir x, el algoritmo procede
calculando las distancias entre x y todos los ejemplos en E, ordenando estas
distancias de menor a mayor y seleccionando los k primeros ejemplos más
cercanos a x. A partir de estos vecinos se realiza entonces la predicción del target de x siguiendo el procedimiento ya descrito.
En este procedimiento, el valor de k juega un papel importante. Un número
mayor de vecinos permite al algoritmo tener más información con base en la
cual tomar una decisión sobre la nueva instancia que predecir. Con k = 1, la
decisión se basa en un único ejemplo, el más cercano. En un problema en que
la variable target pueda estar afectada por alguna fuente de ruido, una predicción basada en un único ejemplo puede no ser confiable. En general, en tanto
que el ruido se suele distribuir aleatoriamente, es factible que exista siempre
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
151
un número reducido de ejemplos afectados en la vecindad de cualquier input.
Por ello, al aumentar k se reduce la potencial influencia de esos pocos ejemplos ruidosos, ya que normalmente se incorporarán al grupo de k vecinos más
ejemplos no ruidosos que ruidosos. Sin embargo, llegado un determinado
punto, es habitual que al aumentar el valor de k se comience a producir una
disminución del rendimiento. La explicación a esto es sencilla: al aumentar k
es también cada vez más probable que los vecinos más alejados de entre los k
seleccionados sean lo suficientemente lejanos como para tener comportamientos diferentes a los de los vecinos no ruidosos más cercanos. Esto ya no
es una cuestión de ruido, sino de que al generalizar se está permitiendo la
influencia de ejemplos que ya no son parecidos a la instancia x que predecir.
Por las razones anteriores, es usualmente conveniente usar un procedimiento
de test para medir el rendimiento con diferentes valores de k y optar por el que
permita un menor error de generalización.
Ejemplo práctico
Veamos cómo realizar con scikit-learn este procedimiento de test del algoritmo de los k vecinos más cercanos con diversos valores de k. Para ello, se
usará el conjunto de datos iris de Fisher, probablemente uno de los datasets
más conocidos en el campo de la clasificación supervisada. Este conjunto iris
contiene 150 ejemplos de tres especies de flor iris (iris setosa, iris virgínica e
iris versicolor), 50 de cada. Cada ejemplo de flor viene descrito por cuatro atributos o variables explicativas, dados por la longitud y anchura del sépalo y del
pétalo (en centímetros), y se cuenta, por supuesto, con la etiqueta (la especie)
de cada ejemplo. El objetivo con este conjunto de datos es entrenar un clasificador que sea capaz de discriminar entre las tres especies a partir de los cuatro atributos disponibles.
Apliquemos a este problema el algoritmo k-NN recién descrito, que se
encuentra en el módulo neighbors de scikit-learn. La función de este módulo
para la tarea de clasificación es KNeighborsClassifier, y la dedicada a regresión
es KNeighborsRegressor. Como se ha comentado, puede ser conveniente normalizar o escalar los atributos para que se encuentren en una escala común (aunque esto no es estrictamente necesario en este conjunto iris, ya que los cuatro
atributos se miden en centímetros). Para ello, se importará la función MinMaxScaler del módulo preprocessing, la cual lleva a cabo el procedimiento de
normalización descrito anteriormente con base en el máximo y el mínimo de
cada atributo, transformando cada atributo al intervalo [0,1]. El conjunto de
datos ya escalado será entonces dividido en una muestra de entrenamiento y en
otra de test. Una vez realizada la división de los ejemplos, se aplicará el algoritmo de los k vecinos usando valores de k entre 1 y 15, y se obtendrá para cada
k la tasa de acierto sobre la muestra de test. La muestra de entrenamiento se
152
CUADERNOS METODOLÓGICOS 60
usa, por tanto, para obtener los k vecinos más cercanos de cada ejemplo de test,
y clasificarlos en función de las clases observadas de esos vecinos.
El código Python necesario para este proceso se da a continuación. Nótese
que, siguiendo la explicación anterior, este se estructura en cinco bloques, de
tres líneas cada uno, excepto los dos últimos, con una y cuatro líneas, respectivamente. El primer bloque realiza la importación de los módulos y funciones
que se utilizarán. El segundo bloque utiliza la función datasets.load_
iris para cargar el conjunto de ejemplos iris. El tercer bloque realiza la normalización de los atributos originales, contenidos en la variable X, que será
reemplazada por los atributos escalados. El cuarto bloque realiza la división de
conjunto de ejemplos normalizados en las muestras de entrenamiento y test
(esta última contendrá el 40% de los ejemplos). Finalmente, el quinto bloque
ajusta el algoritmo k-NN con valores de k entre 1 y 15 y calcula la tasa de
acierto sobre la muestra de test. Obsérvese que este bloque contiene una sentencia for, que repite la ejecución del grupo de sentencias indentadas situadas
debajo, en bucle, de manera que k toma en cada repetición un valor diferente
en el rango entre 1 y 16 (sin incluir este último).
from sklearn import neighbors, datasets
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
iris = datasets.load_iris()
X = iris.data
y = iris.target
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X , y,
test_size=0.4, random_state=13)
for k in range(1,16):
clasificador = neighbors.KNeighborsClassifier(k)
clasificador.fit(X_entrenamiento, y_entrenamiento)
print("Tasa de acierto para k = %s: %s" %
(k,clasificador.score(X_test, y_test)))
La salida que se obtiene tras la ejecución de este código es la siguiente:
Tasa de acierto para k = 1: 0.9333333333333333
Tasa de acierto para k = 2: 0.95
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
Tasa
de
de
de
de
de
de
de
de
de
de
de
de
de
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
acierto
para
para
para
para
para
para
para
para
para
para
para
para
para
k
k
k
k
k
k
k
k
k
k
k
k
k
=
=
=
=
=
=
=
=
=
=
=
=
=
153
3: 0.9666666666666667
4: 0.9666666666666667
5: 0.9666666666666667
6: 0.9666666666666667
7: 0.9833333333333333
8: 0.9833333333333333
9: 0.9666666666666667
10: 0.95
11: 0.9666666666666667
12: 0.9666666666666667
13: 0.9666666666666667
14: 0.9666666666666667
15: 0.95
Nótese que, partiendo de k = 1, el algoritmo parece mejorar su rendimiento
en la clasificación de las iris al aumentar k, hasta que a partir de k = 9 se
empieza a observar una caída del rendimiento. Con las muestras de entrenamiento y test seleccionadas, la consideración de un número mayor de vecinos
parece aportar robustez al clasificador, pero pasado el umbral k = 8 posiblemente este número mayor de vecinos puede empezar a incorporar ejemplos
inadecuados, demasiado alejados de las instancias que clasificar. Así pues, k =
7 u 8 parece ser la mejor elección para aplicar este clasificador al dataset iris.
No obstante, al procedimiento anterior se le pueden realizar algunas objeciones. En primer lugar, el rendimiento del 98,3% estimado para los mejores
valores de k (7 u 8) puede ser altamente dependiente de la selección aleatoria
realizada de las muestras de entrenamiento y test. De hecho, esto se puede
aplicar también a las estimaciones realizadas para todos los (15) valores de k.
La razón es que las tasas de acierto estimadas pueden ser poco robustas, ya
que han sido obtenidas con una única división de los ejemplos entre entrenamiento y test, y una división distinta (con otra semilla aleatoria random_state)
podría arrojar resultados sensiblemente diferentes. Consecuentemente, en
segundo lugar, la selección de k = 7 u 8 como mejores valores de este parámetro podría ser también dependiente de esta falta de robustez, y el rendimiento
real del clasificador obtenido con estos valores de k podría ser significativamente diferente.
Para subsanar esta falta de robustez, se puede aplicar un procedimiento de
validación cruzada (véase la sección 4.1.1.2.1) en la estimación de las tasas
de acierto de los diferentes valores de k, de manera que ahora estas tasas de
acierto para cada k se obtengan como el promedio de una colección de K estimaciones, cada una realizada con una muestra de entrenamiento y test diferentes. De hecho, para permitir la comparación con el ejemplo anterior, lo
que se hará es realizar la misma división del conjunto iris en una muestra de
entrenamiento y otra de test con el 40% de los ejemplos, y, a continuación,
154
CUADERNOS METODOLÓGICOS 60
para cada número de vecinos k, realizar la validación cruzada con K = 10
usando solo la muestra de entrenamiento.
Esto es, la muestra de entrenamiento con el 60% de los ejemplos originales
será dividida, a su vez, en diez partes, y en cada una de las diez iteraciones de
la validación cruzada se formará una muestra de entrenamiento, con la que se
ajustará el k-NN, uniendo nueve de esas diez partes. La parte restante, una
diferente en cada iteración, se usará como muestra de validación para obtener
las K = 10 estimaciones que luego se promediarán para cada k. De este modo,
se obtendrá para cada k una estimación más robusta en tanto que esta será
ahora el promedio de diez estimaciones parciales. Esto puede permitir entonces una selección del mejor valor de k más confiable.
Finalmente, para obtener una estimación real del rendimiento del clasificador k-NN con el mejor k, se ajustará este a toda la muestra de entrenamiento
con el 60% de casos y se usará la muestra de test seleccionada al principio con
el 40% de los ejemplos para estimar su rendimiento. Como estos ejemplos de
test no han participado en la selección del mejor k, este clasificador no estará
sesgado hacia el ruido propio de esta muestra de test, mientras que sí puede
estarlo hacia el ruido de la muestra de entrenamiento con la que se ha realizado
la selección del mejor k. Por esta razón, el uso de la muestra de test separada
permitirá una evaluación más fiable del clasificador k-NN seleccionado.
Nótese que este procedimiento constituye una variante del marco entrenamiento-validación-test (véase de nuevo la sección 4.1.1.2.1), en la que, en lugar
de realizar solo una división en tres muestras, se realiza de entrada la división
entre entrenamiento y test, y en la fase de validación se utiliza validación cruzada sobre la muestra de entrenamiento en lugar de una muestra de validación
independiente. Finalmente, el rendimiento real del clasificador seleccionado se
estima con la muestra de test, que ha permanecido ajena al proceso de selección
de la mejor configuración paramétrica del algoritmo de aprendizaje.
Se da a continuación el código Python que realiza este proceso de validación
cruzada para la selección de k con test posterior, el cual se obtiene con unas
pocas modificaciones del código anterior. Nótese la introducción de comentarios, identificables por ir siempre a continuación del símbolo #, y por su color
verde en el código del repositorio. Estos comentarios permiten insertar en el
código algunas explicaciones o aclaraciones sin que el programa se vea afectado. Al detectar el símbolo #, el programa deja de leer hasta la siguiente línea,
por lo que no procesa estas partes comentadas. Aparte, nótese también la inclusión de una sentencia if en el bucle for. Esta sentencia permite comparar la
tasa de acierto para un k (obtenida como la media de las diez estimaciones de
la validación cruzada) con la mejor media obtenida hasta ese momento, de
manera que si la nueva media es mejor que las anteriores se almacenan su valor
y el valor de k con que se ha obtenido. La variable que almacena esta mejor
media, max_media, se inicializa a 0 antes del bucle para permitir la comparación cuando k = 1.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
155
# Importación de módulos y funciones
from sklearn import neighbors, datasets
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split,cross_
val_score
# Carga y normalización de los datos
iris = datasets.load_iris()
X = iris.data
y = iris.target
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
# División de los ejemplos entre entrenamiento y test
X_entrenamiento, X_test, y_entrenamiento, y_test = train_
test_split(X , y,test_size=0.4, random_state=13)
max_media = 0
# Bucle para la selección de k
for k in range(1,16):
clasificador = neighbors.KNeighborsClassifier(k)
# validación cruzada con cv=10 iteraciones
val_cruzada =
cross_val_score(clasificador, X_entrenamiento,
y_entrenamiento, cv=10)
# val_cruzada = tasas de acierto de las 10 iteraciones
media = val_cruzada.mean() # Media de las 10 tasas de
acierto
# Estimación para k
print("Tasa de acierto para k = %s: %s" %(k,media))
if media > max_media: #Si mejora las anteriores, se almacena con k
max_media = media
mejor_k = k
print("Mejor k =", mejor_k)
print("Tasa de acierto para mejor k =", max_media)
#Entrenar k-NN con el mejor k y la muestra de entrenamiento
completa
clasificador = neighbors.KNeighborsClassifier(mejor_k)
clasificador.fit(X_entrenamiento, y_entrenamiento)
# estimación final del rendimiento del k-NN con la muestra de
test
print("Estimación del rendimiento real:",
clasificador.score(X_test, y_test))
156
CUADERNOS METODOLÓGICOS 60
La salida que proporciona la ejecución de este código es la siguiente:
Tasa de acierto para k = 1: 0.9552777777777777
Tasa de acierto para k = 2: 0.9552777777777777
Tasa de acierto para k = 3: 0.9566666666666667
Tasa de acierto para k = 4: 0.9566666666666667
Tasa de acierto para k = 5: 0.9677777777777777
Tasa de acierto para k = 6: 0.9566666666666667
Tasa de acierto para k = 7: 0.9566666666666667
Tasa de acierto para k = 8: 0.9566666666666667
Tasa de acierto para k = 9: 0.9677777777777777
Tasa de acierto para k = 10: 0.9677777777777777
Tasa de acierto para k = 11: 0.9677777777777777
Tasa de acierto para k = 12: 0.9566666666666667
Tasa de acierto para k = 13: 0.9677777777777777
Tasa de acierto para k = 14: 0.9677777777777777
Tasa de acierto para k = 15: 0.9677777777777777
Mejor k = 5
Tasa de acierto para mejor k = 0.9677777777777777
Estimación del rendimiento real: 0.9666666666666667
Nótese que las tasas de acierto para las diferentes k son ahora más estables
que antes, con menores variaciones entre distintos números de vecinos. Esto
es consecuencia de la mayor robustez que proporciona la validación cruzada.
El procedimiento selecciona ahora k = 5, ya que es el primer k que obtiene el
mejor rendimiento en validación cruzada de 96,77%, aunque otros valores
más altos de k (por ejemplo, k = 9, o 15) alcanzan la misma tasa de acierto. El
5-NN ajustado con la muestra de entrenamiento obtiene una tasa de acierto
del 96,66% en la muestra de test, algo más bajo que el estimado en la validación cruzada, pero igualmente con una diferencia escasa.
De cara a seleccionar un k entre los diferentes valores que alcanzan el rendimiento medio máximo en validación cruzada, se pueden computar sus estimaciones de rendimiento en la muestra de test y seleccionar el mejor. Se deja
al lector la tarea de comprobar que todos los valores de k entre 9 y 14 (excepto
para k = 10) obtienen la misma tasa de acierto en test de 96,77% (excepto para
k = 10, que es peor), y que a partir de k = 15 el rendimiento parece empeorar.
En estas condiciones, aunque la diferencia es en la práctica mínima si no
insignificante, se podría recomendar quizá la elección del 5-NN, ya que un k
más bajo implica un menor coste computacional, y no hay evidencia de que
un k mayor mejore el rendimiento.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
157
4.1.2.2. Árboles de decisión
La idea fundamental de la metodología de aprendizaje de los árboles de decisión consiste en ir dividiendo secuencialmente el conjunto de entrenamiento
E en subconjuntos más pequeños, de modo que la variable objetivo se comporte cada vez más homogéneamente en estos subconjuntos resultado de las
sucesivas divisiones. Este procedimiento suele describirse usando la metáfora
de un árbol (de ahí el nombre de esta metodología), partiendo de un nodo raíz
que se ramifica en un conjunto de nodos hijos, cada uno de los cuales puede
constituir, a su vez, un nodo padre que se ramifica nuevamente, hasta que este
proceso de división concluye en los llamados nodos terminales u hojas.
Inicialmente, por tanto, se considera que todos los ejemplos en E se encuentran en el nodo raíz. El procedimiento anterior, que comienza por el nodo raíz
y divide secuencialmente los ejemplos entre un grupo de nodos hijos que luego
tomarán, a su vez, el rol de nodos padre, se suele denominar «crecimiento del
árbol» (tree growing). Este crecimiento se lleva a cabo mediante ramificaciones
sucesivas de los nodos no divididos o sin descendencia. Cada ramificación consiste en seleccionar una variable explicativa y un test o prueba que aplicar
sobre los valores de esta variable, de modo que cada nodo hijo recibirá los
ejemplos del nodo padre que obtengan un resultado diferente del test. Por ejemplo, la ramificación puede seleccionar el input x1 y consistir en asignar a un
nodo hijo los ejemplos del nodo padre para los cuales x1 > 5, y a otro nodo hijo
los ejemplos que cumplan la condición opuesta, x1 ≤ 5. La selección de la variable con la que se ramifica y del test concreto se realiza atendiendo a algún criterio de rendimiento, eligiéndose aquellos que proporcionan un mayor
incremento del rendimiento según se mide por este criterio. El proceso de crecimiento del árbol continúa produciendo ramificaciones de los nodos no ramificados hasta que se verifica un criterio de parada. Cuando esto sucede,
concluye la fase de entrenamiento, y existirán nodos no ramificados, que se
denominan nodos terminales u hojas. Estas hojas contendrán conjuntamente
todos los ejemplos en E.
La predicción de la variable objetivo se realiza independientemente en
cada hoja, atendiendo a los valores del target de los ejemplos contenidos en
ellas. De este modo, la predicción o inferencia sobre una nueva instancia se
lleva a cabo a partir de la hoja particular en que cae esta instancia siguiendo
desde el nodo raíz los diferentes test aplicados. En problemas de regresión, el
valor predicho en una hoja suele obtenerse como la media aritmética del target de los ejemplos que contiene. En problemas de clasificación, se asigna la
clase mayoritaria en la hoja.
La naturaleza de los criterios de rendimiento empleados para la ramificación varía en función de si el problema que tratar es de regresión o clasificación. En el primer caso, se suele hablar de árboles de regresión, y el criterio
habitual para la ramificación es la reducción de la varianza de la variable
158
CUADERNOS METODOLÓGICOS 60
objetivo al pasar de un nodo padre a sus nodos hijos. Esto es, en cada nodo
que ramificar se seleccionan la variable explicativa y el test que permiten una
mayor reducción de varianza. De este modo, la variable objetivo se comportará en los nodos hijos de manera más homogénea (más concentrada alrededor de la media) que en el nodo padre. Así pues, dado un nodo F que ramificar,
y suponiendo por simplicidad que la ramificación es binaria y se obtendrán
dos nodos hijos A y B, la reducción de varianza (RV) de la variable objetivo Y
que se obtiene mediante la ramificación de F en A y B puede medirse como
(
(
RV (F , A , B ) = Var (Y | F ) −
|A|
|B |
Var (Y | A ) +
Var (Y | B )
|F |
|F |
donde | · | denota el cardinal o número de ejemplos contenidos en el nodo
correspondiente, y V(Y | ·) denota la varianza de la variable objetivo calculada
sobre los ejemplos contenidos en un nodo.
En árboles de clasificación, la clave es intentar que la ramificación de un
nodo padre produzca nodos hijos en que la composición de clases sea más
pura, en el sentido de que los ejemplos que formarán parte de cada hijo pertenezcan en lo posible a una única clase. Esto pretende que la ramificación conduzca a nodos hijos en que los ejemplos de diferentes clases que se encontraban
mezclados en el nodo padre se separen. Para medir esta pureza, o, mejor
dicho, el incremento de pureza que propicia una ramificación particular, se
utilizan principalmente dos medidas, la entropía (entropy) de la distribución
de clases y su correspondiente ganancia de información (information gain), y
la impureza de Gini (Gini impurity).
Dado un problema con C clases y un nodo F, la entropía de la distribución
de clases en F se obtiene como
C
entropía (F ) = −∑ pc ⋅ log 2 pc
c =1
donde log2 representa el logaritmo en base 2 y pc denota la proporción de ejemplos en A que pertenecen a la clase c. Si para algún c se tiene que pc = 0, se toma
la convención pc ⋅ log 2 pc = 0 ⋅ log 2 0 = 0. Para entender cómo esta medida de
entropía refleja la impureza de la distribución de clases en un nodo, supongamos un problema binario, con C = 2, y que en el nodo A la proporción de ejemplos de la clase 1 es 0,6 y, por tanto, la de la clase 2 es 0,4. Para c = 1, se obtiene
p1 ⋅ log 2 p1 = 0,6 ⋅ log 2 0,6 = 0,6 ⋅ (−0,737) = −0,4422.
Para c = 2, se tiene
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
159
p2 ⋅ log 2 p2 = 0,4 ⋅ log 2 0,4 = 0,4 ⋅ (−1,322) = −0,5288.
Por tanto, la entropía en este caso es −(−0,4422 − 0,5288) = 0,971. Si en lugar
de esta proporción 6:4 entre clases se tuviese una proporción 9:1, se tendría
p1 ⋅ log 2 p1 = 0,9 ⋅ log 2 0,9 = 0,9 ⋅ (−0,152) = −0,1368,
p2 ⋅ log 2 p2 = 0,1 ⋅ log 2 0,1 = 0,4 ⋅ (−3,3219) = −0,3322,
por lo que la entropía resultante sería −(−0,1368 − 0,3322) = 0,469. Así pues, la
entropía en el segundo caso, con una distribución más pura, es menor que en
el primer caso, en que hay mayor mezcla de clases.
Si el nodo F se ramifica como antes en dos nodos hijos A y B, para medir la
reducción de entropía propiciada por esta ramificación primero hay que computar la entropía combinada de los nodos A y B, que se obtiene ponderando
las entropías de cada nodo por su tamaño relativo al de F, esto es,
entropía ( A , B ) =
B
A
entropía ( A ) + entropía (B ).
F
F
Entonces, la ganancia de información (information gain) o, equivalentemente, la reducción de entropía propiciada por la ramificación del nodo A en
los nodos B y C se mide como
IG (F , A , B ) = entropía (F ) − entropía ( A , B ).
Un valor mayor de esta medida IG indica una mejor ramificación del nodo
padre en términos de conseguir una mayor separación de las clases en los
nodos hijos.
Como decíamos, otra medida de la impureza de la distribución de clases
en un nodo viene dada por la llamada impureza de Gini (Gini impurity), que
para un nodo F y C clases se obtiene como
C
C
c =1
c =1
GI (F ) = 1 − ∑ pc2 = ∑ p i ∑ pd .
d ≠c
El sentido de esta medida puede comprenderse más fácilmente a partir del
término de la derecha de la igualdad anterior. En particular, la impureza de
Gini mide con qué frecuencia sería clasificado de manera incorrecta un
160
CUADERNOS METODOLÓGICOS 60
elemento de F seleccionado aleatoriamente si este se clasificara aleatoriamente de acuerdo con la distribución de etiquetas en F. Así, la probabilidad de
seleccionar aleatoriamente de F un ejemplo de la clase c es pc, y la de que este
elemento se clasifique incorrectamente es 1 – pc = ∑ pd .
d ≠c
La probabilidad buscada se obtiene entonces sumando los productos
pc·(1 – pc) para las C clases, lo que coincide con el término de la derecha de la
expresión para GI y es equivalente al término central. El mínimo valor que
puede tomar GI es 0, lo cual ocurre cuando todos los ejemplos de F pertenecen a la misma clase. Al igual que con la entropía, la reducción de impureza
obtenida mediante la ramificación se mide entonces ponderando las impurezas de los nodos hijos y restándolas a la impureza del nodo padre, es decir,
(
(
RI (F , A , B ) = GI (F ) −
A
B
GI ( A ) + GI (B ) .
F
F
Para árboles tanto de regresión como de clasificación, para determinar la
ramificación concreta de F en A y B que se producirá, se ha de computar la
ganancia en la medida de rendimiento que propician las diferentes variables
explicativas Xi y test que aplicar sobre los valores de Xi. Cuando Xi es una
variable categórica, el test que se aplica viene dado por la partición del conjunto de categorías posibles de esta variable en dos subconjuntos disjuntos.
En este caso, dado un ejemplo en el nodo padre F, si la categoría que toma
esta variable en este ejemplo se encuentra en el primer subconjunto, el ejemplo se asignará al nodo hijo A, y en caso contrario se asignará al nodo hijo B.
Es decir, para cada ejemplo, el test consiste en este caso en dilucidar si la categoría del ejemplo pertenece a un subconjunto de categorías u otro.
Cuando Xi es una variable numérica, el test se establece mediante un
umbral θ, de modo que si para un ejemplo se tiene que Xi ≤ θ, el ejemplo se
asigna al nodo hijo A, y en caso contrario, si Xi > θ, el ejemplo se asigna al hijo B.
Una vez obtenidas las ganancias de rendimiento que permiten las diferentes
variables explicativas y test que aplicar sobre ellas, se seleccionan la variable
y el test que conducen a una mayor ganancia, y se ramifica el nodo padre F en
los nodos hijos resultantes A y B. Este proceso de ramificación es básicamente
el mismo cuando se permite ramificar en más de dos nodos hijos, con la única
diferencia de que los test que aplicar contemplarán entonces más de dos posibles resultados.
Es importante observar que este proceso de crecimiento del árbol podría,
en principio, extenderse hasta que en cada nodo terminal u hoja solo queden
ejemplos de una única clase (teniendo entonces mínima entropía o impureza)
o con valores iguales si el target es numérico (con varianza 0). De hecho, es
posible ramificar hasta que todas las hojas estén formadas por un único ejemplo. Esto obviamente conduciría a un rendimiento perfecto sobre la muestra
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
161
de entrenamiento, ya que en cada hoja se predeciría el valor del target dado
por ese único ejemplo que la forma, obteniéndose, por tanto, un error nulo.
Sin embargo, este nivel de detalle en la discriminación de los ejemplos suele
ser sinónimo de sobreajuste, ya que lo más habitual es que a partir de cierto
punto las ramificaciones atiendan a pequeñas variaciones en las variables
explicativas, producidas por azar, que separan ejemplos con valores diferentes del target. Es decir, a partir de cierto punto el árbol estaría generalizando
variaciones debidas al azar o ruido, lo cual será contraproducente cuando el
árbol se utilice para inferir el target de nuevas instancias en las que el ruido
añadido ha sido diferente.
Por este motivo, suele considerarse necesario introducir algún criterio de
parada que permita detener el proceso de crecimiento del árbol antes de que
su capacidad de generalización se deteriore. Los criterios más extendidos
imponen algún límite sobre el número de ejemplos que pueden contener los
nodos padre o hijo (de modo que no se dividan nodos que contengan un
número de ejemplos igual o menor que ese límite), la ganancia de rendimiento
que debe darse para que se realice una ramificación (no permitiendo la ramificación si su ganancia es menor que el límite definido), o la profundidad del
árbol, esto es, al número máximo de ramificaciones sucesivas desde el nodo
raíz (de modo que el árbol no pueda crecer más allá de cierto número de niveles de profundidad).
Alternativamente, un método más costoso computacionalmente pero más
efectivo para reducir el sobreajuste es el conocido como «poda» del árbol, que
consiste en permitir el crecimiento del árbol a partir de la muestra de entrenamiento sin criterio de parada, para luego reducirlo atendiendo al comportamiento sobre una muestra independiente de test.
Los árboles de decisión constituyen una de las metodologías de aprendizaje más populares y extendidas. Este éxito se debe en gran medida a su interpretabilidad. Nótese que, por la propia estructura del árbol y su proceso de
crecimiento, cada nodo terminal u hoja se alcanza desde el nodo raíz mediante
una serie de condiciones sobre los valores de diversas variables explicativas.
Conjuntamente, estas condiciones que caracterizan a una hoja conforman
una regla que permite explicar por qué una instancia se ha predicho de una
manera determinada.
Como ejemplo, obsérvese la figura 4.5, que muestra un árbol de clasificación realizado sobre los pasajeros del tristemente célebre trasatlántico Titanic. Cada recuadro en la figura constituye un nodo, en el que se informa del
número de nodo, de la proporción de pasajeros en ese nodo que sobrevivieron
o murieron y del porcentaje de pasajeros en ese nodo respecto al total. La
clase mayoritaria de cada nodo se encuentra resaltada en negrita. En el nodo
raíz, en la parte superior del árbol, se encuentran inicialmente todos los pasajeros. Ramificando este nodo mediante la variable sexo, se obtienen dos nuevos nodos, uno para los pasajeros hombres (nodo 2, con el 64% de todos los
162
CUADERNOS METODOLÓGICOS 60
pasajeros) y otro para las mujeres (nodo 3, con el 36%). A continuación, el
nodo 2 de pasajeros se ramifica por la variable edad, usando el punto de corte
o umbral θ = 9,5 años. El nodo 5 resultante de varones de corta edad se ramifica, a su vez, por la variable n.º de familiares a bordo, usando el umbral θ = 2,5
familiares. Así, resulta un árbol con cuatro hojas, dos de las cuales predicen la
clase sobrevive y otras dos la clase muere.
Figura 4.5.
Árbol de clasificación para los pasajeros del Titanic
Cada una de estas cuatro hojas puede asociarse a una regla, que predice
una determinada clase bajo ciertas condiciones. Por ejemplo, el nodo 3 se asocia con la regla «SI sexo = mujer ENTONCES clase = sobrevive (0,73)», que
indica que, si escogemos una pasajera al azar, la probabilidad de que sobreviviese sería del 73%. El nodo 7 se asocia a la regla «SI sexo = hombre Y edad
< 9,5 Y familiares < 2,5 ENTONCES clase = sobrevive (0,89)», que indica que
un 89% de los varones de corta edad con pocos familiares a bordo sobrevivieron al naufragio. Estas dos reglas permiten, de hecho, dar una explicación a
la mayor parte de casos de supervivencia: mujeres o niños con pocos familiares en el barco. Así, la metodología de árboles de decisión proporciona modelos de generalización de los ejemplos intuitivamente sencillos y con una fuerte
capacidad explicativa.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
163
Otra característica distintiva y muy apreciada de los árboles de decisión
es que realizan por sí mismos una selección de las variables explicativas
relevantes para explicar el target. La propia mecánica del proceso de crecimiento del árbol, escogiendo en cada nodo la variable que mejor ramifica los
ejemplos de ese nodo, lleva a cabo esta selección. En particular, las variables
que aparecen en los primeros niveles del árbol suelen ser casi siempre buenos predictores del target. Por este motivo, los árboles son prácticamente
inmunes a la existencia de variables redundantes e irrelevantes (esto es,
variables con la misma información que otras variables, o con información
que no aporta nada para discriminar el target), y muchas veces se usan como
un mecanismo de selección de variables previo a la aplicación de otros procedimientos de aprendizaje 9.
Ejemplo práctico
Una de las características más populares de los árboles de decisión, como se ha
dicho, es que permiten naturalmente una representación gráfica del modelo, de
manera que es posible aprehender de un vistazo su estructura, las variables y
test de ramificación utilizados, profundidad, etc. Esta característica de los árboles de decisión está íntimamente relacionada con su interpretabilidad, que si
bien no depende necesariamente de la representación gráfica (ya que es posible
interpretar un árbol a partir de las reglas que produce), sí que se favorece de
manera importante cuando es posible visualizar los árboles obtenidos.
Veamos entonces cómo utilizar scikit-learn para ajustar árboles de decisión y visualizarlos. De cara a la visualización, es necesario tener instalada
previamente la librería graphviz, que proporciona diversas herramientas para
la visualización de grafos. Para instalarla, asumiendo que se tiene instalada la
distribución Anaconda, se han de seguir los siguientes pasos.
1.Abrir el Anaconda Navigator, lo que se puede hacer a través del menú de
inicio de Windows, buscando en la lista de programas la letra A, bajo la
que aparecerá la carpeta de Anaconda, y dentro de esta, el icono del Anaconda Navigator.
2.Una vez abierto el Anaconda Navigator, seleccionar Environments en el
menú de la derecha en la figura 4.6.
9
Como comparación, nótese que la presencia de variables redundantes e irrelevantes es, por
lo general, considerablemente dañina para el algoritmo de los k vecinos antes descrito. En este
algoritmo, las variables de este tipo no aportan información que permita una mejor discriminación del target, pero, sin embargo, influyen en el cálculo de las distancias entre ejemplos y distorsionan así, la capacidad de otras variables informativas para realizar la discriminación. Por esto,
suele ser conveniente usar algún procedimiento de selección de variables antes de aplicar este
algoritmo (y otros), y los árboles de decisión proporcionan un mecanismo para ello.
164
CUADERNOS METODOLÓGICOS 60
Figura 4.6.
Instalación de la librería graphviz. Selección del menú Environments
3.En el menú desplegable que aparece con la palabra Installed, hay que
seleccionar la opción Not installed en la figura 4.7, y luego hacer clic en
la flecha que separa los paneles de la pantalla para poder observar el
menú seleccionado más cómodamente.
Figura 4.7.
Instalación de la librería graphviz. Selección de la opción Not installed
4.En el recuadro de búsqueda Search packages en la figura 4.8, se debe
escribir graphviz, lo que actualizará la lista de elementos no instalados.
En esta, se seleccionan ahora los recuadros a la izquierda de los dos elementos que aparecen, graphviz y Python-graphviz, y tras ello se pulsa el
botón Apply en la esquina inferior derecha.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
165
Figura 4.8.
Instalación de la librería graphviz. Selección de los paquetes que instalar
5.En la ventana que aparece a continuación, hay que esperar a que el programa termine de verificar el contenido que va a ser instalado, y pulsar
en el botón Apply una vez se ilumine este.
Una vez instalado graphviz, ya es posible obtener visualizaciones de los
árboles ajustados con scikit-learn. El módulo de esta librería dedicado a los
árboles de decisión es tree, el cual contiene las funciones DecisionTreeClassifier y DecisionTreeRegressor para problemas de clasificación
y regresión, respectivamente. El siguiente código ajusta un árbol de clasificación al conjunto de datos iris, introducido en la sección anterior, y lo representa por pantalla. También crea un archivo pdf en la carpeta de ejecución del
programa llamado arbol_iris.pdf, que contiene el árbol representado.
# Importación de módulos
from sklearn.datasets import load_iris
from sklearn import tree
iris = load_iris() # Carga del dataset Iris
# Definición y ajuste de un árbol de clasificación
arbol = tree.DecisionTreeClassifier()
arbol = arbol.fit(iris.data, iris.target)
# Visualización del árbol
import graphviz
datos_grafico = tree.export_graphviz(arbol,out_file=None)
grafico = graphviz.Source(datos_grafico)
grafico.render("iris") # crea el archivo iris.pdf con el árbol
grafico # Esta sentencia requiere la salida del gráfico por
pantalla
166
CUADERNOS METODOLÓGICOS 60
Figura 4.9.
Árbol de clasificación para el dataset iris
El árbol obtenido se muestra en la figura 4.9. Cada nodo no terminal presenta en su primera línea el test empleado para su ramificación. Nótese que en
Python los índices de las listas o vectores empiezan en 0, por lo que X[0]
representa en realidad el primer input x1, X[1] es el segundo input, etc. Los
ejemplos que cumplen el test correspondiente se asignan al nodo hijo de la
izquierda, mientras que los que no lo cumplen se asignan al hijo derecho. La
segunda línea de cada nodo A informa de la impureza de Gini GI(A) del nodo.
La tercera línea informa del número de ejemplos contenidos en cada nodo.
Finalmente, la cuarta línea informa del número de ejemplos pertenecientes a
cada una de las tres clases (especies de iris) en un nodo.
El módulo graphviz permite controlar la apariencia del árbol y el contenido
de los nodos. Así, si se sustituye la primera sentencia tras la importación de
graphviz por la siguiente:
datos_grafico = tree.expo
rt_graphviz(arbol, out_file=None,
feature_names=iris.feature_names,
class_names=iris.target_names,
filled=True, rounded=True,
special_characters=True)
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
167
y se ejecuta el código de nuevo, se obtiene el árbol mostrado en la figura 4.10.
Este es el mismo árbol de la figura anterior, aunque ahora presenta una apariencia más amable, indicando los nombres de las variables que intervienen en
cada ramificación (por ejemplo, petal width (cm)), así como la etiqueta de la
especie predicha en cada nodo (por ejemplo, setosa). Además, los tonos de gris
de relleno de los nodos representan la asignación de clase que realizan y el
nivel de evidencia en que se basa esta asignación. El color gris más claro se
asocia con la clase setosa, el gris claro, con versicolor, y el gris oscuro, con virginica. Los colores blancos indican nodos en que la composición de clases
mantiene cierta impureza.
Figura 4.10.
Árbol de clasificación para el dataset iris, con nombres de variables y clases
y tonos de gris que representan la asignación de cada nodo a una clase
Nótese que este árbol, entrenado con todos los ejemplos disponibles en el
conjunto iris, asigna correctamente las clases de todos los ejemplos. Esto
puede comprobarse observando los nodos terminales u hojas, que contienen
siempre ejemplos de una única clase, con impureza de Gini 0 en todos ellos.
En particular, si se ejecuta la sentencia siguiente:
168
CUADERNOS METODOLÓGICOS 60
print("Tasa de acierto del árbol:",arbol.score(iris.data,
iris.target))
se obtiene la salida
Tasa de acierto del árbol: 1.0
que indica que, efectivamente, el árbol ajustado clasifica correctamente
todos los ejemplos con los que se ha entrenado. Esto no debería sorprender
en tanto que no se ha impuesto ningún límite al crecimiento del árbol, que
entonces procede a ramificar hasta que la impureza es mínima (i. e., 0) en
todas las hojas, ramificando incluso nodos con solo tres ejemplos o con una
impureza muy baja pero aún mayor que 0. Este comportamiento entraña un
riesgo importante de sobreajuste, ya que estas ramificaciones que separan
solo un ejemplo o dos se basan en diferencias de los inputs muy específicas,
válidas para esos pocos ejemplos y probablemente muy sensibles al ruido de
esos pocos casos.
Ilustremos a continuación cómo especificar criterios de parada del crecimiento del árbol para intentar controlar este sobreajuste. Conviene resaltar
que esta es la única opción que permite scikit-learn, ya que esta librería aún no
incorpora procedimientos de poda de los árboles. En particular, nos centraremos en dos parámetros que especifican estos criterios de parada, max_depth
y min_samples_split. El primero permite imponer un límite a la profundidad del árbol, esto es, al número máximo de ramificaciones sucesivas desde
el nodo raíz. El segundo controla el número mínimo de ejemplos que ha de
contener un nodo para poder ser ramificado. Para encontrar la mejor configuración de estos dos parámetros, se usará una búsqueda en rejilla (grid) con
validación cruzada. Esta consiste en definir un rango de variación para cada
parámetro, de modo que para cada combinación de valores de ambos parámetros en sus respectivos rangos se realizará una validación cruzada para
medir con algo de robustez su rendimiento. Tras ello, se selecciona la configuración paramétrica que ofrece un mejor resultado. Como en la sección anterior, esta búsqueda se realiza en un marco de entrenamiento-validación-test,
para estimar el rendimiento real del árbol seleccionado usando una muestra
de test que no haya intervenido en el proceso de búsqueda de la mejor configuración paramétrica.
El código para realizar esta búsqueda en rejilla y estimar el rendimiento
del árbol con la mejor configuración paramétrica se da a continuación. Los
valores del parámetro max_depth se restringen a 3, 4 o 5 (nótese que el árbol
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
169
anterior tiene profundidad 5, así que no tiene sentido buscar más allá), y los
de min_samples_split se buscarán en el rango de 2 a 50. La validación
cruzada consistirá en cv = 5 iteraciones, y se realiza sobre la muestra de entrenamiento con el 60% de los ejemplos.
# Importación de módulos y funciones
from sklearn.datasets import load_iris
from sklearn import tree
from sklearn.model_selection import train_test_split, GridSearchCV
iris = load_iris() #carga del dataset Iris
# División del dataset en entrenamiento y test
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(iris.data ,
iris.target, test_size=0.4, random_state=13)
# Definición de los rangos de los parámetros
parametros_a_ajustar = [{
'max_depth': [3,4,5],
'min_samples_split': range(2, 51)}]
# Búsqueda en rejilla con validación cruzada
# sobre la muestra de entrenamiento
arbol = GridSearchCV(
tree.DecisionTreeClassifier(),
parametros_a_ajustar,cv=5)
arbol.fit(X_entrenamiento, y_entrenamiento)
# Salida de resultados de la búsqueda
print("Mejor configuración paramétrica:",arbol.best_params_)
print("Tasa de acierto en validación de la mejor configuración:",
arbol.best_score_)
print("Estimación del rendimiento real:", arbol.score(X_
test, y_test))
La salida que proporciona la ejecución de este código es la siguiente:
Mejor configuración paramétrica:
{'max_depth': 3, 'min_samples_split': 30}
Tasa de acierto en validación de la mejor configuración:
0.9666666666666667
Estimación del rendimiento real: 0.95
170
CUADERNOS METODOLÓGICOS 60
Así pues, el procedimiento de búsqueda en rejilla con validación cruzada
estima que la mejor configuración de los parámetros es max_depth = 3,
min_samples_split = 30, esto es, un máximo de tres niveles de profundidad del árbol y un mínimo de treinta ejemplos en un nodo que ramificar. En
la validación cruzada sobre la muestra de entrenamiento, el árbol así configurado obtuvo una tasa de acierto media de 96,67%, aunque al estimar su rendimiento real con la muestra de test con el 40% de los ejemplos se obtiene una
tasa de acierto de 95%. De cara a visualizar este árbol de mejor configuración,
se puede ejecutar a continuación el siguiente código.
# Ajuste del árbol con los mejores parámetros sobre la muestra de entrenamiento
arbol = tree.DecisionTreeClassifier(
max_depth=arbol.best_params_['max_depth'],
min_samples_split=
arbol.best_params_['min_samples_split'])
arbol = arbol.fit(X_entrenamiento, y_entrenamiento)
# Visualización del mejor árbol
import graphviz
datos_grafico = tree.expor
t_graphviz(arbol, out_file=None,
feature_names=iris.feature_names,
class_names=iris.target_names,
filled=True, rounded=True,
special_characters=True)
grafico = graphviz.Source(datos_grafico)
grafico
El árbol obtenido se muestra en la figura 4.11. Nótese que los test de ramificación realizados, a excepción del último, separan siempre un número relativamente alto de ejemplos. Las hojas obtenidas son puras, a excepción de la
hoja de la especie versicolor, que tiene una impureza de 0,111, con dos ejemplos de virginica mal clasificados (del total de 27 de esta clase en la muestra de
entrenamiento, con 90 ejemplos). La tasa de acierto de este árbol en la muestra de entrenamiento es, por tanto, de 88/90 = 97,78%. Esta estimación es
corregida a la baja por la obtenida sobre la muestra de test, un 95%. La característica más importante de este árbol resultado de la búsqueda, sin embargo,
es que es claramente más sencillo que el anterior entrenado sin criterios de
parada. Su número de hojas es cuatro, contra nueve del anterior. Esto significa que el nuevo árbol permite explicar la pertenencia a una especie de un
95% de los ejemplos con solo cuatro reglas, una por hoja. En comparación, el
árbol anterior necesitaba nueve reglas. Y, además, las reglas del nuevo árbol
son más sencillas y generales, en tanto que contienen en su premisa a lo sumo
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
171
Figura 4.11.
Árbol seleccionado tras la búsqueda en rejilla con validación cruzada
de los mejores parámetros para los criterios de parada
tres test, mientras que las del anterior podían llegar a especificar hasta cinco
condiciones. Estas circunstancias hacen del nuevo árbol un modelo bastante
más sencillo de interpretar que el anterior. En general, un modelo más sencillo siempre será más interpretable que uno más complejo, pero esa sencillez
suele venir a costa de una pérdida de flexibilidad y rendimiento predictivo. En
este sentido, el árbol seleccionado parece proporcionar un buen equilibrio
entre eficacia predictiva e interpretabilidad.
4.1.2.3. Clasificador bayesiano/naive Bayes
El procedimiento de generalización de las dos metodologías de aprendizaje
expuestas anteriormente, el algoritmo de los k vecinos más cercanos y los
árboles de decisión, se basa en estimar el comportamiento de la variable objetivo Y de una nueva instancia con vector de inputs x = (x1,...,xn) a partir del
comportamiento de esta variable en los ejemplos en E que se encuentran en
un entorno o vecindad de la instancia x. En el caso del algoritmo de los k vecinos más cercanos, los ejemplos que componen esta vecindad vienen dados
precisamente por los k ejemplos más próximos (según un criterio determinado para medir la distancia entre ejemplos) a x. En los árboles de decisión,
172
CUADERNOS METODOLÓGICOS 60
este procedimiento se refina para intentar que estas vecindades sean lo más
puras u homogéneas posible en relación con el comportamiento de la variable
objetivo, de modo que en este caso la vecindad no se define por proximidad
sino que viene dada por los ejemplos que cumplen la misma serie de condiciones sobre las variables explicativas que la instancia x, donde esas condiciones
se han elegido de manera que en cada conjunto de condiciones el comportamiento de la respuesta Y sea lo más homogéneo posible.
Centrándonos en el contexto de los problemas de clasificación, en que la
variable objetivo Y es categórica, es posible interpretar ambos procedimientos
de generalización como sendas estrategias dirigidas a estimar la probabilidad
condicional de cada una de las C clases consideradas dada la instancia x que
clasificar. Esto es, tanto el algoritmo de los k vecinos más cercanos como los
árboles de decisión estiman implícitamente la probabilidad condicionada
p (Y = j | x ) de que la instancia x pertenezca a la clase j-ésima, con j = 1,…,C,
dado el valor de sus atributos o variables explicativas observado en el vector x.
Como ambas metodologías trabajan sin supuestos probabilísticos acerca de la
distribución conjunta de la variable de clasificación objetivo Y y las variables
explicativas X1,…,Xn, esta probabilidad no es obtenible de manera directa, y de
ahí que se recurra a una estrategia de estimación, que, como hemos dicho, se
basa en observar el comportamiento de los ejemplos en E en las mencionadas
vecindades o entornos de la instancia x. Una vez estimada la probabilidad
p (Y = j | x ) para cada una de las C clases, la instancia x se asigna a la clase que
obtenga mayor probabilidad estimada, que, como se ha visto, es equivalente a
asignarla a la clase mayoritaria de los ejemplos de E que se encuentran en la
vecindad de x considerada.
De manera semejante a las metodologías de k vecinos más cercanos y árboles de decisión, la metodología de clasificación bayesiana que se presenta en
esta sección también se basa en la estimación de las probabilidades p (Y = j | x )
de las diferentes clases para cada instancia x que clasificar. Sin embargo, a
diferencia de las anteriores, esta metodología realiza importantes supuestos
de corte probabilístico y distribucional de cara a obtener esas estimaciones.
De hecho, la denominación más extendida de esta metodología, conocida
como «clasificador Bayes ingenuo» (naive Bayes classifier), hace precisamente
referencia a que uno de estos supuestos es en cierto modo poco realista. En
particular, este supuesto ingenuo asume que las variables explicativas X1,…,Xn
son condicionalmente independientes dada cada una de las clases. Este
supuesto de independencia condicional entre las variables explicativas significa que la probabilidad (o densidad de probabilidad) asociada a que una
variable explicativa, digamos Xi, tome un valor dado xi cuando se conocen el
resto de valores de las demás variables explicativas y la clase a la que pertenece la instancia solo depende del valor xi y de la clase en cuestión, pero no de
los valores que toman las demás variables explicativas.
Formalmente, este supuesto se traduce en la igualdad siguiente:
1
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
173
p ( X i = x i | X 1 = x1 ,..., X i −1 = x i −1 , X i +1 = x i +1 ,..., X n = x n ,Y = j ) = p ( X i = x i | Y = j ),
que por simplicidad denotaremos como p (x i | x1 ,..., x i −1 , x i +1 ,..., x n ,Y = j ) = p (x i | Y = j )
,..., x n ,Y = j ) = p (x i | Y = j ). Este supuesto es fuerte en el sentido de que es muy poco frecuente que se cumpla tal independencia entre las variables explicativas para
los ejemplos de cada clase. No obstante, a partir de este supuesto es posible
llevar a cabo la estimación de las probabilidades de interés p (Y = j | x ) con
relativa facilidad. En particular, esta estimación procede mediante el conocido teorema de Bayes, que, aplicado al contexto de clasificación de una instancia x = (x1,...,xn), establece la igualdad
p (Y = j | x ) =
p (Y = j ) p (x1 ,..., x n | Y = j )
,
p (x1 ,..., x n )
para cada clase j = 1,…,C, donde el término p(Y = j) denota la probabilidad a
priori de la clase j, p (x1 ,..., x n | Y = j ) denota la probabilidad de observar la instancia x cuando se supone que proviene de la clase j y p (x1 ,..., x n ) denota la
probabilidad total de observar la instancia x en las diferentes clases.
En condiciones generales, es habitualmente imposible realizar la estimación de las probabilidades p (Y = j | x ) mediante la fórmula anterior, ya que
implicaría conocer la distribución conjunta de todas las variables explicativas
para cada una de las clases. Sin embargo, bajo el supuesto de independencia
condicional es posible simplificar la expresión anterior, ya que entonces se
cumple que
n
p (x1 ,..., x n | Y = j ) = p (x1 | Y = j ) ⋅ ... ⋅ p (x n | Y = j ) = ∏ p (x i | Y = j ) .
i =1
De este modo, en lugar de conocer la distribución conjunta de todas las
variables explicativas en cada clase, solo se precisa conocer su distribución
marginal condicionada a cada clase. Así, la anterior fórmula para p (Y = j | x )
se puede entonces expresar como
n
p (Y = j | x ) =
p (Y = j )∏ p (x i | Y = j )
i =1
p (x1 ,..., x n )
n
∝ p (Y = j )∏ p (x i | Y = j ) ,
i =1
donde el símbolo ∝ se lee como «proporcional a». Es decir, en tanto que la
probabilidad total p (x1 ,..., x n ) en el denominador es la misma para todas las
clases, obtener la clase con una mayor probabilidad p (Y = j | x ) equivale a
174
CUADERNOS METODOLÓGICOS 60
n
obtener la clase con un mayor valor de la expresión p (Y = j )∏ p (x i | Y = j )
i =1
del numerador.
n
En esta última expresión, p (Y = j )∏ p (x i | Y = j ), el término p (Y = j ) se
i =1
suele estimar a partir de la frecuencia relativa observada de la clase j en el
conjunto de entrenamiento E, aunque es igualmente posible establecer otras
proporciones si se dispone de conocimiento a priori sobre la distribución de
clases. Por tanto, para terminar de definir la metodología de clasificación que
n
nos ocupa, solo restaría obtener el valor del término
∏ p (x
i
| Y = j ). Para
i =1
ello, la metodología del clasificador Bayes asume algún modelo de probabilidad conocido para la distribución marginal de cada variable explicativa Xi
en cada clase, lo que permite obtener los diferentes valores p (x i | Y = j ) que
n
intervienen en el producto
∏ p (x
i
|Y = j ) .
i =1
En el caso en que las variables explicativas son de tipo continuo, el supuesto
más habitual es que estas se distribuyen normalmente en cada clase. Esto es,
centrándonos en los valores que toma la variable explicativa Xi en los ejemplos
en E que pertenecen a una clase j, este supuesto asume un modelo de probabilidad normal para tales valores. De este modo, si en la instancia x = (x1,...,xn)
que clasificar la variable Xi toma el valor xi, entonces ha de ser
p (x i | Y = j ) =
1
2πσ
2
i,j
e
−
( x i − μi , j )2
2σ i2, j
,
donde μi , j y σ i2, j denotan, respectivamente, los parámetros de media y varianza
de la distribución normal del input Xi en la clase j-ésima. Estos parámetros
son estimados a partir de los ejemplos en E pertenecientes a esa clase mediante
su media y (cuasi)varianza muestrales en esa variable. Bajo este supuesto distribucional, la metodología de clasificación obtenida se suele entonces denominar «clasificador Bayes ingenuo gaussiano» (gaussian naive Bayes).
Otro caso muy extendido de esta metodología se obtiene cuando los inputs
representan el número de ocurrencias de un conjunto de eventos de interés.
Este modelo es habitual en el contexto de la clasificación de textos, en que
cada ejemplo está asociado a un documento y cada variable explicativa registra el número de ocurrencias de una palabra determinada en un documento.
Bajo este supuesto, para una clase j de documentos, el conjunto de variables
explicativas sigue entonces una distribución multinomial de parámetros
( p1 j ,..., p nj ), donde n es el tamaño del vocabulario o número total de palabras
diferentes que aparecen en los textos contenidos en la muestra de entrenamiento E y pij denota la probabilidad de aparición de la palabra i en textos de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
175
la clase j. Estas probabilidades se estiman a partir de la frecuencia relativa de
aparición de las respectivas palabras en los documentos en E de cada clase j.
En este caso es posible obtener directamente la distribución conjunta condicional de todas las variables explicativas dada una clase j, y se tiene
i
i =1
n
∏x
i
(
p (x1 ,..., x n | Y = j ) =
(
n
∑x
!
!
n
n
i =1
i =1
∏ pijxi ∝ ∏ pijxi ,
i =1
por lo que entonces
n
p (Y = j | x ) ∝ p (Y = j )∏ p ijx i .
i =1
El clasificador así obtenido se suele denominar clasificador Bayes ingenuo
multinomial (multinomial naive Bayes).
Otros supuestos distribucionales sobre las distribuciones marginales de los
inputs en cada clase conducen a diferentes tipos de clasificador bayesiano,
aunque también es posible una alternativa no paramétrica empleando estimaciones empíricas de esas densidades marginales. En cualquier caso, aparte de
la simplificación referida para la estimación de las probabilidades p (Y = j | x ),
el supuesto de independencia condicional también proporciona otras dos ventajas relacionadas: en primer lugar, en problemas con alta dimensionalidad,
en que el número de inputs n es grande, permite desacoplar las estimaciones
de densidad multivariante en estimaciones unidimensionales, lo que puede
conllevar un importante ahorro computacional y una velocidad de ejecución
muy superior a la de otros modelos con hipótesis más complejas, y, en segundo
lugar, permite combinar naturalmente diferentes tipos de variables explicativas (continuas, discretas, categóricas) sin recurrir a artificios como el uso de
variables indicadoras o medidas de distancia entre categorías.
Finalmente, es preciso señalar que, a pesar de su simplicidad, esta metodología de clasificación bayesiana puede producir clasificadores competitivos, y,
de hecho, su uso es habitual hoy en día en contextos como la detección de
spam, la predicción de escritura o la clasificación de textos. La clave de la sorprendente eficacia de este sencillo modelo es que, aun cuando sus supuestos
ingenuos estén lejos de cumplirse en la práctica, estos pueden no introducir
sesgos demasiado perjudiciales en la clasificación. En particular, los sesgos
asociados a diferentes variables pueden compensarse entre sí, y su efecto
combinado puede ser compensado, a su vez, por la mayor precisión que provee el uso de un modelo más sencillo.
176
CUADERNOS METODOLÓGICOS 60
Ejemplo práctico
En este ejemplo se ilustrará esta eficacia característica del clasificador naive
Bayes gaussiano mediante una comparación con las técnicas ya presentadas
de k vecinos más cercanos y árboles de clasificación. El módulo de scikit-learn
dedicado a los clasificadores bayesianos es naive_bayes, desde el que se accede
al clasificador de tipo gaussiano antes expuesto mediante la función
GaussianNB. Este ejemplo servirá, asimismo, para ilustrar otra característica relevante del clasificador Bayes ingenuo gaussiano, que es su no dependencia de hiperparámetros, como era el caso del valor del número de vecinos
n_neighbors en la metodología de k vecinos más cercanos o la profundidad
máxima del árbol y el número mínimo de ejemplos para poder ramificar un
nodo, respectivamente, controlados por los parámetros max_depth y
min_samples_split, en el caso de los árboles de clasificación. En este sentido, como veremos, el clasificador Bayes gaussiano puede ser considerablemente más eficaz que esas otras dos metodologías de clasificación sin
necesidad de realizar un ajuste fino de hiperparámetros mediante búsqueda
en rejilla con validación cruzada.
La comparación entre las tres metodologías se realizará sobre el conjunto
de datos Wine, otro conocido dataset de clasificación supervisada, que contiene 178 ejemplos de vinos de la misma región de Italia producidos por tres
diferentes productores. Cada ejemplo contiene trece variables explicativas
resultantes del análisis químico del vino, midiendo atributos tales como la
concentración de alcohol, la de ácido málico y la de ceniza, la alcalinidad de
la ceniza, concentración de flavonoides, etc. Todos estos atributos son numéricos y continuos. La variable target, categórica, identifica al productor del
vino, y toma, por tanto, tres posibles valores, que denominaremos como
clase 1, clase 2 y clase 3. Aunque el supuesto de normalidad marginal puede
ser adecuado para algunas de estas variables en algunas clases de vino, el
supuesto de independencia condicional no lo es tanto, ya que existen diversas
relaciones entre los diferentes atributos químicos. No obstante, veremos que
este incumplimiento de la independencia condicional no impide que la metodología bayesiana rinda a buen nivel. Nótese, además, que este conjunto de
datos Wine, sin que su dimensionalidad (trece atributos) sea especialmente
alta, contiene un número bastante superior de variables en comparación con
el conjunto iris empleado en los anteriores ejemplos.
Para obtener los mejores parámetros en los clasificadores de tipo k vecinos
más cercanos y árbol, se llevará a cabo una búsqueda en rejilla con validación
cruzada probando valores de k (parámetro n_neighbors de la función
KNeighborsClassifier) entre 1 y 15, de la máxima profundidad del árbol
(parámetro max_depth de la función DecisionTreeClassifier) entre 3 y
5 y del número mínimo de ejemplos para ramificar (parámetro min_samples_split) entre 2 y 51. Este proceso de validación se llevará a cabo sobre
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
177
una muestra de entrenamiento con el 60% de los ejemplos, destinando el 40%
restante a la muestra de test con la que se estimará finalmente el rendimiento
real de los clasificadores ajustados. En primera instancia se llevará a cabo el
ajuste de los tres clasificadores con los datos sin normalizar a la escala común
[0,1], lo que, como veremos, tendrá un impacto importante en el rendimiento
del clasificador de k vecinos más cercanos, pero no en el del árbol de clasificación y naive Bayes gaussiano.
El código Python para esta primera comparación se muestra a continuación:
#importación de módulos y funciones
from sklearn import datasets
from sklearn import naive_bayes, neighbors, tree
from sklearn.model_selection import train_test_split, GridSearchCV
vinos=datasets.load_wine() #carga del dataset Wine
#división del dataset en entrenamiento y test
X_entrenamiento, X_test, y_entrenamiento, y_test = train_
test_split(vinos.data,
vinos.target, test_size=0.4, random_state=13)
#k vecinos más cercanos
#definición de los rangos de los parámetros
parametros_knn = [{'n_neighbors': range(1, 15)}]
#búsqueda en rejilla con validación cruzada sobre la muestra
de entrenamiento
knn = GridSearchCV(neighbors.KNeighborsClassifier(),parametros_knn,cv=5)
knn.fit(X_entrenamiento, y_entrenamiento)
#salida de resultados
print("Mejor configuración paramétrica del K-NN:",knn.best_
params_)
print("Tasa de acierto en validación de la mejor configuración
del K-NN:",knn.best_score_)
print("Estimación del rendimiento real del K-NN:", knn.score(X_test, y_test))
#árbol de clasificación
#definición de los rangos de los parámetros
parametros_tree = [{'max_depth': [3,4,5],
'min_samples_split': range(2, 51)}]
178
CUADERNOS METODOLÓGICOS 60
#búsqueda en rejilla con validación cruzada sobre la muestra
de entrenamiento
arbol = GridSearchCV(tree.DecisionTreeClassifier(random_state=1),parametros_tree,
cv=5)
arbol.fit(X_entrenamiento, y_entrenamiento)
#salida de resultados
print("Mejor configuración paramétrica del árbol:",arbol.
best_params_)
print("Tasa de acierto en validación de la mejor configuración
del árbol:",arbol.best_score_)
print("Estimación del rendimiento real del árbol:", arbol.
score(X_test, y_test))
#clasificador naive Bayes gaussiano
NB=naive_bayes.GaussianNB()
NB.fit(X_entrenamiento, y_entrenamiento)
print("Estimación del rendimiento real del Naive Bayes:",
NB.score(X_test, y_test))
Los resultados obtenidos por el K-NN son los siguientes:
Mejor configuración paramétrica del K-NN: {'n_neighbors': 14}
Tasa de acierto en validación de la mejor configuración del
K-NN: 0.7641509433962265
Estimación del rendimiento real del K-NN: 0.7222222222222222
Los resultados obtenidos por el árbol de clasificación son los siguientes:
Mejor configuración paramétrica del árbol: {'max_depth': 4,
'min_samples_split': 2}
Tasa de acierto en validación de la mejor configuración del
árbol: 0.9056603773584906
Estimación del rendimiento real del árbol: 0.9027777777777778
Finalmente, los resultados que obtiene el clasificador Bayes gaussiano son
los siguientes:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
179
Estimación del rendimiento real del Naive Bayes:
0.9861111111111112
Como se puede observar, el algoritmo de los k vecinos más cercanos obtiene
un rendimiento bastante pobre, rondando un 72% de tasa de acierto, comparado con el más del 90% que obtiene el árbol de clasificación y el 98% del clasificador Bayes. Nótese que este último clasificador se ajusta con parámetros
por defecto, sin realizar búsqueda en rejilla. Básicamente, estos parámetros por
defecto del clasificador Bayes especifican que la estimación de las probabilidades p(Y = j) se lleve a cabo mediante la proporción de ejemplos de cada clase en
la muestra de entrenamiento. Aun así, sin intentar encontrar la mejor configuración de estas proporciones a priori entre clases, el clasificador Bayes obtiene
un rendimiento sustancialmente mejor que el del árbol con su mejor configuración paramétrica para este conjunto de datos.
Realicemos ahora esta misma comparación pero procediendo previamente
a la normalización de los trece atributos explicativos al intervalo [0,1]
mediante la función MinMaxScaler. El código empleado es el siguiente:
#Normalización de los datos
from sklearn.preprocessing import MinMaxScaler
X = vinos.data
y = vinos.target
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
#división del dataset en entrenamiento y test
X_entrenamiento, X_test, y_entrenamiento, y_test = train_
test_split(X ,
y, test_size=0.4, random_state=13)
#k vecinos más cercanos
#búsqueda en rejilla con validación cruzada sobre la muestra
de entrenamiento
knn = GridSearchCV(neighbors.KNeighborsClassifier(),parametros_knn,cv=5)
knn.fit(X_entrenamiento, y_entrenamiento)
#salida de resultados de la búsqueda
print("Mejor configuración paramétrica del K-NN:",knn.best_
params_)
180
CUADERNOS METODOLÓGICOS 60
print("Tasa de acierto en validación de la mejor configuración
del K-NN:",knn.best_score_)
print("Estimación del rendimiento real del K-NN:", knn.score(X_test, y_test))
#árbol de clasificación
#búsqueda en rejilla con validación cruzada sobre la muestra
de entrenamiento
arbol = GridSearchCV(tree.DecisionTreeClassifier(random_state=1),parametros_tree,
cv=5)
arbol.fit(X_entrenamiento, y_entrenamiento)
#salida de resultados de la búsqueda
print("Mejor configuración paramétrica del árbol:",arbol.
best_params_)
print("Tasa de acierto en validación de la mejor configuración
del árbol:",arbol.best_score_)
print("Estimación del rendimiento real del árbol:", arbol.
score(X_test, y_test))
#clasificador naive Bayes gaussiano
NB=naive_bayes.GaussianNB()
NB.fit(X_entrenamiento, y_entrenamiento)
print("Estimación del rendimiento real del naive Bayes:",
NB.score(X_test, y_test))
Los resultados obtenidos por el K-NN son los siguientes:
Mejor configuración paramétrica del K-NN: {'n_neighbors': 12}
Tasa de acierto en validación de la mejor configuración del
K-NN: 0.9811320754716981
Estimación del rendimiento real del K-NN: 0.9444444444444444
Los resultados obtenidos por el árbol de clasificación son los siguientes:
Mejor configuración paramétrica del árbol: {'max_depth': 4,
'min_samples_split': 2}
Tasa de acierto en validación de la mejor configuración del
árbol: 0.9056603773584906
Estimación del rendimiento real del árbol: 0.9027777777777778
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
181
Finalmente, los resultados que obtiene el clasificador Bayes gaussiano son
los siguientes:
Estimación del rendimiento real del naive Bayes:
0.9861111111111112
En este caso, con los datos normalizados, el rendimiento del algoritmo
de los k vecinos más cercanos mejora notablemente, hasta una tasa de
acierto sobre la muestra de test del 94%. Esto ilustra el fuerte impacto que
puede tener sobre esta metodología el uso de variables explicativas medidas
en diferentes escalas. Por el contrario, los resultados obtenidos por el árbol
de clasificación y por el clasificador Bayes gaussiano son idénticos a los
obtenidos en la comparación anterior, lo que ilustra, a su vez, la no dependencia de estos métodos respecto a las escalas de las variables. En cualquier
caso, nótese que el clasificador Bayes vuelve a obtener un rendimiento claramente superior al de las otras dos metodologías, aun cuando los datos no
se adaptan especialmente bien al supuesto de independencia condicional
que lo sustenta.
4.1.2.4. Redes neuronales artificiales
En las secciones 4.1.2.1 y 4.1.2.2 se han descrito dos metodologías que, de
algún modo, partían de una concepción intuitiva de cómo proceder a la generalización de un conjunto de ejemplos. En el caso del k-NN la idea intuitiva es
que ejemplos cercanos tenderán a tener comportamientos de la variable target
parecidos. Por su parte, los árboles de decisión tratan de delimitar regiones en
que el comportamiento del target es similar. En ambos casos, la generalización hace referencia al espacio de variables explicativas de los ejemplos con
los que se aprende, y los parámetros que determinar (por ejemplo, el número
k de vecinos que considerar en el k-NN, o los umbrales θ que determinan las
ramificaciones en los árboles de decisión) permiten controlar la selección de
los ejemplos que guiarán la predicción de nuevas instancias.
El planteamiento de las redes neuronales artificiales (ANN, del inglés artificial neural networks), o simplemente redes neuronales, es ciertamente diferente y menos intuitivo. Inspirado en la biología, toma el lenguaje de la
neurología como base para la especificación de procesos de red. Así, la base
del proceso de aprendizaje la constituyen ciertas unidades de procesamiento
de información, las neuronas artificiales, que reciben inputs de diversa intensidad provenientes de neuronas previas o de los datos de entrada. Una vez
recibidos, las neuronas artificiales los procesan y emiten una señal que
182
CUADERNOS METODOLÓGICOS 60
constituirá un input para otras neuronas, excepto en las neuronas finales o de
salida, que proporcionan el output del sistema. En este proceso, el principal
elemento ajustable (i. e., parámetros) son esas intensidades o pesos que ponderan el output de las neuronas antes de convertirse en el input de otras. En
este sentido, el mecanismo de aprendizaje de las redes neuronales se centra en
ajustar y obtener los pesos óptimos que permiten a la red devolver, para cada
ejemplo de entrenamiento, un output igual al target del ejemplo o con el
mínimo error.
Prestar tanta atención a la optimización provoca, en buena medida, que las
redes neuronales se comporten para un observador humano como cajas
negras, en las que se lleva a cabo la transformación de unas entradas en determinadas salidas sin facilitarse una interpretación en términos prácticos del
proceso que realiza esa transformación. Sin embargo, a cambio de esta pérdida de interpretabilidad, las redes neuronales pueden obtener rendimientos
muy altos a la hora de replicar y generalizar con éxito las relaciones entre
inputs y targets de conjuntos de ejemplos, y, de hecho, esta metodología de
aprendizaje se ha mostrado en la práctica sumamente efectiva en problemas
actuales de gran complejidad (como, por ejemplo, la predicción de actos delictivos en áreas urbanas [Mohler et al., 2015], la clasificación de la temática de
textos y el procesamiento del lenguaje natural [Conneau et al., 2016], el reconocimiento del habla [Graves et al., 2013], la biología molecular [Chen et al.,
2018], o aprender a jugar al Go mejor que los grandes maestros humanos [Lee
et al., 2016]).
Figura 4.12.
Estructura de una neurona artificial
Así pues, empecemos por detallar la estructura y el funcionamiento de la
entidad básica de estas redes, la neurona artificial. La figura 4.12 muestra
los distintos elementos que componen una neurona artificial. En primer
lugar, se tiene un conjunto de señales de entrada o inputs xi, i = 1,…,n,
en principio asociados a las diferentes variables explicativas o atributos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
183
disponibles, aunque también pueden provenir de la salida de otras neuronas. Estas señales atraviesan las sinapsis, representadas por flechas, cada
una de las cuales está caracterizada por una propia intensidad o peso wi.
Estos pesos serán los parámetros ajustables del modelo, y su función es
ponderar o multiplicar el input xi correspondiente, arrojando el producto
wixi. Estos productos se agregan en la llamada función de red, normalmente
n
a través de la suma. La suma ponderada
∑w x
i
i
de los inputs resultante es a
i =1
continuación transformada por la «función de activación», que denotaremos
por g y que puede tomar diversas formas. Normalmente, se usa la misma función de activación para todas las neuronas de la red, a excepción quizá de la o
las neuronas de salida, cuya función de activación dependerá de la naturaleza
de la tarea (clasificación o regresión). La señal de salida u output de la neurona es el resultado de las operaciones anteriores, luego, viene dada por
hw (x ) = g (w 1x1 + ... + w n x n ),
donde w = (w 1 ,...,w n ) denota el conjunto de pesos aplicados. Este proceso,
que, de izquierda a derecha, recoge unos inputs x = (x1 ,..., x n ) y los transforma
sucesivamente hasta producir la salida hw (x ), se denomina «alimentación
hacia delante» (feedforward). En tanto que este procedimiento determina en
buena medida la topología o configuración de conexiones de una red neuronal, a las redes que lo implementan se las conoce como «redes alimentadas
hacia delante» (feedforward neural networks), que constituyen con mucha
diferencia el tipo de red neuronal artificial más extendido. Otras tipologías,
que no se tratarán aquí, son las «redes recurrentes» (recurrent neural networks), útiles en el ajuste de series temporales, y las «redes completamente
conectadas» (fully-connected neural networks), que se utilizan en problemas
no supervisados.
Dentro de estas redes alimentadas hacia delante, el caso más sencillo es el
de una red con una única neurona, que se suele denominar «perceptrón simple». Ilustremos el funcionamiento del perceptrón en un problema de regresión. En primer lugar, se incorpora un nuevo input x0, que tomará siempre el
valor x0 = 1, y su peso correspondiente w0. En segundo lugar, se toma como
función de activación la identidad g (a ) = a . El output del perceptrón viene
entonces dado por
hw (x ) = g (w 0 x 0 + w 1x1 + ... + w n x n ) = w 0 + w 1x1 + ... + w n x n.
Nótese que esta salida es la misma que la de un modelo de regresión
lineal múltiple, con los pesos w = (w 0 ,...,w n ) realizando el papel de los parámetros o coeficientes de regresión. En otras palabras, el perceptrón simple,
184
CUADERNOS METODOLÓGICOS 60
en un problema de regresión, es básicamente equivalente a un modelo de
regresión lineal. En particular, si se dispone de un conjunto de ejemplos de
la forma (x = (x1 ,..., x n ); y ), es perfectamente posible comparar el output
hw (x ) obtenido con los pesos actuales con el valor real del target y, y medir
el error resultante y − hw (x ).. Como veremos más adelante, el ajuste de los
pesos w se realizará secuencialmente teniendo en cuenta este error cometido en cada ejemplo.
Antes de pasar a describir el mecanismo de aprendizaje (u optimización)
de los pesos de la red, ilustremos el funcionamiento del perceptrón en un problema de clasificación binaria. En este contexto se suele utilizar como función
de activación g la función logística, que toma la forma siguiente:
g (a ) =
1
.
1 + e −a
Como se puede observar en la figura 4.13, esta función devuelve siempre
valores entre 0 y 1, aproximándose a 1 cuando a crece y a 0 cuando a decrece.
Cuando a = 0 se tiene g(a) = 0,5. Así pues, en este caso la neurona transforma
el input x en un valor entre 0 y 1 dado por
hw (x ) = g (w 0 x 0 + w 1x1 + ... + w n x n ) =
1
.
1 + e −(w 0 +w1x1 +...+w n x n )
De cara a asignar una clase al input x, se procede comparando esta salida
con un umbral de discriminación u ∈ [0,1], de manera que si hw (x ) ≥ u
entonces se asigna la clase positiva, y si hw (x ) < u entonces x se clasifica como
negativo. Es decir, la salida hw (x ) del perceptrón puede interpretarse como el
grado de evidencia favorable a la clase positiva. Tomando la convención de
que para ejemplos (x; y) de la clase positiva se tiene que y = 1, y para los
negativos es y = 0, de nuevo es posible medir cuantitativamente el error
y − hw (x ), lo que permite tener en cuenta si un ejemplo es clasificado correctamente (o incorrectamente) por un margen mayor o menor de cara a ajustar los pesos consiguientemente.
Así pues, este procedimiento de alimentación hacia delante constituye el
mecanismo de inferencia de la red, que dados un input y un conjunto de pesos
proporciona un output acorde al problema considerado. El mecanismo de
aprendizaje es entonces el encargado de ajustar los pesos de manera que se
minimicen los errores de la red sobre el conjunto de ejemplos de entrenamiento. Este procedimiento se realiza normalmente de manera secuencial e
iterativa, realizando el ajuste para cada ejemplo disponible. Esto es, dada una
muestra de entrenamiento E con N ejemplos y unos pesos iniciales w, la red
toma el primer ejemplo (x 1 ; y 1 ), calcula la salida hw (x 1 ) correspondiente al
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
185
Figura 4.13.
Función de activación logística
input del ejemplo y mide el error cometido y 1 − hw (x 1 ).. Con base en este error,
se corrige entonces el vector de pesos w siguiendo la fórmula 10
w = w − η ( y 1 − hw (x 1 ))x 1,
donde η representa un hiperparámetro denominado «tasa de aprendizaje»,
a cargo de controlar cuánto varían los pesos en cada corrección de estos. A
continuación, se toma el segundo ejemplo (x 2 ; y 2 ) y se alimenta la red hacia
adelante con el nuevo input x2 y el nuevo vector de pesos, produciendo un
nuevo error y 2 − hw (x 2 ) y la consiguiente corrección de los pesos
w = w − η ( y 2 − hw (x 2 ))x 2. Tras aplicar este procedimiento sucesivamente a
los N ejemplos disponibles, se dice que se ha llevado a cabo una epoch, una
vuelta completa a la muestra de entrenamiento. Habitualmente, el entrenamiento de una red neuronal requiere de un número elevado de epochs, del
Esta fórmula proviene de aplicar el método de optimización conocido como descenso del
gradiente. Básicamente, el gradiente es un vector que proporciona la dirección (y el sentido) en el
espacio de parámetros de mayor decrecimiento local (a partir de los pesos actuales) de la función
de error. En el contexto del perceptrón simple, el gradiente apunta en la misma dirección que el
vector de inputs x, y su sentido y magnitud dependen, respectivamente, del signo y la magnitud
del error. De este modo, el vector de pesos w se modifica avanzando en la dirección y sentido de
este gradiente una distancia o salto proporcional a la magnitud del error | y − hw (x ) | y controlado
por el hiperparámetro η.
10
186
CUADERNOS METODOLÓGICOS 60
orden de centenares, miles o incluso más. En cada epoch, es importante
barajar aleatoriamente la muestra de entrenamiento para que la red reciba
los ejemplos en un orden diferente cada vez.
Un último detalle concierne a la inicialización de la red. Es altamente
aconsejable normalizar todos los inputs a una escala común, típicamente el
intervalo [0,1] o [–1,+1]. Esto favorece que todos los pesos tengan una magnitud relativamente similar, ayudando al proceso de aprendizaje. Además, los
pesos han de inicializarse de manera aleatoria, soliéndose generar los pesos
iniciales como números aleatorios pertenecientes a una distribución uniforme entre –0,5 y 0,5, excluyendo el 0, o valores muy cercanos a 0. La tasa de
aprendizaje η ha de escogerse de acuerdo con las características del problema
tratado, y la mejor estrategia suele ser la de usar una tasa adaptativa, que
tome valores mayores al principio del entrenamiento, para que el ajuste sea
más rápido y brusco de entrada, y que luego vaya decreciendo para permitir
un ajuste más fino de los pesos a medida que el entrenamiento va produciendo una red más ajustada a los datos 11.
Así pues, ya se tiene una idea de cómo funciona y se ajusta una red formada por una única neurona, el perceptrón simple, que, como hemos visto, es
básicamente equivalente a un modelo de regresión lineal múltiple o a un clasificador lineal. En este sentido, las capacidades de un perceptrón simple son
similares a las de esos modelos estadísticos básicos. Por ejemplo, no puede
resolver adecuadamente problemas de regresión no lineal, y tampoco es capaz
de resolver problemas de clasificación no separables linealmente. Este hecho
Una metáfora útil para entender cómo funciona un proceso de optimización como el de las
redes neuronales es la que lo asimila con encontrar el punto más bajo de un paisaje o superficie.
Cada punto de este paisaje está asociado a una configuración de parámetros o pesos particular,
digamos las coordenadas de ese punto. La altura de un lugar concreto depende del error cometido por la red al usar los pesos o coordenadas de ese punto, más alto cuanto mayor error. Así, el
objetivo del proceso de optimización es encontrar las coordenadas del punto más bajo de ese paisaje. Sin embargo, esto puede no ser tan sencillo como podría parecer, en especial cuando este
paisaje se encuentra en un espacio de muchas dimensiones, esto es, cuando las coordenadas de
cada punto vienen dadas por un gran número de pesos. Por ello, los algoritmos de optimización
suelen simplemente implementar alguna estrategia de búsqueda en ese paisaje, por ejemplo,
dejarse caer desde un punto inicial y rodar cuesta abajo hasta llegar a una superficie plana o un
hoyo o agujero del paisaje. Llegados a este punto, el algoritmo de optimización se detiene en
tanto que ya no puede continuar descendiendo, y se dice que el proceso ha convergido a un
mínimo. En este sentido, es importante observar que una diferente inicialización aleatoria de los
pesos podría conducir al algoritmo a otra configuración de pesos, con una función de error
menor. Esto es, la convergencia del proceso de optimización puede darse hacia un mínimo local,
un punto llano del paisaje desde el que es imposible continuar descendiendo, pero con una altura
mayor que la de otros puntos similares del paisaje. En general, llegar al mínimo global, el punto
más bajo del paisaje, puede ser una tarea ardua si no imposible; para empezar, porque no suele
ser posible discernir si realmente un mínimo encontrado es el global o solo un mínimo local más.
Por ello, lo normal es conformarse con que el algoritmo de optimización encuentre un mínimo
local suficientemente bajo, que proporcione un nivel de error aceptable.
11
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
187
y el no haberse descubierto aún un procedimiento para entrenar redes con
más de una neurona hicieron que el desarrollo de las redes neuronales se
estancase durante casi dos décadas, hasta que en 1975 se desbloqueó la situación al aparecer el algoritmo de «retropropagación» (backpropagation). Este
permite entrenar de manera efectiva redes con un número arbitrario de neuronas, conocidas como «perceptrón multicapa» (multilayer perceptron), las
cuales son, al menos teóricamente, capaces de aproximar con la precisión
deseada cualquier relación input-output.
En un perceptrón multicapa, las neuronas se organizan en capas sucesivas,
cada capa conteniendo un número variable de neuronas. Así, se distingue, en
primer lugar, la «capa de entrada», asociada a los inputs x1,…,xn provenientes
de los datos. A continuación, se encuentran una o más «capas ocultas» (hidden
layers), que pueden tener cualquier número de neuronas y que se encargan del
procesamiento de los inputs y de su transformación sucesiva y adecuada para
resolver el problema tratado. Y, finalmente, se encuentra la «capa de salida»,
cuyo número de neuronas depende del tipo de tarea que resolver. En problemas de regresión se usa una única neurona de salida, que, como antes, produce un output hw (x ) comparable con el valor del target y de un ejemplo (x;y).
En un problema de clasificación con C clases, la capa de salida ha de tener C
neuronas, tantas como clases, de modo que cada neurona está destinada a
medir la evidencia a favor de una clase diferente. En este contexto, los valores
y del target asumen la forma
y = ( y 1 ,..., y C ),
donde yi = 1 si la clase del ejemplo es la clase j-ésima, e yi = 0 en otro caso. Por
ejemplo, en un problema con C = 3 clases, si un ejemplo pertenece a la segunda
clase, se tendrá y = (0,1,0), y si pertenece a la tercera será y = (0,0,1). De este
modo, el output hwi (x ) de cada neurona i se compara con yi, la componente
i-ésima del vector y.
La figura 4.14 muestra un ejemplo de perceptrón multicapa para regresión, con tres inputs (más el input constante x0) en la capa de entrada, una
capa oculta formada por tres neuronas (a la que se le añade una neurona con
una salida constante, de manera similar al input x0), y una capa de salida
con una única neurona. Nótese que las neuronas en una misma capa no se
relacionan entre ellas, y que las de capas sucesivas se conectan de manera
exhaustiva, esto es, cada neurona de una capa está conectada con todas las
l
neuronas de la capa siguiente (excepto con las constantes), con el peso w ji
correspondiente representando la ponderación aplicada a la salida de la
neurona j de la capa l + 1 antes de convertirse en un input de la neurona i de
la capa l (esto es, las capas se enumeran desde la capa de salida hacia la de
entrada).
188
CUADERNOS METODOLÓGICOS 60
Figura 4.14.
Ejemplo de perceptrón multicapa
El proceso de alimentación hacia delante de un perceptrón multicapa es
esencialmente el mismo que en el caso del perceptrón simple. Los inputs se
introducen por la izquierda, como en la figura 4.14, y se propagan hacia la derecha, recibiendo las ponderaciones asociadas a las diferentes conexiones, siendo
transformados y agregados en cada neurona por las correspondientes funciones
de red y activación antes de enviarse como salida hacia la siguiente capa, con su
correspondiente ponderación. Las neuronas de las capas ocultas suelen usar
siempre el sumatorio como función de red, y la función logística como función
de activación. En la capa final, la neurona de salida de un problema de regresión
utiliza la identidad como función de activación, manteniéndose la función logística para las neuronas de salida en un problema de clasificación. Nótese que esto
proporciona las instrucciones necesarias para realizar la inferencia de una nueva
instancia x a partir de una red dada con un conjunto de pesos w.
l
El aprendizaje o ajuste de los pesos w ij del perceptrón multicapa se realiza
propagando hacia atrás los errores y − hw (x ) obtenidos en la capa de salida, de
ahí el nombre del método de retropropagación. Básicamente, este método calcula para cada neurona de la capa de salida una responsabilidad o contribución de esa neurona en el error global que se comete, de modo que el ajuste de
los pesos pueda tener en cuenta estas diferentes contribuciones, ajustando en
mayor grado los pesos asociados a esas neuronas más responsables. En el caso
de una red con una única capa oculta, estas contribuciones al error global de
cada neurona i de la capa de salida, dependientes del error particular y i − hwi (x )
cometido en cada una, se obtienen como
δi1 = hwi (x )·(1 − hwi (x ))( y i − hwi (x )) ,
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
189
donde hwi (x ) denota el output de la neurona i-ésima de la capa de salida e yi
denota la componente i-ésima del target del ejemplo (x;y). A partir de estas responsabilidades de las neuronas de la capa de salida, la propagación hacia
atrás permite entonces obtener las responsabilidades de las neuronas de la
capa oculta, dadas por
δ j2 = zwj (x )·(1 − zwj (x ))∑δi1w 1ji ,
i
donde zwj (x ) denota la salida de la neurona j-ésima de la capa oculta. Una vez
obtenidas las contribuciones al error global de cada neurona de la red (excepto
las de entrada), se actualizan los pesos de cada neurona siguiendo las expresiones siguientes:
w 1ji = w 1ji + ηδi1zwj (x ) (para neuronas de la capa de salida), y
w ij2 = w ij2 + ηδ j2 x i (para neuronas de la capa oculta),
Al igual que en el caso del perceptrón simple, el entrenamiento de un perceptrón multicapa puede realizarse de manera secuencial, actualizando los pesos
con cada ejemplo de entrenamiento procesado, lo que se conoce como entrenamiento online. Sin embargo, esto puede ser muy costoso computacionalmente
cuando el tamaño del conjunto de entrenamiento es elevado, especialmente si
se tiene también un número importante de neuronas en las capas ocultas, ya
que obliga a aplicar el método de retropropagación para cada ejemplo (y repetir
en este proceso un número probablemente también alto de epochs). La alternativa es usar el entrenamiento de tipo batch, donde los pesos no se actualizan
hasta procesar todos los ejemplos, es decir, los pesos se modifican al acabar
cada epoch. Esto reduce significativamente el coste computacional, pero tiene
otros inconvenientes, como un mucho mayor consumo de memoria y un
empeoramiento en el rendimiento del proceso de optimización. Por ello, actualmente se tiende a considerar una solución mixta, llamada entrenamiento minibatch, en la que el conjunto de entrenamiento se divide en un número de
subconjuntos de tamaño manejable, de manera que la actualización de pesos se
realiza tras procesar cada subconjunto. Este tipo de entrenamiento equilibra de
algún modo las ventajas e inconvenientes de los aprendizajes online y batch.
El número de capas ocultas y de neuronas que disponer en cada una de
ellas, así como el número de epochs que realizar o el valor de la tasa de aprendizaje η, son hiperparámetros del modelo, que determinan la configuración
particular de una red. En general, un número mayor de neuronas permite una
190
CUADERNOS METODOLÓGICOS 60
mayor flexibilidad a la red, lo que aumenta su capacidad de generalización y
de adaptarse a relaciones entre inputs y outputs más complejas. Sin embargo,
un número excesivo de neuronas, aparte de incrementar el coste computacional del entrenamiento, conlleva un considerable riesgo de sobreajuste. Lo
mismo se aplica al número de epochs en que consistirá el entrenamiento. Por
ello, la elección de estos parámetros es altamente relevante para la consecución de un buen ajuste, dando suficiente libertad a la red para extraer la
máxima información de los ejemplos sin llegar a generalizar su ruido. No obstante, en general, la única manera de obtener un conjunto de hiperparámetros
apropiado para un problema dado es, empíricamente, mediante un marco
entrenamiento-validación-test, en el que una colección de redes con diferentes configuraciones se ajusta mediante el conjunto de entrenamiento, se selecciona aquella con mejor error en la muestra de validación, y, finalmente, se
estima el rendimiento de la red seleccionada sobre la muestra de test.
Históricamente, en la práctica las redes multicapa solo empleaban una o
unas pocas capas ocultas, debido a diversos problemas técnicos que se acentúan al incrementarse el número de capas. Solo recientemente, digamos a
partir de 2005 o 2010, ha sido posible solventar o esquivar estas dificultades,
permitiendo el entrenamiento efectivo de las llamadas redes profundas, que
pueden llegar a contar con una cantidad importante de capas (desde decenas
a cientos o miles) y que actualmente se consideran uno de los mecanismos de
aprendizaje más efectivos. Su eficacia está relacionada con la capacidad de las
primeras capas ocultas de ir aprendiendo a discriminar constructos y patrones de alto nivel (por ejemplo, en una imagen digital, formas rectangulares
con formas redondeadas por debajo) a partir de los datos de bajo nivel (e. g.,
la información a nivel de píxel), lo que permite a las capas posteriores realizar
mejor la generalización que se pretende (por ejemplo, identificar si una imagen contiene un coche), en tanto que la llevan a cabo sobre esos constructos,
que proporcionan información más relevante para la resolución del problema.
Ejemplo práctico
Las redes neuronales artificiales constituyen una de las metodologías de
aprendizaje más potentes y flexibles, en el sentido de que el elevado número
de pesos o parámetros libres que contienen, incluso en redes de tamaño
pequeño o mediano, les permiten amoldarse, con el entrenamiento adecuado,
a casi cualquier relación entre inputs y outputs. El hándicap de esta flexibilidad es que habitualmente el ajuste adecuado de ese elevado número de conexiones entre neuronas conlleva un proceso de optimización más largo y
costoso computacionalmente que el de otros mecanismos de aprendizaje.
Además, las redes neuronales poseen una diversidad de hiperparámetros
con un efecto relevante en el entrenamiento. Una búsqueda en rejilla con
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
191
validación cruzada de la mejor configuración paramétrica puede ser un proceso de una duración bastante considerable. Esta lentitud en el entrenamiento
y el ajuste de la configuración de una red puede, sin embargo, verse compensada por su potencia, de manera que el modelo resultante, una vez entrenado,
puede proporcionar resultados muy competitivos.
Para ilustrar el ajuste y la evaluación de redes neuronales artificiales con
scikit-learn, en este ejemplo se hará hincapié en el efecto de diversos hiper­
parámetros sobre el entrenamiento de las redes, su duración y su eficacia. Sin
embargo, no se llevará a cabo una búsqueda exhaustiva de la mejor configuración paramétrica, ni siquiera un intento por obtener un mejor modelo de red.
Este procedimiento ya se ha ilustrado en secciones anteriores, y puede ser
aplicado al caso de las redes neuronales con mínimas modificaciones.
El conjunto de datos que se utiliza en este ejemplo es el dataset Breast cancer, que plantea un problema de clasificación binaria (C = 2) relativamente
sencillo, en el que se trata de predecir el tipo de células que aparecen en imágenes de biopsias de mama, bien malignas (Malignant, clase positiva codificada como 0) o benignas (Benign, clase negativa codificada como 1), a partir
de un conjunto de características observadas en las imágenes, que proporcionan n = 30 atributos o variables explicativas. El dataset contiene N = 569 ejemplos de estas imágenes con sus respectivas etiquetas de clase, 212 de la clase
positiva y 357 de la negativa.
Para comparar los tiempos de ejecución de los distintos procesos de entrenamiento se usará el módulo time de Python, que proporciona diversas funciones para medir el tiempo transcurrido entre diversos puntos de ejecución
de un programa. En particular, las sentencias
comienzo = time.process_time()
...
print("Duración del proceso:",time.process_time()-comienzo)
permiten medir y mostrar el tiempo transcurrido entre la ejecución de la primera y la última sentencia.
Como referencia para la comparación posterior con el entrenamiento de
las redes, comencemos por repetir con el dataset Breast cancer la búsqueda
de la mejor configuración paramétrica de un árbol de clasificación que se describió en la sección anterior:
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, GridSearchCV
192
CUADERNOS METODOLÓGICOS 60
from sklearn import tree
import time
dataset = load_breast_cancer() # Carga del dataset Breast
cancer
X, y = dataset.data, dataset.target;
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X,y, test_size=0.4, random_state=13)
# Definición de los rangos de los parámetros
parametros_a_ajustar = [{
'max_depth': [4,5,6],
'min_samples_split': range(2, 51)}]
# Búsqueda en rejilla con validación cruzada
# sobre la muestra de entrenamiento
comienzo = time.process_time()
arbol = GridSearchCV(tree.DecisionTreeClassifier(),
parametros_a_ajustar,cv=5)
arbol.fit(X_entrenamiento, y_entrenamiento)
print("Duración del proceso:",
time.process_time()-comienzo,»segundos»)
# Salida de resultados de la búsqueda
print("Mejor configuración paramétrica:",arbol.best_params_)
print("Tasa de acierto en validación de la mejor configuración:",
arbol.best_score_)
print("Estimación del rendimiento real:", arbol.score(X_
test, y_test))
Los resultados obtenidos son los siguientes:
Duración del proceso: 5.34375
Mejor configuración paramétrica:
{'max_depth': 5, 'min_samples_split': 24}
Tasa de acierto en validación de la mejor configuración:
0.9237536656891495
Estimación del rendimiento real: 0.9122807017543859
Así pues, la búsqueda de la mejor profundidad (5) y del mínimo de ejemplos
requeridos en un nodo (24) para realizar la ramificación se lleva a cabo en algo
más de 5 segundos. Nótese que esta búsqueda ha implicado el ajuste de unos
3 × 49 × 5 = 735 árboles, ya que se permiten 3 posibles valores para max_depth,
el rango en que varía min_samples_split (de 2 a 50) tiene 49 elementos y para
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
193
cada combinación de estos parámetros se entrenan 5 árboles en la validación
cruzada. Esta mejor configuración obtiene una tasa de acierto en test del 91%.
Entrenemos ahora un perceptrón multicapa del tipo más sencillo posible,
formado por una única capa oculta con solo una neurona. El módulo de scikitlearn para redes neuronales es neural­_network, del que importaremos la función
MLPClassifier, la versión para clasificación del perceptrón multicapa (MultiLayer Perceptron). La versión para regresión es MLPRegressor. En el código
que sigue, para especificar el número de capas y de neuronas por capa se utiliza
el parámetro hidden_layer_sizes, de modo que, por ejemplo, hidden_
layer_sizes = (5,3) requerirá el ajuste de una red con dos capas ocultas: la
primera, con cinco neuronas, y la segunda, con tres. La red de una única neurona que se entrenará se especifica entonces con hidden_layer_sizes =
(1,). De igual modo, para requerir el uso de funciones de activación logísticas,
se ha de especificar activation = 'logistic'. Las expresiones solver =
'sgd' y batch_size = 1 requieren, respectivamente, el uso del algoritmo
habitual de descenso del gradiente para el entrenamiento por retropropagación
y que la actualización de los pesos se realice tras procesar cada ejemplo. Además, se especifica que el entrenamiento lleve a cabo un máximo de max_iter =
50 epochs o vueltas completas al conjunto de entrenamiento. La semilla para la
inicialización aleatoria de los pesos de la red se fija con random_state = 0.
Antes del ajuste del modelo, se incluye un paso de normalización de los inputs
en la escala [0,1], como se aconsejó más arriba.
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MinMaxScaler
# Normalización de los inputs
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
# Conjuntos de entrenamiento y test normalizados
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X,y,
test_size=0.4, random_state=13)
# Entrenamiento de la red
comienzo=time.process_time()
red = MLPClassifier(hidden_layer_sizes=(1,), activation='logistic',
solver='sgd',
batch_size=1, max_iter=50, random_state=0);
red.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Estimación del rendimiento real:", red.score(X_test,
y_test))
194
CUADERNOS METODOLÓGICOS 60
La salida tras la ejecución de estas sentencias es la siguiente:
Duración del proceso: 4.59375
Estimación del rendimiento real: 0.9605263157894737
Así pues, se ha tardado más de 4,5 segundos en entrenar una única red con
la configuración más básica. Esto es un tiempo comparable al que requirió
entrenar más de 700 árboles, lo que da una muestra de lo que se adelantaba al
decir que el entrenamiento de una red puede ser un proceso relativamente
largo. Además, se recibe el mensaje de advertencia
ConvergenceWarning: Stochastic Optimizer: Maximum iterations
(50) reached and the optimization hasn't converged yet.
que indica que el procedimiento de entrenamiento ha parado tras llegar al
máximo de epochs especificado, pero antes de que el proceso de optimización
pueda darse realmente por concluido, esto es, este aún no ha convergido, por
lo que podría quedar margen de mejora en el entrenamiento de esta red. Sin
embargo, el rendimiento de esta red tan simple, un 96% aún sin terminarse de
entrenar, es notablemente superior al logrado por el mejor árbol encontrado
al buscar entre más de 700 configuraciones paramétricas. Esto da, a su vez,
cuenta de la comentada potencia y eficacia predictiva que proporcionan los
modelos de redes neuronales artificiales.
Antes de continuar con el ajuste de otros modelos de red, hay que señalar
que es posible eliminar la aparición de mensajes de advertencia como el anterior ejecutando las sentencias
import warnings
from sklearn.exceptions import ConvergenceWarning
warnings.filterwarnings('ignore', category=ConvergenceWarning)
Para requerir que estos mensajes vuelvan a mostrarse, se debe ejecutar
warnings.filterwarnings('always', category=ConvergenceWarning)
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
195
Volviendo al ajuste de redes, empecemos por ilustrar la influencia del
máximo de epochs max_iter en la calidad y duración del entrenamiento. Así
pues, volvamos a entrenar y evaluar una red como la anterior, especificando
ahora una duración menor del entrenamiento, por ejemplo, max_iter = 10.
Ejecutando entonces el código
comienzo=time.process_time()
red = MLPClassifier(hidden_layer_sizes=(1,), activation='logistic',
solver='sgd',
batch_size=1, max_iter=10, random_state=0);
red.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Estimación del rendimiento real:", red.score(X_test,
y_test))
se presenta la salida
Duración del proceso: 0.859375
Estimación del rendimiento real: 0.6622807017543859
que muestra que la duración del entrenamiento es de algún modo proporcional al máximo de epochs (dividir entre cinco el número máximo de epochs
conlleva un tiempo de ejecución también cinco veces menor, grosso modo), y
que este puede tener una influencia muy relevante sobre el rendimiento de la
red, que ahora se queda en un muy pobre 66%. De hecho, como es posible
observar en la matriz de confusión resultante
print("Matriz de confusión:\n%s"
% metrics.confusion_matrix(y_test,red.predict(X_test))
Matriz de confusión:
[[ 0 77]
[ 0 151]]
la red ajustada es incapaz de detectar la clase positiva, asignando todos los
ejemplos de test a la clase negativa. Esta red tendría precisión y especificidad 0
sobre la clase positiva, lo cual habla de un clasificador totalmente inútil en el
196
CUADERNOS METODOLÓGICOS 60
contexto del dataset Breast cancer. Lo que sucede aquí es que, al reducir tanto
la longitud del entrenamiento, se ha obtenido una red subentrenada, que en
las diez epochs realizadas básicamente ha aprendido que existe una clase
mayoritaria, y que obtiene menos errores asignando todos los ejemplos a esta
clase que asignándolos al azar (que es lo que sucede con una red inicializada
aleatoriamente). Sin embargo, aún no ha tenido tiempo de ajustar los pesos
para empezar a separar la clase minoritaria de la mayoritaria. De algún modo,
el proceso de optimización ha ido avanzando en ajustar los pesos para reducir
los errores de la red inicial, pero todavía tiene mucho margen para minimizar
más el error, y se ha detenido simplemente porque no se le ha permitido realizar un número mayor de epochs.
Yendo al extremo contrario, si se repite la ejecución del código anterior
especificando ahora max_iter = 500, se obtiene la salida
Duración del proceso: 14.375
Estimación del rendimiento real: 0.9824561403508771
y además, en este caso no se nos muestra el mensaje advirtiendo de la no convergencia del proceso de optimización. De hecho, el tiempo de ejecución en
este caso no es diez veces el obtenido con max_iter = 50 ya que la optimización no ha llegado a realizar las 500 epochs permitidas. En algún momento del
proceso de ajuste de los pesos, el algoritmo de optimización se ha detenido al
advertir que las actualizaciones de pesos realizadas eran cada vez más insignificantes, reduciendo apenas el error global de la red. Esto es, la optimización ha convergido a un mínimo de la función de error de la red, un estado
estable de la configuración de pesos en que el algoritmo ya no puede continuar obteniendo errores más pequeños. Esto se asocia con una red totalmente
entrenada, y, como vemos, el rendimiento de la red ha mejorado sensiblemente hasta un 98,25% en test. La matriz de confusión que se obtiene en este
caso muestra que ahora el clasificador ya ha podido aprender a separar la
clase positiva de la negativa
Matriz de confusión:
[[ 74 3]
[ 1 150]]
Una cuestión que podría plantearse aquí es si permitir este entrenamiento
tan prolongado no podría conllevar de algún modo el sobreajuste del modelo,
esto es, que en su afán por minimizar el error la red haya empezado a ajustar
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
197
características demasiado específicas o ruido de los ejemplos de entrenamiento. Aunque, en general, un número mayor de epochs conlleva un mayor
riesgo de sobreajuste de una red, en este caso particular la respuesta es no, ya
que la red entrenada, con una única neurona, es demasiado simple para poder
adaptarse en exceso a los datos de entrenamiento.
Para ilustrar este punto, entrenemos ahora una red mucho mayor, con 600
neuronas en la capa oculta, esto es, especificando ahora hidden_layer_
sizes = (600,), y manteniendo max_iter = 500. El resultado ahora es el
siguiente:
Duración del proceso: 40.5
Estimación del rendimiento real: 0.9692982456140351
Así pues, al configurar una red con una capacidad de adaptación mucho
mayor (de hecho, en este caso hay más neuronas que ejemplos de entrenamiento) y permitirle entrenarse completamente, se obtiene un rendimiento
menor en test que con una red mucho más sencilla, además, con un coste
computacional bastante mayor. En general, al ajustar una red es necesario
analizar el rendimiento obtenido al especificar diferentes números de neuronas, de cara a encontrar una configuración que sea lo suficientemente flexible
para extraer el máximo de información útil de la muestra de entrenamiento,
pero, a la vez, suficientemente simple para no tender al sobreajuste.
Centrémonos ahora en el efecto del hiperparámetro batch_size, que
especifica el número de ejemplos que procesa la red antes de proceder a la
corrección de los pesos. El valor batch_size = 1 que se ha usado hasta ahora
corresponde al llamado entrenamiento de tipo online, que actualiza los pesos
mediante retropropagación tras procesar cada ejemplo. En el otro extremo, se
tendría el entrenamiento de tipo batch, que corrige los pesos una vez por epoch,
lo que se obtendría en este caso al especificar batch_size = 341, ya que la
muestra de entrenamiento cuenta con 341 ejemplos. Al entrenar la red inicial
con una única neurona con este valor de batch_size se obtiene
Duración del proceso: 0.0
Estimación del rendimiento real: 0.6622807017543859
independientemente del número máximo de epochs max_iter especificado.
En este caso, el entrenamiento de tipo batch se comporta de manera muy
pobre, quedando atrapada la optimización en un mínimo local (el que asigna
todos los casos a la clase negativa mayoritaria) en la primera actualización de
198
CUADERNOS METODOLÓGICOS 60
pesos. Esto es consecuencia de cómo se realiza esta actualización en el modo
batch cuando la red solo cuenta con una neurona. No obstante, usando un
número mayor de neuronas, digamos diez, con hidden_layer_sizes =
(10,) y max_iter = 500 se tiene
Duración del proceso: 0.578125
Estimación del rendimiento real: 0.6622807017543859
luego, tras algunos epochs, la optimización converge a la misma solución consistente en asignar todos los casos a la clase negativa. Esto muestra la posibilidad ya apuntada de que el entrenamiento de tipo batch empeore la eficiencia
del proceso de optimización, aumentando el riesgo de que se estanque en
mínimos locales de poco rendimiento. No obstante, el coste computacional de
entrenar la red es notablemente más bajo, ya que se produce comparativamente un número de actualizaciones de los pesos (i. e., aplicaciones de la
retropropagación) mucho menor.
Una estrategia intermedia entre los modos online y batch es el llamado
entrenamiento mini-batch, que consiste en establecer un batch_size relativamente pequeño respecto al tamaño total de la muestra de entrenamiento,
pero aún considerablemente mayor que 1. Por ejemplo, usando batch_size
= 20 con la red de una neurona y max_iter = 5000 se obtiene
Duración del proceso: 5.796875
Estimación del rendimiento real: 0.9605263157894737
Se ve entonces cómo el uso de mini-batch puede lograr un rendimiento aceptable a la vez que reduce el coste computacional del entrenamiento. En datasets
de gran tamaño esta puede, de hecho, ser la única estrategia viable, en tanto que
compensa los costes computacionales de la aplicación intensiva de la retropropagación del modo online y el uso intensivo de memoria del modo batch.
Un último hiperparámetro con una influencia relevante en el entrenamiento de una red es la tasa de aprendizaje η, que controla la amplitud de
las correcciones de los pesos en cada actualización. Por defecto, si no se
especifica lo contrario, la función MLPClassifier toma η constante e igual
a 0,001. Es posible modificar esta asignación con el parámetro learning_
rate_init. Valores más pequeños tenderán a permitir un ajuste más fino
en el proceso de optimización, a costa de hacer este más lento y más tendente a estancarse. Valores mayores pueden acelerar considerablemente el
entrenamiento, pero a costa de aumentar la posibilidad de sobreajuste y de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
199
no detenerse en los mínimos adecuados. Por ejemplo, volviendo a la red inicial, si se ejecuta el código
comienzo=time.process_time()
red = MLPClassifier(hidden_layer_sizes=(1,), activation='logistic',
solver='sgd',batch_size=1,
learning_rate_init=0.0001,
max_iter=50, random_state=0);
red.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Estimación del rendimiento real:", red.score(X_test,
y_test))
se obtiene la salida
Duración del proceso: 4.578125
Estimación del rendimiento real: 0.6622807017543859
Esto es, al reducir la tasa de aprendizaje (nótese que learning_rate_
init = 0.0001) el algoritmo vuelve a realizar las cincuenta epochs sin converger, pero ahora la tasa de aprendizaje tan baja le ha impedido escapar en
esas cincuenta iteraciones de la solución de optar por la clase mayoritaria.
Obsérvese que el tiempo de ejecución es prácticamente idéntico al registrado
para la primera red de este ejemplo. En otras palabras, el algoritmo ha realizado ahora el mismo número de actualizaciones de los pesos, pero les ha
sacado menos partido en tanto que la tasa de aprendizaje le obliga a modificar
los pesos muy lentamente.
En la otra dirección, si se ejecuta el código anterior especificando learning_rate_init = 0.01, ahora el entrenamiento converge antes de las
cincuenta epochs, y se tiene
Duración del proceso: 2.421875
Estimación del rendimiento real: 0.9824561403508771
por lo que en este caso la red vuelve a obtener la mejor solución que se alcanzó
al permitir max_iter = 500, pero mucho más rápido, en menos de dos segundos y medio, cuando antes precisaba de más de catorce segundos. Esto es, el
200
CUADERNOS METODOLÓGICOS 60
algoritmo en este caso ha precisado de un menor número de epochs para alcanzar el mismo mínimo, ya que en cada actualización la corrección de los pesos ha
sido más amplia. Podría pensarse entonces que con un η todavía mayor el entrenamiento se acelerará aún más, produciendo el mismo rendimiento. Pero al volver a ejecutar el código anterior con learning_rate_init = 0.1 se obtiene
Duración del proceso: 0.96875
Estimación del rendimiento real: 0.9692982456140351
luego, el algoritmo ha convergido bastante rápido, pero empeorando ya su
capacidad de generalización.
En conclusión, este ejemplo muestra que el ajuste de una red neuronal
artificial puede a veces ser casi un arte, y, en todo caso, una tarea laboriosa
que suele consumir una cantidad de tiempo importante. Sin embargo, este
esfuerzo merece muchas veces la pena en tanto que una red bien ajustada
puede proporcionar un modelo excepcionalmente preciso y capaz de afrontar
con éxito tareas complejas.
4.1.2.5. Máquinas de soporte vectorial
Las máquinas de soporte vectorial (SVM, del inglés support vector machine)
son una familia de clasificadores de desarrollo relativamente reciente, cuyo
enfoque consiste en determinar una frontera en el espacio de las variables
explicativas que separe de forma óptima los ejemplos de las diferentes clases.
En este contexto, se entiende que la frontera óptima es aquella que proporciona un mayor margen o distancia de la frontera a los ejemplos de las clases
que separa. Además, la forma de la frontera que determinar se restringe, de
modo que esta ha de venir dada por un hiperplano12 H del espacio de inputs
considerado. De modo que lo que se busca es encontrar el hiperplano separador que proporcione un mayor margen entre clases.
Ilustremos el enfoque de este mecanismo de aprendizaje en el caso de un
problema de clasificación binario, en el que la variable objetivo puede tomar
los valores y = +1 (la clase positiva) e y = –1 (la negativa), y con solo dos inputs
Recuérdese que, en un espacio de dimensión n, un hiperplano es un subespacio de dimensión n – 1, que divide el espacio n-dimensional en dos mitades. Así, por ejemplo, un punto (de
dimensión 0) divide una recta (espacio de dimensión 1) en dos rectas. En el plano, de dos dimensiones, un hiperplano es una recta, que divide el plano en dos. En el espacio tridimensional, un
hiperplano es un plano usual, que de nuevo divide el espacio en dos mitades. La misma idea se
aplica en espacios de dimensión superior a 3.
12
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
201
x1 y x2 para que pueda ser representado en el plano. Obsérvese la figura 4.15,
en la que se representan diez ejemplos, cinco de cada clase, en el plano dado
por los atributos x1 y x2. Un hiperplano H en este espacio bidimensional viene
dado por una recta, que de forma general cumplirá la ecuación
β0 + β1x1 + β2 x 2 = 0,
es decir, cada elección de los parámetros β = (β0 , β1 , β2 ) determinará una recta
H(β) en el plano, de modo que la ecuación se cumplirá para todos los puntos
x = (x1 , x 2 ) de la recta. Además, dado un punto x situado fuera de la recta, se
tendrá que β0 + β1x1 + β2 x 2 > 0 si x se encuentra a un lado de la recta, y
β0 + β1x1 + β2 x 2 < 0 si está al otro lado. Así, dado un hiperplano o recta H(β)
que separe efectivamente los ejemplos de las dos clases, como las que se muestran en la figura 4.15, la asignación de clase a una nueva instancia x es inmediata: si al aplicar a x los parámetros β que caracterizan la recta el resultado
es positivo, la instancia se asigna a una clase; si el resultado es negativo, el
ejemplo se asigna a la otra clase. Por tanto, el mecanismo de inferencia de las
SVM es trivialmente sencillo, solo hay que computar la ecuación del hiperplano para el punto x que clasificar y observar el signo del valor resultante.
Figura 4.15.
Problema de clasificación y diversas rectas o hiperplanos separadores
Por otro lado, como se puede observar en la figura 4.15, no existe una única
recta o hiperplano que realice la separación entre clases. De hecho, es fácil
202
CUADERNOS METODOLÓGICOS 60
convencerse de que existen infinitas rectas con esa cualidad. De entre todas
ellas, el método de las SVM se centra en encontrar aquella que maximiza el
margen o distancia entre la recta separadora y las clases separadas. Dado un
conjunto de ejemplos E, esta es la labor de la fase de entrenamiento del SVM.
Para plantear este problema de encontrar el hiperplano separador óptimo
H(β), o, equivalentemente, los parámetros β que lo determinan, es necesario
observar primero que la distancia entre un punto x = (x1 , x 2 ) y la recta H(β) se
puede obtener como
d (x , H (β )) =
β0 + β1x1 + β2 x 2
,
β
2
2
2
donde β = + β0 + β1 + β2 denota la norma o longitud del vector β. Además, es
preciso observar también que si H(β) es el hiperplano separador óptimo,
entonces este se ha de encontrar a la misma distancia d de los ejemplos más
cercanos de cada clase, esto es, han de existir en E dos ejemplos x + = (x1+ , x 2+ ; +1)
y x − = (x1− , x 2− ; −1), uno positivo y otro negativo, y situados a ambos lados del
hiperplano, cumpliendo
min
i =1,... N | y i =+1
d (x i , H (β )) = d (x + , H (β )) = d y
min
i =1,... N | y i =−1
d (x i , H (β )) = d (x − , H (β )) = d .
Estos ejemplos más cercanos al hiperplano separador reciben el nombre
de vectores soporte, de ahí la denominación del método SVM. Nótese entonces
que es posible escalar el vector de parámetros β para que d = 1, ya que si
β0 + β1x1+ + β2 x 2+
β + β1x1− + β2 x 2−
= +d y 0
= −d ,
β
β
entonces, tomando el vector de parámetros ω dado por
ω=
β
,
β d
se obtiene que
ω0 + ω1x1+ + ω2 x 2+ = +1 y ω0 + ω1x1− + ω2 x 2− = −1.
Además, el hiperplano separador H(ω) es el mismo que H(β), en tanto que β
y ω son proporcionales. Por otro lado, el margen M que maximizar se calcula
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
203
sumando las mínimas distancias existentes entre los vectores soporte y el
hiperplano separador óptimo, esto es,
M=
min
i =1,... N | y i =+1
d (x i , H (ω )) +
min
i =1,... N | y i =−1
d (x i , H (ω )),
y operando al maximizar M en términos de ω se obtiene
ω
ω
= max
ω
(
mini
1
ω
(
= max
1
ω
(ω
ω
d (x i , H (ω )) +
ω0 + ω1x1i + ω2 x 2i
min
i =1,... N | y i =+1
0
min
i =1,... N | y i =+1
i =1,... N | y =+1
= max
ω
(
ω
+
min
i =1,... N | y i =−1
mini
ω0 + ω1x1i + ω2 x 2i
ω
i =1,... N | y =−1
ω0 + ω1x1i + ω2 x 2i +
)
d (x i , H (ω )) =
min
i =1,... N | y i =−1
)
(
max M (ω ) = max
ω0 + ω1x1i + ω2 x
+ ω1x1+ + ω2 x 2+ + ω0 + ω1x1− + ω2 x 2− = max
ω
=
i
2
)=
ω
2
= min
ω
ω
2
Es decir, encontrar el vector de parámetros ω que maximiza el margen
M(ω) es equivalente a encontrar el vector ω de mínima norma ω que cumpla
que todos los ejemplos (x i , y i ) de cada clase queden adecuadamente separados por el hiperplano H(ω), es decir, tal que, para todo i = 1,…,N, se cumpla
y i (ω0 + ω1x1i + ω2 x 2i ) ≥ 1,
asumiendo que para los ejemplos positivos (con yi = +1) se tendrá ω0 + ω1x1i + ω2 x 2i ≥ 1
y para los negativos (con yi = -1) se tendrá ω0 + ω1x1i + ω2 x 2i ≤ −1. En conclusión, en
2
2
2
2
tanto que minimizar ω es equivalente a minimizar ω = ω0 + ω1 + ω2, el problema de encontrar el hiperplano separador de máximo margen es equivalente al
problema de optimización
min ω02 + ω12 + ω22
ω0 ,ω1 ,ω2
sujeto a una restricción y i (ω0 + ω1x1i + ω2 x 2i ) ≥ 1 por cada ejemplo (x i , y i ) en E,
i = 1,…,N. Este problema de optimización no es particularmente complicado,
y puede ser resuelto de manera eficiente por algoritmos de programación cuadrática, incluso para conjuntos de entrenamiento E de gran tamaño. La formulación anterior, para problemas de clasificación binarios con dos inputs,
puede ser generalizada sin muchas dificultades a problemas con más de dos
clases y con cualquier número de inputs, e incluso a problemas de regresión.
204
CUADERNOS METODOLÓGICOS 60
Así pues, mediante un elegante razonamiento matemático, el enfoque de
las SVM reduce el problema de clasificación a un problema de optimización
de márgenes para el que existen técnicas adecuadas para resolverlo. No obstante, es importante observar que el problema de optimización anterior podría
no tener ninguna solución si el conjunto de ejemplos no es linealmente separable, esto es, si no existe ningún hiperplano que consiga separar perfectamente todos los ejemplos de cada clase. Para ilustrar mejor esta situación,
obsérvese la figura 4.16. De nuevo, esta figura representa los ejemplos de un
problema de clasificación binario con dos inputs. Sin embargo, y el lector
puede convencerse intentándolo, ahora no existe ninguna recta que pueda
separar los ejemplos de ambas clases en la figura, de modo que todos los positivos queden a un lado y los negativos, al otro. Un problema de clasificación
con esta propiedad recibe el nombre de «no linealmente separable». Y, en
efecto, sin más añadidos, todo el planteamiento anterior del método SVM
resulta inútil en esta situación: al aplicar el algoritmo de optimización para
resolver el problema antes planteado, este simplemente responderá que no
existe solución.
Figura 4.16.
Problema de clasificación no linealmente separable
En tanto que, en la práctica, la enorme mayoría de problemas son de tipo
no linealmente separable, ¿significa esto que la metodología SVM es solo un
ejemplo más de cómo los matemáticos, como se piensa en el ideario popular,
pierden a veces el tiempo en construcciones de dudosa utilidad? La respuesta,
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
205
al menos en el caso de las SVM, es no, y, de hecho, la solución proviene de las
propias matemáticas. Esta radica en aplicar un truco matemático, conocido
como el truco del kernel (kernel trick), que consiste en transportar los ejemplos, originalmente contenidos en un espacio n-dimensional, donde no son
linealmente separables, a un espacio de más dimensiones donde sí puedan
separarse linealmente. Esto puede ilustrarse mejor atendiendo a la figura
4.17, en la que los ejemplos antes mostrados en la figura 4.16, contenidos en
el plano, han sido llevados a un espacio tridimensional, en el que ahora es sencillo comprobar que existen hiperplanos (en este caso, planos usuales) que los
separan perfectamente.
Figura 4.17.
Aplicación del kernel trick: los ejemplos de la figura 4.16,
al ser transportados del plano al espacio tridimensional,
pueden ahora ser separados linealmente mediante un plano
Aunque la idea del kernel trick es intuitivamente sencilla, su base matemática no lo es tanto y exponerla excedería las limitaciones de este manual, por lo
que nos centraremos en describir brevemente su funcionamiento. Transportar
los ejemplos xi a un espacio de dimensión superior, en el que sean más fácilmente separables, es equivalente a aplicarles una función ϕ: n → m adecuada,
con m > n, de modo que el SVM se aplicaría sobre los ejemplos transformados
(x i ) en el espacio m-dimensional. No obstante, como no es difícil imaginar,
existe una enorme infinidad de transformaciones de este tipo, por lo que es
necesario acotar la búsqueda de algún modo. La clave del truco consiste entonces en que es posible obtener de manera implícita la transformación  y los
206
CUADERNOS METODOLÓGICOS 60
cálculos necesarios para el desarrollo del SVM en el espacio transformado a
través de la aplicación de las llamadas funciones kernel, que resultan sencillas
de computar y permiten ser ajustadas de cara a cumplir con el objetivo deseado.
En particular, un kernel es una función κ : n × n → que se relaciona con la
transformación  a través de la expresión κ (x i , x j ) = ϕ (x i )t ⋅ ϕ (x j ) . Además,
por razones algo largas de explicar, es posible expresar el vector de parámetros
óptimo ω en el espacio transformado como
N
ω = ∑ ai y i ϕ ( x i ) ,
i =1
donde los coeficientes ai se obtienen resolviendo un problema de optimización relacionado con el original, conocido como problema dual. Debido a esta
igualdad, y como la clasificación de nuevas instancias x se realiza aplicando
este vector óptimo ω a su transformación (x) (si ωt ϕ (x ) > 0 , entonces x se clasifica como positivo, y, en caso contrario, como negativo), el truco del kernel
permite realizar los cálculos necesarios para esta tarea sin necesidad de obtener explícitamente la transformación  ni el vector ω, ya que por la relación
entre κ y  antes expuesta se tiene que
N
ωt ϕ (x ) = ∑ ai y i κ (x i , x ).
i =1
De este modo, el truco del kernel permite no solo resolver el problema de
transportar los ejemplos al espacio de dimensión superior, sino también
simplificar de manera significativa la resolución del problema de optimización de márgenes en ese espacio. Algunas funciones kernel habituales son las
siguientes:
— Lineal: κ (x , x j ) = x t x j .
— Polinomial: κ (x , x j ) = (γ x t x j + r )d .
j 2
— Función de base radial: κ (x , x j ) = e −γ ||x −x || .
— Tangente hiperbólica o sigmoidal: κ (x , x j ) = tanh(γ x t x j + r ) .
En tanto que estas funciones dependen de ciertos parámetros, como γ, d
y r, es posible realizar un ajuste del kernel que aplicar entrenando el SVM
con diferentes valores de estos parámetros para luego seleccionar los mejores valores midiendo el desempeño de los clasificadores resultantes en una
muestra de test.
Para concluir, es necesario incidir brevemente en un aspecto importante
del uso de las máquinas de soporte vectorial. Según se ha expuesto esta
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
207
metodología, en el problema de optimización que subyace a la búsqueda del
hiperplano separador se impone que todos los ejemplos de la muestra de
entrenamiento han de ser correctamente clasificados por el hiperplano
óptimo. Si el problema es no linealmente separable en el espacio original, lo
que se detectaría al responder el algoritmo de optimización que no existe solución en ese espacio, entonces, se ha de llevar a un espacio de dimensión superior mediante el truco del kernel. Esto es lo que se conoce como la versión
dura (hard SVM) de la metodología SVM. Pero podría suceder que, al aplicar
una configuración determinada del kernel, otra vez el problema fuese no separable en el nuevo espacio. De este modo, hasta que no se encuentra una configuración apropiada, que haga el problema separable, el algoritmo no podría
proporcionar una solución. Esto es claramente poco operativo en la práctica.
Para afrontar este problema, se desarrolló la versión suave (soft SVM) de la
metodología SVM, que relaja el problema de optimización hard permitiendo
que el hiperplano separador pueda clasificar incorrectamente algunos ejemplos en entrenamiento. En esta versión, para que la solución no degenere, se
penaliza la clasificación incorrecta de ejemplos, pero no se prohíbe, como
sucede en la versión hard. En la práctica actual se emplea casi siempre esta
versión soft, acompañada del truco del kernel para permitir un mejor rendimiento en problemas no separables.
Ejemplo práctico
La selección de los mejores parámetros del kernel de un SVM se puede realizar
mediante búsqueda en rejilla con validación cruzada, como se ha ilustrado en
secciones anteriores. Sin embargo, este proceso de selección se guiaba siempre por la tasa de acierto media en los conjuntos de test de la validación cruzada. En este ejemplo, se ilustrará la manera de obtener diversas medidas de
rendimiento, como curvas ROC, y cómo computar grupos de estas medidas en
un proceso de validación cruzada. Se ilustrará, además, cómo guiar la selección de parámetros por otras medidas de rendimiento diferentes a la tasa de
acierto.
El módulo de scikit-learn para el uso de máquinas de soporte vectorial se
llama svm. Dentro de este existen dos funciones principales, SVC y SVR, dedicadas, respectivamente, a tareas de clasificación y regresión. Comenzaremos
por ilustrar el uso del clasificador SVC, y, más adelante, se hará lo propio con
SVR. Se usará en este primer ejemplo el dataset Breast cancer, introducido en
la sección anterior, que, como se recordará, planteaba un problema de clasificación binaria relativamente sencillo. El siguiente código importa las librerías
necesarias, carga el dataset y normaliza los inputs, muy aconsejable al tratar
con máquinas de soporte vectorial, antes de realizar la partición de los ejemplos entre entrenamiento y test.
208
CUADERNOS METODOLÓGICOS 60
from
from
from
from
sklearn.svm import SVC
sklearn.datasets import load_breast_cancer
sklearn.preprocessing import MinMaxScaler
sklearn.model_selection import train_test_split
dataset = load_breast_cancer() # Carga del dataset Breast
cancer
X, y = dataset.data, dataset.target;
# Normalización de los inputs
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
# Partición en muestras de entrenamiento y test
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X,y,
test_size=0.4, random_state=13)
La función SVC admite una serie de parámetros que caracterizan la configuración del clasificador SVM particular que va a ser usado. En este ejemplo
nos centraremos principalmente en aquellos que permiten controlar la función kernel que aplicar, que básicamente son los siguientes:
—k
ernel: permite especificar el tipo de función kernel que aplicar. Puede
tomar los valores 'linear', que equivale a conservar los ejemplos en
el espacio de partida; 'poly', que especifica un kernel de tipo polinomial; 'rbf', para kernel de tipo función de base radial, y 'sigmoid'
para el sigmoidal.
—g
amma: especifica el valor del parámetro γ de las funciones kernel polinomial, de base radial y sigmoidal.
—d
egree: especifica el grado d de la función kernel polinomial.
—c
oef0: especifica el término independiente r de las funciones kernel
polinomial y sigmoidal.
Para comenzar y para que sirva de referencia, se ajustará y se evaluará en
test el clasificador SVC por defecto, que usa kernel = 'rbf' y gamma = 1/N,
donde N representa el número de ejemplos de la muestra de entrenamiento.
import time
comienzo=time.process_time()
clf = SVC()
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
209
clf.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Tasa de acierto en entrenamiento:",
clf.score(X_entrenamiento,y_entrenamiento))
print("Estimación del rendimiento real:", clf.score(X_test,
y_test))
Al ejecutar este código se presentan los siguientes resultados:
Duración del proceso: 0.015625
Estimación del rendimiento en entrenamiento: 0.9384164222873901
Estimación del rendimiento real: 0.9517543859649122
Veamos ahora el código para requerir el cómputo de otras medidas de rendimiento. Nótese que para las medidas de precisión y exhaustividad se ha de
especificar el valor codificado de la clase positiva, que es 0.
from sklearn import metrics
print("Kappa de Cohen:",
metrics.cohen_kappa_score(y_test,clf.predict(X_test)))
print("Precisión clase positiva:",
metrics.precision_score(y_test,clf.predict(X_test),pos_label=0))
print("Exhaustividad clase positiva:",
metrics.recall_score(y_test,clf.predict(X_test),pos_label=0))
print("AUC:",
metrics.roc_auc_score(y_test,clf.decision_function(X_test)))
Los resultados obtenidos son los siguientes:
Kappa de Cohen: 0.8896903589021816
Precisión clase positiva: 0.9714285714285714
Exhaustividad clase positiva: 0.8831168831168831
AUC: 0.990281241936871
210
CUADERNOS METODOLÓGICOS 60
Nótese que este clasificador proporciona una alta precisión, de más del
97%, al predecir la clase positiva, pero más de un 11% de los casos positivos
no son detectados. De cara a intentar encontrar un clasificador con la mayor
exhaustividad posible, se llevará a cabo una búsqueda en rejilla con validación
cruzada que provea la mejor configuración paramétrica de la función kernel
en términos de maximizar la exhaustividad. A diferencia de los ejemplos ilustrados en secciones pasadas, a la hora de seleccionar la mejor configuración,
esta búsqueda no se guiará por la tasa de acierto media en test de la validación
cruzada, sino que realizará esta selección atendiendo a la exhaustividad media
en test. Las sentencias que introducir para definir el rango de la búsqueda son
las siguientes:
import numpy as np
rango_gamma=(np.linspace(0.0001,0.01,num=15))
parametros_a_ajustar = [{'kernel': ['linear']},
{'kernel': ['rbf'], 'gamma': rango_gamma},
{'kernel': ['poly'], 'gamma': rango_gamma, 'degree':
[1,2,3,4,5],
'coef0': range(-1,2)},
{'kernel':
['sigmoid'],'gamma':
rango_gamma,'coef0':
range(-1,2)}]
Nótese que de esta manera se definen, en realidad, varias búsquedas
simultáneamente. La primera búsqueda usará un kernel lineal, que al no
depender de parámetros implica una única vuelta de validación cruzada. La
segunda búsqueda usará un kernel de base radial, y llevará a cabo 15 vueltas
de validación cruzada, una para cada valor del parámetro gamma en la lista
rango_gamma, formada por 15 valores equiespaciados entre 0,0001 y 0,01.
La búsqueda con el kernel polinomial realizará 15 × 5 × 3 = 225 vueltas, y la
sigmoidal, 15 × 3 = 45. Es decir, en total se realizarán 1 + 15 + 225 + 45 = 286
vueltas de validación cruzada, cada una con las K iteraciones correspondientes.
Por otro lado, es posible requerir que, a lo largo de la búsqueda en rejilla, se computen otras medidas aparte de la que va a guiar la selección posterior. Esto permite conocer estas medidas para todos los modelos ajustados
durante la búsqueda, sin necesidad de realizar una búsqueda independiente
para cada medida, lo que puede suponer un gran ahorro computacional.
Para ello, es necesario definir una lista de las medidas que usar que pueda
ser recogida y utilizada por la función GridSearchCV. Para ello se hace
uso de la función make_scorer, que crea un objeto admitido para tal propósito por la búsqueda en rejilla. El código para definir esta lista de medidas es el siguiente:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
211
medidas={'acierto' : 'accuracy',
'precisión':
metrics.make_scorer(metrics.precision_score,pos_label=0),
'exhaustividad' :
metrics.make_scorer(metrics.recall_score,pos_label=0),
'kappa' : metrics.make_scorer(metrics.cohen_kappa_score),
'auc' : 'roc_auc'}
Ya es posible, entonces, lanzar la búsqueda en rejilla de la mejor función
kernel para este problema. Nótese que se usa validación cruzada con cv=5
iteraciones y que la asignación refit='exhaustividad' es la que permite
especificar la medida que guía la selección de los mejores parámetros.
from sklearn.model_selection import GridSearchCV
comienzo=time.process_time()
clf = GridSearchCV(SVC(),param_grid=parametros_a_ajustar,
scoring=medidas, cv=5, refit='exhaustividad')
clf.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
# Salida de resultados de la búsqueda
print("Mejor configuración paramétrica:",clf.best_params_)
print("Exhaustividad media de la mejor configuración:",
clf.best_score_)
print("Tasa de acierto media de la mejor configuración:",
clf.cv_results_['mean_test_acierto'][clf.best_index_])
print("Precisión media de la mejor configuración:",
clf.cv_results_['mean_test_precisión'][clf.best_index_])
print("AUC media de la mejor configuración:",
clf.cv_results_['mean_test_auc'][clf.best_index_])
La salida obtenida es la siguiente:
Duración del proceso: 62.90625
Mejor configuración paramétrica: {'kernel': 'linear'}
Exhaustividad media de la mejor configuración: 0.9183230150972087
Tasa de acierto media de la mejor configuración: 0.9648093841642229
Precisión media de la mejor configuración: 0.9923302503947664
AUC media de la mejor configuración: 0.9886972455815886
212
CUADERNOS METODOLÓGICOS 60
Por tanto, la función kernel seleccionada para maximizar la exhaustividad es la lineal, que alcanza una exhaustividad media en las cinco iteraciones
de validación cruzada de casi un 92%, obteniendo en las mismas muestras de
entrenamiento y test una precisión de más del 99%. Nótese que se han ajustado un total de 286 × 5 = 1.430 clasificadores SVM en poco más de un minuto,
lo que da una muestra de la rapidez con que es posible entrenar estos clasificadores. Para estimar el rendimiento real de este clasificador seleccionado, se
ha de volver a ajustar con toda la muestra de entrenamiento, y computar predicciones sobre la muestra de test.
clf = SVC(kernel='linear')
clf = clf.fit(X_entrenamiento, y_entrenamiento)
Requiriendo ahora el cálculo de las mismas medidas de rendimiento en
test que antes, se obtiene:
Tasa de acierto: 0.9868421052631579
Kappa de Cohen: 0.9703021882598124
Precisión clase positiva: 1.0
Exhaustividad clase positiva: 0.961038961038961
AUC: 0.9969897652016858
Así pues, este parece ser un buen modelo, con una capacidad predictiva
incluso mayor que la lograda en este dataset Breast cancer con redes neuro­
nales en la sección anterior. El objetivo de maximizar la exhaustividad parece
cumplido, con algo más del 96%, y con una precisión total al predecir la clase
positiva. Finalmente, veamos el código necesario para obtener la curva ROC
de este clasificador seleccionado.
import matplotlib.pyplot as plt
fpr, tpr, thresholds = metrics.roc_curve(y_test,
-1*clf.decision_function(X_test),
pos_label=0)
roc_auc = metrics.auc(fpr, tpr)
plt.figure()
lw = 2
plt.plot(
fpr, tpr, color='darkorange',
lw=lw, label='Curva ROC (AUC = %0.4f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
213
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('Tasa de Falsos Positivos')
plt.ylabel('Tasa de Verdaderos Positivos')
plt.title('Curva ROC')
plt.legend(loc="lower right")
plt.show()
La curva ROC obtenida se muestra en la figura 4.18.
Figura 4.18.
Curva ROC del clasificador SVM
Pasemos ahora a la versión para problemas de regresión de las máquinas
de soporte vectorial. La función que implementa esta técnica, SVR, se comporta de manera prácticamente idéntica al clasificador SVC que se acaba de
ilustrar 13. En particular, los parámetros para controlar la forma del kernel son
exactamente los mismos. La variación más importante, como es lógico en un
contexto de regresión, es que la función SVR se ajusta a ejemplos que proporcionan ahora un target numérico, esto es, una variable dependiente continua.
En este contexto, también varían las medidas de rendimiento del algoritmo de
Esto se cumple igualmente para el resto de metodologías de aprendizaje ilustradas en este
capítulo.
13
214
CUADERNOS METODOLÓGICOS 60
aprendizaje, que se adaptan para medir errores o distancias entre los valores
observados del target y los valores predichos por el algoritmo una vez entrenado.
Ilustremos brevemente estas diferencias. En el ejemplo que sigue se utilizará el conjunto de datos Boston housing, en el que cada ejemplo está asociado a una sección censal del año 1970 de la ciudad de Boston, EE. UU., y
proporciona información de n = 13 diferentes atributos de esas localizaciones,
desde tasas medias de criminalidad a niveles de polución, pasando por ratios
educativas o índices de pobreza. La variable dependiente es el precio mediano
de las viviendas o casas en cada sección, en miles de dólares. El dataset consta
de N = 506 ejemplos, y el objetivo, por tanto, es predecir el precio mediano de
las viviendas a partir de los atributos disponibles.
Para ello, se adapta el código antes visto al contexto de regresión, utilizando la función SVR en lugar de SVC, así como varias medidas de rendimiento para regresión del módulo metrics. No se repetirá la búsqueda
paramétrica en rejilla, que puede ser adaptada fácilmente a este contexto, sino
que se llevarán a cabo un par de ajustes del procedimiento de regresión, primero, utilizando la variable target original, y, después, la obtenida tras aplicarle una transformación. El código para el primer ajuste es el siguiente:
from sklearn.svm import SVR
from sklearn.datasets import load_boston
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn import metrics
import time
dataset = load_boston() # Carga del dataset Boston housing
X, y = dataset.data, dataset.target;
# Normalización de los inputs
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X,y,test_size=0.4, random_state=13)
# Ajuste de la regresión de vector soporte
comienzo=time.process_time()
regresion = SVR();
regresion.fit(X_entrenamiento,y_entrenamiento)
# Salida de resultados
print("Duración del proceso:",time.process_time()-comienzo)
print("Error cuadrático medio:",
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
215
metrics.mean_squared_error(y_test,regresion.predict(X_
test)))
print("Error absoluto medio:",
metrics.mean_absolute_error(y_test,regresion.predict(X_
test)))
print("Error absoluto mediano:",
metrics.median_absolute_error(y_test,regresion.predict(X_
test)))
print("Coef. de determinación:",
metrics.r2_score(y_test,regresion.predict(X_test)))
El resultado de la ejecución es el siguiente:
Duración del proceso: 0.015625
Error cuadrático medio: 49.046853761815505
Error absoluto medio: 4.4856335872786035
Error absoluto mediano: 2.6809154053546074
Coef. de determinación: 0.3742219085556109
Como vemos, el ajuste de la máquina de soporte vectorial para regresión se
realiza a una velocidad comparable a la de clasificación, aunque en este caso
el rendimiento sobre la muestra de test no parece del todo bueno, con un error
medio de casi 4.500 dólares entre la predicción y el precio real de las casas. En
tanto que la media de la variable dependiente en los ejemplos de test es aproximadamente 22 (representando un precio mediano medio de 22.000 dólares), este error medio supone una desviación relativa de más del 20%. Por otro
lado, el error mediano es considerablemente inferior, apuntando a la presencia de algunas secciones censales con errores abultados que inflan la media
respecto a la mediana. Además, el valor obtenido del coeficiente de determinación o R2, el cuadrado de la correlación entre la variable target observada y
la predicha, implica que este modelo solo está explicando un 37% de la variabilidad observada del target.
Se podría ejecutar una búsqueda de parámetros para intentar encontrar un
modelo con mejor ajuste y capacidad predictiva. Sin embargo, en este caso es
mejor opción llevar a cabo, al menos de entrada, una transformación de la variable target. La razón es que diversos estudios, entre ellos aquel en el que apareció
el primer análisis de estos datos (Harrison et al., 1978), apuntan a que los modelos de precio de viviendas han de tener una forma hedónica, en que el efecto de
las variables explicativas sobre las predicciones sea multiplicativo, en lugar de
216
CUADERNOS METODOLÓGICOS 60
lineal. Esto conduce a un modelo de regresión loglineal, en que se ha de transformar la variable target en su logaritmo, de manera que el modelo ha de ajustarse
a esta variable transformada en lugar de a la original. Por supuesto, las predicciones del modelo así ajustado han de ser devueltas a su escala original, lo que se
hace tomando la exponencial de los valores predichos, antes de poder computar
las medidas de rendimiento. El siguiente código realiza estas operaciones:
# Transformación de la variable target, en entrenamiento y en
test
import numpy as np
y_entrenamiento_log=np.log(y_entrenamiento)
y_test_log=np.log(y_test)
# Nuevo ajuste del modelo con la variable transformada
comienzo=time.process_time()
regresion.fit(X_entrenamiento,y_entrenamiento_log)
# Salida de resultados
# los valores predichos han de «destransformarse»
print("Duración del proceso:",time.process_time()-comienzo)
print("Error cuadrático medio:",
metrics.mean_squared_error(
y_test,np.exp(regresion.predict(X_test))))
print("Error absoluto medio:",
metrics.mean_absolute_error(
y_test,np.exp(regresion.predict(X_test))))
print("Error absoluto mediano:",
metrics.median_absolute_error(
y_test,np.exp(regresion.predict(X_test))))
print("Coef. de determinación:",
metrics.r2_score(y_test,np.exp(regresion.predict(X_test))))
Su salida es la que sigue:
Duración del proceso: 0.0137
Error cuadrático medio: 16.402021473810436
Error absoluto medio: 2.7119444823254475
Error absoluto mediano: 1.9563019947248748
Coef. de determinación: 0.7907301915112475
El rendimiento de este modelo es claramente superior al anterior. Ahora el
error absoluto medio es de unos 2.700 dólares, un 12% en relación con el valor
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
217
medio del target. Y el coeficiente de determinación estima que la proporción
de variabilidad del target explicada por este modelo supera el 79%. Difícilmente se podría haber alcanzado esta mejora del modelo anterior recurriendo
solamente a la búsqueda en rejilla. Sin embargo, esta puede ser útil ahora
para intentar conseguir un modelo con aún mejor capacidad predictiva, aunque, como muestran Harrison et al. (1978), la transformación de algunos
inputs también puede dar buenos resultados.
4.1.2.6. Random forest
La metodología de aprendizaje supervisado que se expone a continuación se
basa en la idea de que, debido a que los datos de entrenamiento casi siempre
contienen algo de ruido, para eliminar la influencia de este ruido puede ser
más eficaz entrenar una colección de programas objetivo relativamente sencillos y en parte aleatorios y promediar sus resultados, de modo que la parte de
ruido que cada programa generaliza se compense de algún modo con la de
otros programas, eliminándose al agregar sus resultados. Aunque cada uno
de los programas o modelos entrenados puede tener un rendimiento relativamente débil, siendo sensible al ruido de la muestra con que se entrena y prediciendo o generalizando incorrectamente un número de instancias, es difícil
que la mayoría sea sensible al mismo ruido y prediga incorrectamente las mismas instancias, de modo que el rendimiento agregado de todo el conjunto de
programas puede ser mucho mejor que el de los programas individuales.
En particular, la metodología de random forest (RF) consiste en entrenar
una colección de árboles de decisión (véase la sección 4.1.2.2), cada uno
usando una muestra diferente de ejemplos seleccionados aleatoriamente con
reemplazamiento del conjunto de entrenamiento E y, en cada nodo a ramificar, seleccionando al azar un grupo reducido de variables explicativas para
realizar la ramificación. El entrenamiento de los árboles se realiza sin poda,
de modo que tengan la mayor profundidad posible. A la hora de predecir una
nueva instancia, se envía esta a cada uno de los árboles entrenados, y sus predicciones se combinan, usando algún promedio en el caso de problemas de
regresión, y la moda de las clases predichas (es decir, la clase por la que «vota»
un número mayor de árboles) en problemas de clasificación.
Puede resultar sorprendente que este tipo de procedimiento, que escoge
la muestra y las variables explicativas al azar, pueda tener éxito. Sin embargo,
esta estrategia da, en general, muy buenos resultados. La selección aleatoria
con reemplazamiento de una colección de muestras a partir de un conjunto
de ejemplos inicial recibe el nombre de bootstrapping, y se ha empleado
desde hace ya algunas décadas en la rama más computacional de la estadística para obtener estimadores más robustos y con poca carga de hipótesis
teóricas. Su aplicación al campo del aprendizaje automático, iniciada a
218
CUADERNOS METODOLÓGICOS 60
mediados de la década de los noventa del siglo pasado, se suele conocer
como bootstrap aggregating o simplemente bagging. Esta técnica combina la
idea del remuestreo con reemplazamiento con la de agregar o promediar los
resultados de los modelos obtenidos con cada muestra bootstrap, y se puede
aplicar con prácticamente cualquier metodología de aprendizaje, normalmente mejorando su rendimiento. El inconveniente del bagging es que
requiere entrenar un número elevado de modelos, por lo que suele ser más
eficiente cuando los modelos que entrenar son relativamente sencillos y
poco costosos computacionalmente. Este es el caso de los árboles de decisión, y una de las razones de que los RF sean uno de los ejemplos más exitosos de bagging. La otra clave fundamental del buen funcionamiento de esta
metodología es la selección aleatoria de atributos o variables explicativas,
que se empezó a investigar hará unos veinte años, a finales de la década de
los noventa del siglo pasado. Esta selección aleatoria de las variables que
intervienen en la ramificación de los nodos permite que los diferentes árboles entrenados estén poco correlados, dando lugar a una mayor variedad de
árboles que, conjuntamente, exploran mejor la relación entre inputs y outputs, y evitan que tiendan a generalizar el mismo ruido.
A la hora de entrenar un random forest, es necesario especificar el número M
de árboles en que consistirá, y el número m de atributos escogidos al azar que
participarán en cada ramificación. A partir de estos parámetros y un conjunto
de entrenamiento E con N ejemplos y n variables explicativas, el algoritmo
procede como sigue:
1.Para cada i = 1,…,M, realizar los pasos 2 y 3 siguientes:
2.Seleccionar aleatoriamente con reemplazamiento de E una muestra Ei
con N ejemplos.
3.Entrenar un árbol de decisión Pi a partir de la muestra Ei, seleccionando
aleatoriamente en cada ramificación un grupo de m atributos de los n
disponibles para realizarla.
4.Dada una nueva instancia x que predecir, calcular la predicción Pi(x)
realizada por cada árbol.
5.Agregar las predicciones Pi(x) para obtener la predicción final P(x).
La agregación de las predicciones individuales de los diferentes árboles
se realiza mediante un promedio, típicamente la media aritmética en una
tarea de regresión, y la moda o la clase que predice un número mayor de
árboles en el caso de la clasificación. Valores típicos de los parámetros pueden ser M = 100, 500 o 1.000, m = n/3 en problemas de regresión y m = n
en problemas de clasificación. Como se ha indicado, el entrenamiento de los
árboles se realiza sin poda, aunque algunas implementaciones imponen
algún criterio de parada, como un número mínimo de ejemplos en cada
nodo padre.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
219
El procedimiento de remuestreo con reemplazamiento tiende a dejar
fuera de cada conjunto de entrenamiento Ei una tercera parte de los N ejemplos disponibles, que pueden ser usados a modo de muestra de test sobre
cada árbol para estimar su capacidad de generalización. De este modo, es
posible obtener una estimación del error global del random forest sin necesidad de usar una muestra de test separada. La medida obtenida mediante este
planteamiento se conoce como estimación o error out-of-bag. Su obtención
pasa por establecer un predictor Poob (el predictor out-of-bag) como sigue:
para cada ejemplo (x,y) en E, la predicción Poob(x) de este ejemplo se realiza
agregando o combinando solo las predicciones Pi(x) de los árboles Pi para los
que (x,y) no está contenido en la muestra Ei correspondiente. La estimación
out-of-bag se obtiene entonces como el error que comete este predictor Poob
sobre la muestra E. En tanto que la predicción Poob(x) se computa promediando aproximadamente un tercio de los árboles que se usan efectivamente
para computar la predicción real P(x) del RF, esta estimación out-of-bag
tiende a sobreestimar el error real, aunque para M grande el estimador se
puede considerar insesgado.
Otra característica interesante de los random forest es que permiten estimar la importancia relativa de las variables explicativas, esto es, su capacidad
de explicar la variabilidad observada del target. Esto se realiza computando,
para cada variable, una estimación out-of-bag en la que los valores de esa
variable se han permutado o barajado, y comparando luego esta estimación
con la estimación out-of-bag habitual. En particular, el procedimiento para
calcular esta importancia de una variable explicativa se realiza tras entrenar
cada árbol, momento en el que, dada una variable Xi de las n en el conjunto E,
se permutan aleatoriamente los valores de esta variable en los ejemplos outof-bag, y estos se predicen normalmente con el árbol recién entrenado. Esto se
repite para todas las n variables. Tras entrenar todos los M árboles del RF, y
para cada ejemplo en E, se comparan las diferentes predicciones out-of-bag de
ese ejemplo, realizadas con los valores de Xi permutados, con el valor real de
su target. Esto produce una estimación de error para cada variable, que es
entonces comparada con la estimación out-of-bag habitual. La importancia de
una variable es, luego, igual a esta diferencia entre el error out-of-bag y el
error cometido al introducir ruido aleatorio en esa variable. Una diferencia
mayor implica una mayor importancia de la variable en el rendimiento del
RF. Las variables que reciben una importancia elevada mediante este procedimiento poseerán con seguridad una capacidad explicativa relevante respecto al target. Sin embargo, el recíproco no es cierto: si una variable recibe
una importancia baja, no necesariamente es una variable sin información
explicativa del target. Puede resultar simplemente que la información de esta
variable sea de algún modo redundante con la de otras variables, de forma
que su importancia puede verse disminuida al interactuar con ellas en los
árboles.
220
CUADERNOS METODOLÓGICOS 60
Ejemplo práctico
En este ejemplo práctico se ilustrarán las características distintivas de los random forest, como son la posibilidad de computar estimaciones out-of-bag sobre
la muestra de entrenamiento y el cálculo de la importancia de las variables
explicativas. Además, a través de la creación de datos artificiales de cierta
dimensión, será posible ilustrar algunas de las capacidades de paralelización
que permite la implementación de los random forest en scikit-learn. Previamente, para complementar los ejemplos vistos en otras secciones, se proporcionarán algunas nociones sobre estructuras de datos y el código para exportar
datos desde scikit-learn y para importarlos desde archivos CSV. Esto permitirá
al lector llevar los datos que se han estado usando en este capítulo a otros programas, así como importar sus propios datos para tratarlos con scikit-learn.
Comencemos por la exportación de datos. Para ello, se cargará en scikitlearn el dataset Breast cancer, introducido y usado en secciones previas, y se
exportará a un archivo en formato CSV (comma separated values), que puede
ser leído con Excel u otros programas estadísticos como, por ejemplo, SPSS.
En este archivo CSV se incluirá en la primera fila el nombre de las diferentes
variables o columnas, y, a continuación, los datos en sí, un ejemplo por fila.
Conviene tener en cuenta que, al cargar los datos en scikit-learn mediante la
función load_breast_cancer (o similar para otros datasets), estos se almacenan en
un objeto con formato (o de tipo) bunch, en el que diferentes partes del dataset,
como los nombres de las variables o los propios datos, se encuentran separadas
en diferentes atributos de este objeto. Así, por ejemplo, el atributo feature_names
proporciona una lista con los nombres de las variables explicativas, y el atributo
data (resp. target) contiene la matriz o array con los valores de los inputs (resp. del
target) de todos los ejemplos. Por ello, antes de realizar la exportación al archivo
CSV es necesario juntar de algún modo estas diferentes partes. En particular, se
ha de crear una lista con los nombres de las variables explicativas y de la variable
target, así como una única matriz que contenga en cada fila los valores de los
inputs y del target de cada ejemplo. Esto se lleva a cabo con el siguiente código:
import numpy as np
from sklearn.datasets import load_breast_cancer
# Carga del dataset Breast cancer en un objeto de tipo bunch
mi_bunch = load_breast_cancer()
# En los atributos data y target de este bunch se encuentran
los arrays
# con los valores de inputs y target de cada ejemplo,
# los cuales se cargan en X e y
X, y = mi_bunch.data, mi_bunch.target
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
221
# Lista de nombres de variables o columnas para ser exportada.
# A los nombres de los inputs obtenidos con el atributo feature_names
# se les añade el nombre 'clase' para la variable target
columnas=mi_bunch.feature_names
columnas=np.append(columnas,'clase')
print(columnas)
# Creación de un único array con los datos de inputs y targets
# de todos los ejemplos, uno por fila
Xy=np.hstack((X,np.reshape(y,(X.shape[0],1))))
Nótese que, como se observa en la salida obtenida al ejecutar este código,
que se muestra a continuación, columnas contiene los nombres de todas las
variables, inputs y targets, los primeros extraídos del atributo feature_names
del bunch, y 'clase' para el target. Por su parte, la matriz Xy concatena
horizontalmente los valores de inputs y targets de cada ejemplo.
['mean radius' 'mean texture' 'mean perimeter' 'mean area'
'mean smoothness' 'mean compactness' 'mean concavity' 'mean
concave points' 'mean symmetry' 'mean fractal dimension'
'radius error' 'texture error' 'perimeter error' 'area error' 'smoothness error' 'compactness error' 'concavity error' 'concave points error' 'symmetry error' 'fractal dimension error' 'worst radius' 'worst texture' 'worst perimeter'
'worst area' 'worst smoothness' 'worst compactness' 'worst
concavity' 'worst concave points' 'worst symmetry' 'worst
fractal dimension' 'clase']
La exportación se realiza entonces con el siguiente código, que creará el
archivo breast_cancer.csv en el directorio desde el que se ejecuta el programa.
Nótese que, en este archivo, los valores de los diferentes inputs y targets estarán separados, en cada línea correspondiente a un ejemplo, por el delimitador
o separador «;». Este permite indicar a los programas que han de leer este
archivo el final de cada valor de las diferentes variables, de modo que pueda
leerlos separadamente. Es importante tener en cuenta que en este archivo
CSV la coma decimal se encuentra representada por el carácter «.», por lo que
para que pueda ser leído por algunos programas (como Excel en su configuración habitual para España) puede ser necesario reemplazar estos «.» por «,»
(Excel, por ejemplo, permite especificar esta conversión al importar el CSV).
222
CUADERNOS METODOLÓGICOS 60
import CSV
archivo=open('breast_cancer.csv','w',newline='')
writer = CSV.writer(archivo,delimiter=';')
writer.writerow(columnas)
writer.writerows(Xy)
archivo.close()
Para ilustrar la importación de datos a un formato que pueda ser usado en
scikit-learn, se importará ahora el archivo breast_cancer.csv recién creado. En
este sentido, conviene tener en cuenta que el formato que utilizan los procedimientos de aprendizaje de scikit-learn es el llamado tipo array del paquete numpy,
que básicamente es equivalente a una matriz o vector. Además, como se ha visto
en secciones anteriores, las funciones de scikit-learn que implementan estos procedimientos requieren normalmente que se introduzcan de manera separada un
array con los inputs y otro con el target, esto es, no se les puede introducir toda
la matriz de datos con inputs y target juntos. Así pues, tras importar el archivo
CSV (en el que, recordemos, se han concatenado inputs y target), será necesario
separar en dos arrays diferentes los valores de los inputs y del target. Por comodidad, la importación se realiza a través de un formato o tipo conocido como
pandas dataframe, que es, en cierto modo, parecido a un bunch como los que utiliza scikit-learn, y que permite guardar los encabezados o nombres de columna
del CSV para su uso posterior. El código para la importación es el siguiente:
# Importación del archivo CSV a un pandas dataframe
import pandas as pd
filename='breast_cancer.csv'
datos = pd.read_CSV(filename,sep=';')
# Obtención de los nombres de variable
columnas=datos.columns.get_values()
print(columnas[columnas.shape[0]-1])
# Separación de los datos de inputs y target
X2=datos.get_values()[:,:columnas.shape[0]-1]
y2=datos.get_values()[:,columnas.shape[0]-1]
Obsérvese en la salida producida al ejecutar que la variable columnas
creada ahora contiene exactamente los mismos nombres de inputs y targets
que la creada en el código anterior. Además, la variable X2 contiene los datos
de todas las columnas del archivo CSV a excepción de la última, que contiene
los valores del target de cada ejemplo y que se almacena en y2.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
223
Como muestra de que ambos procesos de exportación e importación han
funcionado correctamente, y para comenzar a ilustrar el uso de random forest
en scikit-learn, a continuación, se ajusta un clasificador a todos los datos. Para
esto, se ha de importar el módulo ensemble de scikit-learn, que contiene las
funciones RandomForestClassifier y RandomForestRegressor, la
primera para problemas de clasificación y la segunda, para regresión. Solo se
ilustrará aquí el uso de la primera, pero, como se ha visto en la sección anterior, la segunda se utiliza de manera casi idéntica a la primera.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=0)
rf.fit(X2,y2)
print("Tasa de acierto:", rf.score(X2, y2))
La salida obtenida es la siguiente:
Tasa de acierto: 0.9982425307557118
Así pues, el clasificador random forest con sus parámetros por defecto consigue clasificar correctamente un 99,82% de los ejemplos de todo el dataset
Breast cancer. Como sabemos, esta evaluación puede sobreestimar el rendimiento del clasificador, ya que se está realizando con los mismos datos usados
para su entrenamiento. Por ello, a continuación, se dividirán los datos originales en las respectivas muestras de entrenamiento y test.
De cara al ajuste de un clasificador random forest, probablemente el parámetro más relevante de la función RandomForestClassifier es n_estimators, que especifica el número M de árboles de los que se compondrá el random
forest. En la implementación por defecto, sin especificar valores de este parámetro como se ha hecho en el código anterior, se usa n_estimators = 10, esto es,
el clasificador ajustará diez árboles a otras tantas muestras bootstrap obtenidas
aleatoriamente con reemplazamiento a partir de la muestra de entrenamiento.
Para garantizar que en diferentes ejecuciones estas muestras sean siempre las
mismas y que las variables seleccionadas en cada ramificación sean también
las mismas, se ha especificado el parámetro random_state = 0, que fija la
semilla aleatoria usada para inicializar el generador de números aleatorios que
se emplea en el ajuste del clasificador. Por otro lado, para requerir que la función
RandomForestClassifier obtenga las estimaciones out-of-bag en cada árbol,
se ha de especificar oob_score = True. El siguiente código permite obtener
una estimación más realista del rendimiento del clasificador anterior:
224
CUADERNOS METODOLÓGICOS 60
from sklearn.model_selection import train_test_split
import time
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X,y, test_size=0.4, random_state=13)
comienzo=time.process_time()
rf = RandomForestClassifier(n_estimators=10,
oob_score=True, random_state=0);
rf.fit(X_entrenamiento,y_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Tasa de acierto en entrenamiento:",
rf.score(X_entrenamiento,y_entrenamiento)
print("Estimación del rendimiento real:", rf.score(X_test,
y_test))
print("Estimación out-of-bag",rf.oob_score_)
La salida mostrada es la siguiente:
Duración del proceso: 0.0625
Tasa de acierto en entrenamiento: 0.9941348973607038
Estimación del rendimiento real: 0.9385964912280702
Estimación out-of-bag 0.9296187683284457
Nótese que el entrenamiento de los diez árboles se ha realizado en unas
centésimas de segundo, y que, como era de esperar, el rendimiento en test es
sensiblemente inferior que en entrenamiento. Además, obsérvese que la estimación out-of-bag proporciona una evaluación cercana a la obtenida sobre la
muestra de test, aunque en este caso se nos muestra un mensaje advirtiendo
de que, por el pequeño número de árboles empleados, algunos ejemplos
podrían no haber quedado fuera de ninguna de las muestras bootstrap, por lo
que no intervendrían en ninguna estimación out-of-bag y esta podría no ser
del todo confiable.
Repitamos la ejecución del código anterior especificando valores diferentes del parámetro n_estimators. Así, con n_estimators = 100 se obtienen las siguientes medidas:
Duración del proceso: 0.296875
Tasa de acierto en entrenamiento: 1.0
Estimación del rendimiento real: 0.956140350877193
Estimación out-of-bag 0.9501466275659824
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
225
Vemos entonces que el rendimiento del clasificador parece mejorar al especificar un número mayor de árboles, y que la estimación out-of-bag, sobre la
que ya no se muestra mensaje de advertencia, es consistente con la obtenida
en test (de hecho, suele ser algo más conservadora). Con n_estimators =
1000 el rendimiento sigue mejorando, aunque ahora ya se empieza a observar que el ajuste del clasificador requiere de un tiempo no insignificante, casi
tres segundos.
Duración del proceso: 2.859375
Tasa de acierto en entrenamiento: 1.0
Estimación del rendimiento real: 0.9605263157894737
Estimación out-of-bag 0.9530791788856305
Finalmente, con n_estimators = 10000 el rendimiento en test empieza
a decrecer, y el tiempo de ejecución es ya ciertamente largo, casi medio
minuto. Este empeoramiento del rendimiento en test es achacable al sobreajuste, ya que un número de árboles demasiado elevado puede ajustar en demasía la muestra de entrenamiento. Por ello, es conveniente siempre buscar un
valor equilibrado de n_estimators, que provea un rendimiento adecuado
sin enlentecer en exceso el entrenamiento y sin sobreajustar los datos.
Duración del proceso: 28.140625
Tasa de acierto en entrenamiento: 1.0
Estimación del rendimiento real: 0.956140350877193
Estimación out-of-bag 0.9530791788856305
Veamos ahora cómo obtener un ranking de importancia de las variables
explicativas disponibles. Como se introdujo más arriba, una de las peculiaridades de la metodología de random forest es que, por su uso de árboles de
decisión en un entorno de bagging, permite computar una importancia de
cada variable atendiendo a cómo estas han rendido en el ajuste de los diferentes árboles. En tanto que esta importancia se obtiene promediando un número
considerable de árboles, esta suele ser una medida robusta para identificar las
variables con mayor capacidad explicativa del target. La importancia de cada
variable obtenida tras el ajuste se encuentra en el atributo feature_importances_ del objeto clasificador. En el código que sigue, los índices de las
variables explicativas se reordenan con la función np.argsort para que las
primeros índices correspondan a las variables de mayor importancia.
226
CUADERNOS METODOLÓGICOS 60
# Obtención de importancias y reordenación de los índices
importancia = rf.feature_importances_
indices = np.argsort(importancia)[::-1]
# Ranking de importancia
print("Ranking de importancia de las variables:")
for i in range(X.shape[1]):
print("%d. %s (%f)"
%(i + 1,mi_bunch.feature_names[indices[i]],
importancia[indices[i]]))
Una muestra de la salida obtenida es la que sigue, dando la posición en el
ranking de cada variable, su nombre y, entre paréntesis, su importancia:
Ranking de importancia de las variables:
1. worst perimeter (0.136134)
2. worst concave points (0.124342)
3. worst radius (0.113386)
4. worst area (0.109645)
...
30. compactness error (0.003098)
El siguiente código permite crear un gráfico de barras representando las
importancias de un número de variables especificado mediante el valor de
variables. El gráfico obtenido se muestra en la figura 4.19.
import matplotlib.pyplot as plt
variables=4
lista=[]
for i in range(variables):
lista.append(mi_bunch.feature_names[indices[i]])
print(lista[:variables])
plt.figure()
plt.title("Importancia de variables")
plt.bar(range(variables), importancia[indices[:variables]],
color="r", align="center")
plt.xticks(range(variables), lista[:variables], size=7)
plt.xlim([-1, variables])
plt.show()
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
227
Figura 4.19.
Importancia de variables para el problema de clasificación de Breast cancer
Finalmente, se ilustrará la capacidad de paralelización de tareas que ofrece
scikit-learn. Esta paralelización se refiere a la posibilidad de separar el total de
tareas que realizar en el ajuste de un modelo en diversos threads o hilos, de
manera que cada hilo se envía a un procesador o CPU diferente, que realizará
las tareas de ese hilo de forma paralela a otros procesadores, encargados, a su
vez, de otros hilos. Este trabajo en paralelo puede permitir entonces realizar
las tareas en menos tiempo que el requerido al procesar las tareas en serie en
un único procesador, lo cual puede conllevar ahorros significativos de tiempo,
especialmente con conjuntos de datos masivos.
Sin embargo, es necesario tener en cuenta que a paralelización genera, a su
vez, una carga de trabajo extra en tanto se han de dividir las tareas entre los
diferentes procesadores y unificar las salidas de cada uno de ellos. Esta carga
de trabajo extra se suele denominar overhead. Por ello, al usar dos procesadores en paralelo no se tarda necesariamente la mitad de tiempo en realizar la
ejecución que al usar un único procesador, ya que los overheads pueden consumir una cantidad de tiempo relevante. De hecho, estos overheads pueden
incluso hacer que el proceso en paralelo se prolongue más que el proceso
habitual en serie. En general, solo se ha de usar la paralelización cuando la
carga de tareas sea suficientemente grande para que el trabajo en paralelo
compense los overheads. En el entorno de scikit-learn, esto se empieza a cumplir solo con datasets con un número de ejemplos mayor a 10.000 o incluso
100.000.
Los conjuntos de datos que provee scikit-learn (como Iris, Breast cancer o
Boston housing) no cumplen esta restricción de tamaño, por lo que se recurrirá a la generación de un dataset aleatorio con un tamaño suficiente. El
228
CUADERNOS METODOLÓGICOS 60
siguiente código genera un conjunto de datos para clasificación binaria con
un millón de ejemplos, cada uno con diez atributos, de los cuales solo tres son
realmente informativos para la predicción del target.
from sklearn.datasets import make_classification
X1, y1 = make_classification(n_samples=1000000, n_features=10,
n_informative=3,
n_redundant=0, n_repeated=0, n_classes=2,
random_state=0, shuffle=False)
El número de procesadores que se requiere usar en el ajuste de un clasificador random forest se controla mediante el parámetro n_jobs. Por defecto,
este parámetro toma el valor 1, que especifica el uso de un único procesador.
En el ejemplo que sigue se usará el valor n_jobs = –1, que especifica que se
usen todos los procesadores disponibles. Este parámetro n_jobs y su funcionalidad de paralelización se encuentran en muchas funciones de scikit-learn,
como, por ejemplo, GridSeacrhCV (búsqueda en rejilla con validación cruzada), MLPClassifier y otras.
Así pues, como referencia, se ejecutará primero sin paralelizar el ajuste de un
random forest de diez árboles a los datos anteriores, esto es, con n_jobs=1. Para
la medición de la duración del ajuste, se usará la función time, aparte de la ya
conocida process_time. La diferencia entre ambas es que la segunda función
mide el tiempo de CPU empleado en la tarea de ajuste, mientras que la primera
simplemente mide la diferencia de tiempo transcurrido entre dos llamadas sucesivas. Como veremos, la diferencia es significativa en el caso de la paralelización.
comienzo=time.process_time()
comienzo2=time.time()
rf = RandomForestClassifier(n_estimators=10,n_jobs=1,random_
state=0)
rf.fit(X1,y1)
print("Duración del proceso:",time.process_time()-comienzo)
print("Tiempo transcurrido:",time.time()-comienzo2)
Al ejecutar este código, sin requerir el empleo de más de un procesador, se
obtienen los siguientes tiempos:
Duración del proceso: 161.9375
Tiempo transcurrido: 162.16108655929565
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
229
Como se puede observar, el ajuste de los diez árboles de clasificación a
estos datos ha consumido algo más de dos minutos y medio, unos 160 segundos. Ambas funciones de tiempo arrojan duraciones similares. Sin embargo,
al ejecutar el siguiente código, especificando que se usen todos los procesadores disponibles en el ordenador,
comienzo=time.process_time()
comienzo2=time.time()
rf = RandomForestClassifier(n_estimators=10,n_jobs=-1,random
_state=0);
rf.fit(X1,y1)
print("Duración del proceso:",time.process_time()-comienzo)
print("Tiempo transcurrido:",time.time()-comienzo2)le=False)
se obtienen los tiempos:
Duración del proceso: 236.203125
Tiempo transcurrido: 45.1796190738678
Por tanto, este segundo ajuste paralelizado ha consumido casi un 50% más
de tiempo de CPU, pero, al distribuirse este entre varios procesadores, el tiempo
realmente transcurrido en el ajuste es sensiblemente inferior al consumido
antes, solo unos 45 segundos, un 75% menor al del ajuste anterior. Así pues, la
paralelización puede contribuir a un ahorro de tiempo muy significativo en el
ajuste de procedimientos de aprendizaje a grandes conjuntos de datos.
4.1.2.7. El algoritmo de las K medias
El algoritmo de las K medias (K-means) es un algoritmo de aprendizaje no
supervisado que trata el problema del clustering o análisis de conglomerados.
Recordemos que en este contexto se busca hallar grupos de ejemplos similares sin conocer a priori la etiqueta de cada ejemplo. Así, cada ejemplo
x = (x1 ,..., x n ) solo contiene información de las variables explicativas o inputs,
y ha de ser asignado a un único grupo o cluster, de manera que los grupos
resultantes han de contener ejemplos similares o cercanos entre sí (en el espacio de inputs) y, a la vez, distantes de los ejemplos de otros clusters. El algoritmo de las K medias proporciona posiblemente una de las técnicas más
sencillas e intuitivas para afrontar este problema cuando se conoce el número
K de grupos que formar. Su idea básica consiste en, dada una asignación
230
CUADERNOS METODOLÓGICOS 60
previa de los N ejemplos en K grupos, computar los centros o medias (también
llamados centroides) de cada grupo en el espacio de inputs, y, a continuación,
corregir la asignación inicial asignando cada ejemplo al grupo cuyo centro le
sea más cercano. Este proceso puede entonces volver a repetirse, y se detiene
cuando ya no hay ejemplos que cambien su asignación tras actualizarse los
centroides.
De modo parecido a lo que ocurría con el algoritmo de los k vecinos más
cercanos o k-NN, el modo de medir las distancias entre ejemplos y centros y
las posibles diferencias de escala de los diferentes inputs son aspectos de gran
importancia en el algoritmo de las K medias. Diferentes modos de medir las
distancias proporcionarán clusters con formas diferentes, de modo que la
asignación de los ejemplos a los K grupos puede diferir considerablemente. Y,
si debido a la escala en que se mide, un input presenta valores en un orden de
magnitud superior al del resto de inputs, este tenderá a dominar las distancias
calculadas, haciendo irrelevantes a los demás. Por esto, suele aconsejarse
siempre normalizar todos los inputs para que varíen en el mismo rango, típicamente el intervalo [0,1]. Para atributos numéricos, esto se consigue mediante
la expresión
xi =
x i − min (x i )
,
max (x i ) − min (x i )
como se describe en la sección 4.1.2.1. Para atributos categóricos, en este caso
es más conveniente introducir variables indicadoras o dummy, más apropiadas para el cálculo posterior de los centros de los grupos. Recordemos que el
procedimiento para traducir una variable categórica con c categorías en un
conjunto de c variables indicadoras vi, i = 1,…,c consiste en asignar el valor
vi = 1 cuando la variable categórica toma la i-ésima categoría, y vi = 0 en otro
caso. De este modo, para cada ejemplo, solo una variable indicadora vi valdrá
1, y las c – 1 restantes serán 0. Como un mecanismo general para calcular distancias entre un ejemplo x = (x1 ,..., x n ) y un centro m = (m1 ,..., mn ), se puede
seguir la expresión
n
d (x , m ) =
∑d (x , m )
2
i
i
,
j =1
donde d (x i , mi ) = x i − mi y se asume que n es el número de inputs tras convertir los atributos categóricos en variables indicadoras.
Dada una asignación de los N ejemplos x i = (x1i ,..., x ni ), i = 1,…,N, en K grupos G1 ,...,GK , los centros o medias m j de estos grupos en el espacio de inputs
se calculan mediante la expresión
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
mj =
1
Gj
∑
231
x i , j = 1,…,K,
x i ∈G j
esto es, cada centro de un grupo se obtiene como el promedio de todos los vectores de inputs de los ejemplos asignados a ese grupo. Este paso del algoritmo
de las K medias se conoce como «actualización de centros». Tras ello, el algoritmo recalcula los grupos, asignando cada ejemplo x al grupo Gh con un centro mh más próximo a x, esto es, tal que
d (x , m h ) ≤ d (x , m j )
para todo j = 1,…,K. Este paso se denomina «asignación de grupos». Si algún
ejemplo cambia de grupo en este paso de asignación, es entonces de nuevo
posible actualizar los centros, ya que estos se habrán desplazado al incorporarse o eliminarse ejemplos de sus respectivos grupos. Este proceso de actualización y asignación sucesiva de centros y de grupos se repite hasta que
ningún ejemplo cambie de grupo en el paso de asignación. Cuando esto
sucede, el algoritmo termina y devuelve las asignaciones definitivas de los N
ejemplos en los K grupos.
Hay algunos aspectos importantes más que tener en cuenta. En primer
lugar, este algoritmo de las K medias está diseñado para minimizar la distancia total de los ejemplos a los centros de los grupos en que son asignados. Esto
es, el algoritmo trata de encontrar la asignación de ejemplos a grupos que
minimice la función
K
J (m1 ,..., mk ) = ∑
2
∑
j =1 x i ∈G j
xi − mj
K
= ∑ G j Var (G j )
j =1
o, en otras palabras, se buscan los K grupos cuya varianza intracluster (i. e., la
varianza de los ejemplos de cada grupo) sea mínima. De hecho, tras cada paso
de actualización y asignación se obtienen grupos con cada vez menor varianza.
En segundo lugar, sin embargo, aunque se puede garantizar que el algoritmo
siempre concluirá o convergerá, esto es, alcanzará siempre una configuración
estable de los grupos que no se modifique tras actualizarla, no es posible
garantizar que esta convergencia alcance siempre el mínimo global de la función anterior. Dicho de otro modo, el algoritmo puede quedarse atascado en
un mínimo local, que constituye una configuración estable de los grupos, pero
con una varianza total mayor que la de la mejor configuración estable, la del
mínimo global.
La convergencia a un mínimo local o global depende en buena medida de la
inicialización del algoritmo, esto es, de las asignaciones iniciales de los ejemplos
232
CUADERNOS METODOLÓGICOS 60
en los K grupos. Los métodos de inicialización más extendidos son dos: asignación aleatoria de grupos y selección aleatoria de centroides. En el primero, cada
ejemplo se asigna aleatoriamente a uno de los K grupos, tras lo cual se pone en
marcha el algoritmo a partir del cálculo de centros. En el segundo, se seleccionan K ejemplos al azar y se usan como centros iniciales de los clusters, comenzando entonces el algoritmo por la asignación de ejemplos a grupos con base en
estos centros. El primer método tiende a producir centroides iniciales cercanos
al centro del conjunto E, mientras que el segundo los suele dispersar más, lo que
acostumbra a considerarse una característica favorable. En cualquier caso, dado
que el algoritmo suele ser suficientemente rápido, es habitual ejecutar todo el
procedimiento varias veces, con una inicialización distinta cada vez, de cara a
intentar evitar los mínimos locales o al menos obtener un mínimo local aceptable. Los resultados proporcionados por estas ejecuciones se suelen comparar
con base en los valores obtenidos de la función J anterior, seleccionándose la
asignación de grupos final con un menor valor.
Quizá la mayor contrapartida del algoritmo de las K medias es que obliga a
especificar el número de clusters K que se desea obtener. En la práctica, sin
embargo, no es infrecuente encontrarse con situaciones en que no se tiene
conocimiento a priori, ni siquiera de manera aproximada, del valor K más conveniente. Esto plantea un problema bastante más complejo aun que el de obtener los grupos cuando se asume un valor de K. Existen diversos procedimientos,
como el clustering jerárquico, que afrontan este problema con diferentes estrategias. No obstante, su rendimiento depende, en general, del problema concreto
y de la medida que se utilice para evaluar la calidad de las diversas particiones
en grupos obtenidas. Por ejemplo, con la herramienta del algoritmo de las K
medias es posible intentar la solución obvia, esto es, ejecutar el algoritmo con
diferentes valores de K, pero esto plantea el problema referido de cómo comparar la calidad de particiones con diferente número de grupos. En particular, la
medida J no se puede utilizar directamente ya que esta tiende a decrecer cuando
K aumenta, siendo siempre 0 cuando K = N, esto es, cuando hay tantos grupos
como ejemplos (cada ejemplo forma su propio grupo).
Sin embargo, es posible dar un método intuitivamente sencillo para escoger un número de clusters adecuado a partir de esta función J. Este método,
conocido como método del codo, procede ejecutando el algoritmo para diferentes valores de K, por ejemplo, desde K = 2 hasta 10 o 20, y, a continuación,
representando en un gráfico de línea los valores obtenidos de la función J
frente al número K de grupos. El método, entonces, aconseja seleccionar el
primer valor de K (esto es, el más bajo) para el que el decrecimiento de la función J a partir de ese valor es considerablemente menos pronunciado que los
anteriores. Este punto toma la forma de un codo en el gráfico, de ahí el nombre del método. Para obtener mayor robustez, es posible replicar un número
de veces la ejecución del algoritmo para cada K con una inicialización diferente, de manera que el valor de J que finalmente se representa en el gráfico
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
233
para cada K se obtiene utilizando el mínimo o el promedio de los valores de J
obtenidos en las repeticiones.
Ejemplo práctico
A continuación, se ilustra el uso del algoritmo de clustering K-means con scikit-learn. El conjunto de datos sobre el que se aplicará el algoritmo será el
dataset iris, introducido en secciones anteriores. Como se recordará, este es
un conjunto de datos usado habitualmente para clasificación supervisada. El
motivo de su uso en un contexto de clustering o clasificación no supervisada
es que provee unos datos para los que se conoce el número de grupos que efectivamente forman estos datos, así como la pertenencia de los ejemplos a estos
grupos. Esto permite entonces un modo de valorar el comportamiento de un
algoritmo de clustering, comparando las asignaciones que este realiza de los
ejemplos con las pertenencias reales. Un algoritmo que detecte adecuadamente el número de grupos y que tienda a agrupar los ejemplos de la misma
clase en un mismo grupo separado del resto de clases aporta confianza de cara
a ser aplicado en un contexto en el que se desconocen las pertenencias y estas
se han de asignar de manera no supervisada.
Por ello, en la primera parte de este ejemplo se usará el algoritmo K-means
al modo supervisado, esto es, se realizará una división de los ejemplos en muestras de entrenamiento y test y se obtendrán unos centroides estables aplicando
el algoritmo a los ejemplos de entrenamiento, con los que luego será posible
predecir los ejemplos de test y obtener las medidas de rendimiento habituales
del aprendizaje supervisado. En tanto que el algoritmo no ve las etiquetas de
clase de los ejemplos de entrenamiento, es posible que las etiquetas que asigne
a un grupo no se correspondan con las etiquetas mayoritarias del target en ese
grupo. Esto conllevará el uso de un subprograma o función, que se definirá en
el código, para permutar las asignaciones de etiquetas del K-means a los grupos de modo que se correspondan con la clase mayoritaria de cada grupo. En
la segunda parte de este ejemplo, se ilustrará el método del codo para seleccionar un número adecuado de grupos o clusters en los datos.
El módulo de herramientas de clustering de scikit-learn se llama cluster, el
cual contiene la función KMeans, que implementa el método antes descrito y
que se utilizará en este ejemplo. Los principales parámetros de interés de esta
función son los siguientes:
—n
_clusters: este parámetro especifica el número K de clusters o grupos que formar. Su valor por defecto es 8.
—i
nit: especifica el método de inicialización del algoritmo. El valor
'random' requiere la selección aleatoria de K ejemplos como centroides iniciales.
234
CUADERNOS METODOLÓGICOS 60
—n
_init: número de veces que se repite la ejecución del algoritmo, cada
vez con centroides iniciales diferentes. El resultado global será el de la
ejecución que obtenga un valor más bajo de la función objetivo o de
coste J, la varianza total intraclúster.
—a
lgorithm: especifica la implementación particular del algoritmo que
se ejecuta. El valor 'full' corresponde con el algoritmo descrito en
este manual.
También se usará el parámetro random_state para fijar la semilla aleatoria y producir el mismo resultado en cada ejecución. Aparte, es posible recuperar el valor de la función objetivo J y de los centroides finales mediante los
atributos cluster_centers_ e inertia_ del objeto KMeans entrenado.
Antes de comenzar, hay que recordar que es aconsejable escalar siempre los
inputs de los ejemplos antes de aplicarles el algoritmo. Además, en este primer
ejemplo, el valor de n_clusters se fijará a 3, ya que se conoce el número de
clases del dataset iris. Por otro lado, se hará uso de muestreo estratificado para
seleccionar las muestras de entrenamiento y test con las mismas proporciones
de ejemplos de cada clase que en el dataset inicial. En tanto que en iris se
cuenta con 50 ejemplos de cada una de las clases, esto implicará obtener muestras equilibradas en cuanto a composición de clase. Así pues, ejecutemos este
código para aplicar el algoritmo K-means a la muestra de entrenamiento:
# Importación de módulos y funciones
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
import time
# Carga del dataset iris
iris = load_iris()
X, y = iris.data, iris.target;
# Escalamiento de los inputs
scaler = MinMaxScaler()
scaler.fit(X)
X = scaler.transform(X)
# División del dataset en entrenamiento y test
# nótese la opción stratify
X_entrenamiento, X_test, y_entrenamiento, y_test =
train_test_split(X , y,
test_size=0.4, random_state=13, stratify=y)
# Ajuste del algoritmo con la muestra de entrenamiento
# nótese que y_entrenamiento no interviene en el ajuste
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
235
# del clasificador no supervisado
comienzo=time.process_time()
kmeans = KMeans(n_clusters=3, init='random', n_init=1, random_state=0,
algorithm='full')
kmeans.fit(X_entrenamiento)
print("Duración del proceso:",time.process_time()-comienzo)
print("Centroides:\n%s" % kmeans.cluster_centers_) # centroides
print("Función objetivo:",kmeans.inertia_) # función objetivo
La salida obtenida es la siguiente:
Duración del proceso: 0.005224
Centroides:
[[0.39236111 0.25520833 0.54713983 0.51302083]
[0.19537037 0.58055556 0.08135593 0.06666667]
[0.68948413 0.44642857 0.78026634 0.79613095]]
Función objetivo: 4.539080070990588
Nótese que, incluso para un dataset de tamaño tan reducido como iris, la
ejecución del ajuste es muy rápida. Los centroides de los tres grupos especifican la posición del centro de cada grupo en el espacio de cuatro inputs normalizados, y es posible observar que corresponden a puntos relativamente
alejados entre sí. El valor de la función objetivo es la suma de los cuadrados
de las distancias de los ejemplos a los centros de sus grupos.
Es difícil dar una referencia absoluta para juzgar el valor de la función
objetivo alcanzado. Sin embargo, como se advertía más arriba, este corresponde normalmente con el de un mínimo local, y la calidad de este mínimo
puede depender de los centroides iniciales seleccionados aleatoriamente.
Por esto es aconsejable repetir un número de veces la ejecución del algoritmo con inicializaciones diferentes, y elegir el resultado que produzca un
menor valor de la función objetivo. Repitiendo la ejecución del código anterior con n_init=10 se obtiene el siguiente resultado:
Duración del proceso: 0.046875
Centroides:
[[0.72222222 0.45108696 0.81208548 0.82427536]
[0.19537037 0.58055556 0.08135593 0.06666667]
[0.41216216 0.27815315 0.55886395 0.53378378]]
Función objetivo: 4.517957000923974
236
CUADERNOS METODOLÓGICOS 60
La ejecución de estas diez repeticiones ha permitido encontrar un mínimo
local algo mejor, aunque la diferencia no es muy notable en este caso. Por otro
lado, obsérvese que los centroides parecen haber permutado su orden: el primero de la última ejecución se parece al último de la anterior, el segundo es el
mismo en ambos casos, y el tercero es similar al primero anterior.
Este último hecho plantea un problema de cara a la evaluación de modo
supervisado de las asignaciones, comparándolas con las etiquetas conocidas
de los ejemplos: la etiqueta asignada por el K-means a los grupos de ejemplos
que obtiene depende del orden en que fueron seleccionados los centroides iniciales, que luego han derivado en los centros de los grupos finales. Por ejemplo, como se desprende de la permutación del orden de los centroides de la
primera ejecución a la segunda, el grupo al que antes se le asignaba la etiqueta 0
(primer grupo) ahora tiene asignada la etiqueta 2 (tercer grupo). Y estas etiquetas no tienen, por supuesto, por qué corresponder con las etiquetas de
clase real de la mayoría de ejemplos de ese grupo, como se puede comprobar
computando las asignaciones de los ejemplos de la muestra de entrenamiento
y obteniendo la matriz de confusión correspondiente:
from sklearn import metrics
predichos=kmeans.predict(X_entrenamiento)
print("Matriz de confusión:\n%s"
% metrics.confusion_matrix(y_entrenamiento, predichos))
Matriz de confusión:
[[ 0 30 0]
[ 1 0 29]
[22 0 8]]
Claramente, todos los ejemplos de la primera clase, la especie iris setosa,
que tiene la etiqueta 0 en el dataset original, están siendo asignados al grupo
con etiqueta 1. La mayor parte de los ejemplos de la clase 1 están siendo asignados a la etiqueta 2, y, finalmente, los de la clase 2, a la etiqueta 0. Así pues,
para poder comparar las asignaciones del algoritmo con las etiquetas de clase
conocidas, es necesario intercambiar las etiquetas asignadas a los grupos para
que se correspondan con la de la clase mayoritaria en ese grupo.
Esto se llevará a cabo programando un subprograma o función, que será
llamado desde el programa principal y que realizará este intercambio de etiquetas en las asignaciones del K-means con base en la composición de clase
de cada grupo. Este procedimiento ahorra tener que escribir el código que
realiza la función cada vez que se quiera intercambiar etiquetas. Básicamente,
esta función ha de calcular la matriz de confusión anterior, a partir de la cual
establece la etiqueta de clase mayoritaria en cada grupo c, y asigna esta
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
237
etiqueta de clase a todos los ejemplos asignados en c. La definición de la función cambia_etiquetas se realiza ejecutando el siguiente código:
# Esta función toma las etiquetas de clase reales (obs)
# y predichas (pred) y realiza una permutación de las últimas
# para que correspondan con la clase real mayoritaria
# en cada etiqueta predicha
import numpy as np
def cambia_etiquetas(obs,pred):
N=obs.shape[0] # Número de ejemplos
C=pred.max() # Número de clases y grupos
# Obtención de la matriz de confusión
matriz=np.zeros((C+1,C+1))
for i in range(N):
matriz[obs[i],pred[i]]+=1
# Intercambio de etiquetas
aux=pred.copy()
for c in range(C+1):
aux[pred==c]=matriz.argmax(axis=0)[c]
return aux # Output de la función
Ahora es posible llamar a esta función para que realice los intercambios de
etiquetas en las asignaciones proporcionadas por el K-means:
predichos = cambia_etiquetas(y_entrenamiento, predichos)
print("Matriz de confusión:\n%s"
% metrics.confusion_matrix(y_entrenamiento, predichos))
print("Tasa de acierto:", metrics.accuracy_score(y_entrenamiento,predichos))
Matriz de confusión:
[[30 0 0]
[ 0 29 1]
[ 0 8 22]]
Tasa de acierto: 0.9
Así pues, tras intercambiar las etiquetas se tiene que el algoritmo K-means
es capaz de aprender a pronosticar la clase real de un 90% de los ejemplos de
la muestra de entrenamiento, sabiendo solo el número correcto de clases,
pero sin haber visto en ningún momento los valores target de los ejemplos.
Esto da una muestra de las capacidades del aprendizaje no supervisado, que
puede proveer mecanismos para la predicción relativamente exitosa de un
238
CUADERNOS METODOLÓGICOS 60
target sin necesidad de pasar por el a menudo lento y costoso proceso de la
supervisión.
No obstante, aún nos falta comprobar si este rendimiento se mantiene
sobre ejemplos no vistos en el ajuste del clasificador. Prediciendo la etiqueta
de los ejemplos de la muestra de test, y tras realizar el correspondiente intercambio
predichos=kmeans.predict(X_test)
predichos=cambia_etiquetas(y_test,predichos)
print("Matriz de confusión:\n%s"
% metrics.confusion_matrix(y_test, predichos))
print("Tasa de acierto:", metrics.accuracy_score(y_test,predichos))
se obtiene
Matriz de confusión:
[[20 0 0]
[ 0 17 3]
[ 0 6 14]]
Tasa de acierto: 0.85
Este rendimiento del 85% en test sigue siendo una tasa relativamente elevada para un mecanismo de aprendizaje no supervisado. En este mismo
dataset iris (y con las mismas divisiones en entrenamiento y test), los procedimientos supervisados descritos en secciones anteriores lograban unas
tasas de acierto algo superiores al 95%. Este margen de un 10% entre unos
procedimientos y otros podría ser crucial en algunas aplicaciones y compensar la necesidad de supervisión de los ejemplos para obtener un clasificador
más preciso. En otros contextos, sin embargo, esta diferencia podría no ser
tan relevante, y, en este sentido, un clasificador no supervisado podría permitir un ahorro importante de esfuerzo y dinero al permitir la supervisión
automática de ejemplos y/o su clasificación sin supervisión.
En el ejemplo anterior hemos usado n_clusters=3 en todas las ejecuciones en tanto que el número de grupos en que se dividen los datos era conocido.
Esto, por supuesto, no es lo habitual en un marco de clustering. Típicamente,
el número adecuado de grupos K es desconocido; para empezar, porque podría
no existir un único valor correcto. La raíz del problema es que no existe una
única manera de entender lo que debe ser un grupo o clúster, por lo que distintos observadores o criterios podrían especificar números de clúster diferentes,
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
239
y hasta cierto punto cada uno podría tener su parte de razón. No obstante, hay
algunas consideraciones que pueden servir de ayuda a la hora de seleccionar el
número de grupos K en que se han de dividir los datos.
Una de ellas es la que basa el método del codo que se ilustrará a continuación. La idea de este método es que la reducción de varianza intraclúster al
aumentar K debe ser pronunciada hasta llegar a la configuración adecuada, y
suave o menos pronunciada a partir de ese punto. Esto es, un número adecuado de clusters debería permitir mejorar considerablemente las varianzas
obtenidas al segmentar menos grupos de los necesarios, ya que en este caso al
menos dos grupos diferentes tendrían que compartir un mismo centro, que
estaría artificialmente situado entre ambos grupos, propiciando la aparición
de una varianza elevada. Similarmente, al especificar más grupos de los necesarios, algún clúster tendría que contener más de un centro, lo que conllevaría
una reducción de varianza escasa respecto a la configuración de ese grupo con
un solo centroide, en tanto que los ejemplos de ese grupo ya se encontrarían
en ese caso relativamente próximos a este.
En la práctica, este método no se implementa mediante herramientas formales sino a través de un análisis gráfico, como, por ejemplo, sucede en el análisis
de residuos de un modelo de regresión lineal. No es sencillo formalizar de
manera precisa y válida para muy diversas situaciones lo que significa un descenso pronunciado o suave de la varianza intraclúster. Sin embargo, es bastante
más fácil e intuitivo detectar este cambio de patrón en la reducción de varianza
observando la curva de valores de varianza total obtenidos para diferentes valores de K. La forma habitual de esta curva es la de un brazo semi-flexionado, más
escarpada al comienzo y más llana al final, de modo que el cambio de patrón se
asocia con un valor de K en el que la curva parece tener un codo o ángulo menos
obtuso o abierto. Ese valor de K es entonces el candidato propuesto por este
método para el número adecuado de grupos que separar en los datos.
Ilustremos cómo implementar este método del codo en Python usando la
función KMeans de scikit-learn. La tarea consiste, por tanto, en obtener un gráfico con una curva uniendo los valores J(K) de la función objetivo del algoritmo
K-means alcanzados con diferentes K. Como se ha apuntado más arriba, estos
valores de la función objetivo están disponibles en el atributo inertia­_ del
objeto que produce la función KMeans al ajustarse. En general, este valor J(K)
suele calcularse para todos los K entre 1 y un número K_máx que marca el
máximo de grupos que considerar. Así pues, para cada K entre 1 y K_máx se ha
de aplicar el algoritmo K-means a los datos usando n_clusters=K, y almacenar el valor J(K) obtenido. Nótese que ahora es aconsejable usar todo el dataset
en el ajuste, sin particionar en entrenamiento y test, en tanto que ya no se precisa
estimar un rendimiento a posteriori con datos no vistos, sino que se pretende
encontrar la mejor configuración de grupos en ese dataset. También es aconsejable repetir un número de veces la ejecución con cada K, digamos n_init=10
veces, de cara a evitar que una posible inicialización desafortunada de los
240
CUADERNOS METODOLÓGICOS 60
centroides conduzca a valores J(K) que enmascaren el patrón real de reducción
de la varianza en ese K. El siguiente código lleva a cabo el gráfico requerido.
import matplotlib.pyplot as plt
K_max=10
J=[]
for K in range(1,K_max+1):
kmeans = KMeans(n_clusters=K, init='random', n_init=10,
random_state=0, algorithm='full')
kmeans.fit(X)
J.append(kmeans.inertia_)
plt.figure()
plt.title("Seleccción de K mediante el método del codo")
plt.plot(range(1, K_max+1), J, color='darkorange',
lw=2, label=' Función objetivo J(K)')
plt.xticks(range(1, K_max +1), range(1, K_max +1), size=10)
plt.xlim([0.5, K_max +0.5])
plt.ylim([0.0, max(J)+1])
plt.xlabel('Valores de K')
plt.ylabel('Valores de J')
plt.legend(loc="lower right")
plt.show()
La curva obtenida se muestra en la figura 4.20. La reducción de varianza al
pasar de uno a dos grupos es muy pronunciada, y algo menos pronunciada
al pasar de dos a tres. A partir de K = 3 la reducción en valores sucesivos es bastante suave. La idea del codo apunta a K = 2, o quizá K = 3, como posibles mejores elecciones para el número de grupos en los datos. En realidad, ambas son
buenas elecciones: de las tres clases o especies de las que consta el dataset iris,
una se encuentra muy claramente separada del resto en el espacio de inputs,
pero las otras dos presentan cierto solapamiento, sin una frontera clara entre
ambas. Por ello, puede decirse que desde esta perspectiva este conjunto de datos
contiene solo dos grupos, el formado por la clase separada y el formado por la
unión de las otras dos clases. Sin embargo, en tanto que estas clases solapadas
no se encuentran realmente mezcladas, los ejemplos en los extremos opuestos
de estas clases tienden a quedar alejados del centro común, y por ello también se
registra una reducción de varianza significativa al pasar de dos a tres grupos.
Luego también es posible identificar tres grupos en los datos, más o menos asociados, respectivamente, a las tres clases de especies. En cualquier caso, lo que
sí parece claro es que no se ha de considerar un número de grupos mayor a tres,
ya que la introducción de nuevos centroides no propicia nunca una mejora sustancial de la función objetivo, y la reducción paulatina de la función J que se
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
241
observa es achacable solamente a la monotonicidad de esta respecto a K, pero no
a la existencia de un mayor número de grupos en los datos.
Figura 4.20.
Figura del método del codo para la selección del número
de grupos que utilizar con el algoritmo K-means en el dataset iris
4.2. Análisis de redes sociales
4.2.1. Introducción al concepto de análisis de redes sociales
Uno de los primeros puntos que debe dejarse claro en cualquier memoria,
libro o manual dentro de la categoría de «análisis de redes sociales» o, en su
traducción natural al inglés, social network analysis, es el error que comúnmente se comete al tratar de identificar el concepto coloquial de «red social»
por el tipo de problemas que se resuelven dentro del marco de investigación
conocido como análisis de redes sociales o social network analysis (SNA).
En términos coloquiales, el concepto de «red social» (por ejemplo, Kadushin, 2013) tiende a asociarse a una plataforma digital en la que los actores
principales o internautas intercambian información personal y/o contenidos
multimedia de modo que crean una comunidad de amigos virtual e interactiva. Algunos ejemplos de este tipo de plataformas serían Facebook, LinkedIn,
Instagram o Twitter, entre muchas otras. Obviamente, este tipo de redes sociales son, por definición, objeto de la sociología, en tanto en cuanto, a través de
ellas, se analizan la naturaleza y características de las relaciones humanas. De
esta forma, las redes sociales, y, por ende, las relaciones entre agentes, pueden
242
CUADERNOS METODOLÓGICOS 60
obedecer a diversos patrones como la reciprocidad, la propincuidad o la
homofilia, y pueden adoptar distintas formas, como una diada, una triada u
otros tipos de redes humanas más complejas.
Sin embargo, ¿es esto lo que queremos decir cuando nos referimos a la
línea de investigación de análisis de redes sociales o a cualquiera de sus técnicas? Claramente no (o, al menos, no únicamente). Si bien es cierto que muchos
de los problemas asociados al tratamiento de la información recogida a partir
de una red social entrarían dentro de la categoría de SNA, el término/línea de
investigación análisis de redes sociales es algo mucho más amplio. Existen
multitud de problemas, técnicas y trabajos dentro del análisis de redes sociales que jamás trabajan con datos obtenidos de una red social (de las anteriormente mencionadas) ni guardan ninguna relación con ellas.
Entonces, ¿a que nos referimos cuando hablamos de análisis de redes sociales? Trataremos de dar a continuación una respuesta a esta pregunta desde el
prisma del análisis de datos (Data Science). El término análisis de redes sociales alude, pues, a la naturaleza y estructura de los datos. Cuando uno piensa
en una base de datos «tradicional», es común tener en mente un conjunto de
observaciones estructuradas en forma de tabla de manera que, asociadas a
cada observación, tendríamos descritas una serie de características. Por ejemplo, si el objeto de estudio son los hogares madrileños y el estudio es de carácter de consumo energético, cada una de las observaciones correspondería a
un hogar y asociadas a cada observación/hogar tendríamos una serie de características, como número de residentes, consumo de electricidad, consumo de
gas o localización, entre otras. Es importante mencionar que, tanto en la recogida de datos como en su posterior tratamiento clásico, cada uno de estos
hogares será tratado como una unidad independiente de las demás, sin tener
en cuenta, por ejemplo, las posibles relaciones de parentesco (o de cualquier
tipo) que existen entre las observaciones (por ejemplo, el hogar x es familia
del hogar y). Como se verá más adelante, existen muchos problemas tradicionales de estadística (clustering, muestreo, estimación entre otros) que únicamente serán abordados de manera adecuada si se tienen en cuenta estas
relaciones, ya que la predicción de variables como compañía de gas que se utiliza o la afinidad política son más fáciles de explicar por las relaciones entre
las observaciones que por las variables/características internas de cada una de
ellas. Este punto es relevante para tratar de clarificar qué se entiende por análisis de redes sociales y el motivo por el cual este tipo de problemas ha cambiado el paradigma de la independencia y el muestreo en muchos de los
problemas de análisis de datos tradicionales.
Sin ningún lugar a duda, uno de los pilares e hipótesis más utilizados en
los problemas de inferencia estadística es el concepto de independencia entre
las observaciones/datos recogidos. Cuando se tiene un conjunto de datos «clásico» es habitual asumir que se dispone de una serie de observaciones y para
cada una de estas observaciones se tienen asociadas una serie de variables
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
243
(características). Esta información suele estar representada por medio de una
matriz/tabla n x m, donde n es el número de observaciones y m, el número de
variables. Las observaciones que se han recogido se asumen independientes
entre sí por el proceso muestral de recogida de información que se ha seguido
y que es la base de la inferencia estadística clásica. Por este motivo, cuando se
analizan las variables por separado (o de manera conjunta) se tiene lo que se
conoce como muestra aleatoria simple.
No obstante, existen muchas situaciones en las que la independencia entre
observaciones no se produce, bien porque la información se está recogiendo
de manera dependiente de una fuente de datos, o bien porque el analista desea
analizar precisamente las relaciones que existen entre dichas observaciones.
Supongamos, por ejemplo, que se desea agrupar o clasificar una población
grande de libros (que en este caso serían las observaciones) según su parecido.
Los problemas de clustering o agrupamiento en estadística son muy habituales para un correcto tratamiento descriptivo de la información. Una aproximación clásica a este problema construiría, en primer lugar, una base de
datos con muchos libros escogidos aleatoriamente y para cada libro se tendrían en cuenta muchas variables. Una vez construida la base de datos tradicional (que es a lo que nos referíamos antes como una tabla de datos de n
libros con m características, n x m), el problema sería establecer clusters de
libros comunes entre sí. Este problema ha sido tradicionalmente difícil de
resolver en estadística pues es complicado cuantificar si dos libros se parecen
entre sí con base en ciertas características como tamaño, número de páginas,
editorial, temática, etc. Esta situación es descrita con detalle en Krebs (2004)
con un ejemplo conocido como political book network. En 2002 la empresa
Amazon quiso realizar un clustering de libros que hablaban sobre un mismo
tema, «la guerra de Irak», como primer paso para establecer posteriormente
un sistema de recomendación de libros a sus clientes. Por tanto, el problema
que se pretendía abordar era el de agrupar (clustering) libros «parecidos entre
sí» para establecer grupos de libros similares.
En primer lugar, el asunto del clustering de libros se realizó de una manera
tradicional, con muy malos resultados. Después de un estudio más pausado la
empresa se percató de que la información realmente relevante para realizar el
clustering de libros no eran las características intrínsecas a los libros sino las
relaciones que existían entre ellos (su red de relaciones). Llegados a este punto,
se establecieron relaciones (se generaron enlaces) entre aquellos libros que
habían sido comprados (por Internet y través de la plataforma Amazon) por un
número significativo de clientes y se modelizo la estructura de datos mediante
un grafo. La información que antes había sido almacenada mediante una tabla
n x m pasaba ser ahora una matriz de relaciones entre objetos (n x n) o, como
se conoce en matemáticas, un grafo. Una vez construido el grafo de relaciones
entre libros, se aplicó un algoritmo típico en problemas de análisis de redes
sociales (algoritmo de detección de comunidades en redes), obviando cualquier
244
CUADERNOS METODOLÓGICOS 60
característica asociada al libro para detectar grupos de nodos fuertemente
conexos entre sí y poco conexos con las otras comunidades. El resultado fue
francamente bueno y permitió establecer tres grupos/comunidades claramente
diferenciados (figura 4.21) según la afinidad política del momento: libros con
tendencia republicana, libros con un perfil demócrata y libros que podrían considerarse neutrales ante la guerra.
Figura 4.21.
Comunidades según afinidad política
De este ejemplo se puede llegar a la conclusión de que existen muchos problemas tradicionales de estadística o, en términos generales, problemas de
tratamiento y análisis de la información en los que el análisis de las relaciones
que existen entre las unidades de información (libros en este caso) revelan
información de gran interés o incluso son más relevantes que la información
intrínseca asociada a los objetos del análisis. Es el compendio de técnicas,
modelización y problemas que investigan las relaciones, enlaces, contactos,
pautas relacionales y estructuras lo que se conoce como análisis de redes
sociales (ARS) o social network analysis (SNA). Por supuesto, estas relaciones,
estructuras o redes pueden ser redes de trasporte, redes biológicas y redes de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
245
comunicaciones, o también podrían ser redes sociales en los términos que
describíamos al principio de este capítulo.
Pongamos otro ejemplo (ilustrado con mayor detalle en Wasserman y
Faust, 1994) para poner de manifiesto con mayor claridad las diferencias entre
un análisis clásico y un análisis vía redes sociales. Supongamos que estamos
interesados ​​en el estudio del comportamiento corporativo en un área metropolitana grande. Más concretamente, estamos interesados en conocer los tipos de
apoyo económico privado a organizaciones locales sin ánimo de lucro. En el
marco de un análisis clásico del problema se definiría, en primer lugar, una
población de relevancia (empresas), se recogería una muestra aleatoria de ellas
(si la población es bastante grande), y, a continuación, se medirían una serie
de características asociadas a cada observación (tamaño, industria, rentabilidad, nivel de apoyo para organizaciones sin fines de lucro, etc.). La suposición
clave aquí es que el comportamiento de una unidad específica tiene una repercusión significativa en otras unidades. Por ejemplo, las empresas del sector
turístico sobre el desarrollo de organizaciones sin ánimo de lucro que luchen
contra la exclusión social. Sin embargo, un análisis de redes sociales ahondaría específicamente en las relaciones que existen entre dichas empresas para
detectar pautas y patrones de comportamiento similares.
Desde la óptica del análisis de redes sociales, lo más relevante es identificar
y analizar lo que se conoce como un grafo o una red (network). En dicho
grafo, los nodos son las observaciones (que son la fuente de estudio) y las aristas o enlaces son las relaciones que existen entre dichos nodos. Así pues, en
términos generales, el análisis de redes sociales se centra en las medidas y
aproximaciones metodológicas para el estudio de las relaciones entre objetos,
ya sean personas, organizaciones, libros, palabras o cualquier otra cosa.
Como se ha mencionado antes, los problemas de redes sociales no atañen
únicamente a problemas derivados de lo que coloquialmente se entiende
como «red social», aunque es importante señalar que las redes sociales son
una fuente natural de información para el análisis de redes sociales. Otros
ejemplos de estructuras sociales comúnmente analizadas desde el prisma
de las redes sociales son las redes biológicas, redes de trasporte o sistemas de
recomendación.
Antes de concluir este apartado introductorio y con el objetivo de marcar
el devenir de los siguientes apartados asociados al SNA, es importante mencionar que dentro del análisis de redes sociales existen multitud de problemas
interesantes (una vez realizado el paso del análisis topológico de la red) y
desde luego sería pretencioso abordar todos ellos en un libro de estas características. Existen muchos artículos, y referencias sobre los principales problemas dentro del análisis de redes, o, al menos, sobre los que más se han
trabajado durante los últimos años. Probablemente las preguntas (de las que
han derivado muchos de los problemas de análisis de redes sociales) que tratan de responder los analistas de redes sociales serían las siguientes:
246
CUADERNOS METODOLÓGICOS 60
—¿Es posible entender/predecir y deducir cómo evoluciona una red a lo
largo del tiempo?
Tratar de clasificar la red para poder entender cómo se ha generado y
cómo podría evolucionar es una de las tareas más habituales y más relevantes en SNA. A continuación, se mencionan algunos tópicos relacionados que pretenden dar respuesta a esta cuestión, que serían, entre
otros, los modelos de generación de redes, redes de pequeño mundo,
redes libres de escala, y modelos aleatorios, de los que hablaremos en la
sección 4.2.1.1, de conceptos básicos y generación de redes.
Es importante mencionar que antes de dar respuesta a esta pregunta u
otras es necesario realizar un «análisis topológico de la red» (sección
4.2.1.4) que permita tener las principales características de la red que
estamos analizando y poder, así, contar con una idea de si nuestra red es
aleatoria, de pequeño mundo o libre de escala, lo que, a su vez, permite
dar respuesta a la pregunta de cómo evoluciona la red y cómo se ha llegado a esta situación. Por poner un ejemplo, existen muchas redes reales
que siguen un comportamiento de enlace preferencial (Barbasi et al.,
1999). Esto quiere decir que a partir de un instante las relaciones de los
nuevos nodos que se incorporan al modelo siguen un modelo preferencial en el que tienden a conectarse con aquellos nodos que tienen más
relaciones en lugar de con los que tienen menos. Estas redes que evolucionan de esta manera son detectables con algunas de las características
que se han definido anteriormente, como el camino medio o el coeficiente de agrupación, entre otros. Esto podría responder a esta pregunta.
—¿Es posible entender/predecir y deducir cómo se forman comunidades
dentro de una red?
El clustering en estadística es uno de los mecanismos más habituales
para poder manejar información compleja y entenderla correctamente.
Agrupar observaciones o conjuntos de objetos según su parecido es una
de las tareas mas repetidas por la mente humana para poder entender
un problema, o tomar decisiones correctamente. Al equivalente al problema de clustering en redes se le llama detección de comunidades y el
objetivo fundamental es el de agrupar/clasificar nodos simialres entre sí.
Un grupo o una comunidad dentro de una red será un conjunto de
nodos en el que las conexiones entre elementos de esa comunidad son
más fuertes que con nodos de otras comunidades. Algunos tópicos relacionados que pretenden dar respuesta a esta cuestión, que serían, entre
otros, problemas de detección de comunidades, medidas de cohesividad, etc.
—¿Podemos entender/predecir cómo se difunde la información a lo largo
de una red?
Algunos tópicos que pretenden dar respuesta a esta cuestión serían, entre
otros, modelos de transmisión de información, modelos SIR, modelos de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
247
transmisión de enfermedades, cadenas de Márkov, medidas de centralidad, etc.
—¿Es posible identificar los principales actores (más poderosos y con más
influencia social en los demás) dentro de una red?
Entre algunos de los tópicos que pretenden dar respuesta a esta cuestión estarían las medidas de centralidad.
—¿Cómo podemos visualizar de la mejor manera una red compleja?
Aunque muchas de estas preguntas están relacionadas, el tratar de dar
respuesta por separado ha dado lugar a diferentes problemas/tópicos en
redes sociales. El estudio de estos problemas ha permitido responder
totalmente o parcialmente a algunas de las preguntas que aquí se mencionan y ha dado lugar a numerosas investigaciones durante los últimos
veinte años.
En este libro se analizarán algunos de los más conocidos que pueden
tener especial relevancia dentro de una investigación sociológica en entorno
big data, dejando, por motivos obvios, otros de ellos para otra memoria. En
particular, en este libro nos centraremos en el problema del análisis de la
importancia de los nodos dentro de una red y los problemas de detección de
comunidades.
4.2.1.1. Conceptos básicos. Grafos y dígrafos y generación de redes
Teniendo en cuenta todo esto, a continuación, se definen algunos conceptos
básicos para entender dicho análisis, así como algunos de los problemas
que se suelen resolver en el análisis de redes sociales, enfatizando aquellos
modelos y métodos que pueden tener un especial interés para nuestra disciplina.
El concepto de grafo
En matemáticas, un grafo es un conjunto de objetos llamados vértices o nodos
que están relacionados por aristas o arcos. Matemáticamente un grafo G es un
par ordenado G=(V,E), donde V representa el conjunto de vértices y E representa el conjunto de aristas. Si las aristas son direccionales o están dirigidas,
al conjunto de aristas se le llama arcos y al grafo se le llama dígrafo o grafo
dirigido.
Por ejemplo, el siguiente grafo G=(V,E) tiene cuatro nodos V={1,2,3,4} y
cuatro relaciones E={{1,2}{2,3}{3,4}{13}}. Gráficamente podemos representar
el grafo G de la siguiente forma.
248
CUADERNOS METODOLÓGICOS 60
Figura 4.22.
Grafo de cuatro nodos con cuatro aristas
Antes de empezar a trabajar con Phyton y la teoría de grafos vamos a definir algunos conceptos básicos en grafos, como camino, componente conexa y
árboles, entre otros.
Definición. Dado un grafo G = (V,E), llamamos camino ≠ ij de longitud k
entre dos nodos i y j de V a una secuencia de vértices dentro de V tal que exista
una arista entre cada vértice y el siguiente.
Formalmente π ij = ( i0 = i , i1 ,… i k = j ) es una secuencia de vértices donde
( il , il +1 ) ∈ E ∀ l = 0,… k − 1 .
Se dice que dos vértices están conectados si existe un camino que vaya de
uno a otro, de lo contrario, estarán desconectados.
Definición. Sea G = (V, E) un grafo no dirigido. G se denomina conexo si
existe un camino entre cualesquiera dos vértices distintos de G.
Definición. Sea G = (V, E) un grafo dirigido. Su grafo no dirigido asociado
es el obtenido de G ignorando la dirección de las aristas. G se considera conexo
si lo es el grafo asociado.
Definición. Un grafo que no sea conexo se denomina no conexo. Un grafo
no conexo puede partirse en subpartes maximales que son conexas y que llamaremos componentes conexas. Un grafo es conexo si y solo si tiene una
única componente conexa.
Ejemplo práctico
Como se ha hecho a lo largo de todo el libro, y con el objetivo de que el lector
pueda realizar algunas de las tareas relacionadas con la teoría de grafos y el
análisis de redes sociales, detallamos a continuación en código Phyton cómo
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
249
generar un grafo y cómo visualizarlo (véase, por ejemplo, Pedregosa et al.
[2011], si se quiere profundizar más en el uso del software Phyton para este
tipo de problemas).
Para poder trabajar con algunas de las técnicas clásicas en el análisis de
redes con el lenguaje de programación Phyton es recomendable instalar la
librería NetworkX. Esta libreria de Phyton permite la creación, manipulación y estudio de las estructuras, dinámicas y funciones de redes complejas.
En general, esta librería se ha diseñado con el fin de poder ayudar al estudio
de dinámicas, entre otras, de interacción social. NetworkX está basada totalmente en Python, por lo que podremos modelar nuestros algoritmos de una
manera más intuitiva con resultados inmediatos. A continuación, se muestra un ejemplo de cómo crear un grafo sencillo (el del ejemplo anterior) de
cuatro nodos con cuatro aristas ponderadas. Igualmente, aprenderemos a
visualizarlo. A continuación, se muestra un ejemplo en Phyton en el que a
partir de un grafo vacío se van añadiendo aristas y nodos hasta su construcción final.
>>>
>>>
>>>
>>>
>>>
>>>
>>>
>>>
>>>
import NetworkX as nx
import matplotlib.pyplot as plt
g = nx.Graph()
g.add_edge('a','b', weight=0.1)
g.add_edge('b','c', weight=1.5)
g.add_edge('a','c', weight=1.0)
g.add_edge('c','d', weight=2.2)
nx.draw_circular(g, ax=plt.axes((.3, .01+.25*2, .3, .22)))
plt.show()
Figura 4.23.
Grafo resultante al aplicar el anterior código
250
CUADERNOS METODOLÓGICOS 60
Una vez definido un grafo como hemos hecho en el caso anterior, Phyton
permite modificarlo de muchas maneras diferentes. Por ejemplo, añadiendo
aristas (como se muestra en el código anterior) o nodos. Esta flexibilidad
sobre las operaciones que permiten modificar un grafo hace muy popular el
uso de este software cuando se trata de analizar redes sociales. A continuación, mostramos más ejemplos sobre el manejo de grafos.
Supongamos que tenemos un grafo (que lo denotamos por G) cualesquiera
ya definido en Phyton (ya sea porque lo hemos leído de un formato específico,
o bien porque lo hemos generado aleatoriamente, o bien porque se ha introducido manualmente como en el ejemplo anterior). Podemos agregar un nodo
(en este caso el nodo 1) al grafo G con la sentencia G.add_node.
>>> G.add_node (1)
O bien agregar una lista de nodos,
>>> G.add_nodes_from ([2,3])
o agregar otro conjunto de nodos como, por ejemplo, una lista, un conjunto,
un gráfico, un archivo, etc.
>>> H = nx.path_graph (10)
>>> G.add_nodes_from (H)
La primera sentencia construye un grafo H que es un camino de diez
nodos. Mientras, la segunda sentencia considera ese conjunto de nodos como
un «supernodo» del grafo G. Es decir, cualquier pieza de información puede
ser un nodo. Obsérvese que esta flexibilidad es muy poderosa, ya que permite
generar grafos de grafos, grafos de archivos, grafos de funciones entre muchos
tipos de grafos.
Para finalizar esta apartado, es importante señalar que, además de la gran
cantidad de formatos sobre grafos que lee Phyton, existe una gran cantidad de
grafos definidos en el sistema de los que se puede «tirar» si fuese necesario.
Estos grafos que tiene Phyton definidos en su sistema interno pueden ser de
naturaleza determinista o pueden ser grafos generados aleatoriamente con
alguno de los modelos clásicos de simulación de redes (como los modelos de
Erdos, de pequeño mundo o de Barabasi, entre muchos otros).
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
251
Los grafos deterministas más conocidos que pueden generarse automáticamente en Phyton son grafos completos, cadenas, grafos circulares y grafos bipartitos, entre muchos otros. A continuación, se muestran algunos ejemplos.
>>> K_5=nx.complete_graph(5)
De esta manera generamos el grafo completo determinista de cinco nodos.
Figura 4.24.
Grafo resultante al aplicar el anterior código
>>> K_3_5=nx.complete_bipartite_graph(3,5)
Figura 4.25.
Grafo resultante al aplicar el anterior código
252
CUADERNOS METODOLÓGICOS 60
De esta manera generamos un grafo bipartito donde el primer conjunto
tiene tres nodos y el segundo tiene cinco. Un grafo G=(V,E) se dice bipartito
si sus vértices V se pueden separar en dos conjuntos disjuntos A y B, de tal
manera que se cumple lo siguiente: las aristas de E solo pueden conectar
vértices de A con vértices de B (es decir, no existen aristas que conecten dos
nodos de A o dos nodos de B). Los grafos bipartitos son especialmente interesantes en estudios sociológicos ya que, naturalmente, representan relaciones entre dos diferentes clases de objetos. Pongamos, por ejemplo, que
se desea modelizar la red de Twitter con dos clases diferenciadas: los tuiteros, por un lado, y a la compañía que representan, por otro. Los nodos de
este grafo serán claramente de dos tipos (personas y empresas), estableciéndose un enlace entre una persona y una empresa si dicha persona trabaja para esa empresa.
A continuación, se muestra cómo generar otras redes deterministas (estructuras fijas) bien conocidas, como el barbell graph (comunidades fuertemente
conexas unidas por una cadena) o el lolipop graph (una comunidad fuertemente conexa unida a una cadena). Mediante la primera sentencia barbell_
graph(10,10) generamos un grafo que tiene dos comunidades de tamaño
10 (el primer input) unidas por una camino de longitud 10. Es posible representar las dos comunidades previamente descritas de dos maneras diferentes
(con una representación circular y con otra estándard).
>>> barbell=nx.barbell_graph(10,10)
>>> nx.draw_circular(barbell, with_labels=True)
>>> nx.draw (barbell, with_labels=True)
Figura 4.26.
Grafo resultante al aplicar el anterior código
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
253
La próxima sentencia permite generar un grafo lollipop formado por una
comunidad de tamaño 10 y un camino de longitud 20.
>>> barbell=nx.barbell_graph(10,10)
>>> nx.draw_circular(barbell, with_labels=True)
>>> nx.draw (barbell, with_labels=True)
Figura 4.27.
Grafo resultante al aplicar el anterior código
Estos ejemplos que se han mostrado anteriormente permiten generar grafos deterministas conocidos de diferentes tamaños.
De manera similar, es posible generar grafos/redes aleatorios usando los
métodos de generación aleatorios más conocidos dentro del ámbito de las
redes sociales. Aunque existen otros métodos para generar redes «reales» no
deterministas, los tres modelos que presentamos muy brevemente a continuación responden a los tres escenarios más estudiados en el marco de las
redes sociales, que son redes completamente aleatorias (son redes en las que
las conexiones no guardan ningún tipo de patrón y se utilizan como punto
de partida en la generación de otros tipos de redes), las redes de pequeño
mundo (son redes que simulan muchas situaciones reales relacionadas con
el experimento de «seis grados de separación» entre otras), y redes libres de
escala con conexión preferencial (son un tipo de redes que simulan las situaciones en las que los nodos tienden a conectarse con mayor probabilidad a
nodos líderes).
Mostramos, a continuación, tres ejemplos de cómo generarlos con el software Phyton:
254
CUADERNOS METODOLÓGICOS 60
— Redes aleatorias: modelo de Erdos-Renyi (1960).
— Redes de pequeño mundo: modelo de Wats-Strogaz (véase Newman y
Wats, 2009).
— Redes libres de escala: modelo Barabási-Albert de conexión preferencial.
Empezando con las redes aleatorias generamos a continuación un modelo
de Erdos-Renyi de diez nodos con probabilidad de conexión de 0,15. El
modelo de Erdos-Renyi tiene la particularidad de que la probabilidad de que
dos nodos se conecten en la red es la misma. Es decir, cuando se está generando la red la probabilidad de establecer un enlace entre dos nodos cualesquiera es la misma. En ese sentido, cada nodo tiene independencia estadística
con el resto de nodos de la red en lo que a conexiones se refiere. Este modelo
se utiliza con frecuencia en SNA como base teórica para la generación de
otras redes.​
>>> er=nx.erdos_renyi_graph(10,0.15)
Figura 4.28.
Red aleatoria de Erdos resultante al aplicar el anterior código
El segundo ejemplo de generación que vamos a mostrar permite obtener
un grafo aleatorio de tamaño 30 siguiendo el modelo de Watts-Strogatz de red
de pequeño mundo con parámetros 3 y 0,1. El modelo de Watts y Strogatz es
probablemente el modelo más común para generar una red de las que se llaman de mundo pequeño. El algoritmo de construcción propuesto por Watts y
Strogatz para construir una red de pequeño mundo empieza con una red de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
255
tipo circular y el tamaño deseado. El primer parámetro establece el número
de vecinos cercanos a los que inicialmente cada nodo va a estar unido. Estas
redes, en principio, en su estadio inicial no serían de pequeño mundo, ya que
la distancia media entre dos nodos escogidos aleatoriamente sería alta (una
condición relevante de las redes de pequeño mundo es la de que todo el mundo
está conectado mediante caminos cortos, es decir, la distancia mínima entre
dos nodos escogidos aleatoriamente debe ser baja). Este grafo inicial (lejos de
ser de pequeño mundo) va evolucionando con el tiempo, permitiendo a dos
nodos lejanos conectarse con probabilidad p (segundo parámetro). De esta
manera, la distancia media entre nodos va disminuyendo significativamente
hasta generarse una red de pequeño mundo.
>>> ws=nx.watts_strogatz_graph(30,3,0.1)
Figura 4.29.
Red aleatoria resultante al aplicar el anterior código
Finalmente, el tercer modelo de generación de redes sociales que vamos a
ver en este capítulo introductorio es conocido como modelo de enlace preferencial. Se llama así porque refleja situaciones en las que los nodos tienden a
conectarse con mayor probabilidad a aquellos nodos que tienen más relaciones que a nodos que tienen pocas. Este fenómeno sociológico es bastante
habitual en redes sociales que simulan comportamientos reales. Por poner un
ejemplo, supongamos que tenemos una red de citas de artículos. Dos artículos
están relacionados si un artículo cita a otro. En este tipo de redes es mucho
más probable que los nuevos artículos que aparecen citen a artículos con
muchas citas que a artículos con menos. Este fenómeno es conocido como de
256
CUADERNOS METODOLÓGICOS 60
conexión preferencial y es muy habitual en redes reales en contrapartida con
los modelos aleatorios, que se dan mucho menos.
Los modelos de enlaces preferenciales de Barabási-Albert son uno de los
mecanismos más utilizados para generar otro tipo de redes de importancia
real dentro del marco del SNA, como son las redes «libres de escala». La idea
para generar este tipo de redes es simple: se parte de una red de Erdos-Renyi
de tamaño pequeño. A partir de este momento los nuevos nodos que llegan se
unen al resto de nodos de la red con una conexión preferencial (es decir, tendrán mayor probabilidad de conectarse con aquellos nodos líderes de la
estructura que tenemos hasta ese momento).
>>> ba=nx.barabasi_albert_graph(30,5)
Figura 4.40.
Red de Barabási-Albert resultante al aplicar el anterior código
4.2.1.2. El concepto de dígrafo
La característica común de los grafos mostrados anteriormente es que las
relaciones que se establecen entre los nodos no guardan ninguna direccionalidad. Relaciones como, por ejemplo, «ser amigo de» o «estar relacionado
con» no suelen ser presentadas sin ninguna direccionalidad.
Sin embargo, existen otro tipo de relaciones en las que la dirección es relevante y debe ser representada. Por ejemplo, un camino que va de i a j pero no
al revés (por ejemplo, de la página web i podemos ir a la página j pero no necesariamente a la inversa), una cita de un artículo siempre presenta una dirección, u otro tipo de relaciones como las de dominancia, en las que queremos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
257
representar que el nodo i domina al nodo j. En estos casos en los que las relaciones entre nodos son dirigidas, diremos que el grafo es un dígrafo. En este
caso, el conjunto de aristas suele denotarse por pares ordenados (i,j) en lugar
de {i,j}.
Ejemplo práctico
En Phyton se utiliza la clase DiGraph para especificar que se trata de un grafo
dirigido. Además de muchas de las sentencias que podíamos utilizar para la
clase Graph tenemos nuevas funciones específicas para esta nueva clase. Por
ejemplo, dado un nodo i antes podíamos preguntar cuántos vecinos (nodos
directamente relacionados) eran amigos de este nodo i. Ahora podemos preguntar cuántos nodos salen de i y cuántos nodos llegan a i (ya que ahora tenemos dirección en las relaciones): DiGraph.out_edges(), DiGraph.
in_degree(). La aparición de las relaciones dirigidas permite preguntarse
dado un nodo i quiénes son sus predecesores, DiGraph.predecessors()
(nodos k para los que existe un camino dirigido de k a i) o el concepto de sucesores del nodo i, DiGraph.successors() (nodos k para los cuales existe un
camino dirigido de i a k). En fácil ver que estos dos conceptos en el caso de grafos no dirigidos son coincidentes, pero en dígrafos no tienen por qué.
A continuación, mostramos un ejemplo de cómo trabajar en Phyton con
dígrafos en los que se trabaja con la generación de un dígrafo a partir de
un dígrafo vacío: se ponderan las dos artistas dirigidas, se calculan el grado
saliente del nodo 1 (suma de los pesos que salen del nodo 1) o el grado entrante
(suma de los pesos de las aristas que entran en el nodo 1), y, finalmente, se
obtienen los predecesores y los vecinos del nodo 1.
>>> DG = nx.DiGraph ()
>>> DG.add_weighted_edges_from ([(1,2,0.5), (3,1,0.75)])
>>> DG.out_degree (1, weight = 'weight')
0.5
>>> DG.degree (1, weight = 'weight')
1.25
>>> DG.successors (1)
[2]
>>> DG.neighbors (1)
[2]
Algunos algoritmos funcionan solo para grafos dirigidos (o dígrafos) y
otros no están bien definidos para grafos no dirigidos (o grafos). De hecho, la
tendencia a tratar grafos dirigidos y no dirigidos como similares es peligrosa
258
CUADERNOS METODOLÓGICOS 60
ya que muchos de los conceptos/algoritmos que vienen a continuación varían
en función de si las relaciones entre nodos son dirigidas o no lo son. Por este
motivo es recomendable definir la red que se está analizando de manera adecuada. Es posible convertir un grafo en un dígrafo mediante el comando
Graph.to_undirected(), y de manera opuesta, un dígrafo en un grafo,
como se muestra a continuación.
>> H = nx.Graph (G) # convertir G en grafo no dirigido
Figura 4.41.
Grafo resultante al aplicar el anterior código
4.2.1.3. Estructuras de representación de redes
Como se ha dicho en la introducción, en esta segunda fase de este capítulo
introductorio mencionaremos los diferentes métodos y estructuras de representación de una red tanto dirigida como no dirigida. Una vez entendido el
objeto matemático (grafo) central del análisis de redes sociales, a continuación se muestran las principales estructuras de representación que se usan
para su almacenamiento. Como es obvio, no existe ninguna estructura mejor
que otra, y dependerá de las características de la red que estemos analizando
o de los objetivos del estudio.
Existen diferentes formas de almacenar grafos como base de datos. La
estructura de datos usada depende de las características del grafo y de los
algoritmos que vamos a utilizar, por lo que son frecuentes las transformaciones de estructura de almacenamiento según se van usando unos algoritmos u
otros. Entre las estructuras más sencillas y usadas se encuentran las listas (de
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
259
aristas) y las matrices (matriz de adyacencia), aunque frecuentemente se usa
una combinación de ambas. Las listas son preferidas en grafos dispersos porque permiten un uso más eficiente de la memoria. Por otro lado, las matrices
proveen de un acceso rápido a la información, aunque pueden consumir grandes cantidades de memoria.
A continuación, se muestran las tres estructuras más habituales para representar un grafo.
a)Lista de incidencia. Es una lista donde las relaciones (dirigidas o no)
son representadas mediante un vector de pares donde cada par representa las relaciones entre los nodos.
Ejemplo:
Origen
Destino
Peso
1
2
1
2
3
1
3
4
1
1
3
1
Nodo
Conexiones
Pesos
1
2,3
1,1
2
1,3
1,1
3
4
1
4
3
1
b) Lista de adyacencia.
c)Matriz de adyacencia. Matriz n × n donde Aij toma el valor 1 cuando
existe la relación entre i y j.
Nodos\Nodos
1
2
3
4
1
0
1
1
0
2
1
0
1
0
3
1
1
0
1
4
0
0
1
0
260
CUADERNOS METODOLÓGICOS 60
Con la llegada del big data y las nuevas fuentes de información es común
encontrarnos con redes de gran tamaño. Cuando el tamaño de la red es significativamente grande, en ocasiones, es necesario (tanto para almacenarla
como para analizarla posteriormente) partir la red en «trozos». Esta idea de
partir la red en trozos tiene esencialmente dos conceptos diferenciados de los
que hablaremos a continuación: grafo parcial y subgrafo.
Definición de grafo parcial. Dado un grafo G=(V,E), se define grafo parcial del grafo G a cualquier grafo H que tenga el mismo número de nodos
que G, y cuyas aristas sean un subconjunto del original E. Así, pues, el grafo
H=(V,L) se considera grafo parcial de G si y solo si L está contenido en el
conjunto E.
Definición de subgrafo. Dado un grafo G=(V,E) con conjunto de nodos V y
conjunto de aristas E, se define subgrafo del grafo G a un grafo con conjunto
de nodos contenido en el original que mantiene las aristas del grafo original
dentro de los nodos del nuevo subconjunto. Así pues, dado un conjunto de
nodos S contenido en V, se define unívocamente el subgrafo G_S como el par
(S, E|S) donde E|S={i,j en S con (i,j) en E}.
Ejemplo práctico
En Phyton es posible generar subgrafos a partir de uno dado. Para ello
basta indicar el subconjunto de nodos sobre el que se quiere obtener el
nuevo grafo.
>>> G = nx.Graph()
>>> G.add_path([0,1,2,3]) # un grafo que es una cadena de 4
nodos
>>> H = G.subgraph([0,1,2]) # subgrafo que contiene los nodos
0,1,2
>>> H.edges()
[(0, 1), (1, 2)]
Para concluir con este capítulo introductorio hablaremos de lo que se
entiende por análisis general de una red. Cualquier análisis de redes sociales
empieza por un análisis básico de algunas características de la red. Por establecer una posible analogía con un análisis de datos donde la información ya
ha sido depurada, esta fase correspondería al análisis estadístico descriptivo
inicial que, obviamente, es independiente del problema que se desee analizar
posteriormente. A este análisis preliminar sobre las características de una red
se le suele denominar «análisis de la topología de una red», y hace referencia
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
261
a una serie de características básicas que permiten entender mejor la red que
tenemos entre manos.
4.2.1.4. Análisis topológico de una red
Como se ha mencionado en la introducción de este capítulo en el tercer bloque de esta pequeña introducción sobre el análisis de redes sociales, hablaremos de lo que se entiende por realizar un análisis topológico de una red dada.
Una vez introducidos los conceptos básicos de redes, así como su representación más natural, el primer paso en cualquier análisis de redes independientemente del análisis posterior que se desee realizar es el de realizar un estudio
general y topológico de la red en el que se describan las principales características de la red que se está estudiando.
Tomando como referencia una red cualquiera, es importante empezar
realizando una serie de medidas globales que nos pueden dar una idea
sobre algunas de sus características relevantes. A continuación, pasamos a
definir algunas de estas medidas, así como su posible cómputo con Phyton.
a)Tamaño. Numero de nodos y de aristas. Dado un grafo G=(V,E), el
número de nodos es el cardinal del conjunto V (|V|) y el número de aristas será el cardinal del conjunto E (|E|).
b)Densidad. La densidad de un grafo refleja el número de conexiones que
tiene un grafo entre todas las posibles conexiones que podría tener.
Dado un grafo G=(V,E) no dirigido, el cardinal de E, |E|, representa el
número de relaciones que tiene dicho grafo. En un grafo completo
(todos los nodos están relacionados) se tienen |V|*(|V|-1)/2 relaciones. La
densidad se define como el cociente entre estos dos valores.
d (G ) =
E
V * ( V − 1)
.
2
Este valor refleja el porcentaje de relaciones que tiene el grafo G sobre
todas las posible que podría tener.
c)Coeficiente de agrupación (clustering coeficient). El clustering coeficient
definido para un nodo i de un grafo trata de cuantificar el nivel de conexión que tiene el subgrafo definido por él y sus vecinos. Si dado un vértice i, el subgrafo generado por sus vecinos forma un grafo completo, su
valor será máximo, mientras que un valor pequeño indica un vértice con
poca conectividad entre sus vecinos. Si denotamos por ki al número de
vecinos que tiene el nodo i y por Li al número de relaciones que tienen
262
CUADERNOS METODOLÓGICOS 60
los vecinos del nodo i, el coeficiente de agrupación para el nodo i (Ci) en
un grafo se define como sigue:
Ci =
Li
.
k i ( k i − 1)
2
Es posible agregar los coeficientes individuales (o locales) de cada uno
de los nodos para obtener un coeficiente global de agrupamiento por
medio de la media aritmética, logrando de esta manera el coeficiente de
agrupación del grafo G:
C (G ) =
1
V
∑C.
i
i en V
d)Diámetro. Dado un grafo no dirigido G=(V,E), se define como diámetro la longitud máxima de todos los caminos mínimos entre cada par
de nodos. Si denotamos por ≠ ij la longitud mínima entre los nodos i y
j, el diámetro de un grafo puede definirse formalmente como
Dia (G ) = max {i , j en V } {π ij }
e)Distancia media de caminos mínimos. Como su nombre indica, representa la distancia media que se debe recorrer (suponiendo que la información fluye a través de caminos mínimos) entre dos nodos en el grafo.
AverSP (G ) =
∑{
π ij
i , j en V con π ij finito }
M
,
donde M es el número de pares de nodos entre los que existe al menos
un camino.
f)Homofilia. Los orígenes del concepto de homofilia en la literatura sociológica se encuentran en Lazarsfeld y Merton (1954). Otra de las medidas
globales asociadas a una red que pueden ser de interés social es el concepto de homofilia. Etimológicamente, su significado es «amor hacia lo
similar». Dado un grafo G=(V,E), diremos que su grado de homofilia es
alto si los nodos de dicho grafo se conectan entre sí porque comparten
características similares. Una primera observación nos hace darnos
cuenta de que es necesario definir alguna característica asociada a cada
nodo para poder responder a la pregunta siguiente: ¿tienen más tendencia los nodos que comparten dicha característica a relacionarse que los
que no la tienen? Teniendo en cuenta esto, y según la característica elegida, la red podrá tener alta homofilia o no.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
263
Figura 4.42.
La blogphera política en las elecciones norteamericanas de 2004
En el caso en que la característica escogida sea, por ejemplo, votar a determinado partido político (véase la figura adjunta), se puede ver que el grafo
anterior tiene una alta homofilia, ya que las personas tienden a relacionarse con gente que pertenece al mismo partido con mayor probabilidad
que con aquella de un partido contrario.
Igualmente, en este ejemplo, la homofilia está asociada a la característica
«partido al que votas». Esta información no siempre está disponible en la
red, por lo que la pregunta que surge es la siguiente: ¿cómo es posible
medir la homofilia para un grafo sin ninguna información sobre sus nodos?
De manera general, y en el caso en el que no se tenga ninguna característica adicional a la red que se está analizando, se suele denominar red
con alta homofilia a aquella red cuyos nodos poderosos (con muchos
«amigos») tienden a juntarse entre ellos con mayor probabilidad que
entre nodos con menos amigos. En estos casos, la característica escogida (para poder explicar por qué se juntan los nodos) sería el grado de
cada nodo. En estos contextos, por tanto, se entienden como homofilia
aquellas situaciones en las que los nodos con muchas relaciones tienden
a juntarse con mayor probabilidad a nodos con muchas relaciones y,
por otro lado, los nodos con pocas relaciones tienden a juntarse con
nodos con pocas relaciones.
Para determinar esta relación se calcula el coeficiente de correlación
entre dos variables X e Y que se construyen como sigue. Dado el grafo
G=(V,E), denotamos por Xi al grado del nodo origen de la arista i-ésima y
por Yi al grado del nodo destino de la arista i-ésima. El coeficiente de
correlación de las variables X e Y representa la homofilia de dicho grafo.
Una correlación alta y positiva indica que los nodos con con muchas relaciones se relacionan con nodos con muchas relaciones, y viceversa. Por el
contrario, redes con coeficiente de correlación negativo indican que los
nodos con muchas relaciones tienden a juntarse con nodos con pocas.
264
CUADERNOS METODOLÓGICOS 60
g)Componentes conexas. Dado un grafo no dirigido G=(V,E), las componentes conexas establecen una partición maximal del conjunto de nodos
V. El número de componentes conexas da una idea inicial sobre los grupos que se conectan en un grafo. Es común en este tipo de análisis el
describir las componentes conexas de mayor tamaño en una red. A la
componente conexa de mayor tamaño en una red se la denomina Giant
connected component.
Ejemplo práctico
En el siguiente código obtenemos algunas de las medidas que se han definido
anteriormente.
G = nx.lollipop_graph(4, 6)
nx.draw(G)
pathlengths = []
print("source vertex {target:length, }")
for v in G.nodes():
spl = dict(nx.single_source_shortest_path_length(G, v))
print('{} {} '.format(v, spl))
for p in spl:
pathlengths.append(spl[p])
print('')
print("average shortest path length %s" % (sum(pathlengths)
/ len(pathlengths)))
# histogram of path lengths
dist = {}
for p in pathlengths:
if p in dist:
dist[p] += 1
else:
dist[p] = 1
print('')
print("length #paths")
verts = dist.keys()
for d in sorted(verts):
print('%s %d' % (d, dist[d]))
print("radius: %d" %
print("diameter: %d"
print("eccentricity:
print("center: %s" %
nx.radius(G))
% nx.diameter(G))
%s" % nx.eccentricity(G))
nx.center(G))
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
265
print("periphery: %s" % nx.periphery(G))
print("density: %s" % nx.density(G))
print("clustering: %s" % nx. clustering (G))
# Average clustering coefficient
ccs = nx.clustering(G)
avg_clust = sum(ccs.values()) / len(ccs)
nx.draw(G, with_labels=True)
plt.show()
La salida asociada a este código sería la siguiente:
Figura 4.43.
Grafo resultante al aplicar el anterior código
source vertex {target:length, }
0 {0: 0, 1: 1, 2: 1, 3: 1, 4: 2, 5:
1 {1: 0, 0: 1, 2: 1, 3: 1, 4: 2, 5:
2 {2: 0, 0: 1, 1: 1, 3: 1, 4: 2, 5:
3 {3: 0, 0: 1, 1: 1, 2: 1, 4: 1, 5:
4 {4: 0, 5: 1, 3: 1, 6: 2, 0: 2, 1:
5 {5: 0, 4: 1, 6: 1, 3: 2, 7: 2, 0:
6 {6: 0, 5: 1, 7: 1, 4: 2, 8: 2, 3:
7 {7: 0, 6: 1, 8: 1, 5: 2, 9: 2, 4:
8 {8: 0, 7: 1, 9: 1, 6: 2, 5: 3, 4:
9 {9: 0, 8: 1, 7: 2, 6: 3, 5: 4, 4:
average shortest path length 2.86
3,
3,
3,
2,
2,
3,
3,
3,
4,
5,
6:
6:
6:
6:
2:
1:
9:
3:
3:
3:
4,
4,
4,
3,
2,
3,
3,
4,
5,
6,
7:
7:
7:
7:
7:
2:
0:
0:
0:
0:
5,
5,
5,
4,
3,
3,
4,
5,
6,
7,
8:
8:
8:
8:
8:
8:
1:
1:
1:
1:
6,
6,
6,
5,
4,
3,
4,
5,
6,
7,
9:
9:
9:
9:
9:
9:
2:
2:
2:
2:
7}
7}
7}
6}
5}
4}
4}
5}
6}
7}
266
CUADERNOS METODOLÓGICOS 60
length #paths
0 10
1 24
2 16
3 14
4 12
5 10
6 8
7 6
radius: 4
diameter: 7
eccentricity: {0: 7, 1: 7, 2: 7, 3: 6, 4: 5, 5: 4, 6: 4, 7:
5, 8: 6, 9: 7}
center: [5, 6]
periphery: [0, 1, 2, 9]
density: 0.26666666666666666
clustering: {0: 1.0, 1: 1.0, 2: 1.0, 3: 0.5, 4: 0, 5: 0, 6:
0, 7: 0, 8: 0, 9: 0}
A continuación, pasamos a explicar brevemente las salidas que se han
obtenido. La salida correspondiente al enunciado source vertex
{target:length, } nos va indicando las distancias mínimas de cada nodo
al resto de nodos de una red. Esta información es necesaria para identificar si
nuestra red es de pequeño mundo o no lo es. ara llevar a cabo este análisis se
usa la sentencia lenght #paths para poder obtener las frecuencias absolutas de la variable distancia entre cada par de nodos. De esta manera podemos
observar que tenemos 10 caminos de distancia cero (que corresponden con
los 10 nodos del grafo que tenemos), 24 caminos de distancia 1 (que corresponden con el número de aristas), 16 caminos de longitud 2 y, así, sucesivamente, hasta llegar al diámetro de la red, que es el camino más largo, 7 en este
caso que corresponde a los caminos entre el nodo 9 y los nodos 0,1 y 2. La
eccentricity de un nodo x es el camino de distancia máxima que empieza en el
nodo x. A través de este concepto se obtienen dos nuevos conceptos en teoría
de grafos, como son centro y periferia de un grafo conexo, como aquellos
nodos en los que se minimiza el valor de eccentricity (nodos centrales, que
para este caso serían los nodos 5 y 6) y se maximiza el valor de eccentricity
(nodos periféricos, que para este caso serían los nodos 0, 1, 2 y 9).
4.2.2. Centralidad en redes sociales
Uno de los problemas más estudiados dentro del análisis de redes es el del
estudio de medidas de centralidad. La centralidad de un nodo es un concepto sociológico que fue definido de manera abstracta. En teoría de grafos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
267
el concepto de centralidad de un nodo tiende a asociarse a la importancia
que tiene la posición que ocupa dicho nodo dentro de un grafo. El concepto
fue introducido inicialmente por Bavelas a finales de los años cuarenta de
manera vaga y abstracta. Se dice que un nodo tiene centralidad alta si:
a) puede comunicarse directamente con muchos otros nodos;
b) puede comunicarse rápidamente con el resto de la red;
c) Es necesario para el resto de nodos pueda comunicarse.
Cada una de estas ideas abstractas sobre centralidad dieron lugar a los tres
primeros grandes conceptos de centralidad (véanse, para más detalle, Freeman, 1978, 1979 y 1983): centralidad por grado (degree centrality), centralidad
por cercanía (closeness centrality) y centralidad por intermediación (betweeness centrality). Posteriormente, otros conceptos de centralidad se han desarrollado durante las últimas décadas. Es muy importante mencionar (véanse,
para más detalle: Bonachi, 1987; Borgatti, 2005 y 2006; Borgatti et al., 2005 y
2006) que cada medida de centralidad trata de medir la importancia de la
posición que ocupa un nodo suponiendo que la información fluye de una
manera, y por eso no podemos afirmar que existan unas medidas mejores que
otras, ya que, en función del tipo de red, y el principal objetivo de la investigación, deberán utilizarse unas medidas u otras.
En la literatura se pueden encontrar numerosos artículos y libros sobre
medidas de centralidad. En este trabajo daremos un enfoque computacional
sobre las principales medidas que se han definido en la literatura.
4.2.2.1. Centralidad por grado
La centralidad por grado es probablemente la medida de centralidad más sencilla tanto computacionalmente hablando como conceptualmente. El grado
de un nodo es la cantidad de relaciones que tiene dicho nodo.
Dado un grafo no dirigido G=(V,E), con matriz de adyacencia asociada A,
la centralidad de un nodo i de V (ki) se define como sigue:
ki =
∑A .
ij
j ∈V
Para grafos dirigidos es posible definir el grado positivo y el grado negativo
como el número de relaciones que salen de un nodo dado o el número de relaciones que llegan. Formalmente:
k i+ =
∑A ,
ij
j ∈V
268
CUADERNOS METODOLÓGICOS 60
k i− =
∑A .
ji
j ∈V
Como puede observarse, la centralidad por grado es relativamente fácil de
obtener en tiempo eficiente, por lo que es muy habitual utilizarla en redes
grandes. Redes conocidas, como Instagram, Twiter y Facebook, entre otras,
ordenan la importancia de sus usuarios (de más influyentes a menos) de
acuerdo con estas medidas. La centralidad por grado es una medida de centralidad excesivamente local que puede tener errores, puesto que solo tiene en
cuenta las relaciones directas. Existen muchas situaciones en las que nodos
con pocas relaciones tienen muchísima influencia en la red. Por eso esta
medida puede ser mejorada teniendo en cuenta más información.
4.2.2.2. Centralidad por cercanía geométrica
Supongamos que quisiéramos mandar un mensaje desde un nodo a todos los
demás de la red. Y supongamos que si dos nodos quieren comunicarse lo harán
a través de su camino más cercano. ¿Cuál sería el nodo que elegir de manera
que minimizáramos la distancia/tiempo total? Esta es la idea de la medida de
centralidad por cercanía (Bavelas, 1940; Freeman, 1978). Para un nodo, se calculan las distancias mínimas de dicho nodo al resto de nodos de la red.
Si se denota por π ij la distancia del camino mínimo entre los nodos i y j, la
centralidad por cercanía de un nodo i se obtiene como la inversa de la suma
de las distancias de dicho nodo al resto de nodos de la red.
En teoría de grafos y análisis de redes la centralidad en un grafo se refiere
a una medida posible de un vértice en dicho grafo, que determina su importancia relativa dentro de este:
Closeness i =
1
∑
j ∈V
π ij
.
La definición original de Bavelas asume que todos los nodos son alcanzables desde cualquier otro nodo. Una definición más general de este concepto
también utilizada es esta versión de closeness:
Closeness i =
1
∑
π ij
.
{ j ∈V , π ij <∞ }
A esta idea de cercanía se le pueden asociar más medidas de centralidad si
entendemos que pueden existir otros mecanismos para agregar las diferentes
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
269
distancias de un nodo al resto de nodos o que la información no necesariamente tiene por qué moverse a través de los caminos mínimos. Algunas variantes son índice Lin, la centralidad por cercanía harmónica o la medida de
centralida de Stefson y Zelen (basada en la teoría de la información).
Todas estas medidas persiguen la misma idea: si un nodo quiere mandar
información al resto de nodos de la red, cuál se encuentra en la «mejor posición».
4.2.2.3. Medidas espectrales de centralidad
Las medidas espectrales (véanse, para más detalle, Bonacich, 1987 y 2001) se
denominan así porque están basadas en el cálculo de autovectores de la matriz
de adyacencia o similares. Pero ¿qué relación existe entre el autovector de una
matriz y la idea de centralidad o importancia de un nodo en un grafo?
Las medidas espectrales tienen su origen en la siguiente premisa. El poder/
importancia de un nodo es el reflejo de la importancia de sus amigos. Si tratamos de formalizar matemáticamente este problema, será formular el problema de la siguiente manera: dado un grafo G=(V,E), denotemos por xi el
poder del nodo i; ahora, si aplicamos directamente la frase anterior tenemos
que
xi =
∑
n
α xj = ∑α Aij xj ∀i = 1,… n.
j amigo i
j =1
Si representamos matricialmente esta ecuación tenemos que x = α ( Ax ) ,
es decir, x debe ser un autovector de la matriz A. Ahora bien, para garantizar
que el vector x tenga todas sus componentes positivas, que sea posible garantizar la existencia de al menos un autovector y ciertas modificaciones a la
expresión anterior, contamos con diversas medidas que se conocen como
medidas de centralidad espectrales.
El autovector dominante izquierdo
La primera y más obvia medida espectral es la del autovector dominante (de
mayor autovalor) de la matriz de adyacencia original. De hecho, el autovector
dominante se puede pensar como el punto fijo de un cálculo iterado en que cada
nodo comienza con el mismo puntaje y luego reemplaza su puntaje con la suma
de los puntajes de sus predecesores. El vector se normaliza y el proceso se repite
hasta la convergencia. Los autovectores dominantes no se comportan como se
espera en los grafos que no son fuertemente conectados. Dependiendo del valor
270
CUADERNOS METODOLÓGICOS 60
propio dominante de los fuertemente conectados componentes, el eigenvector
dominante puede o no ser distinto de cero en componentes no terminales.
Gracias al teorema de Perron-Frobenious podemos asegurar que el autovector x asociado al mayor autovalor es positivo y real si la matriz es simétrica.
Esto hace que la medida de centralidad quede bien definida en grafos no dirigidos. Desde el punto de vista computacional, el algoritmo power iteration (véase,
entre otros, Booth, 2006) es probablemente el más conocido de todos para
encontrar este autovector. Este algoritmo se puede formalizar como sigue:
Power Algorithm
Initialization: x (0) ∈ R n
Step k: For all i=1,...n
n
x i ( k ) = x i ( k − 1) + ∑ Aji x j ( k − 1) (Eq. 1).
j=1
Es importante saber que la convergencia de este algoritmo depende en
gran medida de las propiedades de la matriz A. Según el teorema de PerronFrobenious, tenemos que la secuencia xk converge al vector propio asociado
al autovalor dominante si y solo si se cumplen las dos condiciones siguientes:
—La matriz de adyacencia A tiene un valor propio que es estrictamente
mayor en magnitud que sus otros valores propios.
—El vector de inicio x (0) tiene un componente distinto de cero en la
dirección de un vector propio asociado con el autovalor dominante.
Una condición suficiente para la primera condición es que la matriz de
adyacencia A sea simétrica y definida como positiva. A continuación, se muestra un ejemplo sencillo donde no es fácil obtener la centralidad por autovalor
general.
Ejemplo práctico
Sea G = (V, E) un gráfo dirigido donde el conjunto de nodos viene dado por
V = {1,2,3,4,5,6,7,8}.
Supongamos que existe un enlace (i, j) cuando el nodo i incrementa el
estado del nodo j y deja que E = {(3,1) (4,1) (5,1), (5,2) (6, 2) (7,2) (8,2)} sea el
conjunto de bordes. Para este caso, es fácil ver que el algoritmo no converge
al vector propio asociado con el autovalor real dominante, ya que solo existe
un valor propio ( = 0, con dimensión 8). En general, el método de power iteration y la centralidad asociada al vector propio no funcionan bien con la
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
271
matriz adyacente dispersa y no simétrica, por lo que se han definido diversas
variantes de esta medida con el fin de garantizar esta convergencia.
Índice de Katz/alpha centrality
Katz introdujo su famoso índice en 1953 tratando de generalizar la centralidad por grado. En principio, la idea es ir ponderando el alcance de un nodo al
resto de nodos por la distancia a la que se encuentran. Formalmente se puede
escribir de la siguiente manera:
∞ N
x i = ∑∑α k Ajik
k =1 j =1
Obsérvese que la potencia k-ésima de la matriz de adyacencia para un par
de índices ij (i. e., Ajik ) representa el número de caminos de longitud k que
existen entre el nodo i y el nodo j. Por tanto, el índice de Katz tiene en cuenta
el alcance de un nodo al resto en función de su distancia y ponderando por el
índice α k , por eso puede entenderse como una generalización del grado (que
sería quedarnos con k = 1). No obstante, esta medida puede reescribirse de la
siguiente manera:
N
x i = α ∑Aij ( x j + 1),
j =1
que puede entenderse como una medida de cálculo de autovectores, por lo
que el índice de Kazt se clasifica como una medida espectral. Es importante
reseñar que esta medida se desarrolló en paralelo con la medida conocida
como alpha centrality, cuya equivalencia matemática con la medida de Katz
ha sido demostrada.
La medida de centralidad alpha centrality surge para tratar de resolver
algunos de los problemas en el cómputo de la medida 2.3.1.
Siguiendo la misma idea de centralidad de vectores propios, la centralidad
alfa se calcula como el vector propio, pero permitiendo que los nodos tengan
fuentes externas de influencia. La cantidad de influencia del nodo i generalmente se denota como ei. Cuando no hay información adicional de la red, la
influencia externa se considera 1 para todos los nodos. Formalmente:
n
xi = α
∑A
j=1
ji
xj + ei (Eq. 2).
272
CUADERNOS METODOLÓGICOS 60
El poder de un nodo se obtiene como una agregación de dos valores:
— El poder de los nodos que contribuyen a su estado (es decir, sus vecindarios) ponderado por un valor alfa.
— La influencia externa de este nodo (ei).
Por ejemplo, sea G=(V,E) el grafo que se ha introducido anteriormente
para el cual no se encontraba ninguna solución, las ecuaciones para el cálculo
de la alpha centrality serían las siguientes:
x1 = α ( x 3 + x 4 + x 5 ) + 1,
x 2 = α ( x 3 + x 4 + x 5 ) + 1,
x i = 1 ∀ i = 3…8.
Cuya solución es x = (3α +1,4α +1,1,1,1,1,1,1).
Mediante este ejemplo observamos que las deficiencias que tiene la medida
original de centralidad por autovalor se «corrigen» cuando se introduce esta
mejora.
Page rank
En esta misma línea se desarrolló una de las medidas más famosas de centralidad por su conocido uso que le da Google para ordenar las páginas web
según su importancia (ver Page et al., 1999). En esta misma línea, el poder de
un nodo se expresa de la siguiente manera:
N
x i = α∑
j =1
Aji ( x j )
L(j)
+
(1 − α )
N
,
donde L(j) es el número de vecinos que tiene el nodo i. La idea es que el poder
de un nodo j se traspasa de igual manera a todos sus vecinos de la misma
forma, por eso se divide entre L(j).
Por ejemplo, sea G=(V,E) el grafo introducido en esta sección, las ecuaciones para el cálculo del page rank asociadas a este grafo dan como resultado las
siguientes ecuaciones:
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
273
x1 = α ( x 3 + x 4 + x 5 / 2) + (1 − α )1/ 8
xi =
Cuya solución es x =
[(
+ x 6 + x 7 + x 8 + (1 − α )1/ 8
5
2
(1 − α )1
8
(
∀ i = 3...8
[(
(x
(
x2 = α
1 − α )1 5
7
α , α ,1,1,1,1,1,1,1, .
8
2
2
Hub and authorities
Esta medida de centralidad (Kelinberg, 1999) está pensada para grafos dirigidos
donde la dirección tiene cierto sentido de dominancia, por ejemplo, desde una
página i puedo ir a la página j pero no viceversa. La idea original y diferenciadora
de esta medida es que es una medida de centralidad bidimensional, es decir, para
cada nodo de la red tendremos una valoración de su hubs y otra de su «autoridad». Esta medida se pensó originalmente para determinar la importancia de las
páginas web pero posteriormente se ha utilizado para todo tipo de estructuras.
Supongamos una estructura de relaciones entre diferentes páginas web. En este
tipo de estructuras existen ciertas páginas web, conocidas como hubs, cuya
misión real/operativa era la de servir como grandes directorios. Estas páginas no
eran realmente páginas de referencia (autoridad) sobre la información que contenían sino que se usaban como centro (hub) de información que conducía a los
usuarios a otras páginas autorizadas. En otras palabras, un buen hub representaba a una página que señalaba a muchas otras páginas, y una buena autoridad
representaba a una página que estaba vinculada por muchos hubs diferentes.
Siguiendo esta idea y la manera de desarrollar las medidas de centralidad
espectrales, nos encontramos de nuevo con ecuaciones cuya solución será
autovectores asociados a autovalores de una matriz dada.
Formalmente, si denotamos por un vector n-dimensional asociado con la
potencia del hub y por h el vector n-dimensional asociado, tenemos que
n
ai = α ∑Aji hj
j=1
n
( Eq. 3).
hi = α ∑Aij a j
j=1
Podemos ver que el valor de autoridad de un nodo i es la suma del valor del
eje de los nodos que tienen el nodo i como referencia. Por el contrario, el valor
274
CUADERNOS METODOLÓGICOS 60
del centro de un nodo i es la suma del valor de las autoridades de los nodos en
el conjunto de referencia del nodo i. La expresión anterior puede reescribirse
como
a = α At h
h = α Aa .
Que es equivalente a
a = α At A a
h = α AAt h .
Entonces, para encontrar las autoridades y los valores de hub, tenemos que
calcular el vector propio asociado al autovalor dominante. La principal diferencia en términos de cálculo de vectores propios con la medida de centralidad de
vectores propios clásica es que ahora tenemos la matriz A’A o AA’. Estas dos
matrices tienen buenas propiedades ya que son simétricas y semidefinidas positivas. A partir del teorema de Perron-Frobenious podemos garantizar que el
power iteration converja siempre con el vector propio asociado con el dominante relevante y, por lo tanto, los valores de hub y autoridades se pueden obtener siempre con el power iteration. Sin embargo, es importante señalar que este
método de iteración no siempre converge al mismo vector propio (ya que el
valor propio dominante podría tener una dimensión, dos o más), por lo que hay
algunos autores que han estudiado los primeros pesos apropiados para comenzar con el poder de iteración para garantizar la singularidad.
Por ejemplo, supongamos que volvemos a tener el grafo G = (V, E) del
ejemplo 1. El cálculo de hubs and authorities para este grafo da como resultado las siguientes ecuaciones:
a1 = ( h3 + h4 + h5 ),
a2 = ( h5 + h6 + h7 + h8 ),
ai = 0
∀ i = 3,4,5,6,7,8,
hi = 0 ∀ i = 1,2,
hi = a1 ∀ i = 3,4,
h5 = (a1 + a2 ),
hi = a2 ∀ i = 6,7,8.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
275
La solución que se obtiene mediante el algoritmo power iteration es la
siguiente:
a = [0.5257, 0.8507,0,0,0,0,0,0] and h = [0,0,0.24,0.24,0.64,0.395,0.395,0.3958].
4.2.2.4. Medidas de intermediación basadas en caminos
Las medidas basadas en caminos/rutas explotan no solo la existencia de caminos más cortos, sino que, en realidad, examinan todas las rutas más cortas
que llegan a un nodo. En este sentido, el grado de entrada indegree se puede
considerar como una medida basada en caminos, porque es el equivalente al
número de caminos entrantes de longitud 1.
4.2.2.4.1. Betweenness. Centralidad por intermediación
La centralidad de betweenness fue introducida por Anthonisse en 1971 para
aristas. Posteriormente, Freeman, en 1977, define la misma medida para nodos.
La idea es la siguiente. Si suponemos que la comunicación entre dos nodos se
realiza a través de la ruta más corta, la capacidad para intermediar que tiene un
nodo k es equivalente al número de caminos mínimos entre pares de nodos que
necesitan a este nodo k para su comunicación. Si denotamos por σ ij al número
de caminos entre los nodos nodos i y j y denotamos por σ ij ( k ) al número de
caminos mínimos que necesariamente pasan a través del nodo k, tenemos que
la centralidad por intermediación Bk es la siguiente:
Bk =
∑
i<j
σ ij ( k )
σ ij ( k )
.
i , j ≠k
La intuición detrás de betweenness es que si una gran fracción de caminos
cortos pasa por un nodo k en particular este nodo será intersección de muchos
caminos y, por tanto, un punto de unión relevante en la red. En este sentido,
la capacidad para intermediar de un nodo puede medirse como el efecto que
provocaría si ese nodo se eliminara de la red.
4.2.2.5. Medidas de intermediación basadas en flujo (flow betweenness)
La medida de centralidad de intermediación que examinamos anteriormente
caracteriza a los actores que tienen una ventaja posicional, o poder, en la
medida en que caen en la ruta más corta (geodésica) entre otros pares de
276
CUADERNOS METODOLÓGICOS 60
actores. La idea es que los actores que están «entre» otros actores y de los que
otros actores deben depender para realizar intercambios serán capaces de traducir este rol de intermediario en poder.
Supongamos que dos actores quieren tener una relación, pero la ruta geodésica entre ellos está bloqueada por un intermediario reacio. Si existe otra
vía, es probable que los dos actores la usen, incluso si es más larga y menos
eficiente. En general, los actores pueden usar todas las vías que los conectan,
en lugar de sendas geodésicas. El enfoque de flujo hacia la centralidad amplía
la noción de centralidad de intermediación. Supone que los actores utilizarán
todas las vías que los conectan, proporcionalmente a la longitud de las vías.
La relación se mide por la proporción de todo el flujo entre dos actores (es
decir, a través de todas las vías que los conectan) que se produce en los caminos de los que un actor dado es parte.
Formalmente, si denotamos por f ij al flujo máximo entre los nodos i y j
(i. e., cantidad de información que podría mandarse entre ellos) y denotamos
por f ij ( k ) al flujo máximo entre los nodos debido al nodo k (es decir, f ij (G)-f ij
(G-k), siendo G-k el grafo resultante G al que le quitamos el nodo k y todas sus
conexiones), tenemos que la centralidad por intermediación Fk es la siguiente:
Fk =
∑
i<j
f ij ( k )
f ij ( k )
.
i , j ≠k
Esta medida fue definida por Freeman (1991) y posteriormente analizada
y generalizada por Gómez et al. en 2015. En el caso de grafos valorados y dirigidos, es una de las medidas más populares (véanse Bonacich, 2005 y 2006)
por la gran cantidad de situaciones reales que modeliza. Su complejidad de
cálculo está asociada al problema del cómputo del flujo entre cada par de
nodos.
Ejemplo práctico
Las siguientes funciones permiten el cálculo de las medidas de centralidad
más conocidas.
Centralidad por grado
degree_centrality(G)
Compute the degree centrality for
nodes.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
277
in_degree_centrality(G)
Compute the in-degree centrality
for nodes.
out_degree_centrality(G)
Compute the out-degree centrality
for nodes.
En este ejemplo, calculamos la centralidad por cercania (closeness) para la
red de Karate anteriormente vista.
G_karate=nx.read_pajek("karate.net")
nx.draw(G_karate,with_labels=True)
degree_centra=nx.algorithms.degree_centrality(G_karate)
Figura 4.44.
Grafo de la red de karate
Esta red también está implementada en el paquete NetworkIDX, por lo que
otra posibilidad para leerlo y dar dos visualizaciones es la siguiente:
import matplotlib.pyplot as plt
import NetworkX as nx
G = nx.karate_club_graph()
print("Node Degree")
for v in G:
print('%s %s' % (v, G.degree(v)))
278
CUADERNOS METODOLÓGICOS 60
nx.draw_circular(G, with_labels=True)
plt.show()
nx.draw(G, with_labels=True)
plt.show()
Node Degree
0 16
1 9
2 10
3 6
4 3
5 4
6 4
7 4
8 5
9 2
10 3
11 1
12 2
13 5
14 2
15 2
16 2
17 2
18 2
19 3
20 2
21 2
22 2
23 5
24 3
25 3
26 2
27 4
28 3
29 4
30 4
31 6
32 12
33 17
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
279
Figura 4.45.
Grafo karate con visualización circular y original
Centralidad por cercanía
closeness_centrality(G[, u, distance, ...]) Compute closeness centrality for nodes.
En este ejemplo, calculamos la centralidad por cercania (closeness) para la
red de Karate anteriormente vista.
G_karate=nx.read_pajek("karate.net")
nx.draw(G_karate,with_labels=True)
280
CUADERNOS METODOLÓGICOS 60
close_centra=nx.algorithms. closeness_centrality (G_karate)
print("Closeness Centrality")
close_centra
Closeness Centrality
{0: 0.5689655172413793,
1: 0.4852941176470588,
2: 0.559322033898305,
3: 0.4647887323943662,
4: 0.3793103448275862,
5: 0.38372093023255816,
6: 0.38372093023255816,
7: 0.44,
8: 0.515625,
9: 0.4342105263157895,
10: 0.3793103448275862,
11: 0.36666666666666664,
12: 0.3707865168539326,
13: 0.515625,
14: 0.3707865168539326,
15: 0.3707865168539326,
16: 0.28448275862068967,
17: 0.375,
18: 0.3707865168539326,
19: 0.5,
20: 0.3707865168539326,
21: 0.375,
22: 0.3707865168539326,
23: 0.39285714285714285,
24: 0.375,
25: 0.375,
26: 0.3626373626373626,
27: 0.4583333333333333,
28: 0.4520547945205479,
29: 0.38372093023255816,
30: 0.4583333333333333,
31: 0.5409836065573771,
32: 0.515625,
33: 0.55}
Centralidad por intermediación
betweenness_centrality(G[, k, normalized, ...]) Compute the
shortest-path betweenness centrality for nodes.
edge_betweenness_centrality(G[, k, ...]) Compute betweenness centrality for edges.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
281
En este ejemplo calculamos la centralidad por intermediación basada en
caminos mínimos. También calculamos el poder de intermediación de las
aristas de esta red.
G_karate=nx.read_pajek("karate.net")
nx.draw(G_karate,with_labels=True)
bet_centra=nx.algorithms. betweenness_centrality (G_karate)
ed_bet_centra=nx.algorithms.
edge_betweenness_centrality
(G_karate)
print("edge_betweenness_centrality ")
ed_bet_centra
Medidas de centralidad espectrales
eigenvector_centrality(G[, max_iter, tol, ...]) Compute the
eigenvector centrality for the graph G.
eigenvector_centrality_numpy(G[, weight]) Compute the eigenvector centrality for the graph G.
katz_centrality(G[, alpha, beta, max_iter, ...]) Compute
the Katz centrality for the nodes of the graph G.
katz_centrality_numpy(G[, alpha, beta, ...]) Compute the
Katz centrality for the graph G.
En este ejemplo calculamos la centralidad por autovalor, alpha centrality o
Katz centrality, hubs and authorities para la red de karate.
G_karate=nx.read_pajek("karate.net")
nx.draw(G_karate,with_labels=True)
eigen_centra=nx.algorithms. eigenvector_centrality (G_karate)
katz_centra=nx.algorithms. katz_centrality (G_karate)
print("katz_centrality ")
katz_centra
katz_centrality
Out[32]:
{0: 0.3213245969592325,
1: 0.2354842531944946,
2: 0.2657658848154288,
3: 0.1949132024917254,
4: 0.12190440564948413,
5: 0.1309722793286492,
282
CUADERNOS METODOLÓGICOS 60
6: 0.1309722793286492,
7: 0.166233052026894,
8: 0.2007178109661081,
9: 0.12420150029869696,
10: 0.12190440564948413,
11: 0.09661674181730141,
12: 0.11610805572826272,
13: 0.19937368057318847,
14: 0.12513342642033795,
15: 0.12513342642033795,
16: 0.09067874388549631,
17: 0.12016515915440099,
18: 0.12513342642033795,
19: 0.15330578770069542,
20: 0.12513342642033795,
21: 0.12016515915440099,
22: 0.12513342642033795,
23: 0.16679064809871574,
24: 0.11021106930146936,
25: 0.11156461274962841,
26: 0.11293552094158042,
27: 0.1519016658208186,
28: 0.143581654735333,
29: 0.15310603655041516,
30: 0.16875361802889585,
31: 0.19380160170200547,
32: 0.2750851434662392,
33: 0.3314063975218936}
Centralidad por flujo
current_flow_closeness_centrality(G[, ...]) Compute currentflow closeness centrality for nodes.
current_flow_betweenness_centrality(G[, ...]) Compute current-flow betweenness centrality for nodes.
edge_current_flow_betweenness_centrality(G) Compute currentflow betweenness centrality for edges.
approximate_current_flow_betweenness_centrality(G) Compute
the approximate current-flow betweenness centrality for nodes.
En este ejemplo calculamos la centralidad por flujo para nodos y para aristas, y la aproximación a estos cómputos, que en este caso es casi idéntica a la
original para la red de karate.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
283
G_karate=nx.read_pajek("karate.net")
nx.draw(G_karate,with_labels=True)
flow_clo_centra=nx.algorithms. current_flow_closeness_centrality (G_karate)
flow_bet_centra=nx.algorithms.
current_flow_betweenness_centrality (G_karate)
flow_bet_centra=nx.algorithms. edge_current_flow_betweenness_
centrality (G_karate)
flow_bet_centra=nx.algorithms.
approximate_current_flow_betweenness_centrality (G_karate)
4.2.3. Detección de comunidades
Uno de los problemas más estudiados durante los últimos años dentro del
análisis de redes sociales es el problema conocido como «detección de comunidades» en redes. Desde un punto de vista matemático, la detección de comunidades puede ser entendida como un problema de clustering de nodos de una
red, donde el objeto es encontrar una partición o cubrimiento del conjunto de
nodos. Los elementos de esta partición o cubrimiento se denominan comunidades o grupos.
Una comunidad puede ser definida de manera vaga como un conjunto de
nodos que están más densamente conectados entre ellos que con el resto de la
red. Esta definición de lo que se entiende por una comunidad hace que se
espere que los nodos que están contenidos dentro de una misma comunidad
compartan atributos, características comunes o relaciones funcionales. Sin
embargo, no existe una definición exacta de lo que es o debe ser una comunidad, lo cual veremos más tarde que genera multitud de inconvenientes a la
hora de dividir una red en sus distintas comunidades, lo que se conoce como
partición o clustering.
La solución más tradicional a un problema de detección de comunidades
es una partición del conjunto de nodos. A los elementos de esta partición se
les llama comunidades. Matemáticamente, una partición de un conjunto de
i =k
nodos de un grafo G=(V,E) es un conjunto P = {C1 , ..C k } con V = ∪C i y con
i =1
C i ∩C j = ∅ para todo i distinto de j. Además, a estas comunidades se les exige
conectividad (aunque no siempre aparece formalmente esta condición). Por
supuesto, existen problemas más generales que el problema clásico, como:
—Overlapping (véanse Gómez et al., 2016; Xie et al., 2013, entre otros). Se
trata del problema de detección de comunidades con solapamiento entre
ellas (es decir, se elimina la restricción de que dos comunidades no puedan
compartir un elemento). Matemáticamente, la solución al problema es
284
CUADERNOS METODOLÓGICOS 60
un cubrimiento del conjunto de nodos, es decir, un conjunto P = {C1 , ..C k }
i =k
con V = ∪C i .
i =1
—Fuzzy (Fortunato, 2010; Gómez et al., 2016; Reichard et al., 2004). Una
generalización a este problema es permitir que cada nodo tenga un
grado de pertenencia a las diferentes comunidades. En este caso, la
solución al problema de detección de comunidades sería una partición
borrosa (no necesariamente de Ruspini). Formalmente, la solución
podría verse como una matriz Uˆ = {U ij , i ∈ V , j = 1,... k } donde U ij representa el grado de pertenencia del nodo i a la comunidad Cj.
—Jerarquizada (Gómez et al., 2015; Girvan-Newman, 2003; Blondel et al.
y Clauset et al., 2004). Al igual que pasa con los problemas de clustering
en estadística el problema de agrupar un conjunto de objetos tiene el
problema añadido de determinar el número de objetos en los que quieres realizar la agrupación. El problema de detección de comunidades
jerarquizadas persigue dar como solución una secuencia de particiones
en las que el número de grupos aumenta (algoritmos divisivos) o disminuye (algoritmos aglomerativos) desde una situación inicial en la que
todos los nodos pertenecen a una única comunidad y esta va dividiéndose paso a paso (divisivos) o se parte de una situación en la que todos
los nodos forman comunidades individualizadas y estas se van agrupando paso a paso, formando cada vez comunidades más grandes.
Desde un punto de vista combinatorio conforme crece el número de nodos,
el número de particiones distintas que pueden obtenerse crece de una forma
más que exponencial, lo cual dificulta de manera extrema la selección de la
mejor partición del grafo. Cómo encontrar esta partición óptima es, probablemente, el problema abierto más importante de la investigación en estructura
de comunidades. Una gran variedad de métodos y algoritmos, cada uno de
ellos con su propia definición intrínseca de comunidad, han sido desarrollados para intentar extraer la partición óptima de una red.
Teniendo en cuenta que la definición de que se entiende por una buena
comunidad o una buena solución es algo vago y no mundialmente aceptado,
el problema de cómo comparar dos soluciones para un mismo grafo no es
un problema trivial de resolver. Una de las medidas más importantes de la
literatura que nos permite tener una idea de lo bueno o malo que es una partición es el concepto de modularidad.
Se basa en la idea de que una distribución en clusters no es lo que se espera
por azar en una red, y, por tanto, trata de cuantificar la intensidad de esta
estructura de comunidades comparando la densidad de links dentro y fuera
de cada comunidad con la densidad que esperaríamos si los links estuviesen
distribuidos aleatoriamente en la red. A este modelo estadístico se le debe
dotar de un modelo nulo que especifique qué es lo que se espera por azar. La
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
285
siguiente fórmula es la utilizada para calcular la «modularidad (Q)» (definida
en Girvan-Newman, 2003) de una partición determinada:
(
kk
1
Aij − i j δ (C i ,C j ).
∑
2m i , j
2m
(
Q=
donde ki representa el grado del nodo i, m es el conjunto de aristas y delta de
Ci, Cj es 1 cuando los nodos i y j están en la misma comunidad.
A continuación, nos centraremos en algunos de los diferentes algoritmos
que se han propuesto para abordar problemas de detección de comunidades.
Es importante mencionar que existen una gran cantidad de algoritmos para
estos problemas (ver el impresionante artículo de Fortunato (2010) para más
detalles). Como en los problemas clásicos de agrupamiento, una clasificación
más simple basada en la salida del algoritmo para la detección de comunidades permite una división de los algoritmos de agrupamiento entre algoritmos
jerárquicos y no jerárquicos. Como señala Fortunato (ibid.), la mayoría de los
algoritmos que se ocupan de los problemas de detección de la comunidad se
han desarrollado para obtener un agrupamiento no jerárquico de un grafo.
Existen muchos reviews de este tema al respecto y menos sobre las diferentes
técnicas que permiten obtener una solución jerárquica del grafo. En este libro
nos centraremos en dar una pequeña reseña sobre las principales técnicas
para estos problemas. Ahora damos una breve reseña de algunos de estos
tipos de algoritmos.
—Basados en la construcción de una matriz de disimilaridad (véanse,
para más detalle, Fortunato, 2010; Lancichinetti et al., 2009). Las técnicas clásicas de clustering jerárquico pueden usarse también para resolver el problema de detección de comunidades jerarquizadas, pero es
necesario tener una medida de disimilaridad para cada par de nodos.
Esta medida de disimilaridad representa cuán distintos son los vértices.
Una vez que se ha construido la matriz de distancia/disimilaridad W, los
nodos de la red se pueden agrupar utilizando cualquier técnica de agrupación jerárquica clásica, sin considerar la estructura original. Aunque
esta aproximación es fácil de entender, ha sido criticada por algunos
autores, ya que tiene ciertos inconvenientes obvios, como el hecho de
que la estructura subyacente no se está considerando. Algunos de los
principales problemas (memoria requerida, matriz n por n en lugar del
número de aristas del grafo) se han descrito en Fortunato (2010) y
Gómez et al. (2015). Los llamados algoritmos espectrales obtienen un
clúster jerárquico de la red (véase, por ejemplo, Donetti y Muñoz, 2004,
para más detalles) que transforma los nodos de un grafo en puntos en el
espacio, utilizando g vectores propios de la matriz adyacente.
286
CUADERNOS METODOLÓGICOS 60
—Algoritmos divisivos. Sin tener en cuenta la construcción de una matriz
de similitud o disimilitud para todos los pares de nodos, el trabajo pionero se debe a Girvan y Newman en 2003. Presentaron un algoritmo
divisivo con muy buen rendimiento para redes pequeñas y medianas. De
ahora en adelante, denotaremos este algoritmo como algoritmo GN. Tal
algoritmo se basa en el cálculo de un peso para cada par de nodos adyacentes en la red (y no para todos los pares de nodos, como en el método
tradicional). El peso de un enlace representa su poder de intermediación en la estructura de comunicación definida por la red. Una vez que
se define el peso, el algoritmo divisivo se puede resumir de la siguiente
manera:
1.Calcular la centralidad por intermediación (betweeness) de todas las
aristas de la red.
2.Eliminar la arista con intermediación más alta.
3.Volver a calcular la intermediación una vez eliminada la arista.
4.Repetir desde el Paso 1 hasta que no queden aristas.
Este algoritmo cede a una agrupación jerárquica de los nodos en el gráfico y sus resultados dependerán de cómo se hayan definido los pesos.
Alternativamente, Girvan y Newman (2004) introdujeron, para el caso
no ponderado, el concepto de interdependencia, que se puede expresar
como la frecuencia de participación marginal en la comunicación para
cada par de nodos. Esta frecuencia se puede obtener utilizando dos definiciones alternativas: la brecha de la ruta más corta (SP) (en la que solo
se consideran las rutas geodésicas) o la distancia de recorrido aleatorio,
tal como define Newman (2005).
—Algoritmos de aglomeración. La idea original se debe a Newman (2004),
mejorado por Clauset, Newman y Moore en el mismo año (algoritmo
CNM). La idea del algoritmo es producir un agrupamiento jerárquico de
la red de una manera aglomerativa (es decir, comenzando desde los
nodos aislados y terminando con un solo clúster). Básicamente, los
pasos son los siguientes:
1.Calcular el aumento en la modularidad para cada unión posible en la
red.
2.Seleccionar la combinación que maximiza el aumento de la modularidad y fusionar ambas comunidades.
3.Repetir hasta que solo haya una comunidad.
—Algoritmos aleatorios. Al igual que los algoritmos basados en problemas
de clúster de cadenas de Márkov, los algoritmos aleatorios se basan en
una simulación de un proceso de difusión en el grafo. Estos métodos
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
287
son ampliamente utilizados en problemas de bioinformática. Un algoritmo aleatorio muy conocido y utilizado en problemas de detección de
comunidades fue desarrollado por Pons y Latapy (2005) y es comúnmente llamado algoritmo Walktrap o de paseo aleatorio. Otros algoritmos como el algoritmo de clúster personalizado de rango de página
(PPC) el algoritmo desarrollado por Newman (2012) pueden ser considerados como aleatorios o basados en paseos aleatorios.
4.2.3.1. Complejidad en problemas de detección de comunidades
El análisis de la complejidad y el tipo de problemas que resuelven los numerosos algoritmos de detección de comunidades han sido estudiados por
numerosos autores (véanse, por ejemplo, Fortunato et al., 2010; Gómez et al.,
2015). En la siguiente tabla recogida de Gómez et al. (2015) se observan las
diferentes complejidades algorítmicas de algunos de los algoritmos de detección de comunidades jerárquicas más conocidos.
Figura 4.46.
Complejidad algorítmica de diversos algoritmos de detección de comunidades
4.2.3.2. Detección de comunidades con Phyton
En el siguiente ejemplo primero generamos un grafo con una estructura de
comunidades clara para ver si es identificada por algunos de los algoritmos
que hemos mencionado anteriormente.
288
CUADERNOS METODOLÓGICOS 60
import NetworkX as nx
from NetworkX.algorithms import community
G = nx.barbell_graph(5, 1)
nx.draw(G,with_labels=True)
communities_generator = community.girvan_newman(G)
top_level_communities = next(communities_generator)
next_level_communities = next(communities_generator)
sorted(map(sorted, next_level_communities))
Figura 4.47.
Grafo aleatorio barbell resultante de la aplicación del anterior código
Out[67]: [[0, 1, 2, 3, 4], [5], [6, 7, 8, 9, 10]]
La salida muestra las tres comunidades naturales identificadas por el algoritmo GN (Girvan-Newman) en su corte de tres comunidades, que es el de
máxima modularidad.
Dentro de Phyton también está implementado el algoritmo desarrollado
por Blondel (también conocido como Louvain) en 2008, que es considerado
como uno de los mejores algoritmos para detectar comunidades en redes
grandes. Es un algoritmo aglomerativo. La descripción del algoritmo en
Phyton, así como un ejemplo en el que también se calcula la modularidad, se
introducen a continuación.
community.best_partition(graph, partition=None, weight='weight',
resolution=1.0, randomize=False)
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
289
Los inputs de este algoritmo son los siguientes:
— graph [NetworkX.Graph] the NetworkX graph which is decomposed
— partition [dict, optional] the algorithm will start using
this partition of the nodes. It’s a dictionary where keys
are their nodes and values the communities
— weight [str, optional] the key in graph to use as weight.
Default to 'weight'
— resolution [double, optional] Will change the size of the
communities, default to 1. Represents the time described in
«Laplacian Dynamics and Multiscale Modular Structure in
Networks»,R. Lambiotte, J.-C. Delvenne, M. Barahona
— randomize [boolean, optional] Will randomize the node
evaluation order and the community evaluation order to get
different partitions at each call
Y el algoritmo devuelve una partición donde las comunidades están numeradas de 0 al número de comunidades identificadas. Ejemplo:
import community
import matplotlib.pyplot as plt
import NetworkX as nx
G = nx.Graph()
G = nx.read_weighted_edgelist('graphs/fashionGraph_1.edgelist')
# Find modularity
part = community.best_partition(G)
mod = community.modularity(part,G)
# Plot, color nodes using community structure
values = [part.get(node) for node in G.nodes()]
nx.draw_spring(G, cmap=plt.get_cmap('jet'), node_color
values, node_size=30, with_labels=False)
plt.show()
=
290
CUADERNOS METODOLÓGICOS 60
Figura 4.48.
Visualizacion del grafo fashionGraph1 de la librería NetworkhK de Phyton
Conclusiones
La extensión de las técnicas de big data tiene como objetivo obtener ventajas
de un nuevo conjunto de bases de datos. Estas bases de datos tienen diferentes
características que han sido resumidas en la expresión «las tres V del big
data». Esto es, datos que se producen a una gran velocidad, que tienen un
variado formato (números, pero también imágenes, hipervínculos, vídeos,
etc.) y que, normalmente, representan una gran cantidad de información (de
ahí la última «V»; la de volumen). Estos datos se han convertido en una fuente
de conocimiento para empresas y Gobiernos, así como para cualquier tipo de
organización interesada en conocer mejor el área en la que ejerce su labor y
las personas u organizaciones que compiten/comparten espacio con ella.
La ciencia, como un sistema social más, no ha permanecido ajena a este
proceso. Así, las técnicas de big data han ayudado a biólogos, físicos, lingüistas y equipos de investigación de otras muchas disciplinas a mejorar su descripción y análisis de los asuntos que les ocupan. Naturalmente, los científicos
sociales también se han visto afectados por la aparición de este tipo de fuentes
y han mostrado un creciente interés por las técnicas y métodos que permiten
explorar y explotar los big data. En primer lugar, este libro debe servir para
que estos investigadores tengan un manual básico y cercano que colme su
interés. Ciertamente, existe un número considerable de manuales que describen las técnicas y métodos del big data. Sin embargo, no son tantos los que
están dirigidos específicamente a científicos sociales.
Así, este cuaderno ha sido pensado para introducir a sociólogos, politólogos
y antropólogos, así como a investigadores de cualquier disciplina de las ciencias sociales, en el mundo del big data. Sin embargo, más que dar respuestas
definitivas a todos los desafíos que ofrece esta nueva alternativa de investigación, nuestro objetivo específico ha sido ofrecer un panorama general que permita entender qué es el big data, cómo se usa y cuáles son sus aplicaciones. La
formación en este tipo de técnicas no termina, obviamente, cuando el lector
concluye la lectura de este libro. Todo lo contrario, aspiramos a que, una vez
concluido, el científico social aspire a profundizar y formarse en técnicas más
específicas. Es, permítasenos la metáfora, un aperitivo a una cena llena de platos variados y experimentales, cuya preparación está llena de complejidades.
292
CUADERNOS METODOLÓGICOS 60
En esta cena, el científico social debería ser, a la vez, miembro del equipo
de cocina, comensal y, si se nos permite, crítico gastronómico. Esta triple función la ejerce, siguiendo con la misma metáfora, porque es, cada vez más, un
integrante central de los equipos de investigación cuyo objetivo es extraer
información de fuentes como las redes sociales digitales, las huellas que dejamos y que están recogidas a través de herramientas de geolocalización, etc.
Es, también, comensal, ya que se sienta a la mesa para extraer todo el saber,
interpretando y analizando los resultados obtenidos (los platos de nuestra
metáfora). Sin embargo, no debemos desatender la labor de crítico, es decir,
la de aquel que reflexiona sobre qué supone este estilo experimental de cocina
para el desarrollo del arte culinario.
Dada esta triple función, en este libro se ha puesto el acento en tres cuestiones centrales. En primer lugar, y de forma más pronunciada (esta es la
misión prioritaria de un cuaderno metodológico), en la explicación de qué es
el big data y cuáles son las técnicas más comunes de recogida y análisis de la
información. Esto se ha hecho, además, como no podía ser de otra forma en
el marco de una colección como esta, con ejercicios que ayudan al lector a asimilar los contenidos propuestos y que deberían haberle ayudado a conocer
mejor estas técnicas.
Todo ello ha sido pensado de esta forma porque estamos convencidos de
que el análisis social con big data no es tarea de una única investigación. Tampoco es, exceptuando casos señalados, tarea de un equipo formado únicamente por científicos sociales (aunque sean estos de distintas disciplinas de
esta rama de la ciencia). Por el contrario, se trata de una labor que debe realizarse de forma coordinada y multidisciplinar en la que el científico social es
una pieza más. Leer este libro debe habernos llevado no solo a entender qué
es el big data y cómo funciona, sino a repensar la posición de los sociólogos,
politólogos y demás científicos sociales en un nuevo contexto de investigación. En este sentido, habremos cumplido nuestra misión si el lector ha adquirido las competencias suficientes, no para asumir el rol del matemático o del
informático, pero sí para ser un interlocutor capacitado para entender el proceso de descarga, depuración y análisis de la información, y para establecer,
así, un diálogo horizontal con el resto de miembros del equipo.
Esta capacitación es central, porque, al menos así lo pensamos los autores,
permite que el científico social preñe la investigación con los criterios de
robustez y exigencia propios de nuestra disciplina. No solo eso, gracias a la
participación del científico social en este tipo de equipos es posible ofrecer
una interpretación y análisis de la realidad estudiada que vaya más allá de la
mera descripción de datos. Es decir, una degustación de los platos.
Sin embargo, también nos hemos permitido hacer alguna referencia en
este libro a cuestiones clave consecuencia de este nuevo escenario de investigación que apelan a aspectos epistemológicos y críticos. Así, hemos hecho
referencia a preguntas como cuáles son los límites y posibilidades de la
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
293
ciencia social dependiente del big data. Consideramos que es central pensar
en qué tradición de nuestra disciplina está mejor posicionada para extraer la
máxima información de estas fuentes, así como qué preguntas de investigación podemos y no podemos responder con el big data. Es decir, atender no
solo a cuestiones metodológicas, sino también epistemológicas. Incluso, aunque únicamente en forma de breve apunte, ya que no es materia de este tipo
de textos, hemos pretendido esbozar alguna idea sobre los riesgos a los que
nos enfrentamos socialmente como consecuencia de la transformación que
supone el big data. Se trata, en definitiva, de que, cuando el lector haya terminado de leer este libro, sepa mejor qué es el big data para ser un interlocutor
válido en equipos de investigación multidisciplinares, pero también un científico social que piensa reflexiva y críticamente (ahí entra nuestro crítico de
cocina) sobre los cambios que afectan a su disciplina.
Bibliografía
Agneessens, Filip; Borgatti, Stephen. P. y Everett, Martin (2017). «Geo desic based centrality: Unifying the local and the global». Social Networks, 49, pp. 12-26.
Amigó, José Manuel; Falcó, Antonio; Gálvez, Jorge y Villar, Vicente (2007). «Topología molecular». Boletín de la Sociedad Española de Matemática Aplicada, 39,
pp. 135-149.
Balakrishnan, R. y Ranganathan, K. (2000). A Textbook of Graph Theory. New York:
Springer.
Bavelas, Alex (1948). «A Mathematical Model for Group Structure». Applied Anthropology, 7(3), pp. 271-288.
Barabási, Albert. L. y Albert, Réka (1999). «Emergence of scaling in random networks». Science, 286(5439), pp. 509-512.
Blondel, Vincent, D.; Guillaume, Jean L.; Lambiotte, Renaud y Lefebvre, Etienne
(2008). «Fast unfolding of communities in large networks». Journal of Statistical
Mechanics: theory and experiment, 2008(10), pp. 2-12.
Bonacich, Philip (1972). «Factoring and weighting approaches to status scores and clique identification». The Journal of Mathematical Sociology, 2(1), pp. 113-120.
Bonacich, Philip (1987). «Power and Centrality: A Family of Measures». American
Journal of Sociology, 92(5), pp. 1170-1182.
Bonacich, Philip y Lloyd, Philip (2001). «Eigenvector-like measures of centrality for
asymmetric relations». Social Networks, 23(3), pp. 191-201.
Borgatti, Stephen (2005). «Centrality and network flow». Social Networks, 27(1),
pp. 55-71.
Borgatti, Stephen (2006). «Identifying sets of key players in a social network». Computational and Mathematical Organization Theory, 12(1), pp. 21-34.
Borgatti, Stephen y Everett, Martin (2006). «A Graph-theoretic perspective on centrality». Social Networks, 28(4), pp. 466-484.
Borgatti, Stephen; Jones, Candace y Everett, Martin (2005). «Network Measures of
Social Capital». Connections, 21(2), pp. 36-48.
Booth, Thomas (2006). «Power iteration method for the several largest eigenvalues
and eigenfunctions». Nuclear Science and Engineering, 154(1), pp. 48-62.
Brandes, Ulrik; Borgatti, Stephen y Freeman, Linton (2016). «Maintaining the duality
of closeness and betweenness centrality». Social Networks, 44(2), pp. 153-159.
Chen, Hongming; Engkvist, Ola; Wan, Yinhai; Olivecrona, Marcus y Blaschke, Thomas
(2018). «The rise of deep learning in drug discovery». Drug Discovery Today, 23(6),
pp. 1241-1250.
296
CUADERNOS METODOLÓGICOS 60
Chen, Yiping; Paul, Gerald; Cohen, Reuven; Havlin, Shllomo; Borgatti, Stephen;
Liljeros, Frederik y Eugene Stanley, Eugene (2007). «Percolation theory and fragmentation measures in social networks». Physica A: Statistical Mechanics and its
Applications, 378(1), pp. 11-19.
Cicourel, Aaron Victor (2011). Método y medida en sociología. Madrid: Centro de Investigaciones Sociológicas.
Clauset, Aaron; Newman, Mark y Moore, Cristopher (2004). «Finding community
structure in very large networks». Physical Review, 70(6), pp. 66-111.
Conneau, Alexis; Schwenk, Holger; Barrault, Loïc y Lecun, Yann (2016). «Very deep
convolutional networks for natural language processing». Künstliche Intelligenz,
26, pp. 112-126.
Codd, Edgar F. (1970). «A Relational Model of Data for Large Shared Data Banks».
Communications of the ACM. 13(6), pp. 377-387.
Da Silva, Nadie Felix; Hruschka, Eduardo y Hruschka, Estevam (2014). «Tweet sentiment analysis with classifier ensembles». Decision Support Systems, 66, pp. 170-179.
De Bruijne, Marleen (2016). «Machine learning approaches in medical image analysis:
from detection to diagnosis». Medical Image Analysis, 33, pp. 94-97.
Demsar, Janez (2006). «Statistical comparisons of classifiers over multiple data sets».
Journal of Machine Learning Research, 7, pp. 1-30.
Donetti, Luca y Muñoz, Miguel Ángel (2004). «Detecting network communities: a new
systematic and efficient algorithm». Journal of Statistical Mechanics: Theory and
Experiment, 2004(10), pp. 1-8.
Everett, Martin y Borgatti, Stephan (1999). «The centrality of groups and classes».
Journal of Mathematical Sociology, 23(3), pp. 181-201.
Erdos, Paul y Rényi, Alfred (1960). «On the evolution of random graphs». Publication
of the Mathematical Institute of the Hungarian Academy of Sciences, 5(1), pp. 17-61.
Fortunato, Santo (2010). «Community detection in graphs». Physics Reports, 486(3-5),
pp. 75-174.
Freeman, Linton (1978). «Centrality in social networks conceptual clarification».
Social Networks, 1(3), pp. 215-239.
Freeman, Linton; Roeder, Douglas y Mulholland, Robert (1979). «Centrality in social
networks: II. Experimental results». Social Networks, 2(2), pp. 119-141.
Freeman, Linton (1983). «Spheres, cubes and boxes: Graph dimensionality and network structure». Social Networks, 5(2), pp. 139-156.
Freeman, Linton; Borgatti, Stephan y White, Douglas (1991). «Centrality in valued graphs: A measure of betweenness based on network flow». Social Networks, 13(2),
pp. 141-154.
Friedkin, Noah (1991). «Theoretical Foundations for Centrality Measures». Scientometrics, 96(6), pp. 1478-1504.
Gallian, Joseph (2009). «A dynamic survey of graph labeling». The Electronic Journal
of Combinatorics, 16(6), pp. 1-219.
García, Salvador y Herrera, Francisco (2008). «An extension on statistical comparisons of classifiers over multiple data sets for all pairwise comparisons». Journal of
Machine Learning Research, 9, pp. 2579-2596.
Goldthorpe, John (2010). De la sociología. Números, narrativas e integración de la investigación y la teoría. Madrid: Centro de Investigaciones Sociológicas.
Goldthorpe, John (2017). La sociología como ciencia de la población. Madrid: Alianza.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
297
Gómez, Daniel; Zarrazola, Edwin; Yáñez, Javier y Montero, Javier (2015). «A Divideand-Link algorithm for hierarchical clustering in networks». Information Sciences,
316, pp. 308-328.
Gómez, Daniel; Figueira, José Rui y Eusébio, Augusto (2013). «Modeling centrality
measures in social network analysis using bi-criteria network flow optimization
problems». European Journal of Operational Research, 226(2), pp. 354-365.
Gómez, Daniel; González-Arangüena, Enrique; Manuel, Conrado; Owen, Guillermo;
Pozo, Mónica del y Saboyá, Martha (2008). «The cohesiveness of subgroups in
social networks: A view from game theory». Annals of Operations Research, 158(1),
pp. 33-46.
Gómez, Daniel; González-Arangüena, Enrique; Manuel, Conrado; Owen, Guillermo,
Pozo, Mónica del y Tejada, Juan (2003). «Centrality and power in social networks:
a game theoretic approach». Mathematical Social Sciences, 46(1), pp. 27-54.
Gómez, Daniel; Rodríguez, Juan Tinguaro; Yáñez, Javier y Montero, Javier (2016). «A
new modularity measure for Fuzzy Community detection problems based on overlap and grouping functions». International Journal of Approximate Reasoning, 74,
pp. 88-107.
Graves, Alex; Mohamed, AAbdel-rahman y Hinton, Geoffrey (2013). «Speech recognition with deep recurrent neural networks». Proc. International Conference on Acoustics, Speech and Signal Processing, 1, pp. 6645-6649.
Gregory, Steve (2011). «Fuzzy overlapping communities in networks». Journal of Statistical Mechanics: Theory and Experiment, 2, pp. 1-18.
Guzella, Thiago y Caminhas, Walmir (2009). «A Review of Machine Learning Approaches to Spam Filtering». Expert System with Applications, 36(7), pp. 10206-10222.
Harary, Frank (1969). Graph theory. Reading: Addison-Wesley Pub. Co.
Harrison, David y Rubinfeld, Daniel (1978). «Hedonic prices and the demand for clean
air». Journal Environmental Economics & Management, 5, pp. 81-102.
Hedström, Peter (2010). «La explicación del cambio social: un enfoque analítico». En
Noguera, José Antonio. Teoría sociológica analítica. Madrid: Centro de Investigaciones Sociológicas, pp. 211-235.
Hu, Ren-Jie; Li, Qing; Zhang, Guang-Yu y Ma, Wen-Cong (2015). «Centrality Measures
in Directed Fuzzy Social Networks». Fuzzy Information and Engineering, 7(1),
pp. 115-128.
Jungnickel, Dieter (2013). Graphs, Networks, and Algorithms. London: Springer.
Kleinberg, Jon (1999). «Authoritative sources in a hyperlinked environment». Journal
of the ACM, 46(5), pp. 604-632.
Kremer, Jan; Stensbo-Smidt, Kristoffer; Gieseke, Fabian y Pedersen, Kim Steenstrup
(2017). «Big universe, big data: machine learning and image analysis for astronomy». IEEE Intelligent Systems, 32(2), pp. 16-22.
Kubat, Miroslav (2017). An introduction to machine learning. London: Springer.
Lancichinetti, Andrea y Fortunato, Santo (2011). «Limits of modularity maximization
in community detection». Physical Review E. Statistical, Nonlinear, and SoftMatter
Physics, 84(6), pp. 1-8.
Lancichinetti, Andrea; Fortunato, Santo y Radicchi, Filippo (2008). «Benchmark
graphs for testing community detection algorithms». Physical Review E. Statistical,
Nonlinear, and SoftMatter Physics, 78(4), pp. 46-110.
298
CUADERNOS METODOLÓGICOS 60
Lazarsfeld, Paul Felix y Merton, Robert King (1954). «Friendship as a Social Process:
A Substantive and Methodological Analysis». En: Berger, M.; Abel, T. y Charles, H.
(eds.). Freedom and Control in Modern Society. New York: Van Nostrand.
Lee, Chang-Shing; Wang, Mei-Hui; Yen, Shi-Jim; Wei, Ting-Han; Wu, I-Chen; Chou
Ping-Chiang; Chou, Chun-Hsun; Wang, Ming-Wan y Yan, Tai-Hsiung (2016).
«Human vs. computer Go: review and prospect». IEEE Computational Intelligence
Magazine, 11(3), pp. 67-72.
Leskovec, Jure; Lang, Kevin y Mahoney, Michael (2010). «Empirical comparison of
algorithms for network community detection». Proceedings of the 19th international
conference on World Wide Web, pp. 631-640.
Lovász, Llovász (1993). «Random walks on graphs: A survey, Combinatorics Paul
Erdos is Eighty». Bolyai Society Mathematical Studies, 2(2), pp. 1-46.
Mitchell, Tom (1997). Machine Learning. New York: McGraw Hill.
Mohler, George; Short, Martin; Malinowski, Sean; Johnson, Mark; Tita, George; Bertozzi, Andrea y Brantingham, Jeffrey (2015). «Randomized controlled field trials of
predictive policing». Journal of American Statistical Association, 111(512), pp.
1399-1411.
Moreno, Jonathan (1953). Who shall survive? Foundations of sociometry, group
psychotherapy. New York: Forgotten Books.
Moreno-Torres, José García; Sáez, José Antonio y Herrera, Francisco (2012). «Study
on the impact of partition-induced dataset shift on k-fold cross-validation». IEEE
Transactions on Neural Networks and Learning Systems, 23(8), pp. 1304-1313.
Newman, Mark (2006). «Modularity and community structure in networks». Proceedings of the National Academy of Sciences of the United States of America, 103(23),
pp. 8577-8582.
Newman, Mark (2010). Networks: an introduction. Oxford: Oxford University Press.
Newman, Mark (2005). «A measure of betweenness centrality based on random walks».
Social Networks, 27(1), pp. 39-54.
Newman, Mark y Watts, Duncan (1999). «Renormalization group analysis of the smallworld network model». Physics Letters A., 263, pp. 341-346.
Qi, Xingqin; Fuller, Eddie; Wu, Qin; Wu, Yezhou y Zhang, Cun-Quan (2012). «Laplacian centrality: A new centrality measure for weighted networks». Information
Sciences, 194, pp. 240-253.
Page, Lawrence; Brin, Sergey; Motwani, Rajeev y Winograd, Terry (1999). «The PageRank citation ranking: Bringing order to the web». Stanford InfoLab. Disponible
en: http://ilpubs.stanford.edu:8090/422/
Pedregosa, Fabian; Varoquaux, Gael; Gramfort, Alexandre; Michel, Vincent; Thirion,
Bertrand; Grisel, Olivier y Vanderplas, Jake (2011). «Scikit-learn: Machine learning
in Python». Journal of Machine Learning Research, 12, pp. 2825-2830.
Phua, Clifton; Lee, Vincent; Smith, Kate y Gayler, Ross (2010). «A comprehensive survey of data mining-based fraud detection research». Computers in Human Behavior, 28, pp. 1002-1013.
Reichardt, J. (2009). «Structure in Complex Networks». Lecture Notes in Physics, 766,
pp. 152-167.
Sade, Donald (1989). «Sociometrics of Macaca Mulatta III: n-path centrality in grooming networks». Social Networks, 11(3), pp. 273-292.
BIG DATA PARA CIENTÍFICOS SOCIALES. UNA INTRODUCCIÓN
299
Sun, Yanmin; Wong, Andrew y Kamel, Mohamed (2009). «Classification of imbalanced data: a review». International Journal of Pattern Recognition and Artificial Intelligence, 23(4), pp. 687-719.
Weber, Max (1972). Fundamentos metodológicos de la sociología. Madrid: Anagrama.
Wasserman, Stanley y Faust, Katherine (1994). Social network analysis: Methods and
applications. Cambridge: Cambridge University Press.
Cuadernos Metodológicos ha sido galardonada con
el Premio a la Mejor Colección en los XIII Premios
Nacionales de Edición Universitaria otorgados
por la UNE.
Números publicados
59. Internet como modo de administración de encuestas
Vidal Díaz de Rada Igúzquiza, Juan Antonio
Domínguez Álvarez, Sara Pasadas del Amo
58. Investigación cualitativa en salud
Juan Zarco Colón, Milagros Ramasco Gutiérrez,
Azucena Pedraz Marcos, Ana María Palmar Santos
57.
Análisis sociológico con documentos personales
M.ª José Rodríguez Jaume y José Ignacio Garrigós
56.Análisis Cualitativo Comparado (QCA)
Iván Medina, Pablo José Castillo Ortiz, Priscilla ÁlamosConcha y Benoît Rihoux
55.Análisis on line del Banco de Datos del CIS
Jesús Bouso Freijo
54.Análisis discriminante
M.ª Ángeles Cea D'Ancona
53.Simulación basada en agentes. Introducción a NetLogo
José Ignacio García-Valdecasas
52.Investigación Cualitativa Longitudinal
Jordi Caïs, Laia Folguera y Climent Formoso
51.Indicadores de partidos y sistemas de partidos
Leticia M. Ruiz Rodríguez y Patricia Otero Felipe
50.Representación espacial y mapas
Rodrigo Rodrigues-Silveira
49.Introducción al análisis multinivel
Héctor Cebolla Boado
48.El paquete estadístico R
Jesús Bouso Freijo
47.Análisis de contenido de textos políticos.
Un enfoque cuantitativo
Sonia Alonso, Andrea Volkens y Braulio Gómez
46.Análisis de datos incompletos en ciencias sociales
Gonzalo Rivero Rodríguez
45.Análisis de datos con Stata
Modesto Escobar Mercado, Enrique Fernández Macías
y Fabrizio Bernardi
44.La investigación sobre el uso del tiempo
M.ª Ángeles Durán y Jesús Rogero
43.Análisis sociológico del sistema de discursos
Fernando Conde Gutiérrez del Álamo
42.Encuesta deliberativa
María Cuesta, Joan Font, Ernesto Ganuza, Braulio Gómez
y Sara Pasadas
41.Dinámica del grupo de discusión
Jesús Gutiérrez Brito
40.Evolución de la Teoría Fundamentada como técnica
de análisis cualitativo
Jaime Andréu Abela, Antonio García-Nieto
y Ana M.ª Pérez Corbacho
39.El análisis de segmentación: técnicas y aplicaciones de
los árboles de clasificación
Modesto Escobar Mercado
38.Análisis de la Historia de Acontecimientos
Fabrizio Bernardi
37.Teoría Fundamentada Grounded Theory: El desarrollo
de teoría desde la generalización conceptual
Virginia Carrero Planes, Rosa M.ª Soriano Miras y Antonio
Trinidad Requena
36.Manual de trabajo de campo en la encuesta
Vidal Díaz de Rada
35.La encuesta: una perspectiva general metodológica
Francisco Alvira Martín
José Manuel Robles es doctor en Sociología y miembro del Departamento de Sociología Aplicada
de la Universidad Complutense de Madrid (UCM). Ha sido creador y director del Máster en Estadísticas
Oficiales e Indicadores Sociales y Económicos que forma parte de la red EMOS de Eurostat, es editor
de la Revista Española de Investigaciones Sociológicas (CIS) y codirector del grupo de investigación
Data Science and Soft Computing for Social Analytics and Decision Aid, formado por científicos
sociales, estadísticos, matemáticos e informáticos. Su campo de investigación es la comunicación
y la participación política a través de Internet, así como las consecuencias sociales del desarrollo
tecnológico. Trabaja con datos cuantitativos, procedentes de encuestas y de big data, tomando como
referencia la teoría sociológica analítica. Ha publicado varios libros y un importante número de artículos
en diferentes revistas académicas sujetas al sistema de evaluación por pares.
J. Tinguaro Rodríguez es licenciado y doctor en Matemáticas por la Universidad Complutense
de Madrid, imparte clases en el Departamento de Estadística e Investigación Operativa de la
Facultad de Ciencias Matemáticas de la UCM desde 2011, actualmente como profesor titular. Su
labor investigadora en numerosos proyectos competitivos se relaciona con el estudio y desarrollo
de modelos de representación del conocimiento, en particular la lógica borrosa, y su empleo en el
aprendizaje automático. Ha publicado más de un centenar de artículos con proceso de revisión por
pares sobre esta temática y otras afines.
Rafael Caballero es diplomado en Informática por la Universidad Politécnica de Madrid y licenciado
y doctor en Ciencias Matemáticas por la Universidad Complutense de Madrid y profesor titular del
Departamento de Sistemas Informáticos y Computación de la Facultad de Informática de la UCM. Ha
participado en numerosos proyectos competitivos y dirigido la Cátedra Extraordinaria para Big Data
y Analítica HPE-UCM. Es autor de más de una centena artículos científicos, y actualmente codirige un
proyecto nacional sobre la estructura de la comunicación en las redes sociales.
Daniel Gómez es profesor titular del Departamento de Estadística y Ciencia de Datos de la Facultad de
Estadística de la Universidad Complutense de Madrid. Ha sido investigador en siete proyectos del Plan
Nacional (veinte años en total), así como en otros proyectos competitivos de carácter internacional.
Actualmente, es investigador principal de un proyecto del Plan Nacional, codirector del grupo UCM
evaluado como excelente por la Aneca Data Science and Soft Computing for Social Analytics and
Decision Aid, coordinador del programa de doctorado en Análisis de Datos de la UCM y director actual
de un instituto de investigación. Es autor de más de un centenar de artículos científicos en SCOPUS
(https://www.scopus.com/authid/detail.uri?authorId=7102289879) y en WOS (https://publons.com/
researcher/2824047/daniel-gomez/) y con un índice H de 24 (https://scholar.google.es/citations?us
er=QkzaeUsAAAAJ&hl=es&oi=ao).
ISBN 978-84-7476-843-5
GOBIERNO
DE ESPAÑA
MINISTERIO
DE LA PRESIDENCIA
CIS
Centro de
Investigaciones
Sociológicas
Download