class App: def __init__(self, theme="data", nb_docs=20): #nombre de fois qu'on a appuyé sur entrer dans l'input de filtre self.NSUBMIT_words = 0 #nombre de fois qu'on a appuyé sur le bouton de reset du corpus self.NSUBMIT_corpus = 0 #figure contenant le graphe self.FIG = None self.setup_corpus(theme, nb_docs) self.setup_dash() self.callbacks() self.launch() def setup_corpus(self, theme, nb_docs): #contient les mots filtrés self.WORDS = theme + ";" #les 3 métriques de centralités (pour chaque mot : dictionnaire) self.DEGCEN = {} self.CLOCEN = {} self.BETCEN = {} #le thème du corpus self.THEME = theme #nombre de documents du corpus self.NB_DOCS = nb_docs #le corpus self.corpus = Corpus(theme) self.corpus.download_collection(nb_docs, keyword=theme) self.A = self.corpus.get_adjacency_matrix() def setup_dash(self): # crée l'application dash, # paramètrise le css, # et l'envoie dans l'application self.app = dash.Dash(__name__, external_stylesheets=[ 'https://codepen.io/chriddyp/pen/bWLwgP.css' ]) self.app.title = "Projet Prog" ###################################################################################################################################################################### # styles: pour les composants de droite : click et hover styles = { 'pre': { 'border': 'thin lightgrey solid', 'overflowX': 'scroll' } } self.FIG = self.network_graph() #composants de l'interface graphique self.app.layout = html.Div([ #########################Title html.Div([ html.H1("Co-occurrences of words in the corpus '" + self.corpus.name + "' - Number of words : " + str(self.nb_words), id="title") ], className="row", style={'textAlign': "center"}), #############################################################################################define the row html.Div( className="row", children=[ ##############################################left side two input components html.Div( className="two columns", children=[ html.Div( className="twelve columns", children=[ dcc.Markdown(d("**Corpus theme**")), dcc.Input(id="theme", type="text", placeholder="Theme", value=self.THEME), html.Div(id="output1"), dcc.Markdown( d("**Number of documents to download**" )), dcc.Input(id="nbdocs", type="number", value=self.NB_DOCS, min=1), html.Div(id="output2"), html.Button('Reset the Corpus', id='reset') ], style={'height': '300px'}), html.Div(className="twelve columns", children=[ dcc.Markdown( d("**Words To Search**")), dcc.Input(id="words", type="text", placeholder="Words", value=self.WORDS, n_submit=1), html.Div(id="output3") ], style={'height': '200px'}) ]), ############################################middle graph component html.Div( className="eight columns", children=[dcc.Graph(id="my-graph", figure=self.FIG)]), #########################################right side two output component html.Div(className="two columns", children=[ html.Div(className='twelve columns', children=[ dcc.Markdown( d("**Words metrics**")), html.Pre(id='hover-data', style=styles['pre']) ], style={'height': '250px'}), html.Div(className='twelve columns', children=[ dcc.Markdown( d("**Click Data**")), html.Pre(id='click-data', style=styles['pre']) ], style={'height': '250px'}) ]) ]) ]) def launch(self): # lance l'appli self.app.run_server() def network_graph(self): #génère le graphe #récupère et sépare les mots filtrés WordsToSearch = list(set(self.WORDS.split(";"))) if "" in WordsToSearch: WordsToSearch.remove("") ############################################## FILTRES #################################################### #filtre par rapport à l'input de filtre words = WordsToSearch.copy() #si il y a des mots dans le filtre if len(words) > 0: #on filtre la matrice words.extend(self.A.loc[words].loc[:, (self.A.loc[words] != 0).any( axis=0)].columns) else: #sinon on ne filtre rien et on prend tout words.extend(self.A.columns) words = list(set(words)) Ap = self.A.loc[words, words] #filtre sur les 30 mots les plus fréquents # words = corpus.most_frequent_words(30) #filtre sur les stopwords to_delete = [] for word in words: if self.corpus.is_stop_words(word): to_delete.append(word) for word in to_delete: words.remove(word) Ap = Ap.loc[words, words] #filtre sur les mots plus fréquents que la moyenne freq = self.corpus.frequencies freq = freq.loc[freq.index.isin(Ap.index)] term_freq_mean = freq["term frequency"].mean() words = list(freq[freq["term frequency"] >= term_freq_mean].index) Ap = Ap.loc[words, words] #filtre sur les mots plus co-occurrents que la moyenne moyennes = Ap.mean() moyenne = moyennes.mean() words = list(moyennes[moyennes >= moyenne].index) Ap = Ap.loc[words, words] ########################################################################################################### #test de calcul des collocats # for wordx in Ap.columns: # for wordy in Ap.index: # print(self.pmi_func(Ap, wordx, wordy)) self.nb_words = len(words) #calcul du graphe edge1 = Ap.stack() edge1 = edge1.reset_index() edge1 = edge1[edge1[0] != 0] edge1["from"] = edge1.apply(lambda x: min(x[["level_0", "level_1"]]), axis=1) edge1["to"] = edge1.apply(lambda x: max(x[["level_0", "level_1"]]), axis=1) edge1["qt"] = edge1[0] edge1 = edge1.drop(columns=["level_0", "level_1", 0]) edge1 = edge1.drop_duplicates().reset_index(drop=True) node1 = pd.DataFrame(Ap.index) node1.columns = ("name", ) self.G = nx.from_pandas_edgelist(edge1, 'from', 'to', ['from', 'to', 'qt'], create_using=nx.Graph()) #calcul des différentes couches (shell) du graphe (mots organisés en cercles : centre = plus fréquent) #on détermine le nombre de couches (nb_shells) base = 3 nwords = len(words) initinA = list(Ap.index[Ap.index.isin(WordsToSearch)]) ninitinA = len(initinA) n = nwords - ninitinA if ninitinA > 0: nb_shells = ceil(log(n, base) - log(ninitinA, base)) else: nb_shells = ceil(log(n, base)) #puis la taille de base d'une couche s = (base - 1) * n / (base**nb_shells - 1) if ninitinA > 0: shells = [WordsToSearch] flat_shells = initinA.copy() else: shells = [] flat_shells = [] #puis on crée chaque couche for i in range(int(nb_shells + 0.5) - 1): #x est le nombre de mot pour la couche i x = ceil(s * base**i) if len(flat_shells) > 0: shell = list( Ap[Ap.loc[flat_shells] != 0]. loc[:, ~Ap.index.isin(flat_shells)].max().sort_values( ascending=False).head(x).index) else: shell = list( Ap.max().sort_values(ascending=False).head(x).index) flat_shells.extend(shell) shells.append(shell) #la dernière couche étant tout ce qui n'est pas dans les autres couches shell = list(Ap.index[~Ap.index.isin(flat_shells)]) shells.append(shell) #on envoie ensuite les couches au moteur de networkx pos = nx.drawing.layout.shell_layout(self.G, shells, dim=2) #autres tests de rendu du graphe : peu concluant # pos = nx.drawing.layout.random_layout(self.G, dim=2, center=None) # pos = nx.drawing.layout.spring_layout(self.G) # pos = nx.drawing.layout.kamada_kawai_layout(self.G) # pos = nx.drawing.layout.spectral_layout(self.G) # pos = nx.drawing.layout.spiral_layout(self.G) #on récupère alors les positions des noeuds de networkx et on les stocke quelque part où on peut revenir les chercher for node in self.G.nodes: self.G.nodes[node]['pos'] = list(pos[node]) #traceRecode va contenir tous les éléments du graphe traceRecode = [] ############################################################################################################################################################ #gestion des couleurs des arrêtes colors = list( Color('lightyellow').range_to( Color('darkred'), max(edge1['qt']) - min(edge1['qt']) + 1)) colors = ['rgb' + color_to_str(x.rgb) for x in colors] #et de leur transparance alphas = [((i - min(edge1['qt'])) / (max(edge1['qt']) - min(edge1['qt'])) / 10) + 0.05 for i in range(min(edge1['qt']), max(edge1['qt']) + 1)] #on crée ensuite un trait pour chaque arrête for edge in self.G.edges: x0, y0 = self.G.nodes[edge[0]]['pos'] x1, y1 = self.G.nodes[edge[1]]['pos'] trace = go.Scatter( x=tuple([x0, x1, None]), y=tuple([y0, y1, None]), mode='lines', line={'width': 1}, marker=dict(color=colors[self.G.edges[edge]['qt'] - min(edge1['qt'])]), line_shape='spline', opacity=alphas[self.G.edges[edge]['qt'] - min(edge1['qt'])]) traceRecode.append(trace) ############################################################################################################################################################### #on crée ensuite les marker pour les noeuds sizes = Ap.sum().values * 5 / len(words) + 5 node_trace = go.Scatter( x=[], y=[], hovertext=[], text=[], mode='markers+text', textposition="bottom center", #textfont=dict(color="Orange"), hoverinfo="text", marker={ 'size': sizes, 'color': 'LightSkyBlue' }) #puis on stocke les informations des noeuds dans traceRecode index = 0 for node in self.G.nodes(): x, y = self.G.nodes[node]['pos'] # hovertext = "name: " + node1['name'][index] text = node1['name'][index] node_trace['x'] += tuple([x]) node_trace['y'] += tuple([y]) # node_trace['hovertext'] += tuple([hovertext]) node_trace['text'] += tuple([text]) index = index + 1 traceRecode.append(node_trace) ################################################################################################################################################################# #enfin on crée une "figure" qui pourra être utilisée par dash et plotly figure = { "data": traceRecode, "layout": go.Layout(showlegend=False, hovermode='closest', margin={ 'b': 40, 'l': 40, 'r': 40, 't': 40 }, xaxis={ 'showgrid': False, 'zeroline': False, 'showticklabels': False }, yaxis={ 'showgrid': False, 'zeroline': False, 'showticklabels': False }, height=600, clickmode='event') } return figure #méthode PMI def pmi_func(self, A, x, y): freq_x = A.groupby(x).transform('count') freq_y = A.groupby(y).transform('count') freq_x_y = A.groupby([x, y]).transform('count') return np.log(len(A.index) * (freq_x_y / (freq_x * freq_y))) def callbacks(self): ###################################événements pour le reset de corpus et le filtre de mots @self.app.callback([ dash.dependencies.Output('my-graph', 'figure'), dash.dependencies.Output('title', 'children') ], [ dash.dependencies.Input('words', 'value'), dash.dependencies.Input('words', 'n_submit'), dash.dependencies.Input("reset", "n_clicks"), dash.dependencies.Input('theme', 'value'), dash.dependencies.Input('nbdocs', 'value') ]) def update_output(words, ns, nc, theme, nb_docs): #cette fonction sera appelée si on appuie sur le bouton de reset ou si on appuie sur entrée dans le filtre #si on a appuyé sur le bouton if nc != None and nc > self.NSUBMIT_corpus: self.NSUBMIT_corpus = nc #si le theme ou le nombre de docs à changé if self.FIG == None or self.THEME != theme or self.NB_DOCS != nb_docs: #on re setup le corpus self.setup_corpus(theme, nb_docs) #et on recalcule le graphe self.FIG = self.network_graph() #sinon si on a appuyé sur entrée elif ns != None and ns > self.NSUBMIT_words: #on recalcule le graphe self.NSUBMIT_words = ns self.WORDS = words self.FIG = self.network_graph() #on renvoie le graphe et le titre return self.FIG, "Co-occurrences of words in the corpus '" + self.corpus.name + "' - Number of words : " + str( self.nb_words) ################################événement pour le hover (survol) des noeuds @self.app.callback(dash.dependencies.Output('hover-data', 'children'), dash.dependencies.Input('my-graph', 'hoverData')) def display_hover_data(hoverData): #cette fonction sera appelée si on survole un noeud #si on survole bien un noeud if hoverData != None and "text" in hoverData["points"][0]: #on récupère le mot du noeud survolé word = hoverData["points"][0]["text"] #si on n'a jamais calculé une des métriques, on la calcule if word not in self.DEGCEN: self.DEGCEN[word] = nx.degree_centrality(self.G)[word] if word not in self.CLOCEN: self.CLOCEN[word] = nx.closeness_centrality(self.G, word) if word not in self.BETCEN: self.BETCEN[word] = nx.betweenness_centrality(self.G)[word] #on récupère les fréquences du mot dans le corpus frequencies = self.corpus.get_frequencies(word) #et on renvoie tout ça en json datas = { "Word": word, "Term frequency": int(frequencies["term frequency"].values[0]), "Document frequency": int(frequencies["document frequency"].values[0]), "Degree centrality": self.DEGCEN[word], "Closeness centrality": self.CLOCEN[word], "Betweenness centrality": self.BETCEN[word] } return json.dumps(datas, indent=2) ###############################événement pour le clic des noeuds @self.app.callback(dash.dependencies.Output('click-data', 'children'), [dash.dependencies.Input('my-graph', 'clickData')]) def display_click_data(clickData): #cette fonction sera appelée si on clique sur un noeud #si on clique bien sur un noeud if clickData != None and "text" in clickData["points"][0]: #on récupère le mot du noeud cliqué word = clickData["points"][0]["text"] #si on n'a jamais calculé une des métriques, on la calcule if word not in self.DEGCEN: self.DEGCEN[word] = nx.degree_centrality(self.G)[word] if word not in self.CLOCEN: self.CLOCEN[word] = nx.closeness_centrality(self.G, word) if word not in self.BETCEN: self.BETCEN[word] = nx.betweenness_centrality(self.G)[word] #on récupère les fréquences du mot dans le corpus frequencies = self.corpus.get_frequencies(word) #et on renvoie tout ça en json datas = { "Word": word, "Term frequency": int(frequencies["term frequency"].values[0]), "Document frequency": int(frequencies["document frequency"].values[0]), "Degree centrality": self.DEGCEN[word], "Closeness centrality": self.CLOCEN[word], "Betweenness centrality": self.BETCEN[word] } return json.dumps(datas, indent=2)