"""
# Практикум по философии
"""
"""
Блокнот подготовлен в соавторстве с Алексеем Морозовым, большое ему спасибо!
"""
"""

Материалы для семинарской работы - это философские тексты. Вам предлагается использовать современные инструменты для работы с текстами из области обработки естественных языков. Задача - сопоставить слова в зависимости от их семантической близости и получить таким образом представление о предметной области.

Для работы вам предоставлена модель [fasttext](https://radimrehurek.com/gensim/models/fasttext.html), которая была обучена на `3 407` философских текстах на русском языке. Словарь модели содержит ключевые слова (термы) изучаемой предметной области.



"""
"""
## 1. Загружаем данные
"""
import numpy as np
import pandas as pd

import os

for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))
from gensim.models.fasttext import FastText
import gensim
ft_model = FastText.load('/kaggle/input/philosophy-ru-large/ft_model.model')
"""
Проверяем количество слов в словаре модели.
"""
len(ft_model.wv.key_to_index)
"""
## 2. Поиск похожих слов
"""
"""
Мы можем уточнить, есть ли нужное нам слово в словаре.
"""
print('деньги' in ft_model.wv.key_to_index)
"""
И получить численное представление нужного нам слова в виде его векторов.
"""
print(ft_model.wv['деньги'])
"""
Если в словаре нет нужного нам слова, то поиск будет по n-граммам, если они есть в словаре.
"""
print('постбытие' in ft_model.wv.key_to_index)
print('пос' in ft_model.wv.key_to_index)
"""
Для поиска наиболее похожих слов может быть использовано одно или несколько слов. Префикс "positive=" указывает на положительные примеры для поиска, а "negative=" указывает на отрицательные.
"""
ft_model.wv.most_similar(positive=['карнап'], topn=20)
"""
Можно сравнить два слова на взаимное сходство:
"""
ft_model.wv.similarity("бытие", 'время')
"""
То же самое можно сделать с разными наборами слов.
"""
ft_model.wv.n_similarity(['парменид', 'бытие'], ['демокрит', 'небытие'])
"""
Также можно выбрать слово, которое менее всего похоже на представленные в перечне слова:

"""
ft_model.wv.doesnt_match(["бытие", 'время', 'деньги'])
"""
Настройка поиска по положительным и отрицательным запросам дает более точный результат:
"""
ft_model.wv.most_similar(positive=['время', 'деньги'], negative=["бытие"], topn=20)
"""
Мы можем использовать векторные представления интересующих слов для построения графа.
"""
import matplotlib.pyplot as plt
import matplotlib.cm as cm
%matplotlib inline
words = [
'конфуций',
'фалес',
'анаксимандр',
'анаксимен',
'пифагор',
'парменид',
'зенон',
'мелисс',
'гераклит',
'анаксагор',
'левкипп',
'демокрит',
'эмпедокл',
'протагор',
'горгий',
'продик',
'платон',
'аристотель',
'сенека',
'аврелий',
'эпикур',
'гипатия',
'плотин',
'порфирий',
'ямвлих',
'ориген',
'августин',
'тертуллиан',
'маймонид',
'фараби',
'аверроэс',
'боэций',
'абеляр',
'бонавентура',
'аквинский',
'оккам',
'экхарт',
'спиноза',
'лейбниц',
'декарт',
'гегель',
'бэкон',
'локк',
'милль',
'галилей',
'беркли',
'спенсер',
'авенариус',
'мах',
'гельвеций',
'гольбах',
'дидро',
'ламетри',
'кант',
'вольтер',
'монтескье',
'руссо',
'купер',
'юм',
'гоббс',
'шеллинг',
'дарвин',
'фейербах',
'шопенгауэр',
'конт',
'маркс',
'энгельс',
'ленин',
'рассел',
'витгенштейн',
'карнап',
'поппер',
'кун',
'фейерабенд',
'лакатос',
'гуссерль',
'хайдеггер',
'сартр',
'камю',
'ясперс',
'ницше',
'бергсон',
'дильтей',
'шпенглер',
'ортега',
'ильенков',
'зиновьев',
'мамардашвили',
'щедровицкий',
'паскаль',
'кьеркегор',
'шестов',
'бердяев',
'барт',
'фуко',
'соссюр',
'лакан',
'жижек',
'делез',
'деррида',
'бодрийяр',
'лиотар',
'арендт',
'рэнд',
'дугин',
'харман',
'мейясу',
'латур']
dct_names = dict.fromkeys(words)
for key in dct_names.keys():
    dct_names[key] = dict.fromkeys(words);

for key1 in dct_names.keys():
    for key2 in dct_names[key1].keys():
        if key1 == key2:
            dct_names[key1][key2] = '-';
        else:
            dct_names[key1][key2] = ft_model.wv.similarity(key1, key2)
"""
Можно взять за вершины нужные нам слова, а коэффициенты сходства сделать весами рёбер. Ребро добавляется, если косинусное сходство между вершинами больше порогового значения:
"""
import networkx as nx
G_sim = nx.Graph()
G_sim.add_nodes_from(words)

for key1 in dct_names.keys():
    for key2 in dct_names[key1].keys():
        if key1 != key2:
            if dct_names[key1][key2] > 0.7:
                G_sim.add_weighted_edges_from([(key1, key2, dct_names[key1][key2])])

colors = [i/len(G_sim.nodes) for i in range(len(G_sim.nodes))]

fig = plt.figure(figsize=(12, 12))
nx.draw(
    G_sim,
    with_labels=True,
    node_color=colors,
    edge_color=['silver'] * len(G_sim.edges()),
    cmap=plt.cm.jet,    
    node_size=150,
)
"""
Также для ранжирования вершин можно выбрать метрику, когда ранг расстояния определяется от слова 1 до слова 2 по отношению к расстояниям всех слов от слова 1. То есть веса для второй метрики определяются по формуле: 1/rank, так как чем больше значение rank, тем хуже сходство. Ребро создается с пороговым значением расстояния (rank < n).
"""
"""
**Осторожно, с большим списком слов работает очень медленно.**
"""
dct_vls = dict.fromkeys(words)
for key in dct_vls.keys():
    dct_vls[key] = dict.fromkeys(words)

for key1 in dct_vls.keys():
    for key2 in dct_vls[key1].keys():
        if key1 == key2:
            dct_vls[key1][key2] = '-';
        else:
            dct_vls[key1][key2] = ft_model.wv.rank(key1, key2)
G_rank = nx.Graph()
G_rank.add_nodes_from(words)

for key1 in dct_vls.keys():
    for key2 in dct_vls[key1].keys():
        if key1 != key2:
            if dct_vls[key1][key2] < 50:
                G_rank.add_weighted_edges_from([(key1, key2, 1/dct_vls[key1][key2])])

colors = [i/len(G_rank.nodes) for i in range(len(G_rank.nodes))]

fig = plt.figure(figsize=(10, 10))
nodes = nx.draw(
    G_rank,
    with_labels=True,
    node_color=colors,
    edge_color=['silver'] * len(G_rank.edges()),
    cmap=plt.cm.jet,    
    node_size=150,
)
"""
Выбираем граф с нужными параметрами:
"""
G = G_rank
#G = G_sim
"""
При необходимости убираем изолированные вершины.
"""
#G.remove_nodes_from(list(nx.isolates(G)))
"""
Также при необходимости выбираем отдельный подграф.
"""
#G = G.subgraph(nx.shortest_path(G.to_undirected(),'анаксимен'))
"""
Смотрим характеристики графа: число вершин и рёбер, связанные компоненты, клики:

(код преимущественно отсюда: https://www.kaggle.com/mayeesha/network-analysis-for-dummies-stackoverflow-data)
"""
print(nx.info(G))
nx.is_connected(G)
nx.number_connected_components(G)
cliques = list(nx.find_cliques(G))
clique_number = len(list(cliques))
print(clique_number)
for clique in cliques:
    print(clique)
print(nx.ego_graph(G,'ленин',radius=2).nodes())
nx.algorithms.clique.cliques_containing_node(G,"ленин")
sorted_cliques = sorted(list(nx.find_cliques(G)),key=len)
max_clique_nodes = set()

for nodelist in sorted_cliques[-4:-1]:
    for node in nodelist:
        max_clique_nodes.add(node)
max_clique = G.subgraph(max_clique_nodes)
print(nx.info(max_clique))
colors = [i/len(max_clique.nodes) for i in range(len(max_clique.nodes))]

fig = plt.figure(figsize=(10, 10))
nodes = nx.draw(
    max_clique,
    with_labels=True,
    node_color=colors,
    edge_color=['silver'] * len(max_clique.edges()),
    cmap=plt.cm.jet,    
    node_size=150,
)
"""
## Modularity
"""
"""
Если мы хотим посмотреть, на какие группы можно разделить выбранные слова, применяются алгоритмы модульности, которые "разбивают" граф на связные компоненты-модули. Попробуем часто применяемый для этих целей алгоритм Louvain.
"""
"""
Пример с Лувенским алгоритмом:
"""
!pip install python-louvain
from community import community_louvain
communities =community_louvain.best_partition(G)
community_id = [communities[node] for node in G.nodes()]

fig = plt.figure(figsize=(10, 10))
nx.draw(
    G,
    with_labels=True,
    edge_color=['silver'] * len(G.edges()),
    cmap=plt.cm.tab20,
    node_color=community_id,
    node_size=150,
)
"""
Ещё один вариант визуализации (источник: https://stackoverflow.com/questions/43541376/how-to-draw-communities-with-networkx).

Хорошо работает с отдельным подграфом.
"""
G_sub = G.subgraph(nx.shortest_path(G.to_undirected(),'анаксимен'))
def community_layout(G_sub, partition):
  
    pos_communities = _position_communities(G_sub, partition, scale=3.)

    pos_nodes = _position_nodes(G_sub, partition, scale=1.)

    pos = dict()
    for node in G_sub.nodes():
        pos[node] = pos_communities[node] + pos_nodes[node]

    return pos

def _position_communities(G_sub, partition, **kwargs):

    between_community_edges = _find_between_community_edges(G_sub, partition)

    communities = set(partition.values())
    hypergraph = nx.DiGraph()
    hypergraph.add_nodes_from(communities)
    for (ci, cj), edges in between_community_edges.items():
        hypergraph.add_edge(ci, cj, weight=len(edges))

    pos_communities = nx.spring_layout(hypergraph, **kwargs)

    pos = dict()
    for node, community in partition.items():
        pos[node] = pos_communities[community]

    return pos

def _find_between_community_edges(G_sub, partition):

    edges = dict()

    for (ni, nj) in G_sub.edges():
        ci = partition[ni]
        cj = partition[nj]

        if ci != cj:
            try:
                edges[(ci, cj)] += [(ni, nj)]
            except KeyError:
                edges[(ci, cj)] = [(ni, nj)]

    return edges

def _position_nodes(G_sub, partition, **kwargs):
    
    communities = dict()
    for node, community in partition.items():
        try:
            communities[community] += [node]
        except KeyError:
            communities[community] = [node]

    pos = dict()
    for ci, nodes in communities.items():
        subgraph = G_sub.subgraph(nodes)
        pos_subgraph = nx.spring_layout(subgraph, **kwargs)
        pos.update(pos_subgraph)

    return pos

def output():
    from community import community_louvain

    partition = community_louvain.best_partition(G_sub)
    pos = community_layout(G_sub, partition)
    fig = plt.figure(figsize=(10, 10))

    nx.draw(G_sub, pos, 
    with_labels=True,
    edge_color=['silver'] * len(G_sub.edges()),
    cmap=plt.cm.tab20,    
    node_size=150, node_color=list(partition.values())); plt.show()
    return
output()
"""
Сохраняем идентификаторы модулей в датафрейм:
"""
community_list = pd.DataFrame(np.column_stack([G.nodes, community_id]), 
                               columns=['node', 'community'])

community_list.community = community_list.community.astype(int)
community_list.head(10)
"""
## Centrality
"""
"""
Для ранжирования вершин в графе можно воспользоваться пейджранком.
"""
def eigenvector(G):
    ev = nx.eigenvector_centrality(G)
    df = pd.DataFrame.from_dict({
        'node': list(ev.keys()),
        'eigenvector': list(ev.values())
    })
    return df.sort_values('eigenvector', ascending=False)
eigenv = pd.DataFrame(eigenvector(G), columns=['node', 'eigenvector'])
eigenv.head(10)
def pr_summary(G):
    pr = nx.pagerank(G)
    df = pd.DataFrame.from_dict({
        'node': list(pr.keys()),
        'centrality_pr': list(pr.values())
    })
    return df.sort_values('centrality_pr', ascending=False)
pr = pd.DataFrame(pr_summary(G), columns=['node', 'centrality_pr'])
pr.head()
"""
А также дополнительными мерами центральности (при необходимости):
"""
def cc_summary(G):
    cc = nx.closeness_centrality(G)
    df = pd.DataFrame.from_dict({
        'node': list(cc.keys()),
        'centrality_сс': list(cc.values())
    })
    return df.sort_values('centrality_сс', ascending=False)
cc = pd.DataFrame(cc_summary(G), columns=['node', 'centrality_сс'])
cc.head()
def dc_summary(G):
    dc = nx.degree_centrality(G)
    df = pd.DataFrame.from_dict({
        'node': list(dc.keys()),
        'centrality_dc': list(dc.values())
    })
    return df.sort_values('centrality_dc', ascending=False)
dc = pd.DataFrame(dc_summary(G), columns=['node', 'centrality_dc'])
dc.head()
def bc_summary(G):
    bc = nx.betweenness_centrality(G)
    df = pd.DataFrame.from_dict({
        'node': list(bc.keys()),
        'centrality_bc': list(bc.values())
    })
    return df.sort_values('centrality_bc', ascending=False)
bc = pd.DataFrame(bc_summary(G), columns=['node', 'centrality_bc'])
bc.head()
def clustering(G):
    cl = nx.clustering(G, weight='weight')
    df = pd.DataFrame.from_dict({
        'node': list(cl.keys()),
        'weight_cl': list(cl.values())
    })
    return df.sort_values('weight_cl', ascending=False)
cl = pd.DataFrame(clustering(G), columns=['node', 'weight_cl'])
cl.head()
"""
Объединяем все показатели в один датафрейм.
"""
from functools import reduce

dfs = [community_list, eigenvector(G), cc_summary(G), dc_summary(G), bc_summary(G), pr_summary(G), clustering(G)]  
df_sum = reduce(lambda left,right: pd.merge(left,right,on='node'), dfs)
df_sum.head()
corr = df_sum.corr()
corr.style.background_gradient(cmap='coolwarm')
"""
## Link Prediction
Источник: https://www.geeksforgeeks.org/link-prediction-predict-edges-in-a-network-using-networkx/
"""
"""
Теперь мы можем использовать стандартные алгоритмы для предсказания связей между вершинами. Попробуем некоторые из них.
"""
"""
Triadic Closure:
"""
e = list(G.edges())
  
def triadic(e):
    new_edges = []
  
    for i in e:
        a, b = i
  
        for j in e:
            x, y = j
  
            if i != j:
                if a == x and (b, y) not in e and (y, b) not in e:
                    new_edges.append((b, y))
                if a == y and (b, x) not in e and (x, b) not in e:
                    new_edges.append((b, x))
                if b == x and (a, y) not in e and (y, a) not in e:
                    new_edges.append((a, y))
                if b == y and (a, x) not in e and (x, a) not in e:
                    new_edges.append((a, x))
  
    return new_edges

triadic = pd.DataFrame(triadic(e), columns=['node_1', 'node2'])
triadic.head(10)
"""
Jaccard Coefficient:
"""
jaccard_coefficient = pd.DataFrame(nx.jaccard_coefficient(G), columns=['node_1', 'node2', 'jaccard_coefficient'])
jaccard_coefficient.sort_values(by = 'jaccard_coefficient', ascending = False).head(10)
"""
Resource Allocation Index:
"""
resource_allocation_index = pd.DataFrame(nx.resource_allocation_index(G), columns=['node_1', 'node2', 'resource_allocation_index'])
resource_allocation_index.sort_values(by = 'resource_allocation_index', ascending = False).head(10)
"""
Adamic Adar Index:
"""
adamic_adar_index = pd.DataFrame(nx.adamic_adar_index(G), columns=['node_1', 'node2', 'adamic_adar_index'])
adamic_adar_index.sort_values(by = 'adamic_adar_index', ascending = False).head(10)
"""
Preferential Attachment:
"""
preferential_attachment = pd.DataFrame(nx.preferential_attachment(G), columns=['node_1', 'node2', 'preferential_attachment'])
preferential_attachment.sort_values(by = 'preferential_attachment', ascending = False).head(10)
"""
В целях уточнения деталей можем найти наикратчайший путь между интересующими вершинами:
"""
print(nx.shortest_path(G,source='лакатос',target='карнап'))
print(nx.shortest_path(G,source='лакатос',target='карнап', weight='weight'))
"""
## node2vec
Источник: https://github.com/eliorc/node2vec
"""
"""
Зачем это надо, еще не совсем понятно. Пока что мы можем получить результаты по схожести и кластеризации вершин, возможно, ещё точнее предыдущих. 
"""
!pip install node2vec
from node2vec import Node2Vec
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
%matplotlib inline
node2vec = Node2Vec(G, dimensions=300, walk_length=30, num_walks=200, workers=4)
n2v_model = node2vec.fit(window=10, min_count=1, batch_words=4)
n2v_model.wv.most_similar('карнап')
n2v_output = pd.DataFrame(n2v_model.wv.most_similar(positive=['карнап'], topn=30), columns=['упоминания', 'коэффициент'])
import seaborn as sns
sns.set_theme(style="ticks", color_codes=True)
g=sns.catplot(x='упоминания', y='коэффициент', data=n2v_output)
g.set_xticklabels(rotation=45)
g.fig.set_size_inches(15,5)
plt.title('упоминания, похожие на запрос')
from node2vec.edges import HadamardEmbedder
edges_embs = HadamardEmbedder(keyed_vectors=n2v_model.wv)
edges_embs[('карнап', 'лакатос')]
edges_kv = edges_embs.as_keyed_vectors()
edges_kv.most_similar(str(('карнап', 'лакатос')))
"""
Кластеризуем данные с помощью UMAP и HDBSCAN.

(Источник: https://github.com/Huguet57/Information-Contrast)
"""
labels = []
tokens = []

for word in n2v_model.wv.key_to_index:
    tokens.append(n2v_model.wv[word])
    labels.append(word)
tokens_norm = (tokens - np.mean(tokens))/np.sqrt(np.var(tokens))
!pip install umap-learn
import umap.umap_ as umap

reducer = umap.UMAP(n_neighbors=15,
                    min_dist=0.01,
                    metric='euclidean',
                    n_epochs=2000,
                    random_state=23,
                    verbose=True)
umap_embedding = reducer.fit_transform(tokens_norm)
umap_embedding.shape
x = []
y = []
for value in umap_embedding:
    x.append(value[0])
    y.append(value[1])
    
plt.figure(figsize=(12, 12)) 
for i in range(len(x)):
    plt.scatter(x[i],y[i])
    #plt.annotate(labels[i],
                  #xy = (x[i], y[i]),
                  #xytext = (5, 2),
                  #textcoords = 'offset points',
                  #ha = 'right',
                  #va = 'bottom')
plt.show()
#!pip install --upgrade --user numpy
!pip install hdbscan --no-build-isolation --no-binary :all:
import hdbscan

labels_hdbscan = hdbscan.HDBSCAN(
min_samples=1,
min_cluster_size=2,
).fit_predict(umap_embedding)
clustered = (labels_hdbscan >= 0)
from matplotlib.pyplot import figure
figure(figsize=(10, 10), dpi=80)
plt.scatter(umap_embedding[~clustered, 0],
            umap_embedding[~clustered, 1],
            c=(0.5, 0.5, 0.5),
            s=10,
            alpha=0.5)
plt.scatter(umap_embedding[clustered, 0],
            umap_embedding[clustered, 1],
            c=(labels_hdbscan[clustered]),
            s=20,
            cmap='Spectral')
clustered_list = pd.DataFrame(np.column_stack([G.nodes, labels_hdbscan]), 
                               columns=['node', 'cluster'])
clustered_list.head(20)
clustered_list['cluster'].value_counts()
"""
# Классификация термов
"""
"""
В результате разбиения графа на сообщества у нас есть размеченный датасет. Если сообщества (модули) хорошо интерпретируются, то мы можем использовать значения векторов слов как признаки (фичи) для предсказания целевых переменных. Рассмотрим пример с мультиклассификацией.
"""
"""
Снова построим граф. Мы будем использовать граф только для вычислительных целей, поэтому возьмём любые положительные значения коэффициента сходства между словами для построения взвешенных рёбер между вершинами. 
"""
g = nx.Graph()
g.add_nodes_from(words)

for key1 in dct_names.keys():
    for key2 in dct_names[key1].keys():
        if key1 != key2:
            if dct_names[key1][key2] > 0:
                g.add_weighted_edges_from([(key1, key2, dct_names[key1][key2])])
from community import community_louvain
communities =community_louvain.best_partition(g)
community_id = [communities[node] for node in g.nodes()]

fig = plt.figure(figsize=(10, 10))
nx.draw(
    g,
    with_labels=True,
    edge_color=['silver'] * len(G.edges()),
    cmap=plt.cm.tab20,
    node_color=community_id,
    node_size=150,
)
community_list = pd.DataFrame(np.column_stack([g.nodes, community_id]), 
                               columns=['node', 'community'])

community_list.community = community_list.community.astype(int)
df_target = community_list.copy()
x = [(ft_model.wv[str(i)]) for i in df_target['node']]
emb_df_untarget = pd.DataFrame(x, index = df_target['node'])
df_target.set_index('node', inplace=True)
emb_df_target = df_target.join(emb_df_untarget)
df_train = emb_df_target
data_full = df_train.copy()
X_data = data_full.drop('community', axis=1)
y = data_full.community
from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
label_encoder = label_encoder.fit(y)
label_encoded_y = label_encoder.transform(y)
seed = 7
test_size = 0.33
X_train, X_test, y_train, y_test = train_test_split(X_data, label_encoded_y,test_size=test_size, random_state=seed)
xgb = XGBClassifier(
    max_depth=2,
    gamma=2,
    eta=0.8,
    reg_alpha=0.5,
    reg_lambda=0.5
)
xgb.fit(X_train, y_train)
predictions = xgb.predict(X_test)
accuracy = accuracy_score(y_test, predictions)
print("Accuracy: %.2f%%" % (accuracy * 100.0))
"""
Теперь мы можем предсказать целевые значения на новом наборе слов.
"""
pred_words = [

    'аристотель',
    'стагира',
    'платон',
    'логика',
    'диалоги',
    'науки',
    'теоретические',
    'практические',
    'математика',
    'метафизика',
    'физика',
    'этика',
    'политика',
    'причина',
    'первоначало',
    'материя',
    'первоэлемент',
    'форма',
    'воздух',
    'вода',
    'огонь',
    'земля',
    'эфир',
    'перводвигатель',
    'ум',
    'бог',
    'действующая',
    'вещь',
    'цель',
    'душа',
    'человек',
    'животное',
    'растение',
    'бытие',
    'мышление',
    'понятие',
    'суждение',
    'умозаключение',
    'чувственное',
    'абстрактное',
    'диалектическое',
    'аподиктическое',
    'познание',
    'опыт',
    'достоверность',
    'ощущение',
    'государство',
    'форма',
    'социальное',
    'семья',
    'город',
    'счастье',
    'пространство',
    'время',
    'субстанция',
    'отношения',
    'объект',
    'часть',
    'бесконечное',
    'существование',
    'целое',
    'теперь',
    'движение',
    'изменение',
    'предыдущее',
    'последующее',
    'измерение',
    'счет',
    'покой',
    'неподвижное',
    'относительное',
    'наблюдатель'

]
x1 = [(ft_model.wv[str(i)]) for i in pred_words]
len(x1)
emb_df = pd.DataFrame(x1, index = pred_words)
boosted_predictions = xgb.predict(emb_df.values)
node = emb_df.index
pred_results = pd.DataFrame({'node': node, 'value_prediction': boosted_predictions})
pred_results.value_prediction.value_counts()
select_class = pred_results.loc[pred_results['value_prediction'] == 1]
select_class