4

頻繁に使用する関数があるため、パフォーマンスを可能な限り向上させる必要があります。Excel からデータを取得し、データが特定の期間内にあるかどうか、およびピーク時 (Mo-Fr 8-20) であるかどうかに基づいて、データの一部を合計、平均、またはカウントします。

通常、データは約 30,000 行と 2 列 (時間ごとの日付、値) です。データの重要な特徴の 1 つは、日付列が時系列順に並べられていることです。

私は 3 つの実装を持っています。c# には拡張メソッドがあります (非常に遅く、誰かが興味を持っていない限り、それを示すつもりはありません)。

次に、この f# 実装があります。

let ispeak dts =
    let newdts = DateTime.FromOADate dts
    match newdts.DayOfWeek, newdts.Hour with
    | DayOfWeek.Saturday, _ | DayOfWeek.Sunday, _ -> false
    | _, h when h >= 8 && h < 20 -> true
    | _ -> false

let internal isbetween a std edd =
    match a with
    | r when r >= std && r < edd+1. -> true
    | _ -> false

[<ExcelFunction(Name="aggrF")>]
let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let newd =
        [0 .. (Array.length data) - 1]
        |> List.map (fun i -> (data.[i], data2.[i])) 
        |> Seq.filter (fun (date, _) -> 
            let dateInRange = isbetween date std edd
            match pob with
            | "Peak" -> ispeak date && dateInRange
            | "Offpeak" -> not(ispeak date) && dateInRange
            | _ -> dateInRange)
   match sac with 
   | 0 -> newd |> Seq.averageBy (fun (_, value) -> value)
   | 2 -> newd |> Seq.sumBy (fun (_, value) -> 1.0)
   | _ -> newd |> Seq.sumBy (fun (_, value) -> value)

これには 2 つの問題があります。

  1. 日付も値もdoubleなのでデータを用意する必要があります[]
  2. 日付が時系列であるという知識を利用していないため、不必要な反復を行っています。

これが、ブルート フォースの命令型 C# バージョンと呼ばれるものです。

        public static bool ispeak(double dats)
    {
        var dts = System.DateTime.FromOADate(dats);
        if (dts.DayOfWeek != DayOfWeek.Sunday & dts.DayOfWeek != DayOfWeek.Saturday & dts.Hour > 7 & dts.Hour < 20)
            return true;
        else
            return false;
    }

    [ExcelFunction(Description = "Aggregates HFC/EG into average or sum over period, start date inclusive, end date exclusive")]
    public static double aggrI(double[] dts, double[] vals, double std, double edd, string pob, double sumavg)
    {
        double accsum = 0;
        int acccounter = 0;
        int indicator = 0;
        bool peakbool = pob.Equals("Peak", StringComparison.OrdinalIgnoreCase);
        bool offpeakbool = pob.Equals("Offpeak", StringComparison.OrdinalIgnoreCase);
        bool basebool = pob.Equals("Base", StringComparison.OrdinalIgnoreCase);


        for (int i = 0; i < vals.Length; ++i)
        {
            if (dts[i] >= std && dts[i] < edd + 1)
            {
                indicator = 1;
                if (peakbool && ispeak(dts[i]))
                {
                    accsum += vals[i];
                    ++acccounter;
                }
                else if (offpeakbool && (!ispeak(dts[i])))
                {
                    accsum += vals[i];
                    ++acccounter;
                }
                else if (basebool)
                {
                    accsum += vals[i];
                    ++acccounter;
                }
            }
            else if (indicator == 1)
            {
                break;
            }
        }

        if (sumavg == 0)
        {
            return accsum / acccounter;
        }
        else if (sumavg == 2)
        {
            return acccounter;
        }
        else
        {
            return accsum;
        }
    }

これははるかに高速です(主に期間が終了したときにループが終了するためだと思います)が、明らかに簡潔ではありません。

私の質問:

  1. ソートされたシリーズの f# Seq モジュールで反復を停止する方法はありますか?

  2. f# バージョンを高速化する別の方法はありますか?

  3. 誰かがこれを行うためのさらに良い方法を考えることができますか? どうもありがとう!

更新:速度比較

2013 年 1 月 1 日から 2015 年 12 月 31 日までの 1 時間ごとの日付 (約 30,000 行) と対応する値を含むテスト配列を設定しました。日付配列全体に 150 回の呼び出しを行い、これを 100 回繰り返しました - 15000 回の関数呼び出し:

上記の csharp の実装 (ループの外側に string.compare を使用)

1.36秒

マシューズ再帰 fsharp

1.55秒

トーマス配列 fsharp

1分40秒

私のオリジナルのシャープ

2分20秒

明らかに、これは常に私のマシンにとって主観的なものですが、アイデアを提供し、人々がそれを求めてきました...

また、これは再帰や for ループが array.map などよりも常に高速であることを意味するわけではないことに留意する必要があると思います。この場合、c# と f# の反復から早期に終了しないため、多くの不要な反復が行われます。再帰メソッドが持っている

4

2 に答える 2

7

andArrayの代わりに使用すると、これが約 3 ~ 4 倍速くなります。インデックスのリストを生成し、それをマップして 2 つの配列内の項目をルックアップする必要はありません。代わりに、 を使用して 2 つの配列を 1 つに結合し、 を使用できます。ListSeqArray.zipArray.filter

一般に、パフォーマンスが必要な場合は、データ構造として配列を使用することは理にかなっています (物事の長いパイプラインがない限り)。のような関数は、配列全体のサイズArray.zipArray.map計算し、それを割り当ててから、効率的な命令型操作を実行できます (外からはまだ機能的に見えます)。

let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let newd =
        Array.zip data data2 
        |> Array.filter (fun (date, _) -> 
            let dateInRange = isbetween date std edd
            match pob with
            | "Peak" -> ispeak date && dateInRange
            | "Offpeak" -> not(ispeak date) && dateInRange
            | _ -> dateInRange)
    match sac with 
    | 0 -> newd |> Array.averageBy (fun (_, value) -> value)
    | 2 -> newd |> Array.sumBy (fun (_, value) -> 1.0)
    | _ -> newd |> Array.sumBy (fun (_, value) -> value)

私も変更しましたisbetween-それは単なる式に単純化でき、それをマークすることができますがinline、それはそれほど追加しません:

let inline isbetween r std edd = r >= std && r < edd+1.

完全を期すために、次のコードでこれをテストしました (F# Interactive を使用)。

#time 
let d1 = Array.init 1000000 float
let d2 = Array.init 1000000 float
aggrF d1 d2 0.0 1000000.0 "Test" 0

元のバージョンは約 600 ミリ秒でしたが、配列を使用した新しいバージョンは 160 ミリ秒から 200 ミリ秒かかりました。Matthew によるバージョンでは、約 520 ミリ秒かかります。

それはさておき、私は BlueMountain Capital で過去 2 か月間、F# 用の時系列/データ フレーム ライブラリに取り組んでいました。これは進行中の作業であり、ライブラリの名前も変更されますが、 BlueMountain GitHubで見つけることができます。コードは次のようになります (時系列が順序付けられているという事実を使用し、スライスを使用して、フィルタリングする前に関連部分を取得します)。

let ts = Series(times, values)
ts.[std .. edd] |> Series.filter (fun k _ -> not (ispeak k)) |> Series.mean

現在、これは直接配列操作ほど高速ではありませんが、私はそれを調べます:-)。

于 2013-10-18T15:18:06.060 に答える