Анализ данных с помощью pandas. Часть 6: работа с загрязненными данными
Главная проблема загрязненных данных: понять, они загрязнены или нет?
Используем данные NYC 311 service request из одной из прошлых статей, так как их много и они неочевидны.
%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'
requests = pd.read_csv('data/311-service-requests.csv')
Как узнать, что данные загрязнены?
Посмотрим на некоторые колонки. Есть некоторые проблемы с zip code, поэтому посмотрим сначала на него.
Чтобы предположить, есть ли в колонке проблема, можно использовать .unique()
для просмотра всех значений. Если это численные данные, вместо этого можно построить гистограмму значений, чтобы понять распределение данных.
Когда мы посмотрим на уникальные значения в "Incident Zip", станет сразу ясно, что это мусор. Некоторые проблемы:
- Некоторые значения - строки, некоторые - числа
- Есть
nan
- Некоторые значения
29616-0759
или83
- Некоторые неопределённые значения, которые pandas не смог распознать, такие как 'N/A' и 'NO CLUE'
Что можно сделать:
- Преобразовать 'N/A' и 'NO CLUE' в обычные
nan
- Посмотреть, что такое
83
, и решить, что же делать - Сделать все строками
requests['Incident Zip'].unique()
Исправление ошибок с NAN и различий строки/числа
Можно передать na_values
в pd.read_csv
, чтобы немного очистить данные. Также можно явно указать тип для Incident Zip.
na_values = ['NO CLUE', 'N/A', '0']
requests = pd.read_csv('data/311-service-requests.csv', na_values=na_values, dtype={'Incident Zip': str})
requests['Incident Zip'].unique()
Что с дефисами?
rows_with_dashes = requests['Incident Zip'].str.contains('-').fillna(False)
len(requests[rows_with_dashes])
requests[rows_with_dashes]['Incident Zip']
Сначала родилось предположение о том, что это пропущенные данные, и эти строки нужно удалить:
requests['Incident Zip'][rows_with_dashes] = np.nan
Но оказалось, что zip code из 9 цифр - это норма. Посмотрим на них, поймём, всё ли правильно, и обрежем их.
long_zip_codes = requests['Incident Zip'].str.len() > 5
requests['Incident Zip'][long_zip_codes].unique()
requests['Incident Zip'] = requests['Incident Zip'].str.slice(0, 5)
Было предположение, что 00083 - это неверный zip code, но оказалось, что он принадлежит Central Park! Посмотрим на код 00000:
requests[requests['Incident Zip'] == '00000']
Выглядит удручающе. Заменим на nan
.
zero_zips = requests['Incident Zip'] == '00000'
requests.loc[zero_zips, 'Incident Zip'] = np.nan
Посмотрим, чего мы добились:
unique_zips = requests['Incident Zip'].unique()
unique_zips
Здорово! Гораздо чище. Хотя немного странно: zip code 77056, согласно Google maps, принадлежит Техасу.
Посмотрим поближе:
zips = requests['Incident Zip']
# Let's say the zips starting with '0' and '1' are okay, for now. (this isn't actually true -- 13221 is in Syracuse, and why?)
is_close = zips.str.startswith('0') | zips.str.startswith('1')
# There are a bunch of NaNs, but we're not interested in them right now, so we'll say they're False
is_far = ~(is_close) & zips.notnull()
zips[is_far]
requests[is_far][['Incident Zip', 'Descriptor', 'City']].sort_values('Incident Zip')
О, Лос Анджелес! Фильтрация по zip code не лучший путь для обработки таких ситуаций - гораздо лучше сразу посмотреть на город.
requests['City'].str.upper().value_counts()