熊猫条形图更改日期格式

时间:2015-05-08 21:40:58

标签: pandas matplotlib plot

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

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

enter image description here

然而,当绘制与条形图相同的数据时,日期会神秘地将自身转换为丑陋且难以理解的格式。

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)

enter image description here

将原始数据转换为每周最大值。为什么自动设定日期发生了根本变化?我怎样才能拥有如上所述的格式良好的日期?

这是一些虚拟数据

  date        store_nbr      units            preciptotal
  2014-10-11       1          0                0.00
  2014-10-12       1          0                0.01
  2014-10-13       1          2                0.00
  2014-10-14       1          1                2.13
  2014-10-15       1          0                0.00
  2014-10-16       1          0                0.87
  2014-10-17       1          3                0.01
  2014-10-18       1          0                0.40

6 个答案:

答案 0 :(得分:42)

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

ax.xaxis.set_major_formatter(formatter)

Pandas使用的pandas.tseries.converter.TimeSeries_DateFormatter 格式化“好”图中的日期适用于线图 x值是日期。但是,使用条形图 x值(至少是那些 由TimeSeries_DateFormatter.__call__收到的只是整数开始 在零。如果您尝试将TimeSeries_DateFormatter与条形图一起使用,则所有标签都从大约1970-1-1 UTC开始,因为这是对应于零的日期。所以用于线图的格式化器很遗憾用于吧 情节(至少就我所见)。

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

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()

的产率 enter image description here

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

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()

enter image description here

答案 1 :(得分:5)

我也一直在努力解决这个问题,在阅读了几篇文章之后,提出了以下解决方案,在我看来,这种解决方案比matplotlib.dates方法更清晰。

未修改的标签:

timeline = pd.DatetimeIndex(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')

enter image description here

带有修改的标签:

def line_format(label):
    """
    Convert time label to the format of pandas line plot
    """
    month = label.month_name()[:3]
    if month == 'Jan':
        month += f'\n{label.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(lambda x: line_format(x), df.index))

enter image description here

这种方法只会在年份为一月时才在标签上加上年份

答案 2 :(得分:2)

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

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

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

  1. 第一个是 answer by unutbu 的变体,用于更好地拟合问题中显示的数据;
  2. 第二种是通用解决方案,可让您使用 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))

pd_stackedlines

因为数据是按星期分组的,星期天有时间戳(频率 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 的答案中的七月)。或者他们提供了基于每月时间序列而不是每周时间序列的解决方案,因为每年总是有 12 个月,所以格式更简单。 所以这里有一个解决方案,它提供格式很好的刻度标签,就像熊猫线图一样,适用于任何频率的数据。


解决方案 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')

pd_stackedbars_strftime

据我所知,使用 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')

pd_stackedbars_mdates

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



文档:date format codesmdates.ConciseDateFormatter

答案 3 :(得分:1)

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

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作为循环的示例以创建堆积条形图。)这给了我们

enter image description here

另一种 工作且更容易使用的方法是使用df.plot.bar(ax=ax, stacked=True),但它不允许使用mdates格式化日期轴格式,并且是{{3}的主题}}

答案 4 :(得分:1)

这是一种简单的方法,使用熊猫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()

enter image description here

答案 5 :(得分:0)

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

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

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

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