我有一个功能,我使用了很多,因此性能需要尽可能好.它从excel获取数据,然后根据数据是否在一定时间内以及是否为高峰时段(Mo-Fr 8-20)对数据的部分进行求和,平均或计数.
数据通常约为30,000行和2列(每小时日期,值).数据的一个重要特征是日期列按时间顺序排列
我有三个实现,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)
Run Code Online (Sandbox Code Playgroud)
我看到两个问题:
现在我要称之为蛮力势在必行的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;
}
}
Run Code Online (Sandbox Code Playgroud)
这要快得多(我猜测主要是因为在句号结束时循环的退出),但显然不那么简洁.
我的问题:
有没有办法在f#Seq模块中停止对已排序系列的迭代?
还有另一种加速f#版本的方法吗?
谁能想到一个更好的方法呢?非常感谢!
更新:速度比较
我建立了一个测试数组,每小时的日期为1/1/13-31/12/15(大约30,000行)和相应的值.我在日期数组上进行了150次调用,并重复了100次 - 15000个函数调用:
我上面的csharp实现(在循环外使用string.compare)
1.36秒
马修斯递归fsharp
1.55秒
托马斯阵列fsharp
1m40secs
我原来的fsharp
2m20secs
显然这对我的机器来说总是主观但是给出了一个想法,人们要求它......
我也认为应该记住这并不意味着递归或for循环总是比array.map等更快,只是在这种情况下它会做很多不必要的迭代,因为它没有早期退出迭代c#和f#递归方法有
使用Array,而不是List和Seq让这快约3-4倍.您不需要生成索引列表,然后将其映射到两个数组中的查找项 - 而是可以使用Array.zip将两个数组合并为一个数组然后使用Array.filter.
一般来说,如果你想要性能,那么使用数组作为你的数据结构是有意义的(除非你有很长的事情).像功能Array.zip和Array.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)
Run Code Online (Sandbox Code Playgroud)
我也改变了isbetween- 它可以简化为一个表达式,你可以标记它inline,但这并没有增加那么多:
let inline isbetween r std edd = r >= std && r < edd+1.
Run Code Online (Sandbox Code Playgroud)
为了完整起见,我使用以下代码对其进行了测试(使用F#Interactive):
#time
let d1 = Array.init 1000000 float
let d2 = Array.init 1000000 float
aggrF d1 d2 0.0 1000000.0 "Test" 0
Run Code Online (Sandbox Code Playgroud)
原始版本约为600毫秒,使用数组的新版本需要160毫秒到200毫秒.Matthew的版本大约需要520毫秒.
另外,我花了两个月的时间在BlueMountain Capital开发了一个F#的时间序列/数据框架库,这将使这更加简单.它正在进行中,并且库的名称也会更改,但您可以在BlueMountain GitHub中找到它.代码看起来像这样(它使用时间序列是有序的事实,并使用切片在过滤之前获取相关部分):
let ts = Series(times, values)
ts.[std .. edd] |> Series.filter (fun k _ -> not (ispeak k)) |> Series.mean
Run Code Online (Sandbox Code Playgroud)
目前,这不会像直接阵列操作那么快,但我会调查:-).
加快速度的直接方法是将这些结合起来:
[0 .. (Array.length data) - 1]
|> List.map (fun i -> (data.[i], data2.[i]))
|> Seq.filter (fun (date, _) ->
Run Code Online (Sandbox Code Playgroud)
进入单个列表理解,并且如另一个matthew所说,做一个字符串比较:
let aggrF (data:float[]) (data2:float[]) std edd pob sac =
let isValidTime = match pob with
| "Peak" -> (fun x -> ispeak x)
| "Offpeak" -> (fun x -> not(ispeak x))
| _ -> (fun _ -> true)
let data = [ for i in 0 .. (Array.length data) - 1 do
let (date, value) = (data.[i], data2.[i])
if isbetween date std edd && isValidTime date then
yield (date, value)
else
() ]
match sac with
| 0 -> data |> Seq.averageBy (fun (_, value) -> value)
| 2 -> data.Length
| _ -> data |> Seq.sumBy (fun (_, value) -> value)
Run Code Online (Sandbox Code Playgroud)
或者使用尾递归函数:
let aggrF (data:float[]) (data2:float[]) std edd pob sac =
let isValidTime = match pob with
| "Peak" -> (fun x -> ispeak x)
| "Offpeak" -> (fun x -> not(ispeak x))
| _ -> (fun _ -> true)
let endDate = edd + 1.0
let rec aggr i sum count =
if i >= (Array.length data) || data.[i] >= endDate then
match sac with
| 0 -> sum / float(count)
| 2 -> float(count)
| _ -> float(sum)
else if data.[i] >= std && isValidTime data.[i] then
aggr (i + 1) (sum + data2.[i]) (count + 1)
else
aggr (i + 1) sum count
aggr 0 0.0 0
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1976 次 |
| 最近记录: |