data preprocessing

Data preprocessing – przygotowanie danych

 

(kliknij w obrazek, żeby zobaczyć pliki źródłowe)

Bez danych nie ma wyników!

Mówi się, że kluczem do przygotowania dokładnych modeli predykcyjnych i otrzymania wiarygodnych wyników analiz są dane. Wiele źródeł podaje, że opracowanie wartościowego repozytorium danych może zająć aż 80% czasu przeznaczonego na realizację projektu! Mówiąc o przygotowaniu danych ( data preprocessing ) – należy mieć na uwadze zarówno aspekt techniczy – poprawne załadowanie danych do środowiska programistycznego, jak i aspekt analityczny. O ile pierwszy to zazwyczaj schematyczne procedury, o tyle drugi często może być związany ze znajomością badanego obszaru lub po prostu z intuicją. Często może okazać się, że dane będą zawierały niekompletne dane – będziemy musieli wówczas arbitralnie podjąć decyzję co z takimi danymi zrobić.

Po załadowaniu danych do środowiska programistycznego, należy podjąć wszystkie możliwe kroki poprawiające ich jakość. W tym artykule postaramy się wypracować techniki, które w sposób zautomatyzowany będziemy mogli wykorzystać w kolejnych projektach.

Wartościowy zbiór danych

Poprawnie przygotowane zbiory danych spełniają warunki:

  1. Są zgromadzone w obiekcie typu data.frame z poprawnie nadanymi nazwami kolumn;
  2. Każda kolumna obiektu data.frame posiada odpowiedni typ danych – adekwatny do reprezentatywnych danych;
  3. Nie posiadają wartości nieniosących informacji(N/A, NULL oraz im odpowiadającym, np. “brak_informacji”, “unknown”, “—“);
  4. Wartości liczbowe są standaryzowane i znormalizowane w ramach całego zbioru;

Dobra praktyka: Kiedy pracujesz ze zbiorem danych, który nie jest standardowo obsługiwanym formatem – po opracowaniu go – zapisz go do formatu standardowego. Inaczej mówiąc kiedy już włożysz ogrom pracy, by zaimportować dane z niestandardowego pliku do obiektu data.frame – wyeksportuj go, np.  do pliku CSV.

Przygotowanie danych

Opracowanie danych to czasochłonny proces. W ramach tego artykułu zajmiemy się wyłącznie plikami płaskimi zawierającymi dane zapisane za pomocą znaków. Osobne artykuły będą poświęcone analizowaniu innym źródłom, takim jak obraz, dźwięk, dane strumieniowe etc.  Wspomniany proces możemy podzielić na kilka etapów:

  1. Wczytanie danych – załadowanie plików;
  2. Techniczna korekta danych;
  3. Analityczna korekta danych;
  4. Zapis opracowanych danych.

Każdy z tych etapów zostanie dokładnie opisany. Dodatkowo przedstawię odpowiednie techniki i podejście programistyczne w języku R.

Wczytanie pliku (zbioru danych) do zmiennej

Język R pozwala nam w łatwy sposób zaimportować zbiory danych. W tabeli 1. wskazano wybrane funkcje.

Tabela 1. Funkcje wczytywania danych.
Nazwa funkcji Opis
read.delim() odczyt plików gdzie dane są oddzielone separatorami (wybranym znakiem, domyślnie – TAB)
read.delim2() odczyt plików gdzie dane są oddzielone separatorami (wybranym znakiem, domyślnie – TAB) (dla danych gdzie część dziesiętna oddzielona jest przecinkiem)
read.csv() odczyt plików CSV – dane oddzielone przecinkami
read.csv2() dane oddzielone przecinkami (dla danych, gdzie część dziesiętna oddzielona jest przecinkiem)
read.table() funkcjonalnością identyczna jak powyższe, nie posiada jednak charakterystycznych dla danych plików wartości domyślnych argumentów
read.fwf()  odczyt plików, gdzie każda kolumna posiada predefiniowany rozmiar bitów

Powyższe funkcje działają w podobny sposób, tzn. wymagają podobnych argumentów wejściowych. Poniżej przykładowy fragment kodu – import pliku.

# load the dataset
dataset <- read.csv(file = 'test_dataset.csv', #file path
                   sep = ",", #separator between columns
                   header = TRUE, #if TRUE file contain row (first) with columns names
                   na.strings = c("brak","","NULL"), #a character vector of strings 
                                                     #which will be interpreted as NA values
                   stringsAsFactors = FALSE #if TRUE convert stings as FACTORS
                   )

names(dataset) <- c("job_name","age","salary","paid") #create columns names

Jeśli nie jesteśmy w stanie nazwać kolumn, tak by jednoznacznie opisywały dany atrybut, możemy nazwać je sekwencyjnie według wzorca. Tworzymy sekwencję, która jest równa liczbie kolumn zbioru, kolejne elementy to zlepek wzorca i kolejnego numeru kolumny:

#Create columns names by sequence with pattern 
names(dataset) <- paste("Feature",seq(1:ncol(dataset)),sep = "_")

W tej chwili wczytaliśmy plik z danymi do naszego programu, teraz należy ten zbiór przygotować, tak by był użyteczny do dalszych przetwarzań.

 

Obsługa niekompletnych danych

Ważnym aspektem obszaru data preprocessing jest zadbanie o jakość danych. Szczególnym zagadnieniem jest obsługa danych niekompletnych. Jesteśmy zobowiązani doprowadzić dane do takiego stanu, żeby wszystkie wartości były reprezentatywne – niosły jakąś informację. Należy zauważyć, że rozpoczęliśmy  ten proces już podczas wczytywania danych z pliku:

na.strings = c("brak","","NULL"), #a character vector of strings 
                                  #which will be interpreted as NA values

Wskazanie komórek z wartością nieznaną

W powyższym kodzie wskazaliśmy programowi by interpretował komórki zawierające słowa: “brak”,””,”NULL” jako wartość nieznaną – NA.

Możemy podejrzeć jak wygląda nasz zbiór danych:

View(dataset)

Zapomnieliśmy, że występują jeszcze komórki zawierające słowo “unknown” (dodatkowo uwzględnimy komórki zawierające znak zapytania), czyli de facto to komórki nie niosące informacji. Nic straconego! Możemy przeszukać cały zbiór w poszukiwaniu podobnych przypadków i zastąpić wskazane ciągi znaków na wartość NA:

#Make cell as NA base on string pattern
NAindicators <- c("\\?","unknown")
for(NApattern in NAindicators)(
  dataset <- data.frame(sapply(dataset, function(x) gsub(NApattern, NA, x)))
)

Statystyki występowania wartości nieznanych

Przed dalszymi pracami możemy wyświetlić proste statystki pokazujące, gdzie możemy spodziewać się wartości nieokreślonych:

# NA's statistics
statNaCount <- colSums(is.na(dataset))
statRowsWithNa <- dataset[!complete.cases(dataset),]
statRowsWithNaCount <- nrow(statRowsWithNa)

print("Counter NA's in each column ")
print(statNaCount)
print("=============================================================")
print("Show complete rows with NA's values in any column ")
print(statRowsWithNa)
print("=============================================================")
print("Number of rows with any NA's values")
print(statRowsWithNaCount)
print("=============================================================")

Uzupełnienie danych na podstawie dostarczonych danych

W wielu przypadkach może okazać się, że wartości nieznane możemy oszacować za pomocą podstawowych funkcji statystycznych – średnia, mediana etc. w odniesieniu do pozostałych wartości w zbiorze. Taki zabieg spowoduje, że uzupełnimy nieznaną komórę przybliżoną wartością przez co nie utracimy całego wiersza danych, gdy będziemy stosowali strategię usunięcia wiersza, gdy posiada choćby jedną wartość NA.

W pierwszym kroku określamy, w których kolumnach oczekujemy brakujących danych, w naszym przypadku są to kolumny “job_name”,”salary”:

#columns with incomplete data
columnsNamesToEnterData <- c("job_name","salary")

Następnie ustalamy pozycję tych kolumn w zbiorze danych(na którym miejscu są te kolumny):

columnsPositionToEnterData <- numeric()
featuresNames <- names(dataset)
for(columnName in columnsNamesToEnterData ){
  columnsPositionToEnterData <- append(columnsPositionToEnterData,which(featuresNames == columnName))
}

Ostatecznie wszystkie wartości nieznane uzupełniamy wartością średnią, która jest wyliczana na podstawie pozostałych wartości w danej kolumnie:

for(columnPosition in columnsPositionToEnterData ){
  #Here we can use different strategy: mean, median, sum etc. in "FUN" parameter
  dataset[,columnPosition] = ifelse(is.na(dataset[,columnPosition]),
                                    ave(dataset[,columnPosition], FUN = function(x) mean(x, na.rm = TRUE)),
                                    dataset[,columnPosition])
}

Możemy użyć innych funkcji, np. mediany – median(). Tym sposobem uzupełniliśmy dane w zadanych kolumnach.

Strategia postępowania z brakującymi danymi

Na tym etapie pracy z danymi posiadamy określone komórki, które posiadają wartości nieznane. Nadszedł czas na decyzję ekspercką – decyzję ludzką. Musisz zdecydować co zrobić.W takim przypadku, masz kilka możliwości:

  • Wrócić do źródła danych, spróbować wydobyć kompletne dane ze źródła.
  • Ręcznie poprawić plik z danymi na podstawie Twojej wiedzy.
  • Uzupełnić dane wykorzystując funkcje statystyczne i probabilistyczne.
  • Usunąć cały rekord, w którym znajduje się choćby jedna komórka z wartością NA.

Ja zaimplementuję to ostatnie, to najprostsze rozwiązanie. Godzisz się jednak, że usuwając takie rekordy – tracisz nierzadko wartościowe dane. Należy używać tej strategii w ostateczności i z rozwagą. Kod takiej operacji jest banalny:

#make decision what to do with lost data
dataset<-na.omit(dataset)

Możemy sprawdzić ręcznie czy wszystkie niepożądane wyniki zostały usunięte:

View(dataset)

lub wyświetlić statystki:

statNaCountAfterNaHandle <- colSums(is.na(dataset))
print("Counter NA's in each column ")
print(statNaCountAfterNaHandle)

Uff! Udało się mamy kompletny zbiór danych!

Ustalenie typów danych

Kolejną ważą operacją jest ustalenie typów danych w obrębie każdej kolumny.

Dobra praktyka: Zaleca się, żeby dane w obrębie kolumny reprezentowały wartości o tym samym typie, ułatwi to późniejsze przetwarzanie danych.

Dobra praktyka: Wskazanym jest, by kolumny zawierające dane kategoryczne, czyli takie, które zawierają wartości o ściśle określonych stanach – były implementowane jako kolumny typu factor. Dodatkowo warto zamienić ciągi znakowe na liczby. Przykład: “Yes” można zamienić na 1, a “No” zamienimy wówczas na 0

Wykonujemy to implementując kod:

#Factorization of categorical data (here: features/column)
dataset$paid<- factor(dataset$paid,
                            levels = levels(dataset$paid),#Warning: to speed up You can enter by hand the levels!
                            labels = c(0,1)
)

###------------------------------------------------------------------------------------------------
# Set datatype on clear dataset

dataset$job_name <- as.character(dataset$job_name)
dataset$age <- as.numeric(as.character(dataset$age)) #Note on correct syntax while conversion from character to numeric
dataset$salary <- as.numeric(as.character(dataset$salary)) #Note on correct syntax while conversion from character to numeric
dataset$paid<- as.factor(dataset$paid)

Skalowanie danych

Przydatną operacją może być skalowanie danych stosując normalizację. Ten temat będzie dokładniej poruszany w osobnym artykule. Nadmienię jednak, że ta czynność to skalowanie pierwotnych danych do małego, specyficznego przedziału. Ułatwia to późniejsze obliczenia, stosunek poszczególnych wartości względem samych siebie pozostaje niezmieniony zmienia się jednak skala w jakiej prezentowane są dane. Jest szczególnie użyteczne, gdy wykorzystujemy zbiór danych w algorytmach stosujących działania potęgowania, silni itd. podstawą jest znacznie mniejsza liczba – oszczędzamy dzięki temu czas procesora i pozostałe zasoby komputera podczas złożonych obliczeń.

Zapis danych – obiekt R jako plik

Środowisko R daje możliwość łatwego zapisu wartości zmiennych do plików. Nie są to pliki CSV, EXCEL a pliki reprezentujące obiekty języka R. Inaczej mówiąc, jeżeli opracujesz swój zbioór danych, po wyłączeniu RStudio wszystkie dane Ci znikną. Dzieje się tak, bowiem zmienne lokalne(np. dataset) przechowywane są w pamięci RAM. Możesz temu zaradzić zapisując potrzebne zmienne jako pliki, poniżej dwa sposoby:

#save and load dataset object with original variable name "dataset"
#You are not able to change variable name
save(dataset, "file1.rda")
load(file = "file1.rda")

#save and load dataset object with new variable name "new_dataset"
saveRDS(dataset, "file2.rds")
newDataset<- readRDS(file = "file2.rds")

Stosując pierwszy sposób zapisujesz zmienną na dysku wraz z jej przypisaną nazwą – u nas jest to “dataset”, w drugim przypadku zapisujesz zmienną bez przypisanej nazwy i podczas ładowania możesz utworzyć nową zmienną o dowolnej nazwie.

Ostatnia prosta

Tym sposobem dotarliśmy do końca artykułu. Możemy powiedzieć, że mamy wstępnie przygotowane dane do dalszej pracy! Cały kod dostępny na bitbucket oraz dla wygody poniżej:

# Author: Lukasz Joksch
# Source www.joksch.pl

# load the dataset
dataset <- read.csv(file = 'test_dataset.csv', #file path
                   sep = ",", #separator between columns
                   header = TRUE, #if TRUE file contain row (first) with columns names
                   na.strings = c("brak","","NULL"), #a character vector of strings 
                                                     #which are to be interpreted as NA values
                   stringsAsFactors = FALSE #if TRUE convert stings as FACTORS
                   )

names(dataset) <- c("job_name","age","salary","paid") #create columns names

#Create columns names by sequence with pattern 
#It is optional, UNCOMMENT if You want (when You have not names of columns)
#names(dataset) <- paste("Feature",seq(1:ncol(dataset)),sep = "_")

naSum <- sum(is.na(dataset))


###--------------------------------------------------------------------------------------------------
# Taking care of missing data - replacing NA(null) values by enter column (feature) name of dataset

#Make cell as NA base on string pattern
NAindicators <- c("\\?","unknown")
for(NApattern in NAindicators)(
  dataset <- data.frame(sapply(dataset, function(x) gsub(NApattern, NA, x)))
)

# NA's statistics
statNaCount <- colSums(is.na(dataset))
statRowsWithNa <- dataset[!complete.cases(dataset),]
statRowsWithNaCount <- nrow(statRowsWithNa)

print("Counter NA's in each column ")
print(statNaCount)
print("=============================================================")
print("Show complete rows with NA's values in any column ")
print(statRowsWithNa)
print("=============================================================")
print("Number of rows with any NA's values")
print(statRowsWithNaCount)
print("=============================================================")

#columns with incomplete data
columnsNamesToEnterData <- c( "job_name","salary")

columnsPositionToEnterData <- numeric()
featuresNames <- names(dataset)
for(columnName in columnsNamesToEnterData ){
  columnsPositionToEnterData <- append(columnsPositionToEnterData,which(featuresNames == columnName))
}

for(columnPosition in columnsPositionToEnterData ){
  #Here we can use different strategy: mean, median, sum etc. in "FUN" parameter
  dataset[,columnPosition] = ifelse(is.na(dataset[,columnPosition]),
                                    ave(dataset[,columnPosition], FUN = function(x) mean(x, na.rm = TRUE)),
                                    dataset[,columnPosition])
}

#make decision what to do with lost data
dataset<-na.omit(dataset)


statNaCountAfterNaHandle <- colSums(is.na(dataset))
print("Counter NA's in each column ")
print(statNaCountAfterNaHandle)
###--------------------------------------------------------------------------------------------------
#Factorization of categorical data (here: features/column)
dataset$paid<- factor(dataset$paid,
                            levels = levels(dataset$paid),#Warning: to speed up You can enter by hand the levels!
                            labels = c(0,1)
)

###------------------------------------------------------------------------------------------------
# Set datatype on clear dataset

dataset$job_name <- as.character(dataset$job_name)
dataset$age <- as.numeric(as.character(dataset$age)) #Note on correct syntax while conversion from character to numeric
dataset$salary <- as.numeric(as.character(dataset$salary)) #Note on correct syntax while conversion from character to numeric
dataset$paid<- as.factor(dataset$paid)

###------------------------------------------------------------------------------------------------
# Values normalization

#Here we normalize column age and salary
#We use R’s built-in scale() function - Z-SCORE normalization
dataset[2:3] <- scale(dataset[2:3])

View(dataset)

#save and load dataset object with original variable name "dataset"
#You are not able to change variable name
save(dataset, "file1.rda")
load(file = "file1.rda")

#save and load dataset object with new variable name "new_dataset"
saveRDS(dataset, "file2.rds")
newDataset<- readRDS(file = "file2.rds")


 

 

Dodaj komentarz