Сбор данных первым в 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}