Pythonで1つのホットエンコードを行うにはどうすればよいですか?


132

80%のカテゴリカル変数を使用した機械学習分類問題があります。分類に分類子を使用する場合、1つのホットエンコーディングを使用する必要がありますか?エンコードせずにデータを分類子に渡すことはできますか?

機能を選択するために次のことを実行しようとしています。

  1. 私は列車のファイルを読みました:

    num_rows_to_read = 10000
    train_small = pd.read_csv("../../dataset/train.csv",   nrows=num_rows_to_read)
    
  2. カテゴリー特徴のタイプを「カテゴリー」に変更します。

    non_categorial_features = ['orig_destination_distance',
                              'srch_adults_cnt',
                              'srch_children_cnt',
                              'srch_rm_cnt',
                              'cnt']
    
    for categorical_feature in list(train_small.columns):
        if categorical_feature not in non_categorial_features:
            train_small[categorical_feature] = train_small[categorical_feature].astype('category')
    
  3. 私は1つのホットエンコーディングを使用しています:

    train_small_with_dummies = pd.get_dummies(train_small, sparse=True)

問題は、強力なマシンを使用しているにもかかわらず、3番目の部分が頻繁にスタックすることです。

したがって、1つのホットエンコーディングがないと、機能の重要性を判断するために機能を選択できません。

何がお勧めですか?

回答:


159

アプローチ1:パンダデータフレームでget_dummiesを使用できます。

例1:

import pandas as pd
s = pd.Series(list('abca'))
pd.get_dummies(s)
Out[]: 
     a    b    c
0  1.0  0.0  0.0
1  0.0  1.0  0.0
2  0.0  0.0  1.0
3  1.0  0.0  0.0

例2:

以下は、指定された列を1つのホットに変換します。接頭辞を使用して複数のダミーを作成します。

import pandas as pd

df = pd.DataFrame({
          'A':['a','b','a'],
          'B':['b','a','c']
        })
df
Out[]: 
   A  B
0  a  b
1  b  a
2  a  c

# Get one hot encoding of columns B
one_hot = pd.get_dummies(df['B'])
# Drop column B as it is now encoded
df = df.drop('B',axis = 1)
# Join the encoded df
df = df.join(one_hot)
df  
Out[]: 
       A  a  b  c
    0  a  0  1  0
    1  b  1  0  0
    2  a  0  0  1

アプローチ2:Scikit-learnを使用する

3つの特徴と4つのサンプルを含むデータセットが与えられた場合、エンコーダーに特徴ごとの最大値を検出させ、データをバイナリワンホットエンコーディングに変換します。

>>> from sklearn.preprocessing import OneHotEncoder
>>> enc = OneHotEncoder()
>>> enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]])   
OneHotEncoder(categorical_features='all', dtype=<class 'numpy.float64'>,
   handle_unknown='error', n_values='auto', sparse=True)
>>> enc.n_values_
array([2, 3, 4])
>>> enc.feature_indices_
array([0, 2, 5, 9], dtype=int32)
>>> enc.transform([[0, 1, 1]]).toarray()
array([[ 1.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.]])

この例のリンクは次のとおりです。http//scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html


20
を設定するdrop_first=Trueget_dummies、元の列を個別に削除する必要がなくなります
OverflowingTheGlass

1
例2では、​​結合を使用せずに新しい列をデータフレームに結合する方法はありますか?非常に大きなデータセットを処理しているときに、それを実行しようとするとMemoryErrorが発生します。
J.Dahlgren

同じ行数のdf2がある場合、結合を使用せずに新しい列をデータフレームに追加できます。次のコマンドを使用してコピーできます:df [“ newColname”] = df2 [“ col”]
Sayali Sonawane

1
例2の画像を使用するのは
悪かった

9
@ OverflowingTheGlass-drop-first = Trueを指定しても、元の列は削除されません。これは、カテゴリ機能の最初のレベルを削除するため、k列ではなくk-1列になります。kはカテゴリ機能のカーディナリティです。
Garima Jain

42

基本的なワンホットエンコーディングにPandasを使用する方がはるかに簡単です。あなたがより多くのオプションを探しているなら、あなたは使うことができますscikit-learn

Pandasでの基本的なワンホットエンコーディングの場合は、データフレームをget_dummies関数に渡すだけです。

たとえば、imdb_moviesというデータフレームがある場合:

ここに画像の説明を入力してください

...そして評価された列をワンホットエンコードしたいのですが、これを行うだけです:

pd.get_dummies(imdb_movies.Rated)

ここに画像の説明を入力してください

これは、存在する評価のdataframeすべての「レベル」の列と、指定された観測に対するその評価の存在を指定する1または0のいずれかを含む新しいを返します。

通常、これを元のの一部にする必要がありdataframeます。この場合、「column-binding」を使用して、新しいダミーのコード化されたフレームを元のフレームに単にアタッチします。

Pandasのconcat関数を使用して列バインドできます。

rated_dummies = pd.get_dummies(imdb_movies.Rated)
pd.concat([imdb_movies, rated_dummies], axis=1)

ここに画像の説明を入力してください

これで、フルで分析を実行できますdataframe

シンプルなユーティリティ機能

これをすばやく行うためのユーティリティ関数にすることをお勧めします。

def encode_and_bind(original_dataframe, feature_to_encode):
    dummies = pd.get_dummies(original_dataframe[[feature_to_encode]])
    res = pd.concat([original_dataframe, dummies], axis=1)
    return(res)

使用法

encode_and_bind(imdb_movies, 'Rated')

結果

ここに画像の説明を入力してください

また、@ pmalbuコメントに従って、関数で元のfeature_to_encode削除する場合は、このバージョンを使用します。

def encode_and_bind(original_dataframe, feature_to_encode):
    dummies = pd.get_dummies(original_dataframe[[feature_to_encode]])
    res = pd.concat([original_dataframe, dummies], axis=1)
    res = res.drop([feature_to_encode], axis=1)
    return(res) 

次のように、同時に複数の機能をエンコードできます。

features_to_encode = ['feature_1', 'feature_2', 'feature_3',
                      'feature_4']
for feature in features_to_encode:
    res = encode_and_bind(train_set, feature)

1
1つのホットエンド列を元のデータフレームと連結した後、元のfeature_to_encodeを削除することをお勧めします。
pmalbu

このオプションを回答に追加しました。ありがとう。
サイバネティック

28

numpy.eye配列要素の選択メカニズムを使用してそれを行うことができます:

import numpy as np
nb_classes = 6
data = [[2, 3, 4, 0]]

def indices_to_one_hot(data, nb_classes):
    """Convert an iterable of indices to one-hot encoded labels."""
    targets = np.array(data).reshape(-1)
    return np.eye(nb_classes)[targets]

の戻り値indices_to_one_hot(nb_classes, data)

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

これ.reshape(-1)は、正しいラベル形式であることを確認するためにあります(もある可能性があります[[2], [3], [4], [0]])。


1
これは、文字列値を持つ列のOHEでは機能しません。
Abhilash Awasthi 2017

2
@AbhilashAwasthi確かに...しかし、なぜそれがうまくいくと期待するのですか?
マーティントーマ2017

22

まず、1つのホットエンコードを行う最も簡単な方法は、Sklearnを使用することです。

http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html

次に、パンダを1つのホットエンコードに使用するのはそれほど簡単ではないと思います(ただし、未確認です)。

Python用のパンダでダミー変数を作成する

最後に、ホットエンコードを1つ行う必要がありますか?ホットエンコーディングを1つ実行すると、機能の数が指数関数的に増加し、実行する分類子などの実行時間が大幅に増加します。特に、各カテゴリ機能に多くのレベルがある場合。代わりに、ダミーのコーディングを行うことができます。

ダミーエンコーディングを使用すると、通常はうまく機能し、実行時間と複雑さが大幅に減少します。賢い教授がかつて私に「Less is More」と言った。

必要に応じて、ここに私のカスタムエンコーディング関数のコードを示します。

from sklearn.preprocessing import LabelEncoder

#Auto encodes any dataframe column of type category or object.
def dummyEncode(df):
        columnsToEncode = list(df.select_dtypes(include=['category','object']))
        le = LabelEncoder()
        for feature in columnsToEncode:
            try:
                df[feature] = le.fit_transform(df[feature])
            except:
                print('Error encoding '+feature)
        return df

編集:比較を明確にする:

ワンホットエンコーディング:nレベルをn-1列に変換します。

Index  Animal         Index  cat  mouse
  1     dog             1     0     0
  2     cat       -->   2     1     0
  3    mouse            3     0     1

カテゴリ機能に多くの異なるタイプ(またはレベル)がある場合、これがメモリを爆発させる方法を確認できます。これは1列に過ぎないことに注意してください。

ダミーコーディング:

Index  Animal         Index  Animal
  1     dog             1      0   
  2     cat       -->   2      1 
  3    mouse            3      2

代わりに数値表現に変換してください。精度を少し犠牲にして、機能スペースを大幅に節約します。


1
1.カテゴリ変数が80%のデータセットがあります。私の理解では、このデータに分類子を使用する場合、1つのホットエンコーディングを使用する必要があります。それ以外の場合、1つのホットエンコーディングを実行しない場合、分類子はカテゴリ変数を正しい方法で処理しませんか?エンコードしないオプションはありますか?2. pd.get_dummies(train_small、sparse = True)をsaprse = Trueと一緒に使用すると、メモリの問題は解決しませんか?3.そのような問題にどのように取り組むべきですか?
avicohen 2016年

私が言ったように、2つのオプションがあります。1)1つのホットエンコード->カテゴリ機能のすべてのレベルを新しい列に変換します。2)ダミーコーディング->すべての列を数値表現に変換します。上記の回答をわかりやすく編集します。しかし、あなたは私が提供した機能を実行することができ、それはうまくいくはずです
Wboy

17
「多少の精度を犠牲にして」どのように「少し」と言うことができますか?場合によっては、場合によっては、精度が大幅に低下することもあります。このソリューションでは、定性的特徴が連続的であると見なされます。つまり、モデルはデータから適切に学習できません。
Josh Morel

2
Joshが上で述べたように、2番目の例では、モデルにそれを伝えることになりますmouse > cat > dogが、そうではありません。get_dummies(非常に限られたものですが)私の経験からカテゴリカル変数をモデルフレンドリーなデータに転送する最も簡単な方法です
Martin O Leary

5
他のコメントで指摘されているように、この解決策は非常に危険です。カテゴリー変数に次数と距離を任意に割り当てます。これを行うと、モデルの柔軟性がランダムに低下します。ツリーベースのモデルの場合、このようなエンコーディングにより、サブセットの可能性が減少します。たとえば、[(0)、(1,2)]と[(0,1)、(2)]の2つの可能な分割のみを取得でき、分割[(0,2)、(1)]は不可能な。カテゴリの数が多い場合、損失ははるかに大きくなります。
ランダム確実性2017

19

パンダを使用した1つのホットエンコーディングは非常に簡単です。

def one_hot(df, cols):
    """
    @param df pandas DataFrame
    @param cols a list of columns to encode 
    @return a DataFrame with one-hot encoding
    """
    for each in cols:
        dummies = pd.get_dummies(df[each], prefix=each, drop_first=False)
        df = pd.concat([df, dummies], axis=1)
    return df

編集:

sklearnを使用してone_hotする別の方法LabelBinarizer

from sklearn.preprocessing import LabelBinarizer 
label_binarizer = LabelBinarizer()
label_binarizer.fit(all_your_labels_list) # need to be global or remembered to use it later

def one_hot_encode(x):
    """
    One hot encode a list of sample labels. Return a one-hot encoded vector for each label.
    : x: List of sample Labels
    : return: Numpy array of one-hot encoded labels
    """
    return label_binarizer.transform(x)

14

numpy.eye関数を使用できます。

import numpy as np

def one_hot_encode(x, n_classes):
    """
    One hot encode a list of sample labels. Return a one-hot encoded vector for each label.
    : x: List of sample Labels
    : return: Numpy array of one-hot encoded labels
     """
    return np.eye(n_classes)[x]

def main():
    list = [0,1,2,3,4,3,2,1,0]
    n_classes = 5
    one_hot_list = one_hot_encode(list, n_classes)
    print(one_hot_list)

if __name__ == "__main__":
    main()

結果

D:\Desktop>python test.py
[[ 1.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  1.]
 [ 0.  0.  0.  1.  0.]
 [ 0.  0.  1.  0.  0.]
 [ 0.  1.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.]]

2
私の答えをコピーしましたか?
Martin Thoma

@Martin Thoma-そうだと思います
Dieter

5

その特定の列の1つのホットエンコーディングを取得する組み込み関数「get_dummies」を持つパンダ。

ワンホットエンコーディングの1行のコード:

df=pd.concat([df,pd.get_dummies(df['column name'],prefix='column name')],axis=1).drop(['column name'],axis=1)

4

ここではDictVectorizer、Pandas DataFrame.to_dict('records')メソッドを使用したソリューションを示します。

>>> import pandas as pd
>>> X = pd.DataFrame({'income': [100000,110000,90000,30000,14000,50000],
                      'country':['US', 'CAN', 'US', 'CAN', 'MEX', 'US'],
                      'race':['White', 'Black', 'Latino', 'White', 'White', 'Black']
                     })

>>> from sklearn.feature_extraction import DictVectorizer
>>> v = DictVectorizer()
>>> qualitative_features = ['country','race']
>>> X_qual = v.fit_transform(X[qualitative_features].to_dict('records'))
>>> v.vocabulary_
{'country=CAN': 0,
 'country=MEX': 1,
 'country=US': 2,
 'race=Black': 3,
 'race=Latino': 4,
 'race=White': 5}

>>> X_qual.toarray()
array([[ 0.,  0.,  1.,  0.,  0.,  1.],
       [ 1.,  0.,  0.,  1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  1.,  0.],
       [ 1.,  0.,  0.,  0.,  0.,  1.],
       [ 0.,  1.,  0.,  0.,  0.,  1.],
       [ 0.,  0.,  1.,  1.,  0.,  0.]])

3

ワンホットエンコーディングでは、値をインジケータ変数に変換するだけでは不十分です。通常、MLプロセスでは、このコーディングを検証またはテストデータセットに数回適用し、作成したモデルをリアルタイムの観測データに適用する必要があります。モデルの構築に使用されたマッピング(変換)を保存する必要があります。優れたソリューションを使用するDictVectorizerLabelEncoder(続くget_dummiesここにあなたが使用できることの機能は次のとおりです。

def oneHotEncode2(df, le_dict = {}):
    if not le_dict:
        columnsToEncode = list(df.select_dtypes(include=['category','object']))
        train = True;
    else:
        columnsToEncode = le_dict.keys()   
        train = False;

    for feature in columnsToEncode:
        if train:
            le_dict[feature] = LabelEncoder()
        try:
            if train:
                df[feature] = le_dict[feature].fit_transform(df[feature])
            else:
                df[feature] = le_dict[feature].transform(df[feature])

            df = pd.concat([df, 
                              pd.get_dummies(df[feature]).rename(columns=lambda x: feature + '_' + str(x))], axis=1)
            df = df.drop(feature, axis=1)
        except:
            print('Error encoding '+feature)
            #df[feature]  = df[feature].convert_objects(convert_numeric='force')
            df[feature]  = df[feature].apply(pd.to_numeric, errors='coerce')
    return (df, le_dict)

これはpandasデータフレームで機能し、データフレームの各列に対して作成し、マッピングを返します。したがって、次のように呼び出します。

train_data, le_dict = oneHotEncode2(train_data)

次に、テストデータで、トレーニングから返された辞書を渡すことによって呼び出しが行われます。

test_data, _ = oneHotEncode2(test_data, le_dict)

同等の方法は、を使用することDictVectorizerです。同じことに関する関連記事が私のブログにあります。単にget_dummies ポストを使用することに対してこのアプローチの背後にいくつかの理由を提供するため、ここでそれを述べます (開示:これは私のブログです)。


3

エンコードせずにデータをcatboost分類器に渡すことができます。Catboostは、ワンホットなターゲット拡張平均エンコーディングを実行することにより、カテゴリ変数自体を処理します。


3

次のこともできます。以下を使用する必要がないことに注意してくださいpd.concat

import pandas as pd 
# intialise data of lists. 
data = {'Color':['Red', 'Yellow', 'Red', 'Yellow'], 'Length':[20.1, 21.1, 19.1, 18.1],
       'Group':[1,2,1,2]} 

# Create DataFrame 
df = pd.DataFrame(data) 

for _c in df.select_dtypes(include=['object']).columns:
    print(_c)
    df[_c]  = pd.Categorical(df[_c])
df_transformed = pd.get_dummies(df)
df_transformed

明示的な列をカテゴリに変更することもできます。たとえば、ここで私はColorGroup

import pandas as pd 
# intialise data of lists. 
data = {'Color':['Red', 'Yellow', 'Red', 'Yellow'], 'Length':[20.1, 21.1, 19.1, 18.1],
       'Group':[1,2,1,2]} 

# Create DataFrame 
df = pd.DataFrame(data) 
columns_to_change = list(df.select_dtypes(include=['object']).columns)
columns_to_change.append('Group')
for _c in columns_to_change:
    print(_c)
    df[_c]  = pd.Categorical(df[_c])
df_transformed = pd.get_dummies(df)
df_transformed

2

私はこのパーティーに遅れていることを知っていますが、自動化された方法でデータフレームをホットエンコードする最も簡単な方法は、この関数を使用することです。

def hot_encode(df):
    obj_df = df.select_dtypes(include=['object'])
    return pd.get_dummies(df, columns=obj_df.columns).values

1

私はこれを私の音響モデルで使用しました:おそらくこれはurモデルで役立ちます。

def one_hot_encoding(x, n_out):
    x = x.astype(int)  
    shape = x.shape
    x = x.flatten()
    N = len(x)
    x_categ = np.zeros((N,n_out))
    x_categ[np.arange(N), x] = 1
    return x_categ.reshape((shape)+(n_out,))

0

他の質問に追加するために、Numpyを使用してPython 2.0関数でどのように実行したかを説明しましょう。

def one_hot(y_):
    # Function to encode output labels from number indexes 
    # e.g.: [[5], [0], [3]] --> [[0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0]]

    y_ = y_.reshape(len(y_))
    n_values = np.max(y_) + 1
    return np.eye(n_values)[np.array(y_, dtype=np.int32)]  # Returns FLOATS

n_values = np.max(y_) + 1たとえば、ミニバッチを使用する場合に備えて、適切な数のニューロンを使用するようにラインをハードコーディングすることができます。

この機能が使用されたデモプロジェクト/チュートリアル:https : //github.com/guillaume-chevalier/LSTM-Human-Activity-Recognition



0

それはでき、次のように簡単でなければなりません:

class OneHotEncoder:
    def __init__(self,optionKeys):
        length=len(optionKeys)
        self.__dict__={optionKeys[j]:[0 if i!=j else 1 for i in range(length)] for j in range(length)}

使用法 :

ohe=OneHotEncoder(["A","B","C","D"])
print(ohe.A)
print(ohe.D)

0

@Martin Thomaの答えを拡張する

def one_hot_encode(y):
    """Convert an iterable of indices to one-hot encoded labels."""
    y = y.flatten() # Sometimes not flattened vector is passed e.g (118,1) in these cases
    # the function ends up creating a tensor e.g. (118, 2, 1). flatten removes this issue
    nb_classes = len(np.unique(y)) # get the number of unique classes
    standardised_labels = dict(zip(np.unique(y), np.arange(nb_classes))) # get the class labels as a dictionary
    # which then is standardised. E.g imagine class labels are (4,7,9) if a vector of y containing 4,7 and 9 is
    # directly passed then np.eye(nb_classes)[4] or 7,9 throws an out of index error.
    # standardised labels fixes this issue by returning a dictionary;
    # standardised_labels = {4:0, 7:1, 9:2}. The values of the dictionary are mapped to keys in y array.
    # standardised_labels also removes the error that is raised if the labels are floats. E.g. 1.0; element
    # cannot be called by an integer index e.g y[1.0] - throws an index error.
    targets = np.vectorize(standardised_labels.get)(y) # map the dictionary values to array.
    return np.eye(nb_classes)[targets]

0

短い答え

numpy、pandas、またはその他のパッケージ使用せずにワンホットエンコーディングを実行する関数を次に示します。整数、ブール値、または文字列(そしておそらく他の型も)のリストを取ります。

import typing


def one_hot_encode(items: list) -> typing.List[list]:
    results = []
    # find the unique items (we want to unique items b/c duplicate items will have the same encoding)
    unique_items = list(set(items))
    # sort the unique items
    sorted_items = sorted(unique_items)
    # find how long the list of each item should be
    max_index = len(unique_items)

    for item in items:
        # create a list of zeros the appropriate length
        one_hot_encoded_result = [0 for i in range(0, max_index)]
        # find the index of the item
        one_hot_index = sorted_items.index(item)
        # change the zero at the index from the previous line to a one
        one_hot_encoded_result[one_hot_index] = 1
        # add the result
        results.append(one_hot_encoded_result)

    return results

例:

one_hot_encode([2, 1, 1, 2, 5, 3])

# [[0, 1, 0, 0],
#  [1, 0, 0, 0],
#  [1, 0, 0, 0],
#  [0, 1, 0, 0],
#  [0, 0, 0, 1],
#  [0, 0, 1, 0]]
one_hot_encode([True, False, True])

# [[0, 1], [1, 0], [0, 1]]
one_hot_encode(['a', 'b', 'c', 'a', 'e'])

# [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0], [0, 0, 0, 1]]

より長い回答

この質問にはすでにたくさんの答えがあることは知っていますが、2つのことに気づきました。まず、ほとんどの回答はnumpyやpandasなどのパッケージを使用しています。そして、これは良いことです。量産コードを作成している場合は、numpy / pandasパッケージで提供されているような堅牢で高速なアルゴリズムを使用しているはずです。しかし、教育のために、誰かが他の人のアルゴリズムの実装だけでなく、透明なアルゴリズムを持つ答えを提供するべきだと思います。第二に、以下の要件のいずれかを満たさないため、多くの回答がワンホットエンコーディングの堅牢な実装を提供していないことに気付きました。以下は、便利で正確、かつ堅牢なワンホットエンコーディング機能の要件の一部です(私がそれらを参照)。

ワンホットエンコーディング関数は、次の条件を満たす必要があります。

  • さまざまなタイプ(整数、文字列、浮動小数点数など)のリストを入力として処理する
  • 重複した入力リストを処理する
  • 入力に対応する(同じ順序で)リストのリストを返す
  • 各リストが可能な限り短いリストのリストを返す

私はこの質問に対する答えの多くをテストしましたが、それらのほとんどは上記の要件の1つで失敗しました。


0

これを試して:

!pip install category_encoders
import category_encoders as ce

categorical_columns = [...the list of names of the columns you want to one-hot-encode ...]
encoder = ce.OneHotEncoder(cols=categorical_columns, use_cat_names=True)
df_train_encoded = encoder.fit_transform(df_train_small)

df_encoded.head()

結果のデータフレームdf_train_encodedは元のデータフレームと同じですが、カテゴリ機能はワンホットエンコードされたバージョンに置き換えられています。

詳細はcategory_encoders こちら


-1

ここで私はこのアプローチで試しました:

import numpy as np
#converting to one_hot





def one_hot_encoder(value, datal):

    datal[value] = 1

    return datal


def _one_hot_values(labels_data):
    encoded = [0] * len(labels_data)

    for j, i in enumerate(labels_data):
        max_value = [0] * (np.max(labels_data) + 1)

        encoded[j] = one_hot_encoder(i, max_value)

    return np.array(encoded)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.