mcmdのベンチマークテスト(FIT2018)

FIT2018 に発表した原稿 1 に掲載されたスクリプトについて説明する。 そこでは2つのスクリプトが掲載されており、1つはmcmdとpandasのベンチマークテストに関するもので、 他方は、相関ルール生成のスクリプトである。 いずれもデータとして株価4本値データを利用している。

準備

本節で扱っているPythonスクリプトの全ては github よりダウンロードできる ( リスト 19 )。 ダウンロードしたディレクトリ下 bench/fit2018/nysol_mcmd に 以下の3つのスクリプトがある。

  • mkdata.py : 株価データセット生成用スクリプト

  • bench.py : pandasとのベンチマーク用スクリプト( リスト 21 )

  • apriori.py : 原稿のfig.1に示された相関ルール分析のスクリプト( リスト 22 )

リスト 19 ベンチマークスクリプトのダウンロード
1$ git clone https://github.com/nysol/bench.git
2$ cd bench/fit2018/nysol_mcmd

mcmdベンチマークテスト(vs. pandas)

nysol.mcmdの処理速度を評価する時に、Pythonにおける表構造データの加工によく利用される Pandas をベンチマークとする速度比較テストを実施した。 Pandasは、表データをメモリ上で加工するための多種のメソッドを提供しており、 mcmdと同様にデータの前処理によく利用されるモジュールである。 内部では Numpy のコードも利用しており高速に動作する。 mcmdとの大きな違いは、Pandasはメモリ内に収まるデータを対象としているのに対して、 mcmdはファイル処理が基本のため、メモリサイズの制約を受けない。

利用データ

利用したデータは、「 乱数に基づいた株価データ 」節で解説されている人工的に生成した株価4本値データであり、 上述のgitからダウンロードしたスクリプト mkdata.py を実行すれば( リスト 20 )、 DATAディレクトリの下にデータが生成される。20〜30分ほど時間を要する。

リスト 20 株価生成スクリプトの実行
1$ ./mkdata.py
2id 1000 period 3902
3id 1001 period 6941
4id 1002 period 5896
5         :
6id 6999 period 6877

データは 表 13 に例示されるように、 銘柄ID( id )、日付( date )、 始値( o )、 高値( h )、 安値( l )、 終値( c ) の6項目から構成される株価4本値データである。

表 13 個別銘柄の4本値データ

id

date

o

h

l

c

1000

20180628

63427

63492

58474

61979

1000

20180627

61341

65684

61341

63604

1000

20180626

64200

66672

62960

64267

1000

20180625

65361

66354

63189

65268

:

:

:

:

:

:

このデータからサイズ違いの3種類のデータ(large,middle,small)を用意した。 それぞれのデータ行数とサイズは、 表 14 に示される通りである。 名称=multiは、並列処理用にlargeデータを日別に分割したものである。 株価生成スクリプトでは固定の乱数シードを用いているので、データの内容も同一であるはずであるが、 乱数システムの違いから異なる可能性もあり、サイズを確認されたい。

表 14 ベンチマークに用いたデータ一覧

名称

ファイル名

行数(ヘッダ含む)

サイズ(byte)

内容

large

price_large.csv

27,707,820

1,032,785,399

個別銘柄別4本値データ

middle

price_middle.csv

1,116,001

42,182,505

largeから2017/12/25以降を選択

small

price_small.csv

114,001

4,297,064

largeから2018/6/10以降を選択

multi

sep

11,546(ファイル数)

largeを日別ファイルにしたもの

処理内容

処理内容は、平均計算(task="avg")、移動平均の計算(task="win")、ループ処理(task="for")の3つのである。 それぞれについて、mcmdとpandasの最速と思われる実装によりコーディングしている。 時間計測も含めたベンチマークテストのスクリプトは リスト 21 に示すとおりである。 平均値の計算においては、largeデータを日別に分割しておき、 それぞれのファイルで平均値を並列計算させる処理を参考までに実施した( 関数 nm1a )。 また、mcmdでの移動平均の結果出力において、Pythonのリストに変換せず、ファイルにそのまま出力する実験も追加している( 関数 nm2a )。 ループ処理については、pandasの提供するインデックス参照 iloc が低速であったために、 内部的にnumpyを用いた values を用いた処理(関数 pd3a )も行った。 計測結果は、 OUTPUT/bench/price_5.txt に出力される。

リスト 21 ベンチマークスクリプト
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-/
  3import os
  4import sys
  5import time
  6from pprint import pprint
  7from datetime import datetime
  8from glob import glob
  9
 10import pandas as pd
 11import math
 12import nysol.mcmd as nm
 13
 14loop=5
 15
 16iPath=root="./DATA"
 17oPath=root="./OUTPUT/bench"
 18os.system("mkdir -p %s"%(oPath))
 19oFile="%s/price_%d.txt"%(oPath,loop)
 20
 21# CSVテキストをPython内部のデータ型に変換するための項目名-データ型対応表
 22# pd2,nm2,pd3,pd3aで利用している。
 23t={'id':'str','date':'int','o':'float','h':'float','l':'float','c':'float'}
 24
 25# pandasによる平均計算
 26def pd1(iFile):
 27  df = pd.read_csv(iFile)
 28  dfg=df.groupby("date")
 29  r = dfg.mean(numeric_only = True)
 30
 31# mcmdによる平均計算
 32def nm1(iFile):
 33  r = nm.mhashavg(k="date",f="o,h,l,c",i=iFile).run()
 34
 35# mcmdによる平均計算(並列処理)
 36def nm1a(iPath):
 37  fs=[]
 38  for iFile in glob("%s/*"%iPath):
 39    fs.append(nm.mhashavg(f="o,h,l,c",i=iFile))
 40  r=nm.runs(fs)
 41
 42# pandasによる移動平均
 43def pd2(iFile):
 44  df=pd.read_csv(iFile,dtype=t,usecols=['id','date','c'])
 45  df_id=df.groupby("id", sort=False).apply(lambda x: x.sort_values(["date"])).reset_index(drop=True)
 46  r=df_id.groupby("id", sort=False).rolling(on="date",window=3, min_periods=3).mean()
 47
 48# mcmdによる移動平均(リスト出力)
 49def nm2(iFile):
 50  f=None
 51  f <<= nm.mcut(f="id,date,c",i=iFile)
 52  f <<= nm.mwindow(k="id",wk="date:win",t=3)
 53  f <<= nm.mavg(k="id,win",f="c")
 54  f <<= nm.writelist(dtype="win:int,date:int,c:float")
 55  r=f.run()
 56
 57# mcmdによる移動平均(file出力)
 58def nm2a(iFile):
 59  f=None
 60  f <<= nm.mcut(f="id,date,c",i=iFile)
 61  f <<= nm.mwindow(k="id",wk="date:win",t=3)
 62  f <<= nm.mavg(k="id,win",f="c",o="%s/output_nm2.csv"%oPath)
 63  r=f.run()
 64
 65# pandasによる行単位のloop処理
 66def pd3(iFile):
 67  df = pd.read_csv(iFile,dtype=t)
 68  dfo = df.o.iloc; dfh = df.h.iloc
 69  dfl = df.l.iloc; dfc = df.c.iloc
 70  r=[0.0,0.0,0.0,0.0]
 71  for idx in range(df.shape[0]):
 72    r[0]+=dfo[idx] if not math.isnan(dfo[idx]) else 0.0
 73    r[1]+=dfh[idx] if not math.isnan(dfh[idx]) else 0.0
 74    r[2]+=dfl[idx] if not math.isnan(dfl[idx]) else 0.0
 75    r[3]+=dfc[idx] if not math.isnan(dfc[idx]) else 0.0
 76
 77# pandasによる行単位のloop処理(numpyのvaluesを利用)
 78def pd3a(iFile):
 79  df = pd.read_csv(iFile,dtype=t)
 80  dfo = df.o.values; dfh = df.h.values
 81  dfl = df.l.values; dfc = df.c.values
 82  r=[0.0,0.0,0.0,0.0]
 83  for idx in range(df.shape[0]):
 84    r[0]+=dfo[idx] if not math.isnan(dfo[idx]) else 0.0
 85    r[1]+=dfh[idx] if not math.isnan(dfh[idx]) else 0.0
 86    r[2]+=dfl[idx] if not math.isnan(dfl[idx]) else 0.0
 87    r[3]+=dfc[idx] if not math.isnan(dfc[idx]) else 0.0
 88
 89# mcmdによる行単位のloop処理
 90def nm3(iFile):
 91  r=[0.0,0.0,0.0,0.0]
 92  for line in nm.mnullto(f="*",v=0,i=iFile).convtype(t):
 93    r[0]+= line[2];r[1]+= line[3]
 94    r[2]+= line[4];r[3]+= line[5]
 95
 96# entry point
 97sec={}
 98mean={}
 99params=[]
100small ="%s/price_small.csv"%iPath
101middle="%s/price_middle.csv"%iPath
102large ="%s/price_large.csv"%iPath
103
104params.append(["pd1" ,small])
105params.append(["nm1" ,small])
106params.append(["pd2" ,small])
107params.append(["nm2" ,small])
108params.append(["nm2a",small])
109params.append(["pd3" ,small])
110params.append(["pd3a",small])
111params.append(["nm3" ,small])
112params.append(["pd1" ,middle])
113params.append(["nm1" ,middle])
114params.append(["pd2" ,middle])
115params.append(["nm2" ,middle])
116params.append(["nm2a",middle])
117params.append(["pd3" ,middle])
118params.append(["pd3a",middle])
119params.append(["nm3" ,middle])
120
121params.append(["pd1" ,large])
122params.append(["nm1" ,large])
123params.append(["nm1a","%s/sep"%iPath])
124params.append(["pd2" ,large])
125params.append(["nm2" ,large])
126params.append(["nm2a",large])
127params.append(["pd3" ,large])
128params.append(["pd3a",large])
129params.append(["nm3" ,large])
130
131for param in params:
132  func=param[0]
133  iFile=param[1]
134  name="%s_%s"%(func,iFile)
135  print("START:",name)
136  sec[name]=[]
137  for i in range(loop):
138    st=time.time()
139    eval(func+'("%s")'%iFile)
140    sec[name].append(time.time()-st)
141  mean[name]=0
142  for i in range(loop):
143    mean[name]+=sec[name][i]
144  mean[name]/=loop
145
146print("write to: ",oFile)
147with open(oFile, "w") as file:
148  pprint(sys.argv[0], stream=file)
149  pprint(loop, stream=file)
150  pprint(sec, stream=file)
151  pprint(mean, stream=file)

結果

出力結果をまとめたものを 表 15 に示している。 defは リスト 21 の関数名を表す。 small,mid,largeは 表 14 に示したサイズ別データセットの名称である。 また、fit2018での報告日に実験をやり直した結果は 表 16 に示している。

表 15 ベンチマークの結果(単位:秒)。fit2018のproceeding執筆時の結果。

task

program

def

small

mid

large

avg

pandas

pd1

0.130

1.28

29.18

avg

mcmd

nm1

0.036

0.33

7.39

avg

mcmd(multi)

nm1a

5.38

win

pandas

pd2

16.91

19.22

74.88

win

mcmd

nm2

0.27

2.54

63.94

win

mcmd(file)

nm2a

0.19

1.63

41.87

for

pandas

pd3

18.72

174.54

for

pandas(values)

pd3a

0.35

3.10

73.42

for

mcmd

nm3

0.27

2.73

60.43

表 16 ベンチマークの結果(単位:秒)。2018/9/20の結果。

task

program

def

small

mid

large

avg

pandas

pd1

0.103

0.96

24.10

avg

mcmd

nm1

0.031

0.29

7.32

avg

mcmd(multi)

nm1a

7.85

win

pandas

pd2

26.21

27.77

66.96

win

mcmd

nm2

0.23

2.29

58.70

win

mcmd(file)

nm2a

0.15

1.39

38.03

for

pandas

pd3

16.92

165.96

for

pandas(values)

pd3a

0.30

2.92

71.29

for

mcmd

nm3

0.28

2.65

65.43

ベンチマークテストを実施した計算環境は以下の通りである。

  • PC: MacPro(2013)

  • CPU: 2.7GHz 12-Core Intel Xeon E5

  • memory: 64GB

  • hdd: USB3 HDD

注釈

ここ以降の内容は、近い将来「 チュートリアル 」の節に移動します。

高収益率による銘柄の共起分析

リスト 22 は原稿の図1に掲載したスクリプトである (原稿にはタイポがいくつかあり修正している)。 このスクリプトは、日をトランザクションにして、収益率が高くなる時に共起しやすい銘柄について 2アイテムの相関ルール分析を行ったものである。 内容の詳細については、原稿を参考にしてもらいたい。

リスト 22 株価をランダムに生成するスクリプト
 1import nysol.mcmd as nm
 2tra=None
 3tra <<= nm.mcut(f="id,date,c",i=iFile)
 4tra <<= nm.mjoin(k="date",m=topix,f="i")
 5tra <<= nm.mslide(k="id",s="date",f="date:date2,c:c2,i:i2")
 6tra <<= nm.mcal(c="${c2}/${c}-${i2}/${i}",a="ret")
 7tra <<= nm.mselnum(f="ret",c="[0.05,0.1]")
 8tra <<= nm.mcut(f="id,date2:date,ret")
 9freq=None
10freq <<= nm.mcut(f="id",i=tra)
11freq <<= nm.mcount(k="id",a="freq")
12freq <<= nm.mselnum(f="freq",c="[5,]")
13total=None
14total <<= nm.mcut(f="date",i=tra)
15total <<= nm.muniq(k="date")
16total <<= nm.mcount(a="total")
17coFreq=None
18coFreq <<= nm.mcut(f="date,id",i=tra)
19coFreq <<= nm.mcommon(k="id",m=freq)
20coFreq <<= nm.mcombi(k="date",n=2,f="id",a="id1,id2")
21coFreq <<= nm.mcut(f="id1,id2")
22coFreq <<= nm.mfsort(f="id1,id2")
23coFreq <<= nm.mcount(k="id1,id2",a="coFreq")
24coFreq <<= nm.mjoin(k="id1",m=freq,K="id",f="freq:freq1")
25coFreq <<= nm.mjoin(k="id2",m=freq,K="id",f="freq:freq2")
26coFreq <<= nm.mproduct(m=total,f="total")
27coFreq <<= nm.mcal(c="(${coFreq}*${total})/(${freq1}*${freq2})",a="lift")
28coFreq <<= nm.msel(c="${lift}>10 && ${coFreq}>6",o="result.csv")
29coFreq.run(msg="on")
30# result.csvの内容
31# id1,id2%0,coFreq,freq1,freq2,total,lift
32# 1075,3669,7,126,123,9127,4.122402891
33# 3519,3669,8,96,123,9127,6.183604336
34# 1921,3669,7,94,123,9127,5.525774088
35#   : ,  : ,:, :, :,   : ,     :
36# 4729,6609,8,112,129,9127,5.053709856

Footnotes

1

中元政一,羽室行信,「NYSOL: Pythonにおける大規模データ前処理支援ツール」FIT2018:第17回情報科学技術フォーラム,2018/9/20,福岡工業大学.