DEV Community

Alper San
Alper San

Posted on Originally published at sifting.io

Rebase prices to 100 in Python without hiding missing data

Putting EURUSD, gold, a US stock and Bitcoin on one price axis usually tells you more about their units than their movement. A price near 1 disappears beside a price near 50,000. Normalize prices to 100, and every series starts at the same height: 103 means its quoted price is 3% above its starting value.

This Python example builds that comparison with pandas and Matplotlib. It uses a small, fictional dataset so you can check the arithmetic locally. The useful part is the shared baseline: every line describes the same period, and later missing prices stay missing.

What rebasing a price to 100 means

Choose one baseline time, then calculate each series separately:

indexed price = 100 × current price / price at the baseline

The result has no currency unit. It measures the proportional change in the input price from that baseline. These fictional examples all cover the same observation period:

Series Starting price Later price Rebased value Price change
EURUSD, USD per EUR 1.1000 1.1110 101 +1%
Gold, USD per troy ounce 2,000 2,060 103 +3%
US stock, USD per share 200 198 99 −1%
Bitcoin, USD per BTC 50,000 52,000 104 +4%

The Bitcoin line ends highest because its input price rose most in percentage terms over this example period. Its larger dollar price has no effect on that ranking. Changing dollars to cents would produce the same indexed line.

An indexed value of 103 is three index points above the 100 baseline, corresponding to a 3% price increase. A move from 103 to 104 is about 0.97%, not 1%.

Give every line the same starting time

A common mistake is dividing each series by its own first available price. If the stock starts on Monday and the crypto series starts on Saturday, both lines begin at 100 but describe different periods.

The helper below finds the earliest row with a value for every selected series. It uses that row as the shared baseline and removes earlier rows. Missing values after that point stay missing.

The input must already contain comparable observations. A timezone-aware index makes timestamps explicit; it does not prove that the underlying prices were observed at the same moment. A daily stock close and a midnight crypto close can share a calendar date while referring to different times. For that preparation step, see aligning stock and crypto prices to the same closing time.

Install the plotting dependencies in your Python environment:

python -m pip install pandas numpy matplotlib
Enter fullscreen mode Exit fullscreen mode
import numpy as np
import pandas as pd


def rebase_to_100(prices):
    if prices.empty:
        raise ValueError("Provide at least one row and one price series")
    if not isinstance(prices.index, pd.DatetimeIndex):
        raise ValueError("Use a DatetimeIndex for observation times")
    if prices.index.tz is None or prices.index.hasnans:
        raise ValueError("Observation times must have a timezone and no NaT")
    if prices.index.has_duplicates or prices.columns.has_duplicates:
        raise ValueError("Resolve duplicate timestamps or series names first")

    values = prices.sort_index().astype("float64")
    if np.isinf(values.to_numpy()).any() or values.le(0).any(axis=None):
        raise ValueError("Observed prices must be positive and finite")

    complete = values.dropna(how="any")
    if complete.empty:
        raise ValueError("No shared baseline has prices for every series")

    baseline_at = complete.index[0]
    baseline = complete.iloc[0]
    indexed = values.loc[baseline_at:].div(baseline, axis="columns").mul(100)
    return indexed, baseline_at
Enter fullscreen mode Exit fullscreen mode

The division matches each baseline price to its column name, as specified by pandas DataFrame.div. The helper uses dropna only to select the baseline. It does not delete every incomplete row from the final chart.

Zero and negative prices are rejected because this chart assumes positive price ratios. Use a different comparison method for a series that can cross zero. Floating-point arithmetic is suitable for this display calculation; this is not a money-settlement function.

Run a four-market example

Run this after the helper. All values below are invented observations at matching times, not historical market quotes or SiftingIO API responses. The labels describe the example series, not a complete instrument specification.

times = pd.to_datetime([
    "2026-10-12T14:00:00Z",
    "2026-10-12T15:00:00Z",
    "2026-10-12T16:00:00Z",
    "2026-10-12T17:00:00Z",
])

prices = pd.DataFrame({
    "EURUSD": [1.1000, 1.1022, 1.1055, 1.1110],
    "Gold": [2000.0, 2020.0, 2010.0, 2060.0],
    "US stock": [200.0, 201.0, 199.0, 198.0],
    "Bitcoin": [50000.0, 51000.0, 50500.0, 52000.0],
}, index=times)

indexed, baseline_at = rebase_to_100(prices)
print(indexed.iloc[-1].round(2).to_dict())
# {'EURUSD': 101.0, 'Gold': 103.0, 'US stock': 99.0, 'Bitcoin': 104.0}
Enter fullscreen mode Exit fullscreen mode

Keep the original prices alongside the indexed values. A tooltip can then show both the actual quote and its change from the baseline. Label the chart's starting time so readers can see which period they are comparing.

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, ax = plt.subplots(figsize=(10, 5), layout="constrained")
for name in indexed.columns:
    ax.plot(indexed.index, indexed[name], marker="o", label=name)

ax.axhline(100, color="gray", linewidth=0.8, linestyle="--")
ax.set_title("Price comparison: fictional example")
ax.set_ylabel("Price index (common baseline = 100)")
ax.set_xlabel(f"UTC; baseline {baseline_at.strftime('%Y-%m-%d %H:%M')}")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M", tz=indexed.index.tz))
ax.legend(frameon=False)
ax.grid(axis="y", alpha=0.2)
fig.savefig("rebased-prices.png", dpi=180)
plt.show()
Enter fullscreen mode Exit fullscreen mode

The saved image should show four lines beginning at 100. Later gaps in an input column remain gaps in that line: Matplotlib breaks lines at NaN values. An entirely absent timestamp needs to be represented in your chosen observation grid before it can appear as a gap. The helper does not build that grid or infer market hours.

Bring your own observations

Keep each instrument, quote currency, interval and price definition alongside its observations. A bar timestamp may label the start of an interval even though its close price becomes known at the end. Verify that convention before turning raw bars into a table of comparable prices.

Prepare one column per series and one agreed observation time per row. Use completed periods. Keep a missing observation empty rather than replacing it with zero or silently carrying a Friday price through the weekend.

If a newly added series begins later, the shared baseline will move later too. Show that date change in the interface rather than presenting the refreshed chart as the same comparison.

What the chart can and cannot tell you

Rebasing makes relative price movement readable. It leaves the meaning of each input unchanged.

Check Why it matters
FX pair direction A rising EURUSD means more USD per EUR. A rising USDJPY means more JPY per USD. The upward lines describe different base currencies.
Quote currency Dividing by a baseline does not convert a EUR-priced asset into USD. Convert consistently if the question is performance in one reporting currency.
Stock adjustments Check whether your stock series is adjusted for splits or dividends. A split in an unadjusted series can create a price discontinuity unrelated to an equivalent loss in shareholder value.
Price return versus total return Dividends, interest, FX carry, fees and trading costs are not added by this calculation. A spot commodity price is also different from a rolling futures investment.
Volatility and risk Starting every series at 100 does not give them equal volatility or make the highest line the best investment.

For this chart, “normalize” means rebasing to a starting value of 100. Min-max scaling and z-scores answer different questions. Keep the transformation, baseline time and price definitions visible, and the comparison becomes much easier to read without changing the underlying data.

Top comments (0)