基础建模示例#
在第二个示例中,我们将建模一个固定利率抵押贷款,这是分期偿还抵押贷款的基本类型之一。 在固定利率抵押贷款中,借款人在一定期限内(如20年或30年)定期偿还相同金额。 要了解更多关于抵押贷款的信息,维基百科上有 一篇很好的文章 详细介绍了各种类型的抵押贷款。
在本练习中,我们假设按年进行还款。 我们使用一个知名公式对给定本金、利率和期限下的年还款金额进行建模。 同时,我们还对还款期限内每年的未偿还贷款余额进行建模。
通过这第二个练习,我们将学习许多新技术,例如:
如何显式创建Model和Space
如何设置现有单元格的公式
单元格公式在哪些命名空间中求值,
什么是引用以及如何定义它们,
如何解读错误信息,
如何更改引用的值,
如何参数化Spaces以创建ItemSpaces。
供您参考,抵押贷款也可以在不使用modelx的情况下进行建模。 如果您想了解如何使用Python和Pandas对抵押贷款进行建模, 请查看精彩文章 在Practical Business Python网站上。
显式创建模型和空间#
我们首先为本次练习创建一个新的MxConsole。 右键点击现有MxConsole或默认控制台的标签页, 然后选择新建MxConsole。
MxConsole 标签页上下文菜单#
一个新的MxConsole标签页将打开,几秒钟后,一个新的IPython会话就准备好在MxConsole中接收您的输入了。
在前面的例子中,在创建Cells之前,您没有显式地创建其父Space或Model,但modelx在创建Cells时自动为您生成了它们。这次,我们首先显式地创建一个Model和一个Space,并将它们命名为Mortgage和Fixed。
在MxExplorer的空白处右键点击,选择新建模型。
在对话框的模型名称框中输入Mortgage。
当您输入Mortgage时,导入为框也会自动填充为Mortgage。
这样该模型就可以在MxConsole的IPython中通过名为Morgage的变量进行访问。点击确定。
新建模型对话框#
现在你看到当前模型 - Mortgage显示在MxExplorer右上角的模型框中。
模型选择器#
接下来我们将在Mortgage中创建一个名为Fixed的新Space,它代表固定利率抵押贷款。
在MxExplorer的空白处右键点击,选择新建空间。 在对话框中,您可以看到Mortgage已被选中 在父级框中。由于当前没有空间, 只有模型可以作为待创建空间的父级。
在空间名称框中输入Fixed。导入为框应自动填充为Fixed。
基础空间框用于继承其他空间。 本练习不涉及继承概念, 因此此处留空并点击确定。
新建空间对话框#
现在你应该在MxExplorer中看到Fixed Space项目了。
MxExplorer#
创建单元格并定义其公式#
固定利率抵押贷款的年付款可以通过一个众所周知的公式计算,并表示如下:
其中 \(Principal\) 是借款本金金额, \(Rate\) 是未偿还贷款余额的固定年利率, \(Term\) 是贷款期限的年数。
这个公式可以用Python函数表示如下:
def Payment():
return Principal * Rate * (1+Rate)**Term / ((1+Rate)**Term - 1)
在Python中,数学表达式里的**是幂运算符,因此上面的表达式(1+Rate)**Term计算的是(1+Rate)的Term次方。
让我们创建一个名为Payment的Cells,并通过上面的函数定义其公式。
这次,我们将按照与第一个练习不同的步骤来创建它:
我们先将Payment创建为一个空的Cells,
然后在创建完成后为其分配公式。
在MxExplorer上右键点击,从对话框中选择新建单元格。
然后在单元格名称框中输入Payment。
按照惯例,保持导入为复选框的勾选状态,以便将
Payment导入到MxConsole中的IPython。点击确定
新建单元格对话框#
您可以在MxExplorer中查看Fixed空间下创建的Payment单元格。选中Payment单元格,右键点击并选择显示属性项。Payment单元格的属性会显示在MxExplorer右侧的属性标签页中。
表达式 lambda: None 被设置为 Formula 属性的默认公式。在上方的 Formula 面板中输入 Payment 函数。
MxExplorer#
另一个需要计算的项是未偿还贷款余额。 设\(Balance(t)\)为时间\(t\)时的贷款余额。 \(Balance(t)\)可以用以下递归公式表示:
如果\(Payment\)通过前面的公式正确计算得出,那么\(Balance(Term)\)应该为0。
作为一个Python函数,上述公式可以表示如下:
def Balance(t):
if t > 0:
return Balance(t-1) * (1+Rate) - Payment
else:
return Principle
你可能已经注意到上面的代码有一个拼写错误 Principle,
但我们暂时保留这个错误,以便后续观察由拼写错误引发的报错。
在MxExplorer上右键点击,从对话框中选择新建单元格。
然后在单元格名称框中输入Balance。
保持导入为复选框的勾选状态,以便将
Balance导入到MxConsole中的IPython。点击确定
新建单元格对话框#
按照您对Payment的操作方式,打开显示Balance的属性,并将上述函数放入公式面板中。
MxExplorer#
阅读错误消息#
Payment 公式
引用了诸如 Principal、Rate 和 Term 等名称。
我们尚未定义这些名称,因此计算 Payment 时
应该会报错。在 MxConsole 中输入 Fixed.Payement(),
您应该会看到以下错误信息:
FormulaError: Error raised during formula execution
NameError: name 'Principal' is not defined
Formula traceback:
0: Mortgage.FixedRate.Payment(), line 3
Formula source:
def Payment():
return Principal * Rate * (1+Rate)**Term / ((1+Rate)**Term - 1)
错误信息由3个文本块组成。第一个块显示原始错误的类型和消息。
本例中的原始错误是NameError,因为名称Principal未定义。
第二个区块是公式回溯功能。
它展示了公式调用的堆栈信息,以单元格和参数对的形式呈现,
顶部是您调用的公式,底部则是引发错误的公式调用。
在上面的例子中,由于错误发生在首次公式调用时,
因此仅显示了一个公式调用:Payment()。
最后一个区块显示了引发错误的公式。
创建引用#
Payment 公式引用了名称 Principal、Rate 和 Term,因此我们需要定义这些名称。
假设本金为10万美元,利率为3%,还款期限为30年。
你可能认为在MxConsole中如下定义这些名称会起作用:
>>> Principal = 100000
>>> Rate = 0.03
>>> Term = 30
但实际上并非如此。这是因为,通过上述命令,你只是在IPython的全局命名空间中定义了这些名称。然而,Payment公式是在其父空间Fixed关联的命名空间中进行求值的。为了让Payment公式能够引用这些名称,你需要在Fixed空间中定义引用,如下所示:
>>> Fixed.Principal = 100000
>>> Fixed.Rate = 0.03
>>> Fixed.Term = 30
您刚刚在Fixed空间中创建了3个引用对象。
引用对象的作用是将其父命名空间中的名称绑定到任意对象。
现在您可以看到这3个项目已在MxExplorer中创建。
在Type字段中,Principal和Term的类型为Ref/int,
表示它们是引用对象,关联值的类型是int。
同理,Rate的类型字段显示为Ref/float,
这意味着它是一个引用对象,其值的类型是float。
MxExplorer#
获取计算结果#
现在你已经定义了Payment引用的所有References,调用Formula应该会成功:
>>> Payment()
5101.925932025255
要检查计算值是否正确,我们可以利用pmt函数,它来自numpy-financial包:
>>> import numpy_financial as npf
>>> npf.pmt(0.03, 30, 100000)
-5101.925932025255
你可以看到返回值的绝对值与Payment值相匹配。
注意
pmt 函数原本属于 numpy 包,目前在 numpy 中仍可使用,但已被弃用并迁移至独立包 numpy-financial。 如果您未安装 numpy-financial,pmt 函数可能仍存在于 numpy 中。
接下来尝试获取第30年的贷款余额:
>>> Balance(30)
你应该会得到以下错误,因为公式中存在拼写错误。
FormulaError: Error raised during formula execution
NameError: name 'Principle' is not defined
Formula traceback:
0: Mortgage.FixedRate.Balance(t=30), line 4
...
28: Mortgage.FixedRate.Balance(t=2), line 4
29: Mortgage.FixedRate.Balance(t=1), line 4
30: Mortgage.FixedRate.Balance(t=0), line 6
Formula source:
def Balance(t):
if t > 0:
return Balance(t-1) * (1+Rate) - Payment()
else:
return Principle
错误信息提示在Mortgage.FixedRate.Balance(t=0)的第6行抛出了NameError,因为在执行Mortgage.FixedRate.Balance(t=0)的命名空间中找不到名称Principle。
要修正拼写错误,请前往MxExplorer并在Formula面板中将Principle改为Principal。
MxExplorer#
重新计算余额:
>> Balance(30)
1.2096279533579946e-10
结果是1.2的10次方的倒数,实际上等于零。看起来直到第30年为止,每年的余额计算都是正确的。您可以通过dict(Balance)或Balance.frame来检查余额值,还可以通过以下方式输出余额图表:
>>> Balance.frame.plot()
你将在Spyder的绘图部件中看到一条余额折线图,可以观察到线条平稳下降直至第30年时余额完全偿清。
抵押贷款余额#
修改引用值#
到目前为止,我们只考虑了一种本金、还款期限和利率的组合。通常,您还需要探索其他模式。例如,您可能想知道当还款期限为20年时的年度还款金额。
要将Term从30更改为20,请按以下方式将20赋值给Terms:
>>> Fixed.Term = 20
上述修改将付款期限调整为20年,并且Payment和Balance单元格的值被清空,因为它们的计算依赖于Fixed.Term,除了Balance(0),它仅依赖于Principal。您可以通过内置函数len()来检查单元格有多少个值:
>>> len(Payment)
0
>>> len(Balance)
1
要获取年度付款金额,只需调用Payment:
>>> Payment()
6721.570759685908
同样适用于利率。如果您想知道利率为4%时的付款金额,将0.04赋值给Rate:
>>> Fixed.Rate = 0.04
>>> Payment()
7358.175032862885
在为Reference赋值时,请注意需要指定其父Space,例如Fixed.Term = 20和Fixed.Rate = 0.04,如前一节所述。
像Term = 20和Rate = 0.04这样的语句将不起作用,因为它们会被解释为仅在IPython的全局命名空间中定义变量。
参数化空间#
通过更改参考值来获取不同输入组合结果的一个缺点是,你一次只能得到一个输入组合的结果。如果你更新了参考值,那么之前值的结果就会消失。如果你想在后续计算中使用不同输入组合的结果,这会很不方便。
空间参数化是一项非常强大的功能,能够快速且自然地扩展一个基于特定输入组合编写的空间,使其成为参数化空间。
该参数化空间支持订阅运算符([])和调用运算符(())。通过这两种运算符向参数传递参数时,会在参数化空间中动态创建ItemSpace类型的子空间。
这些ItemSpace是只读空间,它们从父空间继承了子空间、单元格和引用,但那些与参数同名的引用值会被传入的参数覆盖。
使用此功能,您可以获取任意Term和Rate组合的结果,并维护所有组合的结果。
要通过Term和Rate参数化Fixed空间,请按照以下方式将引用名称的元组分配给Fixed的parameters属性:
>>> Fixed.parameters = ("Term", "Rate")
你可以选择性地提供默认值。
例如,要为Term设置默认值30,
并为Rate设置默认值0.03,执行以下赋值操作:
>>> Fixed.parameters = ("Term=30", "Rate=0.03")
现在Fixed空间已通过Term和Rate参数化。
通过向Fixed空间添加参数作为订阅或调用操作符,
将在Fixed空间下创建一个新的子空间:
>>> Fixed[20, 0.03]
<ItemSpace Fixed[20, 0.03] in Mortgage>
ItemSpace拥有与父Space相同的Cells和References,除了Term和Rate的值,这些值被设置为参数:
>>> Fixed[20, 0.03].Term
20
>>> Fixed[20, 0.04].Rate
0.04
让我们尝试计算Payment
针对Term和Rate的不同组合:
>>> Fixed[20, 0.03].Payment()
6721.570759685908
>>> Fixed[30, 0.03].Payment()
5101.925932025255
>>> Fixed[20, 0.04].Payment()
7358.175032862885
>>> Fixed[30, 0.04].Payment()
5783.009913366131
在上面的代码中,你可以使用()来代替[]。
由于Term和Rate有默认值,
像下面这样的表达式会产生与上面相同的ItemSpaces:
>>> Fixed[20].Payment()
6721.570759685908
>>> Fixed().Payment() # Or Fixed[()].Payment()
5101.925932025255
>>> Fixed(Rate=0.04).Payment()
7358.175032862885
>>> Fixed[30].Payment()
5783.009913366131
在MxExplorer中,您可以看到ItemSpaces是在Fixed空间下创建的。
MxExplorer中的ItemSpaces#
打开其中一个ItemSpace,你会看到其中的单元格(Cells)和引用(References)与父空间相同,除了Term和Rate这两个参数,它们的值被设置为该ItemSpace的参数值。
MxExplorer中的ItemSpaces#
无需手动指定ItemSpaces的参数,您可以充分利用Python的迭代器和推导式表达式。例如,假设您想要比较所有可能的还款期限和利率组合下的年度付款金额,其中还款期限范围从20年开始,以5年为步长递增至35年,利率从2%到4%,步长为1%。对于此任务,您可以使用Python标准库中的product迭代器。以下代码展示了如何将所需结果获取为一个dict,其中以Term和Rate的元组作为键,Payment作为值:
>>> from itertools import product
>>> {(term, rate): Fixed[term, rate/100].Payment() for term, rate in product(range(20, 36, 5), range(2, 5))}
{(20, 2): 6115.671812529034,
(20, 3): 6721.570759685908,
(20, 4): 7358.175032862885,
(25, 2): 5122.043841739468,
(25, 3): 5742.787103912777,
(25, 4): 6401.196278645458,
(30, 2): 4464.992229340292,
(30, 3): 5101.925932025255,
(30, 4): 5783.009913366131,
(35, 2): 4000.2209190750104,
(35, 3): 4653.929156959947,
(35, 4): 5357.732236826054}
上面的代码使用了一种称为
字典推导式的表达式形式。
如果您不熟悉这种表达式,
可以简单地使用for语句:
>>> result = {}
>>> for term, rate in product(range(20, 36, 5), range(2, 5)):
result[(term, rate)] = Fixed[term, rate/100].Payment()
>>> result
{(20, 2): 6115.671812529034,
(20, 3): 6721.570759685908,
(20, 4): 7358.175032862885,
(25, 2): 5122.043841739468,
(25, 3): 5742.787103912777,
(25, 4): 6401.196278645458,
(30, 2): 4464.992229340292,
(30, 3): 5101.925932025255,
(30, 4): 5783.009913366131,
(35, 2): 4000.2209190750104,
(35, 3): 4653.929156959947,
(35, 4): 5357.732236826054}
保存工作#
你可以按照第一个练习中的方式保存Model。 在MxExplorer的上下文菜单中选择Write Model, 然后按照第一个示例的相同步骤操作。
请注意,Model中的ItemSpaces不会被保存,因为它们是在您首次通过订阅或调用操作获取时动态创建的。因此,当您读取已保存的Model时,ItemSpaces并不存在,但当您尝试通过订阅或调用操作(例如Fixed[20, 0.02])获取它们时,它们就会出现。