Pandas 条形图更改日期格式

Posted

技术标签:

【中文标题】Pandas 条形图更改日期格式【英文标题】:Pandas bar plot changes date format 【发布时间】:2015-07-19 22:15:36 【问题描述】:

我有一个简单的堆积线图,它具有我想要在使用以下代码时神奇地设置的日期格式。

df_ts = df.resample("W", how='max')
df_ts.plot(figsize=(12,8), stacked=True)

但是,当绘制与条形图相同的数据时,日期会神秘地转变为丑陋且难以阅读的格式。

df_ts = df.resample("W", how='max')
df_ts.plot(kind='bar', figsize=(12,8), stacked=True)

对原始数据进行了一些转换,以获得每周最大值。为什么自动设置的日期会发生这种根本性的变化?我怎样才能获得上述格式良好的日期?

这是一些虚拟数据

start = pd.to_datetime("1-1-2012")
idx = pd.date_range(start, periods= 365).tolist()
df=pd.DataFrame('A':np.random.random(365), 'B':np.random.random(365))
df.index = idx
df_ts = df.resample('W', how= 'max')
df_ts.plot(kind='bar', stacked=True)

【问题讨论】:

【参考方案1】:

绘图代码假定条形图中的每个条都应有自己的标签。 您可以通过指定自己的格式化程序来覆盖此假设:

ax.xaxis.set_major_formatter(formatter)

Pandas 使用的pandas.tseries.converter.TimeSeries_DateFormatter 当 x 值是日期。但是,对于 条形图,x 值(至少那些 TimeSeries_DateFormatter.__call__) 收到的只是整数开始 为零。如果您尝试将TimeSeries_DateFormatter 与条形图一起使用,则所有标签都从 Epoch 1970-1-1 UTC 开始,因为这是对应于零的日期。所以不幸的是,用于线图的格式化程序对 bar 没用 情节(至少据我所知)。

我认为生成所需格式的最简单方法是显式生成和设置标签:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import matplotlib.ticker as ticker

start = pd.to_datetime("5-1-2012")
idx = pd.date_range(start, periods= 365)
df = pd.DataFrame('A':np.random.random(365), 'B':np.random.random(365))
df.index = idx
df_ts = df.resample('W', how= 'max')

ax = df_ts.plot(kind='bar', x=df_ts.index, stacked=True)

# Make most of the ticklabels empty so the labels don't get too crowded
ticklabels = ['']*len(df_ts.index)
# Every 4th ticklable shows the month and day
ticklabels[::4] = [item.strftime('%b %d') for item in df_ts.index[::4]]
# Every 12th ticklabel includes the year
ticklabels[::12] = [item.strftime('%b %d\n%Y') for item in df_ts.index[::12]]
ax.xaxis.set_major_formatter(ticker.FixedFormatter(ticklabels))
plt.gcf().autofmt_xdate()

plt.show()

产量


对于那些寻找带有日期的条形图的简单示例的人:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

dates = pd.date_range('2012-1-1', '2017-1-1', freq='M')
df = pd.DataFrame('A':np.random.random(len(dates)), 'Date':dates)
fig, ax = plt.subplots()
df.plot.bar(x='Date', y='A', ax=ax)
ticklabels = ['']*len(df)
skip = len(df)//12
ticklabels[::skip] = df['Date'].iloc[::skip].dt.strftime('%Y-%m-%d')
ax.xaxis.set_major_formatter(mticker.FixedFormatter(ticklabels))
fig.autofmt_xdate()

# fixes the tracker
# https://matplotlib.org/users/recipes.html
def fmt(x, pos=0, max_i=len(ticklabels)-1):
    i = int(x) 
    i = 0 if i < 0 else max_i if i > max_i else i
    return dates[i]
ax.fmt_xdata = fmt
plt.show()

【讨论】:

@unutbu 为什么日期从 1970 年 1 月开始?我有一个非常相似的问题(熊猫条形图 xtick 格式),当我尝试使用您的代码时,无论实际日期如何,日期都从 1970 年 1 月开始。我的问题是:***.com/questions/33642388/… @marillion:感谢您指出此错误。在进一步研究了底层的 Pandas/matplotlib 代码之后,我认为自定义条形图刻度标签的最简单方法是明确使用 set_major_formatterFixedFormatter 更好地使用基于长度的步骤show = 6; step = int(len(df_ts.index)/show)然后每一步ticklable显示月、日和年ticklabels[::step] = [item.strftime('%b %d\n%Y') for item indf_ts.index[::step]] 这应该是一种更简单的方法(不那么冗长)来完成这个。【参考方案2】:

我也一直在为这个问题苦苦挣扎,在阅读了几篇帖子后提出了以下解决方案,在我看来这比 matplotlib.dates 的方法更清晰。

未经修改的标签:

# Use DatetimeIndex instead of date_range for pandas earlier than 1.0.0 version 
timeline = pd.date_range(start='2018, November', freq='M', periods=15)
df = pd.DataFrame('date': timeline, 'value': np.random.randn(15))
df.set_index('date', inplace=True)
df.plot(kind='bar', figsize=(12, 8), color='#2ecc71')

有修改的标签:

def line_format(label):
    """
    Convert time label to the format of pandas line plot
    """
    month = label.month_name()[:3]
    if month == 'Jan':
        month += f'\nlabel.year'
    return month

# Note that we specify rot here
ax = df.plot(kind='bar', figsize=(12, 8), color='#2ecc71', rot=0)
ax.set_xticklabels(map(line_format, df.index))

这种方法只会在标签中添加年份,如果它是一月

【讨论】:

【参考方案3】:

这是一个简单的方法,使用 pandas plot()不使用 matplotlib dates

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# generate sample data
start = pd.to_datetime("1-1-2012")
index = pd.date_range(start, periods= 365)
df = pd.DataFrame('A' : np.random.random(365), 'B' : np.random.random(365), index=index)

# resample to any timeframe you need, e.g. months
df_months = df.resample("M").sum()

# plot
fig, ax = plt.subplots()
df_months.plot(kind="bar", figsize=(16,5), stacked=True, ax=ax)

# format xtick-labels with list comprehension
ax.set_xticklabels([x.strftime("%Y-%m") for x in df_months.index], rotation=45)
plt.show()

【讨论】:

【参考方案4】:

如何获得格式良好的日期,如熊猫线图

问题在于pandas bar plot 将日期变量作为分类变量处理,其中每个日期都被认为是一个唯一的类别,因此 x 轴单位设置为从 0 开始的整数(就像默认的 DataFrame 索引时none 被分配)并且每个日期的完整字符串都显示出来,没有任何自动格式化。

这里有两种解决方案来格式化时间序列的 pandas(堆叠)条形图的日期刻度标签:

    第一个是answer by unutbu 的变体,旨在更好地拟合问题中显示的数据; 第二个是通用解决方案,可让您使用 matplotlib 日期刻度定位器和格式化程序,为任何频率类型的时间序列生成适当的日期标签。

但首先,让我们看看使用 pandas 线图绘制样本数据时格式良好的刻度标签是什么样的。

默认熊猫线图日期格式

import numpy as np                 # v 1.19.2
import pandas as pd                # v 1.1.3
import matplotlib.dates as mdates  # v 3.3.2

# Create sample dataset with a daily frequency and resample it to a weekly frequency
rng = np.random.default_rng(seed=123) # random number generator
idx = pd.date_range(start='2012-01-01', end='2013-12-31', freq='D')
df_raw = pd.DataFrame(rng.random(size=(idx.size, 3)),
                      index=idx, columns=list('ABC'))
df = df_raw.resample('W').sum()  # default is 'W-SUN'

# Create pandas stacked line plot
ax = df.plot(stacked=True, figsize=(10,5))

由于数据按星期分组,带有星期日的时间戳(频率 W-SUN),每月刻度标签不一定放在每月的第一天,每个第一周之间可能有 3 或 4 周月,因此小刻度线的间距不均匀(如果仔细观察会发现)。以下是主要刻度的确切日期:

# Convert major x ticks to date labels
np.array([mdates.num2date(tick*7-4).strftime('%Y-%b-%d') for tick in ax.get_xticks()])

"""
array(['2012-Jan-01', '2012-Apr-01', '2012-Jul-01', '2012-Oct-07',
       '2013-Jan-06', '2013-Apr-07', '2013-Jul-07', '2013-Oct-06',
       '2014-Jan-05'], dtype='<U11')
"""

挑战在于为每个月的第一周选择刻度,因为它们的间距不相等。其他答案提供了基于固定刻度频率的简单解决方案,这会产生奇怪的间隔标签有时可以重复月份的日期(例如 unutbu 回答中的 7 月份)。或者他们提供了基于每月时间序列而不是每周时间序列的解决方案,这更容易格式化,因为每年总是有 12 个月。 所以这里有一个解决方案,它可以提供格式良好的刻度标签,就像 pandas 线图中一样,并且适用于任何频率的数据。

解决方案 1:基于 DatetimeIndex 的带有刻度标签的 pandas 条形图

# Create pandas stacked bar chart
ax = df.plot.bar(stacked=True, figsize=(10,5))

# Create list of monthly timestamps by selecting the first weekly timestamp of each
# month (in this example, the first Sunday of each month)
monthly_timestamps = [timestamp for idx, timestamp in enumerate(df.index)
                      if (timestamp.month != df.index[idx-1].month) | (idx == 0)]

# Automatically select appropriate number of timestamps so that x-axis does
# not get overcrowded with tick labels
step = 1
while len(monthly_timestamps[::step]) > 10: # increase number if time range >3 years
    step += 1
timestamps = monthly_timestamps[::step]

# Create tick labels from timestamps
labels = [ts.strftime('%b\n%Y') if ts.year != timestamps[idx-1].year
          else ts.strftime('%b') for idx, ts in enumerate(timestamps)]

# Set major ticks and labels
ax.set_xticks([df.index.get_loc(ts) for ts in timestamps])
ax.set_xticklabels(labels)

# Set minor ticks without labels
ax.set_xticks([df.index.get_loc(ts) for ts in monthly_timestamps], minor=True)

# Rotate and center labels
ax.figure.autofmt_xdate(rotation=0, ha='center')

据我所知,使用 matplotlib.dates (mdates) 刻度定位器和格式化程序无法获得这种精确的标签格式。不过,如果您更喜欢使用刻度定位器/格式化程序,或者如果您希望在使用 matplotlib 的交互式界面(平移/放大和缩小)时拥有动态刻度,则将 mdates 功能与 pandas 堆叠条形图相结合会派上用场。

此时,考虑直接在 matplotlib 中创建堆积条形图可能很有用,您需要在其中循环变量以创建堆积条形图。下面显示的基于 pandas 的解决方案通过循环遍历条形块的补丁来根据 matplotlib 日期单位重新定位它们。所以它基本上是一个循环而不是另一个循环,由你来看看哪个更方便。

解决方案 2:使用 matplotlib 刻度定位器和格式化程序的 pandas 条形图

此通用解决方案使用 mdates AutoDateLocator 将刻度放在月/年的开头。如果您在 pandas 中使用pd.date_range 生成数据和时间戳(如本例所示),您应该记住,常用的'M''Y' 频率会为周期的结束日期生成时间戳。以下示例中给出的代码将每月/每年的刻度线与 'MS''YS' 频率对齐。

如果您使用期末日期(或 some other type of pandas frequency 未与 AutoDateLocator 刻度对齐)导入数据集,我不知道有任何方便的方法可以相应地移动 AutoDateLocator 以便标签正确对齐酒吧。我看到两个选项:i)如果这不会导致有关基础数据含义的任何问题,则使用 df.resample('MS').sum() 重新采样数据; ii) 或者使用另一个日期定位器。

这个问题在以下示例中没有问题,因为数据的周末频率为'W-SUN',因此以月/年开始频率放置的月/年标签很好。

# Create pandas stacked bar chart with the default bar width = 0.5
ax = df.plot.bar(stacked=True, figsize=(10,5))

# Compute width of bars in matplotlib date units, 'md' (in days) and adjust it if
# the bar width in df.plot.bar has been set to something else than the default 0.5
bar_width_md_default, = np.diff(mdates.date2num(df.index[:2]))/2
bar_width = ax.patches[0].get_width()
bar_width_md = bar_width*bar_width_md_default/0.5

# Compute new x values in matplotlib date units for the patches (rectangles) that
# make up the stacked bars, adjusting the positions according to the bar width:
# if the frequency is in months (or years), the bars may not always be perfectly
# centered over the tick marks depending on the number of days difference between
# the months (or years) given by df.index[0] and [1] used to compute the bar 
# width, this should not be noticeable if the bars are wide enough.
x_bars_md = mdates.date2num(df.index) - bar_width_md/2
nvar = len(ax.get_legend_handles_labels()[1])
x_patches_md = np.ravel(nvar*[x_bars_md])

# Set bars to new x positions and adjust width: this loop works fine with NaN
# values as well because in bar plot NaNs are drawn with a rectangle of 0 height
# located at the foot of the bar, you can verify this with patch.get_bbox()
for patch, x_md in zip(ax.patches, x_patches_md):
    patch.set_x(x_md)
    patch.set_width(bar_width_md)

# Set major ticks
maj_loc = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(maj_loc)

# Show minor tick under each bar (instead of each month) to highlight
# discrepancy between major tick locator and bar positions seeing as no tick
# locator is available for first-week-of-the-month frequency
ax.set_xticks(x_bars_md + bar_width_md/2, minor=True)

# Set major tick formatter
zfmts = ['', '%b\n%Y', '%b', '%b-%d', '%H:%M', '%H:%M']
fmt = mdates.ConciseDateFormatter(maj_loc, zero_formats=zfmts, show_offset=False)
ax.xaxis.set_major_formatter(fmt)

# Shift the plot frame to where the bars are now located
xmin = min(x_bars_md) - bar_width_md
xmax = max(x_bars_md) + 2*bar_width_md
ax.set_xlim(xmin, xmax)

# Adjust tick label format last, else it may sometimes not be applied correctly
ax.figure.autofmt_xdate(rotation=0, ha='center')

在每个条形下方显示的小刻度 a 以突出显示条形的时间戳通常与 AutoDateLocator 刻度标签标记的月/年开始不一致的事实。我不知道有任何日期定位器可用于选择每个月第一周的刻度并准确重现解决方案 1 中显示的结果。


文档:date format codes、mdates.ConciseDateFormatter

【讨论】:

【参考方案5】:

这是使用mdates 的一种可能更简单的方法,但需要您遍历列,从 matplotlib 调用条形图。这是一个示例,我仅绘制一列并将 mdates 用于自定义刻度和标签(EDIT 添加了循环功能以绘制所有堆叠的列):

import datetime
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

def format_x_date_month_day(ax):   
    # Standard date x-axis formatting block, labels each month and ticks each day
    days = mdates.DayLocator()
    months = mdates.MonthLocator()  # every month
    dayFmt = mdates.DateFormatter('%D')
    monthFmt = mdates.DateFormatter('%Y-%m')
    ax.figure.autofmt_xdate()
    ax.xaxis.set_major_locator(months) 
    ax.xaxis.set_major_formatter(monthFmt)
    ax.xaxis.set_minor_locator(days)

def df_stacked_bar_formattable(df, ax, **kwargs):
    P = []
    lastBar = None

    for col in df.columns:
        X = df.index
        Y = df[col]
        if lastBar is not None:
            P.append(ax.bar(X, Y, bottom=lastBar, **kwargs))
        else:
            P.append(ax.bar(X, Y, **kwargs))
        lastBar = Y
    plt.legend([p[0] for p in P], df.columns)

span_days = 90
start = pd.to_datetime("1-1-2012")
idx = pd.date_range(start, periods=span_days).tolist()
df=pd.DataFrame(index=idx, data='A':np.random.random(span_days), 'B':np.random.random(span_days))

plt.close('all')
fig, ax = plt.subplots(1)
df_stacked_bar_formattable(df, ax)
format_x_date_month_day(ax)
plt.show()

(引用 matplotlib.org 以循环创建堆积条形图的示例。)这给了我们

应该工作且更容易的另一种方法是使用df.plot.bar(ax=ax, stacked=True),但它不允许使用mdates 进行日期轴格式设置,并且是my question 的主题。

【讨论】:

要允许超过 2 个堆叠条,在 if 语句中将 lastBar 编辑为 'lastBar = Y + lastBar',在 else 中编辑 lastBar = Y。 不错的解决方案!确保将 width 参数传递给 df_stacked_bar_formattable 修改条的宽度。这可以作为增强功能自动化。【参考方案6】:

也许不是最优雅的,但希望是最简单的方法:

fig = plt.figure() 
ax = fig.add_subplot(111)

df_ts.plot(kind='bar', figsize=(12,8), stacked=True,ax=ax)
ax.set_xticklabels(''*len(df_ts.index))

df_ts.plot(linewidth=0, ax=ax)  # This sets the nice x_ticks automatically

[编辑]: df_ts.plot() 中需要 ax=ax

【讨论】:

在我看来,新图覆盖了条形图,我看到了很好的 x_tick 标签但没有数据。还需要在绘图调用中设置 (ax=ax)。 感谢修复,忘记设置 df_ts.plot(kind='bar', figsize=(12,8), stacked=True, ax=ax) 似乎对我有用。我在 jupyter 和 python 3 中使用它。 肯定不行。我和@user4815162342在同一条船上

以上是关于Pandas 条形图更改日期格式的主要内容,如果未能解决你的问题,请参考以下文章

更改 Pandas 中特定单元格的日期格式

在 pandas 中创建一个条形图,x 轴为日期,另一列中的每个值一个条形图

如何更改 Pandas 数据框中的日期格式? [复制]

Pandas:按日历周分组,然后为实际日期时间绘制分组条形图

Seaborn 条形图中 X 轴上的排序和格式化日期

将日期时间格式的索引转换为仅限日期的python pandas