はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pandas文学論なんちて

はじめに

Pandasで遊んでいて、便利だなーと思う一方で、Pandasでのある種のDSLとしての記法やライブラリをうまく使えばもっとシンプルに、かつ可読性(ここでは、ビジネスルールとデータクレンジングなどのための前処理(の前処理)をうまく分離したもの)をあげた記述ができるんじゃないだろうかともより欲が出てくることがあります。

Pythonは比較的、誰が書いても近い記述になりやすいと言えそうですが、Pandasはそこにある種の混沌を持ち込む感じがします。

ということで、私がPandasで遊ぶ時に良く出くわす、次の図の処理パターンあたりを念頭に、Pandasでの(より)シンプルで、ビジネスルールをダイレクトに表している(と私が思う)記述スタイル・イディオムを棚卸しすると共に、自分なりに、こういうスタイルが良いんではないだろうかというところを書き下してみました。

f:id:azotar:20190822065026p:plain

記法の棚卸し

ということで/といっても、凝集度など客観的な数字を用いる話ではなくて、プログラムの文字通り文字ヅラの並び方などの見栄えの話なのでPandasのシンタックスのご紹介が必要かなと思っています。

よって、このセクションでは説明と実例を兼ねてコメントモリモリでプログラムを貼り付けています。 これ自体が長く読みづらいコードになってしまっていることについてはご容赦くださいませ。

キーワード

  • ブロードキャスト
  • インデックスアラインメント
  • 列名指定の列選択とlocによる行列選択
  • Booleanインデックス
  • applyメソッド

解説兼事例のプログラム

私がPandasの特徴的&便利だと思っている記法を紹介するプログラム例です。

Pandas動作環境が整っている環境であれば、これをまるっと貼り付ければ動作します。

import pandas as pd
import numpy as np
import sys

"""
◆はじめに
Python自体は他のプログラミング言語に比べると誰が書いても同じスタイルになりやすい言語だと思います。
ただ、PandasやNumpyを用いるとそうでもなくなる気がしています。
Pandasは通常のPython以上に、自分が書いたコードでさえ、後から見ると何をやっているか分からないというのが発生しやすいと思います。
(お前のコーディングスキルのせいだ!というところは突っ込まないでください。)

実際にこのような課題を解消できるかはともかく、よくある同じ結果が得られるシンタックスを複数並べてみて、私はこのスタイルが好みかな〜という例を棚卸ししてみたいと思いこの記事を書きました。

つまるところ、Pandasでドメイン知識をできるだけうまく表すことができるコーディングスタイルとはというところについて、感じたところを書いています。

"""

"""
◆まえおき

無尽蔵にシンタックスを掘り下げてもきりがありません。また、次のURLのように有識者の方がパターンをまとめていただいていますので、Pandas全体を俯瞰するのはサイトにお譲りして、次項に述べたケースに絞って述べることにしています。

参考URL:
https://ikatakos.com/pot/programming/python/packages/pandas/update_multi_column

なお、本来は、コードがドメイン知識をうまく表現しているかという話とは別に/場合によってはそれ以前に、性能(パフォーマンス)、可読性、要件によらないレベルの最低限のカプセル化や構造化の各種作法があり、本来はこれらを差し置いた話をするべきではありません。また、ETL周辺の場合は欠損値の処理も本来は考えるべきです。

ただし、ここでは、論法・積み上げとして甘いところもあると思いますが、私が感じたものを中心に書き連ねています。
その点ご了承ください。


◆念頭においたトランスフォームのパターン

・各行の複数のカラムを元に、ビジネスルール等に従い、別の測定値を算出し、新たなカラムに設定する。
・各行の複数のカラムを元に、ビジネスルール等に従い、別の測定値A、B、Cなど複数の測定値を算出し、それぞれ別の新たなカラムに設定する。

このブログでは、Elasticsearchなど検索エンジンがらみについて自説を述べることが多いのですが、これら検索エンジンを有効活用するにあたり、データのインデクサーで頑張る系の処理を行う場合に上記のようなものが良く出てくるような気がするので、上記に注目しています。

逆に言うと、他のETLやデータ分析によってはあまりあてはまらないものもあると思います。

例えば、時系列分析などは時系列に従い縦方向のアプローチが多いかもしれません。

◆注意事項

以下では、「XXXが良いと思います」のような記述が何度か出てきますが、この類の文では、一律、「あるドメイン知識や論理・ルールをPandasでより直接表現できるコーディングスタイルとしては」という条件を省略しているとして、ご覧ください。

"""


"""
============================================================
説明用の初期化など 
------------------------------------------------------------
"""
orig_df = pd.DataFrame(
    dtype='int',
    columns=['X', 'A', 'B', 'C'],
    data=[
        [10000, 1, 11, 111],
        [20000, 2, 22, 222],
        [30000, 3, 33, 333]],
    index=['1_ONE', '2_TWO', '3_THREE'])

df = orig_df.copy()


def __ex(desc=''):
    print(desc)
    print('インプット')
    print(orig_df)
    return orig_df.copy()


def __p(df):
    print('アウトプット')
    print(df)


desc = """
===========================================================
レシピ1:  列名指定による代入など
・ブロードキャスト
・Seriesを代入
・リストを代入 ※ 要素数と行数が一致している必要がある
・assign

いずれも、PandasではないPythonなどではみられない記述& 演算結果になります。

慣れるまではわかりづらいところもありますが、これらはPandasが最低限わかっている人には、
非常に端的にドメイン知識を伝える例となります。また、他の記述の土台となりますので、
これらについては、好き嫌い以前に条件反射で何がおきているか分かるようにしておきましょう。

"""
df = __ex(desc)

df['D'] = 1000
df['E'] = df['X']
df['E2'] = range(len(df))
df = df.assign(F_by_assign=df['B'])
# 感想:assignはそんなに好きではなかったりする...

__p(df)

"""
レシピ1の出力結果

インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333
アウトプット
             X  A   B    C     D      E  E2  F_by_assign
1_ONE    10000  1  11  111  1000  10000   0           11
2_TWO    20000  2  22  222  1000  20000   1           22
3_THREE  30000  3  33  333  1000  30000   2           33

"""


desc = """
===========================================================
レシピ2: Booleanインデックスとインデックスアライメントなど
・locによる行と新列名指定の代入

locを使うと、行と列を同時に選択できます...というのが教科書的な説明になりますが、
私見では、locは行を列の特定の条件で選択して限定したものに対して、特定の列(追加の列でもOK)を
更新する用途に使う ... と捉えることにしています。

逆に言うと、前項の列名指定でカバーできる要件にloc指定は使わない方が良いと思っています。
(例.   loc[:,'A']みたいなのは、意図がない限りは用いない。)

"""
df = __ex(desc)

# 列Cが111より大きいもの....というような条件を意識した処理のイメージ

df.loc[df['C'] > 111, 'D'] = 1000
df.loc[~(df['C'] > 111), 'D'] = 0
# df.fillna({'D': 0})

df.loc[df['C'] > 111, 'E'] = df['X']

__p(df)


"""
レシピ2の出力結果

インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333

アウトプット
             X  A   B    C       D        E
1_ONE    10000  1  11  111     0.0      NaN
2_TWO    20000  2  22  222  1000.0  20000.0
3_THREE  30000  3  33  333  1000.0  30000.0


※上記の補足:

locだと、この条件に該当する行について新たな(既存の)列Dに、列Xの値を設定する(該当しないものはスキップする)という記述を
表している感が読み取りやすいと思いませんか。

"""

desc = """
===========================================================
レシピ3: Booleanインデックスとインデックスアライメントなど・続

・「 df['C'] > 111 」はBooleanインデックスと呼ばれる(らしい)、DataFrameの該当行はTrueそうでない行はFalseの
 dfの行数分のTrue、Falseの配列(PandasなのでSeries)です。
・「 df['C'] > 111 」はBooleanインデックスというSeriesのデータなので、
 変数に入れることができます。
 ・locは、Seriesを受け取れる&受け取ったBooleanインデクスで対象行を絞り込めるので次のコード例のようなことが可能です。

"""

df = __ex(desc)


# 絞り込み条件文に見えているが、実際はTrue/Falseの配列(のようなもの)
#  → 似たものとして、DataFrameのqueryメソッドがあるが、こちらは、クエリに該当するDataFrameそのものを戻す。
#
print('途中経過~~~~~')
cond = df['C'] > 111

print("df['C'] > 111 の正体 ~~")
print(type(cond))
print("df[df['C'] > 111] で得られるDataFrameとインデックスの値 ~~")
print(df[df['C'] > 111])

# 条件文を取り回しして、同じ条件を抽象化できる。
# 特に、特定の条件にあてはまる行とそうでない行の2パターンの変換という場合は、条件を定数風に取り扱いできる。
df.loc[cond, 'D'] = 1000
df.loc[~(cond), 'D'] = 0
df['E'] = df[cond]['X']

print(cond)

__p(df)


"""

レシピ3の出力結果

インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333

途中経過~~~~~
df['C'] > 111 の正体 ~~
<class 'pandas.core.series.Series'>

df[df['C'] > 111] で得られるDataFrameとインデックスの値 ~~
             X  A   B    C
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333
1_ONE      False
2_TWO       True
3_THREE     True
Name: C, dtype: bool

アウトプット
             X  A   B    C       D        E
1_ONE    10000  1  11  111     0.0      NaN
2_TWO    20000  2  22  222  1000.0  20000.0
3_THREE  30000  3  33  333  1000.0  30000.0

上記の補足

condは、条件文風の体裁だが、実は、{1_ONE: False, 2_TWO:True, 3_THREE: True}のような値。

これをlocや、DataFrameの「[]」部分に渡すと、左辺のDataFrameのインデックス値が「2_TWO」「3_THREE」のようにTrueになっているものが選択され、それに合わせた行に演算が作用していることに注目ください。

厳密な説明ではないですが、おおまかに言うと、オペランドの右辺や左辺のDataFrameについてインデックスの値に合わせた暗黙の軸の合わせがなされていることになります。(公式等の用語定義の原典まで至っていないのですが、この類のPandasの動作を「インデックスアライメント」と読んだりするようです)


"""

desc = """
===========================================================
レシピ4: TIPS

ここまで、列の情報による、行の選択の例を示してきましたが、1セットになるシンプルなスイッチ条件的なものは、おおよそそれをそのまま体現する、便利なメソッドがあるものが多いです。以下は、Booleanインデックスを戻り値として返すものです。

□Series.str.containsなど
・ df['お名前'].str.starstwith('山田')

   → お名前列に文字列の先頭に「山田」を含む行のBooleanインデックスが戻る。
   ※ df['お名前']を1要素の文字列型だと思って、Pythonの文字列処理メソッド風に 「 df['お名前'].startswith() 」と記述してもダメ。
    逆に言うと、この誤記述例で本来やりたかったことが、上記で可能。
     

□betweenなど
・between
  →booleanインデックスが戻る
・isin

また、いわゆるビン分けのためのメソッドがあります。(ただし、下記自体はBooleanインデックスではなくそれらと組み合わせて使う類のもの)

□ビンに分割
・cut
(境界値のリスト、ビンの数(等間隔の分割数)を指定)
・qcut
→各ビンの要素数が同じになるようにする

cutやqcutは、groupbyの引数として使え、
groupbyオブジェクトのtransformメソッドと組み合わせると、ここまで述べたような例を宣言的に実装できる(のでビジネスルールの見通しが良い...ような気がする)
"""
df = __ex(desc)

bet_cond = df['C'].between(111, 222)
cond_isin = df['C'].isin([111, 333])  # isinも便利
cut_cond_境界値 = pd.cut(df['C'], [0, 111, 222, 333])
cut_cond_ビン数 = pd.cut(df['C'], 3, precision=0)
qcut_cond_by2 = pd.qcut(df['C'], 2)
qcut_cond_by4 = pd.qcut(df['C'], 4)


print('各条件の戻り値の値~~~~~~')
print(bet_cond)
print(cond_isin)
print(cut_cond_境界値)
print(cut_cond_ビン数)
print(qcut_cond_by2)
print(qcut_cond_by4)

"""
元のDataFrame ~~~~~~~~
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333

各条件の戻り値の値~~~~~~
1_ONE       True
2_TWO       True
3_THREE    False
Name: C, dtype: bool

1_ONE       True
2_TWO      False
3_THREE     True
Name: C, dtype: bool

1_ONE        (0, 111]
2_TWO      (111, 222]
3_THREE    (222, 333]
Name: C, dtype: category

Categories (3, interval[int64]): [(0, 111] < (111, 222] < (222, 333]]
1_ONE      (111.0, 185.0]
2_TWO      (185.0, 259.0]
3_THREE    (259.0, 333.0]
Name: C, dtype: category

Categories (3, interval[float64]): [(111.0, 185.0] < (185.0, 259.0] < (259.0, 333.0]]
1_ONE      (110.999, 222.0]
2_TWO      (110.999, 222.0]
3_THREE      (222.0, 333.0]
Name: C, dtype: category

Categories (2, interval[float64]): [(110.999, 222.0] < (222.0, 333.0]]
1_ONE      (110.999, 166.5]
2_TWO        (166.5, 222.0]
3_THREE      (277.5, 333.0]
Name: C, dtype: category

Categories (4, interval[float64]): [(110.999, 166.5] < (166.5, 222.0] < (222.0, 277.5] < (277.5, 333.0]]


"""


desc = """
===========================================================
レシピ4続: maskとwhere

ここまでの分岐っぽい例で言うと、同じAならB、AでなければCの場合でも、
例外の方になんらかの重きが置かれる要件があります。
このような例は、普通に条件を記載しても良いですが、whereとmaskというメソッドがあるので、
より意図をはっきりさせるという面でもこれらを利用すると良いと思います。

・where:特定の値でない場合のみ、別の値で上書き
・mask: 特定の値の場合のみ、別の値で上書き
"""
df = __ex(desc)

CRIT = df['C'] > 111
df['D'] = df['A']
df['D'].mask(CRIT, df['X'], inplace=True)
df['E'] = df['A']
df['E'].where(CRIT, df['X'], inplace=True)

# 代入によりあらたなカラムへの挿入もできる
df['F'] = df['A'].where(CRIT)

# ref: whereについては、numpy版の方がシンタックスが直感的かもしれない
df['D_numpy_where'] = np.where(CRIT, '111より大きい', '111より大きくはない')

__p(df)


"""
インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333
アウトプット
             X  A   B    C      D      E    F D_numpy_where
1_ONE    10000  1  11  111      1  10000  NaN   111より大きくはない
2_TWO    20000  2  22  222  20000      2  2.0      111より大きい
3_THREE  30000  3  33  333  30000      3  3.0      111より大きい

"""


desc = """
===========================================================
レシピ5: applyを変換ルール、ビジネスルールを可視化するものとして捉える

DataFrame.apply()は、各行や各列に関する引数の関数での演算後のDataFrameやSeriesを戻すもの...
というのが無駄のない定義だと思いますが、ここでは、関数名や関数の定義のカタチが求められている
ビジネスルールを想起させるか、想起させるために使いたいという立場をとります。

なお、
※DataFrameのapplyメソッドでは、axis=1とすると、「lambda x:」の
xは各行のSeriesとなる。
... ので、x[カラム名]で、元のDataFrameの現在の行の当該カラムの値を取得できる。

というのは、applyのサンプル例などでapply(sum)といった例からは想像がつきにくいので、
ご存知なかった方は再確認ください。

"""
df = __ex(desc)

# 先述の例をapplyに無名関数を渡して実現する記載例
df['D'] = df.apply(
    lambda s: s['A'] if s['C'] > 111
    else s['X'], axis=1)

# Pythonの3項演算子のあえてのネストでの記述
#    3項演算子の見栄えに慣れる必要があるが、表や他の言語のswitch文ぽく見えるので、以外にわかりやすい?
df['E'] = df.apply(
    lambda s:
    s['A'] + 222 if s['C'] > 222 else
    s['A'] + 111 if s['C'] > 111 else
    s['X'],
    axis=1)

# 関数定義に切り出すスタイル --------ここから---------------
# 中身ではなく、確立されたルールなんだというところが大事という考え方にしたがったアプローチ → あ
# 確立されたルールであることと、ルールの象徴的な変数を目立たせるという気持ちでの関数定義 → い


def 業務ルール_あ(s):
    return s['A'] + 222 if s['C'] > 222 else s['A'] + 111 if s['C'] > 111 else s['X']


def 業務ルール_い(s, b):
    QQQ = b["判定カラム"]
    XXX = b["デフォルトカラム"]
    ZZZ = b["例外カラム"]
    return s[XXX] + 222 if s[QQQ] > 222 else s[XXX] + 111 if s[QQQ] > 111 else s[ZZZ]


df['あ'] = df.apply(業務ルール_あ, axis=1)

df['い'] = df.apply(
    業務ルール_い, b={'判定カラム': 'C', 'デフォルトカラム': 'A', '例外カラム': 'X'}, axis=1)
# ↑「業務ルール_い」というのがあるのね、というところと、それは、データ項目のC、A、Xあたりを軸にしたものなんだろうかという見栄えが表現できている!(個人の主観)

# どちらも今回のデータであれば、同じ結果になります。

# 関数定義に切り出すスタイル --------ここまで---------------

__p(df)

"""
インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333

アウトプット
             X  A   B    C      D      E      あ      い
1_ONE    10000  1  11  111  10000  10000  10000  10000
2_TWO    20000  2  22  222      2    113    113    113
3_THREE  30000  3  33  333      3    225    225    225



"""


desc = """
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

思いの外、代入に向けての行選択の話が膨らみました。
ここからは、Pandasのインデックスアライメントの性質を念頭に、(多少暗黙的になるのはともかく)
その分、プログラムの見栄えが、ビジネスルールの見栄えに近い記述ができるかという視点で、
Pandasの各種シンタックスを追ってみます。

===========================================================
レシピ6: インデックスアラインメント再びその1
・DataFrameやSeriesにSeriesを代入すると、インデックスの値を揃えて各行の値が代入される。
 つまり、現カラムと同じ値を元のDataFrameに列を追加して代入しようという場合には、以下の(例1)の記載で十分である。
・インデックスアライメントによるので、インデックスの値が違う左辺の行には、値が代入されない。
また、右辺に存在しないインデックスの行は欠損値となる。

→ このルールを前提に省略できる記述はあえて省略することで、見栄えが宣言的なプログラム近づけられるのでは?という期待。

"""
df = __ex(desc)

# (例1)
df['新カラムにカラムAと同じ値を設定'] = df['A']

df[['新カラムにカラムAと同じ値を設定_左右両辺にDF', '新カラムにカラムBと同じ値を設定_左右両辺にDF']] = df[['A', 'B']]

df['E_欠損値や代入されない例1'] = pd.Series({'1_ONE': 'あ', '2_TWO': 'い', '4_FOUR': 'え'})
df['F_欠損値や代入されない例2'] = pd.Series(
    {'2_TWO': 'イー', '3_THREE': 'ウー', '4_FOUR': 'エー'})

__p(df)

"""
レシピ6の出力

インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333

アウトプット
             X  A   B  ...  新カラムにカラムBと同じ値を設定_左右両辺にDF  E_欠損値や代入されない例1  F_欠損値や代入されない例2
1_ONE    10000  1  11  ...                        11               あ             NaN
2_TWO    20000  2  22  ...                        22               い              イー
3_THREE  30000  3  33  ...                        33             NaN              ウー

[3 rows x 9 columns]

"""


desc = """
===========================================================
レシピ6続:インデックスアラインメント再びその2
・列の値の条件で、行選択した場合のSeries
 および これらの複数のSeriesを(一見以外だが)横方向にconcatで結合
  ↓ 
・インデックスのリストが揃っていることを前提にデータフレーム同士を代入することで、演算した値のカラムを元のデータフレームに追加したのと同じ効果
"""
df = __ex(desc)


# Series視点で演算を考える
# sx、sa、sbはそれぞれ別の行のみ(ただし、元のDataFrameのインデックスの値はそのまま引き継ぎしている)のデータになっていることに注目。

sx = df[df['C'] == 111]['X']
sa = df[df['C'] == 222]['A']
sb = df[df['C'] == 333]['B']

# 別の行のデータをconcatする。→ 何が起きる???
print('SeriesのconcatによるDataFrameの内容')
xab_df = pd.concat([sx, sa, sb], axis=1, sort=True)

# 無事、インデックスを軸に結合された、つまり、3カラムのDataFrameが生成された。
print(xab_df)

# 代入したいカラムの名称を下記の書式でオペランドの左に記述。 ここにここまでで生成したxab_dfを代入。
print('DataFrameへのDataFrame代入によるカラム追加相当')
df[['addX', 'addA', 'addB']] = xab_df

# addXなどにそのまま設定された!!
print(df)


"""
レシピ6続の出力

インプット
             X  A   B    C
1_ONE    10000  1  11  111
2_TWO    20000  2  22  222
3_THREE  30000  3  33  333


アウトプット

SeriesのconcatによるDataFrameの内容
               X    A     B
1_ONE    10000.0  NaN   NaN
2_TWO        NaN  2.0   NaN
3_THREE      NaN  NaN  33.0

DataFrameへのDataFrame代入によるカラム追加相当
             X  A   B    C     addX  addA  addB
1_ONE    10000  1  11  111  10000.0   NaN   NaN
2_TWO    20000  2  22  222      NaN   2.0   NaN
3_THREE  30000  3  33  333      NaN   NaN  33.0



この例では、df['C'] == 111 の条件部分が簡単であるため、次の例をいたずらに複雑にしたにすぎない面もある。

df['addX'] =  df[df['C'] == 111]['X']
df['addA'] =  df[df['C'] == 222]['A']
df['addB'] =  df[df['C'] == 333]['B']

ただし、上記のように、並べた時に差異が目grepで分かるようなものならまだしも、
条件が複雑な場合は、先述のapplyを変換ルール(のポイント)を可視化するアプローチと組み合わせたり、
条件に該当する部分をBooleanインデックスの変数や配列に入れて名付けの上、抽象化するといいんじゃないか...という気がしてきませんか?
(少し例が弱いですが、力尽きたのでここまで....)

"""

文学論のような/文章術のような何か

冒頭の図にあげた例を念頭において、私の思う、Pandasで可能なシンプルな記法のあるある事例を前項であげてみました。

前項のようなパターンに目をならしていただいた上で、それらを下敷きに見栄えを整えるためのプラクティスを 以下に述べてみます。

STEP1: トランスフォームのカタチを見定める

まず今対面している問題が、冒頭に引用の図のex1、2、3に近いと感じたら、そのものズバリのPandasの定石の記述のうち、自分の好きなスタイルで端的に記述しよう。 というか、おそらくそんなに難しく考えずに自分の好きなスタイルで手が動くだろう。

STEP2: 定石と標準化と名付け

ただし、可能なら、Booleanインデックスを変数化して、条件に名前をつけてみると(つけた名前がそれなりに適切な場合に限るが)後から見て見通しが良いだろう。 特に、主条件と反転する条件の2条件で編集方法が違うという場合は、主条件を変数化したものを反転させる記述で実装しよう。

STEP3: locの使い所、右辺と左辺のバランス

なお、ex1、2、3に近いと感じて、定石記述で実装した際、自分の好きなスタイルが、locを使うものだとして、locより右側の記述が長いと感じるようであれば、列名指定の方式に見直したらどうなるか確認しよう。 あるいはloc周辺の記述を代入の左辺から右辺に移動させるとどうなるか。ただし、この場合はやはり列名指定にした方がすっきりする傾向がある。 つまるところ、loc記述は、ぼちぼち分類の数が多く、右辺が小さくなり、これらを並べた際に、プログラムのステートメントの並びが表のような見栄えになる場合に向くが、逆に、それ以外は向かない気がする。なお、この「見栄えが表」というものも、大きな表になりすぎる場合は、表の固定部分は静的な関数に、それ以外はパラメータ用の定数配列などにした方が良い場合もある。また、そもそも表が綺麗なだけで、正しくビジネスルールを分類できているか点検した方が良い。

STEP4: インデックスアライメント

また、インデックスアライメントの性質を活かして、明示的に指定しなくても動作するものは、よりタイプ数が少ない記述にしても良い。 (この方向性は良くない場合もあるが、Pandasの場合は、別の意図が感じられてしまう(がその意図が想像つきにくい)明示的な各種指定よりも インデックスアライメントの性質にはのっかった方が良いと思う。)

STEP5: 条件部分や追加列が複数あるなど複数項目に関連する場合

ex4なら、applyを考えて、無名関数で実現できるか考えよう。違和感を感じたら次に述べるex5風の例にならおう。

ex5のパターンの場合(絵面がそのようにイメージできた場合) 次のパターンでしっくりくるか確認しよう。


def ビジネスルールX(1行の列ごとのSeries):
    略


def ビジネスルールY(1行の列ごとのSeries):
    略


df['新カラムA'] = df.apply(ビジネスルールX, axis=1)
df['新カラムB'] = df.apply(ビジネスルールY, axis=1)

SUB1: ビジネスルール関数

もし ビジネスルールXやYの関数が複雑になるなら、あるいは違和感を感じたら、 ・あえて3項演算子のネストにすることでシンプルになるか確認。 ・ビジネスルールXの内容がYでも既視感があれば、あるいは同じビジネスルールと見なせそうならば、引数で切り替えれるか確認。  あるいは、共通部分を抜き出して、サブビジネスルールCommonを作成。 ・Seriesを意識しなくて良い判定処理などは、切り出して共通化しよう。名前をつけることが重要。ぴったりの名前を見つけられるかが問題を正しく認識できているかの目安。

SUB2: そもそもを疑うかあきらめか

上記でより複雑になりそうだったら、あるいは度を過ぎた共通化や標準化、汎用化になるなどパラメータだらけの関数が増えたりする匂いを感じたら、そもそもインプットのDataFrameが良くない可能性を疑っても良い。例えば、共通的な前処理、ビジネスルールとは関係ないレイヤーのデータ整備の処理を通した方が良い。 データ整備の結果、改めてex5のカタチになるか、ex3あたりのカタチに生まれ変わるかもしれない。

が、そこまで難しそうな場合は、メイン処理は極めてシンプルな上記の見せかけにしておき、apply用の関数にテキトー(適切でも適当でもない、テキトー)な名前を つけるとともに、コメントに「不具合が出た場合にリファクタリングすることとし、verX.Xではこの内容で凍結する整理とした...」とか残して、テストコードを通過させることに徹することにしよう。

おまけ:応用編

上記で述べたような主張を念頭に、DAGの処理をPandasで書いてみました。

コメントが多くてその分逆に見にくく(醜く)なっているのは、本記事のタイトルに反しますが、ご容赦ください。

参考文献

pandasクックブック ―Pythonによるデータ処理のレシピ―

Pythonデータ分析/機械学習のための基本コーディング!  pandasライブラリ活用入門 (impress top gear)