non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

Juliaで日本語のワードクラウドをやってみた

おはようございます。次男は、このところ、就寝時もオムツを卒業してパンツで寝ていますが、たまぁに失敗しちゃいます@kjunichiです。

まえがき

ちょっと前にCSV.jlでゼータ関数のクリティカルライン上のプロットを 良い感じに出来たのに気をよくして

Juliaでファイルを読み込んで処理するというテーマのもと、今回は 長年自分でやってみたかったこれまでの自分のツイートからのワードクラウドの生成を Juliaでチャレンジしたが、いろいろ罠にはまった。

CSV.jlだと完全には読み込めなかった

集計作業まで行けたのだが、集計結果をながめると 古いツイートがどうも反映されていないと、気になり、よくよく中身を確認すると 途中で読み込みが途切れていた。

そこで、素のJuliaでテキストファイルとみなして読み込むことで対処した。

前処理として、ツイッターから取得した全ツイートのcsvファイルからtext列のみを 抽出したテキストをGoogleスプレッドシートで作成。 (しょっぱなからJulia以外の処理系を使っているという。。)

f = open("Dropbox/Untitled\ spreadsheet\ -\ Sheet1.csv")
text = readlines(f);

Juliaでワードカウント

準備

日本語なので、分かち書きが必要になるが、定番のMeCabがJuliaにも用意されており、 これを使った。

MeCab本体のインストールが必要だが、何度かこの手の日本語テキスト処理にチャレンジしようと していたので、すでにインストールしていた。

Windows環境では数年前は64ビット版を用意すうには手動でパッチを当てる必要があったが、 最近はpipで一発で入るパッチ済みのパッケージが用意されているのを見かけたりするので、 ちょっと調べれば、昔のように手動でパッチせずとも64ビット版のMeCabを用意できるはず。

Juliaでの処理

先ほどのMeCab.jlで分かち書きを行い、結果を配列に詰め直している。 (この辺り、もっとJuliaらしい書き方や処理方法がありそうな気が。。)

using MeCab

mecab=Mecab()

words=[]
for i in text
  res = parse_surface(mecab,string(i))
  for j in res
    push!(words, j)
  end
end

Dict型を使ってワードカウント。

counts = Dict{String, Int}()
for word in words
    counts[word] = get(counts, word, 0) + 1
end

カウント結果をソートして、1語の単語を除外。

res = sort(collect(counts), by = tuple -> last(tuple), rev=true)
res2=[]
for i in res
    if(length(i[1])>1 && i[2]>10)
        #println(i[1],i[2])
        item = Dict("word" =>i[1],"count"=>i[2])
        push!(res2,item)
    end
end

Vega.jlでワードクラウドと思いきや

Vega.jlでワードクラウドがサポートされており、JuliaはString型がUTF-8の文字列なので、 2バイトの文字化けと言った20世紀的な問題には悩むわけないと。

が、結果は、真っ白。

エラー解析

仕方ないので、Vega.jlのGithubページよりコードを読んで、 内部でJupyterに出力している箇所を見つけて、そこから、jsを切り出して、ウェブページとして ブラウザで動かして、2バイト文字があるとVegaのレベルでJavaScriptエラーでNG

Juliaから直接D3.jsでwordcloud

頑張ってVega.jlのwordcloudを日本語対応する作戦もなくもないが、 内部でD3.jsを使っていることが分かり、かつD3.jsのワードクラウドライブラリを直接使うなら、 日本語表示に問題なさそうだったので、JuliaというかJupyterから直接D3.jsでワードクラウドを描画する 事にした。

using JSON

function wordcloud(res)
divid = "d3" * randstring(3)
spec = JSON.json(res)

display("text/html", """
<body>
    <svg id=\ "$divid\" width="800" heigth="800"></svg>

    <script type="text/javascript">
        require.config({
            paths: {
                d3: "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min",
                cloud: "https://cdnjs.cloudflare.com/ajax/libs/d3-cloud/1.2.5/d3.layout.cloud.min",

            }
        });
        require(["d3", "cloud"], (d3, cloud) => {
            window.d3 = d3;
            window.d3.layout.cloud = cloud;

            
            const data = $spec
        
            const countMax = d3.max(data, (d) => {
                    return d.count
            })
            const sizeScale = d3.scale.linear().domain([0, countMax]).range([10, 100])
            colorScale = d3.scale.category20c()
            const words = data.map((d) => {
                return {
                    text: d.word,
                    size: sizeScale(d.count) //頻出カウントを文字サイズに反映
                };
            });
            
            d3.layout.cloud().size([800, 800])
                .words(words)
                .rotate( ()=>{return ((1.2 - Math.random())|0)*90})
                .fontSize(function(d) { return d.size; })
                .on("end", draw)
                .start();
        
            function draw(words) {
                d3.selectAll('text').remove();
                d3.select("#$divid")
                .attr({"width": 800, "height": 800})
                .append("g")
                // without the transform, words words would get cutoff to the left and top, they would
                // appear outside of the SVG area
                .attr("transform", "translate(400,400)")
                .selectAll("text")
                .data(words)
                .enter().append("text")
                .style("font-size", function(d) { return d.size + "px" })
                .style("fill", function(d, i) { return colorScale(i); })
                .attr({
                    "text-anchor": "middle",
                    "transform": function(d) {
                        return "translate( " +[d.x, d.y] + ")rotate( "+ d.rotate + ")"
                    }
                })
                .text(function(d) { return d.text; });
            }
        
        
        })
    </script>
</body>
    """)
end

結果

URLが分解されてしまい、httpや://が上位に表示さてしまったり、改善点はあるが、 まえからやりたかった自前のワードクラウド生成が出来たのでひとまずは満足。

f:id:kjw_junichi:20180624102108p:plain

学べたこと

  • Juliaでテキストファイルとして一気に読み込む方法
  • UTF8Stringは0.4までで、0.5以降はString型に統一された
  • ストップワード設定しないと結果が微妙
  • Jupyterではどうやらrequire.jsが使われているっぽい
  • JuliaでJupyterにJavaScriptを送りつける方法

参考資料

関連記事

7年前の記事

1年前の記事