hacktricks/a.i.-exploiting/bra.i.nsmasher-presentation/ml-basics/feature-engineering.md
2023-08-03 19:12:22 +00:00

16 KiB
Raw Blame History

☁️ HackTricks云 ☁️ -🐦 推特 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

可能的基本数据类型

数据可以是连续型无限个值)或分类型(名义)其中可能的值的数量是有限的。

分类型数据类型

二进制

只有2个可能的值1或0。如果数据集中的值是字符串格式例如"True"和"False"),则可以使用以下方式将这些值分配为数字:

dataset["column2"] = dataset.column2.map({"T": 1, "F": 0})

有序

值按照一定顺序排列,例如:第一名,第二名... 如果类别是字符串(例如:"初学者""业余爱好者""专业人士""专家"),你可以像在二进制情况下那样将它们映射为数字。

column2_mapping = {'starter':0,'amateur':1,'professional':2,'expert':3}
dataset['column2'] = dataset.column2.map(column2_mapping)
  • 对于字母列,您可以更轻松地对其进行排序:
# First get all the uniq values alphabetically sorted
possible_values_sorted = dataset.column2.sort_values().unique().tolist()
# Assign each one a value
possible_values_mapping = {value:idx for idx,value in enumerate(possible_values_sorted)}
dataset['column2'] = dataset.column2.map(possible_values_mapping)

循环特征

看起来像是有序值,因为有一种顺序,但并不意味着一个比另一个大。而且它们之间的距离取决于你计数的方向。例如:一周的天数,星期天并不比星期一“大”。

  • 有不同的方法来编码循环特征,其中一些可能只适用于某些算法。通常情况下,可以使用虚拟编码
column2_dummies = pd.get_dummies(dataset.column2, drop_first=True)
dataset_joined = pd.concat([dataset[['column2']], column2_dummies], axis=1)

日期

日期是连续变量。可以被视为循环的(因为它们重复出现)或者作为有序变量(因为一个时间点比前一个时间点大)。

  • 通常日期被用作索引
# Transform dates to datetime
dataset["column_date"] = pd.to_datetime(dataset.column_date)
# Make the date feature the index
dataset.set_index('column_date', inplace=True)
print(dataset.head())

# Sum usage column per day
daily_sum = dataset.groupby(df_daily_usage.index.date).agg({'usage':['sum']})
# Flatten and rename usage column
daily_sum.columns = daily_sum.columns.get_level_values(0)
daily_sum.columns = ['daily_usage']
print(daily_sum.head())

# Fill days with 0 usage
idx = pd.date_range('2020-01-01', '2020-12-31')
daily_sum.index = pd.DatetimeIndex(daily_sum.index)
df_filled = daily_sum.reindex(idx, fill_value=0) # Fill missing values


# Get day of the week, Monday=0, Sunday=6, and week days names
dataset['DoW'] = dataset.transaction_date.dt.dayofweek
# do the same in a different way
dataset['weekday'] = dataset.transaction_date.dt.weekday
# get day names
dataset['day_name'] = dataset.transaction_date.apply(lambda x: x.day_name())

多类别/名义

超过2个类别,没有相关顺序。使用 dataset.describe(include='all') 获取每个特征的类别信息。

  • 引用字符串是一个标识示例的列(比如一个人的名字)。这可能会重复(因为两个人可能有相同的名字),但大多数是唯一的。这些数据是无用的,应该被删除
  • 关键列用于链接表之间的数据。在这种情况下,元素是唯一的。这些数据是无用的,应该被删除

为了将多类别列编码为数字(以便机器学习算法理解它们),使用虚拟编码(而不是独热编码,因为它不能避免完美多重共线性)。

你可以使用 pd.get_dummies(dataset.column1) 来获取多类别列的独热编码。这将把所有类别转换为二进制特征,因此会创建每个可能类别的新列并将1分配给一个列的True值其余列为false。

你可以使用 pd.get_dummies(dataset.column1, drop_first=True) 来获取多类别列的虚拟编码。这将把所有类别转换为二进制特征,因此会创建每个可能类别减一的新列,最后两列将在最后一个二进制列中反映为"1"或"0"。这将避免完美多重共线性,减少列之间的关系。

共线性/多重共线性

两个特征彼此相关时出现共线性。当超过2个特征相关时出现多重共线性。

在机器学习中,你希望特征与可能的结果相关,但不希望它们彼此相关。这就是为什么虚拟编码混合了最后两列,并且比独热编码更好,因为独热编码没有这样做,从而在多类别列的所有新特征之间创建了明确的关系。

VIF是方差膨胀因子,用于衡量特征之间的多重共线性。值大于5意味着应该删除两个或多个共线特征中的一个

from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant

#dummies_encoded = pd.get_dummies(dataset.column1, drop_first=True)
onehot_encoded = pd.get_dummies(dataset.column1)
X = add_constant(onehot_encoded) # Add previously one-hot encoded data
print(pd.Series([variance_inflation_factor(X.values,i) for i in range(X.shape[1])], index=X.columns))

类别不平衡

当训练数据中的每个类别的数量不相等时,就会出现类别不平衡的情况。

# Get statistic of the features
print(dataset.describe(include='all'))
# Get an overview of the features
print(dataset.info())
# Get imbalance information of the target column
print(dataset.target_column.value_counts())

在不平衡的情况下,总会存在多数类别少数类别

解决这个问题有两种主要方法:

  • 欠采样:从多数类别中随机删除数据,使其与少数类别具有相同数量的样本。
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUserSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_under, y_under = rus.fit_resample(X,y)
print(y_under.value_counts()) #Confirm data isn't imbalanced anymore
  • 过采样:为少数类别生成更多的数据,直到其样本数量与多数类别相同。
from imblearn.under_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_over, y_over = ros.fit_resample(X,y)
print(y_over.value_counts()) #Confirm data isn't imbalanced anymore

您可以使用参数**sampling_strategy来指示您想要进行欠采样或过采样百分比**默认为1100%),意味着将少数类别的数量与多数类别相等)。

{% hint style="info" %} 欠采样或过采样并不完美,如果您使用.describe()获取过/欠采样数据的统计信息并将其与原始数据进行比较,您会发现它们已经发生了变化。因此,过采样和欠采样会修改训练数据。 {% endhint %}

SMOTE过采样

SMOTE通常是一种更可靠的过采样数据的方法

from imblearn.over_sampling import SMOTE

# Form SMOTE the target_column need to be numeric, map it if necessary
smote = SMOTE(random_state=1337)
X_smote, y_smote = smote.fit_resample(dataset[['column1', 'column2', 'column3']], dataset.target_column)
dataset_smote = pd.DataFrame(X_smote, columns=['column1', 'column2', 'column3'])
dataset['target_column'] = y_smote
print(y_smote.value_counts()) #Confirm data isn't imbalanced anymore

很少出现的类别

想象一下一个数据集,其中一个目标类别出现的次数非常少

这就像前一节中的类别不平衡问题,但是在这种情况下,很少出现的类别甚至比"少数类"出现的次数还要少。在这里也可以使用原始过采样欠采样方法,但通常这些技术不会给出非常好的结果

权重

在某些算法中,可以修改目标数据的权重,以便在生成模型时默认更重要。

weights = {0: 10 1:1} #Assign weight 10 to False and 1 to True
model = LogisticRegression(class_weight=weights)

您可以使用过/欠采样技术与权重混合来尝试改善结果。

PCA - 主成分分析

这是一种帮助降低数据维度的方法。它将组合不同的特征减少特征数量,从而生成更有用的特征(需要更少的计算)。

生成的特征对人类来说是不可理解的,因此它还匿名化数据

不一致的标签类别

数据可能存在转换失败或人为错误导致的错误。

因此,您可能会发现相同的标签存在拼写错误,不同的大小写缩写,例如:BLUEBluebbule。在训练模型之前,您需要修复数据中的这些标签错误。

您可以通过将所有内容转换为小写并将拼写错误的标签映射到正确的标签来清理这些问题。

非常重要的是检查您拥有的所有数据是否正确标记,因为例如,数据中的一个拼写错误,在对类别进行虚拟编码时,将在最终特征中生成一个新列,对最终模型产生不良后果。通过对一列进行独热编码并检查所创建的列的名称,可以很容易地检测到此示例。

缺失数据

研究中可能缺少一些数据。

可能会发生一些完全随机的数据丢失,这种数据被称为完全随机缺失MCAR)。

可能会有一些随机数据丢失,但某些特定细节更有可能丢失,例如男性更有可能告诉他们的年龄,而女性则不会。这被称为随机缺失MAR)。

最后,可能存在非随机缺失MNAR)的数据。数据的值与具有数据的概率直接相关。例如,如果您想测量某些令人尴尬的事情,某人越尴尬,他分享的可能性就越小。

前两种缺失数据类别可以被忽略。但是第三种需要考虑未受影响的数据部分或尝试以某种方式对缺失数据进行建模

了解缺失数据的一种方法是使用.info()函数,它将指示每个类别的行数和值的数量。如果某个类别的值少于行数,则存在一些缺失数据:

# Get info of the dataset
dataset.info()

# Drop all rows where some value is missing
dataset.dropna(how='any', axis=0).info()

通常建议,如果数据集中缺失的特征超过20%,则应该删除该列

# Remove column
dataset.drop('Column_name', axis='columns', inplace=True)
dataset.info()

{% hint style="info" %} 请注意,数据集中并非所有缺失值都是缺失的。可能缺失值已经被赋予了"Unknown"、"n/a"、""、-1、0等值。您需要检查数据集使用dataset.column_name.value_counts(dropna=False)来检查可能的值)。 {% endhint %}

如果数据集中有一些数据缺失(数量不太多),您需要找到缺失数据的类别。为此,您基本上需要知道缺失数据是否是随机的,而要找出这一点,您需要找出缺失数据是否与数据集的其他数据相关

要找出缺失值是否与另一列相关您可以创建一个新列如果数据缺失则将其设为1否则设为0然后计算它们之间的相关性

# The closer it's to 1 or -1 the more correlated the data is
# Note that columns are always perfectly correlated with themselves.
dataset[['column_name', 'cloumn_missing_data']].corr()

如果你决定忽略缺失的数据,你仍然需要处理它:你可以删除带有缺失数据的行(模型的训练数据会变小),你可以完全删除该特征,或者可以对其进行建模

你应该检查缺失特征与目标列之间的相关性,以了解该特征对目标的重要性,如果它确实很小,你可以选择删除它或填充它

对于缺失的连续数据,你可以使用:均值中位数或使用一个插补算法。插补算法可以尝试使用其他特征来找到缺失特征的值:

from sklearn.impute import KNNImputer

X = dataset[['column1', 'column2', 'column3']]
y = dataset.column_target

# Create the imputer that will fill the data
imputer = KNNImputer(n_neightbors=2, weights='uniform')
X_imp = imputer.fit_transform(X)

# Check new data
dataset_imp = pd.DataFrame(X_imp)
dataset.columns = ['column1', 'column2', 'column3']
dataset.iloc[10:20] # Get some indexes that contained empty data before

为了填充分类数据,首先需要考虑值缺失的原因。如果是由于用户的选择(他们不想提供数据),可以创建一个新的类别来表示。如果是由于人为错误,可以删除行或特征(请参考前面提到的步骤),或者用众数填充(不推荐)。

合并特征

如果你发现两个特征彼此之间存在相关性,通常应该删除其中一个(与目标相关性较低的那个),但也可以尝试将它们合并并创建一个新的特征。

# Create a new feautr combining feature1 and feature2
dataset['new_feature'] = dataset.column1/dataset.column2

# Check correlation with target column
dataset[['new_feature', 'column1', 'column2', 'target']].corr()['target'][:]

# Check for collinearity of the 2 features and the new one
X = add_constant(dataset[['column1', 'column2', 'target']])
# Calculate VIF
pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)
☁️ HackTricks云 ☁️ -🐦 推特 🐦 - 🎙️ Twitch 🎙️ - 🎥 YouTube 🎥