如何绘制分组的多个条形图

2025-02-18 09:25:00
admin
原创
38
摘要:问题描述:如何在 matplotlib 中绘制多个条形图,当我尝试多次调用条形图函数时,它们会重叠,如下图所示,只能看到最高值红色。如何在 x 轴上绘制带有日期的多个条形图?到目前为止,我尝试过这个:import matplotlib.pyplot as plt import datetime x = [ ...

问题描述:

如何在 matplotlib 中绘制多个条形图,当我尝试多次调用条形图函数时,它们会重叠,如下图所示,只能看到最高值红色。如何在 x 轴上绘制带有日期的多个条形图?

到目前为止,我尝试过这个:

import matplotlib.pyplot as plt
import datetime

x = [
    datetime.datetime(2011, 1, 4, 0, 0),
    datetime.datetime(2011, 1, 5, 0, 0),
    datetime.datetime(2011, 1, 6, 0, 0)
]
y = [4, 9, 2]
z = [1, 2, 3]
k = [11, 12, 13]

ax = plt.subplot(111)
ax.bar(x, y, width=0.5, color='b', align='center')
ax.bar(x, z, width=0.5, color='g', align='center')
ax.bar(x, k, width=0.5, color='r', align='center')
ax.xaxis_date()

plt.show()

我得到了这个:

在此处输入图片描述

结果应该是这样的,但是日期在 x 轴上并且条形彼此相邻:

在此处输入图片描述


解决方案 1:

import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import datetime

x = [
    datetime.datetime(2011, 1, 4, 0, 0),
    datetime.datetime(2011, 1, 5, 0, 0),
    datetime.datetime(2011, 1, 6, 0, 0)
]
x = date2num(x)

y = [4, 9, 2]
z = [1, 2, 3]
k = [11, 12, 13]

ax = plt.subplot(111)
ax.bar(x-0.2, y, width=0.2, color='b', align='center')
ax.bar(x, z, width=0.2, color='g', align='center')
ax.bar(x+0.2, k, width=0.2, color='r', align='center')
ax.xaxis_date()

plt.show()

在此处输入图片描述

我不知道“y 值也重叠”是什么意思,下面的代码能解决你的问题吗?

ax = plt.subplot(111)
w = 0.3
ax.bar(x-w, y, width=w, color='b', align='center')
ax.bar(x, z, width=w, color='g', align='center')
ax.bar(x+w, k, width=w, color='r', align='center')
ax.xaxis_date()
ax.autoscale(tight=True)

plt.show()

在此处输入图片描述

解决方案 2:

使用日期作为 x 值的问题在于,如果您想要像第二张图片中那样的条形图,它们会出错。您应该使用堆叠条形图(颜色相互叠加)或按日期分组(x 轴上的“假”日期,基本上只是对数据点进行分组)。

import numpy as np
import matplotlib.pyplot as plt

N = 3
ind = np.arange(N)  # the x locations for the groups
width = 0.27       # the width of the bars

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

yvals = [4, 9, 2]
rects1 = ax.bar(ind, yvals, width, color='r')
zvals = [1,2,3]
rects2 = ax.bar(ind+width, zvals, width, color='g')
kvals = [11,12,13]
rects3 = ax.bar(ind+width*2, kvals, width, color='b')

ax.set_ylabel('Scores')
ax.set_xticks(ind+width)
ax.set_xticklabels( ('2011-Jan-4', '2011-Jan-5', '2011-Jan-6') )
ax.legend( (rects1[0], rects2[0], rects3[0]), ('y', 'z', 'k') )

def autolabel(rects):
    for rect in rects:
        h = rect.get_height()
        ax.text(rect.get_x()+rect.get_width()/2., 1.05*h, '%d'%int(h),
                ha='center', va='bottom')

autolabel(rects1)
autolabel(rects2)
autolabel(rects3)

plt.show()

在此处输入图片描述

解决方案 3:

在寻找类似的解决方案但未找到足够灵活的解决方案后,我决定编写自己的函数。它允许您在每个组中设置任意数量的条形,并指定组的宽度以及组内条形的单个宽度。

享受:

from matplotlib import pyplot as plt


def bar_plot(ax, data, colors=None, total_width=0.8, single_width=1, legend=True):
    """Draws a bar plot with multiple bars per data point.

    Parameters
    ----------
    ax : matplotlib.pyplot.axis
        The axis we want to draw our plot on.

    data: dictionary
        A dictionary containing the data we want to plot. Keys are the names of the
        data, the items is a list of the values.

        Example:
        data = {
            "x":[1,2,3],
            "y":[1,2,3],
            "z":[1,2,3],
        }

    colors : array-like, optional
        A list of colors which are used for the bars. If None, the colors
        will be the standard matplotlib color cyle. (default: None)

    total_width : float, optional, default: 0.8
        The width of a bar group. 0.8 means that 80% of the x-axis is covered
        by bars and 20% will be spaces between the bars.

    single_width: float, optional, default: 1
        The relative width of a single bar within a group. 1 means the bars
        will touch eachother within a group, values less than 1 will make
        these bars thinner.

    legend: bool, optional, default: True
        If this is set to true, a legend will be added to the axis.
    """

    # Check if colors where provided, otherwhise use the default color cycle
    if colors is None:
        colors = plt.rcParams['axes.prop_cycle'].by_key()['color']

    # Number of bars per group
    n_bars = len(data)

    # The width of a single bar
    bar_width = total_width / n_bars

    # List containing handles for the drawn bars, used for the legend
    bars = []

    # Iterate over all data
    for i, (name, values) in enumerate(data.items()):
        # The offset in x direction of that bar
        x_offset = (i - n_bars / 2) * bar_width + bar_width / 2

        # Draw a bar for every value of that type
        for x, y in enumerate(values):
            bar = ax.bar(x + x_offset, y, width=bar_width * single_width, color=colors[i % len(colors)])

        # Add a handle to the last drawn bar, which we'll need for the legend
        bars.append(bar[0])

    # Draw legend if we need
    if legend:
        ax.legend(bars, data.keys())


if __name__ == "__main__":
    # Usage example:
    data = {
        "a": [1, 2, 3, 2, 1],
        "b": [2, 3, 4, 3, 1],
        "c": [3, 2, 1, 4, 2],
        "d": [5, 9, 2, 1, 8],
        "e": [1, 3, 2, 2, 3],
        "f": [4, 3, 1, 1, 4],
    }

    fig, ax = plt.subplots()
    bar_plot(ax, data, total_width=.8, single_width=.9)
    plt.show()

输出:

在此处输入图片描述

解决方案 4:

我知道这是关于的matplotlib,但是使用pandasseaborn可以节省您很多时间:

df = pd.DataFrame(zip(x*3, ["y"]*3+["z"]*3+["k"]*3, y+z+k), columns=["time", "kind", "data"])
plt.figure(figsize=(10, 6))
sns.barplot(x="time", hue="kind", y="data", data=df)
plt.show()

在此处输入图片描述

解决方案 5:

  • 鉴于现有的答案和 OP 中的数据,最简单的解决方案是将数据加载到数据框中并用 绘制pandas.DataFrame.plot

    • 使用 加载值列表到 pandas 中dict,并指定x为索引。索引将自动设置为 x 轴,列将绘制为条形图。

    • pandas.DataFrame.plot用作matplotlib默认后端。

  • 有关使用的详细信息,请参阅如何在条形图上添加值标签.bar_label

  • 已在python 3.12.0pandas 2.2.1、进行测试matplotlib 3.8.1

import pandas as pd
from datetime import datetime

# data
x = [datetime(2011, 1, 4, 0, 0), datetime(2011, 1, 5, 0, 0), datetime(2011, 1, 6, 0, 0)]
y = [4, 9, 2]
z = [1, 2, 3]
k = [11, 12, 13]

# using the existing lists from the OP, create the dataframe
df = pd.DataFrame(data={'y': y, 'z': z, 'k': k}, index=x)

# since there's no time component and x was a datetime dtype, set the index to be just the date
df.index = df.index.date

# display(df)
            y  z   k
2011-01-04  4  1  11
2011-01-05  9  2  12
2011-01-06  2  3  13

# plot bars or kind='barh' for horizontal bars; adjust figsize accordingly
ax = df.plot(kind='bar', rot=0, xlabel='Date', ylabel='Value', title='My Plot', figsize=(6, 4))

# add some labels
for c in ax.containers:
    # set the bar label
    ax.bar_label(c, fmt='%.0f', label_type='edge')
    
# add a little space at the top of the plot for the annotation
ax.margins(y=0.1)

# move the legend out of the plot
ax.legend(title='Columns', bbox_to_anchor=(1, 0.5), loc='center left', frameon=False)

在此处输入图片描述

  • 当列数较多时使用水平条

ax = df.plot(kind='barh', ylabel='Date', title='My Plot', figsize=(5, 4))
ax.set(xlabel='Value')
for c in ax.containers:
    # set the bar label
    ax.bar_label(c, fmt='%.0f', label_type='edge')
    
ax.margins(x=0.1)

# move the legend out of the plot
ax.legend(title='Columns', bbox_to_anchor=(1, 0.5), loc='center left', frameon=False)

在此处输入图片描述

解决方案 6:

我修改了 pascscha 的解决方案并扩展了界面,希望这能帮助到其他人!主要特点:

  • 每组条形图的条目数量可变

  • 可定制颜色

  • 处理 x 刻度

  • 完全可定制的条形标签位于条形顶部

def bar_plot(ax, data, group_stretch=0.8, bar_stretch=0.95,
             legend=True, x_labels=True, label_fontsize=8,
             colors=None, barlabel_offset=1,
             bar_labeler=lambda k, i, s: str(round(s, 3))):
    """
    Draws a bar plot with multiple bars per data point.
    :param dict data: The data we want to plot, where keys are the names of each
      bar group, and items is a list of bar values for the corresponding group.
    :param float group_stretch: 1 means groups occupy the most (largest groups
      touch side to side if they have equal number of bars).
    :param float bar_stretch: If 1, bars within a group will touch side to side.
    :param bool x_labels: If true, x-axis will contain labels with the group
      names given at data, centered at the bar group.
    :param int label_fontsize: Font size for the label on top of each bar.
    :param float barlabel_offset: Distance, in y-values, between the top of the
      bar and its label.
    :param function bar_labeler: If not None, must be a functor with signature
      ``f(group_name, i, scalar)->str``, where each scalar is the entry found at
      data[group_name][i]. When given, returns a label to put on the top of each
      bar. Otherwise no labels on top of bars.
    """
    sorted_data = list(sorted(data.items(), key=lambda elt: elt[0]))
    sorted_k, sorted_v  = zip(*sorted_data)
    max_n_bars = max(len(v) for v in data.values())
    group_centers = np.cumsum([max_n_bars
                               for _ in sorted_data]) - (max_n_bars / 2)
    bar_offset = (1 - bar_stretch) / 2
    bars = collections.defaultdict(list)
    #
    if colors is None:
        colors = {g_name: [f"C{i}" for _ in values]
                  for i, (g_name, values) in enumerate(data.items())}
    #
    for g_i, ((g_name, vals), g_center) in enumerate(zip(sorted_data,
                                                         group_centers)):
        n_bars = len(vals)
        group_radius = group_stretch * (n_bars - bar_stretch) * 0.5
        print("!!!!", vals, n_bars)
        group_beg = g_center - group_radius
        for val_i, val in enumerate(vals):
            bar = ax.bar(group_beg + (val_i + bar_offset) * group_stretch,
                         height=val, width=bar_stretch * group_stretch,
                         color=colors[g_name][val_i])[0]
            bars[g_name].append(bar)
            if  bar_labeler is not None:
                x_pos = bar.get_x() + (bar.get_width() / 2.0)
                y_pos = val + barlabel_offset
                barlbl = bar_labeler(g_name, val_i, val)
                ax.text(x_pos, y_pos, barlbl, ha="center", va="bottom",
                        fontsize=label_fontsize)
    if legend:
        ax.legend([bars[k][0] for k in sorted_k], sorted_k)
    #
    ax.set_xticks(group_centers)
    if x_labels:
        ax.set_xticklabels(sorted_k)
    else:
        ax.set_xticklabels()
    return bars, group_centers

示例运行:

fig, ax = plt.subplots()
data = {"Foo": [1, 2, 3, 4], "Zap": [0.1, 0.2], "Quack": [6], "Bar": [1.1, 2.2, 3.3, 4.4, 5.5]}
bar_plot(ax, data, group_stretch=0.8, bar_stretch=0.95, legend=True,
         labels=True, label_fontsize=8, barlabel_offset=0.05,
         bar_labeler=lambda k, i, s: str(round(s, 3)))
fig.show()

在此处输入图片描述

解决方案 7:

我做了这个解决方案:如果您想在一个图中绘制多个图,请确保在绘制下一个图之前已经设置正确 matplotlib.pyplot.hold(True)
以便能够添加另一个图。

关于 X 轴上的日期时间值,使用条形对齐的解决方案对我来说很有效。当您使用 创建另一个条形图时matplotlib.pyplot.bar(),只需使用align='edge|center'并设置width='+|-distance'

当您正确设置所有条形图(图)时,您将能够正常看到条形图。

解决方案 8:

动机

我有自己的数据pd.DataFrame,但对这里提出的解决方案并不满意。

此解决方案

  • 使用 DataFrame 中的日期时间列

  • 每个日期允许多次输入或零次输入

  • 将数据点集中在日期刻度周围

  • 不会扭曲 x 轴上的时间

按天分组的多个条形图示例

生成虚拟数据

让我们首先生成一些 DataFrame,其中包含一个日期时间列和另一个包含我们想要绘制的数据的列。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import datetime as dt
# Initialize rng
rng = np.random.default_rng(seed=1000)

# Create y values
ys = rng.integers(1, 5, size=30)

# Period
dates = pd.date_range(dt.datetime(2023,11,6), dt.datetime(2023,11,16))
dates = dates.to_pydatetime().tolist()

# Randomly assign dates to ys
rnd_dates = []
for _ in ys:
    index = rng.integers(0, len(dates)-1)
    rnd_dates.append(dates[index])

# Create dummy DataFrame
df = pd.DataFrame(dict(datetime=rnd_dates, ys=ys))

虚拟 DataFramedf如下所示:

     datetime  ys
0  2023-11-07   1
1  2023-11-08   3
2  2023-11-10   4
3  2023-11-08   3
4  2023-11-15   4
...
26 2023-11-07   3
27 2023-11-15   4
28 2023-11-06   1
29 2023-11-13   3

解决方案

我们的想法是,在绘制数据之前,先过滤每个日期的数据。这样我们就可以计算每个条形的单独偏移量,以使其围绕刻度线居中。

参数

  • BAR_WIDTH指定条形的宽度

  • BAR_DIST指定两个条之间的距离

如果每天的数据点过多,条形图就会重叠。可以通过调整这两个参数来解决这个问题。

# Constants for varying the spacing
BAR_WIDTH = 0.12
BAR_DIST = 0.03

# Create figure
fig = plt.figure()
ax = plt.axes()

# Plotting period as datetime list
dates = pd.date_range(dt.datetime(2023,11,6), dt.datetime(2023,11,16))
dates = dates.to_pydatetime().tolist()

# Iterate over every date
for jj, date in enumerate(dates):

    # Get data for certain date
    day_data = df[ df["datetime"].dt.date == date.date() ]

    # Maximal offset per day
    max_offset = (BAR_WIDTH + BAR_DIST) * (len(day_data)-1)

    # Bar index
    bar_ii = 0

    # Plot all bars for one group
    for _, row in day_data.iterrows():

        # Calculate offset
        offset = (BAR_WIDTH + BAR_DIST)* bar_ii - max_offset/2

        ax.bar(jj+offset, row["ys"], color="C0", width=BAR_WIDTH)
        bar_ii += 1

# Create proper ticklabels
lbs = [date.strftime("%d.%m.%y") for date in dates]
ticks = np.arange(0, len(dates))
ax.set_xticks(ticks, lbs, rotation=45, ha='right', rotation_mode='anchor')

plt.show()
plt.close(fig)

解决方案 9:

受@pascscha的原始回答的启发,我对他的函数做了一些细微的修改,因此不需要所有x刻度都具有相同数量的条形。也就是说,我们不再假设每个条形图具有相同数量的条形图,并且以下函数通过将每个条形图置于其分配的条形图数量的x相应位置来处理这个问题。x

from matplotlib import pyplot as plt


def bar_plot(ax, data, colors=None, total_width=0.8, single_width=1, legend=True):
    """Draws a bar plot with multiple bars per data point.

    Parameters
    ----------
    ax : matplotlib.pyplot.axis
        The axis we want to draw our plot on.

    data: dictionary
        A dictionary containing the data we want to plot. Keys are the names of the
        data, the items is a list of the values.

        Example:
    data = {
        "x":[1,2,3],
        "y":[1,2,3],
        "z":[1,2,3],
    }
If there is a `None` value in the list, the bar will be missing for the corresponding `x` and the remaining bars
will be centered around the x tick.

    colors : array-like, optional
A list of colors which are used for the bars. If None, the colors
will be the standard matplotlib color cyle. (default: None)

    total_width : float, optional, default: 0.8
The width of a bar group. 0.8 means that 80% of the x-axis is covered
by bars and 20% will be spaces between the bars.

    single_width: float, optional, default: 1
The relative width of a single bar within a group. 1 means the bars
will touch eachother within a group, values less than 1 will make
these bars thinner.

    legend: bool, optional, default: True
If this is set to true, a legend will be added to the axis.
    """

    # Check if colors where provided, otherwhise use the default color cycle
    if colors is None:
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]

    # Number of bars per group
    n_bars = len(data)

    # The width of a single bar
    bar_width = total_width / n_bars

    # List containing handles for the drawn bars, used for the legend
    bars = []

    # Build a bars_per_x dictionary depending on the number of values that are not None
    bars_per_x = {}
    for _, values_list in data.items():
for i, value in enumerate(values_list):
    if value is not None:
        if i not in bars_per_x:
            bars_per_x[i] = 0
        bars_per_x[i] += 1

    # Instead of using i in calculating the offset, we now use the i_per_x[x]
    i_per_x = {}

    # Iterate over all data
    for i, (name, values) in enumerate(data.items()):
# Draw a bar for every value of that type
for x, y in enumerate(values):
    if x not in i_per_x:
        i_per_x[x] = 0

    if y is not None:
        # The offset in x direction of that bar
        x_offset = (i_per_x[x] - bars_per_x[x] / 2) * bar_width + bar_width / 2
        bar = ax.bar(x + x_offset, y, width=bar_width * single_width, color=colors[i % len(colors)])
        i_per_x[x] += 1

# Add a handle to the last drawn bar, which we'll need for the legend
bars.append(bar[0])

    # Draw legend if we need
    if legend:
ax.legend(bars, data.keys())


if __name__ == "__main__":
    # Usage example:
    data = {
"a": [1, 2, 3, 2, 1],
"b": [2, 3, 4, 3, 1],
"d": [5, 9, 2, 1, 8],
    }

    fig, ax = plt.subplots(1, 2)
    bar_plot(ax[0], data, total_width=0.8, single_width=0.9)
    ax[0].set_title("Complete data")

    # If one of the bars is missing, we put None
    data_with_missing_bars = {
"a": [1, None, None, None, None],
"b": [2, 3, 4, 3, 1],
"d": [5, 9, None, 1, 8],
    }

    bar_plot(ax[1], data_with_missing_bars, total_width=0.8, single_width=0.9)
    ax[1].set_title("With missing bars")
    plt.show()

通过这样做,你可以得到如下的图:

修改后的情节

解决方案 10:

这个函数帮助我绘制了分组条形图

def multibarplot(ax, data, xlabels, ylabels, fill_ratio = 0.8):
    l = len(data.T)
    D = len(data)
    width = fill_ratio/D

    for i,(d,ylabel) in enumerate(zip(data,ylabels)):
        ax.bar(np.arange(l) + (i-(D-1)/2) *width,d, width=width, label=ylabel)

    ax.set_xticks(np.arange(l), xlabels)
    ax.legend()
    #ax.autoscale(tight=True)

示例调用

data = np.array([np.arange(i,i+5) for i in range(3)]) + .123
#plt.figure(figsize=(10,5))
ax=plt.gca()
multibarplot(ax, data, xlabels=["a","b","c","d","e"], ylabels=["A","B","C"])

多条形图示例

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1267  
  IPD(Integrated Product Development)即集成产品开发,是一套先进的、成熟的产品开发管理理念、模式和方法。随着市场竞争的日益激烈,企业对于提升产品开发效率、降低成本、提高产品质量的需求愈发迫切,IPD 项目管理咨询市场也迎来了广阔的发展空间。深入探讨 IPD 项目管理咨询的市场需求与发展,...
IPD集成产品开发流程   27  
  IPD(Integrated Product Development)产品开发流程是一套先进的、被广泛应用的产品开发管理体系,它涵盖了从产品概念产生到产品推向市场并持续优化的全过程。通过将市场、研发、生产、销售等多个环节紧密整合,IPD旨在提高产品开发的效率、质量,降低成本,增强企业的市场竞争力。深入了解IPD产品开发...
IPD流程中TR   31  
  IPD(Integrated Product Development)测试流程是确保产品质量、提升研发效率的关键环节。它贯穿于产品从概念到上市的整个生命周期,对企业的成功至关重要。深入理解IPD测试流程的核心要点,有助于企业优化研发过程,打造更具竞争力的产品。以下将详细阐述IPD测试流程的三大核心要点。测试策略规划测试...
华为IPD   26  
  华为作为全球知名的科技企业,其成功背后的管理体系备受关注。IPD(集成产品开发)流程作为华为核心的产品开发管理模式,在创新管理与技术突破方面发挥了至关重要的作用。深入剖析华为 IPD 流程中的创新管理与技术突破,对于众多企业探索自身发展路径具有重要的借鉴意义。IPD 流程概述IPD 流程是一种先进的产品开发管理理念和方...
TR评审   26  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用