Giter Club home page Giter Club logo

estudio_biblia_grafo's Introduction

Trabajo Mundo Interconectado

Diego Fernández López
Federico Alfaro García

Introducción

En el presente trabajo utilizaremos las redes con el objetivo de extraer información de un texto extenso, ver las palabras relevantes y conexiones entre las mismas.
Como objeto de estudio hemos elegido la Biblia. Entre las razones de esta elección sobresalen que es fácil de conseguir, su temática es variada, carece de derechos de autor, está traducida a una gran cantidad de idiomas (lo que permitiría hacer estudios comparativos en futuros trabajos) y su larga extensión. Además está escrita en un lenguaje fácilmente comprensible lo que facilita el estudio posterior.

Consiguiendo Datos:

El primer paso consiste en transformar el texto de la biblia en un grafo. La idea principal es considerar las palabras como nodos y los enlaces entre palabras se dan si las dos palabras aparecen seguidas.

Para ello se ha escrito código en el lenguaje Python, el cual explicamos a continuación:

Primero cargamos librerías:

import os
os.chdir('files')
import time

tiempo_inic=time.clock()
import nltk#librería para el tratamiento sintáctico
from itertools import imap,ifilter,islice#tanto imap como ifilter son iteradores, son usados
               #por razones de eficiencia ver https://docs.python.org/2/library/itertools.html
import networkx as nx
from nltk.stem import SnowballStemmer
spanish_stemmer = SnowballStemmer("spanish")
#ajustamos el sistema para tratar el utf-8:
import sys
reload(sys)
sys.setdefaultencoding('utf8')
def pairwise(iterable):
    """otro iterador:(extraido de https://docs.python.org/2/library/itertools.html)
    s -> (s0,s1), (s1,s2), (s2, s3), ...
    """
    a, b = next(iterable),next(iterable),
    while 1:
        yield (a, b)
        a,b=b,next(iterable)
endphase='.!?¿!-;.:'
import stop_words
stopwords=stop_words.get_stop_words(u'spanish')#cargamos las stopwords del idioma

La biblia se encuentra en un fichero txt codificado en utf-8 "BIBLIA.txt", al cual aplicamos el siguiente código:

text=open('BIBLIA.txt','r').read()
raw=str.decode( text,'utf8')
tokens = nltk.word_tokenize(raw,'spanish')
tokensLow=imap(unicode.lower,tokens)
from collections import defaultdict
root_original=defaultdict(set)
def getroot(original):
    root=spanish_stemmer.stem(original)
    root_original[root].add(original)
    return root
relevantWords=imap(getroot,ifilter(lambda x: not(x in stopwords), tokensLow) )
digits='01234566789&#()[]'
def punt_gestion(pairs):
    for (a,b) in pairs:
        if a in endphase or b in endphase or (a[0] in digits) or (b[0] in digits):
            pass
        else:
            yield (a,b)
parejas=(punt_gestion(pairwise(ifilter(lambda x: not(x in ','),relevantWords) )))

Este código lee la biblia y separa las palabras. Hay palabras que no se tienen en cuenta, las llamadas "stopwords", ya que no aportan información, como por ejemplo las preposiciones y los artículos; existen disponibles por internet estas listas de palabras, una por cada idioma. Otro tratamiento importante en este paso es que convertimos todas las palabras a su raíz, ya que no es importante diferenciar entre singular/plural, masculino/femenino o los diferentes tiempos verbales; de esta manera reducimos el número de nodos sin perfer información.

Con ayuda de la función pairwise, recorremos las palabras por parejas adyacentes, saltándonos aquellas que contengan signos que impliquen un cambio de frase.

Un ejemplo de esto sería:

"Examinadlo todo; retened lo bueno."

Lo que dividimos en palabras y símbolos:

["examinadlo","todo", ";" ,"retened", "lo", "bueno","."]

Transformamos las palabras en sus raíces (pues así buen/o/a/s se representa por el mismo símbolo) y obviamos las stopwords:

["examin","tod", ";" ,"reten", "buen","."]

Juntamos las palabras contiguas con la función pairwise obteniendo:

[("examin", "tod"),("tod", ";") (";","reten") ("reten","buen") ("buen",".")]

Filtramos las parejas que contengan símbolos impliquen un cambio de frase.Resultando:

[("examinadlo", "todo"),("reten","buen)]

Es de resaltar que los iteradores evitan el tener que guardar las listas intermedias permitiendo una implementación más eficiente tanto en memoría como en tiempo.

Finalmente contabilizamos cuantas veces se repite cada pareja de palabras (obteniendo un diccionario (str,str)->Integer)

def f(x,y):
     x[y]=1+x[y]
     return x

countParejas=reduce(f,parejas,defaultdict(int))

En un principio probamos a hacer un digrafo donde los nodos sean las palabras y hay un vertice $(a,b)$ si $b$ ha aparecido directamente después que $a$. Pero nos sale un grafo muy denso, por lo nos restringimos a cuando la secuencia se ha repetido al menos 5 veces:

aux=ifilter(lambda x:countParejas[x]>5,countParejas)          
wordGraph=nx.DiGraph();
wordGraph.add_edges_from(aux)

El grafo conseguido lo guardaremos para su posterior tratamiento tanto en formato csv (tabla tipo excel) como en formato gml:

nx.write_gml(wordGraph,'grafo.gml')
import pandas as pd
tabla=pd.DataFrame(wordGraph.edges(),None,['Source','Target'])
tabla.to_csv('grafo.csv')

Tratamiento del Grafo:

En esta sección ya tenemos construido el grafo. En una primera aproximación extraeremos las palabras más relevantes, usando el siguiente código:

from itertools import count,izip
import pandas as pd
import networkx as nx

data=pd.read_csv('grafo.csv')#cargamos el grafo en formato tabla
grafo=nx.DiGraph()
grafo.add_edges_from(zip(data['Source'],data['Target']) ) #reconstruimos el grafo desde la tabla
cuentas=pd.DataFrame()#tabla con distintas puntuaciones de nodos
cuentas['nodos']=list(grafo)
def add(func,name):
    """
    func:grafo->dict(nodos=>|R)
    name:str
    añade una nueva columna (si es que no existe) con los resultados dados por func
    """
    if name in cuentas.columns:
        pass
    else:
        sol=func(grafo)
        cuentas[name]=(map(lambda x:sol[x], cuentas['nodos']) )

def sort(columna,n=10):
    '''
    columna:str
    Devuelve una tabla con los n nodos mejor puntuados según la columna 'columna'
    '''
    mejores=sorted(zip(cuentas['nodos'],cuentas[columna],count(0)),key=lambda x:-x[1] )
    
    return cuentas.loc[map(lambda x:x[2],mejores[:10])]
add(nx.degree,'número nodos adyacentes')
add(nx.pagerank,'pagerank')
add(nx.betweenness_centrality,'centralidad')

Algunos estadísticos interesantes son:

estadisticos={}
#numero de vértices:
estadisticos['vertices']=len(grafo)
#número de aristas:
estadisticos['aristas']=len(grafo.edges())
#clústering:
import numpy as np
estadisticos['clustering']=np.mean(nx.clustering(nx.Graph(grafo)).values() )
estadisticos
{'aristas': 5500, 'clustering': 0.169398636244776, 'vertices': 1540}

Este coeficiente de clustering hay que compararlo con la probabilidad de que dos nodos sean vecinos, que es $p=\sharp aristas2/(\sharp vertices(\sharp vertices-1))=0.00464$. El coeficiente de cluster es mucho mayor a este $p$, por lo que nuestro grafo está muy interconectado; esto significa que dos palabras adyacentes a una tercera tienden a aparecer adyacentes entre ellas también, más de lo que cabría esperar.

Mostramos los resultados:

%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(sorted(cuentas['pagerank'],key=lambda x:-x),'o' )
sort('pagerank')
nodos número nodos adyacentes pagerank centralidad
975 hij 351 0.032262 0.106998
1055 jehov 326 0.023401 0.105755
1279 rey 170 0.016629 0.038384
817 dios 254 0.016264 0.056942
625 tierr 180 0.014246 0.048344
436 cas 132 0.010339 0.022818
384 dij 109 0.010187 0.018987
388 dic 63 0.009196 0.004174
139 tod 125 0.008168 0.028112
1210 delant 88 0.007917 0.016370

png

plt.plot(sorted(cuentas['número nodos adyacentes'],key=lambda x:-x),'o' )
sort('número nodos adyacentes')
nodos número nodos adyacentes pagerank centralidad
975 hij 351 0.032262 0.106998
1055 jehov 326 0.023401 0.105755
817 dios 254 0.016264 0.056942
625 tierr 180 0.014246 0.048344
1279 rey 170 0.016629 0.038384
436 cas 132 0.010339 0.022818
1063 hombr 128 0.006439 0.025643
139 tod 125 0.008168 0.028112
814 israel 123 0.006778 0.024241
145 puebl 112 0.007369 0.015338

png

plt.plot(sorted(cuentas['centralidad'],key=lambda x:-x),'o' )
sort('centralidad')
nodos número nodos adyacentes pagerank centralidad
975 hij 351 0.032262 0.106998
1055 jehov 326 0.023401 0.105755
817 dios 254 0.016264 0.056942
625 tierr 180 0.014246 0.048344
1279 rey 170 0.016629 0.038384
139 tod 125 0.008168 0.028112
1063 hombr 128 0.006439 0.025643
814 israel 123 0.006778 0.024241
436 cas 132 0.010339 0.022818
461 man 94 0.007442 0.020690

png

Por lo que se ve que en este caso que los tres estadísticos dan resultados similares a la hora de señalar las palabras más relevantes.

Mostramos el grafo cociente de las palabras más puntuadas:

nodos=set(list(sort('número nodos adyacentes')['nodos'])+list(sort('pagerank')['nodos'])+list(sort('centralidad')['nodos']) )
print( ','.join(nodos) )
nx.draw_networkx(nx.Graph(grafo.subgraph(nodos) ))

png

Por lo que se ve una alta relación entre los nodos.

Comunidades:

En está sección extraeremos comunidades siguiendo distintos algoritmos, siguiendo sobre el programa anterior:

from collections import defaultdict
from itertools import izip
def groupBy(columna):
    #devuelve un diccionario:valoresColumna->[Nodos]
    #                             x->[Nodos(i)|Columna(i)==x]
    def f(x,(nodo,group) ):
        x[group].append(nodo)
        return x
    return reduce(f,izip(cuentas['nodos'],cuentas[columna]),defaultdict(list))

import community#http://mlg.ucd.ie/files/summer/tutorial.pdf
add(lambda x:community.best_partition(nx.Graph(x)),'comBPart')
comunidad=groupBy('comBPart')
cuentas.to_csv('grafoFinal.csv')

Donde comunidad es un diccionario que asigna a cada nodo un valor en la comunidad, partiendo de esto creamos el grafo cociente:

nodos=set(comunidad);
vertices={(i,j) for i in nodos for j in nodos  if (
        any(grafo.has_edge(a,b) for a in comunidad[i] for b in comunidad[j]  ) ) }
Gcomunidad=nx.Graph();Gcomunidad.add_edges_from(vertices)
layout=nx.fruchterman_reingold_layout(Gcomunidad)
nx.draw_networkx(Gcomunidad,node_size=map(lambda x:len(comunidad[x]),Gcomunidad),pos=layout)

png

for n,subgraph in (imap(lambda (n,gr):(n,grafo.subgraph(gr)),comunidad.items() ) ):
    plt.figure()
    plt.title('comunidad '+str(n))
    layout=nx.layout.spectral_layout(subgraph)
    if n<=10:
        nx.write_gml(subgraph,'comunidad '+str(n))
    nx.draw_networkx(subgraph,pos=layout)

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

En el grafo el tamaño de los nodos es proporcional al número de palabras que contiene. El histograma de su tamaño es:

from matplotlib.pyplot import hist
_=hist(list(filter(lambda x:x<=300,map(
                lambda x:(len(comunidad[x])),Gcomunidad ) ) ) ,bins=50
    )

png

Esto nos indica una gran cantidad de parejas de palabras repetidas (comunidades de tamaño menor o igual que 3): salida

from itertools import *
f=open('out.txt','w')
f.write('\n'.join([str( (l,map(lambda x:list(root_original[x])[:3],l)) ) 
                   for l in filter(lambda x:len(x)<=3,map(lambda x:comunidad[x],Gcomunidad ) ) 
    ] ),)
f.close()

De esto se denota que en la biblia se tienden a repetir expresiones, pongamos algunos casos:

  • huerto eden /cuidadano romano: puede considerarse un concepto unitario.
  • trigo cebada: sin duda proviene de la enumeración de "trigo y cebada" por referirse a la fuente de alimentación
  • entensar arco: una acción muy repetida.
  • ...

Estudiando las comunidades:

Por desgracia Python no resulta la opción más comoda ni efectiva a la hora de dibujar grandes redes. Por lo que a partir de ahora usaremos el programa Gephi.

En el código anteriormente usado hemos guardado en los archivos grafoFinal.csv y en grafo.csv tanto el grafo como la información obtenida.

Desde el Gephi importamos las tablas recreando el grafo.

Sobre el grafo aplicamos el algoritmo de ForceAtlas obteniendo (doble click para verlo en toda su extensión):

Grafo Palabras

Si nos fijamos en la parte inferior del grafo vemos que la comunidad azul relacciona muchos nombres propios con hijos.
En cuanto al resto de comunidades grandes las filtraremos para estudiarlas individualmente.
Con ayuda de Gephi extraemos el subgrafo de cada comunidad de gran tamaño , calculamos el pagerank del mismo y aplicamos algoritmos de redistribución de los nodos y de modularidad.

A continuación mostraremos cada subgrafo e interpretaremos los que consideremos más interesantes:

Comunidad 0

Communidad 0

Comunidad 1

Communidad 1

Esta es una de las comunidades más grandes en las que se divide el grafo. Los nodos más importantes son "dios" y "Jehová", que en realidad se refieren a lo mismo en la biblia, por lo que comparten muchas conexiones. Como cabía esperar, suelen ir acompañados de términos positivos, como "bendición", "esperanza", "justicia" o "todopoderoso". En la periferia se pueden ver alguno grupos especiales: abajo a la izquierda se puede ver el nodo "si" (forma condicional) acompañado de verbos; el verbo "hacer" es bastante importante también, con conexiones diversas.

Comunidad 2

<img src="files/C2.png",width=500,height=500>

Comunidad 3

Communidad 3

Comunidad 4

Communidad 4

Comunidad 5

<img src="files/C5.png",width=500,height=500>

En esta comunidad se ven claramente dos clases de palabras, relacionadas entre sí. Por un lado está el grupo de intervalos de tiempo con números, que forman expresiones referentes al paso del tiempo como "mil años" o "cuarenta días". Y por otro lado hay recursos naturales, como minerales ("oro", "plata", "bronce"), ganado ("oveja", "buey"), "madera" y "piedra".

Comunidad 6

<img src="files/C6.png",width=500,height=500>

Comunidad 7

Communidad 7

Comunidad 8

<img src="files/C8.png",width=500,height=500>

Esta comunidad trata claramente sobre la geneología que se cuenta en la biblia. El nodo más importante de este grafo es "hij" (raíz de hijo, hija, hijos e hijas), que está conectado a muchos de los nombres que se mencionan en la biblia y al resto de nodos importantes: "padre", "hermano", "mayor" y "menor".

Comunidad 9

<img src="files/C9.png",width=500,height=500>

Esta comunidad está formada por tres pequeños grupos conectados entre sí. Arriba tenemos la palabra "fuego", acompañada por adjetivos como "consumidor" y "ardiente"; en el centro tenemos "cielo", conectado a las estrellas y la luna; y por último abajo destaca la palabra "ojos".

Comunidad 10

<img src="files/C10.png",width=500,height=500>

Comunidad 13

<img src="files/C13.png",width=500,height=500>

Comunidad 15

<img src="files/C15.png",width=500,height=500>

Palabras autoreferentes:

Si nos fijamos, hay algunos nodos con bucles, es decir, palabras idénticas o que comparten raíz que aparecen dos veces seguidas. Esto nos puede parecer un fenómeno extraño, por lo que hemos buscado algunos ejemplos en el texto. Algunas veces se trata de que se termina una frase y se empieza la siguiente con la misma palabra y otras muchas veces son expresiones como "piedra sobre piedra", "ojo por ojo","día a día" o "por los siglos de los siglos"; estas palabras aparecen con autoenlace porque hemos eliminado las stopwords.

Para mostrar los ejemplos en su contexto hemos extraido un par de párrafos con cada caso (script): párrafos

Conclusiones

El estudio de un texto tan extenso a través de su grafo de palabras no es sencillo. Hemos tenido que reducir la complejidad del grafo mediante algunos trucos y aún así el grafo total es demasiado grande para entenderlo en su totalidad.

Parece, al menos en este caso, relativemente fácil descubrir a los entes con mayor protagonismo (Dios/Jehová) y las distintas comunidades nos permiten ver algunas temáticas, como bien puede ser las descendencias, menciones a los materiales y tiempo, conceptos como fuego y cielo...

En este sentido se puede ver la gran variedad de temáticas en la biblia. Con un conocimiento mayor de la biblia y del contexto en la que surgió se pueden sacar conclusiones más interesantes que escapan de nuestro alcance.

Lo bueno del procedimiento seguido en este trabajo es que podemos utilizar los códigos para estudiar otros textos, pudiendo ampliar el trabajo, por ejemplo, comparando la biblia en distintos idiomas, o ver las diferencias entre un texto antiguo y uno reciente. Pudiendose así profundizar mucho más en estos estudios.

Datos técnicos y reproducibilidad

Los códigos se han ejecutado sobre Jupiter notebook usando el lenguaje Python 2.7.11 sobre un ordenador de sobremesa con procesador Intel (R) Core (TM) i5-2300 CPU @ 2.80GHz con 4 GB ram, sistema operativo Lubuntu 64 bits (Linux).

En cuanto a las librerias usadas:

  • pandas 0.14.1
  • networkx 1.11
  • nltk 3.2.1
  • stop_words 2015.2.23.1
  • matplotlib
  • itertools
  • collections
  • numpy 1.11.0

Los gráficos de la sección Estudiando comunidades se han creado con la herramienta Gephi sobre el sistema descrito anteriormente.

Tiempo ejecución de los script (s):

time.clock()-tiempo_inic
63.356159000000005

El grafo fue construido a partir de una edición de la biblia en txt (por motivos técnicos cambiamos su codificación a UTF-8 con ayuda de un editor de texto plano):
http://www.unoenelsenor.com.ar/biblia.htm

estudio_biblia_grafo's People

Contributors

diegofern avatar

Stargazers

 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.