Data Handling & Visualization: Python for Quant Finance

Michael BrenndoerferNovember 7, 202539 min read

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:

In[2]:
Code
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 str dtype 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.
In[3]:
Code
# 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"
)
Out[4]:
Console
                  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.

In[5]:
Code
# 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())
Out[6]:
Console
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.247694

Three 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.

In[7]:
Code
# Convert to long format
long_format = (
    multi_prices.reset_index()
    .melt(id_vars="index", var_name="ticker", value_name="price")
    .rename(columns={"index": "date"})
)
Out[8]:
Console
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.691553

Each 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

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.

In[9]:
Code
# 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"]
Out[10]:
Console
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.

In[11]:
Code
# 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: sum
Out[12]:
Console
Monthly 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.474786

The 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.

Out[13]:
Visualization
Line chart comparing simulated daily prices with monthly endpoint observations for an AAPL-labelled series.
Simulated daily AAPL-labelled prices and the last available observation in each calendar-month bin. The 12 orange markers reduce the number of displayed observations but do not average or mathematically smooth the daily path.

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 nn at time tt, we compute the statistic using observations from t−n+1t-n+1 to tt.

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.

Out[14]:
Visualization
Diagram showing how a rolling window slides across time series data.
Rolling window concept: a 5-observation window slides across price data. At t=9, indices 5 through 9 contain 104, 106, 108, 107, and 109, whose mean is 106.8. Sliding the window forward produces one statistic per eligible endpoint.

Formally, a rolling statistic StS_t with window size nn is computed as:

St=f(Xt−n+1,Xt−n+2,…,Xt)S_t = f(X_{t-n+1}, X_{t-n+2}, \ldots, X_t)

where:

  • StS_t: statistic value at time tt
  • ff: the statistical function (examples include mean, standard deviation, maximum, minimum, or any aggregate function)
  • XiX_i: the observation at time ii
  • nn: the window size (number of observations to include)
  • tt: current time index

At each time point tt: you gather the most recent nn observations, apply your chosen function ff, and record the result. As tt 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:

In[15]:
Code
# 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)  # Annualized

The 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.

σannual=σdaily×T\sigma_{\text{annual}} = \sigma_{\text{daily}} \times \sqrt{T}

where:

  • σannual\sigma_{\text{annual}}: square-root-of-time annualized volatility estimate
  • σdaily\sigma_{\text{daily}}: daily volatility, which is the standard deviation of daily returns
  • TT: 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 AT=∑t=1TRtA_T = \sum_{t=1}^{T} R_t denote the additive aggregate of TT daily returns. If the daily returns are independent and identically distributed with variance σd2\sigma_d^2 (or, more generally, have zero pairwise covariances and the same variance), then the variance of this sum is:

Var(AT)=∑t=1TVar(Rt)(zero cross-covariances)=Tσd2(sum of T identical variances)\begin{aligned} \text{Var}(A_T) &= \sum_{t=1}^{T} \text{Var}(R_t) && \text{(zero cross-covariances)} \\ &= T\sigma_d^2 && \text{(sum of } T \text{ identical variances)} \end{aligned}

where:

  • Var(AT)\text{Var}(A_T): variance of the additive return aggregate over TT days
  • RtR_t: return at time tt
  • σd2\sigma_d^2: variance of daily returns
  • TT: number of days

For the simple returns used in this chapter, the exact compounded TT-day return is ∏t=1T(1+Rt)−1\prod_{t=1}^{T}(1+R_t)-1, not ATA_T. Multiplying daily simple-return volatility by T\sqrt{T} 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:

σT=Tσd\sigma_T = \sqrt{T}\sigma_d

where:

  • σT\sigma_T: standard deviation of the additive TT-day return aggregate under the stated covariance conditions
  • σd\sigma_d: daily volatility
  • TT: number of days

A daily volatility of 1% does not imply annual volatility of 252% but rather approximately 16%, since 252≈15.9\sqrt{252} \approx 15.9. This square-root-of-time scaling is used in risk management and option pricing.

Out[16]:
Console
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 252\sqrt{252}; it is a square-root-of-time annualized estimate, not the standard deviation of observed annual returns. For a fully observed input, the first n−1n-1 rolling values are NaN when the window requires nn 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.

In[17]:
Code
# 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 prices
Out[18]:
Console
Missing 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.

In[19]:
Code
# 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()
Out[20]:
Console
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.

Out[21]:
Visualization
Line chart comparing forward fill, backward fill, and linear interpolation methods for handling missing data.
Three strategies for handling missing data in AAPL prices. Forward fill carries the last value forward, backward fill projects the next value backward, and linear interpolation draws a straight line between known values. The shaded region highlights the gap period. Each method reflects different assumptions about price behavior during gaps, with material implications for return calculations and volatility estimation.

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 Pt−1P_{t-1} to PtP_t, the simple price return is:

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

where:

  • RtR_t: simple return at time tt (fractional change, e.g., 0.05 for a 5% gain)
  • PtP_t: price at time tt
  • Pt−1P_{t-1}: price at the previous time period

The numerator (Pt−Pt−1)(P_t - P_{t-1}) 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:

rt=ln⁡(PtPt−1)r_t = \ln\left(\frac{P_t}{P_{t-1}}\right)

where:

  • rtr_t: log return at time tt (continuous compounding rate)
  • PtP_t: price at time tt
  • Pt−1P_{t-1}: price at the previous time period t−1t-1
  • ln⁡\ln: natural logarithm (base ee)

The logarithm transforms multiplicative price changes to additive returns, bridging discrete and continuous-time financial theory. Since ln⁡(ab)=ln⁡(a)+ln⁡(b)\ln(ab) = \ln(a) + \ln(b), 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.

In[22]:
Code
# 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))
Out[23]:
Console
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: float64

The 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.

Out[24]:
Visualization
Scatter plot comparing simple returns versus log returns with reference line.
Simulated AAPL-labelled simple and log returns lie close to the diagonal over the displayed range. They are not mathematically identical: log(1+R) equals R only to first order near zero.
Scatter plot showing difference between simple and log returns.
Difference between simulated simple and log returns in basis points. The leading approximation error is R²/2, so whether the discrepancy is negligible depends on the return size and the application's tolerance.
Simple vs. Log Returns

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:

Rp=∑iwiRiR_p = \sum_i w_i R_i

where:

  • RpR_p: total portfolio return, which is the weighted average of individual returns
  • wiw_i: weight of asset ii in the portfolio, which is the fraction of total portfolio value where ∑iwi=1\sum_i w_i = 1
  • RiR_i: simple return of asset ii over the period
  • ∑i\sum_i: sum over all assets in the portfolio, where index ii 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.

r0,T=∑t=1Trtr_{0,T} = \sum_{t=1}^{T} r_t

where:

  • r0,Tr_{0,T}: cumulative log return from time 0 to time TT
  • rtr_t: log return for period tt, from time t−1t-1 to time tt
  • TT: final time period, which is the total number of periods
  • ∑t=1T\sum_{t=1}^{T}: sum over all periods from 1 to TT

This additive property arises because ln⁡(PT/P0)=ln⁡(PT/PT−1)+ln⁡(PT−1/PT−2)+⋯+ln⁡(P1/P0)\ln(P_T/P_0) = \ln(P_T/P_{T-1}) + \ln(P_{T-1}/P_{T-2}) + \cdots + \ln(P_1/P_0), making multi-period calculations simple. You can compute the return over any horizon by summing sub-period returns without any compounding adjustments.

For ∣R∣≪1|R| \ll 1, ln⁡(1+R)≈R\ln(1+R) \approx R and the leading difference R−ln⁡(1+R)R-\ln(1+R) is approximately R2/2R^2/2. 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.

In[25]:
Code
# 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))
]
Out[26]:
Console
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.

In[27]:
Code
# 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")
Out[28]:
Console
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:

In[29]:
Code
# Descriptive statistics
stats = simple_returns.describe()

# Add skewness and kurtosis
stats.loc["skew"] = simple_returns.skew()
stats.loc["kurtosis"] = simple_returns.kurtosis()
Out[30]:
Console
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.2736

This 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.

In[31]:
Code
# 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]
Out[32]:
Console
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

Just-In-Time (JIT) Compilation

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.

In[33]:
Code
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)
Out[34]:
Console
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.

Out[35]:
Visualization
Line chart of AAPL price and running peak over time.
Simulated AAPL-labelled price series over 252 weekday observations with its within-sample running peak (dashed). Running highs correspond to zero drawdown in the companion panel.
Area and line chart of AAPL drawdown percentage over time with a marker for maximum drawdown.
Simulated drawdown series measured from the within-sample running peak. The marked point is the largest observed peak-to-trough decline in this sample, not a worst-case loss bound.

Let's compare performance between a pure Python implementation and the Numba version:

In[36]:
Code
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() - start
Out[37]:
Console
Pure 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.

In[61]:
Code
# 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 drawdowns

Start 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:

  1. Write clear code first using pandas and NumPy
  2. Profile to identify actual bottlenecks with tools like %timeit or cProfile
  3. Optimize measured hot paths that dominate runtime
  4. 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.

Out[41]:
Visualization
Q-Q plot of simulated heavy-tailed returns against a normal reference line.
Q-Q plot comparing simulated heavy-tailed AAPL-labelled returns with a fitted normal reference line. Departures in this constructed sample illustrate how a Q-Q diagnostic can reveal disagreement with a normal model; they do not establish the behavior of observed AAPL returns or every constant-parameter risk model.

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

Question 1 of 80 of 8 completed
What is the primary advantage of using a DatetimeIndex in pandas for financial data?

Comments

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

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025datahandling, author = {Michael Brenndoerfer}, title = {Data Handling & Visualization: Python for Quant Finance}, year = {2025}, url = {https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-13} }
APAAcademic
Michael Brenndoerfer (2025). Data Handling & Visualization: Python for Quant Finance. Retrieved from https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python
MLAAcademic
Michael Brenndoerfer. "Data Handling & Visualization: Python for Quant Finance." 2026. Web. October 13, 2026. <https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python>.
CHICAGOAcademic
Michael Brenndoerfer. "Data Handling & Visualization: Python for Quant Finance." Accessed October 13, 2026. https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Data Handling & Visualization: Python for Quant Finance'. Available at: https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python (Accessed: October 13, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Data Handling & Visualization: Python for Quant Finance. https://mbrenndoerfer.com/writing/data-handling-visualization-quantitative-finance-python

About the author

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 Finance
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.