株式会社スマレジの開発部でスマレジのサーバサイドを作っています

チケットの見える化にチャレンジしてみる(2-6: 準備編 tf-idfの実装)

こんばんは!株式会社スマレジ 、開発部のmasaです。

まだまだ残暑厳しいですが、少しずつ涼しくなってきたことを感じます。 会社でも、暑さが和らいできたので、アウトドアイベントをしたいなーといった話が出るのですが、 緊急事態宣言もあるので、なかなか実現できない今日この頃です。。。

皆さんの会社では、コロナ禍のなかでどんなイベントをしていますか?

今回は、if-idfのpython + chasenでの実装になります。

tf-idfの実装

まずは実装を。

from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *

import json
import requests
import math


def getDescriptionOfRedmineIssueByIssueId(id):
    """
    redmineのチケットをID指定で取得して、説明を返す。
    :param id: int
    :return: Bool|string 取得成功なら文字列、失敗ならFalse
    """

    headers = {'X-Redmine-API-Key': "redmineのキー"}
    response = requests.get("http://redmineのドメイン/issues/" + str(id) + ".json", headers=headers)
    statusCode = response.status_code
    if statusCode >= 400:
        return ""
    text = response.text
    if len(text) < 1:
        return ""

    resJson = json.loads(text)
    if "issue" not in resJson:
        return ""

    issue = resJson["issue"]
    sentence = issue["description"]
    if sentence is None:
        sentence = ""
    return sentence


# 辞書定義
char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')]
tokenizer = Tokenizer()
token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter()]
analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters)

sentences = []

# redmineチケットの説明を取得(ID指定)
for i in 取得したいチケットのIDを持った配列:
    if i == 0:
        continue
    sentences.append(getDescriptionOfRedmineIssueByIssueId(i))

# ベクトル化

vectors = []
wkVector = []
for wkSentence in sentences:
    if len(wkSentence) < 1:
        vectors.append([])
        continue
    wkVector = []
    for result in analyzeInfo.analyze(wkSentence):
        wkVector.append(result[0])
    vectors.append(wkVector)

# TF及びIDFの計算
tfScores = []
idfScore = 0
targetWord = "tf-idfで対象にしたい単語"
for tmpVector in vectors:
    wkTfScore = 0
    if targetWord in tmpVector:
        idfScore += 1
    for currentWord in tmpVector:
        if targetWord == currentWord:
            wkTfScore += 1
    if len(tmpVector) < 1:
        tfScores.append(0)
    else:
        tfScores.append(wkTfScore / len(tmpVector))

print(tfScores)
print(math.log(len(vectors) / idfScore))

getDescriptionOfRedmineIssueByIssueIdは前回の使い回しなので割愛します。 詳しくは↓

masa2019.hatenablog.com

TFの計算

TFの定義は前回のブログ

masa2019.hatenablog.com

こちらで記載しているように、ある文書に対するその単語の出現回数を全文書で割ったものになります。

masa2019.hatenablog.com

この部分に対応しているのが下記の部分になります。(上のソースからIDF関連の部分を除去したものになります)

# TFの計算
tfScores = []
idfScore = 0
targetWord = "tf-idfで対象にしたい単語"
for tmpVector in vectors:
    wkTfScore = 0
    for currentWord in tmpVector:
        if targetWord == currentWord:
            wkTfScore += 1
    if len(tmpVector) < 1:
        tfScores.append(0)
    else:
        tfScores.append(wkTfScore / len(tmpVector))

vectorsには各チケットの単語リストが入っています。(今回は70チケットあり、キーがチケットIDで値が単語リスト)

そして、各単語リストについて、検索対象の単語(targetWord)でマッチングをかけて、マッチした回数をwkTfScoreで記憶、 そして、その文書の全単語数で割っています。

IDFの計算

IDFの計算はもっとシンプルになります。 IDFは比較したい文書の集合(今回でいう70個のチケット)のなかでターゲットの単語が出現した数で全文書数(今回は70)を割ったものに対数をとってあげることになります。

# TF及IDFの計算
idfScore = 0
targetWord = "tf-idfで対象にしたい単語"
for tmpVector in vectors:
    wkTfScore = 0
    if targetWord in tmpVector:
        idfScore += 1

print(math.log(len(vectors) / idfScore))

ループの中では単語の出現した文書の数をカウントしておき、その結果を全文書数(len(vectors)))で割って対数をとっているのがわかると思います。

次回は、この二つのスコアをベースに使用するBM25について説明します。