business-rulesを使ってルールエンジンを使った処理を書く

ルールエンジンをPythonで使えると仕事上便利なことがありそうなので調査してみたところ、 business-rules が目的にあっているように思えたので、READMEを読みつつ試してみた。

使ったモジュール

Venmoが開発した business-rules を使うことにした。VenmoのことはRebuildで聞いたことがあるぐらいで使ったことはない。

github.com

  • Pythonで書かれたルールエンジンを探していた
  • Venmoの中でも使われているのかも。と考えて使ってみたくなった
  • READMEのサンプルコードが分かりやすかった

というのが選定理由。

コミットログを見るとしばらく活動がなさそうだけど、こういうのは一度書いて(安定して)動いてたら、よほどのことがなければ書き換えるものでもないかなと思って気にしないことにした。

サンプルコード

一度ルールエンジンを使って処理を書いたことがあれば、business-rules がなにをするためにものなのかは容易に理解できると思う。

経験がなくても、動かしながらいろいろ試してみればどういったものなのかの感覚はつかめるはず。

仕事柄広告のデータを扱うことが多いので、広告っぽいデータを処理するルールを書いたのがこちら。

from business_rules import run_all
from business_rules.actions import BaseActions, rule_action
from business_rules.fields import FIELD_TEXT
from business_rules.variables import BaseVariables, numeric_rule_variable


class Ad():
    def __init__(self, name, status, **kwargs):
        self.name = name
        self.status = status
        self.spend = kwargs.get('spend', 0)
        self.impressions = kwargs.get('impressions', 0)
        self.clicks = kwargs.get('clicks', 0)

    def save(self):
        print(self.name, self.status)


class AdVariables(BaseVariables):
    def __init__(self, ad):
        self.ad = ad

    @numeric_rule_variable
    def spend(self):
        return self.ad.spend

    @numeric_rule_variable
    def cpc(self):
        return self.ad.spend / self.ad.clicks

    @numeric_rule_variable
    def ctr(self):
        return self.ad.clicks / self.ad.impressions


class AdActions(BaseActions):
    def __init__(self, ad):
        self.ad = ad

    @rule_action(params={"status": FIELD_TEXT})
    def change_status(self, status):
        self.ad.status = status
        self.ad.save()


def main():
    rules = [{
        'conditions': {
            'all': [
                {'name': 'spend', 'operator': 'greater_than_or_equal_to', 'value': 200000},
                {'name': 'cpc', 'operator': 'greater_than_or_equal_to', 'value': 100},
                {'name': 'ctr', 'operator': 'less_than', 'value': 0.03},
            ]
        },
        'actions': [
            {'name': 'change_status', 'params': {'status': 'PAUSED'}},
        ],
    }]

    ads = [
        Ad(name='Ad1', status='ACTIVE', spend=100000, impressions=10000, clicks=1000),
        Ad(name='Ad2', status='ACTIVE', spend=200000, impressions=10000, clicks=500),
        Ad(name='Ad3', status='ACTIVE', spend=400000, impressions=20000, clicks=500),
    ]
    for ad in ads:
        run_all(rule_list=rules,
                defined_variables=AdVariables(ad),
                defined_actions=AdActions(ad),
                stop_on_first_trigger=True)


if __name__ == '__main__':
    main()

サンプルコードの説明

  • Ad は広告そのものを表現する
  • AdVariablesAdbusiness-rules が評価できる形式にするためのもの
    • ここではspend(消化金額)と合わせて、CPC(クリック単価)やCTR(クリック率)を評価対象にしている(簡略化のためにゼロ割は考慮していない)
    • CPCやCTRのように、もとのオブジェクトに無いものでも、計算によって求められた値を評価できる
    • どんな値でも BaseVariables を継承したクラスでラップするような形になるので、対象のデータの形式によらずルールを適用できる
  • AdActions は評価した結果、ルールに一致したものをどのように処理するかを表現する
    • 今回はルールに一致した広告のステータスを変更するようなサンプルにしているので、ステータスを変更するような処理として change_status を定義した
  • main でルールを定義し、与えられたデータをルールに従って評価する
    • ルールには conditionsactions が定義できる
    • conditions では、 less_thangreater_than_or_equal_to などの演算子を用いて、先に定義した AdVariables の値を評価することができる
    • actions では、ルールに一致したものをどのように処理するかを定義する。今回はルールに一致した広告を停止するようなサンプルにしているので、 AdActions に定義した change_statusstatusに、広告の停止状態を表す PAUSED を渡したものを実行する

実行

サンプルコードでは、以下の conditions すべてに一致したものを actions に従って処理する。

  • 消化金額が¥200,000以上
  • CPC(消化金額 / クリック数)が¥100以上
  • CTR(クリック数 / 表示回数)が3%未満

これらを満たすのは Ad3 (消化金額¥400,000、CTR¥800、CTR2.5%)なので、実行結果は以下となる。

$ python main.py
Ad3 PAUSED

まとめ

  • business-rulesPythonにおけるルールエンジンとして実用的かつシンプルで良い
  • Pandas、scikit-learnなどのデータ分析、機械学習の仕組みと組み合わせると面白そう

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎