前面几节的内容,为大家入门数据分析奠定了一定的基础,《Python统计学极简入门》帮你解释了如何从统计的角度来刻画数据,《SQL数据分析极简入门》帮你从数据库提取业务需要的数据、《Python数据分析极简入门》帮你如何用Pandas快速处理数据。
接下来的内容,我们往机器学习领域延伸一下,按照一贯的“MPV(最小可行化产品)”思路,先学机器学习的重中之重————特征工程。在机器学习方法的的实施流程里面,我们拿到了原始数据,做了各种数据清洗后,就需要掌握特征工程的知识,以便于更好地服务于后面的机器学习模型。
众所周知,关于数据与特征,业界广为流传着两句话:“数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限”、“garbage in,garbage out”。前者从机器学习的角度,较为严谨地指出,数据与特征的重要性要大于模型和算法;后者近似戏谑般地表达突出了数据的重要性。
但是市面上关于特征工程的书,却只有寥寥几本 《特征工程入门与实践》、《精通特征工程》、《数据准备及特征工程》不仅数量少,而且里面的方法大部分也都是大家耳熟能详的内容:缺失值填补、归一化、one-hot,只看这些内容对于日常做特征的小伙伴们肯定是意犹未尽,总想着有没有更全面一些的内容,这个系列就尝试着给大家梳理一下这部分内容。
学习入口:《Python特征工程极简入门》
为了使得内容更聚焦一些,我们本次暂不涉及文本及图像特征,如有需要后续会单独写两个教程来总结。本次教程结构如下:
特征工程是对原始数据进行一系列工程处理,将其提炼为特征,作为输入供算法和模型使用。
如果把机器学习模型简化一下:用已有的x和 y 来训练一个模型 f(x)。那么,特征工程就是利用原有的x去构造新的x’,同时配合模型调参,使得最终模型的效果达到最优。我们把通过已有数学公式: x,构建新的x’以取得更好的建模效果,这个特征构建的过程称为特征工程。
大道至简,我们日常分析的数据,尤其是互联网数据,大多数是由人类的生产活动产生的,那就离不开时间、空间的概念——宇宙,“宇”无限空间,“宙”无限时间。我们做特征工程也是一样,时间、空间地理特征也是重中之重,是用来刻画客观世界极其重要的维度,我们先来看一下时间特征:
def get_time_period(hour):
if 0 <= hour < 6:
return "凌晨"
elif 6 <= hour < 12:
return "上午"
elif 12 <= hour < 18:
return "下午"
else:
return "晚上"
# get_time_period(8)
def get_peak_hour(hour):
if 7 <= hour < 10:
return "早高峰"
elif 17 <= hour < 20:
return "晚高峰"
else:
return "非高峰时段"
# get_peak_hour(7)
from datetime import datetime
def is_weekday(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d')
if date.weekday() < 5: # Monday to Friday are considered weekdays (0 to 4)
return True
else:
return False
# is_weekday('2023-01-16')
from datetime import datetime
def get_day_of_week(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d')
return date.strftime("%A") # 返回星期几的字符串表示,比如"Monday"、"Tuesday"等
# get_day_of_week('2023-01-16')
from datetime import datetime
def get_week_of_year(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d')
return date.strftime("%U") # 返回一年中的第几个星期,从0开始计数
# get_week_of_year('2023-01-16')
from datetime import datetime
def get_month_of_year(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d')
return date.strftime("%B") # 返回月份的字符串表示,比如"January"、"February"等
# get_month_of_year('2023-07-15')
from datetime import datetime
def get_quarter_of_year(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d')
quarter = (date.month - 1) // 3 + 1
return quarter
# get_quarter_of_year('2023-07-15')
# !pip3 install chinese_calendar
# !pip3 install --upgrade pip
from chinese_calendar import is_holiday
def is_chinese_holiday(date_string):
date = datetime.strptime(date_string, '%Y-%m-%d').date()
return is_holiday(date)
# is_chinese_holiday('2023-10-01')
# !pip3 install lunardate
from lunardate import LunarDate
def is_lunar_solar_term(date_string):
date = LunarDate.fromSolarDate(*map(int, date_string.split('-')))
return date.getSolarTerm()
# is_lunar_solar_term('2023-10-01')
# !pip3 install holidays
import datetime
import holidays
# 你可以根据你的需要修改国家或地区,以及日期格式。
us_holidays = holidays.US()
def is_public_holiday(date):
return date in us_holidays
date_to_check = datetime.date(2022, 1, 1)
print(is_public_holiday(date_to_check)) # 输出 True 或 False
True
import pandas as pd
from datetime import datetime
# 假设这是你的数据
data = {
'userid': [1, 1, 1, 2, 2],
'url': ['page1', 'page2', 'page1', 'page3', 'page1'],
'create_time': ['2023-05-01 08:30:00', '2023-05-01 08:35:00', '2023-05-01 08:40:00', '2023-05-01 09:00:00', '2023-05-01 09:10:00']
}
df = pd.Datafr ame(data)
df['create_time'] = pd.to_datetime(df['create_time'])
# 首先按照用户ID和URL进行排序
df = df.sort_values(by=['userid', 'url', 'create_time'])
# 计算停留时间
df['duration'] = df.groupby(['userid', 'url'])['create_time'].diff().dt.total_seconds().fillna(0)
print(df)
userid url create_time duration
0 1 page1 2023-05-01 08:30:00 0.0
2 1 page1 2023-05-01 08:40:00 600.0
1 1 page2 2023-05-01 08:35:00 0.0
4 2 page1 2023-05-01 09:10:00 0.0
3 2 page3 2023-05-01 09:00:00 0.0
def city_tier(city_name):
first_tier_cities = ['北京', '上海', '广州', '深圳']
second_tier_cities = ['杭州', '南京', '武汉', '成都', '重庆', '西安', '郑州', '长沙', '青岛', '沈阳', '大连', '厦门', '福州', '哈尔滨', '济南', '宁波', '无锡', '常州', '东莞', '佛山', '珠海', '汕头', '南宁', '昆明', '贵阳', '石家庄', '太原', '合肥', '南昌', '长春', '哈尔滨']
third_tier_cities = ['其他城市']
if city_name in first_tier_cities:
return '一线城市'
elif city_name in second_tier_cities:
return '二线城市'
else:
return '三线城市或其他城市'
# 举例:判断城市'杭州'的城市等级
city = '杭州'
result = city_tier(city)
print(f'城市{city}为:{result}')
城市杭州为:二线城市
import re
import requests
def get_city_by_ip(ip):
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=' + ip
response = requests.get(url)
data = response.json()
if data['code'] == 0:
city = data['data']['city']
return city
else:
return '未知城市'
# 举例:查询IP地址对应的城市
ip_address = '8.8.8.8' # 你可以替换成你要查询的IP地址
city = get_city_by_ip(ip_address)
print(f'IP地址{ip_address}对应的城市为:{city}')
IP地址8.8.8.8对应的城市为:未知城市
import requests
def get_city_by_location(lat, lng):
ak = 'your_baidu_map_api_key'
# 你需要替换成你自己的百度地图API密钥
url = f'http://api.map.baidu.com/reverse_geocoding/v3/?ak={ak}&output=json&coordtype=wgs84ll&location={lat},{lng}'
response = requests.get(url)
data = response.json()
if data['status'] == 0:
city = data['result']['addressComponent']['city']
return city
else:
return '未知城市'
# # 举例:查询经纬度对应的城市
# latitude = 39.4 # 纬度
# longitude = 115.7 # 经度
# city = get_city_by_location(latitude, longitude)
# print(f'经纬度({latitude},{longitude})对应的城市为:{city}')
import requests
def parse_address(address):
ak = 'your_baidu_map_api_key' # 你需要替换成你自己的百度地图API密钥
url = f'http://api.map.baidu.com/geocoding/v3/?address={address}&output=json&ak={ak}'
response = requests.get(url)
data = response.json()
if data['status'] == 0:
result = data['result']
province = result['addressComponent']['province']
city = result['addressComponent']['city']
district = result['addressComponent']['district']
return province, city, district
else:
return '未知', '未知', '未知'
# # 举例:解析地址并获取省市县区信息
# address = '北京市海淀区中关村大街1号'
# province, city, district = parse_address(address)
# print(f'地址"{address}"对应的省份为:{province},城市为:{city},区/县为:{district}')
import pyproj
# 定义投影坐标系
wgs84 = pyproj.Proj(init='epsg:4326') # WGS84经纬度坐标系
mercator = pyproj.Proj(init='epsg:3857') # Web墨卡托投影坐标系
# 经纬度坐标转换为球面坐标
longitude = 116.4074 # 经度
latitude = 39.9042 # 纬度
x, y = pyproj.transform(wgs84, mercator, longitude, latitude)
print(f'经纬度({longitude},{latitude})转换为球面坐标({x},{y})')
经纬度(116.4074,39.9042)转换为球面坐标(12958412.492568914,4852030.634814578)
为什么要进行归一化?简单来说就是将数据缩放在[0,1]区间,防止量纲不一致。公式如下:
xnew=xmax−xminx−xmin
其中,x 是原始数据,xnew 是正则化后的数据,xmin 和 xmax 分别是数据的最小值和最大值。
代码如下:
from sklearn import preprocessing
min_max_scaler= preprocessing.MinMaxScaler()
x = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
x_new = min_max_scaler.fit_transform(x)
x_new
array([[0.5 , 0. , 1. ],
[1. , 0.5 , 0.33333333],
[0. , 1. , 0. ]])
使用sklearn.preprocessing.StandardScaler类也可以实现,使用该类的好处在于可以保存训练集中的参数(均值、方差)
scaler = preprocessing.StandardScaler().fit(x)
scaler
StandardScaler()
scaler.mean_
array([1. , 0. , 0.33333333])
scaler.var_
array([0.66666667, 0.66666667, 1.55555556])
x_new = scaler.transform(x)
x_new
array([[ 0. , -1.22474487, 1.33630621],
[ 1.22474487, 0. , -0.26726124],
[-1.22474487, 1.22474487, -1.06904497]])
为什么要进行标准化?数据经过零-均值标准化后均值为0,方差为1,更方便利于标准正态分布的性质。
将数据转换为均值为0,标准差为1的标准正态分布。
xnew=σx−μ
使用sklearn.preprocessing.scale()函数,可以直接将给定数据进行标准化。代码如下:
from sklearn import preprocessing
min_max_scaler= preprocessing.MinMaxScaler()
x = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
x_new = min_max_scaler.fit_transform(x)
x_new
array([[0.5 , 0. , 1. ],
[1. , 0.5 , 0.33333333],
[0. , 1. , 0. ]])
使用sklearn.preprocessing.StandardScaler类也可以实现,使用该类的好处在于可以保存训练集中的参数(均值、方差)
scaler = preprocessing.StandardScaler().fit(x)
scaler
scaler.mean_
scaler.var_
x_new = scaler.transform(x)
x_new
array([[ 0. , -1.22474487, 1.33630621],
[ 1.22474487, 0. , -0.26726124],
[-1.22474487, 1.22474487, -1.06904497]])
为什么要进行正则化?正则化可以帮助防止过拟合,提高模型的泛化能力。
常见的正则化的方式有l1正则化和l2正则化
这里我们先看看lp范数怎么计算?(这段如果理解不了,可以跳过,原理没有实践重要)
p-范数的计算公式:
∣∣x∣∣p=(∣x1∣p+∣x2∣p+…+∣xn∣p)p1其中,x是一个n维向量,xi 表示向量的第i个元素,p 是范数的阶数。特别地,当p=2时,称为欧几里得范数(Euclidean norm);当>p=1时,称为曼哈顿范数(Manhattan norm)。
比如当我们有一个二维向量 x=[3 4],我们可以计算不同p-范数的值。
当 p=1 时,曼哈顿范数(Manhattan norm):
∣∣x∣∣1=∣3∣+∣4∣=7
当 p=2时,欧几里得范数(Euclidean norm):
∣∣x∣∣2=32+42=5有了范数,我们看看范数正则化的数学原理:
给定一个矩阵 X,其中每一行表示一个样本,每一列表示一个特征。
对于矩阵 X 中的每一行 x_i,L1 范数正则化的过程如下:
- 计算每一行的 L1 范数,即将该行中各个元素的绝对值相加。
- 将每个元素除以该行的 L1 范数,从而得到一个新的矩阵 x_new。
具体来说,对于矩阵 X 中的第 i 行,L1 范数正则化的数学过程如下:
xnew,i=∑j=1n∣xij∣xi其中,xi 表示矩阵 X 中的第 i 行,$ x_{\text{new},i} $ 表示正则化后的第 i 行,n 表示矩阵 X 的列数。通过这个过程,矩阵 X 中的每一行都被重新缩放,使得每一行的元素之和为1,从而实现了 L1 范数正则化。
可以使用preprocessing.normalize()函数对指定数据进行转换,代码如下:
x = [[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]]
x_new = preprocessing.normalize(x, norm='l1')
x_new
array([[ 0.25, -0.25, 0.5 ],
[ 1. , 0. , 0. ],
[ 0. , 0.5 , -0.5 ]])
也可以使用processing.Normalizer()类实现对训练集和测试集的拟合和转换,代码如下:
x = [[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]]
x_new = preprocessing.normalize(x, norm='l2')
x_new
array([[ 0.40824829, -0.40824829, 0.81649658],
[ 1. , 0. , 0. ],
[ 0. , 0.70710678, -0.70710678]])
统计值特征(max, min, mean, std)
import pandas as pd
# 创建一个示例数据集
data = {
'A': [1, 2, 3, 4, 5],
'B': [10, 20, 30, 40, 50]
}
df = pd.Datafr ame(data)
# 计算均值
mean_values = df['A'].mean()
# 计算标准差
std_values = df['A'].std()
# 计算最大值
max_values = df['A'].max()
# 计算最小值
min_values = df['A'].min()
分箱离散化
import pandas as pd
# 创建一个示例数据集
data = {
'age': [25, 30, 35, 40, 45, 50, 55, 60, 65, 70],
'income': [35000, 50000, 65000, 80000, 95000, 110000, 125000, 140000, 155000, 170000]
}
df = pd.Datafr ame(data)
# 定义分箱的边界
bins = [20, 30, 40, 50, 60, 70]
# 使用cut函数进行分箱离散化
df['age_bin'] = pd.cut(df['age'], bins)
# 打印结果
print(df['age_bin'])
0 (20, 30]
1 (20, 30]
2 (30, 40]
3 (30, 40]
4 (40, 50]
5 (40, 50]
6 (50, 60]
7 (50, 60]
8 (60, 70]
9 (60, 70]
Name: age_bin, dtype: category
Categories (5, interval[int64, right]): [(20, 30] < (30, 40] < (40, 50] < (50, 60] < (60, 70]]
每个类别下对应的变量统计值histogram(分布状况)
import pandas as pd
# 创建一个示例数据集
data = {
'category': ['A', 'B', 'A', 'B', 'A', 'B'],
'value': [10, 20, 30, 40, 50, 60]
}
df = pd.Datafr ame(data)
# 按照类别进行分组,然后计算每个类别下对应的变量的统计值
grouped = df.groupby('category')['value'].agg(['mean', 'median', 'std'])
grouped
| mean | median | std | |
|---|---|---|---|
| category | |||
| A | 30.0 | 30.0 | 20.0 |
| B | 40.0 | 40.0 | 20.0 |
数值型转化为类别型
import pandas as pd
# 创建一个示例数据集
data = {
'age': [25, 30, 35, 40, 45, 50, 55, 60, 65, 70],
'income': [35000, 50000, 65000, 80000, 95000, 110000, 125000, 140000, 155000, 170000]
}
df = pd.Datafr ame(data)
# 将数值型变量进行分箱离散化
bins = [20, 30, 40, 50, 60, 70]
df['age_bin'] = pd.cut(df['age'], bins)
# 使用get_dummies函数生成哑变量
dummy_variables = pd.get_dummies(df['age_bin'], prefix='age_bin')
# 将生成的哑变量与原始数据集合并
df = pd.concat([df, dummy_variables], axis=1)
df
| age | income | age_bin | age_bin_(20, 30] | age_bin_(30, 40] | age_bin_(40, 50] | age_bin_(50, 60] | age_bin_(60, 70] | |
|---|---|---|---|---|---|---|---|---|
| 0 | 25 | 35000 | (20, 30] | 1 | 0 | 0 | 0 | 0 |
| 1 | 30 | 50000 | (20, 30] | 1 | 0 | 0 | 0 | 0 |
| 2 | 35 | 65000 | (30, 40] | 0 | 1 | 0 | 0 | 0 |
| 3 | 40 | 80000 | (30, 40] | 0 | 1 | 0 | 0 | 0 |
| 4 | 45 | 95000 | (40, 50] | 0 | 0 | 1 | 0 | 0 |
| 5 | 50 | 110000 | (40, 50] | 0 | 0 | 1 | 0 | 0 |
| 6 | 55 | 125000 | (50, 60] | 0 | 0 | 0 | 1 | 0 |
| 7 | 60 | 140000 | (50, 60] | 0 | 0 | 0 | 1 | 0 |
| 8 | 65 | 155000 | (60, 70] | 0 | 0 | 0 | 0 | 1 |
| 9 | 70 | 170000 | (60, 70] | 0 | 0 | 0 | 0 | 1 |
from sklearn.preprocessing import OrdinalEncoder
data = [['low'], ['medium'], ['high']]
encoder = OrdinalEncoder()
encoded_data = encoder.fit_transform(data)
print(encoded_data)
[[1.]
[2.]
[0.]]
# 使用OneHotEncoder实现
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
data = {'ID': [1, 2, 3],
'Color': ['红色', '蓝色', '绿色']}
df = pd.Datafr ame(data)
encoder = OneHotEncoder()
encoded_data = encoder.fit_transform(df[['Color']]).toarray()
df_encoded = pd.Datafr ame(encoded_data, columns=encoder.get_feature_names(['Color']))
df = pd.concat([df, df_encoded], axis=1)
df
| ID | Color | Color_红色 | Color_绿色 | Color_蓝色 | |
|---|---|---|---|---|---|
| 0 | 1 | 红色 | 1.0 | 0.0 | 0.0 |
| 1 | 2 | 蓝色 | 0.0 | 0.0 | 1.0 |
| 2 | 3 | 绿色 | 0.0 | 1.0 | 0.0 |
# 使用OneHotEncoder实现
# 使用get_dummies实现
data = {'ID': [1, 2, 3],
'Color': ['红色', '蓝色', '绿色']}
df = pd.Datafr ame(data)
dummies = pd.get_dummies(df['Color'], prefix='Color')
df = pd.concat([df, dummies], axis=1)
df
| ID | Color | Color_红色 | Color_绿色 | Color_蓝色 | |
|---|---|---|---|---|---|
| 0 | 1 | 红色 | 1 | 0 | 0 |
| 1 | 2 | 蓝色 | 0 | 0 | 1 |
| 2 | 3 | 绿色 | 0 | 1 | 0 |
df_1 = pd.get_dummies(df['Color'], prefix='Color',drop_first=True)
df_1
| Color_绿色 | Color_蓝色 | |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 1 | 0 |
from sklearn.preprocessing import LabelEncoder
data = [1,5,67,100]
encoder = LabelEncoder()
encoded_data = encoder.fit(data).transform([1,1,100,67,5])
print(encoded_data)
[0 0 3 2 1]
# import category_encoders as ce
# data = ['A', 'B', 'C']
# encoder = ce.BinaryEncoder(cols=['category'])
# encoded_data = encoder.fit_transform(data)
# print(encoded_data)
from sklearn.preprocessing import LabelEncoder
data = ['cat', 'dog', 'rabbit']
encoder = LabelEncoder()
encoded_data = encoder.fit_transform(data)
print(encoded_data)
[0 1 2]
from sklearn.preprocessing import LabelEncoder
data = [1,5,67,100]
encoder = LabelEncoder()
encoded_data = encoder.fit(data).transform([1,1,100,67,5])
print(encoded_data)
[0 0 3 2 1]
数值型特征工程小结:
除了前面常见的统计特征包括平均值、方差、最大值、最小值、中位数、偏度、峰度等。还有一部分特征是业务统计特征,这部分需要结合到业务场景做具体统计,可以帮助我们更好地理解数据的分布和特点,为后续的模型训练和预测提供有用的信息。
另外,组合特征是指将原始特征进行组合,生成新的特征。通过组合不同的特征,可以发现特征之间的关联性,提高模型的表现。常见的组合特征包括特征相加、相乘、相除、取平均值等操作。通过合理地组合特征,可以提高模型的泛化能力和预测准确度。
综合利用业务的统计特征和组合特征可以帮助我们更好地挖掘数据的潜在信息,提高模型的性能和效果。在特征工程的过程中,需要根据具体的问题和数据特点来选择合适的统计特征和组合特征,从而提高模型的预测能力。
再比如,我们把category A和B替换成user ,item
import pandas as pd
import numpy as np
from sklearn.preprocessing import FunctionTransformer
from scipy.stats import boxcox
# 创建一个包含数值特征的数据集
data = {
'A': [1, 2, 3, 4, 5,6,7,8,9,10]
}
df = pd.Datafr ame(data)
df
| A | |
|---|---|
| 0 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 5 |
| 5 | 6 |
| 6 | 7 |
| 7 | 8 |
| 8 | 9 |
| 9 | 10 |
# 对数变换
np.log2(df['A'])
0 0.000000
1 1.000000
2 1.584963
3 2.000000
4 2.321928
5 2.584963
6 2.807355
7 3.000000
8 3.169925
9 3.321928
Name: A, dtype: float64
# 指数变换
np.exp(df['A'])
0 2.718282
1 7.389056
2 20.085537
3 54.598150
4 148.413159
5 403.428793
6 1096.633158
7 2980.957987
8 8103.083928
9 22026.465795
Name: A, dtype: float64
Box-Cox变换的数学公式如下:
对于输入数据 x,Box-Cox变换的公式为:
y(λ)={λxλ−1,log(x),if λ̸=0if λ=0
其中,λ 是Box-Cox变换的参数。在实际应用中,通常会通过最大似然估计等方法来确定最优的λ值。
# Box-Cox变换
from sklearn.preprocessing import FunctionTransformer
from scipy.stats import boxcox
boxcox_features= boxcox(df['A'])
boxcox_features[0]
array([0. , 0.89952679, 1.67649212, 2.38322967, 3.04195194,
3.66477652, 4.25925123, 4.83048782, 5.38215513, 5.91700147])
九层之台,起于累土;千里之行,始于足下——《道德经》。诸位加油,我们下个系列见!
扫码加好友,拉您进群




收藏
