Skip to content

orcahmlee/encoding-categorical-values-in-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

資料科學專案中的資料集(Data set)通常會擁有非常多樣性的數據型態(Data type),數字、文字、空值、NaN...etc.。這些數據型態又可分為數值型變數(Numeric)與類別型變數(Categorical),數值型變數分為連續型(Continuous)與分散型(Discrete),類別型變數分為順序型(Ordinal)與名義型(Nominal)。:confused::confused::confused:

Numeric Categorical
Continuous Discrete Ordinal Nominal
  • Continuous: 連續的數值,如時間、高度、溫度、壓力、年齡...etc.。
  • Discrete: 總數的概念,如1台車、2間分店、3個小孩...etc.。
  • Ordinal: 雖然不是數值,但是邏輯上具有順序性或大小區別,如成績(A、B、C、F)、衣服尺寸(大、中、小)...etc.。
  • Nominal: 性別、顏色、區域、國家...etc.。

雖然許多機器學習演算法不需要經過特別處理就可以直接使用 Categorical variables,但是仍然有許多演算法不支援,因此處理 Categorical values 的技巧也是需要學習的(不然我幹嘛寫這篇😉)。

在資料科學與開源的世界中並不會只有一種答案,有很多方法可以處理這件事。不同處理方法有可能會對分析結果造成潛在的影響,因此必須瞭解為什麼要選擇某種方法,而不是一昧的使用 API。

Data Set & Data Preparation

資料集取自於參考資料中,資料集取得後通常無法直接使用,一般都要進行 ETL(Extract,Transform and Load)。

import pandas as pd
import numpy as np

df = pd.read_csv('http://mlr.cs.umass.edu/ml/machine-learning-databases/autos/imports-85.data')
df.head()

原始資料中的各欄沒有標題,且有些欄位為問號 '?'。

3 ? alfa-romero gas std two convertible rwd front 88.60 ... 130 mpfi 3.47 2.68 9.00 111 5000 21 27 13495
0 3 ? alfa-romero gas std two convertible rwd front 88.6 ... 130 mpfi 3.47 2.68 9.0 111 5000 21 27 16500
1 1 ? alfa-romero gas std two hatchback rwd front 94.5 ... 152 mpfi 2.68 3.47 9.0 154 5000 19 26 16500
2 2 164 audi gas std four sedan fwd front 99.8 ... 109 mpfi 3.19 3.40 10.0 102 5500 24 30 13950
3 2 164 audi gas std four sedan 4wd front 99.4 ... 136 mpfi 3.19 3.40 8.0 115 5500 18 22 17450
4 2 ? audi gas std two sedan fwd front 99.8 ... 136 mpfi 3.19 3.40 8.5 110 5500 19 25 15250

5 rows × 26 columns

由於原始資料的各欄位(Column)並無表頭,因此需先自訂表頭(Header),並同時將問號('?')轉換成 NaN

headers = ['symboling', 'normalized_losses', 'make', 'fuel_type', 'aspiration',
           'num_doors', 'body_style', 'drive_wheels', 'engine_location',
           'wheel_base', 'length', 'width', 'height', 'curb_weight',
           'engine_type', 'num_cylinders', 'engine_size', 'fuel_system',
           'bore', 'stroke', 'compression_ratio', 'horsepower', 'peak_rpm',
           'city_mpg', 'highway_mpg', 'price']

df = pd.read_csv('http://mlr.cs.umass.edu/ml/machine-learning-databases/autos/imports-85.data',
                  header=None, names=headers, na_values='?')
df.head()

如此讀進來的 Dataframe 就會像一般常見的試算表,且問號的部分也轉換成 NaN(Not a Number)。

symboling normalized_losses make fuel_type aspiration num_doors body_style drive_wheels engine_location wheel_base ... engine_size fuel_system bore stroke compression_ratio horsepower peak_rpm city_mpg highway_mpg price
0 3 NaN alfa-romero gas std two convertible rwd front 88.6 ... 130 mpfi 3.47 2.68 9.0 111.0 5000.0 21 27 13495.0
1 3 NaN alfa-romero gas std two convertible rwd front 88.6 ... 130 mpfi 3.47 2.68 9.0 111.0 5000.0 21 27 16500.0
2 1 NaN alfa-romero gas std two hatchback rwd front 94.5 ... 152 mpfi 2.68 3.47 9.0 154.0 5000.0 19 26 16500.0
3 2 164.0 audi gas std four sedan fwd front 99.8 ... 109 mpfi 3.19 3.40 10.0 102.0 5500.0 24 30 13950.0
4 2 164.0 audi gas std four sedan 4wd front 99.4 ... 136 mpfi 3.19 3.40 8.0 115.0 5500.0 18 22 17450.0

5 rows × 26 columns

由於本文只會練習 encoding,因此只取需要的 columns 即可。

obj_df = df.select_dtypes(include=['object']).copy()
obj_df = obj_df[['num_doors', 'body_style', 'drive_wheels']]
obj_df.head()
num_doors body_style drive_wheels
0 two convertible rwd
1 two convertible rwd
2 two hatchback rwd
3 four sedan fwd
4 four sedan 4wd

觀察一下各個 column 的資料型態,可以發現原始資料都是 object

obj_df.dtypes
num_doors       object
body_style      object
drive_wheels    object
dtype: object

接著檢查原始資料中是否有 NaN; 這裡可以利用 DataFrame.isnull() 檢查 Dataframe 中是否有欄位的值為 NaN

.any():只要一個欄位為 True 即為 True
.all():必須全部欄位為 True 才為 True
.any(axis=1):依照 row direction 的方向
.any(axis=0):依照 column direction 的方向

e.g. DataFrame.isnull().any(axis=1) 依照 row driection 去檢查是否有 True 值,只要該 row 有一個欄位為 True,將判定整個 row 的為 True,最後將有 NaN 的 row 取出,便是以下結果。

obj_df[obj_df.isnull().any(axis=1)]
num_doors body_style drive_wheels
27 NaN sedan fwd
63 NaN sedan fwd

利用 Series.value_counts() 顯示各種值的總數

Series.size: 該 Series 的大小,包含 NaN
Series.count(): 該 Series 中所有值的欄位總數,不包含 NaN
Series.value_counts(): 該 Series 中,計算各種值的總數,不包含 NaN

obj_df['num_doors'].value_counts()
four    114
two      89
Name: num_doors, dtype: int64

再利用 DataFrame.fillna() 將資料結構中的 NaN 替換成自己想要的值;這裡將 Dataframe 中的 num_doors 欄中的 NaN 替換成 four

obj_df = obj_df.fillna(value={'num_doors': 'four'})
obj_df['num_doors'].value_counts()
four    116
two      89
Name: num_doors, dtype: int64

再次檢查後發現,four 這個值的數量從 114 變成 116,表示原本的兩個 NaN 已經被我成功替換成我想要的值。

前面玩了這麼久也只搞定了各欄位的表頭和遺失值而已...

Methods

經過前面的數據前處理後,我們終於有個可以乾淨的資料結構可以練習了~:metal: 下面將介紹三種方式來處理這個資料:

1. Find and Replace
2. Label Encoding
3. One Hot Encoding

1. Find & Replace

不要懷疑,你沒看錯,就是文書處理軟體最常見的方法。 最適合的方法就是最好的方法!

子曰:「割雞焉用牛刀」

num_doors 的值其實就是數值,只是機器看不懂,因此這裡的動作就是將 String 轉換成相對應的 Integer,如:

one -> 1
-> 2
-> 3

使用 DataFrame.replace() 並傳入一個對照表 to_replace,即可將文字轉換成數字。

to_replace: 可傳入 str、regex、list、dict、Series、numeric,這裡用 dict 建立對照表。
inplace: 表示直接在原始的資料結構中將值更換掉,這樣就不用再 assign 一個物件,但是要注意的是原始資料將會消失。

to_replace = {'num_doors': {'four': 4, 'two': 2}}
obj_df.replace(to_replace=to_replace, inplace=True)
obj_df.head()
num_doors body_style drive_wheels
0 2 convertible rwd
1 2 convertible rwd
2 2 hatchback rwd
3 4 sedan fwd
4 4 sedan 4wd
obj_df.dtypes
num_doors        int64
body_style      object
drive_wheels    object
dtype: object

再次檢查資料,可以發現文字都成功轉換成數字,而且 num_doors 的資料型態也從 object 變成 int64

2. Label Encoding

另一個轉換類別型變數(Categorical variables)的方法稱之為 Label Encoding,Label Encoding 可以很輕鬆地把文字轉換成數字,如:

  • Ordinal variables

    • 小 --> 0
    • 中 --> 1
    • 大 --> 2
  • Nominal variables

    • 台北市 --> 0
    • 新北市 --> 1
    • 桃園市 --> 2
    • 台中市 --> 3
    • 台南市 --> 4
    • 高雄市 --> 5

從 Ordinal 來看:

  • 大 > 中 > 小 --> 2 > 1 > 0
  • 例如三種尺寸的蘋果各買一顆,平均起來每顆蘋果差不多大;$\frac{0 + 1 + 2}{3} = 1$;

以上看起來都蠻合理。

從 Nominal 來看:

  • 台北市 > 新北市 > 桃園市 > 台中市 > 台南市 > 高雄市 --> 5 > 4 > 3 > 2 > 1 > 0
  • 北部優於南部:unamused:? 六都的平均值 = 2.5,嗯...這是新竹嗎:expressionless:?

以上看起來就有點怪怪的了呦(台中南波萬:thumbsup: 我沒有要戰南北),所以 Nominal variable 就比較適合 One Hot Encoding 的方法。

以下保留,需再確認!
某些文件表示,若是使用 tree-based algorithm 時,這些數字只是類別代號,並無大小順序之別,因此可以用 Label encoding。ref1 ref2

如此轉換後這些資料就可以丟到演算法裡面產生模型,下面介紹兩個好用的技巧。

2-1. pandas

pandas 這一個萬用的瑞士刀 package 中有一個 CategoricalclassCategorical 物件可以直接建構,也可以從既有的 Series 中轉換資料型態。

a1 = obj_df['body_style'] # a1: pandas.Series
print('ori data type(a1):', a1.dtypes)
a2 = obj_df['body_style'].astype('category') # a2: pandas.Series
print('new data type(a2):', a2.dtypes)
ori data type(a1): object
new data type(a2): category

Seriesdtype 轉換成 category 時,才可以使用 Series.cat.codes 將原本的文字轉換成數字。

# a1 為 object; a1.cat.codes 不可行
# a2 為 category; 
pd_cat = a2.cat.codes # convert to int from string
print('pd_cat:', pd_cat.tolist()[:5]) # transform the to list from Seires and shows the first 5 values
pd_cat: [0, 0, 2, 3, 3]

若想知道原本的文字被轉換成哪個數字的話,可用 Series.cat.categories 查詢。

# pd_cat 所相對應的類別
print(a2.cat.categories)
Index(['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon'], dtype='object')

為了方便閱讀,可以將利用 enumerate 將上面的 Index 丟到 dictionary 中。

# 將 pd_cat 所相對應的類別轉換成 dictionary,方便閱讀
a2_classes = dict(enumerate(a2.cat.categories))
print(a2_classes)
{0: 'convertible', 1: 'hardtop', 2: 'hatchback', 3: 'sedan', 4: 'wagon'}

2-2. scikit-learn

另一個方法是利用 scikit-learn 中的 sklearn.preprocessing.LabelEncoder;而且使用 LabelEncoder 時,可直接使用 objectSeries,並不需要事先轉換成 categorical

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder() # 建構物件
sk_cat = le.fit_transform(a1) # 直接將資料傳入
print('sk_cat:', sk_cat.tolist()[0:5])
sk_cat: [0, 0, 2, 3, 3]

比較一下兩個方法所轉換的結果,可以發現是一模模一樣樣的。

print('pandas :', pd_cat.tolist()[0:10])
print('sklearn:', sk_cat.tolist()[0:10])
pandas : [0, 0, 2, 3, 3, 3, 3, 4, 3, 2]
sklearn: [0, 0, 2, 3, 3, 3, 3, 4, 3, 2]

3. One Hot Encoding

Label Encoding 可以很輕鬆且直覺地轉換類別變數,但是它卻有容易讓人誤解的缺點,如先前提到的六都:

  • 台北市 --> 0
  • 新北市 --> 1
  • 桃園市 --> 2
  • 台中市 --> 3
  • 台南市 --> 4
  • 高雄市 --> 5

高雄市比台北市大?台南市是桃園市的兩倍? I don't think so.

因此還有另一種常見的編碼方法稱為 One Hot Encoding,這種方法會依照各個類別另外產生新的欄(column),並且重新指派 1/0(True/False)。 例如,利用原本居住城市欄的值,另外新增五個欄台北市新北市桃園市台中市台南市高雄市,並且依照原本的值重新指派為 1/0,如下表:

同學 居住城市 台北市 新北市 桃園市 台中市 台南市 高雄市
1號同學 台北市 1 0 0 0 0 0
2號同學 新北市 0 1 0 0 0 0
3號同學 桃園市 0 0 1 0 0 0
4號同學 台中市 0 0 0 1 0 0
5號同學 台南市 0 0 0 0 1 0
6號同學 高雄市 0 0 0 0 0 1

如此一來就不會有前述的問題。但是使用 One Hot Encoding 所產生的另一項缺點則是特徵(Features)的數量就會變得很大,在這裡的居住城市只有 1 個特徵,但是使用 One Hot Encoding 之後的特徵數量就變成至少 6 個。

一般而言,當特徵數量變多,可使用 PCA 來降低維度,因此 PCA + One Hot Encoding 的搭配組合很常見。

3-1. pandas

我們在這裡又看到了萬用的瑞士刀 packagepandas 很貼心的提供一個方法 pandas.get_dummies

先將需要的欄取出

b = obj_df[['body_style', 'drive_wheels']]
b.head()
body_style drive_wheels
0 convertible rwd
1 convertible rwd
2 hatchback rwd
3 sedan fwd
4 sedan 4wd

接著就可以直接使用 get_dummies(),並且把欲轉換的資料傳入即可。

pd.get_dummies(b, columns=['drive_wheels']).head()
body_style drive_wheels_4wd drive_wheels_fwd drive_wheels_rwd
0 convertible 0 0 1
1 convertible 0 0 1
2 hatchback 0 0 1
3 sedan 0 1 0
4 sedan 1 0 0

轉換完後可以看到三個新增欄分別是

  • drive_wheels_4wd
  • drive_wheels_rwd
  • drive_wheels_fwd

如果你覺得這個欄位名稱又臭又長,當然也可以隨客官喜好來設定。

pd.get_dummies(c, columns=['body_style', 'drive_wheels'], prefix=['body', 'drive']).head()
body_convertible body_hardtop body_hatchback body_sedan body_wagon drive_4wd drive_fwd drive_rwd
0 1 0 0 0 0 0 0 1
1 1 0 0 0 0 0 0 1
2 0 0 1 0 0 0 0 1
3 0 0 0 1 0 0 1 0
4 0 0 0 1 0 1 0 0

3-2. scikit-learn

既然萬能的瑞士刀可以辦到,那麼另外一個超強 package scikit-learn 應該也有相關的方法囉。 下面介紹 sklearn.preprocessing.LabelBinarizer

from sklearn.preprocessing import LabelBinarizer

lb_style = LabelBinarizer() # 建構物件
lb_results = lb_style.fit_transform(b['body_style']) # 直接將資料傳入
lb_results

轉換後的結果會是一個陣列

array([[1, 0, 0, 0, 0],
	   [1, 0, 0, 0, 0],
	   [0, 0, 1, 0, 0],
	   ..., 
	   [0, 0, 0, 1, 0],
	   [0, 0, 0, 1, 0],
	   [0, 0, 0, 1, 0]])

使用先前所建構的物件的屬性,就可以看到各欄的表頭為何。

lb_style.classes_
array(['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon'],
          dtype='<U11')

由於這樣不太好懂,為了方便閱讀,我利用轉換後的資料建立一個 DataFrame

pd.DataFrame(lb_results, columns=lb_style.classes_).head()
convertible hardtop hatchback sedan wagon
0 1 0 0 0 0
1 1 0 0 0 0
2 0 0 1 0 0
3 0 0 0 1 0
4 0 0 0 1 0

Conclusion

Encoding 在資料處理的過程中是一個很重要的程序,由於有很多的方法可以達到目的,因此更應該瞭解不同方法的處理原理以及它如何影響後續的模型。由於 Python 的社群中有許多好用的方法可以處理 Encoding 的問題,至於要選擇哪個方法就端看各位客倌的選擇。

Reference

Lincense

The MIT License

It's Me

GitHub LinkedIn

About

Encoding Categorical Values in Python

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published