Lab 06 - Czyszczenie danych
Lab 05 - Czyszczenie danych
Celem czyszczenia danych jest:
- wykrycie elementów brakujących i ich uzupełnienie lub usunięcie wierszy
- konwersja danych (np daty) i typów nominalnych (w tym korekta błędów w nazwach elementów, np. poznań, Poznan, Pznan, Poznań)
- analiza rozkładów i usunięcie elementów odstających (outlierów) (Lab 06)
- normalizacja danych i normalizacja rozkładu (Lab 06)
Zbiór danych
W zadaniach będziemy posługiwać się zbiorem opisującym historię sprzedaży budynków, zawierającym szczegóły dotyczące nieruchomości oraz cenę za jaką zostały sprzedane: melb_data.csv.
Ocena efektów - skuteczność klasyfikacji
W zadaniach spróbuj odnieść efekty wprowadzonych zmian do efektu dla zadania klasyfikacji. Do oceny zastosuj funkcję:
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
def score_dataset(x_train, x_valid, y_train, y_valid):
= RandomForestRegressor(n_estimators=100, random_state=0)
model
model.fit(x_train, y_train)= model.predict(x_valid)
preds return mean_absolute_error(y_valid, preds)
Do oceny skuteczności klasyfikacji stosuj zawsze zbiór uczący i testowy, tzn. wszelkie hipotezy, wyznaczanie statystyk (np. wartości średniej, kwartyli) wyznaczaj wyłącznie dla zbioru uczącego, a następnie metodę zastosuj dla zbioru testowego (bez zmiany wartości wyznaczonych parametrów). Podział na zbiór uczący i testowego przeprowadź w proporcji 70/30%. Podział na zbiory przeprowadź na początku i potem używaj tych zbiorów dla wszystkich danych (rozwiązanie to zawiera pewien błąd metodyczny wynikający z wielokrotnego wykorzystania tego samego zbioru, zajmiemy się tym na Lab 07).
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
= train_test_split(df, test_size=0.7)
train_df, test_df # czyszczenie danych
# train_df_cleaned = ...
# test_df_cleaned = ...
= train_df_cleaned.select_dtypes(include=[np.number]).columns.difference(['Price']) # wybiera tylko kolumny z wartosciami numerycznymi, za wyjątkiem kolumny z wartością referencyjną - wejście do klasyfikatora
cols_x = 'Price' # - wyjście z klasyfikatora
cols_y print(score_dataset(train_df_cleaned[cols_x], test_df_cleaned[cols_x], train_df_cleaned[cols_y], test_df_cleaned[cols_y]))
UWAGA
- Funkcja
score_dataset
zwraca średnią z wartości bezwzględnej błędu dla zbioru testowego. - Do oceny skuteczności stosujemy zbiór testowy, który nie został użyty w procedurze uczenia i wyboru metody.
- Pamiętaj jednak, że jednoznaczna interpretacja tego czy różnica między oboma podejściami jest istotna statystycznie wymaga rozszerzonej analizy, zajmiemy się tym na Lab 07.
- Porównując otrzymany wynik błędu spróbuj określić dlaczego nastąpiła zmiana, pamiętaj przy tym, że podejście związane z czyszczeniem danych zależy od typu danych.
Elementy brakujące
- Wczytaj zbiór danych z pliku do zmiennej
df
- Wyznacz liczbę wartości brakujących (pustych) i przeanalizuj w jakich kolumnach występują braki
= df.isnull().sum() missing_values_count
- Spróbuj określić dla każdej kolumny procent występowania wartości brakujących. Wyświetl je w postaci tabeli, gdzie indeksem jest nazwa kolumny, a kolumnami procent braków oraz całkowita liczba braków (możesz użyć metody
pd.concat
)
Podejście 1: usunięcie kolumn/wierszy zawierających przynajmniej 1 element pusty - przetestuj oba podejścia:
= df_set.dropna()
df_cleaned_rows = df_set.dropna(axis=1) df_cleaned_cols
- Zastanów się, które z tych podejść powinno być zastosowane jeśli chcemy stworzyć klasyfikator predykujący ceny nieruchomości?
- Czy wiesz, które wiersze zostały usunięte? Spróbuj wyodrębnić listę ich indeksów.
- Sprawdź dokumentację dropna i zobacz:
- w jaki sposób usunąć tylko wiersze z jeśli wartości puste są w kolumnie
BuildingArea
, - ograniczając liczbę wierszy sprawdź ile zostanie wierszy jeśli usunie się wiersze, które nie mają równocześnie wypełnionego pola
BuildingArea
iYearBuilt
- w jaki sposób usunąć tylko wiersze z jeśli wartości puste są w kolumnie
Podejście 2: wypełnienie pustych wartości np. zerami lub wartością, która poprzedza wartość brakującą
= df.fillna(0) # wypełnia zerami
df_cleaned_zeros = df.fillna(method='bfill', axis=0).fillna(0) # wypełnia wartością poprzedzającą z danej kolumny, jeśli to niemożliwe, wstawia 0 df_cleaned_bfill
- Zastanów się kiedy takie podejście może być stosowane, czy można je użyć do klasyfikacji?, sprawdź w dokumentacji fillna jakie są jeszcze możliwości wypełnienia wypełnienia?
- Kiedy wypełnianie wartością sąsiednią ma sens? Jeśli stosujemy je do klasyfikacji to jaką strategię przyjąć w odniesieniu do brakujących wartości referencyjnych (jest to wartość, która ma być predykowana przez klasyfikator) a jaką w odniesieniu do brakujących cech (wartość/wartości, które są wejściem klasyfikatora)?
Podejście 3: podstawienie wartości średniej/mediany/mody:
from sklearn.impute import SimpleImputer
= SimpleImputer(missing_values=np.nan, strategy='mean')
imp_mean = train_df.select_dtypes(include=[np.number]).copy()
df_train_numeric = test_df.select_dtypes(include=[np.number]).copy()#wybór tylko kolumn przechowujacych liczby, należy wykonać kopię obiektu
df_test_numeric
= imp_mean.fit_transform(df_train_numeric) # dopasowanie parametrów (średnich) i transformacja zbioru uczącego
df_train_numeric.loc[:] = imp_mean.transform(df_test_numeric) # zastosowanie modelu do transformacji zbioru testowego (bez wyznaczania parametrów) df_test_numeric[:]
- Kiedy wypełnianie wartością średnią/medianą ma sens? Jeśli stosujemy je do klasyfikacji to jaką strategię przyjąć w odniesieniu do brakujących wartości referencyjnych (jest to wartość, która ma być predykowana przez klasyfikator) a jaką w odniesieniu do brakujących cech (wartość/wartości, które są wejściem klasyfikatora)?
- Oceń skuteczność klasyfikacji i porównaj ją z pozostałymi podejściami
Konwersja danych
Daty i czasy
Konwersja dat i czasów
Dane bardzo często związane są z datą/czasem wystąpienia, rejestracji itp. Przykładowo, używany zbiór danych w kolumnie Date
przechowuje datę sprzedaży. Ponieważ zawiera ona znaki inne niż cyfry/punkt dziesiętny zaczytywana jest domyślnie z pliku CSV jako string:
print(type(df.loc[0, "Date"]))
W celu dalszego wygodnego wykorzystania takie dane wymagają konwersji na format zrozumiały dla wykorzystywanych narzędzi. W Pandas mamy do dyspozycji funkcję to_datetime()
pozwalającą na utworzenie serii/indeksu typu Datetime
na podstawie źródłowych liczb, napisów etc:
"Datetime"] = pd.to_datetime(df.loc[:, "Date"]) df.loc[:,
Mnogość formatów zapisu dat i czasów (np. DD-MM-YYYY lub MM-DD-YYYY) powoduje jednak, że funkcja to_datetime
może niepoprawnie odgadnąć format wejściowy. Porównaj uzyskane kolumny Date
i Datetime
- czy dane wejściowe były zawsze interpretowane tak samo?
Funkcja to_datetime
ma wiele dodatkowych opcji: to_datetime. Spróbuj za pommocą parametru format=
wymusić poprawny format źródłowy daty.
Wyznaczanie zakresów dat i interwałów
Serię dat z określonym początkiem, końcem i okresem można wygenerować funkcją date_range
: dokumentacja
Zakres (interwał) dat, który może służyć do wyłuskania fragmentu tabeli można wygenerować funkcją Interval
, podając jako granice Timestamp
:
Przykładowo:
= pd.Interval(pd.Timestamp('2017-01-01 00:00:00'), pd.Timestamp('2018-01-01 00:00:00'), closed='left') year_2017
Wyznaczanie dnia tygodnia
Pewne cechy wykazują zmienność nie wprost od upływu czasu (monotonicznie), co np. od dnia tygodnia, dnia miesiąca itp. Dysponując datą/czasem w formacie datetime łatwo skonwertujemy ją na dzień tygodnia w formacie liczbowym od 0 (poniedziałek) do 6 (niedziela) przy pomocy pola DataFrame.dt.dayofweek
.
"Day of week"] = df.loc[:, "Datetime"].dt.dayofweek df.loc[:,
🔥 Zadanie 🔥
Wykreśl histogram liczby dokonanych transakcji w zależności od dnia tygodnia.
Zmienne nominalne
Zmienne nominalne (categorical data) to takie, które przyjmują wartości z określonego, skończonego zbioru, dla których nie istnieje żadne domyślne uporządkowanie (np. miasto urodzenia, płeć). W przypadku programowania można posłużyć się analogią do typów wyliczeniowych (np. enum
z C++
). Zazwyczaj kategorii będzie znacznie mniej niż próbek danych.
Konwersja na zmienną nominalną
Dane typu categorical możemy wygenerować na kilka sposobów, np ręcznie, wymuszając typ danych category
parametrem dtype
:
= pd.Series(["a", "b", "c", "a"], dtype="category")
categorical_series print(categorical_series)
lub konwertując istniejącą kolumnę DataFrame:
"B"] = df["A"].astype("category") df[
Dla omawianej bazy sprzedaży nieruchomości możemy przykładowo skonwertować kolumnę RegionName
:
"RegionName"] = df.loc[:, "RegionName"].astype("category")
df.loc[:, print(df["Regionname"])
Łączenie zmiennych nominalnych (usuwanie literówek) przy pomocy fuzzywuzzy
W przypadku ręcznego wprowadzania danych np. przez osoby ankietowane lub przez różne instytucje, dane nominalne różnią się wielkością liter, sposobem zapisu (ze znakami diakrytycznymi lub bez) lub zawierają literówki. W przypadku nazw miejsc nazwy mogą posiadać lub nie dodatkowe człony (np. Ostrów i Ostrów Wlkp i Ostrow Wielkoposlki, jak również ostrow wlkp). Wszystkie takie wpisy powinny trafić do jednej kategorii.
W przypadku zmiany różnicy w wielkości liter możliwe jest konwersja wszystkich elementów w kolumnie na np. małe litery oraz usunięcie znaków spacji. Sprawdź (używając np.unique(...)
) ile różnych unikalnych elementów w kolumnie Suburb
? Porównaj ten wynik z wynikiem otrzymanym po znormalizowaniu wielkości liter oraz usunięciu końcowych znaków spacji
# zmiana na małe litery
'Suburb'] = df['Suburb'].str.lower()
df[# usunięcie końcowych spacji
'Suburb'] = df['Suburb'].str.strip() df[
W danych mogą się jednak znajdować takie same elementy różniące się literą (literówka) lub posiadające dodatkowe człony w nazwie. Do porównania dwóch napisów, lub napisu z listą innych napisów można użyć moduł fuzzywuzzy
import fuzzywuzzy.process
'Ostrów',['ostrow', 'Ostrów Wlkp', 'ostrów wlkp', 'Ostrzeszów']) fuzzywuzzy.process.extract(
Funkcja zwróci listę krotek, gdzie drugi element określa podobieństwo. Do scalenia pewnego ciągu znaków z elementami kolumny pandas, może służyć funkcja:
import fuzzywuzzy.process
def replace_matches_in_column(df, column, string_to_match, min_ratio = 90):
# get a list of unique strings
= df[column].unique()
strings
# get the top 10 closest matches to our input string
= fuzzywuzzy.process.extract(string_to_match, strings,
matches =10, scorer=fuzzywuzzy.fuzz.token_sort_ratio)
limit
# only get matches with a ratio > 90
= [matches[0] for matches in matches if matches[1] >= min_ratio]
close_matches
# get the rows of all the close matches in our dataframe
= df[column].isin(close_matches)
rows_with_matches
# replace all rows with close matches with the input matches
= string_to_match df.loc[rows_with_matches, column]
🔥 Zadanie 🔥
Podmień wczytywany plik na melb_data_distorted.csv, w którym w niektórych kolumnach tekstowych zostały wprowadzone typowe pomyłki lub różnice w zapisie.
Spróbuj zastosować funkcję replace_matches_in_column
do scalenia elementów w kolumnie Suburb
, pamiętaj, że trzeba ją wywołać osobno dla każdego unikalnego elementu string_to_match
. Ile unikalnych elementów zostanie, jeśli minimalny próg podobieństwa ustalisz na wartość 90?
Konwersja zmienna porządkowa → wartości liczbowe
Wykorzystanie zmiennych jako wejścia w systemach klasyfikacji/regresji wymaga podania wartości liczbowej. Jednym z podejść, które można zastosować jest przypisanie poszczególnym wartościom zmiennej nominalnej specyficznej wartości (np. 1, 2, 3, ...)
from sklearn.preprocessing import LabelEncoder
# Make copy to avoid changing original data
= train_df.copy()
label_train = test_df.copy()
label_test
# Apply label encoder to each column with categorical data
= LabelEncoder()
label_encoder ='CouncilArea'
col= label_encoder.fit_transform(label_train[col])
label_train[col] = label_encoder.transform(label_test[col]) label_test[col]
- ❓Zastanów się czy podejście to sprawdzi się w celu uwzględnienia w predyktorze ceny nazw obszarów administracyjnych, lub dni tygodnia w których nastąpiła sprzedaż?
- ❓Zastanów się czy podejście takie sprawdzi się do klasyfikacji zmiennej nominalnej siła wiatru, gdzie zbiorem wartości jest [brak, słaby, silny, bardzo silny]?
Zmienna nominalna → enkoder binarny
Innym możliwym podejściem jest konwersja zmiennej nominalnej o n
wartościach na n
kolumn, z których każda określa wartością 0 lub 1 to czy dana wartość wystąpiła. Procedura ta nazwya się również one-hot encoder. Porównanie sposobu transformacji za pomocą LabelEncodera
i LabelBinarizer
/(połączenia LabelEncoder
i OneHotEncoder
) przedstawiono na rysunku:
from sklearn.preprocessing import LabelBinarizer
# Make copy to avoid changing original data
= train_df.copy()
label_train = test_df.copy()
label_test
= LabelBinarizer()
label_binarizer
= 'CouncilArea'
col = label_binarizer.fit_transform(label_train[col])
lb_results = pd.DataFrame(lb_results, columns=label_binarizer.classes_) lb_results_df
- ❓Zastanów się czy podejście to sprawdzi się w celu uwzględnienia w predyktorze ceny nazw obszarów administracyjnych, lub dni tygodnia w których nastąpiła sprzedaż?
- ❓Zastanów się czy podejście takie sprawdzi się do klasyfikacji zmiennej nominalnej siła wiatru, gdzie zbiorem wartości jest [brak, słaby, silny, bardzo silny]?
Autorzy: Piotr Kaczmarek i Jakub Tomczyński