繁体   English   中英

搜索命名元组列表的最快方法?

[英]Fastest way to search a list of named tuples?

我有一个名为元组的列表。 每个命名元组都是我创建的DataPoint类型,如下所示:

class DataPoint(NamedTuple):
    data: float
    location_zone: float
    analysis_date: datetime
    error: float

在我的代码中的各个点,我必须通过特定属性获取列表中的所有DataPoints 这是我为analysis_date做的方式,我对其他属性有类似的功能:

def get_data_points_on_date(self, data_points, analysis_date):
    data_on_date = []
    for data_point in data_points:
        if data_point.analysis_date == analysis_date:
            data_on_date.append(data_point)
    return data_on_date

在具有数千个点的列表上称为> 100,000次,因此它显着减慢了我的脚本速度。

而不是列表,我可以为一个显着的加速做一个字典,但因为我需要搜索多个属性,没有一个明显的关键。 我可能会选择占用时间最多的函数(在本例中为analysis_date ),并将其用作关键字。 但是,这会增加我的代码的复杂性。 除了哈希之外还有什么东西可以用来干扰我吗?

如果数据可以预先计算一次,那么你想要避免做过100,000次线性搜索是正确的。 为什么不使用多个词典,每个词典都由不同的感兴趣属性键入?

每个字典都会预先计算一次:

self.by_date = defaultdict(list)
for point in data_points:
    self.by_date[point.analysis_date].append(point)

现在你的get_data_points_for_date函数变成了一个单行:

def get_data_points_for_date(self, date):
    return self.by_date[date]

您可以完全删除此方法,只需使用self.by_date[date]

这不会增加代码的复杂性,但它确实可以预先转移一些簿记负担。 你可以通过一个预先计算你想要的所有字典的set_data方法来解决这个问题:

from collections import defaultdict
from operator import attrgetter

def set_data(self, data_points):
    keygetter):
        d = defaultdict(list)
        for point in data_points:
            d[key(point)].append(point)
        return d

    self.by_date = make_dict(attrgetter('analysis_date'))
    self.by_zone = make_dict(self.zone_code)

def zone_code(self, data_point):
    return int(data_point.location_zone // 0.01)

类似于zone_code东西是将float s转换为整数所必需的,因为依赖float s作为键并不是一个好主意。

也许内存中的SQLite数据库(带有列索引)可能有所帮助。 它甚至有一种方法可以将行映射到命名元组,因为python sqlite中的映射结果行到namedtuple描述。

有关更完整的解决方案,请参阅http://peter-hoffmann.com/2010/python-sqlite-namedtuple-factory.html


基于以上两个链接的基本示例:

from typing import NamedTuple
from datetime import datetime
import sqlite3


class DataPoint(NamedTuple):
    data: float
    location_zone: float
    analysis_date: datetime
    error: float


def datapoint_factory(cursor, row):
    return DataPoint(*row)


def get_data_points_on_date(cursor, analysis_date):
    cursor.execute(
        f"select * from datapoints where analysis_date = '{analysis_date}'"
    )
    return cursor.fetchall()


conn = sqlite3.connect(":memory:")
c = conn.cursor()
c.execute(
    "create table datapoints "
    "(data real, location_zone real, analysis_date text, error timestamp)"
)
c.execute(
    "create index if not exists analysis_date_index on datapoints (analysis_date)"
)


timestamp = datetime.now().isoformat()
data_points = [
    DataPoint(data=0.5, location_zone=0.1, analysis_date=timestamp, error=0.0)
]

for data_point in data_points:
    c.execute(f"insert into datapoints values {tuple(data_point)}")

conn.commit()
c.close()

conn.row_factory = datapoint_factory
c = conn.cursor()

print(get_data_points_on_date(c, timestamp))
# [DataPoint(data=0.5, location_zone=0.1, analysis_date='2019-07-19T20:37:38.309668', error=0)]
c.close()

我强烈建议使用numpy和pandas

numpypandas针对这些东西进行了优化,它们非常快。

我在下面的代码中为你做了一个简单的比较测试,看看pandas DataFrame如何在速度中占主导地位:

import pandas as pd
import numpy as np
from time import perf_counter

# init
a = np.array([0 if 500 < i < 510 else 1 for i in range(100, 1000000)])
data_points = {'data': np.arange(100, 1000000),
        'location_zone': np.arange(100, 1000000),
        'analysis_date': np.arange(100, 1000000) * a,
        'error': np.arange(100, 1000000)}

df = pd.DataFrame(data_points)

# speed of dataframe
t0 = perf_counter()
b = df[df['analysis_date'] == 0]
print("pandas DataFrame took: {:.4f} sec".format(perf_counter() - t0))
print(b)

# speed normal python code
t0 = perf_counter()
indices = [d for d in range(data_points['analysis_date'].shape[0]) if data_points['analysis_date'][d] == 0]
print("normal python code took: {:.4f} sec".format(perf_counter() - t0))
print(indices)

产量

pandas DataFrame took: 0.0049 sec
     analysis_date  data  error  location_zone
401              0   501    501            501
402              0   502    502            502
403              0   503    503            503
404              0   504    504            504
405              0   505    505            505
406              0   506    506            506
407              0   507    507            507
408              0   508    508            508
409              0   509    509            509

normal python code took: 0.2782 sec
[401, 402, 403, 404, 405, 406, 407, 408, 409]

pandas DataFrame参考: 链接

关于DataFrames的一个很好的教程: 链接

以下代码:

def get_data_points_on_date(self, data_points, analysis_date):
    data_on_date = []
    for data_point in data_points:
        if data_point.analysis_date == analysis_date:
            data_on_date.append(data_point)
    return data_on_date

可以重构为:

def get_data_points_on_date(self, data_points, analysis_date):
    return (p for p in data_points if p.analysis_date == analysis_date)

您可以在for循环中访问该返回的值,或者将其作为包含list(returned_value)

如果您有这样的DataPoints列表,可以使用pandas和MultiIndex通过O(1)查找来访问它们:

import pandas as pd

datapoints_series = pd.DataFrame(
    {
        "data": pt.data,
        "location_zone": pt.location_zone,
        "analysis_date": pt.analysis_date,
        "error": pt.error,
        "data_point": pt
    }
    for pt in data_points_list
).set_index([
    "data",
    "location_zone",
    "analysis_date",
    "error"
]).squeeze() # send to Series

要访问特定日期:

def date_accessor(date):
    idx = pd.IndexSlice[:, :, date, :]

date = "2019-07-01"
datapoints_series.loc[date_accessor(date)]

如果您想再次在列表中使用数据点,则可以简单地将.tolist()方法调用追加到该最后一行。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM