48. レポート課題3:業界口コミデータへの自然言語処理適用#
48.1. 使用データ#
合成口コミデータセット(別途Googleドライブで配布)を用いる。 IT系スタートアップ・大手製造業・総合商社・地方金融機関・公共インフラの5業界に関する 口コミ文と感情ラベル(positive / negative / neutral)から構成される。
注意: 使用するデータはすべて合成データです。
実在する企業・個人を示すものではありません。
48.1.1. データの列構成#
列名 |
内容 |
|---|---|
|
サンプルID |
|
業界(5種) |
|
口コミの観点(残業/給与/成長/社風/人間関係) |
|
口コミ本文 |
|
感情ラベル(positive / negative / neutral) |
48.2. 課題の目標#
spacy(ja_ginza)による分かち書きで特徴ベクトルを構築する
「データ前処理フェーズ」と「モデル構築・評価フェーズ」を分離して実験効率を体験する
識別器の失敗要因を業界・観点などのメタ情報も活用して分析し、仮説を立案する
仮説の妥当性を新たなサンプルで軽く検証する
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. 実行手順#
予め、Google Drive 直下に
2026dm-rep3フォルダを作成しておく。このフォルダを作業フォルダと呼ぶことにする。今回の作業は全て作業フォルダ内で行うこと。共有ドライブで配布している以下のファイルをダウンロードし、先ほど用意した作業フォルダにアップロードする。
report3_job_reviews.csv: コーパス。preprocess_corpus.ipynb: Phase 1の前処理用ノートブック。report3_2026.ipynb: 本ノートブック。Phase 2以降の参考コード付き。
Phase 1 のために、
preprocess_corpus.ipynbを実行し、前処理を行った pickle ファイルを生成する。以降は 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.LogisticRegressionsklearn.svm.LinearSVCsklearn.tree.DecisionTreeClassifiersklearn.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. 推奨手順#
テストデータで予測し、失敗事例を抽出する
失敗事例の
industry_type/review_categoryの分布を確認する(クロス集計)全失敗事例から N 件(N ≥ 20)をランダムにサンプリングして元テキストを観察する
※ランダム選択は reporting bias を避けるため仮説ラベルを付与し、出現頻度をカウントして改善案を絞り込む
上記以外の手順で分析&仮説立案しても良い。ただし、改善案は必ず観察結果に基づくこと。根拠のない提案は評価しない。
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_texts と new_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ファイルは提出しないでください。