提升你的数据操作技能
原文:
towardsdatascience.com/turbocharge-your-data-manipulation-skills-3a26897ad6a
解锁 pandas groupby
、apply
和 transform
的强大功能
·发布于 Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 21 日
–
图片由 Kier in Sight 提供,来源于 Unsplash
在一个竞争激烈且数据丰富的世界中,理解分段行为是提供量身定制的洞察和产品服务的关键。
无论是通过描述性统计了解分段趋势,还是通过将分段特征纳入机器学习模型等更细致的方法,都会涉及到一定的数据操作。
幸运的是,pandas 提供了高度多功能的功能,使我们能够轻松应对多种数据分段所需的大量操作。通过一些示例,我们将演示:
-
groupby
操作——它是什么、如何工作以及返回什么。 -
如何使用
apply
与groupby
来应用更复杂和特殊的转换。 -
使用
groupby
和transform
将groupby
和apply
的魔力映射回原始数据形状。 -
一些我在时间中积累的小贴士和技巧。
让我们开始吧——首先,获取一些可以操作的数据。
数据
这次我们将使用从一组消费信用卡¹中收集的信息。
除了列名更改和我应用的一些格式更改之外,数据几乎保持不变——预览如下:
图片由作者提供
我们有各种分类特征(如教育水平)与数值特征(如客户年龄)混合在一起。数据集的宽度超出了上面快照的范围,因此如果下面出现新的和有趣的特征,请不要感到惊讶。
分组
首先是 groupby
。
来自文档²:
groupby
操作涉及将对象拆分、应用函数和合并结果的组合。这可以用来对大量数据进行分组,并对这些组进行计算操作。
这是一个相当好的解释。我也发现将 groupby
想象成一种 for 循环的方式很有帮助。
-
将数据分成不同的组。或者,通过参考给定的分组“规则”来对数据进行分组。
-
对每个组应用指定的函数。这可以是像平均值这样的简单计算,或更为复杂和奇特的计算(稍后会详细讨论)。
-
汇总结果。
现在可能有点模糊,所以让我们通过几个例子来详细了解。
一个起点 — 确定每种客户类型的平均信用额度,按性别分组:
# average credit limit for customer type x gender
segments = ['attrition_flag','gender']
df.groupby(segments)['credit_limit'].mean().round().to_frame()
作者提供的图片
非常简单,柠檬groupby
。
现在我们暂时忽略信用额度中的性别差异,专注于底层代码。pandas
为我们做的是将 DataFrame 分成 attrition_flag x gender
组,计算每组的平均 credit_limit
,并汇总结果。
如果我们想扩展分组以包含教育水平,并且还计算平均信用利用率呢?这并不复杂:
# extended segmentation and feature selection
segments = ['attrition_flag','education_level','gender']
features = ['credit_limit','avg_utilization_ratio']
df.groupby(segments)[features].mean()
作者提供的图片
更改应用于每个组的计算也是简单的。例如,如果我们想计算每个分组中的观察次数,可以使用 size
:
# group counts
segments = ['attrition_flag','gender']
feature = 'customer_age'
df.groupby(segments)[feature].size().to_frame(name = 'group_size')
作者提供的图片
这引导我们进入 groupby
语句的一个更一般的配方:
作者提供的图片
… 在这里,我们可以根据具体的使用情况定制segments
、features
和 function
。我故意没有详细说明 function
实际上是什么 — 稍后我们会看到原因 — 但这个一般思路在大多数情况下是成立的。
首先,让我们讨论一些事情。
-
到目前为止,我们使用了两个内置的
pandas
函数 —mean
和size
进行了非常基础的计算。当然,还有许多其他有用的函数可以使用,如中位数和标准差。稍后,我们将看到如何使用更复杂的函数、自定义函数和来自其他包的函数。 -
按照我们使用的
groupby
语句返回的对象,其列名与原始 DataFrame 相同。你很可能会遇到数据集看起来相似但信息完全不同的情况 — 不要被迷惑。合理的命名约定或代码结构可以帮助解决这个问题,以及适当地重命名列。 -
到目前为止,我们在多个列上使用了相同的函数。如果你想对不同的列应用不同的函数,或者在同一个 groupby 语句中对相同的列应用不同的函数,我建议你查看
agg
功能³。它还提供了一种通过元组定义计算结果名称的巧妙方法。
现在我们对 groupby
有了基本的了解,我们可以继续看看如何开始使用更多的高级函数。
看到我刚才做了什么了吗?真有趣。
专业提示:在 groupby 语句中使用单个特征将返回一个 Series,Jupyter 会将其呈现为一堆数字。 to_frame
将 Series 转换为 DataFrame,Jupyter 会以更美观的方式输出。
Apply
我们之前看到的“标准”函数只能带我们到这一步——这也是标准函数的真正意义所在。
如果我们需要做任何高级或定制的操作,或者依赖于其他包中的功能,我们需要将 groupby
与 apply
结合使用。
apply
语句相当直观,并且在文档中有简明的描述⁴:
在 DataFrame 的一个轴上应用函数。
传递给函数的对象是 Series 对象,其索引可以是 DataFrame 的索引(
axis=0
)或 DataFrame 的列(axis=1
)。
让我们做一些使用定制函数对分组进行操作的例子。
我们将开始做一些不同的计算:通过将组的最大值与组的最小值的比例来缩放组的中位数;我们将对每种类型的客户按性别进行此操作。
# define the bespoke function
def scaled_median(s):
# calculates Series median x Series maximum / Series minimum
return s.median() * s.max() / s.min()
# apply it to data
segments = ['attrition_flag','gender']
feature = 'customer_age'
df.groupby(segments)[feature].apply(scaled_median).to_frame(name='result')
作者提供的图片
如果你感到特别 Pythonic,可以在 apply
语句中使用 lambda 函数。代码示例如下:
apply(lambda u: u.median() * u.max() / u.min())
那么,如何将自定义函数应用于多个特征(列)呢?这需要更多的思考——我们用一个例子来演示。
组织有一个启发式方法,用于指示客户对信用优惠的可能接受度。这个启发式方法——我们称之为“胃口评分”——基于婚姻状况、性别、赡养人数以及平均信用利用率。我们希望了解每种客户类型和性别的平均胃口评分。
从代码的角度来看,可以是这样的:
# maps to convert categorical to numeric
status_mapper = {'Married':1.1, 'Single':1, 'Unknown':0.8, 'Divorced':0.9}
gender_mapper = {'F':1.1, 'M':1}
# define the appetite score function
def cohort_score(dataframe):
score = (
dataframe['marital_status'].map(status_mapper).astype(int)
* dataframe['gender'].map(gender_mapper).astype(int)
* (1 + dataframe['dependent_count'])
* dataframe['avg_utilization_ratio']
).mean()
return score
# apply
segments = ['attrition_flag','gender']
df.groupby(segments).apply(cohort_score).to_frame(name = 'score')
… 这会给出:
作者提供的图片
有几点需要注意。
我们使用字典和 map
将婚姻状况和性别转换为数值,以便在启发式方法中进行计算,并记得适当转换数据类型。
我们不会明确选择用于启发式方法的特征。这实际上是一个巧妙的技巧,我之前没有详细提到——我们可以将 apply
同时用于 Series 和 DataFrames。在这种情况下,由于我们没有指定使用哪些特征,我们实际上是将分组后的 DataFrame 传递给 cohort_score
函数,该函数再进一步过滤出必要的列。
均值的计算已经融入到函数中。现在可以说这有点作弊——在现实生活中,我们可能希望建立一个在个体水平上计算胃口分数的启发式方法,然后从结果中创建组统计数据。这实际上与两个其他概念很好地结合起来:使用来自其他包的函数和使用 lambda 函数。
现在就这样做——定义一个不返回平均值的启发式方法,使用numpy
计算组平均值,并将其包装在一个 lambda 函数中。
# get numpy
import numpy as np
# define the appetite score function for an individual
def cohort_score(series):
score = (
series['marital_status'].map(status_mapper).astype(int)
* series['gender'].map(gender_mapper).astype(int)
* (1 + series['dependent_count'])
* series['avg_utilization_ratio']
)
return score
# apply
segments = ['attrition_flag','gender']
d = df.groupby(segments).apply(lambda u: np.mean(cohort_score(u)))
d.to_frame(name = 'score')
结果如下:
图片由作者提供
看起来很熟悉吗?确实如此,因为这是与上面的计算完全相同的结果。
一点需要注意的是使用来自其他包的函数有多么简单。在这种情况下,我们使用了numpy
的均值计算,但概念是相同的——将函数传递给apply
,并参考其来源的包(当然要记得导入包!)。
转换
你现在可能已经注意到,groupby
返回的是组统计数据,呈现于组级别。如果这正是你所需要的——例如,用于可视化或汇总——那就完美了,但也有需要将组结果“映射”回个别观察值的情况。
我相信还有许多其他应用,但这种方法在特征工程中非常有用。假设你在预测房价——了解一个单独的房子与同一邻里的“平均”房子相比有多大,不是很好吗?
幸运的是,pandas 提供了简便的功能,允许我们做到这一点,无需任何连接或合并。这就是 transform 的魔力,尽管文档对此有所低估⁵:
对 self 调用
func
生成一个与 self 具有相同轴形状的 DataFrame
也许一个示例会很有用!让我们看看如何再次应用缩放中位数函数,但这次使用 transform 在数据中创建一个新列,将组结果映射回每个单独的观察值:
# get example data
df_example = df[['attrition_flag','gender','customer_age']].copy()
# apply it to data
segments = ['attrition_flag','gender']
feature = 'customer_age'
df_example['scaled_median_age'] = (
df_example
.groupby(segments)[feature]
.transform(scaled_median)
)
df_example
结果如下:
图片由作者提供
在这里我们看到相同的组值是如何生成的,但这些值也会映射回大小并包含在 DataFrame 中。
提示与技巧
现在我们已经掌握了groupby
、apply
和transform
,让我们看看一些可能有用的提示和技巧。
apply
的多才多艺
apply
函数非常多才多艺。我们已经看到如何在groupby
操作中应用它,但这并不总是必要的——也就是说,你可以直接在 DataFrame 上使用apply
。
轴与应用
当我开始使用apply
时,我遇到了(可能超过)我应得的错误信息。大多数时候,这都是因为我没有完全理解函数应用的轴——即我错误地指定了是否将函数应用于行或列。
因此,如果你确定你的函数有效但pandas
仍然不合作,我建议你查看一下apply
中的axis
参数。
使用内置函数与 apply
正如我们上面所见,我们可以直接在分组的 DataFrames 上使用内置的pandas
函数。
我们也可以在apply
中使用相同的内置函数,不过我们必须通过其名称来引用该函数。例如,使用内置标准差函数可能会是这样的:
segments = ['attrition_flag','gender']
feature = 'customer_age'
df.groupby(segments)[feature].apply('std')
正如大多数 Python 相关的事情一样,做事的方法不止一种。然而,像这样在apply
语句中使用内置函数实际上可能比直接使用它们更慢。
缺失数据插补
我们之前提到了apply
和transform
在特征工程中的有用性。它们在数据清洗中同样有效,特别是当你想要使用分段统计来插补缺失值时。
这是一个例子,展示了我们如何使用客户类型和婚姻状况的中位数客户年龄来插补缺失的客户年龄值。
# example data frame
df_example = df[['attrition_flag','marital_status','customer_age']].copy()
# randomly create missing values
df_example['customer_age'] = np.where(
np.random.random(size = len(df)) <= 0.5,
df['customer_age'],
np.nan
)
# impute using median
df_example['customer_age_imputed'] = (
df_example
.groupby(['attrition_flag','marital_status'])['customer_age']
.transform('median')
)
df_example
图片由作者提供
这显然是一个比较牵强的例子,但这种逻辑同样适用于其他用例,尤其是当组相对同质时。
我们暂时就到这里,并做一个简要的回顾。
总结
我们已经覆盖了相当多的内容。
首先,我们查看了groupby
操作——它是什么、如何工作以及它返回什么。
一旦我们习惯了使用基本函数和groupby
,我们就进一步拓展,结合使用apply
和groupby
来使用定制函数。
尽管在组级别进行统计很有用,但我们讨论了为什么我们可能想要将这些组统计映射回每个个体观察值。我们展示了如何使用groupby
和transform
来实现这一点。
我还分享了一些我随着时间学到的“技巧和窍门”——希望它们能让你在使用groupby
、apply
和transform
时更顺利。
现在你已经看到了一些示例,我鼓励你自己尝试——实践是完美的关键。
当然,我们可以做一些额外的工作,将我们的分析和展示提升到一个新水平。首先是适当格式化 DataFrames:
使你的 pandas DataFrames 准备好展示的简单格式化技巧
towardsdatascience.com
…第二个是生成准备好展示的图表:
使 matplotlib 图表准备好展示的简单技巧
towardsdatascience.com
一如既往,我希望你和我一样享受阅读这篇文章的过程。也如既往,我很乐意了解新的技巧和窍门,请随时在评论中分享!
参考文献和资源
转身面对陌生事物
如何利用异常检测方法来改善您的监督学习
·
关注 发布于 Towards Data Science ·7 min read·Aug 11, 2023
–
照片由 Stefan Fluck 提供于 Unsplash
传统的预测分析提供了两种视角来查看大多数问题:点估计和分类。现代数据科学主要关注后者,将许多问题框定为分类(想想保险公司如何试图识别哪些客户会产生高成本,而不是预测每个客户的成本;或者营销人员如何更感兴趣于哪些广告会带来正的 ROI,而不是预测每个广告的具体 ROI)。鉴于此,数据科学家已经开发并熟悉了多种分类方法,从逻辑回归到基于树和森林的方法,再到神经网络。然而,有一个问题——许多这些方法在数据结果类别大致平衡时效果最佳,而现实世界中的应用往往无法提供这种平衡。在这篇文章中,我将展示如何使用异常检测方法来缓解监督学习中不平衡结果类别所带来的问题。
设想一下,我计划从我的家乡宾夕法尼亚州的匹兹堡出发。我对去哪里不挑剔,但我真的希望避免旅行中的问题,比如航班被取消、转机,甚至是严重延误。分类模型可以帮助我识别哪些航班可能会遇到问题,而Kaggle有一些数据可以帮助我构建这样一个模型。
我从读取数据并制定自己对糟糕航班的定义开始——任何被取消、转机或到达延迟超过 30 分钟的航班。
import pandas as pd
import numpy as np
from sklearn.compose import make_column_transformer
from sklearn.ensemble import GradientBoostingClassifier, IsolationForest
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
# read in data
airlines2022 = pd.read_csv('myPath/Combined_Flights_2022.csv')
print(airlines2022.shape)
# (4078318, 61)
# subset by my target departure city
airlines2022PIT = airlines2022[airlines2022.Origin == 'PIT']
print(airlines2022PIT.shape)
# (24078, 61)
# combine cancellations, diversions, and 30+ minute delays into one Bad Flight outcome
airlines2022PIT = airlines2022PIT.assign(arrDel30 = airlines2022PIT['ArrDelayMinutes'] >= 30)
airlines2022PIT = (airlines2022PIT
.assign(badFlight = 1 * (airlines2022PIT.Cancelled
+ airlines2022PIT.Diverted
+ airlines2022PIT.arrDel30))
)
print(airlines2022PIT.badFlight.mean())
# 0.15873411412908048
大约 15%的航班属于我的“糟糕航班”类别。这还不够低,不能传统地认为这是一个异常检测问题,但也足够低,以至于监督方法的表现可能没有我希望的那么好。尽管如此,我会开始构建一个简单的梯度提升树模型,以预测航班是否会遇到我想要避免的问题。
首先,我需要确定在我的模型中使用哪些特征。为了这个例子,我将选择一些看起来有前景的特征进行建模;实际上,特征选择是任何数据科学项目中非常重要的一部分。这里大多数可用的特征是分类特征,需要在数据准备阶段进行编码;城市之间的距离需要进行缩放。
# categorize columns by feature type
toFactor = ['Airline', 'Dest', 'Month', 'DayOfWeek'
, 'Marketing_Airline_Network', 'Operating_Airline']
toScale = ['Distance']
# drop fields that don't look helpful for prediction
airlines2022PIT = airlines2022PIT[toFactor + toScale + ['badFlight']]
print(airlines2022PIT.shape)
# (24078, 8)
# split original training data into training and validation sets
train, test = train_test_split(airlines2022PIT
, test_size = 0.2
, random_state = 412)
print(train.shape)
# (19262, 8)
print(test.shape)
# (4816, 8)
# manually scale distance feature
mn = train.Distance.min()
rng = train.Distance.max() - train.Distance.min()
train = train.assign(Distance_sc = (train.Distance - mn) / rng)
test = test.assign(Distance_sc = (test.Distance - mn) / rng)
train.drop('Distance', axis = 1, inplace = True)
test.drop('Distance', axis = 1, inplace = True)
# make an encoder
enc = make_column_transformer(
(OneHotEncoder(min_frequency = 0.025, handle_unknown = 'ignore'), toFactor)
, remainder = 'passthrough'
, sparse_threshold = 0)
# apply it to the training dataset
train_enc = enc.fit_transform(train)
# convert it back to a Pandas dataframe for ease of use
train_enc_pd = pd.DataFrame(train_enc, columns = enc.get_feature_names_out())
# encode the test set in the same way
test_enc = enc.transform(test)
test_enc_pd = pd.DataFrame(test_enc, columns = enc.get_feature_names_out())
树模型的开发和调优可能会单独成篇,所以我在这里不详细讨论。我使用了初始模型的特征重要性评分进行了一些反向特征选择,并从那里对模型进行了调优。最终模型在识别延误、取消或转机的航班方面表现良好。
# feature selection - drop low importance terms|
lowimp = ['onehotencoder__Airline_Delta Air Lines Inc.'
, 'onehotencoder__Dest_IAD'
, 'onehotencoder__Operating_Airline_AA'
, 'onehotencoder__Airline_American Airlines Inc.'
, 'onehotencoder__Airline_Comair Inc.'
, 'onehotencoder__Airline_Southwest Airlines Co.'
, 'onehotencoder__Airline_Spirit Air Lines'
, 'onehotencoder__Airline_United Air Lines Inc.'
, 'onehotencoder__Airline_infrequent_sklearn'
, 'onehotencoder__Dest_ATL'
, 'onehotencoder__Dest_BOS'
, 'onehotencoder__Dest_BWI'
, 'onehotencoder__Dest_CLT'
, 'onehotencoder__Dest_DCA'
, 'onehotencoder__Dest_DEN'
, 'onehotencoder__Dest_DFW'
, 'onehotencoder__Dest_DTW'
, 'onehotencoder__Dest_JFK'
, 'onehotencoder__Dest_MDW'
, 'onehotencoder__Dest_MSP'
, 'onehotencoder__Dest_ORD'
, 'onehotencoder__Dest_PHL'
, 'onehotencoder__Dest_infrequent_sklearn'
, 'onehotencoder__Marketing_Airline_Network_AA'
, 'onehotencoder__Marketing_Airline_Network_DL'
, 'onehotencoder__Marketing_Airline_Network_G4'
, 'onehotencoder__Marketing_Airline_Network_NK'
, 'onehotencoder__Marketing_Airline_Network_WN'
, 'onehotencoder__Marketing_Airline_Network_infrequent_sklearn'
, 'onehotencoder__Operating_Airline_9E'
, 'onehotencoder__Operating_Airline_DL'
, 'onehotencoder__Operating_Airline_NK'
, 'onehotencoder__Operating_Airline_OH'
, 'onehotencoder__Operating_Airline_OO'
, 'onehotencoder__Operating_Airline_UA'
, 'onehotencoder__Operating_Airline_WN'
, 'onehotencoder__Operating_Airline_infrequent_sklearn']
lowimp = [x for x in lowimp if x in train_enc_pd.columns]
train_enc_pd = train_enc_pd.drop(lowimp, axis = 1)
test_enc_pd = test_enc_pd.drop(lowimp, axis = 1)
# separate potential predictors from outcome
train_x = train_enc_pd.drop('remainder__badFlight', axis = 1); train_y = train_enc_pd['remainder__badFlight']
test_x = test_enc_pd.drop('remainder__badFlight', axis = 1); test_y = test_enc_pd['remainder__badFlight']
print(train_x.shape)
print(test_x.shape)
# (19262, 25)
# (4816, 25)
# build model
gbt = GradientBoostingClassifier(learning_rate = 0.1
, n_estimators = 100
, subsample = 0.7
, max_depth = 5
, random_state = 412)
# fit it to the training data
gbt.fit(train_x, train_y)
# calculate the probability scores for each test observation
gbtPreds1Test = gbt.predict_proba(test_x)[:,1]
# use a custom threshold to convert these to binary scores
gbtThresh = np.percentile(gbtPreds1Test, 100 * (1 - obsRate))
gbtPredsCTest = 1 * (gbtPreds1Test > gbtThresh)
# check accuracy of model
acc = accuracy_score(gbtPredsCTest, test_y)
print(acc)
# 0.7742940199335548
# check lift
topDecile = test_y[gbtPreds1Test > np.percentile(gbtPreds1Test, 90)]
lift = sum(topDecile) / len(topDecile) / test_y.mean()
print(lift)
# 1.8591454794381614
# view confusion matrix
cm = (confusion_matrix(gbtPredsCTest, test_y) / len(test_y)).round(2)
print(cm)
# [[0.73 0.11]
# [0.12 0.04]]
但它可以更好吗?也许使用其他方法可以学习到更多关于航班模式的信息。隔离森林是一种基于树的异常检测方法。它通过从输入数据集中迭代地选择一个随机特征,以及该特征范围内的一个随机分裂点来工作。它继续以这种方式构建树,直到输入数据集中的每个观察值都被分裂成自己的叶子。其理念是,异常值或数据离群点与其他观察值不同,因此用这种挑选和分裂过程隔离它们更容易。因此,只需几轮挑选和分裂就被隔离的观察值被视为异常值,而那些无法迅速与邻近点分离的则不是。
隔离森林是一种无监督方法,因此不能用来识别数据科学家自己选择的特定异常类型(例如取消、转移或非常晚的航班)。不过,它在识别与其他观察值在某些未指定方式上不同的观察值时可能仍然有用(例如,在某些方面不同的航班)。
# build an isolation forest
isf = IsolationForest(n_estimators = 800
, max_samples = 0.15
, max_features = 0.1
, random_state = 412)
# fit it to the same training data
isf.fit(train_x)
# calculate the anomaly score of each test observation (lower values are more anomalous)
isfPreds1Test = isf.score_samples(test_x)
# use a custom threshold to convert these to binary scores
isfThresh = np.percentile(isfPreds1Test, 100 * (obsRate / 2))
isfPredsCTest = 1 * (isfPreds1Test < isfThresh)
将异常分数与监督模型分数结合提供了额外的见解。
# combine predictions, anomaly scores, and survival data
comb = pd.concat([pd.Series(gbtPredsCTest), pd.Series(isfPredsCTest), pd.Series(test_y)]
, keys = ['Prediction', 'Outlier', 'badFlight']
, axis = 1)
comb = comb.assign(Correct = 1 * (comb.badFlight == comb.Prediction))
print(comb.mean())
#Prediction 0.159676
#Outlier 0.079942
#badFlight 0.153239
#Correct 0.774294
#dtype: float64
# better accuracy in majority class
print(comb.groupby('badFlight').agg(accuracy = ('Correct', 'mean')))
# accuracy
#badFlight
#0.0 0.862923
#1.0 0.284553
# more bad flights among outliers
print(comb.groupby('Outlier').agg(badFlightRate = ('badFlight', 'mean')))
# badFlightRate
#Outlier
#0 0.148951
#1 0.202597
这里有几点需要注意。一点是监督模型在预测“好”航班方面优于“坏”航班——这是稀有事件预测中的常见动态,因此查看精准度和召回率等指标而不仅仅是简单的准确性很重要。更有趣的是,“坏航班”率在被隔离森林分类为异常的航班中几乎高出 1.5 倍。尽管隔离森林是一种无监督方法,并且一般识别的是非典型航班,而不是那些在我想要避免的特定方式上非典型的航班,这似乎对监督模型来说是有价值的信息。二元异常标志已经是一个很好的格式,可以用作我的监督模型中的预测因子,因此我将其纳入并看看是否能提高模型性能。
# build a second model with outlier labels as input features
isfPreds1Train = isf.score_samples(train_x)
isfPredsCTrain = 1 * (isfPreds1Train < isfThresh)
mn = isfPreds1Train.min(); rng = isfPreds1Train.max() - isfPreds1Train.min()
isfPreds1SCTrain = (isfPreds1Train - mn) / rng
isfPreds1SCTest = (isfPreds1Test - mn) / rng
train_2_x = (pd.concat([train_x, pd.Series(isfPredsCTrain)]
, axis = 1)
.rename(columns = {0:'isfPreds1'}))
test_2_x = (pd.concat([test_x, pd.Series(isfPredsCTest)]
, axis = 1)
.rename(columns = {0:'isfPreds1'}))
# build model
gbt2 = GradientBoostingClassifier(learning_rate = 0.1
, n_estimators = 100
, subsample = 0.7
, max_depth = 5
, random_state = 412)
# fit it to the training data
gbt2.fit(train_2_x, train_y)
# calculate the probability scores for each test observation
gbt2Preds1Test = gbt2.predict_proba(test_2_x)[:,1]
# use a custom threshold to convert these to binary scores
gbtThresh = np.percentile(gbt2Preds1Test, 100 * (1 - obsRate))
gbt2PredsCTest = 1 * (gbt2Preds1Test > gbtThresh)
# check accuracy of model
acc = accuracy_score(gbt2PredsCTest, test_y)
print(acc)
#0.7796926910299004
# check lift
topDecile = test_y[gbt2Preds1Test > np.percentile(gbt2Preds1Test, 90)]
lift = sum(topDecile) / len(topDecile) / test_y.mean()
print(lift)
#1.9138477764819217
# view confusion matrix
cm = (confusion_matrix(gbt2PredsCTest, test_y) / len(test_y)).round(2)
print(cm)
#[[0.73 0.11]
# [0.11 0.05]]
将异常值状态作为预测因子纳入监督模型确实可以提升其前十百分位的提升几分。看来“异常”在某种未定义的方式上与我期望的结果有足够的相关性,从而提供了预测能力。
当然,这种特殊情况的有效性是有限的。它并不适用于所有不平衡分类问题,如果解释性对最终产品非常重要,这种方法也不特别有帮助。不过,这种替代框架可以为各种分类问题提供有益的见解,值得一试。
将 GPT-4 转变为扑克教练
原文:
towardsdatascience.com/turn-gpt-4-into-a-poker-coach-4a28ba5e9541
超越聊天机器人界限的创造力释放
·发表于 Towards Data Science ·阅读时长 13 分钟·2023 年 5 月 5 日
–
照片由 Michał Parzuchowski 提供,刊登在 Unsplash
在这篇文章中,我们不会讨论 LLM 模型如何通过法律考试或替代开发人员。
我们不会讨论如何优化提示以使 GPT 生成动机信或营销内容。
和许多人一样,我认为像 GPT4 这样的 LLM 的出现是一场小小的革命,将会涌现出许多新应用。我还认为我们不应该将其使用仅限于简单的“聊天机器人助手”,而是通过适当的后端和用户体验,这些模型可以被用来实现令人惊叹的下一层应用。
这就是为什么在本文中,我们将稍微跳出框框,围绕 GPT API 创建一个真正的应用程序,而这些 API 不能仅通过聊天机器人接口访问,以及如何通过适当的应用程序设计提供更好的用户体验。
设置一些背景
在商业中利用 GPT4
自发布以来,我玩了很多 GPT4,我认为使用该模型生成业务的主要用例大致分为两类。
第一种方式是使用 GPT4 生成静态内容。假设你想写一本关于特定主题(例如意大利食物)的烹饪书。你可以做详细的提示,从 GPT 生成一些食谱,自己试用,然后将你喜欢的食谱整合到你的书中。在这种情况下,“提示”将有一个固定成本,一旦食谱生成后,你就不再需要 GPT。这种用例可以有很多变体(营销内容、网站内容,甚至生成其他用途的数据集),但如果我们想专注于 AI 导向的应用程序,它就不那么有趣了。
生成内容的逻辑在应用程序之外,作者插图
第二个用例是通过你设计的界面进行实时提示。回到烹饪领域:我们可以想象一个合适的界面,用户可以选择一些食材、一个特色菜,并要求应用程序直接生成食谱。与第一个案例不同,生成的内容可能是无限的,更好地满足用户的需求。
在这种情况下,用户通过精心设计的用户体验直接与 LLM 互动,这将生成提示和内容,作者插图。
缺点是,LLM 的调用次数可能是无限的,并随着用户数量的增加而增长,与之前 LLM 调用次数有限且可控的情况不同。这意味着你必须妥善设计你的商业模式,并在商业模式中仔细考虑提示费用。
在我写这些文字时,GPT4 的“提示”费用为 0.03$/1000 tokens(包括请求和回答的 tokens)。这看起来不多,但如果不加注意,费用可能会迅速增加。为了解决这个问题,你可以例如根据提示的数量向用户提供订阅服务,或者限制每个用户的提示数量(通过登录系统等)。我们将在本文后面详细讨论定价问题。
为什么围绕扑克构建一个用例?
我考虑了很长时间寻找完美的 LLM 用例。
首先,扑克分析理论上是 LLM 表现良好的领域。实际上,每一手扑克都可以被转换为标准化的简单文本,描述手牌的演变。例如,下面的手牌描述了一个序列,其中“player1”在“flop”动作后对“player2”的下注进行了加注,从而赢得了底池。
Seat 2: player1(€5.17 in chips)
Seat 3: player3(€5 in chips)
Seat 4: player2(€5 in chips)
player1: posts small blind €0.02
player2: posts big blind €0.05
*** HOLE CARDS ***
Dealt to player2[4s 4c]
player2: raises €0.10 to €0.15
player1: calls €0.13
player3: folds
*** FLOP *** [Th 7h Td]
player1: checks
player2: bets €0.20
player1: raises €0.30 to €0.50
player2: folds
Uncalled bet (€0.30) returned to player1
player1collected €0.71 from pot
这种标准化很重要,因为它会使开发变得更加简单。我们将能够模拟手牌,将其转换为这种提示消息,并“强制”LLM 的回答继续序列。
许多理论内容可以在书籍、网上等地方找到……这使得 GPT 有可能“学习”到有关游戏和良好策略的知识。
此外,很多附加价值将来自于应用引擎和用户体验,而不仅仅是 LLM 本身(例如,我们需要设计自己的扑克引擎来模拟游戏),这将使得应用程序更难以复制,或者仅仅通过 GPTChat“再现”。
最终,这种用例可能很好地适应上述第二种情况,其中 LLM 和良好的用户体验可以为用户带来全新的体验。我们可以设想我们的应用程序再次与真实用户进行对战,分析手牌并提供评分和改进领域。每次请求的费用不应该成为问题,因为扑克学习者习惯于为这种服务付费,所以在这个特定用例中,可能会采用“按使用付费”的模式(例如与之前提到的食谱应用程序不同)。
关于 GPT4 API
我决定围绕GPT4 API来构建这篇文章,因为它与 GPT3.5 相比更为精准。OpenAI 提供了一个简单的 Python 包装器,可以用来发送输入并从模型中接收输出。例如:
import openai
openai.api_key = os.environ['OPENAI_KEY']
completion = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "system", "content": preprompt_message},
{"role": "user", "content": user_message}]
)
completion.choices[0].message["content"]
使用“系统”角色的“预提示”将帮助模型按你希望的方式行动(你可以使用它来强制执行响应格式),角色“用户”用于添加用户的消息。在我们的案例中,这些消息将由我们的引擎预先设计,例如,传递特定的扑克手牌以完成。
请注意,“系统”、“用户”和答案中的所有令牌都计入价格方案,因此优化这些查询至关重要。
初步探索
首先要评估的是 GPT 在与扑克相关的不同任务中的总体质量。我希望快速评估其完成应用程序核心任务的能力。
继续进行手牌并提供现实的行动和解释
应用程序的核心理念之一将是让一个或多个 GPT 实例与用户对战。我们想要评估 GPT 的表现如何。为此,我提供了我打过的牌局样本,并希望获得关于下一步行动及其解释的分析。
测试示例:
preprompt = """You are my poker coach, I want you to play the next move and
justify simply your action based on your expertise"""
message = """Stacks:
UTG ($54.93) 110bb
UTG+1 ($55.87) 112bb
CO ($61.62) 123bb
BTN ($60.96) 122bb
SB ($25.54) 51bb
BB Hero ($61.50) 123bb
Pre-Flop: (0.75, 6 players) Hero is BB :jc: :jd:
2 folds, CO raises to $1.50, BT raises to $6, 1 fold,
Hero raises to $14, CO folds, BT calls $8
Flop: :3s: :2h: :tc: ($29.75, 2 players)
Hero bets $7.50, BT calls $7.50
Turn: :5h: ($44.75, 2)
Hero (?)
"""
completion = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "system", "content": preprompt},
{"role": "user", "content": message}]
)
答案是:
Hero should bet around $22.
Justification:
Our pocket Jacks have improved to top set on the flop, which is a great
advantage for us. With the current board texture, we need to continue
building the pot to extract value from hands like smaller pairs,
flush draws, or straight draws. The turn 5h doesn't
change much about the board, so we should capitalize on our strong hand
by betting around $22 (around half pot).
This will put pressure on our opponent and potentially make them commit
with worse hands if they decide to call or raise.
我进行了几次这样的测试,验证了模型能够合理地进行游戏。
有趣的是,通过修改预提示,我可以控制模型的行为,让它变得更具攻击性或更为保守。
You are a very tight player who tend to under-evaluate
the strengh of your hand
对于相同的动作,会导致 AI 采取不同的行动:
Action: Hero checks.
Justification: Based on my tight playing style and under-evaluation of my hand
strength tendency, I will check in this situation.
By checking, I can control the pot size and potentially induce a bet from BT
if they have a weaker hand. At the same time,
I can also minimize my losses if BT has a stronger hand.
Considering the board is relatively low and it doesn't have a lot of
threatening draws, the check allows me to proceed cautiously but still
have a chance to win the hand later.
这意味着我们可以潜在地编写不同风格的“AI”代码,以模拟具有不同水平和风格的玩家的真实扑克场景。
从完整手牌历史记录中对玩家的动作进行评分
我们可能想要集成到应用程序中的一个功能是能够对用户进行评分。评分始终是进步的良好指标,或者在学习某项技能时针对特定弱点的有效方法。
为了让我们的应用程序正确处理这些信息,我们必须“强制”机器人的回答,以便可以简单地解析并在应用程序中使用(例如对会话或主题进行平均)。
例如,以下提示组合:
Preprompt:
You are a poker coach. I will provide you a hand.
I want you to identify for a given hand key thematics
and rate the relevant thematics /20\.
The thematics you can use are the following:
"preflop","flop","turn","river","aggressivity","sizing", "overall"
You can use one or several of these thematic during your
evaluation depending on what is relevant.
I expect your output to have the following format:
"theme1: X - theme2: Y - ..."
Message:
Stacks:
UTG ($54.93) 110bb
UTG+1 ($55.87) 112bb
CO ($61.62) 123bb
BTN ($60.96) 122bb
SB ($25.54) 51bb
BB Hero ($61.50) 123bb
Pre-Flop: (0.75, 6 players) Hero is BB :jc: :jd:
2 folds, CO raises to $1.50, BT raises to $6, 1 fold, Hero raises to $14, CO folds, BT calls $8
Flop: :3s: :2h: :tc: ($29.75, 2 players)
Hero bets $7.50, BT calls $7.50
Turn: :5h: ($44.75, 2)
Hero bets $9, BT calls $9 River: :9c: ($62.75, 2)
Hero checks, BT goes all-in $30.46, Hero calls $30.46
将生成以下输出:
'preflop: 18 - flop: 16 - turn: 14 - river: 13 - aggressivity: 15 - sizing: 12 - overall: 15'
这有两个好处:
-
我们可以控制模型显示的令牌数量
-
我们可以轻松地将答案解析成字典,以便我们的应用程序使用
将想法转化为具体应用
在验证上述两点后,是时候编写更大的代码来演示整个概念,并查看 LLM 如何集成到一个更大的应用程序中,这与简单的聊天机器人不同。
在本文的背景下,我们将使用相当简单的东西,但它应当能给你一个关于 LLMs 在开箱即用时所有能力的提示。
我们的演示目标是让 GPT 与我们进行牌局,然后,在牌局完全进行后,要求模型根据这局牌仅给出一些改进的提示。
为此,我快速编写了一个简单的扑克引擎,帮助模拟牌局并与 AI 对手对战。我不会在这里详细讨论引擎(这不是本文的重点),而是简单介绍其设计概况。
扑克教练概念验证示意图
扑克引擎的作用是跟踪游戏的总体元数据(玩家数量、筹码、发牌、玩家回合……)。下一个动作(无论是玩家还是 AI)作为标准文本消息如“call”、“fold”或“raise 60”等添加到引擎中,然后解析并转录为引擎运行下一步的新输入。
扑克引擎还将动作序列转录为文本文件,这些文件用于供 AI 使用以决定下一个动作。
将使用两个预提示:一个用于决定下一个动作,另一个用于牌局结束时评价人类玩家的动作。
进行牌局
为了让我们的应用程序理解 GPT 的动作,我们需要确保 GPT 的消息是标准化的。这可以通过预提示来实现。在我的情况下,我期望 GPT 用最多两个词“动作 数量”来回答我,并通过提供一个示例模型来强制这一行为,模型会根据这个简单的玩法预提示进行适应:
I want you to help me improve in poker by playing games.
Use only keywords 'fold', 'call', 'bet', 'check', or 'raise'
with chip amounts.
Example:
'**hand details**
GPT (?)'
Answer: 'raise 40'
我将把这个预提示与扑克引擎生成的实际牌局历史结合起来:
GPT_0 has [Ts, 9s]
SB: hero (25.0 BB)
BB: GPT_0 (25.0 BB)
hero is BTN
hero pay SB (10 chips)
GPT_0 pay BB (20 chips)
hero: raise 50
GPT_0 (?)
这会导致一个合适的答案,可以解析并整合到扑克引擎中,以更新游戏并继续下一个动作:
call 50
应用程序实际运行中
放在快照中,它可能看起来不那么令人印象深刻,所以我录制了一个关于 AI 在笔记本中运行的小视频。在视频中,我还要求模型在采取行动前解释其动作。所有玩家的牌在调试用的小工具中可见,但在实际条件下,它们会对玩家隐藏。当 AI 采取行动时,仅其自己的牌会传递给提示。
我的消息解析器简单地处理“//”分隔符后的操作,我使用了 input()函数来记录用户输入。
GPT 与我对战的一手扑克,作者插图
手动审核
一旦头部完成,系统就会生成整个手牌,然后可以传递给我们的“评估模块”,在这里,LLM 将对手牌进行评分。
hero has [5c Qs]
SB: hero (500 chips)
BB: GPT_0 (500 chips)
hero is BTN
hero pay SB (10 chips)
GPT_0 pay BB (20 chips)
hero: raise 50
GPT_0: call 50
FLOP
POT:100.0
BOARD: [7c 6d Kc]
hero: bet 100
GPT_0: call 100
TURN
POT:200.0
BOARD: [7c 6d Kc 3d]
hero: bet 200
GPT_0: fold
使用先前设计的提示,我们可以得到如下类型的答案,这些答案易于解析:
preflop: 14 - flop: 12 - turn: 15 - aggressivity: 16 - sizing: 13 - overall: 14
这些输出可以轻松解析,并将数据存储在数据库中,从中我们可以提取所有类型的分析,例如根据模型提供的平均评分识别手牌、位置、配置等方面的弱点。
结论
在这一点上,我们通过一个示例证明了以 LLM 为中心的新概念可以在与合适的应用设计结合时出现,这些设计偏离了简单的聊天机器人助手。
当然,上述示例只是众多步骤中的第一步,才能有一个完全准备好的应用程序,但它应该足以引发思考和创造力。
对于开发一个真正的扑克教练应用程序的下一步,我们需要采取几个行动:
-
提示令牌优化:我们应用程序的大部分成本将来自提示定价。优化提示的数量对于降低成本和保持竞争力至关重要。
-
提示内容优化:输出(评分、下一步行动)的质量可能会因你提供给模型的上下文而有所不同。例如,让模型在采取行动之前先进行分析可以显著提高行动的一致性。与真实玩家进行的许多测试和迭代是必需的,以确保输出质量足以满足生产级应用程序的要求。
-
错误处理:即使 LLM 提供的输出大多数情况下符合你的模板,处理模型提供的不符合你的解析器的答案的情况也很重要。与所有功能不同,这部分应用可能会保持不可预测,因此添加额外的控制层以确保由于格式错误或无法回答的问题不会出现错误是很重要的。
-
用户界面:虽然在笔记本中使用文本输入进行探索足以满足本文的需求,但核心元素应该是一个清晰的用户体验,以提升用户体验并使与模型和引擎的交互顺畅。
虽然这类应用程序的价格可能仍然很高,但我相信大规模采用和未来的改进将趋向于降低成本。我们只是这些新技术发展的开始,创造性地利用 LLMs 的潜力,将其转变为超越传统聊天机器人局限性的强大工具,以提供创新和以用户为中心的体验。
将线性回归转变为逻辑回归
原文:
towardsdatascience.com/turn-linear-regression-into-logistic-regression-e088e2408ec9
关于如何从头实现逻辑回归的全面指南
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 3 月 27 日
–
图片由 Rutger Leistra 提供,来自 Unsplash
动机
如果你阅读了我之前关于简单线性回归和多重线性回归的文章,你会了解到线性回归预测的是连续值。但是并非所有现实中的预测问题都与连续值相关。有时我们需要根据特征对对象或数据进行分类。线性回归算法无法解决这些问题。在这种情况下,逻辑回归的必要性就体现出来了。算法的名称‘逻辑回归’中包含了**‘回归’**一词。它是线性回归的改进版本,以便可以预测离散类别值而不是连续值。
因此,本文将解释逻辑回归如何生成从线性回归派生的类别预测值。
目录
-
**是什么让线性回归变成逻辑回归?**
-
**哪个函数起关键作用?**
-
**线性回归如何转变为逻辑回归?**
-
**生成损失函数**
-
**为什么不能使用均方误差作为成本函数?**
-
**用于参数优化的梯度下降**
-
**将所有概念结合起来进行 Python 从头实现**
什么让线性回归变成逻辑回归?
我会带来我之前文章中提到的两个方程,简单线性回归 和 多重线性回归。
第一个是简单线性回归的方程。
我们将通过插入自变量 (x)
的值来获得预测的回归值 (y)
。但我们需要拟合系数斜率(m)
和 y 截距值 c
。
第二个方程式类似于第一个方程,但存在多个自变量 *(x1……..xn)*
,系数 m *(m1…….m0)*
,以及 y 轴截距 m0
。
对于第 1 个和第 2 个方程,如果我们有系数的最佳拟合值,我们可以轻松得到回归值,例如 34,687.93 等等。
但这并不能给我们将连续值转化为离散分类值的直观感受。因此,我们需要一个函数或方法,通过它可以将所有回归值转换为 **[0,1]**
之间的值。在逻辑回归中,我们正是这样做的。我将在下一节中讨论这个函数。
哪个函数起着关键作用?
我不会直接提到函数,而是会逐步解释。 让我们尝试直观了解线性回归和逻辑回归的效果。
回归模型图(图片由作者提供)
看一下上述回归模型图。对角线上的蓝色线是回归线。我们可以通过插入 ***x***
值来预测任何 ***y***
值。尝试制定一个逻辑回归问题。
图片由作者提供
上述数据集有一个特征,‘年龄’, 基于该特征定义目标类别。值 1 表示该人是学生,0 表示该人不是学生。用线性回归预测这样的分类值是不可能的。 如果我们绘制它,会是什么样子呢?让我们看看。
图片由作者提供
星星表示类别的水平(学生与否)。简单地说,回归线并不是预测分类值的合适方法。
在这里,‘S 形’ 函数,名为 ‘‘sigmoid’’ 发挥了作用。
这个函数可以将任何数字转换到 *[0,1]*
之间。我将给你展示一个 sigmoid 函数的编码示例。
创建一个 sigmoid 函数
绘制 sigmoid 图
这个 S 形 的 sigmoid 图形比直线更适合分类问题。随着 x 的增加,y 值从 0 到 1 变化,当 x=0,y=0.5。这是一个很好的函数,我们可以轻松设置一个阈值,例如 0.5。所有大于阈值(0.5)的值将为 1,否则为 0。
是的!终于,我们找到了合适的函数。
线性回归如何转变为逻辑回归?
现在,我们具备了将线性回归转换为逻辑回归的所有条件。让我们把它们放在一起。
在 第一部分,我展示了简单线性回归和多重线性回归的方程。线性回归的值是连续的,可以是任何连续的数值。
但 sigmoid 函数帮助我们产生诸如 0 和 1 的分类值,如 最后一部分 所示。
因此,逻辑回归的方程将如下所示。
符号 σ 代表 sigmoid 函数。如果我们将方程的输出传入 sigmoid 函数,我们将得到从 0 到 1 的结果。
现在,我们可以通过手动相乘和相加来计算线性方程的值。
但手动过程很耗时。向量化实现要快得多且容易。让我们制定线性方程,使其与向量化实现兼容。
我们添加了一个额外的常数变量 ***xi0=1***
。
矩阵实现线性方程计算(图片来自作者)
X 包含所有自变量的值,M 的转置代表所有系数的转置矩阵。
向量化逻辑回归方程
向量化的逻辑回归实现将是这样的。
它会将线性方程的值转换到 0 到 1 之间。下面是一个 Python 函数。
用演示值测试函数。
是的!我们已经成功创建了这个函数。
生成一个损失函数
如果我们回顾之前的 多重线性回归 文章,我们会发现 均方误差 (MSE) 作为代价函数。
但我们知道逻辑回归不是回归算法。相反,它是一个二分类(两个类别)算法。在逻辑回归中,有两个类别,1 和 0。因此,MSE 不是用于逻辑回归的合适代价函数。*(但为什么?我稍后会解释具体原因)*
现在,我将介绍一个新的成本函数用于这个分类算法。
上述成本函数适用于逻辑回归。
让我们尝试对成本函数有一些直观的了解。对于**yi = 1**
,成本函数为——
这个函数看起来如何?让我们绘制这个函数。
上述图是yi=1
时损失函数的图形表示。图表显示,预测值越接近1,误差越小。当预测值为 0.0 时,误差是无限的。
让我们绘制 ***y0=1***
的成本函数。
对于yi=0
,当预测值接近**1**
时,误差是无限的,通过减小值来减少误差。现在,我们将绘制两个图形的结合。
现在,图形表示更加直观。如果我们将**yi=0 和 yi=1**
的损失函数结合起来,我们将得到一个适合应用梯度下降的函数,它具有全局最小值。
如果我们将目标值***yi=1*** *或* ***yi=0***
代入上述方程,其中一部分将被取消,结果将是我提到的相同方程。这就是我们需要的。
将成本函数转换为代码。
为什么不能使用均方误差(MSE)作为成本函数?
在逻辑回归中,目标或输出值是离散的或分类的。它不像回归问题中的连续值。如果我们将值代入**均方误差(MSE)**成本函数(我们用于线性回归和多重线性回归的成本函数),我们将得到如下图形,而不是一个凸曲线。
多局部最小值的成本函数(图片由作者提供)
由于这种类型的曲线包含多个局部最小值,我们在成本函数中应用梯度下降时会遇到麻烦。这就是为什么我们在逻辑回归中不会使用均方误差(MSE)作为成本函数的原因。
参数优化的梯度下降
梯度下降是一种通过优化机器学习算法的系数来最小化损失/成本函数的方法,这取决于成本函数的形状。
梯度下降(图片由作者提供)
成本函数是一个凸曲线,如损失函数部分所示。现在,我们需要计算成本函数的导数。导数表示成本在什么方向上发生变化。
首先,我们将随机初始化系数的权重并逐步更新权重。主要目标是找到如上图所示的最小成本。
成本函数的导数为——
[注:如果展示导数的详细计算,文章会变得不必要地长。请阅读 文章 以获得详细解释。]
矢量化实现如下。
***X***
是所有特征值的矩阵形式,**M**
代表系数的矢量化形式,**Y**
表示目标值的矢量化表示。
矢量化梯度下降实现的代码。
我们已经迈出了实现最终模型的一步。所有功能都已准备好进行逻辑回归。在下一步中,我们将结合所有工具,实施完整的算法。
将所有概念结合起来进行 Python 从零实现
首先加载泰坦尼克号数据集。 数据集是公开的 可用的 并且在公共领域许可下。
- 导入必要的库
*[我们的主要目标是展示算法的基本机制。因此,我们保持了简单易懂的预处理。我们将重点放在核心实现上,而不是数据分析。]*
为了方便,我们选择了一些特征——
- 让我们对选择的特征进行一些深入了解。
特征‘Age’
和‘Fare’
有一些缺失值。我们将用平均值填补这些缺失值,并将‘Sex’
中的男性映射为1,女性映射为0。
现在,所有特征都是数值型的,没有缺失值。
-
提取自变量(x)和因变量(y)
-
规范化数据,以提高梯度下降的性能
-
拆分训练集和测试集
保留了 25%的数据用于测试,其余数据用于训练。现在,我们将数据输入到我们的初步模型中。
-
将所有功能结合在一起进行逻辑回归
-
用训练数据拟合模型
-
查看模型系数如何优化
-
创建预测函数
在这里,我使用了0.5的阈值来分类数据。所有低于0.5的结果被视为类0,等于或高于0.5的结果被视为类1。
- 让我们将模型与基准 scikit-learn 库进行比较
使用 scikit-learn 创建逻辑回归模型
在测试数据上的预测
- scikit-learn 模型与我们初步模型的结果对比
👉我们初步模型的结果
混淆矩阵
精确度、召回率和 f1-score
👉scikit-learn 模型的结果
混淆矩阵
精确度、召回率和 f1-score
结果显示,我们的初步模型和 scikit-learn 模型具有相同的结果。因此,我们声称我们的初步模型与 scikit-learn 模型相同。
结论
现在,一些内置库使得机器学习模型的实现变得非常简单。因此,学习核心机制可能对你来说并不必要。作为研究人员和学者,我总是从不同的角度考虑这个问题。如果你了解算法的核心概念,这将对你在核心层面的工作,如算法的研究、开发和优化等非常有帮助。你可以在那些没有机器学习库的编程语言中实现这些概念。
[***完整的笔记本和数据集可在仓库中获取***](https://github.com/Zubair063/ML_articles/tree/main/Logistic%20Regression%20from%20Scratch)***.***
参考文献
-
Andrew Ng 的机器学习课程
我之前的**从头开始的算法**系列文章。
从零开始的多重线性回归:深入理解
从零开始的线性回归:详细解释
KNN 算法从零开始 [## KNN 算法从零开始
KNN 算法的实现和详细解释
K-means 从零开始 [## K-means 从零开始
K-means:聚类数据的最佳 ML 算法
**统计和数据可视化** 数据科学系列。
数据科学的终极统计指南 [## 数据科学的终极统计指南
数据科学的一瞥:标准指南
数据科学终极统计指南 [## 数据科学数据可视化终极指南
数据科学中的数据可视化概述:标准指南
将洞察转化为可操作的成果
原文:
towardsdatascience.com/turning-insights-into-actionable-outcomes-f7b2a638fa52
解锁秘密配方。
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 8 月 19 日
–
请检查下面的图片。你认为它描绘的是什么?
如果你认为这张图片描绘的是一块美味的巧克力,你就错了。
这里展示的是复合巧克力,它是由可可、植物脂肪和甜味剂混合而成。由于其成分较便宜,它是比真正巧克力更经济的选择。你可以在经济型巧克力棒或糖果涂层中找到它。这种巧克力在我居住的波兰有着特殊的记忆。在 1980 年代,由于缺少重要的成分,它取代了真正的巧克力。
这种产品通常含有不超过 7%的可可 [1]。相比之下,真正的巧克力至少含有 35%的可可(典型的苦巧克力含 70%) [2]。差别确实很大,对吧?
在我最近的文章《从数字到行动:让数据为公司服务》中,我探讨了包括洞察本质在内的各种问题。许多所谓的洞察类似于复合巧克力。乍一看,这些所谓的洞察可能看起来很真实。但就像那块复合巧克力一样,当你仔细观察或“品尝”它们时,它们并不完全符合标准。
显然,有些问题。图片由作者提供。
在继续之前,我要停下来澄清一下我所说的‘洞察’一词的含义。
什么是洞察,为什么这个术语如此独特?
我探讨了‘洞察’一词的各种词典定义。牛津学习词典将其定义为‘对某事物的 理解 *’。Dictionary.com 描述为‘通过直观的 理解 把握事物的真实本质’。最后,剑桥词典将其定义为‘理解 了解 某事物的真实情况’。
这里反复出现的主题是**‘理解’** — 理解事物的本质。但这个关键的‘事物’是什么?它是特定的、深刻的,且能够带来公司运营显著变化的。
然而,我们对‘洞察’的理解仍不完整。虽然它涉及理解重要的业务问题,但它也包含了‘可行动性’。真正的洞察会导致具体行动,推动体现其建议的决策。
本质上:洞察是对特定业务问题的深刻理解,这种理解促使决策和行动。
为什么洞察如此重要?
今天,数据的有效利用被普遍认可为竞争优势的基础。然而,令人惊讶的是,只有极少数公司能够充分发挥其潜力。仅有 27%的公司认为自己是数据驱动的[3]。当想到真正利用数据并从中获得价值的企业时,通常会想到亚马逊、Meta 和 Netflix 等在线巨头。然而,事实上,任何规模的组织和各个行业都可以通过高效使用数据来推动其增长。
单单拥有数据是不够的。即使数据完美契合特定业务需求,其真正的价值只有在有效应用时才能显现。这意味着决策和后续行动必须基于从数据中获取的趋势、细微差别和洞察。
众多因素可以促进这一过程,但也存在显著的障碍。在通向成功的路线图上,有几个要素至关重要:设计合理且高效管理的数据源、致力于数据驱动决策的公司领导、一个专注且训练有素的数据团队,以及数据驱动的讲故事方式。
为什么与这一定义相符的洞察如此之少?
主要原因在于创建这些洞察的固有挑战。幸运的是,有一些工具可以帮助公司在这一过程中导航。考虑麦肯锡提出的洞察价值链模型[4]。这一概念框架指导组织将原始数据转化为可行动的洞察,从而创造商业价值。该模型展示了四个主要步骤,解释了数据如何从原始状态变为有价值的决策。在下面的图片中,我使用了一个零售公司试图提升销售的例子来展示该模型的工作原理。
麦肯锡的洞察价值链模型以零售公司为例。图片由作者提供。
如上所示,过程是复杂的。为了获得更深入的理解,让我们系统地解剖它。从根本上说,我们要解决的紧迫问题是:
如何提高洞察的百分比?
有四个基本特征定义了“真实”的洞察。一个洞察,如果它的可可含量达到 70%或更多,无论是否苦涩,我在下面的图片中展示了它们:
“真实”洞察的特征。图片由作者提供。
“真实”的洞察:
-
应该提供对业务事务的理解……
-
要具体……
-
… 并且有意义。
-
应该促使决策和行动。
现在,让我们深入探讨这些特征。
如何产生一个能够提供理解的洞察?
真实的洞察必须结合背景以最大化其影响力和理解度。 背景丰富了数据驱动的叙事。赋予洞察背景的六种方法包括:
-
比较背景:每月比较产品销售额或将实际成本与预算或去年同期进行对比。
-
规模调整:突出时间的累计影响,或将年度收益分解为每月或每周的收益,以获得更直观的视角。
-
等效性:通过使用熟悉的例子来帮助理解。与其说:“你的智能手机有 128GB 存储空间”,不如说“它可以存储 25,000 张照片*”。
-
历史背景:展示绩效趋势,考虑季节性或周期性影响。始终比较完整的周期。
-
信息背景:提供有关模式或异常的细节,而不假设相关性意味着因果关系。
-
数据验证:通过引用数据来源、收集方法和时效性来增强可信度[5]。
其次,永远不要满足于初步结论,特别是使用 LLM 进行分析时。进一步深入,直到结论真正体现洞察。
Chat GPT 和代码解释器在形成洞察中的实际应用。来源:作者的 YT 频道。
第三,激发灵感。使用的工具越简单,发生的可能性就越大。即使使用的技术看似简单。 记住阿基米德说过的话:
给我一个支点,我将撬动地球。
以下,我展示了一些使用像 Excel 这样的基本工具执行的分析。虽然这些分析很简单,但它们可以产生有价值的洞察,可能作为使用更复杂程序或技术进行深入探索的基础。
初始图表展示了客户信心指数在一年中的波动,通过 Excel 进行分析。从趋势线和附带的线性回归方程中可以明显看出,总体趋势是下降的。在 C-19 封锁和乌克兰战争爆发等事件期间出现了显著的下降。目前,趋势正向上发展。
趋势分析示例。图片来源于作者。
另一个分析同样在 Excel 中进行,有助于识别结果分布中的特殊性。通过使用基本直方图,我们可以找出异常值,并评估频率分布中的任何不规则性。例如,初看似乎是一个单一分布,实际上可能是三个不同的分布,如下例所示:
使用直方图在 Excel 中检测异常。图片来源于作者。
最终分析同样在 Excel 中进行,包括向图表中添加趋势线。此工具允许应用各种函数,包括线性和非线性函数,以及回归方程。此外,还可以使用 R 平方估计来评估拟合的准确性。
Excel 中的关系分析。图片来源于作者。
我们如何使见解更具体、更有意义?
见解必须与核心业务目标和战略计划紧密相关。这种联系越强,见解被忽视的可能性就越小。
广泛地说,有两种类型的指标:
-
KPI(关键绩效指标)
-
KCI(关键概念指标)。
如果一个指标难以响应,无论其变化的幅度如何,它可能是 KCI——在组织中广泛监控但缺乏可操作价值。相反,与 KPI 相关的见解可以激发真正的紧迫感,推动决策和行动。
KPI 与公司战略的对齐程度越高,它就越自然地转化为战术响应,因为这些响应直接与关键业务组成部分相关。
KPI 必须深深植根于公司的 DNA 中,覆盖从高层领导到后台员工的各个层面。平衡计分卡 可以在将目标和指标传播到每个部门时发挥重要作用。通过培养能够无缝连接管理、财务和数据科学的角色,形成了一种统一的目标实现方法。强调跨组织所有领域的业务合作,从销售到会计。对于适合的组织,采用敏捷管理结构可以提升这种综合策略。
见解如何促使决策和行动?
初始步骤涉及掌握数据讲述的艺术。沟通见解应该超越仅仅向决策者展示复杂表格。这样的做法有可能让他们感到不知所措,导致他们失去兴趣。
有效的数据讲述基于三个原则:
-
理解上下文: 识别驱动我们观众的因素。
-
运用叙事结构: 实施诸如讲故事弧线[6]等元素。
-
利用有效的视觉表现。
什么构成有效的视觉表现? 首先,它应该清晰,不会让观众感到困惑。因此,我提倡使用这三种图表类型:
三种总是有效的图表。作者提供的图片
对于图表选择,当比较汇总值(如预算与实际情况)时使用柱状图或条形图。分析趋势时,折线图是首选。如果你试图理解部分与整体的关系,饼图是理想选择。这三种图表类型可能满足你约 80%的可视化需求,除非有特定场景如队列分析**。
设计图表时,重要的是去除任何杂乱元素。去掉框架、辅助线和不必要的数据点,这些可能会分散主要信息。将颜色和文本视为战略工具;它们应该用来突出和强调关键信息,而不仅仅是美化图表。
始终与观众保持一致。测试你的视觉效果,看看哪些有效,哪些无效,并进行相应调整。这种迭代过程是建立共同理解和确保你的数据讲述引人入胜故事的关键。
最后,确保你的叙事自然流畅。避免用不必要和冗长的悬念分散观众的注意力。使用诸如 3 分钟故事或大创意[7]等方法评估你的讲故事技巧。例如,我会朗读我的叙事,无论是文章还是演示文稿。如果我能顺利地表达故事,这增强了我对其与观众共鸣的信心。 一旦你赢得了他们的注意力,介绍关键结论和行动号召。务必在故事高潮之后立即进行 — 那时他们最为投入和接受。然而,如果出现顾虑,优先考虑积极倾听。解决任何不确定性,并在需要时,建议协作后续活动以促进理解。
结论
在这篇文章中,我讲解了如何打造有力的洞察。这些不仅仅是普通的洞察;它们是引导企业做出明智决策的那种洞察。当使用得当时,这些洞察可以改变游戏规则,帮助公司应对困难局面或利用巨大机遇。拥有正确的数据或最佳工具并不是全部。如何分享和解释这些洞察同样至关重要。 一切都在于确保信息传达准确,让人们思考,并激励他们采取行动。最终,最有价值的洞察是那些能带来有意义的行动和转型的洞察。
假设照片的平均大小为 5MB,而智能手机上的有效空间为 120GB
**作者的主观评估
[1] 维基百科,复合巧克力e
[2] 凯瑟琳·马丁科,巧克力上的可可含量是什么意思?,2021 年 2 月 6 日
[3] 米哈乌·苏德伊科,从数字到行动:让数据为公司发挥作用,2023 年 8 月 14 日
[4] 霍尔格·赫尔特根和尼科·莫尔,利用数据实现业务影响,2018 年 4 月 27 日
[5] 布伦特·戴克斯,情境化洞察:将数字放入上下文的六种方法,2018 年 10 月 18 日
[6] 苹果播客,叙事弧:数据故事中的缺失工具,布伦特·戴克斯,2021 年
科尔·努斯鲍默·克纳夫利克,《数据故事讲述》,Wiley,2015 年
使用 string2string 驯服文本:一个强大的 Python 库,用于字符串对字符串算法
原文:
towardsdatascience.com/tutorial-string2string-python-pkg-f9126b8474c5
教程
利用 string2string 进行自然语言处理任务
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 11 日
–
string2string 库中的概念词云以及一个示例(作者提供的图片)。
string2string
库是一个开源工具,提供了一整套高效的字符串对字符串问题解决方法。该库涵盖了字符串配对比对、距离测量、词汇和语义搜索以及相似性分析。此外,还包括了各种有用的可视化工具和度量标准,使得理解和评估这些方法的结果更加简单。
这个库包含了如 Smith-Waterman、Hirschberg、Wagner-Fisher、BARTScore、BERTScore、Knuth-Morris-Pratt 和 Faiss 搜索等知名算法。它可以用于自然语言处理、生物信息学和计算机社会研究中的许多工作和问题[1]。
斯坦福 NLP 小组,作为斯坦福 AI 实验室的一部分,开发了这个库,并在[1]中介绍了它。该库的 GitHub 仓库有几个教程,你可能会觉得有用。
字符串是代表一段数据或文本的字符(字母、数字和符号)序列。从日常短语到 DNA 序列,甚至计算机程序,字符串可以用来表示几乎一切[1]。
目录
-
安装
-
配对比对
– 用于全局比对的 Needleman-Wunsch 算法
– 动态时间规整
-
搜索问题
– 词汇搜索(精确匹配搜索)
– 语义搜索
–– 通过 Faiss 的语义搜索
-
距离
– Levenshtein 编辑距离
– Jaccard 指数
-
相似性分析
-
结论
-
参考文献
安装
你可以通过运行 pip install string2string
来安装该库。有关更多信息,请访问该库的 GitHub 页面。
成对对齐
字符串成对对齐是一种在 NLP 和其他学科中用于比较两个字符串或字符序列的方法,通过突出它们的共享和独特特征。两个字符串被对齐,并根据共享字符的数量以及共享间隙和不匹配的数量计算相似度分数。这个过程对于定位共享相似性的字符序列和计算两个字符串集合之间的“距离”非常有用。拼写检查、文本分析和生物信息学序列比较(例如 DNA 序列对齐)只是其中的一些用途。
目前,string2string
包提供了以下对齐技术:
-
Needleman-Wunsch 用于全局对齐
-
Smith-Waterman 用于局部对齐
-
Hirchberg 的线性空间全局对齐算法
-
最长公共子序列
-
最长公共子字符串
-
动态时间规整(DTW)用于时间序列对齐
在这篇文章中,我们将看两个示例:一个用于全局对齐,一个用于时间序列对齐。
Needleman-Wunsch 算法用于全局对齐
Needleman-Wunsch 算法是一种动态规划算法,通常用于生物信息学中全局匹配两个 DNA 或蛋白质序列。
The alignment between "ACGTGGA" and "AGCTCGC":
A | C | G | - | T | G | G | A
A | - | G | C | T | C | G | C
为了进行更具信息性的比较,我们可以使用库中的 plot_pairwise_alignment()
函数。
图 1: “ACGTGGA” 与 “AGCTCGC” 之间的全局对齐(图像由作者提供)。
动态时间规整
DTW 是一个有用的工具,用于比较两个可能在速度、持续时间或两者都不同的时间序列。它通过计算两个序列中每对点之间的“距离”,发现最小化序列之间总差异的路径。
让我们通过使用 string2string
库中的 alignment
模块来举个例子。
DTW path: [(0, 0), (1, 1), (1, 2), (2, 3), (3, 4), (4, 5), (4, 6)]
上面是一个借用我之前文章的示例,动态时间规整的插图介绍。对于那些希望深入探讨这一主题的人,在 [2] 中,我以直观和易于理解的方式解释了 DTW 的核心概念。
搜索问题
字符串搜索是找到模式子字符串在另一个字符串中的任务。该库提供了两种搜索算法:词汇搜索和语义搜索。
词汇搜索(精确匹配搜索)
词汇搜索,通俗地说,就是在文本中搜索某些单词或短语,类似于在字典或书籍中查找一个单词或短语。
与其尝试理解一串字母或词语的意思,不如直接尝试准确匹配。在搜索引擎和信息检索中,词汇搜索是一种基本策略,用于根据用户输入的关键词或短语找到相关资源,而不试图理解这些词或短语的语言上下文。
目前,string2string
库提供了以下词汇搜索算法:
-
朴素(暴力)搜索算法
-
Rabin-Karp 搜索算法
-
Knuth-Morris-Pratt(KMP)搜索算法(见下例)
-
Boyer-Moore 搜索算法
The starting index of pattern: 72
The pattern (± characters) inside the text: "of a Redwood tree, and"
语义搜索
语义搜索是一种更复杂的信息检索方法,它超越了简单的词或短语搜索。它利用自然语言处理(NLP)来解读用户的意图,并返回准确的结果。
换句话说,假设你对“如何种植苹果”感兴趣。虽然词汇搜索可能会产生包含“grow”和“apples”这两个词的结果,但语义搜索会识别出你对苹果树栽培的兴趣,并据此提供相关结果。搜索引擎会优先展示那些不仅包含所查短语,还提供关于种植、修剪和收获苹果树的相关信息的结果。
通过 Faiss 进行语义搜索
Faiss(Facebook AI Similarity Search)是一个高效的相似性搜索工具,适用于处理具有数值表示的高维数据[3]。string2string
库为 Facebook 开发的 FAISS 库提供了一个封装(见GitHub 仓库)。
简而言之,Faiss 搜索根据“得分”对结果进行排名,得分表示两个对象之间的相似程度。得分使得根据搜索结果与目标的接近/相关程度来解释和优先排序搜索结果成为可能。
让我们看看string2string
库中如何使用 Faiss 搜索。这里,我们有一个包含 11 个句子的语料库(语料库是用于语言学研究、NLP 和机器学习应用的大型结构化文本集合),我们将通过查询一个目标句子来进行语义搜索,以查看它与这些句子的接近/相关程度。
corpus = {"text": [
"A warm cup of tea in the morning helps me start the day right.",
"Staying active is important for maintaining a healthy lifestyle.",
"I find inspiration in trying out new activities or hobbies.",
"The view from my window is always a source of inspiration.",
"The encouragement from my loved ones keeps me going.",
"The novel I've picked up recently has been a page-turner.",
"Listening to podcasts helps me stay focused during work.",
"I can't wait to explore the new art gallery downtown.",
"Meditating in a peaceful environment brings clarity to my thoughts.",
"I believe empathy is a crucial quality to possess.",
"I like to exercise a few times a week."
]
}
query = "I enjoy walking early morning before I start my work."
让我们初始化FaissSearch
对象。Facebook 的 BART Large 模型是FaissSearch
对象的默认模型和分词器。
让我们在语料库中找到与查询最相似的前 3 个句子,并打印它们及其相似度得分。
Query: I enjoy walking early morning before I start my work.
Result 1 (score=208.49): "I find inspiration in trying out new activities or hobbies."
Result 2 (score=218.21): "I like to exercise a few times a week."
Result 3 (score=225.96): "I can't wait to explore the new art gallery downtown."
距离
字符串距离是量化两个提供的字符串之间差异程度的任务。当前,string2string
库提供了以下距离函数:
-
Levenshtein 编辑距离
-
Damerau-Levenshtein 编辑距离
-
汉明距离
-
Jaccard 距离
Levenshtein 编辑距离
莱文斯坦编辑距离,简称编辑距离,是将一个字符串转换成另一个字符串所需的最小插入、删除或替换次数。
The distance between the following two sentences is 2.0:
"The beautiful cherry blossoms bloom in the spring time."
"The beutiful cherry blosoms bloom in the spring time."
Jaccard 指数
Jaccard 指数可用于量化词汇或标记集之间的相似性,通常用于文档相似性或主题建模等任务。例如,Jaccard 指数可以用来衡量两个不同文档中词汇集的重叠情况,或识别一组文档中最相似的主题。
Jaccard distance between two documents: 0.75
相似性分析
简而言之,字符串相似性决定了两段文本(或字符序列)之间的关联程度或相似性。例如,考虑以下这对句子:
-
“猫坐在垫子上。”
-
“猫坐在地毯上。”
尽管不完全相同,这些陈述共享词汇,并传达了连贯的意义。基于字符串相似性分析的方法揭示并量化了这种文本配对之间的相似度。
字符串 相似性 和 距离 测量之间存在双重性,意味着它们可以互换使用 [1]。
string2string
库的 similarly
模块目前提供以下算法:
-
余弦相似性
-
BERTScore
-
BARTScore
-
Jaro 相似性
-
LCSubsequence 相似性
让我们通过以下四个句子的 BERTScore 相似性算法示例来了解一下:
-
面包店出售各种美味的糕点和面包。
-
公园有一个游乐场、步道和野餐区。
-
该节日展示了来自世界各地的独立电影。
-
面包店提供一系列美味的面包和糕点。
句子 1 和 2 在语义上是相似的,因为它们都涉及面包店和糕点。因此,我们应该预期它们之间会有较高的相似度评分。
让我们在库中实现上述示例。
我们可以使用库中提供的 plot_heatmap()
函数可视化每对句子之间的相似性。
句子之间的语义相似性(BERTScore)(图由作者提供)。
如上所示,句子 1 和 4 的相似性远高于我们的预期(使用 BERTScore 算法)。
结论
string2string
Python 库是一个开源工具,提供了一整套高效的方法用于字符串对字符串问题。特别是,该库有四个主要模块,分别处理以下任务:1. 成对比对,包括全局对齐和局部对齐;2. 距离测量;3. 词汇和语义搜索;4. 相似性分析。该库在每个类别中提供了各种算法,并提供了有用的可视化工具。
📓 您可以在 GitHub找到本帖的笔记本。
感谢阅读! 📚
我是高级数据科学家 📊 和工程师,撰写关于统计学、机器学习、Python 等方面的内容。
🌱 我还策划了一份每周通讯,名为 AI Sprout,在其中提供最新 AI 工具和创新的实际评测和分析。订阅 与我一起探索新兴的 AI!
-
在 Medium 上关注我 👋 以获取我最新的帖子
-
订阅 我的邮件列表 ✉️ 获取直接发送到您收件箱的更新
[## 使用我的推荐链接加入 Medium - Esmaeil Alizadeh
📖 阅读 Esmaeil Alizadeh 以及其他成千上万名 Medium 作家的每一个故事。订阅 Medium 获取完整内容……
medium.ealizadeh.com](https://medium.ealizadeh.com/membership?source=post_page-----f9126b8474c5--------------------------------)
参考文献
[1] M. Suzgun, S. M. Shieber, and D. Jurafsky, “string2string: A modern python library for string-to-string algorithms,” 2023, Available: arxiv.org/abs/2304.14395
[2] E. Alizadeh, “动态时间规整的插图式介绍,” 2020. ealizadeh.com/blog/introduction-to-dynamic-time-warping/
[3] J. Johnson, M. Douze, and H. Jégou, “Billion-scale similarity search with GPUs,” IEEE Transactions on Big Data, vol. 7, no. 3, pp. 535–547, 2019.
最初发布于 https://ealizadeh.com。
变压器在预测推特账户身份中的力量
原文:
towardsdatascience.com/twitter-account-identity-prediction-with-large-language-models-c3ffef114d34
利用大型语言模型进行高级自然语言处理
如何使用最先进的模型进行准确的文本分类
·发表于Towards Data Science ·9 分钟阅读·2023 年 3 月 7 日
–
图片由Jonathan Cooper拍摄,来自Unsplash
介绍
本项目旨在构建一个能够从推文中预测账户身份的模型。我将详细介绍从数据处理、微调到模型性能评估的步骤。
在继续之前,我需要说明的是,这里的身份定义为男性、女性或品牌。这并不反映我对性别身份的看法,这只是一个展示变压器在序列分类中强大能力的玩具项目。在一些代码片段中,你可能会注意到性别被用于表示身份,这只是数据到达的方式。
方法
由于文本数据的复杂性和非线性关系的建模,我排除了更简单的方法,选择利用预训练的变压器模型来完成这个项目。
变压器(Transformers)是当前自然语言处理和理解任务的最先进技术。Transformers库来自 Hugging Face,为你提供了数千个预训练模型以及执行自己微调的 API。大多数模型已经在大量文本语料库上进行了训练,有些跨多个语言。没有经过任何微调,它们在类似的文本分类任务上表现非常好,包括情感分析、情绪检测和仇恨言论识别。
我选择了两个模型进行微调,并使用一个零样本模型作为比较的基准。
Zero-shot 学习提供了一个基准估计,显示了变换器在没有针对特定分类任务进行微调的情况下能有多强大。
笔记本、模型与代码库
由于计算成本,我无法使训练脚本具备交互性。不过,我已经将性能分析笔记本和模型提供给你。你可以使用实时推文自己尝试这些模型!
📒笔记本: 模型性能分析 Jupyter 笔记本
🤗微调 Distilbert-Base-Multilingual-Cased: 模型 1
🤗微调 Albert-base-v2 : 模型 2
💻Github 代码库 : 训练脚本
💾数据来源: Kaggle
数据探索与预处理
数据由 Data For Everyone Library 提供,位于 Crowdflower。你可以在 Kaggle⁴ 下载数据。
注意:数据拥有一个公共 领域许可证⁴*。*
总共有大约 20k 条记录,包含用户名、推文、用户描述和其他推特个人信息。虽然时间限制使我无法详细检查,但从快速检查中可以明显看出这些推文是多语言的。然而,推文文本中混杂了 URL、ascii 字符和特殊字符。这是社交媒体数据的常见现象,幸运的是,使用正则表达式清理这些数据非常简单。
作者提供的图像:推文文本示例、用户描述及数据标签
个人资料图片数据以 URL 链接的形式提供。然而,许多链接已损坏,因此在此预测任务中无用。通常,人们可能期望个人资料图片能很好地预测账户持有者的身份,但在这种情况下,数据质量问题过于严重。由于这个原因,我决定使用推文文本和用户描述进行建模。
缺失与未知变量
大多数账户都提供了身份标签。标签内容丰富,包括女性、男性、品牌和未知——仅 5.6% 的账户被标记为未知。身份标签未知的账户被从分析中移除,因为它们无法进行测试或训练。
大约 19% 的用户描述为空。空白的用户描述可能会暗示账户持有人的身份。对于空白的用户描述,我简单地插入了一些文本,以便模型能够从这些案例中学习。
扩展数据
为了创建更多的示例供模型学习,我将用户描述和推文文本合并到一个通用的 Twitter 文本字段中,有效地将文本样本的数量翻倍。
训练、验证、测试
我将数据拆分为 70% 的训练集、15% 的验证集和 15% 的测试集。为了确保没有重叠,如果数据中有账户出现多次,我会自动将所有这些实例分配到训练数据集中。除此之外,账户会根据上述比例随机分配到各个数据集中。
数据预处理流水线脚本
硬件
微调是在每个模型上分别完成的,并且需要 GPU 才能实际实现。我的笔记本电脑的 GPU 的具体规格是 Nvidia GE Force RTX 2060。
虽然这被认为是个人笔记本电脑的高规格,但我发现一些大型语言模型的性能受到了限制,最终限制了我可以尝试的模型集。
软件
为了充分利用我的 GPU,我必须为我的 GPU 版本和所使用的 Pytorch 版本安装适当的 CUDA 工具包。
CUDA 是一个平台,使计算机能够对数据执行并行计算。这可以大大加快微调 Transformers 的时间。
建议不要在没有 CUDA 支持的 GPU 上进行这种类型的微调,除非你愿意让机器运行几天。
Python 软件包
建模过程的所有步骤都用 Python 脚本编写。我利用了来自 Hugging Face 的开源 Transformers 库。我发现这个库维护良好,并且有大量文档提供最佳实践的指导。
在模型性能测试中,我使用了数据科学家常用的开源机器学习和数据处理工具。关键软件包的列表如下:Transformers、Sci-kit Learn、Pandas、Numpy、Seaborn、Matplotlib、和 Pytorch。
环境管理
Anaconda 作为我的主要环境管理工具,创建了一个 Conda 虚拟环境来安装所有的软件依赖。我强烈建议使用这种方法,因为可能存在大量潜在的依赖冲突。
模型微调
这些模型通过在训练数据集上训练并在验证集上评估性能来进行微调。我已配置微调过程,以根据验证数据集上的表现返回最佳模型。
由于这是一个多类分类问题,因此正在最小化的损失指标是 交叉熵损失。更好的模型性能实质上是在验证集上较低的交叉熵损失。候选模型的超参数设置相同,以便进行比较。
用于微调变换器模型的脚本片段
模型 0:Multilingual-MiniLMv2-L6-mnli-xnli¹
我通过执行零-shot 分类开始分析,以提供一个基线,从中评估微调后的模型。该模型的参考文本表明,它可以在 100 多种语言上进行推断¹,这对我们的问题来说覆盖范围相当好。
模型 1:Distilbert Base Multilingual Cased²
Distilbert-base-multilingual-cased 已在 104 种不同语言上进行训练,提供了广泛的覆盖。该模型是大小写敏感的,因此可以识别文本中的大写和小写。
模型(预)训练: 该模型已在维基百科页面的拼接上进行预训练。
模型架构: 基于变换器的语言模型,具有 6 层,769 维度和 12 个头,总共有 1.34 亿个参数。
微调: 在我的硬件上运行模型微调大约花费了 21 分钟。有一些证据表明,模型已经收敛,这些证据来自评估损失与训练步骤图的对比。
作者提供的图像:评估损失和训练损失
模型 2:Albert-base-v³
该模型已在英文文本上进行预训练,并且是无大小写敏感的,这意味着它不保留文本的大小写信息。Albert 专门设计用于解决训练较大模型时出现的内存限制问题。该模型使用一种自监督损失,重点建模句子间的连贯性。
模型(预)训练: Albert 在 BOOKCORPUS 和英文维基百科上进行预训练以实现其基线。
模型架构: 基于变换器的语言模型,具有 12 个重复层,128 个嵌入,768 个隐藏层,12 个头和 1100 万个参数。
微调: 模型微调大约花费了 35 分钟完成。模型收敛可能通过损失指标的“低谷”来指示。
作者提供的图像:评估损失和训练损失
模型性能
鉴于这是一个多类学习任务,我评估了模型在 F1、召回率、精确率和准确率上的表现,包括每个类和全局水平。性能指标在测试数据集上评分。
零样本的准确率为 37%,Albert 和 Distilbert 的准确率均为 59%。
观察
总体而言,Albert 和 Distilbert 在测试集上的表现均优于零样本分类基线。这是我预期的结果,因为零样本模型对当前分类任务没有任何知识。我认为这更进一步证明了对模型进行微调的价值。
尽管存在显著的性能差异,但在这两种微调模型之间,我们不能明确说哪种模型更好,直到我们在实际应用中对这些模型进行长时间的测试。
显著的性能差异
Albert 在预测时似乎更加自信,其整体预测置信度的 75 百分位数为 82%,而 Distilbert 为 66%。
所有模型在预测男性身份时的精确度、召回率和 F1 值都较低。这可能是由于男性推文的变异性大于女性和品牌推文。
所有模型在预测品牌方面的表现都优于预测其他身份的表现。此外,相比于预测男性或女性用户,模型在预测品牌时表现出显著更高的信心。我猜这是因为品牌在社交媒体上发布信息的方式相对标准化,而个人用户则不然。
作者提供的图像:所有模型的性能指标
作者提供的图像:四分位数间隔的置信度得分
改进方向
我建议采取以下措施以提升模型性能:
增加训练样本
更多的数据可以帮助模型更好地泛化,从而提高整体性能。确实有过拟合的迹象,因为我注意到模型在评估集上的性能开始下降,而在测试集上的性能持续提高,更多的数据可以在一定程度上缓解这种情况。
由于 Distilbert 模型的体积较大,相比于 Albert 模型,它更容易发生过拟合。大型语言模型更加灵活,但也更容易过拟合。
在多个 GPU 上对 twitter-xlm-roberta-base 模型进行微调以实现收敛
有一个由Cardiff NLP开发的模型,专门在推特文本上预训练,并且是多语言的。我确实尝试对这个模型进行微调,但由于硬件限制,效果不佳。这个模型参数多达 198M,运行了近 4 小时却没有显示出收敛的迹象。理论上,由于 Roberta 在推特数据上进行过预训练,它应该比 Distilbert 和 Albert 表现更好。然而,需要更多的数据来防止这个大型模型的过拟合。
探索多模态变换器架构的潜力。
如果我们能改善个人资料图片数据的质量,我认为推文文本和图像的结合可能会显著提升我们分类器的性能。
感谢阅读
[## 通过我的推荐链接加入 Medium - John Adeojo
我分享数据科学项目、经验和专业知识,帮助你在旅途中。你可以通过……
johnadeojo.medium.com [## 首页 | John Adeojo
关于我的一点介绍 欢迎来到我的专业作品集!我是一位经验丰富的数据科学家和机器学习(ML)专家……
[1] Laurer, M., van Atteveldt, W., Salleras Casas, A., & Welbers, K. (2022). 更少标注,更多分类 — 通过深度迁移学习和 BERT 解决监督机器学习的数据稀缺问题 — NLI [预印本]。开放科学框架.
[2] Sanh, V., Debut, L., Chaumond, J., & Wolf, T. (2019). DistilBERT,BERT 的一种精简版本:更小、更快、更便宜且更轻量。arXiv 预印本 arXiv:1910.01108.
[3] Lan, Z., Chen, M., Goodman, S., Gimpel, K., Sharma, P., & Soricut, R. (2019). ALBERT: 一种轻量级的 BERT,用于自监督语言表示学习。CoRR, abs/1909.11942. http://arxiv.org/abs/1909.11942
[4] Twitter 用户性别分类。Kaggle。检索于 2023 年 3 月 15 日,来自 www.kaggle.com/datasets/crowdflower/twitter-user-gender-classification
两种可以显著提升你的查询的高级 SQL 技巧
了解公用表表达式(CTE)和窗口函数
·发布于Towards Data Science ·11 分钟阅读·2023 年 6 月 30 日
–
图片由Karina Szczurek提供,来源于Unsplash
SQL 是每个数据专业人员的必备技能。无论你是数据分析师、数据科学家还是数据工程师,你都需要对如何编写干净高效的 SQL 查询有一个扎实的理解。
这是因为任何严谨的数据分析或复杂的机器学习模型背后都依赖于数据本身,而这些数据必须来自某个地方。
希望在阅读了我关于 SQL 的博客文章后,你已经了解到 SQL 代表结构化查询语言,它是一种用于从关系数据库中检索数据的语言。
在那篇博客文章中,我们讨论了一些基本的 SQL 命令,如 SELECT
、FROM
和 WHERE
,这些命令涵盖了你在使用 SQL 时可能遇到的大多数基础查询。
但如果这些简单的命令不够用怎么办?如果你想查询的数据需要更强大的查询方法怎么办?
好了,今天我们将介绍两种新的 SQL 技巧,你可以将它们添加到你的工具包中,从而将你的查询提升到一个新的水平。这些技巧被称为公用表表达式(CTE)和窗口函数。
为了帮助我们学习这些技巧,我们将使用一个名为DB Fiddle的在线 SQL 编辑器(设置为 SQLite v3.39)和来自 Google Cloud 的出租车行程数据集(NYC Open Data 许可证)。
数据准备
如果你对我如何准备数据集不感兴趣,可以跳过这一部分,直接将以下代码粘贴到 DB Fiddle 中以生成模式。
CREATE TABLE taxi (
id varchar,
vendor_id integer,
pickup_datetime datetime,
dropoff_datetime datetime,
trip_seconds integer,
distance float
);
INSERT INTO taxi
VALUES
('id2875421', 2, '2016-03-14 17:24:55', '2016-03-14 17:32:30', 455, 0.93),
('id2377394', 1, '2016-06-12 00:43:35', '2016-06-12 00:54:38', 663, 1.12),
('id3858529', 2, '2016-01-19 11:35:24', '2016-01-19 12:10:48', 2124, 3.97),
('id3504673', 2, '2016-04-06 19:32:31', '2016-04-06 19:39:40', 429, 0.92),
('id2181028', 2, '2016-03-26 13:30:55', '2016-03-26 13:38:10', 435, 0.74),
('id0801584', 2, '2016-01-30 22:01:40', '2016-01-30 22:09:03', 443, 0.68),
('id1813257', 1, '2016-06-17 22:34:59', '2016-06-17 22:40:40', 341, 0.82),
('id1324603', 2, '2016-05-21 07:54:58', '2016-05-21 08:20:49', 1551, 3.55),
('id1301050', 1, '2016-05-27 23:12:23', '2016-05-27 23:16:38', 255, 0.82),
('id0012891', 2, '2016-03-10 21:45:01', '2016-03-10 22:05:26', 1225, 3.19),
('id1436371', 2, '2016-05-10 22:08:41', '2016-05-10 22:29:55', 1274, 2.37),
('id1299289', 2, '2016-05-15 11:16:11', '2016-05-15 11:34:59', 1128, 2.35),
('id1187965', 2, '2016-02-19 09:52:46', '2016-02-19 10:11:20', 1114, 1.16),
('id0799785', 2, '2016-06-01 20:58:29', '2016-06-01 21:02:49', 260, 0.62),
('id2900608', 2, '2016-05-27 00:43:36', '2016-05-27 01:07:10', 1414, 3.97),
('id3319787', 1, '2016-05-16 15:29:02', '2016-05-16 15:32:33', 211, 0.41),
('id3379579', 2, '2016-04-11 17:29:50', '2016-04-11 18:08:26', 2316, 2.13),
('id1154431', 1, '2016-04-14 08:48:26', '2016-04-14 09:00:37', 731, 1.58),
('id3552682', 1, '2016-06-27 09:55:13', '2016-06-27 10:17:10', 1317, 2.86),
('id3390316', 2, '2016-06-05 13:47:23', '2016-06-05 13:51:34', 251, 0.81),
('id2070428', 1, '2016-02-28 02:23:02', '2016-02-28 02:31:08', 486, 1.56),
('id0809232', 2, '2016-04-01 12:12:25', '2016-04-01 12:23:17', 652, 1.07),
('id2352683', 1, '2016-04-09 03:34:27', '2016-04-09 03:41:30', 423, 1.29),
('id1603037', 1, '2016-06-25 10:36:26', '2016-06-25 10:55:49', 1163, 3.03),
('id3321406', 2, '2016-06-03 08:15:05', '2016-06-03 08:56:30', 2485, 12.82),
('id0129640', 2, '2016-02-14 13:27:56', '2016-02-14 13:49:19', 1283, 2.84),
('id3587298', 1, '2016-02-27 21:56:01', '2016-02-27 22:14:51', 1130, 3.77),
('id2104175', 1, '2016-06-20 23:07:16', '2016-06-20 23:18:50', 694, 2.33),
('id3973319', 2, '2016-06-13 21:57:27', '2016-06-13 22:12:19', 892, 1.57),
('id1410897', 1, '2016-03-23 14:10:39', '2016-03-23 14:49:30', 2331, 6.18);
运行SELECT * from taxi
后,你应该会得到一个类似于下面的表格。
图片由作者提供。
对于那些想了解这个表格是如何来的朋友,我将数据过滤到了前 30 行,只保留了你上面看到的列。至于距离字段,我计算了取车点和放车点之间的正距距离(纬度和经度)。
正距距离是球面上两点之间的最短距离,因此这实际上会低估出租车实际行驶的距离。然而,出于今天的目的,我们可以暂时忽略这一点。
计算正距距离的公式可以在这里找到。现在,回到 SQL。
公共表表达式(CTE)
公共表表达式(CTE)是你在查询中返回的临时表。你可以将它视为一个查询中的查询。它们不仅帮助你将查询拆分为更易读的块,还可以基于已定义的 CTE 编写新查询。
为了演示这个,我们假设我们想分析按小时划分的出租车行程,并过滤 2016 年 1 月至 3 月之间发生的行程。
SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day,
trip_seconds,
distance
FROM taxi
WHERE pickup_datetime > '2016-01-01'
AND pickup_datetime < '2016-04-01'
ORDER BY hour_of_day;
图片由作者提供。
够直接了;让我们更进一步。
假设我们现在想计算每个小时的行程数量和平均速度。这时我们可以利用 CTE 首先获取一个类似于上面观察到的临时表,然后执行后续查询以按小时统计行程数量并计算平均速度。
定义 CTE 的方式是使用WITH
和AS
语句。
WITH relevantrides AS
(
SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day,
trip_seconds,
distance
FROM taxi
WHERE pickup_datetime > '2016-01-01'
AND pickup_datetime < '2016-04-01'
ORDER BY hour_of_day
)
SELECT hour_of_day,
COUNT(1) AS num_trips,
ROUND(3600 * SUM(distance) / SUM(trip_seconds), 2) AS avg_speed
FROM relevantrides
GROUP BY hour_of_day
ORDER BY hour_of_day;
图片由作者提供。
使用 CTE 的替代方法是简单地将临时表包裹在FROM
语句中(见下方代码),这会给你相同的结果。然而,从代码可读性的角度来看,这并不推荐。此外,想象一下如果我们需要创建多个临时表的话。
SELECT hour_of_day,
COUNT(1) AS num_trips,
ROUND(3600 * SUM(distance) / SUM(trip_seconds), 2) AS avg_speed
FROM (
SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day,
trip_seconds,
distance
FROM taxi
WHERE pickup_datetime > '2016-01-01'
AND pickup_datetime < '2016-04-01'
ORDER BY hour_of_day
)
GROUP BY hour_of_day
ORDER BY hour_of_day;
额外信息:从这个练习中我们可以得到一个有趣的见解,那就是出租车在高峰时段的移动速度较慢(平均速度较低),这很可能是由于人们上下班的交通拥堵造成的。
窗口函数
窗口函数对行组执行聚合操作,但它为原始表中的每一行生成一个结果。
要完全理解窗口函数的工作原理,首先快速回顾一下通过GROUP BY
进行的聚合会有所帮助。
假设我们希望使用出租车数据集计算按月份汇总的统计数据。
SELECT CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,
COUNT(1) AS trip_count,
ROUND(SUM(distance), 3) AS total_distance,
ROUND(AVG(distance), 3) AS avg_distance,
MIN(distance) AS min_distance,
MAX(distance) AS max_distance
FROM taxi
GROUP BY month;
图片由作者提供。
在上面的示例中,我们计算了数据集中每个月的计数、总和、平均值、最小值和最大值。请注意,我们原本有 30 行的出租车表现在已经压缩为每个月一行,共六行。
那么,实际在幕后发生了什么?首先,SQL 根据月份对原始表中的 30 行进行了分组。然后,它根据这些分组中的值应用了相关的计算。
以一月份为例。数据集中有两次发生在一月份的旅行,分别行程为 3.97 和 0.68。SQL 根据这两个值计算了计数、总和、平均值、最小值和最大值。这个过程会重复进行,直到所有月份的数据处理完成,最终得到的输出类似于上述结果。
现在,请记住这个想法,我们开始探讨窗口函数的工作原理。窗口函数主要有三大类:聚合函数、排名函数和导航函数。我们将分别查看每一类的示例。
聚合函数
我们在之前的示例中已经见过聚合函数的作用。聚合函数包括计数、求和、平均值、最小值和最大值等函数。
但窗口函数与GROUP BY
的不同之处在于最终输出的行数。具体来说,我们看到在按月份聚合后,我们的输出表只剩下六行(每个月一行)。
窗口函数与聚合字段不同,它不会对表进行汇总,而是简单地在每一行中输出结果到一个新列。输出表中的行数不会改变。换句话说,输出表的行数总是与原始表相同。
执行窗口函数的语法是OVER(PARTITION BY ...)
。你可以将其视为我们之前示例中的GROUP BY
语句。
让我们看看实际情况是如何运作的。
WITH aggregate AS
(
SELECT id,
pickup_datetime,
CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,
distance
FROM taxi
)
SELECT *,
COUNT(1) OVER(PARTITION BY month) AS trip_count,
ROUND(SUM(distance) OVER(PARTITION BY month), 3) AS total_month_distance,
ROUND(AVG(distance) OVER(PARTITION BY month), 3) AS avg_month_distance,
MIN(distance) OVER(PARTITION BY month) AS min_month_distance,
MAX(distance) OVER(PARTITION BY month) AS max_month_distance
FROM aggregate;
图片由作者提供。
在这里,我们希望得到与上次相同的输出,但不是压缩表,而是希望在新列中将结果显示为单独的行。
你会发现聚合后的值没有改变,而是简单地以重复的行显示在表中。例如,前两行(一月)的旅行计数、总月度距离、平均月度距离、最小月度距离和最大月度距离与之前相同。其他月份也是如此。
如果你想知道窗口函数的作用,它帮助我们将每一行的值与聚合值进行比较。在这种情况下,我们可以轻松比较每一行的行驶距离与每月的平均值、最小值和最大值等等。
排名函数
另一种窗口函数是排名函数。顾名思义,这种函数基于聚合字段对一组行进行排名。
WITH ranking AS
(
SELECT id,
pickup_datetime,
CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,
distance
FROM taxi
)
SELECT *,
RANK() OVER(ORDER BY distance DESC) AS overall_rank,
RANK() OVER(PARTITION BY month ORDER BY distance DESC) AS month_rank
FROM ranking
ORDER BY pickup_datetime;
图片由作者提供。
在上面的例子中,我们有两个排名列:一个是整体排名(从 1 到 30),另一个是按月排名,两者均为降序排列。
要指定排名的顺序,你需要在OVER
语句中使用ORDER BY
。
对于第一行的结果,你会解释为它在整个数据集中具有第三长的行驶距离,并且在一月份的行驶距离最长。
导航函数
最后但同样重要的是,我们还有导航函数。
导航函数根据不同于当前行的行的值分配一个值。一些常见的导航函数包括FIRST_VALUE
、LAST_VALUE
、LEAD
和LAG
。
SELECT id,
pickup_datetime,
distance,
LAG(distance) OVER(ORDER BY pickup_datetime) AS prev_distance,
LEAD(distance) OVER(ORDER BY pickup_datetime) AS next_distance
FROM taxi
ORDER BY pickup_datetime;
Lag 返回前一行的值。图片由作者提供。
Lead 返回下一行的值。图片由作者提供。
在上面的例子中,我们使用了LAG
函数返回前一行的值,使用LEAD
函数返回下一行的值。请注意,滞后列的第一行是空的,而前导列的最后一行是空的。
SELECT id,
pickup_datetime,
distance,
LAG(distance, 2) OVER(ORDER BY pickup_datetime) AS prev_distance,
LEAD(distance, 2) OVER(ORDER BY pickup_datetime) AS next_distance
FROM taxi
ORDER BY pickup_datetime;
当滞后偏移设置为 2 时,前两行是空的。图片由作者提供。
当前导偏移设置为 2 时,最后两行是空的。图片由作者提供。
同样,我们还可以对LEAD
和LAG
函数进行偏移,即从特定的索引或位置开始。当偏移设置为二时,你会发现滞后列的前两行是空的,而前导列的最后两行是空的。
我希望这篇博客文章能帮助你了解公共表表达式(CTE)和窗口函数的概念。
总结来说,CTE 是一个临时表或查询中的查询。它们用于将查询拆分成更易读的块,你可以对已定义的 CTE 写新查询。另一方面,窗口函数在一组行上执行聚合,并返回原始表中每行的结果。
如果你希望改进这些技术,我强烈鼓励你在工作中、解决面试问题时,或是随意操作随机数据集时开始在 SQL 查询中实现它们。练习才能达到完美,对吧?
支持我和其他出色的作者,请使用下面的链接注册 Medium 会员。祝学习愉快!
[## 使用我的推荐链接加入 Medium - Jason Chong
作为 Medium 会员,你的会员费用的一部分将用于支持你阅读的作者,并且你可以完全访问每一个故事……
chongjason.medium.com](https://chongjason.medium.com/membership?source=post_page-----81a97c92ddd0--------------------------------)
不知道接下来读什么?以下是一些建议。
## 每个数据分析师都需要知道的 10 个最重要的 SQL 命令
从数据库查询数据不需要复杂。
towardsdatascience.com ## 正则表达式清晰解释及示例
每个数据分析师在处理字符串时应该具备的一项被低估的技能。
towardsdatascience.com ## 可能影响或决定你数据科学项目成败的常见问题
一份有用的指南,介绍如何发现数据问题、这些问题为何可能会带来不利影响,以及如何妥善解决它们。
towardsdatascience.com
两次发球:分析 2000 年至 2020 年的 ATP 发球数据
在巡回赛中,哪些球员应该舍弃他们的第二发球及其原因(以及一个用于可视化的 Dash 应用)
·
关注 发布于Towards Data Science · 7 分钟阅读 · 2023 年 2 月 3 日
–
库存图片由Pixabay提供
在过去十年左右的时间里,数据、分析和机器学习在体育界变得无处不在。同时,非传统的策略也在体育界越来越受欢迎。在足球中,四分之一尝试和两分转换尝试有所上升,因为分析证明了这些策略的增值。2021 年,四分之一尝试达到了 793 次的高峰,比 2011 年增长了超过 70%。在篮球中,长距离两分球几乎已经消失,因为分析专家已证明了角球三分球和上篮的期望价值更高。
就像篮球和足球在数据方面为战术和策略的改变提供了肥沃的土壤一样,我的业余爱好网球也是如此。对大多数网球职业选手来说,体育分析的价值在于对手侦查。使用对手的统计数据可以告诉球员在攻击对手的反手球、迫使他们从底线打长时间的对拉球或试图将他们引向网前等方面,什么样的策略可能是最佳的。然而,职业选手仍然有机会利用该运动产生的大量数据,从传统的发球百分比统计到通过视觉 AI 应用获取的更高级的球员位置统计数据。
去年在我加入一个本地网球联赛、结束了四年的空窗期后,我意识到一个分析网球数据的机会。在我前几场比赛中,我发现虽然我的第一次发球经常出界,但我的对手能够攻击我的弱第二发。在经历了过多的破发点后,我自问我是否更应该放弃第二发球,改用第一次发球两次呢?
前提非常简单。在网球中,发球员具有优势,因为他们可以开始回合——这赋予了他们在球的放置位置和击球旋转类型等方面的战略选择。发球也是网球中最快的击球之一,ATP 巡回赛的球员经常达到大约120 MPH (193 KM/H)的速度。不仅如此,发球员还有两次机会发球,以防第一次失误。
大多数球员,无论是业余还是职业球员,都使用两种不同的发球。实际上,网球的发球策略基于一个相当简单的原则。球员通常利用首发球打出更快、更有利的落点,例如在场地边线或“网眼”处的中线位置,以获取优势,迫使对手回球薄弱,将对手推入不利的位置,或者通过发球直接得分赢得分数。第二发球则通常打得更为安全。球员倾向于将球打得更慢,并在具有更高失误容限的区域进行发球。球员仍然会尝试通过控制对手的场地位置来获取优势,但这种优势通常比成功的首发球要小。
现在,让我们来讨论一下球员是否会从两次使用首发球中受益。这并不是一个特别难回答的问题,它只需要四个变量:
-
首发球成功率: 首发成功并因此开始得分的百分比;
-
首发球赢分率: 球员在成功发入首发球后赢得的分数的百分比;
-
第二发球成功率: 第二发球成功并因此开始得分的百分比;
-
第二发球成功率: 球员在成功发入第二发球后赢得的分数的百分比。双误会影响球员的第二发球成功率。
这引出了一个关键问题:使用两次首发球的期望值*E§*是否大于或小于传统首发和第二发球策略的期望值?
图 1:网球发球的期望值公式(作者创建的截图)
为了回答发球策略的问题,我分析了由TennisAbstract.com创始人 Jeff Sackmann 提供的优秀数据(感谢 Jeff 允许使用他的数据撰写这篇文章)。我从Jeff 的 GitHub中提取了数据,并合并了 2000 年至 2020 年的数据集。本文其余部分的分析使用了 Python 生成。
数据集详细列出了在时间范围内所有 ATP 单打比赛的信息,包括比赛和场地。然而,我的主要关注点是发球数据(即,首发成功率、发球直接得分、双误等)。此外,由于 ATP 是男子巡回赛,这些数据仅针对男性球员。我打算将 WTA 数据添加到此分析和 Dash 应用程序中。
分析
为了不掩盖重点,直截了当的说:在 2000 年至 2020 年间,ATP 巡回赛中有高达五分之一的球员采用双发球策略会更有利。我不是托尼叔叔,但 20%的球员从采用更具攻击性的发球策略中受益,似乎是一个相当大的市场低效。
在准备数据时,我采取的第一步——除了创建我们的关键变量如发球成功率——是将观察结果滚动到球员级别。在比赛级别,由于方差过大,难以推断出全面的发球策略。下图 2 是拉斐尔·纳达尔每场比赛使用双发球策略的预期值直方图。请注意,即使是伟大的网球选手之一,纳达尔的预期值也有相当大的变化。
25 百分位数:60% | 平均值:65% | 75 百分位数:.71%(图表由作者使用 Plotly 生成)
为了进一步展示这种差异,我比较了比赛级别和球员级别的数据。我发现,在任何给定的比赛中,大约 35%的球员如果使用双发球策略会表现更好。然而,当查看每位球员的职业生涯数据时,只有 24%的球员使用双发球会更有利。当我对至少打了 50 场比赛的球员进行子集分析时,这一比例进一步下降到 20%。
Dash 应用程序
为了更深入了解球员发球百分比的因素、决定因素和协变量,我使用 Heroku 构建并部署了一个 Dash 应用程序。对初学者来说,Dash 是一个强大的工具,允许用户创建自定义仪表盘,而 Heroku 是一个平台即服务的云平台,允许个人用户部署简单的应用程序。
我为此分析创建的仪表盘恰到好处地很简单。用户选择 x、y 和颜色变量。默认的 x 和 y 变量分别是单发球策略的预期值和双发球策略的预期值。下图捕捉了这个图表。
图 2:发球策略比较(截图由作者创建)
红色编码的点表示那些双发球策略的预期值高于当前单发球策略的球员。上图虽然信息丰富,但它实际上只是告诉我们谁可能从这种策略中受益。如果我想知道什么因素使得球员更可能从双发球策略中受益呢?
图 3:身高与最佳发球策略的关系(截图由作者创建)
多年来,分析师们已经认识到身高是成功服务的一个关键因素。即使是普通的球迷也许还记得约翰·伊斯内尔,他与尼古拉斯·马胡特的 11 小时 5 分钟对决打破了比赛时长纪录。伊斯内尔身高 6 英尺 10 英寸,在比赛的决胜盘中主宰了自己的发球局,未失一个破发点。而这些数据也证实了网球分析师的普遍智慧和伊斯内尔自身的经历。观察身高与双发球策略之间的关系,那些对这种策略最有利的似乎是运动中的巨人。另一位身高巨大的 ATP 职业选手伊沃·卡尔洛维奇(巧合的是也因长时间比赛而闻名)在 2009 年以超过 0.75 分的预期值位居榜单首位,使用了双发球策略。
对于运动中的巨人来说,双发球策略的好处在于他们能够在第一次发球成功时赢得分数。2009 年,卡尔洛维奇的第一次发球赢球率接近 85%,显著高于约 71%的样本平均水平。类似地,伊斯内尔的第一次发球赢球率为 82%。如此高的成功率,加上即使是适中的第一次发球百分比——即球员能够用第一次发球将球投入比赛的比例——通常足以使发球者在双发球策略下达到更高的预期得分值。
结论
那么,这对 ATP 巡回赛中的职业球员意味着什么呢?对于大多数球员来说,影响不大。四分之三的球员仍然保持传统的发球策略。然而,对于剩下的五分之一的球员,特别是那些身高超过 6 英尺 4 英寸(193 厘米)的球员,我建议尝试双发球策略(尽管,我可能不会等到比赛决赛才尝试)。那些在第一次发球时表现出色的球员可能会通过双发球策略给对手施加压力,即使这意味着更频繁地双误。
你需要知道的两个有趣的 pandas 数据操作函数
数据科学
极其有用的 pandas 函数可以将连续的 pandas 列转换为分类列。
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 8 月 24 日
–
照片由 Brendan Church 提供,来源于 Unsplash
Python pandas 是一个强大且广泛使用的数据分析库。
它提供了 200 多个函数和方法,使数据操作和转换变得容易。然而,了解所有这些函数并在实际工作中按需使用它们并不是一项可行的任务。
数据操作中的常见任务之一是将包含连续数值的列转换为包含离散或分类值的列。pandas 有两个了不起的内置函数,可以节省你几分钟时间。
你可以将这种类型的数据转换用于各种应用,如分组数据、按离散组分析数据或使用直方图可视化数据。
例如,
最近,我计算了赫芬达尔-赫希曼指数 (HHI)以了解多个品牌的市场集中度。因此,在一个 pandas DataFrame 中,我有一个包含所有品牌 HHI 连续值的列。最终,我想将这一列转换为离散列,以将每个品牌分类为低、中和高市场集中度——这就是我获得灵感的地方。
如果不知道这些内置的 pandas 函数,你可能需要编写多个 if-else 和 for 语句来完成相同的工作。
因此,在这里你将探索两个超级有用的 pandas 内置函数以及有趣的示例(包括我的项目),这些示例将大大提升你的数据分析能力,并节省你几分钟时间。
在你的分析项目中,通常需要将一个具有连续值的列转换为另一个具有离散值的列。
所以基本上,你将连续数据分类为几个类别,即桶或箱子。你可以通过指定每个箱子的最小值和最大值,即定义箱子边缘,或通过指定箱子数量来做到这一点。
根据你将连续序列拆分为离散序列的目的,你可以使用 接下来的两种 方法之一。
由于我对工作中的内置函数感到好奇,首先我遇到了 pandas 库中的 cut()
函数。
pandas cut()
当你想将数据分成固定数量的不同桶时,可以使用 pandas cut(),无论每个桶中的值的数量如何。
根据 pandas 官方文档,**pandas.cut()**
函数有 7 个可选参数和 2 个必需参数。
但你不需要记住所有这些。
我已经为你简化了内容。我现在经常使用这个函数,发现一些函数参数比其他参数更有用。
这里是你在几乎 90% 的情况下 会使用的常用可选参数。
pandas.cut(x,
bins,
labels=None,
right=True,
include_lowest=False)
让我们举一个例子来理解这些参数是如何工作的。
假设你有以下连续序列,你想将其转换为 5 个箱子。
import pandas as pd
import numpy as np
# Create random data
Series1 = pd.Series(np.random.randint(0, 100, 10))
# Create DataFrame
df = pd.DataFrame({"Series1": Series1})
# Apply pandas.cut() on the column Series1
df["binned_Series1"] = pd.cut(df["Series1"], bins=5)
pandas cut() | 作者图片
你简单地将整数 5 分配给参数 bin——结果,pandas 将整个列 Series1 拆分为 5 个相等大小的桶。Pandas 将 Series1 中的每个值分配到这 5 个桶中的一个。
如果你检查这些桶中的每一个,你会发现两个共同点。
-
箱子边缘是非整数——你可以通过在 bin 参数中定义箱子边缘来解决这个问题。
-
每个箱子边缘在右侧是封闭的——这是由于参数 right 的默认设置
right=True
。这意味着 pandas 包括桶中的最大值在同一个桶中。这个参数特别帮助你 控制分箱过程,并且切换其值可以帮助你包括或排除某些元素。
让我们再试一次。
这次你将传递一个箱子边缘的列表给相同的 DataFrame 列,看看结果是如何变化的。
df["binned_Series1_defined_binedge"] = pd.cut(df["Series1"],
bins=[0, 10, 15, 40, 65, 100])
pandas cut 定义了箱子边缘 | 作者图片
Pandas 使用你在 bin 参数中提供的整数简单地创建了新的箱子,并将 Series1 中的每个数字分配到这些箱子中。
此外,你还可以使用 Label 参数为每个桶命名,如下所示。
df["bin_name"] = pd.cut(df["Series1"],
bins=[0, 10, 15, 40, 65, 100],
labels=['bin 1', 'bin 2', 'bin 3', 'bin 4', 'bin 5'])
pandas cut() 带有箱子标签 | 作者图片
它工作得非常完美!
回到我的工作——一个真实场景——我在下面的数据集上尝试了函数 **pandas.cut()**
。
# Create a sample DataFrame as I can not disclose the original data
HHI = [random.random() for i in range(10)]
Brands = ["Brand_1", "Brand_2", "Brand_3", "Brand_4", "Brand_5",
"Brand_6", "Brand_7", "Brand_8", "Brand_9", "Brand_10"]
df = pd.DataFrame({"brand": Brands, "hhi": HHI})
# Use pandas.cut()
df["binned_hhi"] = pd.cut(df["hhi"], bins=3)
df["brand_bucket"] = pd.cut(df["hhi"],
bins=3,
labels = ["low", "medium", "high"])
df
在实际例子中使用 pandas.cut() | 图片由作者提供
然而,这些桶中的元素分布不均,即每个桶包含的元素数量不同。5 个品牌属于低,3 个品牌属于中,仅 2 个品牌属于高浓度桶。
但对于我的项目,我想保持分布,即每个桶中的品牌数量相同,这就是我发现下一个 pandas 方法有用的地方。
pandas qcut()
pandas.qcut()
用于在所有桶中获得均等的数据分布。它基于样本分位数的原理。
分位数是将序列分成若干个子集的值——每个子集包含大致相同数量的元素。
因此,当你使用函数**qcut()**切分一个序列时,它只是告诉你序列的哪个元素属于哪个分位数。
函数qcut()
的基本语法几乎与函数cut()
的语法相同。
让我们通过一个例子来理解——在这里你将对相同的数据使用函数cut()
和qcut()
,并将它们分为 4 个桶。
Series1 = pd.Series([17, 47, 35, 6, 6, 16, 78, 14, 79, 98])
df = pd.DataFrame({"Series1": Series1})
df["qcut_Series1"] = pd.qcut(df["Series1"], q=4) # Use qcut()
df["cut_Series1"] = pd.cut(df["Series1"], bins=4) # Use cut()
基于分位数的离散化 Python | 图片由作者提供
现在,当你检查每个桶中的数据分布时——
# Check the data distribution of each bucket when cut() was used
df["cut_Series1"].value_counts()
#Output
(5.908, 29.0] 5
(75.0, 98.0] 3
(29.0, 52.0] 2
(52.0, 75.0] 0
Name: cut_Series1, dtype: int64
# Check the data distribution of each bucket when qcut() was used
df["qcut_Series1"].value_counts()
#Output
(5.999, 14.5] 3
(70.25, 98.0] 3
(14.5, 26.0] 2
(26.0, 70.25] 2
Name: qcut_Series1, dtype: int64
你会看到,当你使用函数**cut()**
时,尽管每个桶的大小相等,即 23,但每个桶中包含的元素数量不同。
而当你使用函数**qcut()**
时,每个桶中存在类似数量的元素。但你可以看到,这种分布是以不同的桶大小为代价的。
因此,在我的项目中,函数pandas.qcut()
是最终解决方案,正如你所看到的——
df["binned_hhi_qcut"] = pd.qcut(df["hhi"], q=3)
df["brand_bucket_qcut"] = pd.qcut(df["hhi"],
q=3,
labels = ["low", "medium", "high"])
df
使用 pandas.qcut()的实际场景 | 图片由作者提供
因此,**qcut()**
将每个中和高浓度桶分配了 3 个品牌,将低浓度桶分配了 4 个品牌。
希望你发现这篇文章既清新又有用。尽管将连续序列转换为离散序列是数据分析中的常见场景,但如果你不了解内置函数,这项任务确实可能非常艰巨。
在数据分析项目中使用这些函数,肯定能帮助你迅速从数据中提取所需的信息。
在评论中告诉我你希望获得哪些精彩的文章!
仅仅了解这些函数是不够的——现在就开始在数据分析任务中使用它们,释放真正的 pandas 力量吧。
准备提升你的数据分析技能了吗?
💡 考虑成为 Medium 会员以访问无限的 Medium 故事和每日有趣的 Medium 摘要。我将获得你费用的一小部分,对你没有额外费用。
💡 确保注册我的邮件列表,以便不错过任何关于数据科学指南、技巧和提示、SQL 和 Python 的文章。
了解更多关于我的项目,请评论你的问题!
感谢你的阅读!