Очищення та підготовка даних за допомогою Python для наукових даних - найкращі практики та корисні пакети

Передмова

Очищення даних - це лише те, з чим вам доведеться займатися в аналітиці. Це не велика робота, але її треба зробити, щоб можна було робити велику роботу.

Я витратив стільки часу на написання та перезапис функцій, щоб допомогти мені очистити дані, що я хотів поділитися деякими з того, що я навчився на цьому шляху. Якщо ви ще не переглядали цю посаду, про те, як краще організувати проекти з наукових даних, перевірте це, оскільки це допоможе сформувати деякі з концепцій, які я переглядаю нижче.

Почавши краще впорядковувати свій код, я почав зберігати спеціальний пакет, в якому зберігаю код «очищення». Якщо що-небудь інше, це дає мені основу для написання користувацьких методів на даних, що не зовсім відповідає моїм попереднім сценаріям очищення. І мені не потрібно писати цей витягник електронної пошти для регулярних викликів у 100-й раз, оскільки я зберег його у доступному місці.

Деякі компанії мають цілі команди, присвячені чищенню коду, але більшість - ні. Тож найкраще зрозуміти деякі найкращі практики. Якщо що-небудь, ви краще зрозумієте структуру своїх даних, тож краще поясніть, чому або чому щось не сталося.

Також, готуючись до цієї посади, я пробіг цей репо від kjam, який був би неймовірно корисним, коли я вперше вчився очищати дані. Якщо ви хочете заглибитись в очищення коду, пропоную вам почати саме там.

Ваша мета - прибрати речі ... або, принаймні, спробувати

Перевірте свої дані… Швидко

Перше, що ви хочете зробити, отримуючи новий набір даних, - це швидко перевірити вміст методом .head ().

імпортувати панди як pd
df = pd.read_csv ('path_to_data')
df.head (10)
>>
... деякий вихід тут ...

Тепер давайте швидко побачимо назви та типи стовпців. Більшу частину часу ви збираєтесь отримувати дані, не зовсім ті, що ви очікували, наприклад, дати, які насправді є рядками та інші дивацтва. Але перевірити заздалегідь.

# Отримати назви стовпців
стовпці_імена = df.колонки
друк (назви стовпців)
# Отримати типи даних стовпців
df.dtypes
# Також перевірте, чи стовпець унікальний
для я в іменах стовпців:
  print ('{} унікальний: {}'. формат (i, df [i] .is_unique))

Тепер давайте подивимося, чи є у фрейму даних пов'язаний з ним індекс, зателефонувавши .index на df. Якщо індексу немає, ви отримаєте AttributeError: об’єкт "функції" не відображає атрибут "індексувати" помилку.

# Перевірте значення індексу
df.index.values
# Перевірте, чи існує певний індекс
'foo' у df.index.values
# Якщо індекс не існує
df.set_index ('стовпець_іме_на_використання', замість = Істинно)

Добре. Наші дані були швидко перевірені, ми знаємо типи даних, якщо стовпці унікальні, і ми знаємо, що він має індекс, щоб потім ми могли робити об'єднання та злиття. Давайте розберемося, які стовпці потрібно зберегти чи видалити. У цьому прикладі ми хочемо позбутися стовпців в індексах 1, 3 та 5, тому я щойно додав рядкові значення до списку, який буде використовуватися для скидання стовпців.

# Створіть розуміння списку стовпців, які ви хочете втратити
column_to_drop = [ім'я стовпців [i] для i в [1, 3, 5]]
# Видалення непотрібних стовпців
df.drop (column_to_drop, inplace = True, вісь = 1)

Inplace = True було додано, тому вам не потрібно зберігати над початковим df, призначивши результат .drop () df. Багато методів у пандах підтримують inplace = True, тому намагайтеся використовувати його якомога більше, щоб уникнути зайвого переназначення.

Що робити з NaN

Якщо вам потрібно заповнити помилки або пробіли, скористайтеся методами fillna () та dropna (). Це здається швидким, але всі маніпуляції з даними повинні бути задокументовані, щоб ви могли їх потім пояснити комусь.

Ви можете заповнити NaNs рядками, або, якщо вони є числами, ви можете використовувати середнє або середнє значення. Існує багато дискусій щодо того, що робити з відсутніми або неправильними даними, і правильна відповідь - це залежить.

Вам доведеться використовувати своє найкраще рішення та інформацію від людей, з якими ви працюєте, щодо того, чому видалення чи заповнення даних є найкращим підходом.

# Заповніть NaN символом ""
df ['col'] = df ['col']. fillna ('')
# Заповніть NaN 99
df ['col'] = df ['col']. fillna (99)
# Заповніть NaN середнім значенням стовпця
df ['col'] = df ['col']. fillna (df ['col']. mean ())

Ви також можете поширювати ненульові значення вперед або назад, додаючи метод = "pad" як аргумент методу. Він заповнить наступне значення у кадрі даних попереднім значенням, що не відповідає NaN. Можливо, ви просто хочете заповнити одне значення (limit = 1) або ви хочете заповнити всі значення. Що б це не було, переконайтеся, що це відповідає решті даних щодо очищення даних.

df = pd.DataFrame (data = {'col1': [np.nan, np.nan, 2,3,4, np.nan, np.nan]})
    col1
0 NaN
1 NaN
2 2.0
3 3.0
4 4.0 # Це значення для заповнення вперед
5 NaN
6 NaN
df.fillna (метод = 'pad', ліміт = 1)
    col1
0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
5 4,0 # Заповнений вперед
6 NaN

Зауважте, як було заповнено лише індекс 5? Якби я не заповнив обмежений майданчик, він би заповнив весь кадр даних. Ми не обмежуємося прямим заповненням, а також засипанням рахунку.

# Заповніть перші два значення NaN першим доступним значенням
df.fillna (method = 'bfill')
    col1
0 2.0 # Заповнено
1 2.0 # Заповнено
2 2.0
3 3.0
4 4.0
5 NaN
6 NaN

Ви можете просто скинути їх із фрейму даних цілком, або за рядком, або за стовпцем.

# Опустіть будь-які рядки, у яких є будь-які нан
df.dropna ()
# Відкиньте стовпці, у яких є будь-які нан
df.dropna (вісь = 1)
# Лише стовпці, що містять щонайменше 90% не-NaN
df.dropna (thresh = int (df.shape [0] * .9), вісь = 1)

Параметр thresh = N вимагає, щоб стовпець мав принаймні N не-NaN, щоб вижити. Подумайте про це як про нижню межу для відсутніх даних, які ви знайдете прийнятними у своїх стовпцях. Розглянемо деякі дані журналу, які можуть пропустити деяку колекцію функцій. Ви хочете, щоб записи, які містять 90% доступних функцій, перш ніж розглядати їх як кандидатів для вашої моделі.

np.where (if_this_is_true, do_this, else_do_that)

Я винен, що не використовував це раніше у своїй аналітичній кар'єрі, тому що це не надто корисно. Це економить стільки часу і розчарувань під час перегляду даних. Якщо ви хочете зробити якусь основну чистку чи інженерну техніку швидко, np.where тут - як ви можете це зробити.

Подумайте, чи оцінюєте ви стовпець, і хочете дізнатися, чи значення їх суворо перевищує 10. Якщо вони є, ви хочете, щоб результат був "foo", а якщо ні, ви хочете, щоб результат був "бар".

# Дотримуйтесь цього синтаксису
np.where (if_this_condition_is_true, do_this, else_this)
# Приклад
df ['new_column'] = np.where (df [i]> 10, 'foo', 'bar)

Ви можете робити більш складні операції, як описано нижче. Тут ми перевіряємо, чи починається запис стовпця з foo і чи не закінчується штрихом. Якщо це перевіримо, ми повернемо True true, ми повернемо поточне значення у стовпці.

df ['new_column'] = np.where (df ['col']. str.startswith ('foo') і
                            не df ['col']. str.endswith ('bar'),
                            Щоправда,
                            df ['col'])

А ще ефективніше, ви можете почати гніздовати np.where, щоб вони укладалися один на одного. Подібно до того, як ви складатимете потрійні операції, переконайтеся, що вони читаються, оскільки ви можете швидко потрапити в безлад з сильно вкладеними заявами.

# Три рівня гніздування з н.п.
np.where (if_this_condition_is_true_one, do_this,
  np.where (if_this_condition_is_true_two, do_that,
    np.where (if_this_condition_is_true_three, do_foo, do_bar)))
# Тривіальний приклад
df ['foo'] = np.where (df ['bar'] == 0, 'нуль',
              np.where (df ['bar'] == 1, 'Один',
                np.where (df ['bar'] == 2, 'Two', 'Three')))

Затвердити і перевірити те, що у вас є

Кредит на https://www.programiz.com

Тільки тому, що у вас є ваші дані в хорошому кадрі даних, без дублікатів, відсутніх значень, у вас все ще можуть виникнути проблеми з базовими даними. І, маючи рамки даних 10М + рядків або новий API, як ви можете переконатися, що значення є такими, якими ви їх очікуєте?

Правда, ви ніколи не знаєте, чи ваші дані правильні, поки не перевірите їх. Найкращі практики інженерії програмного забезпечення в значній мірі покладаються на тестування своєї роботи, але для наукових даних це все ще незавершене виробництво. Краще почати зараз і навчити себе хорошим принципам роботи, а не пізніше перекваліфікуватись.

Створимо простий кадр даних для тестування.

df = pd.DataFrame (data = {'col1': np.random.randint (0, 10, 10), 'col2': np.random.randint (-10, 10, 10)})
>>
   col1 col2
0 0 6
1 6 -1
2 8 4
3 0 5
4 3 -7
5 4 -5
6 3 -10
7 9 -8
8 0 4
9 7 -4

Давайте перевіримо, чи всі значення в col1 є> = 0, використовуючи вбудований метод затвердження, який постачається зі стандартною бібліотекою в python. Що ви запитуєте python, якщо True, всі елементи в df ['col1'] перевищують нуль. Якщо це правда, тоді продовжуйте свій шлях, якщо не киньте помилку.

assert (df ['col1']> = 0) .all () # Не слід нічого повертати

Великий, здається, спрацював. Але що робити, якщо .all () не був включений у ствердження?

assert (df ['col1']> = 0)
>>
ValueError: Значення істинності серії неоднозначне. Використовуйте a.empty, a.bool (), a.item (), a.any () або a.all ().

Humm, схоже, у нас є деякі варіанти, коли ми тестуємо наші фрейми. Перевіримо будь-яке значення - це рядки.

assert (df ['col1']! = str) .any () # Не повинно нічого повертати

Як щодо тестування двох стовпців, щоб побачити, чи вони рівні?

assert (df ['col1'] == df ['col2']). all ()
>>
Відстеження (останній останній дзвінок):
  Файл "", рядок 1, в 
AssertionError

Ах, наша заява тут не вдалася!

Найкращу практику із твердженнями слід використовувати для перевірки умов у ваших даних, які ніколи не повинні відбуватися. Це так, коли ви запускаєте свій код, все припиняється, якщо одне з цих тверджень вийде з ладу.

Метод .all () перевірить, чи всі елементи в об'єктах проходять затвердження, тоді як .any () перевірить, чи проходить будь-який з елементів в об'єктах тест затвердження.

Це може бути корисно, коли ви хочете:

  • Перевірте, чи були внесені якісь негативні значення в дані;
  • Переконайтесь, що два стовпці точно однакові;
  • Визначте результати перетворення, або;
  • Перевірте, чи унікальний кількість ідентифікаторів точний.

Є більше способів утвердження, які я не перейду, але ознайомтеся, які ви можете використовувати тут. Ви ніколи не дізнаєтесь, коли вам потрібно пройти тест на певний стан, і в той же час вам потрібно розпочати тестування на умови, які ви не хочете в своєму коді.

Не тестуйте на все, а перевіряйте речі, які б зламали ваші моделі.

Наприклад Це функція, у якій усі повинні бути 0 і 1, насправді заповнені цими значеннями.

Крім того, ці чудові панди також включають тестовий пакет.

імпортувати pandas.util.testing як tm
tm.assert_series_equal (df ['col1'], df ['col2'])
>>
AssertionError: Серії різні
Значення серії різні (100,0%)
[зліва]: [0, 6, 8, 0, 3, 4, 3, 9, 0, 7]
[праворуч]: [6, -1, 4, 5, -7, -5, -10, -8, 4, -4]

Ми не лише отримали помилку, але й панди сказали нам, що не так.

Приємно.

Крім того, якщо ви хочете почати створювати набір для тестування - і ви можете подумати над цим, - ознайомтеся з пакетом unittest, вбудованим у бібліотеку Python. Ви можете дізнатися більше про це тут.

красуня

Замість того, щоб писати власний регулярний вираз - що в кращі часи - це біль - іноді це робиться за вас. Пакет красунь може допомогти вам очистити деякі часто використовувані зразки для електронних листів або URL-адрес. Це нічого не фантазії, але може швидко допомогти прибирати.

$ pip3 встановити красуню
від імпорту красуня Email, Url
email_string = 'foo@bar.com'
email = електронна пошта (email_string)
друк (email.domain)
друк (email.username)
друк (email.is_free_email)
>>
bar.com
foo
помилковий
url_string = 'https://github.com/labtocat/beautifier/blob/master/beautifier/__init__.py'
url = Url (url_string)
друк (url.param)
друк (url.username)
друк (url.domain)
>>
Немає
{'msg': 'Функція наразі доступна лише із пов'язаними URL-адресами'}
github.com

Я використовую цей пакет, коли у мене є низка URL-адрес, над якими я потребую розробки, і не хочу писати регулярний регекс у 100-й раз, щоб витягти певні частини адреси.

Робота з Unicode

Виконуючи деякі NLP, спілкування з Unicode може бути неприємним у кращі часи. Я буду запускати щось у spaCy і раптом усе зірветься на мені через те, що десь у тілі документа з’явиться якийсь символ Unicode.

Це справді найгірше.

Використовуючи ftfy (виправлено це для вас), ви зможете виправити дійсно зламаний Unicode. Подумайте, коли хтось закодував Unicode одним стандартом і розшифрував його іншим. Тепер вам доведеться розібратися з цим між рядками, як дурницькі послідовності під назвою "mojibake".

# Приклад моджибаке
& macr; \\ _ (ã \ x83 \ x84) _ / & macr;
\ ufeffParty
\ 001 \ 033 [36; 44mI & # x92; m

На щастя, ftfy використовує евристику для виявлення та скасування mojibake з дуже низьким рівнем помилкових позитивних результатів. Давайте подивимось, в які наші передування вище можна перетворити, щоб ми могли їх прочитати. Основний метод - fix_text (), і ви будете використовувати його для декодування.

імпортувати ftfy
foo = '& macr; \\ _ (ã \ x83 \ x84) _ / & macr;'
bar = '\ ufeffParty'
baz = '\ 001 \ 033 [36; 44mI & # x92; m'
друк (ftfy.fix_text (foo))
друк (ftfy.fix_text (bar))
друк (ftfy.fix_text (baz))

Якщо ви хочете побачити, як робиться розшифровка, спробуйте ftfy.explain_unicode (). Я не думаю, що це буде надто корисним, але цікаво бачити процес.

ftfy.explain_unicode (foo)
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] ЛАТИННІ МАЛІ ЛИСТИ M
U + 0061 a [Ll] ЛАТИННИЙ МАЛИЙ ПІСЛЯ A
U + 0063 c [Ll] МАЛИШНІ ПІСНЯ СТРАНИЦІ C
U + 0072 r [Ll] МАЛИЙ ЛІТИН RAT R
U + 003B; [Po] SEMICOLON
U + 005C \ [Po] ПОВЕРНУТИ СОЛІДУС
U + 005F _ [ПК] LOW LINE
U + 0028 ([Ps] ЛІВНІЙ ПАРТЕНТЕЗ
U + 00E3 ã [Ll] ЛАТИННИЙ МАЛИЙ ЛИСТ А з ТИЛДОЮ
U + 0083 \ x83 [Cc] <невідомо>
U + 0084 \ x84 [Cc] <невідомо>
U + 0029) [Пе] ПРАВИЙ ПАРТЕНТЕЗ
U + 005F _ [ПК] LOW LINE
U + 002F / [Po] SOLIDUS
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] ЛАТИННІ МАЛІ ЛИСТИ M
U + 0061 a [Ll] ЛАТИННИЙ МАЛИЙ ПІСЛЯ A
U + 0063 c [Ll] МАЛИШНЕ ПІСЛЯ СТРАНИЦІ C
U + 0072 r [Ll] МАЛИЙ ЛІТИН RAT R
U + 003B; [Po] SEMICOLON
Немає

Дедупе

Це бібліотека, яка використовує машинне навчання для швидкого виконання дублювання та вирішення сутності на структурованих даних. Тут є чудовий пост, який детальніше описується, ніж я, і до якого я звернувся значно більше.

Ми переглянемо дані про місцезнаходження в Чикаго, які можна знайти тут. У ньому є купа пропущених значень і дублюваних значень з різних джерел даних, тож добре дізнатися далі.

Якщо ви коли-небудь переглядали дублювані дані раніше, це буде виглядати дуже добре.

# Стовпці та кількість відсутніх значень у кожному
Id має значення 0 na
Джерело має значення 0 na
Назва сайту має значення 0 na
Адреса має значення 0 na
Zip має 1333 na значення
Телефон має значення 146 на
Факс має значення 3299 на
Назва програми має значення 2009 na
Тривалість дня має значення на 2009 рік
ID провайдера IDHS має 3298 na значень
Агентство має 3325 na значень
Район має 2754 na значень
Фінансова зарахування має 2424 na значення
Варіант програми має значення 2800 на
Кількість EHS на сайті має значення 3319 na
Кількість на сайті HS має 3319 na значень
Режисер має 3337 na значень
Фонд Head Start має 3337 наборів
Фонд Eearly Head Start має 2881 na значень
Фонд CC має 2818 наборів
Progmod має 2818 na значень
Веб-сайт має 2815 na значень
Виконавчий директор має 3114 нан
Директор Центру має 2874 na значень
Наявні програми ECE мають 2379 значень
NAEYC Дійсний, поки не має значення 2968 na
Ідентифікатор програми NAEYC має 3337 значень
Електронна адреса має значення 3203 на
Опис "Унція профілактики" має значення 3185 на
Тип послуги фіолетового в'яжучого має значення 3215 на
Колонка має значення 3337 на
Колонка2 має значення 3018 на

Метод preProcess, наданий дедупе, необхідний для того, щоб помилки не виникали під час фаз вибірки та навчання моделі. Повірте, використання цього деупе допоможе набагато простіше. Збережіть цей метод у вашому локальному "пакеті очищення", щоб ви могли використовувати його в майбутньому при роботі з дублюючими даними.

імпортувати панди як pd
імпорт нуме
імпорт дедуп
import os
імпорт csv
імпорт ре
з імпорту unidecode unidecode
def preProcess (стовпець):
    '' '
    Використовується для запобігання помилок під час процесу дедупінгу.
    '' '
    спробуйте:
        column = column.decode ('utf8')
    крім AttributeError:
        пропуск
    стовпчик = unidecode (стовпець)
    column = re.sub ('+', '', стовпець)
    column = re.sub ('\ n', '', стовпець)
    стовпчик = колонка.стріп (). смуга ('"'). смуга (" '"). нижня (). смуга ()
    
    якщо не стовпець:
        стовпчик = Немає
    повернути стовпчик

Тепер почніть імпортувати колонку .csv за стовпцем, обробляючи дані.

def readData (назва файлу):
    
    data_d = {}
    з відкритим (ім'я файлу) як f:
        читач = csv.DictReader (f)
        для рядка в читальнику:
            clean_row = [(k, preProcess (v)) для (k, v) у row.items ()]
            row_id = int (рядок ['Ідентифікатор'])
            data_d [row_id] = dict (очистити_роу)
повернути df
name_of_file = 'data.csv'
print ("Очищення та імпорт даних ...")
df = readData (name_of_file)

Тепер нам потрібно сказати дедуптацію, які функції ми повинні дивитись, щоб визначити повторювані значення. Нижче кожну функцію позначають полем і присвоюють тип даних і якщо вони мають якісь відсутні значення. Існує цілий список різних типів змінних, які ви можете використовувати тут, але для того, щоб це було просто, ми зараз дотримуватимемося рядків.

Я також не буду використовувати кожен стовпчик для визначення дублювання, але ви можете, якщо ви думаєте, що полегшить визначення значень у вашому фреймі даних.

# Встановити поля
поля = [
        {'field': 'Джерело', 'type': 'Set'},
        {'field': 'Назва сайту', 'type': 'String'},
        {'field': 'Адреса', 'type': 'String'},
        {'field': 'Zip', 'type': 'Точний', 'пропущено': True},
        {'field': 'Phone', 'type': 'String', 'недостатньо': True},
        {'field': 'Адреса електронної пошти', 'type': 'String', 'недостатньо': True},
        ]

Тепер почнемо подавати дані, щоб вивести деякі дані.

# Перейти в нашу модель
deduper = dedupe.Dedupe (поля)
# Перевірте, чи працює він
дедупер
>>
# Подайте кілька зразкових даних у ... 15000 записів
deduper.sample (df, 15000)

Тепер ми переходимо до частини маркування. Коли ви запустите цей спосіб нижче, вам буде запропоновано дедуп зробити просте маркування.

dedupe.consoleLabel (дедупер)
Що ви повинні бачити; ручне навчання дедупера

Справжній "а! Ха"! Момент, коли ви отримаєте це підказку. Це дедупер просить вас навчити його, щоб він знав, що шукати. Ви знаєте, як має виглядати повторюване значення, тому просто передайте ці знання далі.

Чи відносяться ці записи до одного і того ж?
(y) es / (n) o / (u) nsure / (f) inished

Тепер вам більше не доведеться шукати тонни і тонни записів, щоб побачити, чи відбулося дублювання. Натомість нейронна мережа ви навчається пошуку дублікатів у фреймі даних.

Після того, як ви надали йому якусь маркування, закінчіть навчальний процес і збережіть свій прогрес. Ви можете повернутися до своєї нейронної мережі пізніше, якщо виявите, що ви повторили об'єкти фрейму даних, які потребують дедупінгу.

deduper.train ()
# Збережіть навчання
з відкритим (training_file, 'w') як tf:
        deduper.writeTraining (tf)
# Збережіть налаштування
з відкритим (settings_file, 'wb') як sf:
        deduper.writeSettings (sf)

Ми майже закінчили, оскільки далі нам потрібно встановити поріг для наших даних. Коли Rec_weight дорівнює 1, ми говоримо дедуктору, щоб значення відкликали так само, як і точність. Однак, якщо Rec_weight = 3, ми б цінували нагадування втричі більше. Ви можете грати з цими налаштуваннями, щоб побачити, що найкраще підходить для вас.

поріг = deduper.threshold (df, call_weight = 1)

Нарешті, ми можемо шукати наш df і бачити, де дублікати. Давно переходити на цю посаду, але це набагато краще, ніж це робити вручну.

# Кластеризуйте дублікати разом
clustered_dupes = deduper.match (data_d, поріг)
print ("Є {} дублікати наборів'.format (len (clustered_dupes)))

Тож давайте подивимось на наші дублікати.

clustered_dupes
>>
[((0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255),
  масив ([0.88552043, 0.88552043, 0.77351897, 0.88552043, 0.88552043,
         0.88552043, 0.88552043, 0.89765924, 0.75684386, 0.83023088])),
 ((2, 3, 216, 511, 512, 1227, 1228, 2687), ...

Хм, це нам мало що говорить. Власне, що це нам показує? Що сталося з усіма нашими цінностями?

Якщо придивитися уважно, значення (0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255) - всі ідентичні місця дублікатів дедупера вважають, що насправді однакові. І ми можемо переглянути оригінальні дані, щоб це підтвердити.

{'Id': '215',
 'Джерело': 'cps_early_childhood_portal_scrape.csv',
 'Назва сайту': 'Армія храму порятунку',
 'Адреса': '1 n. ogden ',
...
{'Id': '509',
 'Джерело': 'cps_early_childhood_portal_scrape.csv',
 'Назва сайту': 'Армія порятунку - храм / армія порятунку',
 'Адреса': 'пр. 1-го огдена',
 'Zip': Ні,
..

Це схоже на дублікати для мене. Приємно.

Існує багато більш досконалих застосувань дедупера, таких як matchBlocks для послідовностей кластерів або поля взаємодії, де взаємодія між двома полями не є просто аддитивною, а мультиплікативною. Це вже було багато для перегляду, тому я залишу це пояснення до статті вище.

Збіг рядків із нечіткими

Спробуйте цю бібліотеку. Це дійсно цікаво, оскільки дає оцінку того, наскільки близькі рядки при їх порівнянні.

Це був надзвичайно чудовий інструмент, оскільки я робив проекти в минулому, коли мені потрібно було покластися на нечіткий додаток Google Sheet, щоб діагностувати проблеми перевірки даних - думаю, що правила CRM не застосовуються або діють правильно - і потрібно чистити записи, щоб зробити будь-який аналіз.

Але для великих наборів даних такий підхід начебто не відповідає.

Однак за допомогою fuzzywuzzy ви можете почати вступати у відповідність рядків у більш науковій справі. Не надто технічно, але для порівняння використовується щось, що називається відстань Левенштейна. Це показник схожості рядків для двох послідовностей, таким чином, що відстань між ними є кількістю однозначних редагувань символів, необхідних для зміни одного слова на інше.

Наприклад якщо ви хочете змінити рядок foo на смугу, мінімальна кількість символів для зміни буде 3, і це використовується для визначення "відстані".

Подивимося, як це працює на практиці.

$ pip3 встановити fuzzywuzzy
# test.py
від fuzzywuzzy import fuzz
від імпортного процесу імпорту
foo = 'це рядок'
bar = 'як ця струна?'
fuzz.ratio (foo, bar)
>>
71
fuzz.WRatio (foo, bar) # Зважене співвідношення
>>
73
fuzz.UQRatio (foo, bar) # Швидке відношення Unicode
>> 73

У пакеті fuzzywuzzy є різні способи оцінювання рядків (WRatio, UQRatio тощо), і я просто буду дотримуватися стандартної реалізації цієї статті.

Далі ми можемо переглянути токенізовану рядок, яка повертає міру подібності послідовностей між 0 і 100, але сортує маркер перед порівнянням. Це є ключовим, оскільки, можливо, ви хочете просто побачити вміст рядків, а не їхні позиції.

Струни foo та bar мають однакові лексеми, але структурно відрізняються. Ви хочете ставитися до них однаково? Тепер ви можете легко шукати та враховувати цей тип різниці у своїх даних.

foo = 'це foo'
bar = 'foo a це це'
fuzz.ratio (foo, bar)
>>
31
fuzz.token_sort_ratio ("це foo", "foo a is this")
>>
100

Або далі, вам потрібно знайти найближчий збіг рядка зі списку значень. У цьому випадку ми будемо розглядати заголовки Гаррі Поттера.

А що з тією книгою про Гаррі Поттера з… щось заголовок… у ній… я не знаю. Мені просто потрібно здогадатися і подивитися, яка з цих книг наближається до моєї здогадки.

Моя здогадка - "вогонь", і давайте подивимось, як вона оцінюється з можливим списком назв.

lst_to_eval = ['Гаррі Поттер і філософський камінь',
"Гаррі Поттер і Палата таємниць",
"Гаррі Поттер і в'язень Азкабану",
"Гаррі Поттер та вогневий келих",
"Гаррі Поттер і орден Фенікса",
"Гаррі Поттер і напівкровний принц",
"Гаррі Поттер і Дари смерті"]
# Дві найкращі відповіді на основі моєї здогадки
process.extract ("пожежа", lst_to_eval, ліміт = 2)
>>
[("Гаррі Поттер та вогневий келих", 60), ("Гаррі Поттер та камінь чаклуна", 30)
results = process.extract ("пожежа", lst_to_eval, ліміт = 2)
для отримання результатів:
  print ('{}: має оцінку у форматі {}'. (результат [0], результат [1]))
>>
Гаррі Поттер та вогневий келих: має 60 балів
Гаррі Поттер та камінь чаклуна: має бал 30

Або якщо ви просто хочете повернути його, можете.

>>> process.extractOne ("камінь", lst_to_eval)
("Гаррі Поттер і камінь чаклуна", 90)

Я знаю, що ми говорили про дедупцію раніше, але ось ще одна програма того ж процесу з fuzzywuzzy. Ми можемо взяти список рядків, що містять дублікати, і використовує нечітке зіставлення для ідентифікації та видалення дублікатів.

Не така фантазія, як нейронна сітка, але вона зробить роботу для невеликих операцій.

Ми продовжимо тему про Гаррі Поттера та шукатимемо дублікати персонажів із книг у списку.

Вам потрібно буде встановити поріг між 0 і 100. Оскільки поріг зменшиться, кількість знайдених дублікацій збільшиться, тому повернутий список буде скорочений. За замовчуванням - 70.

# Список повторюваних імен символів
contains_dupes = [
'Гаррі Поттер',
'H. Поттер ',
"Гаррі Джеймс Поттер",
"Джеймс Поттер",
"Рональд Біліус \" Рон \ "Візлі",
"Рон Візлі",
'Рональд Візлі]]
# Друк повторюваних значень
process.dedupe (contains_dupes)
>>
dict_keys (["Гаррі Джеймс Поттер", "Рональд Біліус" Рон "Візлі"])
# Друкуйте дублікати значень з більш високим порогом
process.dedupe (містить_надій, поріг = 90)
>>
dict_keys (["Гаррі Джеймс Поттер", "Х. Поттер", "Рональд Біліус" Рон "Візлі"])

І, як швидкий бонус, ви також можете зробити нечітке узгодження з пакетом datetime, щоб витягти дати з рядка тексту. Це чудово, коли ви не хочете (знову) писати вираз регулярного вираження.

з розбору імпорту datautil.parser
dt = розбір ("Сьогодні 1 січня 2047 о 8:21:00 ранку", нечіткий = Правда)
друк (дт)
>>
2047-01-01 08:21:00
dt = розбір ("18 травня, 2049 щось щось", нечіткий = Правда)
друк (дт)
>>
2049-05-18 00:00:00

Спробуйте склеарн

Поряд з очищенням даних вам також потрібно буде підготувати їх, щоб вони були у формі, яку ви можете подати у вашу модель. Більшість прикладів тут витягнуті безпосередньо з документації, яку слід перевірити, оскільки це дійсно добре допомагає пояснити більше новизни кожного пакету.

Ми спочатку імпортуватимемо пакет попередньої обробки, а потім отримуватимемо додаткові методи звідти. Також я використовую sklearn версії 0.20.0, тому якщо у вас виникли проблеми з імпортом деяких пакетів, перевірте свою версію.

Ми будемо працювати з двома різними типами даних, str та int, щоб лише висвітлити, як працюють різні методи попередньої обробки.

# На початку проекту
з попередньої обробки імпорту sklearn
# І давайте створимо випадковий масив ints для обробки
ary_int = np.random.randint (-100, 100, 10)
ary_int
>> [5, -41, -67, 23, -53, -57, -36, -25, 10, 17]
# І якась стріта з якою працювати
ary_str = ['foo', 'bar', 'baz', 'x', 'y', 'z']

Спробуємо кілька швидких позначок за допомогою LabelEncoder на нашому ary_str. Це важливо, оскільки ви не можете просто годувати сирими струнами - добре, але це не виходить за рамки цієї статті - у своїх моделях. Отже, ми будемо кодувати мітки до кожної з рядків зі значенням від 0 до n. У нашому arry_str ми маємо 6 унікальних значень, тому наш діапазон буде 0 - 5.

з sklearn.preprocessing import LabelEncoder
l_encoder = попередня обробка.LabelEncoder ()
l_encoder.fit (ary_str)
>> LabelEncoder ()
# Які наші цінності?
l_encoder.transform (['foo'])
>> масив ([2])
l_encoder.transform (['baz'])
>> масив ([1])
l_encoder.transform (['бар'])
>> масив ([0])

Ви помітите, що вони не впорядковані, оскільки навіть через foo прийшов перед бар в масиві, він був закодований 2, а бар був закодований 1. Ми будемо використовувати інший метод кодування, коли нам потрібно переконатися, що наші значення закодовані. у правильному порядку.

Якщо у вас є багато категорій, які слід відстежувати, ви можете забути, яка сторінка відображається на яку int. Для цього ми можемо створити дикт.

# Перевірте відображення
список (l_encoder.classes_)
>> ['bar', 'baz', 'foo', 'x', 'y', 'z']
# Створити словник відображень
dict (zip (l_encoder.classes_, l_encoder.transform (l_encoder.classes_)))
>> {'bar': 0, 'baz': 1, 'foo': 2, 'x': 3, 'y': 4, 'z': 5}

Процес трохи відрізняється, якщо у вас є фрейм даних, але насправді трохи простіше. Вам просто потрібно застосувати () об’єкт LabelEncoder до DataFrame. Для кожного стовпця ви отримаєте унікальну мітку для значень у цьому стовпці. Зауважте, як foo закодовано до 1, але так є y.

# Спробуйте LabelEncoder на фреймі даних
імпортувати панди як pd
l_encoder = попередня обробка.LabelEncoder () # Новий об'єкт
df = pd.DataFrame (data = {'col1': ['foo', 'bar', 'foo', 'bar'],
                          'col2': ['x', 'y', 'x', 'z'],
                          'col3': [1, 2, 3, 4]})
# Тепер для легкої частини
df.apply (l_encoder.fit_transform)
>>
   col1 col2 col3
0 1 0 0
1 0 1 1
2 1 0 2
3 0 2 3

Тепер ми переходимо до порядкового кодування, де функції все ще виражаються як цілі значення, але вони мають відчуття місця та структури. Такий, що x приходить до y, а y - до z.

Однак ми збираємося тут кинути гайковий ключ. Значення не тільки впорядковані, але вони збиратимуться в парі один з одним.

Ми візьмемо два масиви значень ['foo', 'bar', 'baz'] та ['x', 'y', 'z']. Далі ми будемо кодувати 0, 1 і 2 до кожного набору значень у кожному масиві та створимо кодовану пару для кожного зі значень.

Наприклад ['Foo', 'z'] буде відображено в [0, 2], а ['baz', 'x'] буде відображено в [2, 0].

Це хороший підхід, коли потрібно взяти купу категорій і зробити їх доступними для регресії, а особливо добре, коли у вас є переплетення наборів рядків - окремих категорій, які все ще перетинаються між собою - і потрібне представлення у фреймі даних .

з sklearn.pre обробкою імпорту OrdinalEncoder
o_encoder = OrdinalEncoder ()
ary_2d = [['foo', 'bar', 'baz'], ['x', 'y', 'z']]
o_encoder.fit (2d_ary) # Встановити значення
o_encoder.transform ([['foo', 'y']])
>> масив ([[0., 1.]])

Класичне гаряче або «фіктивне» кодування, де окремі функції категорій потім виражаються у вигляді додаткових стовпців 0 або 1, залежно від того, відображається це значення чи ні. Цей процес створює двійковий стовпчик для кожної категорії і повертає розріджену матрицю або щільний масив.

Кредит на https://blog.myyellowroad.com/

Навіщо навіть використовувати це? Оскільки цей тип кодування необхідний для подачі категоричних даних у багато моделей scikit, таких як лінійні регресійні моделі та SVM. Тож будьте зручні з цим.

з sklearn.preprocessing import OneHotEncoder
hot_encoder = OneHotEncoder (handle_unknown = 'ігнорувати')
hot_encoder.fit (ary_2d)
hot_encoder.categories_
>>
[масив (['foo', 'x'], dtype = об'єкт), array (['bar', 'y'], dtype = object), array (['baz', 'z'], dtype = object )]
hot_encoder.transform ([['foo', 'foo', 'baz'], ['y', 'y', 'x']]). toarray ()
>>
масив ([[1., 0., 0., 0., 1., 0.],
       [0., 0., 0., 1., 0., 0.]])

А як бути, якщо у нас був кадр даних для роботи?

Чи можемо ми все-таки використовувати одне гаряче кодування? Насправді це набагато простіше, ніж ви думаєте, оскільки вам просто потрібно використовувати .get_dummies (), що входить до складу панд.

pd.get_dummies (df)
      col3 col1_bar col1_foo col2_x col2_y col2_z
0 1 0 1 1 0 0
1 2 1 0 0 1 0
2 3 0 1 1 0 0
3 4 1 0 0 0 1

Два з трьох стовпців у df були розділені та двійкові, закодовані у кадр даних.

Наприклад стовпець col1_bar є col1 від df, але має 1 як значення запису, коли bar було значенням у вихідному кадрі даних.

А як щодо того, коли наші функції потрібно трансформувати в певному діапазоні. Використовуючи MinMaxScaler, кожну функцію можна індивідуально масштабувати таким чином, щоб вона знаходилась у заданому діапазоні. За замовчуванням значення становлять від 0 до 1, але ви можете змінити діапазон.

з sklearn.preprocessing import MinMaxScaler
mm_scaler = MinMaxScaler (feature_range = (0, 1)) # Від 0 до 1
mm_scaler.fit ([ary_int])
>> MinMaxScaler (копія = Правда, особливість_порядку = (0, 1))
друк (scaler.data_max_)
>> [5. -41. -67. 23. -53. -57. -36. -25. 10. 17.]
друк (mm_scaler.fit_transform ([ary_int]))
>> [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] # Гум щось не так

Якщо ви помітили, що можливий вихід - це всі нулі ... це не те, що ми хотіли. Тут і тут є хороше пояснення, чому це могло б статися, але коротка історія полягає в тому, що масив відформатований неправильно.

Це матриця (1, n) і її потрібно перетворити на (n, 1) матрицю. Найпростіший спосіб зробити це - переконатися, що ваш масив є нутризованим масивом, щоб ви могли маніпулювати фігурою.

# Створити масивний масив
ary_int = np.array ([5, -41, -67, 23, -53, -57, -36, -25, 10, 17])
# Перетворити
mm_scaler.fit_transform (ary_int [:, np.newaxis])
>>
масив ([[0.8],
       [0.28888889],
       [0. ],
       [1. ],
       [0.15555556],
       [0.11111111],
       [0.34444444],
       [0.46666667],
       [0.85555556],
       [0,93333333]])
# Ви також можете використовувати
mm_scaler.fit_transform (ary_int.reshape (-1, 1))
# Спробуйте також іншу шкалу
mm_scaler = MinMaxScaler (особливість_range = (0, 10))
mm_scaler.fit_transform (ary_int.reshape (-1, 1))
>>
масив ([[8.],
       [2.88888889],
       [0.],
       [10. ],
       [1.55555556],
       [1.11111111],
       [3.44444444],
       [4.66666667],
       [8.55555556],
       [9.33333333]])

Тепер, коли ми можемо швидко масштабувати наші дані, як щодо впровадження якоїсь форми в наші трансформовані дані? Ми розглядаємо стандартизацію даних, яка дасть вам значення, які створюють гаусса із середнім значенням 0 та sd 1. Ви можете розглянути цей підхід при здійсненні градієнтного спуску або якщо вам потрібні зважені входи, такі як регресія та нейронні мережі. Крім того, якщо ви збираєтесь впровадити KNN, спочатку масштабуйте свої дані. Зауважте, що такий підхід відрізняється від нормалізації, тому не варто плутатись.

Просто використовуйте шкалу від попередньої обробки.

precessing.scale (foo)
>> масив ([0.86325871, -0.58600774, -1.40515833, 1.43036297, -0.96407724, -1.09010041, -0.42847877, -0.08191506, 1.02078767, 1.24132821])
precessing.scale (foo) .mean ()
>> -4.4408920985006264e-17 # По суті нуль
 precessing.scale (foo) .std ()
>> 1.0 # Саме те, що ми хотіли

Останній пакет sklearn, на який слід звернути увагу, - це Binarizer, ви все ще отримуєте 0 і 1 через це, але тепер вони визначаються на ваших власних умовах. Це процес чисельного функціонування "порогів" для отримання булевих значень. Поріг значень, що перевищує поріг, буде відображатись на 1, тоді як значення ≤ до буде відповідати 0. Також це звичайний процес при попередній обробці тексту для отримання частотних термінів у документі чи корпусі.

Майте на увазі, що і fit (), і перетворення () потребують 2d масиву, тому я вклав ary_int в інший масив. Для цього прикладу я поставив поріг як -25, тому будь-які числа строго вище, що буде призначено 1.

від sklearn.preprocessing import Binarizer
# Встановіть -25 як наш поріг
tz = Бінанізатор (поріг = -25,0) .fit ([ary_int])
tz.transform ([ary_int])
>> масив ([[1, 0, 0, 1, 0, 0, 0, 0, 1, 1]])

Тепер, коли у нас є декілька різних методик, яка з них найкраща для вашого алгоритму? Напевно, найкраще зберегти кілька різних проміжних фреймів даних із масштабуванням даних, скріпленими даними тощо, щоб ви могли бачити ефект на виході вашої моделі.

Фінальні думки

Очищення та підготовка даних неминуче і, як правило, невдячне завдання, що стосується науки про дані. Якщо вам пощастить мати при собі команду з інженерії даних, яка допоможе налаштувати трубопроводи ETL, щоб полегшити вашу роботу, то, можливо, ви опинитеся в меншості.

Життя - це не лише купа наборів даних Kaggle, де насправді вам доведеться приймати рішення щодо доступу та очищення потрібних даних щодня. Іноді у вас буде багато часу, щоб переконатися, що все в потрібному місці, але більшість часу на вас будуть натискати відповіді. Якщо у вас є правильні інструменти та ви розумієте, що можливо, ви зможете легко дійти до цих відповідей.

Як завжди, сподіваюся, ви дізналися щось нове.

Ура,

Додаткове читання