csvに格納された数値データを、指定した回数分だけ移動平均を求めるプログラムを作成しています。
ファイルからデータを読み込んで、グローバル変数で定義した配列にそのデータを代入していって、指定回数に達した場合にそれまで溜め込んだデータの平均値を計算してファイルに出力するといったプログラムを作ったのですが、どうもデータ量が多いせいかプログラムが終了するまでにとんでもない時間がかかってしまいます。
ただ平均をとるのではなくて、移動平均なので、配列データをシフトしていかなくてはなりません。
多分こんなことをvbでやるのが間違っているのかもしれませんが、使える言語がVBしかないので、困っています。
アドバイスいただけると助かります。
この情報だけじゃ「もっと早くするようにアルゴリズムを考え(ry」としかいえないなぁ…
とりあえずどこがボトルネックになってるかを探して、その部分を熟考しないとな…
一般的なことだが(orz
プログラム実行中はCPUが100%に張り付いた状態で、使用メモリ量は変わらないという状態です。
もしかして計算中にSleepでも入れてあげると計算も追いついたりするんですかね。。
良くわかっていなくてすみません ^_^;
まず
1、およそのデーターのサイズ。
2、指定回数。ところで回数てのは何?
3、書き込むデーターの形式。
位は書きましょう。
>もしかして計算中にSleepでも入れてあげると計算も追いついたりするんですかね。
まさか(^^:
この手の問題はなんと言ってもボトルネックの見極めが全てなんですが、
少しヒントを挙げておきます。
・配列のシフト
もしかしてデータ自体をシフトさせていませんか?
インデックスの方をずらすことで対処できませんか?
・関数やプロパティ、メソッド呼び出しの反復
引数の値が同じであれば毎回同じ値が返る副作用のない関数屋メソッド、
プロパティを 何度も同じ引数で呼び出していませんか?
変数の値の参照と違って、これらの呼び出しの反復はコンパイラでは
最適化しませんので、自分でループの外に出すなり、返り値を変数に
代入して使い回すなりして下さい。
馬鹿も休み休み言えといわれるかもしれませんが、
試しにSleepを入れてみたらまんまとはまりました。
情報が少なくて申し訳ないです。
1、およそのデーターのサイズ。
今扱っているデータは50MBのcsvファイルです。
これからはもっと増える可能性大。
2、指定回数。ところで回数てのは何?
移動平均回数のことです。
例えば1000個あるデータの20回ごとの移動平均値を出したいという場合の
"20回"です。Excelでも移動平均は出せるのですが、一度に扱えるデータ量
が65536なのでExcelはあきらめて自作することにしました。
3、書き込むデーターの形式。
csvファイルから読み込んでcsvに出力します。
>・配列のシフト
> もしかしてデータ自体をシフトさせていませんか?
> インデックスの方をずらすことで対処できませんか?
現在こんな方法で配列をシフトしています。
arrDat1(i-1) = arrDat1(i)
arrDat2(i-1) = arrDat2(i)
・
・
インデックスをずらすというのは、こういうことですか?
皆さん、コメントありがとうございます。
本当に助かります。
> 現在こんな方法で配列をシフトしています。
> arrDat1(i-1) = arrDat1(i)
> arrDat2(i-1) = arrDat2(i)
> ・
> ・
> インデックスをずらすというのは、こういうことですか?
これがデータ自体をシフトさせるということです。
インデックスをずらすというのは
i = i - 1
です。
なら(搭載メモリー)-(ファイルサイズ) > 20Mの範囲なら
全て読み込んで、オン・メモリーで処理します。
それはそれとしてやり方は色々なので。
ただ配列の中身をずらすのはちょっといただけません。
リングバッファ方式を使って見ましょう。
たとえば20個の移動平均を取りたい場合は、
20個のバッファを確保し、データーをバッファの
初めから入れていきます、21個目のデーターは又
一番初めのバッファに入れます、数珠のようにぐるぐる
リング状にエンドレスでデーターを読み込みます。
移動平均は、バッファに全てデーターが読み込まれた時点から
一個読み込む毎に、20個のバッファの平均を取ればいいことになります。
↑コピペしたときに先頭の『私』が抜けた。m(_ _)m
ねろさんの考えがちょと気になったので、少し考えて見ますた。
データ :- →1個のデータ。
元のデータの塊 x():----------------...--- →1〜nまでのn個のデータ
リングバッファ r():-------------------- →1〜20までの20個のデータバッファ
※添え字は、1から数えることにする。
※一つのデータのサイズは特に定めない。
1 元のデータの塊 x()を読み込む。
変数cntを定義し、cntの値はx()の現在読むべき位置を示す値w保存しておく。
cnt=1とする。
2 リングバッファ r()を0でクリア。
変数posを定義し、posの値はリングバッファの次に入れるべき場所を示す値を保存しておく。
pos=1とする。
3 r(pos)にx(cnt)を代入する。
rの全ての要素を足し20で割る(0.05を掛ける)→この値が移動平均の中心をcntとした値…?
cnt=cnt+1とする。
pos=(pos+1) mod 20とする。
4 cntがnになるまで3を繰り返す。
…と想像してみますた。
間違ってたらちょと鬱…
ガッさんありがとう。
私が書きたかったことがちゃんとフローになってる。(^^
>pos=(pos+1) mod 20
は
pos = pos Mod 20 + 1
の方がわかり易いかな。
> ねろさん
> ガッさんありがとう。
> 私が書きたかったことがちゃんとフローになってる。(^^
レスありが㌧←ぁ>(´Д`;)
ちょと気になって考えてみすたが、これで合っていますた?
中心cntの移動平均の値がちゃんと出るか確認してないので不安です;
@リングバッファを使うより、↓の感じがいいかもしれません。
※日本語で説明するよりコードの方がおいしかったです(orz
Sub calc_MovingAverage_A(ByRef buf() As Long)
Dim sum As Long
Dim buf_LB As Long
Dim buf_UB As Long
Dim Last_Out_Index As Long
Dim First_In_Index As Long
buf_LB = LBound(buf)
buf_UB = UBound(buf)
Last_Out_Index = buf_LB - 20
'遅い
For First_In_Index = buf_LB To buf_UB
'一つ追加
sum = sum + buf(First_In_Index)
If Last_Out_Index > buf_LB Then
'一つ削除
sum = sum - buf(Last_Out_Index)
End If
Last_Out_Index = Last_Out_Index + 1
Next
End Sub
Sub calc_MovingAverage_B(ByRef buf() As Long)
Dim sum As Long
Dim buf_LB As Long
Dim buf_UB As Long
Dim Last_Out_Index As Long
Dim First_In_Index As Long
buf_LB = LBound(buf)
buf_UB = UBound(buf)
'Last_Out_Index = buf_LB - 20
'速い?(calc_MovingAverage_Aを改変)
'※境界があやふやなので、少ないデータでテストしてみてください(orz
For First_In_Index = buf_LB To 20
sum = sum + buf(First_In_Index)
Debug.Print "中心 "; First_In_Index; "の移動平均は"; sum * 0.05
Next
Last_Out_Index = buf_LB
For First_In_Index = buf_LB + 21 To buf_UB '"+ 21"でいいのか…?
'一つ追加
sum = sum + buf(First_In_Index)
'一つ削除
sum = sum - buf(Last_Out_Index)
Last_Out_Index = Last_Out_Index + 1
Debug.Print "中心 "; First_In_Index; "の移動平均は"; sum * 0.05
Next
End Sub
長くてスマソ…激しくバグ潜在の(゜∀゜)ヨカーン
…良く考えたら、後半の部分が足りないなぁ…
orz<適便作り変えてください;
ガッさん凝り性ですね。私も。。。。
リングバッファを使うのは、データーを一括で読み込む場合ではなく、
ファイルからデーターを一つずつ読み込む場合です。
ファイルを一括で読み込む場合はガッさんのやり方が速い。
ただしリングの場合も平均値(実際は合計)を求めるのに、頭から足しこむ
のではなく、単に前の合計から、上書きされようとしているデーターを
引き、新しいデーターを足せば良いと思います。
Option Explicit
Const bNo = 20 '平均する個数
Private Sub Command1_Click()
'デバッグ用です、実際はファイルから読み込み
Dim i As Long
For i = 1 To 100
Ring (i)
Next
End Sub
Private Sub Ring(ByVal Value As Double)
Static Rbuff(0 To bNo) As Double 'Ring Buff Rbuff(0)は一番初めに使うのみ
Static Pos As Long 'Ring pointer
Static Sum As Double
Dim Ave As Double
Pos = Pos Mod bNo + 1
Sum = Sum - Rbuff(Pos) + Value 'New Sum Value
Ave = Sum / bNo '移動平均値
Rbuff(Pos) = Value
Debug.Print "pos = " & Pos & " : Value = " _
& Value & " : sum = " & Sum & " : Ave = " & Ave
End Sub
1から100まで20個づつの移動平均を出しています。データーが20を超えたら
1づつ増えていけば成功です。
この方式は宣言を除くと、たった4行のプログラムになり、
アルゴリズムが簡単なのが自慢です。
> ねろさん
これはっ!!…(゜Д゜ )ウマー
面白いですなw
数学苦手さんが読んでいるかわかりませんけど、こういう作業がアルゴリズムを考えるという感じで(ry
これは、参考にさせていただきます。
たしかに、こうゆう考えもあるんですねぇ〜
#と密かに動作確認してみたり。。。
皆さん、沢山のアドバイスありがとうございます。
いただいたアドバイスを参考にして、移動平均算出プログラムを作成して
みたのですが、別の問題が発生してしまい、またつまずいています。
移動平均回数を、テキストボックスに入力した任意の数値を使用するように
したいのですが、Staticで宣言する配列にこの数値を設定できません。
下記のエラーが発生します。
エラー: 定数式が必要です。
調べてみたところ、定数の定義は関数の戻り値などの不特定なもので行えない
ような雰囲気なのですが、他に方法はありませんでしょうか。
私のうすっぺらい脳を絞ってみても、確かにStaticで宣言する変数は
プログラムの実行時にメモリを確保するわけですから、プログラム中に
サイズを受け取るなんて無理なんですかね。。。
ぇーと、文中気になる部分。
> 移動平均算出プログラムを作成してみたのです
…俺も今アプリケーション層辺りのTCPの通信プロトコル組んでるんだが、
どうもイベントが正常に発生しないんだよな。
どうすればいいか分かるか?
とか反面教師風に聞いてみる。
問題の部分をコードしてみてくれ…
> Staticで宣言する配列にこの数値を設定できません
は、できそうで仕方が無いんだが…
もしかして
Static a(Text1.Text) As Long
とかしてませんか?
もしそうだったら
Static a() As Long
ReDim a(Text1.Text)
としてください。
あ、間違い。
Staticだから、ReDimだけじゃ毎回クリアされちゃいますね。
ReDim Preserve a(Text1.Text)
です。
すみません、質問文があまり長くならないようにと思ったのですが、
肝心な部分がなければ本末転倒ですよね。お手間をお掛けします。
<プログラム全体の構成>
ファイルからカンマで区切られている3つのデータを取得し換算後、別ファイルに保存する。また、計算後の結果の移動平均を更に別ファイルに出力する。
・フォーム
ソースファイル指定するTextBox:Text1
計算後のデータを保存するファイル指定するTextBox:Text2
移動平均回数を指定するTextBox:Text3
実行ボタン:Command1
・モジュール
CalcGファンクション:ファイルから取得したデータごとに計算を実行し
結果をファンクションの戻り値として返す
MvAveファンクション:CalcGファンクションの計算結果について移動平均
を求め戻り値を返す(このファンクションでエラーが発生する)
下記ちょっと長いですが、問題のコードです。
'フォームの処理
Private Sub Command1_Click()
Dim strSName As String '読み込みファイル名
Dim strDName As String '出力ファイル名
Dim intSNo As Integer
Dim intDNo As Integer
Dim strTextLine As String 'データ
Dim intMvAv As Integer '移動平均回数
'ファイル名取得
strSName = Trim(Text1.Text) '読み込み側
strDName = Trim(Text2.Text) '出力側
'移動平均回数取得
intMvAv = Val(Trim(Text3.Text))
'計算結果出力ファイルオープン
intDNo = FreeFile
Open strDName For Output As #intDNo
'読み込みファイルオープン
intSNo = FreeFile
Open strSName For Input As #intSNo
'データ処理ループ
Do While Not EOF(intSNo)
Line Input #intSNo, strTextLine
'計算処理
strTextLine = CalcG(strTextLine, strSName, intMvAv)
Print #intDNo, strTextLine
Loop
Close #intSNo
Close #intDNo
End Sub
'モジュールの処理
''数値計算部
Public Function CalcG(Data As String, strFname As String, intMvAv As Integer) As String 'ファイルから読み出したデータ、ファイル名、移動平均回数
Dim Dat1 As Double
Dim Dat2 As Double
Dim Dat3 As Double
Dim dobDat1 As Double
Dim dobDat2 As Double
Dim dobDat3 As Double
Dim strVal As String
Dim strAveFName As String
Dim i As Integer
Dim intDzNo As Integer
'出力ファイル名設定
strAveFName = strFname & "_MvAve"
'カンマで区切られているデータを取得
strVal = Right(Data)
i = InStr(Data, ",")
Dat1 = Val(Mid$(strVal, 1, i - 1))
strVal = Right(strVal, Len(strVal) - i)
i = InStr(strVal, ",")
Dat2 = Val(Mid$(strVal, 1, i - 1))
strVal = Right(strVal, Len(strVal) - i)
i = InStr(strVal, ",")
Dat3 = Val(Mid$(strVal, 1, i - 1))
'取得データの計算
Dat1 = (Dat1 - Dat3) / 330
Dat2 = (Dat2 - Dat3) / 330
'計算結果を出力
CalcG = Data & ", ," & Round(Dat1, 3) & "," & Round(Dat2, 3) & "," & Round(Dat3, 3)
'計算結果の移動平均を求める
dobDat1 = MvAve1(Dat1, intMvAv)
dobDat2 = MvAve2(Dat2, intMvAv)
dobDat3 = MvAve3(Dat3, intMvAv)
intDzNo = FreeFile
Open strAveFName For Append As #intDzNo
Print #intDzNo, Round(dobDat1, 3) & "," & Round(dobDat2, 3) & "," & dobDat3
Close #intDzNo
End Function
''移動平均算出部
Function MvAve1(ByVal Value As Double, ByVal intMvAv as integer) As Double
Static Buff(0 To intMvAv) As Double <- エラーの箇所
Static i As Long
Static Sum As Double
Dim Ave As Double
i = i Mod intMvAv + 1
Sum = Sum - Buff(i) + Value
Ave = Sum / intMvAv
Buff(i) = Value
MvAve = Ave
End Function
'--MvAve2、MvAve3と同様のコードを書く
こんなのの方がいいかな。
先ず
Const bNo = 20 '平均する個数
Static Rbuff(0 To bNo) As Double 'Ring Buff Rbuff(0)は一番初めに使うのみ
の2行をコメントアウトして、
Dim Ave As Double の後に
Static bNo As Long
Static Rbuff() As Double
If bNo <> Val(Text1.Text) Then '個数が変えられた
If Val(Text1.Text) <= 1 Then Exit Sub '個数少なすぎ
bNo = Val(Text1.Text) '個数再定義
ReDim Rbuff(1 To bNo) '配列再定義
Pos = 0: Sum = 0 'イニシャライズ
End If
を入れてみて下さい。
と書きながら、始めにコードで気になっていた、配列の添字の下限を0から1に
何気なく変更したりしてる。。。(^^;
書いてるうちに投稿されてました。
色々有りますが、スピードの点から
intDzNo = FreeFile
Open strAveFName For Append As #intDzNo
Print #intDzNo, Round(dobDat1, 3) & "," & Round(dobDat2, 3) & "," & dobDat3
Close #intDzNo
これはいただけません、その都度Open、Closeをやっていたのでは時間がかかります。
Open、Closeは呼び出し元で行い、値を返して書き込むか、
ファイル番号を引数にして渡し、書き込みだけを行えばAppendモードで書き込む
必要はないのでは。
それと
Public Function CalcG(Data As String, strFname As String, intMvAv As Integer) As String
このstrFname As String や intMvAv As Integeそれと
Function MvAve1(ByVal Value As Double, ByVal intMvAv as integer) As Double
intMvAv はテキストボックスから作ったものですから
引数で渡す必要はありません、まあスピードにはあまり影響はありませんが。
CSVファイルは普通はSplit関数で個別データーを取得します。
まあこれは今のままでもいいのですが。。。
全体に配列を使えばもっとすっきりとすると思いますが、スピードは変わりません。
ねろさん、ありがとうございました。
自分の無知さに悲しくなりましたが、見やすいプログラムにするように
少しずつ直していきたいと思います。
また、他にもアドバイスをいただいた皆様、お忙しい時間を割いていただき
ありがとうございました。
ツイート | ![]() |