高级用法#
警告
本节内容正在审核中,可能不是最新版本。
本节涵盖前几节未涉及到的更高级modelx概念和技术。
高级概念#
在本节中,我们将介绍更多尚未涉及的概念。其中部分概念会在下一节通过示例进行演示。
空间成员#
空间(Spaces)可以包含单元格(Cells)和其他空间。实际上,空间有三种类型的"成员"。您可以通过属性访问(.
)表达式,将这些成员当作包含空间的属性来获取。
- Cells
正如我们在前面的例子中看到的,空间包含单元格,而单元格属于空间。一个单元格必须且只能属于一个空间。
Space的
cells
属性返回一个字典,其中包含所有与其名称关联的单元格。- (Sub)spaces
如前所述,空间可以在另一个空间中创建。位于另一个空间内的空间称为包含空间的子空间。子空间有两种类型:静态子空间和动态子空间。
静态子空间是指手动创建的子空间,就像在模型中创建的那些一样。直接在模型下创建的空间与在空间下创建的静态空间之间没有区别,除了它们的父级类型不同。
您可以通过调用父空间的
new_space
方法来创建静态子空间:model, space = new_model(), new_space() space.new_space('Subspace1') @defcells def foo(): return 123
您可以将子空间作为父空间的属性获取, 或通过访问父空间的
spaces
属性来获取:>>> space.spaces['Subspace1'].foo() 123 >>> space.Subspace1.foo() 123
另一种子空间类型是动态子空间。 与静态子空间不同,动态子空间只能在空间内创建, 而不能直接在模型中创建。
动态空间是通过对其父空间进行调用(
()
)或下标([]
)操作时即时创建的参数化空间。我们将在下一节结合空间继承的示例,更深入地探讨动态空间。- References
很多时候,您希望从空间中的单元格公式访问同一空间内除单元格或子空间之外的其他对象。 引用(References)是指绑定到任意对象的名称,这些对象可以从同一空间内访问:
model, space = new_model(), new_space() @defcells def bar(): return 2 * n
bar
单元格引用了尚未定义的n
。 在未定义n
的情况下调用bar
会引发错误。 要定义引用n
,只需为space
的n
属性赋值即可:>>> space.n = 3 >>> bar() 6
refs
属性返回一个引用名称到其对象的映射:>>> list(space.refs.keys()) ['__builtins__', 'n', '_self']
默认情况下,
__builtins__
和_self
在任何空间中都已定义。实际上,__builtins__
在模型中默认被定义为"全局"引用。全局引用是指在模型的任何空间中都可访问的名称。除了默认引用外,您还可以通过简单地将值赋给模型的属性来定义自己的引用:>>> model.z = 4 >>> list(model.refs.keys()) ['z', '__builtins__'] >>> list(space.refs.keys()) ['z', '__builtins__', 'n', '_self']
__builtins__
指向Python内置模块。 定义它是为了让单元格公式可以使用内置函数。_self
指向空间本身。这使得单元格公式 能够访问其父空间。
如前所述,单元格的公式会在与其父空间关联的命名空间中进行计算。
空间的命名空间是将名称映射到空间成员的映射关系。 如前一节所述, 空间成员可以是空间的单元格、空间的子空间, 或是可从空间访问的引用。
下表按类型和子类型分类展示了该命名空间中的所有成员。
cells |
self cells |
在该空间内定义或重写的单元格 |
派生单元格 |
从基础空间之一继承的单元格 |
|
空间 |
自身空间 |
在空间中定义或覆盖的子空间 |
派生空间 |
从基础空间继承而来的子空间 |
|
引用 (refs) |
自身引用 |
在空间中定义或覆盖的引用 |
派生引用 |
从基础空间之一继承的引用 |
|
全局引用 |
在父模型中定义的全局引用 |
|
本地引用 |
只有 |
|
parameters |
(仅限动态空间) 空间参数 |
每种成员类型都有"自身"成员和"派生"成员。 这种区分源于下一节将解释的空间继承机制。
空间继承#
modelx中的继承功能类似于面向对象编程语言(如Python)中的类继承。通过充分利用空间继承,您可以将具有相似特征的多个空间组织成空间继承树,从而最大限度地减少重复的公式定义,保持模型的条理性和透明度,同时维护模型的完整性。
继承允许一个空间使用(继承)其他空间作为基础空间。 继承的空间被称为基础空间的派生空间。 基础空间中的单元格会自动复制到派生空间中。 在派生空间中,可以覆盖来自基础空间的单元格公式。 您还可以向派生空间添加单元格,这些单元格在任何基础空间中都不存在。
一个空间可以拥有多个基础空间,这被称为多重继承。 基础空间可以有自己的基础空间,派生空间与基础空间之间的关系构成了一个有向依赖图。 在多重继承的情况下,我们需要一种方法来排序基础空间以确定其优先级。 modelx采用与Python相同的基类排序算法,即C3超类线性化算法(又称C3方法解析顺序或MRO)。 以下链接供您深入了解C3 MRO时参考。
更复杂的示例#
让我们通过一个简单的寿险定价代码来看看继承是如何工作的。
首先,我们将创建一个非常简单的寿险模型作为一个空间,并将其命名为
Life
。
然后,我们将在该空间中填充计算死亡人数和按年龄划分的剩余生存人数的单元格。
接下来,为了给定期寿险保单定价,我们将从Life
空间派生出一个TermLife
空间,并添加一些单元格来计算支付给被保险人的死亡保险金及其现值。
接下来,我们想为一份两全保险建模。由于两全保险除了定期寿险涵盖的身故保险金外,还会支付满期保险金,我们从TermLife
派生出一个Endowment
空间,并对benefits
公式进行剩余调整。
创建Life空间#
以下是我们将构建为Life
空间的生命模型的数学表示。
其中,\(l(x)\)表示年龄x时的存活人数, \(d(x)\)表示年龄x到x+1岁之间的死亡人数,\(q\)表示年死亡率 (为简化起见,我们暂时假设所有年龄段的死亡率恒定为0.003)。 在实际应用中,像l、d、q这样的单字母名称可能过于简短, 但我们在此使用它们仅是为了简化,正如它们经常出现在经典的精算教科书中。 另一个简化之处是,我们将起始年龄x设为50岁, 只是为了缩短输出结果。只要我们使用恒定的死亡率, 起始年龄是0岁还是50岁都不应该影响结果。 以下是这个生命模型的modelx代码:
model, life = new_model(), new_space('Life')
def l(x):
if x == x0:
return 100000
else:
return l(x - 1) - d(x - 1)
def d(x):
return l(x) * q
def q():
return 0.003
l, d, q = defcells(l, d, q)
life.x0 = 50
上面代码的倒数第二行与在每个函数定义前添加@defcells
装饰器效果相同。这一行从Life
空间中的3个函数创建了3个新单元格,并将名称l
、d
、q
重新绑定到当前作用域中的这3个单元格。
你一定注意到了l(x)
公式中引用了名称x0
,但该名称尚未定义。
最后一行代码的作用是将x0
定义为Life
模型中的投保年龄,并为其赋值。
要检查这个空间,我们可以查看Life
中的单元格值,如下所示:
>>> l(60)
97040.17769489168
>>> life.frame
l d q
x
50.0 100000.000000 300.000000 NaN
51.0 99700.000000 299.100000 NaN
52.0 99400.900000 298.202700 NaN
53.0 99102.697300 297.308092 NaN
54.0 98805.389208 296.416168 NaN
55.0 98508.973040 295.526919 NaN
56.0 98213.446121 294.640338 NaN
57.0 97918.805783 293.756417 NaN
58.0 97625.049366 292.875148 NaN
59.0 97332.174218 291.996523 NaN
60.0 97040.177695 NaN NaN
NaN NaN NaN 0.003
推导定期寿险空间#
接下来,我们将了解如何扩展这个空间来表示定期寿险保单。
为了简化问题,这里我们重点关注保额为1(以任意货币单位计)的单一保单。
基于这个假设,如果我们定义benefits(x)
为被保险人在年龄x至x+1岁之间所支付保险金的签发时期望值,那么它应该等于被保险人在签发时点处于x至x+1岁之间的死亡概率。用数学表达式表示应为:
其中 \(l(x)\) 和 \(d(x)\) 沿用前例中的相同定义,\(x0\) 表示保单的投保年龄。 我们进一步将年龄x时的给付现值定义为:
n
表示以年为单位的保单期限,disc_rate
表示用于现值计算的折现率。
接续之前的代码,我们将从Life
空间派生出TermLife
空间,以添加保险金和现值计算。
term_life = model.new_space(name='TermLife', bases=life)
@defcells
def benefits(x):
if x < x0 + n:
return d(x) / l(x0)
if x <= x0 + n:
return 0
@defcells
def pv_benefits(x):
if x < x0:
return 0
elif x <= x0 + n:
return benefits(x) + pv_benefits(x + 1) / (1 + disc_rate)
else:
return 0
上面的示例中第一行创建了从Life
空间派生的TermLife
空间,通过将Life
空间作为bases
参数传递给模型的new_space
方法。此时TermLife
空间与其唯一的基础空间Life
空间具有相同的单元格。
以下两个单元格定义(带有defcells
装饰器的两个函数定义),用于添加Life
空间中不存在的单元格。这些公式引用了尚未定义的名称,即n
和disc_rate
。我们需要在TermLife
空间中定义这些名称。引用x0
是从Life
空间继承而来的。
term_life.n = 10
term_life.disc_rate = 0
通过检查TermLife
空间,您将获得以下结果(DataFrame中的列顺序在您的屏幕上可能有所不同):
>>> term_life.pv_benefits(50)
0.02959822305108317
>>> term_life.frame
d q l pv_benefits benefits
x
50.0 300.000000 NaN 100000.000000 0.029598 0.003000
51.0 299.100000 NaN 99700.000000 0.026598 0.002991
52.0 298.202700 NaN 99400.900000 0.023607 0.002982
53.0 297.308092 NaN 99102.697300 0.020625 0.002973
54.0 296.416168 NaN 98805.389208 0.017652 0.002964
55.0 295.526919 NaN 98508.973040 0.014688 0.002955
56.0 294.640338 NaN 98213.446121 0.011733 0.002946
57.0 293.756417 NaN 97918.805783 0.008786 0.002938
58.0 292.875148 NaN 97625.049366 0.005849 0.002929
59.0 291.996523 NaN 97332.174218 0.002920 0.002920
60.0 NaN NaN NaN 0.000000 0.000000
61.0 NaN NaN NaN 0.000000 NaN
NaN NaN 0.003 NaN NaN NaN
可以看到l
、d
、q
单元格的值与Life
空间中的值相同,因为Life
和LifeTerm
对这些单元格使用了完全相同的公式。但请注意,这些单元格在基础空间和派生空间之间并不共享。
与面向对象编程语言中的类继承不同,空间继承是基于空间实例(或对象)而非类的,因此在创建派生空间时,单元格会从基础空间复制到派生空间。
推导禀赋空间#
我们将创建另一个空间来测试覆盖继承单元格的功能。
我们将从LifeTerm
空间派生出Endowment
空间。下图展示了这里考虑的3个空间之间的关系。
箭头起始的空间是从箭头指向的空间派生而来的。

寿险、定期寿险与储蓄寿险#
该寿险保单在保单期限结束时支付1的满期保险金。
我们将benefits
单元格定义为保险金的期望值,
因此在LifeTerm
空间中考虑的死亡保险金之外,
我们还会通过在Endowment
空间中重写benefits
定义
来添加满期保险金。实际上,被保险人不会同时获得死亡
和满期保险金,但这里我们考虑的是概率模型,
因此保险金将是死亡和满期保险金期望值的总和:
endowment = model.new_space(name='Endowment', bases=term_life)
@defcells
def benefits(x):
if x < x0 + n:
return d(x) / l(x0)
elif x == x0 + n:
return l(x) / l(x0)
else:
return 0
对Endowment
空间执行相同操作会产生以下结果:
>>> endowment.pv_benefits(50)
1.0
>>> endowment.frame
pv_benefits benefits l q d
x
50.0 1.000000 0.003000 100000.000000 NaN 300.000000
51.0 0.997000 0.002991 99700.000000 NaN 299.100000
52.0 0.994009 0.002982 99400.900000 NaN 298.202700
53.0 0.991027 0.002973 99102.697300 NaN 297.308092
54.0 0.988054 0.002964 98805.389208 NaN 296.416168
55.0 0.985090 0.002955 98508.973040 NaN 295.526919
56.0 0.982134 0.002946 98213.446121 NaN 294.640338
57.0 0.979188 0.002938 97918.805783 NaN 293.756417
58.0 0.976250 0.002929 97625.049366 NaN 292.875148
59.0 0.973322 0.002920 97332.174218 NaN 291.996523
60.0 0.970402 0.970402 97040.177695 NaN NaN
61.0 0.000000 NaN NaN NaN NaN
NaN NaN NaN NaN 0.003 NaN
你可以看到pv_benefits
对应所有年龄段的值,以及benefits
对应60岁的值,它们与TermLife
显示的值不同,因为我们重写了benefits
。
pv_benefits(50)
结果为1并不令人意外。在TermLife
空间中设置为1的disc_rate
也会被Endowment
空间继承。收益折现率为1意味着,在计算收益现值时,我们实际上只是将所有未来收益的期望值相加,其结果必然等于1,因为被保险人100%会获得1的赔付。
动态空间#
在许多情况下,您希望将一组计算应用于某个空间或空间树中的不同数据集。通过动态空间的空间继承功能即可实现这一目标。
动态空间是通过参数化的空间,当通过对其父空间的调用(()
)或下标([]
)操作请求时即时创建。
要在父空间中定义动态空间,您需要创建一个带有参数函数的空间,该函数的签名用于定义空间参数。参数函数应返回(如果有的话)参数名到其参数的映射,以便在创建动态空间时传递给new_space
方法。
为了理解其工作原理,让我们继续前面的示例。
在上一个示例中,我们手动将保单的问题年龄x0
设置为50,并将保单期限n
设置为10。
我们将扩展这个示例,创建具有不同保单属性的动态空间策略。
假设我们有3份具有以下属性的定期寿险保单:
保单编号 |
签发年龄 |
保单期限 |
---|---|---|
1 |
50 |
10 |
2 |
60 |
15 |
3 |
70 |
5 |
我们将创建这个示例数据作为一个嵌套列表:
data = [[1, 50, 10], [2, 60, 15], [3, 70, 5]]
该图展示了我们将要创建的模型设计。
一端带有实心菱形标记的线条表示
Policy
模型是三个动态空间 Policy1
、
Policy2
、Policy3
的父空间,每个动态空间分别代表
上述三个保单之一。
虽然 Policy
是这三个动态空间的父空间,
但它同时也是它们的基础空间。
Policy
空间从 Term
模型继承其成员,而
Policy
又被这三个动态空间所继承。
这种继承关系由实心菱形旁边的空心箭头表示。

以下是根据我们上述设计扩展模型的脚本。
def params(policy_id):
return {'name': 'Policy%s' % policy_id,
'bases': _self}
policy = model.new_space(name='Policy', bases=term_life, formula=params)
policy.data = data
@defcells
def x0():
return data[policy_id - 1][1]
@defcells
def n():
return data[policy_id - 1][2]
params
函数作为formula
参数的实参传递给Policy
空间的构造函数。params
函数的签名用于确定动态空间的参数,当创建动态空间时,返回的字典会作为参数传递给new_space
。
当您通过调用Policy
的第n个元素来创建其动态子空间时,会调用params
函数。params
在Policy
的命名空间中被求值。_self
是一个指向Policy
的特殊引用。
参数 policy_id
在每个动态空间的命名空间内可用。
在每个动态空间中,x0
和 n
的值都是从 data
中为每个保单获取的:
>>> policy(1).pv_benefits(50)
0.02959822305108317
>>> policy(2).pv_benefits(60)
0.04406717516109439
>>> policy(3).pv_benefits(70)
0.014910269595243001
>>> policy(3).frame
n x0 d benefits l pv_benefits q
x
NaN 5.0 70.0 NaN NaN NaN NaN 0.003
70.0 NaN NaN 300.000000 0.003000 100000.000000 0.014910 NaN
71.0 NaN NaN 299.100000 0.002991 99700.000000 0.011910 NaN
72.0 NaN NaN 298.202700 0.002982 99400.900000 0.008919 NaN
73.0 NaN NaN 297.308092 0.002973 99102.697300 0.005937 NaN
74.0 NaN NaN 296.416168 0.002964 98805.389208 0.002964 NaN
75.0 NaN NaN NaN 0.000000 NaN 0.000000 NaN
76.0 NaN NaN NaN NaN NaN 0.000000 NaN
>>> policy.spaces
{'Policy1': <Space Policy[1] in Model1>,
'Policy2': <Space Policy[2] in Model1>,
'Policy3': <Space Policy[3] in Model1>}
基础空间的动态空间不会传递给派生空间。
当一个空间从具有动态创建子空间的基础空间派生时,那些动态创建的子空间本身不会传递给派生空间。相反,基础空间的参数函数会被继承,因此派生空间的子空间会在调用(使用()
)或下标(使用[]
)操作符时根据参数创建。
读取Excel文件#
您可以将存储在Excel文件中的数据读取到新创建的单元格中。
Space有两种方法new_cells_from_excel
和new_space_from_excel
。
new_space_from_excel
在Model上也可用。
您的Python环境中需要安装Openpyxl包才能使用这些方法。
new_cells_from_excel
方法从Excel文件的一个区域读取数值,创建单元格并用该区域中的数值填充它们。
new_space_from_excel
方法从Excel文件的一个范围中读取值,
创建一个空间,并在该空间中,
使用一个或多个索引行和/或列作为空间参数创建动态空间,
并在动态空间中创建单元格,用该范围内的值填充它们。
有关这些方法的具体描述,请参考modelx的参考文档。
导出为Pandas对象#
如果你的Python环境中安装了Pandas,可以将单元格的值导出到Pandas的DataFrame或Series对象中。
空间具有frame
属性,该属性会生成一个DataFrame对象,其列名为单元格名称,索引为单元格参数。一个空间中的多个单元格可能具有不同的参数集。生成的