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で学ぶ特徴量エンジニアリングと機械学習の基礎

Pythonでgoogle-cloud-bigqueryを使用してLegacy SQLを実行する

google-cloud-bigquery はデフォルトでStandard SQLを使用することになっているため、Legacy SQLを使用する場合は明示的に指定する必要がある。

github.com

さっとソースを読んでみたところ、QueryJobConfigというのを使うといいらしい。

サンプル

from google.cloud import bigquery

if __name__ == '__main__':
    client = bigquery.Client.from_service_account_json('./credentials.json')

    query = 'SELECT * FROM [project:dataset.table]'
    config = bigquery.QueryJobConfig()
    config.use_legacy_sql = True

    rows = client.query(query, job_config=config).result()
    for row in rows:
        print(row)

歴史があるサービスだとLegacy SQLを使うこともあるので覚えておく。

Google BigQuery

Google BigQuery

React Nativeで買い物リストアプリを作ってiOS/Androidで動作させた

やりたかったこと

React Nativeに入門するも、Hello Worldより先になかなか進まなかったので、小さな物でもいいのである程度使えるアプリを作ってみることにした。

お題としてTODOリストのような一般的な物を作ることにしたため、どう実装するかはさておいて、機能については悩みどころがなかったのはよかったと思う。

何を作ろう?どんな機能が必要だろう?から考えてしまうとなかなか進まないので、おきまりの物を作ろうと割り切った。

スクリーンショット

見た目はiOS/Androidもほぼ同じ。

iOS

iOSに合わせてレイアウトした。

f:id:ariarijp:20171107131645p:plain

Android

上に若干余白があるが、本来はプラットフォーム別に調整できるところをサボっているだけ。

f:id:ariarijp:20171107133404p:plain

リポジトリ

この記事を書いた時点ではREADMEすら書いてないけど、React Nativeの環境ができていれば、 git clone して cd して npm i の後に react-native run-ios とかやれば動くはず。

github.com

主に使ったコンポーネント/API

悩んだところ

コンポーネントの分割粒度

今回は作りきることが目的だったのでざっくり分けたが、ネイティブもReactも経験が少ないので筋のいい分け方が知りたい。

FlatListの扱いに悩んだ

アイテムが追加されても、FlatListをスクロールするまで新しいアイテムが再描画されなかった。

extraDataというpropを設定し、その値も合わせて更新することで再描画してくれるようになったが、extraDataに配列を渡しても変更を検出してくれないようなので、数値などの値にする必要があるようだ。今回は表示対象のアイテムを保持する配列の長さをextraDataとして使用した。

stackoverflow.com

thisの扱い

必要に応じてbindを使って解決したが、bindを使わずにスマートにかけるのであればその方法を知りたい。

developer.mozilla.org

まとめ

  • ある程度JavaScriptが書ければ簡単なアプリは作れる
  • 小さい規模であれば、差異をほぼ気にせずにクロスプラットフォーム対応できた
  • 検索して得られる情報も増えていきたので、エラーや予期しない挙動があってもなんとか解決できる状況になってきた
  • 環境構築やシミュレーターでの動作確認に躓くと厳しそう。その場合はネイティブの知識が必要になりそうな気がする

個人的にはもうちょっとJavaScriptとその周辺エコシステムを学んだら、もうちょっと気分良くかけるかも。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

Pythonのリストを決まったサイズで分割するときはmore-itertoolsのchunkedを使う

いつも忘れては同じこと調べてるのでメモ。

More Itertools — more-itertools 3.2.0 documentation

pipでインストールして

$ pip install more-itertools

こんな感じで使うと

from more_itertools import chunked

if __name__ == '__main__':
    items = range(28)
    print(list(chunked(items, 10)))

こんな結果になる。

$ python main.py
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27]]

これで忘れても大丈夫。

入門 Python 3

入門 Python 3

標準入力から受け取った文字列をSlackに通知するhouを作った

ぼんやりとみんGoを眺めていたら、horensoみたいに賢くなくていいから、標準入力をそのままSlackに投げるCLIツールが自分用にあったらいいような気がしてきた。

github.com

そこで作ったのがこれ。horenso(報・連・相)でいうところの「報」しかやらないのでhouという名前。

以下の記事のことも頭の片隅にあったかもしれないけど、nofify_slackのほうが対ISUCONを意識されてるっぽいし実戦的だと思う。くらべてhouはもっと雑。

medium.com

使い方

READMEの通り。オプションで色々渡せるようにしてはあるけど、無くてもいいねって感じのオプションばかり。ついついオプションを増やしたくなってしまう悪い癖がどうも抜けない。

$ export SLACK_API_TOKEN=YOUR_SLACK_API_TOKEN
$ w | hou -channel hello

環境変数にSlackのトークンを入れて、何らかの標準入力を受け取って -channel で指定されたSlackのチャンネルにメッセージを投げる。

メッセージの受信イメージはこんな感じ。

f:id:ariarijp:20170925224302p:plain

せっかく作ったので、ちょっとしたバッチを crontab に設定したときの動作確認とか、VMなどでちょっと重めのバッチを回しているときの完了通知とかに使おうかなと思っているところ。

余談

「ゆとりの法則 - 誰も書かなかったプロジェクト管理の誤解」読んでない気がするし、マーケットプレイスで送料込み500円ちょっとだったのでとりあえず買ってみた。

原題は「Slack: Getting Past Burnout, Busywork, and the Myth of Total Efficiency」らしい。

ゆとりの法則 ? 誰も書かなかったプロジェクト管理の誤解

ゆとりの法則 ? 誰も書かなかったプロジェクト管理の誤解