Python - подсчитайте частоту сообщений в диапазоне дат в пределах динамического интервала.

Подсчитайте количество сообщений в диапазоне дат за интервал. Я использую только Python 2.6.5.

Например Дата начала: 11.12.2014 Дата окончания: 12.12.2014 Время начала: 02:00 Время окончания: 02:05 Интервал: За 1 мин.

Таким образом, это означает, сколько сообщений находится между каждым минутным интервалом с даты начала 11 декабря до даты окончания 12 декабря. Итак, мой вывод будет выглядеть так: (не обязательно иметь строки min и messages)

datetime(2014, 12, 11, 2, 0) min : 0 messages,
datetime(2014, 12, 11, 2, 1) min: 1 message,
datetime(2014, 12, 11, 2, 2) min: 2 messages, 
datetime(2014, 12, 11, 2, 3) min: 1 message,
datetime(2014, 12, 11, 2, 4) min : 0 messages,
datetime(2014, 12, 11, 2, 5) min : 0 messages

Я считаю, что я достиг этого, но это очень медленно с большими наборами данных. Я думаю, потому что он использует два цикла, и если второй цикл очень большой, то это занимает очень много времени и делает это для каждой итерации первого цикла. Мне нужна лучшая процедура или alrogithm, чтобы выполнить это?

Изменить: мне нужно включить ноль для интервалов, в которых нет сообщений. Я также пытаюсь найти пик, минимум и среднее значение.

from datetime import date,datetime, timedelta, time

def perdelta(start, end, delta):
    curr = start
    while curr < end:
        yield curr
        curr += delta


def rdata(table, fromDate, toDate, fromTime, toTime, interval): 
    date_to_alert = {}
    start_date = datetime(fromDate.year, fromDate.month, fromDate.day, fromTime.hour, fromTime.minute)
    end_date = datetime(toDate.year, toDate.month, toDate.day, toTime.hour, toTime.minute)

    list_range_of_dates = []
    for date_range in perdelta(start_date ,end_date ,interval):
        list_range_of_dates.append(date_range)
    print list_range_of_dates
    index = 0
    for date_range in list_range_of_dates:
        for row in table:    

            print('first_alerted_time 1: %s index: %s len: %s' % ( row['first_alerted_time'], index, len(list_range_of_dates)-1))          
            if row['first_alerted_time'] and row['first_alerted_time'] >= list_range_of_dates[index] and row['first_alerted_time'] < list_range_of_dates[index + 1]:
                print('Start date: %s' % list_range_of_dates[index] )
                print('first_alerted_time: %s' % row['first_alerted_time'])
                print('end date: %s' % list_range_of_dates[index + 1])
                if list_range_of_dates[index] in date_to_alert:
                    date_to_alert[list_range_of_dates[index]].append(row)                                     
                else:
                    date_to_alert[list_range_of_dates[index]] = [row]                       

            elif row['first_alerted_time']:
                print('first_alerted_time 2: %s' % row['first_alerted_time'])        
        index = index + 1  

        print   date_to_alert    
for key, value in date_to_alert.items():
    date_to_alert[key] = len(value)
print   date_to_alert
t1 = []
if date_to_alert:
    avg = sum(date_to_alert.values())/len(date_to_alert.keys())
    for date_period, num_of_alerts in date_to_alert.items():
        #[date_period] = date_to_alert[date_period]
        t1.append( [ date_period, num_of_alerts, avg] )
print t1
return t1

def main():
    example_table = [ 
                {'first_alerted_time':datetime(2014, 12, 11, 2, 1,45)},
                {'first_alerted_time':datetime(2014, 12, 11, 2, 2,33)},
                {'first_alerted_time':datetime(2014, 12, 11, 2, 2,45)},
                {'first_alerted_time':datetime(2014, 12, 11, 2, 3,45)},
                ]
    example_table.sort()     
    print example_table
    print rdata(example_table, date(2014,12,11), date(2014,12,12), time(00,00,00), time(00,00,00), timedelta(minutes=1)) 

Обновление: первая попытка улучшения:

Словарь по умолчанию

def default_dict_approach(table, fromDate, toDate, fromTime, toTime, interval):
    from collections import defaultdict

    t1 = []
    start_date = datetime.combine(fromDate, fromTime)
    end_date = datetime.combine(toDate, toTime)+ interval


    times = (d['first_alerted_time'] for d in table)
    counter = defaultdict(int)
    for dt in times:
        if start_date <= dt < end_date:
            counter[to_s(dt - start_date) // to_s(interval)] += 1

    date_to_alert = {}
    date_to_alert = dict((ts*interval + start_date, count) for ts, count in counter.iteritems())

    max_num,min_num,avg = 0,0,0
    list_of_dates = list(perdelta(start_date,end_date,interval))
    if date_to_alert:
        freq_values = date_to_alert.values()
        size_freq_values = len(freq_values)
        avg = sum(freq_values)/ size_freq_values
        max_num = max(freq_values)
        if size_freq_values == len(list_of_dates):
            min_num = min(freq_values)
        else:
            min_num = 0
        for date_period in list_of_dates:
            if date_period in date_to_alert:
                t1.append([ date_period.strftime("%Y-%m-%d %H:%M"), date_to_alert[date_period], avg, max_num, min_num])
            else:
                t1.append([ date_period.strftime("%Y-%m-%d %H:%M"), 0, avg, max_num, min_num])

    return (t1,max_num,min_num,avg)

пустой подход

def numpy_approach(table, fromDate, toDate, fromTime, toTime, interval):
    date_to_alert = {}
    start_date = datetime.combine(fromDate, fromTime)
    end_date = datetime.combine(toDate, toTime)+ interval

    list_range_of_dates = []
    for date_range in perdelta(start_date ,end_date ,interval):
        list_range_of_dates.append(date_range)
    #print list_range_of_dates

    index = 0
    times = np.fromiter((d['first_alerted_time'] for d in table),
                     dtype='datetime64[us]', count=len(table))

    print times
    bins = np.fromiter(list_range_of_dates,
                       dtype=times.dtype)                
    print bin                 
    a, bins = np.histogram(times, bins)  
    print(dict(zip(bins[a.nonzero()].tolist(), a[a.nonzero()])))

person user3590149    schedule 24.12.2014    source источник


Ответы (3)


Вы хотите реализовать numpy.histogram() для дат:

import numpy as np

times = np.fromiter((d['first_alerted_time'] for d in example_table),
                     dtype='datetime64[us]', count=len(example_table))
bins = np.fromiter(date_range(start_date, end_date + step, step),
                   dtype=times.dtype)
a, bins = np.histogram(times, bins)
print(dict(zip(bins[a.nonzero()].tolist(), a[a.nonzero()])))

Вывод

{datetime.datetime(2014, 12, 11, 2, 0): 3,
 datetime.datetime(2014, 12, 11, 2, 3): 1}

numpy.historgram() работает, даже если шаг непостоянен и массив times не отсортирован. В противном случае вызов можно оптимизировать, если вы решите использовать numpy.

Есть два общих подхода, которые вы можете использовать в Python 2.6 для реализации numpy.historgram:

  • На основе itertools.groupby: ввод должен быть отсортирован, но это позволяет реализовать однопроходный алгоритм с постоянной памятью.
  • collections.defaultdict на основе: входные данные могут быть несортированными, и это также линейный алгоритм, но он O(number_of_nonempty_bins) в памяти

Решение на основе groupby():

from itertools import groupby

times = (d['first_alerted_time'] for d in example_table)
bins = date_range(start_date, end_date + step, step)
def key(dt, end=[next(bins)]):
    while end[0] <= dt:
        end[0] = next(bins)
    return end[0]
print dict((end-step, sum(1 for _ in g)) for end, g in groupby(times, key=key))

Он дает тот же результат, что и подход на основе histogram().

Примечание: все даты меньше start_date помещаются в первую ячейку.

решение на основе defaultdict()

from collections import defaultdict

def to_s(td): # for Python 2.6
    return td.days*86400 + td.seconds #NOTE: ignore microseconds

times = (d['first_alerted_time'] for d in example_table)
counter = defaultdict(int)
for dt in times:
    if start_date <= dt < end_date:
        counter[to_s(dt - start_date) // to_s(step)] += 1

print dict((ts*step + start_date, count) for ts, count in counter.iteritems())

Результат такой же, как и у двух других решений.

person jfs    schedule 24.12.2014
comment
Спасибо за отличные ответы. Мне удалось реализовать решение defaultdict. Это работает лучше, чем мой оригинальный подход. У меня есть доступ к библиотеке numpy. Я попытался реализовать подход numpy, но продолжаю получать эту ошибку, также см. Код выше для этого. TypeError: ufunc 'less' не поддерживается для входных типов, и входы не могут быть безопасно приведены к каким-либо поддерживаемым типам в соответствии с правилом приведения "safe" - person user3590149; 31.12.2014
comment
@user3590149: нажмите на ссылку в ответе: он содержит полный пример кода, показывает, как использовать numpy.histogram в вашем случае. В коде следует избегать подхода «кухонной раковины»: 1. написать простую функцию, которая хорошо выполняет одну работу 2. протестировать ее 3. использовать ее для решения более сложной задачи 4. повторять до тех пор, пока программа делает то, что вам нужно. -- на каждом шагу вам нужно беспокоиться только об одном. Одним из последствий является то, что вы не должны не смешивать вычисления (получение результата) и ввод-вывод (print result) — это упрощает написание тестов. - person jfs; 02.01.2015

Я бы сделал это одним проходом на table, создав dict. Если таблица должна быть отсортирована по полю first_alerted_time, то можно найти нижнюю и верхнюю границы прохода с помощью модуля bisect, тогда даже проверки по start_date и end_date не нужны, просто что-то вроде:

counts = collections.Counter()
counts.update(datrow['first_alerted_time'].date() 
              for row in table[start_index:end_index])

или очевидные варианты (например, с itertools), если срез table занимает слишком много памяти.

Если нельзя предположить, что table отсортировано, подход не сильно отличается, вам просто нужно добавить предложение if и выполнить цикл по всей таблице:

counts = collections.Counter()
counts.update(row['first_alerted_time'].date() 
              for row in table[start_index:end_index]
              if start_date < row['first_alerted_time'] < end_date)

Если вы можете пояснить, что можно предположить о table, то (если применимо, и у вас есть проблемы с этим) я могу показать, как использовать bisect для поиска границ в отсортированной таблице.

Итак, это дает вам Counter с подсчетами по дате. Если вам нужен список, отсортированный по дате (например, пар (дата, количество)), sorted(counts.items()) даст вам это и т. д. Опять же, если вы можете уточнить, какие именно результаты являются приемлемыми, и у вас есть проблемы с их получением, я могу показать, как это сделать.

Если минуты также важны (как указано в тексте вопроса, но не показано в примере кода, который считается работающим), просто замените row['first_alerted_time'].date() в любом из приведенных выше фрагментов на row['first_alerted_time'].replace(second=0,microsecond=0). Опять же, полученный счетчик можно отсортировать и т. д.

person Alex Martelli    schedule 24.12.2014
comment
Я обновил свой ответ. Это ясно по результатам. Итак, я хочу подсчитать частоту сообщений между каждым интервалом даты начала и окончания. - person user3590149; 24.12.2014
comment
Кажется, что в вашем примере кода в качестве индекса используются только даты, а не даты + минуты, поэтому я использовал date выше. Легко исправить, позвольте мне отредактировать ответ. - person Alex Martelli; 24.12.2014
comment
Я просто пропустил API для счетчика в коллекциях. Я использую 2.6.5. Есть предложения по альтернативе? - person user3590149; 24.12.2014
comment
Используйте counts = collections.defaultdict(int) и цикл for, например for x in (то же самое, что и в вызовах update выше) : counts[x] += 1 - person Alex Martelli; 24.12.2014

У вас так много print в цикле. Я не уверен, что это просто отладочная информация, или вы действительно хотите их распечатать? Я просто предполагаю, что это отладочная информация.

Во-первых, вам нужно не два цикла, а один. Первый вообще бесполезен. Поскольку ваш интервал равен 1 minute, вы можете изменить

date_to_alert[list_range_of_dates[index]]...

TO

t = datetime(row['first_alerted_time'].year, row['first_alerted_time'].month, .... row.['first_alerted_time'].minute]
if t in data_to_alert:
    ......

Так можно избавиться от первой петли.

== РЕДАКТИРОВАТЬ ==

Если интервал динамический, вы все равно можете рассчитать время.

def calc(alert_time, interval):
    t =  datetime(row['first_alerted_time'].year, row['first_alerted_time'].month, .... row.['first_alerted_time'].minute]
    from_t = datetime(fromDate.year, fromDate.month, fromDate.day, fromTime.hour, fromTime.minute)
    return (t - from_t) / interval * interval + from_t

Например, пусть from_time равно 10, а interval равно 5, ваше ведро будет 10, 15, 20, 25, .....

потом

  • 17 должно вернуться (17-10) / 5 * 5 + 10 = 15
  • 25 должно вернуться (25-10) / 5 * 5 + 10 = 25

И чтобы вызвать эту функцию:

t = calc_t(row['first_alerted_time'], interval)
if t in data_to_alert:
    ....

==РЕДАКТИРОВАТЬ КОНЕЦ==

Более того, если ваша таблица отсортирована, вы можете выполнять двоичный поиск в начальной/конечной строке таблицы и подсчитывать только строки между ними, что делает ваш код еще быстрее.

== РЕДАКТИРОВАТЬ ==

Например

1: 2015-01-01-10:00:00
2: 2015-01-01-11:00:00
3: 2015-01-01-12:00:00
4: 2015-01-02-11:13:00
5: 2015-01-02-12:45:00

Ваше время от 2015-01-01-10:30:00 и время окончания 2015-01-02:11:30:00. Итак, выполните бинарный поиск этих двух временных меток в таблице, вам нужно работать только со строками 2, 3 и 4, но игнорировать первую и последнюю строки. Это увеличит скорость вашей программы, если вы заботитесь только о нескольких строках в очень большой таблице. Однако это работает только в том случае, если ваша таблица отсортирована, поэтому вы можете выполнять двоичный поиск.

==РЕДАКТИРОВАТЬ КОНЕЦ==

person owenwater    schedule 24.12.2014
comment
Интервал динамический. Я использую пример 1 минуты, но он может быть 2 минуты, 2 дня или 3 часа. Я не уверен, что вы подразумеваете под подсчетом строк между ними, вы имеете в виду подсчет строк между двумя индексами? - person user3590149; 24.12.2014
comment
Индекс по-прежнему можно рассчитать, даже если интервал динамический, но это немного сложно. Я обновлю ответ на ваш вопрос. - person owenwater; 25.12.2014