Skip to main content

Chapter 5 · Quantitative Finance

Stock Analysis with Python: Modeling Prices, Returns & Distributions

Pull real stock prices with yfinance, turn them into daily returns, and see why the normal distribution underestimates crashes in the tails.

By Updated 9 min Intermediate

Banner illustration displaying financial stock charts and data analysis trends
On this page

Everything in Phase 1 was a contract. A loan, a bond, a tranche, and a yield curve all rest on someone promising to pay someone else a known amount on a known date, and our whole job was to price that promise. A stock makes no such promise. It does not mature, it does not pay you back, and its price on any given day is whatever buyers and sellers happen to agree on. That makes it a completely different kind of object to model, and it is where the second phase of this series begins.

We start with stocks because almost everything else in Phase 2 is built on top of them. Options are written on stocks, portfolios are made of them, and the risk measures we reach for in Chapter 8 all begin with a stock’s returns. Before any of that, we need to understand how a single stock actually behaves. A stock represents fractional ownership in a company with limited liability, and its value comes from a mix of what the company is fundamentally worth and the day-to-day push and pull of people buying and selling. We are going to ignore the balance-sheet half almost entirely and focus on that second part, because it is the part that shows up in the price series and the part every model in this phase has to reckon with.

Pulling Real Stock Data with yfinance

The best way to get a feel for how a stock behaves is to take a real one apart, so let’s use Apple. For the analysis we will pull daily data from the last five years, which gives us the Open, Close, Low, Volume, and Adjusted Close for every trading day. The one column that matters most here is the Adjusted Close, which is just the closing price corrected for the effect of dividends and stock splits, so that changes in it reflect what an investor actually earned. We will work with that throughout. To start, let’s just plot it. If you want the details of the pandas and plotting workflow, our Data Science with Python series covers it end to end.

import yfinance as yf
import matplotlib.pyplot as plt
import pandas as pd

# 'Adj Close' is preferred as it accounts for dividends and stock splits
aapl_data = yf.download('AAPL', period='5y')
df = aapl_data[['Adj Close']].copy()

# Plot the Price History
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['Adj Close'], label='AAPL Adjusted Close', color='blue')
plt.title('Apple (AAPL) Stock Price - Last 5 Years')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.grid(True, linestyle='--', alpha=0.5)
plt.legend()
plt.show()

Why Model Returns Instead of Prices?

Looking at that plot, the price wanders around in a way that feels close to random, which raises the obvious question of how we could ever model it. The trick is to stop looking at the price and start looking at the return. What an investor actually cares about is not the raw price but the percentage change in it, because a €1 move on a €10 stock and a €10 move on a €1000 stock are wildly different things even though both are worth a euro. We compute the daily return with:

Rt=Pt−Pt−1Pt−1R_{t} = \frac{P_{t} - P_{t-1}}{P_{t-1}}

Where:

  • RtR_{t} is the return at time tt
  • PtP_{t} is the price at time tt
  • Pt−1P_{t-1} is the price on the previous day

Apply that to the Apple data and the picture changes completely. Instead of a line drifting upward over the years, we get a series that rattles around zero, and that difference is exactly what makes the returns easier to work with.

import yfinance as yf
import matplotlib.pyplot as plt
import pandas as pd

# pct_change() calculates (Price_t - Price_t-1) / Price_t-1
aapl_data = yf.download('AAPL', period='5y')
df['Daily Return'] = df['Adj Close'].pct_change()

# Drop the first row (NaN) created by the calculation
df.dropna(inplace=True)

# 4. Plot Daily Returns (Volatility)
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['Daily Return'], label='Daily Returns', color='orange', linewidth=0.7)
plt.axhline(0, color='black', linewidth=0.5) # Add a line at 0 for reference
plt.title('Apple (AAPL) Daily Stock Returns')
plt.xlabel('Date')
plt.ylabel('Return') # e.g., 0.02 means 2%
plt.grid(True, linestyle='--', alpha=0.5)
plt.legend()
plt.show()

Volatility Clustering and Stationary Returns

Working in returns matters because it makes the data stationary. In plain terms, the price of Apple can go anywhere over five years, but the daily return tends to stay inside a fairly predictable band, and a predictable band is something we can actually model. Looking at the returns, two features stand out. The first is clustering: the big moves tend to bunch together, so a volatile day is usually followed by another volatile day rather than a calm one. The second is the range, because for a large, stable company like Apple the overwhelming majority of daily moves sit between roughly -2% and +2%.

Do Stock Returns Follow a Normal Distribution?

To turn that into something we can reason about probabilistically, we collect all the daily returns into a histogram. The histogram is the stock’s fingerprint, and the natural next move is to ask whether it matches a shape we already know how to work with: the normal distribution.

import numpy as np
import scipy.stats as stats
import yfinance as yf
import matplotlib.pyplot as plt
import pandas as pd

aapl_data = yf.download('AAPL', period='5y')
df['Daily Return'] = df['Adj Close'].pct_change()

plt.figure(figsize=(10, 6))

# Plot the actual data histogram
# bins=50 breaks the data into 50 "buckets"
plt.hist(df['Daily Return'], bins=50, density=True, alpha=0.6, color='green', label='Actual Returns')

# Plot the Normal Distribution curve for comparison
mu, std = df['Daily Return'].mean(), df['Daily Return'].std()
x = np.linspace(df['Daily Return'].min(), df['Daily Return'].max(), 100)
p = stats.norm.pdf(x, mu, std)
plt.plot(x, p, 'k', linewidth=2, label='Normal Distribution')

plt.title('Distribution of Apple Daily Returns')
plt.xlabel('Daily Return')
plt.ylabel('Density')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# Optional: Print statistics
print(f"Mean Return: {mu:.4f}")
print(f"Standard Deviation (Volatility): {std:.4f}")

At a glance the fit looks reasonable. The histogram is a bump centred a hair to the right of zero, the normal curve lays over the top of it, and if you stopped here you would happily conclude that Apple’s returns are more or less normal. That conclusion is comfortable, widely taught, and wrong in exactly the place that costs people money. The disagreement between the real returns and the normal curve does not live in the middle, where the fit genuinely is good. It lives out in the tails, and on an ordinary linear chart the tails are so small you cannot see them.

Seeing the Fat Tails on a Log Scale

So rather than take my word for it, pull the fit apart yourself. The widget below is Apple’s return histogram with a fitted normal laid over it, exactly as the script draws it. Flip the y-axis to a log scale and watch what happens at the edges: the white normal line dives toward zero while the real returns stay stubbornly propped up, because days that the bell curve treats as near-impossible actually happen several times a year. Then bring in the Student-t and drag its degrees of freedom down, and you can watch a genuinely fat-tailed distribution settle onto those same tails that the normal could never reach.

ReturnDistribution · does the bell curve fit?1,258 daily returns
Mean / day
0.09%
Volatility (σ)
1.77%
Excess kurtosis
4.0
0 = normal; higher = fatter tails
Days ≤ -5%
11
normal expects ~2.5
▮ actual returns— normal fit- - student-t

On a linear axis the normal looks like a fair fit, because the disagreement lives entirely in the tails and the tails are tiny. Flip to a log axis and the normal line dives while the real returns stay up: extreme days are far more common than the bell curve allows. Drop the Student-t degrees of freedom and watch a fat-tailed curve settle onto those same tails.

A histogram of AAPL daily returns with a fitted normal curve. In the far tails, beyond about minus 4 percent, the bars stand well above the normal curve, showing that extreme down days happen far more often than a bell curve predicts.
Linear scale flatters the normal; a log scale exposes it. The real returns keep a fat tail the bell curve says should not exist.

What the Normal Fit Gets Wrong

So the curve is close to normal, but the differences are the whole story.

  • The mean. The centre sits slightly to the right of zero, which just reflects Apple’s long-run upward drift over these five years.
  • Fat tails. The real distribution carries far more weight in the extremes than a normal does. A 5% single-day drop on an earnings miss is not the once-in-a-lifetime event the bell curve implies. It is something that shows up again and again.

Knowing the shape of this distribution is what lets us start putting numbers on risk. Roughly 68% of the time tomorrow’s return lands within one standard deviation of the mean, and that kind of statement is the seed of everything ahead, from Monte Carlo simulation to the Value at Risk numbers in Chapter 8. The catch is that those statements lean on the normal fit, and the fat tails we just saw are exactly where that fit fails, so each one quietly understates how often a bad day really arrives.

From a Single Stock to a Portfolio

We took a real stock, turned its wandering price into a stationary return series, and found that its distribution is close enough to normal to be dangerous and fat-tailed enough to matter. That single fact, that extreme days are more common than the tidy model admits, is the thread that runs through the rest of Phase 2 and most of the risk work later in the series. Hold onto it, because every time we assume normality from here on, we are making a convenient simplification and taking on a known, quantifiable amount of self-deception in exchange.

Next up is Chapter 6: Portfolio Allocation and the Efficient Frontier. We stop looking at one stock and start combining several, and we bring back the diversification idea from Chapter 1 in its full mathematical form. The question becomes how to mix a set of risky assets so that, for a given amount of risk, you squeeze out the most return, which is exactly the problem Markowitz solved and the reason the efficient frontier exists.

⚠️ Financial Education Disclaimer

This post uses Apple Inc. (AAPL) historical data for educational and illustrative purposes only.

  • Not an Endorsement: The mention of specific tickers or companies does not constitute a recommendation to buy, sell, or hold any security.
  • Model Limitations: Stock market returns often exhibit “Fat Tails” (Kurtosis) and non-stationary behavior. The Normal Distribution model shown here is a simplifying tool and frequently underestimates the probability of extreme market events.
  • Data Accuracy: While yfinance is a popular tool, data can contain errors or be delayed. Never base financial decisions solely on automated scripts.
  • Risk Warning: Trading stocks involves a high risk of losing capital. Only invest money you can afford to lose.
Subscribe / Stdin

Get the next part

New parts of Finding P values and false hope land in your inbox the day they publish. No spam, unsubscribe in one click.

We send your address to Kit, our email provider, and ask you to confirm by email before anything is sent. See our Privacy Statement.