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 1. Funkcja score_dataset
zwraca średnią z wartości bezwzględnej błędu dla zbioru testowego. 2. Do oceny skuteczności stosujemy zbiór testowy, który nie został użyty w procedurze uczenia i wyboru metody. 3. 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. 4. 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