Company Valuation Using Probabilistic Models With Python | by Julian Marx | Towards Data Science

Note from Towards Data Science’s editors: While we allow independent authors to publish articles in accordance with our rules and guidelines, we do not endorse each author’s contribution. You should not rely on an author’s works without seeking professional advice. See our Reader Terms for details.
In a perfectly predictable world, we would have exact information about the future growth rates and cash flows of a business, which would lead to a single accurate intrinsic value of the company. However, in reality, things are more uncertain. In fact, while some analysts give out exact stock price targets, various inputs into financial models are actually randomly distributed variables. The resulting complexities aggravate the challenge of finding an adequate company value. While there is no longer a “single version of the truth” in case of random inputs, if our inputs obey certain rules, it is possible to find a range of values that is likely to contain the true mean value of our company. In writing the following model, I drew on insights provided by NYU-professor Aswath Damodaran in his paper on using probabilistic approaches to conduct company valuation [1].
Let’s assume that we only have four random variables as an input to our stock valuation model: revenue CAGR, EBIT margins, the Weighted Average Cost of Capital (WACC) and the long-term growth rate of the company (let’s say Microsoft). We can further assume that these values have a certain mean value and are Normally distributed. This entails that the distribution of these variables follows a set of rules pertaining, for instance, to their spread.
Given these random variables, there is some “true” mean company value, which we try to determine. This true mean value is the mean of infinitely many intrinsic value forecasts based on our random input variables. As it is impossible to find this exact population mean, we need to limit ourselves to creating a single sample of values and drawing inferences about the population mean from this sample.
In order to understand how a sample behaves relative to the overall population of company values, it makes sense to first envision a hypothetical scenario in which we know the true mean company value and its standard deviation. If we now draw repeated samples from our population of company values, the means of these samples will follow a sampling distribution which can be approximated using the Normal distribution. The mean of this distribution is the true mean company value and its standard deviation is the standard deviation of the company values divided by square root of the number of observations in the sample. Using this standard deviation, we can make predictions regarding the probability of the occurence of certain events. For instance, we know that 68% of sample means will fall within one standard deviation of the population mean, around 95% will fall within two, and so on.
So how does this help us? If we know the population mean and the standard deviation of its sampling distribution, we can theoretically generate a sample and draw inferences about the true population mean. For instance, if we take the mean of our sample, and create a range that extends two standard deviations from that mean to either side, we can be 95% confident that the true mean value will lie in this interval. The only problem is that we do not know the population standard deviation.
It turns out that we can take the sample standard deviation as an approximation for the true population value and account for this by extending the width of our confidence interval (by using t-values). If a sufficiently large sample is drawn from the population, this increases the accuracy of our population standard deviation estimate and we can therefore use z-scores instead.
Essentially, what all this means is that we can forecast our company value a certain number of times, which will lead to a sample from the population of company values. Then, we can create a 95% confidence interval for the true mean value to get an indication of where the true mean value (based on the four random input variables) will likely lie.
While the underlying theory is somewhat complicated, the implementation of the model with Python is actually rather straightforward, as is explained in the subsequent sections.
Using the yahoo_fin library, it is possible to get financial data on publicly traded companies. We can use this information to derive mean values for the Normally distributed variables used in the following simulations. In each iteration of the simulation, we forecast one specific company value, based on the values of the random variables. The code I used to get this value is quite similar to the one I wrote some time ago (except that yahoo_fin freed me from having to scrape all the financial data manually). As the focus here is on drawing inferences from sample data generated by a Monte Carlo simulation, I will not go into detail regarding the code and the simplifying assumptions underlying it. All that is important is that the following block of code will (hopefully) return a somewhat accurate estimate of the intrinsic value of a company, given specific inputs for company revenue CAGR, EBIT margins, WACC, and long-term growth rates.
from yahoo_fin import stock_info as sifrom matplotlib import pyplot as plt import pandas_datareader as drimport numpy as npimport pandas as pd'''----// General input variables //----'''company_ticker = 'MSFT'market_risk_premium = 0.059debt_return = 0.01long_term_growth = 0.01tax_rate = 0.3iterations = 1000'''----// Get financial information from yahoo finance //----'''income_statement_df = si.get_income_statement(company_ticker)pars_df = income_statement_df.loc[['totalRevenue', 'ebit']]input_df = pars_df.iloc[:, ::-1]'''----// Calculate average revenue CAGR & EBIT margin //----'''def get_cagr(past_revs):    CAGR = (past_revs.iloc[0,3]/past_revs.iloc[0,0])**(1/4)-1    return(CAGR)def get_average_margin(past_ebit):    margin = 0    margin_lst = []    for i in range(len(past_ebit.columns)):        margin = past_ebit.iloc[1,i]/past_ebit.iloc[0,i]    margin_lst.append(margin)    return(sum(margin_lst)/len(margin_lst))mean_cagr = get_cagr(input_df)mean_margin = get_average_margin(input_df)'''----// Create forecast function through which random variables will flow //----'''def get_forecast(input_df, cagr, margin, long_term_growth):    forecast_lst = []    for i in range(6):        if i < 5:            forecast_lst.append(input_df.iloc[0,3]*(1+cagr)**(i+1)*margin)        else:            forecast_lst.append(input_df.iloc[0,3]*(1+cagr)**(i)*(1+long_term_growth)*margin)    return forecast_lst'''----// Get WACC and net debt //----'''def get_wacc(company_ticker, market_risk_premium, debt_return, tax_rate):    risk_free_rate_df = dr.DataReader('^TNX', 'yahoo')     risk_free_rate = (risk_free_rate_df.iloc[len(risk_free_rate_df)-1,5])/100    equity_beta = si.get_quote_table('msft')['Beta (5Y Monthly)']    equity_return = risk_free_rate+equity_beta*(market_risk_premium)    balance_sheet_df = si.get_balance_sheet(company_ticker)    short_term_debt_series = balance_sheet_df.loc['shortLongTermDebt']    long_term_debt_series = balance_sheet_df.loc['longTermDebt']    cash_series = balance_sheet_df.loc['cash']    net_debt = short_term_debt_series.iloc[0] +  + long_term_debt_series.iloc[0] - cash_series.iloc[0]    market_cap_str = si.get_quote_table(company_ticker)['Market Cap']    market_cap_lst = market_cap_str.split('.')    if market_cap_str[len(market_cap_str)-1] == 'T':        market_cap_length = len(market_cap_lst[1])-1        market_cap_lst[1] = market_cap_lst[1].replace('T',(12-market_cap_length)*'0')        market_cap = int(''.join(market_cap_lst))    if market_cap_str[len(market_cap_str)-1] == 'B':        market_cap_length = len(market_cap_lst[1])-1        market_cap_lst[1] = market_cap_lst[1].replace('B',(9-market_cap_length)*'0')        market_cap = int(''.join(market_cap_lst))    company_value = market_cap + net_debt    WACC = market_cap/company_value * equity_return + net_debt/company_value * debt_return * (1-tax_rate)    return WACCdef get_net_debt():        balance_sheet_df = si.get_balance_sheet(company_ticker)    short_term_debt_series = balance_sheet_df.loc['shortLongTermDebt']    long_term_debt_series = balance_sheet_df.loc['longTermDebt']    cash_series = balance_sheet_df.loc['cash']    return short_term_debt_series.iloc[0] + long_term_debt_series.iloc[0] - cash_series.iloc[0]    mean_wacc = get_wacc(company_ticker, market_risk_premium, debt_return, tax_rate)net_debt = get_net_debt()'''----// Discount EBIT figures to arrive at the PV of the firm's cash flows //----'''def discount(forecast, discount_rate, long_term_rate):    discount_lst = []    for x,i in enumerate(forecast):        if x < 5:            discount_lst.append(i/(1+discount_rate)**(x+1))        else:            discount_lst.append(i/(discount_rate-long_term_rate)*(1/(1+discount_rate)**5))    return sum(discount_lst)forecast = get_forecast(input_df, cagr, margin, long_term_rate)present_value = discount(forecast, discount_rate, long_term_rate)-net_debt
The idea is to forecast company values repeatedly using loops and storing the resulting model outputs in a list that is later used to determine the sample mean and standard deviation.
I arrived at the mean CAGR, EBIT margin, and WACC figures by assuming that past values of these variables were accurate predictors of future values. The long-term growth rate of the company is more difficult to determine and should be entered on a case by case basis. The same is true for the standard deviation of each of the four variables. Given a mean value and standard deviation for each of the four variables, it is easy to get draws from a random distribution with numpy. In fact, we only need one line of code after importing the library.
cagr = np.random.normal(mean_cagr, 0.01)
Doing the same for the EBIT margin, WACC, and long-term growth rates, we can use the resulting figures in the calculation of the company value. Doing this once entails drawing a single value from the population of company values. Using a loop, we can repeat the process several times (1,000 in this case) and store the resulting company values in a list.
'''----// Run simulation //----'''hist_lst = []for i in range(iterations):    cagr = np.random.normal(mean_cagr, 0.01)    margin = np.random.normal(mean_margin, 0.005)    long_term_rate = np.random.normal(long_term_growth, 0.001)    discount_rate = np.random.normal(mean_wacc, 0.001)    forecast = get_forecast(input_df, cagr, margin, long_term_rate)    hist_lst.append(discount(forecast, discount_rate, long_term_rate)-net_debt)hist_array = np.array(hist_lst)
Using numpy, we can easily find the mean and standard deviation of this list. Subsequently, the upper and lower bound of our 95% confidence interval can be calculated.
mean = hist_array.mean()standard_error = hist_array.std()/(iterations**(1/2))lower_bound = mean-1.96*standard_errorupper_bound = mean+1.96*standard_error
Using matplotlib, we can also display the sample data graphically. This can help understand the distribution of the sample data better and also allows us to verify the assumption of Normality underlying the inferences drawn.
plt.hist(hist_array, bins=50, align='mid', color = 'steelblue', edgecolor='black')plt.title('Sample Distribution ' + company_ticker, {'fontname':'Calibri'})plt.xlabel('Equity Value in $', {'fontname':'Calibri'})plt.ylabel('Frequency', {'fontname':'Calibri'})plt.show()
To get an impression of how the model was performing, I computed confidence intervals for the true mean value of different companies and subsequently compared the intervals with actual company market caps. The results look as follows.
When comparing the confidence intervals to the actual market caps of the companies, it appears as if the model is somewhat off. While the program is certainly not a perfect representation of the real world, we must still take into account that the confidence intervals provide a range for the true mean value of the company and not for a single point estimate.
Like the previous model I built to calculate the fair value of a company, this program also makes several simplified assumptions. For instance, I implicitly assume that the past values of the four random input variables are adequate approximations of their population mean value going forward. However, this assumption is potentially unwarranted and even allowing for some randomness in these input variables will not solve that issue. Despite this, I believe that incorporating randomness into the model takes it one step closer to becoming an adequate representation of real-world dynamics.
The model and code is simply an exercise in applying Python programming to company valuation. Therefore, the code should obviously not be used to make investment decisions. Further, information pulled from Yahoo finance should not be used for any commercial purposes.
The following is the overall code needed to run the simulations and draw inferences from the resulting sample data. All that is required for it to work is Python (I used 3.7.8) and several packages, namely yahoo_fin, matplotlib, pandas-datareader, numpy, and pandas.
from yahoo_fin import stock_info as sifrom matplotlib import pyplot as plt import pandas_datareader as drimport numpy as npimport pandas as pd'''----// General input variables //----'''company_ticker = 'MSFT'market_risk_premium = 0.059debt_return = 0.01long_term_growth = 0.01tax_rate = 0.3iterations = 1000'''----// Get financial information from yahoo finance //----'''income_statement_df = si.get_income_statement(company_ticker)pars_df = income_statement_df.loc[['totalRevenue', 'ebit']]input_df = pars_df.iloc[:, ::-1]'''----// Calculate average revenue CAGR & EBIT margin //----'''def get_cagr(past_revs):    CAGR = (past_revs.iloc[0,3]/past_revs.iloc[0,0])**(1/4)-1    return(CAGR)def get_average_margin(past_ebit):    margin = 0    margin_lst = []    for i in range(len(past_ebit.columns)):        margin = past_ebit.iloc[1,i]/past_ebit.iloc[0,i]    margin_lst.append(margin)    return(sum(margin_lst)/len(margin_lst))mean_cagr = get_cagr(input_df)mean_margin = get_average_margin(input_df)'''----// Create forecast function through which random variables will flow //----'''def get_forecast(input_df, cagr, margin, long_term_growth):    forecast_lst = []    for i in range(6):        if i < 5:            forecast_lst.append(input_df.iloc[0,3]*(1+cagr)**(i+1)*margin)        else:            forecast_lst.append(input_df.iloc[0,3]*(1+cagr)**(i)*(1+long_term_growth)*margin)    return forecast_lst'''----// Get WACC and net debt //----'''def get_wacc(company_ticker, market_risk_premium, debt_return, tax_rate):    risk_free_rate_df = dr.DataReader('^TNX', 'yahoo')     risk_free_rate = (risk_free_rate_df.iloc[len(risk_free_rate_df)-1,5])/100    equity_beta = si.get_quote_table('msft')['Beta (5Y Monthly)']    equity_return = risk_free_rate+equity_beta*(market_risk_premium)    balance_sheet_df = si.get_balance_sheet(company_ticker)    short_term_debt_series = balance_sheet_df.loc['shortLongTermDebt']    long_term_debt_series = balance_sheet_df.loc['longTermDebt']    cash_series = balance_sheet_df.loc['cash']    net_debt = short_term_debt_series.iloc[0] +  + long_term_debt_series.iloc[0] - cash_series.iloc[0]    market_cap_str = si.get_quote_table(company_ticker)['Market Cap']    market_cap_lst = market_cap_str.split('.')    if market_cap_str[len(market_cap_str)-1] == 'T':        market_cap_length = len(market_cap_lst[1])-1        market_cap_lst[1] = market_cap_lst[1].replace('T',(12-market_cap_length)*'0')        market_cap = int(''.join(market_cap_lst))    if market_cap_str[len(market_cap_str)-1] == 'B':        market_cap_length = len(market_cap_lst[1])-1        market_cap_lst[1] = market_cap_lst[1].replace('B',(9-market_cap_length)*'0')        market_cap = int(''.join(market_cap_lst))    company_value = market_cap + net_debt    WACC = market_cap/company_value * equity_return + net_debt/company_value * debt_return * (1-tax_rate)    return WACCdef get_net_debt():        balance_sheet_df = si.get_balance_sheet(company_ticker)    short_term_debt_series = balance_sheet_df.loc['shortLongTermDebt']    long_term_debt_series = balance_sheet_df.loc['longTermDebt']    cash_series = balance_sheet_df.loc['cash']    return short_term_debt_series.iloc[0] + long_term_debt_series.iloc[0] - cash_series.iloc[0]    mean_wacc = get_wacc(company_ticker, market_risk_premium, debt_return, tax_rate)net_debt = get_net_debt()'''----// Discount EBIT figures to arrive at the PV of the firm's cash flows //----'''def discount(forecast, discount_rate, long_term_rate):    discount_lst = []    for x,i in enumerate(forecast):        if x < 5:            discount_lst.append(i/(1+discount_rate)**(x+1))        else:            discount_lst.append(i/(discount_rate-long_term_rate)*(1/(1+discount_rate)**5))    return sum(discount_lst)'''----// Run simulation and plot distribution of model forecasts //----'''hist_lst = []for i in range(iterations):    cagr = np.random.normal(mean_cagr, 0.01)    margin = np.random.normal(mean_margin, 0.005)    long_term_rate = np.random.normal(long_term_growth, 0.001)    discount_rate = np.random.normal(mean_wacc, 0.001)    forecast = get_forecast(input_df, cagr, margin, long_term_rate)    hist_lst.append(discount(forecast, discount_rate, long_term_rate)-net_debt)hist_array = np.array(hist_lst)plt.hist(hist_array, bins=50, align='mid', color = 'steelblue', edgecolor='black')plt.title('Sample Distribution ' + company_ticker, {'fontname':'Calibri'})plt.xlabel('Equity Value in $', {'fontname':'Calibri'})plt.ylabel('Frequency', {'fontname':'Calibri'})plt.show()mean = hist_array.mean()standard_error = hist_array.std()/(iterations**(1/2))lower_bound = mean-1.96*standard_errorupper_bound = mean+1.96*standard_error print(lower_bound)print(upper_bound)
References:
[1] A. Damodaran, Facing Up To Uncertainty: Using Probabilistic Approaches In Valuation (2018), SSRN