Because neither sklearn nor Pandas provide a straightforward and complete one-hot encoder, I decided to write one myself. Both Pandas and sklearn do have an encoder with no option to decode, and the sklearn.LabelEncoder
that has the decoding only produces that, labels.
Here's the class:
import numpy as np
class OneHotEncoder:
def __init__(self):
self.unq = np.array([])
self.n_features = len(self.unq)
def set_unq(self, unq):
self.unq = unq
self.n_features = len(unq)
@staticmethod
def _assure(cond, msg):
if not cond:
raise ValueError(msg)
def fit_transform(self, np_arr):
"""
From categories to one-hot encoding. Calculate unique occurences.
:param np_arr: categorical data of shape (samples, 1)
:return: one-hot encoding with shape (sample, categories)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
unq, idx = np.unique(np_arr, return_inverse=True)
self.set_unq(unq)
arr = np.zeros((len(idx), len(self.unq)))
arr[range(len(idx)), idx] = 1
return arr
def transform(self, np_arr):
"""
From categories to one-hot encoding based on previous samples.
:param np_arr: categorical data of shape (samples, 1)
:return: one-hot encoding with shape (sample, categories)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
arr = np.argwhere(self.unq == np_arr)[:, 1]
zr = np.zeros((len(arr), len(self.unq)))
zr[range(len(arr)), arr] = 1
return zr
def fit_transform_to_labels(self, np_arr):
"""
From categories to label values. Calculate unique occurences.
:param np_arr: categorical data of shape (samples, 1)
:return: label values with shape (sample, 1)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
unq, idx = np.unique(np_arr, return_inverse=True)
self.set_unq(unq)
return idx.reshape(-1, 1)
def transform_to_labels(self, np_arr):
"""
From categories to label values based on previous samples.
:param np_arr: categorical data of shape (samples, 1)
:return: label values with shape (sample, 1)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
arr = np.argwhere(self.unq == np_arr)
return arr[:, 1:2]
def transform_from_labels(self, np_arr):
"""
From label values to one-hot encoding.
:param np_arr: label values of shape (samples, 1)
:return: one-hot encoding with shape (samples, categories)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
arr = np.zeros((len(np_arr), len(self.unq)))
arr[range(len(arr)), np_arr.reshape(-1)] = 1
return arr
def inverse_from_labels(self, np_arr):
"""
From label values to original categorical values.
:param np_arr: label values of shape (samples, 1)
:return: original categorical values with shape (samples, 1)
"""
self._assure(np_arr.shape[-1] == 1, 'Last axis must be length 1.')
return self.unq[np_arr]
def inverse_to_lables(self, np_arr):
"""
From one-hot encoding to label values.
:param np_arr: one-hot encoding of shape (samples, categories)
:return: label values with shape (samples, 1)
"""
self._assure(np_arr.shape[-1] == len(self.unq), 'Inverting array must be same length as available labels.')
return np.argmax(np_arr, axis=-1).reshape(-1, 1)
def inverse(self, np_arr):
"""
From one-hot encoding to original categorical values.
:param np_arr: one-hot encoding of shape (samples, categories)
:return: original categorical values with shape (samples, 1)
"""
self._assure(np_arr.shape[-1] == len(self.unq), 'Inverting array must be same length as available labels.')
return self.inverse_from_labels(np.argmax(np_arr, axis=-1).reshape(-1, 1))
So in short, this class combines the functionality of sklearn.LabelEncoder
and sklearn.OneHotEncoder
. The assertions are a bit redundant, I just like to keep my vectors as column vectors.
This class does work.
- Is it missing something in terms of functionality or safety?
- Could it be expanded to some different cases I haven't yet taken into account?
Here's a small snippet of using the class:
a = np.array([1,2,3,4,3,2,1]).reshape(-1, 1)
oh = OneHotEncoder()
labs = oh.fit_transform_to_labels(a)
encoded = oh.transform_from_labels(labs)
decoded = oh.inverse(encoded)