Как правило, большинство дневных данных в розничной торговле не содержит пропусков за исключением такого показателя как количество зашедших посетителей. Чаще всего это связано с неисправностью счетчиков. Однако очень важно иметь достоверную информацию о посетителях. При этом важно работать не только с пропусками, но и с выбросами, которые также связаны с неисправностью счетчиков. Поэтому при подготовке данных по посетителям вначале надо отбросить выбросы, пусть они также будут считаться пропусками. Таким образом, под пропусками будем понимать как отсутствие значений, так и недостоверные данные.
Начнем с подготовки данных, на которых будем отрабатывать приемы выявления выбросов и заполнения пропусков. Набор данных представляет собой дневные показатели продажи в магазине одежды в сентябре - октябре 2023 года, всего 48 строк и 8 столбцов.
df=pd.read_excel('data/shday.xlsx')
df.head()
date | rev | visit | ch | sales | lch | price | conv |
---|
0 | 2023-09-01 | 225795 | 413 | 25 | 88 | 3.520 | 2565.852 | 0.061 |
---|
1 | 2023-09-02 | 612489 | 678 | 64 | 188 | 2.938 | 3257.920 | 0.094 |
---|
2 | 2023-09-03 | 609786 | 856 | 63 | 179 | 2.841 | 3406.626 | 0.074 |
---|
3 | 2023-09-04 | 266596 | 381 | 31 | 78 | 2.516 | 3417.897 | 0.081 |
---|
4 | 2023-09-05 | 277030 | 321 | 28 | 71 | 2.536 | 3901.831 | 0.087 |
---|
Обозначения :
- date - дата
- rev - выручка
- visit - количество посетителей
- sales - количество покупок
- ch - количество чеков
- conv - конверсия = количество чеков / количество посетителей
- lch - длина чека = количество покупок / количество чеков
- price - средняя цена покупки = выручка / количество покупок
Интересную информацию может дать корреляционная матрица
plt.figure(figsize=(12, 7))
sns.heatmap(df.corr(numeric_only=False),vmin=-0.3,vmax=0.6,
center=0,annot=True,fmt='.2f',
mask=~np.tri(df.corr(numeric_only=False).shape[1], k=-1, dtype=bool),
linewidth=2,cbar=False)
Корреляция по дате показывает, что процесс торговли идет с падением количественных показателей, таких как выручка, посетители, чеки и с ростом двух качественных : средней цены продажи и конверсии. Это как бы быстрый метод провести экспресс-анализ тенденции основных показателей.
В наших данных по посетителям нет ни выбросов, ни пропущенных значений, мы их искусственно добавим. Для этого сгенерируем случайным образом номера строк для пропусков и выбросов. Работать будем с копией данных без колонки с датой, она нам не нужна.
df0=df.loc[:,df.columns[1:]].copy()
df0.head()
rev | visit | ch | sales | lch | price | conv |
---|
0 | 225795 | 413 | 25 | 88 | 3.520 | 2565.852 | 0.061 |
---|
1 | 612489 | 678 | 64 | 188 | 2.938 | 3257.920 | 0.094 |
---|
2 | 609786 | 856 | 63 | 179 | 2.841 | 3406.626 | 0.074 |
---|
3 | 266596 | 381 | 31 | 78 | 2.516 | 3417.897 | 0.081 |
---|
4 | 277030 | 321 | 28 | 71 | 2.536 | 3901.831 | 0.087 |
---|
Сгенерируем номера строк для вставки выбросов
random.seed(1000)
random_list = random.sample(range(1, df.shape[0]), k=6)
random_list
[28, 43, 7, 26, 23, 5]
Для того, чтобы получить выбросы по посетителям, используем "нереальные" значения конверсии, слишком большие и слишком маленькие и по ним рассчитаем
выбросы посетителей
conv=np.array([0.85,0.9,0.95,0.007,0.008,0.009])
df0.loc[random_list,'visit']=round(df0.loc[random_list,'ch']/conv,0)
df0.loc[random_list,'conv']=df0.loc[random_list,'ch']/df0.loc[random_list,'visit']
df0.loc[random_list]
rev | visit | ch | sales | lch | price | conv |
---|
28 | 158917 | 21.0 | 18 | 48 | 2.667 | 3310.771 | 0.857 |
---|
43 | 521849 | 64.0 | 58 | 146 | 2.517 | 3574.308 | 0.906 |
---|
7 | 174745 | 24.0 | 23 | 75 | 3.261 | 2329.933 | 0.958 |
---|
26 | 55927 | 1143.0 | 8 | 26 | 3.250 | 2151.038 | 0.007 |
---|
23 | 364848 | 4125.0 | 33 | 113 | 3.424 | 3228.743 | 0.008 |
---|
5 | 195820 | 2556.0 | 23 | 95 | 4.130 | 2061.263 | 0.009 |
---|
Для вывода сводной статистике используем метод pandas describe(). Сводная статистика дает нам представление о распределении данных и может просигнализировать наличие выбросов в наборе данных. Например, мы можем видеть большой разрыв между минимальным и максимальные значения. В этом наборе мы видим большой разрыв между средним и максимальным и минимальным значениями посетителей. Этот большой разрыв является указателем на наличие выбросов, как с очень высокими значениями, так и с очень низкими значениями.
df0.describe()
rev | visit | ch | sales | lch | price | conv |
---|
count | 48.000 | 48.000 | 48.000 | 48.000 | 48.000 | 48.000 | 48.000 |
---|
mean | 262182.500 | 445.667 | 27.958 | 80.354 | 2.887 | 3302.341 | 0.135 |
---|
std | 137015.629 | 663.446 | 13.937 | 40.427 | 0.518 | 622.306 | 0.203 |
---|
min | 55927.000 | 21.000 | 8.000 | 26.000 | 1.792 | 2061.263 | 0.007 |
---|
25% | 174748.750 | 207.000 | 19.750 | 49.750 | 2.531 | 2922.729 | 0.074 |
---|
50% | 215608.000 | 241.500 | 23.000 | 71.000 | 2.841 | 3261.992 | 0.090 |
---|
75% | 304316.750 | 436.250 | 33.000 | 95.000 | 3.170 | 3618.550 | 0.105 |
---|
max | 612489.000 | 4125.000 | 64.000 | 188.000 | 4.250 | 5218.800 | 0.958 |
---|
Далее для выявления выбросов используем метод коробочной диаграммы в Seaborn. Выбросы — это значения за пределами усов. Они представлены в виде черных кружков.
fig, ax = plt.subplots(1, 2,figsize=(40,18))
sns.set(font_scale = 3)
ax1 = sns.boxplot(data= df0, x= 'visit', ax = ax[0])
ax1.set_xlabel('visit', fontsize = 30)
ax1.set_title('Outlier visit', fontsize = 40)
ax2 = sns.boxplot(data= df0, x= 'conv', ax=ax[1])
ax2.set_xlabel('conv', fontsize = 30)
ax2.set_title('Outlier conv', fontsize = 40)
Одномерные выбросы — это очень большие или маленькие значения, одной переменной в наборе данных. Важно выявить их и разобраться с ними, прежде чем проводить какой-либо дальнейший анализ или моделирование. Существует два основных метода выявления одномерных выбросов:
• Статистические измерения: мы можем использовать статистические методы, такие как межквартильный размах (IQR), Z-показатель и мера асимметрии.
• Визуализация данных: мы также можем использовать различные визуальные параметры для выявления выбросов, гистограммы, блочные диаграммы и графики "скрипки" — очень полезные диаграммы, которые отображают распределение нашего набора данных.
Мы рассмотрим, как обнаружить одномерные выбросы, используя межквартильный размах (IQR) и гистограмму histplot.
Начнем с IQR отдельно для посетителей и конверсии
Рассчитаем IQR для посетителей, используя метод квантилей в pandas:
Q1 = df0['visit'].quantile(0.25)
Q3 = df0['visit'].quantile(0.75)
IQR = Q3 - Q1
print(IQR)
229.25
Определим выбросы посетителей с помощью IQR:
df0.loc[(df0['visit'] < (Q1 - 1.5 *IQR)) |
(df0['visit'] > (Q3 + 1.5 * IQR)),['visit','conv']]
visit | conv |
---|
2 | 856.0 | 0.074 |
---|
5 | 2556.0 | 0.009 |
---|
23 | 4125.0 | 0.008 |
---|
26 | 1143.0 | 0.007 |
---|
Как видно этот метод выявил четыре выброса, причем один неверный, т.е. из шести выбросов выявились только три. Посмотрим, как он сработает на конверсии.
Q1 = df0['conv'].quantile(0.25)
Q3 = df0['conv'].quantile(0.75)
IQR = Q3 - Q1
print(IQR)
0.031164399035558107
df0.loc[(df0['conv'] < (Q1 - 1.5 *IQR)) |
(df0['conv'] > (Q3 + 1.5 * IQR)),['visit','conv']]
visit | conv |
---|
5 | 2556.0 | 0.009 |
---|
7 | 24.0 | 0.958 |
---|
23 | 4125.0 | 0.008 |
---|
26 | 1143.0 | 0.007 |
---|
28 | 21.0 | 0.857 |
---|
43 | 64.0 | 0.906 |
---|
По конверсии выявились все шесть выбросов. Конечно, конверсия - это нормированная величина, заранее известен диапазон ее изменения - от 0 до 1 и по ее значениям гораздо проще выделить выброс.
Теперь посмотрим, как нам покажут выбросы гистограммы, построенные по посетителям и конверсии.
По посетителям
plt.figure(figsize = (40,18))
ax = sns.histplot(data= df0,x= 'visit',bins=60)
ax.set_xlabel('visit', fontsize = 30)
ax.set_ylabel('Count', fontsize = 30)
ax.set_yticks(range(1,20))
ax.set_title('Outlier visit', fontsize = 40)
plt.xticks(fontsize = 30)
plt.yticks(fontsize = 30)
Все шесть выбросов проявились как отдельно стоящие столбики, наглядно, но не очевидно, за выброс можно принять еще одно значение.
Посмотрим что покажет конверсия
plt.figure(figsize = (40,18))
ax = sns.histplot(data= df0, x= 'conv',bins=60)
ax.set_xlabel('conv', fontsize = 30)
ax.set_ylabel('Count', fontsize = 30)
ax.set_title('Outlier conv', fontsize = 40)
ax.set_yticks(range(1,15))
plt.xticks(fontsize = 30)
plt.yticks(fontsize = 30)
Все шесть выбросов на гистограмме отразились.
Поиск двумерных выбросов
Двумерные выбросы обычно представляют собой большие или малые значения, которые встречаются в двух переменных одновременно. Проще говоря, эти значения отличаются от других наблюдений, когда мы рассматриваем две переменные вместе. По отдельности значения каждой переменной могут быть выбросами, а могут и не быть; однако в совокупности они являются выбросами. Чтобы обнаружить двумерные выбросы, нам обычно необходимо проверить взаимосвязь между двумя переменными. Основной метод — визуализировать взаимосвязь с помощью диаграммы рассеяния. Также для выявления выбросов можно использовать коробчатую диаграмму. Используя коробчатую диаграмму, мы можем легко определить контекстуальные выбросы, которые обычно представляют собой наблюдения, считающиеся аномальными в конкретном контексте. Контекстуальный выброс значительно отличается от остальных точек данных в конкретном контексте. Например, при анализе посетителей мы можем анализировать такие показатели как выручку, количество чеков и конверсию. Контекстные выбросы вряд ли проявятся как выбросы, если проводится только одномерный анализ посетителей. Они часто выявляются, когда мы рассматриваем посетителей в пределах конкретного контекста, например вместе с конверсией. Опять же, важно понимать контекст и цель анализа, прежде чем выбирать соответствующий метод для обработки двумерных выбросов. Мы рассмотрим, как идентифицировать двумерные выбросы, используя методы точечной диаграммы и коробчатой диаграммы.
Рассмотрим три точечные диаграммы посетителей с конверсией, выручкой и количеством чеков.
sns.set_style("darkgrid")
plt.figure(figsize = (40,18))
ax = sns.scatterplot(data= df0, x= 'visit', y ='conv', s = 200)
ax.set_xlabel('visit', fontsize = 30)
ax.set_ylabel('conv', fontsize = 30)
plt.xticks(fontsize=30)
plt.yticks(fontsize=30)
ax.set_title('Bivariate Outlier Analysis visit and conv', fontsize = 40)
plt.figure(figsize = (40,18))
ax = sns.scatterplot(data= df0, x= 'visit', y ='rev', s = 200)
ax.set_xlabel('visit', fontsize = 30)
ax.set_ylabel('rev', fontsize = 30)
plt.xticks(fontsize=30)
plt.yticks(fontsize=30)
ax.set_title('Bivariate Outlier Analysis visit and rev', fontsize = 40)
plt.figure(figsize = (40,18))
ax = sns.scatterplot(data= df0, x= 'visit', y ='ch', s = 200)
ax.set_xlabel('visit', fontsize = 30)
ax.set_ylabel('ch', fontsize = 30)
plt.xticks(fontsize=30)
plt.yticks(fontsize=30)
ax.set_title('Bivariate Outlier Analysis visit and ch', fontsize = 40)
Как видно, лучше всего выбросы видны на диаграмме посетители - конверсия, с этими параметрами будем работать дальше и для определение выбросов будем использовать расстояние Махаланобиса.
Расстояние Махаланобиса — это расстояние между заданной точкой и центром масс, делённое на ширину эллипсоида в направлении заданной точки. Оно отличается от расстояния Евклида тем, что учитывает корреляции между переменными и инвариантно к масштабу.
Оно часто используется для поиска выбросов в статистическом анализе, включающем несколько переменных. Для расчета этого расстояния используем функцию mahalanobis из библиотеки scipy.
Для начала отмасштабируем данные, чтобы все переменные имели одинаковый масштаб, с помощью класса StandardScaler.в scikit-learn
from sklearn.preprocessing import StandardScaler
from scipy.spatial.distance import mahalanobis
scaler = StandardScaler()
df0_scaled = scaler.fit_transform(df0.loc[:,['visit','conv']])
df0_scaled = pd.DataFrame(df0_scaled,columns=['visit','conv'])
df0_scaled.head()
visit | conv |
---|
0 | -0.050 | -0.371 |
---|
1 | 0.354 | -0.203 |
---|
2 | 0.625 | -0.306 |
---|
3 | -0.099 | -0.268 |
---|
4 | -0.190 | -0.239 |
---|
А теперь рассчитаем расстояние Махаланобиса в нашем двумерном пространстве посетители - конверсия.
mean = df0_scaled.mean()
cov = df0_scaled.cov()
inv_cov = np.linalg.inv(cov)
distances = []
for _, x in df0_scaled.iterrows():
d = mahalanobis(x, mean, inv_cov)
distances.append(d)
df0['mahalanobis_distances'] = distances
df0.loc[:,['visit','conv','mahalanobis_distances']].head(10)
visit | conv | mahalanobis_distances |
---|
0 | 413 | 0.061 | 0.394 |
---|
1 | 678 | 0.094 | 0.370 |
---|
2 | 856 | 0.074 | 0.638 |
---|
3 | 381 | 0.081 | 0.313 |
---|
4 | 321 | 0.087 | 0.346 |
---|
5 | 2556 | 0.009 | 3.185 |
---|
6 | 237 | 0.097 | 0.416 |
---|
7 | 24 | 0.958 | 4.066 |
---|
8 | 379 | 0.092 | 0.263 |
---|
9 | 596 | 0.094 | 0.273 |
---|
Чтобы идентифицировать выбросы построим блочную диаграмму
plt.figure(figsize = (40,18))
ax = sns.boxplot(data= df0, x= 'mahalanobis_distances')
ax.set_xlabel('Mahalanobis Distances', fontsize = 30)
plt.xticks(fontsize=30)
ax.set_title('Outlier Analysis of Mahalanobis Distances',fontsize = 40)
На блочной диаграмме определились все шесть выбросов. Выделим их с помощью IQR-метода.
Q1 = df0['mahalanobis_distances'].quantile(0.25)
Q3 = df0['mahalanobis_distances'].quantile(0.75)
IQR = Q3 - Q1
df0.loc[(df0['mahalanobis_distances'] < (Q1 - 1.5 *IQR)) |
(df0['mahalanobis_distances'] > (Q3 + 1.5 * IQR))]
rev | visit | ch | sales | lch | price | conv | mahalanobis_distances |
---|
5 | 195820 | 2556 | 23 | 95 | 4.130 | 2061.263 | 0.009 | 3.185 |
---|
7 | 174745 | 24 | 23 | 75 | 3.261 | 2329.933 | 0.958 | 4.066 |
---|
23 | 364848 | 4125 | 33 | 113 | 3.424 | 3228.743 | 0.008 | 5.597 |
---|
26 | 55927 | 1143 | 8 | 26 | 3.250 | 2151.038 | 0.007 | 1.119 |
---|
28 | 158917 | 21 | 18 | 48 | 2.667 | 3310.771 | 0.857 | 3.559 |
---|
43 | 521849 | 64 | 58 | 146 | 2.517 | 3574.308 | 0.906 | 3.810 |
---|
Обозначим выбросы посетителей как пропущенные значения
df0.loc[(df0['mahalanobis_distances'] < (Q1 - 1.5 * IQR))
|(df0['mahalanobis_distances'] > (Q3 + 1.5 * IQR)),'visit']=np.nan
Дальше наша задача заменить пропущенные в посетителях значения. Будем работать с новым набором данных
df1=df0.loc[:,['rev','visit','ch','sales','lch']].copy()
df1.isnull().sum()
rev 0
visit 6
ch 0
sales 0
lch 0
dtype: int64
Рассмотрим замену пропущенных значений методом к-ближайших соседей из библиотеки sklearn. Подготовим наши данные.
Набор данных с пропусками
dfnan=df1.iloc[random_list,:]
dfnan
rev | visit | ch | sales | lch |
---|
28 | 158917 | NaN | 18 | 48 | 2.667 |
---|
43 | 521849 | NaN | 58 | 146 | 2.517 |
---|
7 | 174745 | NaN | 23 | 75 | 3.261 |
---|
26 | 55927 | NaN | 8 | 26 | 3.250 |
---|
23 | 364848 | NaN | 33 | 113 | 3.424 |
---|
5 | 195820 | NaN | 23 | 95 | 4.130 |
---|
Набор данных без пропусков
dfnonan=df1.drop(random_list)
dfnonan.head()
rev | visit | ch | sales | lch |
---|
0 | 225795 | 413.0 | 25 | 88 | 3.520 |
---|
1 | 612489 | 678.0 | 64 | 188 | 2.938 |
---|
2 | 609786 | 856.0 | 63 | 179 | 2.841 |
---|
3 | 266596 | 381.0 | 31 | 78 | 2.516 |
---|
4 | 277030 | 321.0 | 28 | 71 | 2.536 |
---|
Подготовим наборы для обучения и для предсказания
X = pd.DataFrame(dfnonan.ch)
y = dfnonan['visit']
X_with_nan=pd.DataFrame(dfnan['ch'])
Метод к-ближайших соседей можно использовать для классификации и для регрессии, Рассмотрим оба метода для решения нашей задачи.
Вариант 1 - используем метод KNeighborsClassifier
В этом случае нашу задачу заполнения пропущенных значений можно рассматривать как задачу мультиклассовой классификации. Тогда, для того, чтобы сделать прогноз для новой точки данных (в нашем случае это будет количество чеков), алгоритм находит точку в обучающем наборе (в нашем случае количество посетителей), которая находится ближе всего к новой точке. Затем он присваивает метку (количество посетителей) , принадлежащую этой точке обучающего набора, новой точке данных.
Выведем метки классов нашей задачи
print(sorted(list(dfnonan.visit.unique())))
[181.0, 191.0, 193.0, 195.0, 200.0, 201.0, 207.0, 209.0, 210.0, 214.0, 215.0, 218.0, 219.0, 223.0, 226.0, 230.0, 236.0, 237.0, 246.0, 247.0, 257.0, 261.0, 265.0, 296.0, 321.0, 379.0, 381.0, 413.0, 420.0, 433.0, 446.0, 450.0, 474.0, 525.0, 527.0, 596.0, 614.0, 678.0, 856.0]
Также выведем значения количества чеков
print(sorted(list(dfnonan.ch.unique())))
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 29, 31, 32, 33, 34, 35, 39,
48, 52, 56, 63, 64]
Выведем также значения, по которым надо предсказать количество посетителей
print(sorted(list(X_with_nan.ch.unique())))
[8, 18, 23, 33, 58]
Загружаем библиотеку
from sklearn.neighbors import KNeighborsClassifier
Создаем модель и обучаем ее на наших данных. Для построения модели на обучающем
наборе вызываем метод fit объекта knn, который принимает в качестве аргумента
массив NumPy "X", содержащий обучающие данные и массив "y", соответствующий
обучающим меткам, в нашем случае это количество чеков и количество посетителей,
им соответствующих. Самым важным параметром в этом методе является количество
соседей, которое мы установим равным 1 :
clf = KNeighborsClassifier(n_neighbors=1, weights='distance')
trained_model = clf.fit(X, y)
Прогнозировать класс (посетителей) пропущенных значений, чтобы сделать прогноз,
мы вызываем метод predict объекта knn
imputed_values = trained_model.predict(X_with_nan)
imputed_values
array([226., 596., 237., 200., 433., 237.])
Объединим факт и прогноз
df_1=pd.DataFrame({'ch':dfnan['ch'],
'visit_act':df.loc[random_list,'visit'],'visit_clf':imputed_values})
df_1
ch | visit_act | visit_clf |
---|
28 | 18 | 196 | 226.0 |
---|
43 | 58 | 644 | 596.0 |
---|
7 | 23 | 220 | 237.0 |
---|
26 | 8 | 167 | 200.0 |
---|
23 | 33 | 413 | 433.0 |
---|
5 | 23 | 258 | 237.0 |
---|
И оценим ошибку прогноза по суммарному показателю
df_1.visit_clf.sum()/df_1.visit_act.sum()
1.016
Прогноз завысил количество посетителей на 1.6%
Вариант 2 - используем метод KNeighborsRegressor
В этом случае нашу задачу заполнения пропущенных значений можно рассматривать
как задачу регрессии. В качестве независимой переменной принимаем количество
чеков, в качестве зависимой - количество посетителей.
Алгоритм регрессии k ближайших соседей реализован в классе KNeighborsRegressor.
Он используется точно так же, как KNeighborsClassifier.
from sklearn.neighbors import KNeighborsRegressor
reg = KNeighborsRegressor(n_neighbors=3)
trained_model_reg = reg.fit(X, y)
imputed_values = trained_model_reg.predict(X_with_nan)
df_1['visit_reg']=imputed_values
df_1
ch | visit_act | visit_clf | visit_reg |
---|
28 | 18 | 196 | 226.0 | 214.000 |
---|
43 | 58 | 644 | 596.0 | 642.000 |
---|
7 | 23 | 220 | 237.0 | 219.667 |
---|
26 | 8 | 167 | 200.0 | 209.000 |
---|
23 | 33 | 413 | 433.0 | 434.333 |
---|
5 | 23 | 258 | 237.0 | 219.667 |
---|
Ошибка прогноза в этом случае
df_1.visit_reg.sum()/df_1.visit_act.sum()
Прогноз завысил количество посетителей на 2.1%.
Рассмотрим еще один метод заполнения пропущенных значений. Класс IterativeImputer библиотеки sklearn реализует многомерные алгоритмы восстановления пропущенных
значений, оценивая другие значения в наборе. Данный класс моделирует каждый
признак пропущенного значения как функцию от других признаков и использует оценку
для замены значений. IterativeImputer фактически итеративно строит модель регрессии, используя подмножества столбцов для прогнозирования отсутствующих значений.
Создадим для этого новый набор данных, с "урезанным" количеством признаков.
df11=df1.loc[:,['visit','ch','sales']].copy()
df11.isnull().sum()
visit 6
ch 0
sales 0
dtype: int64
Загружаем библиотеку и создаем экземпляр модели
from sklearn.impute import IterativeImputer
imputer=IterativeImputer()
Получаем восстановленные данные
imputer_data = imputer.transform(df11)
полученные данные преобразовываем в DataFrame
df011 = pd.DataFrame(imputer_data,columns = df11.columns)
Добавляем восстановленные данные в наш проверочный набор
df_1['visit_iimp']=df011.loc[random_list,'visit']
df_1
ch | visit_act | visit_clf | visit_reg | visit_imp | visit_iimp |
---|
28 | 18 | 196 | 226.0 | 214.000 | 204.968 | 209.444 |
---|
43 | 58 | 644 | 596.0 | 642.000 | 601.604 | 610.654 |
---|
7 | 23 | 220 | 237.0 | 219.667 | 222.417 | 280.110 |
---|
26 | 8 | 167 | 200.0 | 209.000 | 92.579 | 112.619 |
---|
23 | 33 | 413 | 433.0 | 434.333 | 430.058 | 399.189 |
---|
5 | 23 | 258 | 237.0 | 219.667 | 245.493 | 307.926 |
---|
И оценим точность
df_1.visit_iimp.sum()/df_1.visit_act.sum()
1.012
В этом варианте получили самую низкую ошибку прогноза 1,2%. Этот метод и выберем для дальнейшей работы. Пройдемся еще раз по алгоритму :
На входе имеем набор данных - дневные показатели торговли за определенный
период
df=pd.read_excel('data/sh001.xlsx')
df.head()
rev | visit | ch | sales | lch | price | conv |
---|
0 | 225795 | 413 | 25 | 88 | 3.520 | 2565.852 | 0.061 |
---|
1 | 612489 | 678 | 64 | 188 | 2.938 | 3257.920 | 0.094 |
---|
2 | 609786 | 856 | 63 | 179 | 2.841 | 3406.626 | 0.074 |
---|
3 | 266596 | 381 | 31 | 78 | 2.516 | 3417.897 | 0.081 |
---|
4 | 277030 | 321 | 28 | 71 | 2.536 | 3901.831 | 0.087 |
---|
Проверяем на наличие пропущенных значений
df.isnull().sum()
rev 0
visit 2
ch 0
sales 0
lch 0
price 0
conv 0
Определили два пропуска, теперь выявим выбросы с помощью расстояние Махаланобиса
Отмасштабируем данные
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df.loc[:,['visit','conv']])
df_scaled = pd.DataFrame(df_scaled,columns=['visit','conv'])
df_scaled.head()
Рассчитаем расстояние Махаланобиса и добавим его в наш набор
mean = df_scaled.mean()
cov = df_scaled.cov()
inv_cov = np.linalg.inv(cov)
distances = []
for _, x in df_scaled.iterrows():
d = mahalanobis(x, mean, inv_cov)
distances.append(d)
df['mahalanobis_distances'] = distances
df.loc[:,['visit','conv','mahalanobis_distances']].head(10)
visit | conv | mahalanobis_distances |
---|
0 | 413.0 | 0.061 | 0.398 |
---|
1 | 678.0 | 0.094 | 0.357 |
---|
2 | 856.0 | 0.074 | 0.619 |
---|
3 | 381.0 | 0.081 | 0.318 |
---|
4 | 321.0 | 0.087 | 0.351 |
---|
5 | 2556.0 | 0.009 | 3.114 |
---|
6 | 237.0 | 0.097 | 0.419 |
---|
7 | 24.0 | 0.958 | 4.070 |
---|
8 | 379.0 | 0.092 | 0.267 |
---|
9 | 596.0 | 0.094 | 0.264 |
---|
Выделим выбросы с помощью IQR-метода.
Q1 = df['mahalanobis_distances'].quantile(0.25)
Q3 = df['mahalanobis_distances'].quantile(0.75)
IQR = Q3 - Q1
df.loc[(df['mahalanobis_distances'] < (Q1 - 1.5 *IQR)) |
(df['mahalanobis_distances'] > (Q3 + 1.5 * IQR))]
rev | visit | ch | sales | lch | price | conv | mahalanobis_distances |
---|
5 | 195820 | 2556.0 | 23 | 95 | 4.130 | 2061.263 | 0.009 | 3.114 |
---|
7 | 174745 | 24.0 | 23 | 75 | 3.261 | 2329.933 | 0.958 | 4.070 |
---|
23 | 364848 | 4125.0 | 33 | 113 | 3.424 | 3228.743 | 0.008 | 5.481 |
---|
26 | 55927 | 1143.0 | 8 | 26 | 3.250 | 2151.038 | 0.007 | 1.093 |
---|
28 | 158917 | 21.0 | 18 | 48 | 2.667 | 3310.771 | 0.857 | 3.562 |
---|
43 | 521849 | 64.0 | 58 | 146 | 2.517 | 3574.308 | 0.906 | 3.814 |
---|
Обозначим выбросы как пропущенные значения
df.loc[(df['mahalanobis_distances'] < (Q1 - 1.5 * IQR))
|(df['mahalanobis_distances'] > (Q3 + 1.5 * IQR)),'visit']=np.nan
Удалим ненужную колонку и посмотрим на количество пропусков
df.drop(['mahalanobis_distances'], axis = 1,inplace=True)
df.isnull().sum()
rev 0
visit 8
ch 0
sales 0
lch 0
price 0
conv 0
dtype: int64
Сохраним индексы строк с пропусками
ind_nan=df[df['visit'].isnull()].index
ind_nan
Int64Index([5, 7, 15, 23, 26, 28, 39, 43], dtype='int64')
Заполним пропущенные значения с помощью класса IterativeImputer библиотеки sklearn, который реализует многомерные алгоритмы восстановления пропущенных значений
imputer=IterativeImputer()
imputer.fit(df)
imputer_data = imputer.transform(df)
df0 = pd.DataFrame(imputer_data,columns = df.columns)
df0.iloc[ind_nan]
rev | visit | ch | sales | lch | price | conv |
---|
5 | 195820.0 | 243.300 | 23.0 | 95.0 | 4.130 | 2061.263 | 0.009 |
---|
7 | 174745.0 | 220.271 | 23.0 | 75.0 | 3.261 | 2329.933 | 0.958 |
---|
15 | 310276.0 | 367.913 | 32.0 | 95.0 | 2.969 | 3266.063 | 0.071 |
---|
23 | 364848.0 | 427.432 | 33.0 | 113.0 | 3.424 | 3228.743 | 0.008 |
---|
26 | 55927.0 | 90.726 | 8.0 | 26.0 | 3.250 | 2151.038 | 0.007 |
---|
28 | 158917.0 | 202.842 | 18.0 | 48.0 | 2.667 | 3310.771 | 0.857 |
---|
39 | 156564.0 | 199.950 | 14.0 | 30.0 | 2.143 | 5218.800 | 0.064 |
---|
43 | 521849.0 | 598.589 | 58.0 | 146.0 | 2.517 | 3574.308 | 0.906 |
---|
Не забудем также пересчитать конверсия с заполненными значениями посетителей
df0['conv']=df0.ch/df0.visit
df0.iloc[ind_nan]
rev | visit | ch | sales | lch | price | conv |
---|
5 | 195820.0 | 243.300 | 23.0 | 95.0 | 4.130 | 2061.263 | 0.095 |
---|
7 | 174745.0 | 220.271 | 23.0 | 75.0 | 3.261 | 2329.933 | 0.104 |
---|
15 | 310276.0 | 367.913 | 32.0 | 95.0 | 2.969 | 3266.063 | 0.087 |
---|
23 | 364848.0 | 427.432 | 33.0 | 113.0 | 3.424 | 3228.743 | 0.077 |
---|
26 | 55927.0 | 90.726 | 8.0 | 26.0 | 3.250 | 2151.038 | 0.088 |
---|
28 | 158917.0 | 202.842 | 18.0 | 48.0 | 2.667 | 3310.771 | 0.089 |
---|
39 | 156564.0 | 199.950 | 14.0 | 30.0 | 2.143 | 5218.800 | 0.070 |
---|
43 | 521849.0 | 598.589 | 58.0 | 146.0 | 2.517 | 3574.308 | 0.097 |
---|
На этом работу с выбросами и пропущенными значениями можно считать законченной. Теперь наши данные готовы для дальнейшего анализа.
Комментариев нет:
Отправить комментарий