NetworkXのグラフを可視化するときに頂点の座標を指定する

久々のNetworkXの記事です。
今回はグラフを可視化するときに、明示的に頂点の座標を指定する方法を紹介します。

ドキュメントの Docs » Reference » Drawing
のページを見ると、draw_networkxなどの可視化関数がposという引数を受け取ることがわかります。
ここに、{頂点:(x座標, y座標), …} という形式の辞書を渡してあげると、可視化するときにその座標にノードを配置できます。
これを使って頂点を格子点においたり、一直線に並べたりできます。

いい感じに座標の辞書を作ってくれる関数もいくつか用意されているのですが、
今回は自分で直接円周上のてんを定義してやってみます。

まず一つ目の例は座標は指定せずにそのまま可視化したものです。


import matplotlib.pyplot as plt
import networkx as nx
import numpy as np

# 12個の頂点と、ランダムに引いた辺を持つグラフを定義
node_labels = "abcdefghijkl"
G = nx.Graph()
G.add_nodes_from(node_labels)
for i in range(len(G.nodes)):
    for j in range(i+1, len(G.nodes)):
        if np.random.uniform() < 0.3:
            G.add_edge(node_labels[i], node_labels[j])

# 座標を指定せずに描写する
nx.draw_networkx(G, node_color="c")
plt.show()

結果がこちら。頂点はある程度バラバラに配置されていますね。

次にグラフはそのままで座標を指定してみます。


# 各頂点に対して円周上の座標を割り当てる
pos = {
        n: (np.cos(2*i*np.pi/12), np.sin(2*i*np.pi/12))
        for i, n in enumerate(G.nodes)
    }
print(pos)
'''
{'a': (1.0, 0.0),
 'b': (0.8660254037844387, 0.49999999999999994),
 'c': (0.5000000000000001, 0.8660254037844386),
 'd': (6.123233995736766e-17, 1.0),
 'e': (-0.4999999999999998, 0.8660254037844388),
 'f': (-0.8660254037844387, 0.49999999999999994),
 'g': (-1.0, 1.2246467991473532e-16),
 'h': (-0.8660254037844388, -0.4999999999999998),
 'i': (-0.5000000000000004, -0.8660254037844384),
 'j': (-1.8369701987210297e-16, -1.0),
 'k': (0.5, -0.8660254037844386),
 'l': (0.8660254037844384, -0.5000000000000004)}
'''

# 指定した座標を用いてグラフを可視化する
nx.draw_networkx(G, pos=pos, node_color="c")
plt.show()

結果がこちらです。

きちんと円周上に頂点が並びました。

ダイス係数とシンプソン係数

集合の類似度を表す係数には、前回の記事で紹介したジャッカード係数のほか、
ダイス係数(Sørensen–Dice coefficient)と、シンプソン係数(Szymkiewicz–Simpson coefficient)というものがあります。
自分はジャッカード係数を使うことが多いので、あまり利用しないのですがこの二つも有名なもののようなので定義を紹介します。

まず、ダイス係数です。
ジャッカード係数と比較すると、二つの集合の和集合の要素の数の代わりに、二つの集合の要素数の平均を用いています。
$$
DSC(A, B) = \frac{2|A\cap B|}{|A| + |B|}
$$

続いて、シンプソン係数。
こちらは二つの集合の和集合の要素の数の代わりに、二つの集合の要素数のうち小さい方を用います。

$$
SSC(A, B) = \frac{|A\cap B|}{\min(|A|, |B|)}
$$

シンプソン係数は二つの集合のうち一方がもう一方に包含されている時、値が$1$になってしまうのが嫌なので利用を避けることが多いです。
この性質が便利な場面もあるのかもしれませんが、ぱっと思いつくものがない。

ジャッカード係数

二つの集合がどのくらい似ているのか表す指標である、ジャッカード係数(Jaccard index)を紹介します。

Wikipedia: Jaccard index

これは二つの集合$A, B$に対して、その共通部分の元の個数を、和集合の元の個数で割ったものです。
$A, B$のジャッカード係数$J(A, B)$を数式でと次のようになります。

$$
J (A, B) = \frac{|A \cap B|}{|A \cup B|}
$$
単に共通部分の大きさを数えるだけでなく、和集合の元の個数で割ることにより正規化していることがポイントです。
定義から明らかに、 $0\leq J (A, B) \leq 1$ であり、二つの集合に交わりが大きいほど値が大きくなります。
また、二つの集合がどちらも空集合の時は$1$と定義するそうです。(これは知らなかった)
空集合同士で等しいからそれを表現するためと考えると納得です。

よく似た概念に、ジャッカード距離(Jaccard distance)があります。
距離なので、二つの集合が似てるほど値が小さくなって欲しく、差が大きいほど値が大きくなって欲しいので、次のように定義されています。
$$
d_J(A, B) = 1 – J (A, B) = \frac{|A \cup B| – |A \cap B|}{|A \cup B|}
$$

自分はこれを自然言語処理で使うことが多くあります。
テキストを単語の集合としてテキストの類似度を測ったり、
単語を文字の集合として単語の類似を測ったりですね。

pythonのfrozenset型の紹介

Pythonで集合を扱うデータ型として一般的なのはset型だと思いますが、
実は集合を扱う組み込み型にfrozensetというものがあるのでその紹介です。

ドキュメント:set(集合)型 — set, frozenset

setとfrozensetの何が違うかというと、setはミュータブルで、frozensetはイミュータブルです。
リストとタプルのような関係ですね。

それぞれの主なメリット/デメリットをあげると、
set は要素の追加や削除ができ、frozensetはそれができません。一度定義したらそのままです。
また、setは辞書のキーや他の集合の要素には使えませんが、frozensetは使うことができます。

軽く動かしてみましょう。


# frozensetを定義する
frozenset_1 = frozenset({'a', 'b', 'c'})
print(frozenset_1)
# frozenset({'b', 'a', 'c'})

# setを定義する
set_1 = {'a', 'b', 'c'}
print(set_1)
# {'b', 'a', 'c'}

# setは要素の追加削除可能。
set_1.add("d")
set_1.remove("d")
print(set_1)
# {'b', 'a', 'c'}

sample_dict = {}
# frozenset は辞書のキーに使える
sample_dict[frozenset_1] = "value1"
print(sample_dict)
# {frozenset({'b', 'a', 'c'}): 'value1'}

# set は辞書のキーにできない
sample_dict[set_1] = "value2"
'''
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 sample_dict[set_1] = "value2"

TypeError: unhashable type: 'set'
'''

最近集合をハッシュキーに使いたいことがあり、無理やりタプルで代用するなど不便な思いをしていたので、
これを知ってコードがスッキリしました。

sample_dic[{“key1”, “key2”}] = “value1”
とるすことが不可能なので、代わりに下の二つを登録して、呼び出すときは逐一keyをタプル化するような不恰好なコードとはこれでサヨナラです。
sample_dic[(“key1”, “key2”)] = “value1”
sample_dic[(“key2”, “key1”)] = “value1”

Googleアナリティクスで、特定のページを訪れたセッションの数を見たい時の注意点

前の記事で、Googleアナリティクスのセッションの定義を紹介しました。
このブログはセッションあたりのpvが1.2くらいしかないのであまり気になりませんが、業務でサイトの分析をするときなど、
各ページを訪問したセッション数を知りたいことがあります。

例えば、
ページA -> ページB -> ページC
と遷移したユーザーと、
ページB -> ページD -> ページB
と遷移したユーザーがいた時に、ページBを訪問したセッションが2個だ、と数えたいケースです。

これを知るには、「ページ別訪問数」という指標をみる必要があります。
「セッション」ではないので、非常に間違えやすく、注意が必要です。

ちなみに「セッション」は、各ページでセッションが始まった時だけカウントされます。
上の例で言えばページAとページBはセッションが1です。

ヘルプではこの辺りを読むと参考になります。
アナリティクスの Google 広告クリック数、セッション数、ユーザー数、閲覧開始数、ページビュー数、ページ別訪問数の違い

もともと用意されている各レポートでは適切に使い分けられて表示されているのですが、
カスタムレポートなどを使うときは特に注意したいですね。

Googleアナリティクスにおけるセッションの数え方

セッションというのはWebサービスに対する一連のアクセスのことです。
ある人がこのサイトを訪れ、3ページ見て離脱していったらその3pvが1セッションとなります。

ただ、pvのような数え方が明確なものと違って、セッションを数えるには何を基準に離脱していったかを定義する必要があります。
Googleアナリティクスにおいてもセッション数は取得されているのですが、
それを正しく使うにはGoogleアナリティクスにおけるセッションの定義を知っておく必要があります。

その内容ですが、アナリティクスのヘルプの下記ページに書かれています。
アナリティクスでのウェブ セッションの算出方法

ようは次の3つの条件のどれかを満たすと、そのセッションは終わりと判断され、新しいセッションに移ります。

  • 30分間操作がない
  • 午前0時をまたぐ
  • 他サイトを経由してアクセスし直した

普段自分でクエリを書いてセッションを区別するときはユーザーのアクセスが30分途切れたら別セッション、
という条件だけで済ませていることが多いので2番目と3番目の条件をよく忘れてしまいます。
(そんなのは自分だけだと思いますが。)

Kerasの学習履歴(History)をDataFrameに変換する

Kerasのちょっとした小ネタです。
Kerasで作ったモデルをfitすると、戻り値として損失関数や正解率を格納したHistoryオブジェクトが返されます。
それを使って、学習の進みなどを可視化できます。

例えばこちらの記事を参照: CNNで手書き数字文字の分類
こちらでは可視化だけで10行くらいのコードになっています。

で、改めてHistoryの中身をみてみると、DataFrameに変換できる形式であることに気づきました。
長いので、実データは {数値のリスト} に置換しましたが、次のようなイメージです。


print(history.history)
'''
{'val_loss': {数値のリスト},
 'val_acc': {数値のリスト},
 'loss': {数値のリスト},
 'acc': {数値のリスト}
'''

これは容易にDataFrameに変換できます。


print(pd.DataFrame(history.history))
'''
    val_loss  val_acc      loss       acc
0   0.106729   0.9677  0.590888  0.811850
1   0.072338   0.9764  0.227665  0.931233
2   0.059273   0.9800  0.174741  0.948033
3   0.047335   0.9837  0.149136  0.955500
4   0.042737   0.9859  0.132351  0.960167
5   0.039058   0.9868  0.121810  0.964600
6   0.034511   0.9881  0.110556  0.967050
7   0.032818   0.9882  0.105487  0.967867
8   0.032139   0.9893  0.100333  0.970167
9   0.030482   0.9898  0.095932  0.971383
10  0.027904   0.9900  0.089120  0.973267
11  0.028368   0.9898  0.086760  0.973683
'''

DataFrameになると、これ自体を分析しやすいですし、さらに非常に容易に可視化できます。


history_df = pd.DataFrame(history.history)
history_df.plot()
plt.show()

たったこれだけです。以前の記事の可視化に比べると非常に楽ですね。
出力はこのようになります。

INSERT文でWITH句を使う

PrestoのINSERT文で、別のテーブルからSELECTした結果を挿入する書き方があります。
こういうの。


INSERT INTO
    new_table (
        col1,
        col2,
        col3
    )
SELECT
   col1,
   col2,
   col3
FROM
   old_table

このとき、SELECT文がそこそこ複雑になると、以前の記事で紹介したWITH句を使いたくなるのですが、うまく動かず困っていました。
それはどうやら、 INSERT INTO より先に WITH句を書いてしまっていたのが原因のようです。

WITH は INSERT INTO と SELECT の間に 書くのが正解のようです。
例としてはこんな感じ。


INSERT INTO
    new_table (
        col1,
        col2,
        col3
    )
WITH
    tmp_table AS (
        SELECT
            col1,
            col2,
            col3
        FROM
            old_table
    )
SELECT
   col1,
   col2,
   col3
FROM
   tmp_table

scikit-learnでK-分割交差検証

普段、機械学習のパラメーターを最適化するために
K分割交差検証をするときはGridSearchCVで済ましているのですが、
今回別の目的があって、K-分割交差検証のような分割が必要になりました。
参考:scikit-learn でグリッドサーチ

そこで、GridSearchCVの中でも使われているKFoldを使ってみたのでその記録です。
ドキュメントはこちら。
sklearn.model_selection.KFold

注意点は、splitした時に得られる結果が、データそのものではなくデータのインデックスである点くらいです。

実行してみるために、まずダミーデータを準備します。


from sklearn.model_selection import KFold
import numpy as np

# ダミーデータの準備
X = np.random.randint(20, size=(10, 2))
y = np.random.choice([0, 1], size=10)
print(X)
'''
[[ 2 12]
 [ 6  3]
 [ 0  3]
 [15  9]
 [19  0]
 [12 14]
 [ 0  0]
 [12 18]
 [ 1  9]
 [ 3 14]]
'''
print(y)
# [0 0 0 1 1 1 0 1 0 0]

次に、 KFoldを動かしてみましょう。
まず挙動を確認したいので、splitして得られるイテレータの中身を表示してみます。


kf = KFold(5, shuffle=True)
for train_index, test_index in kf.split(X):
    print("train_index:", train_index)
    print("test_index:", test_index)
    print()  # 1行開ける

# 以下出力
'''
train_index: [1 2 3 4 5 6 8 9]
test_index: [0 7]

train_index: [0 1 2 3 4 6 7 9]
test_index: [5 8]

train_index: [0 1 3 4 5 6 7 8]
test_index: [2 9]

train_index: [0 1 2 5 6 7 8 9]
test_index: [3 4]

train_index: [0 2 3 4 5 7 8 9]
test_index: [1 6]

'''

ご覧の通り、0〜9の値を、5つのグループに分けて、順番に一個をテスト用にしながら交差検証用のデータセットのインデックスを返してくれています。
インデックスを元にデータを分割するには、つぎのようにして別の変数に取り出すと以降のコードが読みやすくなります。


X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]

pandasでユニコード正規化

3記事続けてのユニコード正規化の話です。
これまで標準ライブラリのunicodedata.normalizeを使っていましたが、
実はpandasのDataFrameやSeriesにもユニコード正規化のメソッドが実装されています。

ドキュメント: pandas.Series.str.normalize

これを使うと大量の文字列を一気に正規化できるので、個人的にはこちらを使うことが多いです。
機械学習で、学習時はpandasのnormalizeを使い、
その後、個々のデータを予測する時にunicodedata.normalizeを使ってしまうと、結果変わってしまう恐れがあるのではないかと
心配して調べたことがあるのですが、pandasのnormalizeはunicodedataのラッパーになっていて、
中では同じモジュールを使っているので問題ありませんでした。
(ドキュメントを読んでもわかりますね。)

pandas の v0.25.0 のコードから抜粋しますが、この通り、unicodedata.normalizeを呼び出しているだけです。


    @forbid_nonstring_types(["bytes"])
    def normalize(self, form):
        """
        Return the Unicode normal form for the strings in the Series/Index.
        For more information on the forms, see the
        :func:`unicodedata.normalize`.
        Parameters
        ----------
        form : {'NFC', 'NFKC', 'NFD', 'NFKD'}
            Unicode form
        Returns
        -------
        normalized : Series/Index of objects
        """
        import unicodedata

        f = lambda x: unicodedata.normalize(form, x)
        result = _na_map(f, self._parent)
        return self._wrap_result(result)

実際に使うと、次のようになります。
Seriesを例にとりましたが、DataFrameの列を対象にする場合も同様です。


import pandas as pd
series = pd.Series(
    [
        "パピプペポ",
        "①⑵⒊",
        "㍾㍽㍼㍻",
        "㌢ ㌔ ㍍"
    ]
)
print(series.str.normalize("NFKC"))

# 以下出力
'''
0          パピプペポ
1         1(2)3.
2       明治大正昭和平成
3    センチ キロ メートル
dtype: object
'''