Rengøring og forberedelse af data med Python til datavidenskab - bedste praksis og nyttige pakker

Forord

Rengøring af data er bare noget, du skal beskæftige dig med inden for analyse. Det er ikke godt arbejde, men det skal gøres, så du kan producere godt arbejde.

Jeg har brugt så meget tid på at skrive og omskrive funktioner til at hjælpe mig med at rense data, at jeg ville dele noget af det, jeg har lært undervejs. Hvis du ikke er gået over dette indlæg, kan du undersøge, hvordan du bedre kan organisere datavidenskabelige projekter, da det vil hjælpe med at danne nogle af de begreber, jeg går nærmere nedenfor.

Efter at have startet med at organisere min kode bedre, er jeg begyndt at opbevare en brugerdefineret pakke, hvor jeg holder min 'rydde op' kode. Hvis noget andet, giver det mig en basislinje for at skrive tilpassede metoder til data, der ikke helt passer til mine tidligere oprydningsskripts. Og jeg behøver ikke at skrive denne regex-e-mail-extractor for 100. gang, fordi jeg har gemt den på et tilgængeligt sted.

Nogle virksomheder har hele hold, der er afsat til rengørings kode, men de fleste gør det ikke. Så det er bedst at forstå nogle af de bedste fremgangsmåder. Hvis noget, vil du blive bedre til at forstå strukturen i dine data, så for bedre at forklare hvorfor eller hvorfor ikke noget er sket.

Under forberedelsen til dette indlæg løb jeg også over denne repo af kjam, hvilket ville have været utroligt nyttigt, da jeg først lærte at rense data. Hvis du vil gå dybere ind i kodrensning, foreslår jeg, at du starter der.

Dit mål er at rydde op i tingene… eller i det mindste prøve at gøre det

Kontroller dine data ... hurtigt

Den første ting, du vil gøre, når du får et nyt datasæt, er at hurtigt verificere indholdet med .head () -metoden.

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

Lad os nu hurtigt se navnene og typerne af kolonnerne. Det meste af tiden får du data, som ikke er helt, hvad du forventede, f.eks. Datoer, der faktisk er strenge og andre mænd. Men for at tjekke på forhånd.

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

Lad os nu se, om dataframe har et indeks, der er knyttet til det, ved at kalde .index på df. Hvis der ikke er noget indeks, får du en AttributError: 'funktion'-objekt har ingen attribut' index '-fejl vist.

# Kontroller indeksværdierne
df.index.values
# Kontroller, om der findes et bestemt indeks
'foo' i df.index.values
# Hvis indeks ikke findes
df.set_index ('column_name_to_use', inplace = sandt)

Godt. Vores data er hurtigt blevet kontrolleret, vi kender datatyperne, hvis kolonner er unikke, og vi ved, at de har et indeks, så vi kan gøre sammenføjninger og fusioner senere. Lad os finde ud af, hvilke kolonner du vil beholde eller fjerne. I dette eksempel ønsker vi at slippe af med kolonnerne i indekserne 1, 3 og 5, så jeg har lige tilføjet strengværdierne til en liste, som vil blive brugt til at droppe kolonnerne.

# Opret listeforståelse af de kolonner, du vil miste
columns_to_drop = [column_names [i] for i i [1, 3, 5]]
# Slip uønskede kolonner
df.drop (kolonner_til_drop, i stedet = Sandt, akse = 1)

Inplace = True er tilføjet, så du ikke behøver at gemme over den originale df ved at tildele resultatet af .drop () til df. Mange af panda-metoderne understøtter stedet = Sandt, så prøv at bruge det så meget som muligt for at undgå unødvendig omfordeling.

Hvad skal man gøre med NaN

Hvis du har brug for at udfylde fejl eller emner, skal du bruge metoderne fillna () og dropna (). Det virker hurtigt, men alle manipulationer af dataene skal dokumenteres, så du kan forklare dem for nogen på et senere tidspunkt.

Du kan fylde NaN'erne med strenge, eller hvis de er tal, kan du bruge middelværdien eller medianværdien. Der er en masse debat om, hvad der sker med manglende eller misdannede data, og det rigtige svar er ... det afhænger.

Du skal bruge din bedste bedømmelse og input fra de mennesker, du arbejder med, hvorfor det er den bedste fremgangsmåde at fjerne eller udfylde dataene.

# Fyld NaN med ''
df ['col'] = df ['col']. fillna ('')
# Udfyld NaN med 99
df ['col'] = df ['col']. fillna (99)
# Fyld NaN med middelværdien af ​​kolonnen
df ['col'] = df ['col']. fillna (df ['col']. middel ())

Du kan også propagere værdier, som ikke er nul, fremad eller bagud ved at sætte metode = 'pad' som metodargumentet. Det udfylder den næste værdi i dataframe med den forrige værdi, der ikke er NaN. Måske vil du bare udfylde en værdi (grænse = 1), eller du vil udfylde alle værdier. Uanset hvad det er, skal du sørge for, at det stemmer overens med resten af ​​din datarengøring.

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 # Dette er den værdi, der skal udfyldes
5 NaN
6 NaN
df.fillna (metode = 'pad', limit = 1)
    col1
0 NaN
1 NaN
2 2,0
3 3.0
4 4.0
5 4.0 # Udfyldt
6 NaN

Bemærk, hvordan kun indeks 5 blev udfyldt? Hvis jeg ikke havde udfyldt begrænset puden, ville det have udfyldt hele dataframmen. Vi er ikke begrænset til fremadfyldning, men også påfyldning med bfill.

# Fyld de to første NaN-værdier med den første tilgængelige værdi
df.fillna (metode = 'bfill')
    col1
0 2,0 # Fyldt
1 2,0 # Fyldt
2 2,0
3 3.0
4 4.0
5 NaN
6 NaN

Du kan bare slippe dem helt fra dataframmen, enten ved rækken eller ved kolonnen.

# Slip alle rækker, der har nogen nans
df.dropna ()
# Drop kolonner, der har nogen nans
df.dropna (akse = 1)
# Slip kun kolonner, der har mindst 90% ikke-NaN'er
df.dropna (tærskel = int (df.shape [0] * .9), akse = 1)

Parametertærsklen = N kræver, at en søjle har mindst N ikke-NaN'er for at overleve. Tænk på dette som den nedre grænse for manglende data, som du finder acceptabel i dine kolonner. Overvej nogle logføringsdata, som muligvis går glip af en række funktioner. Du vil kun have de poster, der har 90% af de tilgængelige funktioner, før du betragter dem som kandidater til din model.

np.where (hvis_dette_dette_dette, gør_dette, ellers_do_dette)

Jeg er skyldig i ikke at bruge dette tidligere i min analytiske karriere, fordi det er meget nyttigt. Det sparer så meget tid og frustration, når man blander sig gennem en dataframe. Hvis du hurtigt vil udføre grundlæggende rengøring eller funktionsteknik, skal du n.p. hvor her du gør det.

Overvej, om du vurderer en kolonne, og du vil vide, om værdierne er strengt større end 10. Hvis de er, vil du have, at resultatet skal være 'foo', og hvis ikke vil du have, at resultatet skal være 'bjælke'.

# Følg denne syntaks
np.where (hvis_ dette_kondition_is_ sandt, gør dette, ellers dette)
# Eksempel
df ['new_column'] = np.where (df [i]> 10, 'foo', 'bar)

Du er i stand til at udføre mere komplekse handlinger som den nedenfor. Her kontrollerer vi, om kolonneposten starter med foo og ikke slutter med bjælke. Hvis dette tjekker, returnerer vi sandt ellers returnerer vi den aktuelle værdi i kolonnen.

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

Og endnu mere effektiv kan du begynde at indlejre din np.where, så de stak på hinanden. Ligesom hvordan du stabler ternære operationer, skal du sørge for, at de er læsbare, da du hurtigt kan komme i rod med stærkt indlejrede udsagn.

# Tre niveau hekkende med np.where
np.where (hvis_ dette_kondition_is_true_one, gør_ dette,
  np.where (hvis_ dette_kondition_is_true_til, do_that,
    np.where (if_this_condition_is_true_three, do_foo, do_bar)))
# Et trivielt eksempel
df ['foo'] = np.where (df ['bar'] == 0, 'Nul',
              np.where (df ['bar'] == 1, 'One',
                np.where (df ['bar'] == 2, 'Two', 'Three')))

Assert og test hvad du har

Kredit til https://www.programiz.com

Bare fordi du har dine data i en dejlig dataramme, ingen duplikater, ingen manglende værdier, har du muligvis stadig nogle problemer med de underliggende data. Og med et dataframe på 10M + rækker eller ny API, hvordan kan du sikre dig, at værdierne er nøjagtigt, som du forventer, at de skal være?

Sandheden er, at du aldrig rigtig ved, om dine data er korrekte, indtil du tester dem. Bedste fremgangsmåder inden for software engineering er meget afhængige af at teste deres arbejde, men for datavidenskab er det stadig et igangværende arbejde. Bedre at starte nu og lære dig selv gode arbejdsprincipper, snarere end at skulle omskolere dig selv på et senere tidspunkt.

Lad os lave en simpel dataframe til test.

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

Lad os teste, om alle værdierne i col1 er> = 0 ved hjælp af den indbyggede metodeopgave, der følger med standardbiblioteket i python. Hvad du spørger python, hvis det er sandt, alle elementerne i df ['col1'] er større end nul. Hvis dette er sandt, fortsæt på din vej, hvis ikke kast en fejl.

påstå (df ['col1']> = 0) .all () # Bør ikke returnere noget

Fantastisk ser ud til at have fungeret. Men hvad hvis .all () ikke var inkluderet i påstanden?

påstå (df ['col1']> = 0)
>>
ValueError: Sandhedens værdi af en serie er tvetydig. Brug a.empty, a.bool (), a.item (), a.any () eller a.all ().

Humm ser ud som om vi har nogle muligheder, når vi tester vores dataframes. Lad os test er nogen af ​​værdierne er strenge.

påstå (df ['col1']! = str) .any () # Bør ikke returnere noget

Hvad med at teste de to kolonner for at se, om de er ens?

påstå (df ['col1'] == df ['col2']). alle ()
>>
Traceback (seneste opkald sidst):
  Fil "", linje 1, i 
AssertionError

Ah, vores påstand mislykkedes her!

Den bedste praksis med påstande skal bruges til at teste forhold i dine data, som aldrig skulle ske. Dette er tilfældet, når du kører din kode, alt stopper, hvis en af ​​disse påstande mislykkes.

Metoden .all () vil kontrollere, om alle elementerne i objekterne passerer påstanden, mens .any () vil kontrollere, om nogen af ​​elementerne i objekterne klarer assert-testen.

Dette kan være nyttigt, når du vil:

  • Kontroller, om der er indført negative værdier i dataene;
  • Sørg for, at to kolonner er nøjagtig de samme;
  • Bestem resultaterne af en transformation eller;
  • Kontroller, om det unikke id-antal er nøjagtigt.

Der er flere hævningsmetoder, som jeg ikke vil gå over, men kend, som du kan bruge her. Du vil aldrig vide, hvornår du har brug for at teste for en bestemt tilstand, og på samme tid skal du begynde at teste for forhold, som du ikke ønsker i din kode.

Test ikke for alt, men test for ting, der vil bryde dine modeller.

For eksempel. Er en funktion med skal alle være 0'ere og 1'ere, faktisk befolket med disse værdier.

Derudover inkluderer den vidunderpakke-pandaer også en testpakke.

importer pandas.util.testing som tm
tm.assert_series_equal (df ['col1'], df ['col2'])
>>
AssertionError: Serierne er forskellige
Serieværdierne er forskellige (100,0%)
[venstre]: [0, 6, 8, 0, 3, 4, 3, 9, 0, 7]
[højre]: [6, -1, 4, 5, -7, -5, -10, -8, 4, -4]

Ikke kun fik vi kastet en fejl, men pandaer fortalte os, hvad der var galt.

Pæn.

Hvis du desuden vil begynde at opbygge dig en testsuite - og du måske vil overveje at gøre dette - bliv fortrolig med den uniteste pakke, der er indbygget i Python-biblioteket. Du kan lære mere om det her.

beautifier

I stedet for at skulle skrive din egen regex - hvilket er en smerte på de bedste tidspunkter - er det undertiden blevet gjort for dig. Skønhedspakken er i stand til at hjælpe dig med at rydde op i nogle ofte anvendte mønstre til e-mails eller webadresser. Det er intet fan, men kan hurtigt hjælpe med oprydning.

$ pip3 installerer forskønner
fra skønhedsimport-e-mail, URL
email_string = 'foo@bar.com'
email = E-mail (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': 'funktionen er i øjeblikket kun tilgængelig med linkin-URL'er'}
github.com

Jeg bruger denne pakke, når jeg har en række URL-adresser, jeg har brug for at gennemgå og ikke ønsker at skrive regex for 100. gang for at udtrække bestemte dele af adressen.

Håndtering af Unicode

Når du laver noget NLP, kan det at arbejde med Unicode være frustrerende på de bedste tidspunkter. Jeg kører noget i spaCy, og pludselig går alt i stykker på mig på grund af et unicode-tegn der vises et sted i dokumentkroppen.

Det er virkelig det værste.

Ved at bruge ftfy (fikseret det for dig) er du i stand til at løse virkelig ødelagte Unicode. Overvej, når nogen har kodet Unicode med en standard og afkodet den med en anden. Nu er du nødt til at beskæftige sig med denne i mellem streng, som nonsens-sekvenser kaldet "mojibake".

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

Heldigvis bruger ftfy heuristik til at opdage og fortryde mojibake med en meget lav grad af falske positiver. Lad os se, hvad vores strenge ovenfor kan konverteres til, så vi kan læse det. Hovedmetoden er fix_text (), og du bruger den til at udføre afkodningen.

import 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 afkodningen udføres, kan du prøve ftfy.explain_unicode (). Jeg tror ikke, dette vil være alt for nyttigt, men det er interessant at se processen.

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

dedupe

Dette er et bibliotek, der bruger maskinlæring til hurtigt at udføre de-duplikering og enhedsopløsning på strukturerede data. Der er et stort indlæg her, der går langt mere detaljeret end jeg vil, og som jeg har trukket meget på.

Vi gennemgår download af Chicago placering af tidlige barndomsoplysninger, som kan findes her. Det har en masse manglende værdier og duplikerede værdier fra forskellige datakilder, så det er godt at lære videre.

Hvis du nogensinde har gennemgået duplikerede data før, ser dette meget kendt ud.

# Kolonner og antallet af manglende værdier i hver
Id har 0 na værdier
Kilde har 0 na-værdier
Webstedsnavn har 0 na-værdier
Adressen har 0 na-værdier
Zip har 1333 na-værdier
Telefonen har 146 na-værdier
Fax har 3299 na-værdier
Programnavn har 2009 na-værdier
Længden af ​​dagen har 2009 na værdier
IDHS-udbyder-ID har 3298 na-værdier
Agenturet har 3325 na-værdier
Kvarter har 2754 na-værdier
Fundet tilmelding har 2424 na-værdier
Programmet har 2800 na-værdier
Antal pr. Sted EHS har 3319 na-værdier
Antal pr. Sted HS har 3319 na-værdier
Direktør har 3337 na-værdier
Head Start Fund har 3337 na-værdier
Eearly Head Start Fund har 2881 na-værdier
CC-fonden har 2818 na-værdier
Progmod har 2818 na-værdier
Webstedet har 2815 na-værdier
Direktør har 3114 na værdier
Centerdirektør har 2874 na-værdier
ECE-tilgængelige programmer har 2379 na-værdier
NAEYC gyldigt indtil har 2968 na-værdier
NAEYC-program-id har 3337 na-værdier
E-mail-adresse har 3203 na-værdier
Ounce of Prevention Description har 3185 na-værdier
Servicetype for lilla bindemiddel har 3215 na-værdier
Kolonne har 3337 na-værdier
Kolonne2 har 3018 na-værdier

PreProcess-metoden leveret af dedupe er nødvendig for at sikre, at der ikke opstår fejl i prøveudtagnings- og træningsfasen for modellen. Tro mig, at bruge dette vil gøre det lettere at bruge dedupe. Gem denne metode i din lokale 'rengøringspakke', så du kan bruge den i fremtiden, når du håndterer duplikerede data.

importer pandaer som pd
import numpy
import fradrag
import os
import csv
import re
fra unidecode import unidecode
def preProcess (kolonne):
    '''
    Bruges til at forhindre fejl under deduktionsprocessen.
    '''
    prøve :
        column = column.decode ('utf8')
    undtagen AttributeError:
        passere
    column = unidecode (column)
    column = re.sub ('+', '', kolonne)
    column = re.sub ('\ n', '', kolonne)
    column = column.strip (). strip ('"'). strip (" '"). lavere (). strip ()
    
    hvis ikke kolonne:
        kolonne = Ingen
    retur kolonne

Begynd nu på at importere .csv-kolonnen efter kolonne, mens du behandler dataene.

def readData (filnavn):
    
    data_d = {}
    med åben (filnavn) som f:
        reader = csv.DictReader (f)
        til række i læser:
            clean_row = [(k, preProcess (v)) for (k, v) i række.emner ()]
            row_id = int (række ['Id'])
            data_d [row_id] = dict (clean_row)
returnere df
name_of_file = 'data.csv'
print ('Rengøring og import af data ...')
df = readData (name_of_file)

Nu skal vi fortælle deduktioner, hvilke funktioner vi skal se på for at bestemme duplikatværdier. Nedenfor markeres hver funktion efter felt og tildeles en datatype, og hvis den har nogle manglende værdier. Der er en hel liste over forskellige variabeltyper, du kan bruge her, men for at holde det let holder vi os med strengene lige nu.

Jeg vil heller ikke bruge hver eneste kolonne til at bestemme duplikationen, men du kan, hvis du tror, ​​det gør det lettere at identificere værdierne i din dataframe.

# Indstil felter
felter = [
        {'felt': 'Kilde', 'type': 'Sæt'},
        {'felt': 'Webstedsnavn', 'type': 'String'},
        {'felt': 'Adresse', 'type': 'String'},
        {'felt': 'Zip', 'type': 'Exact', 'mangler': True},
        {'felt': 'Telefon', 'type': 'Streng', 'mangler': Sand},
        {'felt': 'E-mail-adresse', 'type': 'String', 'mangler': True},
        ]

Lad os nu begynde at indtage data.

# Gå ind i vores model
deduper = dedupe.Dedupe (felter)
# Kontroller, om det fungerer
deduper
>>
# Indsæt nogle eksempeldata i ... 15000 poster
deduper.ample (df, 15000)

Nu går vi videre til mærkningsdelen. Når du kører denne metode nedenfor, bliver du bedt om at dedupe til at gøre noget simpel mærkning.

dedupe.consoleLabel (deduper)
Hvad du skulle se; manuel træning af deduperen

Det virkelige 'a ha!' Øjeblik er, når du får denne prompt. Dette er en deduper, der beder dig om at træne den, så den ved, hvad den skal kigge efter. Du ved, hvordan en duplikatværdi skal se ud, så bare videregive denne viden.

Henviser disse poster til den samme ting?
(y) es / (n) o / (u) nsure / (f) inished

Nu behøver du ikke længere at søge i tonsvis af tonsvis af poster for at se, om der er foretaget duplikation. I stedet trænes et neuralt net af dig til at finde duplikater i dataframmen.

Når du har givet det en vis mærkning, skal du afslutte træningsprocessen og gemme dine fremskridt. Du kan vende tilbage til dit neurale net senere, hvis du finder ud af, at du har gentagne dataframe-objekter, der skal trækkes fra.

deduper.train ()
# Gem træning
med åben (training_file, 'w') som tf:
        deduper.writeTraining (tf)
# Gem indstillinger
med åben (settings_file, 'wb') som sf:
        deduper.writeSettings (sf)

Vi er næsten færdige, som næste gang skal vi indstille en tærskel for vores data. Når tilbagekaldelsesvægt er lig med 1, fortæller vi fradragsgiver for værdigenkaldelse lige så meget som præcision. Men hvis husk_vægt = 3, ville vi værdsætte genkald tre gange så meget. Du kan lege med disse indstillinger for at se, hvad der fungerer bedst for dig.

tærskel = deduper.threshold (df, husk_vægt = 1)

Endelig kan vi nu søge gennem vores df og se, hvor duplikaterne er. Det har været lang tid at komme i denne position, men dette er meget meget bedre end at gøre dette for hånd.

# Cluster duplikaterne sammen
clustered_dupes = deduper.match (data_d, tærskel)
print ('Der er {} duplikatsæt'.format (len (clustered_dupes)))

Så lad os se på vores duplikater.

clustered_dupes
>>
[((0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255),
  array ([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 fortæller os ikke meget. Hvad er det, der faktisk viser os? Hvad skete der med alle vores værdier?

Hvis du ser nøje på, at værdierne (0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255) er alle id-placeringer af duplikater, som deduper mener faktisk er den samme værdi. Og vi kan se på de originale data for at bekræfte dette.

{'Id': '215',
 'Kilde': 'cps_early_childhood_portal_scrape.csv',
 'Webstedsnavn': 'frelseshærens tempel',
 'Adresse': '1 n. ogden',
...
{'Id': '509',
 'Kilde': 'cps_early_childhood_portal_scrape.csv',
 'Webstedsnavn': 'frelseshær - tempel / frelseshær',
 'Adresse': '1 n ogden ave',
 'Zip': Ingen,
..

Dette ligner duplikater for mig. Pæn.

Der er mange mere avancerede anvendelser af deduper, såsom matchBlocks til sekvenser af klynger, eller interaktionsfelter, hvor interaktionen mellem to felter ikke kun er additiv, men multiplikativ. Dette har allerede været meget at gå over, så jeg lader den forklaring til artiklen ovenfor.

String Matching med fuzzywuzzy

Prøv dette bibliotek. Det er virkelig interessant, fordi det giver dig en score for, hvor tæt strenge er, når de sammenlignes.

Dette har været et utroligt godt værktøj, da jeg tidligere har lavet projekter, hvor jeg har været nødt til at stole på Google Sheets fuzzymatch-tilføjelse til at diagnosticere datavalideringsproblemer - tænk, at CRM-regler ikke anvendes eller handles korrekt - og nødvendige for at rengøre poster for at udføre enhver form for analyse.

Men for store datasæt falder denne tilgang ganske fladt.

Imidlertid kan du med fuzzywuzzy begynde at komme ind i streng matching i en mere videnskabelig sag. Ikke for at blive for teknisk, men det bruger noget, der hedder Levenshtein-afstand, når man sammenligner. Dette er en streng-lighedsmetrik for to sekvenser, således at afstanden imellem er antallet af enkelt tegnredigeringer, der kræves for at ændre det ene ord til det andet ord.

For eksempel. Hvis du vil ændre strengfooen til bjælke, vil det minimale antal tegn, der skal ændres, være 3, og dette bruges til at bestemme 'afstanden'.

Lad os se, hvordan dette fungerer i praksis.

$ pip3 installerer fuzzywuzzy
# test.py
fra fuzzywuzzy import fuzz
fra fuzzywuzzy importproces
foo = 'er denne streng'
bar = 'som den streng?'
fuzz.ratio (foo, bar)
>>
71
fuzz.WRatio (foo, bar) # Vægtet forhold
>>
73
fuzz.UQRatio (foo, bar) # Unicode-hurtigforhold
>> 73

Den fuzzywuzzy-pakke har forskellige måder at evaluere strenge (WRatio, UQRatio osv.), Og jeg vil bare holde mig til standardimplementeringen til denne artikel.

Dernæst kan vi se på en tokeniseret streng, der returnerer et mål for sekvensernes lighed mellem 0 og 100 men sortere tokenet før sammenligning. Dette er nøglen, da du måske bare ønsker at se indholdet af strengene snarere end deres positioner.

Strengene foo og bar har de samme symboler, men er strukturelt forskellige. Vil du behandle dem på samme måde? Nu kan du let se og redegøre for denne type forskel i dine data.

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 næste, skal du finde det nærmeste match for en streng fra en liste over værdier. I dette tilfælde vil vi se på Harry Potter-titler.

Hvad med den Harry Potter-bog med ... noget titel ... den har ... Jeg ved ikke. Jeg skal bare gætte og se, hvilken af ​​disse bøger der er tættest på mit gæt.

Min gæt er 'ild', og lad os se, hvordan det scorer mod den mulige liste med titler.

lst_to_eval = ['Harry Potter og filosofens sten',
'Harry Potter og hemmelighedskammeret',
'Harry Potter og fangen fra Azkaban',
'Harry Potter and the Goblet of Fire',
'Harry Potter og Føniksordenen',
'Harry Potter og halvblodsprinsen',
'Harry potter og dødsregalierne']
# Top to svar baseret på mit gæt
process.extract ("brand", lst_to_eval, limit = 2)
>>
[('Harry Potter and the Goblet of Fire', 60), ("Harry Potter and the Sorcerer's Stone", 30)
results = process.extract ("brand", lst_to_eval, limit = 2)
for resultat i resultater:
  print ('{}: har en score på {}'. format (resultat [0], resultat [1]))
>>
Harry Potter og 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.

>>> proces.extractOne ("sten", lst_to_eval)
("Harry Potter og troldmandens sten", 90)

Jeg ved, at vi talte om dedupeing tidligere, men her er en anden anvendelse af den samme proces med fuzzywuzzy. Vi kan tage en liste over strenge, der indeholder duplikater, og bruger fuzzy matching til at identificere og fjerne duplikater.

Ikke så dekorativ som et neuralt net, men det vil gøre jobbet til små operationer.

Vi fortsætter med Harry Potter-temaet og ser efter dublerede karakterer fra bøgerne på en liste.

Du bliver nødt til at indstille en tærskel mellem 0 og 100. Når tærsklen mindskes, vil antallet af fundne duplikationer stige, så den returnerede liste bliver korteret. Standard er 70.

# Liste over duplikerede karakternavne
indeholder_dupes = [
'Harry Potter',
'H. Potter',
'Harry James Potter',
'James Potter',
'Ronald Bilius \' Ron \ 'Weasley',
'Ron Weasley',
'Ronald Weasley']
# Udskriv duplikatværdierne
process.dedupe (contains_dupes)
>>
dict_keys (['Harry James Potter', "Ronald Bilius 'Ron' Weasley"])
# Udskriv duplikatværdierne med en højere tærskel
process.dedupe (indeholder_dupes, tærskel = 90)
>>
dict_keys (['Harry James Potter', 'H. Potter', 'Ronald Bilius' Ron 'Weasley "])

Og som en hurtig bonus kan du også gøre noget fuzzy matching med datetime-pakken for at udtrække datoer fra en streng tekst. Dette er godt, når du ikke vil (igen) skrive et regex-udtryk.

fra dateutil.parser importparse
dt = parse ("I dag er den 1. januar 2047 kl. 08:21:00", fuzzy = sandt)
print (dt)
>>
2047-01-01 08:21:00
dt = parse ("18. maj 2049 noget noget", uklar = sandt)
print (dt)
>>
2049-05-18 00:00:00

Prøv noget

Sammen med rengøring af dataene skal du også forberede dataene, så de er i en form, du kan tilføje til din model. De fleste af eksemplerne her er trukket direkte fra dokumentationen, som bør tjekkes ud, da det virkelig gør et godt stykke arbejde med at forklare mere om nyheden i hver pakke.

Vi importerer først forarbejdningspakken og derefter får yderligere metoder derfra, når vi går sammen. Jeg bruger også sklearn version 0.20.0, så hvis du har problemer med at importere nogle af pakkerne, skal du tjekke din version.

Vi arbejder med to forskellige typer data, str og int bare for at fremhæve, hvordan de forskellige forarbejdningsteknikker fungerer.

# Ved projektstart
fra sklearn importforarbejdning
# Og lad os oprette en tilfældig række ints, der skal behandles
ary_int = np.random.randint (-100, 100, 10)
ary_int
>> [5, -41, -67, 23, -53, -57, -36, -25, 10, 17]
# Og nogle str at arbejde med
ary_str = ['foo', 'bar', 'baz', 'x', 'y', 'z']

Lad os prøve nogle hurtige mærker med LabelEncoder på vores ary_str. Dette er vigtigt, fordi du ikke kun kan fodre rå strenge - det kan du godt, men det er uden for denne artikels rækkevidde - i dine modeller. Så vi koder etiketter til hver af strengene med en værdi mellem 0 og n. I vores ary_str har vi 6 unikke værdier, så vores interval ville være 0 - 5.

fra sklearn.preprocessing import LabelEncoder
l_encoder = forbehandling.LabelEncoder ()
l_encoder.fit (ary_str)
>> LabelEncoder ()
# Hvad er vores værdier?
l_encoder.transform ([ 'foo'])
>> matrix ([2])
l_encoder.transform ([ 'baz'])
>> matrix ([1])
l_encoder.transform ([ 'bar'])
>> array ([0])

Du vil bemærke, at disse ikke er bestilt, da selv gennem foo kom før bjælke i matrixen, det blev kodet med 2, mens linjen blev kodet med 1. Vi bruger en anden kodningsmetode, når vi er nødt til at sikre, at vores værdier er kodet i den rigtige rækkefølge.

Hvis du har en masse kategorier at holde styr på, kan du måske glemme hvilke str kort, til hvilke int. Til det kan vi oprette et dikter.

# Kontroller kortlægninger
liste (l_encoder.classes_)
>> ['bar', 'baz', 'foo', 'x', 'y', 'z']
# Opret ordbog over kortlægninger
dict (zip (l_encoder.classes_, l_encoder.transform (l_encoder.classes_)))
>> {'bar': 0, 'baz': 1, 'foo': 2, 'x': 3, 'y': 4, 'z': 5}

Processen er lidt anderledes, hvis du har et dataframe, men faktisk lidt lettere. Du skal bare .tilpasse () LabelEncoder-objektet til DataFrame. For hver kolonne får du en unik etiket til værdierne i denne kolonne. Bemærk, hvordan foo er kodet til 1, men det er også y.

# Prøv LabelEncoder på en dataframe
importer pandaer som pd
l_encoder = forbehandling.LabelEncoder () # Nyt objekt
df = pd.DataFrame (data = {'col1': ['foo', 'bar', 'foo', 'bar'],
                          'col2': ['x', 'y', 'x', 'z'],
                          'col3': [1, 2, 3, 4]})
# Nu til den lette del
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

Nu går vi videre til ordinær kodning, hvor funktioner stadig udtrykkes som heltalværdier, men de har en fornemmelse af sted og struktur. Sådan at x kommer før y, og y kommer før z.

Dog vil vi kaste en skruenøgle ind her. Ikke kun er de bestilte værdier, men de vil blive parret med hinanden.

Vi tager en række to værdier [‘foo’, ‘bar’, ‘baz’] og [‘x’, ‘y’, ‘z’]. Dernæst koder vi 0, 1 og 2 til hvert sæt værdier i hver array og opretter et kodet par for hver af værdierne.

For eksempel. [‘Foo’, ‘z’] vil blive kortlagt til [0, 2] og [‘baz’, ‘x’] blev kortlagt til [2, 0].

Dette er en god tilgang til at tage, når du har brug for at tage en masse kategorier og gøre dem tilgængelige for en regression, og især god, når du har sammenføjningssæt af strenge - separate kategorier, der stadig overlapper hinanden - og har brug for repræsentation i dataframe .

fra sklearn.preprocessing import OrdinalEncoder
o_encoder = OrdinalEncoder ()
ary_2d = [['foo', 'bar', 'baz'], ['x', 'y', 'z']]
o_encoder.fit (2d_ary) # Tilpas værdierne
o_encoder.transform ([['foo', 'y']])
>> array ([[0, 1.]])

Den klassiske hot eller "dummy" -kodning, hvor enkeltfunktioner i kategorier derefter udtrykkes som yderligere kolonner på 0s eller 1s, afhængigt af det vises værdien eller ej. Denne proces opretter en binær kolonne for hver kategori og returnerer en sparsom matrix eller et tæt array.

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

Hvorfor endda bruge dette? Fordi denne type kodning er nødvendig for at tilføje kategoriske data til mange scikit-modeller, såsom lineære regressionsmodeller og SVM'er. Så vær 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 = objekt), array (['bar', 'y'], dtype = object), array (['baz', 'z'], dtype = object )]
hot_encoder.transform ([['foo', 'foo', 'baz'], ['y', 'y', 'x']]). toarray ()
>>
array ([[1., 0., 0., 0., 1., 0.],
       [0., 0., 0., 1., 0., 0.]])

Hvad med hvis vi havde en dataramme at arbejde med?

Kunne vi stadig bruge en varm kodning? Det er faktisk meget lettere, end du tror, ​​da du bare har brug for at bruge .get_dummies () inkluderet 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 af de tre kolonner i df er delt op og binært kodet til et dataframe.

For eksempel. kolonnen col1_bar er col1 fra df, men har 1 som postværdien, når bjælke var værdien i den originale dataframe.

Hvad med, når vores funktioner skal transformeres inden for et bestemt interval. Ved at bruge MinMaxScaler kan hver funktion skaleres individuelt, så den ligger inden for det givne interval. Som standard er værdierne mellem 0 og 1, men du kan ændre området.

fra sklearn.preprocessing import MinMaxScaler
mm_scaler = MinMaxScaler (feature_range = (0, 1)) # Mellem 0 og 1
mm_scaler.fit ([ary_int])
>> MinMaxScaler (kopi = sandt, funktion_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 noget er galt

Hvis du bemærker, at udgangen er alle nuller ... hvilket ikke er det, vi ønskede. Der er en god forklaring her og her, hvorfor det ville have sket, men den korte historie er, at matrixen er formateret forkert.

Det er en (1, n) matrix og skal konverteres til en (n, 1) matrix. Den nemmeste måde at gøre dette på er at sikre dig, at din matrix er en numpy matrix, så du er i stand til at manipulere formen.

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

Nu hvor vi hurtigt kan skalere vores data, hvad med at implementere en slags form til vores transformerede data? Vi ser på standardisering af dataene, som vil give dig værdier, der skaber en gaussian med et gennemsnit på 0 og en sd på 1. Du kan overveje denne tilgang, når du implementerer gradientafstamning, eller hvis du har brug for vægtede input som regression og neurale netværk. Hvis du vil implementere et KNN, skal du også skalere dine data først. Bemærk, at denne tilgang er forskellig fra normalisering, så lad dig ikke forveksle.

Brug skalaen fra forbehandling.

preprocessing.scale (foo)
>> matrix ([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 det væsentlige nul
 preprocessing.scale (foo) .std ()
>> 1.0 # Præcis hvad vi ønskede

Den sidste sklearn-pakke at se på er Binarizer, du får stadig 0'er og 1'er igennem dette, men nu er de defineret på dine egne vilkår. Dette er processen med 'tærskelværdi' numeriske funktioner for at få booleske værdier. Værdiets tærskelværdi, der er større end tærsklen, kort til 1, mens disse ≤ til kortlægger til 0. Dette er også en almindelig proces, når tekstforarbejdning for at få udtrykket frekvenser inden for et dokument eller et korpus.

Husk, at både fit () og transformation () kræver en 2d-matrix, og det er grunden til, at jeg har indlejret ary_int i en anden matrix. For dette eksempel har jeg sat tærsklen som -25, så alle numre, der er strengt over dette, tildeles en 1.

fra sklearn.preprocessing import Binarizer
# Sæt -25 som vores tærskel
tz = Binarizer (tærskel = -25,0). fit ([ary_int])
tz.transform ([ary_int])
>> array ([[1, 0, 0, 1, 0, 0, 0, 0, 1, 1]])

Nu hvor vi har disse få forskellige teknikker, hvilken er den bedste til din algoritme? Det er sandsynligvis bedst at gemme et par forskellige mellemliggende datarammer med skalerede data, indlejrede data osv., Så du er i stand til at se effekten på output fra din model (er).

Afsluttende tanker

Rengøring og præpping af data er uundgåelig og generelt en unødvendig opgave, når det kommer til datavidenskab. Hvis du er heldig nok med et datateknik-team med dig, der kan hjælpe med at oprette ETL-rørledninger for at gøre dit job lettere, kan du muligvis være i mindretal af dataforskere.

Livet er ikke kun en masse Kaggle-datasæt, hvor du i virkeligheden skal tage beslutninger om, hvordan du får adgang til og renser de data, du har brug for hver dag. Nogle gange har du meget tid til at sikre dig, at alt er på det rigtige sted, men for det meste bliver du presset på for at få svar. Hvis du har de rigtige værktøjer på plads og forståelse af hvad der er muligt, vil du være i stand til let at komme til disse svar.

Som altid håber jeg, at du har lært noget nyt.

Skål,

Yderligere læsning