printでお手軽プログレスバー

前回のprintのオプションの記事の応用です。
参考:Python3のprintの引数

大きめのデータの前処理など待ち時間が長い処理をする時に、進捗を表示したくなる時があります。
専用の良いライブラリもたくさんあるので、実用上はそれで十分なのですが、
前回のprintのオプションを使うと、簡易的なプログレスバーを作成できます。
(何も考えずに printしても進捗はわかるのですが、notebookが見にくくなったりします。)

ポイントは end を使って改行を止めることと、 “\r”(キャリッジリターン)を出力して、
次の文字の出力先を行の先頭にし、前回の出力を上書きすることです。

一例としてやってみます。
本当は重い処理のループなどの中でやるのですが、今回例として time の sleepで無理やり待ち時間を入れています。


import time
for i in range(1, 101):
    print("■" * (i//10), "□"*(10-i//10), sep="", end=" : ")
    print(str(i).zfill(3), "/", 100, "\r", sep="", end="")
    time.sleep(1)

手軽に、という割にコードが汚くなりました。
■と□は余計かもしれませんね。

これを実行すると、こんな感じの表示が出て数字が増えながら、徐々に黒く塗りつぶされていきます。
■■■■□□□□□□ : 040/100

Python3のprintの引数

printは表示したい文字列以外にも色々引数を指定して出力をカスタマイズできるよという話。

Python3系では、Python2と違って、printは関数です。(2では文)
そのため、色々と引数をとることができます。(文ならカスタマイズできないわけでもないし、関数が絶対引数とるわけでもないのですが、その辺は置いといて。)

あまりにも使い慣れすぎてドキュメントを読んでなかったのですが、printの説明には次のようにあります。
print(*objects, sep=’ ‘, end=’\n’, file=sys.stdout, flush=False)

objects を sep で区切りながらテキストストリーム file に表示し、最後に end を表示します。sep 、 end 、 file 、 flush を与える場合、キーワード引数として与える必要があります。

キーワードなしの引数はすべて、 str() がするように文字列に変換され、 sep で区切られながらストリームに書き出され、最後に end が続きます。 sep と end の両方とも、文字列でなければなりません。これらを None にすると、デフォルトの値が使われます。 objects が与えられなければ、 print() は end だけを書き出します。

file 引数は、 write(string) メソッドを持つオブジェクトでなければなりません。指定されないか、 None である場合、 sys.stdout が使われます。表示される引数は全てテキスト文字列に変換されますから、 print() はバイナリモードファイルオブジェクトには使用できません。代わりに file.write(…) を使ってください。

出力がバッファ化されるかどうかは通常 file で決まりますが、flush キーワード引数が真ならストリームは強制的にフラッシュされます。

バージョン 3.3 で変更: キーワード引数 flush が追加されました。

printにカンマ区切りで複数文字列を渡すと、スペースで結合して表示してくれるのは知っていたのですが、
それがsepって引数で調整できることは初めて知りました。


# デフォルトではsep=" "で区切って出力
print("abc", "def", "ghi")
# abc def ghi

# sepを指定すると区切り文字を変えられる
print("abc", "def", "ghi", sep="==>")
# abc==>def==>ghi

# 末尾につける文字もend(デフォルトは改行) で変更可能
print("abc", "def", "ghi", end="jkl")
# abc def ghijkl

これまで、いくつかの文字列を出力したいけど、途中にスペースや改行を入れたくないときは、
“+”で文字列を連結して表示文字列を作ったりしていましたが、
これでそんな面倒なことをしなくても大丈夫になりました。

numpyの高次元配列に対するdot積の挙動について

numpyの比較的よく使う関数に、dot積があります。
スカラーとベクトルを渡せばベクトルをスカラー倍してくれて、ベクトル同士なら内積を取ってくれ、
行列を二つ渡せばそれらの行列積を戻してくれるとても便利な関数です。
(本当は行列積は、 np.matmul を使ったほうがいいらしい。)

さて、そのnp.dot ですが、行列よりもより高次元の配列についても定義されていることを最近知りました。
ドキュメント : numpy.dot

二つの多次元配列$a$と$b$に対して、$a$の一番最後の次元の長さと、$b$の最後から2番目の次元の長さが等しい時に、
np.dot(a, b)を計算することができます。
i*j行列と、j*k行列に積が定義されて、その積がi*k行列になるのと似ています。

具体的な挙動について、コード動かしてみていきましょう。
まず、サンプルとなるデータを作ります。
aの最後の次元の要素数と、bの最後から2番目の次元の要素数は 5で揃えましたが、
それ以外の次元の要素数はバラバラにして、結果と比較しやすいようにしました。


import numpy as np
a = np.random.randn(2, 3, 4, 5).round(2)
b = np.random.randn(6, 7, 5, 8).round(2)

print(a.shape, b.shape)
# (2, 3, 4, 5) (6, 7, 5, 8)

このdot積を取って、shapeをみてみます。


c = np.dot(a, b)
print(c.shape)
# (2, 3, 4, 6, 7, 8)

5は消えましたが残りの数はそのまま残りましたね。

さて、結果の$c$の各要素の値ですが、次のように計算されたものが入っています。
$$
c[i, j, k, m, n, o] = \sum_{l} a[i, j, k, l] \cdot b[m, n, l, o].
$$
別の書き方をすればこうです。
$$
c[i, j, k, m, n, o] = np.dot(a[i, j, k, :], b[m, n, :, o]).
$$

一応確認しておきましょう。


c[1, 2, 3, 4, 5, 6] ==  np.dot(a[1, 2, 3, :], b[4, 5, :, 6])
# True

データフレームの列からapplyで新しい列を作る時、複数列まとめて作成する

DataFrame(その列なので正確にはSeries)に、何か関数を適用して新しい列を作ることは、
機械学習の特徴量作成や前処理などで頻繁に行う処理だと思います。

いつも、1列作るごとに、applyして結果を得ています。
例えば、とある列の値を2乗した列と、3乗した値が欲しいときは次のように書きます。


df = pd.DataFrame(
        np.random.randint(10, size=(5, 3)),
        columns=[f"col{str(i)}" for i in range(3)]
)

# 生成するデータごとにapplyする
df["pow_2"] = df["col0"].apply(lambda x: x**2)
df["pow_3"] = df["col0"].apply(lambda x: x**3)

print(df)

"""
   col0  col1  col2  pow_2  pow_3
0     0     1     6      0      0
1     9     8     8     81    729
2     1     6     8      1      1
3     5     1     2     25    125
4     0     2     5      0      0
"""

このくらい簡単な例であれば、計算負荷も大したことがないのですが、
物によっては、非常に無駄な処理をすることがあります。
例えば日本語の自然言語処理で大量のテキストを形態素解析し、表層形と原形と品詞の列を
それぞれ取得したいときなど、共通の形態素解析処理部分は一回で済ましたいので
3列個別にapplyするなどやりたくありません。

このような場合、applyする関数の戻り値をSeriesで戻せば、
applyの戻りを服す列にできることを知りました。

例えば次のように書きます。


# もう一度サンプルデータ生成
df = pd.DataFrame(
        np.random.randint(10, size=(5, 3)),
        columns=[f"col{str(i)}" for i in range(3)]
)

df[["pow_2", "pow_3"]] = df["col0"].apply(lambda x: pd.Series([x**2, x**3]))
print(df)
"""
   col0  col1  col2  pow_2  pow_3
0     6     3     1     36    216
1     8     4     9     64    512
2     4     4     7     16     64
3     1     1     4      1      1
4     1     8     3      1      1
"""

タプルや配列ではだめで、Seriesで返した場合のみの挙動です。
lambda式を遣わず、普通に定義したSeriesを返す関数でもできます。

なぜこのような挙動になるのか公式ドキュメント内からは該当箇所を探せていないのですが、
とても便利なので積極的に使っていきたいです。

matplotlibでgif動画生成

3次元グラフの次は動画(gif)を用いたデータの可視化方法のメモです。

matplotlibでは、次のクラスに、パラパラ漫画のようにグラフのリストを渡してあげることで、
アニメーションさせることができます。
matplotlib.animation.ArtistAnimation

例として、サインカーブを少しずつずらしながら描いてみました。


import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import ArtistAnimation

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

# 0 <=x < 2pi の範囲の点列を作成。
x = np.linspace(0, 2*np.pi, 101)[: -1]
# 各コマの画像を格納する配列
image_list = []

for i in range(100):
    # ずらしながらsinカーブを描写し、配列に格納
    y = np.sin(np.roll(x, -i))
    image = ax.plot(x, y)
    image_list.append(image)

# アニメーションを作成
ani = ArtistAnimation(fig, image_list, interval=10)
# gifに保存
ani.save('sin_animation.gif', writer='pillow')

保存されたgifがこちらです。

フルサイズで貼り付けると記事中でも動くのですね。
(いつものようにサムネイルで張ったら止まってしまっていて、クリックしないと動画になりませんでした。)

動画が使えると少しデータの可視化の幅が広がりそうです。
とりあえず機械学習の学習の進捗とかの可視化などに使ってみたいです。

matplotlibでSurface plots

昨日に続いてmatplotlibの3次元グラフの話です。
今回のテーマは Surface plots。(日本語では表面プロットでいいのかな?)
2変数関数の可視化等に便利なやつですね。

ドキュメントは今回もこちら。 : The mplot3d Toolkit

今回は例として 鞍点を持つ次の関数を可視化してみましょう。
$$
z = f(x, y) = x^2 – y^2.
$$

比較用に等高線で可視化したグラフと並べてみました。
参考: matplotlibで等高線


import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np


def f(x, y):
    return x**2 - y**2


X, Y = np.meshgrid(
        np.linspace(-10, 10, 101),
        np.linspace(-10, 10, 101),
    )
Z = f(X, Y)

fig = plt.figure(figsize=(16, 6), facecolor="w")
ax_3d = fig.add_subplot(121, projection="3d")
ax_3d.plot_surface(X, Y, Z)

ax = fig.add_subplot(122)
contour = ax.contourf(X, Y, Z)
fig.colorbar(contour)
plt.show()

結果はこちら。

可視化する対象によって向き不向きがあるのでいつもそうだというわけではないのですが、
今回のサンプルでは圧倒的に3次元プロットの方が圧倒的に関数の形をつかみやすいですね。

matplotlibで3D散布図

matplotlibで3次元のグラフを作成する方法のメモです。
今回は散布図を描いてみます。

matplotlibで3次元のグラフを書くには、mplot3d Toolkitというのを使います。
ドキュメント: The mplot3d Toolkit
また、 3次元散布図についてはこちらのドキュメントも参考になります。 3D scatterplot

ポイントとしては、(importした後明示的には使わないので忘れがちですが、)
Axes3Dを必ずインポートしておくことと、axを取得するときに、
projection="3d"を忘れないことですね。

iris のデータの4つの特徴量の中から適当に3個選んでやってみます。


import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.datasets import load_iris

iris = load_iris()
data = iris.data
label = iris.target

fig = plt.figure(figsize=(8, 8), facecolor="w")
ax = fig.add_subplot(111, projection="3d")
for c in range(3):
    ax.scatter(
        data[label == c, 0],
        data[label == c, 2],
        data[label == c, 3],
        label=iris.target_names[c]
    )

ax.set_xlabel(iris.feature_names[0])
ax.set_ylabel(iris.feature_names[2])
ax.set_zlabel(iris.feature_names[3])

ax.legend()
plt.show()

結果がこちら。
綺麗に3次元のプロットができました。

pandasのSeriesを辞書型に変換する3つの方法

以前書いた、 DataFrameの2列の値からdictを作る に近い話です。
今回の対象は Dataframeではなく Series。
元々、辞書と同じようにSeries[kye]で、値を取り出すことができるので、
DataFrameに比べて辞書型に変換するニーズも少ないと思うのですが、
to_dict() メソッドを持ってることを最近知ったのでこの記事を書きました。

まず、サンプルルとなるデータを作ります。


import pandas as pd
data = pd.Series({chr(i): i for i in range(97, 105)})
print(data)
"""
a     97
b     98
c     99
d    100
e    101
f    102
g    103
h    104
dtype: int64
"""

それでは、これを3種類の方法で辞書に変換してみましょう。
それぞれ、内包表記を使う方法(昔よく使っていた。)、 to_dict()を使う方法、 dict()にいれてしまう方法(一番楽)です。


print({k: v for k, v in chr_codes.items()})
# {'a': 97, 'b': 98, 'c': 99, 'd': 100, 'e': 101, 'f': 102, 'g': 103, 'h': 104}

print(chr_codes.to_dict())
# {'a': 97, 'b': 98, 'c': 99, 'd': 100, 'e': 101, 'f': 102, 'g': 103, 'h': 104}

print(dict(chr_codes))
# {'a': 97, 'b': 98, 'c': 99, 'd': 100, 'e': 101, 'f': 102, 'g': 103, 'h': 104}

dictに変換するメリットとして、dictならgetメソッドが使えて、keyの中に取りたい値がない場合のデフォルト値の設定もできますよっていう話を
最後に書こうと思っていたのですが、
Seriesにも getメソッドは用意されていて、ほぼ同じように動作するようでした。
pandas.Series.get

どちらかというと、辞書に変換する方法よりも辞書に変換しなくても
同じように使えるってことを覚えておいた方が有益に思います。

matplotlibで二重軸

matplotlibで一つの枠の中に複数のグラフを書く場合、値のレンジが近ければ良いのですが、
10倍以上も違うと片方のグラフが潰れてしまうなど、不便なことがあります。

そのような時はだいたいグラフを分けて書いたり、
Tableauなどの別のソフトを使って2重軸のグラフを書くなどの対応をしているのですが、
matplotlibでも左右の軸を使ったグラフを書くことはできます。

その際は、 twinx というメソッドを使います。
以下、サンプルコードです。
この時、凡例をつけておかないと、それぞれのグラフがどちらの軸を見るのかわからないので、つけるのですが、
ちょっとつけかたが特殊なので、そのサンプルも兼ねています。
(get_legend_handles_labels というメソッドを使います。)


import matplotlib.pyplot as plt
import numpy as np

# データの作成
x = np.arange(0, 20)
y1 = np.random.randint(300, 500, size=20)
y2 = np.random.randint(0, 20, size=20)

fig = plt.figure(facecolor="w")
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(x, y1, label="折れ線グラフ")
ax1.set_ylabel("折れ線グラフ")
ax1.set_ylim([200, 500])

# 2重軸の作成
ax2 = ax1.twinx()
ax2.bar(x, y2, label="棒グラフ", color="g", alpha=0.5)
ax2.set_ylabel("棒グラフ")
ax2.set_ylim([0, 60])

# 凡例をまとめて出力する
handler1, label1 = ax1.get_legend_handles_labels()
handler2, label2 = ax2.get_legend_handles_labels()
ax1.legend(handler1 + handler2, label1 + label2)

plt.show()

出力される図がこちら。

scikit-imageで画像データの読み書き

常にデータセットの画像データばかり使うのも飽きてくるので、そのほかの画像ファイルをpythonで読み込む方法を調べました。
いくつか方法があるようですが、anacondaに付いてきた、scikit-imageというライブラリが以前から気になっていたので今回はこれを使います。

ドキュメント: scikit-image

どうやら、 scikit-image.io.img_data で読み込めそうです。

試しに画像を読み込んでみて、numpy配列で読み込めたことを確認しました。
また、ついでにimsaveで別名をつけて保存しています。


from skimage import io
# 画像の読み込み
img_data = io.imread("./sample.jpg")
# 縦*横*チャンネル数のnumpy配列で読み込まれていることを確認。
print(type(img_data))
# 
print(img_data.shape)
# (1066, 1600, 3)

# 別名で保存
io.imsave("./sample_2.png", img_data)

このほか、 as_gray オプションで白黒画像としても読み込めました。
配列の次元数も違えば値の範囲も違うので扱いは要注意です。


# 白黒で読み込み
gray_image_data = io.imread("./sample.jpg", as_gray=True)
# チャンネルがなくなり、2次元のデータになる。
print(gray_image_data.shape)
# (1066, 1600)

# カラーで読み込むと、0〜255の整数値
print(img_data.max())
# 255
# 白黒で読み込むと、0.0〜1.0の浮動小数
print(gray_image_data.max())
# 0.9910556862745099

(最大値が中途半端なのは読み込んだ画像によるものです。)

これで、画像を扱うモデルを作ったら任意の画像で試せそうですね。
(あとはサイズの加工とか必要かな。)