Rengjøring og prepping av data med Python for datavitenskap - beste fremgangsmåter og nyttige pakker

Forord

Rengjøring av data er bare noe du må håndtere i analysen. Det er ikke bra arbeid, men det må gjøres slik at du kan produsere bra arbeid.

Jeg har brukt så mye tid på å skrive og skrive om funksjoner for å hjelpe meg med å rense data, at jeg ønsket å dele noe av det jeg har lært underveis. Hvis du ikke har gått over dette innlegget, kan du undersøke hvordan du kan organisere datavitenskapelige prosjekter bedre, for det kan hjelpe deg med å danne noen av konseptene jeg går over nedenfor.

Etter å ha begynt å organisere koden min bedre, har jeg begynt å holde en tilpasset pakke der jeg holder "rydde opp" koden. Hvis noe annet, gir det meg en grunnleggende måte å skrive egendefinerte metoder på data som ikke helt passer til mine tidligere ryddeskript. Og jeg trenger ikke skrive den regex-e-postuttrekkeren for 100. gang fordi jeg har lagret den på et tilgjengelig sted.

Noen selskaper har hele teamene som er viet til rengjøringskode, men de fleste gjør det ikke. Så det er best å forstå noen av de beste praksisene. Hvis noe, vil du bli flinkere til å forstå strukturen til dataene dine, slik at du bedre kan forklare hvorfor eller hvorfor ikke noe har skjedd.

I løpet av forberedelsene til dette innlegget kjørte jeg over denne repoen av kjam, noe som ville vært utrolig nyttig da jeg først lærte å rense data. Hvis du vil gå dypere inn i koderengjøring, foreslår jeg at du starter der.

Målet ditt er å rydde opp i ting ... eller i det minste prøve å gjøre det

Kontroller dataene dine ... raskt

Det første du vil gjøre når du får et nytt datasett, er å raskt verifisere innholdet med .head () -metoden.

importer pandaer som pd
df = pd.read_csv ('path_to_data')
df.head (10)
>>
... noe output her ...

La oss raskt se navnene og typene på kolonnene. Det meste av tiden du kommer til å få data som ikke er helt som du forventet, for eksempel datoer som faktisk er strenger og andre godheter. Men for å sjekke på forhånd.

# Få kolonnenavn
column_names = df.columns
print (COLUMN_NAMES)
# Få kolonnedatatyper
df.dtypes
# Sjekk også om kolonnen er unik
for i i column_names:
  print ('{} er unikt: {}'. format (i, df [i] .is_unique))

La oss se om dataframmen har en indeks tilknyttet den ved å ringe .index på df. Hvis det ikke er noen indeks, får du et AttributeError: 'funksjon'-objekt har ingen attributt' indeks '-feil.

# Kontroller indeksverdiene
df.index.values
# Sjekk om det finnes en viss indeks
'foo' i df.index.values
# Hvis indeks ikke eksisterer
df.set_index ('column_name_to_use', inplace = True)

God. Dataene våre er raskt sjekket, vi vet datatypene, hvis kolonnene er unike, og vi vet at de har en indeks slik at vi kan gjøre sammenføyninger og sammenslåing senere. La oss finne ut hvilke kolonner du vil beholde eller fjerne. I dette eksemplet ønsker vi å bli kvitt kolonnene i indeksene 1, 3 og 5, så jeg har nettopp lagt strengverdiene til en liste, som vil bli brukt til å slippe kolonnene.

# Lag listeforståelse av kolonnene du vil miste
columns_to_drop = [column_names [i] for i i [1, 3, 5]]
# Slipp uønskede kolonner
df.drop (columns_to_drop, inplace = True, axis = 1)

Inplace = True er lagt til, slik at du ikke trenger å lagre over den opprinnelige df ved å tilordne resultatet av .drop () til df. Mange av metodene i pandaer støtter på stedet = Sant, så prøv å bruke det så mye som mulig for å unngå unødvendig omfordeling.

Hva å gjøre med NaN

Hvis du trenger å fylle ut feil eller emner, bruker du metodene fillna () og dropna (). Det virker raskt, men all manipulasjon av dataene skal dokumenteres slik at du kan forklare dem for noen på et senere tidspunkt.

Du kan fylle NaN-ene med strenger, eller hvis de er tall, kan du bruke middelverdien eller medianverdien. Det er mye debatt om hva som gjør med manglende eller misdannede data, og riktig svar er ... det kommer an på.

Du må bruke ditt beste skjønn og innspill fra menneskene du jobber med, hvorfor fjerning eller fylling av data er den beste tilnærmingen.

# Fyll NaN med ''
df ['col'] = df ['col']. fillna ('')
# Fyll NaN med 99
df ['col'] = df ['col']. fillna (99)
# Fyll NaN med gjennomsnittet av kolonnen
df ['col'] = df ['col']. fillna (df ['col']. middel ())

Du kan også formidle ikke-nullverdier fremover eller bakover ved å sette metode = 'pad' som metodeargumentet. Den vil fylle den neste verdien i dataframmen med den forrige ikke-NaN-verdien. Kanskje du bare vil fylle en verdi (limit = 1), eller du vil fylle alle verdiene. Uansett hva det er, sørg for at det stemmer overens med resten av datarengjøringen din.

df = pd.DataFrame (data = {'col1': [np.nan, np.nan, 2,3,4, np.nan, np.nan]})
    kol1
0 NaN
1 NaN
2 2,0
3 3.0
4 4.0 # Dette er verdien som skal fylles frem
5 NaN
6 NaN
df.fillna (metode = 'pad', limit = 1)
    kol1
0 NaN
1 NaN
2 2,0
3 3.0
4 4.0
5 4.0 # Fylt frem
6 NaN

Legg merke til hvordan bare indeks 5 var fylt? Hvis jeg ikke hadde fylt begrenset puten, ville det fylt hele dataframmen. Vi er ikke begrenset til foroverfylling, men også påfylling med bfill.

# Fyll de to første NaN-verdiene med den første tilgjengelige verdien
df.fillna (method = 'bfill')
    kol1
0 2.0 # Fylt
1 2,0 # Fyllt
2 2,0
3 3.0
4 4.0
5 NaN
6 NaN

Du kan bare slippe dem fra dataframmen helt, enten ved raden eller ved kolonnen.

# Slipp alle rader som har noen nans
df.dropna ()
# Slipp kolonner som har noen nans
df.dropna (akse = 1)
# Bare slipp kolonner som har minst 90% ikke-NaN-er
df.dropna (terskel = int (df.shape [0] * .9), akse = 1)

Parameteren terskel = N krever at en kolonne har minst N ikke-NaNer for å overleve. Tenk på dette som den nedre grensen for manglende data du vil finne akseptabelt i kolonnene dine. Vurder noen loggføringsdata som kan gå glipp av noen funksjoner. Du vil bare ha postene som har 90% av de tilgjengelige funksjonene før du anser dem som kandidater til modellen din.

np.where (hvis_dette_dette_dette, gjør_dette, ellers_ gjør_dette)

Jeg er skyldig i ikke å bruke dette tidligere i analysekarrieren min, fordi det er hinsides nyttig. Det sparer så mye tid og frustrasjon når man munger gjennom et dataframe. Hvis du vil gjøre grunnleggende rengjøring eller funksjonsteknikk raskt, kan du finne ut hvor du kan gjøre det her.

Vurder om du vurderer en kolonne, og du vil vite om verdiene strengt tatt er større enn 10. Hvis de er, vil du at resultatet skal være 'foo', og hvis ikke vil du at resultatet skal være 'bar'.

# Følg denne syntaks
np.where (hvis_dette_kondisjon_en_ sannhet, gjør dette, ellers dette)
# Eksempel
df ['new_column'] = np.where (df [i]> 10, 'foo', 'bar)

Du kan utføre mer komplekse operasjoner som nedenfor. Her sjekker vi om kolonneposten starter med foo og ikke slutter med stolpe. Hvis dette sjekker ut kommer vi tilbake True ellers returnerer vi den gjeldende verdien i kolonnen.

df ['new_column'] = np.where (df ['col']. str.startswith ('foo') og
                            ikke df ['col']. str.endswith ('bar'),
                            Ekte,
                            df [ 'col'])

Og enda mer effektiv, kan du begynne å hekke np.where, slik at de stabler på hverandre. På samme måte som hvordan du stabler ternære operasjoner, må du sørge for at de er lesbare, da du raskt kan komme inn i et rot med sterkt nestede utsagn.

# Tre nivå hekkende med np.where
np.where (hvis_ dette_kondisjonen_truen_en, gjør dette
  np.where (hvis_dette_kondisjon_en_trå_tv, gjør-det,
    np.where (hvis_ dette_stand_et_tritt_tred, do_foo, do_bar)))
# Et trivielt eksempel
df ['foo'] = np.where (df ['bar'] == 0, 'Zero',
              np.where (df ['bar'] == 1, 'One',
                np.where (df ['bar'] == 2, 'Two', 'Three')))

Hev og test det du har

Kreditt til https://www.programiz.com

Bare fordi du har dataene dine i et fint dataramme, ingen duplikater, ingen manglende verdier, har du fremdeles noen problemer med de underliggende dataene. Og med et dataramme på 10M + rader eller ny API, hvordan kan du sørge for at verdiene er nøyaktig det du forventer at de skal være?

Sannheten er at du aldri virkelig vet om dataene dine er riktige før du tester dem. Beste praksis innen programvareteknikk er avhengige av å teste arbeidet sitt, men for datavitenskap er det fremdeles et arbeid som pågår. Bedre å starte nå og lære deg gode arbeidsprinsipper, i stedet for å måtte omskolere deg på et senere tidspunkt.

La oss lage en enkel dataframme å teste.

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

La oss teste om alle verdiene i col1 er> = 0 ved å bruke den innebygde metodeanføringen som følger med standardbiblioteket i python. Det du spør python om er sant, alle elementene i df [‘col1’] er større enn null. Hvis dette er sant, fortsett på vei, hvis ikke kast en feil.

påstå (df ['col1']> = 0) .all () # Skal ikke returnere noe

Flott ser ut til å ha fungert. Men hva om .all () ikke var inkludert i påstanden?

påstå (df ['col1']> = 0)
>>
ValueError: Sannhetsverdien til en serie er tvetydig. Bruk a.empty, a.bool (), a.item (), a.any () eller a.all ().

Humm ser ut som om vi har noen alternativer når vi tester dataframmen. La oss prøve er at verdiene er strengene.

påstå (df ['col1']! = str) .any () # Skal ikke returnere noe

Hva med å teste de to kolonnene for å se om de er like?

påstå (df ['col1'] == df ['col2']). alle ()
>>
Traceback (siste samtale sist):
  Filen "", linje 1, i 
AssertionError

Ah, påstanden vår mislyktes her!

Den beste fremgangsmåten med påstander skal brukes til å teste forholdene i dine data som aldri skal skje. Dette er slik at når du kjører koden din, stopper alt hvis en av disse påstandene mislykkes.

Metoden .all () vil sjekke om alle elementene i objektene har bestått påstanden, mens .any () vil sjekke om noen av elementene i objektene klarer påstandstesten.

Dette kan være nyttig når du vil:

  • Sjekk om det har blitt introdusert negative verdier i dataene;
  • Forsikre deg om at to kolonner er nøyaktig de samme.
  • Bestem resultatene av en transformasjon, eller;
  • Sjekk om unikt ID-antall er nøyaktig.

Det er flere påståelsesmetoder som jeg ikke vil gå over, men bli kjent som du kan bruke her. Du vil aldri vite når du trenger å teste for en viss tilstand, og samtidig må du begynne å teste for forhold du ikke vil ha i koden.

Ikke test for alt, men test for ting som kan ødelegge modellene dine.

F.eks Er en funksjon med bør alle være 0s og 1s, faktisk befolket med disse verdiene.

I tillegg inkluderer den rart pakke-pandaene også en testpakke.

import pandas.util.testing som tm
tm.assert_series_equal (df ['col1'], df ['col2'])
>>
AssertionError: Seriene er forskjellige
Seriens verdier er forskjellige (100,0%)
[venstre]: [0, 6, 8, 0, 3, 4, 3, 9, 0, 7]
[høyre]: [6, -1, 4, 5, -7, -5, -10, -8, 4, -4]

Ikke bare fikk vi en feil som ble kastet, men pandaer fortalte oss hva som var galt.

Hyggelig.

I tillegg, hvis du vil begynne å bygge deg en testsuite - og det kan være lurt å tenke på å gjøre dette - bli kjent med den unitteste pakken som er innebygd i Python-biblioteket. Du kan lære mer om det her.

beautifier

I stedet for å måtte skrive din egen regex - noe som er vondt i de beste tider - er det noen ganger gjort for deg. Kosmetikkpakken kan hjelpe deg med å rydde opp i noen vanlige mønstre for e-post eller nettadresser. Det er ikke noe fancy, men kan raskt hjelpe med opprydding.

$ pip3 installer forskjønnelsesmiddel
fra beautifier import e-post, URL
email_string = 'foo@bar.com'
email = Email (email_string)
print (email.domain)
print (email.username)
print (email.is_free_email)
>>
bar.com
foo
Falsk
url_string = 'https://github.com/labtocat/beautifier/blob/master/beautifier/__init__.py'
url = URL (url_string)
print (url.param)
print (url.username)
print (url.domain)
>>
Ingen
{'msg': '-funksjonen er for øyeblikket bare tilgjengelig med koble URL-adresser'}
github.com

Jeg bruker denne pakken når jeg har en rekke nettadresser jeg trenger å jobbe gjennom og ikke ønsker å skrive regex for 100. gang for å trekke ut visse deler av adressen.

Håndterer Unicode

Når du gjør noe NLP, kan det å jobbe med Unicode være frustrerende i de beste tider. Jeg kjører noe i spaCy, og plutselig vil alt ødelegge meg på grunn av at en unicode-karakter vises et sted i dokumentet.

Det er virkelig det verste.

Ved å bruke ftfy (fikset det for deg) kan du fikse virkelig ødelagte Unicode. Tenk på når noen har kodet Unicode med en standard og dekodet den med en annen. Nå må du ta tak i dette mellom strengen, som tullete sekvenser som kalles "mojibake".

# Eksempel på mojibake
& Macr; \\ _ (A \ x83 \ x84) _ / & macr;
\ ufeffParty
\ 001 \ 033 [36; 44mI & # x92; m

Heldigvis bruker ftfy heuristikk for å oppdage og angre mojibake, med en veldig lav grad av falske positiver. La oss se hva strengene våre over kan konverteres til, så vi kan lese det. Hovedmetoden er fix_text (), og du vil bruke den til å utføre avkodingen.

importer ftfy
foo = '& macr; \\ _ (ã \ x83 \ x84) _ / & macr;'
bar = '\ ufeffParty'
baz = '\ 001 \ 033 [36; 44mI & # x92; m'
print (ftfy.fix_text (foo))
print (ftfy.fix_text (bar))
print (ftfy.fix_text (baz))

Hvis du vil se hvordan avkodingen gjøres, kan du prøve ftfy.explain_unicode (). Jeg tror ikke dette vil være for nyttig, men det er interessant å se prosessen.

ftfy.explain_unicode (foo)
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] LATIN SMALL LETTER M
U + 0061 a [Ll] LATIN SMALL LETTER A
U + 0063 c [Ll] LATIN SMALL LETTER C
U + 0072 r [Ll] LATIN SMALL LETTER R
U + 003B; [Po] SEMICOLON
U + 005C \ [Po] REVERSE SOLIDUS
U + 005F _ [Stk] LAV LINE
U + 0028 ([Ps] VENSTRE FORELDRE
U + 00E3 ã [Ll] LATIN LITEN BREV A MED TILDE
U + 0083 \ x83 [CC] 
U + 0084 \ x84 [CC] 
U + 0029) [Pe] HØYRE FORELDRE
U + 005F _ [Stk] LAV LINE
U + 002F / [Po] SOLIDUS
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] LATIN SMALL LETTER M
U + 0061 a [Ll] LATIN SMALL LETTER A
U + 0063 c [Ll] LATIN SMALL LETTER C
U + 0072 r [Ll] LATIN SMALL LETTER R
U + 003B; [Po] SEMICOLON
Ingen

dedupe

Dette er et bibliotek som bruker maskinlæring for å utføre de-duplisering og enhetsoppløsning raskt på strukturerte data. Det er et flott innlegg her som går langt mer detaljert enn jeg vil og som jeg har trukket sterkt på.

Vi vil gå gjennom nedlastingsdata for Chicago Early Childhood Location, som du finner her. Den har en mengde manglende verdier og dupliserte verdier fra forskjellige datakilder, så det er bra å lære videre.

Hvis du noen gang har gått gjennom dupliserte data før, vil dette se veldig kjent ut.

# Kolonner og antall manglende verdier i hver
ID har 0 na verdier
Kilden har 0 na verdier
Nettstedsnavn har 0 na verdier
Adressen har 0 na verdier
Zip har 1333 na verdier
Telefonen har 146 na verdier
Faks har 3299 na-verdier
Programnavn har 2009 na verdier
Length of Day har 2009 na verdier
IDHS-leverandør-ID har 3298 na-verdier
Byrået har 3325 na verdier
Nabolaget har 2754 na verdier
Finansiert innmelding har 2424 na verdier
Programalternativet har 2800 na verdier
Antall per nettsted EHS har 3319 na verdier
Antall per nettsted HS har 3319 na verdier
Direktør har 3337 na verdier
Head Start Fund har 3337 na verdier
Eearly Head Start Fund har 2881 verdier
CC-fond har 2818 na verdier
Progmod har 2818 na-verdier
Nettstedet har 2815 na verdier
Konserndirektør har 3114 na verdier
Senterdirektør har 2874 na verdier
ECE-tilgjengelige programmer har 2379 na-verdier
NAEYC Valid Till har 2968 na verdier
NAEYC-program-ID har 3337 na-verdier
E-postadresse har 3203 na-verdier
Ounce of Prevention Description har 3185 na verdier
Servicetype for lilla bindemiddel har 3215 na verdier
Kolonne har 3337 na-verdier
Kolonne2 har 3018 na-verdier

Forprosessmetoden levert av dedupe er nødvendig for å sikre at feil ikke oppstår i prøvetakings- og opplæringsfasene til modellen. Stol på meg, å bruke dette vil gjøre bruk av dedupe mye enklere. Lagre denne metoden i din lokale ‘rengjøringspakke’, slik at du kan bruke den i fremtiden når du arbeider med dupliserte data.

importer pandaer som pd
importer numpy
importere dedupe
import os
import csv
importer re
fra unidecode import unidecode
def preProcess (kolonne):
    '''
    Brukes for å forhindre feil under uttaksprosessen.
    '''
    prøv:
        column = column.decode ('utf8')
    unntatt AttributeError:
        passere
    column = unidecode (column)
    column = re.sub ('+', '', kolonne)
    column = re.sub ('\ n', '', kolonne)
    column = column.strip (). strip ('"'). strip (" '"). nedre (). strip ()
    
    hvis ikke kolonne:
        kolonne = Ingen
    returkolonne

Begynn nå å importere .csv-kolonnen etter kolonne, mens du behandler dataene.

def readData (filnavn):
    
    data_d = {}
    med åpent (filnavn) som f:
        reader = csv.DictReader (f)
        for rad i leser:
            clean_row = [(k, preProcess (v)) for (k, v) i row.items ()]
            row_id = int (rad ['Id'])
            data_d [row_id] = dict (clean_row)
returner df
name_of_file = 'data.csv'
print ('Rengjøring og import av data ...')
df = readData (name_of_file)

Nå må vi fortelle om hvilke funksjoner vi bør se på for å bestemme dupliserte verdier. Nedenfor er hver funksjon betegnet etter felt, og tildelt en datatype og hvis den har noen manglende verdier. Det er en hel liste over forskjellige variabeltyper du kan bruke her, men for å holde det enkelt holder vi fast med strenger foreløpig.

Jeg kommer heller ikke til å bruke hver eneste kolonne for å bestemme dupliseringen, men du kan hvis du tror det vil gjøre det lettere å identifisere verdiene i dataframmen.

# Angi felt
felt = [
        {'felt': 'Kilde', 'type': 'Sett'},
        {'felt': 'Nettstednavn', 'type': 'String'},
        {'felt': 'Adresse', 'type': 'String'},
        {'felt': 'Zip', 'type': 'Exact', 'mangler': True},
        {'felt': 'Telefon', 'type': 'Streng', 'mangler': True},
        {'felt': 'E-postadresse', 'type': 'Streng', 'mangler': True},
        ]

La oss begynne å mate noen data.

# Pass i vår modell
deduper = dedupe.Dedupe (felt)
# Sjekk om det fungerer
deduper
>>
# Legg inn noen eksempeldata i ... 15000 poster
deduper.ample (df, 15000)

Nå går vi videre til merkingsdelen. Når du kjører denne metoden nedenfor, vil du bli bedt om å gjøre noe enkelt merking.

dedupe.consoleLabel (deduper)
Hva du bør se; manuell trening av deduperen

Det virkelige ‘a ha!’ Øyeblikket er når du får denne beskjeden. Dette er en deduper som ber deg om å trene den, så den vet hva den skal se etter. Du vet hvordan en duplikatverdi skal se ut, så bare gi den kunnskapen videre.

Henviser disse postene til det samme?
(y) es / (n) o / (u) nsure / (f) inished

Nå trenger du ikke lenger å søke i tonn og tonn med poster for å se om duplisering har skjedd. I stedet blir du opplært et nevralt nett for å finne duplikater i dataframmen.

Når du har gitt det noen merking, fullfør treningsprosessen og lagre fremgangen din. Du kan komme tilbake til nevrale nettet senere hvis du finner ut at du har gjentatte dataframe-objekter som trenger deduksjon.

deduper.train ()
# Lagre trening
med åpen (training_file, 'w') som tf:
        deduper.writeTraining (tf)
# Lagre innstillinger
med åpen (innstillinger_fil, 'wb') som sf:
        deduper.writeSettings (sf)

Vi er nesten ferdig, siden vi neste gang må sette en terskel for dataene våre. Når tilbakekallingsvekt er lik 1, forteller vi deduper til verdiinnkalling like mye som presisjon. Imidlertid, hvis tilbakekallingsvekt = 3, vil vi verdsette tilbakekalling tre ganger så mye. Du kan leke med disse innstillingene for å se hva som fungerer best for deg.

terskel = deduper.threshold (df, tilbakekallingsvekt = 1)

Endelig kan vi nå søke gjennom df og se hvor duplikatene er. Det har vært lenge å komme til denne stillingen, men dette er mye bedre enn å gjøre dette for hånd.

# Cluster duplikatene sammen
clustered_dupes = deduper.match (data_d, terskel)
print ('Det er {} dupliserte sett'.format (len (clustered_dupes)))

Så la oss se på duplikatene våre.

clustered_dupes
>>
[((0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255),
  matrise ([0,88552043, 0,88552043, 0,777351897, 0,88552043, 0,88552043,
         0,88552043, 0,88552043, 0,89765924, 0,75684386, 0,83023088])),
 ((2, 3, 216, 511, 512, 1227, 1228, 2687), ...

Hum, det forteller oss ikke så mye. Hva er det som viser oss? Hva skjedde med alle verdiene våre?

Hvis du ser nøye på verdiene (0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255) er alle ID-plasseringene til duplikater deduper mener faktisk er den samme verdien. Og vi kan se på de opprinnelige dataene for å bekrefte dette.

{'Id': '215',
 'Kilde': 'cps_early_childhood_portal_scrape.csv',
 'Nettstednavn': 'frelsehærens tempel',
 'Adresse': '1 n. ogden',
...
{'Id': '509',
 'Kilde': 'cps_early_childhood_portal_scrape.csv',
 'Nettstednavn': 'frelsesarme - tempel / frelsesarme',
 'Adresse': '1 n ogden ave',
 'Zip': Ingen,
..

Dette ser ut som duplikater for meg. Hyggelig.

Det er mange mer avanserte bruksområder for deduper, for eksempel matchBlocks for sekvenser av klynger, eller Interaction felt der interaksjonen mellom to felt ikke bare er additiv, men multiplikativ. Dette har allerede vært mye å gå over, så jeg får legge igjen den forklaringen til artikkelen over.

String Matching med fuzzywuzzy

Prøv dette biblioteket. Det er veldig interessant fordi det gir deg poengsum for hvor nære strenger det er når de sammenliknes.

Dette har vært et utrolig flott verktøy, siden jeg har utført prosjekter i det siste der jeg har hatt behov for å stole på Google Sheets fuzzymatch-addon for å diagnostisere datavalideringsproblemer - tror CRM-regler ikke blir brukt eller handlet på riktig måte - og nødvendig å rengjøre poster for å gjøre noen form for analyse.

Men for store datasett faller denne tilnærmingen ganske flat.

Imidlertid kan du med fuzzywuzzy begynne å komme i streng matching i en mer vitenskapelig sak. Ikke for å bli for teknisk, men det bruker noe som heter Levenshtein distanse når man sammenligner. Dette er en streng likhetsmetrikk for to sekvenser, slik at avstanden mellom er antall endringer med ett tegn som kreves for å endre det ene ordet til det andre ordet.

F.eks Hvis du vil endre streng foo til stolpe, vil minimum antall tegn som skal endres være 3, og dette brukes til å bestemme ‘avstanden’.

La oss se hvordan dette fungerer i praksis.

$ pip3 installer fuzzywuzzy
# test.py
fra fuzzywuzzy import fuzz
fra fuzzywuzzy importprosess
foo = 'er denne strengen'
bar = 'som den strengen?'
fuzz.ratio (foo, bar)
>>
71
fuzz.WRatio (foo, bar) # Vektet forhold
>>
73
fuzz.UQRatio (foo, bar) # Unicode-hurtigforhold
>> 73

Den fuzzywuzzy pakken har forskjellige måter å evaluere strenger (WRatio, UQRatio, etc.), og jeg vil bare følge standardimplementeringen for denne artikkelen.

Deretter kan vi se på en tokenisert streng, som returnerer et mål på sekvensenes likhet mellom 0 og 100, men sorterer tokenet før du sammenligner. Dette er nøkkelen, siden du kanskje bare vil se innholdet i strengene, i stedet for deres posisjoner.

Strengene foo og bar har de samme symbolene, men er strukturelt forskjellige. Vil du behandle dem på samme måte? Nå kan du enkelt se og redegjøre for denne typen forskjeller i dataene dine.

foo = 'dette er en foo'
bar = 'foo a er dette'
fuzz.ratio (foo, bar)
>>
31
fuzz.token_sort_ratio ('dette er en foo', 'foo a er dette')
>>
100

Eller neste, må du finne det nærmeste samsvaret med en streng fra en liste over verdier. I dette tilfellet kommer vi til å se på Harry Potter-titler.

Hva med den Harry Potter-boken med ... noe tittel ... den har ... jeg vet ikke. Jeg trenger bare å gjette og se hvilken av disse bøkene som er nærmest min gjetning.

Min gjetning er ‘ild’ og la oss se hvordan det scorer mot den mulige listen med titler.

lst_to_eval = ['Harry Potter og filosofens stein',
'Harry Potter og hemmelighetskammeret',
'Harry Potter og fangen fra Azkaban',
'Harry Potter og ildbegeret',
'Harry Potter og Føniksordenen',
'Harry Potter og Halvblodsprinsen',
'Harry Potter og dødstalismanene']
# Topp to svar basert på gjetningen min
prosess.extract ("brann", lst_to_eval, limit = 2)
>>
[('Harry Potter and the Goblet of Fire', 60), ("Harry Potter and the Sorcerer's Stone", 30)
results = process.extract ("brann", lst_to_eval, limit = 2)
for resultat i resultater:
  print ('{}: har en poengsum på {}'. format (resultat [0], resultat [1]))
>>
Harry Potter and the Goblet of Fire: har en score på 60
Harry Potter and the Sorcerer's Stone: har en score på 30

Eller hvis du bare vil returnere en, kan du det.

>>> prosess.extractOne ("stein", lst_to_eval)
("Harry Potter and the Sorcerer's Stone", 90)

Jeg vet at vi snakket om dedupeing tidligere, men her er en annen applikasjon av den samme prosessen med fuzzywuzzy. Vi kan ta en liste over strenger som inneholder duplikater og bruker uklar matching for å identifisere og fjerne duplikater.

Ikke så fancy som et nevralt nett, men det vil gjøre jobben for små operasjoner.

Vi fortsetter med Harry Potter-temaet, og ser etter dupliserte tegn fra bøkene på en liste.

Du må angi en terskel mellom 0 og 100. Når terskelen reduseres, vil antall duplikater som vil øke, øke listen som blir returnert. Standard er 70.

# Liste over dupliserte karakternavn
inneholder_dupes = [
'Harry Potter',
'H. Potter',
'Harry James Potter',
'James Potter',
'Ronald Bilius \' Ron \ 'Weasley',
'Ron Weasley',
'Ronald Weasley']
# Skriv ut duplikatverdiene
process.dedupe (contains_dupes)
>>
dict_keys (['Harry James Potter', "Ronald Bilius 'Ron' Weasley"])
# Skriv ut duplikatverdiene med en høyere terskel
process.dedupe (inneholder_dupes, terskel = 90)
>>
dict_keys (['Harry James Potter', 'H. Potter', 'Ronald Bilius' Ron 'Weasley "])

Og som en rask bonus kan du også gjøre litt uklare samsvar med datetime-pakken for å trekke ut datoer fra en tekststreng. Dette er flott når du ikke vil (igjen) skrive et regex-uttrykk.

fra dateutil.parser importparse
dt = parse ("I dag er det 1. januar 2047 kl. 21.21)", uklar = Sann)
print (dt)
>>
2047-01-01 08:21:00
dt = parse ("18. mai 2049 noe noe", uklar = Sann)
print (dt)
>>
2049-05-18 00:00:00

Prøv litt sklearn

Sammen med rengjøring av dataene, må du også utarbeide dataene slik at de er i et skjema du kan mate inn i modellen din. De fleste eksemplene her er trukket direkte fra dokumentasjonen, som bør sjekkes ut, da den virkelig gjør en god jobb med å forklare mer om nyheten til hver pakke.

Vi importerer forbehandlingspakken først, så får vi flere metoder derfra når vi følger med. Jeg bruker også sklearn versjon 0.20.0, så hvis du har problemer med å importere noen av pakkene, kan du sjekke versjonen din.

Vi jobber med to forskjellige typer data, str og int bare for å synliggjøre hvordan de forskjellige forbehandlingsteknikkene fungerer.

# Ved prosjektstart
fra sklearn importforbehandling
# Og la oss lage et tilfeldig utvalg av ints å behandle
ary_int = np.random.randint (-100, 100, 10)
ary_int
>> [5, -41, -67, 23, -53, -57, -36, -25, 10, 17]
# Og noen str å jobbe med
ary_str = ['foo', 'bar', 'baz', 'x', 'y', 'z']

La oss prøve en rask merking med LabelEncoder på ary_str. Dette er viktig fordi du ikke bare kan mate rå strenger - det kan du godt, men det er utenfor rammen for denne artikkelen - i modellene dine. Så vi koder etiketter til hver av strengene, med verdi mellom 0 og n. I ary_str har vi 6 unike verdier, så området vårt vil være 0 - 5.

fra sklearn.preprocessing import LabelEncoder
l_encoder = forbehandling.LabelEncoder ()
l_encoder.fit (ary_str)
>> LabelEncoder ()
# Hva er verdiene våre?
l_encoder.transform ([ 'foo'])
>> matrise ([2])
l_encoder.transform ([ 'Bas'])
>> matrise ([1])
l_encoder.transform ([ 'bar'])
>> matrise ([0])

Du vil merke at disse ikke er bestilt, da selv gjennom foo kom før linjen i matrisen, den ble kodet med 2 mens linjen ble kodet med 1. Vi bruker en annen kodingsmetode når vi trenger å sørge for at verdiene våre er kodet i riktig rekkefølge.

Hvis du har mange kategorier å holde rede på, kan du glemme hvilke str-kart som int. For det kan vi lage et dikt.

# Kontroller kartlegginger
Listen (l_encoder.classes_)
>> ['bar', 'baz', 'foo', 'x', 'y', 'z']
# Lag ordbok for kartlegginger
dict (zip (l_encoder.classes_, l_encoder.transform (l_encoder.classes_)))
>> {'bar': 0, 'baz': 1, 'foo': 2, 'x': 3, 'y': 4, 'z': 5}

Prosessen er litt annerledes hvis du har et dataframe, men faktisk litt enklere. Du trenger bare å. Bruke () LabelEncoder-objektet på DataFrame. For hver kolonne får du en unik etikett for verdiene i den kolonnen. Legg merke til hvordan foo er kodet til 1, men det samme er y.

# Prøv LabelEncoder på et dataramme
importer pandaer som pd
l_encoder = forbehandling.LabelEncoder () # Nytt objekt
df = pd.DataFrame (data = {'col1': ['foo', 'bar', 'foo', 'bar'],
                          'col2': ['x', 'y', 'x', 'z'],
                          'col3': [1, 2, 3, 4]})
# Nå for den enkle delen
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

Nå går vi videre til ordinær koding der funksjoner fremdeles er uttrykt som heltallverdier, men de har en følelse av sted og struktur. Slik at x kommer før y, og y kommer før z.

Imidlertid kommer vi til å kaste en skiftenøkkel inn her. Ikke bare blir verdiene bestilt, men de kommer til å bli parret med hverandre.

Vi kommer til å ta et utvalg av verdier [‘foo’, ‘bar’, ‘baz’] og [‘x’, ‘y’, ‘z’]. Deretter koder vi 0, 1 og 2 til hvert sett med verdier i hver gruppe, og lager et kodet par for hver av verdiene.

F.eks [‘Foo’, ‘z’] vil bli kartlagt til [0, 2], og [‘baz’, ‘x’] vil bli kartlagt til [2, 0].

Dette er en god tilnærming å ta når du trenger å ta en haug med kategorier og gjøre dem tilgjengelige for en regresjon, og spesielt bra når du har sammenflettende sett med strenger - separate kategorier som fremdeles overlapper hverandre - og trenger representasjon i dataframmen .

fra sklearn.preprocessing import OrdinalEncoder
o_encoder = OrdinalEncoder ()
ary_2d = [['foo', 'bar', 'baz'], ['x', 'y', 'z']]
o_encoder.fit (2d_ary) # Tilpass verdiene
o_encoder.transform ([['foo', 'y']])
>> matrise ([[0, 1.]])

Den klassiske kodningen "hot" eller "dummy", der enkeltfunksjoner i kategoriene deretter uttrykkes som ytterligere kolonner på 0s eller 1s, avhengig av om verdien vises eller ikke. Denne prosessen lager en binær kolonne for hver kategori og returnerer en sparsom matrise eller tett matrise.

Kreditt til https://blog.myyellowroad.com/

Hvorfor til og med bruke dette? Fordi denne typen koding er nødvendig for å mate kategoriske data til mange scikit-modeller som lineære regresjonsmodeller og SVM-er. Så bli komfortabel med dette.

fra sklearn.preprocessing import OneHotEncoder
hot_encoder = OneHotEncoder (handle_unknown = 'ignorere')
hot_encoder.fit (ary_2d)
hot_encoder.categories_
>>
[array (['foo', 'x'], dtype = object), array (['bar', 'y'], dtype = object), array (['baz', 'z'], dtype = object )]
hot_encoder.transform ([['foo', 'foo', 'baz'], ['y', 'y', 'x']]). toarray ()
>>
matrise ([[1., 0., 0., 0., 1., 0.],
       [0., 0., 0., 1., 0., 0.]])

Hva med hvis vi hadde en dataramme å jobbe med?

Kunne vi fortsatt bruke en varm koding? Det er faktisk mye enklere enn du tror, ​​fordi du bare trenger å bruke .get_dummies () som er inkludert i pandaer.

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

To av de tre kolonnene i df er delt opp og binært kodet til et dataframe.

F.eks kolonnen col1_bar er col1 fra df, men har 1 som postverdi når linjen var verdien i den opprinnelige dataframmen.

Hva med når funksjonene våre må transformeres innenfor et visst område. Ved å bruke MinMaxScaler kan hver funksjon skaleres individuelt slik at den ligger i det gitte området. Verdiene er som standard mellom 0 og 1, men du kan endre rekkevidden.

fra sklearn.preprocessing import MinMaxScaler
mm_scaler = MinMaxScaler (feature_range = (0, 1)) # Mellom 0 og 1
mm_scaler.fit ([ary_int])
>> MinMaxScaler (kopi = True, feature_range = (0, 1))
print (scaler.data_max_)
>> [5. -41. -67. 23. -53. -57. -36. -25. 10. 17.]
print (mm_scaler.fit_transform ([ary_int]))
>> [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] # Humm noe er galt

Hvis du merker at utdataene er alle nuller ... som ikke er det vi ønsket. Her og her er det en god forklaring på hvorfor det ville ha skjedd, men novellen er at matrisen er formatert feil.

Det er en (1, n) matrise og må konverteres til en (n, 1) matrise. Den enkleste måten å gjøre dette på er å sørge for at matrisen din er en numpy matrise, slik at du kan manipulere formen.

# Lag numpy array
ary_int = n.array ([5, -41, -67, 23, -53, -57, -36, -25, 10, 17])
# Forvandle
mm_scaler.fit_transform (ary_int [:, np.newaxis])
>>
matrise ([[0,8],
       [0.28888889],
       [0. ]
       [1. ]
       [0.15555556],
       [0.11111111],
       [0.34444444],
       [0.46666667],
       [0.85555556],
       [0.93333333]])
# Du kan også bruke
mm_scaler.fit_transform (ary_int.reshape (-1, 1))
# Prøv også en annen skala
mm_scaler = MinMaxScaler (feature_range = (0, 10))
mm_scaler.fit_transform (ary_int.reshape (-1, 1))
>>
matrise ([[8.],
       [2.88888889],
       [0.],
       [10. ]
       [1.55555556],
       [1.11111111],
       [3.44444444],
       [4.66666667],
       [8.55555556],
       [9.33333333]])

Nå som vi raskt kan skalere dataene våre, hva med å implementere en slags form til de transformerte dataene våre? Vi ser på å standardisere dataene, som vil gi deg verdier som skaper en gaussian med gjennomsnittet 0 og en SD på 1. Du kan vurdere denne tilnærmingen når du implementerer gradient nedstigning, eller hvis du trenger vektede innspill som regresjon og nevrale nettverk. Hvis du skal implementere et KNN, skal du også skalere dataene først. Merk at denne tilnærmingen er forskjellig fra normalisering, så ikke bli forvirret.

Bare bruk skalaen fra forbehandlingen.

preprocessing.scale (foo)
>> matrise ([0,86325871, -0,58600774, -1.40515833, 1.43036297, -0.96407724, -1.09010041, -0.42847877, -0.08191506, 1.02078767, 1.24132821])
preprocessing.scale (foo) .mean ()
>> -4.4408920985006264e-17 # I hovedsak null
 preprocessing.scale (foo) .std ()
>> 1.0 # Nøyaktig hva vi ønsket

Den siste sklearn-pakken å se på er Binarizer, du får fremdeles 0er og 1er gjennom dette, men nå er de definert på dine egne premisser. Dette er prosessen med å "terskle" numeriske funksjoner for å få boolske verdier. Verdiene terskelen større enn terskelen vil kartlegge til 1, mens de ≤ til vil kartlegge til 0. I tillegg er dette en vanlig prosess når tekstforbehandling for å få begrepet frekvenser i et dokument eller et korpus.

Husk at både passform () og transformering () krever en 2d-matrise, og det er grunnen til at jeg har nestet ary_int i en annen gruppe. For dette eksemplet har jeg satt terskelen som -25, så alle numre strengt over vil bli tildelt en 1.

fra sklearn.preprocessing import Binarizer
# Sett -25 som vår terskel
tz = Binarizer (terskel = -25,0). utstyr ([ary_int])
tz.transform ([ary_int])
>> matrise ([[1, 0, 0, 1, 0, 0, 0, 0, 1, 1]])

Nå som vi har disse få forskjellige teknikkene, hvilken er best for algoritmen din? Det er sannsynligvis best å lagre noen forskjellige mellomliggende datarammer med skalerte data, innlagte data osv., Slik at du kan se effekten på utdataene til modellene dine.

Siste tanker

Rengjøring og prepping av data er uunngåelig og generelt en takknemlig oppgave når det gjelder datavitenskap. Hvis du er heldig nok med et datateknisk team som kan hjelpe deg med å sette opp ETL-rørledninger for å gjøre jobben din enklere, kan det hende du er i mindretall av dataforskere.

Livet er ikke bare en haug med Kaggle datasett, der du i virkeligheten må ta beslutninger om hvordan du får tilgang til og renser dataene du trenger hver dag. Noen ganger har du mye tid til å sørge for at alt er på rett sted, men mesteparten av tiden blir du presset for svar. Hvis du har de riktige verktøyene på plass og har forståelse for hva som er mulig, vil du kunne komme til svarene enkelt.

Som alltid håper jeg du har lært noe nytt.

Jubel,

Ekstra lesing