帮助累积表达式的库
项目描述
dataframe_expressions
简单的DataFrame操作表达式累积
表达式示例
您从一个顶层数据帧开始
from dataframe_expressions import DataFrame
d = DataFrame()
现在您可以用简单操作来掩码它
d1 = d[d.x > 10]
运算符 <,>, <=, >=, ==,
和 !=
都受支持。您还可以组合逻辑表达式,但请注意运算符优先级
d1 = d[(d.x > 10) & (d.x < 20)]
当然,也可以链式调用
d1 = d[dx > 10]
d2 = d1[d1.x < 20]
并且 d2
将与上一个示例中的 d1
相同。
基本的4个二进制数学运算符也按预期工作
d1 = d.x/1000.0
扩展函数受支持
d1 = d.x.count()
同样,numpy
函数也受支持
import numpy as np
d1 = np.sin(d.x)
以及一些Python函数
d1 = abs(d.x)
内部,这被表示为 d.x.sin()
。
Lambda函数和捕获变量
可以使用捕获变量的lambda,允许组合对象。例如
d.jets.map(lambda j: d.eles.map(lambda e: j.DeltaR(e)))
将产生每个喷气式飞机和每个电子的DataFrame流。如何使用像map
这样的函数取决于后端(当然还有DeltaR
)。此外,后端必须运行解析,因为参数可以是任意的,所以dataframe_expressions
不能自行理解其含义。例如,这里的函数map
在这个库中没有特殊含义。
后端函数
有时后端定义了一些可以直接调用的函数。例如,可能需要几个参数的DataR
。通过一些提示,这些在最终的ast
中被编码为直接函数调用
from dataframe_expressions import user_func
@user_func
def calc_it (pt1: float) -> float:
assert False, 'Should never be called'
calced = calc_it(d.jets.pt)
在这种情况下,calced
预计将是所有组合在一起的喷气式飞机pt
的列。
筛选函数
如果过滤器变得过于复杂(位于 [
和 ]
之间的代码),将其放入单独的函数中可能更简单。
def good_jet(j):
(j.pt > 30) & (abs(j.eta) < 2.4)
good_jets_pt = df.jets[good_jet].pt
向数据模型添加计算表达式
在数据模型中定义新列有两种方法。在这两种情况下,新计算表达式可以替换旧的表达式。第一种方法看起来更像 pandas
,第二种方法则更像正则表达式替换。第二种方法相当通用、强大,因此很可能导致错误。不确定它能否在原型中生存。
添加新的计算表达式列
这是向数据模型添加新表达式的最常见方法:提供一个在渲染过程中由 dataframe_expressions
计算的 lambda 函数。
df.jets['ptgev'] = lambda j: j.pt / 1000.0
默认情况下,参数是方括号之前的所有内容——在本例中为 df.jets
。所有关于捕获变量的规则都适用于此处,因此可以添加一组靠近喷流的轨迹,例如,使用这个(只要后端实现)。例如
def near(tks, j):
return tks[tks.map(lambda t: DeltaR(t, j) < 0.4)]
df.jets['tracks'] = lambda j: near(df.tracks, j)
# This will now get you the number of tracks near each jet:
df.jets.tracks.Count()
上面的假设很多后端实现:DeltaR
、map
、Count
,以及具有喷气和轨迹的探测器数据模型,但希望给出一种可用能力的概念。
替换列的内容
在必要时,可以将数据模型的一部分嫁接到另一部分,可以使用上面的 lambda 表达式来实现,但这是一种简化的方法。
df.jets['mcs'] = df.mcs[df.mcs.pdgId == 11]
how_many_mcs = df.jets.mcs.Count()
但这将使每个喷流的数字相同。
由于渲染方式的原因,以下操作也会达到预期效果
df.jets['ptgev'] = df.jets.pt/1000.0
jetpt_in_gev = df.jets.ptgev
这是因为,在当前的 dataframe_expressions
模型中,每个公共表达式的每次出现,如 df.jets
,都对应同一组喷气。换句话说,隐含迭代器在这里很常见。在这个原型中,这并不明显。
所有这些操作都会像预期的那样通过过滤器进行。
df.jets['ptgev'] = df.jets.pt / 1000.0
jetpt_in_gev = df.jets[df.jets.ptgev > 30].ptgev
原型实现特别脆弱——但这更多是因为设计不佳,而不是技术限制。
使用对象向数据模型添加内容
另一种方法是构建一个对象。例如,假设你想使 3 向量运算更容易。你可能写成这样
class vec(DataFrame):
def __init__(self, df: DataFrame):
DataFrame.__init__(self, df)
@property
def x(self) -> DataFrame:
return self.x
@property
def y(self) -> DataFrame:
return self.y
@property
def z(self) -> DataFrame:
return self.z
@property
def xy(self) -> DataFrame:
import numpy as np
return np.sqrt(self.x*self.x + self.y*self.y)
现在你可以写 v.xy
,你就有从原点到 L_xy
的距离。也可以实现向量运算。这个库不会帮你做这件事,但这并不困难。
如果你只想让提供的属性可用,可以添加 exclusive_class
类装饰器(因此 v.zz
会引发错误)。
支持此功能所需的工作几乎微不足道——请参阅测试用例,包括文件 test_object.py
中的向量加法测试用例,以获取更多示例。
使用别名向数据模型添加内容
这是一个简单的功能,允许你为更复杂的表达式发明简写。这使得它很容易使用。此外,后端永远不会知道这些简写脚本——它们只是作为 DAG 构建时的即时替换。例如,在 ATLAS 实验中,我需要总是将喷气的 pT 除以 1000。所以
define_alias('', 'pt', lambda o: o.pt / 1000.0)
现在如果输入 d.jets.pt
,后端会将其视为我输入了 df.jets.pt/1000.0
。同样的操作也可以用于集合。例如
define_alias('.', 'eles', lambda e: e.Electrons("Electrons"))
当输入 d.eles.pt
时,后端会将其视为 df.Electrons("Electrons").pt / 1000.0
。
别名可以相互引用(尽管不允许递归),因此可以构建相当复杂的表达式。这个库的别名解析相当简单(这是一个原型)。匹配是可能的。例如,如果第一个参数是 .
,则仅翻译直接从数据框引用的内容。这个功能可以用来为实验的分析定义一个 personality 模块。
与后端一起使用
虽然上面显示了您希望库可以跟踪的内容,但它并没有说明如何使用它。以下步骤是必要的。
-
使用您的类继承
dataframe_expressions.DataFrame
。确保初始化DataFrame
子类。但是,无需传递任何参数。为了讨论,让我们将其称为MyDF
-
用户构建表达式的方式与您预期的一样,
df = MyDF(...)
,以及df1 = df.jets[df.jets.pt > 10]
-
用户以某种合理的方式触发在您的库中渲染表达式,
get_data(df1)
-
当您通过顶级
DataFrame
表达式获得控制权时,现在可以执行以下操作以渲染它
from dataframe_expressions import render
expression = render(df1)
expression
是一个 ast.AST
,描述了正在查看的内容(例如 df.jets.pt
)。如果表达式类似于 df.jets.pt
,则 ast 是一系列 Python ast.Attribute
节点,最后一个将是一个特殊的 ast_Dataframe
对象,它包含一个成员 dataframe
,该成员指向您的原始子类 MyDF
。
如果有过滤器,还需要处理另一个特殊 ast 对象,ast_Filter
。例如,df[df.met > 50].jets.pt
将具有以两个 ast.Attribute
节点开始的 expression
,后跟一个 ast_Filter
节点。那里有两个成员,一个 expr
,在这种情况下它将包含 df
或指向 df
的 ast_Dataframe
。第二个成员是 filter
,它指向一个表达式,该表达式是过滤器。它应该评估为真或假。只要存在重复短语,如 df
在 df[df.met > 50].jets.pt
中或 df.jets
在 df.jets[df.jets.count() == 2]
中,它们将指向相同的 ast.AST
对象 - 因此您可以在遍历树时识别相同的表达式(s)。
技术选择
不确定这些是否是正确的事情,但...
-
使用 Python
ast
模块记录表达式。主要是因为它已经完整,并且有很好的访问者对象,这使得遍历它变得容易。缺点是 Python 每隔几个版本就会更改 ast。 -
DataFrame 上的属性引用某些数据。然而,方法调用不引用数据。因此,您可以说
d.pt
来获取 pt,但如果您说d.pt()
,那将是“错误的”。这样做的原因是我们可以添加执行流畅操作的功能。例如,d.jets.count()
来计算喷气式飞机的数量。或者d.jets[d.jets.pt > 100].count()
或类似的。实际上,后端可以解释这一点,但前端语义大致假设了这一点。
架构问题
这不是一个详尽的列表。只是我为了启动这个项目而必须做出的某些选择。
-
是否应该有
Column
和Dataset
?- 是的 - 结果我们发现为什么 numpy 中存在掩码和列的区别。因此,列对象实际上是掩码对象。这很糟糕的命名,但希望在这个原型中这不会造成太大的影响。因此,我们应该稍微考虑一下为什么掩码必须以不同的方式处理 - 直到你进入代码,这并不直观。
- 不 - 因为事情可以返回“bool”值,而我们不知道,因为我们没有类型系统,它们与列相同,除了我们假设它们是 df:例如
df[df.hasProdVtx & df.hasDecayVtx]
。 - 我们应该消除父级、动态等概念,用 ast_DataFrame 替换它们 - 我们已经在这里有了 - 因此,为什么不一视同仁地坚持使用它而不是同时使用它和
p
。
-
我们应该允许使用"&"和"|"作为逻辑运算符,并在Python中重新定义它们的含义吗?NumPy定义了几个逻辑运算符,应该进行翻译,但尚未实现。
-
我在表达式中使用了"p"作为父节点,但之后我们有dataframe ast和列ast,这使得它变得不再需要。为什么不让ast中用同一事物来引用df呢?
- 在内部,"parent" dataframe被表示为
p
,这意味着任何东西都不能有p
对象,否则可能会引发严重错误。这是不支持这种方式的非常好的论据。
- 在内部,"parent" dataframe被表示为
-
对于类型,我不知道如何进行前向声明,因此无法在方法定义中使用Column和DataFrame。静态类型检查器现在应该通过简单的逻辑来捕获这一点。
-
使用BitAnd和BitOr进行与和或运算 - 但我是否应该在这里使用逻辑与和或,以便在AST中清楚地说明我们所说的内容?
-
d1[d[d.x > 0].jets.pt > 20].pt
意味着什么?这是否是我们遇到的事情的极限?我认为它没有意义,应该创建一个错误。然而,d1[(d[d.x > 0].jets.pt > 20).count()].pt
可以工作。实际上,上面的代码又是什么意思?正确的方式是不是d1[(d[d.x > 0].jets[d.jets.pt>0].count())]
或类似的?呃,好吧 - 目前要做的就是对规则进行严格限制,我们可以在以后添加使生活更简单的东西。 -
有时函数被定义在毫无意义的地方。例如,
abs
(或任何numpy
函数)总是被定义,即使你的DataFrame
代表一组喷射。这是将columns
和collections
作为不同对象来帮助用户,并帮助编辑猜测可能性的原因。 -
DataFrame
中不应存在parent
的概念。表达式应该是所有内容,并指向任何引用的对象。如果将来要使用多个根DataFrame
,这一点将特别正确。 -
使用"="符号定义新列是否很重要?例如,
df.jets.ptgev = df.jets.pt/1000.0
? -
每个相同的表达式都意味着相同的隐含迭代器的规则。这意味着当前代码无法做两个喷射,例如。然而,有几种方法可以"修复"这个问题,但最大的问题是:这是否合理?
-
exclusive_object
的功能是在运行时实现的 - 我们或许可以想出一个方案,其中我们只定义对象,它们可以"正确地"适应?这样,编辑器等就可以将其标记为问题。
项目详情
下载文件
下载适用于您平台的项目文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。