Непрерывное совершенствование проектов по науке о данных требует упорного труда
Одним из наиболее недооцененных аспектов организаций с относительно зрелыми командами по обработке и анализу данных является сильная культура постоянного совершенствования. В контексте технических групп, ответственных за создание и обслуживание сложных программных решений, особенно важно использовать методы, которые улучшают общее качество кодовой базы команды. Будь то существующие процессы, которые уже находятся в производстве, или стандартизированные пакеты/код для выполнения повторяющихся задач, периодическая и тщательная проверка кода приносит пользу как первичным, так и третичным заинтересованным сторонам, уменьшая вероятность ошибок, проблем с безопасностью и использования ресурсов.
Просмотр кода, написанного кем-то другим, часто с небольшим количеством документации или контекста, иногда может быть трудным и неприятным. Но колбаса так делается.
Здесь я иллюстрирую пример, в котором мне удалось значительно сократить максимальное вычислительное пространство и общее время обработки, необходимое для запуска программы. Что действительно здорово, так это то, что я изначально не стремился к этому; он возник в результате инициативы, направленной на переоснащение этой программы, чтобы она лучше работала с набором установленных KPI. Иногда лучшие вещи случаются, когда вы их не ожидаете.
Во-первых, немного контекста. В определенный момент программа (написанная на Python) берет случайную выборку из набора объектов и записывает ключевые показатели для этой группы. Он делает это много раз. Цель состоит в том, чтобы определить выборку объектов, которые максимизируют значения ключевых показателей, которые будут использоваться позднее. Проверяя код на правильность, я обнаружил нечто, что значительно увеличивало общее время выполнения и использование памяти, чего не должно было быть.
Проблема возникла в том, как сохранялись результаты случайной выборки. Поскольку образец был необходим на более поздних этапах, программа изначально создала список, в котором хранился кадр данных pandas каждого образца для каждой итерации с использованием цикла for. Хотя это и послужило своей цели, размер списка в памяти увеличился в зависимости от двух переменных: количества итераций в цикле for, а также размера берущейся выборки. Для относительно небольших выборок и итераций этот процесс работает нормально. Но что произойдет, если вы увеличите размер выборки, скажем, с 1 000 до 100 000, а количество итераций — с 5 000 до 500 000? Это может радикально изменить требуемое использование памяти — и независимо от вашего технического стека неэффективные программы стоят организации реальных денег из-за ненужных вычислительных ресурсов и потерянного времени.
Давайте быстро создадим примерный набор данных, чтобы проиллюстрировать проблему. Мы будем использовать пример идентификаторов покупателей с продажами за определенный период времени; это могут быть продажи, относящиеся к набору продуктов, каналу продаж, географическому местоположению и т. д. — используйте свое воображение!
import pandas as pd import numpy as np # Set how many iterations to run, & size of ID sample in each iteration n_iters = 100000 smpl_sz = 500000 # Create a sample df with 10MM unique IDs # - Generate some dummy sales data to play with later id_samp = pd.DataFrame(range(10000000,20000000)) id_samp.columns = ['Customer_ID'] id_samp['Sales_Per_ID'] = np.random.normal(loc = 1000, scale = 150, size = len(id_samp)) print(id_samp.head())
Как уже говорилось, программа изначально сохраняла каждую выборку по мере ее создания в мастер-списке, что вводит размер выборки в уравнение хранения в памяти. Это может стать еще хуже с более дорогими объектами данных, т. е. строками или созданными пользователями объектами по сравнению с целыми числами.
# Original version # Initialize data objects to store sample, info for each iteration’s sample metric_of_interest = [] samples = [] # In the loop, store each sample as it's created for i in range(n_iters): samp = id_samp.sample(n = smpl_sz, axis = ‘rows’, random_state = i) samples.append(samp) # Do something with the sample & record add’l info as necessary metric_of_interest.append(samp[‘Sales_Per_ID’].mean())
Ключевым моментом здесь является то, что нам не нужно хранить каждый образец в том виде, в котором он был создан, чтобы получить к нему доступ позже. Мы можем воспользоваться неотъемлемыми свойствами функции случайной выборки, чтобы использовать случайное начальное число для воспроизводимости. Я не буду углубляться в использование случайных семян в качестве лучшей практики; но вот еще одна статья на Medium с довольно подробным объяснением, а здесь вы можете увидеть документацию NumPy и Pandas об использовании seed/случайных состояний. Главный вывод заключается в том, что целочисленное значение можно использовать для выбора начала процесса выборки; поэтому, если вы сохраните значение, вы можете воспроизвести образец. Таким образом, мы смогли устранить влияние размера выборки и типов данных на использование памяти, оптимизировав методологию хранения.
Результатом будет создание случайно выбранной группы целых чисел с 1 значением на итерацию цикла для создания выборок. Ниже я показываю свой собственный уникальный метод случайного выбора этих целых чисел, но этого можно добиться разными способами.
# Create a random sample of integers for use as ID sample random state seed #Here, we pull a seq of nums 50x greater than num iters to samp from rndm_st_sd_pop = pd.Series(range(0,n_iters*50)) rndm_st_sd_samp = rndm_st_sd_pop.sample(n = n_iters, axis = ‘rows’) del(rndm_st_sd_pop) print(rndm_st_sd_samp.head())
Примечание: в этом примере индекс и случайное начальное значение равны
Теперь вы можете перебрать свою случайную выборку начальных значений и передать каждое значение в качестве параметра функции выборки. Имейте в виду, что этот параметр должен существовать для любой функции выборки, которую стоит использовать, независимо от языка.
# Initialize data object(s) to store info for each iter’s sample metric_of_interest = [] # In the loop, use the random state/seed to produce a sample you can easily reproduce later for i in rndm_st_sd_samp: samp = id_samp.sample(n = smpl_sz, axis = ‘rows’, random_state = i) # Do something with the sample & record add’l info as necessary metric_of_interest.append(samp[‘Sales_Per_ID’].mean()) # Bind the info saved for each iter to resp seed val for easy lookup sample_data = pd.DataFrame( {'Avg_Sales_Per_ID': metric_of_interest, 'Smpl_Rndm_St_Seed': rndm_st_sd_samp }) sample_data.reset_index(inplace = True) print(sample_data.head())
В этом случае глубокое понимание пакетов и конкретного языка использования в сочетании со стандартизированным процессом проверки кода привело к существенному повышению производительности во время выполнения. Это был пример лишь одного из способов повлиять на производительность и надежность вашего кода, и, надеюсь, он вдохновит вас по-новому взглянуть на то, что вы написали давным-давно. Что касается дополнительной информации о формальном рассмотрении кода, вот действительно хорошее обсуждение темы из блога Stack Overflow; и есть сообщество Stack Exchange, посвященное этой теме.