決定木の可視化ライブラリ dtreeviz を conda でインストールする

本記事の免責事項:
dtreevizの公式ではpipでのインストールが推奨されているようです。
手順を見ると、condaでgraphviz が入っている場合はそのアンインストールまで明記されています。
そのため、本記事を真似される場合は自己責任でお願いします。
Python環境の破損やその他の動作不良の責任は負いません。
自分自身、将来的にそれらの事象が発生したらpipで入れ直す可能性もあります。

また、この記事でインストールしたのは、version 0.8.2 です。
最新のバージョンでは挙動が異なる可能性があります。

免責事項終わり。

さて、決定木をとても綺麗に可視化してくれるという dtreeviz というライブラリがあるのを聞いて以来、試したいと思っていましたが、
conda(と、conda-forge)のリポジトリには見つからないので後回しにしていたのをやってみることにしました。

個人のMacでは環境構築をcondaに統一しているので、pipはあまり使いたくありません。
しかし免責事項の通り、ドキュメントではpipが推奨されています。

自分の端末ならよかろうということで(職場の端末で試す前の毒見も兼ねて)condaで入れることにしました。
使うのは conda skeleton です。
このブログのこちらの記事が参考になります。
PyPIのパッケージをcondaでインストールする方法

dot や python-graphviz など、必要ライブラリがすでに入っているのもあり、非常にスムーズにインストールできました。


# skeleton で dtreeviz インストール
$ conda skeleton pypi dtreeviz
$ conda build dtreeviz
$ conda install --use-local dtreeviz

# インストール結果確認
$ conda list dtreeviz
# packages in environment at {HOMEPATH}/.pyenv/versions/anaconda3-2019.10:
#
# Name                    Version                   Build  Channel
dtreeviz                  0.8.2            py37h39e3cac_0    local

# 一次ファイル削除
$ conda build purge

Pythonで
from dtreeviz.trees import dtreeviz
をやってみると無事にインポートできたので、導入は成功した様です。
これからの記事で使い方とか書いていきたいと思います。

WordPressのテーマに子テーマを設定する

ほとんど初期設定で運営しているこのブログのデザインなのですが、将来的にはより読みやすい形へのカスタマイズもしたいと思っています。
その場合、子テーマというのを使ってそれをカスタマイズするのがお作法らしいです。

メインのテーマが、WordPressが用意してくれている、 Twenty Seventeenなのですが、これが時々バージョンアップがあり、
子テーマを作っておかないと、その度に自分のカスタマイズ分がリセットされるからだそうです。
(このテーマ特有の事象ではなく、自作ではないテーマを使う人全体に言えることです。)

ということで、子テーマの設定をしました。
なお、このブログのサーバーには、 Amazon Lightsail を利用しています。

子テーマの作成にあたって参照したドキュメントはこちらです。
子テーマ – WordPress Codex 日本語版

管理画面からGUIで作れると思っていたのですが、サーバーに入って作業が必要みたいですね。
まず、ディレクトリ作成からです。 wp-content/themes ディレクトリ下に、親テーマと並列で子テーマのディレクトリを作ります。
(親テーマの配下ではないので注意が必要です。) Lightsail の場合、 wp-content/themes は次の場所にあります。
このディレクトリを探すのにも少してこずりました。

~/apps/wordpress/htdocs/wp-content/themes/
直下に親テーマになる twentyseventeen のフォルダもあります。

ここに、ディレクトリを掘って、style.css, functions.php の2ファイルを作ります。


$ mkdir twentyseventeen-child
$ cd  twentyseventeen-child
$ touch style.css
$ touch functions.php

そして、作った2ファイルに内容を書きます。
まず、style.css の方は、スタイルシートヘッダで始める必要があるそうです。(今回は子テーマ作るだけなので、スタイルシートヘッダだけです。)
具体的にどう書くかは、ドキュメントの記載例に加えて、親テーマのstyle.cssも見ながら次の様にしました。
Template行は、親テーマのディレクトリ名を指します。この例では親テーマが Twenty Fifteen テーマですので、Template は twentyfifteen です。別のテーマが親テーマの場合、該当のディレクトリ名を指定してください。
とある通り、 Template行 が重要です。僕は最初、何も考えずに親テーマのヘッダーをコピーしただけで済ませようとして、Template行がなくてハマりました。


/*
Theme Name: Twenty Seventeen Child
Author: Yutaro
Author URI: https://analytics-note.xyz/
Template: twentyseventeen
Version: 2.3
Requires at least: 4.7
Requires PHP: 5.2.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: twenty-seventeen-child
*/

次は、 functions.php です。 これはドキュメントに指示された通り次の様に書きます。
<?php の部分重要です。PHP の開始タグではじめろとドキュメントの説明にはありますが、コードの例には入っていません。


<?php
add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );
function theme_enqueue_styles() {
    wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );

}

ここまで済ませばあとは管理画面から作業できます。
左ペインの外観を選ぶと、空っぽのテーマができているのでこれを有効化するだけです。
いくつかの設定がリセットされてしまうので、それは再設定が必要になります。
(このブログでいえば無効にしていたヘッダー画像が復活するなど。)

pandasのDataFrameのappendは遅い

この記事のタイトルは悩んだのですが、一番伝えたかった内容がそれなので、上記の様になりました。
他のタイトル候補は次の様になります。

– 配列を要素に持つDataFrameの縦横変換
– 高速にDataFrameを作成する方法

要するに、1件ごとに発生するデータを毎回appendしてデータフレームを構成する処理は、
コーデイングの観点ではわかりやすいですが、件数によっては非常に時間がかかります。

僕の場合ですが、DataFrameの要素に配列(もしくはカンマ区切りなどの文字列)が入っていた時に、
それを要素ごとに縦横変換する時によく遭遇します。

例えば以下の感じのDataFrameを


       key                                         value
0    key_0          [value_0, value_1, value_2, value_3]
1    key_1                            [value_4, value_5]
2    key_2                   [value_6, value_7, value_8]
3    key_3       [value_9, value_10, value_11, value_12]
4    key_4                          [value_13, value_14]
# (以下略)

次の様なDataFrameに変換したい場合です。


     key    value
0  key_0  value_0
1  key_0  value_1
2  key_0  value_2
3  key_0  value_3
4  key_1  value_4
5  key_1  value_5
6  key_2  value_6
7  key_2  value_7
8  key_2  value_8
# (以下略)

データ量が少ない場合は、1件ずつ appendしても問題ありません。
次の様に、iterrows で1行1行取り出して、出力先にpivot_dfに追加してくと、素直で理解しやすい処理になります。


pivot_df = pd.DataFrame(columns=["key", "value"])

for _, row in df.iterrows():
    value_list = row["value"]
    for v in value_list:
        pivot_df = pivot_df.append(
            pd.DataFrame(
                {
                    "key": [row.key],
                    "value": [v],
                }
            )
        )

ただし、この処理は対象のデータ量が2倍になれば2倍以上の時間がかかる様になります。
そこで、大量データ(自分の場合は数十万件以上くらい。)を処理する時は別の書き方を使っていました。

つい最近まで、自分の環境が悪いのか、謎のバグが潜んでいるのだと思っていたのですが、
ドキュメントにも行の追加を繰り返すと計算負荷が高くなるからリストに一回入れろと書いてあるのを見つけました。
どうやら仕様だった様です。

引用

Iteratively appending rows to a DataFrame can be more computationally intensive than a single concatenate. A better solution is to append those rows to a list and then concatenate the list with the original DataFrame all at once.

その様なわけで、DataFrame中の配列の縦横変換は自分は以下の様に、一度配列を作って配列に追加を続け、
出来上がった配列をDataFrameに変換して完成としています。


keys = []
values = []

for _, row in df.iterrows():
    keys += [row.key] * len(row.value)
    values += row.value

pivot_df = pd.DataFrame(
    {
        "key": keys,
        "value": values,
    }
)

jupyter notebookの拡張機能を導入する

職場のMacでは以前から導入していたのですが、私物のMacにも入れたのでメモしておきます。

そのままでも十分使いやすい jupyter notebookですが、
jupyter_contrib_nbextensions というライブラリを入れるといろいろな拡張機能が使える様になり、一層便利になります。
自動で pep8 を満たす様に整形してくれたり、入力セルを隠せたり、補完機能が強化されたり、
テーブルがきれいに表示されるなどの設定ができます。

インストールはこちらのページにもある通り、
pipでも condaでも入れることができます。 自分はnotebook自体がcondaで導入したものなので、
condaで入れています。
これを jupyter notebookが起動してない時に実行し、その後、notebookを起動します。


# pipで入れる場合のコマンド
# $ pip install jupyter_contrib_nbextensions
# condaで入れる場合のコマンド
$ conda install -c conda-forge jupyter_contrib_nbextensions

すると、 jupyterの画面に、 Nbextensions というタブが生成されます。

チェックボックスのチェックを外すと設定を変えられるようになり、
あとは好みの設定を選んで入れていくだけです。

実際に notebookを触りながら色々試すと楽しいと思います。

(補足) サイトによっては、利用するため(タブを表示する)にはアクティベーションの操作が必要といったことが書いてありますが、
僕の環境ではインストールしたらタブが勝手に出てきました。
バージョンによって違うのかもしれません。

Pythonで特異値分解する方法(SciPy利用)

最近使う機会があったので、特異値分解について紹介します。
まず、$A$をランク$r$の$m$行$n$列の実行列とします。
(複素行列も考慮したい場合は、この後出てくる転置行列を随伴行列に読み替え、直行行列をユニタリ行列で読み替えてください。)

このとき、$U$は$m\times m$の直行行列、$V$は$n\times n$の直行行列、Sは非対角成分が0の$m \times n$行列を用いて、
$$A = USV^{\top}$$
と分解することを特異値分解と言います。
$S$の対角成分のうち$0$でないもの(特異値)の個数が$A$のランク$r$になります。

さて、この特異値分解をPythonで実行する方法は複数あり、numpy, SciPy, scikit-learnにそれぞれ実装があります。
参考:
numpy.linalg.svd
scipy.linalg.svd
sklearn.decomposition.TruncatedSVD

これらの使い分けですが、機械学習のパイプラインに組み込んだり、可視化が目的の時など次元削減のために利用するのであればscikit-learnがおすすめです。
それ以外の場合は、SciPyが良いでしょう。numpyより設定できるオプションが多く、また、いくつか補助的な便利関数が用意されています。

とりあえず乱数で生成した行列を、SciPyで特異値分解してみます。


import numpy as np
import scipy

m = 5
n = 3

np.random.seed(110)  # 乱数固定
A = np.random.randint(-10, 10, size=(m, n))
print(A)
"""
[[-10  -7   5]
 [  5 -10   6]
 [  6  -4  -8]
 [ -2  -1  -5]
 [-10   7  -9]]
"""

U, S_diags, V_t = scipy.linalg.svd(A)

# U
print(U)
"""
U:
[[ 0.10801327  0.81765612 -0.427367   -0.18878079 -0.31857632]
 [ 0.62638958  0.06527027 -0.28451679  0.07142207  0.71925307]
 [ 0.04632544 -0.55033827 -0.69658511 -0.39810108 -0.226421  ]
 [-0.16944935 -0.04718317 -0.43857367  0.87461141 -0.1084836 ]
 [-0.75173806  0.14859275 -0.24254891 -0.18929111  0.56404698]]
"""

# Sの対角成分:
print(S_diags)
# [19.70238709 15.08068891  9.76671719]

# Vの転置行列:"
print(V_t)
"""
[[ 0.51699558 -0.6241887   0.58575083]
 [-0.83177902 -0.20473932  0.51597042]
 [ 0.20213668  0.75396968  0.62503639]]
"""

変数名で表現してみましたが、 $U$以外の二つは少し癖のある形で返ってきます。
$S$は対角成分しか戻ってきませんし、$V$は転置された状態で戻されます。
再度掛け算して結果を角にする時などは注意が必要です。

さて、$S$は対角成分だけ返ってきましたが、これを$m\times n$行列に変換するには、専用の関数が用意されています。
scipy.linalg.diagsvd


S = scipy.linalg.diagsvd(S_diags, m, n)
print(S)
"""
[[19.70238709  0.          0.        ]
 [ 0.         15.08068891  0.        ]
 [ 0.          0.          9.76671719]
 [ 0.          0.          0.        ]
 [ 0.          0.          0.        ]]
"""

あとは、 $USV^{\top}$計算して、元の$A$になることをみておきましょう。


print(U@S@V_t)
"""
[[-10.  -7.   5.]
 [  5. -10.   6.]
 [  6.  -4.  -8.]
 [ -2.  -1.  -5.]
 [-10.   7.  -9.]]
"""

元の値が復元できましたね。

Googleアナリティクスでアクセスの増加量が大きいページを探す

2020年の5月4日ごろから、Googleの検索アルゴリズムのコアップデートが行われ、このブログの訪問者数にも大きな影響が出ています。
アップデートの前後3週間の、自然検索による流入数をグラフにしたのが下図です。
明らかに流入が増えていますね。

さて、もっと詳しく、具体的にどのページの流入が増えて、どのページの流入は減ったのか調べるため、方法を調べたので紹介します。
デフォルトだと、多い順になっており、逆順になれべても絶対値の少ない順になってしまって、変化量順にはならないですよね。

変化量でソートしたい場合は、「並べ替えの種類:」という項目を「変化量」に設定する必要があります。
これで、アクセスの増加が激しいページを特定できました。
この期間はゴールデンウィークなどの要因もあるので、変化量は注意しいてみないといけないのですが、
なんとなくテクニカルな記事が恩恵を受けてるように感じます。

2020年上半期(1月~6月)によく読まれた記事

早いもので2020年が半分終わってしまいました。
今年はブログ更新頻度を落としているのもあり、四半期でのランキング発表をやめているので、
この辺で半年間に読まれた記事のランキングを出したいと思います。

参考ですが、昨年1年間のランキングはこちらです。
参考: 2019年のまとめ

では早速ランキング発表です。
集計期間は 2020年1月から6月まで。pvで並べています。

  1. matplotlibのグラフを高解像度で保存する
  2. macにgraphvizをインストールする
  3. pythonで累積和
  4. DataFrameを特定の列の値によって分割する
  5. INSERT文でWITH句を使う
  6. kerasのto_categoricalを使ってみる
  7. numpyのpercentile関数の仕様を確認する
  8. scipyで階層的クラスタリング
  9. matplotlibのデフォルトのフォントを変更する
  10. graphvizで決定木を可視化

データサイエンスというより、プログラミングのちょっとしたTips的な記事の方がよくpvを集めていますね。
今年はネットワーク解析/グラフ理論の記事も頑張って書いたので今後はそれらのランクインも期待したいです。

PrestoのUNNESTを利用した横縦変換

以前Prestoのクエリで縦横変換(pivot)を行う方法を初回しましたが、今回はその逆で横縦変換(unpivot)を紹介します。

参考: PrestoのMap型を使った縦横変換

参考記事と逆の変換やるわけですね。
そのため元のテーブルがこちら。

横持ちのテーブル
uid c1 c2 c3
101 11 12 13
102 21 22 23

結果として出力したいテーブルがこちらになります。

縦持ちのテーブル (vtable)
uid key value
101 c1 11
101 c2 12
101 c3 13
102 c1 21
102 c2 22
102 c3 23

一番シンプルな書き方は、UNIONを使う方法だと思います。
key の値ごとにvalue を抽出してそれぞの結果を縦に積み上げます。


SELECT
    uid,
    'c1' AS key,
    c1 AS value
FROM
    htable
UNION ALL SELECT
    uid,
    'c2' AS key,
    c2 AS value
FROM
    htable
UNION ALL SELECT
    uid,
    'c3' AS key,
    c3 AS value
FROM
    htable

ただ、この書き方には課題もあって、元の列数が多いとクエリが非常に冗長になります。
そこで、 UNNESTを使った方法を紹介しておきます。

ドキュメントは SELECT の説明のページの途中に UNNESTの章があります。

これを使うと次の様に書けます。


SELECT
    uid,
    t.key,
    t.value
FROM
    htable
CROSS JOIN UNNEST (
  array['c1', 'c2', 'c3'],
  array[c1, c2, c3]
) AS t (key, value)

とてもシンプルに書けました。

NetworkXを使ってエイト・クイーンパズルを解く

“Python言語によるビジネスアナリティクス”と言う本の中に、NetworkXについての記述があるのですが、
その演習問題(問題43)として8-クイーン問題が出題されています。
面白そうだったので挑戦してみたところ、なんとか解けました。
この問題がネットワーク解析に関係あるとは意外ですね。

まず、8-クイーン問題については、Wikipediaの説明が分かりやすいと思います。
エイト・クイーン – Wikipedia

簡単に言うと、8×8の盤面状にチェスのクイーン(将棋の飛車と角を足した動きができる最強のコマ)をお互いに取り合えない位置に並べるパズルです。
正解は92通り(盤面の反転や回転を除いた本質的なものは12通り)あるそうです。

さて、これを解くために、次のアプローチを考えました。

1. 8×8の各マスをノードとするグラフを構築する。
2. クイーンがお互いに取り合えるマス同士をエッジで結ぶ。
3. そのグラフの補グラフを取得することで、クイーンがお互いに取り合えないマス通しが結ばれたグラフを作る。
4. ノード数が8のクリーク(その中に含まれる全てのノードが結ばれている部分グラフ)を探索する。

最初からお互いに取り合えないマスの間を結ぶのではなく、2.と3.に分けてるのはその方が実装がシンプルになるからです。


import networkx as nx

# 盤面の一辺のマス数
bord_size = 8

# グラフ生成
G = nx.Graph()

# ノードを作成する
for i in range(bord_size):
    for j in range(bord_size):
        G.add_node((i, j))

# 同じ行か同じ列に並ぶノードをエッジで結ぶ
for i in range(bord_size):
    for m in range(bord_size):
        for n in range(m+1, bord_size):
            G.add_edge((i, m), (i, n))
            G.add_edge((m, i), (n, i))

# 同じ斜め線状に並ぶノードをエッジで結ぶ
for n1 in G.nodes:
    for n2 in G.nodes:
        if n1 == n2:
            continue
        elif n1[0]+n1[1] == n2[0]+n2[1]:
            G.add_edge(n1, n2)
        elif n1[0]-n1[1] == n2[0]-n2[1]:
            G.add_edge(n1, n2)


# 補グラフを得る (お互いに取り合わない辺が結ばれている)
G_complement = nx.complement(G)

# サイズが変の数に等しいクリークの一覧を得る
answers = [
        clieque for clieque in nx.find_cliques(G_complement)
        if len(clieque) == bord_size
    ]

# 得られた解の個数
print(len(answers))
# 92

こうして、92個の解が得られました。
あとは実際にこれを可視化してみましょう。


import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 30), facecolor="w")

for i, answer in enumerate(answers, start=1):
    bord = np.zeros([bord_size, bord_size])
    ax = fig.add_subplot(16, 6, i)

    for cell in answer:
        bord[cell] = 1

    ax.imshow(bord, cmap="gray_r")

plt.show()

出力がこちらです。 一個一個みていくと確かに 縦横斜めにダブらない様にQueen(黒い点)が配置できています。
ただ、全部相異なることを確認するのは少し手間ですね。

もともと出題されていた本には解答が載っていないので、
もしかしたらこの記事のコードでは相当非効率なことをやっているかもしれませんが、
おそらくこんなイメージで解くのだと思います。

Pythonのlistをsortするときに指定するkey引数について

先日とある目的で、配列を要素に持つ配列を、各配列の3番目の値を使ってソートするコードが必要になりました。
最初はそのキーになる要素だけ取り出して、 argsortしてどうにか並べ替えようとしたのですが、実はkeyって引数を使えば綺麗に実装できることがわかりました。

ドキュメントは組み込み型のlistのsortメソッドの部分が該当します。

key って引数に 単一の引数を取る関数を渡せば、listの各要素にその関数が適用され、戻り値によってソートされる様です。
なので、配列の配列を3番目の要素でソートするには、lambda 式あたりでそう言う関数を作ってあげれば実現できます。
(インデックスは0始まりなので、実際にはインデックス2を取得します。)


# ソートされるデータ作成
list_0 = [
        [9, 8, 5],
        [0, 8, 3],
        [1, 6, 5],
        [9, 0, 0],
        [4, 9, 3],
        [1, 4, 8],
        [4, 0, 6],
        [0, 3, 5],
        [1, 3, 1],
        [5, 2, 7],
    ]

# 3番目(index 2)の要素でソート
list_0.sort(key=lambda x: x[2])
print(list_0)
# [[9, 0, 0], [1, 3, 1], [0, 8, 3], [4, 9, 3], [9, 8, 5], [1, 6, 5], [0, 3, 5], [4, 0, 6], [5, 2, 7], [1, 4, 8]]

これを応用すれば、辞書の配列を特定のkeyに対応する値でソートするといったことも簡単にできます。
(lambda式に辞書オブジェクトのgetメソッド渡すだけですね。)

もちろん、lambda 式以外にも事前に定義した関数を渡すこともできます。
次の例は、トランプのカード (2〜9とTJQKAで表したランクと、cdhsで表したスートの2文字で表したもの)を強い順にソートするものです。


# カードの強さを返す関数
def card_rank(card):
    rank_dict = {
            "T": 10,
            "J": 11,
            "Q": 12,
            "K": 13,
            "A": 14,
        }
    suit_dict = {
            "s": 3,
            "h": 2,
            "d": 1,
            "c": 0,
        }
    return int(rank_dict.get(card[0], card[0]))*4 + suit_dict[card[1]]


card_list = ["Ks", "7d", "Ah",  "3c", "2s", "Td", "Kc"]

# ソート前の並び
print(card_list)
# ['Ks', '7d', 'Ah', '3c', '2s', 'Td', 'Kc']

# 強い順にソート
card_list.sort(key=card_rank, reverse=True)
print(card_list)
# ['Ah', 'Ks', 'Kc', 'Td', '7d', '3c', '2s']

reverse = True を指定しているのはソートを降順にするためです。

このほかにも str.lowerを使って、アルファベットを小文字に統一してソートするなど、
使い方は色々ありそうです。