InteractiveShell オブジェクトを利用してマジックコマンドを実行したり変数を操作したりする

いつも使っているJupyterに関する話です。Jupyterの中では、InteractiveShellというオブジェクトが使われていて、これを利用することでさまざまな高度な操作を行うことができます。

多くのことができるのですが、最近これを使って変数を操作する機会があったのでその辺を紹介していきます。

そもそも、InteractiveShell オブジェクトとは?という話ですが、これはIPython(Jupyter Labのバックエンドとして動作する対話的なPythonシェル)の中心的なクラスです。このオブジェクトを通じて、現在のセッションの状態を取得したり、マジックコマンドを実行したりすることができます。

このInteractiveShellはget_ipython() という関数を使うことでそのnotebook自身で取得し、アクセスすることができます。

マジックコマンドの動的な実行

このInteractiveShellオブジェクトを使うと例えば、マジックコマンドをプログラムで実行したりできます。

例えば、%pwd っていう現在のでディレクトリを取得するマジックコマンドがありますが、これが次のようにして実行できます。

ip = get_ipython()
ip.run_line_magic('pwd', '')

(pwdをメソッドとして実行するメリットはなかなか薄いので例が適切でなくてすみません。)

マジックコマンドをそのままマジックコマンドととして実行するのと比べると、結果を文字列として取得して以降の処理で使えるとか、実行するコマンドをプログラムで動的に変化させて実行させることができるといった利点があります。

もちろん、 run_cell_magic() って関数もあるのでセルマジックも実行できますよ。

変数の取得や設定

今回紹介するもう一つの使い方は変数の取得や追加です。名前空間の操作といった方がわかりやすいかもしれません。

InteractiveShellオブジェクトを使って、upyter Labのセッション内で定義されている変数を取得したり、新しい変数を設定したりすることができます。

変数の取得は user_ns 属性を使います。現在定義されている変数が全部辞書として入っています。デフォルトで入ってるJupyter/IPython固有の変数も多いので、自分が宣言した覚えのないものもあると思います。

例えば次のようにして使います。

x = 10
y = "Hello"

ip = get_ipython()
print(ip.user_ns['x'])
# 10
print(ip.user_ns['y'])
# "Hello"

変数の取得としてはあまり用途ないかもしれないですね。どちらかというと既に宣言されているかどうかのチェックとかで使えるかもしれません。

逆に、次のようにして新しい変数を定義することもできます。

ip = get_ipython()
ip.push({'z': "新しい変数"})

print(z)
# 新しい変数

この例だけ見てると、 z=”新しい変数” って直接書くのと同じやん、と思えますが、この方法の利点は変数名の方もプログラムで動的に指定できることです。

まぁ、 exec(“z=’新しい変数'”) ってすれば変数名を動的に変えられるので固有のメリットというわけではないのですが、execは本当にリスクの大きな手段なのでそれよりは安全に使えるという利点があると思います。

この変数の名の指定は、関数やマジックコマンドの中で使うと、例えばユーザーが指定した変数名に結果を格納するといった応用ができます。

最近、別の場所で書いているnote記事の中で自分が自作したマジックコマンドを紹介したのですが、そこで使っているクエリの結果を引数で指定した変数に格納するのにこのip.pushを使っています。

参考: Snowflakeに手軽にSQLを打てるJupyterマジックコマンドを作ってみた

ip.push を使わずに、
ip.user_ns[‘z’] = “新しい変数”
みたいに、user_nsオブジェクトに直接突っ込んでも同じことができますので合わせて覚えておいてください。

InteractiveShellオブジェクトでできることのごく一部ではありますが、今回の記事としては以上となります。

二つの区間の重複を判定する効率的な方法

はじめに

ちょっと重い記事が続いてたので今回は小ネタです。

二つの区間データ、要するに数値の区間であれば最小値と最大値のペア、時刻の区間であれば開始時間と終了時間のデータが二つあったときに、それらの区間に重複があるかどうかをサクッと判定するアルゴリズムを紹介します。昔どこかでみた覚えがあるのですが、久々に実装で必要になったときにちょっとだけ悩んだのでそのメモです。

結論

結果だけ知りたい人向けに先に結論だけ書いときます。

区間[s1, e1]と[s2, e2]に重複があるかどうかは次の2つの方法で判定できます。

方法1: $(s1 \leq e2) \land (s2 \leq e1)$ ならばその2区間には重複があります。

方法2: $\max(s1, s2) \leq \max(e1, e2)$ ならばその2区間には重複があります。

両方の方法のメリットデメリットですが、方法1の方が計算速度が速いです。大小比較2個と論理演算ですからmax関数呼び出すより有利ですね。ただ、方法2の方は、応用として、二つの区間に重複があった場合、$\max(e1, e2) – \max(s1, s2)$で重複区間の長さを算出できます。(値が負になったら重複無し。)

(max関数がそんな極端に遅いってこともなく、わざわざ時間測定しない限り体感することもない速度差ではあるのであくまでも参考にどうぞ。)

方法1の導出

これだけで終えてしまうと記事量としてあまりに少ないのと、結論だけ書いてるとすぐ忘れそうなので、真面目に導出を説明しておきます。まずは方法1の方からです。

まず、二つの区間の相対的な位置関係は次の6パターンが存在します。(説明の単純化のため。s1,s2,e1,e2は全て異なる値とします。)

  1. 区間1全体が区間2より小さい。つまり、 $s1 < e1 < s2 < e2$.
  2. 区間1全体が区間2より大きい。つまり、 $s2 < e2 < s1 < e1$.
  3. 区間1が区間2に内包される。つまり、$s2 < s1 < e1 < e2$.
  4. 区間1が区間2を内包する。つまり、$s1 < s2 < e2 < e1$.
  5. 区間1の後半と区間2の前半が重なる。つまり、$s1 < s2 < e1 < e2$.
  6. 区間1の前半と区間2の後半が重なる。つまり、$s2 < s1 < e2 < e1$.

上記の6パターンのうち、1,2 が区間に重複がなく、3,4,5,6は区間に重複があります。

ここで、「そうか!3,4,5,6のどれかを満たすことを確認すればいいんだ!」と判断して4パターンの論理式を書いてorで繋ぐ、としないことがコツです。

区間に重複がないパターンの方が2種類しかなくて判定が単純なんですよね。そして、区間の定義から$s1<e1$と$s2<e2$はわかってるので、あと確認するのはe1とs2, e2とs1の代償関係だけなんですよ。

ということで、 $(e1 < s2) \lor (e2 < s1)$ であれば二つの区間に重複はないと言えます。

これの否定を考えると、ドモルガンの法則から方法1としてあげた式が導かれます。

方法2の導出

続いて方法2の導出です。

これは、先ほど挙げた3,4,5,6の4パターンの不等式を眺めると見つかる法則性なのですが、左の2辺はs1,かs2で、右の2辺はe1, e2なんですよね。

これを素直に数式に落とすと、$\max(s1, s2) \leq \max(e1, e2)$ となります。等号が成立するのはただ1点だけが重複する場合。

そして、区間が重複する場合は、$\max(s1, s2)$は重複区間の開始点であり、$\max(e1, e2)$は重複区間の終了点なので、この2項の差を取ると重複部分の長さも得られます。

まとめ

そんなに難しい計算ではなく、覚えてさえればサクッと実装できる式ではありますが、重複のパターンって4種類あるよなぁと考え始めてしまうと意外に手間取ります。

結果を丸暗記しなくても、区間が重複しないパターンは2個しかなくてそれを否定したら簡単だってことを頭の片隅にでも置いといてもらえると幸いです。

ジニ不純度について

ここ数週間にわたってエントロピー関連の話が続きましたので、ついでみたいになるのですが決定木の評価関数として同じようによく使われるジニ不純度についても紹介しておきます。
ジニ係数(はじパタなど)や、ジニ指数(カステラ本など)といった呼び名もあるのですが、経済学で所得格差の指標に用いられるジニ係数と紛らわしくなるので、この記事ではジニ不純度と呼びます。

ジニ不純度の定義

ジニ不純度は、決定木やランダムフォレストなどの機械学習のアルゴリズムにおいて、ノードの純度を計測するための指標の一つとして用いられます。要するに、そのノードに分類される観測値のクラスの内訳が全部同じだとなると不純度は小さく、同じノードに複数のクラスの観測値が混在してたら不純度は高くなります。エントロピーと似た挙動ですね。

数式としては次のように定義されます。$n$クラスに分類する問題の場合、$i$番目のクラスのサンプル比率を$p_i$とすると、ジニ不純度は次のように定義されます。

$$I = 1 – \sum_{i=1}^n p_i^2.$$

例えば、3クラスの分類問題を考えると、あるノードの観測値が全部同じクラスだったとします。すると、$p_1 = 1, p_2=0, p_3 = 0$みたいになるのですが、この時$I=0$と、不純度が0になります。また、3クラスが均等に混ざっていた場合、$p_1 = p_2 = p_3 = 2/3$となり、時に不純度は$I = 2/3$となります。$n$クラスの分類問題の場合、この$1-1/n$が最大値です。

もう7年近く前になりますが、初めてこの定義式を見た時は、1から確率の平方和を引くことに対してちょっと違和感を感じました。ただ、これは計算を効率的にやるためにこの形にしているだけで、実際は次の形で定義される値と考えておくと納得しやすいです。

$$I = \sum_{i=1}^n p_i (1-p_i).$$

これ展開すると、$\sum_{i=1}^n p_i – p_i^2$となり、$\sum_{i=1}^n p_i =1$ですから、先に出した定義式と一致することがわかります。

ジニ不純度の解釈

それでは、$p_i(1-p_i)$とは何か、という話なのですが解釈が二つあります。

まず一つ目。ある観測値がクラスiであれば1、クラスi出なければ0というベルヌーイ試行を考えるとその分散がこの$p_i(1-p_i)$になります。これを各クラス分足し合わせたのがジニ不純度ですね。

クラスiである確率が0の場合と1の2種類の場合がまじりっけ無しで不純度0ということでとてもわかりやすいです。

もう一つは、そのノードにおける誤分類率です。あるノードにおける観測値を、それぞれ確率$p_i$で、クラス$i$である、とジャッジすると、それが本当はクラス$i$ではない確率が$(1-p_i)$ありますから、誤分類率は$\sum_{i=1}^n p_i (1-p_i)$となります。

あるノードの観測値のクラスが全部一緒なら誤分類は発生しませんし、$n$クラスが均等に含まれていたら誤分類率は最大になりますね。不純度の定義としてわかりやすいです。

エントロピーと比較した場合のメリット

決定木の分割の評価として使う場合、エントロピーとジニ不純度は少しだけ異なる振る舞いをし、一長一短あるのですが、ジニ不純度には計算効率の面で優位性があるとも聞きます。というのも、$p\log{p}$より$p^2$のほうが計算が簡単なんですよね。scikit-learnがginiの方をデフォルトにしているのはこれが理由じゃないかなと思ってます。(個人的な予想ですが。)

ただ、実際に実験してみると計算速度はそこまで大差はないです(np.log2の実装は優秀)。なんなら自分で書いて試したらエントロピーの方が速かったりもします。とはいえ、$p\log{p}$の計算はちゃんとやるなら、$p=0$ではないことをチェックして場合分けを入れたりしないといけないのでそこまできちんと作ったらジニ不純度の方が速くなりそうです。

Pythonでの実装

数式面の説明が続きましたが、最後に実装の話です。実は主要ライブラリではジニ不純度を計算する便利な関数等は提供されていません。まぁ、簡単なので自分で書けば良いのですが。

scikit-learnのソースコードを見ると、ここで実装されて内部で使われていますね。

確率$p$のリストが得られたら、全部二乗して足して1から引けば良いです。
$1/6+1/3+1/2=1$なので、確率のリストは$[1/6, 1/3, 1/2]$としてやってみます。

import numpy as np


p_list = np.array([1/6, 1/3, 1/2])
print(1-(p_list**2).sum())
# 0.6111111111111112

まぁ、確かにこれならわざわざライブラリ用意してもらわなくてもって感じですね。

割合ではなく要素数でデータがある場合は全体を合計で割って割合にすればよいだけですし。

以上で、ジニ不純度の説明を終えます。エントロピーと比べて一長一短あるのでお好みの方を使ってみてください。

Pythonによる各種エントロピーや相互情報量の計算

エントロピーや相互情報量の記事が続いていますが、今回の記事で計算の実装方法を紹介して一旦区切りとします。

エントロピーも相互情報量も数式はそこまで難しくないので、numpyで定義通り計算しても良いのですが、エントロピー関係はSciPyに、相互情報量はscikit-learnに用意されているので今回の記事ではそれを使っていきます。

計算対象のデータは、[“a1”, “a2”, “a1”, “a1”, “a2”] みたいにローデータの一覧で保有している場合もあれば、”a1″が3個で”a2″が2個のようにカウント済みのものがある場合もあると思うのでそれぞれ説明していきます。

エントロピーの計算

まず一番基本的なエントロピーの計算からです。これは、scipy.stats.entropy メソッドを使います。
参考: scipy.stats.entropy — SciPy v1.11.3 Manual

基本的な引数はpkなので、確率の一覧を渡すのが想定されていますが、和が1でないなら1になるように正規化してくれるのでサンプルがある場合は個数を渡しても大丈夫です。また、base引数で対数関数の底を指定でき、デフォルトが$e$なので、情報理論で使う場合は$2$を指定しましょう。

やってみます。

import numpy as np  # データ作りに利用
import pandas as pd  # データ作りに利用
from scipy.stats import entropy


pk = np.array([1/2, 1/3, 1/6])  # 確率の一覧が得られた場合。
print(entropy(pk, base=2))
# 1.459147917027245

count_list = np.array([3, 2, 1])  # データの個数の場合
print(entropy(count_list, base=2))
# 1.4591479170272446

# カウント前のデータの一覧がある場合
data_sr = pd.Series(["a1", "a1", "a1", "a2", "a2", "a3"])
# value_counts()で数えあげたものをentropyに渡す
print(entropy(data_sr.value_counts(), base=2))
# 1.4591479170272446

結合エントロピーの計算

次は結合エントロピーです。エントロピーを単純に2次元に拡張したやつですね。(条件付きエントロピーではないので注意してください、

例えば次のような例を考えましょうか。

b1b2
a141
a223

結合エントロピーの場合はですね、元のカウントデータを2次元から1次元に並び替えて渡します。

matrix_data = np.array([[4, 1], [2, 3]])
print(matrix_data)
"""
[[4 1]
 [2 3]]
"""

# ravel か flattenで1次元化して計算する
print(entropy(matrix_data.ravel(), base=2))
# 1.8464393446710157

# 標本データがある場合
df = pd.DataFrame({
    "A": ["a1", "a1", "a1", "a1", "a1", "a2", "a2", "a2", "a2", "a2"],
    "B": ["b1", "b1", "b1", "b1", "b2", "b1", "b1", "b2", "b2", "b2"],
})

# カウントしたデータを使う
print(df.groupby(["A", "B"]).size())
"""
A   B
a1  b1    4
    b2    1
a2  b1    2
    b2    3
dtype: int64
"""

print(entropy(df.groupby(['A', 'B']).size(), base=2))
# 1.8464393446710157

条件付きエントロピー

次は条件付きエントロピーです。残念なことなのですが、メジャーなライブラリでは条件付きエントロピー専用の関数は提供されていません。

そこで、$H(A|B) = H(A, B) – H(B)$などのエントロピー間の関係式を使って計算することになります。相互情報量も含めて、$H(A|B) = H(A) – I(A; B)$などで計算してもいいのですが、SciPyで完結できるので最初の式のほうが良いでしょう。

先ほどの表データをサンプルとします。$H(B)$については、表データを縦に足し合わせてBだけのカウントデータを作って計算します。

data_B = matrix_data.sum(axis=0)
print(data_B)
# [6 4]

# H(B)の計算
entropy_B = entropy(data_B, base=2)
print(entropy_B)
# 0.9709505944546688

# H(A, B)の計算
joint_entropy = entropy(matrix_data.ravel(), base=2)
print(joint_entropy)
# 1.8464393446710157

# H(A|B) = H(A, B) - H(B)
conditional_entropy_A_given_B = joint_entropy - entropy_B
print(conditional_entropy_A_given_B)
# 0.8754887502163469

# 標本データがある場合
entropy_B = entropy(df["B"].value_counts(), base=2)
joint_entropy = entropy(df.groupby(["A", "B"]).size(), base=2)

conditional_entropy_A_given_B = joint_entropy - entropy_B
print(conditional_entropy_A_given_B)
# 0.8754887502163469

以上で、3種類のエントロピーが計算できました。

相互情報量

最後に相互情報量の計算方法です。

$I(A; B) =H(A)-H(A|B)$など複数の表現方法があるので、ここまでに計算してきた値から算出することもできます。

entropy_A = entropy(df["A"].value_counts(), base=2)
print(entropy(df["A"].value_counts(), base=2) - conditional_entropy_A_given_B)
# 0.12451124978365313

ただ、scikit-learnに専用のメソッドがあるのでこちらの使い方も見ておきましょう。
参考: sklearn.metrics.mutual_info_score — scikit-learn 0.18.2 documentation

引数は、mutual_info_score(labels_truelabels_predcontingency=None)
となっており、標本データを受け取るのが標準的な使い方で、その第一,第二引数をNoneにしてcontingency引数にカウントデータを渡すこともできます。(contingencyがNoneでない場合はこれが優先されて、先の二つの引数が無視されます。)

1点注意しないといけないのは、entropyと違って対数の底が指定できず、自然対数に固定されてしまうことです。底を$2$で考えたい場合は、$\ln{x}/\ln{2} = \log_2{x}$を使って変換が必要です。

from sklearn.metrics import mutual_info_score


# np.log(2)で割ることを忘れない
# カウントした表データがある場合
print(mutual_info_score(None, None, contingency=matrix_data)/np.log(2))
# 0.12451124978365345

# 標本データがある場合
print(mutual_info_score(df["A"], df["B"])/np.log(2))
# 0.12451124978365345

計算の都合上超軽微な誤差がありますが、それ以外は想定通りの値が得られていますね。

以上で、相互情報量も計算できるようになりました。

シャノンの補助定理とその応用

今回も引き続き情報量(エントロピー)関係の記事です。直近、エントロピーに関する記事を複数書いていますが、その中でいくつか証明をしていない性質があります。シャノンの補助定理という面白い定理を使うとそれらが証明できるので見ていきます。

情報量の話なのでこの記事では底を省略して書いた対数関数の底は$2$とします。$\log=log_2$です。一方で、自然対数も使うのでそれは$\ln=\log_e$と書きます。

早速、シャノンの補助定理を見ていきます。

定理

二つの確率事象系$A = \{a_k | k=1,\dots, n \}$と$B = \{b_k | k=1,\dots, n\}$の確率をそれぞれ$p_k$, $q_k$とします。つまり、
$\sum_{k=1}^n p_k = 1$, $\sum_{k=1}^n q_k = 1$です。この時次の関係が成り立ちます。

$$ – \sum_{k=1}^n p_k\log{p_k} \leq – \sum_{k=1}^n p_k\log{q_k}.$$

以上が、シャノンの補助定理の主張です。左辺は$A$のエントロピーで、右辺は対数関数の中身だけ別の確率事象系の確率になっていますね。

証明

定理を証明する前に、自然体数に関する$\ln{x} \leq x – 1$ $(x > 0)$という関係を使うのでこれを先に証明します。

正の実数$x$に対して、$f(x) = x -1 – \ln{x}$ とおきます。すると$f'(x) = 1 – \frac{1}{x}$となります。$f'(x)$の値を見ていくと、 $0 < x < 1$ の時$f'(x)<0$であり、$f'(1)=0$、そして$1 < x$の時$f'(x)>0$となるので、$x=1$で最小値をとり、その値は$f(1) = 1-1-\ln{1} = 0$です。

よって、$f(x) \leq 0$であり、$\ln{x} \leq x – 1$ が示されました。

ここから定理の証明に入ります。定理の左辺から右辺を引いたものを考えて、底の変換をし、先ほどの関係式を適用します。

$$ \begin{align}
– \sum_k p_k \log{p_k} + \sum_k p_k\log{q_k} &= \sum_k p_k \log{\frac{q_k}{p_k}}\\
&=\frac{1}{\ln{2}} \sum_k p_k \ln{\frac{q_k}{p_k}}\\
&\leq \frac{1}{\ln{2}} \sum_k p_k \left(\frac{q_k}{p_k} – 1 \right)\\
&= \frac{1}{\ln{2}} \sum_k (q_k – p_k)\\
&= \frac{1}{\ln{2}} \sum_k q_k – \frac{1}{\ln{2}} \sum_k p_k\\
&=0\end{align}.$$

よって、$ – \sum_{k=1}^n p_k\log{p_k} \leq – \sum_{k=1}^n p_k\log{q_k}$ が示されました。等号が成立するのは$p_k = q_k$の場合です。

この補助定理を使って、エントロピー関連の性質を見ていきます。

応用1. エントロピーの最大値

最初に見ていくのは、「平均情報量(エントロピー)の定義について」で見た、エントロピーの範囲、$0 \le H(a) \le \log{n}$ の最大値の方です。これは、$q_k = \frac{1}{n}$としてシャノンの補助定理を使うと証明できます。

$$H(a) = – \sum_k p_k\log{p_k} \leq – \sum_k p_k\log{\frac{1}{n}} = \log{n}$$

となります。

応用2. 相互情報量が0以上であること

次に証明するのは、「相互情報量について」で最後に書いた、相互情報量が非負の値を取るという話です。

これは、シャノンの補助定理をそのまま2次元の確率分布に拡張して使います。
$k = 1, \dots, n$, $l = 1, \dots, m$とし、$\sum_{k, l} p_{kl} = 1$、$\sum_{k, l} q_{kl} = 1$とすると、

$$- \sum_k \sum_l p_{kl} \log{p_{kl}} \leq – \sum_k \sum_l p_{kl} \log{q_{kl}}$$

ですから、

$$\sum_k \sum_l p_{kl} \log{\frac{p_{kl}}{q_{kl}}} \geq 0$$

となります。ここで、$p_{kl} = P(a_k, b_l)$とし、$q_{kl} = P(a_k) P(b_l)$とすると、

$$\sum_k \sum_l P(a_k, b_l) \log{\frac{P(a_k, b_l)}{P(a_k) P(b_l)}} \geq 0$$

となります。この左辺が相互情報量$I(A; B)$の定義そのものなのでこれが非負であることが示されました。

応用3. 条件付きエントロピーがエントロピーより小さいこと

3つ目はさっきの相互情報量の話から続けて導けるものです。「結合エントロピーと条件付きエントロピー」で、重要な性質として$H(A|B) \le H(A)$が成り立つと証明無しで言いました。

これはシンプルに、$I(A; B) = H(A) – H(A|B)$であり、$I(A; B) \geq 0$なので、移項するだけで、$$H(A) \geq H(A|B)$$ が言えます。

まとめ

この記事ではシャノンの補助定理とその応用をいくつか紹介しました。応用1,2,3で証明したことはそれぞれの記事で証明無しで紹介していたのを個人的にモヤモヤしていたので、それを解消できてよかったです。

エントロピーの最大値についても、相互情報量の非負性についても結構シンプルな主張なのですが、シャノンの補助定理無しで証明するのは結構難しいのでこの定理の偉大さを感じます。

ここしばらくエントロピーの理論面の記事が続いていますが、次あたりでPythonで実際に計算する方法を紹介してこのシリーズを完結させようかなと思っています。