Web scraping con Python, Neo4j y R

Siguiendo las indicaciones de Víctor con respecto a la instalación de un entorno virtual de Python, se abren un mundo de posibilidades. Más allá del “Hello world!”, un ejemplo iniciático podría ser el de recopilar datos de esta web (ibón.es) con Python, montar un pequeño grafo en Neo4j con los datos capturados y trabajar con ellos en el entorno R.

El comunmente conocido como scraping parte del interés de recopilación de datos de forma automática y, en la mayoría de casos, masiva. En este ejemplo se utilizará Python como fuente de captura de datos. Python goza de un crecimiento estable en su uso y aplicación así como extensas librerías disponibles. Este lenguaje de programación es de fácil lectura y comprensión para los que tengan una base en C y es muy versátil para la programación orientada a objetos e imperativa conteniendo además otros paradigmas.

Scraping en Python

Una buena guía para empezar con lo que significa scrapping y siguiendo las indicaciones para no hacer un mal uso de dicha técnica, se encuentra el libro Web Scraping with Python. En dicho libro se comenta que un uso responsable y razonable ayuda a la distribucion de la información a la vez que se publicita y da autoría al origen de los datos. En este artículo se sigue tanto la estructura como el código desarrollado por Richard Lawson presente en dicho libro (también presente en un artículo).

Se inicia el estudio con el análisis del contenido que tiene habilitada la recopilación de datos. Posteriormente se evalúan los campos a recopilar de las páginas de interés. Una vez identificados los campos se procede a la automatización de captura de datos en todo el sitio web de forma automatizada teniendo como destino una base de datos de grafos.

Los pasos a seguir a la hora de recopilar datos de una web es identificar las rutas disponibles en el archivo robots.txt del dominio. Vemos que sólo está bloqueada la parte administrativa.

https://ibón.es/robots.txt
User-agent: *
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php
Podemos también identificar qué componentes están instalados para predefinir áreas de interés y formas de adquirir la información.

En el caso que nos ocupa, el sitio web es un WordPress con varios artículos, etiquetas y autores. Empezando por esas tres entidades, se tienen que identificar aquellas páginas que van a contener datos relevantes para ser guardados. Los artículos que se han publicado son los candidatos para este ejemplo en el que encontrar las cuatro entidades referidas anteriormente.

Buceando en el contenido de un artículo [http://ibón.es/2016/12/17/analisis-geografico-de-lineas-de-autobus-en-barcelona-lineas-autobus/] identificamos en el código HTML los cuatro elementos. Las páginas web están estructuradas siguiendo el estándar de W3C de HTML CSS (basado en el estándar XML). En una página de ejemplo se deben identificar los campos que se quieren guardar e identificarlos en el código fuente de la página.Fácilmente se encuentran con la herramienta ‘Inspect Element‘ de Firefox o ‘Inspect‘ en Chrome a través del menú contextual.

<div class="primary content-area">
            <main id="main" class="site-main" role="main">
                <article id="post-188" class="post-188 post type-post status-publish format-standard hentry category-practicas tag-barcelona tag-codigos-postales tag-gis tag-lineas tag-lineas-autobus tag-poligonos tag-r tag-spatiallines tag-spatialpolygons">
    <header class="entry-header">
        <h1 class="entry-title">Análisis geográfico de líneas de autobús en Barcelona (2/2)</h1>
        <div class="entry-meta">
            <span class="posted-on"><a href="http://ibón.es/2016/12/17/analisis-geografico-de-lineas-de-autobus-en-barcelona-lineas-autobus/" rel="bookmark">
        <time class="entry-date published" datetime="2016-12-17T12:44:24+00:00">17 Diciembre, 2016</time>
        <time class="updated" datetime="2016-12-19T10:16:53+00:00">19 Diciembre, 2016</time></a>
      </span>
      <span class="byline"> por <span class="author vcard"><a class="url fn n" href="http://ibón.es/author/esteve/">Esteve</a></span>
      </span>        
    </div>

Se identifican fácilmente el título del artículo y el autor (y su página de perfil).

<footer class="entry-footer">
        <span class="cat-links">Publicado en <a href="http://ibón.es/category/practicas/" rel="category tag">Prácticas</a></span>
    <span class="tags-links">Etiquetado <a href="http://ibón.es/tag/barcelona/" rel="tag">Barcelona</a>, 
<a href="http://ibón.es/tag/codigos-postales/" rel="tag">códigos postales</a>, 
<a href="http://ibón.es/tag/gis/" rel="tag">GIS</a>, 
<a href="http://ibón.es/tag/lineas/" rel="tag">líneas</a>, 
<a href="http://ibón.es/tag/lineas-autobus/" rel="tag">líneas autobús</a>, 
<a href="http://ibón.es/tag/poligonos/" rel="tag">polígonos</a>, 
<a href="http://ibón.es/tag/r/" rel="tag">R</a>, 
<a href="http://ibón.es/tag/spatiallines/" rel="tag">SpatialLines</a>, 
<a href="http://ibón.es/tag/spatialpolygons/" rel="tag">SpatialPolygons</a></span>
<span class="edit-link"><a class="post-edit-link" href="http://ibón.es/wp-admin/post.php?post=188&amp;action=edit">Edita <span class="screen-reader-text">"Análisis geográfico de líneas de autobús en Barcelona (2/2)"</span></a>
</span>    
</footer>

Las etiquetas del artículo se encuentran al final del artículo y se concatenan bajo la clase tags-links.

Partiendo del código comentado al principio del artículo, definimos una función para bajar el contenido de la página web llamada download y con la librería LXML seleccionamos el contenido a recuperar. En el libro se comentan otras librerías de más alto nivel como Beautiful Soup que permiten fácilmente capturar la información a la vez que se pierde la versatilidad del uso de expresiones regulares dispoible en la librería re. Cabe mencionar que al tratar con contenido web en diferentes idiomas y en la versión de Python 2.7 es muy recomendable codificar cada trozo de código como UTF-8 como se observa en la primera línea. De la misma manera, al querer obtener información de un dominio que no tiene todos sus caracteres como ASCII, se puede tratar mediante la librería w3lib y la conversión con safe_url_string.

La captura de datos se realiza identificando los elementos que contienen información. Puede buscarse por el tipo y clase de la etiqueta (por defecto así trabaja LXML) pero es recomendable proveer la estructura en la que se anida la etiqueta para estar seguros de que se está recopilando el contenido de interés. Los elementos se informan por el tipo y por nombre de la clase seguido de un punto (.).

<h1 class="entry-title">

se identifica como

h1.entry-title

En el caso que la etiqueta class del elemento que queremos obtener tiene espacios pueden concatenarse los nombres con un punto (.) como se realiza con el título del artículo. Para denotar etiquetas anidadas se intercala ‘>’.

<span class="author vcard"><a class="url fn n" href="http://ibón.es/author/esteve/">Esteve</a></span>

se identifica como

span.author.vcard > a

La librería LXML como se comentaba anteriormente permite mucha versatilidad como la búsqueda por id (elemento#id_de_la_elemento; a#zoom1), por una etiqueta específica (elemento[nombre_etiqueta=valor_búsqueda]; input[name=product]) o hasta recopilar etiquetas en base a una cadena regexp (div[class^=»thumb25″]).

Para estar seguros que se recopila el primer elemento que coincide con la cadena proporcionada, se añade [0]. La librería cssselect permite obtener el valor del campo deseado. Para guardar sólo el texto y en formato UTF-8 (en Python 2.7 por defecto es ASCII y en versión 3 ya es UTF-8) se añaden las conversiones.

Una vez se dispone de una página de ejemplo capturada, el paso siguiente es el de automatizar el proceso con todas las páginas de interés de ese sitio web.

La rutina de la recopilación empieza por la identificación del dominio y de una página desde dónde empezar a identificar enlaces a analizar. En este caso el inicio puede ser el dominio en sí. A partir de esa página web se compilan todos los enlaces existentes en dicha página. En caso de que los enlaces cumplan con una estructura definida, se almacenan y se procederá a consultar la misma y la recopilación de más enlaces. La función link_crawler continúa hasta que no haya más enlaces a procesar. Con dicha lista se procesan las páginas para obtener bien más enlaces o contenido.

La llamada se hace con link_crawler(página_de_inicio,expresion_regular). Los enlaces de los articulos son del estilo http://ibón.es/2016/12/17/analisis-geografico-de-lineas-de-autobus-en-barcelona-lineas-autobus/ y también sabemos que se pueden encontrar bajo otras páginas como por ejemplo http://ibón.es/category/practicas/. En este ejemplo se definen las páginas que contengan tres números separados de una barra (identificativo de un artículo al ser la fecha de publicación del mismo), las páginas de las etiquetas (tag), prácticas, noticias y autor (author).

r'(.*?)(/\d+./\d+./\d+./|tag|practicas|noticias|author)(.*?)

No se quiere recopilar el resto de enlaces como ‘quienes somos’ o enlaces externos. A modo ilustrativo se muestran los primeros enlaces recopilados y su inclusión o no a la lista de páginas a analizar. La definición de la cadena de texto para las expresiones regulares se encabeza con r el contenido es puro para explicitar que y no contiene combinaciones de caracteres especiales como saltos de línea (\n).

El dominio del ejemplo tiene caracteres que no son ASCII, debemos transformarlo a UTF-8 para poder gestionar la descarga y análisis mediante safe_url_string.

Queda la última parte del desarrollo de la recopilación de datos de las páginas: la automatización de la compilación de enlaces y la obtención de los elementos deseados. Siguiendo la estructura del código de Richard, se incluye un callback al código anterior de obtención de los enlaces con el nombre ScrapeCallBack. En dicha función se analiza el tipo de página que se ha bajado para determinar qué campos obtener. En este ejemplo sólo hay páginas de artículos pero es normal obtener páginas de diferentes tipos como por ejemplo páginas de reseñas de libros y de autores, con formatos, estructuras y contenido diferenciado. Esta función de callback también incluirá la creación del grafo en Neo4j de los datos capturados.

Neo4j es uno de los proyectos más relevantes de bases de datos de grafos. Aunque no del todo conocidos, los grafos permiten establecer relaciones entre entidades de forma similar a cómo las estructuramos en nuestras mentes y con mucha flexibilidad para buscar relaciones complejas entre entidades. Tanto las redes sociales, recomendadores de productos o análisis de fraude son algunos de los casos de uso al poder procesar relaciones con origen y destino en las mismas entidades y con varios niveles de distancia.

A modo introductorio hay tres conceptos clave en Neo4j: nodos (node), relaciones (path) y etiquetas (label). Los nodos son las entidades/objetos. Las relaciones establecen conexiones entre nodos con dirección (por ejemplo, juan es padre de miguel), bidireccional (por ejemplo, juan es amigo de laura) o hasta sin dirección si no es relevante el origen y destino o es ambivalente. Tanto los nodos como las relaciones pueden tener tipología (autor, ha escrito, etc) como etiquetas descriptivas tales como nombres, fechas, etc.

Un par de libros interesantes sobre los grafos en Neo4j son Graph Databases y Neo4j Cookbook.

En el siguiente ejemplo se crean en lenguaje Cypher nodos de tipo Author (victor y esteve) y Article (Análisis…) así como la relación entre un author (esteve) y un articulo (esteve-ha escrito->articulo). El lenguaje Cypher es eminentemente visual, descriptivo y declarativo.

CREATE (esteve:Author { name: "Esteve", from: "Spain" }),
(victor:Author { name: "Victor", from: "Spain" }),
(analysisgeo2:Article { name:"Análisis geográfico de líneas de autobús en Barcelona (2/2)", url: "http://ibón.es/2016/12/17/analisis-geografico-de-lineas-de-autobus-en-barcelona-lineas-autobus/" }),
(esteve)-[:Wrote {date:"2016-12-16"}]->(analysisgeo2)

Hay notables diferencias con una base de datos relacional tradicional como su adaptabilidad para cualquier cambio en la definición del modelo de datos. En una base de datos tradicional, la base de datos no almacena la lógica de las relaciones entre entidades sino que éstas están en los diseños funcionales o en los procesos de cargas de datos. En Neo4j las entidades tienen las relaciones autodefinidas. En una relacional un cambio en el modelo implica rehacer las tablas, índices así como los procesos de carga de datos para acomodar el nuevo modelo. En las de grafos tan sólo hace falta actualizar las relaciones entre las entidades.

El contenido de esta función de callback parte del diseño anterior de captura de los elementos de un artículo. En este caso en vez de pasar una URL específica, recuperará el valor pasado por la función de gestión de enlaces.

Hay diferentes librerías de conexión a Neo4j para Python. Para la creación del grafo podemos utilizar la librería py2neo que extiende el driver oficial de Neo4j. Aunque existe una versión oficial, la alternativa de py2neo es muy interesante e intuitiva. Llamando a los módulos de Graph se conecta a la base de datos. Con los de Node y Relationship se crean nodos y sus relaciones. Para empezar se pueden utilizar las funciones predeterminadas para crear los nodos y relaciones pero cuando los grafos empiezan a ser más complejos es preferible dar el salto a pasar los comandos en formato Cypher.

py2neo trabaja con una representación de un grafo en local y con la información almacenada en la base de datos de Neo4j. Así pues, en local se definen los nodos a crear y luego se materializan en la base de datos. Es por eso que inicialmente se define el objeto Node() y posteriormente se ejecuta graph.merge(), que crea el nodo en la base de datos remota o fusiona con un nodo existente en base a coincidencias parametrizadas como por ejemplo si tienen el mismo nombre. Las relaciones en este ejemplo se crean directamente en la base de datos a la vez que se definen.

Siguiendo el libro de Richard, se completa el código con funciones de gestión del cache (para agilizar el procesado de las páginas), de los proxies así como de velocidad de acceso a las páginas (throttle; para evitar bloqueos). En este ejemplo se han dejado fuera para simplificar el codigo pero se han incluído algunas clases como Downloader para modular el código.

Hay algunos puntos a tener en cuenta a la hora de realizar el código de recopilación de datos:

  • corrección de URL mal definidas (con caracteres inválidos)
  • corrección de contenido HTML mal definido con caracteres como � que hacen saltar por los aires el código de captura del contenido de los elementos
  • bucles recursivos de búquedas tipo id=23&id=23&id=23….

Finalmente se puede paquetizar y crear módulos para la reutilización de las funciones y variables con otras páginas y proyectos y ganar en agilidad y complejidad.

Grafos en Neo4j

Una vez ejecutado el código se puede visualizar la información que se ha guardado pudiendo ver los cuantos artículos ha escrito cada autor y qué etiquetas se han reutilizado a través del entorno web de Neo4j.

Las consultas en Cypher declaran inicialmente los objetos de interés y posteriormente se reclama algunos de esos objetos y relaciones. Para devolver todos los nodos de tipo autor (Author) se encuentran se detalla

MATCH (n:Author) RETURN n

El límite a 25 en este caso no aplica pero por defecto el entorno web de Neo4j lo asigna.

Autores y artículos en ibón (Neo4j)

Los atributos (etiquetas) de un autor se muestran al seleccionarlo.

Detalle de las etiquetas de un autor en ibón (Neo4j)

De la misma manera se pueden consultar las categorías de los artículos.

Artículos y categorías en ibón (Neo4j)

Desplegando las características de un artículo se muestran sus etiquetas.

Detalle de etiquetas de artículo en ibón (Neo4j)

El artículo de Ranjan Kumar da muchas pistas del potencial.

Modelado en R

El entorno web de Neo4j permite hacer consultas y visualizaciones de los datos aunque con limitaciones. En caso de querer trabajar con los datos en un entorno ya conocido como puede ser R, fácilmente se puede conectar a la base de datos y tanto consultarla como aplicar cambios mediante la librería RNeo4j.

Desde R se procede a conectar a la base de datos con las credenciales explicitadas y a partir de allí se puede consultar para recuperar un listado con información de las entidades (nodos y relaciones) o bien en formato tabulado muy recomendable para trabajar como data frame.

Se realiza una consulta con cypherToList() para obtener los artículos y las categorías en formato lista. Este formato es interesante porque contiene toda la información sobre los elementos que se desee con la versatilidad según laexistencia de atributos o relaciones pero carece de la estructura regular de una tabla. Con la consulta cypher() se deben especificar los atributos a recuperar ya que el resultado debe tener el mismo número de atributos por registro (como si se tratara de una consulta SQL).

En este caso se quieren recuperar todas las categorías relacionadas con los autores. Nótese el condicional optional match para poder recuperar todos los autores aunque no tengan categorías a modo de símil con una outer join.

Las categorías se relacionan con los artículos y éstos con los autores así que para poder obtener el resultado se define una relación con uno o dos saltos ([r*1..2]). En el primer salto desde los autores se encuentran los nodos de artículos. En el segundo salto se encuentran las categorías.

De una forma fácil e intuitiva se consigue recuperar información condicional. El resultado para el autor ibón es <NA> como era de esperar.

Fácilmente se pueden obtener unos someros cálculos de número de categorías utilizadas por cada usuario. En este caso esteve utiliza más categorías y las repite en sus artículos.

library("RNeo4j")
library("plyr")
library("dplyr")
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:plyr':
## 
##     arrange, count, desc, failwith, id, mutate, rename, summarise,
##     summarize
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
graph = startGraph("http://localhost:7474/db/data/",user="neo4j",password="password")
#Return content as a list including relationships
head(cypherToList(graph, "MATCH (s:Tag)-[r]-(a:Article) RETURN r,a.name as author, s.name as tag, COUNT(s) as num_available"))
## [[1]]
## [[1]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[1]]$author
## [1] "De Data Science a Intelligent Apps (I)"
## 
## [[1]]$tag
## [1] "intelligent apps"
## 
## [[1]]$num_available
## [1] 1
## 
## 
## [[2]]
## [[2]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[2]]$author
## [1] "Instalar R y RStudio Server en un VPS"
## 
## [[2]]$tag
## [1] "R"
## 
## [[2]]$num_available
## [1] 1
## 
## 
## [[3]]
## [[3]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[3]]$author
## [1] "Hortonworks en un VPS con Ubuntu 16.04"
## 
## [[3]]$tag
## [1] "hadoop"
## 
## [[3]]$num_available
## [1] 1
## 
## 
## [[4]]
## [[4]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[4]]$author
## [1] "Análisis geográfico de líneas de autobús en Barcelona (2/2)"
## 
## [[4]]$tag
## [1] "líneas autobús"
## 
## [[4]]$num_available
## [1] 1
## 
## 
## [[5]]
## [[5]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[5]]$author
## [1] "Instalar R y RStudio Server en un VPS"
## 
## [[5]]$tag
## [1] "Instalación"
## 
## [[5]]$num_available
## [1] 1
## 
## 
## [[6]]
## [[6]]$r
## < Relationship > 
## CONTAINS
## 
## 
## [[6]]$author
## [1] "Hortonworks en un VPS con Ubuntu 16.04"
## 
## [[6]]$tag
## [1] "big data"
## 
## [[6]]$num_available
## [1] 1
#Return content suitable for a data frame
#Retrieving the tags related to authors regardless if they have any associated tag
cypher(graph,"match (a:Author)
optional MATCH (a)-[r*1..2]-(t:Tag) RETURN a.name as name, t.name order by name asc")
##              name              t.name
## 1          Esteve               knitr
## 2          Esteve                   R
## 3          Esteve           WordPress
## 4          Esteve         Instalación
## 5          Esteve                   R
## 6          Esteve      RStudio Server
## 7          Esteve Ubuntu Server 16.04
## 8          Esteve                 VPS
## 9          Esteve           Barcelona
## 10         Esteve    códigos postales
## 11         Esteve                 GIS
## 12         Esteve                 KML
## 13         Esteve           polígonos
## 14         Esteve                   R
## 15         Esteve     SpatialPolygons
## 16         Esteve           Barcelona
## 17         Esteve    códigos postales
## 18         Esteve                 GIS
## 19         Esteve              líneas
## 20         Esteve      líneas autobús
## 21         Esteve           polígonos
## 22         Esteve                   R
## 23         Esteve        SpatialLines
## 24         Esteve     SpatialPolygons
## 25           Ibón                <NA>
## 26 Victor Cutilla        data science
## 27 Victor Cutilla               flask
## 28 Victor Cutilla    intelligent apps
## 29 Victor Cutilla              python
## 30 Victor Cutilla            big data
## 31 Victor Cutilla              hadoop
## 32 Victor Cutilla         hortonworks
## 33 Victor Cutilla         Instalación
## 34 Victor Cutilla Ubuntu Server 16.04
## 35 Victor Cutilla                 VPS
#Retrieving the tags related to authors
cypher(graph,"match (a:Author)
optional MATCH (a)-[r*1..2]-(t:Tag) RETURN a.name as name, count(t) as num_tags , count(distinct(t)) as num_tags_unique order by name asc")
##             name num_tags num_tags_unique
## 1         Esteve       24              16
## 2           Ibón        0               0
## 3 Victor Cutilla       10              10
#Retrieving the amount of times a tag is used on an article (ie, relationships)
head(cypher(graph,"match (a:Author)
optional MATCH (a)-[r*1..2]-(t:Tag) RETURN a.name as name, t.name, count(r) as num_tags order by num_tags desc"))
##     name           t.name num_tags
## 1 Esteve                R        4
## 2 Esteve        Barcelona        2
## 3 Esteve              GIS        2
## 4 Esteve  SpatialPolygons        2
## 5 Esteve        polígonos        2
## 6 Esteve códigos postales        2
results<-cypher(graph,"match (a:Author)
match (b:Article)
match (a)-[]-(b)
optional match (b)-[]-(t:Tag)
return a.name as author, b.name as article, t.name as tag order by author, article, tag asc")
head(results)
##   author                                                     article
## 1 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
## 2 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
## 3 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
## 4 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
## 5 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
## 6 Esteve Análisis geográfico de líneas de autobús en Barcelona (1/2)
##                tag
## 1        Barcelona
## 2              GIS
## 3              KML
## 4                R
## 5  SpatialPolygons
## 6 códigos postales
#Get occurences within the results
lapply(results,function(x) table(unlist(x)))
## $author
## 
##         Esteve           Ibón Victor Cutilla 
##             24              1             10 
## 
## $article
## 
## Análisis geográfico de líneas de autobús en Barcelona (1/2) 
##                                                           7 
## Análisis geográfico de líneas de autobús en Barcelona (2/2) 
##                                                           9 
##                      De Data Science a Intelligent Apps (I) 
##                                                           4 
##                      Hortonworks en un VPS con Ubuntu 16.04 
##                                                           6 
##                       Instalar R y RStudio Server en un VPS 
##                                                           5 
##             Publicar directamente a WordPress con RMarkdown 
##                                                           3 
##                                       Se acerca el invierno 
##                                                           1 
## 
## $tag
## 
##           Barcelona            big data    códigos postales 
##                   2                   1                   2 
##        data science               flask                 GIS 
##                   1                   1                   2 
##              hadoop         hortonworks         Instalación 
##                   1                   1                   2 
##    intelligent apps                 KML               knitr 
##                   1                   1                   1 
##              líneas      líneas autobús           polígonos 
##                   1                   1                   2 
##              python                   R      RStudio Server 
##                   1                   4                   1 
##        SpatialLines     SpatialPolygons Ubuntu Server 16.04 
##                   1                   2                   2 
##                 VPS           WordPress 
##                   2                   1
results %>%
  group_by(author) %>%
  summarise(num_articles=n_distinct(article),num_tags = n_distinct(tag), num_tags_tot=length(tag), reuse_tags=num_tags/num_tags_tot, mean_tags=num_tags/num_articles) %>%
  as.data.frame
##           author num_articles num_tags num_tags_tot reuse_tags mean_tags
## 1         Esteve            4       16           24  0.6666667         4
## 2           Ibón            1        1            1  1.0000000         1
## 3 Victor Cutilla            2       10           10  1.0000000         5
results %>%
  group_by(author,tag) %>%
  summarise(num_used=n_distinct(article)) %>%
  as.data.frame %>%
  arrange(desc(num_used))
##            author                 tag num_used
## 1          Esteve                   R        4
## 2          Esteve           Barcelona        2
## 3          Esteve    códigos postales        2
## 4          Esteve                 GIS        2
## 5          Esteve           polígonos        2
## 6          Esteve     SpatialPolygons        2
## 7          Esteve         Instalación        1
## 8          Esteve                 KML        1
## 9          Esteve               knitr        1
## 10         Esteve              líneas        1
## 11         Esteve      líneas autobús        1
## 12         Esteve      RStudio Server        1
## 13         Esteve        SpatialLines        1
## 14         Esteve Ubuntu Server 16.04        1
## 15         Esteve                 VPS        1
## 16         Esteve           WordPress        1
## 17           Ibón                <NA>        1
## 18 Victor Cutilla            big data        1
## 19 Victor Cutilla        data science        1
## 20 Victor Cutilla               flask        1
## 21 Victor Cutilla              hadoop        1
## 22 Victor Cutilla         hortonworks        1
## 23 Victor Cutilla         Instalación        1
## 24 Victor Cutilla    intelligent apps        1
## 25 Victor Cutilla              python        1
## 26 Victor Cutilla Ubuntu Server 16.04        1
## 27 Victor Cutilla                 VPS        1

A simple vista se ve que Esteve utiliza más categorías que Víctor y que Víctor no reutiliza categorías en sus artículos (eguramente por tratarse de temáticas diferentes). R es la categoría más utilizada y lo más seguro es que aparezca también asociada a este artículo. De media Esteve utiliza 4 categorías por atrículo; ¿cuantas habrá en este último?

Al disponer de un flujo de captación de datos con Python, su inclusión en una base de datos de grafos Neo4j y la gestión de dichos datos desde R, las posibilidades que se abren son inmensas para la realización de un proyecto completo.