26 KiB
Uczenie maszynowe — laboratoria
3. Reprezentacja danych
Na tych zajęciach dowiemy się, w jaki sposób reprezentować różnego rodzaju dane tak, żeby można było używać ich do uczenia maszynowego.
Plik _mieszkania4.tsv zawiera dane wydobyte z serwisu gratka.pl dotyczące cen mieszkań w Poznaniu.
# Wczytanie danych (mieszkania) przy pomocy biblioteki pandas
import numpy as np
import pandas as pd
alldata = pd.read_csv(
"mieszkania4.tsv",
header=0,
sep="\t",
usecols=[
"cena",
"Powierzchnia w m2",
"Liczba pokoi",
"Garaż",
"Liczba pięter w budynku",
"Piętro",
"Typ zabudowy",
"Materiał budynku",
"Rok budowy",
"opis",
],
)
print(alldata[:5])
cena Powierzchnia w m2 Liczba pokoi Garaż Liczba pięter w budynku \ 0 290386 46 2 False 5.0 1 450000 59 2 False 3.0 2 375000 79 3 False 16.0 3 400000 63 3 True 2.0 4 389285 59 3 False 13.0 Piętro Typ zabudowy Materiał budynku Rok budowy \ 0 parter apartamentowiec cegła 2017.0 1 2 kamienica cegła 1902.0 2 5 blok płyta 1990.0 3 2 blok cegła 2009.0 4 12 blok NaN NaN opis 0 Polecam mieszkanie 2 pokojowe o metrażu 46,68 ... 1 Ekskluzywna oferta - tylko u nas! Projekt arch... 2 Polecam do kupna przestronne mieszkanie trzypo... 3 Dla rodziny albo pod wynajem. Świetna lokaliza... 4 NaN
Jak widać powyżej, w pliku _mieszkania4.tsv znajdują się dane różnych typów:
- dane numeryczne (po prostu liczby):
- cena
- powierzchnia w m2
- liczba pokoi
- dane częściowo numeryczne (liczby oraz wartości specjalne):
- liczba pięter w budynku
- piętro
- rok budowy
- dane boole'owskie (prawda/fałsz):
- garaż
- dane kategoryczne (wybór jednej z kilku kategorii):
- typ zabudowy
- dane tekstowe (dowolny tekst):
- opis
Algorytmy uczenia maszynowego działają na danych liczbowych. Z tego powodu musimy znaleźć właściwy sposób reprezentowania pozostałych danych.
Dane numeryczne
Dane numeryczne to takie, które są liczbami. W większości przypadków możemy na nich operować bezpośrednio. Przykładem takich danych jest kolumna _Powierzchnia w m2 z powyższego przykładu:
print(pd.unique(alldata["Powierzchnia w m2"]))
# (funkcja `pandas.unique` służy do pomijania duplikatów wartości)
[ 46 59 79 63 90 66 32 38 68 43 185 64 165 71 73 51 70 48 42 33 203 88 41 31 45 62 60 295 53 84 170 56 47 228 44 67 49 37 87 36 55 57 118 65 30 28 230 54 52 95 50 26 171 282 77 40 150 300 39 145 370 140 225 29 61 135 27 270 177 85 92 132 75 200 74 219 220 96 235 20 153 318 104 58 72 117 189 81 111 35 280 141 195 120 250 97 154 114 76 287 34 180 160 176 148 98 217 86 260 198 78 183 80 163 82 100 156 320 89 103 159 125 340 149 175 237 110 182 186 106 233 197 136 162 157 240 211 83 196 69 102 91 108 130 510 143 1200 178 226 190 151 138 161 142 683 146 94 109 263 112 855 376 218 113 215 264 139 129 167 600 24 174 296 315 232 298 330 93 301 127 290 275 375 124 252 173 158 25 269 128 192 155 99 126 147 288 119 206 105 224 346 339 204 1100 392 243 101 18 202 205 107 199 137 134 144 216 172 239 116 364 121 23 267 369 11930 122 400 209 210 268 500 123 245 15 22 335 262 438 307 184 354 249 431 214 164 328 800 16 229 152 650 241 187 276 297 443 353 360 350 213 19 265]
Czasami w danej kolumnie oprócz liczb występują również inne wartości. Przykładem takiej cechy może być _Piętro:
print(pd.unique(alldata["Piętro"]))
['parter' '2' '5' '12' '1' '3' nan '8' '4' '16' '7' '6' 'poddasze' '9' '11' '13' '14' '10' '15' 'niski parter']
Jak widać powyżej, tutaj oprócz liczb pojawiają się pewne tekstowe wartości specjalne, takie jak parter
, poddasze
czy niski parter
.
Takie wartości należy zamienić na liczby. Jak?
- Wydaje się, że
parter
czyniski parter
można z powodzeniem potraktować jako piętro „zerowe” i zamienić na0
. - Z poddaszem sytuacja nie jest już tak oczywista. Czy mają Państwo jakieś propozycje?
- Może zamienić
poddasze
na wartość NaN (zobacz poniżej)? - Może wykorzystać w tym celu wartość z sąsiedniej kolumny _Liczba pięter w budynku?
- Może zamienić
Można w tym celu wykorzystać funkcje apply i to_numeric z biblioteki pandas
.
print("Przed zamianą:")
print(alldata["Piętro"][122:128])
Przed zamianą: 122 1 123 2 124 poddasze 125 5 126 parter 127 3 Name: Piętro, dtype: object
# Zamiana wartości 'parter' i 'niski parter' w kolumnie 'Piętro' na 0.
alldata["Piętro"] = alldata["Piętro"].apply(
lambda x: 0 if x in ["parter", "niski parter"] else x
)
# Zamiana wszystkich wartości w kolumnie 'Piętro' na numeryczne.
# Parametr errors='coerce' powoduje, że napotkane nieliczbowe wartości będą zamieniane na NaN.
alldata["Piętro"] = alldata["Piętro"].apply(pd.to_numeric, errors="coerce")
print()
print("Po zamianie:")
print(alldata["Piętro"][122:128])
Po zamianie: 122 1.0 123 2.0 124 NaN 125 5.0 126 0.0 127 3.0 Name: Piętro, dtype: float64
Wartości NaN
Wartość NaN (zob. też na Wikipedii) – to wartość numeryczna oznaczająca „nie-liczbę”, „wartość niezdefiniowaną”, np. niezdefiniowany wynik działania lub brak danych:
print(np.sqrt(-1)) # niezdefiniowany wynik działania (pierwiastek z liczby ujemnej)
print(alldata["Piętro"][14]) # brak danych na temat piętra w rekordzie 14.
# Jak uzyskać wartość NaN?
print(float("NaN"))
print(np.nan)
nan nan nan nan
/tmp/ipykernel_11063/3804580172.py:1: RuntimeWarning: invalid value encountered in sqrt print(np.sqrt(-1)) # niezdefiniowany wynik działania (pierwiastek z liczby ujemnej)
Co można zrobić z wartością NaN?
- Czasami można wartość NaN zamienić na
0
, np. być może w kolumnie „przychód” wartość NaN oznacza brak przychodu. Należy jednak być z tym ostrożnym. W większości przypadków wstawienie 0 zamiast NaN będzie niepoprawne, np. „rok 0” to nie to samo co „rok nieznany”. Nawet w kolumnie „cena” wartość NaN raczej oznacza, że cena jest nieznana, a to przecież nie to samo, co „cena równa 0 zł”. - Najbezpieczniej jest usunąć cały rekord (wiersz), który zawiera jakąkolwiek wartość NaN. Należy przy tym pamiętać, że pozbywamy się w ten sposób (być może wartościowych) danych. Jest to istotne zwłaszcza wtedy, gdy nasze dane zawierają dużo wartości niezdefiniowanych.
- Wartość NaN można też zamienić na średnią, medianę, modę itp. z pozostałych wartości w zbiorze danych. To dobra opcja, jeżeli usunięcie całych wierszy zawierających NaN pozbawiłoby nas zbyt wielu rekordów.
- Można użyć też bardziej zaawansowanych technik, np. MICE czy KNN.
Przydatne artykuły na temat usuwania wartości niezdefiniowanych ze zbioru danych:
Biblioteka pandas
dostarcza narzędzi do automatycznego usuwania wartości NaN: dropna
print("Liczba rekordów przed usunięciem NaN:", len(alldata))
alldata = alldata.dropna() # usunięcie rekordów zawierających NaN
print("Liczba rekordów po usunięciu NaN:", len(alldata))
Liczba rekordów przed usunięciem NaN: 4938 Liczba rekordów po usunięciu NaN: 888
Dane boole'owskie
W przypadku danych typu prawda/fałsz, wystarczy zamienić wartości True
na 1
, a False
na 0
:
print("Przed zamianą:")
print(alldata["Garaż"])
Przed zamianą: 0 False 1 False 2 False 3 True 13 False ... 4909 False 4917 True 4918 False 4920 False 4937 False Name: Garaż, Length: 888, dtype: bool
alldata["Garaż"] = alldata["Garaż"].apply(lambda x: 1 if x == True else 0)
print()
print("Po zamianie:")
print(alldata["Garaż"])
Po zamianie: 0 0 1 0 2 0 3 1 13 0 .. 4909 0 4917 1 4918 0 4920 0 4937 0 Name: Garaż, Length: 888, dtype: int64
Dane kategoryczne
O danych kategorycznych mówimy, jeżeli dane mogą przyjmować wartości ze skończonej listy („kategorii”), np.:
# "Typ zabudowy" może przyjmować jedną z następujących wartości:
typ_zabudowy_values = list(pd.unique(alldata["Typ zabudowy"]))
print(typ_zabudowy_values)
['apartamentowiec', 'kamienica', 'blok', 'dom wielorodzinny/szeregowiec', 'plomba']
# "Materiał budynku" może przyjmować jedną z następujących wartości:
material_budynku_values = list(pd.unique(alldata["Materiał budynku"]))
print(material_budynku_values)
['cegła', 'płyta', 'inne', 'pustak', 'silikat', 'beton']
Cechę kategoryczną można rozbić na skończoną liczbę cech boole'owskich:
# Skopiujmy dane, żeby przedstawić 2 alternatywne rozwiązania
alldata_1 = alldata.copy()
alldata_2 = alldata.copy()
# Rozwiązanie 1
select_column_names = []
for typ_zabudowy in typ_zabudowy_values:
new_column_name = "Czy {}?".format(typ_zabudowy)
alldata_1[new_column_name] = alldata_1["Typ zabudowy"] == typ_zabudowy
select_column_names.append(new_column_name)
print("Nowo utworzone kolumny (cechy boole'owskie):")
print(select_column_names)
select_column_names = ["Typ zabudowy"] + select_column_names
print()
print(alldata_1[select_column_names])
Nowo utworzone kolumny (cechy boole'owskie): ['Czy apartamentowiec?', 'Czy kamienica?', 'Czy blok?', 'Czy dom wielorodzinny/szeregowiec?', 'Czy plomba?'] Typ zabudowy Czy apartamentowiec? Czy kamienica? \ 0 apartamentowiec True False 1 kamienica False True 2 blok False False 3 blok False False 13 blok False False ... ... ... ... 4909 apartamentowiec True False 4917 dom wielorodzinny/szeregowiec False False 4918 blok False False 4920 dom wielorodzinny/szeregowiec False False 4937 dom wielorodzinny/szeregowiec False False Czy blok? Czy dom wielorodzinny/szeregowiec? Czy plomba? 0 False False False 1 False False False 2 True False False 3 True False False 13 True False False ... ... ... ... 4909 False False False 4917 False True False 4918 True False False 4920 False True False 4937 False True False [888 rows x 6 columns]
Nie trzeba tego robić ręcznie. Można do tego celu użyć funkcji get_dummies z biblioteki pandas
:
alldata_2 = pd.get_dummies(alldata_2, columns=["Typ zabudowy", "Materiał budynku"])
print(alldata_2)
[0;31m---------------------------------------------------------------------------[0m [0;31mNameError[0m Traceback (most recent call last) Cell [0;32mIn [14], line 1[0m [0;32m----> 1[0m alldata_2 [39m=[39m pandas[39m.[39mget_dummies(alldata_2, columns[39m=[39m[[39m"[39m[39mTyp zabudowy[39m[39m"[39m, [39m"[39m[39mMateriał budynku[39m[39m"[39m]) [1;32m 3[0m [39mprint[39m(alldata_2) [0;31mNameError[0m: name 'pandas' is not defined
Zwróćmy uwagę, że dzięki użyciu get_dummies
nowe kolumny zostały utworzone i nazwane automatycznie, nie trzeba też już ręcznie konwertować wartości boole'owskich do numerycznych.
Funkcja get_dummies
do określenia, na ile i jakich kolumn podzielić daną kolumnę kategoryczną, używa bieżącej zawartości tabeli. Dlatego należy jej użyć przed dokonaniem podziału na zbiory uczący i testowy.
Więcej na ten temat można przeczytać w artykule How to use pandas.get_dummies with the test set.
Do preprocessingu danych kategorycznych można też użyć narzędzi z biblioteki _scikit-learn: https://scikit-learn.org/stable/modules/preprocessing.html#encoding-categorical-features
Dane tekstowe
Przetwarzanie danych tekstowych to szeroki temat, którym można zapełnić cały wykład. Dlatego tutaj przedstawię tylko najważniejsze metody.
Możemy na przykład tworzyć cechy sprawdzające występowanie poszczególnych wyrazów lub ciągów znaków w tekście:
alldata["nowe_w_opisie"] = alldata["opis"].apply(
lambda x: True if "nowe" in x.lower() else False
)
print(alldata[["nowe_w_opisie", "opis"]])
nowe_w_opisie opis 0 True Polecam mieszkanie 2 pokojowe o metrażu 46,68 ... 1 False Ekskluzywna oferta - tylko u nas! Projekt arch... 2 False Polecam do kupna przestronne mieszkanie trzypo... 3 False Dla rodziny albo pod wynajem. Świetna lokaliza... 13 False Witam,Mam na imię Jędrzej i w biurze Platan po... ... ... ... 4909 True !!! Apartamenty w Lusówku oddawane w stanie de... 4917 False Sprzedam lokal mieszkalny odrębna własność w b... 4918 False Polecam do kupna mieszkanie 5 pokojowe o pow. ... 4920 True Przestronne mieszkanie z pięknym widokiem!Dwup... 4937 True Sprzedaż nowego mieszkania w FAŁKOWIE - Osiedl... [888 rows x 2 columns]
Można też zamienić tekst na wektory używając algorytmów TF–IDF, Word2Vec lub podobnych.
Ciekawy artykuł na temat przygotowywania danych tekstowych do uczenia maszynowego można znaleźć na przykład tutaj: https://machinelearningmastery.com/prepare-text-data-machine-learning-scikit-learn/
Zadanie 7
Na podstawie danych z pliku _flats.tsv wytrenuj model, który przewidzi cenę mieszkania na podstawie różnych jego cech. Wykorzystaj cechy różnych typów (numeryczne, boole'owskie, kategoryczne, tekstowe). Dokonaj odpowiedniego preprocessingu danych.
Zastanów się, jak poprawić wyniki uzyskane przez klasyfikator. Może przez stworzenie nowych cech pochodnych? Może przez odrzucenie mało wiarygodnych danych (obserwacji odstających)? Porównaj uzyskane wyniki z wynikami uzyskanymi w pierwszej części zadania.