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