Chapter 5

Original content created by Cam Davidson-Pilon

Ported to Python 3 and PyMC3 by Max Margenot (@clean_utensils) and Thomas Wiecki (@twiecki) at Quantopian (@quantopian)

Ported to Julia and Turing by Abid


Would you rather lose an arm or a leg?

Loss Functions

We introduce what statisticians and decision theorists call loss functions. A loss function is a function of the true parameter, and an estimate of that parameter

$$ L( \theta, \hat{\theta} ) = f( \theta, \hat{\theta} )$$

The important point of loss functions is that it measures how bad our current estimate is: the larger the loss, the worse the estimate is according to the loss function. A simple, and very common, example of a loss function is the squared-error loss:

$$ L( \theta, \hat{\theta} ) = ( \theta - \hat{\theta} )^2$$

The squared-error loss function is used in estimators like linear regression, UMVUEs and many areas of machine learning. We can also consider an asymmetric squared-error loss function, something like:

$$ L( \theta, \hat{\theta} ) = \begin{cases} ( \theta - \hat{\theta} )^2 & \hat{\theta} \lt \theta \\\\ c( \theta - \hat{\theta} )^2 & \hat{\theta} \ge \theta, \;\; 0\lt c \lt 1 \end{cases}$$

which represents that estimating a value larger than the true estimate is preferable to estimating a value below. A situation where this might be useful is in estimating web traffic for the next month, where an over-estimated outlook is preferred so as to avoid an underallocation of server resources.

A negative property about the squared-error loss is that it puts a disproportionate emphasis on large outliers. This is because the loss increases quadratically, and not linearly, as the estimate moves away. That is, the penalty of being three units away is much less than being five units away, but the penalty is not much greater than being one unit away, though in both cases the magnitude of difference is the same:

$$ \frac{1^2}{3^2} \lt \frac{3^2}{5^2}, \;\; \text{although} \;\; 3-1 = 5-3 $$

This loss function imposes that large errors are very bad. A more robust loss function that increases linearly with the difference is the absolute-loss

$$ L( \theta, \hat{\theta} ) = | \theta - \hat{\theta} | $$

Other popular loss functions include:

  • $L( \theta, \hat{\theta} ) = \mathbb{1}_{ \hat{\theta} \neq \theta }$ is the zero-one loss often used in machine learning classification algorithms.
  • $L( \theta, \hat{\theta} ) = -\theta\log( \hat{\theta} ) - (1- \theta)\log( 1 - \hat{\theta} ), \; \; \theta \in {0,1}, \; \hat{\theta} \in [0,1]$, called the log-loss, also used in machine learning.

Historically, loss functions have been motivated from 1) mathematical convenience, and 2) they are robust to application, i.e., they are objective measures of loss. The first reason has really held back the full breadth of loss functions. With computers being agnostic to mathematical convenience, we are free to design our own loss functions, which we take full advantage of later in this Chapter.

With respect to the second point, the above loss functions are indeed objective, in that they are most often a function of the difference between estimate and true parameter, independent of signage or payoff of choosing that estimate. This last point, its independence of payoff, causes quite pathological results though. Consider our hurricane example above: the statistician equivalently predicted that the probability of the hurricane striking was between 0% to 1%. But if he had ignored being precise and instead focused on outcomes (99% chance of no flood, 1% chance of flood), he might have advised differently.

By shifting our focus from trying to be incredibly precise about parameter estimation to focusing on the outcomes of our parameter estimation, we can customize our estimates to be optimized for our application. This requires us to design new loss functions that reflect our goals and outcomes. Some examples of more interesting loss functions:

  • $L( \theta, \hat{\theta} ) = \frac{ | \theta - \hat{\theta} | }{ \theta(1-\theta) }, \; \; \hat{\theta}, \theta \in [0,1]$ emphasizes an estimate closer to 0 or 1 since if the true value $\theta$ is near 0 or 1, the loss will be very large unless $\hat{\theta}$ is similarly close to 0 or 1. This loss function might be used by a political pundit whose job requires him or her to give confident "Yes/No" answers. This loss reflects that if the true parameter is close to 1 (for example, if a political outcome is very likely to occur), he or she would want to strongly agree as to not look like a skeptic.

  • $L( \theta, \hat{\theta} ) = 1 - \exp \left( -(\theta - \hat{\theta} )^2 \right)$ is bounded between 0 and 1 and reflects that the user is indifferent to sufficiently-far-away estimates. It is similar to the zero-one loss above, but not quite as penalizing to estimates that are close to the true parameter.

  • Complicated non-linear loss functions can programmed:

     def loss(true_value, estimate):
         if estimate*true_value > 0:
             return abs(estimate - true_value)
         else:
            return abs(estimate)*(estimate - true_value)**2
  • Another example is from the book The Signal and The Noise. Weather forecasters have an interesting loss function for their predictions.

People notice one type of mistake — the failure to predict rain — more than other, false alarms. If it rains when it isn't supposed to, they curse the weatherman for ruining their picnic, whereas an unexpectedly sunny day is taken as a serendipitous bonus.

[The Weather Channel's bias] is limited to slightly exaggerating the probability of rain when it is unlikely to occur — saying there is a 20 percent change when they know it is really a 5 or 10 percent chance — covering their butts in the case of an unexpected sprinkle.

As you can see, loss functions can be used for good and evil: with great power, comes great — well you know.

Loss functions in the real world

So far we have been under the unrealistic assumption that we know the true parameter. Of course if we knew the true parameter, bothering to guess an estimate is pointless. Hence a loss function is really only practical when the true parameter is unknown.

In Bayesian inference, we have a mindset that the unknown parameters are really random variables with prior and posterior distributions. Concerning the posterior distribution, a value drawn from it is a possible realization of what the true parameter could be. Given that realization, we can compute a loss associated with an estimate. As we have a whole distribution of what the unknown parameter could be (the posterior), we should be more interested in computing the expected loss given an estimate. This expected loss is a better estimate of the true loss than comparing the given loss from only a single sample from the posterior.

First it will be useful to explain a Bayesian point estimate. The systems and machinery present in the modern world are not built to accept posterior distributions as input. It is also rude to hand someone over a distribution when all they asked for was an estimate. In the course of an individual's day, when faced with uncertainty we still act by distilling our uncertainty down to a single action. Similarly, we need to distill our posterior distribution down to a single value (or vector in the multivariate case). If the value is chosen intelligently, we can avoid the flaw of frequentist methodologies that mask the uncertainty and provide a more informative result.The value chosen, if from a Bayesian posterior, is a Bayesian point estimate.

Suppose $P(\theta | X)$ is the posterior distribution of $\theta$ after observing data $X$, then the following function is understandable as the expected loss of choosing estimate $\hat{\theta}$ to estimate $\theta$:

$$ l(\hat{\theta} ) = E_{\theta}\left[ \; L(\theta, \hat{\theta}) \; \right] $$

This is also known as the risk of estimate $\hat{\theta}$. The subscript $\theta$ under the expectation symbol is used to denote that $\theta$ is the unknown (random) variable in the expectation, something that at first can be difficult to consider.

We spent all of last chapter discussing how to approximate expected values. Given $N$ samples $\theta_i,\; i=1,...,N$ from the posterior distribution, and a loss function $L$, we can approximate the expected loss of using estimate $\hat{\theta}$ by the Law of Large Numbers:

$$\frac{1}{N} \sum_{i=1}^N \;L(\theta_i, \hat{\theta} ) \approx E_{\theta}\left[ \; L(\theta, \hat{\theta}) \; \right] = l(\hat{\theta} ) $$

Notice that measuring your loss via an expected value uses more information from the distribution than the MAP estimate which, if you recall, will only find the maximum value of the distribution and ignore the shape of the distribution. Ignoring information can over-expose yourself to tail risks, like the unlikely hurricane, and leaves your estimate ignorant of how ignorant you really are about the parameter.

Similarly, compare this with frequentist methods, that traditionally only aim to minimize the error, and do not consider the loss associated with the result of that error. Compound this with the fact that frequentist methods are almost guaranteed to never be absolutely accurate. Bayesian point estimates fix this by planning ahead: your estimate is going to be wrong, you might as well err on the right side of wrong.

Example: Optimizing for the Showcase on The Price is Right

Bless you if you are ever chosen as a contestant on the Price is Right, for here we will show you how to optimize your final price on the Showcase. For those who forget the rules:

  1. Two contestants compete in The Showcase.
  2. Each contestant is shown a unique suite of prizes.
  3. After the viewing, the contestants are asked to bid on the price for their unique suite of prizes.
  4. If a bid price is over the actual price, the bid's owner is disqualified from winning.
  5. If a bid price is under the true price by less than $250, the winner is awarded both prizes.

The difficulty in the game is balancing your uncertainty in the prices, keeping your bid low enough so as to not bid over, and trying to bid close to the price.

Suppose we have recorded the Showcases from previous The Price is Right episodes and have prior beliefs about what distribution the true price follows. For simplicity, suppose it follows a Normal:

$$\text{True Price} \sim \text{Normal}(\mu_p, \sigma_p )$$

In a later chapter, we will actually use real Price is Right Showcase data to form the historical prior, but this requires some advanced PyMC3 use so we will not use it here. For now, we will assume $\mu_p = 35 000$ and $\sigma_p = 7500$.

We need a model of how we should be playing the Showcase. For each prize in the prize suite, we have an idea of what it might cost, but this guess could differ significantly from the true price. (Couple this with increased pressure being onstage and you can see why some bids are so wildly off). Let's suppose your beliefs about the prices of prizes also follow Normal distributions:

$$ \text{Prize}_i \sim \text{Normal}(\mu_i, \sigma_i ),\;\; i=1,2$$

This is really why Bayesian analysis is great: we can specify what we think a fair price is through the $\mu_i$ parameter, and express uncertainty of our guess in the $\sigma_i$ parameter.

We'll assume two prizes per suite for brevity, but this can be extended to any number. The true price of the prize suite is then given by $\text{Prize}_1 + \text{Prize}_2 + \epsilon$, where $\epsilon$ is some error term.

We are interested in the updated $\text{True Price}$ given we have observed both prizes and have belief distributions about them. We can perform this using PyMC3.

Lets make some values concrete. Suppose there are two prizes in the observed prize suite:

  1. A trip to wonderful Toronto, Canada!
  2. A lovely new snowblower!

We have some guesses about the true prices of these objects, but we are also pretty uncertain about them. I can express this uncertainty through the parameters of the Normals:

\begin{align} & \text{snowblower} \sim \text{Normal}(3 000, 500 )\\\\ & \text{Toronto} \sim \text{Normal}(12 000, 3000 )\\\\ \end{align}

For example, I believe that the true price of the trip to Toronto is 12 000 dollars, and that there is a 68.2% chance the price falls 1 standard deviation away from this, i.e. my confidence is that there is a 68.2% chance the trip is in [9 000, 15 000].

We can create some PyMC3 code to perform inference on the true price of the suite.

In [1]:
using Plots,Distributions

x        = LinRange(0,60000,200)
y        = LinRange(0, 10000, 200)
z        = LinRange(0, 25000, 200)
p1 = plot(x ,pdf(Normal(35000,7500),x),label= "historical total prices",size = (800, 600),xticks = 0:10000:60000,ylims = (0,0.00006),
           fillcolor ="#348ABD" ,fillrange = 0)
p2 = plot(y ,pdf(Normal(3000,500),y),label= "snowblower price guess",size = (800, 600),xticks = 0:2000:10000,ylims = (0,0.0008),
           fillcolor ="#A60628" ,fillrange = 0)
p3 = plot(z ,pdf(Normal(12000,3000),z),label= "Trip price guess",size = (800, 600),xticks = 0:5000:25000,ylims = (0,0.00014),
           fillcolor ="#7A68A6" ,fillrange = 0)
plot(p1, p2,p3, layout = (3, 1))
Out[1]:
In [2]:
using Turing

data_mu = [3e3, 12e3]

data_std =  [5e2, 3e3] 

mu_prior = 35e3
std_prior =  75e2

@model function Model(mu_prior,std_prior,data_mu,data_std)
    true_price     ~ Normal(mu_prior,std_prior)
    prize_1        ~ Normal(data_mu[1],data_std[1])
    prize_2        ~ Normal(data_mu[2],data_std[2])
    price_estimate = prize_1 + prize_2
    Error  = logpdf(Normal(price_estimate,3e3),true_price)

    # use pymc3.Potential to introduce arbitrary terms into the log likelihood in Python
    # use Turing.@addlogprob! to introduce arbitrary terms into the log likelihood in Julia
    # refer to https://github.com/TuringLang/Turing.jl/issues/1332 
    Turing.@addlogprob! Error
    
    return Error,price_estimate,prize_1,prize_2,true_price
end
true_price_model = Model(mu_prior,std_prior,data_mu,data_std)
chain = sample(true_price_model,MH(),204000)
Sampling: 100%|█████████████████████████████████████████| Time: 0:00:06
Out[2]:
Chains MCMC chain (204000×4×1 Array{Float64, 3}):

Iterations        = 1:1:204000
Number of chains  = 1
Samples per chain = 204000
Wall duration     = 17.74 seconds
Compute duration  = 17.74 seconds
parameters        = prize_1, prize_2, true_price
internals         = lp

Summary Statistics
  parameters         mean         std   naive_se      mcse         ess      rh     Symbol      Float64     Float64    Float64   Float64     Float64   Float ⋯

  true_price   19914.2339   3711.2851     8.2169   52.9674   3405.2040    1.00 ⋯
     prize_1    3073.7944    495.9761     1.0981    6.9986   3564.1624    1.00 ⋯
     prize_2   14417.5631   2836.0759     6.2792   40.3139   3344.5284    1.00 ⋯
                                                               2 columns omitted

Quantiles
  parameters         2.5%        25.0%        50.0%        75.0%        97.5% 
      Symbol      Float64      Float64      Float64      Float64      Float64 

  true_price   12659.3284   17395.3343   19943.8578   22411.2952   27212.4589
     prize_1    2082.6111    2746.4089    3077.9266    3401.6945    4036.6731
     prize_2    8709.9426   12464.2040   14434.3080   16346.6242   19918.1735
In [3]:
# Extract the traces
x  = LinRange(5000, 40000,50)
price_trace = chain[:true_price]
price_trace = price_trace[10000:length(price_trace),1]

histogram(price_trace,
          normalize = true,
          bins = 35 ,
          title = "Posterior of the true price estimate",
          titlefontsize = 10,
          size = (800, 400),
          label= nothing
          )
vline!([mean(price_trace)],label = "posterior's mean",line =  ( :dot,  0.5, 4,:red),legend=:topleft)
plot!(x ,pdf(Normal(35000,7500),x),label= "historical total prices",size = (800, 400),xticks = 5000:5000:40000,ylims = (0,0.00014),lw = 3,legend=:topleft)
vline!([mu_prior],label = "prior's mean",line =  ( :dot,  0.5, 4,:green),legend=:topleft)
Out[3]:

Notice that because of our two observed prizes and subsequent guesses (including uncertainty about those guesses), we shifted our mean price estimate down about \$15,000 dollars from the previous mean price.

A frequentist, seeing the two prizes and having the same beliefs about their prices, would bid $\mu_1 + \mu_2 = 35000$, regardless of any uncertainty. Meanwhile, the naive Bayesian would simply pick the mean of the posterior distribution. But we have more information about our eventual outcomes; we should incorporate this into our bid. We will use the loss function above to find the best bid (best according to our loss).

What might a contestant's loss function look like? I would think it would look something like:

def showcase_loss(guess, true_price, risk = 80000):
    if true_price < guess:
        return risk
    elif abs(true_price - guess) <= 250:
        return -2*np.abs(true_price)
    else:
        return np.abs(true_price - guess - 250)

where risk is a parameter that defines of how bad it is if your guess is over the true price. A lower risk means that you are more comfortable with the idea of going over. If we do bid under and the difference is less than $250, we receive both prizes (modeled here as receiving twice the original prize). Otherwise, when we bid under the true_price we want to be as close as possible, hence the else loss is a increasing function of the distance between the guess and true price.

For every possible bid, we calculate the expected loss associated with that bid. We vary the risk parameter to see how it affects our loss:

In [4]:
function showdown_loss(guess,true_price,risk = 80000)
    if true_price < guess
        loss = risk
    elseif abs(true_price - guess) <= 250
        loss = -2*abs(true_price)
    else
        loss = abs(true_price - guess - 250)
    end
    return loss
end

guesses = LinRange(5000, 50000, 70)
risks   = LinRange(30000, 150000, 6)

results = [mean(showdown_loss.(_g,price_trace,_p)) for _p in risks,_g in guesses]

p = plot(size = (800,550),
      title = "Expected loss of different guesses, \nvarious risk-levels of overestimating", xlims=(5000,30000),
      ylabel = "expected loss", yguidefontsize=10,titlefontsize = 10,legendtitle = "Risk Parameter",
      xlabel = "price bid", xguidefontsize=10,formatter = identity)

for i in 1:6
    y = risks[i]
    plot!(guesses,results[i,:],lw = 2,label = "$y " ,legend=:topleft,palette = :tab10)
end
p
Out[4]:
In [5]:
function showdown_loss(guess,true_price,risk = 80000)
    if true_price < guess
        loss = risk
    elseif abs(true_price - guess) <= 250
        loss = -2*abs(true_price)
    else
        loss = abs(true_price - guess - 250)
    end
    return loss
end

expected_loss(guess,risk) = mean(showdown_loss.(guess,price_trace,risk))
Out[5]:
expected_loss (generic function with 1 method)
In [6]:
import SciPy.optimize as sop
q = plot(size = (800,550),
      title = "Expected loss of different guesses, \nvarious risk-levels of overestimating", xlims=(5000,30000),
      ylabel = "expected loss", yguidefontsize=10,titlefontsize = 10,
      xlabel = "price bid", xguidefontsize=10,formatter = identity)
for _p in eachindex(risks)
    _min_results = sop.fmin(expected_loss, 15000, args=(risks[_p],),disp = false)
    y = risks[_p]
    plot!(guesses,results[_p,:],lw = 2,palette = :tab10,label = nothing)
    vline!([_min_results],label = "$y ",legend=:topright,palette = :tab10,legendtitle = "Bayes action at risk")
end
q
Out[6]:

As intuition suggests, as we decrease the risk threshold (care about overbidding less), we increase our bid, willing to edge closer to the true price. It is interesting how far away our optimized loss is from the posterior mean, which was about 20 000.

Suffice to say, in higher dimensions being able to eyeball the minimum expected loss is impossible. Hence why we require use of Scipy's fmin function.


Shortcuts

For some loss functions, the Bayes action is known in closed form. We list some of them below:

  • If using the mean-squared loss, the Bayes action is the mean of the posterior distribution, i.e. the value $$ E_{\theta}\left[ \theta \right] $$ minimizes $E_{\theta}\left[ \; (\theta - \hat{\theta})^2 \; \right]$. Computationally this requires us to calculate the average of the posterior samples [See chapter 4 on The Law of Large Numbers]

  • Whereas the median of the posterior distribution minimizes the expected absolute-loss. The sample median of the posterior samples is an appropriate and very accurate approximation to the true median.

  • In fact, it is possible to show that the MAP estimate is the solution to using a loss function that shrinks to the zero-one loss.

Maybe it is clear now why the first-introduced loss functions are used most often in the mathematics of Bayesian inference: no complicated optimizations are necessary. Luckily, we have machines to do the complications for us.

Machine Learning via Bayesian Methods

Whereas frequentist methods strive to achieve the best precision about all possible parameters, machine learning cares to achieve the best prediction among all possible parameters. Of course, one way to achieve accurate predictions is to aim for accurate predictions, but often your prediction measure and what frequentist methods are optimizing for are very different.

For example, least-squares linear regression is the most simple active machine learning algorithm. I say active as it engages in some learning, whereas predicting the sample mean is technically simpler, but is learning very little if anything. The loss that determines the coefficients of the regressors is a squared-error loss. On the other hand, if your prediction loss function (or score function, which is the negative loss) is not a squared-error, like AUC, ROC, precision, etc., your least-squares line will not be optimal for the prediction loss function. This can lead to prediction results that are suboptimal.

Finding Bayes actions is equivalent to finding parameters that optimize not parameter accuracy but an arbitrary performance measure, however we wish to define performance (loss functions, AUC, ROC, precision/recall etc.).

The next two examples demonstrate these ideas. The first example is a linear model where we can choose to predict using the least-squares loss or a novel, outcome-sensitive loss.

The second example is adapted from a Kaggle data science project. The loss function associated with our predictions is incredibly complicated.

Example: Financial prediction

Suppose the future return of a stock price is very small, say 0.01 (or 1%). We have a model that predicts the stock's future price, and our profit and loss is directly tied to us acting on the prediction. How should we measure the loss associated with the model's predictions, and subsequent future predictions? A squared-error loss is agnostic to the signage and would penalize a prediction of -0.01 equally as bad a prediction of 0.03:

$$ (0.01 - (-0.01))^2 = (0.01 - 0.03)^2 = 0.004$$

If you had made a bet based on your model's prediction, you would have earned money with a prediction of 0.03, and lost money with a prediction of -0.01, yet our loss did not capture this. We need a better loss that takes into account the sign of the prediction and true value. We design a new loss that is better for financial applications below:

In [7]:
function stock_loss(true_return, yhat, alpha = 100.)
    if true_return * yhat < 0
        #opposite signs, not good
        return alpha*yhat^2 - sign(true_return)*yhat + abs(true_return)
    else
        return abs(true_return-yhat)
    end
end
true_value = 0.05
pred = LinRange(-.04, .12, 75)
plot(pred,[stock_loss(true_value, _p) for _p in pred],size = (800,450),
        title = "Stock returns loss if true value = 0.05, -0.02",
        label = "Loss associated with\n prediction if true value = 0.05",lw = 3,
        xlabel = "prediction",ylabel = "loss",xlims = (-0.04,0.12),ylims=(0,0.25),
         yguidefontsize=10,titlefontsize = 10 )
true_value = -0.02
plot!(pred, [stock_loss(true_value, _p) for _p in pred],lw = 3 , color = :maroon,
       label = "Loss associated with\n prediction if true value = -0.02")
vline!([0.0],line =  ( :dot,  0.5, 4,:black),label = nothing)
Out[7]:

Note the change in the shape of the loss as the prediction crosses zero. This loss reflects that the user really does not want to guess the wrong sign, especially be wrong and a large magnitude.

Why would the user care about the magnitude? Why is the loss not 0 for predicting the correct sign? Surely, if the return is 0.01 and we bet millions we will still be (very) happy.

Financial institutions treat downside risk, as in predicting a lot on the wrong side, and upside risk, as in predicting a lot on the right side, similarly. Both are seen as risky behaviour and discouraged. Hence why we have an increasing loss as we move further away from the true price. (With less extreme loss in the direction of the correct sign.)

We will perform a regression on a trading signal that we believe predicts future returns well. Our dataset is artificial, as most financial data is not even close to linear. Below, we plot the data along with the least-squares line.

In [8]:
## Code to create artificial data
N = 100
X = 0.025*randn(N)
Y = 0.5*X + 0.01*randn(N)

ls_coef = cov(X,Y)/var(X)
ls_intercept = mean(Y) - ls_coef*mean(X)

scatter(X,Y,label = nothing,size = (800,450),color = :black)
plot!(X, ls_coef.*X .+ ls_intercept ,legend =:topleft,label = "Least-squares line",
      lw = 3,color = :red,title ="Empirical returns vs trading signal",
       xlabel = "trading signal",ylabel = "returns",yguidefontsize=10,titlefontsize = 10 )
Out[8]:

We perform a simple Bayesian linear regression on this dataset. We look for a model like:

$$ R = \alpha + \beta x + \epsilon$$

where $\alpha, \beta$ are our unknown parameters and $\epsilon \sim \text{Normal}(0, \sigma)$. The most common priors on $\beta$ and $\alpha$ are Normal priors. We will also assign a prior on $\sigma$, so that $\sigma$ is uniform over 0 to 100.

In [9]:
using Turing,Distributions

@model function Bayesian_LinReg(X_data,Y_data)
    σ ~ Uniform(0,0.01)
    
    β  ~ Normal(0,0.1)
    α  ~ Normal(0,0.1)
    μ  = α .+ β.*X_data
    
    for i in 1:length(Y_data)
        Y_data[i]  ~ Normal(μ[i],σ)
    end
end
Out[9]:
Bayesian_LinReg (generic function with 1 method)
In [10]:
using StatsPlots
Bayesian_LinReg_model = Bayesian_LinReg(X,Y)
chain = sample(Bayesian_LinReg_model,MH(),400000)

plot(chain)
Sampling: 100%|█████████████████████████████████████████| Time: 0:00:10
Out[10]:

It appears the MCMC has converged so we may continue.

For a specific trading signal, call it $x$, the distribution of possible returns has the form:

$$R_i(x) = \alpha_i + \beta_ix + \epsilon $$

where $\epsilon \sim \text{Normal}(0, \sigma_i)$ and $i$ indexes our posterior samples. We wish to find the solution to

$$ \arg \min_{r} \;\;E_{R(x)}\left[ \; L(R(x), r) \; \right] $$

according to the loss given above. This $r$ is our Bayes action for trading signal $x$. Below we plot the Bayes action over different trading signals. What do you notice?

In [11]:
import SciPy.optimize as sop

function stock_loss(price, pred, coef = 500)
    if price * pred < 0
        #opposite signs, not good
        return coef*pred^2 - sign(price)*pred + abs(price)
    else
        return abs(price-pred)
    end
end

std_samples = chain[:σ][:,1]
alpha_samples = chain[:α][:,1]
beta_samples  = chain[:β][:,1]

N = size(std_samples)[1]

noise = std_samples.*randn(N)

possible_outcomes(signal) = alpha_samples + beta_samples*signal + noise

opt_predictions = zeros(50)
trading_signals = LinRange(minimum(X),maximum(X),50)

for i in eachindex(trading_signals)
    _possible_outcomes = possible_outcomes(trading_signals[i])
    tomin(pred) = mean(stock_loss.(_possible_outcomes,pred))
    opt_predictions[i] = sop.fmin(tomin, 0, disp = false)[1]
end
In [12]:
plot(X,ls_coef.*X .+ ls_intercept ,legend =:topleft,label = "Least-squares prediction",size(1000,450),
      lw = 3,color = :red,title ="Empirical returns vs trading signal",
       xlabel = "trading signal",ylabel = "prediction",yguidefontsize=10,titlefontsize = 10,
       xlims = (-0.06,0.06),ylims=(-0.04,0.04))
plot!(trading_signals,opt_predictions,label ="Bayes action prediction")
Out[12]:

What is interesting about the above graph is that when the signal is near 0, and many of the possible returns outcomes are possibly both positive and negative, our best (with respect to our loss) prediction is to predict close to 0, hence take on no position. Only when we are very confident do we enter into a position. I call this style of model a sparse prediction, where we feel uncomfortable with our uncertainty so choose not to act. (Compare with the least-squares prediction which will rarely, if ever, predict zero).

A good sanity check that our model is still reasonable: as the signal becomes more and more extreme, and we feel more and more confident about the positive/negativeness of returns, our position converges with that of the least-squares line.

The sparse-prediction model is not trying to fit the data the best (according to a squared-error loss definition of fit). That honor would go to the least-squares model. The sparse-prediction model is trying to find the best prediction with respect to our stock_loss-defined loss. We can turn this reasoning around: the least-squares model is not trying to predict the best (according to a stock-loss definition of predict). That honor would go the sparse prediction model. The least-squares model is trying to find the best fit of the data with respect to the squared-error loss.

Example: Kaggle contest on Observing Dark World

A personal motivation for learning Bayesian methods was trying to piece together the winning solution to Kaggle's Observing Dark Worlds contest. From the contest's website:

There is more to the Universe than meets the eye. Out in the cosmos exists a form of matter that outnumbers the stuff we can see by almost 7 to 1, and we don’t know what it is. What we do know is that it does not emit or absorb light, so we call it Dark Matter. Such a vast amount of aggregated matter does not go unnoticed. In fact we observe that this stuff aggregates and forms massive structures called Dark Matter Halos. Although dark, it warps and bends spacetime such that any light from a background galaxy which passes close to the Dark Matter will have its path altered and changed. This bending causes the galaxy to appear as an ellipse in the sky.

The contest required predictions about where dark matter was likely to be. The winner, Tim Salimans, used Bayesian inference to find the best locations for the halos (interestingly, the second-place winner also used Bayesian inference). With Tim's permission, we provided his solution [1] here:

  1. Construct a prior distribution for the halo positions $p(x)$, i.e. formulate our expectations about the halo positions before looking at the data.
  2. Construct a probabilistic model for the data (observed ellipticities of the galaxies) given the positions of the dark matter halos: $p(e | x)$.
  3. Use Bayes’ rule to get the posterior distribution of the halo positions, i.e. use to the data to guess where the dark matter halos might be.
  4. Minimize the expected loss with respect to the posterior distribution over the predictions for the halo positions: $\hat{x} = \arg \min_{\text{prediction} } E_{p(x|e)}[ L( \text{prediction}, x) ]$ , i.e. tune our predictions to be as good as possible for the given error metric.

The loss function in this problem is very complicated. For the very determined, the loss function is contained in the file DarkWorldsMetric.py in the parent folder. Though I suggest not reading it all, suffice to say the loss function is about 160 lines of code — not something that can be written down in a single mathematical line. The loss function attempts to measure the accuracy of prediction, in a Euclidean distance sense, such that no shift-bias is present. More details can be found on the metric's main page.

We will attempt to implement Tim's winning solution using PyMC3 and our knowledge of loss functions.

The Data

The dataset is actually 300 separate files, each representing a sky. In each file, or sky, are between 300 and 720 galaxies. Each galaxy has an $x$ and $y$ position associated with it, ranging from 0 to 4200, and measures of ellipticity: $e_1$ and $e_2$. Information about what these measures mean can be found here, but for our purposes it does not matter besides for visualization purposes. Thus a typical sky might look like the following:

In [119]:
using PyCall,PyPlot
plt = pyimport("matplotlib.pyplot")
Ellipse = pyimport("matplotlib.patches")
np  = pyimport("numpy")

n_sky = 3
path = string("data/Train_Skies/Train_Skies/Training_Sky",n_sky,".csv")
data = np.genfromtxt(path,
                      skip_header = 1,
                      delimiter = ",",
                      usecols = [1,2,3,4])
println(string("Data on galaxies in sky ",n_sky))
println("position_x, position_y, e_1, e_2 ")
println(data[1:3,:])

# this is the function found inside the drawsky2.py file
py"""
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np

def draw_sky(galaxies):
    size_multiplier = 45
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(111, aspect='equal')
    n = galaxies.shape[0]
    for i in range(n):
        _g = galaxies[i,:]
        x,y = _g[0], _g[1]
        d = np.sqrt( _g[2]**2 + _g[3]**2 )
        a = 1.0/ ( 1 - d )
        b = 1.0/( 1 + d)
        theta = np.degrees( np.arctan2( _g[3], _g[2])*0.5 )
        ax.add_patch( Ellipse(xy=(x, y), width=size_multiplier*a, height=size_multiplier*b, angle=theta)) 
    ax.autoscale_view(tight=True)
    return fig

"""

fig = py"draw_sky"(data)
plt.title("Galaxy positions and ellipcities of sky " )
plt.xlabel("x-position")
plt.ylabel("y-position")
plt.show()
Data on galaxies in sky 3
position_x, position_y, e_1, e_2 
[162.69 1600.06 0.114664 -0.190326; 2272.28 540.04 0.623555 0.214979; 3553.64 2697.71 0.283527 -0.30187]

Priors

Each sky has one, two or three dark matter halos in it. Tim's solution details that his prior distribution of halo positions was uniform, i.e.

\begin{align} & x_i \sim \text{Uniform}( 0, 4200)\\\\ & y_i \sim \text{Uniform}( 0, 4200), \;\; i=1,2,3\\\\ \end{align}

Tim and other competitors noted that most skies had one large halo and other halos, if present, were much smaller. Larger halos, having more mass, will influence the surrounding galaxies more. He decided that the large halos would have a mass distributed as a log-uniform random variable between 40 and 180 i.e.

$$ m_{\text{large} } = \log \text{Uniform}( 40, 180 ) $$

and in PyMC3,

exp_mass_large = pm.Uniform("exp_mass_large", 40, 180)
mass_large = pm.Deterministic("mass_large", np.log(exp_max_large))

(This is what we mean when we say log-uniform.) For smaller galaxies, Tim set the mass to be the logarithm of 20. Why did Tim not create a prior for the smaller mass, nor treat it as a unknown? I believe this decision was made to speed up convergence of the algorithm. This is not too restrictive, as by construction the smaller halos have less influence on the galaxies.

Tim logically assumed that the ellipticity of each galaxy is dependent on the position of the halos, the distance between the galaxy and halo, and the mass of the halos. Thus the vector of ellipticity of each galaxy, $\mathbf{e}_i$, are children variables of the vector of halo positions $(\mathbf{x},\mathbf{y})$, distance (which we will formalize), and halo masses.

Tim conceived a relationship to connect positions and ellipticity by reading literature and forum posts. He supposed the following was a reasonable relationship:

$$ e_i | ( \mathbf{x}, \mathbf{y} ) \sim \text{Normal}( \sum_{j = \text{halo positions} }d_{i,j} m_j f( r_{i,j} ), \sigma^2 ) $$

where $d_{i,j}$ is the tangential direction (the direction in which halo $j$ bends the light of galaxy $i$), $m_j$ is the mass of halo $j$, $f(r_{i,j})$ is a decreasing function of the Euclidean distance between halo $j$ and galaxy $i$.

Tim's function $f$ was defined:

$$ f( r_{i,j} ) = \frac{1}{\min( r_{i,j}, 240 ) } $$

for large halos, and for small halos

$$ f( r_{i,j} ) = \frac{1}{\min( r_{i,j}, 70 ) } $$

This fully bridges our observations and unknown. This model is incredibly simple, and Tim mentions this simplicity was purposefully designed: it prevents the model from overfitting.

Training & Turing implementation

For each sky, we run our Bayesian model to find the posteriors for the halo positions — we ignore the (known) halo position. This is slightly different than perhaps traditional approaches to Kaggle competitions, where this model uses no data from other skies nor the known halo location. That does not mean other data are not necessary — in fact, the model was created by comparing different skies.

The Turing implementation differs a bit from the PyMC3 as explained by ElOceanografo : "In general, you can just write Julia code that uses the probabilistic variables as though they were ordinary variables.So mean in the model above could be written similarly, minus the pm.Deterministic and T.as_tensor boilerplate."

In [146]:
using Turing
using LinearAlgebra: norm
using Plots, StatsPlots
# Copied and pasted from the dark sky example linked above.  Columns are
# galaxy x and y positions, and the two components of their eccentricity.
# Transposed to get things column-major.
dataNew = data'
galaxy_positions = dataNew[1:2, :]
galaxy_eccs = dataNew[3:4, :]

f_distance(galaxy_pos, halo_pos, c) = 1 / min(norm(galaxy_pos .- halo_pos), c)

function tangential_distance(galaxy_pos, halo_pos)
    Δ = galaxy_pos .- halo_pos
    t = 2 * atan(Δ[2] / Δ[1])
    return [-cos(t), -sin(t)]
end

function predict_ecc(galaxy_pos, halo_positions, halo_masses, c)
    d = [tangential_distance(galaxy_pos, hp) for hp in eachcol(halo_positions)]
    f = [f_distance(galaxy_pos, hp, c) for hp in eachcol(halo_positions)]
    return(reduce(+, d .* halo_masses .* f))
end
    
@model function DarkSky(galaxy_positions, galaxy_eccs, nhalos=3, c=240)
    n = size(galaxy_positions, 2)
    halo_positions ~ filldist(Uniform(0, 4200), 2, nhalos)
    halo_masses ~ filldist(Uniform(40, 180), nhalos)
    σ2 = 0.05

    # for each galaxy, predict its observed eccentricity and compare it with the data,
    # assuming m.v. normal errors with diagonal covariance
    for i in 1:n
        μ_ecc = predict_ecc(galaxy_positions[:, i], halo_positions, halo_masses, 240)
        galaxy_eccs[:, i] ~ MvNormal(μ_ecc, sqrt(σ2))
    end
end

model = DarkSky(galaxy_positions, galaxy_eccs, 1, 240)
chain = sample(model, NUTS(), 10000)
┌ Info: Found initial step size
│   ϵ = 0.0125
└ @ Turing.Inference C:\Users\91863\.julia\packages\Turing\YGtAo\src\inference\hmc.jl:188
┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   1%|█                                        |  ETA: 0:03:38┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   2%|█                                        |  ETA: 0:02:34┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   4%|██                                       |  ETA: 0:01:50┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   4%|██                                       |  ETA: 0:01:47┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   5%|███                                      |  ETA: 0:01:41┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   7%|███                                      |  ETA: 0:01:27┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   8%|████                                     |  ETA: 0:01:20┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:   9%|████                                     |  ETA: 0:01:19┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  42%|██████████████████                       |  ETA: 0:00:52┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  46%|███████████████████                      |  ETA: 0:00:55┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  57%|████████████████████████                 |  ETA: 0:00:55┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  63%|██████████████████████████               |  ETA: 0:00:51┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  64%|███████████████████████████              |  ETA: 0:00:50┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  68%|████████████████████████████             |  ETA: 0:00:46┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  72%|██████████████████████████████           |  ETA: 0:00:40┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  76%|███████████████████████████████          |  ETA: 0:00:37┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  80%|█████████████████████████████████        |  ETA: 0:00:31┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  92%|██████████████████████████████████████   |  ETA: 0:00:13┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  92%|██████████████████████████████████████   |  ETA: 0:00:12┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling:  95%|███████████████████████████████████████  |  ETA: 0:00:08┌ Warning: The current proposal will be rejected due to numerical error(s).
│   isfinite.((θ, r, ℓπ, ℓκ)) = (true, false, false, false)
└ @ AdvancedHMC C:\Users\91863\.julia\packages\AdvancedHMC\bv9VV\src\hamiltonian.jl:47
Sampling: 100%|█████████████████████████████████████████| Time: 0:02:44
Out[146]:
Chains MCMC chain (10000×15×1 Array{Float64, 3}):

Iterations        = 1001:1:11000
Number of chains  = 1
Samples per chain = 10000
Wall duration     = 171.62 seconds
Compute duration  = 171.62 seconds
parameters        = halo_masses[1], halo_positions[2,1], halo_positions[1,1]
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, max_hamiltonian_energy_error, tree_depth, numerical_error, step_size, nom_step_size

Summary Statistics
           parameters        mean        std   naive_se      mcse         ess               Symbol     Float64    Float64    Float64   Float64     Float64  ⋯

  halo_positions[1,1]   2306.9207    57.4122     0.5741    4.8871     26.1914  ⋯
  halo_positions[2,1]   1296.6187   191.3701     1.9137   18.6841     21.3203  ⋯
       halo_masses[1]     40.2683     0.2727     0.0027    0.0045   1594.1061  ⋯
                                                               2 columns omitted

Quantiles
           parameters        2.5%       25.0%       50.0%       75.0%       97              Symbol     Float64     Float64     Float64     Float64     Floa ⋯

  halo_positions[1,1]   2228.9059   2242.9073   2315.8985   2353.9958   2405.3 ⋯
  halo_positions[2,1]   1071.6940   1146.2174   1199.0534   1548.9033   1569.6 ⋯
       halo_masses[1]     40.0064     40.0714     40.1803     40.3803     40.9 ⋯
                                                                1 column omitted

Below we plot a "heatmap" of the posterior distribution. (Which is just a scatter plot of the posterior, but we can visualize it as a heatmap.)

In [155]:
chain
Out[155]:
Chains MCMC chain (10000×15×1 Array{Float64, 3}):

Iterations        = 1001:1:11000
Number of chains  = 1
Samples per chain = 10000
Wall duration     = 171.62 seconds
Compute duration  = 171.62 seconds
parameters        = halo_masses[1], halo_positions[2,1], halo_positions[1,1]
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, max_hamiltonian_energy_error, tree_depth, numerical_error, step_size, nom_step_size

Summary Statistics
           parameters        mean        std   naive_se      mcse         ess               Symbol     Float64    Float64    Float64   Float64     Float64  ⋯

  halo_positions[1,1]   2306.9207    57.4122     0.5741    4.8871     26.1914  ⋯
  halo_positions[2,1]   1296.6187   191.3701     1.9137   18.6841     21.3203  ⋯
       halo_masses[1]     40.2683     0.2727     0.0027    0.0045   1594.1061  ⋯
                                                               2 columns omitted

Quantiles
           parameters        2.5%       25.0%       50.0%       75.0%       97              Symbol     Float64     Float64     Float64     Float64     Floa ⋯

  halo_positions[1,1]   2228.9059   2242.9073   2315.8985   2353.9958   2405.3 ⋯
  halo_positions[2,1]   1071.6940   1146.2174   1199.0534   1548.9033   1569.6 ⋯
       halo_masses[1]     40.0064     40.0714     40.1803     40.3803     40.9 ⋯
                                                                1 column omitted
In [164]:
t = chain[4000:10000,1:2,:]

fig = py"draw_sky"(data)
plt.title("Galaxy positions and ellipcities of sky 3" )
plt.xlabel("x-position")
plt.ylabel("y-position")
plt.scatter(t[:,1,1], t[:,2,1], alpha = 0.015, c = "r")
plt.show()

The most probable position reveals itself like a lethal wound.

Associated with each sky is another data point, located in ./data/Training_halos.csv that holds the locations of up to three dark matter halos contained in the sky. For example, the night sky we trained on has halo locations:

In [169]:
halo_data = np.genfromtxt("data/Training_halos.csv", 
                          delimiter = ",",
                          usecols = [1, 2,3, 4,5,6,7,8,9],
                          skip_header = 1)
print(halo_data[n_sky,:])
[1.0, 2315.78, 1081.95, 2315.78, 1081.95, 0.0, 0.0, 0.0, 0.0]

The third and fourth column represent the true $x$ and $y$ position of the halo. It appears that the Bayesian method has located the halo within a tight vicinity.

In [175]:
fig = py"draw_sky"(data)
plt.title("Galaxy positions and ellipcities of sky.")
plt.xlabel("x-position")
plt.ylabel("y-position")
plt.scatter(t[:,1,1], t[:,2,1], alpha = 0.015, c = "r")
plt.scatter(halo_data[n_sky,:][4], halo_data[n_sky,:][3], 
            label = "True halo position",
            c = "k", s = 70)
plt.legend(scatterpoints = 1, loc = "lower left")
plt.xlim(0, 4200)
plt.ylim(0, 4200);

print("True halo location:", halo_data[n_sky,:][4]," ", halo_data[n_sky,:][3])
True halo location:2315.78 1081.95

Perfect. Our next step is to use the loss function to optimize our location. A naive strategy would be to simply choose the mean:

In [184]:
mean_posterior = mean(group(t, :halo_positions))[:,2]
println(mean_posterior)
[2341.573343838651, 1161.3998994294845]

References

  1. Antifragile: Things That Gain from Disorder. New York: Random House. 2012. ISBN 978-1-4000-6782-4.
  2. Tim Saliman's solution to the Dark World's Contest
  3. Silver, Nate. The Signal and the Noise: Why So Many Predictions Fail — but Some Don't. 1. Penguin Press HC, The, 2012. Print.
In [ ]: