Конвейер машинного обучения для начинающих — набор данных о розничных доходах, часть I

Вы, наверное, слышали о машинном обучении, учитывая, насколько известным и влиятельным оно было в последнее десятилетие. Многие люди, которые могут не обладать достаточными знаниями о том, как это работает, по-прежнему считают это «волшебством», когда мы точно предсказываем, что произойдет, используя данные. Поскольку эта область развивается с бесчисленным количеством приложений, люди, не имеющие опыта в этой области, находят это интересным и хотят исследовать, что на самом деле происходит за кулисами. В этой статье мы рассмотрим эту магию и научимся строить модель машинного обучения (ML) с учетом некоторых данных. Это поможет начинающим учащимся понять базовый конвейер ML.

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

#Reading the dataset
df=pd.read_csv("training_set.csv")
#finding total rows and columns
print ("Columns in the dataset: ", df.shape[1])
print ("Rows in the dataset   : ", df.shape[0])
df.head()

Заметим, что у нас всего 14 столбцов. Из этих 14 столбец return является целевой переменной, где значение 1 означает, что покупатель вернет товар, а 0 означает, что покупатель не вернет товар. Всего в наборе данных 100 000 строк. Посмотрим соотношение значений в нашей целевой функции:

df['return'].value_counts()

Мы заметили, что в нашем наборе данных почти равное количество 1 и 0. Этот аспект сводит на нет дисбаланс классов и дает нам сбалансированный набор данных.

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

Очистка данных и предварительная обработка:

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

#checking the count of null values in each column
df.isna().sum()

Теперь большинство столбцов имеют 0 нулевых значений, однако 2 столбца имеют несколько нулевых значений. Существуют определенные способы правильной работы с такими значениями. Я собираюсь объяснить 3 основных метода следующим образом:

  1. Полностью удалите 2 столбца из набора данных. Однако, поскольку отсутствует только около 10 % значений, это не выглядит благоприятным вариантом. Кроме того, эти столбцы кажутся полезными для дальнейшего использования.
  2. Удалить строки с отсутствующими значениями. Этот метод может работать, если есть незначительное количество нулевых значений, но в этом случае мы можем потерять почти 18 000 наших записей данных, что совершенно недопустимо.
  3. Замените отсутствующее значение данных средним или медианным значением соответствующего столбца. Этот шаг также называется вменением данных. Это включает в себя вычисление среднего или медианы известных значений и подстановку их вместо нулевых значений. Это хорошая практика, поскольку она не связана с потерей данных. К сожалению, в нашем случае мы не можем вычислить среднюю дату и подставить ее, поэтому нам придется выполнить проектирование признаков позже.

Мы приступаем к варианту 3 выше и продвигаемся к указанному этапу разработки функций следующим образом:

Разработка функций:

В этом разделе мы отформатируем несколько функций (столбцов), чтобы сделать их более полезными для нас. Мы видим, что у нас есть несколько функций с форматом даты/времени. Чтобы извлечь из них значимую информацию, начнем с user_dob. Чтобы сделать эту функцию простой и понятной, мы извлекаем из нее возраст клиента, вычитая дату рождения из текущей даты. Кроме того, поскольку мы знаем, что в этом столбце есть нулевые значения, позже мы заменяем их медианой рассчитанного возраста. Соответствующий код будет выглядеть примерно так:

#function to convert date to speicif format
def __datetime(date_str):
    
    #for null values set dob as current date to identify them as their age will be 0
    if date_str!=date_str:
        date_str=str(date.today())

    return datetime.strptime(date_str, '%Y-%m-%d')

#subtracting dob with current date and converting to years.
df['Age']=(__datetime(str(date.today()))-df.user_dob.apply(__datetime))/365
df['Age']=df.Age.apply(lambda x: int(str(x).split(" ")[0]) )

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

#function to impute 0 values
def imputing_age(x,y):
    x0=x[x['return']==1] # subset comprising of return
    x1=x[x['return']==0] # subset comprising of non-return
    x0[y] =x0[y].map( lambda x : x0[y].median() if x == 0 else x) #replacing returns 0s with returns median
    x1[y] =x1[y].map( lambda x : x1[y].median() if x == 0 else x) #replacing non-returns 0s with non-returns median
    return pd.concat([x0,x1])
#Imputing 0 values with median for each class
df=imputing_age(df,'Age')

Мы видим, что мы успешно преобразовали данные даты/времени в числовые данные.

Мы можем сделать аналогичные шаги для даты доставки и заказа. Мы можем найти время, необходимое для доставки заказа, вычитая дату заказа из даты доставки. Это может быть выполнимой функцией, поскольку потребители могут вернуть товары, если их доставка заняла значительное время. Кроме того, мы можем найти месяц доставки или день недели по дате, так как товары, доставленные в понедельник, имеют больше шансов на возврат, или товары, заказанные во время Пасхи или Рождества, с большей вероятностью будут сохранены. Полный код этого шага можно найти по ссылке на Github, упомянутой в нижней части статьи, посвященной части 2.

После вычисления этих функций мы можем выполнить аналогичную импутацию данных для delivery_time, месяца, дня недели, как и раньше. Мы можем сделать аналогичные шаги, например, вычислить возраст для user_reg. После извлечения полезных функций мы можем удалить исходные столбцы следующим образом:

df.drop(['order_date','delivery_date','user_reg_date','user_dob'], axis=1,inplace=True)

Категориальные столбцы:

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

#getting total columns
cols=df.columns
print("Total Columns in dataset are: ",list(cols))
#getting only numerical columns
numerical_cols=df._get_numeric_data().columns
print("Numerical Columns in dataset are: ",list(numerical_cols))
#getting categorical columns by subtracting numerical columns from total columns
categorical_cols=set(list(cols))-set(list(numerical_cols))
print("Categorical Columns in dataset are: ",list(categorical_cols))

Мы видим, что существует несколько категориальных столбцов. Для месяца и дня недели мы можем легко преобразовать их в числовой формат: 1–12 для месяца и 0–6 для дней недели. Для других столбцов у нас есть несколько методов, то есть мы можем использовать однократное кодирование или просто присвоить числовую метку значению. Я склонен использовать кодирование меток в этом руководстве для начинающих, используя встроенные библиотеки. Выполним кодирование метки для user_title:

le = preprocessing.LabelEncoder()
print ("Before Encoding: ",df['user_title'].unique())
#encoding user_title to numerical values
df['user_title'] = le.fit_transform(df.user_title.values)
print ("After Encoding:  ",df['user_title'].unique())

Мы видим, что до кодирования у нас было 5 разных значений категориальных данных, но теперь, после кодирования, у нас есть числовые данные. Нам не нужно использовать библиотеку для месяца/дня недели, так как мы можем назначить для нее собственную кодировку. Item_size — следующая категориальная переменная, которая кажется важной. Посмотрим на его значения:

Мы видим, что размер элемента имеет смесь нечисловых и числовых значений. Этот аспект интересен тем, что нам нужно будет преобразовать лишь несколько значений. Возникает проблема: поскольку у нас нет отображения того, что xl, l и xxl означают в числовом выражении, мы не можем просто заменить их. Исходя из реального сценария, мы можем с уверенностью предположить, что xxl › xl › l и так далее. Следовательно, мы можем разделить наши числовые данные на 5–6 групп в зависимости от количества категориальных значений, а затем отнести каждую группу к одной категории значений. Мы можем легко сделать это с помощью квантилей, которые сегментируют данные на группы на основе значений.

def item_map(x):
    #removing + sign from sizes
    x=x.replace("+","")
    
    #if size is categorical replace it with nan
    if x.isnumeric()==False:  
        x=np.nan
    return float(x)
df['item_size_numerical']=df.item_size.apply(item_map)
df.item_size_numerical.quantile([0.02,0.08,.12, .3,.6, .8,0.98])

Здесь у нас есть 6 квантилей, 0,98 означает значения, которые лежат выше 98% данных, а 0,02 относится к значениям, которые составляют 2%. Следовательно, мы можем сказать, что 48 больше, чем 98% значений, поэтому мы можем присвоить ему xxxl и повторить итерацию аналогичным образом для других квантилей.

def item_map_category(x):
    x=x.replace("+","")
    if x.isnumeric()==False:  
        if x=='xxxl':
            x=48
        elif x=='xxl':
            x=42
        elif x=='xl':
            x=40
        elif x=='l':
            x=38
        elif x=='m':
            x=29
        elif x=='s':
            x=10
        elif x=='xs':
            x=6
        else: #for non-reported size we choosing mid value
            x=40
    return int(x)
df['item_size_fixed']=df.item_size.apply(item_map_category)
df.item_size_fixed.value_counts()

Теперь это преобразует все наши нечисловые размеры в числовые значения следующим образом:

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

Вывод:

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

https://wahabaftab.medium.com/machine-learning-pipeline-for-beginners-retail-returns-dataset-part-ii-57cd8b8a0743

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