Сбор данных первым в Python для проведения операций

Недавно у меня был тест. Мне дали следующую проблему, где я должен был соответствовать logdata и expected_result, Код выглядит следующим образом, отредактированный с помощью моего решения:

import collections


log_data = """1.1.2014 12:01,111-222-333,454-333-222,COMPLETED
1.1.2014 13:01,111-222-333,111-333,FAILED
1.1.2014 13:04,111-222-333,454-333-222,FAILED
1.1.2014 13:05,111-222-333,454-333-222,COMPLETED
2.1.2014 13:01,111-333,111-222-333,FAILED
"""

expected_result = {
    "111-222-333": "40.00%",
    "454-333-222": "66.67%",
    "111-333" : "0.00%"
}

def compute_success_ratio(logdata):
    #! better option to use .splitlines()
    #! or even better recognize the CSV structure and use csv.reader
    entries = logdata.split('\n')
    #! interesting choice to collect the data first
    #! which could result in explosive growth of memory hunger, are there
    #! alternatives to this structure?
    complst = []
    faillst = []
    #! probably no need for attaching `lst` to the variable name, no? 

    for entry in entries:
        #! variable naming could be clearer here
        #! a good way might involve destructuring the entry like:
        #! _, caller, callee, result
        #! which also avoids using magic indices further down (-1, 1, 2)
        ent = entry.split(',')
        if ent[-1] == 'COMPLETED':
            #! complst.extend(ent[1:3]) for even more brevity
            complst.append(ent[1])
            complst.append(ent[2])
        elif ent[-1] == 'FAILED':
            faillst.append(ent[1])
            faillst.append(ent[2])

    #! variable postfix `lst` could let us falsely assume that the result of set()
    #! is a list.
    numlst = set(complst + faillst)

    #! good use of collections.Counter,
    #! but: Counter() already is a dictionary, there is no need to convert it to one
    comps = dict(collections.Counter(complst))
    fails = dict(collections.Counter(faillst))
    #! variable naming overlaps with global, and doesn't make sense in this context
    expected_result = {}

    for e in numlst:
        #! good: dealt with possibility of a number not showing up in `comps` or `fails`
        #! bad: using a try/except block to deal with this when a simpler .get("e", 0)
        #! would've allowed dealing with this more elegantly
        try:
            #! variable naming not very expressive
            rat = float(comps[e]) / float(comps[e] + fails[e]) * 100
            perc = round(rat, 2)
            #! here we are rounding twice, and then don't use the formatting string
            #! to attach the % -- '{:.2f}%'.format(perc) would've been the right
            #! way if one doesn't know percentage formatting (see below)
            expected_result[e] = "{:.2f}".format(perc) + '%'
            #! a generally better way would be to either
            #! from __future__ import division
            #! or to compute the ratio as 
            #! ratio = float(comps[e]) / (comps[e] + fails[e])
            #! and then use percentage formatting for the ratio
            #! "{:.2%}".format(ratio) 
        except KeyError:
            expected_result[e] = '0.00%'

    return expected_result

if __name__ == "__main__":
    assert(compute_success_ratio(log_data) == expected_result)

#! overall
#! + correct 
#! ~ implementation not optimal, relatively wasteful in terms of memory 
#! - variable naming inconsistent, overly shortened, not expressive
#! - some redundant operations
#! + good use of standard library collections.Counter
#! ~ code could be a tad bit more idiomatic

Я понял некоторые проблемы, такие как соглашения об именовании переменных и избегание try_block раздел как можно больше. Тем не менее, я не понимаю, как использование csv.reader улучшает код. Кроме того, как я должен понимать комментарий о сборе данных в первую очередь? Какие могут быть альтернативы? Кто-нибудь может пролить свет на эти две проблемы?

3 ответа

Решение

Когда вы делаете entries = logdata.split('\n') Вы создадите список с разделенными строками. Поскольку файлы журнала могут быть довольно большими, это может занимать большой объем памяти.

Способ, которым csv.reader работает то, что он откроет файл и прочитает только одну строку за раз (приблизительно). Это означает, что данные остаются в файле, и в памяти остается только одна строка.

Забыв о синтаксическом анализе csv на минуту, проблема иллюстрируется различием между этими подходами:

В подходе 1 мы читаем весь файл в память:

data = open('logfile').read().split('\n')
for line in data:
   # do something with the line

В подходе 2 мы читаем по одной строке за раз:

data = open('logfile')
for line in data:
    # do something with the line

Подход 1 будет занимать больше памяти, поскольку весь файл должен быть считан в память. Он также проходит через данные дважды - один раз, когда мы их читаем, и один раз, чтобы разбить на строки. Недостатком подхода 2 является то, что мы можем сделать только один цикл data,

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

split('\n') а также splitlines создаст копию ваших данных, где каждая строка является отдельным элементом в списке. Поскольку вам нужно передавать данные только один раз вместо случайного доступа к линиям, это расточительно по сравнению с CSV-ридером, который может вернуть вам одну строку за раз. Другое преимущество использования ридера состоит в том, что вам не придется разбивать данные на строки и строки на столбцы вручную.

Комментарий о сборе данных относится к тому факту, что вы добавили все завершенные и не выполненные задания в два списка. Допустим, этот пункт 111-333 завершается пять раз и терпит неудачу дважды. Ваши данные будут выглядеть примерно так:

complst = ['111-333', '111-333', '111-333', '111-333', '111-333']
faillst = ['111-333', '111-333']

Вам не нужны эти повторяющиеся предметы, чтобы вы могли использовать Counter непосредственно, не собирая элементы в списки и сэкономить много памяти.

Вот альтернативная реализация, которая использует csv.reader и собирает данные об успехах и неудачах dict где имя элемента является ключом, а значение - списком [success count, failure count]:

from collections import defaultdict
import csv
from io import StringIO

log_data = """1.1.2014 12:01,111-222-333,454-333-222,COMPLETED
1.1.2014 13:01,111-222-333,111-333,FAILED
1.1.2014 13:04,111-222-333,454-333-222,FAILED
1.1.2014 13:05,111-222-333,454-333-222,COMPLETED
2.1.2014 13:01,111-333,111-222-333,FAILED
"""

RESULT_STRINGS = ['COMPLETED', 'FAILED']
counts = defaultdict(lambda: [0, 0])
for _, *params, result in csv.reader(StringIO(log_data)):
    try:
        index = RESULT_STRINGS.index(result)
        for param in params:
            counts[param][index] += 1
    except ValueError:
        pass # Skip line in case last column is not in RESULT_STRINGS

result = {k: '{0:.2f}%'.format(v[0] / sum(v) * 100) for k, v in counts.items()} 

Обратите внимание, что выше будет работать только на Python 3.

Кроме того, Pandas выглядит хорошим решением для этой цели, если вы согласны с его использованием.

import pandas as pd

log_data = pd.read_csv('data.csv',header=None)
log_data.columns = ['date', 'key1','key2','outcome']

meltedData = pd.melt(log_data, id_vars=['date','outcome'], value_vars=['key1','key2'],
              value_name = 'key') # we transpose the keys here
meltedData['result'] = [int(x.lower() == 'completed') for x in meltedData['outcome']] # add summary variable

groupedData = meltedData.groupby(['key'])['result'].mean()
groupedDict = groupedData.to_dict()

print groupedDict

Результат:

{'111-333': 0.0, '111-222-333': 0.40000000000000002, '454-333-222': 0.66666666666666663}
Другие вопросы по тегам