WordCloudをHTML出力し、各単語にリンクを設定する

このブログで何度かワードクラウドを作って来ましたが、ちょっとした興味からそれを画像ではなくHTML出力したいと思いやってみました。
そして、単にHTML出力をするだけでなく、ワードクラウド中の単語をクリックしたら単語ごとに指定したURLにジャンプできるようにします。

この記事のサンプルではその単語でのGoogle検索結果へアクセスできるようにします。ワードクラウド自体はいつものライブドアニュースコーパスのデータで作ります。
参考: livedoorニュースコーパスのファイルをデータフレームにまとめる

実はいつも使っているワードクラウドのライブラリ(wordcloud)には、to_html()というメソッドが形だけあって、中身が実装されていないという状態でした。僕はこれが実装されるのを待っていたのですが、残念ながら最近の更新でこのメソッド自体が消されてしまいました。
参考: remove empty to_html in favor of to_svg (#607) · amueller/word_cloud@be9bb5e · GitHub

HTML出力機能はもう実装されないんだと思い、wordcloudのオブジェクトが持っているlayout_プロパティ(単語や配置場所、色などがまとまった配列)を読み解いて、自分でHTMLを作るコードを書くしかないと思ってコーディングに着手していました。
しかし改めてライブラリのコードをよく見るとto_svg()なるメソッドを持っているではないですか。これを使えばSVGで書き出せるので、簡単にHTMLに埋め込めます。
参考: wordcloud.WordCloud — wordcloud 1.8.1 documentation

SVGで書き出せたらあとはリンクを貼るだけなので簡単です。textタグたちをaタグで囲むだけです。ただ、今回はJavaScriptの addEventListener で、click したら指定のURLを開く関数をセットしました。

前置きが長くなって来たので、ここからやってみます。まずは普通にワードクラウドを作ります。

import re
import subprocess
import unicodedata
import pandas as pd
import MeCab
from wordcloud import WordCloud


# データの読み込み
df = pd.read_csv("./livedoor_news_corpus.csv")
# ユニコード正規化とアルファベットの小文字統一
df.text = df.text.str.normalize("NFKC").str.lower()
# 改行コードを取り除く
df.text = df.text.str.replace("\n", " ")
# エラーになる文字があるので取り除く (ライブドアニュースコーパス使う場合だけの処理。普通は不要)
df.text = df.text.str.replace("\u2028", "")

# 辞書のパス取得
dicdir = subprocess.run(["mecab-config", "--dicdir"], capture_output=True, text=True).stdout.strip()
tagger = MeCab.Tagger(f"-d {dicdir}/ipadic")

# ひらがなのみの文字列にマッチする正規表現
kana_re = re.compile("^[ぁ-ゖ]+$")

# 分かち書きして、ひらがな以外を含む 名詞/動詞/形容詞 を返す関数
def mecab_tokenizer(text):
    # 分かち書き
    parsed_lines = tagger.parse(text).splitlines()[:-1]
    surfaces = [l.split('\t')[0] for l in parsed_lines]
    features = [l.split('\t')[1] for l in parsed_lines]
    # 原型を取得
    bases = [f.split(',')[6] for f in features]
    # 品詞を取得
    pos = [f.split(',')[0] for f in features]
    # 各単語を原型に変換する
    token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
    # 名詞/動詞/形容詞に絞り込み
    target_pos = ["名詞", "動詞", "形容詞"]
    token_list = [t for t, p in zip(token_list, pos) if (p in target_pos)]
    # ひらがなのみの単語を除く
    token_list = [t for t in token_list if not kana_re.match(t)]
    # アルファベットを小文字に統一
    token_list = [t.lower() for t in token_list]
    # 半角スペースを挟んで結合する。
    result = " ".join(token_list)
    # 念のためもう一度ユニコード正規化
    result = unicodedata.normalize("NFKC", result)
    return result


# 形態素解析
df["tokens"] = df.text.apply(mecab_tokenizer)
# wordcloud入力データ用に連結する
text_data = " ".join(df["tokens"])

# ワードクラウドのオブジェクト生成
wc = WordCloud(
        font_path="/Library/Fonts/ipaexg.ttf",  # 日本語フォントファイル
        width=600,  # 幅
        height=400,  # 高さ
        prefer_horizontal=0.9,  # 横書きで配置することを試す確率 (デフォルト0.9)
        background_color='white',  # 背景色
        include_numbers=False,  # 数値だけの単語を含まない
        colormap='tab20',  # 文字色のカラーマップ指定
        regexp=r"\w{2,}",  # 2文字以上の単語のみ含む
        relative_scaling=1,  # 頻度のみで文字サイズを決める
        collocations=False,  # bi-gramを考慮しない
        max_font_size=60,  # 最大フォントサイズ
        random_state=42,  # 乱数の初期値
    ).generate(text_data)

# この時点で作成できたwordcloudを確認する場合は以下の関数を実行。 
wc.to_image()
# 出力省略

さて、これでワードクラウドができました。これを to_svg() で SVGに出力するとどうなるか見てみましょう。

# to_svg() で svgの文字列を生成できる
print(wc.to_svg())
"""
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="400">
<style>text{font-family:'IPAexGothic';font-weight:normal;font-style:normal;}</style>
<rect width="100%" height="100%" style="fill:white"></rect>
<text transform="translate(342,63)" font-size="60" style="fill:rgb(127, 127, 127)">思う</text>
<text transform="translate(391,134)" font-size="44" style="fill:rgb(127, 127, 127)">映画</text>
<text transform="translate(239,359)" font-size="41" style="fill:rgb(174, 199, 232)">自分</text>
<text transform="translate(458,55)" font-size="39" style="fill:rgb(174, 199, 232)">記事</text>

 # 中略
 
<text transform="translate(411,86)" font-size="12" style="fill:rgb(158, 218, 229)">一番</text>
<text transform="translate(408,50)" font-size="12" style="fill:rgb(23, 190, 207)">転職</text>
<text transform="translate(58,277)" font-size="12" style="fill:rgb(255, 152, 150)">得る</text>
</svg>
"""

引数にファイル名の指定などはできず、SVGタグで囲まれた文字列を返してくれるようです。
これを .svg の拡張子で保存すると、ワードクラウドのSVGファイルが出来上がります。
先頭に、”<!DOCTYPE HTML>”をくっつけて、.htmlファイルで保存してもよいでしょう。
文字列にリンクを設定するなどの要件が無いのであれば、これをファイルに保存して完成です。

ここからリンクを設定していきます。リンク先は今回はお試しなので、Google検索結果にします。URLは、 https://www.google.com/search?q={検索キーワード} です。

前の方でも書きましたが、リンクっていう観点だとaタグで囲むのがセオリーですし、テキストを加工するスクリプトを描くのもそんなに難しくありません。ただ、文字列の加工の総量を少なめにしたいのと、イベントリスナーを使う方法の方がページ遷移させる以外の用途への応用も多いと思うので、こちらを採用しました。

Google検索結果のURLの検索キーワードはパーセントエンコーディングする必要があるので、JavaScriptのencodeURI関数を使います。僕はあまりJavaScript得意ではないので自信がないですが、次のようなコードでいかがでしょうか。
(無理矢理PythonでJavaScriptを書き出してますが、一旦SVGを書き出してテキストエディタでスクリプトを書き込んでも良いと思います。)

# textタグたちに、クリック時にGoogle検索結果を開くイベントリスナーを追加するJavaScript
link_script = """
<script>
    svg = document.getElementsByTagName("svg")[0];
    text_tags =  svg.getElementsByTagName("text")
    for(var i=0; i<text_tags.length; i++){
        text_tags[i].addEventListener(
            "click",
            function(){
                word = this.textContent;
                word_uri = encodeURI(word);
                url = "https://www.google.com/search?q=" + word_uri;
                window.open(url, "_bkank");
            }
        )
    }
</script>"""

# HTMLファイルに書き出し
with open("word_cloud.html", "w") as f:
    f.write("<!DOCTYPE HTML>\n")
    f.write(wc.to_svg())
    f.write(link_script)

出力されるhtmlファイルが次です。

ブログに公開できるデータということで、無難なデータと無難なリンク先で作っていますが、これはデータと使い方によっては結構面白いものを作れる可能性を感じませんか?

pyvisでネットワーク可視化

このブログではネットワーク(グラフ)の可視化にはNetworkX(+Matplotlib)やGraphvizを使って来たのですが、最近pyvisというライブラリを気に入っていてよく使うようになりました。
訪問者の方にもぜひ触ってみていただきたいので軽く紹介していきます。

まず、どんなものが描けるのかの例です。このpyvisはvis.jsというJavaScriptのライブラリのPythonラッパーになっています。 そのため、vis.jsのドキュメントの examples ページを見るとさまざまな可視化例が載っています。こんなのが作れるんだ!ってモチベーションが上がるので一度見ていただけると幸いです。(実はこのvis.jsはネットワークの可視化以外の機能も持っているのですが、もっぱら僕の関心はネットワークの可視化にあります。)
参考: Vis Network Examples

各サンプルなのですが、それぞれのサンプル個別のページのアクセスして触っていただけると分かる通り、このライブラリで描いたネットワーク図はノードをマウスでドラッグして動かせます。これが1番のお勧めポイントです。また、単一のHTMLファイルで結果が出力されるので仕事で可視化した場合は結果は他の人に共有しやすいというのもいいですね。

それでは早速、使ってみましょう。pyvis 自体のドキュメントはこちらですが、母体のvis.jsのドキュメントや、Examplesのソースコードなども目を通されると理解が深まると思います。

本当に適当に作ってみました。インスタンス作って、ノードとエッジを追加して、最後HTML出力のメソッドを呼び出して完成です。

from pyvis.network import Network


# ネットワークのインスタンス生成
network = Network(
    height="300px",  # デフォルト "500px"
    width="400px",  # デフォルト "500px"
    notebook=True,  # これをTrueにしておくとjupyter上で結果が見れる
    bgcolor='#ffffff',  # 背景色。デフォルト "#ffffff"
    directed=True,  # Trueにすると有向グラフ。デフォルトはFalseで無向グラフ
)

# add_node でノードを追加
# label は省略可能。省略するとidと同じになる。
network.add_node(n_id=1, label=1, shape="circle")
network.add_node(n_id=2, label=2, shape="box", color="green")
network.add_node(n_id=3, label=3, shape="triangle")
network.add_node(n_id=4, label=4)  # shape を省略すると、 shape="dot"と同じになる

# add_edge でエッジを追加
network.add_edge(1, 2,)
network.add_edge(2, 4, width=2)  # width で太さを変えられる
network.add_edge(3, 4, smooth="dynamic")  # smooth を指定することで、エッジを曲線にできる
network.add_edge(4, 3, smooth="dynamic")  # エッジを曲線にすると双方向のエッジを別の線にできる。(直線だと重なる)

# 指定したファイル名でHTMLを出力。
network.show("pyvis_sample1.html")

出来上がりはこんな感じです。

動かせるっていいですね。

ソースコード中にコメントをいろいろ書いてますが、ノードもエッジも細かい設定がたくさんできます。pyvisのドキュメントだとこのページが該当するのですが、そこまで丁寧ではないので、vis.jsの方のドキュメントを読み込んで、そこから類推して使った方が理解が早いかもしれません。(場合によっては出力されたHTMLファイルを直接編集するとか。)
vis.js の方のドキュメントでは以下のページに設定可能な項目がまとまっています。
vis.js – Nodes documentation.
vis.js – Edges documentation.

もう一つ、HTML出力前に network.show_buttons() ってメソッドを実行しておくと、大量のコントロールボタンがHTMLに出力されます。(jupyterでは表示されないので、別途ブラウザでHTMLファイルを開いてください。)
これを使うと、ノードやエッジのオプションやノード配置の計算に使われている力学の設定などをインタラクティブに変更できます。

# ネットワークの作成までのコードは共通
network.show_buttons()  # 各種ボタンの表示
network.show("pyvis_sample2.html")

これも楽しいので試してみてください。(ただ、楽しいだけで今のところ有効な使い方を見出せずすみません。) ボタン多すぎ、って場合は、ドキュメントにある通り、filter_ って引数で絞り込めます。

最後に、vis.js から pyvis に移行した人への注意が一つあります。(僕はこのパターン。当初直接HTMLでvis.jsファイル書いてました。)
それは、 node の shape のデフォルト値で、 HTML/JavaScriptで書いてる場合、shapeを何も指定しなかった場合の挙動は”ellipse”です。しかし、pyvis は add_nodeでshape 引数を省略すると、出力されるJavaScirptに勝手に “shape”: “dot” をつけます。

何も指定せずに超単純なネットワークを描いたときの挙動が違うので僕は結構戸惑いました。
(ellipseは図の内側にラベルが入り、dotはラベルが外に出るという違いがちょっと気に入らなかった。その辺の話も、上記の Nodes documentation に書いてあります。)

PythonでWikipediaの情報を取得する

仕事で諸事情あり、Wikipediaの情報を取得したいことがあったのでその方法を紹介します。
最初はrequestsなどのHTTPクライアントでデータを取ってきて頑張ってHTMLをパースしようかと思っていたのですが、実はPythonでWikipediaを利用するには専用のライブラリが公開されています。それがこちらの wikipedia です。 PyWikipediaとかじゃなくて そのままの名前なのですね。
参考: PyPIのページ wikipedia · PyPI
ドキュメント Wikipedia — wikipedia 0.9 documentation

どうやら、Wikipedia本体がそのそも、Media Wiki API というAPIを公開してくれていて、それをラップしているようです。

APIにはかなり多様な機能が実装されているようですが、いったん僕の用途としては用語の検索と、該当ページのコンテンツの取得の二つができれば良いのでその点に絞って使い方を紹介していきます。

一番シンプルな使い方は Quickstart ページを見るのが良いでしょう。
とりあえず、適当な単語で検索してみます。僕たちは日本語を使うので、最初に言語を設定します。(設定しない場合はデフォルトの言語は英語で、日本語の単語を検索したとしても英語版Wikipedia内で検索されます。)

import wikipedia


wikipedia.set_lang("ja")
print(wikipedia.search("データ分析"))
"""
['データ解析',
 'データ',
 '慶應義塾大学パネルデータ設計・解析センター',
 'データベース',
 'データ・クラスタリング',
 'ビッグデータ',
 '主成分分析',
 '分散分析',
 'NTTデータ',
 '精神分析学']
"""

言語を設定した後、「データ分析」で検索してマッチしたページのタイトルの一覧が取得できましたね。(実際のWikipediaで検索すると、一番目の候補のデータ解析のページにリダイレクトされます。)

set_lang() で設定できる略号(上の例で言えばja)と言語(上の例では日本語)の対応は、languages()ってメソッドで一覧が取れます

print(wikipedia.languages())
"""
{'aa': 'Qafár af',
 'ab': 'аԥсшәа',
 'abs': 'bahasa ambon',
 'ace': 'Acèh',
 'ady': 'адыгабзэ',
 'ady-cyrl': 'адыгабзэ',
 'aeb': 'تونسي/Tûnsî',
 'aeb-arab': 'تونسي',
 # 以下略
"""

さて、先ほどのsearchの結果、「データ解析」ってページがあることがわかったので具体的にそのページのコンテンツを取得してみましょう。それには、page というメソッドを使います。結果は、WikipediaPageというオブジェクトで取得でき、タイトルや中身をその属性として持ちます。とりあえず、タイトルとサマリーを表示してみます。

wp = wikipedia.page("データ解析")

print(wp.title)
# データ解析

print(wp.summary)
"""
データ解析(データかいせき、英: data analysis)は、データ分析(データぶんせき)とも呼ばれ、
有用な情報を発見し、結論を報告し、意思決定を支援することを目的として、データを検査し、
クリーニングや変換を経て、モデル化する一連のプロセスである。データ解析には多数の側面とアプローチがあり、
色々な名称のもとで多様な手法を包含し、ビジネス、科学、社会科学のさまざまな領域で用いられている。
今日のビジネス界において、データ解析は、より科学的な意思決定を行い、ビジネスの効率的な運営に貢献する役割を担っている。
データマイニングは、(純粋な記述的な目的ではなく)予測的な目的で統計的モデリングと
知識獲得に重点を置いた固有のデータ解析技術である。
これに対し、ビジネスインテリジェンスは、主にビジネス情報に重点を置いて、集計に大きく依存するデータ解析を対象としている。
統計学的な用途では、データ解析は記述統計学 (en:英語版) 、探査的データ解析(EDA)、確認的データ解析(仮説検定)(CDA)に分けられる。
EDAはデータの新たな特徴を発見することに重点を置き、CDAは既存の仮説の確認または反証に焦点を当てる。
予測分析は、予測的な発生予報あるいは分類のための統計モデルの応用に重点を置き、
テキスト分析は、統計的、言語的、および構造的な手法を用いて、非構造化データの一種であるテキストデータから情報を抽出し知識の発見や分類を行う。
上記はどれも、データ解析の一種である。
データ統合はデータ解析の前段階であり、データ可視化およびデータ配布はデータ解析と密接に関連している。
"""

ページのコンテンツ全体は、content プロパティに持っていて、またhtml()メソッドを使うと、そのページのHTML全体を取得することもできます。

wp.content  # コンテンツ全体
wp.html()  # HTMLが取得できる

どちらも結果が非常に大きいので出力は省略します。

このほか、そのページのurl (wp.url)やリンクされている他の単語の一覧(wp.links)などの属性も持っています。どんな属性があるのかの一覧はドキュメントの、class wikipedia.WikipediaPage の部分を見ていただくのが良いと思います。

上記では、まず、wikipedia.page() で ページオブジェクトを取得して、その後そこからsummaryを取得しましたが、実は summaryを直接取得することもできます。それは単に、 wikipedia.summary()を使うだけです。(contentにはこれはないんですよね。summary専用の機能と考えて良さそうです。)

# summaryは直接取得できる
print(wikipedia.summary("データ解析"))
"""
データ解析(データかいせき、英: data analysis)は、 # 長いので以下略
"""

# 実はデータ分析、で検索しても勝手にリダイレクトしてデータ解析の結果を返してくれる
print(wikipedia.summary("データ分析"))
"""
データ解析(データかいせき、英: data analysis)は、 # 長いので以下略
"""

これで非常に簡単にWikipediaのデータが使えるようになりました。
ただ、一点気をつけないといけないことがあります。それは、検索すると曖昧さ回避のページに飛ぶような単語で検索する場合です。例えば、地名、人名、企業名、等々で使われる「豊田」でやってみると、DisambiguationError という例外が発生します。ライブラリが例外オブジェクトを作ってくれているのでそれでキャッチできます。

try: 
    wp = wikipedia.page("豊田")
except wikipedia.exceptions.DisambiguationError as e:
    print(e)
    
"""
"豊田" may refer to: 
豊田市
豊田町 (曖昧さ回避)
豊田町 (山口県)
豊田町 (静岡県)
豊田村 (曖昧さ回避)
豊田郡 (曖昧さ回避)
豊田郡
豊田 (名古屋市)
豊田地区
豊田 (日野市)
豊田 (紀の川市)
豊田鎮 (通遼市)
豊田鎮 (南靖県)
豊田郷 (彰武県)
豊田郷 (新寧県)
フォンディエン県 (トゥアティエン=フエ省)
フォンディエン県 (カントー)
豊田駅
豊田駅 (北海道)
豊田駅 (花蓮県)
新豊田駅
新豊田駅
三河豊田駅
豊田市駅
上豊田駅
肥後豊田駅
豊田町駅
豊田本町駅
トヨタグループ
トヨタ自動車
豊田自動織機
トヨタ紡織
豊田通商
豊田中央研究所
豊田合成
豊田鉄工
豊田スタジアム
豊田 (飲食業)
豊田エリー
豊田清
豊田順子
豊田孝治
豊田真由子
豊田萌絵
豊田泰光
豊田穣
豊田陽平
豊田ルナ
豊田佐吉
豊田喜一郎
豊田英二
豊田章男
豊田インターチェンジ
豊田市#道路
豊田 (小惑星)
豊田館跡
豊田ナンバー
豊田工業大学
^
「豊田」で始まるページの一覧
"""

用途が多様な単語で使う場合は気をつけるようにしましょう。

Jupyterのウィジェットを使ったアノテーションツール

前回の記事で紹介したJupyter Notebookのウィジェット(ボタン)の簡単な応用事例です。
参考: Jupyter Notebook でボタンを使う

この記事でいくつか事例を紹介しようと思いますが、まずは機械学習等でテキストデータの分類モデルをつくつる時の教師データ作成(アノテーション)のツールを作ってみようと思います。
何かしらテキストデータを受け取って、事前に定義されたいくつかのカテゴリ(ラベル)の中から1個選んで、「これはカテゴリ0だ」とか「こいつはカテゴリ3だ」みたいなのを記録してくツールですね。

とりあえず、使うデータの準備と必要なライブラリを読み込んでおきます。サンプルなのでテキストの内容自体は適当です。

import pandas as pd
import ipywidgets as widgets
from IPython.display import display


# サンプルとして適当なテキストの集合を作る
df = pd.DataFrame(
    {
        "text": [f"ラベル付されるテキストその{i:03d}" for i in range(100)],
        "label": None
    }
)
print(df.head())
"""
               text label
0  ラベル付されるテキストその000  None
1  ラベル付されるテキストその001  None
2  ラベル付されるテキストその002  None
3  ラベル付されるテキストその003  None
4  ラベル付されるテキストその004  None
"""

前回の記事ではあまり名前空間のことを考えずにいろいろ書いていましたが、各種ウィジェットがグローバルスコープにあると予期せぬ挙動につながったりもするので、今回はアノテーションのクラスを定義します。
コードはやや長いですがやっていることはシンプルで、渡されたラベルの数だけボタンを作り、データフレーム内のテキストデータを1つ表示し、ボタンが押されたらボタンに対応したlabelのidをデータフレームに書き込んで、また次のテキストを表示するというそれだけです。
そして、全データ処理し終わった時に、ボタンを非活性化する処理も入れています。

今回はサンプルデータが適当で、データ中に何番目のテキストなのか入ってますが、それとは別に i 番目のデータだって表示する機能も入れています。実務では何かしら進捗がわかる機能をつけた方が良いです。どこまで進んだかわからない長いアノテーションは心が折れます。

class annotation():

    def __init__(self, df, labels):
        self.data = df
        self.i = 0  # 何番目のデータをアノテーションしているかのカウンタ
        self.buttons = [widgets.Button(description=label) for label in labels]
        self.output = widgets.Output()

        # ボタンにvalueプロパティを持たせておく
        for j in range(len(self.buttons)):
            self.buttons[j].value = j

        # ボタンにクリック寺の処理を追加
        for button in self.buttons:
            button.on_click(self.select_label)

        # ツールの初期表示実行
        self.display_tools()

    def display_tools(self):
        # ボタンや出力領域の初期表示
        self.hbox = widgets.HBox(self.buttons)
        self.output.clear_output()
        display(self.hbox, self.output)
        with self.output:
            print(f"{self.i}番目のデータ:\n")
            print(self.data.iloc[self.i]["text"])

    def select_label(self, button):
        # ボタンを押した時の処理
        # データフレームにセットされたボタンのvalueを書き込む
        self.data.iloc[self.i]["label"] = button.value
        # 次のデータに移行
        self.i += 1
        # 次のデータを表示
        self.output.clear_output(True)

        # 全データ処理し終わったら完了
        if self.i >= len(self.data):
            with self.output:
                print("完了!")
                for button in self.buttons:
                    button.disabled = True  # ボタンを非活性化
            return

        with self.output:
            print(f"{self.i}番目のデータ:\n")
            print(self.data.iloc[self.i]["text"])

上で作ったクラスは次のように使います。

labels = ["カテゴリー0", "カテゴリー1", "カテゴリー2", "カテゴリー3"]
tool = annotation(df, labels)

こうすると次の画像のようにボタンが表示され、クリックするごとにデータフレームに値が書き込まれていきます。

適当のぽちぽちとボタンを押していくと、DataFrameに次のように値が入ります。

print(df.head())
"""
               text label
0  ラベル付されるテキストその000     1
1  ラベル付されるテキストその001     2
2  ラベル付されるテキストその002     0
3  ラベル付されるテキストその003     3
4  ラベル付されるテキストその004     0
"""

ボタンの配置をもっと整えたり、スキップボタン、戻るボタンの実装などいろいろカスタマイズは考えられますが、一旦これで最低限の役割は果たせると思います。

次にもう一つ、MeCabのユーザー辞書作成の補助ツールを作っておきます。
これは、ボタンを押したらそのボタンに紐づいたラベルが記録されるのではなく、
ドロップダウンで品詞情報を選び、さらに、原形、読み、発音を手入力で入れて、確定ボタンを押したら単語辞書作成用の配列に結果格納するというものです。

今回の記事はお試しなので、追加する単語は適当にピックアップした数個ですが本来はそのリストを作らないといけないので、こちらの記事を参考にしてください。
参考: gensimでフレーズ抽出
また、アウトプットのMeCabユーザー辞書の書式はこちらです。
参考: MeCabでユーザー辞書を作って単語を追加する

ではやってみます。今回も各種パーツを一つのクラスにまとめます。

class create_dictionaly():
    def __init__(self, new_words):
        self.data = new_words  # 辞書に追加する候補のワード
        self.i = 0  # カウンタ
        self.results = []  # 結果格納用の配列

        # 確定とスキップの2種類のボタンを用意する
        self.decision_button = widgets.Button(description="確定")
        self.decision_button.on_click(self.decision_click)
        self.skip_button = widgets.Button(description="スキップ")
        self.skip_button.on_click(self.skip_click)

        # 品詞の選択機能(サンプルコードなのでIPA辞書の品詞の一部だけ実装)
        self.pos_list = [
            "名詞,一般,*,*,*,*,*",
            "名詞,固有名詞,一般,*,*,*,*",
            "名詞,サ変接続,*,*,*,*,*",
            "名詞,ナイ形容詞語幹,*,*,*,*,*",
            "名詞,形容動詞語幹,*,*,*,*,*",
            "名詞,固有名詞,人名,一般,*,*,*",
            "名詞,固有名詞,人名,姓,*,*,*",
            "名詞,固有名詞,人名,名,*,*,*",
            "名詞,固有名詞,組織,*,*,*,*",
        ]
        self.pos_select = widgets.Dropdown(options=self.pos_list)

        # 原形, 読み, 発音を設定する項目
        self.base = widgets.Text(description="原形: ")
        self.reading = widgets.Text(description="読み: ")
        self.pronunciation = widgets.Text(description="発音: ")

        # 次の単語候補表示場所
        self.new_word = widgets.Output()

        self.display_tools()

    def display_tools(self):
        # ボタンや出力領域の初期表示
        self.text_hbox = widgets.HBox(
            [self.base, self.reading, self.pronunciation])
        self.button_hbox = widgets.HBox(
            [self.decision_button, self.skip_button])
        display(self.new_word, self.text_hbox,
                self.pos_select, self.button_hbox)
        # 最初の単語を表示しておく
        self.next_word()

    def next_word(self):
        # 次の単語の表示
        # 全データ処理し終わったら完了
        if self.i >= len(self.data):
            self.new_word.clear_output(True)
            with self.new_word:
                print("完了!")
                self.decision_button.disabled = True
                self.skip_button.disabled = True
            return

        self.word = self.data[self.i]

        self.new_word.clear_output(True)

        with self.new_word:
            print(self.word)

        self.base.value = self.word
        self.reading.value = ""
        self.pronunciation.value = ""
        self.pos_select.value = self.pos_list[0]
        self.i += 1

    def decision_click(self, button):
        # 確定ボタンクリック
        # MeCabユーザー辞書の形式のテキストを生成
        result_text = f"{self.word},,,,{self.pos_select.value},"
        result_text += f"{self.base.value},{self.reading.value},{self.pronunciation.value}"
        # 結果の一覧に格納
        self.results.append(result_text)
        # 次の単語表示
        self.next_word()

    def skip_click(self, button):
        # スキップボタンクリック
        # 次の単語表示
        self.next_word()

実行は次のコードです。

cd_tool = create_dictionaly(["スマホ", "クラウド", "ガジェット", "インターフェース", "ブログ"])

このように、単語候補が出現し、原形(一旦そのままの値で補完)と読み、発音を入力して、品詞を選んで確定を押すと辞書用のデータが記録されていく仕組みになっています。

結果は、cd_tool.result に入ってます。

print("\n".join(cd_tool.results))
"""
スマホ,,,,名詞,一般,*,*,*,*,*,スマホ,スマホ,スマホ
クラウド,,,,名詞,一般,*,*,*,*,*,クラウド,クラウド,クラウド
ガジェット,,,,名詞,一般,*,*,*,*,*,ガジェット,ガジェット,ガジェット
インターフェース,,,,名詞,一般,*,*,*,*,*,インターフェース,インターフェース,インターフェース
ブログ,,,,名詞,一般,*,*,*,*,*,ブログ,ブログ,ブログ
"""

これをテキストファイルに書き出せば、ユーザー辞書のseedデータになります。

原形/読み/発音はテキストボックスを一つにまとめて自分でカンマを打つ方が早いかもと思ったり、全体的に配置のデザインがイケて無いなとか思うところはあるのですが、この先徐々に改良していきたいと思います。

以上の二つの例で、ボタンの結果をそのまま記録していくパターン、何かしらのウィジェットでデータを入力してボタンを押して確定するパターンの二つを紹介できたので、ラベル付のタスクであれば、これらの組み合わせで大抵は対応できるのではないでしょうか。