Jupyter Notebookでインタラクティブに関数を実行する

Juputer Notebookで関数を実行するとき、結果を見ながら何度も引数を変更して再実行したくなることはないでしょうか。
毎回コード中の引数を書き換えて実行しても良いのですが、それをGUIで実行できると便利です。

その便利な機能が、ウィジェットとして用意されているのでそれを紹介します。
使うのは ipywidgets です。
ドキュメントはこちら: Jupyter Widgets
実は細かい設定でGUIをいろいろ作れるのですが、今回はとりあえず最低限度の機能でGUIで操作しながらグラフを描写する関数を実行します。

実行するサンプル関数としてリサージュ曲線を描く関数を用意しました。


import numpy as np
import matplotlib.pyplot as plt


def lissajous_curve(a, b, delta, color="b", title="リサージュ曲線"):

    t = np.linspace(0, 2, 601) * np.pi
    x = np.cos(a * t)
    y = np.sin(b * t + delta)

    fig = plt.figure(figsize=(8, 8))
    ax = fig.add_subplot(111, aspect="equal", title=title)
    ax.plot(x, y, color=color, )
    plt.show()

一応説明しておくと、これは変数$t$を媒介変数とし、
$x=A\cos(at), y=B\sin(bt+\delta)$で表される点をプロットした曲線です。
($A,B$はただ図形の縮尺が変わるだけなので今回のコードは$1$で固定しました。)

さて、通常は$a, b, \delta$や色とタイトルに値を指定して実行することで目当てのグラフを描写しますが、
これらの引数をGUIで指定できる様にします。

方法は簡単で、ipywidgets.interact に先ほどの関数と、指定できる引数の範囲を渡すだけです。
引数の範囲は次の様に指定できます。
– (開始, 終了, ステップ) のタプル(ステップを省略したら1)
– 指定できる値のリスト
– テキスト
– 表示用文字列: 渡す値の辞書


from ipywidgets import interact


interact(
    lissajous_curve,
    a=(1, 10, 1),
    b=(1, 10, 1),
    delta=(0, 2*np.pi, 0.01),
    title="リサージュ曲線",
    # ただの配列で指定することも可能。
    # color=["b", "k", "r", "g", "y", "m", "c"]
    # 表示用文字列: 渡す値の辞書も使える
    color={
        "Blue": "b",
        "Green": "g",
        "Red": "r",
        "Black": "k",
    }
)

実行したのがこちらです。
Blogなので画像キャプチャになってしまっていますが、実際はスライドバーやドロップダウンで操作し、グラフを書き直すことができます。

scikit-learnの学習済み決定木モデルから学習結果を抽出する

scikit-learnで学習した決定木の学習結果を確認するにはライブラリを使うのが便利ですが、
自分でも直接取得してみたかったので方法を調べてみました。

参考:
dtreevizで決定木の可視化
graphvizで決定木を可視化

とりあえず、 iris を学習しておきます。dtreevizの記事とパラメーターを揃えたので、
この後の結果はそちらと見比べていただくとわかりやすいです。
ただし、最初の分岐が2パターンあって乱数でどちらになるか決まるので、運が悪いと結果が変わります。


from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

iris = load_iris()
clf = DecisionTreeClassifier(min_samples_split=5)
clf.fit(
    iris.data,
    iris.target
)

"""
DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='gini',
                       max_depth=None, max_features=None, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=5,
                       min_weight_fraction_leaf=0.0, presort='deprecated',
                       random_state=None, splitter='best')
"""

ロジスティック回帰などであれば、係数が coef_に入っているだけなので簡単なのですが、
決定木の場合読み解くのに少し手間がかかります。

その辺りのことは、ドキュメントにも
Understanding the decision tree structureとしてまとめてあるのでこちらも参照しながら読み解いてみました。

必要な情報は clf.tree_の属性としてまとまっているので順番に取り出してみます。


# ノードの数
n_nodes = clf.tree_.node_count
print(n_nodes)
# 13

# 各ノードに振り分けられた学習データの数。
node_values = clf.tree_.value

# 各ノードの左の子ノード。 葉の場合は -1
children_left = clf.tree_.children_left
print(children_left)
# [ 1 -1  3  4  5 -1 -1  8 -1 -1 11 -1 -1]

# 各ノードの右の子ノード。 葉の場合は -1
children_right = clf.tree_.children_right
print(children_right)
# [ 2 -1 10  7  6 -1 -1  9 -1 -1 12 -1 -1]

# 分割に使う特徴量。 葉の場合は-2
feature = clf.tree_.feature
print(feature)
# [ 3 -2  3  2  3 -2 -2  3 -2 -2  2 -2 -2]

# 分割に使う閾値。 葉の場合は-2
threshold = clf.tree_.threshold
print(threshold)
"""
[ 0.80000001 -2.          1.75        4.95000005  1.65000004 -2.
 -2.          1.55000001 -2.         -2.          4.85000014 -2.
 -2.        ]
"""

要するに、各ノードが配列の要素に対応しており、
それぞれ配列に、左の子ノード、右の子ノード、分割に使う特徴量、分割に使う閾値が順番に入っています。

これらの情報を日本語に変化して表示すると次の様になるでしょうか。


for i in range(n_nodes):
    print("\nノード番号:", i)
    if children_left[i] == -1:
        print("    このノードは葉です。")
        print("        予測結果: ")
        for v, t in zip(node_values[i][0], iris.target_names):
            print("            "+t+": ", round(v/sum(node_values[i][0]), 3))
    else:
        print(
            "    "+iris.feature_names[feature[i]],
            "が",
            round(threshold[i], 3),
            "未満の場合、ノード:",
            children_left[i],
            "に進み、それ以外の場合は、",
            children_right[i],
            "に進む。"
        )

出力結果のテキストはこちらです。


ノード番号: 0
    petal width (cm) が 0.8 未満の場合、ノード: 1 に進み、それ以外の場合は、 2 に進む。

ノード番号: 1
    このノードは葉です。
        予測結果: 
            setosa:  1.0
            versicolor:  0.0
            virginica:  0.0

ノード番号: 2
    petal width (cm) が 1.75 未満の場合、ノード: 3 に進み、それ以外の場合は、 10 に進む。

ノード番号: 3
    petal length (cm) が 4.95 未満の場合、ノード: 4 に進み、それ以外の場合は、 7 に進む。

ノード番号: 4
    petal width (cm) が 1.65 未満の場合、ノード: 5 に進み、それ以外の場合は、 6 に進む。

ノード番号: 5
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  1.0
            virginica:  0.0

ノード番号: 6
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  0.0
            virginica:  1.0

ノード番号: 7
    petal width (cm) が 1.55 未満の場合、ノード: 8 に進み、それ以外の場合は、 9 に進む。

ノード番号: 8
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  0.0
            virginica:  1.0

ノード番号: 9
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  0.667
            virginica:  0.333

ノード番号: 10
    petal length (cm) が 4.85 未満の場合、ノード: 11 に進み、それ以外の場合は、 12 に進む。

ノード番号: 11
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  0.333
            virginica:  0.667

ノード番号: 12
    このノードは葉です。
        予測結果: 
            setosa:  0.0
            versicolor:  0.0
            virginica:  1.0

先日可視化した結果とバッチリ対応していますね。

NumPyのブロードキャストで変換できる型

NumPyを普段使いしてると便利な機能に、ブロードキャストがあります。
これは配列のサイズが違うもの通しを演算するときに、いい感じに小さい方を拡張して演算してくれるものです。

例えば、配列とスカラーの和や、行列とベクトルの和を次の様に計算してくれます。


a = np.array(range(4))
print(a)
# [0 1 2 3]

# 7 を [7, 7, 7, 7] として扱って足してくれる
b = a + 7
print(b)
# [ 7  8  9 10]

c = np.array(range(6)).reshape(2, 3)
print(c)
"""
[[0 1 2]
 [3 4 5]]
"""

d = np.array([5, 5, 5])

# [5, 5, 5] を [[5, 5, 5], [5, 5, 5]] として扱って足してくれる
e = c+d
print(e)
"""
[[ 5  6  7]
 [ 8  9 10]]
 """

本当にいい感じにやってくれるのであまり意識せずに使っていましたが、仕様を正確に把握しておきたかったので改めてドキュメントを読みました。
と言うのも、いつでも動くわけではありませんし、正方行列とベクトルの和の様にどちらの軸にブロードキャストされるか迷うことなどあるからです。


# 動かない例
a = np.array(range(6)).reshape(3, 2)
b = np.array(range(3))
a + b
# ValueError: operands could not be broadcast together with shapes (3,2) (3,)

# 行か列か引き伸ばされる方向がわかりにくい例
c = np.zeros(shape=(3, 3))
d = np.array(range(3))
print(c+d)
"""
[[0. 1. 2.]
 [0. 1. 2.]
 [0. 1. 2.]]
"""

ドキュメントはこちらです。
参考: Broadcasting

長いのですが、基本的なルールは General Broadcasting Rules に書かれてる次の法則だけです。
配列の次元数は後ろから順番に前に向かって比較されます。(長さが違う場合は、短い方に1が追加されて揃えられます。)
そして、それらの値が等しいか、もしくは一方が1であればブロードキャストされます。

ブロードキャストされるときは、値が1だった(もしくは無かった)次元の向きにデータがコピーされて拡張されます。

先ほどのエラーが起きた例で言えば、 (3, 2)次元と (3) 次元の 2と3が比較されてこれが等しくないからエラーになったわけですね。
その次の例についてはまず、
[0, 1, 2] (shapeは(3,))に、次元が一個追加されて、
[[0, 1, 2]] (shapeは(1, 3)) に変換され、それが各行にコピーされたので上の例の様な結果になっています。

先述のシンプルなルールさえ満たしていれば、次の例の様な少々無茶でイメージしにくい配列同士でもブロードキャストされます。


a = np.array(range(21)).reshape(1,1,3,1,7)
b = np.array(range(10)).reshape(2,1,5,1)

print(a.shape)
# (1, 1, 3, 1, 7)
print(b.shape)
# (2, 1, 5, 1)

c = a+b
print(c.shape)
# (1, 2, 3, 5, 7)

(1, 1, 3, 1, 7) と (2, 1, 5, 1) では長さが違うので、後者の方の先頭に1が挿入され
以下の二つになるわけですね。
(1, 1, 3, 1, 7)
(1, 2, 1, 5, 1)
これを順番に見ていくと、ブロードキャストのルールをみたいしているので足りない向きについてはデータがコピーされ、
和がとれているわけです。

外部リンクへのクリックをイベントとして計測する

このブログの改善のために、外部サイトへのリンクが(特に各種技術記事から公式のドキュメントへ)どれだけクリックされているのか計測しようと思います。
それが、GoogleタグマネージャーとGoogleアナリティクスの組みあわせでできるので設定していきます。

順番に設定していきます。

1. タグマネージャーのコンテナとワークスペースを選択。
2. 新しいタグを追加を選択し、タグに名前をつける。
3. タグの設定をクリック。
4. タグタイプは、 「Googleアナリティクス:ユニバーサル アナリティクス」を選択。
5. トラッキングタイプに「イベント」を選択し、以下の内容を入力。
カテゴリ: External link
アクション: {{Click URL}}
ラベル: {{Page Path}}
値: 1
非イタンラクション ヒット: 真
6. トリガーの追加を押し、トリガーの選択画面で右上の「+」を押す。
7. トリガーの設定で、クリックの下のリンクのみを選択。
8. 以下の内容設定
タグの配信を待つと妥当性をチェックは一旦無効のまま
一部のリンククリックを選択し、以下の二つを設定する
Click URL正規表現に一致 http.*
Click URL 含まない analytics-note.xyz
9. トリガーに名前をつける
10. 保存し、プレビューでテストして公開

しばらく経過を見てまた調整するかもしれませんが、一旦はこれで計測できる様になっているはずです。

Google タグマネージャー導入

タイトルの通り、このBlogでもGoogleタグマネージャーを使い始めました。
目的は主にGoogleアナリティクスでのイベント計測です。
多くの記事で参照している各種のドキュメントへのリンクや、画像へのアクセス状況を分析しより使いやすいサイトにしたいと思っています。

これまでは、GAは「Googleアナリティクス導入」の記事で書いた通り、
SEOのプラグインで設定していましたが、これがタグマネージャーによる管理に移行します。

以下、導入手順のメモです。

1. タグマネージャのサイトにアクセスし、Googleアカウントでログイン。
2. 「アカウントを作成するにはここをクリックしてください」 をクリック。
3. アカウント名(適当)、 国(日本)、コンテナ名(URLでも良いのですがサイト名にしました。)、ターゲットプラットフォーム(ウェブ)を入力。
4. 作成ボタンクリック。
5. 利用規約を読んで「はい」をクリック。
6. サイト内に貼り付ける様に指示されるコードを保存して「OK」で閉じる。 (GTM-******* と言うコードを後で使う。)

コンテナIDはタグマネージャーのホーム画面でいつでも確認できます。

次にこれをWordPressに設定します。
1. プラグインで Google Tag Manager を検索。
2. 今回はこちらをインストール。 Google Tag Manager
3. プラグインを有効化。
4. 「設定」「一般」の一番下に Google Tag Manager ID の設定ができているので、ここにコンテナIDを入力して保存。
(プラグインの設定ではないので注意。)

これでタグマネージャー自体の導入完了です。

次に、肝心のGoogleアナリティクスをタグマネージャーで設定します。

1. タグマネージャーの「ワークスペース」ページの「サマリー」を開く。
2. 「新しいタグ」 をクリック。
3. 「タグの設定」をクリックして「Google アナリティクス: ユニバーサル アナリティクス」 を選択する。
4. 「トラッキングタイプ」に「ページビュー」を選択。
5. Googleアナリティクス設定で、トラッキングコードを入力。 (UA-*********-* 形式の値)
6. 変数名を入れて保存。(とりあえず、「分析ノートUA」にしました。)
7. トリガーの設定。 「All Pages ページビュー」を選択。
8. タグに名前(分析ノートページビュー)を設定して保存。

この段階ではまだ公開されていないので、テストしてから公開します。
1. ワークスペースに戻ってプレピューをクリック。
2. そのブラウザで、このブログにアクセスすると画面下に発火したタグが確認できる。
(分析ノートページビューがFiredになっている)
3. プレビューモードを終了
4. 「公開」ボタンをクリック。
5. バージョン名と説明を求められるので入力し、もう一度「公開」をクリック。

最後に、今回に限り重要なのが、すでに直接設定してたGoogleアナリティクスの設定を消すこと。
SEOプラグインに設定してたUAの値を消しました。これをしないとpvが二重カウントになります。

dtreevizで特徴量とラベルの関係を可視化

※この記事では dtreevizの version 0.8.2 を使っています。

前回の記事では、dtreeviz を使って学習済みの決定木を可視化しました。
dtreevizではこの他にも、1個か2個の特徴量とラベルの関係を可視化できます。

それが、 ctreeviz_univar と、ctreeviz_bivar です。
扱える特徴量がuniの方が1個、biの方が2個です。

データは必要なので、irisを読み込んでおきます。今回は木は不要です。
(その代わり、max_depsかmin_samples_leafのどちらかの設定が必須です。)


from sklearn.datasets import load_iris
iris = load_iris()

まず1個のほうをやってみます。
特徴量4個しかないので全部出します。


import matplotlib.pyplot as plt
from dtreeviz.trees import ctreeviz_univar

figure = plt.figure(figsize=(13, 7), facecolor="w")
for i in range(4):
    ax = figure.add_subplot(2, 2, i+1)
    ctreeviz_univar(
        ax,
        iris.data[:, i],
        iris.target,
        max_depth=2,
        feature_name=iris.feature_names[i],
        class_names=iris.target_names.tolist(),
        target_name='types'
    )

plt.tight_layout()
plt.show()

どの特徴量が有効なのか、自分的にはこれまでで一番わかりやすいと感じました。

次は2個の方です。特徴量2種類とラベルを渡すと、それらの関係を可視化してくれます。
2個ずつ選んで2つのグラフで可視化してみました。
引数、ですがfeature_name が feature_names になっており、渡す値も文字列が配列になっているので注意が必要です。


from dtreeviz.trees import ctreeviz_bivar

figure = plt.figure(figsize=(5, 12), facecolor="w")
ax = figure.add_subplot(2, 1, 1)
ctreeviz_bivar(
    ax,
    iris.data[:, :2],
    iris.target,
    max_depth=2,
    feature_names=iris.feature_names[:2],
    class_names=iris.target_names.tolist(),
    target_name='types'
)

ax = figure.add_subplot(2, 1, 2)
ctreeviz_bivar(
    ax,
    iris.data[:, 2:],
    iris.target,
    max_depth=2,
    feature_names=iris.feature_names[2:],
    class_names=iris.target_names.tolist(),
    target_name='types'
)

plt.show()

出力がこちら。

これもわかりやすいですね。

dtreevizで決定木の可視化

早速、前回の記事でインストールした dtreeviz を使ってみます。

※この記事では dtreevizの version 0.8.2 を使っています。
1.0.0 では一部引数の名前などが違う様です。(X_train が x_dataになるなど。)

とりあえず、データと可視化する木がないと話にならないので、いつものirisで作っておきます。


from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

iris = load_iris()
clf = DecisionTreeClassifier(min_samples_split=5)
clf.fit(
    iris.data,
    iris.target
)

さて、これで学習したモデル(コード中のclf)を可視化します。
リポジトリのコードを見ながらやってみます。

まず、一番シンプルな可視化は、 dtreeviz.trees.dtreevizにモデルと必要なデータを全部渡すものの様です。
(省略不可能な引数だけ設定して実行しましたが、結構多いですね。)


from dtreeviz.trees import dtreeviz

tree_viz = dtreeviz(
    tree_model=clf,
    X_train=iris.data,
    y_train=iris.target,
    feature_names=iris.feature_names,
    target_name="types",
    class_names=iris. target_names.tolist(),
)
tree_viz

出力がこちら。

graphvizで決定木を可視化 でやったのと比べて、とてもスタイリッシュで解釈しやすいですね。

orientation(デフォルトは’TD’)に’LR’を指定すると、向きを縦から横に変更できます。


tree_viz = dtreeviz(
    tree_model=clf,
    X_train=iris.data,
    y_train=iris.target,
    feature_names=iris.feature_names,
    target_name="types",
    class_names=iris. target_names.tolist(),
    orientation='LR',
)
tree_viz

出力がこちら。

木のサイズによってはこれも選択肢に入りそうですね。

決定木の可視化ライブラリ dtreeviz を conda でインストールする

本記事の免責事項:
dtreevizの公式ではpipでのインストールが推奨されているようです。
手順を見ると、condaでgraphviz が入っている場合はそのアンインストールまで明記されています。
そのため、本記事を真似される場合は自己責任でお願いします。
Python環境の破損やその他の動作不良の責任は負いません。
自分自身、将来的にそれらの事象が発生したらpipで入れ直す可能性もあります。

また、この記事でインストールしたのは、version 0.8.2 です。
最新のバージョンでは挙動が異なる可能性があります。

免責事項終わり。

さて、決定木をとても綺麗に可視化してくれるという dtreeviz というライブラリがあるのを聞いて以来、試したいと思っていましたが、
conda(と、conda-forge)のリポジトリには見つからないので後回しにしていたのをやってみることにしました。

個人のMacでは環境構築をcondaに統一しているので、pipはあまり使いたくありません。
しかし免責事項の通り、ドキュメントではpipが推奨されています。

自分の端末ならよかろうということで(職場の端末で試す前の毒見も兼ねて)condaで入れることにしました。
使うのは conda skeleton です。
このブログのこちらの記事が参考になります。
PyPIのパッケージをcondaでインストールする方法

dot や python-graphviz など、必要ライブラリがすでに入っているのもあり、非常にスムーズにインストールできました。


# skeleton で dtreeviz インストール
$ conda skeleton pypi dtreeviz
$ conda build dtreeviz
$ conda install --use-local dtreeviz

# インストール結果確認
$ conda list dtreeviz
# packages in environment at {HOMEPATH}/.pyenv/versions/anaconda3-2019.10:
#
# Name                    Version                   Build  Channel
dtreeviz                  0.8.2            py37h39e3cac_0    local

# 一次ファイル削除
$ conda build purge

Pythonで
from dtreeviz.trees import dtreeviz
をやってみると無事にインポートできたので、導入は成功した様です。
これからの記事で使い方とか書いていきたいと思います。

WordPressのテーマに子テーマを設定する

ほとんど初期設定で運営しているこのブログのデザインなのですが、将来的にはより読みやすい形へのカスタマイズもしたいと思っています。
その場合、子テーマというのを使ってそれをカスタマイズするのがお作法らしいです。

メインのテーマが、WordPressが用意してくれている、 Twenty Seventeenなのですが、これが時々バージョンアップがあり、
子テーマを作っておかないと、その度に自分のカスタマイズ分がリセットされるからだそうです。
(このテーマ特有の事象ではなく、自作ではないテーマを使う人全体に言えることです。)

ということで、子テーマの設定をしました。
なお、このブログのサーバーには、 Amazon Lightsail を利用しています。

子テーマの作成にあたって参照したドキュメントはこちらです。
子テーマ – WordPress Codex 日本語版

管理画面からGUIで作れると思っていたのですが、サーバーに入って作業が必要みたいですね。
まず、ディレクトリ作成からです。 wp-content/themes ディレクトリ下に、親テーマと並列で子テーマのディレクトリを作ります。
(親テーマの配下ではないので注意が必要です。) Lightsail の場合、 wp-content/themes は次の場所にあります。
このディレクトリを探すのにも少してこずりました。

~/apps/wordpress/htdocs/wp-content/themes/
直下に親テーマになる twentyseventeen のフォルダもあります。

ここに、ディレクトリを掘って、style.css, functions.php の2ファイルを作ります。


$ mkdir twentyseventeen-child
$ cd  twentyseventeen-child
$ touch style.css
$ touch functions.php

そして、作った2ファイルに内容を書きます。
まず、style.css の方は、スタイルシートヘッダで始める必要があるそうです。(今回は子テーマ作るだけなので、スタイルシートヘッダだけです。)
具体的にどう書くかは、ドキュメントの記載例に加えて、親テーマのstyle.cssも見ながら次の様にしました。
Template行は、親テーマのディレクトリ名を指します。この例では親テーマが Twenty Fifteen テーマですので、Template は twentyfifteen です。別のテーマが親テーマの場合、該当のディレクトリ名を指定してください。
とある通り、 Template行 が重要です。僕は最初、何も考えずに親テーマのヘッダーをコピーしただけで済ませようとして、Template行がなくてハマりました。


/*
Theme Name: Twenty Seventeen Child
Author: Yutaro
Author URI: https://analytics-note.xyz/
Template: twentyseventeen
Version: 2.3
Requires at least: 4.7
Requires PHP: 5.2.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: twenty-seventeen-child
*/

次は、 functions.php です。 これはドキュメントに指示された通り次の様に書きます。
<?php の部分重要です。PHP の開始タグではじめろとドキュメントの説明にはありますが、コードの例には入っていません。


<?php
add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );
function theme_enqueue_styles() {
    wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );

}

ここまで済ませばあとは管理画面から作業できます。
左ペインの外観を選ぶと、空っぽのテーマができているのでこれを有効化するだけです。
いくつかの設定がリセットされてしまうので、それは再設定が必要になります。
(このブログでいえば無効にしていたヘッダー画像が復活するなど。)

pandasのDataFrameのappendは遅い

この記事のタイトルは悩んだのですが、一番伝えたかった内容がそれなので、上記の様になりました。
他のタイトル候補は次の様になります。

– 配列を要素に持つDataFrameの縦横変換
– 高速にDataFrameを作成する方法

要するに、1件ごとに発生するデータを毎回appendしてデータフレームを構成する処理は、
コーデイングの観点ではわかりやすいですが、件数によっては非常に時間がかかります。

僕の場合ですが、DataFrameの要素に配列(もしくはカンマ区切りなどの文字列)が入っていた時に、
それを要素ごとに縦横変換する時によく遭遇します。

例えば以下の感じのDataFrameを


       key                                         value
0    key_0          [value_0, value_1, value_2, value_3]
1    key_1                            [value_4, value_5]
2    key_2                   [value_6, value_7, value_8]
3    key_3       [value_9, value_10, value_11, value_12]
4    key_4                          [value_13, value_14]
# (以下略)

次の様なDataFrameに変換したい場合です。


     key    value
0  key_0  value_0
1  key_0  value_1
2  key_0  value_2
3  key_0  value_3
4  key_1  value_4
5  key_1  value_5
6  key_2  value_6
7  key_2  value_7
8  key_2  value_8
# (以下略)

データ量が少ない場合は、1件ずつ appendしても問題ありません。
次の様に、iterrows で1行1行取り出して、出力先にpivot_dfに追加してくと、素直で理解しやすい処理になります。


pivot_df = pd.DataFrame(columns=["key", "value"])

for _, row in df.iterrows():
    value_list = row["value"]
    for v in value_list:
        pivot_df = pivot_df.append(
            pd.DataFrame(
                {
                    "key": [row.key],
                    "value": [v],
                }
            )
        )

ただし、この処理は対象のデータ量が2倍になれば2倍以上の時間がかかる様になります。
そこで、大量データ(自分の場合は数十万件以上くらい。)を処理する時は別の書き方を使っていました。

つい最近まで、自分の環境が悪いのか、謎のバグが潜んでいるのだと思っていたのですが、
ドキュメントにも行の追加を繰り返すと計算負荷が高くなるからリストに一回入れろと書いてあるのを見つけました。
どうやら仕様だった様です。

引用

Iteratively appending rows to a DataFrame can be more computationally intensive than a single concatenate. A better solution is to append those rows to a list and then concatenate the list with the original DataFrame all at once.

その様なわけで、DataFrame中の配列の縦横変換は自分は以下の様に、一度配列を作って配列に追加を続け、
出来上がった配列をDataFrameに変換して完成としています。


keys = []
values = []

for _, row in df.iterrows():
    keys += [row.key] * len(row.value)
    values += row.value

pivot_df = pd.DataFrame(
    {
        "key": keys,
        "value": values,
    }
)