В качестве примера применение линейной регрессии рассмотрим задачу, часто возникающую в розничной торговле. Мы планируем месячные продажи нескольких товарных групп в нашей сети магазинов одежды и нам необходимо определить товарный запас по каждой товарной группе, который должен находится в каждом магазине исходя из планового количества продаж. Для того, чтобы определить товарный запас по плану продаж в штуках надо задать плановую оборачиваемость.
При этом у нас есть статистика продаж и остатков по некоторому количеству магазинов, то есть у нас есть месячные продажи, средний остаток и оборачиваемость. Попробуем на основании этих данных с помощью линейной регрессии сделать модель, которая по заданному количеству продаж определяла нам плановую оборачиваемость для каждой группы товаров.
Итак, у нас есть данные по продажам 70 магазинов одежды в марте 2021 года. Данные представлены в виде Data Frame с колонками : "Prod_gr" (товарная группа), "Turnover" (оборачиваемость), "Amount" (количество продаж).
df = pd.DataFrame({
'Prod_gr':
70*['Pullover']+70*['Hoody']+70*['Underpants']+69*['T-shirt']+['T-shirt ']),
'Turnover':
np.array([0.29,0.32,0.24,0.22,0.28,0.27,0.26,0.2,0.26,0.2,0.25,0.21,0.34,0.27,
0.23,0.2,0.26,0.2,0.23,0.26,0.21,0.2,0.26,0.24,0.27,0.21,0.2,0.11,
0.17,0.26,0.21,0.25,0.16,0.18,0.3,0.14,0.3,0.23,0.21,0.12,0.17,0.16,
0.27,0.21,0.24,0.19,0.23,0.19,0.23,0.3,0.15,0.26,0.28,0.19,0.24,0.24,
0.17,0.16,0.28,0.7,0.2,0.17,0.27,0.16,0.13,0.14,0.28,0.2,0.15,0.23,0.45,
0.48,0.24,0.2,0.4,0.34,0.21,0.36,0.3,0.3,0.22,0.15,0.58,0.33,0.31,0.3,
0.34,0.3,0.27,0.32,0.24,0.22,0.39,0.4,0.28,0.32,0.23,0.15,0.23,0.24,0.21,
0.32,0.18,0.17,0.3,0.2,0.28,0.31,0.19,0.21,0.16,0.17,0.38,0.21,0.31,0.15,
0.24,0.17,0.18,0.31,0.16,0.36,0.31,0.24,0.29,0.18,0.31,0.18,0.49,0.25,0.26,
0.21,0.45,0.29,0.18,0.34,0.43,0.45,0.24,0.23,0.24,0.37,0.2,0.38,0.63,0.51,
0.35,0.28,0.29,0.19,0.27,0.44,0.35,0.23,0.28,0.24,0.28,0.35,0.61,0.42,0.31,
0.31,0.53,0.2,0.42,0.21,0.2,0.15,0.33,0.26,0.22,0.29,0.25,0.17,0.3,0.14,
0.38,0.43,0.33,0.14,0.12,0.13,0.42,0.25,0.22,0.31,0.31,0.29,0.19,0.33,0.18,
0.15,0.42,0.33,0.36,0.36,0.31,0.26,0.32,0.88,0.43,0.17,0.36,0.08,0.08,0.27,
0.26,0.35,0.17,0.16,0.2,0.35,0.18,0.11,0.23,0.3,0.29,0.18,0.19,0.26,0.24,
0.26,0.4,0.14,0.19,0.14,0.33,0.1,0.4,0.45,0.21,0.17,0.3,0.2,0.41,0.25,0.2,
0.15,0.27,0.22,0.12,0.28,0.13,0.2,0.19,0.08,0.33,0.31,0.23,0.15,0.12,0.18,
0.32,0.26,0.26,0.13,0.24,0.21,0.24,0.34,0.12,0.3,0.31,0.22,0.16,0.39,0.29,
0.22,0.34,0.6,0.18,0.15,0.38,0.14,0.17,0.23,0.33,0.27,0.2,0.19,0.29,0.25,0.17,
0.16,0.33,0.3,0.19,0.23,0.23,0.1,0.21,0.12,0.3,0.25,0.19,0.16,0.26,0.15,0.19,
0.19,0.14,0.23,0.15,0.19,0.11,0.17,0.17,0.12,0.11,0.11,0.11,0.19,0.22,0.18,
0.25,0.13,0.19,0.29,0.08,0.13,0.1,0.09,0.2,0.19,0.22,0.25,0.19,0.17,0.11,0.2,
0.07,0.24,0.16,0.13,0.14,0.15,0.12,0.13,0.2,0.76,0.19,0.18,0.18,0.2,0.08,0.1,
0.19,0.19,0.13,0.16,0.26,0.23,0.23,0.16,0.44,0.34,0.36,0.23,0.31,0.16,0.13,
0.21,0.25,0.26,0.19,0.11,0.1,0.2,0.25,0.13,0.21,0.16,0.2,0.21,0.19,0.21,0.15,
0.22,0.15,0.13,0.15,0.21,0.28,0.2,0.25,0.18,0.24,0.24,0.17,0.13,0.12,0.17,0.36,
0.25,0.35,0.25,0.22,0.19,0.24,0.21,0.07,0.34,0.3,0.23,0.23,0.19,0.21,0.23,0.57,
0.69,0.16,0.12,0.14,0.34,0.07,0.2,0.23,0.31,0.2,0.24,0.39,0.39,0.25,0.15,0.34,
0.34,0.3,0.3,0.31,0.24,0.27,0.15,0.39,0.22,0.35,0.23,0.22,0.24,0.25,0.3,0.23,
0.17,0.35,0.25,0.23,0.21,0.28,0.13,0.14,0.17,0.15,0.23,0.22,0.21,0.34,0.12,0.24,
0.25,0.22,0.18,0.23,0.17,0.27,0.2,0.25,0.19,0.23,0.19,0.16,0.32,0.13,0.34,0.24,
0.18,0.14,0.26,0.18,0.19,0.37,0.96,0.22,0.14,0.27,0.21,0.1,0.16,0.28,0.32,0.25,
0.25,0.47,0.57,0.4,0.42,0.24,0.82,0.36,0.56,0.84,0.36,0.17,0.32,0.48,0.21,0.4,
0.11,0.24,0.37,0.64,0.44,0.19,0.42,0.29,0.52,0.54,0.49,0.52,0.59,0.5,0.47,0.07,
0.39,0.4,0.26,0.38,0.51,0.45,0.54,0.42,0.46,0.24,0.41,0.42,0.22,0.39,0.44,0.51,
0.31,0.18,0.59,0.21,0.43,0.42,0.39,0.28,0.35,0.29,0.23,0.44,0.83,0.5,0.26,0.48,
0.47,0.36,0.33,0.46,0.35,0.34,0.47,0.27,0.29,0.21,0.22,0.28,0.26,0.26,0.26,0.21,
0.13,0.19,0.12,0.27,0.18,0.22,0.2,0.2,0.15,0.26,0.25,0.19,0.2,0.21,0.3,0.17,0.19,
0.19,0.11,0.16,0.18,0.14,0.18,0.16,0.14,0.2,0.1,0.16,0.2,0.19,0.12,0.12,0.16,0.24,
0.17,0.13,0.16,0.27,0.17,0.14,0.24,0.14,0.22,0.24,0.12,0.18,0.18,0.23,0.18,0.26,
0.25,0.25,0.16,0.19,0.18,0.12,0.15,0.18,0.14,0.13,0.2]),
})
df.head()
Prod_gr | Turnover | Amount | |
---|---|---|---|
0 | Jeans | 0.29 | 378 |
1 | Jeans | 0.32 | 595 |
2 | Jeans | 0.24 | 214 |
3 | Jeans | 0.22 | 117 |
4 | Jeans | 0.28 | 262 |
Выведем основные сведения по нашему датафрейму
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 630 entries, 0 to 629 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Prod_gr 630 non-null object 1 Turnover 630 non-null float64 2 Amount 630 non-null int32 dtypes: float64(1), int32(1), object(1) memory usage: 12.4+ KB
630 наблюдений по девяти товарным группам в 70-ти магазинах, пропусков нет.
Основные описательные статистики по числовым данным
df.describe().T
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
Turnover | 630.0 | 0.256206 | 0.121334 | 0.07 | 0.18 | 0.23 | 0.30 | 0.96 |
Amount | 630.0 | 115.007937 | 103.781896 | 5.00 | 38.00 | 74.00 | 165.75 | 595.00 |
Описательные статистики не вызывают явных сомнений, все значения возможны, в ходе дальнейшего анализа возможно придется от каких-то показаний отказаться.
Часто в исходных данных , описывающих категории товара встречаются ошибки в написании, поэтому начнем с проверки на ошибки. Выведем список уникальных категорий товара :
df.Prod_gr.unique()
array(['Jeans', 'Jеans', 'Jacket', 'Socks', 'Belt', 'Shirt', 'Pullover', 'Hoody', 'Underpants', 'T-shirt', 'T-shirt '], dtype=object)
Две категории повторяются, посмотрим на количество значений по этим категориям
df.Prod_gr.value_counts()
Underpants 70 Belt 70 Shirt 70 Jacket 70 Pullover 70 Socks 70 Hoody 70 Jeans 69 T-shirt 69 T-shirt 1 Jеans 1 Name: Prod_gr, dtype: int64Явно два наименования категории записаны ошибочно, для подтверждения выведем
коды их символов
for pg in sorted(df.Prod_gr.unique()): print(f"{pg:>10s}", [ord(c) for c in pg])
Belt [66, 101, 108, 116] Hoody [72, 111, 111, 100, 121] Jacket [74, 97, 99, 107, 101, 116] Jeans [74, 101, 97, 110, 115] Jеans [74, 1077, 97, 110, 115] Pullover [80, 117, 108, 108, 111, 118, 101, 114] Shirt [83, 104, 105, 114, 116] Socks [83, 111, 99, 107, 115] T-shirt [84, 45, 115, 104, 105, 114, 116] T-shirt [84, 45, 115, 104, 105, 114, 116, 32] Underpants [85, 110, 100, 101, 114, 112, 97, 110, 116, 115]В категории "Jeans" не совпадают вторые символы, во втором варианте "е" написано
кириллицей, а категории T-shirt в втором варианте в конце лишний пробел. Заменим эти значения.
df.loc[df.Prod_gr.isin(['Jeans', 'Jеans']), 'Prod_gr'] = 'Jeans' df.loc[df.Prod_gr.isin(['T-shirt', 'T-shirt ']), 'Prod_gr'] = 'T-shirt' df.Prod_gr.value_counts()
Underpants 70 Jeans 70 Belt 70 Shirt 70 T-shirt 70 Jacket 70 Socks 70 Pullover 70 Hoody 70 Name: Prod_gr, dtype: int64Проверим числовые показатели, оборачиваемость и количество продаж не должно быть меньше или рано нулю
((df.Turnover <= 0) | (df.Amount <= 0)).any()
FalseТеперь переходим к основной задаче - выявить связь межу продажами и
оборачиваемостью в зависимости от товарной группы. Для этого выведем графики,
показывающие насколько эта зависимость близка к линейной
sns.lmplot(x='Amount', y='Turnover', data =df,col='Prod_gr',col_wrap=3)
Зависимость для всех товарных групп близка к линейной, но во-первых, коэффициенты линейной регрессии явно отличаются, и во-вторых, в данных явно присутствуют
выбросы.
Выведем другой график, который это подтвердит.
plt.figure(figsize=(15, 5)) sns.set_style('whitegrid') sns.boxplot('Prod_gr', 'Turnover', data=df)
Это так называемые ящичные диаграммы с усами. Каждый ящик представляет значения, располагающиеся между первым и третьим квартилями, усы охватывают
значения в пределах 1,5 межквартильных размахов от границ ящика, все значения за их пределами считаем выбросами.
Для того, чтобы убрать выбросы используем функцию outliers модуля stats из пакета dautil, она выдает картеж с значениями нижних и верхних усов.
t_min,t_max=stats.outliers(df.query('Prod_gr == "Jeans"')['Turnover']) df_new=df[((df.Prod_gr == "Jeans")&(df.Turnover<=t_max))|(df.Prod_gr != "Jeans")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Jacket"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Jacket")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Jacket")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Socks"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Socks")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Socks")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Belt"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Belt")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Belt")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Shirt"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Shirt")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Shirt")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Pullover"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Pullover")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Pullover")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Hoody"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Hoody")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Hoody")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "Underpants"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "Underpants")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "Underpants")]
t_min,t_max=stats.outliers(df.query('Prod_gr == "T-shirt"')['Turnover']) df_new=df_new[((df_new.Prod_gr == "T-shirt")&(df_new.Turnover<=t_max))|
(df_new.Prod_gr != "T-shirt")]
Данные в новом датафрейме df_new без выбросов
plt.figure(figsize=(15, 5)) sns.set_style('whitegrid') sns.boxplot('Prod_gr', 'Turnover', data=df_new)
Теперь можно переходить к моделям регрессии для каждой товарной группы.
Импортируем необходимые функции
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression
Для оценки точности будем использовать Средняя абсолютная процентная ошибка(MAPE), определим для ее расчета функциюdef mean_absolute_percentage_error(y_true, y_pred):y_true, y_pred = np.array(y_true), np.array(y_pred)return round(np.mean(np.abs((y_true - y_pred) / y_true)) * 100,2)Функция для получения MAPE на обучающей и тестовой выборкеdef get_train_test_mape( model,X_train,X_test,y_train,y_test ):y_train_pred = model.predict( X_train )mape_train = round(mean_absolute_percentage_error( y_train,y_train_pred),2)y_test_pred = model.predict( X_test )mape_test = round(mean_absolute_percentage_error( y_test,y_test_pred),2)print( 'mape train: ', mape_train, ' mape test:', mape_test )Также определим функцию для линейной регрессииdef turnover(df,prod_gr):str_query='Prod_gr =='+prod_grX=df.query(str_query)['Amount']y=df.query(str_query)['Turnover']X=X[:,np.newaxis]#Разделяем данные на обучающую и тестовую частиX_train, X_test, y_train, y_test = train_test_split(X,y,random_state=42)#Определяем модель линейной регрессииreg = LinearRegression()#Обучаем модель на обучающих данныхreg.fit(X_train, y_train)#Делаем прогноз по тестовым даннымy_test_pred = reg.predict(X_test)#Выводим результатыprint(prod_gr)print(f"Turnover = {reg.intercept_:.2f} + {reg.coef_[0]:.4f} Amount")get_train_test_mape(reg,X_train,X_test,y_train,y_test)test_pred_df = pd.DataFrame( {'amount': X_test.flatten(),'turn_act': y_test,'turn_pred': np.round( y_test_pred, 2 ),'balance_act' : np.round(X_test.flatten()/y_test,0),'balance_pred' : np.round(X_test.flatten()/y_test_pred,0),'mape_turn': np.round((y_test - y_test_pred)*100 / y_test,2)} )print(test_pred_df.sample(10))Вот так она работает для товарной группы джинсы"Jeans" Turnover = 0.12 + 0.0004 Amount mape train: 10.7 mape test: 7.64 amount turn_act turn_pred balance_act balance_pred mape_turn 10 311 0.25 0.25 1244.0 1252.0 0.65 5 290 0.27 0.24 1074.0 1211.0 11.31 57 126 0.16 0.17 788.0 742.0 -6.17 33 142 0.18 0.18 789.0 804.0 1.85 60 154 0.20 0.18 770.0 847.0 9.12 35 152 0.14 0.18 1086.0 840.0 -29.22 9 236 0.20 0.22 1180.0 1090.0 -8.27 47 222 0.19 0.21 1168.0 1054.0 -10.85 12 579 0.34 0.36 1703.0 1599.0 -6.49 30 198 0.21 0.20 943.0 988.0 4.56В таблице balance_act и balance_pred - это расчетный остаток, необходимый для
тестовых продаж, рассчитанный по фактической и расчетной (регрессионной)
оборачиваемости.
Комментариев нет:
Отправить комментарий