Pythonでアプリオリ・アルゴリズムを実装してPyPI登録

アソシエーション分析(バスケット分析)

Pythonでアプリオリ・アルゴリズムを実装したライブラリはいくつかありますが、リフト(Lift)値を考慮に入れたものは、Orangeしか見当たりませんでした。

しかし、Orangeはpip installできないので不便だと思い、自前で実装してPyPIにパッケージ登録しました。

https://github.com/aknd/akapriori

使用方法

$ pip install akapriori

インストールしておき、

from akapriori import apriori

transactions = [
    ("apricot", "apple", "cherry", "plum", "banana"),
    ("strawberry", "plum", "cherry"),
    ("persimmon", "peach", "banana", "apple"),
    ("kiwi fruit", "apple", "pear"),
    ("cherry", "pear", "banana"),
    ("watermelon", "apple"),
    ("plum", "banana"),
    ("pear", "peach", "cherry", "banana", "apricot"),
    ("pineapple", "apple", "plum"),
    ("banana", "plum", "peach"),
    ("grape", "cherry"),
    ("mandarin", "plum"),
    ("melon", "apple", "persimmon", "plum"),
    ("peach", "cherry", "apple"),
    ("apple", "mandarin", "plum", "persimmon"),
]

“support >= 0.05 and confidence >= 0.3 and lift > 2"を満たすルールを抽出したいなら、以下のように実装します。

rules = apriori(transactions, support=0.05, confidence=0.3, lift=2)
rules_sorted = sorted(rules, key=lambda x: (x[4], x[3], x[2]), reverse=True) # ORDER BY lift DESC, confidence DESC, support DESC

for r in rules_sorted:
    print(r)

出力結果

(frozenset(['kiwi fruit']), frozenset(['pear']), 0.06666666666666667, 1.0, 5.0)
(frozenset(['melon']), frozenset(['persimmon']), 0.06666666666666667, 1.0, 5.0)
(frozenset(['pear']), frozenset(['kiwi fruit']), 0.06666666666666667, 0.3333333333333333, 5.0)
(frozenset(['persimmon']), frozenset(['melon']), 0.06666666666666667, 0.3333333333333333, 5.0)
(frozenset(['apricot']), frozenset(['banana']), 0.13333333333333333, 1.0, 2.5)
(frozenset(['apricot']), frozenset(['cherry']), 0.13333333333333333, 1.0, 2.5)
(frozenset(['grape']), frozenset(['cherry']), 0.06666666666666667, 1.0, 2.5)
(frozenset(['strawberry']), frozenset(['cherry']), 0.06666666666666667, 1.0, 2.5)
(frozenset(['apricot']), frozenset(['pear']), 0.06666666666666667, 0.5, 2.5)
(frozenset(['mandarin']), frozenset(['persimmon']), 0.06666666666666667, 0.5, 2.5)
(frozenset(['banana']), frozenset(['apricot']), 0.13333333333333333, 0.3333333333333333, 2.5)
(frozenset(['cherry']), frozenset(['apricot']), 0.13333333333333333, 0.3333333333333333, 2.5)
(frozenset(['pear']), frozenset(['apricot']), 0.06666666666666667, 0.3333333333333333, 2.5)
(frozenset(['persimmon']), frozenset(['mandarin']), 0.06666666666666667, 0.3333333333333333, 2.5)

基本的な定義

トランザクション: 個々の客の1回の買い物(バスケット)

Ω: 全トランザクション

A: 商品Aの購入を含むトランザクション

n(Ω): 全トランザクション数(全バスケット数)

n(A): 商品Aの購入を含むトランザクションの総数(商品Aを含むバスケット数)

n(A ∩ B): 商品Aと商品Bの両方の購入を含むトランザクションの総数(商品Aと商品Bの両方を含むバスケット数)

P(A): 商品Aが購入される確率

P(A ∩ B): 商品Aと商品Bが同時購入される確率

P(B | A): 商品Aを購入しているという条件での、商品Bを購入している条件付き確率

A => B: 「商品Aを購入していると商品Bも購入している」というルール(左側を条件部、右側を結論部と呼ぶ)

Support

Supp(A): 商品AのSupport(支持度)

Supp(A) := n(A) / n(Ω) = P(A)

Supp(A => B): ルール A => B のSupport(支持度)

Supp(A => B) := n(A ∩ B) / n(Ω) = P(A ∩ B) = P(B ∩ A) = n(B ∩ A) / n(Ω) =: Supp(B => A)

矢印が付いていますが、Supp(A => B)に方向性はないことに注意しましょう。

Supportが大きいルールを抽出することが必要な理由は、そもそもほとんど売れていな商品Aと商品Bがたまたま同時に買われた様な場合に、そのルールを重要視してしまうことを防ぐためです。

また、Supportが小さいものは、そもそもほとんど売れていないので、たとえルールとして意味があったとしても、それを発見したところでビジネスとしての旨味はないとも考えられます。

Confidence

Conf(A => B): ルール A => B のConfidence(確信度)

Conf(A => B) := Supp(A => B) / Supp(A) = P(A ∩ B) / P(A) = P(B | A)

Confidenceが大きいルールほど、併売の結びつきが強いルールと言えます。

Lift

Lift(A => B): ルール A => B のLift(リフト)

Lift(A => B) := Conf(A => B) / Supp(B) = P(B | A) / P(B)

単純に商品Bを購入する割合よりも商品Aを購入した中で商品Bも購入した割合の方が高いとき、リフトは1を超えます。

したがって、リフトが1を超えたルールが意味のあるルールです。

リフトが大きいルールを抽出することで、ルールに関係なく売れている商品を除外することができます。

BigQueryのライブラリ(Python)が変更

AttributeError: ‘QueryJob’ object has no attribute ‘results’

以前に作ったJobで、PythonでBigQueryを叩くものがあり、cronで毎日動かしていたのですが、昨日はエラーになっていました。

予想通り、ライブラリ https://github.com/GoogleCloudPlatform/google-cloud-python が変更されていました。

QueryJobオブジェクトのメソッドresultsが削除されたようです。よく見ると、もともとDEPRECATEDとなっていましたね。result(単数系)があるので、こちらを使えということでしょうか。

変更箇所を全部確認していないので不安ですが、resultsをresultに書き換えたら動いたので、ひとまず良しとしておきます。

変更箇所

pip installがこけた

AttributeError: ‘_NamespacePath’ object has no attribute ‘sort’

今日新しくmkvirtualenvでPython3.5.2の環境を作り、pip installするとエラーになりました。

$ pip install --upgrade pip

としても直らず。エラーをよく見ると、setuptoolsが怪しいことに気づき、

$ pip install --upgrade setuptools

として解決しました。

RubyによるFX自動売買システム(導入検討)

Jiji

FXの自動売買システムを構築しようとすると、メタトレーダー4(MT4)という有名なツールを使うのが普通です。しかし、取引アルゴリズムを実装する際には、MQLという独自言語を使う必要があります。

Pythonから実行できるブリッジライブラリもあるんですが、情報が少なく、他に方法がないか調べていたところ、Jijiというのを発見しました。

http://jiji2.unageanu.net/

Rubyでシステムトレードできるのが良いですね。

Pythonの方が機械学習のライブラリを使えるので良いかなと始めは思ったのですが、よくよく考えれば、どのみち学習は事前にさせておく必要があります。

なので、機械学習の部分はPythonで実装し、学習結果をもとに予測値を取得するAPIを用意すれば、システムを綺麗に分離することができます。

むしろ、様々な指標や予測値を基に取引ルールを実装するのは、Rubyの方が簡単にできる気がします。

機械学習の部分はディープラーニングを試してみようかと思っています。

ダミー変数を生成するgemを作ってみた

dummy_variables

時系列分析をする際に、量的データ以外の事項を考慮に入れたい場合があります。 例えば、商品の売上推移をモデリングするために、価格などの数値以外にも、曜日の影響を取り入れたい場合です。曜日は数値ではないので、何らかの形で数値化する必要があります。通常はダミー変数を使うことになります。つまり、土曜日なら「1」、それ以外なら「0」のような変数を用意します。

このようなダミー変数の生成を簡単にできるようにしたい、ということで、Rubyでdummy_variablesというライブラリを作り、Gemにしました。公式ドキュメントに書き忘れましたが(そのうち追記します)、Ruby2.0以上でないとダメなはずです(実装にキーワード引数を使っていますが、キーワード引数は2.0で追加された仕様なので)。

https://github.com/aknd/dummy_variables

インストール

$ gem install dummy_variablesとしても良いですが、 bundlerはインストールされている前提で、Gemfileに

gem 'dummy_variables'

と書いて

$ bundle install --path vendor/bundle

のようにするのがオススメです。

使用方法

requireしておいて、

require "dummy_variables"

DummyVariables::Calendar.newの引数に開始日と終了日を渡します。 to_csv_strで、CSV形式の文字列を出力するので、それをファイルに書き込みます。

cal = DummyVariables::Calendar.new("20140101", "20151231")
File.open("cal.csv", "w") { |file| file.write(cal.to_csv_str) }

中身を見てみると、デフォルトで曜日が全てダミー変数になっています。

date,sun,mon,tue,wed,thu,fri,sat
2014-01-01,0,0,0,1,0,0,0
2014-01-02,0,0,0,0,1,0,0
2014-01-03,0,0,0,0,0,1,0
2014-01-04,0,0,0,0,0,0,1
2014-01-05,1,0,0,0,0,0,0
2014-01-06,0,1,0,0,0,0,0
2014-01-07,0,0,1,0,0,0,0
2014-01-08,0,0,0,1,0,0,0
2014-01-09,0,0,0,0,1,0,0

日付のフォーマットを指定したり、出力するカラムを選択したり、ヘッダーの有無やダブルクォーテーションの有無などのオプションを指定したりできます。

cal = DummyVariables::Calendar.new("20140101", "20151231", "%Y/%m/%d") # add date format

File.open("cal.csv", "w") do |file|
  file.write(
    cal.to_csv_str(
      [:sat, :sun], # select columns
      options: { # add options
        :write_headers => false,
        :force_quotes => true
      }
    )
  )
end

中身を確認。

"2014/01/01","0","0"
"2014/01/02","0","0"
"2014/01/03","0","0"
"2014/01/04","1","0"
"2014/01/05","0","1"
"2014/01/06","0","0"
"2014/01/07","0","0"
"2014/01/08","0","0"
"2014/01/09","0","0"
"2014/01/10","0","0"

曜日以外のダミー変数を生成するためには、設定ファイルを作ります。 YAMLかJSONですが、今回はYAMLにして、config_file.ymlというファイル名で作成します。

holiday:
  dates:
    - 2014-01-01
    - 2014-01-13
    - 2014-02-11
    - 2014-03-21
    - 2014-04-29
    - 2014-05-03
    - 2014-05-04
    - 2014-05-05
    - 2014-05-06
    - 2014-07-21
    - 2014-09-15
    - 2014-09-23
    - 2014-10-13
    - 2014-11-03
    - 2014-11-23
    - 2014-11-24
    - 2014-12-23
    - 2015-01-01
    - 2015-01-12
    - 2015-02-11
    - 2015-03-21
    - 2015-04-29
    - 2015-05-03
    - 2015-05-04
    - 2015-05-05
    - 2015-05-06
    - 2015-07-20
    - 2015-09-21
    - 2015-09-22
    - 2015-09-23
    - 2015-10-12
    - 2015-11-03
    - 2015-11-23
    - 2015-12-23
break:
  dates:
    - 2014-01-02
    - 2014-01-03
    - 2014-08-13
    - 2014-08-14
    - 2014-08-15
    - 2014-12-29
    - 2014-12-30
    - 2014-12-31
    - 2015-01-02
    - 2015-08-13
    - 2015-08-14
    - 2015-12-29
    - 2015-12-30
    - 2015-12-31

設定ファイル名config_file.ymlを引数に渡します。config_file: とキーワードを指定する必要があるので注意してください。

cal = DummyVariables::Calendar.new("20140101", "20151231", config_file: "config_file.yml")
File.open("cal.csv", "w") { |file| file.write(cal.to_csv_str) }

中身を確認すると、設定ファイルに書いたholiday(祝日)とbreak(年末年始など、祝日以外の長期休暇)がダミー変数になっています。

date,sun,mon,tue,wed,thu,fri,sat,holiday,break
2014-01-01,0,0,0,1,0,0,0,1,0
2014-01-02,0,0,0,0,1,0,0,0,1
2014-01-03,0,0,0,0,0,1,0,0,1
2014-01-04,0,0,0,0,0,0,1,0,0
2014-01-05,1,0,0,0,0,0,0,0,0
2014-01-06,0,1,0,0,0,0,0,0,0
2014-01-07,0,0,1,0,0,0,0,0,0
2014-01-08,0,0,0,1,0,0,0,0,0
2014-01-09,0,0,0,0,1,0,0,0,0

設定ファイルを用意するのではなく、設定データを引数で直接渡すこともできます。

config_data = {
  "holiday" => {
    "dates" => [
      "2014-01-01", "2014-01-13", "2014-02-11", "2014-03-21", "2014-04-29", "2014-05-03", "2014-05-04",
      "2014-05-05", "2014-05-06", "2014-07-21", "2014-09-15", "2014-09-23", "2014-10-13", "2014-11-03",
      "2014-11-23", "2014-11-24", "2014-12-23", "2015-01-01", "2015-01-12", "2015-02-11", "2015-03-21",
      "2015-04-29", "2015-05-03", "2015-05-04", "2015-05-05", "2015-05-06", "2015-07-20", "2015-09-21",
      "2015-09-22", "2015-09-23", "2015-10-12", "2015-11-03", "2015-11-23", "2015-12-23"
    ]
  },
  "break" => {
    "dates" => [
      "2014-01-02", "2014-01-03", "2014-08-13", "2014-08-14", "2014-08-15", "2014-12-29", "2014-12-30",
      "2014-12-31", "2015-01-02", "2015-08-13", "2015-08-14", "2015-12-29", "2015-12-30", "2015-12-31"
    ]
  }
}

cal = DummyVariables::Calendar.new("20140101", "20151231", config_data: config_data)
File.open("cal.csv", "w") { |file| file.write(cal.to_csv_str) }

さっきと同様に上手くいきました。

date,sun,mon,tue,wed,thu,fri,sat,holiday,break
2014-01-01,0,0,0,1,0,0,0,1,0
2014-01-02,0,0,0,0,1,0,0,0,1
2014-01-03,0,0,0,0,0,1,0,0,1
2014-01-04,0,0,0,0,0,0,1,0,0
2014-01-05,1,0,0,0,0,0,0,0,0
2014-01-06,0,1,0,0,0,0,0,0,0
2014-01-07,0,0,1,0,0,0,0,0,0
2014-01-08,0,0,0,1,0,0,0,0,0
2014-01-09,0,0,0,0,1,0,0,0,0