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

时间:2019-07-19 17:27:48

标签: python optimization

我有一个命名元组列表。每个命名的元组都是我创建的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),并将其用作键。但是,这将大大增加我的代码的复杂性。除了散列之外/还有逃避我的聪明的散列方法?

5 个答案:

答案 0 :(得分:1)

您是对的,如果可以将数据预先计算一次,则可以避免进行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之类的东西,因为依靠float作为键不是一个好主意。

答案 1 :(得分:1)

也许内存中的SQLite数据库(带有列索引)可能会有所帮助。它甚至可以按照Mapping result rows to namedtuple in python sqlite的描述将行映射到命名元组。

有关更完整的解决方案,请参见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()

答案 2 :(得分:1)

我强烈建议您使用numpy和pandas

numpy 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参考:Link

关于DataFrames的好教程:Link

答案 3 :(得分:0)

以下代码:

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)使其成为列表。

答案 4 :(得分:0)

如果有此类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()方法调用即可。