利用python进行蒙特卡罗模拟

利用python进行蒙特卡罗模拟

我们可以构建许多复杂的模型来解决预测问题。但是,我们常用到的是基于历史平均值,直觉或者某些特定领域启发式发展出来的Excel模型。这种方法也许足够解决现在的问题,但是通过一些合理的方式,我们可以为预测提供更多信息。

蒙特卡罗模拟是一种更直观地理解潜在结果的方法,并且有助于避免“均值的缺陷”。整篇文章主要描述如何用python构建一个蒙特卡罗模拟用于预测销售佣金支出的潜在范围。这种方法足够解决你现在所遇到的问题,同时也提供有力的见解,这都是那些靠直觉构建的Excel模型所办不到的。

问题背景

例如,我们试着预测下一年的销售佣金支出。这个问题很容易去建模,因为我们知道怎样算佣金,可能还有记录之前几年的佣金支出数据。

从业务的角度来看,销售佣金预测是很有必要的。销售佣金是一大笔费用,所以有必要合理地计划这笔支出。另外,对比以前常用的测算销售佣金的过程,蒙特卡罗模拟可以提供相对低成本的优化。

在下面的示例中,5个销售人员的销售佣金如下所示:

此例中,佣金计算公式如下:

佣金 = 实际销售额 × 提成比例

提成比例根据销售计划完成百分比调整如下:

在构建蒙特卡罗模拟前,我们先了解一下在一般情况下预测明年佣金费用的过程。

销售佣金预测 –朴素法

假设一下,我们的任务是预测下一年的销售佣金支出并为此筹备资金。我们可能假设每个销售人员都全额完成销售目标,也即按4%比例赚取佣金。如下图所示:

但在实际业务过程中,不是每个人都能确保全额完成销售目标,因此该预测并不合理。

接下来,我们尝试了两组对于销售计划百分比的预测:

至此,我们有了更符合实际的预测,并且可以进行下一步筹资活动了。这时有人会产生几个疑虑a)我们对于以上这种预测有多少准确度?b)假如给出500人的销售团队,并给更多的提成比例阶级。我们该怎么做呢?

以上这种预测支出过程给蒙特卡罗模拟提供了最基础的迭代方式。将上述预测支出过程迭代数百次,我们可以得到一个下年度销售佣金支出预算的大致区间。由于蒙特卡罗模拟中需要大量重复上述预测支出的过程,用Excel去计算有些难度,python可以更好地完成迭代任务。

蒙特卡罗

我们可以更进一步讨论佣金支出预算问题。蒙特卡罗模拟是一个有效的工具,主要体现在这种方法能够根据每次迭代时输入的随机参数得到许多不同的情景,并且可以得到目标结果的分布情况。

利用上一节中的佣金预算方法,我们可以重复这一过程100次,甚至1000次之后,我们可以得到潜在的佣金支出的分布情况。该分布可以展示佣金支出落入某个区间的可能性。归根结底,这仅仅是一种预测,因此我们无法准确地预测佣金支出。但是我们拥有了更多的信息去为预算不足或预算超支的潜在风险制定方案。

要进行蒙特卡罗模拟,有两个要件:

相同的过程

随机的输入变量

我们参照上一节已经清楚了整个预测支出的流程。现在我们需要思考的是如何产生随机变量。

一种简易的方法是销售计划百分比在0%-200%之间取随机值。但是,由于我们过去每年都在支付佣金,因此我们可以理解佣金支出里的更多的细节(例如过去几年的销售人员销售进度的分布情况)。根据我们对于过去的经验,我们可以构建一个更符合实际的模型。

我们先看看历史销售目标完成状况的分布图,如下:

这个分布看起来似乎是一个均值100%,方差10%的正态分布。这有利于我们去模拟随机输入变量,以去匹配更真实的状况。

假如对于分布类型想要更多的了解,可以自行百度,谷歌搜索更多关于各种分布的详细资料。

建立基于python的蒙特卡罗模拟

这节主要用pandas去复刻Excel表格中的计算过程。在python中有其他方法去构建蒙特卡罗模拟,这里用pandas库为了方便Excel基础的用户去理解模拟过程。

首先,完成库的导入和绘图风格的设置:

import pandas as pd
import numpy as np
import seaborn as sns
sns.set_style('whitegrid')

此次建模,用了numpy库中的随机数生成器。numpy的优势在于它的随机数生成器可以根据预定义的分布去生成随机样本。

此前已经知道历史的佣金支出服从均值100%,方差10%的正态分布。在python中,我们可以如下定义:

avg = 1
std_dev = .1
num_reps = 500
num_simulations = 1000

现在可以根据历史的分布,用numpy产生一组销售计划完成百分比模拟数据:

pct_to_target = np.random.normal(avg, std_dev, num_reps).round(2)

本例中,为了整洁只保留小数点2位。以下展示500销售人员中部分的销售人员的销售计划完成百分比预测情况,以确认数值范围是否与预期相符:

array([0.92, 0.98, 1.1 , 0.93, 0.92, 0.99, 1.14, 1.28, 0.91, 1.  ])

可以看到使用销售目标百分比的正态分布可以改善模型。分布的选择可以根据你正在研究对象或者模型进行调整。但是,最好在分析并理解了新的数据分布之后,再确定所对应的分布并应用在你的模型中。

这里有一组需要被模拟的实际销售额目标图,直方图看起来如下:

很明显这组数据不符合正态分布。这个分布展示了销售额目标频数随着销售额的增加而减少。这个分布可以理解成:根据层级,渠道,地域等等的区分,需完成的销售额目标也不同。

为了模拟这组数据,可以使用均匀分布,但是给一些值分配较低的概率。这里我们使用了numpy.random.choice模块,代码如下:

sales_target_values = [75_000, 100_000, 200_000, 300_000, 400_000, 500_000]
sales_target_prob = [.3, .3, .2, .1, .05, .05]
sales_target = np.random.choice(sales_target_values, num_reps, p=sales_target_prob)

这组数据其实不具有代表性,这样做只是为了展示如何将不同的分布应用在我们的模型中。

现在有了两组不同分布的模拟数据(销售计划完成百分比数组和销售额目标数组),将它们并入一个DataFrame:

df = pd.DataFrame(index=range(num_reps), data={'Pct_To_Target': pct_to_target,
                                               'Sales_Target': sales_target})
df['Sales'] = df['Pct_To_Target'] * df['Sales_Target']

新的DataFrame如下:

从上边的代码中可以看到我们使用了两组模拟数组计算出了模拟的最终销售额。对于这个情况,最终销售额(sales)也许会随着年份的变化出现大幅波动,但是这组被组合的新数组的分布大体是被前两组数组确定下来的。

最后一步,需要根据销售目标完成进度映射对应的提成比例:

def calc_commission_rate(x):
    """ Return the commission rate based on the table:
    0-90% = 2%
    91-99% = 3%
    >= 100 = 4%
    """
    if x         return .02
    if x         return .03
    else:
        return .04

相比Excel,python优势在于可以创建更复杂的逻辑,且更易实现。现在我们向DataFrame里加入对应提成比例和销售佣金,代码如下:

df['Commission_Rate'] = df['Pct_To_Target'].apply(calc_commission_rate)
df['Commission_Amount'] = df['Commission_Rate'] * df['Sales']

最新的DataFrame如下:

至此,我们完成了一次销售佣金预测。我们复刻了Excel中的算法,并且加入了两组随机分布的数组。从上表,可以计算出这次模拟的销售佣金支出为2,923,100美元。

开始循环(Loop)

蒙特卡罗魅力之处在于循环许多次之后,可以看到最终结果的分布情况。在Excel中,可能需要VBA或者其他插件才能实现多次迭代。在python中,使用for语句可以自己定义循环次数。

另外,每一次模拟,可以将需要的结果保存在DataFrame中,方便分析更多数组的分布情况。

代码如下:

# Define a list to keep all the results from each simulation that we want to analyzeall_stats = []
# Loop through many simulations
for i in range(num_simulations):
    # Choose random inputs for the sales targets and percent to target
    sales_target = np.random.choice(sales_target_values, num_reps, p=sales_target_prob)
    pct_to_target = np.random.normal(avg, std_dev, num_reps).round(2)
    # Build the dataframe based on the inputs and number of reps
    df = pd.DataFrame(index=range(num_reps), data={'Pct_To_Target': pct_to_target,
                                                   'Sales_Target': sales_target})
    # Back into the sales number using the percent to target rate
    df['Sales'] = df['Pct_To_Target'] * df['Sales_Target']
    # Determine the commissions rate and calculate it
    df['Commission_Rate'] = df['Pct_To_Target'].apply(calc_commission_rate)
    df['Commission_Amount'] = df['Commission_Rate'] * df['Sales']
    # We want to track sales,commission amounts and sales targets over all the simulations
    all_stats.append([df['Sales'].sum().round(0),
                      df['Commission_Amount'].sum().round(0),
                      df['Sales_Target'].sum().round(0)])

上述代码中,每次模拟只有7个python语句。在普通的笔记本电脑上执行1000次模拟只需要2.75秒,可以根据需要去调整模拟的次数。在模拟次数的问题上,一般是随次数增加收益递减的。一百万次的模拟不一定比10,000次的模拟更有效,笔者的建议是:尝试不同的模拟次数,看看输出结果将如何变化。

为了分析模拟结果,将最终数据写入all_stats变量中:

results_df = pd.DataFrame.from_records(all_stats,  columns=['Sales','Commission_Amount', 'Sales_Target'])

结果如下:

results_df.describe().style.format('{:,}')

直方图如下:

可以看到,平均佣金支出为285万美元,标准差为103,000美元。同时佣金支出最低值250万美元,最高值320万美元。

基础上述结果,可以确定佣金支出会低于300万美元吗?显然不能(毕竟只是个预测)!蒙特卡罗模拟的优势在于,可以展示出易于理解的结果分布情况,并且凭借商业嗅觉和经验去做出合理的估计。

你也可以根据不同的假设去做模拟出不同的结果,例如:

提成比例调至5%

减少销售人员

分布中用更大的标准差

调整目标数组的分布

现在模型已经建立好了,只需要更改这些参数并重新运行模拟就好了。

蒙特卡罗的另一个优势是便于向最终用户解释该预测。最终用户也许没有良好数学背景,但也能直观地了解此模拟正在做什么以及如何评估潜在结果范围的可能性。

最后,笔者认为,用python比用Excel更易展示和理解。由于python是编程语言的缘故,它将整个计算流程都列了出来。

结论

蒙特卡罗模模拟是一个非常好用的预测工具。在Excel中,如果没有VBA或者一些付费的第三方插件,整个模拟很难进行。使用Numpy和Pandas库去建立模型和产生多组潜在的目标数组,并加以分析都变得相对简单直观。

另外,分析师可以通过改变参数实现更多的模拟情景,也可以根据需求加入更多的模型。最后,这些结论还能够与非技术人员分享,并且围绕结果的不确定性进行讨论。

本文来自互联网,由Tushare金融与技术翻译兴趣小组翻译,作者:One,西南财经大学应用数据本科,英国曼彻斯特大学金融数学硕士,金融分析师,专注于利用数据建立金融模型,发掘潜在投资价值。更多内容请关注“挖地兔”公众号。

相关阅读:

Python数据分析从小白到高手的几个步骤

Pandas 0.24发布,将放弃Python 2

VSCode Python插件3月发行版,优化了这几个重要功能

在Jupyter Notebook构建规范和可复用的数据分析过程

利用Python实现摘要自动提取,完美瘦身只需一行代码

雪球转发:3回复:7喜欢:24

全部评论

公主家的喵042304-04 01:35

其实就是调用了np的random函数重复抽样吧?
话说python居然没人做一个专门的蒙特卡洛模拟的库么。。

jackc04-03 17:33

很少看这这类基础文章了

Tushare挖地兔04-03 08:59

哈哈哈,今天最幽默的一句

GYouth04-03 08:57

雪球的一股清流

扬红旗04-03 08:51

Monte Carlo 我知道,Python 我也知道,可这两个放一块我不知道