背景
我们在《报童问题》、《报童问题的简单解法》等文中介绍了一种通过考虑需求的不确定性来最大化销售利润的商品采购模型:首先预测需求所服从的概率分布,然后取能使得期望收益最大的分位数作为预估的需求,据此来决定采购量,对应的分位值定义为服务水平。
在实际应用中,一次采购需要满足未来一段时间的总需求,具体是多长时间取决于商品的提前期和目标库转等因素。理论上我们可以直接预测这段时间的总需求所服从的概率分布。但站在甲方的角度,一段时间的总需求?还概率分布?没概念啊!你不告诉我每天的情况,直接丢一个最终结果给我,我怎么知道你靠不靠谱呢?
甲方有这样的需求无疑是十分合理的,作为乙方我们没有理由不去满足。为此,我们可以预测每一天的需求所服从的概率分布,然后计算总需求所服从的概率分布。如下图所示。
考虑到可控性,需要允许甲方人为调整服务水平。站在甲方的角度看,问题又来了,我调整服务水平的时候,只能看到预估的总需求量在变,我想知道对应的每天的需求量是怎么变的。这就不好讲了,您想啊,总需求多 10 件:有可能是第一天多了 5 件,第二天多了 3 件,第三天多了 2 件;也有可能是第一天多了 1 件,第二天多了 3 件,第三天多了 6 件……可能的情况多了去了。不出意外的话,甲方这时候就会问了,在这么多的情况中,你能不能告诉我哪一种的可能性最高呢?你不是概率分布预测吗?算算概率呗。
甲方有这样的需求无疑是十分合理的,作为乙方我们没有理由不去满足。为此,我们需要求解一个最大化条件概率的问题。
问题
考虑一组独立的随机变量 X1,X2,⋯,Xn,它们各自所服从的概率分布已知。
令
X=(X1,X2,⋯,Xn)Z=i=1∑nXi
给定一个数 z,求使得条件概率 P(X=x∣Z=z) 最大的 x 的取值,即
x∗=xmaxP(X=x∣Z=z)
求解
我们用 fi(xi) 表示随机变量 Xi 的概率密度函数或概率质量函数,则
P(X=x∣Z=z)∝fn(z−i=1∑n−1xi)⋅i=1∏n−1fi(xi)≡g(x1,x2,⋯,xn−1)
定义
η(x)=f(x)f′(x)
令
∂xi∂g=0=−fn′(z−j=1∑n−1xj)⋅j=1∏n−1fj(xj)+fn(z−j=1∑n−1xj)⋅j=1,j=i∏n−1fj(xj)⋅fi′(xi)=−ηn(z−j=1∑n−1xj)⋅fn(z−j=1∑n−1xj)⋅j=1∏n−1fj(xj)+fn(z−j=1∑n−1xj)⋅j=1,j=i∏n−1fj(xj)⋅fi(xi)⋅ηi(xi)=g⋅(−ηn(z−j=1∑n−1xj)+ηi(xi))
得
ηi(xi)=ηn(z−j=1∑n−1xj)
也就是说,原问题的解 x∗=(x1,x2,⋯,xn) 满足
{x1+x2+⋯+xn=zη1(x1)=η2(x2)=⋯=ηn(xn)
对于正态分布,有
η(x)=σ2μ−x
如果随机变量 X1,X2,⋯,Xn 均服从正态分布,则只需要求解线性方程组
⎣⎢⎢⎢⎢⎢⎡1−1/σ120⋮010−1/σ22⋮0⋯⋯⋯⋱⋯100⋮−1/σn−1211/σn21/σn2⋮1/σn2⎦⎥⎥⎥⎥⎥⎤⎣⎢⎢⎢⎢⎢⎡x1x2x3⋮xn⎦⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎢⎡zμn/σn2−μ1/σ12μn/σn2−μ2/σ22⋮μn/σn2−μn−1/σn−12⎦⎥⎥⎥⎥⎥⎤
即可得到 x∗。
遗憾的是,销量通常并不服从正态分布。我们来考虑一下销量预测中常用的分布形式。
η(x)=lnλ−ψ(x+1)
η(x)=ψ(n−x+1)−ψ(x+1)+ln1−pp
η(x)=ψ(x+r)−ψ(x+1)+lnp
其中 ψ(x) 为 digamma 函数,定义为
ψ(x)=dxdln(Γ(x))=Γ(x)Γ′(x)
对于这些分布,η(x) 都是非线性的,无法通过求解线性方程组的方式来计算 x∗。考虑到
η′(x)=−ψ1(x+1)<0
η′(x)=−ψ1(n−x+1)−ψ1(x+1)<0
η′(x)=ψ1(x+r)−ψ1(x+1)<0ifr>1
其中 ψ1(x) 为 trigamma 函数,定义为
ψ1(x)=dx2d2ln(Γ(x))=dxdψ(x)
也就是说这些分布的 η(x) 都是单调递减的。因此只要令
ηminηmax=max(η1(z),η2(z),⋯,η2(z))=max(η1(nz),η2(nz),⋯,η2(nz))
就可以使用二分法求得 η∗∈[ηmin,ηmax],使得
i=1∑nxi∗=i=1∑nηi−1(η∗)=z
从而求得 x∗。如下图所示。
这里的问题在于我们并不知道 η(x) 的反函数 η−1(y) 的解析形式。好在同样可以使用二分法来求解(其实我原本用的是牛顿法,但实验中发现存在一些难以收敛的情况,故改用二分法)。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
| from abc import ABC, abstractmethod
import numpy as np from scipy.stats import norm, poisson, binom, nbinom from scipy.special import digamma
class Distribution(ABC): """ 概率分布基类 """ @property @abstractmethod def mu(self): """ 概率分布的期望 """ raise NotImplementedError()
@abstractmethod def quantile(self, q): """ 概率分布的 q 分位数 """ raise NotImplementedError() @abstractmethod def eta(self, x): """ $\eta(x)$ """ raise NotImplementedError() def ieta(self, y, x_min, x_max): """ $\eta^{-1}(y)$ """ while True: x_mid = (x_min + x_max) / 2 diff = self.eta(x_mid) - y if np.abs(diff) <= 1e-6: break if diff > 0: x_min = x_mid else: x_max = x_mid
if x_max == x_min: raise ValueError('Unable to solve') return x_mid
class Normal(Distribution): """ 正态分布 """ def __init__(self, mu, sigma): super().__init__() self._mu = mu self._sigma = sigma @property def mu(self): return self._mu def quantile(self, q): return norm.ppf(q=q, loc=self._mu, scale=self._sigma) def eta(self, x): return (self._mu - x) / self._sigma ** 2
def ieta(self, y, x_mid=None, x_max=None): return self._mu - y * self._sigma ** 2
class Poisson(Distribution): """ 泊松分布 """ def __init__(self, mu): super().__init__() self._mu = mu @property def mu(self): return self._mu def quantile(self, q): return poisson.ppf(q=q, mu=self._mu) def eta(self, x): return np.log(self._mu) - digamma(x + 1)
class Binomial(Distribution): """ 二项分布 """ def __init__(self, n, p): super().__init__() self._n = n self._p = p @property def mu(self): return self._n * self._p def quantile(self, q): return binom.ppf(q=q, n=self._n, p=self._p) def eta(self, x): return digamma(self._n - x + 1) - digamma(x + 1) + np.log(self._p/(1 - self._p)) def ieta(self, y, x_min, x_max): return super().ieta(y, x_min, min(x_max, self._n))
class NegativeBinomial(Distribution): """ 负二项分布 """ def __init__(self, r, p): super().__init__() self._r = r self._p = p @property def mu(self): return self._r * self._p / (1 - self._p) def quantile(self, q): return nbinom.ppf(q=q, n=self._r, p=1-self._p) def eta(self, x): return digamma(x + self._r) - digamma(x + 1) + np.log(self._p)
def max_posterior(distrs, z): """ 用二分法求解使条件概率 $P(X=\vec x|Z=z)$ 最大的 $\vec_x^*$ Parameters ---------- distrs : List<Distribution> 概率分布列表 z : float 总和的目标值 $z$ Returns ------- List<float> $\vec_x^*$ """ n = len(distrs) e_max = max(d.eta(z/n) for d in distrs) e_min = max(d.eta(z) for d in distrs) while True: e_mid = (e_min + e_max) / 2 xs = [d.ieta(e_mid, 0, z) for d in distrs] z_hat = sum(xs) if np.abs(z_hat - z) <= 1e-2: break if z_hat < z: e_max = e_mid else: e_min = e_mid
if e_max - e_min < 1e-6: raise ValueError('Unable to solve') return xs
|
我们用一个可以解析求解的例子来验证一下代码。设 X1∼N(0,1),X2∼N(0,4),z=5。根据前面的推导,只需要求解线性方程组:
[1−111/16][x1x2]=[50]
用高斯消元法解得 x1=5/17、x2=80/17。我们来看看数值解:
1 2 3 4 5 6 7 8 9
| >>> distrs = [Normal(0, 1), Normal(0, 4)] >>> print(max_posterior(distrs, 5)) [0.294189453125, 4.70703125] >>> >>> print(5/17) 0.29411764705882354 >>> >>> print(80/17) 4.705882352941177
|
可以看到,数值解法给出了与解析解非常接近的结果。
最后用一个复杂的例子直观地感受一下效果:
1 2 3 4 5 6 7 8 9 10 11 12
| distrs = [ Poisson(20), Poisson(19), Poisson(18), Poisson(19), Binomial(40, 0.2), Binomial(45, 0.25), NegativeBinomial(60, 0.3), NegativeBinomial(60, 0.25), Poisson(21), Poisson(20) ]
|
附录
附上 η(x) 的推导供感兴趣的同学参考。
f(x)=σ2π1exp(−2σ2(x−μ)2)
故
η(x)=f(x)f′(x)=dxdlnf(x)=dxd(lnσ2π1−2σ2(x−μ)2)=dxd(−2σ2(x−μ)2)=σ2μ−x
f(x)=x!e−λλx=Γ(x+1)e−λλx
故
η(x)=f(x)f′(x)=dxdlnf(x)=dxd(−λ+xlnλ−lnΓ(x+1))=lnλ−dxdlnΓ(x+1)=lnλ−ψ(x+1)
f(x)=(xn)px(1−p)n−x=x!(n−x)!n!px(1−p)n−x=Γ(x+1)Γ(n−x+1)Γ(n+1)px(1−p)n−x
故
η(x)=f(x)f′(x)=dxdlnf(x)=dxd(lnΓ(n+1)−lnΓ(x+1)−lnΓ(n−x+1)+xlnp+(n−x)ln(1−p))=dxd(−lnΓ(x+1)−lnΓ(n−x+1)+xlnp−xln(1−p))=−dxdlnΓ(n−x+1)−dxdlnΓ(x+1)+ln1−pp=ψ(n−x+1)−ψ(x+1)+ln1−pp
f(x)=(xx+r−1)px(1−p)r=x!(r−1)!(x+r−1)!px(1−p)r=Γ(x+1)Γ(r)Γ(x+r)px(1−p)r
故
η(x)=f(x)f′(x)=dxdlnf(x)=dxd(lnΓ(x+r)−lnΓ(x+1)−lnΓ(r)+xlnp+rln(1−p))=dxd(lnΓ(x+r)−lnΓ(x+1)+xlnp)=ψ(x+r)−ψ(x+1)+lnp
完。
参考文献