在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?

Posted

技术标签:

【中文标题】在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?【英文标题】:Is there a faster method to filter a DateTime based Dictionary in C#?在 C# 中是否有更快的方法来过滤基于 DateTime 的字典? 【发布时间】:2021-03-07 07:03:27 【问题描述】:

非常简单,我将 TimeSeries 存储为带有 IDictionary<Datetime, double?> 签名的 DateTime 字典。这将包含一个月的 10 分钟分辨率数据,因此最多 4,464 个条目。

为了处理这个时间序列的部分,我们需要在 startend DateTime 之间提取一个部分。

这样做的一种天真的方法是获取我们感兴趣的范围的字典键的子集:

var reducedKeys = timeSeries.Keys.Where(k => k >= start && k <= end).ToList();

然后从大timeSeries中提取相关部分

var reducedTimeSeries = timeSeries.Where(kvp => reducedKeys .Contains(kvp.Key)).ToDictionary(w => w.Key, w => w.Value);

感觉这不是最佳解决方案;对更快的提取策略有什么建议吗?

为了清楚起见,时间戳的顺序在此阶段基本上是无关紧要的,因为更高级别的计算发生在多个时间序列中,而不是在同一个序列中。提取后有一个扁平线过滤器要运行;但这可以通过迭代时间序列提取中的键的排序副本来运行,因为在从较长的源序列中提取后,我们通常会有 12-24 个样本序列。

【问题讨论】:

“更好”和“最佳”都是广义词,包含许多不同的因素。请告诉我们您最感兴趣的指标。“所有指标”是不可接受的:)。另请阅读Eric Lippert's blog post on performance。 感谢@HereticMonkey,这是一本好书 :-) 在这种情况下,我们正在考虑的绝对是速度,因为该操作是在一个不适合基于设置的处理的大型循环操作中。 如果您不介意向您的应用添加第三方依赖项,您可以考虑使用C5 库。它有一个 TreeDictionary 集合,其中包含方法 RangeFromTo,该方法有效地返回集合内的一系列键。 如果偏移量总是来自同一个日期,那么你可以只使用一个带有开始日期的数组,并且每个后续元素都是10分钟的偏移量 【参考方案1】:

Dictionary&lt;TKey, TValue&gt; 根据TKey 值的哈希值将其条目存储在存储桶中。简而言之,您的时间序列条目未按顺序存储。这使得使用日期时间范围非常低效,因为您需要枚举所有项目以获取适当的项目。

您可以考虑使用SortedDictionary&lt;TKey, TValue&gt;,因为它结合了TKey 值的散列和排序。请参阅此回复:https://***.com/a/9053294/1323798。

如果您关心性能,我个人会寻找专门设计用于时间序列数据的数据结构。但与往常一样,这完全取决于您打算如何处理时间序列。

【讨论】:

我一直对使用 SortedDictionary 有点谨慎,但在这种情况下,它可能是一个不错的选择,因为我们在从数据库中提取数据时构建数据集,然后广泛使用数据。值得一试:-)【参考方案2】:

不知道这是否是最佳解决方案,但肯定更短且更容易理解

var reducedTimeSeries = timeSeries.Where(x => x.Key >= start && x.Key <= end).Select(x => x)

【讨论】:

您可以删除Select() 调用。【参考方案3】:

如果 SortedDictionary 不适合您,那么您可以创建自己的自定义解决方案。

例如,如果偏移量是恒定的,您可以将数据存储在数组中并按偏移量进行搜索。

就我而言,自定义解决方案的速度提高了 3 倍

//(1000 iterations)
// DateTimeArray:00:00:00.1629569
// SortedDictionary:00:00:00.5831970

public sealed class DateTimeArray<TValue>

    private readonly DateTime _startDate;
    private readonly int _step;
    private readonly int _size;
    private TValue[] _values;
    public DateTimeArray(DateTime startDate, int step = 10, int size = 4464)
    
        if (step <= 0)
            throw new InvalidDataException("step can not be less than 1");
        _startDate = startDate;
        _step = step;
        _size = size;
        _values = new TValue[size];
    

    public DateTimeArray(DateTime startDate, DateTime endDate, int step = 10)
        : this(startDate, step, (int) ((endDate - startDate).TotalMinutes / step))
    
    

    public void Add(DateTime date, TValue value)
    
        var offset = (int)((date - _startDate).TotalMinutes);
        var current = offset / _step;
        if (_size <= current)
            throw new IndexOutOfRangeException($"current>=_size");
        _values[current] = value;
    

    public TValue[] Between(DateTime from, DateTime? to = null)
    
        var offsetFrom = (int)((from - _startDate).TotalMinutes) / _step;
        var offsetTo = _size;
        if (to.HasValue)
        
            offsetTo = (int) ((to.Value- _startDate).TotalMinutes) / _step;
        

        if (offsetFrom >= offsetTo)
            throw new IndexOutOfRangeException($"offsetFrom>=offsetTo");
        return _values.Skip(offsetFrom).Take(offsetTo - offsetFrom ).ToArray();
    

【讨论】:

【参考方案4】:

Dictionary&lt;DateTime, double?&gt; 实现接口IEnumerable&lt;KeyValuePair&lt;DateTime, double?&gt;&gt;。因此,如果您希望字典中的所有值都介于两个 DateTimes 之间,请使用此界面选择要放入新字典中的键值对。

DateTime startTime = ...
Datetime endTime = ...
Dictionary<DateTime, double?> dictionary = ...

var itemsToPutInNewDictionary = dictionary
    .Where(keyValuePair => startTime <= keyValuePair.Key
                                     && endTime >= keyValuePair.Key);
// note: the query is not executed yet!
var limitedDictionary = new Dictionary<DateTime, double?>(itemsToPutInNewDictionary);

或者您可以在 LINQ 末尾使用 ToDictionary:

.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value);

您必须查看字典是如何构建的,但我认为后一种解决方案较慢。

【讨论】:

【参考方案5】:

您可以将Dictionary 的内容复制到一个排序数组中,然后使用Array.BinarySearch 方法以O(log n) 的效率在数组中搜索日期。

下面是一个扩展方法RangeFromTo 用于字典的排序副本。它返回两个键之间的内容范围:

public static IEnumerable<KeyValuePair<TKey, TValue>> RangeFromTo<TKey, TValue>(
    this KeyValuePair<TKey, TValue>[] sortedArray,
    TKey from, TKey to)

    var keyComparer = Comparer<TKey>.Default;
    var pairComparer = Comparer<KeyValuePair<TKey, TValue>>.Create(
        (x, y) => keyComparer.Compare(x.Key, y.Key));

    int fromIndex = Array.BinarySearch(sortedArray,
        new KeyValuePair<TKey, TValue>(from, default), pairComparer);
    int toIndex = Array.BinarySearch(sortedArray,
        new KeyValuePair<TKey, TValue>(to, default), pairComparer);

    if (fromIndex < 0) fromIndex = ~fromIndex;
    if (toIndex < 0) toIndex = Math.Min(~toIndex, sortedArray.Length - 1);

    for (int i = fromIndex; i <= toIndex; i++)
    
        yield return sortedArray[i];
    

使用示例:

var dictionary = new Dictionary<DateTime, double>();
/* Code that fills the dictionary omitted */

var sortedArray = dictionary.OrderBy(e => e.Key).ToArray();

var selection = sortedArray.RangeFromTo(
    new DateTime(2020, 1, 1), new DateTime(2020, 12, 31));

foreach (var pair in selection)

    DateTime date = pair.Key;
    double value = pair.Value;
    Console.WriteLine($"date:yyyy/MM/dd => value");

有一个问题:每次更新字典时都必须重新创建排序数组。这两者必须始终保持同步。


警告:您应该对每个副本进行足够的搜索,以证明其成本合理。理想情况下,您应该能够使用单个副本进行多对多搜索。在极其不利的情况下,您使用每个副本进行单次搜索,其性能将明显比简单的线性搜索差。

【讨论】:

排序数据是 O(nlog(n))。进行线性搜索是 O(n)。你刚刚让它的表现明显*差,而不是更好。 @Servy 是的,如果每次搜索都必须对字典进行排序,那肯定会更糟。希望 OP 有一本不经常更新但需要经常搜索的字典。

以上是关于在 C# 中是不是有更快的方法来过滤基于 DateTime 的字典?的主要内容,如果未能解决你的问题,请参考以下文章

C#以int数组为索引过滤for循环的更快方法?

在 SAS 中,如果空变量不存在,是不是有更快的方法来创建它?

在 C# 中复制数组的任何更快的方法?

基于时间的过滤器然后得到 Min(date) SQL

是否有更快的方法来获取基于线性回归模型的值并将其附加到 DataFrame 中的新列?

使用存储过程从视图中检索或过滤数据是不是比使用存储过程从表中获取或过滤数据更快?