😊

【Python】TF-IDFでワインレビューから代表的なコメントを抽出する

に公開

最近はアルゴリズムに興味があり、篠田 裕之 著『となりのアルゴリズム 自分で答えを出すためのデータサイエンス思考』(https://www.amazon.co.jp/となりのアルゴリズム-自分で答えを出すためのデータサイエンス思考-篠田-裕之/dp/4334953417)
を読んでいたときに出てきたTF-IDFという概念を知り、「ワインレビューから代表的なコメントを抽出できるのではないか」というアイデアが浮かびました。

アイデアを思いついた背景

私が個人開発しているワインアプリでは、ユーザーがワインの感想を入力するとデータベースに登録される仕組みになっています。しかし、同じワイン名とヴィンテージ(ワインの原料となったブドウが収穫された年)のレビューが複数登録されると、常に最新のコメントだけが表示されるという問題がありました。
例えば、こんな状況です

(同じワイン、ヴィンテージに対する)
レビュー1つめ. 「いちごの香りがしてフレッシュ」
レビュー2つめ. 「フルーティーでおいしい」
レビュー3つめ. 「まずい!まずい!まずすぎる!」

この順で登録されると、次に同じワインを検索した人に3つ目の「まずい!まずい!まずすぎる!」が表示されてしまいます。これではたった1回のネガティブなコメントが他のユーザーに誤解を招いてしまいます。
「複数のコメントから、そのワインを最もよく表現している代表的なコメントを選べないか?」
そこで思いついたのがTF-IDFの応用です。

TF-IDFとは

TF-IDF(Term Frequency-Inverse Document Frequency)は、文書の中で重要な単語を見つけ出すための指標です。検索エンジンやテキスト分析でよく使われます。
TF-IDFは2つの要素から成り立っています。
TF(Term Frequency):単語の出現頻度
TFは、ある文書の中で特定の単語が何回出現するかを表します。

TF = (特定の単語の出現回数) / (文書内の総単語数)

3つのワインレビューがあるとします
(実際はこのような細切れなコメントは来ませんが、一旦ここでは考慮しません)

reviews = [
    ["フルーティー", "飲みやすい"],                      # レビュー1
    ["渋み", "強い", "深み", "ある", "味わい"],          # レビュー2
    ["フルーティー", "華やか"],                         # レビュー3
]

レビュー1での「フルーティー」のTFを計算:
TF(フルーティー, レビュー1) = 1 / 2 = 0.5
「フルーティー」は1回出現、レビュー1の総単語数は2なので0.5となります。
IDF(Inverse Document Frequency):単語の希少性
IDFは、その単語が複数の文書でどれだけ珍しいかを表します。多くの文書に出現する単語は重要度が低く、特定の文書にしか出現しない単語は重要度が高くなります。

IDF = log((文書の総数) / (その単語を含む文書数))

先ほどの3つのレビューで「フルーティー」のIDFを計算すると、3文書中2文書に出現しているので:
IDF(フルーティー) = log(3 / 2) = log(1.5) ≈ 0.176
一方、「渋み」は1文書にしか出現しないので:
IDF(渋み) = log(3 / 1) = log(3) ≈ 0.477
希少な単語ほどIDFの値が大きくなります。
TF-IDF:重要度の総合評価
TF-IDFは、TFとIDFを掛け合わせた値です。

TF-IDF = TF × IDF

文書内で頻繁に出現(TFが高い)
他の文書にはあまり出現しない(IDFが高い)

この両方を満たす単語が、その文書を特徴づける重要な単語となります。
ワインレビューへの応用
アイデア:文書全体のTF-IDFスコアで評価
各レビューに対して、含まれる全単語のTF-IDFスコアを合計します。スコアが高いレビューほど、他のレビューにはない独自の情報を持っていると判断できます。

import math
from collections import Counter, defaultdict

def calculate_tf(tokens):
    """TF(単語頻度)を計算"""
    word_count = len(tokens)
    word_freq = Counter(tokens)
    return {word: count / word_count for word, count in word_freq.items()}

def calculate_idf(documents_tokens):
    """IDF(逆文書頻度)を計算"""
    n_docs = len(documents_tokens)
    word_doc_count = defaultdict(int)
    
    for tokens in documents_tokens:
        for word in set(tokens):
            word_doc_count[word] += 1
    
    return {
        word: math.log(n_docs / count)
        for word, count in word_doc_count.items()
    }

def calculate_tfidf_score(tokens, idf):
    """文書全体のTF-IDFスコアを計算"""
    tf = calculate_tf(tokens)
    score = 0
    
    for word, tf_value in tf.items():
        if word in idf:
            score += tf_value * idf[word]
    
    return score

def select_representative_review(reviews_tokens):
    """代表的なレビューを選択"""
    idf = calculate_idf(reviews_tokens)
    
    scores = []
    for i, tokens in enumerate(reviews_tokens):
        score = calculate_tfidf_score(tokens, idf)
        scores.append((i, score))
    
    scores.sort(key=lambda x: x[1], reverse=True)
    return scores

# 使用例(形態素解析済みと仮定 ここについては別記事でまとめます)
wine_reviews = [
    ["おいしい"],
    ["フルーティー", "飲みやすい", "初心者", "おすすめ"],
    ["まずい"],
    ["複雑", "味わい", "タンニン", "豊富", "熟成", "向い", "いる"],
    ["おいしい"]
]

results = select_representative_review(wine_reviews)

print("=== レビューランキング ===")
for rank, (idx, score) in enumerate(results, 1):
    print(f"{rank}位 (スコア: {score:.3f}): レビュー{idx + 1}")
実行結果
=== レビューランキング ===
1(スコア: 0.524): レビュー4
2(スコア: 0.398): レビュー2
3(スコア: 0.183): レビュー3
4(スコア: 0.183): レビュー1
5(スコア: 0.183): レビュー5

・結果の解釈
レビュー4「複雑な味わいでタンニンが豊富、熟成に向いている」は、他にはない専門的な単語を含むため、最も情報量が多いと判断されました
「おいしい」「まずい」だけの短いレビューは、情報量が少ないため低スコアになりました
これにより、最新のコメントが「まずい」でも、より詳細なレビューが選ばれます

まとめ

TF-IDFを使うことで、単純に「最新のコメント」を表示するのではなく、最も情報量が多く、そのワインを特徴づけるレビューを自動的に選べることがわかりました。

ポイント
TF: 文書内での単語の出現頻度(よく使われている単語)
IDF: 単語の希少性(他の文書には少ない単語)
TF-IDF: 両者を掛け合わせて重要度を評価

最後に

調べていて出会った言葉たち
・形態素解析
・ストップワード
・mecab
・Cos類似度
等は調べてまた記事で補足できればと思います。

そしてきっかけになった本がおもしろいので再宣伝でこの記事をしめます。

篠田 裕之 著『となりのアルゴリズム 自分で答えを出すためのデータサイエンス思考』(https://www.amazon.co.jp/となりのアルゴリズム-自分で答えを出すためのデータサイエンス思考-篠田-裕之/dp/4334953417)

Discussion