matplotlibのグラフを高解像度で保存する

普段はmatplotlibで作ったグラフに美しさを求めるようなことは無いのですが、
とある業務で、解像度の高い状態で出力する必要が発生したので、そのメモです。

普段は画像ファイルが必要な場合もplt.show()でjupyter notebook上に表示した物を保存していましたが、
綺麗に出力するために、画像ファイルに直接書き出しました。
使うのはplt.savefig()です。
ドキュメント: matplotlib.pyplot.savefig
そして、保存するときの引数、dpiに大きめの値を与えることで、高解像度に保存することができます。
デフォルトと、dpiを指定した場合の2通りやってみましょう。


import matplotlib.pyplot as plt
import numpy as np
# ダミーデータ生成
x = np.random.randn(50)
y = np.random.randn(50)

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.scatter(x, y)
ax.set_xlabel("x軸")
ax.set_ylabel("y軸")
ax.set_title("タイトル")
# 解像度の指定をせずに保存
plt.savefig("default_dpi_scatter.png", format="png")

# もう一度同じグラフを作る
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.scatter(x, y)
ax.set_xlabel("x軸")
ax.set_ylabel("y軸")
ax.set_title("タイトル")
# 解像度を指定して保存
plt.savefig("300_dpi_scatter.png", format="png", dpi=300)

結果がこちらです。
デフォルトdpi。

dpi=300を設定。

デフォルトの方は明らかにピンボケしていて、dpiに300を指定した方はくっきりしています。
文字を見れば明らかです。

ちなみに、dpiを指定しなかった場合は、
rcParams[“savefig.dpi”] の値が使われます。そしてこれに”figure”が指定されているときは、
plt.rcParams[“figure.dpi”] の値が使用されます。
(matplotlibの設定フィアルでデフォルト値を変えることはできますが、僕の環境では特に変更していません。)

一応初期値を確認しておきましょう。


print(plt.rcParams["savefig.dpi"])
# figure

print(plt.rcParams["figure.dpi"])
# 72.0

何も指定しないと dpi=72 になるようですね。

DataFrameの2列の値からdictを作る

DataFrameの2列の値のうち、一方の列の値をKey、もう一方の列のValueとする辞書を作る方法の紹介です。
自分はよくやるのですが、意外に知られてないらしいことと、なぜこれが動くのか自分も十分に理解していなかったのでこの機会に調べました。

例えば次のようなデータフレームがあったとします。

id col1 col2
1 key1 value1
2 key2 value2
3 key3 value3
4 key4 value4
5 key5 value5

そして、このcol1の値をkey, col2の値をvalueとして、
{‘key1’: ‘value1’, ‘key2’: ‘value2’, ‘key3’: ‘value3’, ‘key4’: ‘value4’, ‘key5’: ‘value5’}
のようなdictを作りたいとします。

このような場合、僕は次のコードのようにデータフレームの該当の2列を抽出して、そのvaluesプロパティをdict関数に渡します。


import pandas as pd
# サンプルとなるデータフレームを作る
data = [[i, "key"+str(i), "value"+str(i)] for i in range(1, 6)]
df = pd.DataFrame(data, columns=["id", "col1", "col2"])

result_dict = dict(df[["col1", "col2"]].values)
print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

注意ですが .values を忘れると次のように 列名:Seriesの辞書になってしまいます。


print(dict(df[["col1", "col2"]]))
"""
{'col1': 0    key1
1    key2
2    key3
3    key4
4    key5
Name: col1, dtype: object, 'col2': 0    value1
1    value2
2    value3
3    value4
4    value5
Name: col2, dtype: object}
"""

これも参考ですがよく見かけるのは次ような書き方。


result_dict = dict()
for i in range(len(df)):
    result_dict[df.iloc[i]["col1"]] = df.iloc[i]["col2"]

print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

さて、話を戻してdict(df[["col1", "col2"]].values)がなぜうまく動くのかです。

改めてドキュメントを読んでみるとdictはiterableを引数に取ることができます。
class dict(iterable, **kwarg)
そして、
iterable のそれぞれの要素自身は、ちょうど 2 個のオブジェクトを持つイテラブルでなければなりません。
とのことです。

実際見てみると、df[[“col1”, “col2”]].valuesはその条件を満たすデータになっています。


print(df[["col1", "col2"]].values)
"""
[['key1' 'value1']
 ['key2' 'value2']
 ['key3' 'value3']
 ['key4' 'value4']
 ['key5' 'value5']]
"""

DataFrame側にdictに渡したら空気を読んでいい感じに変換される機能が実装されている、と勘違いしていたこともあるのですが、
dictの通常の挙動にマッチした動きだったようです。
for分で回すのに比べてかなりスマートに書けるので、最初に紹介した書き方は結構おすすめです。

ユークリッド空間内の複数の点の中から、最も近い点を探す短いコード

実務上次の問題を解く必要が発生し、短くて効率のいい書き方を探したのでそのメモです。

n次元ユークリッド空間内に、m個の点が存在し、その座標がm*n行列で与えられる。
新たに1つの点の座標がn次元ベクトルで与えられた時、m個の点のうち最も近い点のインデックスを求める。

結論から言うと次のコードで求まります。
最後の行が今回作成したコードです。(それ以外はサンプルの点を作成しています。)
ここでは、 n=10, m=5です。


import numpy as np
# m個の点の座標を生成
X = (np.random.randn(5, 10) * 20).round(2)
print(X)
"""
[[-25.3   -3.49 -21.3   -1.98  10.99   2.77  -8.37  -9.01  20.05   7.91]
 [ 16.33  -9.    16.55 -10.27   6.56   7.81  16.03 -14.13  10.58 -32.39]
 [ 12.01 -11.79  34.79  -2.94 -12.24   0.71 -35.01 -17.92  20.6   33.73]
 [-11.47  30.95   4.92 -19.94  34.81   2.54 -11.73  21.91  24.16   4.87]
 [ 38.32  22.13  11.5   18.82  -8.29   4.02 -17.15  -5.26 -35.62 -19.43]]
"""
# 新しい点の座標
y = (np.random.randn(10) * 20).round(2)
print(y)
"""
[-23.09 -57.02  28.07  39.45 -22.52  -5.91   9.58  30.66 -11.82  21.5 ]
"""
# 最も近い点のインデックス
print(((X-y)**2).sum(axis=1).argmin())
"""
2
"""

今回は最も近い点を探せればよく、具体的な距離は必要としないので、
ユークリッド距離の定義にある平方根の計算は省略しました。

各点との距離のリストが必要な場合は次のコードで求めることができます。
((X-y)**2).sum(axis=1)**0.5
もしくは線形代数モジュールを使い次のように計算することもできます。
np.linalg.norm(X-y, axis=1)

掲題の問いについても、次のようにかけるのですが、
不要な平方根の計算が入るせいか少しだけ遅かったので、最初の書き方を採用しています。
np.linalg.norm(X-y, axis=1).argmin()

データフレームの背景に棒グラフを表示する

データフレームの書式設定シリーズの最後の記事です。
今回は背景にセルの値の大きさを表す棒グラフを表示します。
これ、地味に便利です。
ドキュメントはこちら

使い方は簡単で、df.style.bar()を呼び出すだけです。
オプションも色々指定子できますので、いくつか設定してやってみます。


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(-100, 100, size=(10, 4)),
        columns=["col0", "col1", "col2", "col3"]
    )

df.style.bar(
    align="mid",
    width=90,
    axis=None,
    color=['#d65f5f', '#5fba7d']
)

出力結果がこちら。

引数をいくつか説明しておきます。
まず align。 以下の3種類の値のどれかを取ります。

left
値が最小のセルの値が左端。デフォルト。
zero
ゼロがセルの中心
mid
最大値と最小値の平均か、もし正の数と負の数を両方含む場合は0が中心。

次に width は0〜100の値を取り、棒グラフの最大長がセルの何%を締めるかを表します。
colerは棒グラフの色です。文字列を一つ渡せば全てその色、配列にして2つ渡せばそれぞれ負の値の時と正の値の時の色です。
axis は最大最小値の基準が列方向(0)、行方向(1)、テーブル全体(None)のどれかを表します。
例では使っていませんが、vmin, vmax で最小値、最大値を指定することもできます。
外れ値があるような時は便利です。

データフレームの書式設定に組込書式を使う

データフレームを表示するときにCSSで書式設定する方法(apply,applymapを使う)を紹介してきましたが、
最初の方で少し書いた通り、これらの方法を使わなくてもよく使う書式はあらかじめ関数が用意されています。

ドキュメント: Builtin styles

非常によく使う、最大値最小値やNullの強調には、それぞれ次の3つの関数が使えます。

  • df.style.highlight_null(null_color=’red’)
  • df.style.highlight_max(subset=None, color=’yellow’, axis=0)
  • df.style.highlight_min(subset=None, color=’yellow’, axis=0)

subsetは対象の列を指定でき、colorは背景色、axisは最大値や最小値を評価する軸方向を指定します。

また、値の大小を色の濃淡(もしくは色相など)で、表現するには、df.style.background_gradientが使えます。
引数は次の通り。だいたいイメージ通り動きます。
df.style.background_gradient(cmap=’PuBu’, low=0, high=0, axis=0, subset=None, text_color_threshold=0.408)

cmapに渡すのはMatplotlibのcolormapです。
具体的な名前と色はこちらを参照して選びます。
Choosing Colormaps in Matplotlib

そして最後に、値によらず、データフレーム全体の書式を一括変更する時は、
set_propertiesを使います。
ドキュメントに例が載っていますが、これだけ少し引数の渡し方が独特なので注意です。
サンプル:


df.style.set_properties(**{'background-color': 'black',
                           'color': 'lawngreen',
                           'border-color': 'white'})

データフレームの書式を列や行ごとに設定する

前回の記事でデータフレームの書式設定をセル単位で設定する方法を紹介しました。
個々のセルの値だけで書式が決められうる場合はそれで十分なのですが、
実際の業務では列ごとの最大値や最小値などを目立たせたい場合がよくあります。
この場合、他のセルも参照しなければ書式を決められません。(厳密に言えば別途変数か何かを定義して実装できますが面倒です。)

このような時は、style.applymapの代わりに、style.applyを使います。
そして、渡す関数は、セルの値ではなく、DataFrameの行や列、つまりSeriesを受け取り、
CSS書式の配列を返す関数です。
引数axisに0(既定)を渡すと列ごと、1を渡すと行ごとに書式を設定できます。

試しに、最大値に色を塗ってみます。


def max_style(values):
    max_value = max(values)
    styles = [
        "background-color: yellow" if value == max_value else ""
        for value in values
    ]
    return styles


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )

# 各列の最大値に着色 (axis=0 は省略可能)
print(df.style.apply(max_style).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15


# 各行の最大値に着色
print(df.style.apply(max_style, axis=1).render())
col0 col1 col2
0 9 5 47
1 6 99 17
2 46 11 63
3 27 92 54
4 6 35 15

キャプチャ貼るのが面倒だったので、
結果はrender()で貼りました。

notebookでデータフレームを表示するときにセルの書式を設定する

pandasのデータフレームの値をjupyter notebookで確認するとき、
エクセルの条件付き書式のようにセルの値によって色を塗ったりするとわかりやすくなることが多くあります。

ネットで少し探せば、すぐにコードが出てくるのでよく理解せずに background_gradient などを使っていましたが、
先日のPyConで、@komo_frさんのセッション、pandasのStyling機能で強化するJupyter実験レポートを聞いて、ちゃんと体系立てて覚えて使おうというモチベーションが湧いてきたので、ドキュメントを読み始めました。

先述の background_gradient とか、 highlight_null とか 便利関数が用意されているのですが、
その前に基本から紹介していこうと思います。

今回は、単純にセルの値によって書式を指定する Styler.applymapです。
ドキュメントはここ

「データフレームの値を引数として受け取り、セルに設定したいCSS文字列を返す関数」をapplymapに渡すことで、
DataFrameの書式を設定します。

CSSっぽいな、というのは前々から感じてたのですが、CSSそのものだったんですね。
(CSSとよく似た独自構文を覚えなきゃ使えないのかと思ってました。)
ドキュメントにもそのまま「スタイル設定は、CSSを使用して行われます。」と書いてあるのでちゃんと読んでおけばよかったです。
The styling is accomplished using CSS.

では早速ですが、適当なデータフレームを作ってみて、値が入ってないセル、一定値より小さいセル、
その他のセルで書式を変えて表示してみました。


def cell_style(value):
    if value != value:
        return "background-color: gray; color: white"
    if value <= 40:
        return "background-color: yellow; font-weight: bold"
    else:
        return ""


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(0, 100, size=(5, 3)),
        columns=["col0", "col1", "col2"]
    )
df.loc[3, "col0"] = None
df.loc[1, "col2"] = None
df.style.applymap(cell_style)

jupyter notebookで実行したときに表示されるのがこちら。

また、.render()を使ってHTML出力もできます。
スタイルが思ったように適用されてないように感じたら、これを使って確認すると良いそうです。


print(df.style.applymap(cell_style).render())

実行して出力されたHTMLを記事中にそのまま貼り付けたのがこちらです。便利ですね。

col0 col1 col2
0 52 35 48
1 81 18 nan
2 37 80 33
3 nan 80 72
4 91 4 63

Type Hintで引数と戻り値の型を注記する

Python 3.5 から実装されている機能で、関数を定義するときに引数や戻り値の型を注記(アノテーション)する
Type Hint という機能があります。
ドキュメント 

次の例のように、引数の後ろには「:」をつけて型を書き、戻り値は行末の「:」の前に「->」を付けて型を書きます。
このように定義しておくと、help関数などでその関数が想定しているデータ型を確認できます。


def add_sample(x: int, y: float) -> float:
    return x + y


help(add_sample)
"""
Help on function add_sample in module __main__:

add_sample(x:int, y:float) -> float
"""

注意としては、あくまでもこれは注記で、本当にその型しか受け付けなくなったり、その方の戻り値を返すことを保証したりしないことです。
サンプルの例で言えば、float同士を受け取っても普通に計算しますし、文字列を渡せば結合します。


print(add_sample(2.5, 3.7))
# 6.2
print(add_sample("Type ", "Hint"))
# Type Hint

あくまでも可読性のための機能ですが、
便利に使える場面は多そうなので今後積極的に使っていこうと思います。


個人的な話になりますが、エンジニア?としてのキャリアの初期にJavaやExcel VBAばかり触っていた影響か、
実は静的型付け言語のほうが好きだったりします。(Pythonは動的型付け)
Python自体はかなり気に入っているので別に良いのですが。

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を使うところに注意が必要です。