
クリエイティブ戦略部デザインユニットの島田です。
最近、顧客アンケートの結果が手元にあったり、解約理由を眺めたりして、これらの自由に書かれたテキストをどうにか解析したい欲があったので、今回「形態素解析」を実践してみました。
最近、顧客アンケートの結果が手元にあったり、解約理由を眺めたりして、これらの自由に書かれたテキストをどうにか解析したい欲があったので、今回「形態素解析」を実践してみました。
形態素とは
Wikipedia
文を意味を持つ最小の単位のこと。
「私は札幌でラーメンを食べました」この文章を形態素で分解するとこうなります。
私(名詞)は(助詞)札幌(名詞)で(助詞)ラーメン(名詞)を(助詞)食べ(動詞)ました(接尾辞)
中学国語の授業で「品詞分解」や「活用」などで勉強しましたね。(私はほとんど覚えていない)
文を意味を持つ最小の単位のこと。
「私は札幌でラーメンを食べました」この文章を形態素で分解するとこうなります。
私(名詞)は(助詞)札幌(名詞)で(助詞)ラーメン(名詞)を(助詞)食べ(動詞)ました(接尾辞)
中学国語の授業で「品詞分解」や「活用」などで勉強しましたね。(私はほとんど覚えていない)
なんで形態素に分解することが解析の役に立つのか?
例えばアンケートの自由記述欄に書かれた回答は、回答者の生の声であり、貴重な情報源です。しかし、自由記述欄の回答は、回答者によって表現が異なるため、そのままでは分析が困難です。そこで、形態素解析を用いることで、自由記述欄の回答を効率的に分析することができます。
形態素解析によって得られた情報を用いることで、以下の様な分析に役立てることが可能です。
【キーワードの抽出】
名詞や形容詞を抽出することで、回答者がどのようなキーワードを頻繁に使用しているかを把握できます。
【感情分析】
例えば、抽出された形態素に喜怒哀楽のタグをつけて分類することで、ポジティブな感情が多いか、ネガティブな感情が多いか等を把握することができます。
【トピックの分析】
キーワードの出現頻度や、前後の共起関係を確認することで、回答者がどのようなトピックについて言及しているかを把握することができます。
利点は4つ
・自由記述欄の回答を定量的に分析することができます。
・回答者の意見の傾向を把握することができます。
・新製品の開発やサービスの改善に役立てることができます。
・大量のアンケート結果から効率よく情報を抽出することが出来る。
形態素解析によって得られた情報を用いることで、以下の様な分析に役立てることが可能です。
【キーワードの抽出】
名詞や形容詞を抽出することで、回答者がどのようなキーワードを頻繁に使用しているかを把握できます。
【感情分析】
例えば、抽出された形態素に喜怒哀楽のタグをつけて分類することで、ポジティブな感情が多いか、ネガティブな感情が多いか等を把握することができます。
【トピックの分析】
キーワードの出現頻度や、前後の共起関係を確認することで、回答者がどのようなトピックについて言及しているかを把握することができます。
利点は4つ
・自由記述欄の回答を定量的に分析することができます。
・回答者の意見の傾向を把握することができます。
・新製品の開発やサービスの改善に役立てることができます。
・大量のアンケート結果から効率よく情報を抽出することが出来る。
実践、形態素解析
私のような非プログラマーでもできるように、「全部無料」「出来るだけコピペで簡潔」を目指しました!
(多少自分で書き換えないといけない部分あります。)
【レシピ】
・スプレッドシート
・GAS
・YahooAPI
(多少自分で書き換えないといけない部分あります。)
【レシピ】
・スプレッドシート
・GAS
・YahooAPI

1,スプレッドシートを準備する
今回はアンケートの自由記述欄を解析します。(ダミーデータお借りしました:https://note.com/tmook/n/ned666ac05ada)
構成は、1行目にはタイトル的なものがあり、2行目からデータがベタッと貼ってあります。
元データがエクセルの場合は、まずGoogleドライブにアップロードした後、上部メニューの「ファイル」>「Googleスプレッドシートとして保存」をしましょう。スプレッドシートの形式にならないとGASが使えません。
同じスプレッドシート内に「ストップワードリスト」というシートを新しく作って、このページの長ーい単語コピペしてください。行列A1からベタッと貼って良いです。
http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt
ざっくり説明すると、ストップワードとは形態素に分解したときに、集計の邪魔になりそうな言葉をリストにしたものです。
構成は、1行目にはタイトル的なものがあり、2行目からデータがベタッと貼ってあります。
元データがエクセルの場合は、まずGoogleドライブにアップロードした後、上部メニューの「ファイル」>「Googleスプレッドシートとして保存」をしましょう。スプレッドシートの形式にならないとGASが使えません。
同じスプレッドシート内に「ストップワードリスト」というシートを新しく作って、このページの長ーい単語コピペしてください。行列A1からベタッと貼って良いです。
http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt
ざっくり説明すると、ストップワードとは形態素に分解したときに、集計の邪魔になりそうな言葉をリストにしたものです。

2,yahooAPI
yahooAPIの「日本語形態素解析」を使うため、yahooアカウントを登録します。(すでに持っている人はそのアカウントでOK)
https://account.edit.yahoo.co.jp/signup
yahooデベロッパーネットワークで、APIキーを獲得する。
「アプリケーションの管理」>「新しいアプリケーションを開発」
色々な設定内容は以下のとおり。
https://developer.yahoo.co.jp/
======
[Web APIを利用する場所]
ID連携利用有無 ID連携を利用しない
[アプリケーションの利用者情報(契約者情報)]
利用者情報 個人
メールアドレス 設定済み
[個人情報授受にかかる確認事項]
個人情報提供先としてユーザーへ開示することへの同意 同意しない
[住所]
契約者住所の国または地域 日本
[アプリケーションの基本情報]
アプリケーション名 テキスト解析
サイトURL なし
アプリケーションの説明 なし
利用するスコープ 利用可能なスコープはありません
======
クライアントIDというものがあります。あとで使うのでコピーして保管しておきましょう。
https://account.edit.yahoo.co.jp/signup
yahooデベロッパーネットワークで、APIキーを獲得する。
「アプリケーションの管理」>「新しいアプリケーションを開発」
色々な設定内容は以下のとおり。
https://developer.yahoo.co.jp/
======
[Web APIを利用する場所]
ID連携利用有無 ID連携を利用しない
[アプリケーションの利用者情報(契約者情報)]
利用者情報 個人
メールアドレス 設定済み
[個人情報授受にかかる確認事項]
個人情報提供先としてユーザーへ開示することへの同意 同意しない
[住所]
契約者住所の国または地域 日本
[アプリケーションの基本情報]
アプリケーション名 テキスト解析
サイトURL なし
アプリケーションの説明 なし
利用するスコープ 利用可能なスコープはありません
======
クライアントIDというものがあります。あとで使うのでコピーして保管しておきましょう。
3,GASにスクリプトを書く
スプレッドシートの上部メニュー「拡張機能」>「Apps Script」を開く
そこに以下のソースコードをコピペ。
Client IDの部分に、先程取得したものをコピペして、シート名など自分用に編集してください。
そこに以下のソースコードをコピペ。
Client IDの部分に、先程取得したものをコピペして、シート名など自分用に編集してください。
function analyzeTextYahooBunsetsu() {
const apiKey = encodeURIComponent("Client ID"); // yahooAPIのClient IDをコピペ
const apiUrl = `https://jlp.yahooapis.jp/MAService/V2/parse?appid=${apiKey}`;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName("感想"); // データが入っているシート名をコピペ
const sheets = [
{ name: "感想_形態素", column: 1 }, // 出力するシート名を任意に設定
];
const stopWordSheet = ss.getSheetByName("ストップワードリスト");
const stopWords = stopWordSheet.getRange(1, 1, stopWordSheet.getLastRow(), 1).getValues().flat();
sheets.forEach(({ name, column }) => {
const data = sh.getRange(2, column, sh.getLastRow() - 1, 1).getValues();
const numRows = data.filter(String).length;
const results = [];
const allPhrases = [];
for (let i = 1; i <= numRows; i++) {
const text = data[i - 1][0];
if (text) {
const payload = JSON.stringify({
id: "1111",
jsonrpc: "2.0",
method: "jlp.maservice.parse",
params: {
q: text,
context: {
entries: ["5W1H","5W1H法","教科書"], //形態素で分解されたくないテキストを登録する
},
},
});
const options = {
method: "post",
contentType: "application/json",
payload: payload,
};
try {
const response = UrlFetchApp.fetch(apiUrl, options);
const json = JSON.parse(response.getContentText());
if (json && json.result && json.result.tokens) {
const m_result = json.result.tokens.map((token) => token.join("\t"));
m_result.pop();
const break_pos = ["名詞", "動詞", "接頭詞", "副詞", "感動詞", "形容詞", "形容動詞", "連体詞"];
const wakachi = [""];
let afterPrepos = false;
let afterSahenNoun = false;
for (const v of m_result) {
if (!v.includes("\t")) continue;
const surface = v.split("\t")[0];
const pos = v.split("\t")[3].split(",");
const pos_detail = v.split("\t")[4];
let noBreak = !break_pos.includes(pos[0]);
noBreak = noBreak || pos_detail.includes("接尾");
noBreak = noBreak || (pos[0] === "動詞" && pos_detail.includes("サ変接続"));
noBreak = noBreak || pos_detail.includes("非自立");
noBreak = noBreak || afterPrepos;
noBreak = noBreak || (afterSahenNoun && pos[0] === "動詞" && pos_detail.includes("サ変・スル"));
if (!noBreak) {
wakachi.push("");
}
wakachi[wakachi.length - 1] += surface;
afterPrepos = pos[0] === "接頭詞";
afterSahenNoun = pos_detail.includes("サ変接続");
}
if (wakachi[0] === "") wakachi.shift();
results.push(wakachi);
allPhrases.push(...wakachi);
} else {
results.push(["解析結果なし"]);
}
} catch (e) {
Logger.log("? エラー発生: " + e.message);
results.push(["エラー"]);
}
} else {
results.push([""]);
}
}
// 半角数字だけの文節と「や」「【」だけの文節と一文字だけのひらがな・カタカナ・漢字を除外
const filteredPhrases = allPhrases.filter((phrase) => !/^[0-9]+$/.test(phrase) && phrase !== "「" && phrase !== "【" && !/^[\u3040-\u309F\u30A0-\u30FF]$/.test(phrase) && !/^[\u4E00-\u9FFF]$/.test(phrase));
const phraseCounts = {};
for (const phrase of filteredPhrases) {
phraseCounts[phrase] = (phraseCounts[phrase] || 0) + 1;
}
const sortedPhrases = Object.entries(phraseCounts)
.sort(([, countA], [, countB]) => countB - countA)
.map(([phrase, count], index) => ({ 順位: index + 1, 文節: phrase, 出現回数: count }))
.filter(({ 文節 }) => !stopWords.includes(文節));
const coOccurrences = {};
for (const wakachi of results) {
for (let i = 0; i < wakachi.length; i++) {
const phrase = wakachi[i];
const prevPhrase = i > 0 ? wakachi[i - 1] : null;
const nextPhrase = i < wakachi.length - 1 ? wakachi[i + 1] : null;
coOccurrences[phrase] = coOccurrences[phrase] || { 前: [], 後: [] };
if (prevPhrase) {
coOccurrences[phrase].前.push(prevPhrase);
}
if (nextPhrase) {
coOccurrences[phrase].後.push(nextPhrase);
}
}
}
const output = sortedPhrases.map(({ 順位, 文節, 出現回数 }) => {
const coOccur = coOccurrences[文節] || { 前: [], 後: [] };
let prevCoOccur = "";
let prevCoOccurCount = 0;
if (coOccur.前.length > 0) {
const prevCoOccurCounts = coOccur.前.reduce((acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {});
const maxPrevCoOccur = Object.entries(prevCoOccurCounts).reduce((a, b) => (a[1] > b[1] ? a : b));
prevCoOccur = maxPrevCoOccur ? maxPrevCoOccur[0] : "";
prevCoOccurCount = maxPrevCoOccur ? maxPrevCoOccur[1] : 0;
}
let nextCoOccur = "";
let nextCoOccurCount = 0;
if (coOccur.後.length > 0) {
const nextCoOccurCounts = coOccur.後.reduce((acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {});
const maxNextCoOccur = Object.entries(nextCoOccurCounts).reduce((a, b) => (a[1] > b[1] ? a : b));
nextCoOccur = maxNextCoOccur ? maxNextCoOccur[0] : "";
nextCoOccurCount = maxNextCoOccur ? maxNextCoOccur[1] : 0;
}
const prevCoOccurWithCount = prevCoOccur ? `${prevCoOccur} (${prevCoOccurCount}回)` : "";
const nextCoOccurWithCount = nextCoOccur ? `${nextCoOccur} (${nextCoOccurCount}回)` : "";
return [順位, 文節, 出現回数, prevCoOccurWithCount, nextCoOccurWithCount];
});
let newSheet = ss.getSheetByName(name);
if (newSheet) {
ss.deleteSheet(newSheet);
}
newSheet = ss.insertSheet(name);
newSheet.getRange(1, 1, 1, 5).setValues([["順位", "文節", "出現回数", "共起語(前)", "共起語(後)"]]);
newSheet.getRange(2, 1, output.length, 5).setValues(output);
});
}

4,実行!
「▶実行」をクリック
形態素に分解された文節が出力されました。
この時エラーで出力出来ないなどあれば、もしかしたらデータ量が多いのかもしれません。
yahooAPIの無料範囲は1分で300回です。それを超えた場合に制限がかかるので、データを分割するなど工夫しましょう。
形態素に分解された文節が出力されました。
この時エラーで出力出来ないなどあれば、もしかしたらデータ量が多いのかもしれません。
yahooAPIの無料範囲は1分で300回です。それを超えた場合に制限がかかるので、データを分割するなど工夫しましょう。
5,出力結果をみてデータをキレイにする(データクレンジング)
出力された結果を見て、ストップワードや、辞書登録を調整しましょう。
これがとっても大事で、とっても面倒くさいです。形態素解析の弱点とも言えるかもしれません。
*番外編
ちなみにGoogle Workspace利用者の方は、スプレッドシートに自由記述を収集したら、Gemini(右上のキラキラマーク)をクリックして、概要をまとめてもらうだけで、ある程度内容把握できます
これがとっても大事で、とっても面倒くさいです。形態素解析の弱点とも言えるかもしれません。
*番外編
ちなみにGoogle Workspace利用者の方は、スプレッドシートに自由記述を収集したら、Gemini(右上のキラキラマーク)をクリックして、概要をまとめてもらうだけで、ある程度内容把握できます
データクレンジングの重要性
データクレンジングとは、分析に適さないデータやノイズとなるデータを識別し、修正または除去するプロセスです。
形態素解析においては、特に以下の点が重要になります。
1. 不要な記号や文字の除去
句読点、括弧、記号: 文末の句読点(。、!、?)、括弧(()、【】)、記号(~、※、…)などは、形態素解析の際にノイズとなる可能性があります。
・空白、改行: 余分な空白や改行は、解析結果に影響を与える可能性があります。
・HTMLタグ、URL: Webページからデータを収集する場合、HTMLタグやURLが含まれていることがありますが、これらは解析に不要な情報です。
2. 特殊な表現の処理
・絵文字、顔文字: 絵文字や顔文字は、感情分析などに利用できる場合もありますが、基本的にはノイズとして除去することが多いです。
・スラングやネット用語は、一般的な辞書に登録されていないため、正しく解析されない可能性があります。
・略語や誤字脱字は、解析結果の精度を低下させる可能性があります。
3. データの正規化
表記ゆれ: 同じ意味の単語が異なる表記で記述されている場合、統一する必要があります。
例:「美味しい」と「おいしい」、「ユーザ」と「ユーザー」
単位の統一: 数量を表す単語の単位を統一する必要があります。
例:「1000円」と「1,000円」、「5キロ」と「5km」
形態素解析においては、特に以下の点が重要になります。
1. 不要な記号や文字の除去
句読点、括弧、記号: 文末の句読点(。、!、?)、括弧(()、【】)、記号(~、※、…)などは、形態素解析の際にノイズとなる可能性があります。
・空白、改行: 余分な空白や改行は、解析結果に影響を与える可能性があります。
・HTMLタグ、URL: Webページからデータを収集する場合、HTMLタグやURLが含まれていることがありますが、これらは解析に不要な情報です。
2. 特殊な表現の処理
・絵文字、顔文字: 絵文字や顔文字は、感情分析などに利用できる場合もありますが、基本的にはノイズとして除去することが多いです。
・スラングやネット用語は、一般的な辞書に登録されていないため、正しく解析されない可能性があります。
・略語や誤字脱字は、解析結果の精度を低下させる可能性があります。
3. データの正規化
表記ゆれ: 同じ意味の単語が異なる表記で記述されている場合、統一する必要があります。
例:「美味しい」と「おいしい」、「ユーザ」と「ユーザー」
単位の統一: 数量を表す単語の単位を統一する必要があります。
例:「1000円」と「1,000円」、「5キロ」と「5km」
データクレンジングのメリット
★解析精度の向上: ノイズとなるデータを排除することで、形態素解析の精度が向上し、より正確な分析結果を得られます。
★分析効率の向上: 不要なデータを除去することで、処理速度が向上し、分析効率が向上します。
★信頼性の向上: 正確なデータに基づいた分析結果を得ることで、分析の信頼性が向上します。
形態素解析には、前処理が最重要です!
★分析効率の向上: 不要なデータを除去することで、処理速度が向上し、分析効率が向上します。
★信頼性の向上: 正確なデータに基づいた分析結果を得ることで、分析の信頼性が向上します。
形態素解析には、前処理が最重要です!
データクレンジングの方法は色々
★手作業: データ量が少ない場合は、手作業でクレンジングを行うことができます。
★正規表現: 正規表現を用いることで、特定のパターンに一致する文字列を効率的に処理することができます。
★ツール: データクレンジング専用のツールを利用することで、自動的にクレンジングを行うことができます。
★プログラミング: Pythonなどのプログラミング言語を用いることで、より柔軟なクレンジング処理を行うことができます。
今回はデータも少ないので、分離されたくない単語の登録と、いらない単語の登録を手作業でやりました。
★正規表現: 正規表現を用いることで、特定のパターンに一致する文字列を効率的に処理することができます。
★ツール: データクレンジング専用のツールを利用することで、自動的にクレンジングを行うことができます。
★プログラミング: Pythonなどのプログラミング言語を用いることで、より柔軟なクレンジング処理を行うことができます。
今回はデータも少ないので、分離されたくない単語の登録と、いらない単語の登録を手作業でやりました。
データをどうやって使う?

WordCloud
WordCloud(わーどくらうど)とは、出現頻度が高い形態素を文字の大きさで表現することができます。
視覚的にインパクトがあり、データの特徴を直感的に理解するのに役立ちます。
視覚的にインパクトがあり、データの特徴を直感的に理解するのに役立ちます。
以下のスクリプトをノートブックにコピペします。
スプレッドシートのIDと、先ほど出力した形態素があるシート名をコピペします。
スプレッドシートのIDはURLをチェック
https://docs.google.com/spreadsheets/d/(この部分がID)/edit?gid=0#gid=0
スプレッドシートのIDと、先ほど出力した形態素があるシート名をコピペします。
スプレッドシートのIDはURLをチェック
https://docs.google.com/spreadsheets/d/(この部分がID)/edit?gid=0#gid=0
fileId = 'スプレッドシートID' # スプレッドシートIDをコピペ
# ライブラリのインポート
from google.colab import auth
from wordcloud import WordCloud, STOPWORDS # STOPWORDS をインポート
import gspread
from google.oauth2.service_account import Credentials
from google.auth import default
import matplotlib.pyplot as plt
# Google Driveへの接続要求
auth.authenticate_user()
credentials, _ = default()
gc = gspread.authorize(credentials)
# 日本語フォントを取得する
!apt-get -y install fonts-ipafont-gothic
# 対象シート名をリストで管理
sheet_names = ["シート名"]
# 日本語のストップワードを追加、画像の邪魔になりそうな言葉登録
STOPWORDS.update(["の", "に", "と", "が", "した", "による", "での","北海道","札幌","予報","電話","を", \
"てる", "いる", "なる", "れる", "する", "ある", "こと", "これ", "さん", "して", \
"くれる", "やる", "くださる", "そう", "せる", "した", "思う", \
"それ", "ここ", "ちゃん", "くん", "", "て","に","を","は","の", "が", "と", "た", "し", "で", \
"ない", "も", "な", "い", "か", "ので", "よう",\
"です", "ます", "まし", "この", "あの", "その", "どの","という","から","なり","でしょ","ませ", \
"まで","について","なく","なら","ため","でき","します","お","為","なりました","できました","のは",""])
for sheet_name in sheet_names:
# Google SpreadSheetから情報を収集
book = gc.open_by_key(fileId)
shts = book.worksheet(sheet_name)
data = shts.get_values("B2:B") # B2からB列のデータを取得
gotWords = [cell[0] for cell in data if cell]
splitted = ' '.join(gotWords)
# ワードクラウドの作成
fpath = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
wordc = WordCloud(font_path=fpath,
background_color='white',
width = 1600, height = 1200,
repeat = False,
collocations = False,
random_state = 42,
relative_scaling = 0.5, # 頻度調整
contour_width = 0,
colormap = 'bwr', # 好きなデザイン設定
stopwords=STOPWORDS)
wordc.generate(splitted)
# matplotlibでワードクラウドを表示
plt.figure(figsize=(18, 12)) # サイズ調整
plt.imshow(wordc, interpolation='bilinear')
plt.axis("off")
plt.title(sheet_name)
plt.show()
# (オプション) 画像を保存する場合
wordc.to_file(f"wordcloud_{sheet_name}.png")
自分用に編集したら、左上の「▶」をクリックして実行
すると、Googleコラボが「スプレッドシートにアクセスしてもいいですか?」と許可を求めてくるので、許可してあげる。
すると画像が出力されます。出力した画像は、Googleコラボのフォルダに保存されてます。
すると、Googleコラボが「スプレッドシートにアクセスしてもいいですか?」と許可を求めてくるので、許可してあげる。
すると画像が出力されます。出力した画像は、Googleコラボのフォルダに保存されてます。

「難しかった」「欲しかった」という言葉が大きいので、もしかしたら説明が不十分で授業が難しかったと感じている生徒が多かったかもしれませんが、「面白かった」「良かった」という文字も大きいので、難しいなりに楽しめたのかもしれません。
デザインはたくさんあるので「colormap = 'Dark2'」この部分をお好みに設定してください。
デザインをまとめているサイトがあるので、ここで好みを見つけましょう↓↓
https://kristendavis27.medium.com/wordcloud-style-guide-2f348a03a7f8
画像にしてみた時にいらないノイズになる単語がある場合は、「STOPWORDS.update」に単語追加して調整してください。
デザインはたくさんあるので「colormap = 'Dark2'」この部分をお好みに設定してください。
デザインをまとめているサイトがあるので、ここで好みを見つけましょう↓↓
https://kristendavis27.medium.com/wordcloud-style-guide-2f348a03a7f8
画像にしてみた時にいらないノイズになる単語がある場合は、「STOPWORDS.update」に単語追加して調整してください。
NetworkX
NetworkX(ねっとわーくえっくす)を使って共起ネットワークを作成します。
共起ネットワークとは、テキストデータなどに含まれる単語や要素の共起関係(単語の結び付きの強さ)をグラフで表現したものです。共起とは、複数の単語や要素が同じ文や文書中に出現することを指します。
共起ネットワークでは、単語や要素がノード、共起関係がエッジとして表現されます。
★単語や要素の関係性: どの単語や要素が互いに関連性が高いかを視覚的に把握することができます。
★トピックの抽出: 共起関係が強い単語や要素の集まりを分析することで、テキストデータのトピックを抽出することができます。
★情報の可視化: 大量のテキストデータに含まれる情報を、ネットワークという形で分かりやすく可視化することができます。
共起ネットワークとは、テキストデータなどに含まれる単語や要素の共起関係(単語の結び付きの強さ)をグラフで表現したものです。共起とは、複数の単語や要素が同じ文や文書中に出現することを指します。
共起ネットワークでは、単語や要素がノード、共起関係がエッジとして表現されます。
★単語や要素の関係性: どの単語や要素が互いに関連性が高いかを視覚的に把握することができます。
★トピックの抽出: 共起関係が強い単語や要素の集まりを分析することで、テキストデータのトピックを抽出することができます。
★情報の可視化: 大量のテキストデータに含まれる情報を、ネットワークという形で分かりやすく可視化することができます。
先程のワードクラウドと同じように以下のスクリプトをコピペして、自分用に編集。
fileId = 'スプレッドシートのID' # スプレッドシートIDをコピペ
# ライブラリのインポート
from google.colab import auth
import networkx as nx
import gspread
from google.oauth2.service_account import Credentials
from google.auth import default
import matplotlib.pyplot as plt
# japanize_matplotlibのインストール
!pip install japanize_matplotlib
import japanize_matplotlib # 日本語表示に対応
# Google Driveへの接続要求
auth.authenticate_user()
credentials, _ = default()
gc = gspread.authorize(credentials)
# 日本語フォントを取得する
!apt-get -y install fonts-ipafont-gothic
# 対象シート名を明記
sheet_names = ["シート名"]
# 共起ネットワーク図の出力処理
for sheet_name in sheet_names:
# シートを開く
sheet = gc.open_by_key(fileId).worksheet(sheet_name)
# データを取得
data = sheet.get_all_values()
# pandas DataFrame に変換
import pandas as pd
df = pd.DataFrame(data[1:], columns=data[0])
# グラフの作成
G = nx.Graph()
# ノードの追加
threshold_node = 5 # 出現頻度の閾値
for index, row in df.iterrows():
if int(row["出現回数"]) >= threshold_node: # 出現頻度が閾値以上のノードのみ追加
G.add_node(row["文節"], 出現回数=int(row["出現回数"]))
# エッジの追加
threshold_edge = 3 # 共起回数の閾値
for index, row in df.iterrows():
前共起語 = row["共起語(前)"].split("(")
後共起語 = row["共起語(後)"].split("(")
for i in range(len(前共起語) // 2):
if 前共起語[i*2].strip() != "" and row["文節"] in G and 前共起語[i*2].strip() in G: # 両方のノードが存在する場合のみエッジを追加
# weight の値が空文字列の場合は 0 を代入
weight_str = 前共起語[i*2+1][:-2].strip()
weight = int(weight_str) if weight_str else 0
if weight >= threshold_edge: # 共起回数が閾値以上のエッジのみ追加
G.add_edge(row["文節"], 前共起語[i*2].strip(), weight=weight)
for i in range(len(後共起語) // 2):
if 後共起語[i*2].strip() != "" and row["文節"] in G and 後共起語[i*2].strip() in G: # 両方のノードが存在する場合のみエッジを追加
# weight の値が空文字列の場合は 0 を代入
weight_str = 後共起語[i*2+1][:-2].strip()
weight = int(weight_str) if weight_str else 0
if weight >= threshold_edge: # 共起回数が閾値以上のエッジのみ追加
G.add_edge(row["文節"], 後共起語[i*2].strip(), weight=weight)
# グラフの描画
plt.figure(figsize=(10, 10)) # 描画する図のサイズを10x10インチに設定
pos = nx.spring_layout(G, k=0.5) # 数字を大きくすると要素同士の距離が離れる
node_colors = plt.cm.Pastel2(np.linspace(0, 1, len(G.nodes())))
nx.draw(G, pos, with_labels=True, node_size=[v * 100 for v in nx.get_node_attributes(G, '出現回数').values()], font_size=10, font_family='IPAexGothic', node_color=node_colors) # v * 100の数字の部分は要素の円の大きさ。数字を大きくすると円が大きくなる。font_size=10は文字の大きさ。数字を大きくすれば文字が大きくなる。
edge_labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
plt.title(f"{sheet_name} 共起ネットワーク")
plt.savefig(f"{sheet_name}_共起ネットワーク.png", bbox_inches='tight')
plt.show()

実行すると、画像が出力されます。
先生や友達と協力しながら進めることができた様子がうかがえますね。あとは、自分のテーマについて深く考えられたりもしたようです。あとは、難しかったという意見もあるようですね。
先生や友達と協力しながら進めることができた様子がうかがえますね。あとは、自分のテーマについて深く考えられたりもしたようです。あとは、難しかったという意見もあるようですね。
感想
自由記述テキストの集計は大変で、数が多ければ多いほど、一つ一つ読むのは大変です。
そこで、このような解析方法ができれば効率的に解析できると思ったのですが、データクレンジングが大変でした。
クレンジングを何度も繰り返して、純度の高いデータにしてやっと信頼できる解析になるわけですが、あまりにもデータ量が多い場合はデータクレンジングを機械的に出来るようにしたほうがいいかもしれません。今回の場合は100行くらいのデータ数だったので、手動クレンジングでそれなりになりました。
あとは、形態素解析によって出てきた結果は、あくまで全体の傾向を見るものなので、お客様の声ひとつひとつを丁寧に読むことも必要だと思います。全体を見る視点と、単体を見る視点、両方を持って自由記述を扱えたら良いなと思います。
yahooAPI以外にも有名どころだと、Mecab(めかぶ)とかKuromojiとか、いろんな形態素解析ツールがあるので興味がございましたら色々お試しください。
余談ですが、この勉強会の発表時に「ゆる言語学ラジオ」の聴取匂わせ発言をしたところ、なんと身近に用例が2名もいました。「ゆる言語学ラジオ」、その他ゆる学徒系おすすめです。ポッドキャスト&youtubeで好評配信中!
そこで、このような解析方法ができれば効率的に解析できると思ったのですが、データクレンジングが大変でした。
クレンジングを何度も繰り返して、純度の高いデータにしてやっと信頼できる解析になるわけですが、あまりにもデータ量が多い場合はデータクレンジングを機械的に出来るようにしたほうがいいかもしれません。今回の場合は100行くらいのデータ数だったので、手動クレンジングでそれなりになりました。
あとは、形態素解析によって出てきた結果は、あくまで全体の傾向を見るものなので、お客様の声ひとつひとつを丁寧に読むことも必要だと思います。全体を見る視点と、単体を見る視点、両方を持って自由記述を扱えたら良いなと思います。
yahooAPI以外にも有名どころだと、Mecab(めかぶ)とかKuromojiとか、いろんな形態素解析ツールがあるので興味がございましたら色々お試しください。
余談ですが、この勉強会の発表時に「ゆる言語学ラジオ」の聴取匂わせ発言をしたところ、なんと身近に用例が2名もいました。「ゆる言語学ラジオ」、その他ゆる学徒系おすすめです。ポッドキャスト&youtubeで好評配信中!
たくさんの先駆者様ありがとうございました!(参考サイト)
https://prtn-life.com/blog/gas-yahoo-api
https://note.com/yokoe817/n/n84a6885232ec
https://www.issoh.co.jp/column/details/4146/
https://www.stat.go.jp/teacher/dl/pdf/c3learn/materials/third/dai1.pdf
https://engineerblog.mynavi.jp/technology/nlp_stopword/
https://qiita.com/yosasou/items/3a0f45fd3327519f7fd4
https://kristendavis27.medium.com/wordcloud-style-guide-2f348a03a7f8
https://tool.konisimple.net/text/hinshi_keitaiso
https://note.com/noa813/n/ne506f884e467
https://note.com/yokoe817/n/n84a6885232ec
https://www.issoh.co.jp/column/details/4146/
https://www.stat.go.jp/teacher/dl/pdf/c3learn/materials/third/dai1.pdf
https://engineerblog.mynavi.jp/technology/nlp_stopword/
https://qiita.com/yosasou/items/3a0f45fd3327519f7fd4
https://kristendavis27.medium.com/wordcloud-style-guide-2f348a03a7f8
https://tool.konisimple.net/text/hinshi_keitaiso
https://note.com/noa813/n/ne506f884e467