Непрерывное совершенствование проектов по науке о данных требует упорного труда

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

Просмотр кода, написанного кем-то другим, часто с небольшим количеством документации или контекста, иногда может быть трудным и неприятным. Но колбаса так делается.

Здесь я иллюстрирую пример, в котором мне удалось значительно сократить максимальное вычислительное пространство и общее время обработки, необходимое для запуска программы. Что действительно здорово, так это то, что я изначально не стремился к этому; он возник в результате инициативы, направленной на переоснащение этой программы, чтобы она лучше работала с набором установленных 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, посвященное этой теме.