from jyquickhelper import add_notebook_menu
add_notebook_menu()

"""
Explanation: TD7 - Analyse de texte
Analyse de texte, TF-IDF, LDA, moteur de recherche, expressions régulières.
End of explanation
"""


from pyensae.datasource import download_data
download_data("df_pocket.zip")

"""
Explanation: Récupération des données
Il est possible de télécharger les données df_pocket.zip.
End of explanation
"""


import json
from pprint import pprint

with open('./data_pocket.json') as fp:    
    dict_pocket = json.load(fp)
dict_to_df = {}

keys = ['resolved_url', 'resolved_title', 'excerpt', 'tags']

for (k,v) in dict_pocket.items():
    dict_to_df[k] = dict(zip(keys, [v[key] for key in keys if key in v]))
import pandas as p
df_pocket = p.DataFrame.from_dict(dict_to_df, orient = "index")
df_pocket.head()

import pandas as p
import ast
df_pocket = p.read_csv('./df_pocket.csv')
df_pocket.head()

df_pocket['html_soup'] = df_pocket['html_soup'].apply(lambda x : ast.literal_eval(x) if x != "scraper banned" else x)
df_pocket['tags'] = df_pocket['tags'].apply(lambda x : ast.literal_eval(x) if x == x else x)
df_pocket.head()

def nan_to_string(x):
    if x==x:
        return x
    else:
        return ''

title_string = ' '.join(df_pocket['title'].apply( lambda x: nan_to_string(x)))

import re
def url_cleaning(url):
    return ' '.join(re.split(r'\/|\.|\:|-|\?',url))

url_string = ' '.join(df_pocket['url'].apply(lambda x : url_cleaning(x)))

def hx_cleaning(d,hx):
    if str(hx) in d:
        return ' '.join(d[str(hx)])
    else: 
        return ''
     
h1_string = ' '.join(df_pocket['html_soup'].apply(lambda x : hx_cleaning(x,'h1')))

h2_string = ' '.join(df_pocket['html_soup'].apply(lambda x : hx_cleaning(x,'h2')))
h3_string = ' '.join(df_pocket['html_soup'].apply(lambda x : hx_cleaning(x, 'h3')))
excerpt_string = ' '.join(df_pocket['excerpt'].apply( lambda x: nan_to_string(x)))

def p_cleaning(x):
    if (type(x) == dict) & ('p' in x ):
        return ' '.join(x['p'])
    else: 
        return ''

p_string = ' '.join(df_pocket['html_soup'].apply(lambda x : p_cleaning(x)))

words = ' '.join([title_string,url_string,h1_string,h2_string,h3_string,excerpt_string])
# on ne conserve que les mots
words_string = re.sub('[^A-Za-z ]','', words)
#on "nettoie les espaces"
words_string = re.sub('\s+',' ', words_string)
words_string[:1000] + "..."

! pip install nltk

"""
Explanation: Nettoyer les données (regexp et nltk)
Mais avant tout, nous devrions augmenter la qualité de nos données (en améliorant les parsers et la liste des stopwords). C'est l'objet de cette section.
End of explanation
"""


from wordcloud import WordCloud, STOPWORDS

stopwords = set(STOPWORDS)
print(STOPWORDS)

for wds in ['http', 'https', 'www', 'fr', 'com', 'io', 'org', 'co', 'jo', 'edu', 'news', 'html', 'htm',\
            'github', 'youtube', 'google', 'blog', 'watch', 'de', 'le', 'la', 'en', 'sur', 'vous', 'les', \
           'ajouter', 'README', 'md', 'et', 'PROCESS', 'CMYK', 'des', 'chargement', 'playlists', 'endobj', \
           'obj','est', 'use', 'using', 'will', 'web', 'first','pour', 'du', 'une', 'que']:
    stopwords.add(wds)

import nltk

stopwords_fr_ntlk = set(nltk.corpus.stopwords.words('french'))
stopwords_en_ntlk = set(nltk.corpus.stopwords.words('english'))
stopwords_clean = [ l.lower() for l in list(stopwords.union(stopwords_fr_ntlk).union(stopwords_en_ntlk))]
stopwords_clean[:50] + ['...']

%pylab inline

wordcloud = WordCloud(stopwords=stopwords, background_color="white")

wordcloud.generate(words_string)

plt.imshow(wordcloud)
plt.axis('off')
plt.show()

"""
Explanation: Nltk contient un corpus de stopwords en plusieurs langues. On peut enrichir la liste déjà créée.
End of explanation
"""


def words_cleaning(url,title,excerpt,html_soup):
    url_clean = url_cleaning(url)
    title_clean = nan_to_string(title)
    excerpt_clean = nan_to_string(excerpt)
    h1_clean = hx_cleaning(html_soup,'h1')
    h2_clean = hx_cleaning(html_soup,'h2')
    h3_clean = hx_cleaning(html_soup,'h3')
    p_clean = p_cleaning(html_soup)
    words = ' '.join([url_clean, title_clean, excerpt_clean, h1_clean, h2_clean, h3_clean, p_clean])
    words_clean = re.sub('[^A-Za-z ]','', words)
    words_clean = re.sub('\s+',' ', words_clean)
    words_list = words_clean.split(' ')
    return ' '.join([w.lower() for w in words_list if w not in stopwords_clean])

df_pocket['words_string'] = np.vectorize(words_cleaning)(df_pocket['url'], \
                                                         df_pocket['title'], \
                                                         df_pocket['excerpt'], \
                                                         df_pocket['html_soup'])
df_pocket.head()

"""
Explanation: On applique tout ça à df_pocket.
End of explanation
"""


corpus = { 
 'a' : "Mr. Green killed Colonel Mustard in the study with the candlestick. \
Mr. Green is not a very nice fellow.",
 'b' : "Professor Plum has a green plant in his study.",
 'c' : "Miss Scarlett watered Professor Plum's green plant while he was away \
from his office last week."
}
terms = {
 'a' : [ i.lower() for i in corpus['a'].split() ],
 'b' : [ i.lower() for i in corpus['b'].split() ],
 'c' : [ i.lower() for i in corpus['c'].split() ]
 }

from math import log

QUERY_TERMS = ['mr.', 'green']

def tf(term, doc, normalize=True):
    doc = doc.lower().split()
    if normalize:
        return doc.count(term.lower()) / float(len(doc))
    else:
        return doc.count(term.lower()) / 1.0


def idf(term, corpus):
    num_texts_with_term = len([True for text in corpus if term.lower() \
                              in text.lower().split()])
    try:
        return 1.0 + log(float(len(corpus)) / num_texts_with_term)
    except ZeroDivisionError:
        return 1.0
    
def tf_idf(term, doc, corpus):
    return tf(term, doc) * idf(term, corpus)

for (k, v) in sorted(corpus.items()):
    print(k, ':', v)
print('\n')

query_scores = {'a': 0, 'b': 0, 'c': 0}
for term in [t.lower() for t in QUERY_TERMS]:
    for doc in sorted(corpus):
        print('TF({}): {}'.format(doc, term), tf(term, corpus[doc]))
    print('IDF: {}'.format(term, ), idf(term, corpus.values()))
    print('\n')
    for doc in sorted(corpus):
        score = tf_idf(term, corpus[doc], corpus.values())
        print('TF-IDF({}): {}'.format(doc, term), score)
        query_scores[doc] += score
    print('\n')

print("Score TF-IDF total pour le terme '{}'".format(' '.join(QUERY_TERMS), ))
for (doc, score) in sorted(query_scores.items()):
    print(doc, score)

"""
Explanation: A présent la base df_pocket est nettoyée et prête à être utilisée pour les analyses de textes. 
Analyse des données textuelles - TD-IDF, similarité cosine et n-grams
Le calcul tf-idf (term frequency–inverse document frequency) permet de calculer un score de proximité entre un terme de recherche et un document (c'est ce que font les moteurs de recherche). 
La partie tf calcule une fonction croissante de la fréquence du terme de recherche dans le document à l'étude, la partie idf calcule une fonction inversement proportionnelle à la fréquence du terme dans l'ensemble des documents (ou corpus). 
Le score total, obtenu en multipliant les deux composantes, permet ainsi de donner un score d'autant plus élevé que le terme est surréprésenté dans un document (par rapport à l'ensemble des documents). Il existe plusieurs fonctions, qui pénalisent plus ou moins les documents longs, ou qui sont plus ou moins smooth.
End of explanation
"""



from math import log

def tf(term, doc, normalize=True):
    doc = doc.lower().split()
    if normalize:
        return doc.count(term.lower()) / float(len(doc))
    else:
        return doc.count(term.lower()) / 1.0


def idf(term, corpus):
    num_texts_with_term = len([True for text in corpus if term.lower()
                              in text.lower().split()])
    try:
        return 1.0 + log(float(len(corpus)) / num_texts_with_term)
    except ZeroDivisionError:
        return 1.0

def tf_idf(term, doc, corpus):
    return tf(term, doc) * idf(term, corpus)



QUERY_TERMS = ['green', 'plant']
query_scores = {'a': 0, 'b': 0, 'c': 0}
for term in [t.lower() for t in QUERY_TERMS]:
    for doc in sorted(corpus):
        score = tf_idf(term, corpus[doc], corpus.values())
        query_scores[doc] += score

print("Score TF-IDF total pour le terme '{}'".format(' '.join(QUERY_TERMS), ))
for (doc, score) in sorted(query_scores.items()):
    print(doc, score)

"""
Explanation: Exercice guidé - Calcul de TF-IDF
Quel document est le plus proche du terme "green plant" ? Calculer les scores TF-IDF pour le terme "green plant". Cela correspond-il à vos attentes ? Que se passe-t-il avec "green" seul ?
Green plant
End of explanation
"""


QUERY_TERMS = ['green']
term = [t.lower() for t in QUERY_TERMS]
term = 'green'

query_scores = {'a': 0, 'b': 0, 'c': 0}

for doc in sorted(corpus):
    score = tf_idf(term, corpus[doc], corpus.values())
    query_scores[doc] += score

print("Score TF-IDF total pour le terme '{}'".format(term))
for (doc, score) in sorted(query_scores.items()):
    print(doc, score)

"""
Explanation: Deux documents possibles : b ou c (a ne contient pas le mot « plant »). B est plus court : donc green plant « pèse » plus.
Green
End of explanation
"""


import re, nltk
#Tokenisation naïve sur les espaces entre les mots => on obtient une liste de mots
tokens = re.split('\s+', ' '.join(df_pocket['words_string']))

#On transforme cette liste en objet nltk "Text" (objet chaine de caractère qui conserve la notion de tokens, et qui 
#comprend  un certain nombre de méthodes utiles pour explorer les données.
text = nltk.Text(tokens)

## la méthode vocab permet d'obtenir pour chaque terme présent dans la liste text nltk, le nombre d'occurence des termes
## ici on crée le dictionnaire fdist

fdist = text.vocab()

list(fdist.items())[:30]

# Une autre méthode "concordance" : montre les occurences d'un mot dans son contexte
print("Exemples d'occurences du terme 'python' :")
text.concordance("python")
print('\n')

"""
Explanation: Exercice 1 - TF-IDF sur des données pocket
Prenez 5 articles enregistrés dans pocket et déterminer leur score pour les mots python, data et science.
Exercice 2 - Création d'un moteur de recherche pour les données pocket
L'idée de cet exercice est de créer un moteur de recherche pour ['python','data','science']. 
Le but : trouver les 5 articles les plus pertinents pour ces termes. 
1) La première étape sera de calculer pour chaque article de la base le score td-idf.

2) La seconde étape sera de trier ces scores du plus élevé au moins fort.

Exercice 3 - Catégorisation automatique avec des méthodes non supervisées
Avec ce que vous venez d'apprendre (le tf-idf), il est possible de créer un modèle de catégorisation automatique "non-supervisé". Ce terme barbare signifie que l'on peut créér des tags à partir des seules variables explicatives, sans utiliser de "label", c'est-à-dire de données qui valident si la prédiction (ici, présence de tel ou tel mot dans les tags) est correcte. Normalement, on utilise ce genre de méthode quand on a pas de labels et que l'on cherche à faire ressortir des régularités (des patterns) dans les données. D'autres méthodes de machine learning non-supervisées connues sont : le clustering, les ACP.
Pour bien comprendre le tf-idf, on vous l'a fait coder "à la main". En réalité, c'est tellement classique, qu'il existe des librairies qui l'ont déjà codé. Voir scikitlearn.feature_extraction.text
L'idée est la suivante : on va retenir comme "tags", les 3 mots les plus "caractéristiques" d'un document. C'est-à-dire, les mots correspondants aux 3 scores tf-idf les plus élevés.
Les étapes à suivre : 
- transformer les mots en vecteurs. L'idée est de créer une matrice, avec en ligne les documents, en colonne les mots possibles (prendre tous le smots uniques présents dans l'ensemble des documents). Cela se fait en 3 lignes de code, voir la documentation scikitlearn.feature_extraction.text
- calculer les tf-idf normalisés
- récupérer les indices dont les mots ont le score le plus élevé : voir la méthode argsort
- récupérer la correspondance mots et indices
- récupérer les 3 mots les plus caractéristiques, et comparer aux tags de la table df_pocket
Approche contextuelle
Les approches bag-of-words, bien que simplistes, permettent de créer, d'indexer et de comparer des documents. La prise en compte des suites de 2, 3 ou plus mots serait un moyen d'affiner de tels modèles. Cela permet aussi de mieux comprendre le sens des homonymes, et des phrases (d'une manière générale, la sémantique).
nltk offre des methodes pour tenir compte du contexte : pour ce faire, nous calculons les n-grams, c'est-à-dire l'ensemble des co-occurrences successives de mots deux-à-deux (bigrams), trois-à-trois (tri-grams), etc. 
En général, on se contente de bi-grams, au mieux de tri-grams : 
- les modèles de classification, analyse du sentiment, comparaison de documents, etc. qui comparent des n-grams avec n trop grands sont rapidement confrontés au problème de données sparse, cela réduit la capacité prédictive des modèles ; 
- les performances décroissent très rapidement en fonction de n, et les coûts de stockage des données augmentent rapidement (environ n fois plus élevé que la base de donnée initiale).
référence : introduction à nltk 
Dans cette partie, nous allons nous intéresser au nombre d'occurences et de co-occurences des termes dans les articles de la base pocket. Pour cela, nous utilisons les méthodes disponibles dans le package nltk
End of explanation
"""


