Почистване и подготвяне на данни с Python за наука за данни - най-добри практики и полезни пакети

предговор

Почистването на данни е просто нещо, с което ще трябва да се справите в аналитиката. Не е голяма работа, но трябва да се свърши, за да можете да произвеждате страхотна работа.

Прекарах толкова време да пиша и пренаписвам функции, за да ми помогне да изчистя данните, че исках да споделя някои от наученото по пътя. Ако не сте минали през тази публикация, как да организирате по-добре проектите за наука за данни, проверете го, тъй като това ще помогне да се формират някои от концепциите, които преглеждам по-долу.

След като започнах да организирам по-добре кода си, започнах да поддържам персонализиран пакет, в който запазвам кода „почистване“ Ако не друго, то ми дава основа за писане на персонализирани методи върху данни, които не отговарят напълно на предишните ми скриптове за изчистване. И не е нужно да пиша този регекс-мейл екстрактор за 100-ти път, защото го запазих на достъпно място.

Някои компании имат цели екипи, посветени на кода за почистване, но повечето не. Затова е най-добре да разберете някои от най-добрите практики. Ако не друго, ще разберете по-добре разбирането на структурата на вашите данни, така че да обясните по-добре защо или защо не е станало нещо.

Също така, при подготовката за тази публикация се натъкнах на това репо от kjam, което би било невероятно полезно, когато за първи път се научих как да почиствам данни. Ако искате да се задълбочите в почистването на кода, предлагам да започнете там.

Вашата цел е да почистите нещата… или поне да опитате

Проверете данните си ... Бързо

Първото нещо, което искате да направите, когато получите нов набор от данни, е бързо да проверите съдържанието с метода .head ().

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

Сега да видим бързо имената и типовете колони. През повечето време ще получите данни, които не са точно това, което сте очаквали, като дати, които всъщност са низове и други странности. Но за да проверите предварително.

# Вземете имена на колони
имена на колони = df.колони
печат (COLUMN_NAMES)
# Вземете типове данни на колоните
df.dtypes
# Проверете също дали колоната е уникална
за i в имена на колони:
  print ('{} е уникален: {}'. формат (i, df [i] .is_unique))

Сега нека да видим дали кадърът на данни има индекс, свързан с него, като се обадите на .index на df. Ако няма индекс, ще получите AttributeError: обектът „функция“ няма атрибут „индекс“ се показва грешка.

# Проверете стойностите на индекса
df.index.values
# Проверете дали съществува определен индекс
'foo' в df.index.values
# Ако индекс не съществува
df.set_index ('колона-име_то_условие', inplace = Вярно)

Добре. Нашите данни са бързо проверени, знаем видовете данни, ако колоните са уникални и знаем, че има индекс, така че по-късно можем да правим присъединявания и сливания. Нека да разберем кои колони искате да запазите или премахнете. В този пример искаме да се отървем от колоните в индекси 1, 3 и 5, така че току-що добавих низовите стойности в списък, който ще се използва за пускане на колоните.

# Създайте разбиране на списъка на колоните, които искате да загубите
column_to_drop = [имена на колони [i] за i в [1, 3, 5]]
# Пуснете нежелани колони
df.drop (columns_to_drop, inplace = Вярно, ос = 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']. средно ())

Можете също да разпространявате ненулеви стойности напред или назад, като добавите метод = „pad“ като аргумент на метода. Той ще попълва следващата стойност в рамката от данни с предишната не-NaN стойност. Може би просто искате да попълните една стойност (граница = 1) или искате да попълните всички стойности. Каквото и да е, уверете се, че е в съответствие с останалото почистване на вашите данни.

df = pd.DataFrame (данни = {'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? Ако не бях попълнил ограничен тампон, той щеше да запълни цялата рамка от данни. Ние не се ограничаваме само до попълване напред, но и до пълнене с bfill.

# Попълнете първите две NaN стойности с първата налична стойност
df.fillna (метод = 'bfill)
    col1
0 2.0 # Попълнено
1 2.0 # Попълнено
2 2.0
3 3.0
4 4.0
5 NaN
6 NaN

Можете просто да ги пуснете изцяло от рамката на данни, или от реда, или от колоната.

# Изхвърлете всички редове, в които има всякакви гантели
df.dropna ()
# Пуснете колони, в които има всякакви Nans
df.dropna (ос = 1)
# Пускайте само колони, които имат най-малко 90% не-NaN
df.dropna (thresh = int (df.shape [0] * .9), ос = 1)

Параметърът thresh = N изисква колоната да има поне N не-NaNs, за да оцелее. Помислете за това като за долната граница за липсващи данни, които ще намерите приемливи в колоните си. Помислете за някои данни за регистриране, които може да пропуснат колекция от функции. Искате само записите, които имат 90% от наличните функции, преди да ги считате за кандидати за вашия модел.

np.where (if_this_is_true, do_this, else_do_that)

Виновна съм, че не използвам това по-рано в моята кариера в областта на аналитиката, защото е извън полезното. Спестява толкова много време и неудовлетвореност, когато преглеждате през рамка от данни. Ако искате да направите основно основно почистване или функция на техниката, np.дък тук е как можете да го направите.

Помислете дали оценявате колона и искате да знаете дали стойностите са строго по-големи от 10. Ако те са, искате резултата да е „foo“ и ако не, искате резултатът да е „bar“.

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

Можете да извършвате по-сложни операции като тази по-долу. Тук проверяваме дали записа на колоната започва с foo и не завършва с лента. Ако това се провери, ще върнем True, иначе ще върнем текущата стойност в колоната.

df ['new_column'] = np.where (df ['col']. str.startswith ('foo') и
                            не df ['col']. str.endswith ('bar'),
                            Вярно,
                            DF [ 'колона "])

И дори по-ефективно, можете да започнете да гнездите np.р., така че те да се подреждат един върху друг. Подобно на това, как бихте стекирали тройни операции, уверете се, че те са четими, тъй като можете бързо да се забъркате в силно вложени изявления.

# Три ниво гнездене с 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, 'Zero',
              np.where (df ['bar'] == 1, 'One',
                np.where (df ['bar'] == 2, 'Two', 'Three')))

Утвърдете и тествайте какво имате

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

Само защото разполагате с вашите данни в хубав кадър от данни, без дубликати, липсващи стойности, все още може да имате някои проблеми с основните данни. И с рамка от данни от 10M + редове или нов 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 ()
>>
Traceback (последно последно обаждане):
  Файл "", ред 1, в 
AssertionError

Ах, нашето твърдение се провали тук!

Най-добрата практика с твърдения е да се използва за тестване на условията във вашите данни, които никога не трябва да се случват. Това е така, когато стартирате кода си, всичко спира, ако едно от тези твърдения се провали.

Методът .all () ще провери дали всички елементи в обектите преминават аскрета, докато .any () ще провери дали някой от елементите в обектите преминава теста за акредитация.

Това може да бъде полезно, когато искате:

  • Проверете дали в данните са въведени някакви отрицателни стойности;
  • Уверете се, че две колони са абсолютно еднакви;
  • Определете резултатите от трансформация, или;
  • Проверете дали уникалният брой на идентификаторите е точен.

Има още методи за утвърждаване, които няма да преценя, но се запознайте, които можете да използвате тук. Никога няма да разберете кога трябва да се тествате за определено състояние и в същото време ще трябва да започнете тестване за условия, които не искате в кода си.

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

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

Освен това, тези панди за чудене включват и пакет за тестване.

импортиране на 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. Можете да научите повече за това тук.

beautifier

Вместо да се налага да напишете свой собствен регекс - което е болка в най-добрите моменти - понякога това се прави за вас. Пакетът за разкрасяване е в състояние да ви помогне да изчистите някои често използвани модели за имейли или URL адреси. Това не е нищо фантазия, но може бързо да помогне за почистване.

$ pip3 инсталирайте разкрасител
от внос на разкрасител Имейл, URL адрес
email_string = 'foo@bar.com'
имейл = имейл (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 (бара))
печат (ftfy.fix_text (Баз))

Ако искате да видите как се извършва декодирането, опитайте ftfy.explain_unicode (). Не мисля, че това ще бъде прекалено полезно, но е интересно да видите процеса.

ftfy.explain_unicode (Фу)
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] ЛАТИННО МАЛКО ПИСМО M
U + 0061 a [Ll] ЛАТИННО МАЛКО ПИСМО A
U + 0063 c [Ll] ЛАТИННО МАЛКО ПИСМО C
U + 0072 r [Ll] МАЛКО ПИСМО ЛАТИН R
U + 003B; [Po] SEMICOLON
U + 005C \ [Po] ОБРАТЕН СОЛИДУС
U + 005F _ [Pc] LOW LINE
U + 0028 ([Ps] ЛЯВО ПАРЕНТЕЗА
U + 00E3 ã [Ll] ЛАТИН МАЛКО ПИСМО А С ТИЛДЕ
U + 0083 \ x83 [Cc] <неизвестен>
U + 0084 \ x84 [Cc] <неизвестен>
U + 0029) [Пе] ПРАВЕН ПАРЕНТЕЗ
U + 005F _ [Pc] 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] МАЛКО ПИСМО ЛАТИН R
U + 003B; [Po] SEMICOLON
Нито един

Премахване на дубликати

Това е библиотека, която използва машинно обучение, за да извършва бързо дублиране и разделителна способност на структурирани данни. Тук има страхотна публикация, която обхваща много по-подробни неща, отколкото аз, и на които съм привлечена по-силно.

Ще преминем през изтегляне на данни за местоположението на Чикаго в ранното детство, които можете да намерите тук. Той има куп липсващи стойности и дублирани стойности от различни източници на данни, така че е добре да се научите на.

Ако някога сте минавали през дублирани данни, това ще изглежда много познато.

# Графи и броя на липсващите стойности във всяка
Id има 0 na стойности
Източникът има 0 na стойности
Името на сайта има 0 na стойности
Адресът има 0 na стойности
Zip има 1333 na стойности
Телефонът има 146 na стойности
Факсът има 3299 na стойности
Името на програмата има стойности за 2009 г.
Дължината на деня има стойности за 2009 г.
IDH доставчикът IDHS има 3298 na стойности
Агенцията има 3325 na стойности
Съседството има 2754 на стойности
Финансираното записване има 2424 на стойности
Опцията на програмата има 2800 на стойности
Броят на сайта EHS има 3319 na стойности
Броят на сайта HS има 3319 na стойности
Режисьорът има 3337 на стойности
Head Start Fund има 3337 на стойност
Eearly Head Start Fund има 2881 na стойности
CC фондът има 2818 na стойности
Progmod има 2818 na стойности
Уебсайтът има 2815 na стойности
Изпълнителният директор има 3114 на стойност
Директорът на центъра има 2874 na стойности
Наличните програми на ECE имат 2379 na стойности
NAEYC Валиден до има 2968 na стойности
Програмата NAEYC има 3337 на стойности
Имейл адресът има 3203 na стойности
Ounce of Prevention Описание има 3185 na стойности
Лилавият тип услуга за свързване има 3215 na стойности
Колоната има 3337 na стойности
Колона2 има 3018 na стойности

Методът preProcess, предоставен от dedupe, е необходим, за да се гарантира, че грешките не възникват по време на фазите на вземане на проби и обучение на модела. Повярвайте ми, използването на това ще направи използването на дедуп много по-лесно. Запишете този метод във вашия местен „пакет за почистване“, за да можете да го използвате в бъдеще при работа с дублирани данни.

импортиране на панди като pd
импортиране
импорт дедуп
import os
import csv
внос повторно
от импортиране
def preProcess (колона):
    '' '
    Използва се за предотвратяване на грешки по време на процеса на дедупване.
    '' '
    опитвам :
        column = column.decode ('utf8')
    с изключение на AttributeError:
        минавам
    колона = унидекод (колона)
    колона = re.sub ('+', '', колона)
    колона = re.sub ('\ n', '', колона)
    колона = колона.стрип (). лента ('"'). лента (" '"). долна (). лента ()
    
    ако не колона:
        колона = Няма
    графа за връщане

Сега започнете да импортирате колоната .csv по колона, докато обработвате данните.

def readData (име на файл):
    
    data_d = {}
    с отворен (име на файл) като f:
        читател = csv.DictReader (f)
        за ред в четеца:
            clean_row = [(k, preProcess (v)) за (k, v) в row.items ()]
            row_id = int (ред ['Id'])
            data_d [row_id] = dict (clean_row)
връщане df
name_of_file = 'data.csv'
print („Почистване и импортиране на данни ...“)
df = readData (name_of_file)

Сега трябва да кажем дедупт какви функции трябва да гледаме, за да определим дублиращи се стойности. По-долу всяка характеристика се обозначава с поле и се присвоява тип данни и ако има някакви липсващи стойности. Има цял списък от различни типове променливи, които можете да използвате тук, но за да бъдем лесни, засега ще се придържаме към низовете.

Аз също няма да използвам всяка една колона за определяне на дублирането, но можете, ако смятате, че ще улесни идентифицирането на стойностите във вашия кадър с данни.

# Задаване на полета
polja = [
        {'field': 'Source', 'type': 'Set'},
        {'field': 'Име на сайта', 'type': 'String'},
        {'field': 'Адрес', 'type': 'String'},
        {'field': 'Zip', 'type': 'Точно', 'липсва': True},
        {'field': 'Phone', 'type': 'String', 'has missing': True},
        {'field': 'Email Address', 'type': 'String', 'has missing': True},
        ]

Сега нека започнем да извеждаме изваждане на някои данни.

# Преминете в нашия модел
deduper = dedupe.Dedupe (полета)
# Проверете дали работи
deduper
>>
# Подайте някои примерни данни в ... 15000 записа
deduper.sample (df, 15000)

Сега сме на частта за етикетиране. Когато стартирате този метод по-долу, ще бъдете подканени от dedupe да направите някои прости етикети.

dedupe.consoleLabel (deduper)
Какво трябва да видите; ръчно обучение на дедупера

Истинският „ха!“ Момент е, когато получите това подкана. Това е deduper с молба да го обучите, така че да знаете какво да търсите. Знаете как трябва да изглежда дублирана стойност, така че просто предайте това знание.

Тези записи се отнасят ли към едно и също нещо?
(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, ние казваме deduper да стойност припомня точно толкова, колкото и точността. Въпреки това, ако Rec_weight = 3, бихме оценили припомнянето три пъти повече. Можете да играете с тези настройки, за да видите какво работи най-добре за вас.

праг = deduper.threshold (df, извикване_ тегло = 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. Огдън "
...
{'Id': '509',
 'Източник': 'cps_early_childhood_portal_scrape.csv',
 „Име на сайта“: „Армия за спасение - храм / спасителна армия“,
 „Адрес“: „1 n ogden ave“,
 „Цип“: Няма,
..

Това ми прилича на дубликати. Ница.

Има много по-усъвършенствани приложения на дедупъра, като matchBlocks за последователности на клъстери или полета за взаимодействие, при които взаимодействието между две полета не е просто аддитивно, а мултипликативно. Това вече е много за преодоляване, така че ще оставя това обяснение на статията по-горе.

Съпоставяне на струни с fuzzywuzzy

Опитайте тази библиотека. Това е наистина интересно, защото ви дава оценка за това колко близо са низовете, когато се сравняват.

Това беше изключително добър инструмент, тъй като в миналото съм правил проекти, в които трябваше да разчитам на неясната добавка на Google Sheet, за да диагностицирам проблеми с валидирането на данните - мислете, че правилата за CRM не се прилагат или действат правилно - и трябва да се почисти записи за извършване на всякакъв вид анализ.

Но за големи набори от данни този подход някак си пада.

Въпреки това, с fuzzywuzzy можете да започнете да влизате в съвпадение на низове в по-научен въпрос. За да не стане твърде технически, но използва нещо, наречено Левенщайн разстояние, когато сравняваме. Това е показател за сходство на низовете за две последователности, така че разстоянието между тях е броят на редакциите на един символ, необходими за промяна на една дума на друга.

Например ако искате да промените низовото фоно в лента, минималният брой знаци, които да промените, ще бъде 3 и това се използва за определяне на „разстоянието“.

Нека да видим как това работи на практика.

$ pip3 инсталирате fuzzywuzzy
# test.py
от fuzzywuzzy import fuzz
от процеса на импортиране fuzzywuzzy
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.

# Списък на дублиращи се имена на символи
съдържа_dupes = [
'Хари Потър',
"Х. Потър ",
„Хари Джеймс Потър“,
„Джеймс Потър“,
"Роналд Билиус" Рон \ "Уизли",
„Рон Уизли“,
„Роналд Уизли“]
# Отпечатайте дублиращите се стойности
process.dedupe (contains_dupes)
>>
dict_keys (['Хари Джеймс Потър', "Роналд Билиус 'Рон' Уизли '])
# Отпечатайте дублиращите се стойности с по-висок праг
process.dedupe (съдържа_дупи, праг = 90)
>>
dict_keys ([„Хари Джеймс Потър“, „Х. Потър“, „Роналд Билиус“ Рон „Уизли“])

И като бърз бонус можете да направите и размито съвпадение с пакета datetime, за да извлечете дати от низ от текст. Това е чудесно, когато не искате (отново) да пишете регекс.

от datautil.parser разбор на импортиране
dt = разбор („Днес е 1 януари 2047 г. в 8:21:00 ч.“, размит = Вярно)
печат (DT)
>>
2047-01-01 08:21:00
dt = разбор ("18 май, 2049 нещо нещо", размит = Вярно)
печат (DT)
>>
2049-05-18 00:00:00

Опитайте малко sklearn

Заедно с почистването на данните ще трябва да подготвите данните, така че да са във форма, в която можете да въведете вашия модел. Повечето от примерите тук са извлечени директно от документацията, която трябва да се провери, тъй като наистина върши добра работа по обяснението на повече от новостите на всеки пакет.

Първо ще импортираме пакета за предварителна обработка, а след това ще получим допълнителни методи оттам, докато продължаваме. Също така използвам 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]
# И някои str за работа
ary_str = ['foo', 'bar', 'baz', 'x', 'y', 'z']

Нека опитаме бързо етикетиране с LabelEncoder на нашия ary_str. Това е важно, защото не можете просто да храните сурови низове - добре можете, но това е извън обхвата на тази статия - във вашите модели. Така че, ние ще кодираме етикети към всеки от низовете, със стойност между 0 и n. В нашия ary_str имаме 6 уникални стойности, така че диапазонът ни ще бъде 0 - 5.

от sklearn.preprocessing import LabelEncoder
l_encoder = предварителна обработка.LabelEncoder ()
l_encoder.fit (ary_str)
>> LabelEncoder ()
# Какви са нашите ценности?
l_encoder.transform ([ "Фу '])
>> масив ([2])
l_encoder.transform ([ 'Баз'])
>> масив ([1])
l_encoder.transform ([ "лента"])
>> масив ([0])

Ще забележите, че те не са подредени, тъй като дори и чрез foo дойде преди бар в масива, той беше кодиран с 2, докато бар е кодиран с 1. Ще използваме различен метод на кодиране, когато трябва да се уверим, че нашите стойности са кодирани в правилния ред.

Ако имате много категории, които да следите, може да забравите кои str карти към кой 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.preprocessing import OrdinalEncoder
o_encoder = OrdinalEncoder ()
ary_2d = [['foo', 'bar', 'baz'], ['x', 'y', 'z']]
o_encoder.fit (2d_ary) # Поставете стойностите
o_encoder.transform ([['foo', 'y']])
>> масив ([[0., 1.]])

Класическото горещо или „манекенно“ кодиране, при което единичните характеристики на категориите след това се изразяват като допълнителни колони от 0s или 1s, в зависимост от това стойността се появява или не. Този процес създава двоична колона за всяка категория и връща оскъдна матрица или плътен масив.

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

Защо дори да използвате това? Тъй като този тип кодиране е необходим за подаване на категорични данни към много модели scikit, като модели с линейна регресия и SVM. Затова се успокойте с това.

от sklearn.preprocessing import OneHotEncoder
hot_encoder = OneHotEncoder (handle_unknown = 'игнорирай')
hot_encoder.fit (ary_2d)
hot_encoder.categories_
>>
[array (['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 (copy = True, feature_range = (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.] # Humm нещо не е наред

Ако забележите, че изходният резултат е всички нули ... което не е това, което искахме. Тук има и добро обяснение защо това би се случило, но кратката история е, че масивът е форматиран неправилно.

Това е (1, n) матрица и трябва да се преобразува в (n, 1) матрица. Най-лесният начин да направите това е да се уверите, че масивът е масив масив, така че да можете да манипулирате формата.

# Създайте numpy масив
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 (характеристика_оранжева = (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, първо мащабирайте данните си. Имайте предвид, че този подход е различен от нормализирането, така че не се бъркайте.

Просто използвайте скалата от предварителна обработка.

preprocessing.scale (Фу)
>> масив ([0.86325871, -0.58600774, -1.40515833, 1.43036297, -0.96407724, -1.09010041, -0.42847877, -0.08191506, 1.02078767, 1.24132821])
preprocessing.scale (Фу) .mean ()
>> -4.4408920985006264e-17 # По същество нула
 preprocessing.scale (Фу) .std ()
>> 1.0 # Точно това, което искахме

Последният склеарн пакет, който трябва да разгледате, е Binarizer, все още получавате 0 и 1 през това, но сега те са определени според вашите собствени условия. Това е процес на „прагови“ числови функции, за да се получат булеви стойности. Прагът на стойностите, по-голям от прага, ще се преобразува на 1, докато тези ≤ към ще се отбележат на 0. Освен това, това е често срещан процес при предварителна обработка на текст, за да получите термина честоти в рамките на документ или корпус.

Имайте предвид, че и fit (), и transform () изискват 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, където в действителност ще трябва да вземате решения как да осъществявате достъп и почистване на данните, от които се нуждаете всеки ден. Понякога ще имате много време, за да се уверите, че всичко е на правилното място, но през повечето време ще бъдете притиснати за отговори. Ако разполагате с подходящите инструменти и разбирате какво е възможно, ще можете лесно да стигнете до тези отговори.

Както винаги, надявам се да сте научили нещо ново.

Наздраве,

Допълнително четене