Gegevens opschonen en voorbereiden met Python voor Data Science - Best Practices en handige pakketten

Voorwoord

Het opschonen van gegevens is gewoon iets waar u mee te maken zult krijgen in analyses. Het is geen geweldig werk, maar het moet worden gedaan zodat je geweldig werk kunt produceren.

Ik heb zoveel tijd besteed aan het schrijven en herschrijven van functies om me te helpen bij het opschonen van gegevens, dat ik wat van wat ik onderweg heb geleerd wilde delen. Als je dit bericht niet hebt besproken, kun je het bekijken hoe je data science-projecten beter kunt organiseren, want het zal een aantal van de concepten helpen vormen die ik hieronder ga bespreken.

Nadat ik mijn code beter begon te organiseren, ben ik begonnen met het bijhouden van een aangepast pakket waar ik mijn 'opruim'-code bewaar. Als het iets anders is, geeft het me een baseline voor het schrijven van aangepaste methoden op gegevens die niet helemaal passen bij mijn vorige opschoningsscripts. En ik hoef die regex e-mailextractor niet voor de 100ste keer te schrijven, omdat ik hem op een toegankelijke locatie heb opgeslagen.

Sommige bedrijven hebben hele teams gewijd aan het opschonen van code, maar de meeste niet. Het is dus het beste om enkele van de best practices te begrijpen. Als u iets begrijpt, wordt u beter in het begrijpen van de structuur van uw gegevens, zodat u beter kunt uitleggen waarom wel of niet iets is gebeurd.

Bij het voorbereiden van dit bericht kwam ik ook deze repo van kjam tegen, wat ongelooflijk nuttig zou zijn geweest toen ik voor het eerst leerde hoe ik gegevens moest opschonen. Als je verder wilt gaan met het opschonen van codes, stel ik voor dat je daar begint.

Je doel is om dingen op te ruimen ... of op zijn minst proberen

Controleer uw gegevens ... snel

Het eerste wat u wilt doen wanneer u een nieuwe gegevensset krijgt, is om de inhoud snel te verifiëren met de methode .head ().

panda's importeren als pd
df = pd.read_csv ('path_to_data')
df.head (10)
>>
... wat output hier ...

Laten we nu snel de namen en typen van de kolommen bekijken. Meestal krijg je gegevens die niet helemaal zijn wat je had verwacht, zoals datums die eigenlijk strings zijn en andere eigenaardigheden. Maar om vooraf te controleren.

# Kolomnamen ophalen
column_names = df.columns
afdruk (COLUMN_NAMES)
# Krijg kolom datatypes
df.dtypes
# Controleer ook of de kolom uniek is
voor i in column_names:
  print ('{} is uniek: {}'. format (i, df [i] .is_unique))

Laten we nu kijken of aan het dataframe een index is gekoppeld door .index op de df aan te roepen. Als er geen index is, krijgt u een AttributeError: ‘functie’ object wordt geen attribuut ‘index’ weergegeven.

# Controleer de indexwaarden
df.index.values
# Controleer of een bepaalde index bestaat
'foo' in df.index.waarden
# Als index niet bestaat
df.set_index ('column_name_to_use', inplace = True)

Is goed. Onze gegevens zijn snel gecontroleerd, we kennen de gegevenstypen, als kolommen uniek zijn, en we weten dat het een index heeft, zodat we later joins en merges kunnen doen. Laten we uitzoeken welke kolommen u wilt behouden of verwijderen. In dit voorbeeld willen we de kolommen in indexen 1, 3 en 5 verwijderen, dus ik heb zojuist de tekenreekswaarden toegevoegd aan een lijst, die zal worden gebruikt om de kolommen te verwijderen.

# Maak lijstbegrip van de kolommen die u wilt verliezen
columns_to_drop = [column_names [i] voor i in [1, 3, 5]]
# Zet ongewenste kolommen neer
df.drop (columns_to_drop, inplace = True, axis = 1)

De inplace = True is toegevoegd, zodat u de originele df niet hoeft te besparen door het resultaat van .drop () toe te wijzen aan df. Veel van de methoden in panda's ondersteunen inplace = True, dus probeer het zo veel mogelijk te gebruiken om onnodige nieuwe toewijzing te voorkomen.

Wat te doen met NaN

Als u fouten of spaties moet invullen, gebruikt u de methoden fillna () en dropna (). Het lijkt snel, maar alle manipulaties van de gegevens moeten worden gedocumenteerd, zodat u ze later aan iemand kunt uitleggen.

U kunt de NaN's vullen met tekenreeksen, of als het getallen zijn, kunt u het gemiddelde of de mediaanwaarde gebruiken. Er is veel discussie over wat te doen met ontbrekende of onjuiste gegevens, en het juiste antwoord is ... het hangt ervan af.

U moet uw gezond verstand gebruiken en inbrengen van de mensen met wie u werkt, waarom het verwijderen of invullen van de gegevens de beste aanpak is.

# Vul NaN met ''
df ['col'] = df ['col']. fillna ('')
# Vul NaN met 99
df ['col'] = df ['col']. fillna (99)
# Vul NaN met het gemiddelde van de kolom
df ['col'] = df ['col']. fillna (df ['col']. mean ())

U kunt ook niet-nulwaarden naar voren of naar achteren propageren door method = ’pad’ als het argument van de methode te plaatsen. Het vult de volgende waarde in het dataframe met de vorige niet-NaN-waarde. Misschien wilt u slechts één waarde invullen (limiet = 1) of wilt u alle waarden invullen. Wat het ook is, zorg ervoor dat het consistent is met de rest van uw gegevensopschoning.

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 # Dit is de waarde die moet worden ingevuld
5 NaN
6 NaN
df.fillna (methode = 'pad', limiet = 1)
    col1
0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
5 4.0 # Voorwaarts ingevuld
6 NaN

Merk op hoe alleen index 5 werd gevuld? Als ik de pad niet had gevuld, zou deze het hele dataframe hebben gevuld. We zijn niet beperkt tot voorwaarts vullen, maar ook opvullen met bfill.

# Vul de eerste twee NaN-waarden met de eerste beschikbare waarde
df.fillna (method = "bfill)
    col1
0 2.0 # gevuld
1 2.0 # gevuld
2 2.0
3 3.0
4 4.0
5 NaN
6 NaN

Je kunt ze gewoon helemaal uit het dataframe verwijderen, hetzij per rij of per kolom.

# Laat rijen vallen die nans hebben
df.dropna ()
# Laat kolommen vallen die nans hebben
df.dropna (as = 1)
# Laat alleen kolommen vallen die ten minste 90% niet-NaN's hebben
df.dropna (thresh = int (df.shape [0] * .9), axis = 1)

De parameter thresh = N vereist dat een kolom ten minste N niet-NaN's heeft om te overleven. Zie dit als de ondergrens voor ontbrekende gegevens die u in uw kolommen acceptabel zult vinden. Overweeg een aantal loggegevens die mogelijk een verzameling functies missen. U wilt alleen de records die 90% van de beschikbare functies hebben voordat u ze als kandidaten voor uw model beschouwt.

np.where (if_this_is_true, do_this, else_do_that)

Ik ben schuldig omdat ik dit niet eerder in mijn analysecarrière heb gebruikt, omdat het niet nuttig is. Het bespaart zoveel tijd en frustratie bij het mungen door een dataframe. Als u snel wat basisreiniging of functie-engineering wilt doen, kunt u hier het volgende lezen.

Overweeg of u een kolom evalueert en wilt weten of de waarden strikt groter zijn dan 10. Als dit het geval is, wilt u dat het resultaat 'foo' is en zo niet, dan wilt u dat het resultaat 'bar' is.

# Volg deze syntaxis
np.where (if_this_condition_is_true, do_this, else_this)
# Voorbeeld
df ['new_column'] = np.where (df [i]> 10, 'foo', 'bar)

U kunt complexere bewerkingen uitvoeren zoals hieronder. Hier controleren we of het kolomrecord begint met foo en niet eindigt met een balk. Als dit niet lukt, retourneren we True, anders retourneren we de huidige waarde in de kolom.

df ['new_column'] = np.where (df ['col']. str.startswith ('foo') en
                            niet df ['col']. str.endswith ('bar'),
                            True,
                            df [ 'col'])

En nog effectiever, u kunt beginnen met het nesten van uw np.where zodat ze op elkaar worden gestapeld. Vergelijkbaar met hoe je ternaire operaties zou stapelen, zorg ervoor dat ze leesbaar zijn, want je kunt snel in een puinhoop belanden met zwaar geneste verklaringen.

# Nesten op drie niveaus met 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)))
# Een triviaal voorbeeld
df ['foo'] = np.where (df ['bar'] == 0, 'Zero',
              np.where (df ['bar'] == 1, 'One',
                np.where (df ['bar'] == 2, 'Two', 'Three')))

Beweer en test wat je hebt

Met dank aan https://www.programiz.com

Alleen omdat u uw gegevens in een mooi dataframe hebt, geen dubbele waarden, geen ontbrekende waarden, heeft u mogelijk nog steeds problemen met de onderliggende gegevens. En met een dataframe van 10 miljoen rijen of nieuwe API, hoe kunt u ervoor zorgen dat de waarden precies zijn wat u ervan verwacht?

De waarheid is dat je nooit echt weet of je gegevens correct zijn totdat je ze test. Best practices in software engineering zijn sterk afhankelijk van het testen van hun werk, maar voor data science is het nog steeds een work in progress. Het is beter om nu te beginnen en jezelf goede werkprincipes te leren, in plaats van jezelf op een later tijdstip te moeten omscholen.

Laten we een eenvoudig dataframe maken om te testen.

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

Laten we testen of alle waarden in col1> = 0 zijn met behulp van de ingebouwde methode-assert die bij de standaardbibliotheek in python wordt geleverd. Wat je python vraagt ​​of waar is, zijn alle items in df [‘col1]] groter dan nul. Als dit waar is, ga dan verder, anders gooi je een fout.

assert (df ['col1']> = 0) .all () # Moet niets retourneren

Geweldig lijkt te hebben gewerkt. Maar wat als .all () niet in de bewering was opgenomen?

assert (df ['col1']> = 0)
>>
ValueError: De waarheidswaarde van een serie is dubbelzinnig. Gebruik a.empty, a.bool (), a.item (), a.any () of a.all ().

Humm ziet eruit alsof we een aantal opties hebben bij het testen van onze dataframes. Laten we testen of een van de waarden tekenreeksen zijn.

assert (df ['col1']! = str) .any () # Moet niets retourneren

Hoe zit het met het testen van de twee kolommen om te zien of ze gelijk zijn?

assert (df ['col1'] == df ['col2']). all ()
>>
Hertraceren (meest recente oproep als laatste)
  Bestand "", regel 1, in 
AssertionError

Ah, onze bewering is hier mislukt!

De beste praktijk met beweringen moet worden gebruikt om omstandigheden in uw gegevens te testen die nooit mogen gebeuren. Dit is het geval wanneer u uw code uitvoert, alles stopt als een van deze beweringen faalt.

De .all () -methode controleert of alle elementen in de objecten de assert passeren, terwijl .any () controleert of een van de elementen in de objecten de assert-test doorstaat.

Dit kan handig zijn als u:

  • Controleer of er negatieve waarden in de gegevens zijn ingevoerd;
  • Zorg ervoor dat twee kolommen exact hetzelfde zijn;
  • Bepaal de resultaten van een transformatie, of;
  • Controleer of het aantal unieke id's correct is.

Er zijn meer beweermethoden die ik niet zal bespreken, maar kom er bekend mee die u hier kunt gebruiken. Je weet nooit wanneer je moet testen op een bepaalde voorwaarde en tegelijkertijd moet je beginnen met testen op voorwaarden die je niet in je code wilt.

Test niet voor alles, maar test voor dingen die uw modellen zouden breken.

Bijv. Is een functie met moeten allemaal 0'en en 1'en zijn, eigenlijk gevuld met die waarden.

Bovendien bevat dat wonderpakket panda's ook een testpakket.

pandas.util.test importeren als tm
tm.assert_series_equal (df ['col1'], df ['col2'])
>>
AssertionError: Series zijn anders
Reekswaarden zijn verschillend (100,0%)
[links]: [0, 6, 8, 0, 3, 4, 3, 9, 0, 7]
[rechts]: [6, -1, 4, 5, -7, -5, -10, -8, 4, -4]

We kregen niet alleen een fout, maar panda's vertelden ons wat er mis was.

Leuk.

Als je bovendien wilt beginnen met het bouwen van een testpakket - en je zou kunnen overwegen dit te doen - maak je bekend met het meest ongeschikte pakket dat in de Python-bibliotheek is ingebouwd. Je kunt hier meer over leren.

beautifier

In plaats van je eigen regex te moeten schrijven - wat op zijn best lastig is - is het soms voor jou gedaan. Het beautifier-pakket kan u helpen bij het opschonen van enkele veelgebruikte patronen voor e-mails of URL's. Het is niets bijzonders, maar kan snel helpen met opruimen.

$ pip3 installeer schoonheidsspecialiste
van beautifier import Email, Url
email_string = 'foo@bar.com'
email = Email (email_string)
afdruk (email.domain)
afdruk (email.username)
afdruk (email.is_free_email)
>>
bar.com
foo
vals
url_string = 'https://github.com/labtocat/beautifier/blob/master/beautifier/__init__.py'
url = Url (url_string)
afdruk (url.param)
afdruk (url.username)
afdruk (url.domain)
>>
Geen
{'msg': 'functie is momenteel alleen beschikbaar met linkedin-URL's'}
github.com

Ik gebruik dit pakket wanneer ik een hele reeks URL's heb die ik moet doornemen en die niet voor de 100e keer de regex willen schrijven om bepaalde delen van het adres te extraheren.

Omgaan met Unicode

Wanneer u wat NLP doet, kan het omgaan met Unicode op het beste moment frustrerend zijn. Ik voer iets uit in spaCy en plotseling breekt alles in me vanwege een of ander unicode-personage dat ergens in de hoofdtekst van het document verschijnt.

Het is echt het ergste.

Door ftfy te gebruiken (voor jou opgelost), kun je echt kapotte Unicode repareren. Bedenk wanneer iemand Unicode met één standaard heeft gecodeerd en met een andere heeft gedecodeerd. Nu moet je hier tussen de tekenreeksen mee omgaan, omdat onzinsequenties "mojibake" worden genoemd.

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

Gelukkig gebruikt ftfy heuristieken om mojibake te detecteren en ongedaan te maken, met een zeer laag aantal valse positieven. Laten we eens kijken waar onze bovenstaande strings naar kunnen worden omgezet, zodat we het kunnen lezen. De belangrijkste methode is fix_text (), en die gebruik je om de decodering uit te voeren.

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

Als je wilt zien hoe de decodering wordt uitgevoerd, probeer ftfy.explain_unicode (). Ik denk niet dat dit te nuttig zal zijn, maar het is interessant om het proces te zien.

ftfy.explain_unicode (foo)
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] LATIJNSE KLEINE BRIEF M
U + 0061 a [Ll] LATIJNSE KLEINE BRIEF A
U + 0063 c [Ll] LATIJNSE KLEINE BRIEF C
U + 0072 r [Ll] LATIJNSE KLEINE BRIEF R
U + 003B; [Po] SEMICOLON
U + 005C \ [Po] REVERSE SOLIDUS
U + 005F _ [Pc] LAGE LIJN
U + 0028 ([Ps] LINKER PARENTHESIS
U + 00E3 ã [Ll] LATIJNSE KLEINE BRIEF A MET TILDE
U + 0083 \ x83 [Cc] 
U + 0084 \ x84 [Cc] 
U + 0029) [Pe] JUISTE PARENTHESIS
U + 005F _ [Pc] LAGE LIJN
U + 002F / [Po] SOLIDUS
U + 0026 & [Po] AMPERSAND
U + 006D m [Ll] LATIJNSE KLEINE BRIEF M
U + 0061 a [Ll] LATIJNSE KLEINE BRIEF A
U + 0063 c [Ll] LATIJNSE KLEINE BRIEF C
U + 0072 r [Ll] LATIJNSE KLEINE BRIEF R
U + 003B; [Po] SEMICOLON
Geen

dedupe

Dit is een bibliotheek die machine learning gebruikt om de-duplicatie en entiteitsresolutie snel uit te voeren op gestructureerde gegevens. Er is een geweldige post hier die veel meer in detail gaat dan ik zal doen en waar ik veel op heb getekend.

We zullen de Chicago Chicago Childhood-locatiegegevens downloaden, die u hier kunt vinden. Het heeft een aantal ontbrekende waarden en dubbele waarden uit verschillende gegevensbronnen, dus het is goed om erover te leren.

Als je ooit eerder dubbele gegevens hebt doorgenomen, komt dit je heel bekend voor.

# Kolommen en het aantal ontbrekende waarden in elk
Id heeft 0 na waarden
Bron heeft 0 na waarden
Sitenaam heeft 0 na waarden
Adres heeft 0 na waarden
Zip heeft 1333 na waarden
Telefoon heeft 146 nvt-waarden
Fax heeft 3299 na waarden
Programmanaam heeft na waarden voor 2009
Lengte van de dag heeft 2009 na waarden
IDHS-provider-ID heeft 3298 nvt-waarden
Agentschap heeft 3325 na waarden
Wijk heeft 2754 na waarden
Funded Enrollment heeft 2424 na waarden
Programma-optie heeft 2800 na waarden
Aantal per site EHS heeft 3319 na waarden
Aantal per site HS heeft 3319 na waarden
Director heeft 3337 nvt waarden
Head Start Fund heeft 3337 na waarden
Eearly Head Start Fund heeft 2881 na waarden
CC fund heeft 2818 na waarden
Progmod heeft 2818 na waarden
Website heeft 2815 na waarden
Uitvoerend directeur heeft 3114 nvt waarden
Centre Director heeft 2874 nvt waarden
Beschikbare ECE-programma's hebben 2379 na waarden
NAEYC Geldig tot heeft 2968 na waarden
NAEYC programma-ID heeft 3337 na waarden
E-mailadres heeft 3203 waarden na
Ounce of Prevention Description heeft 3185 nvt waarden
Het type paarse binderservice heeft 3215 na-waarden
Kolom heeft 3337 na-waarden
Kolom2 heeft 3018 na-waarden

De pre-process-methode van dedupe is nodig om ervoor te zorgen dat er geen fouten optreden tijdens de bemonsterings- en trainingsfasen van het model. Geloof me, het gebruik hiervan maakt het gebruik van dedupe veel eenvoudiger. Bewaar deze methode in uw lokale ‘schoonmaakpakket’ zodat u deze in de toekomst kunt gebruiken bij het verwerken van dubbele gegevens.

panda's importeren als pd
import numpy
dedupe importeren
os importeren
csv importeren
importeren re
van unidecode import unidecode
def preProcess (kolom):
    '''
    Wordt gebruikt om fouten tijdens het dedupe-proces te voorkomen.
    '''
    proberen :
        column = column.decode ('utf8')
    behalve AttributeError:
        voorbij lopen
    column = unidecode (column)
    column = re.sub ('+', '', kolom)
    column = re.sub ('\ n', '', kolom)
    column = column.strip (). strip ('"'). strip (" '"). onderste (). strip ()
    
    zo niet kolom:
        kolom = Geen
    terug kolom

Begin nu de .csv kolom per kolom te importeren, terwijl u de gegevens verwerkt.

def readData (bestandsnaam):
    
    data_d = {}
    met open (bestandsnaam) als f:
        reader = csv.DictReader (f)
        voor rij in lezer:
            clean_row = [(k, preProcess (v)) voor (k, v) in row.items ()]
            row_id = int (row ['Id'])
            data_d [row_id] = dict (clean_row)
retour df
name_of_file = 'data.csv'
print ('Gegevens opschonen en importeren ...')
df = readData (name_of_file)

Nu moeten we dedupe vertellen naar welke functies we moeten kijken om dubbele waarden te bepalen. Hieronder wordt elk kenmerk aangegeven door een veld en wordt een gegevenstype toegewezen en of er ontbrekende waarden zijn. Er is een hele lijst met verschillende soorten variabelen die je hier kunt gebruiken, maar om het gemakkelijk te houden, houden we ons nu aan de touwtjes.

Ik ga ook niet elke kolom gebruiken om de duplicatie te bepalen, maar dat kan als je denkt dat het gemakkelijker wordt om de waarden in je dataframe te identificeren.

# Velden instellen
velden = [
        {'field': 'Source', 'type': 'Set'},
        {'field': 'Site name', 'type': 'String'},
        {'field': 'Address', 'type': 'String'},
        {'field': 'Zip', 'type': 'Exact', 'ontbreekt': True},
        {'field': 'Phone', 'type': 'String', 'ontbreekt': True},
        {'field': 'Email Address', 'type': 'String', 'has missing': True},
        ]

Laten we nu beginnen met het invoeren van enkele gegevens.

# Pas in ons model
deduper = dedupe.Dedupe (velden)
# Controleer of het werkt
Deduper
>>
# Voer enkele voorbeeldgegevens in ... 15.000 records
deduper.sample (df, 15000)

Nu gaan we naar het labelgedeelte. Wanneer u deze methode hieronder uitvoert, wordt u door dedupe gevraagd enkele eenvoudige labels te maken.

dedupe.consoleLabel (Deduper)
Wat u zou moeten zien; handmatig de deduper trainen

Het echte ‘a ha!’ Moment is wanneer je deze prompt krijgt. Dit vraagt ​​je om het te trainen, zodat het weet waar het op moet letten. Je weet hoe een dubbele waarde eruit moet zien, dus geef die kennis gewoon door.

Verwijzen deze records naar hetzelfde?
(y) es / (n) o / (u) nsure / (f) voltooid

Nu hoef je niet meer door tonnen en tonnen records te zoeken om te zien of er duplicatie is opgetreden. In plaats daarvan wordt een neuraal netwerk door u getraind om duplicaten in het dataframe te vinden.

Zodra je het hebt voorzien van wat labels, voltooi je het trainingsproces en sla je voortgang op. Je kunt later terugkeren naar je neurale netwerk als je merkt dat je dataframe-objecten hebt die moeten worden ontdaan.

deduper.train ()
# Training opslaan
met open (training_file, 'w') als tf:
        deduper.writeTraining (tf)
# Instellingen opslaan
met open (settings_file, 'wb') als sf:
        deduper.writeSettings (sf)

We zijn bijna klaar, want vervolgens moeten we een drempel instellen voor onze gegevens. Wanneer recall_gewicht gelijk is aan 1, vertellen we deduper om herinnering evenveel te waarderen als precisie. Als recall_gewicht = 3, zouden we recall echter driemaal zoveel waarderen. U kunt met deze instellingen spelen om te zien wat het beste voor u werkt.

drempel = deduper.drempel (df, recall_gewicht = 1)

Eindelijk kunnen we nu door onze df zoeken en zien waar de duplicaten zijn. Het is lang geleden om op deze positie te komen, maar dit is veel beter dan dit met de hand te doen.

# Cluster de duplicaten samen
clustered_dupes = deduper.match (data_d, drempel)
print ('Er zijn {} dubbele sets'.format (len (clustered_dupes)))

Laten we onze duplicaten eens bekijken.

clustered_dupes
>>
[((0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255),
  array ([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), ...

Hum, dat zegt ons niet veel. Wat laat ons dat eigenlijk zien? Wat is er gebeurd met al onze waarden?

Als u goed kijkt, zijn de waarden (0, 1, 215, 509, 510, 1225, 1226, 1879, 2758, 3255) alle id-locaties van dubbele deduper denkt dat deze eigenlijk dezelfde waarde hebben. En we kunnen de originele gegevens bekijken om dit te verifiëren.

{'Id': '215',
 'Bron': 'cps_early_childhood_portal_scrape.csv',
 'Sitenaam': 'tempel van het leger van de redding',
 'Adres': '1 n. ogden',
...
{'Id': '509',
 'Bron': 'cps_early_childhood_portal_scrape.csv',
 'Sitenaam': 'reddingsleger - tempel / reddingsleger',
 'Address': '1 n ogden ave',
 'Zip': Geen,
..

Dit lijkt mij dubbel. Leuk.

Er zijn veel geavanceerdere toepassingen van deduper, zoals matchBlocks voor reeksen van clusters, of interactievelden waarbij de interactie tussen twee velden niet alleen additief is, maar ook multiplicatief. Dit is al heel wat geweest om over te gaan, dus ik zal die uitleg voor het bovenstaande artikel verlaten.

String Matching met fuzzywuzzy

Probeer deze bibliotheek. Het is echt interessant omdat het je een score geeft voor hoe nauw de snaren zijn wanneer ze worden vergeleken.

Dit is een enorm geweldig hulpmiddel geweest, omdat ik in het verleden projecten heb gedaan waarbij ik moest vertrouwen op de fuzzymatch-add-on van Google Sheet om problemen met gegevensvalidatie te diagnosticeren - denk dat CRM-regels niet correct worden toegepast of opgevolgd - en nodig zijn om schoon te maken records om elke vorm van analyse uit te voeren.

Maar voor grote datasets valt deze benadering nogal plat.

Met fuzzywuzzy kun je echter beginnen met snaaraanpassing in een meer wetenschappelijke kwestie. Niet om te technisch te worden, maar het gebruikt iets dat Levenshtein-afstand wordt genoemd bij het vergelijken. Dit is een tekenreeksvergelijkingsmaat voor twee reeksen, zodat de afstand tussen het aantal bewerkingen met één teken is dat nodig is om het ene woord in het andere te veranderen.

Bijv. als je de string foo in bar wilt veranderen, zou het minimum aantal te wijzigen karakters 3 zijn, en dit wordt gebruikt om de ‘afstand’ te bepalen.

Laten we eens kijken hoe dit in de praktijk werkt.

$ pip3 installeer fuzzywuzzy
# test.py
van fuzzywuzzy import fuzz
van fuzzywuzzy importproces
foo = 'is deze string'
bar = 'zoals die string?'
fuzz.ratio (foo, bar)
>>
71
fuzz.WRatio (foo, bar) # Gewogen verhouding
>>
73
fuzz.UQRatio (foo, bar) # Unicode snelle verhouding
>> 73

Het fuzzywuzzy-pakket heeft verschillende manieren om tekenreeksen te evalueren (WRatio, UQRatio, enz.) En ik blijf gewoon bij de standaardimplementatie voor dit artikel.

Vervolgens kunnen we kijken naar een tokenized string, die een maat geeft voor de gelijkenis van de sequenties tussen 0 en 100, maar het token sorteert voordat het wordt vergeleken. Dit is belangrijk omdat u misschien alleen de inhoud van de tekenreeksen wilt zien, in plaats van hun posities.

De tekenreeksen foo en bar hebben dezelfde tokens, maar zijn structureel verschillend. Wil je ze hetzelfde behandelen? Nu kunt u eenvoudig dit soort verschillen in uw gegevens bekijken en verantwoorden.

foo = 'dit is een foo'
bar = 'foo a is dit'
fuzz.ratio (foo, bar)
>>
31
fuzz.token_sort_ratio ('dit is een foo', 'foo a is dit')
>>
100

Of vervolgens moet u de meest overeenkomende string van een string uit een zoeklijst vinden. In dit geval gaan we kijken naar Harry Potter-titels.

Hoe zit het met dat Harry Potter-boek met de ... iets titel ... het heeft ... Ik weet het niet. Ik moet gewoon raden en kijken welke van deze boeken het dichtst bij mijn gok scoort.

Mijn gok is ‘vuur’ en laten we kijken hoe het scoort ten opzichte van de mogelijke lijst met titels.

lst_to_eval = ['Harry Potter en de Steen der Wijzen',
'Harry Potter en de Geheime kamer',
'Harry Potter en de gevangene van Azkaban',
'Harry Potter and the Goblet of Fire',
'Harry Potter and the Order of the Phoenix',
'Harry Potter en de Halfbloed Prins',
'Harry Potter en de relieken van de dood']
# Top twee reacties op basis van mijn gok
process.extract ("fire", lst_to_eval, limit = 2)
>>
[('Harry Potter and the Goblet of Fire', 60), ('Harry Potter and the Sorcerer's Stone', 30)
results = process.extract ("fire", lst_to_eval, limit = 2)
voor resultaat in resultaten:
  print ('{}: heeft een score van {}'. formaat (resultaat [0], resultaat [1]))
>>
Harry Potter and the Goblet of Fire: heeft een score van 60
Harry Potter and the Sorcerer's Stone: heeft een score van 30

Of als je er gewoon een wilt retourneren, dan kan dat.

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

Ik weet dat we eerder over dedupe’s hebben gesproken, maar hier is een andere toepassing van hetzelfde proces met fuzzywuzzy. We kunnen een lijst met strings met duplicaten maken en gebruiken fuzzy matching om duplicaten te identificeren en te verwijderen.

Niet zo luxe als een neuraal net, maar het zal het werk doen voor kleine operaties.

We gaan door met het Harry Potter-thema en zoeken naar dubbele tekens uit de boeken in een lijst.

U moet een drempelwaarde instellen tussen 0 en 100. Naarmate de drempelwaarde afneemt, neemt het aantal gevonden duplicaten toe, zodat de teruggekeerde lijst wordt kortgesloten. De standaardwaarde is 70.

# Lijst met dubbele karakternamen
bevat_dupes = [
'Harry Potter',
'H. Pottenbakker',
'Harry James Potter',
'James Potter',
'Ronald Bilius \' Ron \ 'Weasley',
'Ron Wemel',
'Ronald Weasley']
# Druk de dubbele waarden af
process.dedupe (contains_dupes)
>>
dict_keys (['Harry James Potter', "Ronald Bilius 'Ron' Weasley"])
# Druk de dubbele waarden af ​​met een hogere drempel
process.dedupe (bevat_dupes, drempel = 90)
>>
dict_keys (['Harry James Potter', 'H. Potter', "Ronald Bilius 'Ron' Weasley"])

En als een snelle bonus kun je ook wat fuzzy matching doen met het datetime-pakket om datums uit een reeks tekst te extraheren. Dit is geweldig als je niet (opnieuw) een regex-expressie wilt schrijven.

van dateutil.parser import parse
dt = parse ("Vandaag is 1 januari 2047 om 8:21:00 uur", fuzzy = True)
afdruk (dt)
>>
2047-01-01 08:21:00
dt = parse ("18 mei 2049 iets iets", fuzzy = True)
afdruk (dt)
>>
2049-05-18 00:00:00

Probeer wat sklearn

Naast het opschonen van de gegevens, moet u ook de gegevens voorbereiden, zodat deze de vorm hebben die u in uw model kunt invoeren. De meeste voorbeelden hier zijn rechtstreeks afkomstig uit de documentatie, die moet worden gecontroleerd, omdat het echt goed werk is om meer uit te leggen over de nieuwheid van elk pakket.

We zullen eerst het preprocessing-pakket importeren en daarna aanvullende methoden krijgen terwijl we verder gaan. Ik gebruik ook sklearn-versie 0.20.0, dus als u problemen ondervindt bij het importeren van sommige pakketten, controleer dan uw versie.

We werken met twee verschillende soorten gegevens, str en int, gewoon om te benadrukken hoe de verschillende voorbewerkingstechnieken werken.

# Bij aanvang van het project
van sklearn import preprocessing
# En laten we een willekeurige reeks int's maken om te verwerken
ary_int = np.random.randint (-100, 100, 10)
ary_int
>> [5, -41, -67, 23, -53, -57, -36, -25, 10, 17]
# En sommige str om mee te werken
ary_str = ['foo', 'bar', 'baz', 'x', 'y', 'z']

Laten we een aantal snelle labels proberen met LabelEncoder op onze ary_str. Dit is belangrijk omdat je niet alleen onbewerkte snaren kunt invoeren - nou ja, maar dat valt buiten het bestek van dit artikel - in je modellen. We coderen dus labels voor elk van de strings, met een waarde tussen 0 en n. In onze ary_str hebben we 6 unieke waarden, dus ons bereik zou 0 - 5 zijn.

van sklearn.preprocessing import LabelEncoder
l_encoder = preprocessing.LabelEncoder ()
l_encoder.fit (ary_str)
>> LabelEncoder ()
# Wat zijn onze waarden?
l_encoder.transform ([ 'foo'])
>> array ([2])
l_encoder.transform ([ 'BAZ'])
>> array ([1])
l_encoder.transform ([ 'bar'])
>> array ([0])

Je zult merken dat deze niet zijn besteld, omdat zelfs via foo vóór bar in de array kwam, deze werd gecodeerd met 2 terwijl bar werd gecodeerd met 1. We gebruiken een andere coderingsmethode als we ervoor moeten zorgen dat onze waarden zijn gecodeerd in de juiste volgorde.

Als je veel categorieën hebt om bij te houden, vergeet je misschien welke str naar welke int verwijst. Daarvoor kunnen we een dictaat maken.

# Controleer toewijzingen
lijst (l_encoder.classes_)
>> ['bar', 'baz', 'foo', 'x', 'y', 'z']
# Maak een woordenboek met toewijzingen
dict (zip (l_encoder.classes_, l_encoder.transform (l_encoder.classes_)))
>> {'bar': 0, 'baz': 1, 'foo': 2, 'x': 3, 'y': 4, 'z': 5}

Het proces is een beetje anders als je een dataframe hebt, maar eigenlijk een beetje eenvoudiger. U hoeft alleen .apply () het LabelEncoder-object toe te passen op het DataFrame. Voor elke kolom krijgt u een uniek label voor de waarden in die kolom. Merk op hoe foo is gecodeerd naar 1, maar y ook.

# Probeer LabelEncoder op een dataframe
panda's importeren als pd
l_encoder = preprocessing.LabelEncoder () # Nieuw object
df = pd.DataFrame (data = {'col1': ['foo', 'bar', 'foo', 'bar'],
                          'col2': ['x', 'y', 'x', 'z'],
                          'col3': [1, 2, 3, 4]})
# Nu voor het gemakkelijke gedeelte
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 gaan we verder met ordinale codering waar functies nog steeds worden uitgedrukt als gehele waarden, maar ze hebben een gevoel van plaats en structuur. Zodanig dat x voor y komt en y voor z komt.

We gaan hier echter een sleutel in gooien. De waarden zijn niet alleen geordend, maar ze worden ook aan elkaar gekoppeld.

We nemen een reeks van twee waarden [‘foo’, ‘bar’, ‘baz’] en [‘x’, ‘y’, ‘z’]. Vervolgens coderen we 0, 1 en 2 voor elke set waarden in elke array en maken we een gecodeerd paar voor elk van de waarden.

Bijv. [‘Foo’, ‘z’] wordt toegewezen aan [0, 2] en [‘baz’, ‘x’] wordt toegewezen aan [2, 0].

Dit is een goede aanpak om te nemen wanneer je een aantal categorieën moet nemen en ze beschikbaar moet maken voor een regressie, en vooral goed als je interleaving sets strings hebt - afzonderlijke categorieën die elkaar nog overlappen - en vertegenwoordiging in het dataframe nodig hebben .

van sklearn.preprocessing import OrdinalEncoder
o_encoder = OrdinalEncoder ()
ary_2d = [['' foo ',' bar ',' baz '], [' x ',' y ',' z ']]
o_encoder.fit (2d_ary) # Pas de waarden aan
o_encoder.transform ([['' foo ',' y ']])
>> array ([[0., 1.]])

De klassieke hot of ‘dummy’ codering, waarbij afzonderlijke kenmerken van categorieën vervolgens worden uitgedrukt als extra kolommen van 0'en of 1'en, afhankelijk van de waarde verschijnt of niet. Dit proces maakt een binaire kolom voor elke categorie en retourneert een schaarse matrix of dichte matrix.

Met dank aan https://blog.myyellowroad.com/

Waarom dit zelfs gebruiken? Omdat dit type codering nodig is om categorische gegevens naar veel scikit-modellen te voeren, zoals lineaire regressiemodellen en SVM's. Dus ga hier gerust mee zitten.

van sklearn.preprocessing import OneHotEncoder
hot_encoder = OneHotEncoder (handle_unknown = 'negeren')
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 ()
>>
array ([[1., 0., 0., 0., 1., 0.],
       [0., 0., 0., 1., 0., 0.]])

Hoe zit het als we een dataframe hadden om mee te werken?

Kunnen we nog steeds één hot-codering gebruiken? Het is eigenlijk veel eenvoudiger dan je denkt, omdat je alleen de .get_dummies () in panda's moet gebruiken.

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

Twee van de drie kolommen in df zijn opgesplitst en binair gecodeerd naar een dataframe.

Bijv. de kolom col1_bar is col1 van df, maar heeft 1 als recordwaarde toen bar de waarde was in het oorspronkelijke dataframe.

Hoe zit het als onze functies binnen een bepaald bereik moeten worden getransformeerd. Door MinMaxScaler te gebruiken, kan elke functie afzonderlijk worden geschaald zodat deze binnen het gegeven bereik valt. Standaard liggen de waarden tussen 0 en 1, maar u kunt het bereik wijzigen.

van sklearn.preprocessing import MinMaxScaler
mm_scaler = MinMaxScaler (feature_range = (0, 1)) # Tussen 0 en 1
mm_scaler.fit ([ary_int])
>> MinMaxScaler (copy = True, feature_range = (0, 1))
afdruk (scaler.data_max_)
>> [5. -41. -67. 23. -53. -57. -36. -25. 10. 17.]
drukken (mm_scaler.fit_transform ([ary_int]))
>> [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] # Humm er is iets mis

Als je merkt dat de uitvoer allemaal nullen is ... wat we niet wilden. Er is hier en hier een goede uitleg over waarom dat zou zijn gebeurd, maar het korte verhaal is dat de array onjuist is opgemaakt.

Het is een (1, n) matrix en moet worden omgezet in een (n, 1) matrix. De eenvoudigste manier om dit te doen, is ervoor te zorgen dat uw array een numpy array is, zodat u de vorm kunt manipuleren.

# Maak een numpy array
ary_int = np.array ([5, -41, -67, 23, -53, -57, -36, -25, 10, 17])
# Transformeren
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]])
# Je kan ook gebruiken
mm_scaler.fit_transform (ary_int.reshape (-1, 1))
# Probeer ook een andere schaal
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 we onze gegevens snel kunnen schalen, hoe zit het met het implementeren van een vorm in onze getransformeerde gegevens? We kijken naar het standaardiseren van de gegevens, die je waarden gaan geven die een Gaussiaans maken met een gemiddelde van 0 en een SD van 1. Je zou deze aanpak kunnen overwegen bij het implementeren van gradiëntdaling, of als je gewogen invoer nodig hebt, zoals regressie en neurale netwerken. Als u een KNN gaat implementeren, moet u eerst uw gegevens schalen. Merk op dat deze aanpak anders is dan normalisatie, dus raak niet in de war.

Gebruik gewoon de schaal van voorverwerking.

preprocessing.scale (foo)
>> array ([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 # In wezen nul
 preprocessing.scale (foo) .std ()
>> 1.0 # Precies wat we wilden

Het laatste sklearn-pakket om naar te kijken is Binarizer, je krijgt hier nog steeds 0'en en 1'en maar nu zijn ze op jouw eigen voorwaarden gedefinieerd. Dit is het proces van ‘thresholding’ numerieke functies om booleaanse waarden te krijgen. De waardendrempel die groter is dan de drempelwaarde wordt toegewezen aan 1, terwijl die ≤ wordt toegewezen aan 0. Dit is ook een veel voorkomend proces bij het voorbewerken van tekst om de termfrequenties in een document of corpus te krijgen.

Houd er rekening mee dat zowel fit () als transform () een 2d-array vereisen, en daarom heb ik ary_int in een andere array genest. Voor dit voorbeeld heb ik de drempelwaarde ingesteld op -25, dus alle getallen die daar strikt boven liggen, krijgen een 1.

van sklearn.preprocessing import Binarizer
# Stel -25 in als onze drempel
tz = Binarizer (drempel = -25.0) .fit ([ary_int])
tz.transform ([ary_int])
>> array ([[1, 0, 0, 1, 0, 0, 0, 0, 1, 1]])

Nu we deze paar verschillende technieken hebben, welke is de beste voor uw algoritme? Het is waarschijnlijk het beste om een ​​paar verschillende tussenliggende dataframes op te slaan met geschaalde gegevens, binned gegevens, enz. Zodat u het effect op de uitvoer van uw model (len) kunt zien.

Laatste gedachten

Het opschonen en voorbereiden van gegevens is onvermijdelijk en over het algemeen een ondankbare taak als het gaat om data science. Als u het geluk hebt een data-engineeringteam bij u te hebben dat kan helpen bij het opzetten van ETL-pijpleidingen om uw werk gemakkelijker te maken, dan bevindt u zich misschien in de minderheid van datawetenschappers.

Het leven is niet alleen een verzameling Kaggle-gegevenssets, waar u in werkelijkheid beslissingen moet nemen over hoe u de gegevens die u elke dag nodig hebt kunt openen en opschonen. Soms heb je veel tijd om te zorgen dat alles op de juiste plek staat, maar meestal word je gedwongen om antwoorden te vinden. Als u over de juiste tools beschikt en begrijpt wat mogelijk is, kunt u die antwoorden gemakkelijk vinden.

Zoals altijd hoop ik dat je iets nieuws hebt geleerd.

cheers,

Extra lezen