An Outlier is a data-item/object that deviates significantly from the rest of the (so-called normal)objects. They can be caused by measurement or execution errors. The analysis for outlier detection is referred to as outlier mining. There are many ways to detect the outliers, and the removal process is the data frame same as removing a data item from the panda’s data frame.
Here pandas data frame is used for a more realistic approach as in real-world projects need to detect the outliers arouse during the data analysis step, the same approach can be used on lists and series-type objects.
Dataset Used For Outlier Detection
The dataset used in this article is the Diabetes dataset and it is preloaded in the sklearn library.
Python3
import
sklearn
from
sklearn.datasets
import
load_diabetes
import
pandas as pd
import
matplotlib.pyplot as plt
diabetics
=
load_diabetes()
column_name
=
diabetics.feature_names
df_diabetics
=
pd.DataFrame(diabetics.data)
df_diabetics.columns
=
column_name
df_diabetics.head()
Output:
first five rows of the dataset
Outliers can be detected using visualization, implementing mathematical formulas on the dataset, or using the statistical approach. All of these are discussed below.
Outliers Visualization
Visualizing Outliers Using Box Plot
It captures the summary of the data effectively and efficiently with only a simple box and whiskers. Boxplot summarizes sample data using 25th, 50th, and 75th percentiles. One can just get insights(quartiles, median, and outliers) into the dataset by just looking at its boxplot.
Python3
import
seaborn as sns
sns.boxplot(df_diabetics[
'bmi'
])
Output:
Outliers present in the bmi columns
In the above graph, can clearly see that values above 10 are acting as outliers.
Python3
import
numpy as np
print
(np.where(df_diabetics[
'bmi'
]>
0.12
))
Output:
(array([ 32, 145, 256, 262, 366, 367, 405]),)
Visualizing Outliers Using ScatterPlot.
It is used when you have paired numerical data and when your dependent variable has multiple values for each reading independent variable, or when trying to determine the relationship between the two variables. In the process of utilizing the scatter plot, one can also use it for outlier detection.
To plot the scatter plot one requires two variables that are somehow related to each other. So here, ‘Proportion of non-retail business acres per town’ and ‘Full-value property-tax rate per $10,000’ are used whose column names are “INDUS” and “TAX” respectively.
Python3
fig, ax
=
plt.subplots(figsize
=
(
6
,
4
))
ax.scatter(df_diabetics[
'bmi'
],df_diabetics[
'bp'
])
ax.set_xlabel(
'(body mass index of people)'
)
ax.set_ylabel(
'(bp of the people )'
)
plt.show()
Output:
Scatter plot of bp and bmi
Looking at the graph can summarize that most of the data points are in the bottom left corner of the graph but there are few points that are exactly;y opposite that is the top right corner of the graph. Those points in the top right corner can be regarded as Outliers.
Using approximation can say all those data points that are x>20 and y>600 are outliers. The following code can fetch the exact position of all those points that satisfy these conditions.
Outliers in BMI and BP Column Combined
Python3
print
(np.where((df_diabetics[
'bmi'
]>
0.12
) & (df_diabetics[
'bp'
]<
0.8
)))
Output:
(array([ 32, 145, 256, 262, 366, 367, 405]),)
Z-score
Z- Score is also called a standard score. This value/score helps to understand that how far is the data point from the mean. And after setting up a threshold value one can utilize z score values of data points to define the outliers.
Zscore = (data_point -mean) / std. deviation
Python3
from
scipy
import
stats
import
numpy as np
z
=
np.
abs
(stats.zscore(df_diabetics[
'age'
]))
print
(z)
Output:
[0.80050009 0.03956713 1.79330681 1.87244107 0.11317236 1.94881082 0.9560041 1.33508832 0.87686984 1.49059233 2.02518057 0.57139085 0.34228161 0.11317236 0.95323959 1.1087436 0.11593688 1.48782782 0.80326461 0.57415536 1.03237385 1.79607132 1.79607132 0.95323959 1.33785284 1.41422259 2.25428981 0.49778562 1.10597908 1.41145807 1.26148309 0.49778562 0.72413034 0.6477606 0.34228161 1.02960933 0.26591186 0.19230663 0.03956713 0.03956713 0.11317236 2.10155031 1.26148309 0.41865135 0.95323959 0.57139085 1.18511334 1.64333183 1.41145807 0.87963435 0.72413034 1.25871858 1.1087436 0.19230663 1.03237385 0.87963435 0.87963435 0.57415536 0.87686984 1.33508832 1.49059233 0.87963435 0.57415536 0.72689486 1.41145807 0.9560041 0.19230663 0.87686984 0.80050009 0.34228161 0.03956713 0.03956713 1.33508832 0.26591186 0.26591186 0.19230663 0.65052511 2.02518057 0.11317236 2.17792006 1.48782782 0.26591186 0.34504612 0.80326461 0.03680262 0.95323959 1.49059233 0.95323959 1.1087436 0.9560041 0.26591186 0.95323959 0.42141587 1.03237385 1.64333183 1.49059233 1.18234883 0.57415536 0.03680262 0.03956713 0.34228161 0.34228161]
The above output is just a snapshot of part of the data; the actual length of the list(z) is 506 that is the number of rows. It prints the z-score values of each data item of the column
Now to define an outlier threshold value is chosen which is generally 3.0. As 99.7% of the data points lie between +/- 3 standard deviation (using Gaussian Distribution approach).
Rows where Z value is greater than 2
Python3
threshold
=
2
print
(np.where(z >
2
))
Output:
(array([ 10, 26, 41, 77, 79, 106, 131, 204, 223, 226, 242, 311, 321,344, 374, 402]),)
IQR (Inter Quartile Range)
IQR (Inter Quartile Range) Inter Quartile Range approach to finding the outliers is the most commonly used and most trusted approach used in the research field.
IQR = Quartile3 – Quartile1
Python3
Q1
=
np.percentile(df_diabetics[
'bmi'
],
25
, method
=
'midpoint'
)
Q3
=
np.percentile(df_diabetics[
'bmi'
],
75
, method
=
'midpoint'
)
IQR
=
Q3
-
Q1
print
(IQR)
Output:
0.06520763046978838
Syntax: numpy.percentile(arr, n, axis=None, out=None)
Parameters :
arr :input array.
n : percentile value.
To define the outlier base value is defined above and below dataset’s normal range namely Upper and Lower bounds, define the upper and the lower bound (1.5*IQR value is considered) :
upper = Q3 +1.5*IQR
lower = Q1 – 1.5*IQR
In the above formula as according to statistics, the 0.5 scale-up of IQR (new_IQR = IQR + 0.5*IQR) is taken, to consider all the data between 2.7 standard deviations in the Gaussian Distribution.
Python3
upper
=
Q3
+
1.5
*
IQR
upper_array
=
np.array(df_diabetics[
'bmi'
]>
=
upper)
print
(
"Upper Bound:"
,upper)
print
(upper_array.
sum
())
lower
=
Q1
-
1.5
*
IQR
lower_array
=
np.array(df_diabetics[
'bmi'
]<
=
lower)
print
(
"Lower Bound:"
,lower)
print
(lower_array.
sum
())
Output: Upper Bound: 0.12879000811776306 3 Lower Bound: -0.13204051376139045 0
Removing the outliers
For removing the outlier, one must follow the same process of removing an entry from the dataset using its exact position in the dataset because in all the above methods of detecting the outliers end result is the list of all those data items that satisfy the outlier definition according to the method used.
References: How to delete exactly one row in python?
dataframe.drop(row index,inplace=True)
The above code can be used to drop a row from the dataset given the row_indexes to be dropped. Inplace =True is used to tell Python to make the required change in the original dataset. row_index can be only one value or list of values or NumPy array but it must be one dimensional.
Example:
df_diabetics.drop(lists[0],inplace = True)
Full Code: Detecting the outliers using IQR and removing them.
Python3
import
sklearn
from
sklearn.datasets
import
load_diabetes
import
pandas as pd
diabetes
=
load_diabetes()
column_name
=
diabetes.feature_names
df_diabetes
=
pd.DataFrame(diabetes.data)
df_diabetes .columns
=
column_name
df_diabetes .head()
print
(
"Old Shape: "
, df_diabetes.shape)
Q1
=
df_diabetes[
'bmi'
].quantile(
0.25
)
Q3
=
df_diabetes[
'bmi'
].quantile(
0.75
)
IQR
=
Q3
-
Q1
lower
=
Q1
-
1.5
*
IQR
upper
=
Q3
+
1.5
*
IQR
upper_array
=
np.where(df_diabetes[
'bmi'
]>
=
upper)[
0
]
lower_array
=
np.where(df_diabetes[
'bmi'
]<
=
lower)[
0
]
df_diabetes.drop(index
=
upper_array, inplace
=
True
)
df_diabetes.drop(index
=
lower_array, inplace
=
True
)
print
(
"New Shape: "
, df_diabetes.shape)
Output:
Old Shape: (442, 10) New Shape: (439, 10)
Before answering the actual question we should ask another one that’s very relevant depending on the nature of your data:
What is an outlier?
Imagine the series of values [3, 2, 3, 4, 999]
(where the 999
seemingly doesn’t fit in) and analyse various ways of outlier detection
Z-Score
The problem here is that the value in question distorts our measures mean
and std
heavily, resulting in inconspicious z-scores of roughly [-0.5, -0.5, -0.5, -0.5, 2.0]
, keeping every value within two standard deviations of the mean. One very large outlier might hence distort your whole assessment of outliers. I would discourage this approach.
Quantile Filter
A way more robust approach is given is this answer, eliminating the bottom and top 1% of data. However, this eliminates a fixed fraction independant of the question if these data are really outliers. You might loose a lot of valid data, and on the other hand still keep some outliers if you have more than 1% or 2% of your data as outliers.
IQR-distance from Median
Even more robust version of the quantile principle: Eliminate all data that is more than f
times the interquartile range away from the median of the data. That’s also the transformation that sklearn
‘s RobustScaler
uses for example. IQR and median are robust to outliers, so you outsmart the problems of the z-score approach.
In a normal distribution, we have roughly iqr=1.35*s
, so you would translate z=3
of a z-score filter to f=2.22
of an iqr-filter. This will drop the 999
in the above example.
The basic assumption is that at least the «middle half» of your data is valid and resembles the distribution well, whereas you also mess up if your distribution has wide tails and a narrow q_25% to q_75% interval.
Advanced Statistical Methods
Of course there are fancy mathematical methods like the Peirce criterion, Grubb’s test or Dixon’s Q-test just to mention a few that are also suitable for non-normally distributed data. None of them are easily implemented and hence not addressed further.
Code
Replacing all outliers for all numerical columns with np.nan
on an example data frame. The method is robust against all dtypes that pandas provides and can easily be applied to data frames with mixed types:
import pandas as pd
import numpy as np
# sample data of all dtypes in pandas (column 'a' has an outlier) # dtype:
df = pd.DataFrame({'a': list(np.random.rand(8)) + [123456, np.nan], # float64
'b': [0,1,2,3,np.nan,5,6,np.nan,8,9], # int64
'c': [np.nan] + list("qwertzuio"), # object
'd': [pd.to_datetime(_) for _ in range(10)], # datetime64[ns]
'e': [pd.Timedelta(_) for _ in range(10)], # timedelta[ns]
'f': [True] * 5 + [False] * 5, # bool
'g': pd.Series(list("abcbabbcaa"), dtype="category")}) # category
cols = df.select_dtypes('number').columns # limits to a (float), b (int) and e (timedelta)
df_sub = df.loc[:, cols]
# OPTION 1: z-score filter: z-score < 3
lim = np.abs((df_sub - df_sub.mean()) / df_sub.std(ddof=0)) < 3
# OPTION 2: quantile filter: discard 1% upper / lower values
lim = np.logical_and(df_sub < df_sub.quantile(0.99, numeric_only=False),
df_sub > df_sub.quantile(0.01, numeric_only=False))
# OPTION 3: iqr filter: within 2.22 IQR (equiv. to z-score < 3)
iqr = df_sub.quantile(0.75, numeric_only=False) - df_sub.quantile(0.25, numeric_only=False)
lim = np.abs((df_sub - df_sub.median()) / iqr) < 2.22
# replace outliers with nan
df.loc[:, cols] = df_sub.where(lim, np.nan)
To drop all rows that contain at least one nan-value:
df.dropna(subset=cols, inplace=True) # drop rows with NaN in numerical columns
# or
df.dropna(inplace=True) # drop rows with NaN in any column
Using pandas 1.3 functions:
pandas.DataFrame.select_dtypes()
pandas.DataFrame.quantile()
pandas.DataFrame.where()
pandas.DataFrame.dropna()
Ни одна модель машинного обучения не выдаст осмысленных результатов, если вы предоставите ей сырые данные. После формирования выборки данных их необходимо очистить.
Очистка данных – это процесс обнаружения и исправления (или удаления) поврежденных или неточных записей из набора записей, таблицы или базы данных. Процесс включает в себя выявление неполных, неправильных, неточных или несущественных данных, а затем замену, изменение или удаление «загрязненных» данных.
Определение очень длинное и не очень понятное
Чтобы детально во всем разобраться, мы разбили это определение на составные части и создали пошаговый гайд по очистке данных на Python. Здесь мы разберем методы поиска и исправления:
- отсутствующих данных;
- нетипичных данных – выбросов;
- неинформативных данных – дубликатов;
- несогласованных данных – одних и тех же данных, представленных в разных регистрах или форматах.
Для работы с данными мы использовали Jupyter Notebook и библиотеку Pandas.
***
Базой для наших экспериментов послужит набор данных по ценам на жилье в России, найденный на Kaggle. Мы не станем очищать всю базу целиком, но разберем на ее основе главные методы и операции.
Прежде чем переходить к процессу очистки, всегда нужно представлять исходный датасет. Давайте быстро взглянем на сами данные:
# импорт пакетов
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import matplotlib
plt.style.use('ggplot')
from matplotlib.pyplot import figure
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (12,8)
pd.options.mode.chained_assignment = None
# чтение данных
df = pd.read_csv('sberbank.csv')
# shape and data types of the data
print(df.shape)
print(df.dtypes)
# отбор числовых колонок
df_numeric = df.select_dtypes(include=[np.number])
numeric_cols = df_numeric.columns.values
print(numeric_cols)
# отбор нечисловых колонок
df_non_numeric = df.select_dtypes(exclude=[np.number])
non_numeric_cols = df_non_numeric.columns.values
print(non_numeric_cols)
Этот код покажет нам, что набор данных состоит из 30471 строки и 292 столбцов. Мы увидим, являются ли эти столбцы числовыми или категориальными признаками.
Теперь мы можем пробежаться по чек-листу «грязных» типов данных и очистить их один за другим.
***
1. Отсутствующие данные
Работа с отсутствующими значениями – одна из самых сложных, но и самых распространенных проблем очистки. Большинство моделей не предполагают пропусков.
1.1. Как обнаружить?
Рассмотрим три метода обнаружения отсутствующих данных в наборе.
1.1.1. Тепловая карта пропущенных значений
Когда признаков в наборе не очень много, визуализируйте пропущенные значения с помощью тепловой карты.
cols = df.columns[:30] # первые 30 колонок
# определяем цвета
# желтый - пропущенные данные, синий - не пропущенные
colours = ['#000099', '#ffff00']
sns.heatmap(df[cols].isnull(), cmap=sns.color_palette(colours))
Приведенная ниже карта демонстрирует паттерн пропущенных значений для первых 30 признаков набора. По горизонтальной оси расположены признаки, по вертикальной – количество записей/строк. Желтый цвет соответствует пропускам данных.
Заметно, например, что признак life_sq
имеет довольно много пустых строк, а признак floor
– напротив, всего парочку – около 7000 строки.
1.1.2. Процентный список пропущенных данных
Если в наборе много признаков и визуализация занимает много времени, можно составить список долей отсутствующих записей для каждого признака.
for col in df.columns:
pct_missing = np.mean(df[col].isnull())
print('{} - {}%'.format(col, round(pct_missing*100)))
Такой список для тех же 30 первых признаков выглядит следующим образом:
У признака life_sq
отсутствует 21% значений, а у floor
– только 1%.
Этот список является полезным резюме, которое может отлично дополнить визуализацию тепловой карты.
1.1.3. Гистограмма пропущенных данных
Еще одна хорошая техника визуализации для наборов с большим количеством признаков – построение гистограммы для числа отсутствующих значений в записи.
# сначала создаем индикатор для признаков с пропущенными данными
for col in df.columns:
missing = df[col].isnull()
num_missing = np.sum(missing)
if num_missing > 0:
print('created missing indicator for: {}'.format(col))
df['{}_ismissing'.format(col)] = missing
# затем на основе индикатора строим гистограмму
ismissing_cols = [col for col in df.columns if 'ismissing' in col]
df['num_missing'] = df[ismissing_cols].sum(axis=1)
df['num_missing'].value_counts().reset_index().sort_values(by='index').plot.bar(x='index', y='num_missing')
Отсюда понятно, что из 30 тыс. записей более 6 тыс. строк не имеют ни одного пропущенного значения, а еще около 4 тыс.– всего одно. Такие строки можно использовать в качестве «эталонных» для проверки различных гипотез по дополнению данных.
1.2. Что делать с пропущенными значениями?
Не существует общих решений для проблемы отсутствующих данных. Для каждого конкретного набора приходится искать наиболее подходящие методы или их комбинации.
Разберем четыре самых распространенных техники. Они помогут в простых ситуациях, но, скорее всего, придется проявить творческий подход и поискать нетривиальные решения, например, промоделировать пропуски.
1.2.1. Отбрасывание записей
Первая техника в статистике называется методом удаления по списку и заключается в простом отбрасывании записи, содержащей пропущенные значения. Это решение подходит только в том случае, если недостающие данные не являются информативными.
Для отбрасывания можно использовать и другие критерии. Например, из гистограммы, построенной в предыдущем разделе, мы узнали, что лишь небольшое количество строк содержат более 35 пропусков. Мы можем создать новый набор данных df_less_missing_rows
, в котором отбросим эти строки.
# отбрасываем строки с большим количеством пропусков
ind_missing = df[df['num_missing'] > 35].index
df_less_missing_rows = df.drop(ind_missing, axis=0)
1.2.2. Отбрасывание признаков
Как и предыдущая техника, отбрасывание признаков может применяться только для неинформативных признаков.
В процентном списке, построенном ранее, мы увидели, что признак hospital_beds_raion
имеет высокий процент недостающих значений – 47%. Мы можем полностью отказаться от этого признака:
cols_to_drop = ['hospital_beds_raion']
df_less_hos_beds_raion = df.drop(cols_to_drop, axis=1)
1.2.3. Внесение недостающих значений
Для численных признаков можно воспользоваться методом принудительного заполнения пропусков. Например, на место пропуска можно записать среднее или медианное значение, полученное из остальных записей.
Для категориальных признаков можно использовать в качестве заполнителя наиболее часто встречающееся значение.
Возьмем для примера признак life_sq
и заменим все недостающие значения медианой этого признака:
med = df['life_sq'].median()
print(med)
df['life_sq'] = df['life_sq'].fillna(med)
Одну и ту же стратегию принудительного заполнения можно применить сразу для всех числовых признаков:
# impute the missing values and create the missing value indicator variables for each numeric column.
df_numeric = df.select_dtypes(include=[np.number])
numeric_cols = df_numeric.columns.values
for col in numeric_cols:
missing = df[col].isnull()
num_missing = np.sum(missing)
if num_missing > 0: # only do the imputation for the columns that have missing values.
print('imputing missing values for: {}'.format(col))
df['{}_ismissing'.format(col)] = missing
med = df[col].median()
df[col] = df[col].fillna(med)
К счастью, в нашем наборе не нашлось пропусков в категориальных признаках. Но это не мешает нам продемонстрировать использование той же стратегии:
df_non_numeric = df.select_dtypes(exclude=[np.number])
non_numeric_cols = df_non_numeric.columns.values
for col in non_numeric_cols:
missing = df[col].isnull()
num_missing = np.sum(missing)
if num_missing > 0: # only do the imputation for the columns that have missing values.
print('imputing missing values for: {}'.format(col))
df['{}_ismissing'.format(col)] = missing
top = df[col].describe()['top'] # impute with the most frequent value.
df[col] = df[col].fillna(top)
1.2.4. Замена недостающих значений
Можно использовать некоторый дефолтный плейсхолдер для пропусков, например, новую категорию _MISSING_
для категориальных признаков или число -999
для числовых.
Таким образом, мы сохраняем данные о пропущенных значениях, что тоже может быть ценной информацией.
# категориальные признаки
df['sub_area'] = df['sub_area'].fillna('_MISSING_')
# численные признаки
df['life_sq'] = df['life_sq'].fillna(-999)
***
2. Нетипичные данные (выбросы)
Выбросы – это данные, которые существенно отличаются от других наблюдений. Они могут соответствовать реальным отклонениям, но могут быть и просто ошибками.
2.1. Как обнаружить выбросы?
Для численных и категориальных признаков используются разные методы изучения распределения, позволяющие обнаружить выбросы.
2.1.1. Гистограмма/коробчатая диаграмма
Если признак численный, можно построить гистограмму или коробчатую диаграмму (ящик с усами). Посмотрим на примере уже знакомого нам признака life_sq
.
df['life_sq'].hist(bins=100)
Из-за возможных выбросов данные выглядят сильно искаженными.
Чтобы изучить особенность поближе, построим коробчатую диаграмму.
df.boxplot(column=['life_sq'])
Видим, что есть выброс со значением более 7000.
2.1.2. Описательная статистика
Отклонения численных признаков могут быть слишком четкими, чтобы не визуализироваться коробчатой диаграммой. Вместо этого можно проанализировать их описательную статистику.
Например, для признака life_sq
видно, что максимальное значение равно 7478, в то время как 75% квартиль равен только 43. Значение 7478 – выброс.
df['life_sq'].describe()
2.1.3. Столбчатая диаграмма
Для категориальных признаков можно построить столбчатую диаграмму – для визуализации данных о категориях и их распределении.
Например, распределение признака ecology
вполне равномерно и допустимо. Но если существует категория только с одним значением "другое"
, то это будет выброс.
df['ecology'].value_counts().plot.bar()
2.1.4. Другие методы
Для обнаружения выбросов можно использовать другие методы, например, построение точечной диаграммы, z-оценку или кластеризацию. В этом руководстве они не рассматриваются.
2.2. Что делать?
Выбросы довольно просто обнаружить, но выбор способа их устранения слишком существенно зависит от специфики набора данных и целей проекта. Их обработка во многом похожа на обработку пропущенных данных, которую мы разбирали в предыдущем разделе. Можно удалить записи или признаки с выбросами, либо скорректировать их, либо оставить без изменений.
***
Переходим к более простой части очистки данных – удалению мусора.
Вся информация, поступающая в модель, должна служить целям проекта. Если она не добавляет никакой ценности, от нее следует избавиться.
Три основных типа «ненужных» данных:
- неинформативные признаки с большим количеством одинаковых значений,
- нерелевантные признаки,
- дубликаты записей.
Рассмотрим работу с каждым типом отдельно.
3. Неинформативные признаки
Если признак имеет слишком много строк с одинаковыми значениями, он не несет полезной информации для проекта.
3.1. Как обнаружить?
Составим список признаков, у которых более 95% строк содержат одно и то же значение.
num_rows = len(df.index)
low_information_cols = [] #
for col in df.columns:
cnts = df[col].value_counts(dropna=False)
top_pct = (cnts/num_rows).iloc[0]
if top_pct > 0.95:
low_information_cols.append(col)
print('{0}: {1:.5f}%'.format(col, top_pct*100))
print(cnts)
print()
Теперь можно последовательно перебрать их и определить, несут ли они полезную информацию.
3.2. Что делать?
Если после анализа причин получения повторяющихся значений вы пришли к выводу, что признак не несет полезной информации, используйте drop()
.
4. Нерелевантные признаки
Нерелевантные признаки обнаруживаются ручным отбором и оценкой значимости. Например, признак, регистрирующий температуру воздуха в Торонто точно не имеет никакого отношения к прогнозированию цен на российское жилье. Если признак не имеет значения для проекта, его нужно исключить.
5. Дубликаты записей
Если значения признаков (всех или большинства) в двух разных записях совпадают, эти записи называются дубликатами.
5.1. Как обнаружить повторяющиеся записи?
Способ обнаружения дубликатов зависит от того, что именно мы считаем дубликатами. Например, в наборе данных есть уникальный идентификатор id
. Если две записи имеют одинаковый id
, мы считаем, что это одна и та же запись. Удалим все неуникальные записи:
# отбрасываем неуникальные строки
df_dedupped = df.drop('id', axis=1).drop_duplicates()
# сравниваем формы старого и нового наборов
print(df.shape)
print(df_dedupped.shape)
Получаем в результате 10 отброшенных дубликатов:
Другой распространенный способ вычисления дубликатов: по набору ключевых признаков. Например, неуникальными можно считать записи с одной и той же площадью жилья, ценой и годом постройки.
Найдем в нашем наборе дубликаты по группе критических признаков – full_sq
, life_sq
, floor
, build_year
, num_room
, price_doc
:
key = ['timestamp', 'full_sq', 'life_sq', 'floor', 'build_year', 'num_room', 'price_doc']
df.fillna(-999).groupby(key)['id'].count().sort_values(ascending=False).head(20)
Получаем в результате 16 дублирующихся записей:
5.2. Что делать с дубликатами?
Очевидно, что повторяющиеся записи нам не нужны, значит, их нужно исключить из набора.
Вот так выглядит удаление дубликатов, основанное на наборе ключевых признаков:
key = ['timestamp', 'full_sq', 'life_sq', 'floor', 'build_year', 'num_room', 'price_doc']
df_dedupped2 = df.drop_duplicates(subset=key)
print(df.shape)
print(df_dedupped2.shape)
В результате новый набор df_dedupped2
стал короче на 16 записей.
***
Большая проблема очистки данных – разные форматы записей. Для корректной работы модели важно, чтобы набор данных соответствовал определенным стандартам – необходимо тщательное исследование с учетом специфики самих данных. Мы рассмотрим четыре самых распространенных несогласованности:
- Разные регистры символов.
- Разные форматы данных (например, даты).
- Опечатки в значениях категориальных признаков.
- Адреса.
6. Разные регистры символов
Непоследовательное использование разных регистров в категориальных значениях является очень распространенной ошибкой, которая может существенно повлиять на анализ данных.
6.1. Как обнаружить?
Давайте посмотрим на признак sub_area
:
df['sub_area'].value_counts(dropna=False)
В нем содержатся названия населенных пунктов. Все выглядит вполне стандартизированным:
Но если в какой-то записи вместо Poselenie Sosenskoe
окажется poselenie sosenskoe
, они будут расценены как два разных значения.
6.2. Что делать?
Эта проблема легко решается принудительным изменением регистра:
# пусть все будет в нижнем регистре
df['sub_area_lower'] = df['sub_area'].str.lower()
df['sub_area_lower'].value_counts(dropna=False)
7. Разные форматы данных
Ряд данных в наборе находится не в том формате, с которым нам было бы удобно работать. Например, даты, записанные в виде строки, следует преобразовать в формат DateTime
.
7.1. Как обнаружить?
Признак timestamp
представляет собой строку, хотя является датой:
df
7.2. Что же делать?
Чтобы было проще анализировать транзакции по годам и месяцам, значения признака timestamp
следует преобразовать в удобный формат:
df['timestamp_dt'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d')
df['year'] = df['timestamp_dt'].dt.year
df['month'] = df['timestamp_dt'].dt.month
df['weekday'] = df['timestamp_dt'].dt.weekday
print(df['year'].value_counts(dropna=False))
print()
print(df['month'].value_counts(dropna=False))
Взгляните также на публикацию How To Manipulate Date And Time In Python Like A Boss.
8. Опечатки
Опечатки в значениях категориальных признаков приводят к таким же проблемам, как и разные регистры символов.
8.1. Как обнаружить?
Для обнаружения опечаток требуется особый подход. В нашем наборе данных о недвижимости опечаток нет, поэтому для примера создадим новый набор. В нем будет признак city
, а его значениями будут torontoo
и tronto
. В обоих случаях это опечатки, а правильное значение – toronto
.
Простой способ идентификации подобных элементов – нечеткая логика или редактирование расстояния. Суть этого метода заключается в измерении количества букв (расстояния), которые нам нужно изменить, чтобы из одного слова получить другое.
Предположим, нам известно, что в признаке city
должно находиться одно из четырех значений: toronto
, vancouver
, montreal
или calgary
. Мы вычисляем расстояние между всеми значениями и словом toronto
(и vancouver
).
Те слова, в которых содержатся опечатки, имеют меньшее расстояние с правильным словом, так как отличаются всего на пару букв.
from nltk.metrics import edit_distance
df_city_ex = pd.DataFrame(data={'city': ['torontoo', 'toronto', 'tronto', 'vancouver', 'vancover', 'vancouvr', 'montreal', 'calgary']})
df_city_ex['city_distance_toronto'] = df_city_ex['city'].map(lambda x: edit_distance(x, 'toronto'))
df_city_ex['city_distance_vancouver'] = df_city_ex['city'].map(lambda x: edit_distance(x, 'vancouver'))
df_city_ex
8.2. Что делать?
Мы можем установить критерии для преобразования этих опечаток в правильные значения.
Например, если расстояние некоторого значения от слова toronto
не превышает 2 буквы, мы преобразуем это значение в правильное – toronto
.
msk = df_city_ex['city_distance_toronto'] <= 2
df_city_ex.loc[msk, 'city'] = 'toronto'
msk = df_city_ex['city_distance_vancouver'] <= 2
df_city_ex.loc[msk, 'city'] = 'vancouver'
df_city_ex
9. Адреса
Адреса – ужасная головная боль для всех аналитиков данных. Ведь мало кто следует стандартному формату, вводя свой адрес в базу данных.
9.1. Как обнаружить?
Проще предположить, что проблема разных форматов адреса точно существует. Даже если визуально вы не обнаружили беспорядка в этом признаке, все равно стоит стандартизировать их для надежности.
В нашем наборе данных по соображениям конфиденциальности отсутствует признак адреса, поэтому создадим новый набор df_add_ex
:
df_add_ex = pd.DataFrame(['123 MAIN St Apartment 15', '123 Main Street Apt 12 ', '543 FirSt Av', ' 876 FIRst Ave.'], columns=['address'])
df_add_ex
Признак адреса здесь загрязнен:
9.2. Что делать?
Минимальное форматирование включает следующие операции:
- приведение всех символов к нижнему регистру;
- удаление пробелов в начале и конце строки;
- удаление точек;
- стандартизация формулировок: замена
street
наst
,apartment
наapt
и т. д.
df_add_ex['address_std'] = df_add_ex['address'].str.lower()
df_add_ex['address_std'] = df_add_ex['address_std'].str.strip()
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\.', '')
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\bstreet\b', 'st')
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\bapartment\b', 'apt')
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\bav\b', 'ave')
df_add_ex
Теперь признак стал намного чище:
***
Мы сделали это! Это был долгий и трудный путь, но теперь все «грязные» данные очищены и готовы к анализу, а вы стали спецом по чистке данных
У нас есть еще куча полезных статей по Data Science, например, среди недавних:
- 10 Data Science книг к прочтению в 2020 году
- 10 инструментов искусственного интеллекта Google, доступных каждому
- 100+ лекций экспертов Постнауки об анализе данных, ИИ, роботах, математике и сетях
Или просто посмотрите тег Data Science.
Параллельно с выходом материала «Обнаружение выбросов в R» предлагаем посмотреть, как те же методы обнаружения выбросов реализовать в Python.
Данные
Для наглядности эксперимента возьмём тот же пакет данных mpg — скачать его в виде csv-таблицы можно с GitHub. Импортируем библиотеки и читаем таблицу в DataFrame:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.read_csv('mpg.csv')
Минимальные и максимальные значения
Тут всё просто. Выводим описание всего датасета методом describe():
df.describe()
Гистограмма
Такой график тоже можно построить в одну строку, используя внутренние средства библиотеки pandas:
df.hwy.plot(kind='hist', density=1, bins=20, stacked=False, alpha=.5, color='grey')
Box plot
В случае ящика с усами далеко идти тоже не приходится — в pandas есть метод и для этого:
_, bp = df.hwy.plot.box(return_type='both')
Получим точки с графика и выведем их в таблице, используя объект bp:
outliers = [flier.get_ydata() for flier in bp["fliers"]][0]
df[df.hwy.isin(outliers)]
Процентили
При помощи метода quantile получаем соответствующую нижнюю и верхнюю границы, а затем выводим всё, что выходит за их рамки:
lower_bound = df.hwy.quantile(q=0.025)
upper_bound = df.hwy.quantile(q=0.975)
df[(df.hwy < lower_bound) | (df.hwy > upper_bound)]
Фильтр Хэмпеля
Мы используем реализацию фильтра Хэмпеля, найденную на StackOverflow
Опишем функцию, которая заменяет на nan все значения, у которых разница с медианой больше, чем три медианных абсолютных отклонения.
def hampel(vals_orig):
vals = vals_orig.copy()
difference = np.abs(vals.median()-vals)
median_abs_deviation = difference.median()
threshold = 3 * median_abs_deviation
outlier_idx = difference > threshold
vals[outlier_idx] = np.nan
return(vals)
И применим к нашему набору данных:
hampel(df.hwy)
0 29.0
1 29.0
2 31.0
3 30.0
4 26.0
...
229 28.0
230 29.0
231 26.0
232 26.0
233 26.0
Name: hwy, Length: 234, dtype: float64
В выводе нет nan-значений, а значит и выбросов фильтр Хэмпеля не обнаружил.
Тест Граббса
Автор реализации теста Граббса и теста Рознера для Python
Опишем три функции: первая находит значение критерия Граббса и максимальное значение в наборе данных, вторая — критическое значение с учётом объёма выборки и уровня значимости, а третья проверяет, является ли значение с максимальным индексом выбросом:
import numpy as np
from scipy import stats
def grubbs_stat(y):
std_dev = np.std(y)
avg_y = np.mean(y)
abs_val_minus_avg = abs(y - avg_y)
max_of_deviations = max(abs_val_minus_avg)
max_ind = np.argmax(abs_val_minus_avg)
Gcal = max_of_deviations / std_dev
print(f"Grubbs Statistics Value: {Gcal}")
return Gcal, max_ind
def calculate_critical_value(size, alpha):
t_dist = stats.t.ppf(1 - alpha / (2 * size), size - 2)
numerator = (size - 1) * np.sqrt(np.square(t_dist))
denominator = np.sqrt(size) * np.sqrt(size - 2 + np.square(t_dist))
critical_value = numerator / denominator
print(f"Grubbs Critical Value: {critical_value}")
return critical_value
def check_G_values(Gs, Gc, inp, max_index):
if Gs > Gc:
print(f"{inp[max_index]} is an outlier")
else:
print(f"{inp[max_index]} is not an outlier")
Заменим значение в 34 строке на 212:
df.hwy[34] = 212
И выполним три функции:
Gcritical = calculate_critical_value(len(df.hwy), 0.05)
Gstat, max_index = grubbs_stat(df.hwy)
check_G_values(Gstat, Gcritical, df.hwy, max_index)
Grubbs Critical Value: 3.652090929984981
Grubbs Statistics Value: 13.745808761040397
212 is an outlier
Тест Рознера
Для теста Рознера достаточно дописать одну функцию, которая принимает набор данных, уровень значимости и число потенциальных выбросов:
def ESD_test(input_series, alpha, max_outliers):
for iteration in range(max_outliers):
Gcritical = calculate_critical_value(len(input_series), alpha)
Gstat, max_index = grubbs_stat(input_series)
check_G_values(Gstat, Gcritical, input_series, max_index)
input_series = np.delete(input_series, max_index)
Используя функцию на нашем наборе данных получаем, что значение 212 является выбросом, а 44 — нет:
ESD_test(np.array(df.hwy), 0.05, 3)
Grubbs Critical Value: 3.652090929984981
Grubbs Statistics Value: 13.745808761040408
212 is an outlier
Grubbs Critical Value: 3.6508358337727187
Grubbs Statistics Value: 3.455960616168714
44 is not an outlier
Grubbs Critical Value: 3.649574509044683
Grubbs Statistics Value: 3.5561478280392245
44 is not an outlier
17 авг. 2022 г.
читать 2 мин
Выброс — это наблюдение, которое лежит аномально далеко от других значений в наборе данных. Выбросы могут быть проблематичными, поскольку они могут повлиять на результаты анализа.
В этом руководстве объясняется, как идентифицировать и удалять выбросы в Python.
Как идентифицировать выбросы в Python
Прежде чем вы сможете удалить выбросы, вы должны сначала решить, что вы считаете выбросом. Есть два распространенных способа сделать это:
1. Используйте межквартильный диапазон.
Межквартильный размах (IQR) — это разница между 75-м процентилем (Q3) и 25-м процентилем (Q1) в наборе данных. Он измеряет разброс средних 50% значений.
Вы можете определить наблюдение как выброс, если оно в 1,5 раза превышает межквартильный размах, превышающий третий квартиль (Q3), или в 1,5 раза превышает межквартильный размах, меньше первого квартиля (Q1).
Выбросы = наблюдения > Q3 + 1,5*IQR или Q1 – 1,5*IQR
2. Используйте z-значения.
Z-оценка показывает, сколько стандартных отклонений данного значения от среднего. Мы используем следующую формулу для расчета z-показателя:
z = (X — μ) / σ
куда:
- X — это одно необработанное значение данных.
- μ — среднее значение населения
- σ — стандартное отклонение населения
Вы можете определить наблюдение как выброс, если его z-оценка меньше -3 или больше 3.
Выбросы = наблюдения с z-показателями> 3 или <-3
Как удалить выбросы в Python
Как только вы решите, что вы считаете выбросом, вы можете идентифицировать и удалить их из набора данных. Чтобы проиллюстрировать, как это сделать, мы будем использовать следующий кадр данных pandas:
import numpy as np
import pandas as pd
import scipy.stats as stats
#create dataframe with three columns 'A', 'B', 'C'
np.random.seed(10)
data = pd.DataFrame(np.random.randint(0, 10, size=(100, 3)), columns=['A', 'B', 'C'])
#view first 10 rows
data[:10]
A B C
0 13.315865 7.152790 -15.454003
1 -0.083838 6.213360 -7.200856
2 2.655116 1.085485 0.042914
3 -1.746002 4.330262 12.030374
4 -9.650657 10.282741 2.286301
5 4.451376 -11.366022 1.351369
6 14.845370 -10.798049 -19.777283
7 -17.433723 2.660702 23.849673
8 11.236913 16.726222 0.991492
9 13.979964 -2.712480 6.132042
Затем мы можем определить и удалить выбросы, используя метод z-оценки или метод межквартильного диапазона:
Метод Z-оценки:
#find absolute value of z-score for each observation
z = np.abs(stats.zscore(data))
#only keep rows in dataframe with all z-scores less than absolute value of 3
data_clean = data[(z<3).all(axis=1)]
#find how many rows are left in the dataframe
data_clean.shape
(99,3)
Метод межквартильного диапазона:
#find Q1, Q3, and interquartile range for each column
Q1 = data.quantile(q=.25)
Q3 = data.quantile(q=.75)
IQR = data.apply(stats.iqr)
#only keep rows in dataframe that have values within 1.5*IQR of Q1 and Q3
data_clean = data[~((data < (Q1-1.5*IQR)) | (data > (Q3+1.5*IQR))).any(axis=1)]
#find how many rows are left in the dataframe
data_clean.shape
(89,3)
Мы можем видеть, что метод z-показателя идентифицировал и удалил одно наблюдение как выброс, в то время как метод межквартильного диапазона идентифицировал и удалил 11 наблюдений как выбросы.
Когда удалять выбросы
Если в ваших данных присутствует один или несколько выбросов, вы должны сначала убедиться, что они не являются результатом ошибки ввода данных. Иногда человек просто вводит неправильное значение данных при записи данных.
Если выброс оказался результатом ошибки ввода данных, вы можете решить присвоить ему новое значение, такое как среднее значение или медиана набора данных.
Если значение является истинным выбросом, вы можете удалить его, если оно окажет значительное влияние на общий анализ. Просто не забудьте упомянуть в своем окончательном отчете или анализе, что вы удалили выброс.
Дополнительные ресурсы
Если вы работаете с несколькими переменными одновременно, вы можете использовать расстояние Махаланобиса для обнаружения выбросов.