Create your own
Lesson illustration

Calculating Simple and Logarithmic Returns

Hello. In the previous lesson, you built the discipline needed before any calculation: raw data remain untouched, anomalies are investigated rather than silently deleted, and corporate actions and vendor conventions are documented. Now we can make the central transformation used throughout quantitative finance: converting an approved adjusted-price series into returns.

This matters because prices in rupees or dollars are not directly comparable across assets or dates. A ₹10 move is radically different for a ₹100 stock and a ₹2,000 stock. Returns express the proportional economic change instead. By the end of this lesson, you should be able to calculate simple and logarithmic returns from adjusted prices, explain how they aggregate through time, and produce a clean return dataset in pandas.


Choose the price series before choosing the formula

A return is only as meaningful as the price series behind it. The prior audit lesson established why raw closing prices can jump mechanically around splits, bonus issues, or dividends. For most equity research focused on investor performance, your input should be a documented adjusted close series.

Let denote the adjusted price at the end of period . Depending on the data vendor, this may be adjusted for:

  • stock splits and bonus issues only;
  • splits plus cash dividends;
  • further distributions or corporate actions.

These conventions differ by provider. Read the data documentation and store the convention in your project README or metadata. Do not assume that every field called Adj Close is a total-return series.

There are two coherent approaches:

  1. Use vendor-adjusted prices. Calculate returns directly from the adjusted series. Do not add dividends a second time.
  2. Use unadjusted prices and explicit cash flows. Add dividends yourself, provided you have reliable dates and amounts.

For your NIFTY 50 research workflow, the first approach is generally practical, provided the adjustment convention is clearly documented.

If you use adjusted prices, retain the raw close, corporate-action columns, vendor name, and retrieval date in the audited dataset. The adjusted series is an analysis input; it does not replace the original evidence.


Simple returns: the investor-accounting view

The simple net return from to is the percentage change in adjusted price:

The associated gross return is:

The gross return has a direct wealth interpretation. If you begin with ₹1, then after one period you hold rupees. A simple return of means a gain of 3%; its gross return is .

Suppose the adjusted closing price of a stock is ₹100 on Monday and ₹105 on Tuesday:

The Tuesday simple return is 5%.

If instead the adjusted price falls from ₹105 to ₹94.50:

The Wednesday simple return is .

Simple returns are particularly important for one-period portfolio accounting. With beginning-of-period portfolio weights , the portfolio’s one-period simple return is:

This relationship is one reason that simple returns are natural for backtests, portfolio aggregation, and P&L reporting.

1.2 Asset Return Calculations

Read 1.2 Asset Return Calculations from the Computational Finance text to connect the return formulas to their investment interpretation, dividends, and multi-period compounding.

Begin in Section 1.2.2, “Simple returns.” Read the one-period setup, then continue through Section 1.2.2.1, “Multi-period returns.” Focus on why gross returns multiply across time rather than why net returns simply add. Next, read Section 1.2.2.2, “Adjusting for dividends,” especially the total-return example. Relate this to the adjusted-price convention of your dataset: adjusted prices typically aim to incorporate this effect already. Finally, read Section 1.2.3, “Continuously compounded returns,” through Section 1.2.3.2, “Multi-period returns.” Read the definition and motivation, then follow the two-period derivation carefully. The key distinction is that simple returns compound multiplicatively, whereas log returns add through time.

Dividends when you do not have adjusted prices

If is an unadjusted price and the holder receives a cash dividend over the period, the total simple return is:

It can be separated into a capital-gain return and dividend yield:

This formula is conceptually important, but do not combine it with an adjusted-close return without checking your provider’s adjustment methodology. Otherwise, the dividend can be counted twice.


Compounding: why returns cannot usually be summed

Suppose a price goes from ₹100 to ₹110, then from ₹110 to ₹99. The two daily simple returns are:

It is tempting to add them and claim the overall return is zero. But the actual two-period return is:

The investment lost 1%, exactly as the endpoint calculation shows:

A 10% loss requires an 11.11% gain to recover, because the gain is earned on a smaller base. In general, over periods:

and therefore:

For small daily returns, summing simple returns may be a rough approximation. It is not an exact cumulative-return calculation and should not be used as one in a backtest or project report.

How To Calculate Stock Returns [Excel and in Python] - Returns, Cumulative Returns, Log returns

Watch How To Calculate Stock Returns [Excel and in Python] - Returns, Cumulative Returns, Log returns by Algovibes for a compact visual demonstration of the calculations and their Python implementation.

Start with simple returns to reinforce why proportional changes make assets comparable. Then watch compounding and logs; pay particular attention to why cumulative simple returns use products and why log returns can be summed. For the pandas workflow, watch the Python implementation. Treat this as a demonstration of the mechanics, while keeping the data-quality rules from the previous lesson in place: do not calculate returns from an unexamined or automatically filled price series.


Log returns: the time-additive view

The log return, also called the continuously compounded return, is:

Because the simple gross return is , the equivalent form is:

You can convert in either direction:

For the earlier ₹100 to ₹105 movement:

The simple and log returns are close, but not identical. For modest daily returns, the approximation

is often good. For large moves, it is not. A 10% simple return corresponds to a log return of approximately , not 10%.

The main advantage of log returns is time additivity. For consecutive periods:

More generally:

For the ₹100, ₹110, ₹99 example:

This equals the endpoint log return:

To return to the cumulative simple return:

When to use each return type

Neither type is universally “better.” Use the one that matches the calculation.

TaskUsually appropriate return
One-period portfolio return from asset weightsSimple return
Portfolio P&L and backtest equity curveSimple return
Cumulative wealth calculationCompounded simple returns
Statistical modeling of a single asset through timeOften log return
Aggregating an individual asset’s returns through timeLog return, then convert back if needed
Option pricing and continuous-time modelsLog-return representation is common

A crucial caveat: log returns add across time, not across assets. The log return of a portfolio is generally not the weighted average of individual asset log returns. For cross-sectional portfolio aggregation, work with simple returns and beginning-of-period weights.

Both return types impose validity conditions:

  • Simple returns require .
  • Log returns require both and .
  • A first observation has no preceding price, so its return should be missing, not zero.
  • A data gap should remain visible. Never manufacture a return by forward-filling a missing price.

A defensible pandas implementation

Assume that the output of your audit pipeline has one record per symbol and date, and contains a documented adj_close field:

date,symbol,adj_close
2024-01-02,RELIANCE,2580.40
2024-01-03,RELIANCE,2612.10
2024-01-04,RELIANCE,2590.30

The following function creates simple and log return columns without filling missing prices or mixing symbols.

from pathlib import Path

import numpy as np
import pandas as pd


def add_returns(prices: pd.DataFrame) -> pd.DataFrame:
    required = {"date", "symbol", "adj_close"}
    missing_columns = required.difference(prices.columns)

    if missing_columns:
        raise ValueError(f"Missing required columns: {sorted(missing_columns)}")

    data = prices.copy()
    data["date"] = pd.to_datetime(data["date"], errors="coerce")
    data["adj_close"] = pd.to_numeric(data["adj_close"], errors="coerce")

    if data["date"].isna().any():
        raise ValueError("Unparseable dates remain in the input.")

    if data.duplicated(["symbol", "date"]).any():
        raise ValueError("Duplicate symbol-date keys remain in the input.")

    data = data.sort_values(["symbol", "date"]).reset_index(drop=True)

    previous_price = (
        data.groupby("symbol", sort=False)["adj_close"]
            .shift(1)
    )

    valid_pair = (
        data["adj_close"].notna()
        & previous_price.notna()
        & data["adj_close"].gt(0)
        & previous_price.gt(0)
    )

    price_ratio = (
        data["adj_close"].where(valid_pair)
        .div(previous_price.where(valid_pair))
    )

    data["simple_return"] = price_ratio.sub(1)
    data["log_return"] = np.log(price_ratio)

    return data


if __name__ == "__main__":
    input_path = Path("data/processed/nifty_adjusted_prices.csv")
    output_path = Path("data/processed/nifty_daily_returns.csv")

    prices = pd.read_csv(input_path)
    returns = add_returns(prices)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    returns.to_csv(output_path, index=False)

    print(returns.head(10).to_string(index=False))

Several details in this implementation are deliberate:

  • groupby("symbol") prevents the final price of one stock from being paired with the first price of another.
  • Sorting occurs before shift(1), so “previous” has a temporal meaning.
  • The first observation for each symbol yields missing returns because no previous price exists.
  • A zero, negative, or missing input price yields missing returns rather than an invalid division or logarithm.
  • No fillna(), forward-fill, or interpolation is applied.
  • The source’s adjustment convention remains an external, documented assumption.

The function assumes each asset’s rows represent consecutive intended trading sessions. If a vendor entirely omits a session rather than providing an explicit missing observation, the price ratio can span more than one session. In the next lesson, you will handle time alignment and resampling explicitly, using a defined calendar rather than assuming every adjacent row represents one trading day.

The concise wide-panel form

Once you have a properly aligned matrix with dates as rows and symbols as columns, the same calculation is compact:

wide_prices = (
    prices.pivot(index="date", columns="symbol", values="adj_close")
          .sort_index()
)

if (wide_prices.dropna() <= 0).any().any():
    raise ValueError("Positive adjusted prices are required.")

simple_returns = wide_prices.div(wide_prices.shift(1)).sub(1)
log_returns = np.log(wide_prices.div(wide_prices.shift(1)))

This is useful for multi-asset analysis, but it should come after validating timestamps, duplicate keys, and coverage. The next lesson will formalize how to construct such a panel without accidentally treating a non-trading day or a data outage as a zero-return day.


Sanity checks that catch common mistakes

Before writing a return file to disk, check a small sample manually. For a valid pair of prices, the following identities should hold:

In pandas, you can verify the latter two identities numerically:

valid = returns["simple_return"].notna()

np.testing.assert_allclose(
    returns.loc[valid, "log_return"],
    np.log1p(returns.loc[valid, "simple_return"]),
)

np.testing.assert_allclose(
    returns.loc[valid, "simple_return"],
    np.expm1(returns.loc[valid, "log_return"]),
)

Use np.log1p(x) rather than np.log(1 + x) when converting simple returns to log returns, and use np.expm1(x) rather than np.exp(x) - 1 for the reverse conversion. These functions are designed to retain numerical precision when is very close to zero.

For a short contiguous price segment, calculate the cumulative return two ways:

and

They should agree, up to floating-point precision. Likewise, the endpoint log return should equal the sum of the period log returns:

These are not merely mathematical niceties. They are practical invariants that reveal sorting errors, accidental mixing of raw and adjusted prices, or an incorrectly shifted series.


Key takeaways

Prices become quantitatively comparable only after transforming them into returns.

  • Calculate simple returns from approved, consistently adjusted prices:
  • Calculate log returns as:
  • Simple returns compound through time using gross returns, not arithmetic sums.
  • Log returns add through time, then convert back to a simple cumulative return with .
  • Use simple returns for one-period portfolio aggregation; use log returns often for time-series modeling of individual assets.
  • Never silently fill missing prices to make a return series look complete.
  • Keep the vendor’s adjustment convention explicit, and never add dividends again if adjusted prices already embed them.

Next, you will align and resample multiple financial time series while preserving time order and avoiding future information. That step turns separate asset-level return series into a panel suitable for correlation analysis, portfolio construction, and later backtesting.

Can't find a good explanation? Sign up and we'll make it for you

Sign up