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

Думали ли вы, как и я, что строгие правила написания кода предназначены для разработчиков программного обеспечения, а не для специалистов по данным? Пришло время использовать опыт наших коллег и внедрять передовые методы кодирования один за другим.

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

Если ваш ответ «хм», то читайте дальше — давайте изобретем велосипед.

Чистый код

Если вы читали «Чистый код» Роберта Мартина, то большинство описанных здесь моментов будут вам знакомы. Если нет — просто прочитайте ее, как только закончите эту статью и поставите лайк. Прочитав ее, вы измените жизнь. В нем описано огромное количество приемов, позволяющих сделать ваш код структурно лучше. Основная идея, которую я почерпнул, заключается в том, что ваш код должен быть удобным для чтения, а также гибким для модификации.

Вы можете возразить, что это относится к крупным «настоящим» программным приложениям, над которыми работают многие инженеры. Если вы специалист по машинному обучению, скорее всего, вы работаете над своим кодом в одиночку. Так зачем беспокоиться о читабельности? Вы знаете каждый закоулок кода, верно? Конечно, вы делаете. Теперь представьте, что вы возвращаетесь к своему коду после перерыва в несколько месяцев, чтобы что-то исправить или поднять одну из его частей, чтобы использовать ее в своем текущем проекте. Здесь вы сталкиваетесь с переменной s в середине огромной функции с именем func. Теперь вам нужна огромная чашка кофе, поскольку вы знаете, что вам нужно будет потратить время, пытаясь понять, что здесь происходит.

«Хорошо, я убежден», надеюсь, вы скажете. Если это так, читайте дальше!

Советы по именованию

Имена, раскрывающие намерения

Это просто. Имена переменных и функций должны раскрывать намерения.

if e > 10:
    break

Если вы посмотрите на этот код сам по себе, сможете ли вы догадаться, что здесь происходит? Возможно нет.

С такой небольшой поправкой:

if epoch > 10:
    break

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

Вы говорите, что epoch — это стандартное имя в науке о данных? Абсолютно! Это пример осмысленного названия: каждый, кто обучил хотя бы одну нейросеть, знает, что эпоха — это одна итерация процесса обучения. Вам не нужно полагаться на дополнительные комментарии или догадки, чтобы понять, о чем эта часть кода.

А плохие имена? Я, вероятно, навлеку на себя гнев сообщества специалистов по данным, но data и df — плохие имена.

Давайте посмотрим на пример. Общий научный код данных может выглядеть так:

def process_data():
    df = df.fillna()
    df['column_name'] = df['another_column'] * 5
    df = df.groupby('major_column').sum()
    df = pandas.concat([df.iloc[0:100], df.iloc[200:300])
    return df

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

Чтобы решить эту проблему, было бы неплохо выделить операции одной цели в отдельную функцию с осмысленным именем. Там вы бы знали, что происходит внутри, что входит и выходит. Затем организовать все в конвейер данных, что избавит от необходимости переназначать все результаты в одну переменную (об этом мы поговорим в одной из следующих статей).

Комментарии для объяснения переменных

Еще одна подсказка — комментарий. Если вам это нужно, чтобы объяснить, что означает переменная, то, вероятно, для нее есть лучшее имя.

Сравните два примера кода, которые делают одно и то же:

p = numpy.percentile(df.groupby('user')['sales'].mean(), 0.95)
x = df.groupby('user')['date'].min().max()
df = df[(df['sales'] >= p) & (df['date'] > x)]
u = df['user'].unique()

и

user_mean_sale = sales.groupby('user')['sales'].mean()
mean_sales_percentile = numpy.percentile(user_mean_sale, 0.95)

user_first_sale_date = sales.groupby('user')['date'].min()
latest_first_sale_date = user_first_sale_date.max()

recent_high_value_sales = sales[
                           (sales['sales'] >= mean_sales_percentile) & 
                           (sales['date'] > latest_first_sale_date)]
unique_hv_users = recent_high_value_sales['user'].unique()

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

Значимые отличия

Иногда требуется создать несколько объектов с похожим содержанием. Важно сделать имена, которые выражают различие между объектами. Наиболее очевидным примером здесь могут быть переменные df и data. Что находится в data и что в df? Чем они отличаются? Невозможно понять без детективного расследования происхождения обоих объектов.

Точно так же имена объектов, такие как df1 и df2, не помогут вам понять код.

Попробуйте проверить свой нейминг, ответив на вопрос: помогает ли мне понять код это конкретное слово. Например, если вы называете переменную the_user_data, все ли слова имеют назначение? — определенно нет. Его можно опустить и смысл имени ничуть не изменится. Пользователь — абсолютно. Извлеките его, и вы потеряете массу информации. Данные — возможно. Добавление типа данных к именованию не считается хорошей практикой, но все же может быть полезно различать такие важные объекты в проектах машинного обучения, как фреймы данных.

Названия с возможностью поиска

Это говорит само за себя. Представьте, что вы хотите переименовать переменную в своем классе или записной книжке. Допустим, вы хотите изменить его с train на test. Если вы правильно назвали, все, что вам нужно сделать, это найти и заменить все имена. Если нет, вам будет трудно понять, является ли текущий train train, который вы хотите изменить, или он означает что-то другое.

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

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

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

Имена классов и методов

Идеальный код можно читать как книгу. И этот совет — один из самых элегантных способов приблизиться к нему. Вот идея: вы называете свои классы существительными, потому что класс — это сервис, предоставляющий вам конкретную обработку; а функции должны быть глаголами, потому что функция выполняет определенную задачу. Аргументы функции также являются существительными, поскольку они в основном представляют собой структуры данных или примитивные типы данных. В результате можно получить действительно гладкую нотацию, вот так:

train_data = DataLoader().load_data(path)

or

encoded_train = Encoder().encode_country(train_data)

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

RandomForestClassifier().train(train_data, train_target)

or

pandas.DataFrame().corrwith(another_df)

Выглядит красиво и легко читается.

Заключение

Хорошо написанный код не заставит вашу модель работать быстрее или лучше. Иногда это может сделать ваш код короче, но в большинстве случаев, наоборот, часто увеличивает размер программы. Может показаться, что для проектов машинного обучения это избыточно и требует дополнительного времени и места. Более того, если вы возьмете на вооружение эту практику, у вас наверняка возникнут многочисленные споры с коллегами о том, хорошо это или плохо. Я не могу обещать, что это сработает для вас, но это определенно сработало для меня. Надеюсь, я убедил вас выработать еще одну полезную привычку писать код. Это, безусловно, повысит читабельность вашего кода и поможет следующему читателю поддерживать его, даже если следующим читателем станете вы в будущем.