CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: inspect.signature() で関数のシグネチャを調べる

Python は inspect モジュールを使うことでオブジェクトから多くの情報を取得できる。 そして、関数のシグネチャを調べる方法としては、これまで getargspec() 関数が使われることが多かった。 ただ、この関数は Python 3 系では非推奨 (Deprecated) となっている。 そのため、今後は使わない方が良い。

getargspec() 関数が非推奨となった代わりに、今は signature() 関数を使うことが推奨されているようだ。 今回は、この関数の使い方について扱う。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1004
$ python --version
Python 3.5.2

インストール

Python 3.3 以降であれば signature() 関数は標準ライブラリの inspect モジュールに用意されている。 そのため、特に何も意識することなく使うことができる。

しかし、もし使っているのが Python 3.2 未満であれば PyPI からバックポート版をダウンロードして使うことができる。

$ pip install funcsigs

使ってみる

今回は Python の REPL を使って動作を確認していこう。

$ python

早速 inspect モジュールの signature() 関数をインポートしよう。

>>> from inspect import signature

もし、Python 2 系でバックポート版を使っているのであれば、次のようにしてインポートできる。

>>> from funcsigs import signature

まず手始めに、次のような関数 f() を定義しておく。 この関数は foo と bar というふたつのパラメータを受け取る。 bar に関してはデフォルト値が設定されている。

>>> def f(foo, bar='Hello'):
...     pass
...

それでは、signature() 関数に先ほど定義した関数 f() を渡してみよう。 すると Signature というオブジェクトが得られることが分かる。

>>> signature(f)
<Signature (foo, bar='Hello')>

このオブジェクトには parameters という名前で受け取るパラメータの情報が格納されている。 このメンバは次のように順序関係を持った辞書型のオブジェクトとして得られる。

>>> sig = signature(f)
>>> sig.parameters
mappingproxy(OrderedDict([('foo', <Parameter "foo">), ('bar', <Parameter "bar='Hello'">)]))

例えばパラメータの foo について知りたいときは、次のように名前をキーとしてアクセスすれば良い。 得られるのは Parameter というオブジェクトになっている。

>>> sig.parameters['foo']
<Parameter "foo">

このオブジェクトには名前はもちろん、デフォルト値などの情報も入っている。 もしデフォルト値が設定されていないときは inspect.Signature.empty で得られるオブジェクトになる。

>>> sig.parameters['foo'].name
'foo'
>>> sig.parameters['foo'].default
<class 'inspect._empty'>
>>> sig.parameters['bar'].default
'Hello'

その他、パラメータがどのような使われ方をしているかも得られる。 例えば、通常であれば次のように POSITIONAL_OR_KEYWORD になる。

>>> sig.parameters['foo'].kind
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>

それに対し *args, **kwargs といった可変長引数のときは VAR_POSITIONAL や VAR_KEYWORD になる。

>>> def f(*args, **kwargs):
...     pass
...
>>> signature(f)
<Signature (*args, **kwargs)>
>>> sig = signature(f)
>>> sig.parameters['args'].kind
<_ParameterKind.VAR_POSITIONAL: 2>
>>> sig.parameters['kwargs'].kind
<_ParameterKind.VAR_KEYWORD: 4>

また、Python 3 の機能ということもあってアノテーションにも対応している。 次のように引数や返り値にアノテーションをつけた関数を用意して signature() にかけてみる。

>>> def f(foo: str) -> int:
...     return -1
...
>>> signature(f)
<Signature (foo:str) -> int>
>>> sig = signature(f)

すると Parameter からは annotation というメンバでアノテーションに指定した型が得られる。

>>> sig.parameters['foo'].annotation
<class 'str'>

また Signature からも return_annotation というメンバで返り値のアノテーションが取得できる。

>>> sig.return_annotation
<class 'int'>

それ以外にも便利な機能がある。 例えば、その関数を特定の引数で実行したときに、パラメータが引数に対してどのようにバインドされるかを確かめることもできる。 これには Signature#bind() メソッドを使う。

>>> def f(foo, bar='Hello', baz=None, *args, **kwargs):
...     pass
...
>>> sig = signature(f)
>>> sig.bind('foo', baz=1, bar='Bye', hoge=True)
<BoundArguments (foo='foo', bar='Bye', baz=1, kwargs={'hoge': True})>

上記で得られるのは BoundArguments というオブジェクトになっている。 このオブジェクトからは arguments というメンバでバインドした結果が得られる。

>>> bound_args = sig.bind('foo', baz=1, bar='Bye', hoge=True)
>>> bound_args.arguments
OrderedDict([('foo', 'foo'), ('bar', 'Bye'), ('baz', 1), ('kwargs', {'hoge': True})])

上記を応用すると、関数の呼び出しをトレースするようなデコレータを書きやすいなと思った。 次のサンプルコードでは @recording というデコレータを定義している。 このデコレータは修飾した関数をラップして、呼び出し内容と結果をログに自動で残す機能を持っている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

try:
    from inspect import signature
except ImportError:
    from funcsigs import signature

import logging
import functools


LOG = logging.getLogger(__name__)


def recording(f):
    """関数 (メソッド) の呼び出しを記録するデコレータです。

    受け取ったパラメータと返り値をログに出力します。"""
    @functools.wraps(f)
    def _recording(*args, **kwargs):
        # 表面上は元々の関数 (メソッド) がそのまま実行されたように振る舞う
        result = f(*args, **kwargs)
        # デコレーションする関数のシグネチャを取得する
        sig = signature(f)
        # 受け取ったパラメータをシグネチャにバインドする
        bound_args = sig.bind(*args, **kwargs)
        # 関数名やバインドしたパラメータの対応関係を取得する
        func_name = f.__name__
        func_args = ','.join('{k}={v}'.format(k=k, v=v)
                             for k, v in bound_args.arguments.items())
        # ログに残す
        fmt = '{func_name}({func_args}) -> {result}'
        msg = fmt.format(func_name=func_name,
                         func_args=func_args,
                         result=result)
        LOG.debug(msg)
        # 結果を返す
        return result
    return _recording


# デコレーションすると、その関数の呼び出しをログに記録する
@recording
def add(a, b):
    return a + b


def main():
    # デバッグレベルのログも出力する
    logging.basicConfig(level=logging.DEBUG)
    # デコレーションされた関数を呼び出すと自動でログが出力される
    print(add(1, 2))


if __name__ == '__main__':
    main()

上記を実行すると、次のような出力が得られる。

$ python recording.py
DEBUG:__main__:add(a=1,b=2) -> 3
3

見事に関数の呼び出しと、その返り値までが自動でログに記録された。

めでたしめでたし。

Mac OS X に Homebrew で R をインストールする

R は統計の世界でよく使われているプログラミング言語とその実行環境。 Mac OS X なら Homebrew を使うと割りとサクッとインストールできる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1004

下準備

ここでは Homebrew が既にインストールされている状態を仮定する。 もし、まだインストールしていないときは公式サイトの記載にもとづいて入れる。

brew.sh

まずは Homebrew を最新の状態にしておく。 Homebrew では、何をするにしても最初に brew doctor コマンドを打って問題がないことを確認しておくとハマることが少なくなる。

$ brew update && brew doctor && brew upgrade

インストール

(2019-06-19 追記) 現在、R は標準リポジトリに存在するため science リポジトリをタップする必要はないと教えていただきました。 ありがとうございます。

R はデフォルトでは参照されない science リポジトリにあるので、まずは tap しておく。

$ brew tap homebrew/science

あとはパッケージ名に r を指定してインストールするだけ。

$ brew install r

使い方

インストールすると r というコマンドで起動できる。

$ r

R version 3.3.1 (2016-06-21) -- "Bug in Your Hair"
Copyright (C) 2016 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin15.6.0 (64-bit)

R は、自由なソフトウェアであり、「完全に無保証」です。
一定の条件に従えば、自由にこれを再配布することができます。
配布条件の詳細に関しては、'license()' あるいは 'licence()' と入力してください。

R は多くの貢献者による共同プロジェクトです。
詳しくは 'contributors()' と入力してください。
また、R や R のパッケージを出版物で引用する際の形式については
'citation()' と入力してください。

'demo()' と入力すればデモをみることができます。
'help()' とすればオンラインヘルプが出ます。
'help.start()' で HTML ブラウザによるヘルプがみられます。
'q()' と入力すれば R を終了します。

>

zsh を使っているときの注意点

シェルに zsh を使っていると、上記の手順で r が起動できない問題にハマる。

$ echo $SHELL
/bin/zsh

具体的には、インストールするときに使ったコマンドがなぜか実行されてしまう。

$ r
brew install r
Warning: homebrew/science/r-3.3.1_3 already installed

実は zsh では r がシェルの組み込みコマンドになっている。 これを打ち込むと、前回のコマンドを繰り返し実行してしまう。

$ which r
r: shell built-in command

本来の r を起動したいときは、シェルの組み込みコマンドの r を無効にしよう。

$ disable r

しかる後に r と打ち込もう。

$ r

R version 3.3.1 (2016-06-21) -- "Bug in Your Hair"
Copyright (C) 2016 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin15.6.0 (64-bit)

R は、自由なソフトウェアであり、「完全に無保証」です。
一定の条件に従えば、自由にこれを再配布することができます。
配布条件の詳細に関しては、'license()' あるいは 'licence()' と入力してください。

R は多くの貢献者による共同プロジェクトです。
詳しくは 'contributors()' と入力してください。
また、R や R のパッケージを出版物で引用する際の形式については
'citation()' と入力してください。

'demo()' と入力すればデモをみることができます。
'help()' とすればオンラインヘルプが出ます。
'help.start()' で HTML ブラウザによるヘルプがみられます。
'q()' と入力すれば R を終了します。

>

RStudio

お好みに応じて RStudio も入れたりしよう。 こちらは cask リポジトリにある。

$ brew tap caskroom/cask
$ brew cask install rstudio
$ open /Applications/RStudio.app

めでたしめでたし。

統計: 最小二乗法を使って回帰直線を求める

今回は、データセットのある次元の値から別の次元の値を予測する方法の一つとして、最小二乗法というやり方で回帰直線を求めてみる。 とはいえ、いきなり最小二乗法や回帰直線といわれても何が何やらという感じなので、最初はその説明からしていく。

回帰

まず、ある次元の値から別の次元の値を予測するというのは具体的にどういうことだろうか。 例えば、賃貸の部屋の広さと家賃の値段に相関関係があるとしよう。 つまり、広い部屋はそれだけ家賃も高いということで、これは直感に反していない。 だとすれば、部屋の広さと家賃の値段の関係が含まれたデータセットがあるとき、部屋の広さから家賃の値段を予測できるはずだ。

統計や機械学習の世界では、このような問題のジャンルに「教師あり学習」の「回帰」という名前がついている。 ただし、ある次元の値から別の次元の値を予測する問題がすべて回帰と呼ばれるわけではない。 なぜかというと、その次元に含まれるデータの種類によって、やることが異なるため。

データの種類には、大きく分けて二つある。 まず、男性や女性、あるいは好きな食べ物といった、いくつかの種類からひとつを選ぶようなものは質的データという。 それに対し、年齢や一日の摂取カロリーなど、値が連続性を持ったものは量的データという。

ある次元から別の次元を予測するとき、予測する対象が質的データなら「分類」で、量的データなら「回帰」になる。 そして、予測の元ネタになるデータのことを説明変数、予測する値のことを応答変数と呼ぶ。 このとき、説明変数がひとつなら単回帰、複数なら重回帰と呼ばれる。

以上から、今回このエントリでやってみるのは単回帰になる。 回帰直線というのは、説明変数と応答変数の関係を次の一次式で表した直線のこと。

y = \alpha + \beta x

このとき、x が説明変数で y は応答変数を表している。 \alpha\beta は切片と傾きに対応していて、これは回帰係数と呼ばれている。

最小二乗法

最小二乗法というのは、教師データに最もフィットする回帰係数を見つける手法のことをいう。

最小二乗法では、次に示す e_i を最小化するようになっている。 ここで \hat{y_i}y の予測値を表している。 予測値にはハットの記号をつける慣例になっているため。 この e_i のことを残差という。 ようするに、これは真の値と予測値との差のこと。

e_i = y_i - \hat{y_i}

より具体的には、この残差を自乗した合計値が最も小さくなる回帰係数を見つけるのが目的になる。

S(\hat{\alpha}, \hat{\beta}) = \sum_{i = 1}^{N} e_i ^2

これを残差平方和という。 自乗しているのは符号をすべてプラスにするため。

これは e_i を展開すると、こうなる。

S(\hat{\alpha}, \hat{\beta}) = \sum_{i = 1}^{N} ( y_i - \hat{ y_i }) ^2 = \sum_{i = 1}^{N} (y_i - (\hat{\alpha} + \hat{\beta} x_i)) ^2

これを最小にする回帰係数を得る手法を最小二乗法という

具体的な手順

先ほどの式は \hat{\alpha}\hat{\beta} で偏微分して、それが 0 に等しいとすると連立一次方程式に直すことができる、らしい。 が、そこに至るまでの過程はひとまず飛ばして、先人の知恵だけ享受することにする。

まず、必要なのは次の回帰直線を表した一次式。

\hat{y} = \hat{\alpha} + \hat{\beta} x

この中で、まずは \hat{\beta} は、次の式で得られる。

\hat{\beta} = \frac{s_{xy}}{s_x ^2}

ここで、s_{xy}xy の共分散を表している。 つまり、こう。

s_{xy}  = \frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x})(y_i - \bar{y}) = \frac{1}{N} \sum_{i = 1}^{N} x_i y_i - \bar{x} \bar{y}

共分散や相関関係については、前回のエントリに詳しく書いたので見てもらいたい。

blog.amedama.jp

また、s_x ^2x の分散を表している。 つまり、こう。

s_x ^2 = \frac{1}{N} \sum_{i = 1}^{N} (xi - \bar{x}) ^2

\bar{x}x の平均値を表すので、こうだね。

\bar{x} = \frac{1}{N} \sum_{i = 1}^{N} x_i

そして、次に \hat{\alpha} は、次のようにして得られる。

\hat{\alpha} = \bar{y} - \hat{\beta}\bar{x}

\bar{x} と同様に、\bar{y}y の平均値を表している。

\bar{y} = \frac{1}{N} \sum_{i = 1}^{N} y_i

数式がずらずら並んでなんのこっちゃという感じになってきたので、まとめると…

\hat{y} = \hat{\alpha} + \hat{\beta} x

という回帰直線の一次式に含まれる回帰係数 \hat{\alpha}\hat{\beta} は、それぞれ

\hat{\beta} = \frac{s_{xy}}{s_x ^2}

\hat{\alpha} = \bar{y} - \hat{\beta}\bar{x}

という式で得られる、ということ。

実際に計算してみよう

では、上記を Python のプログラムで実際に計算して上手くいくか試してみよう。

サンプルプログラムでは matplotlib を使ってグラフを描いているので、まずはインストールしておく。

$ pip install matplotlib

次が肝心のサンプルプログラム。 中で最小二乗法を使って回帰直線を得ている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import division

from matplotlib import pyplot as plt

import numpy as np


def main():
    # データ点数は 100 にする
    N = 100
    # 0 ~ 10 (-5 ~ +5) の範囲で、ランダムに値を散らばらせる
    R = 10

    x = np.arange(N)
    # y = 50 + x の直線に (-5 ~ +5) でランダム性をもたせる
    y = np.arange(N) + np.random.rand(N) * R - R // 2 + 50

    # 平均値と標準偏差
    xmu, xsigma2 = x.mean(), x.var()
    # 平均値
    ymu = y.mean()

    # 共分散
    sxy = np.sum(x * y) / N - xmu * ymu

    # 回帰係数ベータ
    beta = sxy / xsigma2
    # 回帰係数アルファ
    alpha = ymu - beta * xmu

    # 得られた回帰直線の一次式
    print('y = {0} + {1}x'.format(alpha, beta))

    # グラフに回帰直線をひく
    lsm = np.array([alpha + beta * xi for xi in x])
    plt.plot(x, lsm, color='r')

    # 元データもプロットする
    plt.scatter(x, y)

    # グラフを表示する
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

これを lsm.py という名前をつけて実行してみる。

$ python lsm.py 
y = 49.52693410299069 + 0.9984255719901004x

データにはランダム性を持たせているため、微妙に異なるものの、ほぼ y = 50 + x の一次式が得られている!

また、可視化したグラフは次のようになった。

f:id:momijiame:20161008224310p:plain

データに沿って、ちゃんと回帰直線がひけていることが分かる。

まとめ

  • データセットのある次元から別の次元を予測するのに、回帰という問題のジャンルがある
  • 予測するデータの種類によって問題の呼び方は異なる
    • 質的データなら「分類」になる
    • 量的データなら「回帰」になる
  • 予測の元ネタを説明変数、予測する値のことを応答変数という
  • 説明変数がひとつの次元なら単回帰、ふたつ以上なら重回帰という
  • 回帰のやり方のひとつに最小二乗法という手法がある
    • 最小二乗法では残差平方和を最小化する
    • 予測値と真の値との差を残差という
    • 残差の自乗をすべて足したものが残差平方和
  • 説明変数と応答変数の関係を一次式で表した直線を回帰直線という
    • 回帰直線のパラメータとなる \hat{\alpha}\hat{\beta} を回帰係数という

おまけ

回帰係数の \hat{\beta} は式変形すると別の求め方もできる。

これには、回帰係数 r_{xy}xy の標準偏差を使う。

\hat{\beta} = r_{xy} \frac{s_y}{s_x}

回帰係数は、次のように求める。

r_{xy} = \frac{s_{xy}}{s_x s_y}

ここで、s_{xy} は共分散なので、次のように求める。

s_{xy}  = \frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x})(y_i - \bar{y}) = \frac{1}{N} \sum_{i = 1}^{N} x_i y_i - \bar{x} \bar{y}

s_xs_y は標準偏差なので、こう。

s_x = \sqrt{\frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x}) ^2 }

s_y = \sqrt{\frac{1}{N} \sum_{i = 1}^{N} (y_i - \bar{y}) ^2 }

\bar{x}\bar{y} は平均値なので、こう。

\bar{x} = \frac{1}{N} \sum_{i = 1}^{N} x_i

\bar{y} = \frac{1}{N} \sum_{i = 1}^{N} y_i

これを、先ほどのプログラムに適用してみる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import division

from matplotlib import pyplot as plt

import numpy as np


def main():
    N = 100
    R = 10
    x = np.arange(N)
    y = np.arange(N) + np.random.rand(N) * R - R // 2 + 50

    xmu, xsigma = x.mean(), x.std()
    ymu, ysigma = y.mean(), x.std()

    sxy = np.sum(x * y) / N - xmu * ymu

    rxy = sxy / (xsigma * ysigma)

    beta = rxy * (ysigma / xsigma)
    alpha = ymu - beta * xmu

    print('y = {0} + {1}x'.format(alpha, beta))

    lsm = np.array([alpha + beta * xi for xi in x])
    plt.plot(x, lsm, color='r')

    plt.scatter(x, y)

    plt.xlabel('x')
    plt.ylabel('y')

    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

実行結果は次の通り。

$ python lsm2.py 
y = 50.359765922390736 + 0.9931534165033539x

グラフもこのように。 f:id:momijiame:20161008225924p:plain

どちらでも、大丈夫だね。

統計: 共分散と相関係数でデータセットの相関を調べる

まず、二次元の特徴量をもったデータセットがあるときを考えてみよう。

もし、一方の次元の値が高いときに、もう一方も高い傾向があるときは、両者に正の相関があるという。 反対に、一方の次元の値が高いときに、もう一方は低い傾向があるときは、両者に負の相関があるという。

では、それぞれの次元に正または負の相関があるか否かを調べるには、具体的にどうしたら良いのだろうか。

散布図を描いてみる

それにはまず、散布図を描いてみるという選択肢がある。 x 軸と y 軸に、それぞれの次元の値をプロットするやり方だ。

このとき、例えば正の相関があるなら、値は次のように左下から右上にかけてプロットされる。

f:id:momijiame:20160924161630p:plain

これはつまり x 軸の次元の値が高いときに y 軸の次元の値も高くなることを示す。

反対に、負の相関があるなら、値は次のように左上から右下にかけてプロットされる。

f:id:momijiame:20160924161639p:plain

これはつまり x 軸の次元の値が高いときに y 軸の次元の値は低くなることを示す。

共分散・相関係数という統計量

ただ、散布図を描いただけでは、具体的にどれくらいの相関があるのかが分からない。 そう、相関には強弱がある。

例えば、次のようなふたつの散布図がある。 相関の強弱でいえば、前者の方が後者よりも強い。

f:id:momijiame:20160924161630p:plain

上記の相関は、後者と比較すると強い。

f:id:momijiame:20160924161729p:plain

上記の相関は、前者と比較すると弱い。

では、相関の強弱は具体的にどのようにすれば分かるのだろうか。 それには、相関の強弱を表す統計量を計算することになる。 それが今回紹介する共分散や相関係数といったもの。

共分散

共分散というのは、各次元の値から平均値を引いたもの同士をかけ合わせた上で、総和を取ってデータの点数で割ったもの。 これは、一言で表せば各次元の偏差 (平均値を引いた値) の積 (かけ算した値) の平均値 (総和をデータ点数で割った値) を計算している。

数式で表すと、次のようになる。

{ \displaystyle
s_{xy} = \frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x}) (y_i - \bar{y})
}

上記で \bar{x}\bar{y} は、各次元の平均値を表している。

この共分散という統計量は、正の相関が強いほど数値がプラスに大きくなる。 そして、反対に負の相関が強いほど数値はマイナスに大きくなる。 また、相関が弱ければ数値はゼロに近づく。

例えば、先ほどの散布図であれば、前者は共分散が 835 であるのに対し、後者は 820 だった。

なぜ、そうなるのか?

先ほど、共分散の性質として、正の相関が強いほど数値がプラスに大きく、負の相関が強いほど数値がマイナスに大きくなる、と説明した。 ここでは、「そういうものなんだ」と納得してしまうよりも、理屈から理解しておきたい。

そこで、まずは補助線と補足を入れた次の散布図を見てほしい。

f:id:momijiame:20160924162131p:plain

補助線は x 軸と y 軸の平均値を示している。

各次元の補助線にもとづいて、それぞれの点が意味するところを考えてみよう。 点が、もし x 軸の補助線よりも右側にあれば偏差はプラスとなり、そして左側にあればマイナスになることが分かる。 同様に、点がもし y 軸の補助線よりも上側にあれば偏差はプラスとなり、そして下側にあればマイナスになることが分かる。

先ほどの例で示した散布図では、補助線を引くと第一象限と第三象限に点が集まっていた。 それにもとづいて、各象限における偏差と偏差の積の関係を図示してみよう。

f:id:momijiame:20160924162313p:plain

第一象限においては各次元の偏差がプラスになるため、積はプラスになる。 同様に第三象限においては各次元の偏差がマイナスになるため、積はプラスになる。

上記のようなパターンでは偏差の積はほとんどがプラスになることから、その平均値もプラスになることが分かる。

同じように、第二象限と第四象限に点が集まっているパターンについても考えてみよう。

f:id:momijiame:20160924162442p:plain

先ほどの例から、既にだいたい分かると思うんだけど、このときは偏差の積がマイナスになる。 そのため、偏差の積の平均値を取ると、その値がマイナスになることが分かる。

では、値がまんべんなく分布しているパターンではどうなるか。

f:id:momijiame:20160924162529p:plain

このようなときは、偏差の積がプラスだったりマイナスだったりとまちまちになる。 そのため、平均値を計算するとゼロに近づくというわけ。

ということで、共分散を見れば相関が正なのか負なのか分かることが理解できた。

相関係数への拡張

じゃあ共分散さえ見ておけばいつでもオッケーかというと、そうはいかない。 なぜかというと、共分散はそれ単独では相関の強弱が分かりづらいし、異なるデータセットで比較ができない。 どういうことかというと、共分散は元々のデータセットの値の大小 (単位) に影響を受けてしまう。

例えば、カブトムシとクジラの体長と体高の共分散について考えてみよう。 それぞれの値は、カブトムシが数 cm オーダーだとしたら、クジラは数千 cm オーダーになる。 それぞれの偏差の積の平均値を考えると、値の大きさが全く異なるだろうことが分かる。

相関の強度を異なるデータセットで比べるには、まずは単位に依存しない無名数に変換しないといけない。 それが次に紹介する相関係数だ。 これは、共分散を、各次元の標準偏差の積で割ったもの。

数式で表すと次のようになる。

r = \frac{s_{xy}}{s_x s_y}

s_{xy} が共分散で、s_xs_y が各次元の標準偏差。

省略せずに書くと、こう。

{ \displaystyle
r = \frac
{
  \frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x}) (y_i - \bar{y})}
{
  \sqrt{\frac{1}{N} \sum_{i = 1}^{N} (x_i - \bar{x}) {}^2}
  \sqrt{\frac{1}{N} \sum_{i = 1}^{N} (y_i - \bar{y}) {}^2}
}
}

分母となる各次元の標準偏差は、二乗して平方根を取ることで符号をすべてプラスにしている。 そのため、共分散と比べるとプラスとマイナスの値が互いに打ち消し合うことがない。 つまり、分母の標準偏差の積は共分散が取りうる最大値となることが分かる。 結果として、相関係数は相関の強弱が -1 から 1 の間で得られることになる。

例えば、共分散で相関の強弱を示したときに使った散布図で、同じように相関係数も計算してみよう。 まず、この散布図では相関係数が 0.9897 として得られた。

f:id:momijiame:20160924161630p:plain

続いて、こちらの場合には相関係数は 0.805 だった。

f:id:momijiame:20160924161729p:plain

相関の強弱が、共分散よりも分かりやすく得られている。

また、次のようにふたつの次元が完全に比例するときは、相関係数が 1.0 になる。

f:id:momijiame:20160924163017p:plain

共分散と相関係数の注意点

ただ、共分散や相関係数でふたつの次元の相関関係が分かるとはいえ、それだけに頼るのは避けた方が良い。

例えば、次の散布図を見てもらいたい。

f:id:momijiame:20160924163634p:plain

上記の散布図には、ふたつの次元に明らかな規則性が見て取れる。

では、上記の共分散と相関係数はどうなるだろうか? なんと、どちらもゼロになるのだ。

共分散と相関係数は、あくまでふたつの次元の間に線形な関係があるか否かしか見ることができない。 先ほどのように、人の目でみればあきらかな法則性があったとして、数値の上ではそれが分からない。 そのため、ふたつの次元の間に関係性を見いだそうとするときは、共分散や相関係数だけを確認して終わることは避ける必要がある。

まとめ

  • ふたつの次元の相関関係の強弱は、共分散や相関係数といった統計量を計算することで分かる
  • ただし、それで分かるのは線形な関係があるか否かだけなので、それだけを確認して終わることは避ける必要がある

おまけ

最初のグラフを描くのに使った Python のソースコードは次の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from matplotlib import pyplot as plt

import numpy as np


def main():
    N = 100
    R = 10
    x = np.arange(N) + np.random.rand(N) * R - R // 2
    y = np.arange(N) + np.random.rand(N) * R - R // 2

    xmu, xsigma = x.mean(), x.std()
    ymu, ysigma = y.mean(), y.std()

    covariance = sum([(xi - xmu) * (yi - ymu) for xi, yi in zip(x, y)]) / N
    print('共分散:', covariance)
    correlation_coefficient = covariance / (xsigma * ysigma)
    print('相関係数:', correlation_coefficient)

    plt.scatter(x, y)

    plt.xlabel('x')
    plt.ylabel('y')

    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

依存ライブラリとして matplotlib を使っているので、実行する前にインストールする必要がある。

$ pip install matplotlib

統計: 異なるデータセットの標本を標準得点で比較する

例えば、次のような二種類のデータセットをプロットしたヒストグラムがあったとする。 どちらも一般正規分布となっているようだ。

f:id:momijiame:20160902215826p:plain

このデータセットを、あるテストの点数と捉えてみよう。 本来、テストなら上限値と下限値があるはずだけど、そこは簡略化している。

それぞれのデータセット (テストの得点) は次のようなパラメータになっている。 平均値も標準偏差 (値のバラつき) も異なることがわかる。

    • 平均値: 66
    • 標準偏差: 20
    • 平均値: 122
    • 標準偏差: 41

ある人が、このふたつのテストを受けて、それぞれ別々の点数が得られたとする。 そのとき、ふたつの得点がどれだけ優れているのかを比較するには、どうしたら良いだろうか?

例えば A さんは青のテストで 86 点を取って、緑のテストでは 183 点を取ったとする。 どちらも平均値を大きく上回っていることから、良い点数であることは分かる。 しかし、具体的に、どの程度良い点数なのかを知るには、どうしたら良いか。

標準得点で比較する

上記の A さんが取得した得点は、データセット全体 (母集団) から抽出した標本と見なせるはず。 このように、パラメータの異なる分布の標本同士を比べるには、それを標準得点に加工する必要がある。

標準得点というのは、いくつか種類はあるものの、代表的なものに z スコアがある。 z スコアというのは、標本 (x) から平均値 (\mu) を引いて標準偏差 (\sigma) で割ったものをいう。

z = \frac{x - \sigma}{\mu}

この操作を標準化という。 結果として得られるのは、標本が平均値から標準偏差を基準にして何個分離れているかを表したものになる。

標準化の意味

例えば青のデータセットを例にして、z スコアを使った標準化の意味を考えてみよう。

もし、標本が平均値ちょうど (66) なら、最初に平均値を引いた時点で 0 になる。 それを標準偏差 (20) で割っても 0 のままだ。 もし、平均値から標準偏差ひとつ分だけプラスに離れている (66 + 20 = 86) なら、平均値を引くと 20 になる。 それを標準偏差 (20) で割ると 1 だ。

このように、z スコアを使った標準化では、標本が平均値から標準偏差を基準にしていくつ分離れているかが分かる。

z スコアに標準化した標本同士は、比較できる。 なぜなら、どちらも平均値から標準偏差を基準にして、いくつ分離れているかという同じ尺度になっているため。

例を当てはめて比べてみる

例えば、先ほど A さんが取得した得点について考えてみよう。

まず、青のテストで取った 86 点を z スコアに標準化してみる。

z = \frac{x - \sigma}{\mu} = \frac{86 - 66}{20} = 1.0

次に、緑のテストで取った 183 点も z スコアに標準化する。

z = \frac{x - \sigma}{\mu} = \frac{183 - 122}{41} = 1.4878

ふたつの z スコアを比較すると、緑のテストで取った点数の方が高いことが分かった。

1.0 \lt 1.4878

つまり、緑のテストで取った 183 点の方が、青のテストで取った 86 点よりもすごさでいえば上ということが分かった。

z スコアと偏差値

実は、先ほど計算した z スコアは、一度は耳にしたことがあるはずの、とある指数とも深い関わりがある。 それが、受験などでよく使われる偏差値だ。 これも、標準得点のひとつとなる。

偏差値というのは、実は z スコアを少し加工するだけで作ることができる。 具体的には、z スコアを 10 倍して 50 を足したものが偏差値だ。

z スコアというのは、元々のデータセットをすべてこれに変換すると平均値 (\mu) が 0 で標準偏差 (\sigma) が 1 の分布になる。 それに対し、偏差値は平均値 (\mu) が 50 で標準偏差 (\sigma) が 10 になったものをいう。

平均値を 0 から 50 にするには 50 を足せばいい。 標準偏差を 1 から 10 にするには 10 倍すればいい。 つまり、偏差値は z スコアを 10 倍して 50 を足せば得られるということになる。

Z = 10\frac{x - \sigma}{\mu} + 50

ようするに z スコアが 1.0 というのと、偏差値が 60 というのは本質的に同じものを表している。 まあ、後者の方が数値がある程度大きい分、分かりやすいかもしれない。

一応、最初の A さんの得点を偏差値でも比較しておこう。

まずは青の点数から。

Z = 10\frac{x - \sigma}{\mu} + 50 = 10\frac{86 - 66}{20} + 50 = 60

次に緑の点数。

Z = 10\frac{x - \sigma}{\mu} + 50 = 10\frac{183 - 122}{41} + 50 = 64.878

偏差値は z スコアを元にしているので当たり前のことだけど緑の点数の方がすごいということがわかる。

60 \lt 64.878

つまり、テストの平均値と標準偏差さえ分かれば次の式にもとづいて偏差値は自分で計算できる。

Z = 10\frac{x - \sigma}{\mu} + 50

まとめ

  • 異なるデータセットの標本同士を比較するには標準得点を使う
    • 標準得点にはいくつかの種類がある
  • 最も基本的な標準得点は z スコア
    • z スコアは標本から平均値 (\mu) を引いて標準偏差 (\sigma) で割る
  • z スコアは偏差値の元となる
    • z スコアを 10 倍して 50 を足すと偏差値になる

Mac で USB シリアルケーブルの iBUFFALO BSUSRC0610BS を使う

2021-03-23 追記: macOS Mojave (10.14) 以降、ドライバのインストールは不要になっています。

法人向けのネットワーク機器を使ったりするときなんかは、コンソールを取るのにシリアルケーブルが必要なことがある。 今回は、家で使っている iBUFFALO BSUSRC0610BS を Mac OS X で使う方法についてメモっておく。

商品はこちら。

この製品のドライバはチップメーカーが Web サイトで公開している。

www.ftdichip.com

通常であれば Mac OS X 10.9 and above の x64 (64-bit) を選んでインストールすれば良い。

あるいは Homebrew Cask をインストールしているなら、それ経由で入れることもできる。

$ brew cask install ftdi-vcp-driver

インストールしてケーブルをつなぐと /dev 以下にデバイスが見えるようになる。

$ ls /dev | grep usbserial           
cu.usbserial-FTF6PD08
tty.usbserial-FTF6PD08

あとは、このデバイス経由で通信すれば良い。 後ろに指定している 9600 はボーレート (通信速度) なので、機器に設定されているものを使おう。

$ screen /dev/tty.usbserial-FTF6PD08 9600

機器のログインコンソールが表示されれば成功。

login: 

Mac 用のドライバがない USB シリアルケーブル (のチップ) もたまにあったりするので、使えることが確認できているものは安心できる。

めでたしめでたし。

Python: Fabric をスクリプトに組み込んで使う

Fabric は Python で書かれたデプロイやオペレーションを自動化するためのツール。 Fabric では、タスクと呼ばれるオペレーション内容も Python で書く。

今回は、普段ならコマンドラインツールから使うことが多い Fabric を Python のスクリプトに組み込んで使う方法について書く。 尚、Fabric はリモートのホストに接続して使うことが多いため、そのホストとして Vagrant を使って Ubuntu 16.04 LTS のマシンを用意した。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G1004
$ python --version
Python 2.7.10

インストール

まずは pip を使って Fabric をインストールしておく。

$ pip install fabric

Vagrant で仮想マシンを用意する

ここでは Vagrant がすでにインストール済みであることを想定する。 もし、入っていないときは公式サイトのバイナリか Homebrew Cask などを使ってインストールしてほしい。

まずは Vagrant の設定ファイル (Vagrantfile) を用意する。 イメージは Chef 社の提供する bento リポジトリにあるものを使った。

$ cat << 'EOF' > Vagrantfile
# -*- mode: ruby -*-
Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-16.04"
  config.vm.provider "virtualbox" do |vb|
    vb.cpus = "2"
    vb.memory = "1024"
  end
end
EOF

仮想マシンを起動する。

$ vagrant up

起動できたら vagrant ssh-config コマンドを実行しよう。 ここで Port の項目を確認しておく。 おそらく通常は 2222 が割り振られているはず。 ただ、異なる場合には後述する手順で指定するポート番号を変更する必要がある。

$ vagrant ssh-config
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/amedama/Documents/vagrant/ubuntu1604/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

これは OpenSSH の設定ファイルと同じフォーマットになっている。 上記の設定から、先ほど Vagrant で作った仮想マシンには 127.0.0.1:2222 に SSH することでログインできることがわかる。

さて、これで Fabric を使う対象となる仮想マシンの準備ができた。

Fabric の設定ファイルを用意する

Fabric はデフォルトで fabfile という名前のモジュール (またはパッケージ) を、タスクが記述された処理対象として扱う。 なので fabfile.py という名前でファイルを用意しよう。 Python ではスクリプトファイル (*.py) とモジュールが 1:1 で対応している。 例えば fabfile.py であれば Python インタプリタはそれを fabfile というモジュールとして扱うということ。

それでは、今回の主役となる fabfile.py を用意する。 これには task() 関数として Fabric のタスクが定義されている。 この task() 関数はリモートのホスト上で、通常ユーザ権限を使って uptime コマンドを実行することを示している。 また、main() 関数では task 関数を引数にして execute() 関数を実行することもわかる。

$ cat << 'EOF' > fabfile.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute

def task():
    run('uptime')


def main():
    execute(task, hosts=['vagrant@127.0.0.1:2222'])


if __name__ == '__main__':
    main()
EOF

普段の使い方

Fabric がインストールされていると fab コマンドが使えるようになる。 この fab コマンド経由でタスクを実行するのが Fabric の普段の使い方だ。

先ほど用意した fabfile.py も fab コマンド経由で実行してみよう。 -H オプションでホスト名、--port オプションでポート名、-u オプションでユーザ名を指定する。 最後の引数は実行するタスク名だ。 仮想マシンのログインパスワードを聞かれるので「vagrant」答えよう。

$ fab -H localhost --port 2222 -u vagrant task
[localhost] Executing task 'task'
[localhost] run: uptime
[localhost] Login password for 'vagrant':
[localhost] out:  10:46:20 up 6 min,  1 user,  load average: 0.00, 0.05, 0.02
[localhost] out:


Done.
Disconnecting from localhost:2222... done.

上手く実行できた。

Python スクリプトとして実行する

先ほどは fab コマンドからタスクを実行した。 今度は Python スクリプトからタスクを実行してみよう。

これは、ただ単に python コマンドで fabfile.py ファイルを実行するだけだ。 先ほどと同じようにログインパスワードを聞かれるので「vagrant」と答える。

$ python fabfile.py
[vagrant@127.0.0.1:2222] Executing task 'task'
[vagrant@127.0.0.1:2222] run: uptime
[vagrant@127.0.0.1:2222] Login password for 'vagrant':
[vagrant@127.0.0.1:2222] out:  10:48:08 up 8 min,  1 user,  load average: 0.00, 0.03, 0.01
[vagrant@127.0.0.1:2222] out:

ちゃんと実行できたことがわかる。

実行した fabfile.py の内容をもう一度掲載しておく。 Python スクリプトとして実行したときは、この中の main() 関数が実行される。 そして、その中では execute() 関数にタスクとログイン名、ホスト名、ポート番号を指定していた。 つまり、組み込みで実行したいときはこの execute() 関数を使えば良いということが分かる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute

def task():
    run('uptime')


def main():
    execute(task, hosts=['vagrant@127.0.0.1:2222'])


if __name__ == '__main__':
    main()

環境を設定する

先ほどの例ではパスワードを手動で入力しなければいけなかった。 今度は、そこも自動化してみよう。

次の fabfile.py ではパスワードの手動入力が不要になっている。 ホスト名やユーザ名、パスワードといった Fabric の環境設定は fabric.api.env を指定すれば良い。

$ cat << 'EOF' > fabfile.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from fabric.api import run
from fabric.api import execute
from fabric.api import env

def task():
    run('uptime')


def main():
    env.hosts = ['127.0.0.1:2222']
    env.user = 'vagrant'
    env.password = 'vagrant'
    execute(task)


if __name__ == '__main__':
    main()
EOF

実行してみよう。

$ python fabfile.py
[127.0.0.1:2222] Executing task 'task'
[127.0.0.1:2222] run: uptime
[127.0.0.1:2222] out:  10:48:44 up 8 min,  1 user,  load average: 0.00, 0.03, 0.00
[127.0.0.1:2222] out:

ちゃんと実行できて、今度はパスワードを聞かれることもない。

まとめ

Fabric を Python スクリプトに組み込んで使うときは fabric.api.execute() 関数を使おう。

追記

このやり方にはちょっとした注意点があるため、次の記事で補足している。

blog.amedama.jp