NumPyのブロードキャストで変換できる型

NumPyを普段使いしてると便利な機能に、ブロードキャストがあります。
これは配列のサイズが違うもの通しを演算するときに、いい感じに小さい方を拡張して演算してくれるものです。

例えば、配列とスカラーの和や、行列とベクトルの和を次の様に計算してくれます。


a = np.array(range(4))
print(a)
# [0 1 2 3]

# 7 を [7, 7, 7, 7] として扱って足してくれる
b = a + 7
print(b)
# [ 7  8  9 10]

c = np.array(range(6)).reshape(2, 3)
print(c)
"""
[[0 1 2]
 [3 4 5]]
"""

d = np.array([5, 5, 5])

# [5, 5, 5] を [[5, 5, 5], [5, 5, 5]] として扱って足してくれる
e = c+d
print(e)
"""
[[ 5  6  7]
 [ 8  9 10]]
 """

本当にいい感じにやってくれるのであまり意識せずに使っていましたが、仕様を正確に把握しておきたかったので改めてドキュメントを読みました。
と言うのも、いつでも動くわけではありませんし、正方行列とベクトルの和の様にどちらの軸にブロードキャストされるか迷うことなどあるからです。


# 動かない例
a = np.array(range(6)).reshape(3, 2)
b = np.array(range(3))
a + b
# ValueError: operands could not be broadcast together with shapes (3,2) (3,)

# 行か列か引き伸ばされる方向がわかりにくい例
c = np.zeros(shape=(3, 3))
d = np.array(range(3))
print(c+d)
"""
[[0. 1. 2.]
 [0. 1. 2.]
 [0. 1. 2.]]
"""

ドキュメントはこちらです。
参考: Broadcasting

長いのですが、基本的なルールは General Broadcasting Rules に書かれてる次の法則だけです。
配列の次元数は後ろから順番に前に向かって比較されます。(長さが違う場合は、短い方に1が追加されて揃えられます。)
そして、それらの値が等しいか、もしくは一方が1であればブロードキャストされます。

ブロードキャストされるときは、値が1だった(もしくは無かった)次元の向きにデータがコピーされて拡張されます。

先ほどのエラーが起きた例で言えば、 (3, 2)次元と (3) 次元の 2と3が比較されてこれが等しくないからエラーになったわけですね。
その次の例についてはまず、
[0, 1, 2] (shapeは(3,))に、次元が一個追加されて、
[[0, 1, 2]] (shapeは(1, 3)) に変換され、それが各行にコピーされたので上の例の様な結果になっています。

先述のシンプルなルールさえ満たしていれば、次の例の様な少々無茶でイメージしにくい配列同士でもブロードキャストされます。


a = np.array(range(21)).reshape(1,1,3,1,7)
b = np.array(range(10)).reshape(2,1,5,1)

print(a.shape)
# (1, 1, 3, 1, 7)
print(b.shape)
# (2, 1, 5, 1)

c = a+b
print(c.shape)
# (1, 2, 3, 5, 7)

(1, 1, 3, 1, 7) と (2, 1, 5, 1) では長さが違うので、後者の方の先頭に1が挿入され
以下の二つになるわけですね。
(1, 1, 3, 1, 7)
(1, 2, 1, 5, 1)
これを順番に見ていくと、ブロードキャストのルールをみたいしているので足りない向きについてはデータがコピーされ、
和がとれているわけです。

外部リンクへのクリックをイベントとして計測する

このブログの改善のために、外部サイトへのリンクが(特に各種技術記事から公式のドキュメントへ)どれだけクリックされているのか計測しようと思います。
それが、GoogleタグマネージャーとGoogleアナリティクスの組みあわせでできるので設定していきます。

順番に設定していきます。

1. タグマネージャーのコンテナとワークスペースを選択。
2. 新しいタグを追加を選択し、タグに名前をつける。
3. タグの設定をクリック。
4. タグタイプは、 「Googleアナリティクス:ユニバーサル アナリティクス」を選択。
5. トラッキングタイプに「イベント」を選択し、以下の内容を入力。
カテゴリ: External link
アクション: {{Click URL}}
ラベル: {{Page Path}}
値: 1
非イタンラクション ヒット: 真
6. トリガーの追加を押し、トリガーの選択画面で右上の「+」を押す。
7. トリガーの設定で、クリックの下のリンクのみを選択。
8. 以下の内容設定
タグの配信を待つと妥当性をチェックは一旦無効のまま
一部のリンククリックを選択し、以下の二つを設定する
Click URL正規表現に一致 http.*
Click URL 含まない analytics-note.xyz
9. トリガーに名前をつける
10. 保存し、プレビューでテストして公開

しばらく経過を見てまた調整するかもしれませんが、一旦はこれで計測できる様になっているはずです。

Google タグマネージャー導入

タイトルの通り、このBlogでもGoogleタグマネージャーを使い始めました。
目的は主にGoogleアナリティクスでのイベント計測です。
多くの記事で参照している各種のドキュメントへのリンクや、画像へのアクセス状況を分析しより使いやすいサイトにしたいと思っています。

これまでは、GAは「Googleアナリティクス導入」の記事で書いた通り、
SEOのプラグインで設定していましたが、これがタグマネージャーによる管理に移行します。

以下、導入手順のメモです。

1. タグマネージャのサイトにアクセスし、Googleアカウントでログイン。
2. 「アカウントを作成するにはここをクリックしてください」 をクリック。
3. アカウント名(適当)、 国(日本)、コンテナ名(URLでも良いのですがサイト名にしました。)、ターゲットプラットフォーム(ウェブ)を入力。
4. 作成ボタンクリック。
5. 利用規約を読んで「はい」をクリック。
6. サイト内に貼り付ける様に指示されるコードを保存して「OK」で閉じる。 (GTM-******* と言うコードを後で使う。)

コンテナIDはタグマネージャーのホーム画面でいつでも確認できます。

次にこれをWordPressに設定します。
1. プラグインで Google Tag Manager を検索。
2. 今回はこちらをインストール。 Google Tag Manager
3. プラグインを有効化。
4. 「設定」「一般」の一番下に Google Tag Manager ID の設定ができているので、ここにコンテナIDを入力して保存。
(プラグインの設定ではないので注意。)

これでタグマネージャー自体の導入完了です。

次に、肝心のGoogleアナリティクスをタグマネージャーで設定します。

1. タグマネージャーの「ワークスペース」ページの「サマリー」を開く。
2. 「新しいタグ」 をクリック。
3. 「タグの設定」をクリックして「Google アナリティクス: ユニバーサル アナリティクス」 を選択する。
4. 「トラッキングタイプ」に「ページビュー」を選択。
5. Googleアナリティクス設定で、トラッキングコードを入力。 (UA-*********-* 形式の値)
6. 変数名を入れて保存。(とりあえず、「分析ノートUA」にしました。)
7. トリガーの設定。 「All Pages ページビュー」を選択。
8. タグに名前(分析ノートページビュー)を設定して保存。

この段階ではまだ公開されていないので、テストしてから公開します。
1. ワークスペースに戻ってプレピューをクリック。
2. そのブラウザで、このブログにアクセスすると画面下に発火したタグが確認できる。
(分析ノートページビューがFiredになっている)
3. プレビューモードを終了
4. 「公開」ボタンをクリック。
5. バージョン名と説明を求められるので入力し、もう一度「公開」をクリック。

最後に、今回に限り重要なのが、すでに直接設定してたGoogleアナリティクスの設定を消すこと。
SEOプラグインに設定してたUAの値を消しました。これをしないとpvが二重カウントになります。

dtreevizで特徴量とラベルの関係を可視化

※この記事では dtreevizの version 0.8.2 を使っています。

前回の記事では、dtreeviz を使って学習済みの決定木を可視化しました。
dtreevizではこの他にも、1個か2個の特徴量とラベルの関係を可視化できます。

それが、 ctreeviz_univar と、ctreeviz_bivar です。
扱える特徴量がuniの方が1個、biの方が2個です。

データは必要なので、irisを読み込んでおきます。今回は木は不要です。
(その代わり、max_depsかmin_samples_leafのどちらかの設定が必須です。)


from sklearn.datasets import load_iris
iris = load_iris()

まず1個のほうをやってみます。
特徴量4個しかないので全部出します。


import matplotlib.pyplot as plt
from dtreeviz.trees import ctreeviz_univar

figure = plt.figure(figsize=(13, 7), facecolor="w")
for i in range(4):
    ax = figure.add_subplot(2, 2, i+1)
    ctreeviz_univar(
        ax,
        iris.data[:, i],
        iris.target,
        max_depth=2,
        feature_name=iris.feature_names[i],
        class_names=iris.target_names.tolist(),
        target_name='types'
    )

plt.tight_layout()
plt.show()

どの特徴量が有効なのか、自分的にはこれまでで一番わかりやすいと感じました。

次は2個の方です。特徴量2種類とラベルを渡すと、それらの関係を可視化してくれます。
2個ずつ選んで2つのグラフで可視化してみました。
引数、ですがfeature_name が feature_names になっており、渡す値も文字列が配列になっているので注意が必要です。


from dtreeviz.trees import ctreeviz_bivar

figure = plt.figure(figsize=(5, 12), facecolor="w")
ax = figure.add_subplot(2, 1, 1)
ctreeviz_bivar(
    ax,
    iris.data[:, :2],
    iris.target,
    max_depth=2,
    feature_names=iris.feature_names[:2],
    class_names=iris.target_names.tolist(),
    target_name='types'
)

ax = figure.add_subplot(2, 1, 2)
ctreeviz_bivar(
    ax,
    iris.data[:, 2:],
    iris.target,
    max_depth=2,
    feature_names=iris.feature_names[2:],
    class_names=iris.target_names.tolist(),
    target_name='types'
)

plt.show()

出力がこちら。

これもわかりやすいですね。

dtreevizで決定木の可視化

早速、前回の記事でインストールした dtreeviz を使ってみます。

※この記事では dtreevizの version 0.8.2 を使っています。
1.0.0 では一部引数の名前などが違う様です。(X_train が x_dataになるなど。)

とりあえず、データと可視化する木がないと話にならないので、いつものirisで作っておきます。


from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

iris = load_iris()
clf = DecisionTreeClassifier(min_samples_split=5)
clf.fit(
    iris.data,
    iris.target
)

さて、これで学習したモデル(コード中のclf)を可視化します。
リポジトリのコードを見ながらやってみます。

まず、一番シンプルな可視化は、 dtreeviz.trees.dtreevizにモデルと必要なデータを全部渡すものの様です。
(省略不可能な引数だけ設定して実行しましたが、結構多いですね。)


from dtreeviz.trees import dtreeviz

tree_viz = dtreeviz(
    tree_model=clf,
    X_train=iris.data,
    y_train=iris.target,
    feature_names=iris.feature_names,
    target_name="types",
    class_names=iris. target_names.tolist(),
)
tree_viz

出力がこちら。

graphvizで決定木を可視化 でやったのと比べて、とてもスタイリッシュで解釈しやすいですね。

orientation(デフォルトは’TD’)に’LR’を指定すると、向きを縦から横に変更できます。


tree_viz = dtreeviz(
    tree_model=clf,
    X_train=iris.data,
    y_train=iris.target,
    feature_names=iris.feature_names,
    target_name="types",
    class_names=iris. target_names.tolist(),
    orientation='LR',
)
tree_viz

出力がこちら。

木のサイズによってはこれも選択肢に入りそうですね。

決定木の可視化ライブラリ 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.]]
"""

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