Основная сложность при работе с загрязненными данными — как определить, что перед вами действительно загрязненные данные?

Рассмотрим данные NYC 311 service request из одной из предыдущих статей. Они многочисленны и имеют явно неочевидную структуру.

In [1]:
%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

pd.options.display.max_rows = 7
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = (15, 3)
plt.rcParams['font.family'] = 'sans-serif'
In [2]:
requests = pd.read_csv('data/311-service-requests.csv')
/usr/local/lib/python3.5/dist-packages/IPython/core/interactiveshell.py:2683: DtypeWarning: Columns (8) have mixed types. Specify dtype option on import or set low_memory=False.
  interactivity=interactivity, compiler=compiler, result=result)

Как определить, что данные загрязнены?

Начнем с анализа некоторых колонок. Проблемы встречаются в zip code, поэтому начнем с этой колонки.

Чтобы определить, есть ли проблема в данных, можно воспользоваться методом .unique() и посмотреть все значения. Для численных данных можно построить гистограмму, чтобы выяснить, как распределены данные.

Изучая уникальные значения в "Incident Zip", легко выявить некорректные данные:

  • Часть значений представлены как строки, часть - как числа
  • Присутствуют nan значения
  • Некорректные значения включают 29616-0759 или 83
  • Неопределенные значения, которые pandas не распознал, такие как 'N/A' и 'NO CLUE'

Возможные действия:

  • Заменить 'N/A' и 'NO CLUE' на стандартные nan
  • Изучить значение 83 и принять решение
  • Конвертировать все значения в строки
In [3]:
requests['Incident Zip'].unique()
Out[3]:
array([11432.0, 11378.0, 10032.0, 10023.0, 10027.0, 11372.0, 11419.0,
       11417.0, 10011.0, 11225.0, 11218.0, 10003.0, 10029.0, 10466.0,
       11219.0, 10025.0, 10310.0, 11236.0, nan, 10033.0, 11216.0, 10016.0,
       10305.0, 10312.0, 10026.0, 10309.0, 10036.0, 11433.0, 11235.0,
       11213.0, 11379.0, 11101.0, 10014.0, 11231.0, 11234.0, 10457.0,
       10459.0, 10465.0, 11207.0, 10002.0, 10034.0, 11233.0, 10453.0,
       10456.0, 10469.0, 11374.0, 11221.0, 11421.0, 11215.0, 10007.0,
       10019.0, 11205.0, 11418.0, 11369.0, 11249.0, 10005.0, 10009.0,
       11211.0, 11412.0, 10458.0, 11229.0, 10065.0, 10030.0, 11222.0,
       10024.0, 10013.0, 11420.0, 11365.0, 10012.0, 11214.0, 11212.0,
       10022.0, 11232.0, 11040.0, 11226.0, 10281.0, 11102.0, 11208.0,
       10001.0, 10472.0, 11414.0, 11223.0, 10040.0, 11220.0, 11373.0,
       11203.0, 11691.0, 11356.0, 10017.0, 10452.0, 10280.0, 11217.0,
       10031.0, 11201.0, 11358.0, 10128.0, 11423.0, 10039.0, 10010.0,
       11209.0, 10021.0, 10037.0, 11413.0, 11375.0, 11238.0, 10473.0,
       11103.0, 11354.0, 11361.0, 11106.0, 11385.0, 10463.0, 10467.0,
       11204.0, 11237.0, 11377.0, 11364.0, 11434.0, 11435.0, 11210.0,
       11228.0, 11368.0, 11694.0, 10464.0, 11415.0, 10314.0, 10301.0,
       10018.0, 10038.0, 11105.0, 11230.0, 10468.0, 11104.0, 10471.0,
       11416.0, 10075.0, 11422.0, 11355.0, 10028.0, 10462.0, 10306.0,
       10461.0, 11224.0, 11429.0, 10035.0, 11366.0, 11362.0, 11206.0,
       10460.0, 10304.0, 11360.0, 11411.0, 10455.0, 10475.0, 10069.0,
       10303.0, 10308.0, 10302.0, 11357.0, 10470.0, 11367.0, 11370.0,
       10454.0, 10451.0, 11436.0, 11426.0, 10153.0, 11004.0, 11428.0,
       11427.0, 11001.0, 11363.0, 10004.0, 10474.0, 11430.0, 10000.0,
       10307.0, 11239.0, 10119.0, 10006.0, 10048.0, 11697.0, 11692.0,
       11693.0, 10573.0, 83.0, 11559.0, 10020.0, 77056.0, 11776.0, 70711.0,
       10282.0, 11109.0, 10044.0, '10452', '11233', '10468', '10310',
       '11105', '10462', '10029', '10301', '10457', '10467', '10469',
       '11225', '10035', '10031', '11226', '10454', '11221', '10025',
       '11229', '11235', '11422', '10472', '11208', '11102', '10032',
       '11216', '10473', '10463', '11213', '10040', '10302', '11231',
       '10470', '11204', '11104', '11212', '10466', '11416', '11214',
       '10009', '11692', '11385', '11423', '11201', '10024', '11435',
       '10312', '10030', '11106', '10033', '10303', '11215', '11222',
       '11354', '10016', '10034', '11420', '10304', '10019', '11237',
       '11249', '11230', '11372', '11207', '11378', '11419', '11361',
       '10011', '11357', '10012', '11358', '10003', '10002', '11374',
       '10007', '11234', '10065', '11369', '11434', '11205', '11206',
       '11415', '11236', '11218', '11413', '10458', '11101', '10306',
       '11355', '10023', '11368', '10314', '11421', '10010', '10018',
       '11223', '10455', '11377', '11433', '11375', '10037', '11209',
       '10459', '10128', '10014', '10282', '11373', '10451', '11238',
       '11211', '10038', '11694', '11203', '11691', '11232', '10305',
       '10021', '11228', '10036', '10001', '10017', '11217', '11219',
       '10308', '10465', '11379', '11414', '10460', '11417', '11220',
       '11366', '10027', '11370', '10309', '11412', '11356', '10456',
       '11432', '10022', '10013', '11367', '11040', '10026', '10475',
       '11210', '11364', '11426', '10471', '10119', '11224', '11418',
       '11429', '11365', '10461', '11239', '10039', '00083', '11411',
       '10075', '11004', '11360', '10453', '10028', '11430', '10307',
       '11103', '10004', '10069', '10005', '10474', '11428', '11436',
       '10020', '11001', '11362', '11693', '10464', '11427', '10044',
       '11363', '10006', '10000', '02061', '77092-2016', '10280', '11109',
       '14225', '55164-0737', '19711', '07306', '000000', 'NO CLUE',
       '90010', '10281', '11747', '23541', '11776', '11697', '11788',
       '07604', 10112.0, 11788.0, 11563.0, 11580.0, 7087.0, 11042.0,
       7093.0, 11501.0, 92123.0, 0.0, 11575.0, 7109.0, 11797.0, '10803',
       '11716', '11722', '11549-3650', '10162', '92123', '23502', '11518',
       '07020', '08807', '11577', '07114', '11003', '07201', '11563',
       '61702', '10103', '29616-0759', '35209-3114', '11520', '11735',
       '10129', '11005', '41042', '11590', 6901.0, 7208.0, 11530.0,
       13221.0, 10954.0, 11735.0, 10103.0, 7114.0, 11111.0, 10107.0], dtype=object)

Корректировка NAN и различий между строками и числами

Для предварительной очистки данных можно воспользоваться параметром na_values в pd.read_csv. Также возможно четко указать тип данных для столбца Incident Zip.

In [4]:
na_values = ['NO CLUE', 'N/A', '0']
requests = pd.read_csv('data/311-service-requests.csv', na_values=na_values, dtype={'Incident Zip': str})
In [5]:
requests['Incident Zip'].unique()
Out[5]:
array(['11432', '11378', '10032', '10023', '10027', '11372', '11419',
       '11417', '10011', '11225', '11218', '10003', '10029', '10466',
       '11219', '10025', '10310', '11236', nan, '10033', '11216', '10016',
       '10305', '10312', '10026', '10309', '10036', '11433', '11235',
       '11213', '11379', '11101', '10014', '11231', '11234', '10457',
       '10459', '10465', '11207', '10002', '10034', '11233', '10453',
       '10456', '10469', '11374', '11221', '11421', '11215', '10007',
       '10019', '11205', '11418', '11369', '11249', '10005', '10009',
       '11211', '11412', '10458', '11229', '10065', '10030', '11222',
       '10024', '10013', '11420', '11365', '10012', '11214', '11212',
       '10022', '11232', '11040', '11226', '10281', '11102', '11208',
       '10001', '10472', '11414', '11223', '10040', '11220', '11373',
       '11203', '11691', '11356', '10017', '10452', '10280', '11217',
       '10031', '11201', '11358', '10128', '11423', '10039', '10010',
       '11209', '10021', '10037', '11413', '11375', '11238', '10473',
       '11103', '11354', '11361', '11106', '11385', '10463', '10467',
       '11204', '11237', '11377', '11364', '11434', '11435', '11210',
       '11228', '11368', '11694', '10464', '11415', '10314', '10301',
       '10018', '10038', '11105', '11230', '10468', '11104', '10471',
       '11416', '10075', '11422', '11355', '10028', '10462', '10306',
       '10461', '11224', '11429', '10035', '11366', '11362', '11206',
       '10460', '10304', '11360', '11411', '10455', '10475', '10069',
       '10303', '10308', '10302', '11357', '10470', '11367', '11370',
       '10454', '10451', '11436', '11426', '10153', '11004', '11428',
       '11427', '11001', '11363', '10004', '10474', '11430', '10000',
       '10307', '11239', '10119', '10006', '10048', '11697', '11692',
       '11693', '10573', '00083', '11559', '10020', '77056', '11776',
       '70711', '10282', '11109', '10044', '02061', '77092-2016', '14225',
       '55164-0737', '19711', '07306', '000000', '90010', '11747', '23541',
       '11788', '07604', '10112', '11563', '11580', '07087', '11042',
       '07093', '11501', '92123', '00000', '11575', '07109', '11797',
       '10803', '11716', '11722', '11549-3650', '10162', '23502', '11518',
       '07020', '08807', '11577', '07114', '11003', '07201', '61702',
       '10103', '29616-0759', '35209-3114', '11520', '11735', '10129',
       '11005', '41042', '11590', '06901', '07208', '11530', '13221',
       '10954', '11111', '10107'], dtype=object)

Что делать с дефисами?

In [6]:
rows_with_dashes = requests['Incident Zip'].str.contains('-').fillna(False)
len(requests[rows_with_dashes])
Out[6]:
5
In [7]:
requests[rows_with_dashes]['Incident Zip']
Out[7]:
29136    77092-2016
30939    55164-0737
70539    11549-3650
85821    29616-0759
89304    35209-3114
Name: Incident Zip, dtype: object

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

requests['Incident Zip'][rows_with_dashes] = np.nan

Однако выяснилось, что девятизначные zip коды допустимы. Рассмотрим их поближе и, при необходимости, укоротим их до первых пяти символов.

In [8]:
long_zip_codes = requests['Incident Zip'].str.len() > 5
requests['Incident Zip'][long_zip_codes].unique()
Out[8]:
array(['77092-2016', '55164-0737', '000000', '11549-3650', '29616-0759',
       '35209-3114'], dtype=object)
In [9]:
requests['Incident Zip'] = requests['Incident Zip'].str.slice(0, 5)

Было высказано предположение, что 00083 — ошибочный zip code, но оно оказалось принадлежит Central Park! Проверьте значение 00000:

```html
In [10]:
requests[requests['Incident Zip'] == '00000']
Out[10]:
Unique Key Created Date Closed Date Agency Agency Name Complaint Type Descriptor Location Type Incident Zip Incident Address ... Bridge Highway Name Bridge Highway Direction Road Ramp Bridge Highway Segment Garage Lot Name Ferry Direction Ferry Terminal Name Latitude Longitude Location
42600 26529313 10/22/2013 02:51:06 PM NaN TLC Taxi and Limousine Commission Taxi Complaint Driver Complaint NaN 00000 EWR EWR ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
60843 26507389 10/17/2013 05:48:44 PM NaN TLC Taxi and Limousine Commission Taxi Complaint Driver Complaint Street 00000 1 NEWARK AIRPORT ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

2 rows × 52 columns

Это выглядит не очень хорошо. Заменим значение на nan.

In [11]:
zero_zips = requests['Incident Zip'] == '00000'
requests.loc[zero_zips, 'Incident Zip'] = np.nan

Давайте проверим, что получилось:

In [12]:
unique_zips = requests['Incident Zip'].unique()
unique_zips
Out[12]:
array(['11432', '11378', '10032', '10023', '10027', '11372', '11419',
   '11417', '10011', '11225', '11218', '10003', '10029', '10466',
   '11219', '10025', '10310', '11236', nan, '10033', '11216', '10016',
   '10305', '10312', '10026', '10309', '10036', '11433', '11235',
   '11213', '11379', '11101', '10014', '11231', '11234', '10457',
   '10459', '10465', '11207', '10002', '10034', '11233', '10453',
   '10456', '10469', '11374', '11221', '11421', '11215', '10007',
   '10019', '11205', '11418', '11369', '11249', '10005', '10009',
   '11211', '11412', '10458', '11229', '10065', '10030', '11222',
   '10024', '10013', '11420', '11365', '10012', '11214', '11212',
   '10022', '11232', '11040', '11226', '10281', '11102', '11208',
   '10001', '10472', '11414', '11223', '10040', '11220', '11373',
   '11203', '11691', '11356', '10017', '10452', '10280', '11217',
   '10031', '11201', '11358', '10128', '11423', '10039', '10010',
   '11209', '10021', '10037', '11413', '11375', '11238', '10473',
   '11103', '11354', '11361', '11106', '11385', '10463', '10467',
   '11204', '11237', '11377', '11364', '11434', '11435', '11210',
   '11228', '11368', '11694', '10464', '11415', '10314', '10301',
   '10018', '10038', '11105', '11230', '10468', '11104', '10471',
   '11416', '10075', '11422', '11355', '10028', '10462', '10306',
   '10461', '11224', '11429', '10035', '11366', '11362', '11206',
   '10460', '10304', '11360', '11411', '10455', '10475', '10069',
   '10303', '10308', '10302', '11357', '10470', '11367', '11370',
   '10454', '10451', '11436', '11426', '10153', '11004', '11428',
   '11427', '11001', '11363', '10004', '10474', '11430', '10000',
   '10307', '11239', '10119', '10006', '10048', '11697', '11692',
   '11693', '10573', '00083', '11559', '10020', '77056', '11776',
   '70711', '10282', '11109', '10044', '02061', '77092', '14225',
   '55164', '19711', '07306', '90010', '11747', '23541', '11788',
   '07604', '10112', '11563', '11580', '07087', '11042', '07093',
   '11501', '92123', '11575', '07109', '11797', '10803', '11716',
   '11722', '11549', '10162', '23502', '11518', '07020', '08807',
   '11577', '07114', '11003', '07201', '61702', '10103', '29616',
   '35209', '11520', '11735', '10129', '11005', '41042', '11590',
   '06901', '07208', '11530', '13221', '10954', '11111', '10107'], dtype=object)

Отлично! Стало намного чище. Хотя немного любопытно: zip code 77056, согласно Google Maps, относится к Техасу.

Давайте выясним это подробнее:

In [13]:
zips = requests['Incident Zip']
# Пока что предположим, что зип коды начинающиеся на '0' и '1' - это нормально (на самом деле это не совсем так - 13221 находится в Сиракузах, но почему это так?)
is_close = zips.str.startswith('0') | zips.str.startswith('1')
# Существует много NaNs, но нас они сейчас не интересуют, поэтому мы говорим, что они принимают значение False
is_far = ~(is_close) & zips.notnull()
In [14]:
zips[is_far]
Out[14]:
12102    77056
13450    70711
29136    77092
     ...
85821    29616
89304    35209
94201    41042
Name: Incident Zip, dtype: object
In [15]:
requests[is_far][['Incident Zip', 'Descriptor', 'City']].sort_values('Incident Zip')
Out[15]:
Incident Zip Descriptor City
71834 23502 Harassment NORFOLK
47048 23541 Harassment NORFOLK
85821 29616 Debt Not Owed GREENVILLE
... ... ... ...
44008 90010 Billing Dispute LOS ANGELES
57636 92123 Harassment SAN DIEGO
71001 92123 Billing Dispute SAN DIEGO

13 rows × 3 columns

О, у нас Лос-Анджелес! Отфильтровать по zip-коду не всегда лучший подход для таких случаев. Лучше сразу посмотреть на город.

In [16]:
requests['City'].str.upper().value_counts()
Out[16]:
BROOKLYN    31662
NEW YORK    22664
BRONX       18438
        ...
MELVILLE        1
LAWRENCE        1
BOHIEMA         1
Name: City, dtype: int64

```

Вопросы для самопроверки:

  1. Какие проблемы могут возникнуть при работе с загрязненными данными?
  2. Какие методы можно использовать для выявления проблем в колонке с данными?
  3. Какие есть методы эффективной фильтрации данных?