Part of Quantitative Finance
Covers financial data handling with pandas, NumPy, and Numba. Topics include time series operations, return calculations, and visualization for quant finance.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Data Handling and Visualization
A weekday index is not an exchange calendar, filling a price gap changes the returns around that gap, and simple and log returns separate as moves grow. These choices affect an analysis before any pricing or forecasting model is fitted.
This chapter builds a reproducible pandas and NumPy workflow around those choices: load and reshape synthetic price data, resample and clean time series, compute returns and rolling statistics, profile a numerical bottleneck, and inspect the results with plots.
Loading Financial Data
Financial data comes in many formats: CSV files from data vendors, Excel spreadsheets from analysts, SQL databases at institutions, and API responses from market data providers. Pandas supplies readers for many tabular formats and can organize results returned by database drivers, vendor SDKs, or custom API clients. Authentication and source-specific connectors still sit outside pandas. Once the records are available, pandas provides a consistent table interface for analysis.
Reading CSV Files
CSV is a common introductory and interchange format for historical price data. It is human-readable and broadly supported by tabular-data tools, although dialects, encodings, schemas, and alternative formats vary. Let's load a sample dataset to illustrate basic loading patterns:
import numpy as np
import pandas as pd
# Create sample price data (simulating what you'd load from a CSV)
np.random.seed(42)
dates = pd.date_range(
"2020-01-01", periods=252, freq="B"
) # Weekdays, not an exchange calendar
prices = 100 * np.cumprod(1 + np.random.randn(252) * 0.02)
open_prices = prices * (1 + np.random.randn(252) * 0.005)
high_spread = np.abs(np.random.randn(252)) * 0.01
low_spread = np.abs(np.random.randn(252)) * 0.01
df = pd.DataFrame(
{
"date": dates,
"open": open_prices,
"high": np.maximum(open_prices, prices) * (1 + high_spread),
"low": np.minimum(open_prices, prices) * (1 - low_spread),
"close": prices,
"volume": np.random.randint(1_000_000, 10_000_000, 252),
}
)Use pd.read_csv() to load data. Three parameters matter for financial data:
- parse_dates: Attempts to parse specified columns as datetimes. Under pandas 3's default string inference, unparsable values or incompatible mixed time zones can leave the column with
strdtype instead of a datetime dtype, so validate the result. - index_col: Sets a column as the DataFrame index (typically the date)
- dtype: Specifies column data types to prevent incorrect type inference, for example forcing numeric columns to float64.
# Save our sample data to demonstrate loading
df.to_csv("sample_prices.csv", index=False)
# Load it back, parsing dates and setting the index
loaded_df = pd.read_csv(
"sample_prices.csv", parse_dates=["date"], index_col="date"
)open high low close volume date 2020-01-01 102.065047 102.729126 100.495210 100.993428 1100734 2020-01-02 101.234072 101.727209 100.527996 100.714153 9546310 2020-01-03 101.243759 102.623134 100.374726 102.018781 3851274 2020-01-06 104.871805 106.034615 104.137377 105.126334 2225811 2020-01-07 105.296830 105.347922 104.031707 104.634020 4081091 Index type: DatetimeIndex Data shape: (252, 5)
This displays the first five rows of synthetic OHLC (open, high, low, close) data and volume. The generator explicitly keeps each high at or above both open and close, and each low at or below both. The index is a DatetimeIndex, as the type output confirms. The shape shows 252 synthetic weekday observations and 5 data columns. Pandas frequency "B" excludes weekends but is not an exchange calendar and does not remove exchange-specific holidays. The DatetimeIndex enables date-range slicing and frequency-based resampling that plain text dates do not provide directly.
Handling Multiple Securities
Real-world analysis typically involves multiple securities. A portfolio manager might track dozens or hundreds of positions, while a factor model might require returns for thousands of stocks. Organize this data in two ways: wide format with one column per security, or long format with a single column for security identifier. Each format has advantages for different analyses.
# Create multi-security data in wide format
np.random.seed(42)
dates = pd.date_range("2020-01-01", periods=252, freq="B")
n = len(dates)
# Simulate correlated, heavy-tailed log returns with a stress period.
# The ticker names are illustrative labels, not observed market data.
stress_mask = (dates >= "2020-02-20") & (dates <= "2020-04-15")
# Common market factor: higher volatility and negative drift during stress
market_vol = np.where(stress_mask, 0.03, 0.012)
market_drift = np.where(stress_mask, -0.0015, 0.0003)
market_factor = market_drift + market_vol * np.random.standard_t(df=5, size=n)
# Asset-specific exposures and idiosyncratic risk
betas = {"AAPL": 1.2, "GOOGL": 1.0, "MSFT": 1.1}
idio_vol = {
"AAPL": np.where(stress_mask, 0.020, 0.012),
"GOOGL": np.where(stress_mask, 0.018, 0.011),
"MSFT": np.where(stress_mask, 0.021, 0.013),
}
asset_drift = {"AAPL": 0.0004, "GOOGL": 0.0002, "MSFT": 0.00035}
log_returns_sim = pd.DataFrame(
{
ticker: (
asset_drift[ticker]
+ betas[ticker] * market_factor
+ idio_vol[ticker] * np.random.standard_t(df=7, size=n)
)
for ticker in ["AAPL", "GOOGL", "MSFT"]
},
index=dates,
)
start_prices = pd.Series({"AAPL": 100.0, "GOOGL": 150.0, "MSFT": 80.0})
multi_prices = start_prices * np.exp(log_returns_sim.cumsum())Wide format (one column per security):
AAPL GOOGL MSFT
2020-01-01 101.264291 149.781536 80.022172
2020-01-02 98.706922 147.383256 77.148026
2020-01-03 100.009852 150.899989 78.238160
2020-01-06 101.610677 152.126586 79.573452
2020-01-07 103.564120 152.944752 80.247694Three columns appear (AAPL, GOOGL, MSFT), each with price data for one security. Each row is a date, enabling easy cross-security price comparison. This format aligns values for cross-security calculations in the same row. Computing a correlation matrix or portfolio returns becomes straightforward.
Long format works better for filtering and grouping operations: especially when applying the same analysis across multiple securities or including additional attributes like sector or fundamentals.
# Convert to long format
long_format = (
multi_prices.reset_index()
.melt(id_vars="index", var_name="ticker", value_name="price")
.rename(columns={"index": "date"})
)Long format (stacked):
date ticker price
0 2020-01-01 AAPL 101.264291
1 2020-01-02 AAPL 98.706922
2 2020-01-03 AAPL 100.009852
3 2020-01-06 AAPL 101.610677
4 2020-01-07 AAPL 103.564120
5 2020-01-08 AAPL 102.658508
6 2020-01-09 AAPL 104.309018
7 2020-01-10 AAPL 102.718245
8 2020-01-13 AAPL 102.366038
9 2020-01-14 AAPL 101.691553Each price observation is a separate row with columns for date, ticker, and price. By default, melt() emits the complete AAPL value block first, so the first rows are consecutive dates for AAPL rather than multiple tickers on one date. Sort by date and ticker if that row order is needed. Long format enables filtering and grouping, though cross-security calculations generally require pivoting to wide format.
Time Series Fundamentals
Financial analysis revolves around time series: sequences of observations indexed by time. Time series have an inherent ordering, whereas cross-sectional observations are indexed by entities or units; neither structure guarantees independence. Prices can depend on prior values, and return distributions can exhibit serial dependence or volatility clustering. Pandas provides specialized tools for temporal data that simplify time-based selection, alignment, and aggregation.
The DatetimeIndex
A pandas index type for timestamps enabling time-based slicing, resampling, and alignment, essential for financial data analysis.
DatetimeIndex: The DatetimeIndex is one of pandas' most powerful features for financial applications. When your DataFrame has a DatetimeIndex, you gain access to intuitive time-based selection that understands calendar concepts like months, quarters, and years. This enables simple string-based slicing instead of complex boolean logic.
# Select data for a specific month using .loc for partial string indexing
jan_2020 = multi_prices.loc["2020-01"]
# Select a date range
q1_2020 = multi_prices.loc["2020-01":"2020-03"]
# Select a specific date
specific_day = multi_prices.loc["2020-06-15"]January 2020 data points: 23 Q1 2020 data points: 65 Prices on 2020-06-15: AAPL 137.188506 GOOGL 162.577217 MSFT 78.243302 Name: 2020-06-15 00:00:00, dtype: float64
For this exact generated index, January contains 23 observations and Q1 contains 65. These are pandas "B" weekdays, not exchange-calendar trading sessions. The specific-day selection returns a Series with one price value for each simulated security. Partial string indexing expresses the requested calendar range without a separate boolean mask.
Resampling Time Series
Financial data often needs to be converted between frequencies. Daily prices might need weekly or monthly conversion for longer-term analysis, risk reporting, or comparison with lower-frequency economic data. Alternatively, you might aggregate intraday tick data into daily bars for overnight processing. The resample() method handles this transformation elegantly, treating time series frequency conversion as a specialized form of grouping.
# Resample daily prices to monthly using last observation
monthly_prices = multi_prices.resample("ME").last()
# For OHLC data, you'd use different aggregations
# open: first, high: max, low: min, close: last, volume: sumMonthly prices (end of month):
AAPL GOOGL MSFT
2020-01-31 98.680295 154.108855 73.578792
2020-02-29 98.157671 143.712118 69.690895
2020-03-31 87.685277 131.849042 54.452322
2020-04-30 76.466313 105.191337 53.847502
2020-05-31 120.107901 149.071665 71.474786The result has 12 rows labeled at calendar month end. Each value is the last available observation in its monthly bin; the label itself can fall on a weekend or holiday. Downsampling is useful when the analysis needs lower-frequency endpoints or aggregates. Upsampling requires an explicit missing-value policy, and inserted values are not new market observations. Frequency codes include "D" for calendar day, "W" for weekly, "ME" for month end, and "QE" for quarter end. The aggregation function matters: last selects an endpoint observation, while mean computes a period average. For OHLC data, use first for open, max for high, min for low, last for close, and sum for volume when those operations match the source's bar semantics.

Rolling Window Calculations
Many financial metrics rely on rolling windows, including moving averages, rolling volatility, and rolling correlations. These calculations capture how a statistic evolves over time and provide insight into changing market conditions that a single summary statistic would obscure. A rolling window calculation computes a statistic over a fixed-size moving window of data. For a window of size at time , we compute the statistic using observations from to .
Imagine sliding a frame across the data. At each position, you calculate a statistic using observations inside the frame, record it, then shift the frame forward by one observation. This generates a time series showing how the statistic evolves. For moving averages and many other smooth rolling estimators, window size creates a common tradeoff: smaller windows usually react sooner to recent changes but vary more from sample to sample, while larger windows usually produce smoother estimates but react more slowly. This pattern is a heuristic, not a guarantee for every rolling statistic or dataset.

Formally, a rolling statistic with window size is computed as:
where:
- : statistic value at time
- : the statistical function (examples include mean, standard deviation, maximum, minimum, or any aggregate function)
- : the observation at time
- : the window size (number of observations to include)
- : current time index
At each time point : you gather the most recent observations, apply your chosen function , and record the result. As advances, the window slides forward by dropping the oldest observation and incorporating the newest one. This creates a dynamic view of your statistic that reveals patterns invisible in static summaries.
Pandas makes these calculations straightforward:
# Calculate 20-day and 50-day moving averages for AAPL
aapl = multi_prices["AAPL"].to_frame()
aapl["MA20"] = aapl["AAPL"].rolling(window=20).mean()
aapl["MA50"] = aapl["AAPL"].rolling(window=50).mean()
# Rolling 20-day volatility of returns
returns = multi_prices["AAPL"].pct_change()
rolling_vol = returns.rolling(window=20).std() * np.sqrt(252) # AnnualizedThe annualization formula converts daily volatility to annual volatility. Volatility is often annualized to support comparison across assets and horizons. Other risk metrics use application-specific horizons, so state the horizon and scaling assumptions explicitly. To understand why the square root appears, consider that volatility scales with the square root of time under the assumption of independent returns (the "square root of time" rule). This relationship, known as the "square root of time" rule, shows how risk scales across investment horizons.
where:
- : square-root-of-time annualized volatility estimate
- : daily volatility, which is the standard deviation of daily returns
- : annualization factor in observations per year; 252 is a common convention for daily US-market data, not the exact count in every calendar year
Let denote the additive aggregate of daily returns. If the daily returns are independent and identically distributed with variance (or, more generally, have zero pairwise covariances and the same variance), then the variance of this sum is:
where:
- : variance of the additive return aggregate over days
- : return at time
- : variance of daily returns
- : number of days
For the simple returns used in this chapter, the exact compounded -day return is , not . Multiplying daily simple-return volatility by is therefore a square-root-of-time convention or small-return approximation, not the exact standard deviation of compounded simple returns in general. The variance relation is exact for additive returns such as log returns when the stated covariance conditions hold.
Taking the square root to obtain standard deviation gives:
where:
- : standard deviation of the additive -day return aggregate under the stated covariance conditions
- : daily volatility
- : number of days
A daily volatility of 1% does not imply annual volatility of 252% but rather approximately 16%, since . This square-root-of-time scaling is used in risk management and option pricing.
Prices with moving averages:
AAPL MA20 MA50
2020-03-10 93.269925 103.915180 103.268932
2020-03-11 90.534324 103.177259 103.054333
2020-03-12 93.769109 102.561292 102.955577
2020-03-13 90.276702 101.720663 102.760914
2020-03-16 87.935324 100.611557 102.487407
Current 20-day annualized volatility: 32.86%AAPL-labelled simulated prices appear alongside their 20- and 50-observation moving averages. The shorter average responds more quickly because it uses fewer past observations. The current volatility value is the latest 20-observation sample standard deviation of daily simple returns multiplied by ; it is a square-root-of-time annualized estimate, not the standard deviation of observed annual returns. For a fully observed input, the first rolling values are NaN when the window requires observations. Here, pct_change() contributes an additional leading NaN, so the first 20 rolling-volatility values are NaN. Use min_periods to permit earlier results with smaller effective samples.
Handling Missing Data
Financial time series frequently contain missing values. Markets close for holidays, securities halt trading, and data feeds occasionally fail. Depending on the function and its parameters, missing values can propagate, be skipped, or change the effective sample. The treatment therefore affects the meaning of downstream results.
# Create data with some missing values
prices_with_gaps = multi_prices.copy()
prices_with_gaps.iloc[10:15, 0] = np.nan # Remove some AAPL prices
prices_with_gaps.iloc[20:22, 1] = np.nan # Remove some GOOGL pricesMissing values per column: AAPL 5 GOOGL 2 MSFT 0 dtype: int64
AAPL has 5 missing values and GOOGL has 2, while MSFT has none. These gaps represent periods when we artificially removed data to demonstrate handling techniques. In real data, missing values might indicate trading halts, data feed failures, or corporate actions. Pandas provides several strategies for handling missing data: each strategy has different assumptions and use cases.
# Forward fill: use last known value
ffill_prices = prices_with_gaps.ffill()
# Backward fill: use next known value
bfill_prices = prices_with_gaps.bfill()
# Interpolation: linear interpolation between known values
interp_prices = prices_with_gaps.interpolate(method="linear")
# Drop rows with any missing values
clean_prices = prices_with_gaps.dropna()Original rows: 252 After dropna: 245 Missing after forward fill: 0
Dropping rows with any missing value reduces this dataset from 252 to 245 rows. Forward filling removes the gaps by carrying the last observed value forward, which assumes a stale price until a new observation arrives. That can create artificial zero returns and suppress measured volatility. Whether it is appropriate depends on the asset, trading calendar, halt status, valuation policy, and analysis. Backward filling directly uses a future observation and can introduce look-ahead bias in a backtest.

Data Manipulation
Beyond loading and time series operations, you need to filter, transform, and aggregate data for analysis. These manipulation skills form the bridge between raw data and actionable insights.
Computing Returns
Converting prices to returns is a common first transformation. Returns are comparable across price levels and often have more stable location than price levels, although their distributions and volatility can still change over time.
Simple price returns measure the percentage price change from one period to the next. For an investment that moves from price to , the simple price return is:
where:
- : simple return at time (fractional change, e.g., 0.05 for a 5% gain)
- : price at time
- : price at the previous time period
The numerator is the price change. For example, a stock moving from $100 to $105 gains $5 per share, so the price return is $5 / $100 = 0.05, or 5%. Price-only returns omit dividends, distributions, fees, financing, taxes, and corporate-action adjustments. Use adjusted prices or explicit cash flows when the objective is investor total return.
The simple-return formula requires a nonzero previous price. Financial price series are normally strictly positive, but imported data should still be checked before division.
Log returns use the natural logarithm of the price ratio and have attractive mathematical properties. Importantly, log returns sum across time periods:
where:
- : log return at time (continuous compounding rate)
- : price at time
- : price at the previous time period
- : natural logarithm (base )
The logarithm transforms multiplicative price changes to additive returns, bridging discrete and continuous-time financial theory. Since , multi-period log returns sum simply. This additive property greatly simplifies calculations involving cumulative returns over many periods.
Real-valued log returns require both prices to be strictly positive. A zero or negative input makes the logarithm undefined in this setting, so validate or clean such observations before applying np.log.
# Calculate simple returns: (P_t - P_{t-1}) / P_{t-1}
simple_returns = multi_prices.pct_change()
# Calculate log returns: ln(P_t / P_{t-1})
log_returns = np.log(multi_prices / multi_prices.shift(1))Simple returns (first 5 rows):
AAPL GOOGL MSFT
2020-01-01 NaN NaN NaN
2020-01-02 -0.025254 -0.016012 -0.035917
2020-01-03 0.013200 0.023861 0.014130
2020-01-06 0.016007 0.008129 0.017067
2020-01-07 0.019225 0.005378 0.008473
Mean daily return:
AAPL 0.001398
GOOGL -0.000494
MSFT 0.000621
dtype: float64The first five daily price returns appear for each simulated series. The first row is NaN because no earlier price is available. The displayed means summarize this constructed sample only; their signs describe the realized average daily changes, not a general empirical property of the named securities.


Simple returns computed here are price returns. With beginning-of-period weights, a fully invested portfolio, and explicit cash or financing legs where needed, same-period simple total returns aggregate across assets as:
where:
- : total portfolio return, which is the weighted average of individual returns
- : weight of asset in the portfolio, which is the fraction of total portfolio value where
- : simple return of asset over the period
- : sum over all assets in the portfolio, where index ranges over all holdings
This linear aggregation holds for the stated one-period accounting setup. Each asset contributes in proportion to its beginning weight. For investor performance, the asset returns must include the relevant distributions and cash flows rather than unadjusted price changes alone.
Log returns aggregate additively across time. To find the total return over multiple periods, simply sum the individual period returns.
where:
- : cumulative log return from time 0 to time
- : log return for period , from time to time
- : final time period, which is the total number of periods
- : sum over all periods from 1 to
This additive property arises because , making multi-period calculations simple. You can compute the return over any horizon by summing sub-period returns without any compounding adjustments.
For , and the leading difference is approximately . At a 10% simple return, the exact difference is about 46.9 basis points, which is not negligible for every use. Log returns are common in models because they add across time; simple returns are used for one-period portfolio accounting under the conditions above.
Filtering and Selecting
Boolean indexing extracts data subsets that meet specific criteria, enabling event studies, anomaly detection, and conditional sample construction.
# Find days where AAPL return exceeded 3% in absolute value
large_moves = simple_returns[simple_returns["AAPL"].abs() > 0.03]
# Find days where all three stocks moved in the same direction
same_direction = simple_returns[
((simple_returns > 0).all(axis=1)) | ((simple_returns < 0).all(axis=1))
]Days with AAPL moves > 3%: 49 Days all stocks moved together: 127
The simulated AAPL-labelled series had moves exceeding 3% on several days. The same-sign filter counts dates when all three constructed returns have the same sign; it is a co-movement diagnostic, not a correlation estimate or evidence of market-wide dependence. Compute correlations separately and interpret them in light of the data-generating process.
Merging Datasets
Financial analysis often requires combining data from different sources such as prices with fundamentals, returns with factor exposures, or multiple time series with different starting dates.
# Create a synthetic benchmark from the same constructed common factor
market_returns = pd.DataFrame(
{"synthetic_market": np.expm1(market_factor)}, index=dates
)
# Join stock and market returns
combined = simple_returns.join(market_returns, how="inner")Combined dataset:
AAPL GOOGL MSFT synthetic_market
2020-01-01 NaN NaN NaN 0.007040
2020-01-02 -0.025254 -0.016012 -0.035917 -0.012530
2020-01-03 0.013200 0.023861 0.014130 0.016441
2020-01-06 0.016007 0.008129 0.017067 -0.008715
2020-01-07 0.019225 0.005378 0.008473 0.007639
Shape: (252, 4)Four columns appear: three simulated security returns plus the synthetic common-factor benchmark. The inner join keeps only index values present in both datasets; it does not by itself verify that timestamps, calendars, or return conventions are economically comparable. A real beta or attribution study would require sourced benchmark returns and those additional alignment checks.
Summary Statistics
Compute statistical summaries to understand your data's characteristics:
# Descriptive statistics
stats = simple_returns.describe()
# Add skewness and kurtosis
stats.loc["skew"] = simple_returns.skew()
stats.loc["kurtosis"] = simple_returns.kurtosis()Return statistics:
AAPL GOOGL MSFT
count 251.0000 251.0000 251.0000
mean 0.0014 -0.0005 0.0006
std 0.0284 0.0249 0.0269
min -0.0834 -0.0928 -0.0767
25% -0.0133 -0.0128 -0.0136
50% -0.0002 0.0011 0.0008
75% 0.0154 0.0119 0.0140
max 0.2197 0.1846 0.2060
skew 1.7774 1.3094 1.7720
kurtosis 13.6171 12.3428 13.2736This view summarizes the constructed return sample. Mean and standard deviation describe its location and spread, while min and max show its observed endpoints. The three skewness estimates range from about 1.31 to 1.78, indicating pronounced positive sample asymmetry driven in part by a few large positive simulated observations. Positive excess kurtosis records a fourth standardized moment above the normal benchmark; here it is expected because the generator uses Student-t innovations. Many empirical return series also exhibit heavy tails, but the synthetic result is not evidence about the named securities.
Performance Optimization
Pandas and NumPy are highly optimized, but certain operations become bottlenecks with large datasets or real-time systems. Knowing when and how to optimize matters.
NumPy: The Foundation
Pandas commonly stores homogeneous numeric blocks in NumPy arrays. Vectorization applies operations to arrays instead of looping through Python objects. Direct NumPy can reduce labeled-data overhead for suitable homogeneous kernels, but conversion costs, memory layout, dtype, and alternative engines can change the result. Profile the actual workload.
# Extract NumPy arrays from DataFrame
prices_array = multi_prices.values
dates_array = multi_prices.index.values
# Vectorized calculation avoids a Python-level element loop
returns_numpy = np.diff(prices_array, axis=0) / prices_array[:-1]Array shape: (251, 3) First few returns: [[-0.0252544 -0.01601185 -0.03591687] [ 0.01319998 0.02386114 0.01413042] [ 0.01600667 0.00812855 0.01706702]]
One fewer return observation exists per security because returns require two prices. The first few returns form a matrix with one column per simulated security. This direct NumPy expression omits pandas labels and alignment. It may reduce overhead for this homogeneous calculation, but .values can copy data and performance must be measured rather than assumed.
Numba: JIT Compilation for Custom Functions
Numba specializes and JIT-compiles a supported subset of Python and NumPy. Suitable numeric kernels can approach compiled-language performance after warm-up, but support and speed are workload-dependent.
When standard NumPy operations do not express custom loop logic clearly, Numba can JIT-compile supported Python and NumPy operations after inferring concrete types. Unsupported Python features can fail compilation or require code changes.
from numba import jit
@jit(nopython=True)
def calculate_drawdowns(prices):
r"""Calculate drawdown series from price array.
Drawdown measures the percentage decline from a historical peak. At time $t$, the drawdown is computed as:
$$
DD_t = \frac{P_t - \text{Peak}_t}{\text{Peak}_t}
$$
where:
- $DD_t$: drawdown at time $t$ (a non-positive value where more negative numbers indicate larger percentage declines from the peak)
- $P_t$: current price at time $t$
- $\text{Peak}_t$: maximum price observed from the start of the series up to and including time $t$ (where $\text{Peak}_t = \max_{s \leq t} P_s$)
The drawdown is zero when the price equals its historical peak, indicating a new high, and becomes negative when the price falls below the peak. More negative values indicate larger declines. For example, a drawdown of $-0.20$ means the current price is 20% below its historical high. If the peak was $100 and the current price is $80: the drawdown is $(80-100)/100 = -0.20$.
"""
n = len(prices)
if n == 0:
raise ValueError("prices must be nonempty")
for price in prices:
if not np.isfinite(price) or price <= 0:
raise ValueError("prices must contain only finite positive values")
drawdowns = np.empty(n)
peak = prices[0]
for i in range(n):
if prices[i] > peak:
peak = prices[i]
drawdowns[i] = (prices[i] - peak) / peak
return drawdowns
# Apply to our price data
aapl_prices = multi_prices["AAPL"].values
drawdowns = calculate_drawdowns(aapl_prices)Maximum drawdown: -35.88% Current drawdown: -16.08%
The minimum drawdown is the largest observed peak-to-trough decline within the supplied sample. The current drawdown measures distance below the running high since the sample began; 0% means the last observation equals that within-sample high, not necessarily an all-time market high. The @jit(nopython=True) decorator requires operations and types that Numba can compile without Python-object fallback. It supports more than only arrays and scalars, but not arbitrary Python semantics.


Let's compare performance between a pure Python implementation and the Numba version:
import time
def calculate_drawdowns_python(prices):
"""Pure Python implementation for comparison."""
n = len(prices)
if n == 0:
raise ValueError("prices must be nonempty")
for price in prices:
if not np.isfinite(price) or price <= 0:
raise ValueError("prices must contain only finite positive values")
drawdowns = np.empty(n)
peak = prices[0]
for i in range(n):
if prices[i] > peak:
peak = prices[i]
drawdowns[i] = (prices[i] - peak) / peak
return drawdowns
# Create larger dataset for timing
large_prices = np.cumprod(1 + np.random.randn(100_000) * 0.02) * 100
# Warm up Numba (first call includes compilation time)
_ = calculate_drawdowns(large_prices)
# Time both implementations
start = time.time()
for _ in range(100):
_ = calculate_drawdowns_python(large_prices)
python_time = time.time() - start
start = time.time()
for _ in range(100):
_ = calculate_drawdowns(large_prices)
numba_time = time.time() - startPure Python: 5.503 seconds Numba JIT: 0.023 seconds Speedup: 236.4x
For this warmed numeric loop in the current environment, the printed multiplier compares 100 calls to the pure-Python and compiled implementations. It is an illustrative local benchmark, not a general Numba guarantee: compilation support, data layout, hardware, versions, and the kernel itself affect both absolute time and speedup. Production decisions require repeated benchmarks in the target environment.
Cython: When You Need More Control
Cython offers another route by translating annotated Python-like code to C. Compared with Numba's type-specialized JIT workflow, Cython can be useful when a project needs explicit C declarations, extension modules, or direct C-library integration. The appropriate tool depends on the kernel and deployment requirements.
# Cython code (would be in a .pyx file)
# cimport numpy as np
# import numpy as np
#
# def calculate_drawdowns_cython(np.ndarray[np.float64_t, ndim=1] prices):
# cdef int n = prices.shape[0]
# cdef int i
# cdef np.ndarray[np.float64_t, ndim=1] drawdowns
# cdef double peak
#
# if n == 0:
# raise ValueError("prices must be nonempty")
# for i in range(n):
# if not np.isfinite(prices[i]) or prices[i] <= 0:
# raise ValueError("prices must contain only finite positive values")
# drawdowns = np.empty(n)
# peak = prices[0]
#
# for i in range(n):
# if prices[i] > peak:
# peak = prices[i]
# drawdowns[i] = (prices[i] - peak) / peak
#
# return drawdownsStart with pandas and NumPy. After profiling, test Numba for supported numeric kernels and Cython when explicit C integration or extension-module control is useful. Benchmark the candidate implementation under the target workload.
When to Optimize
Follow this approach to optimization:
- Write clear code first using pandas and NumPy
- Profile to identify actual bottlenecks with tools like %timeit or cProfile
- Optimize measured hot paths that dominate runtime
- Verify correctness after optimization
Judge optimization against the workload's latency and throughput budget. A 50 ms versus 5 ms difference may be irrelevant for one interactive calculation but material when repeated across a large parameter search. In a live system, insufficient throughput or excessive latency can produce stale decisions or missed service targets; the trading consequence depends on the strategy and execution design.
Data Visualization
Visualizations reveal patterns and relationships in data that remain invisible in raw numbers.
Price Charts
A price chart is a common starting visualization for showing how a price series evolves over time.
In this simulated sample, AAPL and MSFT finish above their starting values while GOOGL finishes below. All three paths fluctuate and respond to the injected stress regime. Because the series begin at different price levels, indexing them to a common base is one useful way to compare relative price paths; returns or indexed total-wealth series are alternatives when their inputs are available.
The common baseline of 100 makes relative price changes easier to compare. All three simulated series dip during the injected stress period and then follow different paths. Indexing is useful here, but return charts, excess-return charts, and total-wealth series may be more appropriate for other comparisons.
Return Distributions
Return distribution visualizations reveal characteristics critical for quantitative models.
The simulated sample differs from its moment-matched normal reference. That behavior is expected because the generator uses Student-t common and idiosyncratic innovations. Many empirical financial return series also exhibit heavy tails, but this constructed example demonstrates the plotting method rather than establishing an empirical result for AAPL.

Scatter Plots and Correlations
Correlations between securities matter for portfolio construction and risk management. Scatter plots visualize pairwise relationships by plotting one variable against another.
The two simulated return series have positive sample correlation because the generator gives them a common factor as well as idiosyncratic terms. The regression line summarizes their linear association in this sample. Correlation of 1 places centered observations on a positive-slope line; zero correlation means no sample linear association, not necessarily an unstructured random cloud or independence. For multiple series, a correlation matrix summarizes pairwise linear relationships.
The diagonal is 1.0 by construction, while off-diagonal entries are sample correlations between the simulated series. All are positive because the generator includes a shared factor. In portfolio analysis, positive dependence can reduce diversification relative to otherwise comparable less-correlated assets, but this heatmap does not estimate market-wide or sector-wide dependence.
Rolling Statistics
Financial relationships are not static: rolling correlations reveal how relationships evolve over time.
The rolling estimates vary across this synthetic sample. The generator explicitly changes drift and volatility during the stress interval and includes a common factor, so these plots illustrate information that a single full-sample correlation or volatility estimate would omit. They do not test a constant-parameter model against observed market data or show that every such model is inadequate.
Limitations and Practical Considerations
Pandas prioritizes convenience, but row-wise iteration and complex conditional logic can be orders of magnitude slower than necessary. Performance optimization techniques using Numba address many of these bottlenecks. Optimization requires careful attention to data types: unexpected inputs can introduce subtle bugs.
Data quality issues include missing values, unadjusted corporate actions, universe-selection bias, identifier errors, and inconsistent timestamps. fillna() and interpolate() address only missing entries under explicit assumptions. They do not adjust stock splits or distributions, reconstruct delisted securities, repair identifiers, or normalize timestamps. Forward fill uses only past observations, so it does not intrinsically introduce look-ahead; it can instead create stale prices, artificial zero returns, and valuation bias. Backward fill uses future observations and is the direct look-ahead risk shown earlier.
Effective financial visualization balances art and science and requires customization for specific audiences and purposes. A chart for internal research can prioritize information density, while a client presentation might emphasize clarity and visual appeal. Matplotlib offers extensive customization, though mastering it takes practice.
These patterns assume datasets that fit in memory. Depending on measured volume, latency, durability, and concurrency requirements, larger workloads may need columnar storage, databases, streaming, chunked execution, or distributed computing. Distribution is one option, not a universal requirement.
Summary
This chapter covered essential data handling and visualization skills for quantitative finance.
Data loading and management: Pandas reads many tabular formats and organizes records returned by source-specific connectors. A DatetimeIndex enables time-based slicing, resampling, and rolling calculations.
Time series operations: Resampling converts between frequencies, rolling windows compute statistics, and proper missing data handling prevents errors.
Data manipulation: Convert prices to returns, then filter, merge, and summarize data for analysis.
Performance optimization: Start with clear pandas and NumPy code, profile the target workload, and test Numba on supported numeric kernels when Python-level loops dominate. Speedups require warm, controlled benchmarks and are not guaranteed.
Visualization: Charts reveal patterns invisible in summary statistics. Use price charts, distributions, correlations, and rolling metrics to explore data.
You will repeatedly use these data handling patterns when calculating risk, building factor models, or training machine learning models.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about data handling and visualization in quantitative finance.
Data Handling and Visualization
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Quantitative Finance. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Quantitative FinanceStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!