基础建模示例#

在第二个示例中,我们将建模一个固定利率抵押贷款,这是分期偿还抵押贷款的基本类型之一。 在固定利率抵押贷款中,借款人在一定期限内(如20年或30年)定期偿还相同金额。 要了解更多关于抵押贷款的信息,维基百科上有 一篇很好的文章 详细介绍了各种类型的抵押贷款。

在本练习中,我们假设按年进行还款。 我们使用一个知名公式对给定本金、利率和期限下的年还款金额进行建模。 同时,我们还对还款期限内每年的未偿还贷款余额进行建模。

通过这第二个练习,我们将学习许多新技术,例如:

  • 如何显式创建Model和Space

  • 如何设置现有单元格的公式

  • 单元格公式在哪些命名空间中求值,

  • 什么是引用以及如何定义它们,

  • 如何解读错误信息,

  • 如何更改引用的值,

  • 如何参数化Spaces以创建ItemSpaces

供您参考,抵押贷款也可以在不使用modelx的情况下进行建模。 如果您想了解如何使用Python和Pandas对抵押贷款进行建模, 请查看精彩文章Practical Business Python网站上。

显式创建模型和空间#

我们首先为本次练习创建一个新的MxConsole。 右键点击现有MxConsole或默认控制台的标签页, 然后选择新建MxConsole

../_images/OpenNewMxConsole2.png

MxConsole 标签页上下文菜单#

一个新的MxConsole标签页将打开,几秒钟后,一个新的IPython会话就准备好在MxConsole中接收您的输入了。

在前面的例子中,在创建Cells之前,您没有显式地创建其父Space或Model,但modelx在创建Cells时自动为您生成了它们。这次,我们首先显式地创建一个Model和一个Space,并将它们命名为MortgageFixed

MxExplorer的空白处右键点击,选择新建模型。 在对话框的模型名称框中输入Mortgage。 当您输入Mortgage时,导入为框也会自动填充为Mortgage。 这样该模型就可以在MxConsole的IPython中通过名为Morgage的变量进行访问。点击确定

../_images/NewModelDialogMortgage.png

新建模型对话框#

现在你看到当前模型 - Mortgage显示在MxExplorer右上角的模型框中。

../_images/ModelSelectorMortgage.png

模型选择器#

接下来我们将在Mortgage中创建一个名为Fixed的新Space,它代表固定利率抵押贷款。

MxExplorer的空白处右键点击,选择新建空间。 在对话框中,您可以看到Mortgage已被选中 在父级框中。由于当前没有空间, 只有模型可以作为待创建空间的父级。

空间名称框中输入Fixed导入为框应自动填充为Fixed

基础空间框用于继承其他空间。 本练习不涉及继承概念, 因此此处留空并点击确定

../_images/NewSpaceDialogFixed.png

新建空间对话框#

现在你应该在MxExplorer中看到Fixed Space项目了。

../_images/MxExplorerFixed.png

MxExplorer#

创建单元格并定义其公式#

固定利率抵押贷款的年付款可以通过一个众所周知的公式计算,并表示如下:

\[Payment = Principal\cdot\frac{Rate(1+Rate)^{Term}}{(1+Rate)^{Term}-1}\]

其中 \(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。点击确定

../_images/NewCellsDialogPayment.png

新建单元格对话框#

您可以在MxExplorer中查看Fixed空间下创建的Payment单元格。选中Payment单元格,右键点击并选择显示属性项。Payment单元格的属性会显示在MxExplorer右侧的属性标签页中。

表达式 lambda: None 被设置为 Formula 属性的默认公式。在上方的 Formula 面板中输入 Payment 函数。

../_images/MxExplorerFixedPayment.png

MxExplorer#

另一个需要计算的项是未偿还贷款余额。 设\(Balance(t)\)为时间\(t\)时的贷款余额。 \(Balance(t)\)可以用以下递归公式表示:

\[\begin{split}&余额(t)=余额(t-1)\cdot(1+利率)-还款\qquad&(0

如果\(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。点击确定

../_images/NewCellsDialogBalance.png

新建单元格对话框#

按照您对Payment的操作方式,打开显示Balance的属性,并将上述函数放入公式面板中。

../_images/MxExplorerFixedBalanceWrongFormula.png

MxExplorer#

阅读错误消息#

Payment 公式 引用了诸如 PrincipalRateTerm 等名称。 我们尚未定义这些名称,因此计算 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 公式引用了名称 PrincipalRateTerm,因此我们需要定义这些名称。 假设本金为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字段中,PrincipalTerm的类型为Ref/int, 表示它们是引用对象,关联值的类型是int。 同理,Rate的类型字段显示为Ref/float, 这意味着它是一个引用对象,其值的类型是float

../_images/MxExplorerFixedReferences.png

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-financialpmt 函数可能仍存在于 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

../_images/MxExplorerBalance.png

MxExplorer#

重新计算余额:

>> Balance(30)
1.2096279533579946e-10

结果是1.2的10次方的倒数,实际上等于零。看起来直到第30年为止,每年的余额计算都是正确的。您可以通过dict(Balance)Balance.frame来检查余额值,还可以通过以下方式输出余额图表:

>>> Balance.frame.plot()

你将在Spyder的绘图部件中看到一条余额折线图,可以观察到线条平稳下降直至第30年时余额完全偿清。

../_images/BalanceGraph.png

抵押贷款余额#

修改引用值#

到目前为止,我们只考虑了一种本金、还款期限和利率的组合。通常,您还需要探索其他模式。例如,您可能想知道当还款期限为20年时的年度还款金额。

要将Term30更改为20,请按以下方式将20赋值给Terms

>>> Fixed.Term = 20

上述修改将付款期限调整为20年,并且PaymentBalance单元格的值被清空,因为它们的计算依赖于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 = 20Fixed.Rate = 0.04,如前一节所述。 像Term = 20Rate = 0.04这样的语句将不起作用,因为它们会被解释为仅在IPython的全局命名空间中定义变量。

参数化空间#

通过更改参考值来获取不同输入组合结果的一个缺点是,你一次只能得到一个输入组合的结果。如果你更新了参考值,那么之前值的结果就会消失。如果你想在后续计算中使用不同输入组合的结果,这会很不方便。

空间参数化是一项非常强大的功能,能够快速且自然地扩展一个基于特定输入组合编写的空间,使其成为参数化空间。 该参数化空间支持订阅运算符([])和调用运算符(())。通过这两种运算符向参数传递参数时,会在参数化空间中动态创建ItemSpace类型的子空间。 这些ItemSpace是只读空间,它们从父空间继承了子空间、单元格和引用,但那些与参数同名的引用值会被传入的参数覆盖。

使用此功能,您可以获取任意TermRate组合的结果,并维护所有组合的结果。 要通过TermRate参数化Fixed空间,请按照以下方式将引用名称的元组分配给Fixedparameters属性:

>>> Fixed.parameters = ("Term", "Rate")

你可以选择性地提供默认值。 例如,要为Term设置默认值30, 并为Rate设置默认值0.03,执行以下赋值操作:

>>> Fixed.parameters = ("Term=30", "Rate=0.03")

现在Fixed空间已通过TermRate参数化。 通过向Fixed空间添加参数作为订阅或调用操作符, 将在Fixed空间下创建一个新的子空间:

>>> Fixed[20, 0.03]
<ItemSpace Fixed[20, 0.03] in Mortgage>

ItemSpace拥有与父Space相同的Cells和References,除了TermRate的值,这些值被设置为参数:

>>> Fixed[20, 0.03].Term
20

>>> Fixed[20, 0.04].Rate
0.04

让我们尝试计算Payment 针对TermRate的不同组合:

>>> 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

在上面的代码中,你可以使用()来代替[]。 由于TermRate有默认值, 像下面这样的表达式会产生与上面相同的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空间下创建的。

../_images/ItemSpaces.png

MxExplorer中的ItemSpaces#

打开其中一个ItemSpace,你会看到其中的单元格(Cells)和引用(References)与父空间相同,除了TermRate这两个参数,它们的值被设置为该ItemSpace的参数值。

../_images/ItemSpaces2.png

MxExplorer中的ItemSpaces#

无需手动指定ItemSpaces的参数,您可以充分利用Python的迭代器和推导式表达式。例如,假设您想要比较所有可能的还款期限和利率组合下的年度付款金额,其中还款期限范围从20年开始,以5年为步长递增至35年,利率从2%到4%,步长为1%。对于此任务,您可以使用Python标准库中的product迭代器。以下代码展示了如何将所需结果获取为一个dict,其中以TermRate的元组作为键,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])获取它们时,它们就会出现。