pythonのfilter関数の使い方

前回の記事がmap関数の話だったので、今回は使い方のよく似たfilter関数です。

ドキュメントはこちら。
組み込み関数

基本的な構文は以下の形で、iterable の各要素に functionを適用して、
結果が新なものだけを取り出せます。
filter(function, iterable)

map関数と同様に、戻り値はリストではなくイテレーターになるので最初は少し戸惑いました。
試しに、整数のうち偶数だけ抽出するフィルターを書いてみます。


def f(n):
    return n % 2 == 0


m = filter(f, range(10))
print(m)
# <filter object at 0x114d178d0>
print(list(m))
# [0, 2, 4, 6, 8]
print(list(m))
# []

細かい説明は mapの記事に書いた通りなのですが、
イテレーターを使うために、リストに変換するなり、nextで取り出すなりする必要があり、
一度取り出すともう一回listに変換しようとしても空のリストしか返ってこなくなります。

なお、内包表記でもほぼ同じ処理が実装でき、自分はこちらを使うことが多いです。


[x for x in range(10) if f(x)]
[0, 2, 4, 6, 8]

pythonのmap関数の使い方

前の記事でさらっと使っていたmap関数の使い方の紹介です。

ドキュメントはこちら。
組み込み関数

MathematicaのMapとよく似た関数(と聞いて、「ああ、あれね」と通じる人がどのくらいいらっしゃるのかわかりませんが)であり、
配列などの各要素に関数を順番に適用することができます。

使い方は map(適用したい関数, 配列1, ) です。
僕はpython初心者の頃、関数を適用した結果のリストがすぐ戻ってくると期待していたのに、イテレーターが戻ってきたので結構戸惑いました。

例えば、引数を2乗する関数で試してみましょう。


def f(x):
    return x**2


m = map(f, range(10))
print(m)
# <map object at 0x1156f19b0>

リストにしたければ、型変換してあげる必要があります。
ただし、通常のリストと違い、イテレーターなので、一度最後のデータかまで取り出すと、次の値が取れなくなります。


# 1回目は与えられたリストに関数を適用した結果が戻る
print(list(m))
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 続けて全く同じように呼び出すと空のリストがかえる。
print(list(m))
# []

内包表記でほぼ同じ処理を実装できますが、違いは関数が実行されるタイミングです。
内包表記は、それが定義されたタイミングで計算され、
mapの場合は、値が必要になったタイミングで実行されます。

適用する関数にprint文を仕込んで、様子を見てみましょう。
最初に内包表記の場合、


def g(x):
    print(x)
    return x**2


l = [g(x) for x in range(10)]
# この時点で g が実行されているので、以下が出力される。
'''
0
1
2
3
4
5
6
7
8
9
'''
print(l)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

次にmapの場合。


m1 =  map(g, range(10))
# まだgが実行されてないので、何も出力されない。

# nextを使って、最初の3個の値を取り出すと、その3この値に対してだけ関数gが実行される。
print(next(m1))
'''
0
0
'''
print(next(m1))
'''
1
1
'''
print(next(m1))
'''
2
4
'''

これらの性質により、上手く使えば実行時間やメモリの節約が可能になるそうです。
(それを実感するほど上手く使えたことはほとんどないのですが)
ただ、pythonを使っていく上で、イテレーターの扱いに慣れておくのは有益なので、学んでおいて損はなかったと思ってます。

pythonのthisモジュールに定義されている変数について

以前紹介した The Zen of Python の記事の中で、
thisというモジュールに仕掛けられたEaster Eggを紹介しました。

その記事中ではインポートした瞬間に表示される文字列にしか焦点を当ててませんでしたが、
このモジュールに含まれている関数や定数についてもちょっと面白いので紹介します。

このthisの中で、どんな変数やメソッドが定義されているのか、dir関数で見てみましょう。


import this
# 略

dir(this)
'''
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'c',
 'd',
 'i',
 's']
'''

“__”で始まる特別な値以外に、c, d, i, s の4つの変数が含まれています。
これらのうち、iとcはそれぞれ整数ですがあまり意味はありません。
ちょっと面白いのは、dとsです。

まず、 dの方は次の辞書です。


print(this.d)
# 以下出力
{'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 'G': 'T', 'H': 'U', 'I': 'V', 'J': 'W', 'K': 'X', 'L': 'Y', 'M': 'Z', 'N': 'A', 'O': 'B', 'P': 'C', 'Q': 'D', 'R': 'E', 'S': 'F', 'T': 'G', 'U': 'H', 'V': 'I', 'W': 'J', 'X': 'K', 'Y': 'L', 'Z': 'M', 'a': 'n', 'b': 'o', 'c': 'p', 'd': 'q', 'e': 'r', 'f': 's', 'g': 't', 'h': 'u', 'i': 'v', 'j': 'w', 'k': 'x', 'l': 'y', 'm': 'z', 'n': 'a', 'o': 'b', 'p': 'c', 'q': 'd', 'r': 'e', 's': 'f', 't': 'g', 'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'}

そして、 sは次の意味不明な文字列が入ってます。


print(this.s)
Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!

ただこの文字列、薄目にみてみると、各単語の文字数が The Zen of Python が似ています。

実はこれはちょっとした暗号になっていて、
this.s の各文字を、this.dの辞書で変換すると、 The Zen of Python が現れます。

書き方はいろいろあると思いますが次のような形でやってみましょう。


print("".join(map(lambda x: this.d.get(x, x), this.s)))
# 以下出力
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

モジュール this の実装自体が
The Zen of Python に反してややこしい値になっているというちょっとした遊び心のようです。

ちなみに、 this モジュールのソースコードも読んでみたのですが、 mapではなく、内包表記でやっているようですね。
sやdを定義したあとに、次のように書いてありました。
よく考えなくてもこれで十分ですね。


print("".join([d.get(c, c) for c in s]))

DataFrameのsampleメソッドのドキュメントを読む

超高頻度で使っているメソッドなのに、公式ドキュメントを読んだことがなかった、
pandas.DataFrame.sample
についてドキュメンを読んでみました。

元々の目的はデータ件数が不明なデータフレームからn個のサンプルが欲しい時に、
df.sample(n)とすると、データフレームの件数が少ないとエラーになるのが面倒だし、
事前にlen(df)して、条件分岐するのが面倒なので都合のいいオプションを探していました。


df.sample(200)
# ValueError: Cannot take a larger sample than population when 'replace=False'

これを回避するために、いつも何かしら工夫をしていますが、正直無駄に行数が増えてる気がしています。
(コードのイメージ)


n = 200
if len(df) >= n:
    df.sample(n)
else:
    df

dfの行数がnより大きいならn件返して欲しくて、行数が少ないなら全部のデータをそのまま渡すというのを、
if文を使わずにsample()の引数で実現したかったわけです。
(結論からいうと、そのような引数は用意されていませんでした。)

エラーメッセージの指示に従ってreplace=True を設定すると、
同じ行が複数回サンプリングされるのを許すのでnが行数より大きくても大丈夫になります。
(ただ、これは自分が元々やりたかったのとは違う。)

dfを150行のデータフレームとすると次のような感じ。


print(len(df))  # 150
print(len(df.sample(200, replace=True)))  # 200

目的のオプションは見つかりませんでしたが、その代わり、今まで知らなかった引数が使えることを知りました。
正確なステートメントは公式ドキュメントの方に任せて、ざっくりと書くと以下のようなものが使えます。

frac : サンプリングするデータの数を、個数ではなく割合で指定する。
replace : Trueにすると、同じデータを重複してサンプリングできる。
weights : サンプリングされる確率を重み付けできます。
random_state : 乱数固定。
axis : 1を指定すると、行ではなく列をサンプリングできる。

決定木系の機械学習モデルを自分で実装するときなどに便利そうですね。

matplotlibのグラフ間の間隔の調整

普段の分析でもこのブログでも、matplotlibの一つのfigureに複数のグラフを描写することがよくあります。

その時、稀に困るのがタイトルやラベルが他のグラフに重なってしまう時です。
それ以外にも微妙に感覚が詰まりすぎてたり広すぎたりで見た目が悪いなぁと思うことがあります。

その場合、subplots_adjustを使って、間隔を調整できます。
ドキュメントはこちら。
matplotlib.pyplot.subplots_adjust
引数の意味はこちらにもあります。
matplotlib.figure.SubplotParams

次の6種類の引数を渡せますが、間隔の調整に使うのは、hspaceとwspaceです。

The parameter meanings (and suggested defaults) are:

left = 0.125 # the left side of the subplots of the figure
right = 0.9 # the right side of the subplots of the figure
bottom = 0.1 # the bottom of the subplots of the figure
top = 0.9 # the top of the subplots of the figure
wspace = 0.2 # the amount of width reserved for space between subplots,
# expressed as a fraction of the average axis width
hspace = 0.2 # the amount of height reserved for space between subplots,
# expressed as a fraction of the average axis height
Copy to clipboard
The actual defaults are controlled by the rc file

無理やり作った例で恐縮ですが、調整せずにグラフ間で重なってしまった例と、
subplots_adjustで調整した例を作りました。


import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [1, 10, 100, 1000, 10000]
# 悪い例
fig = plt.figure(figsize=(9, 6),  facecolor="white")
for i in range(3):
    for j in range(3):
        ax = fig.add_subplot(3, 3, 1 + j + 3*i)
        ax.set_title("複数行の\nタイトル")
        ax.plot(x, y)
plt.show()
# 間隔を調整した例
fig = plt.figure(figsize=(9, 6),  facecolor="white")
fig.subplots_adjust(hspace=0.6, wspace=0.4)
for i in range(3):
    for j in range(3):
        ax = fig.add_subplot(3, 3, 1 + j + 3*i)
        ax.set_title("複数行の\nタイトル")
        ax.plot(x, y)
plt.show()

出力はそれぞれこちらです。

hspace=0.6 と wspace=0.4 の値ですが、これはドキュメント読んで何かしら計算して決めるより、
適当な値を入れて何度か試すのがおすすめです。
初期値はそれぞれ0.2なので、それより大きい値を入れると広くなります。

スタージェスの公式によるヒストグラムのビンの数の決定

データの傾向を見るときにヒストグラムを描くことは頻繁にありますが、
そのとき課題になるのが、ビンの数を何本にするかです。

いつもmatplotlibのデフォルトである10本でとりあえず試したり、
適当に変えながら何パターンか試したりしています。
Tableauであれば、本数ではなく幅での指定ですね。

ただ、何かしら参考指標が欲しいとは思っていたので調べたところ、複数の方法が提案されていました、
その中でスタージェスの公式(Sturges’ formula)が良さそうだったので試してみました。

定義はwikipediaのものを採用しましょう。
(他のサイトを見ると、小数点以下の扱いで微妙に異なるパターンがあります。)
ヒストグラム – Wikipedia

スタージェスの公式によると、n個のデータがあるとき、ビンの数kの目安は次の式で得られます。

$$k = \lceil \log_{2}{n} + 1\rceil$$

注意として、スタージェスの公式はその導出の背景に、二項分布が正規分布で近似できるという性質を使っています。
そのため、nが小さすぎる場合にはあまり参考になりません。
(そのような時はそもそも、ヒストグラム自体があまり有効ではないです。)
また、 データの分布が二項分布/正規分布と大きく異なる時もうまくいきません。

それではいくつかのデータで試してみましょう。
次のコードは、7種類の件数に対して、ランダムにデータを取得し、
スタージェスの公式で得られたビンの数ののヒストグラム(中央列)と、
それよりビンが2本少ないヒストグラム(左列)、2本多いヒストグラム(右列)を描写します。


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def sturges_formula(data_size):
    return int(np.floor(1+np.log2(data_size)))


fig = plt.figure(figsize=(15, 28), facecolor="white")
for i in range(7):
    data_size = int(1.5 * 2 ** (i+4))
    data = np.random.randn(data_size)
    bin_count = sturges_formula(data_size)

    for j in range(3):
        ax = fig.add_subplot(7, 3, 3*i+j+1)
        ax.set_title("データ件数:{d}件, ビン:{b}本".format(
            d=data_size, b=bin_count-2+2*j
        ))
        ax.hist(data, bins=bin_count-2+2*j, rwidth=0.8)

plt.show()

乱数を使っているので毎回結果は変わりますが、出力の一例がこちら。

左列は確かに若干少ないかなという気がします。
ただ、データ件数が多くなると、もう少し多い数(右列)でもいいので、やはり目安として使うのが良さそうですね。

pandas DataFrameをエクセルで開いても文字化けしないcsvファイルに書き出す

pandasのDataFrameをファイルに保存したい時、
to_csvやto_excelなど、とても便利なメソッドが用意されています。

pandas.DataFrame.to_csv
pandas.DataFrame.to_excel

ただ、日本語文字を含むデータフレームをto_csvで書き出すと、
それをエクセルで開いたときに文字化けしてしまいます。
(他職種のメンバーに連携するとき非常に厄介な現象です。)

エクセルで開きたいなら to_excel() を使えばいいので、普段はそれでクリアしているのですが、
データ量やその後の用途の問題で、csvファイルで要求されるが、その人はエクセルでも使うといったパターンがあります。

このような時は、 encoding="utf-8_sig"を指定することで文字化けしないようにできます。
(pandasにとってExcelはスコープ外だからか、公式ドキュメントにはこの辺りのことを明記している記述は見つかりませんでした)


import pandas as pd
df = pd.DataFrame(
        {
            'col1': ["あ", "い", "う", "え", "お"],
            'col2': ["か", "き", "く", "け", "こ"],
        }
    )
df.to_csv("export_1.csv", index=None)
df.to_csv("export_2.csv", encoding="utf-8_sig", index=None)

# jupyter で実行しているので、!をつけることでBashコマンドを実行
!ls -la *.csv
-rw-r--r--@ 1 yutaro  staff  50  5 25 01:25 export_1.csv
-rw-r--r--@ 1 yutaro  staff  53  5 25 01:25 export_2.csv

2つのファイルを見比べると、 encoding=”utf-8_sig” をつけた方は 3バイトファイルサイズが大きいことがわかります。
これは、BOM(バイトオーダーマーク)と呼ばれる情報が付加されたためで、
これによって、 export_2.csv の方はExcelで開いても文字化けしなくなります。
(export_1.csv は文字化けします。)

matplotlibでバイオリン図

データの可視化方法として、バイオリン図というものがあります。
バイオリン図 – Wikipedia

pythonでこれを書こうと思ったら、 Seaborn を使うのが定番なのですが、
実はmatplotlibにもAPIが用意されていたことを知ったのでその紹介です。

ドキュメント
matplotlib.axes.Axes.violinplot
(ただ、どうも Seaborn の方が使いやすそう。)

とりあえず動かしてみましょう。データは手頃なところで、 iris の4つの特徴量を可視化してみます。


import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
data = load_iris().data
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, title="iris data")
ax.violinplot(data)
plt.show()

出力はこちら。

先ほどのドキュメントに記載されている通り、他にも細かなオプションで見た目が調整できるのですが、
Seaborn の方が多機能のようです。
バイオリンの左右に違う分布を表示するといったことを行いたかったのですが、
どうやらmatplotlibではできなさそう。

MeCabの出力フォーマット

しばらくふれない間にまたど忘れしてしまったので、MeCabの出力フォーマットのメモです。

公式サイトの使い方 に書いてあります。

表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

よくある例の「すもももももももものうち」を形態素解析してみても * が多くてわかりにくいので、
先の公式ページの説明を読んだ方が早いです。


~$ mecab
すもももももももものうち
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

一通り例が見たい場合、*になってばかりでなかなか登場しないのが品詞細分類3ですが、
文章に地名や人名が含まれている時に登場するので、次のような文章はいかがでしょうか。


~$ mecab
渋谷で働くデータサイエンティスト
渋谷	名詞,固有名詞,地域,一般,*,*,渋谷,シブヤ,シブヤ
で	助詞,格助詞,一般,*,*,*,で,デ,デ
働く	動詞,自立,*,*,五段・カ行イ音便,基本形,働く,ハタラク,ハタラク
データ	名詞,一般,*,*,*,*,データ,データ,データ
サイエンティスト	名詞,一般,*,*,*,*,サイエンティスト,サイエンティスト,サイエンティスト
EOS

自分は表層形、品詞、原形をよく使うので、
‘\t’でsplitした結果の、index 0 が、表層形、
index 1をさらに’,’ split して、 index 0 が品詞、index 6が原型、
ってのだけ把握していればなんとかなることが多いです。

それで、適当なテキストを試しに形態素解析して、
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
と出てきて、何番目が原型だ?? となって公式サイト見に行く、ということを何度もやらかしています。

pipでライブラリのバージョンを指定してインストール

pipでライブラリのバージョンを気軽にあげていたら、動かなくなるものが出たので戻し作業をやりました。
良い機会なので、pipで指定したバージョンのライブラリをインストールする方法を紹介します。

方法は、ドキュメントの、examplesの中にあるように、ライブラリ名の後ろに==でつなげてバージョンを指定するだけです。
pip install – examples

ちなみに、今回動かなくなったのは statsmodels です。(バージョンを上げたのはscipy)


# 動かなくなったコード。 ただのインポートさえできなくなった。
import statsmodels.api as sm

# 出力されたエラー
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
 in ()
      1 import numpy as np
      2 import matplotlib.pyplot as plt
----> 3 import statsmodels.api as sm

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/api.py in ()
     14 from . import robust
     15 from .robust.robust_linear_model import RLM
---> 16 from .discrete.discrete_model import (Poisson, Logit, Probit,
     17                                       MNLogit, NegativeBinomial,
     18                                       GeneralizedPoisson,

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/discrete/discrete_model.py in ()
     43 
     44 from statsmodels.base.l1_slsqp import fit_l1_slsqp
---> 45 from statsmodels.distributions import genpoisson_p
     46 
     47 try:

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/distributions/__init__.py in ()
      1 from .empirical_distribution import ECDF, monotone_fn_inverter, StepFunction
----> 2 from .edgeworth import ExpandedNormal
      3 from .discrete import genpoisson_p, zipoisson, zigenpoisson, zinegbin

~/.pyenv/versions/anaconda3-5.2.0/lib/python3.6/site-packages/statsmodels/distributions/edgeworth.py in ()
      5 import numpy as np
      6 from numpy.polynomial.hermite_e import HermiteE
----> 7 from scipy.misc import factorial
      8 from scipy.stats import rv_continuous
      9 import scipy.special as special

ImportError: cannot import name 'factorial'

statsmodelsがインポートできないので、statsmodelsを疑ってしまいそうですが、
根本的な原因は、scipyから、 factorialをインポートできなかったことでした。

----> 7 from scipy.misc import factorial

最近、scipyのバージョンを 1.1.0から 1.3.0 へ上げたのが怪しいです。
真の原因は何か他にありそうな気もするのですが、とりあえずscipy のバージョンを戻します。

戻し手順は最初に対象のライブラリをアンインストールした後に、再度バージョン指定してインストールです。


$ pip uninstall scipy
Uninstalling scipy-1.3.0:
# --- 略 ---
Proceed (y/n)? y
  Successfully uninstalled scipy-1.3.0

$ pip install scipy==1.1.0
# --- 略 ---
Installing collected packages: scipy

これでscipyのバージョンが戻り、再びstatsmodelsをimport できるようになりました。