如何绘制分组的多个条形图
- 2025-02-18 09:25:00
- admin 原创
- 38
问题描述:
如何在 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
,但是使用pandas
和seaborn
可以节省您很多时间:
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.0
、pandas 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"])
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)