48. レポート課題3:業界口コミデータへの自然言語処理適用#


48.1. 使用データ#

合成口コミデータセット(別途Googleドライブで配布)を用いる。 IT系スタートアップ・大手製造業・総合商社・地方金融機関・公共インフラの5業界に関する 口コミ文と感情ラベル(positive / negative / neutral)から構成される。

注意: 使用するデータはすべて合成データです。
実在する企業・個人を示すものではありません。

48.1.1. データの列構成#

列名

内容

review_id

サンプルID

industry_type

業界(5種)

review_category

口コミの観点(残業/給与/成長/社風/人間関係)

review_text

口コミ本文

sentiment_label

感情ラベル(positive / negative / neutral)

48.2. 課題の目標#

  1. spacy(ja_ginza)による分かち書きで特徴ベクトルを構築する

  2. 「データ前処理フェーズ」と「モデル構築・評価フェーズ」を分離して実験効率を体験する

  3. 識別器の失敗要因を業界・観点などのメタ情報も活用して分析し、仮説を立案する

  4. 仮説の妥当性を新たなサンプルで軽く検証する

48.3. 全体の流れ#

フェーズ

Level

内容

所要時間

実行頻度

Phase 1:前処理フェーズ

0〜1

CSVの読み込み・spacy 処理・pickle 保存

数分

最初の1回のみ

Phase 2:モデル構築・評価フェーズ

2〜5

pickle 読み込み・分割・ベクトル化・学習・評価・分析

数秒〜数十秒

繰り返し可

上記の所要時間とは、一旦コードを書いてある状態での処理時間を指しています。コード理解や出力結果の分析等に要する時間は含みません。

48.3.1. 再現性について#

このノートブックでは、train / test の分割に シード値 2026 で固定した層化抽出を用いる。 全員が同じデータセットから同じコードを実行すれば、同一の分割結果が得られる。

48.3.2. 実行手順#

  1. 予め、Google Drive 直下に 2026dm-rep3 フォルダを作成しておく。このフォルダを作業フォルダと呼ぶことにする。今回の作業は全て作業フォルダ内で行うこと。

  2. 共有ドライブで配布している以下のファイルをダウンロードし、先ほど用意した作業フォルダにアップロードする。

  • report3_job_reviews.csv: コーパス。

  • preprocess_corpus.ipynb: Phase 1の前処理用ノートブック。

  • report3_2026.ipynb: 本ノートブック。Phase 2以降の参考コード付き。

  1. Phase 1 のために、preprocess_corpus.ipynb を実行し、前処理を行った pickle ファイルを生成する。

  2. 以降は Phase 2 から実行する(pickle 読み込みだけでよい)

  • 本ノートブックには Phase 2 用のコード例を含んでいます。Phase1を含め実行するだけ終わる部分がありますが、主要な流れについては読み解いた方が良いでしょう。

  • Level 1は、そのまま実行できます。

  • Level 2は、モデルを変更しても良いし、そのままでも良いです。モデル変更する場合にはimportしてから利用してください。

  • Level 3以降もコード例を書いてありますが、不完全であり、そのまま実行しても欲しい結果は得られません。課題で求めていることを理解して自分なりに取り組んでください。(コードを書いて自動分析することを求めているわけではありません)


48.4. Phase 1:前処理フェーズ#

preprocess_corpus.ipynb を参照。


48.5. Phase 2:モデル構築・評価フェーズ#

一度Phase 1を実行して pickle ファイルを用意したら、その後はここから実行するだけで良い。新しい Colab セッションを開いた場合は、次のセル(Drive マウント+pickle 読み込み)から始めること。

# ── Phase 2 開始時は必ずこのセルから実行する ─────────────────────────────────
from google.colab import drive
drive.mount('/content/drive')

import pickle
import pandas as pd

path = "/content/drive/MyDrive/2026dm-rep3/"
pkl_file = path + "job_reviews_add.pkl"

with open(pkl_file, "rb") as f:
    df = pickle.load(f)

print(f"読み込み完了: {len(df)} 件")
#display(df[["review_id", "industry_type", "review_category", "review_text", "sentiment_label"]].head(3))
df.head()

48.5.1. Level 1:train / test 分割と特徴ベクトルの構築(BoW)#

48.5.1.1. train / test 分割の方針#

jrte-corpus(v1)とは異なり、このデータセットには事前定義の分割がない。 そこで シード値固定の層化抽出(stratified split)で分割する。

  • stratify: 各ラベル(positive / negative / neutral)の比率を train / test で揃える

  • random_state=2026: 全員が同じコードを実行すれば同一の分割結果になる

train_df, test_df = train_test_split(
    df, test_size=0.2, random_state=2026, stratify=df["sentiment_label"]
)

48.5.1.2. CountVectorizer の使い方#

  • train データのみで fit_transform → 語彙を決定

  • test データには transform のみ(語彙を固定したまま変換)

注意: Level 1〜3 では CountVectorizer をカスタマイズしてはならない。

from sklearn.model_selection import train_test_split

# シード値 2026・層化抽出で再現性を保証する
train_df, test_df = train_test_split(
    df,
    test_size=0.2,
    random_state=2026,
    stratify=df["sentiment_label"]
)
train_df = train_df.reset_index(drop=True)
test_df  = test_df.reset_index(drop=True)

print(f"train: {len(train_df)} 件")
print(f"test : {len(test_df)} 件")
print()
print("train の sentiment_label 分布:")
print(train_df["sentiment_label"].value_counts())
print()
print("test の sentiment_label 分布:")
print(test_df["sentiment_label"].value_counts())
from sklearn.feature_extraction.text import CountVectorizer

corpus_train = train_df["wakati"].tolist()
corpus_test  = test_df["wakati"].tolist()
y_train = train_df["sentiment_label"].tolist()
y_test  = test_df["sentiment_label"].tolist()

# CountVectorizer はカスタマイズ禁止(Level 2〜4)
vectorizer = CountVectorizer()

X_train = vectorizer.fit_transform(corpus_train)  # train で fit&transform
X_test  = vectorizer.transform(corpus_test)        # test は transform のみ
features = vectorizer.get_feature_names_out()

print(f"語彙数(特徴数): {len(features)}")
print(f"X_train の形状: {X_train.shape}")
print(f"X_test  の形状: {X_test.shape}")

48.5.2. ✍️ Level 1 報告事項#

(1) features[:5] の出力結果。

(2) len(features) の結果(語彙サイズ)。

(3) X_train[0] の出力結果(最初の訓練サンプルのスパースベクトル表現)。

(4) X_train[0] の非ゼロ要素のうち、最初に現れる特徴のインデックスと特徴名を報告せよ。

(5) X_train[0] に対応する元テキスト(train_df.iloc[0]["review_text"])を掲載せよ。

(6) train / test の件数および各ラベルの件数を報告せよ(stratify が正しく機能しているか確認)。

# ── (1) features[:5] ──────────────────────────────────────────────────────
print("(1) features[:5]:")
print(features[:5])
print()

# ── (2) len(features) ─────────────────────────────────────────────────────
print(f"(2) len(features): {len(features)}")
print()

# ── (3) X_train[0] ────────────────────────────────────────────────────────
print("(3) X_train[0]:")
print(X_train[0])
print()

# ── (4) 最初の非ゼロ特徴 ──────────────────────────────────────────────────
first_idx = X_train[0].indices[0]
print(f"(4) 最初の非ゼロ特徴: インデックス = {first_idx}, 特徴名 = '{features[first_idx]}'")
print()

# ── (5) 元テキスト ────────────────────────────────────────────────────────
print("(5) 元テキスト:")
print(train_df.iloc[0]["review_text"])
print()

# ── (6) 分割件数の確認 ────────────────────────────────────────────────────
print(f"(6) len(y_train) = {len(y_train)},  len(y_test) = {len(y_test)}")

48.5.3. Level 2:識別学習と評価#

任意の分類器を用いて識別学習を行い、train / test データに対する分類精度を求めよ。

ヒント(使える分類器の例):

  • sklearn.linear_model.LogisticRegression

  • sklearn.svm.LinearSVC

  • sklearn.tree.DecisionTreeClassifier

  • sklearn.ensemble.RandomForestClassifier

分類器の選択理由をコメントとして簡単に記載すること。

from sklearn.linear_model import LogisticRegression

# 選択理由: (ここにコメントを書く)
model = LogisticRegression(max_iter=1000)  # ← 分類器を変更してみよう

model.fit(X_train, y_train)

train_acc = model.score(X_train, y_train)
test_acc  = model.score(X_test, y_test)
print(f"train 精度: {train_acc:.4f}")
print(f"test  精度: {test_acc:.4f}")
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix

label_order = ["negative", "neutral", "positive"]
y_pred_test = model.predict(X_test)

print("=== 混同行列(test データ)===")
cm = confusion_matrix(y_test, y_pred_test, labels=label_order)
print(pd.DataFrame(cm,
                   index=[f"実際: {l}" for l in label_order],
                   columns=[f"予測: {l}" for l in label_order]))
print()
print("=== 分類レポート ===")
print(classification_report(y_test, y_pred_test, labels=label_order))

48.5.4. ✍️ Level 2 報告事項#

(1) 使用した分類器と、選択理由。

(2) train データおよび test データに対する分類精度。

(3) 混同行列(または分類レポート)を掲載し、どのラベルで誤分類が多いかを簡単にコメントせよ。

48.5.5. Level 3:失敗要因分析と仮説立案#

識別器が誤分類した事例を観察し、エビデンスに基づいた改善案(仮説)を立案する。

48.5.5.1. このデータセット特有の分析方針#

このデータセットには industry_type(業界)review_category(観点) の列がある。 誤分類をこれらの軸でクロス集計することで、 「特定の業界や観点でモデルが失敗しやすい」かどうかを定量的に確認できる。

分析の例:
  失敗事例を industry_type でクロス集計
  → 「総合商社の口コミで誤分類が多い」などの傾向を発見
  → 「なぜか」をテキストレベルで確認
  → 仮説を立案

48.5.5.2. 推奨手順#

  1. テストデータで予測し、失敗事例を抽出する

  2. 失敗事例の industry_type / review_category の分布を確認する(クロス集計)

  3. 全失敗事例から N 件(N ≥ 20)をランダムにサンプリングして元テキストを観察する
    ※ランダム選択は reporting bias を避けるため

  4. 仮説ラベルを付与し、出現頻度をカウントして改善案を絞り込む

上記以外の手順で分析&仮説立案しても良い。ただし、改善案は必ず観察結果に基づくこと。根拠のない提案は評価しない。

import random

# ── テストデータの失敗事例を抽出 ─────────────────────────────────────────────
y_pred_arr  = np.array(y_pred_test)
y_test_arr  = np.array(y_test)
wrong_mask  = y_pred_arr != y_test_arr

wrong_test_df = test_df[wrong_mask].copy()
wrong_test_df["predicted"] = y_pred_arr[wrong_mask]

print(f"test の失敗件数: {wrong_mask.sum()} / {len(y_test)}  ({wrong_mask.mean():.1%})")
print()

# ── industry_type 別の誤分類傾向 ─────────────────────────────────────────────
print("industry_type 別の失敗件数:")
ind_err = wrong_test_df["industry_type"].value_counts()
ind_tot = test_df["industry_type"].value_counts()
print(pd.DataFrame({"失敗数": ind_err, "テスト数": ind_tot,
                    "誤分類率": (ind_err / ind_tot).map("{:.0%}".format)})
      .fillna(0).sort_values("失敗数", ascending=False))
print()

# ── review_category 別の誤分類傾向 ───────────────────────────────────────────
print("review_category 別の失敗件数:")
cat_err = wrong_test_df["review_category"].value_counts()
cat_tot = test_df["review_category"].value_counts()
print(pd.DataFrame({"失敗数": cat_err, "テスト数": cat_tot,
                    "誤分類率": (cat_err / cat_tot).map("{:.0%}".format)})
      .fillna(0).sort_values("失敗数", ascending=False))
# ── ランダムに N 件サンプリングして元テキストを観察 ──────────────────────────
random.seed(2026)
N = 20
sampled_df = wrong_test_df.sample(n=min(N, len(wrong_test_df)), random_state=2026)

print(f"── 失敗事例({len(sampled_df)} 件)──\n")
for _, row in sampled_df.iterrows():
    print(f"[正解: {row['sentiment_label']:8s}, 予測: {row['predicted']:8s}]  "
          f"[{row['industry_type']} / {row['review_category']}]")
    print(f"  {row['review_text']}")
    print()
# ── ここに分析結果を書く ──────────────────────────────────────────────────
# 各失敗事例に原因ラベルを付与し、Counter で集計する。
#
# 例:
#   hypothesis_labels.append("negation")      # 否定表現が原因
#   hypothesis_labels.append("out_of_vocab")  # 語彙外表現(BoWで拾えない)
#   hypothesis_labels.append("neutral_hard")  # neutral の判断が難しい
#   hypothesis_labels.append("short")         # 短い文(情報不足)

from collections import Counter

hypothesis_labels = []  # ← ここにラベルを追加していく

print("仮説ラベルの出現回数:")
print(Counter(hypothesis_labels).most_common())

48.5.6. ✍️ Level 3 報告事項#

(1) 分析方法の説明。

  • どのように分析したのか。

  • 分析対象は何か(train/test/両方)

  • 何件の失敗事例を観察したか。

  • 分析結果に基づく傾向があれば報告すること。(傾向が見当たらない場合にもそのことを報告すること)

(2) 分析結果と仮説の立案。

  • 観察した失敗事例の具体例(テキストと正解・予測ラベル)を示すこと。

  • 仮説は「〜という表現を含む文は、〜という理由で分類に失敗しやすい」の形で記述すること。

  • 仮説の優先順位付け(ラベルの出現頻度など)を示すこと。

48.5.7. Level 4:仮説の妥当性検証#

Level 3 で立案した仮説を検証するため、 新たな文と正解ラベルを N 件(N ≥ 2)用意し、予測結果を確認する。

Note: Level 1,2で構築した vectorizer を利用する必要があるため、もし別日に実行するなら改めて Level 1 から実行し直そう。

48.5.7.1. 重要:vectorizer.transform() を使うこと(fit_transform() ではない)#

Level 1,2 で構築した「語彙(特徴空間)」を共有するために、 vectorizer を再学習(fit)せず、transform() のみを使う。 fit_transform() を使うと語彙が変わり、元のモデルで正しく予測できなくなる。

new_X = vectorizer.transform(new_wakati)    # ✓ 正しい
new_X = vectorizer.fit_transform(new_wakati)  # ✗ 誤り

new_X.shape[1] == X_test.shape[1]True になることを確認しよう。

# セッションが新しい場合には ja_ginza を実行できる状態にする必要があります。
# 既に実行できる状態ならば、このセルは実行不要です。

!pip install -U ginza ja_ginza

import spacy
config = {
    "components": {
        "compound_splitter": {
            "split_mode": "A"
        }
    }
}
nlp = spacy.load("ja_ginza", config=config)
print("spacy をロードしました")
# ── spacy のロード(セッションが新しい場合) ─────────────────────────────────
try:
    nlp
except NameError:
    print("spacy をロードできません")
    # 上記がprint出力されるなら、環境構築が必要です。
    # 一つ手前のセルのコメントアウトを外して実行してください。

# ── 検証用テキストと正解ラベルを用意する ──────────────────────────────────
# 仮説に基づいて「モデルが失敗しそうな」文を自分で作成すること(N ≥ 2)
# ラベル: "positive", "neutral", "negative"

new_texts = [
    "文1",   # ← 変更すること
    "文2",   # ← 変更すること
]
new_labels = ["positive", "negative"]  # ← 正解ラベルを設定すること

# ── spacy で分かち書き ────────────────────────────────────────────────────
def text_to_wakati(text):
    return " ".join([token.lemma_ for token in nlp(str(text))])

new_wakati = [text_to_wakati(t) for t in new_texts]
print("分かち書き結果:")
for orig, wak in zip(new_texts, new_wakati):
    print(f"  {orig}")
    print(f"  → {wak}")
    print()

# ── vectorizer.transform() でベクトル化 ───────────────────────────────────
new_X = vectorizer.transform(new_wakati)
print(f"new_X の形状: {new_X.shape}")
print(f"X_test と列数が一致: {new_X.shape[1] == X_test.shape[1]}")
print()

# ── 予測 ──────────────────────────────────────────────────────────────────
new_preds = model.predict(new_X)

print("予測結果:")
for text, true, pred in zip(new_texts, new_labels, new_preds):
    mark = "✓ 正解" if true == pred else "✗ 不正解(仮説通り失敗)"
    print(f"  [{mark}] 正解: {true:10s}, 予測: {pred}")
    print(f"    {text}")

48.5.8. ✍️ Level 4 報告事項#

(1) 作成した new_textsnew_labels を表形式で掲載すること。

(2) 予測結果を掲載し、仮説の妥当性についての考察を記述すること。

  • 予測が仮説通り失敗した場合:なぜこの仮説は妥当と言えるか。

  • 予測が仮説に反して成功した場合:なぜ失敗しなかったと考えられるか。仮説をどう修正するか。

注意: 仮説が「誤り」という結論でも問題ありません。
観察結果に基づいて論理的に考察してください(減点しません)。


48.6. オプション#

以下は必須ではない。取り組んだ場合はレポートに記載すること(加点対象)。

48.6.1. (A) CountVectorizer のカスタマイズ#

vectorizer = CountVectorizer(token_pattern=r'(?u)\b\w+\b')  # 1文字語を含める
vectorizer = CountVectorizer(min_df=2)                       # 2件以上に出現する語のみ
vectorizer = CountVectorizer(ngram_range=(1, 2))             # ユニグラム+バイグラム
vectorizer = CountVectorizer(max_features=500)               # 語彙数を制限

なお CountVectorizer がデフォルトで1文字の語を除外する理由を確認しよう(ヒント: token_pattern のデフォルト値を調べよ)。

48.6.2. (B) TfidfVectorizer による重み付き特徴ベクトル#

CountVectorizer の代わりに TfidfVectorizer を使い、精度を比較せよ。

48.6.3. (C) spacy の文ベクトル#

spacy の文ベクトル(doc.vector)を特徴として使い、BoW との精度を比較せよ。

import numpy as np
X_vec_train = np.array([nlp(text).vector for text in train_df["review_text"]])
X_vec_test  = np.array([nlp(text).vector for text in test_df["review_text"]])

48.6.4. (D) 業界・観点別の詳細分析#

Level 4 で発見した誤分類の傾向を深掘りし、 「特定の業界や観点に特化した改善案」を提案せよ。
例: 業界ごとに異なる分類器を使う / 業界名・観点名を特徴に加える など。

# ── オプション (B): TfidfVectorizer の例 ──────────────────────────────────
# 以下のコメントを外して実行してみよう

# from sklearn.feature_extraction.text import TfidfVectorizer
# from sklearn.linear_model import LogisticRegression
#
# vec_tfidf = TfidfVectorizer()
# X_tr_tfidf = vec_tfidf.fit_transform(corpus_train)
# X_te_tfidf = vec_tfidf.transform(corpus_test)
#
# m_tfidf = LogisticRegression(max_iter=1000)
# m_tfidf.fit(X_tr_tfidf, y_train)
# print(f"TF-IDF + LR の test 精度: {m_tfidf.score(X_te_tfidf, y_test):.4f}")

print("オプション: コメントを外して実行してみよう")

48.7. 提出物#

  • このノートブック(report3_2026.ipynb)

    • Phase 2 の実行済み出力(Output)が残っている状態で提出すること

    • Level 1〜4 の報告事項(✍️ マーク)に対する回答・考察をノートブック内のセルに記入すること

    • Level 3 の仮説ラベル、Level 4 の new_texts など、自分が追記した部分が明確になっていること

  • ノートブックでレポートを書きづらい場合には、別途自由形式で作成して提出しても良い。ただし実行結果を保存したノートブックは必ず提出すること。

注意: CSVファイルやpickleファイルは提出しないでください。