[PYTHON] [Traitement du langage naturel] J'ai essayé de visualiser les sujets d'actualité cette semaine dans la communauté Slack

À propos de cet article

Dans cet article, je vais vous montrer comment utiliser Wordcloud pour visualiser les sujets qui ont été soulevés dans la communauté Slack sur une période donnée (1 semaine ici).

Le code source peut être trouvé ici: octocat:

Je veux aussi lire: [Traitement du langage naturel] J'ai essayé de visualiser les remarques de chaque membre de la communauté Slack

table des matières

  1. Exemple d'utilisation et de sortie
  2. Recevez le message de Slack
  3. Prétraitement: création d'une table de magasin de messages
  4. Prétraitement: nettoyage
  5. Prétraitement: analyse morphologique (Janome)
  6. Prétraitement: normalisation
  7. Prétraitement: suppression du mot d'arrêt
  8. Prétraitement: extraction de phrases importantes (tf-idf)
  9. Processus de visualisation avec Wordcloud
  10. Bonus

* Je voudrais résumer le prétraitement dans un autre article à l'avenir </ font>

1. Exemple d'utilisation et de sortie

1.1. Comment utiliser

Pour plus d'informations, consultez Mise en route dans README. Le flux est comme ça.

  1. Créez un environnement virtuel avec docker-compose up -d
  2. Entrez dans le shell avec docker exec -it ds-py3 bash
  3. Exécutez run_wordcloud_by_term.sh

1.2. Exemple de sortie

Ceci est un exemple de sortie réelle. Wordcloud est la remarque de chaque période différente.

anim__.gif

2. Recevez le message de Slack

2.1. Utiliser l'API Slack

Obtenez des jetons d'API Slack auprès de Slack API Official

Comment démarrer avec l'API Slack n'est pas montré ici.

Pour effectuer le traitement ultérieur, veuillez obtenir les jetons suivants

  • Channel API
  • Users API

2.2. Créer une classe pour obtenir des informations Slack via l'API

Ici, nous allons créer une classe ** SlackApp ** qui obtient des informations Slack via l'API. Les informations acquises sont enregistrées au format JSON sans traitement.

slack_app.py


#Une classe qui utilise slackapi pour obtenir les informations souhaitées(Ne pas traiter)
import requests
import json
from tqdm import tqdm
import pandas as pd


class SlackApp:
    ch_list_url = 'https://slack.com/api/channels.list'
    ch_history_url = 'https://slack.com/api/channels.history'
    usr_list_url = 'https://slack.com/api/users.list'

    def __init__(self, ch_api_key, usr_api_key):
        # NEW members
        self.channels_info = []
        self.users_info = []
        self.messages_info = []
        # OLD members
        self.channelInfo = {}  # k: ch_name, v: ch_id
        self.messages_in_chs = {}
        self.userInfo = {}
        self.ch_api_token = str(ch_api_key)
        self.usr_api_token = str(usr_api_key)

    def load_save_channel_info(self, outdir: str):
        #Obtenez des informations sur la chaîne via l'API Slack et enregistrez-les dans un fichier
        payload = {'token': self.ch_api_token}
        response = requests.get(SlackApp.ch_list_url, params=payload)
        if response.status_code == 200:
            json_data = response.json()
            if 'channels' in json_data.keys():
                self.channels_info = json_data['channels']
            with open(outdir + '/' + 'channel_info.json', 'w', encoding='utf-8') as f:
                json.dump(self.channels_info, f, indent=4, ensure_ascii=False)

    def load_save_user_info(self, outdir: str):
        #Obtenez des informations sur l'utilisateur via l'API Slack et enregistrez-les dans un fichier
        payload = {'token': self.usr_api_token}
        response = requests.get(SlackApp.usr_list_url, params=payload)
        if response.status_code == 200:
            json_data = response.json()
            if 'members' in json_data.keys():
                self.users_info = json_data['members']
            with open(outdir + '/' + 'user_info.json', 'w', encoding='utf-8') as f:
                json.dump(self.users_info, f, indent=4, ensure_ascii=False)

    def load_save_messages_info(self, outdir: str):
        #Créer une liste d'identifiants de chaînes
        channel_id_list = []
        for ch in self.channels_info:
            channel_id_list.append(ch['id'])
        #Obtenez des informations sur l'utilisateur via l'API Slack et enregistrez-les dans un fichier
        for ch_id in tqdm(channel_id_list, desc='[loading...]'):
            payload = {'token': self.ch_api_token, 'channel': ch_id}
            response = requests.get(SlackApp.ch_history_url, params=payload)
            if response.status_code == 200:
                json_data = response.json()
                msg_in_ch = {}
                msg_in_ch['channel_id'] = ch_id
                if 'messages' in json_data.keys():
                    msg_in_ch['messages'] = json_data['messages']
                else:
                    msg_in_ch['messages'] = ''
                self.messages_info.append(msg_in_ch)
        with open(outdir + '/' + 'messages_info.json', 'w', encoding='utf-8') as f:
            json.dump(self.messages_info, f, indent=4, ensure_ascii=False)        

2.3. Obtenir des informations sur Slack

Utilisez la classe ** SlackApp ** plus tôt pour obtenir les informations.

Les informations à acquérir sont les trois suivantes

  1. Liste des chaînes
  2. Liste de messages (obtenue pour chaque canal)
  3. Liste des utilisateurs

slack_msg_extraction.py


#Une classe qui utilise slackapi pour obtenir les informations souhaitées
import sys
import json
sys.path.append('../../src/d000_utils') #Ajout du chemin de stockage pour les scripts SlackApp
import slack_app as sa


def main():
    # -------------------------------------
    # load api token
    # -------------------------------------
    credentials_root = '../../conf/local/'
    credential_fpath = credentials_root + 'credentials.json'
    print('load credential.json ...')
    with open(credential_fpath, 'r') as f:
        credentials = json.load(f)
    # -------------------------------------
    # start slack app
    # -------------------------------------    
    print('start slack app ...')
    app = sa.SlackApp(
        credentials['channel_api_key'],
        credentials['user_api_key']
        )
    outdir = '../../data/010_raw'
    # -------------------------------------
    # get channels info
    # -------------------------------------
    app.load_save_channel_info(outdir)
    # -------------------------------------
    # get user info
    # -------------------------------------
    app.load_save_user_info(outdir)
    # -------------------------------------
    # get msg info
    # -------------------------------------
    app.load_save_messages_info(outdir)


if __name__ == "__main__":
    main()

3. Prétraitement: création d'une table de magasin de messages

3.1. Conception de la table du magasin de messages

Les informations acquises par SlackAPI ont été enregistrées au format JSON selon les spécifications de SlackAPI. Mettez en forme les données du tableau afin qu'elles puissent être facilement analysées. Lors de la conception d'une table, tenez compte de Tidy Data.

Cette fois, je l'ai conçu comme suit. J'ai fait le tableau suivant avec l'image des informations minimales requises + α.

message mart table

No Column Name Type Content
0 index int AUTO INCREMENT
1 ch_id str Identifiant de la chaine
2 msg str Chaîne de message
3 uid str ID utilisateur de l'orateur
4 timestamp datetime Il est temps de parler

table des canaux (bonus)

No Column Name Type Content
0 index int AUTO INCREMENT
1 ch_id str Identifiant de la chaine
2 ch_name str Nom du canal (nom affiché sur l'interface utilisateur de Slack)
3 ch_namenorm str Nom de canal normalisé
4 ch_membernum int Nombre de participants à la chaîne

table des utilisateurs (bonus)

No Column Name Type Content
0 index int AUTO INCREMENT
1 uid str Identifiant d'utilisateur
2 uname str Nom d'utilisateur

3.2. Données RAW → Format de la table du magasin de messages

Le code réel est ci-dessous.

  • 【Remarque】 -- ../../ data / 010_raw: informations obtenues à partir de l'emplacement de stockage au format Slack JSON --ʻUser_info.json: nom du fichier d'informations utilisateur (JSON) --messages_info.json: nom du fichier d'informations de message (JSON) pour tous les canaux --channel_info.json`: nom du fichier d'informations de canal (JSON)

make_msg_mart_table.py


import json
import pandas as pd


def make_user_table(usr_dict: dict) -> pd.DataFrame:
    uid_list = []
    uname_list = []
    for usr_ditem in usr_dict:
        if usr_ditem['deleted'] == True:
            continue
        uid_list.append(usr_ditem['id'])
        uname_list.append(usr_ditem['profile']['real_name_normalized'])
    user_table = pd.DataFrame({'uid': uid_list, 'uname': uname_list})
    return user_table


def make_msg_table(msg_dict: dict) -> pd.DataFrame:
    ch_id_list = []
    msg_list = []
    uid_list = []
    ts_list = []
    for msg_ditem in msg_dict:
        if 'channel_id' in msg_ditem.keys():
            ch_id = msg_ditem['channel_id']
        else:
            continue
        if 'messages' in msg_ditem.keys():
            msgs_in_ch = msg_ditem['messages']
        else:
            continue
        # get message in channel
        for i, msg in enumerate(msgs_in_ch):
            # if msg by bot, continue
            if 'user' not in msg:
                continue
            ch_id_list.append(ch_id)
            msg_list.append(msg['text'])
            uid_list.append(msg['user'])  #Il n'y a pas cette clé pour le bot
            ts_list.append(msg['ts'])
    df_msgs = pd.DataFrame({
        'ch_id': ch_id_list,
        'msg': msg_list,
        'uid': uid_list,
        'timestamp': ts_list
    })
    return df_msgs


def make_ch_table(ch_dict: dict) -> pd.DataFrame:
    chid_list = []
    chname_list = []
    chnormname_list = []
    chmembernum_list = []
    for ch_ditem in ch_dict:
        chid_list.append(ch_ditem['id'])
        chname_list.append(ch_ditem['name'])
        chnormname_list.append(ch_ditem['name_normalized'])
        chmembernum_list.append(ch_ditem['num_members'])
    ch_table = pd.DataFrame({
        'ch_id': chid_list,
        'ch_name': chname_list,
        'ch_namenorm': chnormname_list,
        'ch_membernum': chmembernum_list
    })
    return ch_table


def main():
    # 1. load user/message/channels
    input_root = '../../data/010_raw'
    user_info_fpath = input_root + '/' + 'user_info.json'
    with open(user_info_fpath, 'r', encoding='utf-8') as f:
        user_info_rawdict = json.load(f)
        print('load ... ', user_info_fpath)
    msg_info_fpath = input_root + '/' + 'messages_info.json'
    with open(msg_info_fpath, 'r', encoding='utf-8') as f:
        msgs_info_rawdict = json.load(f)
        print('load ... ', msg_info_fpath)
    ch_info_fpath = input_root + '/' + 'channel_info.json'
    with open(ch_info_fpath, 'r', encoding='utf-8') as f:
        ch_info_rawdict = json.load(f)
        print('load ... ', ch_info_fpath)
    # 2. make and save tables
    # user
    output_root = '../../data/020_intermediate'
    df_user_info = make_user_table(user_info_rawdict)
    user_tbl_fpath = output_root + '/' + 'users.csv'
    df_user_info.to_csv(user_tbl_fpath, index=False)
    print('save ... ', user_tbl_fpath)
    # msg
    df_msg_info = make_msg_table(msgs_info_rawdict)
    msg_tbl_fpath = output_root + '/' + 'messages.csv'
    df_msg_info.to_csv(msg_tbl_fpath, index=False)
    print('save ... ', msg_tbl_fpath)
    # channel
    df_ch_info = make_ch_table(ch_info_rawdict)
    ch_tbl_fpath = output_root + '/' + 'channels.csv'
    df_ch_info.to_csv(ch_tbl_fpath, index=False)
    print('save ... ', ch_tbl_fpath)


if __name__ == "__main__":
    main()

4. Prétraitement: nettoyage

4.1. Contenu du processus de nettoyage

Fait généralement référence à l'action de supprimer le bruit Il est nécessaire d'effectuer divers traitements en fonction des données cibles et du but. Ici, le traitement suivant a été exécuté.

  1. Supprimer la chaîne d'URL
  2. Supprimez la chaîne de mention
  3. Supprimer les pictogrammes Unicode
  4. Supprimez les caractères spéciaux en HTML (tels que & gt)
  5. Supprimer le bloc de code
  6. Supprimer le bloc de code en ligne
  7. Suppression du message "○○ a rejoint la chaîne"
  8. Autre suppression du bruit propre à cette communauté

4.2. Mise en œuvre du processus de nettoyage

cleaning.py


import re
import pandas as pd
import argparse
from pathlib import Path


def clean_msg(msg: str) -> str:
    # sub 'Return and Space'
    result = re.sub(r'\s', '', msg)
    # sub 'url link'
    result = re.sub(r'(<)http.+(>)', '', result)
    # sub 'mention'
    result = re.sub(r'(<)@.+\w(>)', '', result)
    # sub 'reaction'
    result = re.sub(r'(:).+\w(:)', '', result)
    # sub 'html key words'
    result = re.sub(r'(&).+?\w(;)', '', result)
    # sub 'multi lines code block'
    result = re.sub(r'(```).+(```)', '', result)
    # sub 'inline code block'
    result = re.sub(r'(`).+(`)', '', result)
    return result


def clean_msg_ser(msg_ser: pd.Series) -> pd.Series:
    cleaned_msg_list = []
    for i, msg in enumerate(msg_ser):
        cleaned_msg = clean_msg(str(msg))
        if 'Rejoint la chaîne' in cleaned_msg:
            continue
        cleaned_msg_list.append(cleaned_msg)
    cleaned_msg_ser = pd.Series(cleaned_msg_list)
    return cleaned_msg_ser


def get_ch_id_from_table(ch_name_parts: list, input_fpath: str) -> list:
    df_ch = pd.read_csv(input_fpath)
    ch_id = []
    for ch_name_part in ch_name_parts:
        for i, row in df_ch.iterrows():
            if ch_name_part in row.ch_name:
                ch_id.append(row.ch_id)
                break
    return ch_id


def main(input_fname: str):
    input_root = '../../data/020_intermediate'
    output_root = input_root
    # 1. load messages.csv (including noise)
    msgs_fpath = input_root + '/' + input_fname
    df_msgs = pd.read_csv(msgs_fpath)
    print('load :{0}'.format(msgs_fpath))
    # 2. Drop Not Target Records
    print('drop records (drop non-target channel\'s messages)')
    non_target_ch_name = ['general', 'Annonce de la direction']
    non_target_ch_ids = get_ch_id_from_table(non_target_ch_name, input_root + '/' + 'channels.csv')
    print('=== non target channels bellow ====')
    print(non_target_ch_ids)
    for non_target_ch_id in non_target_ch_ids:
        df_msgs = df_msgs.query('ch_id != @non_target_ch_id')
    # 3. clean message string list
    ser_msg = df_msgs.msg
    df_msgs.msg = clean_msg_ser(ser_msg)
    # 4. save it
    pin = Path(msgs_fpath)
    msgs_cleaned_fpath = output_root + '/' + pin.stem + '_cleaned.csv'
    df_msgs.to_csv(msgs_cleaned_fpath, index=False)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_fname", help="set input file name", type=str)
    args = parser.parse_args()
    input_fname = args.input_fname
    main(input_fname)

En fait, je voulais "supprimer uniquement lorsque le bloc de code contient du code source", mais je ne pouvais pas. : transpiration:

5. Prétraitement: analyse morphologique (Janome)

5.1 Qu'est-ce que l'analyse morphologique?

Généralement, c'est le processus de recherche de «morphologie» dans une phrase. Les détails de l'analyse morphologique seront laissés à d'autres articles.

Le véritable objectif ici est de réaliser un «partage».

5.2 Qu'est-ce que la division?

En gros, c'est le processus de conversion d'une phrase en information appelée "mot mot mot". Par exemple

** Exemple de phrase: J'aime le baseball. ** ** ** Exemple de segmentation de phrase: "J'aime le baseball" **

Ce sera sous la forme de.

En pratique, si vous souhaitez gérer des mots qui représentent le contexte d'une phrase comme celle-ci, extract "nouns" </ font> est préférable. Donc,

** Séparation d'exemples de phrases (nom uniquement): "Private baseball" **

Ce serait encore mieux.

  • N'est-il pas préférable d'effacer également «je»? Je vous ai pensé là-bas. Voir le chapitre "Suppression de mots vides"

5.3. Mise en œuvre de l'analyse morphologique et de la division

Si vous souhaitez mettre en œuvre une analyse morphologique

--Que utiliser pour la bibliothèque d'analyse morphologique --Que utiliser comme données de dictionnaire

Nous devons en décider.

Cette fois, j'ai fait ce qui suit.

--Bibliothèque d'analyse morphologique: Janome --Dictionary data: Janome default (NEologd pour les nouveaux mots est encore mieux)

De plus, les mots de partie à extraire sont ceux nécessaires pour atteindre l'objectif en jetant un coup d'œil à "Système de partie de partie de l'outil d'analyse morphologique". quelque chose? J'ai pensé du point de vue.

Janome, la mascotte officielle, est mignonne. (Je ne sais pas si Janome est le nom)

morphological_analysis.py


from janome.tokenizer import Tokenizer
from tqdm import tqdm
import pandas as pd
import argparse
from pathlib import Path
import sys


exc_part_of_speech = {
    "nom": ["Non indépendant", "代nom", "nombre"]
}
inc_part_of_speech = {
    "nom": ["Changer de connexion", "Général", "固有nom"],
}


class MorphologicalAnalysis:

    def __init__(self):
        self.janome_tokenizer = Tokenizer()

    def tokenize_janome(self, line: str) -> list:
        # list of janome.tokenizer.Token
        tokens = self.janome_tokenizer.tokenize(line)
        return tokens

    def exists_pos_in_dict(self, pos0: str, pos1: str, pos_dict: dict) -> bool:
        # Retrurn where pos0, pos1 are in pos_dict or not.
        # ** pos = part of speech
        for type0 in pos_dict.keys():
            if pos0 == type0:
                for type1 in pos_dict[type0]:
                    if pos1 == type1:
                        return True
        return False

    def get_wakati_str(self, line: str, exclude_pos: dict,
                       include_pos: dict) -> str:
        '''
        exclude/include_pos is like this
        {"nom": ["Non indépendant", "代nom", "nombre"], "adjectif": ["xxx", "yyy"]}
        '''
        tokens = self.janome_tokenizer.tokenize(line, stream=True)  #Générateur pour économiser de la mémoire
        extracted_words = []
        for token in tokens:
            part_of_speech0 = token.part_of_speech.split(',')[0]
            part_of_speech1 = token.part_of_speech.split(',')[1]
            # check for excluding words
            exists = self.exists_pos_in_dict(part_of_speech0, part_of_speech1,
                                             exclude_pos)
            if exists:
                continue
            # check for including words
            exists = self.exists_pos_in_dict(part_of_speech0, part_of_speech1,
                                             include_pos)
            if not exists:
                continue
            # append(Forme de couche de surface acquise pour absorber les fluctuations de notation)
            extracted_words.append(token.surface)
        # wakati string with extracted words
        wakati_str = ' '.join(extracted_words)
        return wakati_str


def make_wakati_for_lines(msg_ser: pd.Series) -> pd.Series:
    manalyzer = MorphologicalAnalysis()
    wakati_msg_list = []
    for msg in tqdm(msg_ser, desc='[mk wakati msgs]'):
        wakati_msg = manalyzer.get_wakati_str(str(msg), exc_part_of_speech,
                                              inc_part_of_speech)
        wakati_msg_list.append(wakati_msg)
    wakati_msg_ser = pd.Series(wakati_msg_list)
    return wakati_msg_ser


def main(input_fname: str):
    input_root = '../../data/020_intermediate'
    output_root = '../../data/030_processed'
    # 1. load messages_cleaned.csv
    msgs_cleaned_fpath = input_root + '/' + input_fname
    df_msgs = pd.read_csv(msgs_cleaned_fpath)
    # 2. make wakati string by record
    ser_msg = df_msgs.msg
    df_msgs['wakati_msg'] = make_wakati_for_lines(ser_msg)
    # 3. save it
    pin = Path(msgs_cleaned_fpath)
    msgs_wakati_fpath = output_root + '/' + pin.stem + '_wakati.csv'
    df_msgs.to_csv(msgs_wakati_fpath, index=False)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_fname", help="set input file name", type=str)
    args = parser.parse_args()
    input_fname = args.input_fname
    # input file must been cleaned
    if 'cleaned' not in input_fname:
        print('input file name is invalid.: {0}'.format(input_fname))
        print('input file name must include \'cleaned\'')
        sys.exit(1)
    main(input_fname)

6. Prétraitement: normalisation

6.1 Qu'est-ce que la normalisation?

La normalisation dans le prétraitement du traitement du langage naturel fait référence au traitement suivant. Il est également appelé «identification du nom».

  1. Unification des types de caractères
  2. Unifier Kana en plein angle
  3. L'alphabet est unifié aux rangs inférieurs ... etc.
  4. Remplacement du numéro
  5. Remplacez tous les nombres par 0, etc.
    • Il existe peu de situations où les nombres sont importants dans le traitement du langage naturel.
  6. Unification des mots à l'aide d'un dictionnaire
  7. Jugez que «Sony» et «Sony» sont les mêmes et unifiez la notation en «Sony»

Le monde de la normalisation est profond, je vais donc le laisser ici.

6.2. Mise en œuvre de la normalisation

Cette fois, par souci de simplicité, seul le traitement suivant a été mis en œuvre. C'est incroyablement facile.

  1. Unifiez l'alphabet en lettres minuscules
  2. Remplacez tous les nombres par 0

normalization.py


import re
import pandas as pd
from tqdm import tqdm
import argparse
from pathlib import Path
import sys


def normarize_text(text: str):
    normalized_text = normalize_number(text)
    normalized_text = lower_text(normalized_text)
    return normalized_text


def normalize_number(text: str) -> str:
    """
    pattern = r'\d+'
    replacer = re.compile(pattern)
    result = replacer.sub('0', text)
    """
    #Remplacer les nombres consécutifs par 0
    replaced_text = re.sub(r'\d+', '0', text)
    return replaced_text


def lower_text(text: str) -> str:
    return text.lower()


def normalize_msgs(wktmsg_ser: pd.Series) -> pd.Series:
    normalized_msg_list = []
    for wktstr in tqdm(wktmsg_ser, desc='normalize words...'):
        normalized = normarize_text(str(wktstr))
        normalized_msg_list.append(normalized)
    normalized_msg_ser = pd.Series(normalized_msg_list)
    return normalized_msg_ser


def main(input_fname: str):
    input_root = '../../data/030_processed'
    output_root = input_root
    # 1. load wakati messages
    msgs_fpath = input_root + '/' + input_fname
    df_msgs = pd.read_csv(msgs_fpath)
    # 2. normalize wakati_msg (update)
    ser_msg = df_msgs.wakati_msg
    df_msgs.wakati_msg = normalize_msgs(ser_msg)
    # 3. save it
    pin = Path(msgs_fpath)
    msgs_ofpath = output_root + '/' + pin.stem + '_norm.csv'
    df_msgs.to_csv(msgs_ofpath, index=False)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_fname", help="set input file name", type=str)
    args = parser.parse_args()
    input_fname = args.input_fname
    # input file must been cleaned
    if 'wakati' not in input_fname:
        print('input file name is invalid.: {0}'.format(input_fname))
        print('input file name must include \'wakati\'')
        sys.exit(1)
    main(input_fname)

7. Prétraitement: suppression du mot d'arrêt

7.1. Qu'est-ce que la suppression des mots vides?

"Suppression des mots vides" est le processus de suppression des mots vides. (Trop comme ça ...)

Alors qu'est-ce qu'un "** mot d'arrêt **"?

Selon le dictionnaire de langue nationale goo

Mots qui sont si courants dans les recherches en texte intégral qu'ils sont exclus de la recherche par eux-mêmes. Fait référence à "ha", "no", "desu" en japonais et a, the, of en anglais. Mots vides.

Les tâches de traitement du langage naturel ont souvent pour but de «comprendre le contexte d'une phrase». Les mots vides ne sont pas nécessaires à cette fin et doivent être supprimés.

7.2 Comment décider du mot d'arrêt?

Il y en a deux principaux. Cette fois, nous utiliserons 1.

  1. Méthode utilisant un dictionnaire (** ← Cette fois est adoptée **)
  2. Méthode utilisant la fréquence d'apparition

Pour les données de dictionnaire, utilisez here.

Il y a plusieurs raisons de choisir la méthode 1.

--Lors de l'utilisation d'un dictionnaire Dictionnaire existant facilite l'installation.

  • Lors de l'utilisation de la fréquence d'occurrence, il est nécessaire d'extraire uniquement "les mots qui apparaissent fréquemment dans ** la conversation générale ** et doivent donc être supprimés". Si vous utilisez uniquement les données Slack dont vous disposez pour déterminer la fréquence d'apparition **, il existe un risque que les "mots liés à des sujets passionnants" soient supprimés par inadvertance **, j'ai donc pensé qu'il était nécessaire de préparer des données séparées et de les agréger. .. ...... C'est un peu difficile

7.3. Mise en œuvre de la suppression des mots vides

Supprime les mots enregistrés dans les données de dictionnaire introduites dans la section précédente. De plus, les caractères suivants ont également été supprimés. C'est un mot que j'ai trouvé ennuyeux en réglant diverses choses.

  • 「-」
  • "-"
  • 「w」
  • 「m」
  • "Lol"

stopword_removal.py


import pandas as pd
import urllib.request
from pathlib import Path
from tqdm import tqdm
import argparse
from pathlib import Path
import sys


def maybe_download(path: str):
    stopword_def_page_url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    p = Path(path)
    if p.exists():
        print('File already exists.')
    else:
        print('downloading stop words definition ...')
        # Download the file from `url` and save it locally under `file_name`:
        urllib.request.urlretrieve(stopword_def_page_url, path)
    #mot d'arrêt ajouté
    sw_added_list = [
        '-',
        '-',
        'w',
        'W',
        'm',
        'Lol'
    ]
    sw_added_str = '\n'.join(sw_added_list)
    with open(path, 'a') as f:
        print(sw_added_str, file=f)


def load_sw_definition(path: str) -> list:
    with open(path, 'r', encoding='utf-8') as f:
        lines = f.read()
        line_list = lines.split('\n')
        line_list = [x for x in line_list if x != '']
    return line_list


def remove_sw_from_text(wktstr: str, stopwords: list) -> str:
    words_list = wktstr.split(' ')
    words_list_swrm = [x for x in words_list if x not in stopwords]
    swremoved_str = ' '.join(words_list_swrm)
    return swremoved_str


def remove_sw_from_msgs(wktmsg_ser: pd.Series, stopwords: list) -> pd.Series:
    swremved_msg_list = []
    for wktstr in tqdm(wktmsg_ser, desc='remove stopwords...'):
        removed_str = remove_sw_from_text(str(wktstr), stopwords)
        swremved_msg_list.append(removed_str)
    swremved_msg_ser = pd.Series(swremved_msg_list)
    return swremved_msg_ser


def main(input_fname: str):
    input_root = '../../data/030_processed'
    output_root = input_root
    # 1. load stop words
    sw_def_fpath = 'stopwords.txt'
    maybe_download(sw_def_fpath)
    stopwords = load_sw_definition(sw_def_fpath)
    # 2. load messages
    msgs_fpath = input_root + '/' + input_fname
    df_msgs = pd.read_csv(msgs_fpath)
    # 3. remove stop words
    ser_msg = df_msgs.wakati_msg
    df_msgs.wakati_msg = remove_sw_from_msgs(ser_msg, stopwords)
    # 4. save it
    pin = Path(msgs_fpath)
    msgs_ofpath = output_root + '/' + pin.stem + '_rmsw.csv'
    df_msgs.to_csv(msgs_ofpath, index=False)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_fname", help="set input file name", type=str)
    args = parser.parse_args()
    input_fname = args.input_fname
    # input file must been cleaned
    if 'norm' not in input_fname:
        print('input file name is invalid.: {0}'.format(input_fname))
        print('input file name must include \'norm\'')
        sys.exit(1)
    main(input_fname)

8. Prétraitement: extraction de phrases importantes (tf-idf)

8.1. Qu'est-ce que tf-idf?

Il existe de nombreux articles de commentaires intéressants. J'ai fait référence à cet article. TF-IDF | Qiita

Voici une explication approximative.

  • tf (Term Frequency) --Fréquence d'occurrence de mot dans un document ――S'il est gros, "le mot apparaît souvent dans le document"
  • idf (Inverse Document Frequency) --Ratio des documents dans lesquels un mot apparaît (pour tous les documents) ** Nombre inverse ** ――Si c'est gros, cela n'apparaît pas beaucoup dans les autres documents.

tf-idf est le produit de tf et idf. En d'autres termes ** Grand tf-idf ** = ** Apparaissant fréquemment dans un document ** & ** Ne figurant pas souvent dans d'autres documents ** = Important pour comprendre le ** contexte du document **

8.2. Implémentation du traitement de notation de mots par tf-idf

8.2.1 Que devrait être un document / tous les documents?

Le but de ce temps est de voir les caractéristiques des remarques pour une certaine période (1 semaine). Par conséquent, j'ai pensé qu'il devrait être possible de comprendre ** quelles sont les caractéristiques des remarques d'une certaine période (1 semaine) pour tous les postes jusqu'à présent **.

Donc,

  • ** Tous les documents **: Tous les messages à ce jour
  • ** 1 document **: Une masse de postes pour une certaine période (1 semaine)

J'ai calculé tf-idf comme.

8.2.2. Mise en œuvre

Écrivez facilement le flux de processus

  1. Lire le tableau du magasin de messages (prétraité)
  2. Regroupement par période de message
  3. Divisez et regroupez les données passées en unités de 7 jours à partir du moment de l'exécution du traitement
  4. Calculez tf-idf avec un groupe de messages comme un document
  5. Extraire les mots dont le score tf-idf est supérieur au seuil (sortie sous forme de dictionnaire)

important_word_extraction.py


import pandas as pd
import json
from datetime import datetime, date, timedelta, timezone
from pathlib import Path
from sklearn.feature_extraction.text import TfidfVectorizer
JST = timezone(timedelta(hours=+9), 'JST')

#Regroupement des messages par période spécifiée
def group_msgs_by_term(df_msgs: pd.DataFrame, term: str) -> dict:
    # set term
    term_days = 8
    if term == 'lm':
        term_days = 31
    print('group messages every {0} days'.format(term_days))
    # analyze timestamp
    now_in_sec = (datetime.now(JST) - datetime.fromtimestamp(0, JST)).total_seconds()
    interval_days = timedelta(days=term_days)
    interval_seconds = interval_days.total_seconds()
    oldest_timestamp = df_msgs.min().timestamp
    oldest_ts_in_sec = (datetime.fromtimestamp(oldest_timestamp, JST) - datetime.fromtimestamp(0, JST)).total_seconds()
    loop_num = (abs(now_in_sec - oldest_ts_in_sec) / interval_seconds) + 1
    # extract by term
    dict_msgs_by_term = {}
    df_tmp = df_msgs
    now_tmp = now_in_sec
    for i in range(int(loop_num)):
        # make current term string
        cur_term_s = 'term_ago_{0}'.format(str(i).zfill(3))
        print(cur_term_s)
        # current messages
        df_msgs_cur = df_tmp.query('@now_tmp - timestamp < @interval_seconds')
        df_msgs_other = df_tmp.query('@now_tmp - timestamp >= @interval_seconds')
        # messages does not exist. break.
        if df_msgs_cur.shape[0] == 0:
            break
        # add current messages to dict
        dict_msgs_by_term[cur_term_s] = ' '.join(df_msgs_cur.wakati_msg.dropna().values.tolist())
        # update temp value for next loop
        now_tmp = now_tmp - interval_seconds
        df_tmp = df_msgs_other
    return dict_msgs_by_term

# tf-Extraire les mots importants et les renvoyer sous forme de dictionnaire en se référant au score idf
def extract_important_word_by_key(feature_names: list, bow_df: pd.DataFrame, uids: list) -> dict:
    # >Regardez chaque ligne et extrayez les mots importants(tfidf Top X mots)
    dict_important_words_by_user = {}
    for uid, (i, scores) in zip(uids, bow_df.iterrows()):
        #Créer une table de mots et de scores tfidf pour l'utilisateur
        words_score_tbl = pd.DataFrame()
        words_score_tbl['scores'] = scores
        words_score_tbl['words'] = feature_names
        #Trier par ordre décroissant par score tfidf
        words_score_tbl = words_score_tbl.sort_values('scores', ascending=False)
        words_score_tbl = words_score_tbl.reset_index()
        # extract : tf-idf score > 0.001
        important_words = words_score_tbl.query('scores > 0.001')
        #Créer un dictionnaire pour l'utilisateur'uid0': {'w0': 0.9, 'w1': 0.87}
        d = {}
        for i, row in important_words.iterrows():
            d[row.words] = row.scores
        #Ajouter au tableau uniquement si le dictionnaire de l'utilisateur contient au moins un mot
        if len(d.keys()) > 0:
            dict_important_words_by_user[uid] = d
    return dict_important_words_by_user

#Extraire les mots importants dans l'unité de période spécifiée
def extraction_by_term(input_root: str, output_root: str, term: str) -> dict:
    # ---------------------------------------------
    # 1. load messages (processed)
    # ---------------------------------------------
    print('load msgs (all of history and last term) ...')
    msg_fpath = input_root + '/' + 'messages_cleaned_wakati_norm_rmsw.csv'
    df_msgs_all = pd.read_csv(msg_fpath)
    # ---------------------------------------------
    # 2. group messages by term
    # ---------------------------------------------
    print('group messages by term and save it.')
    msgs_grouped_by_term = group_msgs_by_term(df_msgs_all, term)
    msg_grouped_fpath = input_root + '/' + 'messages_grouped_by_term.json'
    with open(msg_grouped_fpath, 'w', encoding='utf-8') as f:
        json.dump(msgs_grouped_by_term, f, ensure_ascii=False, indent=4)
    # ---------------------------------------------
    # 3.Tf pour tous les documents-calcul idf
    # ---------------------------------------------
    print('tfidf vectorizing ...')
    # >Les mots de tous les documents sont des colonnes et le nombre de documents (=Une matrice est créée dans laquelle l'utilisateur) est la ligne. Tf pour chaque élément-Il y a une valeur idf
    tfidf_vectorizer = TfidfVectorizer(token_pattern=u'(?u)\\b\\w+\\b')

    bow_vec = tfidf_vectorizer.fit_transform(msgs_grouped_by_term.values())
    bow_array = bow_vec.toarray()
    bow_df = pd.DataFrame(bow_array,
                        index=msgs_grouped_by_term.keys(),
                        columns=tfidf_vectorizer.get_feature_names())
    # ---------------------------------------------
    # 5. tf-Extraire des mots importants basés sur idf
    # ---------------------------------------------
    print('extract important words ...')
    dict_word_score_by_term = extract_important_word_by_key(
        tfidf_vectorizer.get_feature_names(),
        bow_df, msgs_grouped_by_term.keys())
    return dict_word_score_by_term

9. Processus de visualisation avec Wordcloud

9.1. Qu'est-ce que Wordcloud?

Les mots avec un score élevé sont grands et les mots avec un score faible sont petits. Diverses valeurs telles que "fréquence d'apparition" et "importance" peuvent être librement définies pour la partition.

Dépôt officiel: amueller / word_cloud: octocat:

9.2. Polices Wordcloud disponibles

Cette fois, je vais l'utiliser. Qu'est-ce que Homemade Rounded M +

9.3. Implémentation de Wordcloud

Dans le chapitre précédent "8. Prétraitement: extraction de mots importants (tf-idf)", le fichier JSON suivant a été généré.

important_word_tfidf_by_term.json


{
  "term_ago_000": {
    "Les données": 0.890021,
    "Jeu": 0.780122,
    "article": 0.720025,
    :
  },
  "term_ago_001": {
    "Traduction": 0.680021,
    "Les données": 0.620122,
    "deepl": 0.580025,
    :
  },
  :
}

Chargez ceci et créez une image Wordcloud. Utilisez la méthode WordCloud.generate_from_frequencies ().

wordcloud_from_score.py


from wordcloud import WordCloud
import matplotlib.pyplot as plt
import json
from pathlib import Path
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
from tqdm import tqdm
import sys
import argparse


def main(input_fname: str):
    input_root = '../../data/031_features'
    output_root = './wordcloud_by_user' if 'by_user' in input_fname else './wordcloud_by_term'
    p = Path(output_root)
    if p.exists() is False:
        p.mkdir()
    # -------------------------------------
    # 1. load tf-idf score dictionary
    # -------------------------------------
    d_word_score_by_user = {}
    tfidf_fpath = input_root + '/' + input_fname
    with open(tfidf_fpath, 'r', encoding='utf-8') as f:
        d_word_score_by_user = json.load(f)
    # -------------------------------------
    # 2. gen word cloud from score
    # -------------------------------------
    fontpath = './rounded-l-mplus-1c-regular.ttf'
    for uname, d_word_score in tqdm(d_word_score_by_user.items(), desc='word cloud ...'):
        # img file name is user.png
        uname = str(uname).replace('/', '-')
        out_img_fpath = output_root + '/' + uname + '.png'
        # gen
        wc = WordCloud(
            background_color='white',
            font_path=fontpath,
            width=900, height=600,
            collocations=False
            )
        wc.generate_from_frequencies(d_word_score)
        wc.to_file(out_img_fpath)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_fname", help="set input file name", type=str)
    args = parser.parse_args()
    input_fname = args.input_fname
    main(input_fname)

10. Bonus

Articles auxquels j'ai fait référence en particulier

D'autres documents de référence (en grande quantité) sont résumés dans ici: octocat:.

Publicité

Cette fois, nous utilisons les données de la communauté Slack appelée Data Learning Guild. La Data Learning Guild est une communauté en ligne de personnel d'analyse de données. Si vous êtes intéressé, veuillez vérifier ici.

Site Web officiel de Data Learning Guild

Calendrier de l'Avent Data Learning Guild 2019

Recommended Posts