Esta hoja muestra cómo acceder a bases de datos Neo4j y también a conectar la salida con Jupyter.
Se puede utilizar el propio interfaz de Neo4j también en la dirección http://127.0.0.1:7474.
In [ ]:
from pprint import pprint as pp
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
%matplotlib inline
matplotlib.style.use('ggplot')
Vamos a cargar la extensión ipython-cypher
para poder lanzar consultas Cypher directamente a través de la hoja.
Todas las celdas que comiencen por %%cypher
y todas las instrucciones Python que comiencen por %cypher
se enviarán a Neo4j para su interpretación.
In [ ]:
!pip install ipython-cypher
In [ ]:
%load_ext cypher
%config CypherMagic.uri='http://neo4j:7474/db/data'
In [ ]:
%config CypherMagic.auto_html=False
La siguiente celda genera una consulta en Cypher que devuelve los 10 primeros nodos. Al inicio la base de datos está vacía, pero se puede probar después para ver la salida. Existen plugins para ver gráficamente la salida como un grafo, pero para eso usaremos el interfaz gráfico del propio Neo4j.
In [ ]:
%%cypher
match (n) return n limit 10;
La carga de datos CSV no se podía realizar directamente desde los ficheros CSV la hoja, porque el CSV que acepta Neo4j no es estándar. Envié un issue para que lo arreglaran, y en la versión 3.3 parece que ya funciona si se añade un parámetro de configuración: https://github.com/neo4j/neo4j/issues/8472
dbms.import.csv.legacy_quote_escaping = false
He añadido al contenedor de la práctica esta opción en la carga de Neo4j. Tened en cuenta que si usáis otra configuración hay que añadírselo.
Primero se crea un índice sobre el atributo Id
de User
, que se usará después para crear usuarios y relacionarlos con la pregunta o respuesta que se ha leído. Si no se hace esto, la carga del CSV es muy lenta.
In [ ]:
%%cypher
CREATE INDEX ON :User(Id);
El siguiente código carga el CSV de las preguntas y respuestas. El código primero todos los nodos con la etiqueta Post
, y después añade la etiqueta Question
ó Answer
dependiendo del valor del atributo PostTypeId
.
In [ ]:
%%cypher
USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM "http://neuromancer.inf.um.es:8080/es.stackoverflow/Posts.csv" AS row
CREATE (n)
SET n=row
SET n :Post
;
A todas las preguntas, se las etiqueta con Question
.
In [ ]:
%%cypher
MATCH (n:Post {PostTypeId : "1"})
SET n:Question;
A todas las respuestas se las etiqueta con Answer
.
In [ ]:
%%cypher
MATCH (n:Post {PostTypeId : "2"})
SET n:Answer;
Se crea un nodo usuario (o se utiliza uno si ya existe) usando el campo OwnerUserId
, siempre que no esté vacío. Nótese que se puede utilizar CREATE
porque esta combinación de relación usuario y pregunta no existe. Cuidado, si se ejecuta dos veces creará el doble de relaciones.
In [ ]:
%%cypher
MATCH (n:Post)
WHERE n.OwnerUserId <> ""
MERGE (u:User {Id: n.OwnerUserId})
CREATE (u)-[:WROTE]->(n);
El lenguaje Cypher tiene una sintaxis de Query By Example. Acepta funciones y permite creación y búsqueda de nodos y relaciones. Tiene algunas peculiaridades que veremos a continuación. Por lo pronto, se puede ver un resumen de características en la Cypher Reference Card.
La anterior consulta utiliza la construcción LOAD CSV
para leer datos CSV dentro de nodos. La cláusula CREATE
crea nuevos nodos. La SET
permite poner valores a las propiedades de los nodos.
En el caso de la consulta de arriba, a todos los datos leídos se les copia los datos de la línea (primer SET
). Después, dependiendo del valor de PostTypeId
, se les etiqueta como :Question
o como :Answer
. Si tienen un usuario asignado a través de OwnerUserId
, se añade un usuario si no existe y se crea la relación :WROTE
.
También hay otros posts especiales que no eran preguntas ni respuestas. A estos no se les asigna una segunda etiqueta:
In [ ]:
%%cypher
match (n:Post) WHERE size(labels(n)) = 1 RETURN n;
Creamos un índice sobre el Id
para acelerar las siguientes búsquedas:
In [ ]:
%%cypher
CREATE INDEX ON :Post(Id);
Añadimos una relación entre las preguntas y las respuestas:
In [ ]:
%%cypher
MATCH (a:Answer), (q:Question {Id: a.ParentId})
CREATE (a)-[:ANSWERS]->(q)
;
Las construcciones %cypher
retornan resultados de los que se puede obtener un dataframe
de pandas
:
In [ ]:
#%%cypher
res = %cypher MATCH q=(r)-[:ANSWERS]->(p) RETURN p.Id,r.Id;
df = res.get_dataframe()
df['r.Id'] = pd.to_numeric(df['r.Id'],downcast='unsigned')
df['p.Id'] = pd.to_numeric(df['p.Id'],downcast='unsigned')
df.plot(kind='scatter',x='p.Id',y='r.Id',figsize=(15,15))
La consulta RQ4 se puede resolver de manera muy fácil. En esta primera consulta se devuelve los nodos:
In [ ]:
%%cypher
// RQ4
MATCH
(u1:User)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u2:User),
(u2)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u1)
WHERE u1 <> u2 AND u1.Id < u2.Id
RETURN DISTINCT u1,u2
;
O bien retornar los Id
de cada usuario:
In [ ]:
%%cypher
MATCH
(u1:User)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u2:User),
(u2)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u1)
WHERE u1 <> u2 AND toInt(u1.Id) < toInt(u2.Id)
RETURN DISTINCT u1.Id,u2.Id
ORDER BY toInt(u1.Id)
;
Y finalmente, la creación de relaciones :RECIPROCATE
entre los usuarios. Se introduce también la construcción WITH
.
WITH
sirve para introducir "espacios de nombres". Permite importar nombres de filas anteriores, hacer alias con AS
e introducir nuevos valores con funciones de Cypher. La siguiente consulta es la misma de arriba, RQ4, pero creando relaciones :RECIPROCATE
entre cada dos usuarios que se ayudan recíprocamente.
In [ ]:
%%cypher
// RQ4 creando relaciones de reciprocidad
MATCH
(u1:User)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u2:User),
(u2)-[:WROTE]->()-[:ANSWERS]->()<-[:WROTE]-(u1)
WHERE u1 <> u2 AND u1.Id < u2.Id
WITH u1 AS user1,u2 AS user2
MERGE (user1)-[:RECIPROCATE]->(user2)
MERGE (user2)-[:RECIPROCATE]->(user1)
;
También se puede buscar el camino mínimo entre dos usuarios cualesquiera. Si existe un camino a través de alguna pregunta o respuesta, la encontrará. Un ejemplo donde hay una comunicación directa:
In [ ]:
%%cypher
MATCH p=shortestPath( (u1:User {Id: '24'})-[*]-(u2:User {Id:'25'}) ) RETURN p
Mientras que con otro usuario la cadena es más larga:
In [ ]:
%%cypher
MATCH p=shortestPath( (u1:User {Id: '324'})-[*]-(u2:User {Id:'25'}) ) RETURN p
Finalmente se pueden encontrar todos los caminos mínimos en donde se ve que tiene que existir al menos un par pregunta/respuesta entre los usuarios que son recíprocos:
In [ ]:
%%cypher
MATCH p=allShortestPaths( (u1:User {Id: '24'})-[*]-(u2:User {Id:'25'}) ) RETURN p
:Tag
para cada uno de los tags que aparecen en las preguntas. Construir las relaciones post-[:TAGGED_BY]->tag
para cada tag y también tag-[:TAGS]->post
Para ello, buscar en la ayuda las construcciones WITH
y UNWIND
y las funciones replace()
y split()
de Cypher. La siguiente consulta debe retornar 5703 resultados:
In [ ]:
%%cypher
MATCH p=(t:Tag)-[:TAGS]->(:Question) WHERE t.name =~ "^java$|^c\\+\\+$" RETURN count(p);
La siguiente consulta muestra los usuarios que preguntan por cada Tag:
In [ ]:
%%cypher
MATCH (t:Tag)-->(:Question)<--(u:User) RETURN t.name,collect(distinct u.Id) ORDER BY t.name;
El mismo MATCH
se puede usar para encontrar qué conjunto de tags ha usado cada usuario cambiando lo que retornamos:
In [ ]:
%%cypher
MATCH (t:Tag)-->(:Question)<--(u:User) RETURN u.Id, collect(distinct t.name) ORDER BY toInt(u.Id);
In [ ]:
In [ ]:
In [ ]:
In [ ]: