uczenie-maszynowe/lab/03_Reprezentacja_danych.ipynb
2022-11-03 15:37:40 +01:00

26 KiB
Raw Blame History

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 czy niski parter można z powodzeniem potraktować jako piętro „zerowe” i zamienić na 0.
  • 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ż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)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [14], line 1
----> 1 alldata_2 = pandas.get_dummies(alldata_2, columns=["Typ zabudowy", "Materiał budynku"])
      3 print(alldata_2)

NameError: 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 TFIDF, 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.