以下の内容はhttps://nealle-dev.hatenablog.com/entry/2025/05/01/102722より取得しました。


Gemini 2.5 Proと取り組んだデータ分析のリアルな道のり

はじめに

はじめまして。Analyticsチームの清水です。 2024年12月に入社しまして、約4ヶ月が経過しました。今回が初めてのテックブログになります。

▼先日、入社エントリも公開しました。

本稿のテーマは、自由記述のテキストをラベリングして分類する分析タスクに対し、Geminiと共に取り組んで分かったことの共有です。

私は生成AIをそれほどたくさん使った経験があるわけではないので、これが最良の使い方というわけではないと思いますが、どのようにプロンプトを組み立て、どう効率的に分析を進められたのかを可能な限りリアルに書いていきます。

※今回利用したモデルは、Gemini 2.5 Proです。

Geminiを活用したデータ分析の進め方

今回の分析のゴールは、自由記述で寄せられるテキストから、含まれる要望を自動でタグ付けするマルチラベル分類モデルを構築し、業務で活用できるレベルまで精度を高めることでした。

このプロセス全体を通じて、まさに二人三脚のようにGeminiと対話しながら進めました。正直、「ここまで考えて文脈を汲み取ってくれるのか!」と驚くほど日本語の読解力が高く、自分の"Gemini"(双子)というには優秀過ぎると感じました。具体的な分析フェーズごとに、どのようにGeminiを活用したかをご紹介します。


フェーズ0: アプローチの模索 - Notebook LMや教師なし学習の試行

最初の発想: 前提としてGeminiを使っていきなり最適なアプローチに到達したわけではありませんでした。まずやってみたことは、最もローコストな方法としてNotebook LMにCSV出力したテキストを直接入力する方法でした。この方法はそれらしいアウトプットが出るものの、詳細を確認すると全体の件数が違っていたり、内容に対する解釈が異なっていたりで、分析結果として信頼に足るものではありませんでした。そこで本稿のテーマである、Geminiとの分析が始まるわけですが、Gemini分析のゴールに直接到達できたわけではありませんでした。最初の状態として、手元には大量の自由記述テキストデータがありましたが、ラベル付けはされていませんでした。「まずは、これらのテキストデータに内在するパターンやトピックを自動で見つけ出せないか?」と考え、教師なし学習のアプローチから試してみることにしました。

Gemini活用 (模索):

私: 「大量の日本語テキストデータがあります。これらのテキストに含まれる主要な話題や傾向を自動でグループ分けするには、どのような教師なし学習の手法がありますか? scikit-learn で実装できるものを教えてください。」

Geminiは、トピックモデリング (LDA)クラスタリング (K-Means) といった代表的な手法を提案してくれました。それぞれの簡単な解説と、TF-IDFでテキストをベクトル化した後にこれらの手法を適用するサンプルコードも生成してくれました。

# --- トピックモデリング (LDA) のコード例 (Gemini生成の骨子) ---
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# vectorizer = TfidfVectorizer(...) # MeCab tokenizer を使うなど
# X = vectorizer.fit_transform(df['text_column'])

# n_components でトピック数を指定
# lda = LatentDirichletAllocation(n_components=10, random_state=42) 
# lda.fit(X)

# 各トピックの主要単語を表示する処理などを追加...

# --- クラスタリング (K-Means) のコード例 (Gemini生成の骨子) ---
from sklearn.cluster import KMeans

# vectorizer = TfidfVectorizer(...)
# X = vectorizer.fit_transform(df['text_column'])

# n_clusters でクラスター数を指定
# kmeans = KMeans(n_clusters=5, random_state=42, n_init='auto')
# clusters = kmeans.fit_predict(X)
# df['cluster'] = clusters

# 各クラスターに含まれるテキスト例を表示する処理などを追加...

結果と課題: 生成されたコードを元にこれらの手法を試してみましたが、結果は芳しくありませんでした。

  • LDA: 生成されたトピックは、「駐車場」「利用」「希望」のような非常に曖昧で一般的な単語の集まりが多く、具体的なトピックを捉えるには解像度が低すぎました。

  • K-Means: テキストをいくつかのクラスターに分けることはできましたが、各クラスターに様々な種類の要望が混在してしまい、明確な意味付けが困難でした。

Geminiとの再相談と方針転換:

私: (教師なし学習の結果を見せながら)「トピックやクラスターが曖昧で、具体的な要望を抽出するという目的には合わなそうです。もっと具体的に、判明すると施策に繋がるような情報量を持ったタグを付けたいのですが、どうすればいいでしょうか?」

この結果と目的を伝えたところ、Geminiは次のようにアドバイスしてくれました。

Gemini: 「教師なし学習はデータの全体的な構造を見るのには役立ちますが、事前に定義された特定のカテゴリ(ラベル)を精度良く付与したいのであれば、教師あり学習、特にマルチラベル分類のアプローチが適しています。 もし、ある程度の量のテキストに対して人手で正しいラベルを付与できるのであれば、その教師データを使ってモデルを学習させる方が、今回の目的には遥かに効果的でしょう。」

学び/ポイント: うまくいかなかったアプローチとその結果をGeminiにフィードバックすることで、より目的に合った代替アプローチ(教師あり学習)とその根拠を得ることができました。最初の試みがうまくいかなくても、その結果を共有して相談することで、軌道修正のヒントをもらえます。この時点で、「遠回りに見えても、ラベル付けのコストをかけて教師あり学習に進むべきだ」と判断しました。


フェーズ1: データ理解とラベルチェック - コード生成と探索的分析支援

課題: まずは、手作業でラベル付けした教師データの品質確認から始める必要がありました。(ラベルの種類や件数、表記揺れの有無、欠損などを把握するためです。)

Gemini活用: ここは言語化しやすい定型的な作業だったので、Geminiに丸投げしてみることにしました。

私: 「CSVファイルを読み込み、指定したラベル列の欠損値、ユニークなラベルの種類と件数、特定のテキスト列に対して複数の異なるラベルが割り当てられていないかチェックする Python (pandas) コードを生成して」

とお願いしたところ、以下のようなコード片を含むスクリプトがすぐに生成されました。

# (ファイル読み込み部分などは省略)

# --- 1. 欠損値の確認 ---
print("\n--- 欠損値の確認 ---")
print(df.isnull().sum())

# --- 2. ラベルの分布と種類を確認 ---
label_column_name = 'your_label_column' # 対象のラベル列
if label_column_name in df.columns:
    print(f"\n--- ラベル '{label_column_name}' の分布と種類 ---")
    print(f"ユニークなラベルの種類: {df[label_column_name].nunique()}種類")
    print("各ラベルの件数:")
    print(df[label_column_name].value_counts())

# --- 3. 同じテキストに異なるラベルが付いていないか ---
text_column_name = 'your_text_column' # 対象のテキスト列
if text_column_name in df.columns and label_column_name in df.columns:
     # (テキストとラベル両方に欠損がないデータでグループ化)
     text_label_counts = df.dropna(subset=[text_column_name, label_column_name]) \
                              .groupby(text_column_name)[label_column_name].nunique()
     inconsistent_texts = text_label_counts[text_label_counts > 1].index.tolist()
     if inconsistent_texts:
         print(f"★★★警告: 同じ '{text_column_name}' に複数の異なる '{label_column_name}' が割り当てられています!★★★")
         # (該当データの表示処理)
     else:
         print(f"'{text_column_name}' と '{label_column_name}' の組み合わせに、同一テキストに対するラベルの揺れは見つかりませんでした。")

実行してみると、当初用意したデータにはいくつか特徴や課題があることがわかりました。具体的には、ラベル欠損が1件存在すること、ユニークラベルが58種類もありかなり多いこと(これはマルチラベルをカンマ区切りで1列に格納していたためです)、そして件数が極端に少ないラベルがあること、などです。

さらに、

私: 「各ラベルに対応するテキスト例もいくつか見たいな」

と追加で頼むと、それもすぐに対応するコードを出してくれました。これにより、ラベル付けの基準が曖昧になっていないか、おかしなラベルが付いていないかを目視で確認でき、データの全体像を掴む良いスタートが切れました。

学び/ポイント: データ分析の最初のステップである探索的データ分析(EDA)や品質チェックは、やることが決まっている部分が多いです。Geminiに具体的な処理内容を指示すれば、定型的なコードは一瞬で生成してくれます。これにより、自分でコードを書いたり調べたりする時間を大幅に削減し、データの「中身」の確認に集中できました。


フェーズ2: ベースラインモデル構築 - MLワークフローの提示と基本コード生成

課題: データが整ってきたので、いよいよ本題のマルチラベル分類モデルの構築です。まずは基本的なモデル(ベースライン)をサッと作りたいと考えました。

Gemini活用: 機械学習モデルの実装にはある程度決まった手順(前処理→学習→評価)があります。これもGeminiにお任せできそうでした。

私: 「テキスト列を入力として、複数ラベル('label_1', 'label_2', 'label_3'列)を予測するマルチラベル分類のベースラインモデルを scikit-learn で実装するコードをください。テキスト前処理はTF-IDF、分類器はロジスティック回帰を使ってください」

と、使いたいライブラリや手法を指定して依頼しました。すると、以下のような骨子を持つパイプラインコードがすぐに生成されました。

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from sklearn.metrics import classification_report

# --- データ準備 ---
# df = pd.read_csv(...)
text_column = 'ここにテキストカラム名をセットする'
label_columns = ['label_1', 'label_2', 'label_3']
# ラベルを MultiLabelBinarizer 用のリスト形式に変換
# labels_list = ... (NaNを除外しリスト化する処理) 
mlb = MultiLabelBinarizer()
y = mlb.fit_transform(labels_list)
X_text = df[text_column]

# --- テキスト前処理 & データ分割 ---
vectorizer = TfidfVectorizer(analyzer='char_wb', ngram_range=(2, 4)) # ← 初期案は日本語向きではなかった
X = vectorizer.fit_transform(X_text)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# --- モデル学習 ---
base_model = LogisticRegression(solver='liblinear', random_state=42)
ovr_model = OneVsRestClassifier(base_model)
ovr_model.fit(X_train, y_train)

# --- モデル評価 ---
y_pred = ovr_model.predict(X_test)
print(classification_report(y_test, y_pred, target_names=mlb.classes_, zero_division=0))

このように、テキストの前処理 TfidfVectorizer、ラベルのバイナリ化 MultiLabelBinarizer、マルチラベル化 OneVsRestClassifier、分類器 LogisticRegression を組み合わせた、一連の処理を行うパイプラインコードがすぐに手に入りました。また、手法についての専門知識がなくても、やりたいことに対してどんなアプローチが有効か尋ねれば、根拠とともに提案してくれるため、モデル構築のハードルがさらに下がると感じました。

学び/ポイント: 標準的な機械学習のワークフロー構築もGeminiの得意とするところです。必要な要素(入力、出力、使いたい手法)を伝えれば、ボイラープレートコード(お決まりのコード)を書く手間が省け、大幅な時短につながります。


フェーズ3: モデル精度改善のイテレーション - 課題特定と改善策提案

課題: しかし、ベースラインモデルを評価したところ、多くのラベルでRecall(再現率)が非常に低く(ほとんど0に近い!)、実用には程遠い精度でした。

Gemini活用: 評価結果を見ながらGeminiと対話的に改善を進めるフェーズです。

私: (評価レポートを提示し)「この結果だと全然ダメですね…。特にRecallが低いです。原因と改善策として何が考えられますか?」

改善1 (前処理): Geminiはまず「日本語のテキスト前処理が単純すぎる(analyzer='char_wb' では単語分割されていない)のが原因かも」と指摘。MeCabを使った形態素解析の導入を提案し、TfidfVectorizerに組み込むための以下のような tokenizer 関数と修正コードを提供してくれました。

import MeCab
mecab_tagger = MeCab.Tagger("-Owakati") 

def mecab_tokenizer(text):
    if mecab_tagger is None: return text.split()
    node = mecab_tagger.parse(str(text)) 
    return node.strip().split()

# TfidfVectorizer の修正
vectorizer = TfidfVectorizer(tokenizer=mecab_tokenizer, ngram_range=(1, 1)) 

これを試すと、精度が劇的に向上! やはり日本語処理には適切なトークナイズが不可欠だと再認識しました。

改善2 (不均衡データ): それでもまだRecallが低いラベルがあったため、再度Geminiに相談。「ラベルごとのデータ数に偏りがある(クラス不均衡)のが影響していそう」と指摘があり、分類器のオプション class_weight='balanced' の利用を提案されました。

# モデル定義の修正
base_model = LogisticRegression(solver='liblinear', random_state=42, class_weight='balanced') 
ovr_model = OneVsRestClassifier(base_model)

これも適用すると、少数派ラベルのRecallが大幅に改善し、全体の精度もさらに大きく向上しました!class_weight='balanced' は不均衡データに対して非常に有効な手段ですが、自分だけではすぐには思いつかなかったかもしれません。

改善3 (モデル比較): さらに精度を追求するため、「LogisticRegression 以外に試すべきモデルは?」と尋ねたところ、LinearSVC(SVM)を提案されました。コードを修正して比較した結果、わずかですがLinearSVCの方が全体的な指標が良いことがわかりました。

学び/ポイント: このフェーズがGeminiの真骨頂かもしれません。評価結果という状況証拠を与え、課題を投げかけると、考えられる原因と具体的な改善策(しかも複数!)を提示してくれます。「なぜその改善策が有効なのか」という理由も説明してくれるため、納得感を持って試すことができます。前処理の改善、不均衡データへの対処、モデルの選択といったモデル改善の定石を、対話しながら効率的に試していくことができました。まさに壁打ち相手です。


フェーズ4: 予測ロジックの調整とエッジケース対応

課題: モデルの精度は向上しましたが、実際の運用を考えると、調整が必要な点が出てきました。具体的には、①予測結果が空になることがある(本来は「無効」が付くはず)、②ある希少ラベルが異常に多く予測されてしまう、という問題です。

Gemini活用:

課題1対応 (ビジネスルール実装): 「空予測をなくして『無効』をFallbackにしたい」「『無効』と他のラベルは共存させない」というルールを伝え、解決策を相談しました。Geminiは predict() の閾値の問題を解説し、decision_function のスコアを利用するロジックを提案。最終的に以下の骨子を持つコードを一緒に作り上げました。

# (モデル読み込み、スコア計算までは省略)
label_names = loaded_mlb.classes_
invalid_label_name = '無効'
predicted_labels_final = []

for i in range(len(decision_scores)):
    sample_scores = decision_scores[i]
    positive_indices = np.where(sample_scores >= 0)[0] # スコア0以上を取得
    
    if len(positive_indices) == 0:
        predicted_labels_final.append([invalid_label_name]) # 空なら無効
    else:
        positive_labels_set = set(label_names[positive_indices])
        valid_positive_labels = positive_labels_set - {invalid_label_name} # 無効を除外
        
        if valid_positive_labels: # 有効ラベルがあればそれを使う
            predicted_labels_final.append(sorted(list(valid_positive_labels)))
        else: # 無効しかなければ無効を使う
            predicted_labels_final.append([invalid_label_name])

# df['predicted_labels_final'] = predicted_labels_final

課題2対応 (異常検知・対策): 本来は低頻度に出現するはずのあるラベルの大量予測について相談したところ、「極めて稀なラベルに対してclass_weight='balanced'を適用したことで、モデルがわずかな特徴に過剰反応している(バイアスがかかっている)可能性がある」と原因を推測してくれました。そして、対策として「該当する希少すぎるラベルを学習データから除外(枝刈り)する」ことを提案されました。実際に教師データから出現頻度が極端に低いラベルを除外して再学習したところ、問題は完全に解消され、モデル全体の性能もより安定しました。これも一種の「うまくいかなかったアプローチ」(希少ラベルをそのまま学習させること)からの学びでした。

学び/ポイント: モデルの精度向上だけでなく、予期せぬモデルの挙動(異常予測)に対する原因分析と対策立案においても、Geminiは非常に頼りになるパートナーでした。特に原因不明の現象に対して、可能性のあるメカニズムを解説し、具体的な対策(データクレンジングや枝刈り)を提示してくれる点は驚きでした。ビジネスルールをコードに落とし込む際も、的確なサポートが得られました。


フェーズ5: 結果の集計と可視化 - レポート用コード生成

課題: 最終的なモデルで大量データ(約5万件)に予測を適用した後、どのラベルがどれくらい予測されたのかを集計し、グラフで可視化する必要がありました。

Gemini活用:

集計・可視化コード: 「予測結果(ラベルリストの列)を集計して、ラベルごとの件数と割合を算出し、棒グラフで表示する Python コードをください」と依頼。pandasexplodevalue_counts を使った集計コード、seaborn を使ったグラフ描画コードをすぐに入手できました。

# (データ読み込み、列名指定などは省略)

# --- ラベルごとの件数と割合の計算 ---
exploded_labels = df_dataset[label_col_name].explode().dropna()
label_counts = exploded_labels.value_counts()
total_rows = len(df_dataset)
label_percentages = (label_counts / total_rows) * 100
label_stats = pd.DataFrame({'件数': label_counts.astype(int), '割合 (%)': label_percentages}) 
label_stats['割合 (%)'] = label_stats['割合 (%)'].round(2) # 割合を丸める
label_stats = label_stats.sort_values(by='件数', ascending=False)

# --- 結果の表示 (display) と '無効' の分離 ---
# ... (display(label_stats) や '無効' の個別表示処理) ...

# --- グラフ描画 ('無効'を除く) ---
label_stats_for_plot = label_stats.drop('無効', errors='ignore') 
if not label_stats_for_plot.empty: 
    plt.figure(figsize=(12, 7)) 
    sns.barplot(x=label_stats_for_plot.index, y=label_stats_for_plot['件数'], palette='viridis') 
    # ... (グラフのラベル、タイトル、回転などの設定) ...
    plt.show()

Geminiが生成したコードにより出力したグラフ

さらに、表示に関する細かい要望(表形式の調整、数値フォーマット、グラフから特定のラベルを除外するなど)にも柔軟に対応してくれました。

学び/ポイント: 最終的なレポーティングのための集計・可視化コード生成もGeminiの得意分野です。これにより、最後のまとめ作業もスムーズに進められました。


おわりに

今回の経験から、生成AIが分析作業における多くの「手足」となる部分(コーディング、デバッグ、定型処理、改善策の技術的な提示など)を効率化・代行できることを強く実感しました。

また、作業の効率化・代行に加え、教科書通りにはいかないことが多い分析タスクにおいて、行き詰まった際の壁打ち相手として非常に有用であることも重要な学びでした。

もちろん、生成AIの回答を鵜呑みにせず、批判的に評価する姿勢は不可欠です。しかし、分析モデル構築のように、状況に応じて最適なアプローチを見つけ出す必要があるタスクにおいては、今回のような使い方が非常に有効だと感じています。

この知見は組織全体のデータ活用レベルの底上げにも繋がる可能性があります。 他部署のメンバーがデータ分析に取り組む際、生成AIを強力な「副操縦士」として活用できるようサポート(適切なプロンプト設計や結果の解釈を伴走支援=イネーブルメント)する体制を構築できれば、これまで分析が難しかった課題にも組織全体でアプローチできるようになるかもしれません。そのためには、生成AIと既存のデータ基盤(Redashなど)をどう連携させるかも鍵となりそうです。

受託的な分析依頼を「こなす」だけでなく、組織に分析能力を「広める」という、Analyticsチームの役割のシフトを加速させる可能性を強く感じました。ユーザー ↔︎ 生成AI ↔︎ データ基盤のつながりをセキュリティやコストなどを考慮した上で構築することが、まさにその実現に向けた大きな一歩だと思います。




以上の内容はhttps://nealle-dev.hatenablog.com/entry/2025/05/01/102722より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14