Python:多对多比较以找到所需的数据集

Posted

技术标签:

【中文标题】Python:多对多比较以找到所需的数据集【英文标题】:Python: many-to-many comparison to find required set of data 【发布时间】:2016-06-24 18:21:43 【问题描述】:

这是我的第一个问题,请原谅任何错误。

我有一个大文件 (csv),其中包含几行 (~10000000+) 行信息,如下例所示:

date;box_id;box_length;box_width;box_height;weight;type
--snip--
1999-01-01 00:00:20;nx1124;10;4;5.5;2.1;oversea
1999-01-01 00:00:20;np11r4;8;3.25;2;4.666;local
--snip--

我的目标是阅读每一行并计算盒子的体积,并在 1 小时窗口内(例如,00:00:00 - 00:00:59)我必须记录 2 个或更多盒子的体积是否相似(+-10% 差异),然后记录他们的时间戳和类型。

目前,我正在使用蛮力方法:

贯穿每一行 计算体积 转到下一行并计算音量 比较 重复直到检测到 1 小时的时差 从列表中删除第一个框 将另一个框添加到列表中 用第二个框重复该过程

例如,如果我的 1 小时窗口有 1、2、3、4,我正在这样做

1
2 == 1
3 == 1 then == 2
4 == 1 then == 2 then == 3
5 == 2 then == 3 then == 4 # removed 1 from list(1hr window moved down)
6 == 2 then == 3 then == 4 then == 5
7 == 2 then == 3 then == 4 then == 5 then == 6
.... so on ....

这是我能想到的最好的方法,因为我必须在给定的时间窗口内将每个盒子与其他盒子进行比较。但目前这非常非常慢。

我正在寻找更好的算法,但我不确定我必须朝哪个方向发展。 我正在尝试学习一些优秀的工具(到目前为止,Pandas 是我最喜欢的),但我假设我需要首先实现一些算法,以允许这些工具以我需要的方式处理数据。

如果有帮助,我会发布我的 python 代码(源代码)。

更新以下是我的代码。我省略了几行(例如用于无效文件路径/格式的 try/catch 块、类型转换错误处理等)。我已经对代码进行了一些定制,使其适用于 5 秒窗口。

下面是Box类

from datetime import datetime
from time import mktime

class Box(object):
    """ Box model """

    def __init__(self,data_set):
        self.date = data_set[0]
        self.timestamp = self.__get_time()
        self.id = data_set[1]
        self.length = float(data_set[2])
        self.width = float(data_set[3])
        self.height = float(data_set[4])
        self.weight = int(data_set[5])
        self.volume = self.__get_volume()

    def __get_time(self):
        """ convert from date string to unix-timestamp """
        str_format = '%Y-%m-%d %H:%M:%S'
        t_tuple = datetime.strptime(self.date, str_format).timetuple()
        return mktime(t_tuple)

    def __get_volume(self):
        """ calculate volume of the box """
        return (self.length * self.width * self.height)

以下是执行比较的实际程序。为方便起见,我将实用程序文件和 main.py 文件组合在一起。

from csv import reader
from io import open as open_file
from os import path
from sys import argv, exit
from time import time

# custom lib
from Box import Box

def main():

    file_name = str.strip(argv[1])
    boxes_5s = []
    diff = 0

    similar_boxes = []

    for row in get_file(file_name):
        if row:
            box = Box(row)

            if len(boxes_5s) > 0:
                diff = box.timestamp - boxes_5s[0].timestamp
                if diff < 6:
                    boxes_5s.append(box)
                else:
                    similar_boxes += get_similar(boxes_5s)
                    del boxes_5s[0] # remove the oldest box
                    boxes_5s.append(box)
            else:
                boxes_5s.append(box)

        print(similar_boxes)


def get_file(file_name):
    """ open and return csv file pointer line by line """
    with open_file(file_name,'rb') as f:
        header = f.readline()
        print(header)
        rows = reader(f, delimiter=';')

        for r in rows:
            yield r
        else:
            yield ''


def get_similar(box_list):
    """ compare boxes for similar volume """    

    num_boxes = len(box_list)

    similar_boxes = []
    record_str = "Box# Volm: and # Volm:"
    for i in xrange(num_boxes):
        box_1 = box_list[i]

        for j in xrange(i+1, num_boxes):
            box_2 = box_list[j]

            vol_diff = abs((box_1.volume - box_2.volume)/box_1.volume) <= 0.1


            if vol_diff: similar_boxes.append(record_str.format(box_1.id,box_1.volume,box_2.id, box_2.volume))

    return similar_boxes

if __name__ == "__main__":
    main()

谢谢。

【问题讨论】:

您可能应该先订购它们,然后再找到类似的盒子 它有帮助。见How to create a Minimal, Complete, and Verifiable example 我的问题含糊不清,我很抱歉,等我回家后会发布一些代码 谢谢。我认为这是一个好主意,我的理解是,你的意思是我应该先计算体积,然后对它们进行排序,最后挑选出相似的值。我会试试的。 @armonge “1 小时内”是指当前盒子时间戳后的小时,还是当前盒子时间戳内的小时?看起来你的蛮力版本正在使用第一种情况。 【参考方案1】:

将第一个时间戳作为一个小时窗口的开始(而不是时钟小时箱总是盯着小时:00:00)我认为对于小到几千万行数据的数据量来说,一个非常可行的实现可能是(预计文件中的时间排序条目):

#! /usr/bin/env python
from __future__ import print_function

import csv
import datetime as dt
import math
import collections


FILE_PATH_IN = './box_data_time_ordered_100k_sparse.csv'
TS_FORMAT = '%Y-%m-%d %H:%M:%S'
TS_TOKEN = 'date'
SIMILAR_ENOUGH = 0.1
BoxEntry = collections.namedtuple(
    'BoxEntry', ['start_ts', 'a_ts', 't_type', 'b_volume'])


def box_volume(box_length, box_width, box_height):
    """Volume in cubic of length units given."""
    return box_length * box_width * box_height


def filter_similar_box_volumes(box_entries):
    """Ordered binary similarity comparator using complex algorithm
    on a medium large slice of data."""

    def _key(r):
        """sort on volume."""
        return r.b_volume

    entries_volume_ordered = sorted(box_entries, key=_key)
    collector = []
    for n, box_entry in enumerate(entries_volume_ordered[1:], start=1):
        one = box_entry.b_volume
        prev_box_entry = entries_volume_ordered[n]
        previous = prev_box_entry.b_volume
        if one and math.fabs(one - previous) / one < SIMILAR_ENOUGH:
            if box_entry not in collector:
                collector.append(box_entry)
            if prev_box_entry not in collector:
                collector.append(prev_box_entry)
    return collector


def hourly_boxes_gen(file_path):
    """Simplistic generator, yielding hour slices of parsed
    box data lines belonging to 1 hour window per yield."""

    csv.register_dialect('boxes', delimiter=';', quoting=csv.QUOTE_NONE)
    start_ts = None
    cx_map = None
    hour_data = []
    an_hour = dt.timedelta(hours=1)
    with open(file_path, 'rt') as f_i:
        for row in csv.reader(f_i, 'boxes'):
            if cx_map is None and row and row[0] == TS_TOKEN:
                cx_map = dict(zip(row, range(len(row))))
                continue
            if cx_map and row:
                a_ts = dt.datetime.strptime(row[cx_map[TS_TOKEN]], TS_FORMAT)
                t_type = row[cx_map['type']]
                b_length = float(row[cx_map['box_length']])
                b_width = float(row[cx_map['box_width']])
                b_height = float(row[cx_map['box_height']])
                b_volume = box_volume(b_length, b_width, b_height)
                if start_ts is None:
                    start_ts = a_ts
                    hour_data.append(
                        BoxEntry(start_ts, a_ts, t_type, b_volume))
                elif a_ts - an_hour < start_ts:
                    hour_data.append(
                        BoxEntry(start_ts, a_ts, t_type, b_volume))
                else:
                    yield filter_similar_box_volumes(hour_data)
                    hour_data = [BoxEntry(start_ts, a_ts, t_type, b_volume)]
                    start_ts = a_ts
        if hour_data:
            yield filter_similar_box_volumes(hour_data)


def main():
    """Do the thing."""
    for box_entries in hourly_boxes_gen(FILE_PATH_IN):
        for box_entry in box_entries:
            print(box_entry.start_ts, box_entry.a_ts, box_entry.t_type)

if __name__ == '__main__':
    main()

带有示例输入文件:

date;box_id;box_length;box_width;box_height;weight;type
1999-01-01 00:00:20;nx1124;10;4;5.5;2.1;oversea
1999-01-01 00:00:20;np11r4;8;3.25;2;4.666;local
1999-01-01 00:10:20;np11r3;8;3.25;2.1;4.665;local
1999-01-01 00:20:20;np11r2;8;3.25;2.05;4.664;local
1999-01-01 00:30:20;np11r1;8;3.23;2;4.663;local
1999-01-01 00:40:20;np11r0;8;3.22;2;4.662;local
1999-01-01 00:50:20;dp11r4;8;3.24;2;4.661;local
1999-01-01 01:00:20;cp11r3;8;3.25;2;4.666;local
1999-01-01 01:01:20;bp11r2;8;3.26;2;4.665;local
1999-01-01 01:02:20;ap11r1;8;3.22;2;4.664;local
1999-01-01 01:03:20;zp11r0;12;3.23;2;4.663;local
1999-01-01 02:00:20;yp11r4;8;3.24;2;4.662;local
1999-01-01 04:00:20;xp11r4;8;3.25;2;4.661;local
1999-01-01 04:00:21;yy11r4;8;3.25;2;4.661;local
1999-01-01 04:00:22;xx11r4;8;3.25;2;4.661;oversea
1999-01-01 04:59:19;zz11r4;8;3.25;2;4.661;local

产量:

1999-01-01 00:00:20 1999-01-01 00:30:20 local
1999-01-01 00:00:20 1999-01-01 00:50:20 local
1999-01-01 00:00:20 1999-01-01 00:00:20 local
1999-01-01 00:00:20 1999-01-01 00:20:20 local
1999-01-01 00:00:20 1999-01-01 00:10:20 local
1999-01-01 00:00:20 1999-01-01 00:00:20 oversea
1999-01-01 00:00:20 1999-01-01 01:00:20 local
1999-01-01 01:00:20 1999-01-01 01:01:20 local
1999-01-01 01:00:20 1999-01-01 01:03:20 local
1999-01-01 04:00:20 1999-01-01 04:00:21 local
1999-01-01 04:00:20 1999-01-01 04:00:22 oversea
1999-01-01 04:00:20 1999-01-01 04:59:19 local

一些注意事项:

    用于读取的csv模块,带有特定的方言(因为分号不是默认分隔符)

    使用别名导入日期时间,以便在不覆盖模块名称的情况下访问 strptime 方法的日期时间类 - YMMV

    将分块的小时窗口阅读器封装在生成器函数中

    在不同的功能中计算体积和相似度。

    体积排序的简单过滤算法应该是 O(m),因为 m 是候选匹配的数量。

    使用命名元组进行紧凑存储,但也可以进行有意义的寻址。

    要实现时钟调整的 1 小时窗口(不使用第一个时间戳来引导),需要稍微调整代码,但应该是微不足道的

否则好奇地等待来自 OP 的代码示例 ;-)

更新了足够相似的过滤算法,以便事件丰富的时间,不要让 O(n^2) 算法占用我们所有的时间......(删除了嵌套循环的 _naive )。

对于这大约 100k 行 (86400+),每秒将一天的条目添加到具有 3600 个候选者的样本中进行相似性检查大约需要 10 秒。

【讨论】:

谢谢!是的,这很有帮助。我正在沿着这条线使用一些东西,但目前它非常粗糙。我明天将尝试这个解决方案。 :)

以上是关于Python:多对多比较以找到所需的数据集的主要内容,如果未能解决你的问题,请参考以下文章

Django 包含多对多字段

Python学习第135天(Django的ORM多对多查询)

Flask-msearch python ,在多对多表上添加 msearch

django之间的区别 - 一对一,多对一,多对多

如何找到共享多对多关系的模型类型的所有对象?

Hibernate学习笔记 --- 创建基于中间关联表的多对多映射关系