#! /usr/bin/env python

"""
# ---------------------------------------------------------------------------
#
# funciones_network_science.py
#
# Archivo con todas las funciones que he utilizado para aplicar métricas de 
# ciencia de redes a redes bipartitas y proyectadas.
#
# TFG: Ciencia de redes y reposicionamiento de fármacos: potencial a través 
# de la medicina de redes
#
# María Marín Tercero
# ----------------------------------------------------------------------------
"""

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import networkx as nx
from networkx.algorithms import bipartite
import nxpd
    
# =================================================================================

def asociaciones(archivo, var1, var2, nombre_var1, nombre_var2, ax):
    """
    Esta función toma un archivo que representa asociaciones entre dos grupos disjuntos de nodos. Finalmente, representa en un histograma la distribución del número de asociaciones de un grupo de nodos con nodos de la otra clase. 
    Datos de entrada: 
    1. archivo: Archivo con asociaciones entre nodos organizado en dos columnas, una para cada grupo de nodos 
    2. var1: Nombre de una columna
    3. var2: Nombre de la otra columna
    4. nombre_var1: Nombre de los nodos representados en la columna 1
    5 nombre_var2: Nombre de los nodos representados en la columna 2
    6. ax: Variable empleada para la representación organizada en un subplot
    
    """
    conteo = archivo.groupby(var1)[var2].count().reset_index() #cuento el número de asociaciones del grupo var1 con el grupo var2
    
    #Represento los resultados
    ax.hist(conteo[var2], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Nº de ' + nombre_var2, fontsize=12)
    ax.set_ylabel(nombre_var1, fontsize=12)
    ax.set_title('Distribución de '+ r'$\mathbf{' + nombre_var2 + '}$' + " por " + r'$\mathbf{' + nombre_var1 + '}$' ,  fontsize=12)

# =================================================================================

def matriz_spl_enfermedades_bip(archivo, graf):
    """
    Esta función genera una matriz bidimensional que contiene los Shortest Path 
    Lenghts de todos los nodos enfermedades de una red bipartita compuesta por dos 
    grupos disjuntos de nodos.
    Datos de entrada:
    1. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    2. graf: Red bipartita 
    """
    
    enfermedades = list(set(archivo['dis'])) #lista de enfermedades (columna "dis" del archivo)

    matriz_spl = np.zeros((len(enfermedades), len(enfermedades))) #Inicializo una matriz de ceros para almacenar los SPL

    #Calculo los SPL entre todas las parejas de enfermedades
    for i in range(len(enfermedades)): #enfermedad_i
        for j in range(i + 1, len(enfermedades)): #enfermedad_i+1
            enfermedad_i = enfermedades[i] #guardo el nodo enfermedad_i en la fila i de la matriz
            enfermedad_j = enfermedades[j] #guardo el nodo enfermedad_i+1 en la columna j de la matriz
            try: #me aseguro de que exista un camino entre ambas enfermedades
                spl = nx.shortest_path_length(graf, source=enfermedad_i, target=enfermedad_j) #calculo su spl
                matriz_spl[i, j] = spl #añado el valor a la matriz en su posición correspondiente
                matriz_spl[j, i] = spl #añado el valor a la matriz en su posición correspondiente
            except nx.NetworkXNoPath: #si no hay camino, continúo
                pass
    #Creo un DataFrame con la matriz de SPLs
    df_matriz_spl = pd.DataFrame(matriz_spl, index=enfermedades, columns=enfermedades)

    return df_matriz_spl

# =================================================================================

def matriz_spl_var_bip(archivo, graf, nombre_var):
    """
    Esta función genera una matriz bidimensional que contiene los Shortest Path 
    Lenghts para el grupo de nodos distinto a enfermedades en una red bipartita 
    que contiene dos grupos diferenets de nodos, siendo uno enfermedades. El otro
    grupo varía en función de la red. 
    Datos de entrada:
    1. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    2.  graf: Red bipartita
    3. nombre_var: Identificador del grupo de nodos distinto a enfermedades en el archivo
    """

    var = list(set(archivo[str(nombre_var)])) #lista de nodos del grupo disjunto a enfermedades (columna con el identificador añadido en los datos de entrada)

    matriz_spl = np.zeros((len(var), len(var))) #Inicializo una matriz de ceros para almacenar los SPL

    #Calculo los SPL entre todas las parejas de los nodos seleccionados
    for i in range(len(var)): #nodos_i
        for j in range(i + 1, len(var)): #nodo_i+1
            var_i = var[i] #guardo el nodo_i en la fila i de la matriz
            var_j =  var[j] #guardo el nodo_i+1 en la columna j de la matriz
            try: #me aseguro de que exista un camino entre ambos nodos
                spl = nx.shortest_path_length(graf, source=var_i, target=var_j) #calculo su spl
                matriz_spl[i, j] = spl #añado el valor a la matriz en su posición correspondiente
                matriz_spl[j, i] = spl #añado el valor a la matriz en su posición correspondiente
            except nx.NetworkXNoPath: #si no hay camino, continúo
                pass

    #Creo un DataFrame con la matriz de SPLs
    df_matriz_spl = pd.DataFrame(matriz_spl, index=var, columns=var)

    return df_matriz_spl

# =================================================================================

def matriz_spl_enfermedades_proj(archivo, graf):
    """
    Esta función genera una matriz bidimensional que contiene los Shortest Path 
    Lenghts para las enfermedades en una red proyectada. 
    Datos de entrada:
    1. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    2. graf: Red bipartita
    """
    all_shortest_paths = dict(nx.all_pairs_shortest_path_length(graf)) #creo un diccionario con los spls entre todas las parejas de nodos de la red 
    
    enfermedades = list(graf.nodes) # Obtengo la lista de nodos enfermedades
    
    matriz_spl = np.zeros((len(enfermedades), len(enfermedades))) #Inicializo una matriz de ceros para almacenar los SPL
   
    for i, enfermedad_i in enumerate(enfermedades): #enfermedad_i
        for j, enfermedad_j in enumerate(enfermedades): #enfermedad_j
            matriz_spl[i, j] = all_shortest_paths[enfermedad_i].get(enfermedad_j, 0) #añado a la matriz el spl entre la enfermedad_i y la enfermedad_j según el diccionario con el total de spls
    
    #Creo un DataFrame con la matriz de SPLs
    df_matriz_spl = pd.DataFrame(matriz_spl, index=enfermedades, columns=enfermedades)
    return df_matriz_spl

# =================================================================================
def degree_bip(graf, archivo, nombre_graf, nombre_var, ax, ax1):
    """
    Esta función calcula y representa la distribución de grados de dos 
    grupos disjuntos de nodos en una red bipartita, siendo uno de esos grupos
    el correspondiente a enfermedades
    Datos de entrada:
    1. graf: Red bipartita
    2. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    3. nombre_graf: Nombre de la red
    4. nombre_var: Nombre del grupo disjunto de nodos distinto a enfermedades
    5. ax: Variable empleada para la representación organizada de la primera distribución en un subplot
    6. ax1: Variable empleada para la representación organizada  de la segunda distribución en un subplot
    """
    degrees_dict = dict(graf.degree())  # diccionario con los grados de los nodos del grafo
    
    nodos_enf = set(archivo.iloc[:, 0])  # selecciono el set de nodos enfermedades en el archivo (que se encuentran en la primera columna)
    nodos_var = set(archivo.iloc[:, 1])  # selecciono el set de nodos del grupo variable en el archivo (que se encuentran en la segunda columna)

    # Filtro los degrees para nodos de enfermedades y variables
    degree_enf = [degrees_dict[clave] for clave in nodos_enf if clave in degrees_dict]
    degree_var = [degrees_dict[clave] for clave in nodos_var if clave in degrees_dict]

    # Represento la distribución de grados de las enfermedades
    G_lista_enf = pd.DataFrame(list(zip(nodos_enf, degree_enf)), columns=['node', 'degree'])
    G_plot_enf = G_lista_enf.groupby('degree').count()
    ax.scatter(G_plot_enf.index, G_plot_enf['node'], marker='o', color='#79C4FF', s=10)
    ax.set_yscale('log')
    ax.set_xscale('log')
    ax.set_xlabel('Grado', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de grados de ' + r'$\mathbf{' + "enfermedades" + '}$' +' en\n' + r'$\mathbf{' + "red" + '}$' + " " + r'$\mathbf{' + "bipartita" + '}$'+ " "+ r'$\mathbf{' + nombre_graf + '}$' , fontsize=12)
    
    # Represento la distribución de grados del grupo disjunto de nodos distinto a enfermedades
    G_lista_var = pd.DataFrame(list(zip(nodos_var, degree_var)), columns=['node', 'degree'])
    G_plot_var = G_lista_var.groupby('degree').count()
    ax1.scatter(G_plot_var.index, G_plot_var['node'], marker='o', color='#79C4FF', s=10)
    ax1.set_yscale('log')
    ax1.set_xscale('log')
    ax1.set_xlabel('Grado', fontsize=10)
    ax1.set_ylabel('Nº nodos', fontsize=10)
    ax1.set_title('Distribución de grados de ' +  r'$\mathbf{' + nombre_var + '}$' + ' en\n' + r'$\mathbf{' + " red" + '}$' + " "+ r'$\mathbf{' + "bipartita" + '}$'+ " " + r'$\mathbf{' + nombre_graf + '}$', fontsize=12)


# =================================================================================

def degree_proj(graf, nombre_graf, ax):
    """
    Esta función calcula y representa la distribución de grados de las enfermedades
    en una red proyectada.
    Datos de entrada:
    1. graf: Red bipartita
    2. nombre_graf: Nombre de la red
    3. ax: Variable empleada para la representación organizada de la distribución en un subplot
    """
    degrees_dict = dict(graf.degree())  # diccionario con los grados de los nodos del grafo
    
    nodos = list(degrees_dict.keys())  # lista con los nodos de la red (claves del diccionario anterior)
    degrees = list(degrees_dict.values())  # lista con los grados de los nodos de la red (valores del diccionario anterior)
    
    # Represento la distribución de grados
    G_lista = pd.DataFrame(list(zip(nodos, degrees)), columns=['node', 'degree'])
    G_plot = G_lista.groupby('degree').count()
    ax.scatter(G_plot.index, G_plot['node'], marker='o', color='#79C4FF', s=10)
    ax.set_yscale('log')
    ax.set_xscale('log')
    ax.set_xlabel('Grado', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de grados de ' + r'$\mathbf{' + "enfermedades" + '}$'+  ' en\n' + r'$\mathbf{' + " red "+ '}$'+ " "+ r'$\mathbf{' + "proyectada" + '}$' + " " + r'$\mathbf{' + nombre_graf + '}$',  fontsize=12)

# =================================================================================

def average_spl(matriz_spl, nombre_graf, nombre_var, ax, bipartita = True):
    """
    Esta función realiza una distribución de los Average SPL d los nodos de un grafo.
    Datos de entrada: 
    1. matriz_spl: Matriz con la información sobre los SPLs entre todas las parejas de nodos
    2. nombre_graf: Nombre de la red
    3. nombre_var: Nombre del tipo de nodos representado
    4. ax: Variable empleada para la representación organizada de la distribución en un subplot
    5. Bipartita: dependiendo de si es igualue o False, añade el título correspondiente a una red bipartita o una proyectada, respectivamente.
    """
    average_spl_por_nodo = matriz_spl.mean(axis=0) #Calculo el Average_SPL de cada nodo (media de cada columna de la matriz de SPLs)
    
    df_average_spl = pd.DataFrame({'Nodo': matriz_spl.index, 'AverageSPL': average_spl_por_nodo.values}) #creo un DataFrame con los resultados
    
     # Represento la distribución de los Average Shortest Path Lengths
    ax.hist(df_average_spl['AverageSPL'], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Average Shortest Path Lengths', style='italic', fontsize=12)
    ax.set_ylabel('Nº nodos', fontsize=12)
    if bipartita == True:
        ax.set_title('Distribución de Average SPLs de ' + r'$\mathbf{' + nombre_var + '}$'+  '  en\n' + r'$\mathbf{' + " red "+ '}$'+ " "+ r'$\mathbf{' + "bipartita" + '}$' + " " + r'$\mathbf{' + nombre_graf + '}$',  fontsize=12)
    else:
         ax.set_title('Distribución de Average SPLs de ' + r'$\mathbf{' + "enfermedades" + '}$'+  ' en\n' + r'$\mathbf{' + " red "+ '}$'+ " "+ r'$\mathbf{' + "proyectada" + '}$' + " " + r'$\mathbf{' + nombre_graf + '}$',  fontsize=12)

# =================================================================================

def centrality_b_bip(graf, archivo, nombre_graf, nombre_var, ax, ax1):
    """
    Esta función calcula y representa la distribución de centralidad de intermediación 
    de dos grupos disjuntos de nodos en una red bipartita, siendo uno de esos grupos
    el correspondiente a enfermedades
    Datos de entrada:
    1. graf: Red bipartita
    2. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    3. nombre_graf: Nombre de la red
    4. nombre_var: Nombre del grupo disjunto de nodos distinto a enfermedades
    5. ax: Variable empleada para la representación organizada de la primera distribución en un subplot
    6. ax1: Variable empleada para la representación organizada  de la segunda distribución en un subplot
    
    """
    #Calculo la centralidad de intermediación de todos los nodos del grafo
    bc = nx.betweenness_centrality(graf, k=None, normalized=True, weight=None, endpoints=False, seed=None)

    nodos_enf = set(archivo.iloc[:, 0]) #selecciono el set de nodos enfermedades en el archivo (que se encuentran en la primera columna)
    nodos_var = set(archivo.iloc[:, 1]) #selecciono el set de nodos del grupo variable en el archivo (que se encuentran en la segunda columna)

    #Filtro la centralidad de intermediación para ambos grupos de nodos
    bc_enf = {clave: bc[clave] for clave in nodos_enf if clave in bc}
    bc_var = {clave: bc[clave] for clave in nodos_var if clave in bc}

    #Creo DataFrames para la centralidad de intermediación de ambos grupos de nodos
    df_bc_enf = pd.DataFrame({'Nodo': list(bc_enf.keys()), 'Centralidad de intermediación': list(bc_enf.values())})
    df_bc_var = pd.DataFrame({'Nodo': list(bc_var.keys()), 'Centralidad de intermediación': list(bc_var.values())})

    #Represento la centralidad de intermediación de las enfermedades
    ax.hist(df_bc_enf['Centralidad de intermediación'], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Centralidad de intermediación', style='italic', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de centralidad de intermediación de ' + r'$\mathbf{' + "enfermedades" + '}$' +' en\n' + r'$\mathbf{' + "red" + '}$' + " " + r'$\mathbf{' + "bipartita" + '}$'+ " "+ r'$\mathbf{' + nombre_graf + '}$' , fontsize=10)

    #Represento la centralidad de intermediación del grupo disjunto de nodos distinto a enfermedades
    ax1.hist(df_bc_var['Centralidad de intermediación'], bins=20, color='#79C4FF', edgecolor='black')
    ax1.set_xlabel('Centralidad de intermediación', style='italic', fontsize=10)
    ax1.set_ylabel('Nº nodos', fontsize=10)
    ax1.set_title('Distribución de centralidad de intermediación de ' +  r'$\mathbf{' + nombre_var + '}$' + ' en\n' + r'$\mathbf{' + " red" + '}$' + " "+ r'$\mathbf{' + "bipartita" + '}$'+ " " + r'$\mathbf{' + nombre_graf + '}$', fontsize=10)

# =================================================================================

def centrality_b_proj(graf, nombre_graf, ax):
    """
    Esta función calcula y representa la distribución de centralidad de intermediación 
    de enfermedades en una red proyectada.
    Datos de entrada:
    1. graf: Red bipartita
    2. nombre_graf: Nombre de la red
    3. ax: Variable empleada para la representación organizada de la distribución en un subplot
    """
    #Calculo la centralidad de intermediación de todos los nodos del grafo
    bc= nx.betweenness_centrality(graf, k=None, normalized=True, weight=None, endpoints=False, seed=None)
    
    #Creo un DataFrame para la centralidad de intermediación de las enfermedades
    df_bc = pd.DataFrame({'Nodo': list(bc.keys()), 'Centralidad de intermediación': list(bc.values())})
    
    #Represento la centralidad de intermediación de las enfermedades
    ax.hist(df_bc['Centralidad de intermediación'], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Centralidad de intermediación',style='italic', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de centralidad de intermediación de ' + r'$\mathbf{' + "enfermedades" + '}$'+  ' en\n' + r'$\mathbf{' + " red "+ '}$'+ " "+ r'$\mathbf{' + "proyectada" + '}$' + " " + r'$\mathbf{' + nombre_graf + '}$',  fontsize=10)

# =================================================================================

def centrality_c_bip(graf, archivo, nombre_graf, nombre_var, ax, ax1):
    """
    Esta función calcula y representa la distribución de centralidad de cercanía 
    de dos grupos disjuntos de nodos en una red bipartita, siendo uno de esos grupos
    el correspondiente a enfermedades
    Datos de entrada:
    1. graf: Red bipartita
    2. archivo: Archivo con los datos sobre los enlaces entre los nodos de la red
    3. nombre_graf: Nombre de la red
    4. nombre_var: Nombre del grupo disjunto de nodos distinto a enfermedades
    5. ax: Variable empleada para la representación organizada de la primera distribución en un subplot
    6. ax1: Variable empleada para la representación organizada  de la segunda distribución en un subplot
    
    """
    #Calculo la centralidad de intermediación de todos los nodos del grafo
    cc = nx.closeness_centrality(graf, u=None, distance=None, wf_improved=True)

    nodos_enf = set(archivo.iloc[:, 0]) #selecciono el set de nodos enfermedades en el archivo (que se encuentran en la primera columna)
    nodos_var = set(archivo.iloc[:, 1]) #selecciono el set de nodos del grupo variable en el archivo (que se encuentran en la segunda columna)

    #Filtro la centralidad de cercanía para ambos grupos de nodos
    cc_enf = {clave: cc[clave] for clave in nodos_enf if clave in cc}
    cc_var = {clave: cc[clave] for clave in nodos_var if clave in cc}

    #Creo DataFrames para la centralidad de cercanía de ambos grupos de nodos
    df_cc_enf = pd.DataFrame({'Nodo': list(cc_enf.keys()), 'Centralidad de cercanía': list(cc_enf.values())})
    df_cc_var = pd.DataFrame({'Nodo': list(cc_var.keys()), 'Centralidad de cercanía': list(cc_var.values())})

    #Represento la centralidad de intermediación de las enfermedades
    ax.hist(df_cc_enf['Centralidad de cercanía'], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Centralidad de cercanía', style='italic', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de centralidad de cercanía de ' + r'$\mathbf{' + "enfermedades" + '}$' +' en\n' + r'$\mathbf{' + "red" + '}$' + " " + r'$\mathbf{' + "bipartita" + '}$'+ " "+ r'$\mathbf{' + nombre_graf + '}$' , fontsize=10)

    #Represento la centralidad de cercanía del grupo disjunto de nodos distinto a enfermedades
    ax1.hist(df_cc_var['Centralidad de cercanía'], bins=20, color='#79C4FF', edgecolor='black')
    ax1.set_xlabel('Centralidad de cercanía', style='italic', fontsize=10)
    ax1.set_ylabel('Nº nodos', fontsize=10)
    ax1.set_title('Distribución de centralidad de cercanía de ' +  r'$\mathbf{' + nombre_var + '}$' + ' en\n' + r'$\mathbf{' + " red" + '}$' + " "+ r'$\mathbf{' + "bipartita" + '}$'+ " " + r'$\mathbf{' + nombre_graf + '}$', fontsize=10)
    
# =================================================================================

def centrality_c_proj(graf, nombre_graf,ax):
    """
    Esta función calcula y representa la distribución de centralidad de cercanía 
    de enfermedades en una red proyectada.
    Datos de entrada:
    1. graf: Red bipartita
    2. nombre_graf: Nombre de la red
    3. ax: Variable empleada para la representación organizada de la distribución en un subplot
    """
    #Calculo la centralidad de cercanía de todos los nodos del grafo
    cc= nx.closeness_centrality(graf, u=None, distance=None, wf_improved=True)
    #Creo un DataFrame para la centralidad de cercanía de las enfermedades
    df_cc = pd.DataFrame({'Nodo': list(cc.keys()), 'Centralidad de cercanía': list(cc.values())})
    #Represento la centralidad de cercanía de alas enfermedades
    ax.hist(df_cc['Centralidad de cercanía'], bins=20, color='#79C4FF', edgecolor='black')
    ax.set_xlabel('Centralidad de cercanía',style='italic', fontsize=10)
    ax.set_ylabel('Nº nodos', fontsize=10)
    ax.set_title('Distribución de centralidad de cercanía de ' + r'$\mathbf{' + "enfermedades" + '}$'+  ' en\n' + r'$\mathbf{' + " red "+ '}$'+ " "+ r'$\mathbf{' + "proyectada" + '}$' + " " + r'$\mathbf{' + nombre_graf + '}$',  fontsize=10)

# =================================================================================
