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")

matplotlibで複数のグラフを含む図に全体のタイトルをつける

matplotlibで複数のグラフを含む図を作る時、全体にタイトルをつけたいことがあります。
個々のグラフはadd_subplotsする時にtitle引数で指定するか、もしくは、set_titleすれば指定できるのですが、
figureについては、plt.figure()する時にtitle引数を受け取ってくれないですし、
set_titleのようなメソッドも持っていません。

そのため、ちょっと迷っていたのですが、fig.suptitleでタイトルをつけることができることがわかりました。

ドキュメントはここです。: suptitle

適当な4グラフでやってみます。


import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(10, 8), facecolor="w")
# 全体にタイトルをつける
fig.suptitle("全体のタイトル")

# 以下、適当なグラフを作る
ax = fig.add_subplot(2, 2, 1, title="棒グラフ")
ax.bar(range(5), np.random.randint(100, size=5))
ax = fig.add_subplot(2, 2, 2, title="折れ線グラフ")
ax.plot(range(5), np.random.randint(100, size=5))
ax = fig.add_subplot(2, 2, 3, title="散布図")
ax.scatter(np.random.randn(10), np.random.randn(10))
ax = fig.add_subplot(2, 2, 4, title="円グラフ")
ax.pie(np.random.randint(1, 10, size=5))

plt.show()

出力がこちら。

手軽ですね。

もしタイトルの位置を微調整したい場合は、
引数 x, y で位置を変えることができます。
デフォルトの値は x=0.5 と y=0.98 です。
それずれタイトルを左右中央に配置することと、図形の上部に置くことを意味しています。
xを0にすれば左揃えになりますし、 yを0付近にすれば図の下部にタイトルを置くことができます。

Numpyだけで重回帰分析

興味本位でNumPyの多項式回帰(polyfit)のソースコードを読んでいたのですが、
その中でNumPyにも重回帰分析のメソッドが用意されていて使われているのを見つけました。
てっきり重回帰分析は、scikit-learnかstatsmodelsを使うか、もしくはNumpyでやるならスクラッチ実装しないといけないと思い込んでいたので意外でした。
使ってみるとかなり手軽に使えたのでこの記事で紹介します。

ちなみに多項式回帰については既に記事を書いているのでご参照ください。
参考記事: NumPyで多項式回帰

紹介する関数はこちらです。
numpy.linalg.lstsq
(正直この名前はドキュメントでは探しにくい。おそらく、least-squaresの略語です。)

とりあえず、使ってみましょう。
ダミーデータとして、
$$
y = 3x_0 -2 x_1 + 5x_2 + \varepsilon
$$
のデータを作っておきます。$\varepsilon$はノイズです。


import numpy as np
X = np.random.randn(100, 3)
y = X@np.array([[3], [-2], [5]]) + np.random.randn(100).reshape(100, 1)

さて、早速lstsqを使ってみます。
使い方は簡単で、先ほどのXとyを渡してあげて、あと、rcondという引数を指定するだけです。
rcondは指定しないとwarningが出ますが、指定しなくても動きます。
小さい特異値を切り捨てる割合を指定する方法で、Noneか-1か指定しておけば良さそうです。


print(np.linalg.lstsq(X, y, rcond=None))
"""
(array([[ 2.97422787],
       [-2.01082975],
       [ 4.81883873]]),
       array([112.27261964]),
       3,
       array([10.54157065,  9.62381814,  8.55906245]))
"""

さて、ご覧の通り、結構いろいろな値がタプルで戻ってきました。
最初のArrayの
array([[ 2.97422787],
[-2.01082975],
[ 4.81883873]]),
の部分が推定した係数です。正解の 3, -2, 5 に近い値になっているのがわかります。
次の、[112.27261964]の値は残差の平方和です。
そして、 3 は Xのrank、次の array([10.54157065, 9.62381814, 8.55906245]) は Xの特異値です。

残渣平方和が 112.27261964 になるのは計算してみておきましょう。


coef, rss, rank, s = np.linalg.lstsq(X, y, rcond=None)
# 予測値を計算
p = X@coef
# 残差の平方和を計算
print(((y-p)**2).sum())
# 112.2726196447206

バッチリですね。

ここまでの流れで、気付いた方もいらっしゃると思いますが、lstsqで重回帰分析すると、定数項が出てきません。
定数項を含めて重回帰分析するには、Xに値が全部1になる列を追加して、それを渡す必要があります。
Numpyだけでもできますが、 statsmodels の add_constant あたりを使ってもいいでしょう。

例えば、
$$
y = 3x_0 -2 x_1 + 5x_2 + 4 + \varepsilon
$$
をダミーデータを作って、回帰分析するとこまでやると次のようになります。


import numpy as np
import statsmodels.api as sm

# ダミーデータ生成
X = np.random.randn(100, 3)
y = X@np.array([[3], [-2], [5]]) + 4 + np.random.randn(100).reshape(100, 1)

# Xに定数項を追加したデータを生成
X_add_const = sm.add_constant(X)

# 回帰分析
coef, rss, rank, s = np.linalg.lstsq(X_add_const, y, rcond=None)

# 推定した係数を表示
print(coef)
"""
[[ 4.01847308]
 [ 3.02763718]
 [-1.98363017]
 [ 4.99985177]]
"""

最初の 4.018…が定数項で残りが係数です。

matplotlibで複数のグラフを含むアニメーションを作成する

再びmatplotlibの動画の話です。
先日、一枚のfigureの中に複数のグラフを持っている図をアニメーションにしたいことがありました。
これもすぐにできると思ったのですが、方法が理解できるまで結構手間取ったので記録として残しておきます。

ドキュメントの ArtistAnimation のページには明記されてないし、
Examples の Animationの一覧 をみても、
サンプルは全部1枚の図に1つのグラフのもので、2グラフを動かしている例はないんですよね。(※記事執筆時点の情報です)

さて、方法ですが結論としては、次のようなコードで実現できました。
matplotlibでgif動画生成 の記事のコードがグラフ一つの例なので、見比べてみてください。


import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import ArtistAnimation
fig = plt.figure(facecolor="w")
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
# 0 <=x < 2pi の範囲の点列を作成。
x = np.linspace(0, 2*np.pi, 101)[: -1]
# 各コマの画像を格納する配列
image_list = []
# 1周だと短すぎたので5回繰り返す
for _ in range(5):
    for i in range(100):
        # ずらしながらsinカーブを描写し、配列に格納
        y1 = np.sin(np.roll(x, -i))
        y2 = np.cos(np.roll(x, -i))
        # 一つ目のグラフを描写する
        image1 = ax1.plot(x, y1, c="b")
        # 二つ目のグラフを描写する
        image2 = ax2.plot(x, y2, c="g")
        # 同時に描写したいグラフを連結したものを配列に追加する。
        image_list.append(image1+image2)
# アニメーションを作成
ani = ArtistAnimation(fig, image_list, interval=10)
# mp4ファイルに保存
ani.save('animation.mp4', writer='ffmpeg')
# gifファイルに保存する場合
# ani.save('animation.gif', writer='pillow')

ポイントになるのはこの部分です。
作成したグラフの配列を連結して、それをコマの配列に追加していっています。


image_list.append(image1+image2)

image1(とimage2)はそれぞれ、グラフの配列(要素は一個だけ)です。


print(image1)
# [<matplotlib.lines.Line2D object at 0x7fbe7dc4eed0>]
print(image2)
# [<matplotlib.lines.Line2D object at 0x7fbe7db8cf90>]

これを足すことで配列が連結されています。


print(image1+image2)
# [<matplotlib.lines.Line2D object at 0x7fbe7dc4eed0>, <matplotlib.lines.Line2D object at 0x7fbe7db8cf90>]

これ一コマとして、それらの配列を作り、ArtistAnimation メソッドに渡すことで動画にできます。
出来上がったのがこちらです。

最初、順番に更新したらいいかと思ってこういうのを試しましたが、これは2つのグラフが同時には表示されず、
1枚ずつ表示されるので非常にチカチカした見た目になります。


# 失敗例1
image_list.append(image1)
image_list.append(image2)

次に試みたのがこれ。これはこの後の ArtistAnimation でエラーになりました。
(配列の階層が深くなりすぎたようです)


# 失敗例2
image_list.append([image1, image2])

FFmpegでgif動画をmp4動画に変換する

以前、matplotlibでgif動画を作る方法を紹介しましたが、実際に業務で使っていると出来上がったgifファイルのサイズの大きさに困ることがよくありました。
参考: matplotlibでgif動画生成

gifファイルのままサイズを圧縮する方法をいろいろ調べていたのですが、どうやらmp4形式に変換するのが一番圧縮率が良さそうということがわかりました。
(mp4よりgifの方が軽いと勘違いしていたのですが、完全にただの思い込みだったようです。)

ということで、Macでgifファイルをmp4に変換する方法を紹介します。

利用するのは、FFmpegというツールです。
公式サイト: FFmpeg

HomeBrewでインストールできるという情報が各所にあったのですが、今時点の環境では失敗しました。


$ brew install ffmpeg 
#
# いろんなメッセージ (略)
#
Error: The following formula
  [#<Dependency: "python@3.9" []>, #<Options: []>]
cannot be installed as binary package and must be built from source.
Install the Command Line Tools:
  xcode-select --install

最後に出てくる、 xcode-select --install も試しても動かないのでお手上げです。

その一方で、 condaでもインストールできるらしいことがわかったので、僕はcondaでインストールしました。
(バージョンが古いという噂もあります。)


$ conda install ffmpeg

これでインストールしてしまえば、次のコマンドでmp4形式に変換できます。


$ ffmpeg -i [元のgifファイル名].gif -pix_fmt yuv420p [作成するmp4ファイル名].mp4 

先日の記事の sinカーブを動かす動画であれば、元々1.4MBもあったのが、33KBまで軽くなりました。

NumPyで多項式回帰

前回の記事で、NumPyの多項式オブジェクトを紹介したので、ついでに多項式回帰を行う方法を紹介したいと思います。
1変数の多項式回帰に関してはscikit-learnを使うよりもNumPyの方が手軽なのでおすすめです。

使うのは、 numpy.polyfit というメソッドです。
これの引数に、回帰したいデータセットのx座標とy座標をそれぞれリストで渡し、3つ目の引数で回帰する次元を渡すだけです。
戻り値は回帰した結果の係数が配列で得られます。


import numpy as np

# 真の関数
f = np.poly1d([2/9, -3, 9, 0])

# ノイズを加えて10点サンプルを取得する
x_sample = np.array(range(10))
np.random.seed(1)
y_sample = f(x_sample) + np.random.randn(10)

# 3次多項式で回帰した係数を取得する
c = np.polyfit(x_sample, y_sample, 3)
print(c)
# [ 0.19640272 -2.60339584  7.33961579  1.29981883]

簡単ですね。

この戻り値ですが、高次の項の係数から順番に格納されており、前回の記事で紹介した多項式オブジェクトを作成するnumpy.poly1dの引数としてそのまま渡すことができます。
とても便利です。

これを使って、回帰した多項式も含めてグラフにプロットしてみます。
機械学習のテキストによくある、次数が低くて学習できてないパターンと、高くて過学習してるパターンを出してみました。


import matplotlib.pyplot as plt

# プロット用のxメモリ
x = np.linspace(-0, 9, 101)
y = f(x)
fig = plt.figure(figsize=(10, 10), facecolor="w")

for i, d in enumerate([0, 1, 3, 9], 1):
    # d次関数で回帰した係数
    c = np.polyfit(x_sample, y_sample, d)
    # d次関数オブジェクトに変換
    g = np.poly1d(c)
    ax = fig.add_subplot(2, 2, i)
    ax.plot(x, y, label="真の関数")
    ax.plot(x, g(x), label=f"d={d}")
    ax.scatter(x_sample, y_sample, label="標本")
    ax.legend()

plt.show()

出力がこちら。

どこかでみたことあるような図がバッチリ出ました。
真の関数の次数である3が一番もっともらしくフィットしていますね。