Search code examples
pythonpandasmatplotlibbar-chart

Showing one x tick per month on a pandas plot


I have a time series with daily data, that I want to plot, and only plot a x-tick every month. I have tried multiple approaches (including the ones described here, but it seems that pandas considers my data from 1970, even if the date index is from another date. Example below:

date_range = pd.date_range(start='2023-01-01', periods=periods)
data = {
    'A': np.random.randint(1, 10, periods),
    'B': np.random.randint(1, 10, periods),
}
df = pd.DataFrame(data, index=date_range)
df

df['total'] = df.sum(axis=1)
df['rolling'] = df['total'].rolling(3).mean()
ax = df[['A', 'B']].plot(kind='bar', stacked=True)

# this one defaults to 1970—which seems to be a known problem
# https://stackoverflow.com/questions/69101233/using-dateformatter-resets-starting-date-to-1970
# ax.xaxis.set_major_locator(MonthLocator())
# ax.xaxis.set_major_formatter(DateFormatter('%b %Y'))

# this one also doesn't work, because the data is thought to be also from 1970 so the picture comes out very wrong
monthly_ticks = pd.date_range(start=df.index.min(), end=df.index.max(), freq='ME')
ax.set_xticks(monthly_ticks)
ax.set_xticklabels(monthly_ticks, rotation=45);```

Solution

  • The issue stems from Pandas struggling to correctly interpret your date format (yyyy-mm-dd). There's a possibility that adopting a UNIX-timestamp-ish format could work since that seems to be the default behavior (defaulting to 1st of January 1970).

    Regardless, reading from the dataframe and directly using ax.bar() (as opposed to df.plot()/df[<some_data>].plot()) to create your stacked bar plots gives you the correct monthly x-ticks.


    I've used random y-axis data across the whole year of 2023 (365 data points each for A and B). I've also slightly rotated the ticks to prevent textual overlap.

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.dates import DateFormatter, MonthLocator
    
    periods = 365  # example for one year of daily data
    date_range = pd.date_range(start='2023-01-01', periods=periods)
    data = {
        'A': np.random.randint(1, 10, periods),
        'B': np.random.randint(1, 10, periods),
    }
    df = pd.DataFrame(data, index=date_range)
    
    df['total'] = df.sum(axis=1)
    df['rolling'] = df['total'].rolling(3).mean()
    
    # Favor plt.bar over df.plot
    fig, ax = plt.subplots(figsize=(10, 6))
    ax.bar(df.index, df['A'], label='A', width=1)
    ax.bar(df.index, df['B'], bottom=df['A'], label='B', width=1)
    
    # Month interval formatting
    ax.xaxis.set_major_locator(MonthLocator())
    ax.xaxis.set_major_formatter(DateFormatter('%b %Y'))
    
    # Slight rotation
    plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
    
    ax.legend()
    plt.show()
    

    Output:

    Stacked Monthly Ticks