matplotlibのメモリラベルテキストの回転だけをオブジェクト指向インターフェースで行いたい

主にX軸のメモリが日付などの場合に発生するのですが、matplotlibのメモリテキストが重なって読めなくなることがあります。
それをテキストを回転させて読めるようにする方法のまとめです。

とりあえず、プロットするデータを生成しておきます。


import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import numpy as np
import pandas as pd

# 日付のリスト生成
dates = [datetime(2021, 1, 1) + timedelta(days=i) for i in range(10)]
date_str_list = [d.strftime("%Y-%m-%d") for d in dates]
# ランダムウォークデータ作成
data = np.cumsum(np.random.randn(10))

他サイトなどを見ると、よく用いられているのは、
plt.xticks(rotation=角度)
です。
結果だけ見れば正直これで十分なのですが、
これでも確かに回転できますが、これは、オブジェクト指向インターフェースではなく、ここだけpyplotインターフェースになります。
個人的に、これらを混在させるのは好きではありません。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
plt.xticks(rotation=90)
plt.show()

そしてオブジェクト指向インターフェースで回転させる方法として、よく見かけるのは、
ax.set_xticklabels(xlabels, rotation=角度)
とするものです。
これでもラベルを回転できるのですが、第一引数のxlabelsが省略できず、改めて指定する必要があります。
回転だけやりたいんだというときにこの仕様は面倒です。

前置き長くなりましたが、以上の2個の方法の良いとこどりで、オブジェクト指向インターフェースで回転だけを実行する方法を調べました。
結果として以下の方法があることがわかりました。

ax.tick_params(axis="x", labelrotation=角度)
ax.xaxis.set_tick_params(rotation=角度)
引数はどちらもlabelrotation=でもrotation=でも両方対応しているようです。

それぞれコード例は以下のようになります。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
ax.tick_params(axis="x", labelrotation=90)
plt.show()

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(date_str_list, data)
ax.xaxis.set_tick_params(rotation=90)
plt.show()

matplotlibのdpiとfigsizeの正確な意味を調べてみた

以前、「matplotlibはdpiの数値を大きくすると画像の解像度が上がる」、みたいな非常に雑な理解で記事を書いたのですが、改めて仕様を確認したのでそのメモです。
参考: matplotlibのグラフを高解像度で保存する
前提として、「dpi」は「dots per inch」の略で1インチあたりのドット数を意味します。
正直、画面に表示される画像のサイズというのは、PCのディスプレイサイズに大きく依存するので、1インチを定義することなんてできないだろうと思って流していたのですが、
実は、 figsizeで指定する数値の単位がインチだったようです。

参考: matplotlib.figure.Figure

figsize(float, float), default: rcParams[“figure.figsize”] (default: [6.4, 4.8])
Width, height in inches.

したがって、 figsize で指定した(横幅, 縦幅) と dpi を掛け合わせると、
生成される図のピクセル数が (横幅*dpi) × (縦幅*dpi) と求まるということのようです。

デフォルトの設定(rcParamsでfigsizeとdpiは確認できる)で、figureを生成して確認してみましょう。


import matplotlib.pyplot as plt

print(plt.rcParams["figure.figsize"])
# [6.0, 4.0]
print(plt.rcParams["figure.dpi"])
# 72.0
fig = plt.figure()
# <Figure size 432x288 with 0 Axes>

ドキュメントによると figsizeのデフォルトは、[6.4, 4.8]、dpiのデフォルトは100のはずなのですが、なぜか僕の環境はそれぞれ、
[6.0, 4.0]と 72になってます。これの原因は不明ですが話の本題とズレるので置いておきます。

生成された図のサイズは、横幅が 6*72=432ピクセル、縦幅が4*72=288ピクセルと、理論通りになりました。

例えば、(滅多にそんな要件はないでしょうが)フルHDサイズのグラフ(1920×1080)が作りたかったら、
figsizeを(16, 9)、dpiを120で作成すれば良いことになります。


fig = plt.figure(figsize=(16, 9), dpi=120)
# <Figure size 1920x1080 with 0 Axes>

文字列のバイト数を取得する

Amazon Comprehend も Amazon Translate も、1度に処理できるテキストサイズの上限が文字数ではなく、バイト数で決まっています。(どちらも5000byte)
とりあえずAPIに投げてエラー処理で対応してもいいのですが、あまり無駄にAPIを叩くようなことをはしない方がいいので、
できるだけ事前にAPIに渡すテキストのサイズが上限を超えていないことを確認することが望ましいです。

そこで、Pythonの文字列の長さをバイト数で取得する方法を調べました。
結論から言うと、バイト列オブジェクトにエンコードして、その後に長さを調べると良いようです。
エンコードには、 str.encodeを使います。

以下のようにして、”走れメロス”が15バイトであることがわかりました。


title="走れメロス"
print(len(title.encode()))
# 15

matplotlibでグラフ枠から見た指定の位置にテキストを挿入する

matplotlibでは、 matplotlib.axes.Axes.text メソッドを指定して、グラフ中にテキストを挿入できます。
このとき、通常は、 ax.text({x座標}, {y座標}, {挿入したいテキスト}) という構文でテキストが入れられます。
ここで言う、 x座標/ y座標は それぞれx軸、y軸に対応した座標になります。当然ですね。
散布図の点や、グラフの頂点に文章を補足するときはとてもありがたい仕様ですが、
その反面、グラフの左上の方、とか中央といった、グラフの中の特定の場所にテキストを入れたい場合はちょと不便です。
それは、グラフの枠の左上や中央の座標が何になるか非自明だからです。

このニーズに対応して、x軸、y軸の座標ではなく、グラフ内部の位置でテキストの位置を指定できることがわかったので紹介します。

ドキュメントの Examples のところに 例があるのですが、transform=ax.transAxesを指定すると、ax.textのx座標、y座標はx軸y軸とは関係なくなり、
グラフの枠の左下を(0, 0), 右上を(1, 1)とする相対座標に変わります。

これを使って、グラフのすみの方や中央に簡単にテキストを置けます。

ちなみに、デフォルトでは、指定した座標の位置に、テキストの左下が当たるように配置されます。
これは、縦位置は verticalalignment{‘center’, ‘top’, ‘bottom’, ‘baseline'(デフォルト), ‘center_baseline’}、
横位置はhorizontalalignment{‘center’, ‘right’, ‘left'(デフォルト)} という引数で変更することができます。
引数名が長いので、それぞれ、va, ha というエイリアスを用意してくれています。
これらの引数は、 transform=ax.transAxes を指定しない時も同じように使えるので覚えておきましょう。

あまり面白い例でなくて恐縮ですが、色々パターンを試したコードとその出力を置いておきます。


import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-np.pi, np.pi, 101)
y = np.sin(x)


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(1, 1, 1)
ax.plot(x, y)

# 通常は、指定した座標の位置にテキストが挿入される
ax.text(np.pi/2, 1, "$(\pi/2, 1)$")
ax.text(-np.pi/2, -1, "$(-\pi/2, 1)$")

# transform=ax.transAxes を指定すると、図形の枠を基準にした位置にテキストが挿入される
ax.text(0.01, 0.01, "左下", transform=ax.transAxes)
ax.text(0.01, 0.99, "左上", verticalalignment='top', transform=ax.transAxes)
ax.text(0.99, 0.01, "右下", horizontalalignment='right', transform=ax.transAxes)
ax.text(0.99, 0.99, "右上", va='top', ha='right', transform=ax.transAxes)
ax.text(0.5, 0.5, "中央", va='center', ha='center', transform=ax.transAxes)
# 0 ~ 1 の範囲を超えるとグラフの外にテキストを配置できる
ax.text(0.5, 1.05, "上部枠外", ha='center', transform=ax.transAxes)
ax.text(1.02, 0.5, "右側枠外", va='center', transform=ax.transAxes, rotation=270)

plt.show()

出力はこちらです。

transform 引数に他にどんな値を設定しうるのか探したところ、以下のページが見つかりました。
参考: Transformations Tutorial
ax.transData がデフォルトのデータにしがたった座標軸みたいですね。
ax.transAxes の他にも、fig.transFigureやfig.dpi_scale_trans を使って、図全体の中での相対位置で
テキストを配置することもできるようです。
fig.transFigure のほうが、左下が(0, 0)、右上が(1, 1) となる座標で、
fig.dpi_scale_trans はピクセル数を使った具体的な座標指定です。

matplotlibのグラフのx軸とy軸を反転する方法まとめ

先日、matplotlibで作った少し手の込んだグラフを、x軸とy軸を反転したくなることがありました。
少々手こずったのでその時調べた内容をまとめておきます。

matplotlibでは、グラフの種類によってx軸y軸の反転方法が異なります。
大きく分けて次の3パターンがあるようです。

1. x座標とy座標の引数の順番を入れ替えるだけ。
2. x軸とy軸を反転させたバージョンの別のメソッドが用意されている。
3. グラフを書くときに引数で向き(orientation)を指定する。

また、物によっては反転させることができないものもあるようです。

具体的に一つやってみます。
まずデータの準備です。


# 必要ライブラリのインポートとデータ生成
import matplotlib.pyplot as plt
from scipy.stats import beta
import numpy as np

beta_fz = beta(a=3, b=2)
data = beta_fz.rvs(100)
# 真の期待値
x_mean  = beta_fz.stats("m")

そして、ここで生成したデータを元に、以下のグラフと、そのxy軸反転版を書いてみました。
– データのヒストグラム
– 元の分布の確率密度関数
– 元の分布の期待値の位置を表す線
– 元の分布のデータの70%のデータが含まれる区間の塗り潰し


x = np.linspace(0, 1, 101)
x_clip = np.linspace(beta_fz.ppf(0.15), beta_fz.ppf(0.85), 101)

fig = plt.figure(figsize=(12, 6), facecolor="w")

ax = fig.add_subplot(1, 2, 1, title="元のグラフ")
ax.hist(data, density=True, alpha=0.3)
ax.plot(x, beta_fz.pdf(x))
ax.fill_between(x_clip, beta_fz.pdf(x_clip), alpha=0.3, color="orange")
ax.vlines(x_mean, ymin=0, ymax=beta_fz.pdf(x_mean))

ax = fig.add_subplot(1, 2, 2, title="x軸y軸反転したグラフ")
# ヒストグラムは orientation="horizontal" を指定する
ax.hist(data, density=True, orientation="horizontal",  alpha=0.3)
# plot は渡す引数の順番を入れ替えるだけ
ax.plot(beta_fz.pdf(x), x)
# fill_between に対しては、 fill_betweenx メソッドが用意されている
ax.fill_betweenx(x_clip, beta_fz.pdf(x_clip), alpha=0.3, color="orange")
# vlines に対しては、 hlines メソッドが用意されている
ax.hlines(x_mean, xmin=0, xmax=beta_fz.pdf(x_mean))

plt.show()

出力されたグラフが次です。

x軸とy軸が入れ替わりました。 コードをみていただくとそれぞれ反転方法が異なっていることがわかると思います。

x座標とy座標の引数の順番を入れ替えるだけのパターンには、
ax.plot(折れ線グラフ)やax.scatter(散布図)があります。

x軸とy軸を反転させたバージョンの別のメソッドが用意されているパターンには、
棒グラフであれば、ax.bar と ax.barh、
エリアの塗りつぶしであれば、ax.fill_between と ax.fill_betweenx、
縦線であれば、 ax.vlines と ax.hlines
などの組み合わせがあります。
ドキュメントのこちらのページで似てる名前のメソッドを探すと良いでしょう。

グラフを書くときに引数で向き(orientation)を指定するパターンは、
だいたい、ヒストグラム ax.hist だけと考えても良さそうです。
これはそれぞれのメソッドのドキュメントを読めば判断することができます。
(もしかしたら他にもあるかもしれませんが。)

一部のグラフには反転方法がわからないものもありましたが、実用上困ることはなさそうです。

pytablewriterで、pandasのDataFrameからMarkdownの表を生成する

前回の記事に続いて、pytablewriterの機能の紹介です。
参考: pytablewriterでMarkdownの表を生成する

今回はpandasのDataFrameからMarkdownを生成します。
個人的には使う頻度が高いのはこちらです。

まずサンプルとなるデータを作っておきましょう。


import pandas as pd

df = pd.DataFrame(
    data=[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
    columns=['col1', 'col2', 'col3'],
    index=["data1", "data2", "data3", "data4"]
)
print(df)
"""
       col1  col2  col3
data1     1     2     3
data2     4     5     6
data3     7     8     9
data4    10    11    12
"""

早速、このDataFrameをMarkdownに変更します。

利用するのは、 from_dataframe と言うメソッドです。
ドキュメント: 4.4.3. Using pandas DataFrame as tabular data source

次のコードの例のように、 from_dataframe() にDataFrameを渡し、
write_table()メソッドを呼び出すと標準出力にマークダウンが出力されます。


from pytablewriter import MarkdownTableWriter


writer = MarkdownTableWriter()
writer.from_dataframe(df)
writer.write_table()
"""
|col1|col2|col3|
|---:|---:|---:|
|   1|   2|   3|
|   4|   5|   6|
|   7|   8|   9|
|  10|  11|  12|
"""

配列から生成するときはメソッドではなく属性にデータを代入していたのでちょっと使い方が違いますね。

さて、結果をご覧の通り、デフォルトではindexは出力されません。
もしindexが必要な場合は、 from_dataframe()を呼び出す時に、 add_index_column=True を指定する必要があります。


writer = MarkdownTableWriter()
writer.from_dataframe(df, add_index_column=True)
writer.write_table()
"""
|     |col1|col2|col3|
|-----|---:|---:|---:|
|data1|   1|   2|   3|
|data2|   4|   5|   6|
|data3|   7|   8|   9|
|data4|  10|  11|  12|
"""

これでindexも出力されました。

pytablewriterでMarkdownの表を生成する

集計結果を社内に共有する時など、Markdownでテーブルを書く機会はそこそこ頻繁にあります。
(実はBacklogを使っているので、正式なMarkdownとは違うのですが似たようなものです。)

毎回、Vimで置換等を使って書いて、それを貼り付けたりとかしているのですが、
Pythonに pytablewriter という便利なライブラリがあるのを見つけたのでそれを紹介します。
自分の場合は pandas の DataFrameを変換することが多く、それ専用の関数もあるのですが、
まずはこの記事ではドキュメントの Basic usage に沿った基本的な使い方を紹介します。

ドキュメント: 4.1. Basic usage — pytablewriter

ドキュメントの例を参考に、少し書き換えたコードでやってみます。


from pytablewriter import MarkdownTableWriter

writer = MarkdownTableWriter()
writer.table_name = "zone"
writer.headers = ["zone_id", "country_code", "zone_name"]
writer.value_matrix = [
    ["1", "AD", "Europe/Andorra"],
    ["2", "AE", "Asia/Dubai"],
    ["3", "AF", "Asia/Kabul"],
    ["4", "AG", "America/Antigua"],
    ["5", "AI", "America/Anguilla"],
]

writer.write_table()
"""
# zone
|zone_id|country_code|   zone_name    |
|------:|------------|----------------|
|      1|AD          |Europe/Andorra  |
|      2|AE          |Asia/Dubai      |
|      3|AF          |Asia/Kabul      |
|      4|AG          |America/Antigua |
|      5|AI          |America/Anguilla|
"""

テーブル名、ヘッダー、データを順番に指定してあげて、
その後、write_tableを呼び出すとマークダウンのテキストを生成してくれますね。

カラム名は指定しないと、A,B,C,Dとアルファベットが割り当てられるようです。


writer = MarkdownTableWriter()
writer.value_matrix = [
    ["1", "AD", "Europe/Andorra"],
    ["2", "AE", "Asia/Dubai"],
    ["3", "AF", "Asia/Kabul"],
    ["4", "AG", "America/Antigua"],
    ["5", "AI", "America/Anguilla"],
]
writer.write_table()
"""
| A | B |       C        |
|--:|---|----------------|
|  1|AD |Europe/Andorra  |
|  2|AE |Asia/Dubai      |
|  3|AF |Asia/Kabul      |
|  4|AG |America/Antigua |
|  5|AI |America/Anguilla|
"""

引数で渡すのではなく、.value_matrixなどの変数にデータを渡すのが特徴的で、
ちょっと使い方に癖がありますが手軽にマークダウンを生成できるので便利ですね。

n進法(特に2進法,8進法,16進法)の文字列を10進法の数値に変換する

以前書いた記事の逆向きの操作が必要になったのでそのメモです。
参考: Pythonの数値を2進法、8進法、16進法の表記に変換する

結論から言うと intメソッドでできます。
そのまま数字として解釈できる文字列(要するに0から9と+-)しか受け付けないと思い込んでいたのですが、
ドキュメントを読んでみると、2つ目の引数baseで、基数を指定できます。
ここに8なり16なりを渡してあげれば良いようです。


print(int("FF", 16))
# 255

print(int("100", 8))
# 64

base は 0と2~36までの引数を指定できます。(1はダメ)
36進法は 0~9までの数値と、a~z(A~Zでも可)の文字を使った表記ですね。


print(int("AZ09", 36))
# 511929

base=0 を指定した場合、文字列の先頭のプレフィックスを使って、2進法,8進法,10進法,16進法を判定するようです。


# 2進法
print(int("0b100", 0))
# 4

# 8進法
print(int("0o100", 0))
# 64

# 10進法
print(int("100", 0))
# 100

# 16進法
print(int("0x100", 0))
# 256

PythonでROT13暗号

業務で使う機会があったので紹介します。

非常に単純な暗号方式のに、シーザー暗号と呼ばれるものがあります。
参考: シーザー暗号 – Wikipedia

暗号鍵として整数nを決め、アルファベットをn文字ずらしたものに変換するものです。
$n=3$ であれば、aはdに、GはJに、Zはまた先頭に戻ってCに変換されます。
複合はその逆です。

この暗号鍵を13にしたものをROT13と呼びます。
参考: ROT13 – Wikipedia
アルファベットが大文字小文字それぞれ26文字であり、そのちょうど半分をスライドさせることから、
2回適用すると元に戻ると言う特徴があります。要するに暗号化と複合が全く同じと言う特徴があります。

これはPythonでは、 codecs という標準ライブラリの中に実装されています。
参考: codecs — codec レジストリと基底クラス — Python 3.9.1 ドキュメント

早速やってみます。


import codecs


text = "Hello World"
print(codecs.encode(text, "rot_13"))
# Uryyb Jbeyq

# もう一度適応すると元に戻る
print(codecs.encode("Uryyb Jbeyq", "rot_13"))
# Hello World

# encode と decodeの結果が等しい
print(codecs.decode(text, "rot_13"))
# Uryyb Jbeyq

簡単ですね。

コード中の “rot_13” ですが、 “rot13″というアンダーバー無しのエイリアスも用意されています。
そのため、以下のコードでも動きます。


print(codecs.encode(text, "rot13"))
# Uryyb Jbeyq

このROT13ですが、アルファベット以外の文字にには作用しません。数字や日本語の文字などはそのまま残ります。


print(codecs.decode("ROT13暗号はアルファベット以外そのまま", "rot13"))
# EBG13暗号はアルファベット以外そのまま

pandasのDataFrameをある列の値が特定の区間に含まれる行のみに絞る

pandasのSeriesにbetweenという便利なメソッドが定義されていたのでその紹介です。
これを使わなくても何も難しいことのない話なのですが、コードが少し短くなってすっきりするので気に入っています。

ドキュメント: pandas.Series.between

これは、Seriesに対して、最小値、最大値を渡すと、
Seriesの各値に対して、その値が最小値と最大値の範囲に入っていればTrue, 入っていなければFalseを返すメソッドです。
これを使って、DataFrameの列の絞り込みができます。

試すために、いつものirisでDataFrameを作っておきます。


import pandas as pd
from sklearn.datasets import load_iris
# アヤメのデータを読み込んでDataFrameに整形
iris = load_iris()
columns = [c.replace(" (cm)", "") for c in iris.feature_names]
df = pd.DataFrame(iris.data, columns=columns)
df["target"] = [iris.target_names[t] for t in iris.target]
print(df.head())
"""
   sepal length  sepal width  petal length  petal width  target
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa
"""

このようなデータから、たとえば、”petal length” が 4.0以上4.5以下の行を抽出したいとします。
それを単純に書くとこうなります。


df[(df["petal length"] >= 4.0) & (df["petal length"] <= 4.5)]

DataFrameの変数名を3回も書かないといけないですし、ちょっと冗長ですね。

これが betweenを使うと、次のように書けます。


df[df["petal length"].between(4.0, 4.5)]

少しだけすっきりしました。

実際に絞り込めているのを確認しておきましょう。 describe()メソッドを使って、その中から最小値と最大値だけ取ってみます。


print(df[df["petal length"].between(4.0, 4.5)].describe().loc[["min", "max"]])
"""
     sepal length  sepal width  petal length  petal width
min           4.9          2.2           4.0          1.0
max           6.7          3.4           4.5          1.7
"""

注意ないといけないのは、選択されるのは、
1つ目の引数 $<=$ 指定列の値 $<=$ 2つ目の引数 と言うふうに左右両方とも等号が入った閉区間の値であることです。 3つ目の引数、 inclusive にFalse を指定すると等号を含まなくなるのですが、これは最大最小両方とも統合を含まず、 1つ目の引数 $<$ 指定列の値 $<$ 2つ目の引数 の区間を取得するようになります。


print(df[df["petal length"].between(4.0, 4.5, False)].describe().loc[["min", "max"]])
"""
     sepal length  sepal width  petal length  petal width
min           5.5          2.3           4.1          1.0
max           6.7          3.1           4.4          1.5
"""

一番頻繁に使う、x以上、y未満、と言う形式の指定ができないのが短所ですね。

以上未満で指定したい場合は、下記のようにqueryメソッドなど別の方法も検討しましょう。


df.query("4.0 <= `petal length` < 4.5")