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オブジェクトも便利なのかもしれませんがまだ慣れない。)

numpyで重み付き平均

つい最近まで、numpyやpandasには重み付き平均を求める関数は無いと勘違いしていて、
必要な時は自分で実装したのを使っていました。

データと重みが numpy の array で渡される場合だけ対応するのであればこのような関数で計算できます。
(listなどにも対応しようと思うとこれでは動きません)


def weighted_mean(data, weights):
    return np.sum(data * weights) / np.sum(weights)

しかしよくよく調べてみると、いつも使っている numpy.mean のほかにも、
numpy.averageという関数があって、これは引数にweightsを渡せるでは無いですか。
(averageの方が常に優秀というわけではなく、 meanにしか無い引数もあります。)

参考:
numpy.average
numpy.mean

numpy.average を使うと重み付き平均を手軽に計算できます。
せっかくなので適当なデータについて上の関数と結果を見比べましょう。
(ついでに計算直書きしたのも並べて確認しました。)


import numpy as np
data = np.array([1, 3, 7])
weights = np.array([5, 12, 2])
print(weighted_mean(data, weights))
# 2.8947368421052633
print(np.average(data, weights=weights))
# 2.8947368421052633
print((1*5+3*12+7*2)/(5+12+2))
# 2.8947368421052633

完全一致してますね。

Pythonでワードクラウドを作成する

テキスト中の単語の出現頻度を可視化する方法として、ワードクラウド(word cloud)というのがあります。
要は頻出の単語ほどでっかい面積を占拠できるように可視化する方法ですね。

これをPythonで作る時、その名もズバリ wordcloudというライブラリがあり、非常に手軽に使うことができます。

リポジトリ: amueller/word_cloud

インストールはpipでできます。


$ pip install wordcloud

20newsgroups のデータを使ってやってみましょう。
あまりにもごちゃごちゃすると意味がわからないので、カテゴリを一個に絞ってやってみます。(今回は sci.electronics にしました)
細かいですが、STOPWORDS があらかじめ用意されているのもありがたいですね。


from wordcloud import WordCloud
from wordcloud import STOPWORDS
from sklearn.datasets import fetch_20newsgroups
import matplotlib.pyplot as plt
remove = ('headers', 'footers', 'quotes')
categorys = [
        "sci.electronics",
    ]
twenty_news = fetch_20newsgroups(
                                subset='train',
                                remove=remove,
                                categories=categorys
                            )
raw_data = twenty_news.data
wordcloud = WordCloud(
                            stopwords=STOPWORDS, background_color="white"
                        ).generate(" ".join(raw_data))

fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(1, 1, 1)
ax.imshow(wordcloud, interpolation='bilinear')
ax.axis("off")
plt.show()

結果がこちらです。