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が一番もっともらしくフィットしていますね。

NumPyの1変数多項式クラス

Pythonで多項式を扱う時はSympyを使うものだと思い込んでいたのですが、
NumPyにも多項式クラスが用意されいるのを見つけ、しかもかなり使い勝手が良かったので紹介します。

ドキュメント: numpy.poly1d

これを使えば簡単に多項式を生成し、多項式間の演算を行ったり値を代入したりすることができます。
まず、多項式の生成は次の2種類のやりかでできます。

– 高次の項から順番に係数を指定する。
– その多項式の根を指定する。(この場合、再高次の項の係数は1)。

$2x^3-4x^2-22x+24$ と $(z-1)(z+2)=z^2+z-2$ をそれぞれの方法で定義したのが次のコードです。


import numpy as np

# 係数を指定する場合は、高次の項から順番に指定する
p1 = np.poly1d([2, -4, -22, 24])
print(p1)
"""
   3     2
2 x - 4 x - 22 x + 24
"""

# r=True を指定することで、根を指定して生成できる
# variable で変数の文字も指定できる(デフォルトはx)
p2 = np.poly1d([-2, 1], r=True, variable="z")
print(p2)
"""
   2
1 z + 1 z - 2
"""

係数のリストはc、次数はo、根の一覧はr、という属性でそれぞれアクセスすることができます。
係数と根はその多項式オブジェクトを作る時に指定した方法に関係なく取得できて便利です。
方程式を解くのもこれでできますね。


# 係数のリスト
print(p1.c)
# [  2  -4 -22  24]

# 次数
print(p1.o)
# 3

# 根
print(p1.r)
# [-3.  4.  1.]

また、指定した次数の係数は辞書と同じように[次数]でアクセスできます。


# x^2 の係数
print(p1[2])
# -4

値の代入は通常の関数と同じように、 多項式オブジェクト(代入したい値) で計算できます。


# p1 に 2 を代入
print(p1(2))
# -20

このほか、 通常の +, -, * の 演算子で演算もできます。
/ は割り算ですが、商と余りを返してくれます。とても便利ですね。


p = np.poly1d([2, 7, 1, 4, 3])
q = np.poly1d([1, 3, 5])

print(p+q)
"""
   4     3     2
2 x + 7 x + 2 x + 7 x + 8
"""

print(p-q)
"""
   4     3     2
2 x + 7 x + 2 x + 7 x + 8
"""

print(p*q)
"""
   6      5      4      3      2
2 x + 13 x + 32 x + 42 x + 20 x + 29 x + 15
"""

print(p/q)
# (poly1d([  2.,   1., -12.]), poly1d([35., 63.]))

このほかさらに、polyderとpolyintで微分と不定積分も用意されています。


# 微分
print(np.polyder(p))
"""
   3      2
8 x + 21 x + 2 x + 4
"""

# 不定積分
print(np.polyint(p))
"""
     5        4          3     2
0.4 x + 1.75 x + 0.3333 x + 2 x + 3 x
"""

例は省略しますが、2個目の引数に自然数を渡せばn回微分や、n回積分もやってくれます。
積分の方は、3つ目の引数に積分定数を渡すこともできます。

数値をゼロ埋めして桁数を揃える

桁数の揃ったIDを振るときや、表の見栄えを整える時など、桁数が少ない数字の左側に0をくっつけて表示することがあります。
そうそう頻繁にあることではないので、これまでそういう操作が必要な時は、
単純に0をたくさんくっつけて規定文字数を右側から取り出す関数を作って対応していました。

例えば次のようなメソッドを定義していました。
例として 123 を 0埋めして6桁にしています。


def zero_padding(n, length):
    m = "0" * length + str(n)
    return m[-length:]


print(zero_padding(123, 6))
# 000123

普段の用途だとこれでもあまり困らないのですが、実はPythonにはゼロ埋め専用の関数が用意されていたのに気づいたのでそちらを紹介します。
文字列オブジェクトに定義されている、zfillメソッドがそれです。

ドキュメント: str.zfill(width)
str型が持ってるメソッドなので、数値型のデータに適用するにはstrに変換してから呼び出す必要があります。

このメソッドは上のコードで僕が定義した単純な関数に比べて次の2点で優れています。
– 数字がwidth桁未満の時は、元の数字をそのまま返す。(上の方の桁をロストしない)
– 文字列の先頭が+か-の符号の場合、符号を先頭としてその後ろを0埋めする。
対応する符号は+と-だけで、±はダメみたいでした。

挙動を確認するため、いくつかのパターンでやってみます。


# 123をゼロ埋めして7桁にする
print("123".zfill(7))
# 0000123

# 幅が元の数値の桁数未満なら元の数値をそのまま返す。
print("12345".zfill(3))
# 12345

# 先頭が符号の場合は符号と数字の間をゼロ埋めする。符号も結果の文字数にカウントされる。
print("-5678".zfill(8))
# -0005678

# 小数点も文字数に数えられる。
print("+12.34".zfill(6))
# +12.34

# 数字以外の文字列に対しても使える。
print("abc".zfill(5))
# 00abc

小数や負の数をゼロパディングする機会にはまだ遭遇したことないのですが、覚えておいて損はないと思います。

Matplotlibでヒストグラムを描く時に各binのレンジを明示的に指定する方法

そもそも、Tableauなどを使えばこんな手間もないのですが、Python(Matplotlib)でヒストグラムを描く時に、各ビンの区間を指定したいことがよくあります。
0かはじめて0.5区切りにしたいとか20区切りにしたいとかです。
Matplotlibのhistメソッドでは、bins引数で、binの引数を指定でき、range引数でヒストグラムに描写する幅を指定できるので、
僕はこれまではこの二つを組み合わせて使うことで、想定通りのヒストグラムを描いていました。

試しに、 0 〜 300のデータを 20区切りで、15本のbinでヒストグラムに表示するコードがこれです。
hist メソッドの戻り値で binの区切り位置が取れるので、そちらを確認し、出力の図は省略します。
参考: matplotlibのhist()の戻り値


import matplotlib.pyplot as plt
from scipy.stats import beta

# データ生成
beta_frozen = beta(a=1, b=1, scale=300)
data = beta_frozen.rvs(100)

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ns, bins, _ = ax.hist(data, bins=15, range=(0, 300))
print(bins)
# [  0.  20.  40.  60.  80. 100. 120. 140. 160. 180. 200. 220. 240. 260. 280. 300.]

これ、もしreangeを指定せずに、bins=15だけ指定しているととても中途半端なところで区切られます。


fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ns, bins, _ = ax.hist(data, bins=15)
print(bins)
"""
[  0.59033775  20.53472376  40.47910977  60.42349579  80.3678818
 100.31226782 120.25665383 140.20103985 160.14542586 180.08981188
 200.03419789 219.9785839  239.92296992 259.86735593 279.81174195
 299.75612796]
"""

さて、上記のようなblog記事ように自分で生成したデータなど取りうるレンジがわかりきってるものであれば、上記のやり方でも問題なのですが、実データでは少しだけ面倒です。
レンジとデータ量を確認して、何本くらいのbinを指定すれば切りの良い区切りで可視化でできるか考えないといけません。

しかしドキュメントあたらめて読んでみると、bins引数で、ビンの本数ではなく、区間を配列で指定できることがわかりました。
参考: matplotlib.pyplot.hist

bins : int or sequence or str, optional

If an integer is given, bins + 1 bin edges are calculated and returned, consistent with numpy.histogram.
If bins is a sequence, gives bin edges, including left edge of first bin and right edge of last bin. In this case, bins is returned unmodified.

これを使うと、例えば20区切りで可視化したい、と言った時は次のような書き方ができます。


# 300を含めるため、2個目の引数は301にしました。
bins = range(0, 301, 20)
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ns, bins, _ = ax.hist(data, bins=bins)
print(bins)
# [  0  20  40  60  80 100 120 140 160 180 200 220 240 260 280 300]

楽ですね。
注意点として、 bisで渡した配列の区間の外側のデータは可視化されないということがあります。
bins = range(0, 300, 20) とすると、binsの配列は、
[ 0 20 40 60 80 100 120 140 160 180 200 220 240 260 280]
になるので、 280~300のデータは可視化されません。
区間の下側も同様です。
なので、binsを指定する時に、データが全部その範囲に含まれているのかは確認しておく必要があります。

ちなみに豆知識ですが、各区間は最後(最大)の区間以外は、左の値を含み右の値を含みません、
[20, 40) の区間であれば、 $20 \leq x < 40$ のデータを数えます。
ただし、最後(最大)の区間に限って、右の値も含み、最大のbinが
[280, 300] の区間であれば、 $280 \leq x \leq 300$ のデータを数えます。

使いどこがすぐには思いつかないのですが、 bins を配列で指定する場合は、等間隔以外の区切りもサポートされているということも覚えておきましょう。

pandasのDataFrameの欠損値をfillnaで埋める時の小技

DataFrameの欠損値(NoneやNaN)を定数で埋める時、fillnaというメソッドをよく使っていましたが、
定数で埋める以外にもいろいろ指定ができることがわかったので紹介します。

ドキュメントはこちらです。
pandas.DataFrame.fillna — pandas 1.1.2 documentation

まず、サンプルとして欠損値を含むDataFrameを作っておきます。


import pandas as pd
import numpy as np
df = pd.DataFrame(
    [[np.nan, 2, np.nan, 0],
     [3, 4, np.nan, 1],
     [np.nan, np.nan, np.nan, 5],
     [np.nan, 3, np.nan, 4]],
    columns=list('ABCD')
)
print(df)
"""
     A    B   C  D
0  NaN  2.0 NaN  0
1  3.0  4.0 NaN  1
2  NaN  NaN NaN  5
3  NaN  3.0 NaN  4
"""

fillnaの一番基本的な使い方は定数を指定するものです。
例えば、0を渡せばNaNを0に置き換えたDataFrameを返します。(inplace=True も指定すれば、データフレームそのものを書き換えます)


print(df.fillna(0))
"""
     A    B    C  D
0  0.0  2.0  0.0  0
1  3.0  4.0  0.0  1
2  0.0  0.0  0.0  5
3  0.0  3.0  0.0  4
"""

いつもはこうやって、数値なら0、文字列なら空文字列(“”)で埋めるだけの使い方をしていました。

しかし、ドキュメントを読むと、定数で埋める以外にもいろいろできます。

まず、定数ではなく、 列名: 値 の辞書を渡すことで、列ごとに別々の値で埋めることができます。


fill_values = {'A': 0, 'B': 1, 'C': 2, 'D': 3}
print(df.fillna(value=fill_values))
"""
     A    B    C  D
0  0.0  2.0  2.0  0
1  3.0  4.0  2.0  1
2  0.0  1.0  2.0  5
3  0.0  3.0  2.0  4
"""

そしてこれは、辞書の代わりに、indexに列名、valueに値を持つSeriesを渡しても同じ挙動になります。


fill_values_sr = pd.Series(fill_values)
print(fill_values_sr)
"""
A    0
B    1
C    2
D    3
dtype: int64
"""

print(df.fillna(value=fill_values_sr))
"""
     A    B    C  D
0  0.0  2.0  2.0  0
1  3.0  4.0  2.0  1
2  0.0  1.0  2.0  5
3  0.0  3.0  2.0  4
"""

これを応用すると、各列をその列の平均値や最大値、最小値で埋めることも簡単にできます。
平均値でやってみたコードが次です。


print(df.fillna(value=df.mean()))
"""
     A    B   C  D
0  3.0  2.0 NaN  0
1  3.0  4.0 NaN  1
2  3.0  3.0 NaN  5
3  3.0  3.0 NaN  4
"""

さて、ここまでは列単位である一定の値で欠損値を補完する方法でしたが、時系列データなどを扱っている時は、
一定の値ではなく、直前や直後の値で埋めたいこともあると思います。

実はこの fillna はそのようなケースにも対応しており、
method という引数に “backfill”か”bfill”を指定すれば直後の値、
“pad”, “ffill”を指定すれば直前の値で埋めることができます。

bfillとffillだけですがそれぞれ試してみたのが次のコードです。


print(df.fillna(method="bfill"))
"""
     A    B   C  D
0  3.0  2.0 NaN  0
1  3.0  4.0 NaN  1
2  NaN  3.0 NaN  5
3  NaN  3.0 NaN  4
"""

print(df.fillna(method="ffill"))
"""
     A    B   C  D
0  NaN  2.0 NaN  0
1  3.0  4.0 NaN  1
2  3.0  4.0 NaN  5
3  3.0  3.0 NaN  4
"""

以上のように、fillnaを使うと定数での補完(0埋めなど)以外にもいろんなことができることがわかりました。

pandasのデータフレームのexplodeメソッドの紹介

以前の記事で、pandasのDataFrameの列中に含まれている配列を行に展開する方法について書きました。
参考: pandasのDataFrameのappendは遅い

お恥ずかしながらこの記事を書いた時は知らなかったのですが、pandasの version 0.25.0 から、専用のメソッドが準備されていて、
先述の記事のような面倒なことはしなくて良くなっています。

そのメソッドが pandas.DataFrame.explode です。

使い方は簡単で、行に展開したい、つまり配列を含んだ列の名前を渡すだけです。

実際にやってみます。


import pandas as pd

df = pd.DataFrame(
    {'A': [[1, 2, 3], 'foo', [], [3, 4]], 'B': 1, "C": [1, 2, 3, 4]})
print(df)
"""
           A  B  C
0  [1, 2, 3]  1  1
1        foo  1  2
2         []  1  3
3     [3, 4]  1  4
"""

df_explode = df.explode('A')
print(df_explode)
"""
     A  B  C
0    1  1  1
0    2  1  1
0    3  1  1
1  foo  1  2
2  NaN  1  3
3    3  1  4
3    4  1  4
"""

めちゃくちゃお手軽ですね。
前の記事で書いていたようにappendの遅さに文句を言ったり、自分で配列を作るコードを書いたりと言ったことは全くしなくて良くなりました。