itertools でリストの部分集合をリストアップする

Pythonの標準ライブラリである、itertools を使うと、リストや集合(set)の部分集合を手軽にリストアップすることができます。
ドキュメントはこちら。
itertools — 効率的なループ実行のためのイテレータ生成関数

欲しい部分集合の性質(重複の可否や、ソートの有無)に応じて、次の3種つの関数が用意されています。

itertools.permutations(iterable, r=None)
長さrのタプル列、重複なしのあらゆる並び

itertools.combinations(iterable, r)
長さrのタプル列、ソートされた順で重複なし

itertools.combinations_with_replacement(iterable, r)
長さrのタプル列、ソートされた順で重複あり

試しに要素が5個のリストを用意して、 r=3 でソートされた順番で重複無しの
部分集合をリストアップしてみましょう。
$_5\mathrm{C}_3=\frac{5\cdot4\cdot3}{3\cdot2\cdot1}=10$なので、10組みの結果が得られるはずです。


import itertools
data = list("ABCDE")

for subset in itertools.combinations(data, 3):
    print(subset)

# 以下出力
('A', 'B', 'C')
('A', 'B', 'D')
('A', 'B', 'E')
('A', 'C', 'D')
('A', 'C', 'E')
('A', 'D', 'E')
('B', 'C', 'D')
('B', 'C', 'E')
('B', 'D', 'E')
('C', 'D', 'E')

想定通りの組み合わせが出ました。
また、結果はsetやlistではなく、タプルで返されることも確認できます。

複数のDataFarameを1つのExcelファイルに書き出す

pandasのDataFrameを保存したい時、to_excel()関数を使うと手軽にExcelファイルに書き出せます。
ただ、 df.to_excel(“ファイルパス”) というやり方だと、1ファイルに1個のデータフレームしか書き出せません。

複数のデータフレームをシートを分けてエクセルファイルに保存するときは、
ExcelWriter というのを使います。
ドキュメント: pandas.ExcelWriter

実際に使ってみましょう。
with を使うと便利です。 (使わない方法もあります)


import numpy as np
import pandas as pd
dataframe_list = [
    pd.DataFrame(np.random.randn(100, 10)) for _ in range(3)
]
with pd.ExcelWriter("./3sheets.xlsx") as writer:
    for i, df in enumerate(dataframe_list):
        df.to_excel(writer, sheet_name=f"シート_{i}")

sheet_name は指定しておかないと、同じシートに上書きされてしまい、
最後のDataFrameしか残らないので注意です。

フォーマット済み文字列リテラル

Pythonの3.6からの新機能で、フォーマット済み文字列リテラルというのがあることを知りました。
ドキュメント:2.4.3. フォーマット済み文字列リテラル

要は、文字列を定義する時にfかFを頭につけておくと、
その文字列の中で{}で囲んだ変数がその値に置換されるというものです。


name = "Fred"
print(f"He said his name is {name}.")
# He said his name is Fred.

これまでformat()を使うことが多かったのですが、それよりも便利そうです。
ちなみに、format()を使うなら次のように書きます。少し長いですね。


print("He said his name is {name}.".format(name=name))
# He said his name is Fred.

{, }の文字を使いたい時は{{, }}のようにそれぞれ2重に書いておけば使えます。


print(f"{{He said his name is {name}.}}")
# {He said his name is Fred.}

この他、文字列をクオーテーションで囲んだり、数値の桁数の指定など様々なオプションが使えるようです。
ほぼコピペですが、ドキュメントに乗っていたサンプルコードを一通り動かしてみました。


import decimal
from datetime import datetime

name = "Fred"
print(f"He said his name is {name!r}.")
# "He said his name is 'Fred'."
print(f"He said his name is {repr(name)}.")  # repr() is equivalent to !r
# "He said his name is 'Fred'."
width = 10
precision = 4
value = decimal.Decimal("12.34567")
# : を使って表示桁数を指定
print(f"result: {value:{width}.{precision}}")
# 'result:      12.35'
today = datetime(year=2017, month=1, day=27)
# 日付の書式指定
print(f"{today:%B %d, %Y}")
# 'January 27, 2017'
number = 1024
# 16進法表記
print(f"{number:#0x}")
# '0x400'

DataFrameの変化率の計算

前回の記事が差分だったので次は変化率です。
ファイナンス系のデータをはじめとして変化量よりも変化率の方が着目される例は多々あります。

pandasのDataFrameにおいては、pct_changeというメソッドで算出することができます。
pandas.DataFrame.pct_change

とりあえず使ってみましょう。


import pandas as pd
df = pd.DataFrame(
        {
            'a': [1, 2, 3, 4, 5],
            'b': [1, 4, 9, 16, 25],
            'c': [1, 8, 27, 64, 125],
        }
    )
print(df.pct_change())
'''
          a         b         c
0       NaN       NaN       NaN
1  1.000000  3.000000  7.000000
2  0.500000  1.250000  2.375000
3  0.333333  0.777778  1.370370
4  0.250000  0.562500  0.953125
'''

結果を見てわかる通り、1から4への変化は 4/1 = 4 と計算されるのではなく、 (4-1)/1 = 3 になります。
その点だけ注意です。

Dataframeの差分を取る

pandasの累積和をとったり、特定のwindowごとの和をとったりする方法はこれまで紹介してきたので、
次はDataFrameの差分を取る関数の紹介です。
元のデータが単位根過程の時に差分をとって定常化するなど、結構頻繁に使います。

さて、その方法ですが、DataFrameのdiffというメソッドを使います。

pandas.DataFrame.diff

引数には periods と axis を指定できますが、大抵はどちらもデフォルトでいけるでしょう。
periods(デフォルト:1)を指定することで、いくつ前の値との差分を取るかを指定でき、
axis(デフォルト:0) に1を指定すると隣の行との差分をれます。


import pandas as pd
df = pd.DataFrame(
        {
            'a': [1, 2, 3, 4, 5],
            'b': [1, 4, 9, 16, 25],
            'c': [1, 8, 27, 64, 125],
        }
    )
print(df.diff())
'''
     a    b     c
0  NaN  NaN   NaN
1  1.0  3.0   7.0
2  1.0  5.0  19.0
3  1.0  7.0  37.0
4  1.0  9.0  61.0
'''

print(df.diff(periods=2))
'''
     a     b     c
0  NaN   NaN   NaN
1  NaN   NaN   NaN
2  2.0   8.0  26.0
3  2.0  12.0  56.0
4  2.0  16.0  98.0
'''

print(df.diff(axis=1))
'''
    a     b      c
0 NaN   0.0    0.0
1 NaN   2.0    4.0
2 NaN   6.0   18.0
3 NaN  12.0   48.0
4 NaN  20.0  100.0
'''

なお、それぞれ次のようにshiftして値をずらしたものとの差分を取るのと結果は同じです。


df - df.shift()
df - df.shift(periods=2)
df - df.shift(axis=1)

DataFrameのexpandingについて

以前の記事で、pandas DataFrameの window関数である、 rollingを紹介しました。
参考:pandasで移動平均や高値線、安値線を計算する
これは、直近n個の値に対して和や最大値、最小値、平均や分散を求めるものです。

場合によっては、直近n個ではなく最初の値から全てに対して順に行いたいことがあると思います。
演算が和であればcumsumで累積和を取るように。

どうやらpandasのversion 0.18.0からそのような関数が実装されたようです。
pandas.DataFrame.expanding

とりあえずやってみましょう。
和だったらcumsumを使えば済む話なので、
平均と標準偏差でやってみます。
(元データが乱数だとそんなに面白いものでも無いので、結果は省略。コピーして動かしてみてください。)


# データ生成
import pandas as pd
import numpy as np
df = pd.DataFrame(np.random.randn(100, 5))
print(df.shape)  # (100, 5)

# 平均
df.expanding().mean()

# 標準偏差
df.expanding().std()

# aggを使ってまとめて算出も可能。
df.expanding().agg(["mean", "std"])

pandasのDataFrameから特定のデータ型の列を抜き出す

読み込んだデータフレームが、列ごとにバラバラのデータ型を持っていて特定の型の列だけ抜き出したいという場面はよくあります。
以前は dtype 属性で調べて列名をピックアップして処理したりしていたのですが、専用のメソッドがあったので紹介します。

pandas.DataFrame.select_dtypes

これを使うと文字列型の列だけとか、数値型の列だけとか値を抽出できます。

とりあえず実験用のデータフレーム作成。


import pandas as pd
import numpy as np
df = pd.DataFrame(
        {
            'a': np.random.randint(100, size=10),
            'b': np.random.choice([True, False], size=10),
            'c': np.random.randn(10),
            'd': np.random.choice(list('abcd'), size=10),
        }
    )
print(df.dtypes)
'''
a      int64
b       bool
c    float64
d     object
dtype: object
'''

使い方は簡単で、
select_dtypesの引数に必要なデータ型を値かリストで渡すだけです。
また、excludeで逆に不要なデータ型を指定することもできます。

例えば数値だけ欲しい時(列aと、列c)次のように指定できます。
3つとも結果は同じです。


df.select_dtypes([int, float])
df.select_dtypes([np.int, np.float64])
df.select_dtypes(["int", "float"])

欲しいデータ型一種類のときは配列ではなくスカラーで渡しても大丈夫です。


df.select_dtypes(object)
df.select_dtypes(bool)

逆に不要な型を指定する例。


df.select_dtypes(exclude=object)

圧縮行格納方式(CRS)の疎行列のデータ構造

今回も疎行列のお話です。
前回の記事で登場したcrsとcscについて、具体的にどのようなデータ構造なのかを紹介します。
ちなみにcrsとcscはそれぞれ、
圧縮行格納方式 (Compressed Sparse Row) と、
圧縮列格納方式 (Compressed Sparse Column) の略です。
ほぼ同じ処理を行方向に行うか列方向の違いしかなく、転置を取るとそれぞれ入れ替わるので、 CSRの方を紹介します。

ちなみに、 wikipediaの説明で理解したので、それをみながら記事を書いています。
例として取り上げる行列はこれ。(wikipediaの例と同じ。)
$$
\left(
\begin{matrix}
1 & 2 & 3 & 0 \\
0 & 0 & 0 & 1 \\
2 & 0 & 0 & 2 \\
0 & 0 & 0 & 1 \\
\end{matrix}
\right)
$$

まず、csr形式のデータで作りましょう。
今回はarrayで作って変換するのではなく、お作法にしたがい、lil形式で生成してから変換します。


from scipy import sparse
# 成分が全て0の 4 * 4 の lil形式の疎行列を作成。
M_lil = sparse.lil_matrix((4, 4))
# 各成分を代入。
M_lil[0, 0] = 1
M_lil[0, 1] = 2
M_lil[0, 2] = 3
M_lil[1, 3] = 1
M_lil[2, 0] = 2
M_lil[2, 3] = 2
M_lil[3, 3] = 1
# M_csr形式に変換
M_csr = sparse.csr_matrix(M_lil)
# 確認
print(M_csr)
# 出力
'''
  (0, 0)    1.0
  (0, 1)    2.0
  (0, 2)    3.0
  (1, 3)    1.0
  (2, 0)    2.0
  (2, 3)    2.0
  (3, 3)    1.0
'''

これで、csr形式の変数、M_csrに例の行列が格納されました。
printすると整形されて表示されるのですが、実際のデータ構造はこうはなっていません。
wikipediaの説明と、ドキュメントをみながら確認します。
まず、 実際のデータは、次の3つの属性に格納されています。

data ・・・ CSR format data array of the matrix
indices ・・・ CSR format index array of the matrix
indptr ・・・ CSR format index pointer array of the matrix

具体例を見てから説明します。


print(M_csr.data)
# [1. 2. 3. 1. 2. 2. 1.]
print(M_csr.indices)
# [0 1 2 3 0 3 3]
print(M_csr.indptr)
# [0 3 4 6 7]

まず、data が 疎行列の0では無い要素の値を、左上から行方向(右側へ)に順番に並べたものです。
(csrのrが対応。 cscの場合はここで列方向(下向き)に並べたものになります。)
そして、indices が、それぞれの要素が、何列目の要素なのかを示す配列です。

明らかにわかるように、data と indices の要素の数はその疎行列の0では無い成分の個数です。
あとは、dataの各要素が何行目なのかがわかれば、行列を復元できますが、
それを担っているのが、indptr です。
これだけ、wikipediaの説明と異なっていて非常にわかりにくいですが、次のように解釈できます。


# 行列の最初の行のデータは、indptrの最初の2個のデータで作ったスライスの値
print(M_csr.data[0: 3])
# [1. 2. 3.]
# 次の行のデータは、indptrの一つずらした2個のデータで作ったスライスの値
print(M_csr.data[3: 4])
# [1.]
# 以下繰り返し
print(M_csr.data[4: 6])
# [2. 2.]
print(M_csr.data[6: 7])
# [1.]

明らかにわかる通り、 indptr の要素の個数は行の数より1つ大きくなります。

これで、csr_matrixの中のデータの構造がわかりました。
また、data属性の中に行単位でデータが固まって存在してて、
行単位の取り出しや演算が得意なことにも納得できると思います。

csc_matrixとcsr_matrix

前回の記事に続いて疎行列の話です。
参考:scipyにおける疎行列

今回は疎行列を実装する型(クラス)のうち、csc_matrixcsr_matrixの紹介です。
前回紹介した、lil_matrixには効率的に疎行列を構成できるメリットがありますが、
計算が遅いというデメリットがあります。
ドキュメントに次のように書いてある通り、演算するときはcsrかcsc形式に変換した方が良いです。
arithmetic operations LIL + LIL are slow (consider CSR or CSC)

とりあえず本当に遅いのか時間を測っておきましょう。
今回は 10000*10000の大きめの疎行列を作り、実験します。
(arrayの場合の時間も測定したいので、最初にarrayで作成して変換していますが、本来は最初からlil型で生成してメモリを節約の恩恵を受けた方が良いです。)
また、csrと、cscはそれぞれ行方向、列方向の取り出しに有利という特徴があるので、実験用データによって有利不利無いように対称行列を作りました。


from scipy import sparse
import numpy as np
# 疎行列作成
M_array = np.random.choice(
    [0, 1, 2, 3, 4, 5],
    p=[0.99, 0.002, 0.002, 0.002, 0.002, 0.002],
    size=[10000, 10000]
)
# 自身の転置行列と足し合わせて対称行列にする
M_array = M_array + M_array.T

# それぞれの型に変換する
M_lil = sparse.lil_matrix(M_array)
M_csc = sparse.csc_matrix(M_lil)
M_csr = sparse.csr_matrix(M_lil)

# 和をとるのにかかる時間を計測
%timeit M_array + M_array
# 406 ms ± 2.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit M_lil + M_lil
# 618 ms ± 7.04 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit M_csc + M_csc
# 7.92 ms ± 17.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csr + M_csr
# 7.94 ms ± 51.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

ご覧の通り、 lil 同士の演算は通常のarray形式よりも遅く、一方でcscやcsr同士の演算は圧倒的に速くなりました。

あとは、cscとcsrの違いですが、それぞれ、列方向のスライスと行方向のスライスが効率的に行えます。
(あまり指摘されないようですが、スライスをとるだけであればarrayが一番早い。)

これもそれぞれみておきましょう。


# 列方向のスライス
%timeit M_array[:, 400]
# 225 ns ± 3.06 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit M_lil[:, 400]
# 11.4 ms ± 201 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csc[:, 400]
# 151 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit M_csr[:, 400]
# 2.86 ms ± 127 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

列方向のスライスについては、array が最速ではありますが、 csc が csr に比べてかなり早いことが確認できます。

次は行方向。


# 行方向のスライス
%timeit M_array[400, :]
# 235 ns ± 10.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit M_lil[400, :]
# 36.6 µs ± 1.37 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit M_csc[400, :]
# 2.9 ms ± 112 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit M_csr[400, :]
# 66 µs ± 3.39 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

今度は、csr が csc より速くなりました。
とはいえ、lil が csrよりも速く処理できています。(array最速は変わらず。)

これは、前回の記事で説明した通り、lilがrows属性に、行ごとの値を保有しているからのようです。

csrの方が演算が速く、わざわざ疎行列を作ったうえでなんの演算もせずにスライスで値を取り出すだけという場面もあまり無いので、
csrかcscを選んで使うことが多いと思いますが、行方向のスライスだけならlilが速いことは一応覚えておこうと思います。

scipyにおける疎行列

自然言語処理やレコメンドシステムの開発などをやっていると、サイズは非常に大きいが成分のほとんどは0という行列が頻繁に登場します。
そのような行列を疎行列と言います。
疎行列 – Wikipedia
これをそのまま扱うと、メモリは無駄に消費するし無駄な計算は増えるしで非常に効率の悪いことになります。
そのため、scipyでは疎行列を専門に扱う scipy.sparse というモジュールが用意されています。

ドキュメント: Sparse matrices (scipy.sparse)

リンク先を見たらわかる通り、疎行列を表す型は結構色々な種類があります。(Sparse matrix classes 参照)
それぞれ、他の型との変換が高速とか、行方向/列方向の取り出しが早いとか個別にメリットを持っていますが、
共通しているのはデータ量を大幅に節約できる点です。

今後の記事でいくつか紹介する予定ですが、とりあえずデータ量を削減できるって特徴だけでも確認しておきましょう。

疎行列のメリットを感じるにはかなり小さいのですが、乱数で10*10の疎行列(array型)を作って、
それを lil_matrix に変換して、中のデータを見てみましょう。

まずデータ作成。
(今回はサンプルとして、array型でデータを作って変換していますが、
省メモリの恩恵をきちんと受けるには最初から疎行列でデータを生成するべきである点には注意してください。)


from scipy import sparse
import numpy as np
M_array = np.random.choice(
    [0, 1, 2, 3, 4, 5],
    p=[0.9, 0.02, 0.02, 0.02, 0.02, 0.02],
    size=[10, 10]
)
M_array

# 出力例
array([[0, 3, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 4, 0, 0, 0, 0],
       [0, 0, 0, 0, 5, 0, 3, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
       [0, 5, 0, 0, 0, 3, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

これを型変換します。方法は、strとintの変換と似たような感じで、lil_matrix(D)のようにするとできます。


M_lil = sparse.lil_matrix(M_array)
M_lil
# 出力
<10x10 sparse matrix of type ''
	with 9 stored elements in LInked List format>

これで変換できました。

printすると、0ではない成分の座標と値を保有していることが確認できます。
(ただし、実際のデータ構造は結構違います。print用に整形しているようです。)


print(M_lil)
# 以下出力
  (0, 1)	3
  (1, 3)	1
  (1, 5)	4
  (2, 4)	5
  (2, 6)	3
  (3, 7)	2
  (4, 1)	5
  (4, 5)	3
  (8, 9)	2

0行1列目が3, 1行3列目が1,という風に読んでいくと、確かに0以外の成分が記録されているのがわかります。

なお、実際のデータ構造は型ごとに違うのですが、lil_matrixの場合は次ように、
dataとrowsとうい二つの属性を使ってデータを保有しています。


print(M_lil.data)
'''
[list([3]) list([1, 4]) list([5, 3]) list([2]) list([5, 3]) list([])
 list([]) list([]) list([2]) list([])]
'''

print(M_lil.rows)
'''
[list([1]) list([3, 5]) list([4, 6]) list([7]) list([1, 5]) list([])
 list([]) list([]) list([9]) list([])]
'''

data方は、各行の0ではない要素の値、rowsにそれぞれの要素が何番目に入っているのかの情報を保有しています。
合計18個の数字で、10*10=100個の要素を持つ行列を表現できています。

あと、lil_matrixの重要なメリットして、スライスで値を取り出せるし、代入もできるという点があります。


print(M_lil[0, 1])
# 3

(できて当然に思えますが、他の疎行列のデータ型によってはこれができないものがある。)
そのため、 lil_matrix で疎行列を作って、それを(その他のメリットを持つ)他の型に変換して使うというやり方をよくやります。
(Intended Usageにそう書いてあるので従っています。)

最後に、arrayに戻す方法は toarray()です。


M_lil.toarray()
# 出力
array([[0, 3, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 4, 0, 0, 0, 0],
       [0, 0, 0, 0, 5, 0, 3, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 2, 0, 0],
       [0, 5, 0, 0, 0, 3, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int64)