SciPyを使って特定の確率分布にしたがう乱数を生成する

ここまでの数回の記事でいろいろな方法で特定の確率分布に従う乱数を得る方法を紹介してきましたが、
SciPyで生成する方法についてきちんと紹介してないことに気づいたので書いておきます。
numpyについてはこちらで書いてます。

といってもこれまでの実験中で使っている通り、SciPyのstatsモジュールに定義されている各確率分布ごとに、
rvsという関数があるのでそれを使うだけです。
確率分布が連続であっても、離散であっても同じ名前です。

ドキュメント:
(連続の例)正規分布の場合 scipy.stats.norm
(離散の例)二項分布の場合 scipy.stats.binom

最近の記事でも一様分布からのサンプリングで使いまくってるでほぼ説明不要なのですが、
以下の例のように各確率分布に従う乱数を得ることができます。


from scipy.stats import norm
from scipy.stats import binom
print(norm.rvs(loc=2, scale=5, size=5))
# [-1.46417053 -2.76659505  0.80006028  4.83473226  4.05597588]
print(binom.rvs(n=20, p=0.3, size=10))
# [9 7 7 5 9 5 8 4 9 8]

rvs ってなんの略だろう? 特にsは何かということが気になって調べていたのですが、
今の所、明確な答えは見つけられていません。(なんの略語かわからないと覚えにくい。)

sampling かな? と思っていたこともあるのですが、GitHubでソースを見ると _rvs_sampling ってのも登場するので違いそう。
チュートリアルの中に、
random variables (RVs)という記載があるので、random variablesの略である可能性が一番高いかなと思います。

Pythonの数値を2進法、8進法、16進法の表記に変換する

以前の記事で、 pythonで、2進法/8進法/16進法で数値を定義する というのを書きました。
今回はその逆に、10進法の数値を2進法、8進法、16進法での表記に変換します。
(なお、結果的にデータ型は文字列になってしまいます。)

これには、組み込み関数として実装されている bin(), oct(), hex()を使います。


num = 23456
print(bin(num))
# 0b101101110100000
print(oct(num))
# 0o55640
print(hex(num))
# 0x5ba0

先頭の0b等の有無の調整や、16進法でアルファベットを大文字/小文字のどちらで表記するかの制御なども行いたい場合、
format関数が使えます。
ずらっと並べると次のような感じ。


print(format(num, "b"))
# 101101110100000
print(format(num, "#b"))
# 0b101101110100000
print(format(num, "o"))
# 55640
print(format(num, "#o"))
# 0o55640
print(format(num, "x"))
# 5ba0
print(format(num, "#x"))
# 0x5ba0
print(format(num, "X"))
# 5BA0
print(format(num, "#X"))
# 0X5BA0

16進法(hex)の場合に、hではなくxを使うところに注意が必要です。

globで手軽にファイル名の一覧を取得する

特定のディレクトリの配下にあるファイルの一覧が欲しい場面というのはよくあります。
サブディレクトリの探索等少々高度なことをする時はもっと違うライブラリを使ったほうがいいのですが、
特定ディレクトリ直下の特定のパターンのファイル名のファイルの一覧を取得する時などは、
glob を使うと便利です。

ドキュメント: glob — Unix 形式のパス名のパターン展開

次の例はカレントディレクトリ直下のテキストファイルをリストアップしたもの。


import glob
glob.glob("./*.txt")
# ['./text1.txt', './text2.txt']

ご覧の通り、ワイルドカードとして*が使えます。また、?も使えます。
パスの指定は相対パス、絶対パスの両方に対応していて、イメージ通りの挙動をしてくれるのでとても手軽です。

scipyで定積分

タイトルの通り、scipyで定積分を計算する方法の紹介です。

とりあえず今回は $\frac{4}{1+x^2}$ を 区間$[0,1]$で積分しみてみましょう。
なお、この答えは$\pi$になります。

scipyで定積分をする時は integrate モジュールに定義されている、quad という関数を使います。
ドキュメント: scipy.integrate.quad


import scipy.integrate as integrate


def f(x):
    return 4/(1+x**2)


print(integrate.quad(f, 0, 1))
# (3.1415926535897936, 3.4878684980086326e-14)

ご覧通り、結果はタプルで戻ってきます。
一つ目の要素が積分の答えであり、確かに円周率ぽい値になっています。
そして、二つ目の要素は、誤差の推定値です。

これはscipyが代数的に積分を計算しているのではなく、
数値計算で結果を返しているため、どうしても誤差が発生するためです。

matplotlibでTableau風の色を使う

滅多なことでは使う機会がないと思うのですが、matplotlibで使える色名の中に、
Tableauのカラーパレット(Tableau 10)の色を指定するものがあるのを見つけたので紹介します。

参考:matplotlib.colors
こちらのページにひっそりと次の記載があります。

one of the Tableau Colors from the ‘T10’ categorical palette (the default color cycle): {‘tab:blue’, ‘tab:orange’, ‘tab:green’, ‘tab:red’, ‘tab:purple’, ‘tab:brown’, ‘tab:pink’, ‘tab:gray’, ‘tab:olive’, ‘tab:cyan’} (case-insensitive);

定数も用意されているのでそれを確認してみましょう。


import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

mcolors.TABLEAU_COLORS
'''
OrderedDict([('tab:blue', '#1f77b4'),
             ('tab:orange', '#ff7f0e'),
             ('tab:green', '#2ca02c'),
             ('tab:red', '#d62728'),
             ('tab:purple', '#9467bd'),
             ('tab:brown', '#8c564b'),
             ('tab:pink', '#e377c2'),
             ('tab:gray', '#7f7f7f'),
             ('tab:olive', '#bcbd22'),
             ('tab:cyan', '#17becf')])
'''

matplotlibで色を指定する部分に”tab:blue”と入れてやればいつも見慣れたTableauの青が表示されます。
(“tab:olive”は何か違うような気がするのですが僕の環境のせいでしょうか)

せっかくなので、10本の棒グラフを用意して使ってみましょう。


x = range(1, 11)
y = range(10, 0, -1)
fig = plt.figure()
ax = fig.add_subplot(111, title="Tableau Colors")
ax.bar(x, y, color=mcolors.TABLEAU_COLORS)
plt.show()

出力はこちら。

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”

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
'''

正規化形式別のユニコード正規化の振る舞いの違いを見てみる

前回の記事でユニコード正規化を紹介し、NFD/NFC/NFKD/NFKCの4種類の形式があるという話をしました。
今回はそれぞれの形式で正規化した時の振る舞いを見ていこうと思います。

元々、各形式の厳密な定義の話をしようと結構前から調べていたのですが、
正準等価の方(NFDとNFC)がまだ自分の中で腑に落ちてないので、今回は色々動かして結果を眺めることにします。
互換等価の方は、意味は同じで見た目が違う文字、というざっくりとした理解で大丈夫のようです。(たぶん)

ちなみに、4つの中でどれを使えば良いか迷ってるだけ、という人は NFKC を使えば大丈夫だと思います。
それでは、どの型で正規化されるかによって結果が変わる文字をいくつか取り上げて挙動を見てみましょう。

一つ目はひらがなの「が」です。
NFC/NFKCでは特に変化がなく、 NFD/NFKDでも、見た目は変化してないのですが、文字コードに直すと「か」と「濁点」に分解されていることがわかります。


import unicodedata
forms = ["NFC", "NFD", "NFKC",  "NFKD"]

text = "が"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )
    
# 以下出力
'''
原型: が   文字コード: b'\xe3\x81\x8c'
NFC : が   文字コード: b'\xe3\x81\x8c'
NFD : が   文字コード: b'\xe3\x81\x8b\xe3\x82\x99'
NFKC : が   文字コード: b'\xe3\x81\x8c'
NFKD : が   文字コード: b'\xe3\x81\x8b\xe3\x82\x99'
'''

つぎは半角カタカナの「カ」です。
NFKC/NFKDでは全角のカタカナに正規化してくれていることがわかります。
NFC/NFDは変化なしです。


text = "カ"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )

# 以下出力
'''
原型: カ   文字コード: b'\xef\xbd\xb6'
NFC : カ   文字コード: b'\xef\xbd\xb6'
NFD : カ   文字コード: b'\xef\xbd\xb6'
NFKC : カ   文字コード: b'\xe3\x82\xab'
NFKD : カ   文字コード: b'\xe3\x82\xab'
'''

次は、「ガ」です。
NFC/NFDは変化しないのは「カ」の時と同じですが、
NFKCとNFKDで、文字コードが違います。NFKDの方は「カ」と「濁点」に分解されたままですが、
NHKCではそれが結合されています。


text = "ガ"
print("原型:", text, "  文字コード:", text.encode("utf-8"))
for form in forms:
    print(
        form,
        ":",
        unicodedata.normalize(form, text),
        "  文字コード:",
        unicodedata.normalize(form, text).encode("utf-8")
    )

# 以下出力
'''
原型: ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFC : ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFD : ガ   文字コード: b'\xef\xbd\xb6\xef\xbe\x9e'
NFKC : ガ   文字コード: b'\xe3\x82\xac'
NFKD : ガ   文字コード: b'\xe3\x82\xab\xe3\x82\x99'
'''

互換等価性は正準等価性より広い概念で、正準等価であるものは何であれ互換等価とのこと(参考:wikipedia – Unicodeの等価性)
なので、NFKCやNFKDが元の文字列と同じで、NFCやNFDは元の文字列と異なる、という例はおそらく無いのでしょう。

このほか特殊記号など色々試してみましたが、全体的に NFKC が僕が欲しい結果になることが多かったので、
普段はこれを利用しています。

Pythonでユニコード正規化

テキストデータを機械学習にかける時などによく行われる処理に、ユニコード正規化(Unicode normalization)と呼ばれるものがあります。
要するに同じ意味(等価)な文字や文字列の内部表記を統一する一種の名寄せのようなものです。
なお、正規化にはNFD/NFC/NFKD/NFKCの4種類があります。
Unicode正規化 出典: フリー百科事典『ウィキペディア(Wikipedia)』

日本語で使われる等価な文字の例をあげると、 アとア、①と1 ㍼と昭和、㌕と、キログラム、などがあります。

Pythonには unicodedata という標準ライブラリが用意されており、
手軽にユニコード正規化を行えます。

参考:unicodedata — Unicode データベース

使うのはunicodedata.normalize("正規化形式", "正規化したい文字列") という関数です。

上に上げていた例でやってみるとこのような感じになります。 


import unicodedata
print(unicodedata.normalize("NFKC", "ア ① ㍼ ㌕"))
# ア 1 昭和 キログラム

ちなみに “㋿” (令和) には対応していませんでした。
僕の環境のライブラリが古いのかもしれません。
(そもそも人によってはブラウザでも正しく表示されていないかもしれません。)

numpyでビンを作成する

以前の記事で、pandas.cutを使ってデータをビンに区切る方法を紹介しました。
参考:pandasで数値データを区間ごとに区切って数える

これはこれで便利なのですが、似たようなことを行う関数がnumpyにも実装されていたのでその紹介です。
個人的にはこちらの方が好きです。
numpy.digitize

引数は次の3つをとります。
x : 元のデータ
bins : 区切り位置のリスト (1次元のリストで単調増加か単調減少のどちらかであることが必須)
right : 統合をどちらの端に含むか。(binsが単調増加か減少かも関係するのでドキュメントの説明を見ていただくのが確実です)

これを使うと、xの各データが、binsで区切られたなんばんめの区画に含まれるのかのリストを返してくれます。
binsは配列で渡すので等間隔でなくても使えます。

動かしてみたのがこちら。


import numpy as np
x = np.random.randint(200, size=10) - 100
print(x)
# [ 20  77  23 -50 -18 -80 -17  45  66  83]
print(np.digitize(x, bins=[-50, -10, 0, 10, 50]))
# [4 5 4 1 1 0 1 4 5 5]

bins に 5つの要素があるので、両端も含めて6つのbin(0〜5)にデータが区切られます。
例えば最初の20は、10<=20<50 なので、4番目の区画ですね。
right を省略し、左側に統合がついているので、
-50<=-50<-10 となり、-50は1番目の区画に入るということも確認できます。

Interval オブジェクトではなく、ただの数列で値を返してくれるのもありがたい。
(Intervalオブジェクトも便利なのかもしれませんがまだ慣れない。)