我的目标是显示一个条形图,其中3维数据,x,类别和y1,y2为连续序列;条形图的高度应为y1,颜色应为y2。
这对我来说似乎并不是特别难理解,但是我没有找到一种简单/内置的方式来使用条形图来可视化三个维度-在调查关系之前,我主要是出于探索目的更正式地。
我在图书馆中缺少某种情节吗?除了显示3D数据,还有其他不错的选择吗?
无论如何,这里有一些我尝试过的并没有特别令人满意的东西:
这些尝试的一些数据
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Example data with explicit (-ve) correlation in the two series
n = 10; sd = 2.5
fruits = [ 'Lemon', 'Cantaloupe', 'Redcurrant', 'Raspberry', 'Papaya',
'Apricot', 'Cherry', 'Durian', 'Guava', 'Jujube']
np.random.seed(101)
cost = np.random.uniform(3, 15, n)
harvest = 50 - (np.random.randn(n) * sd + cost)
df = pd.DataFrame(data={'fruit':fruits, 'cost':cost, 'harvest':harvest})
df.sort_values(by="cost", inplace=True) # preferrable to sort during plot only
# set up several subplots to show progress.
n_colors = 5; cmap_base = "coolwarm" # a diverging map
fig, axs = plt.subplots(3,2)
ax = axs.flat
尝试1 对hue
中的第3个暗淡数据使用barplot
。但是,这会为系列中的每个值产生单一颜色,并且似乎在条形宽度和间距方面做得很奇怪。
import seaborn as sns
sns.barplot(ax=ax[0], x='fruit', y='cost', hue='harvest',
data=df, palette=cmap_base)
# fix the sns barplot label orientation
ax[0].set_xticklabels(ax[0].get_xticklabels(), rotation=90)
尝试2 使用具有连续颜色范围的熊猫DataFrame.plot.bar
,然后添加颜色条(需要标量可映射)。我从medium post那里借鉴了一些技术。
import matplotlib as mpl
norm = mpl.colors.Normalize(vmin=min(df.harvest), vmax=max(df.harvest), clip=True)
mapper1 = mpl.cm.ScalarMappable(norm=norm, cmap=cmap_base)
colors1 = [mapper1.to_rgba(x) for x in df.harvest]
df.plot.bar(ax=ax[1], x='fruit', y='cost', color=colors1, legend=False)
mapper1._A = []
plt.colorbar(mapper1, ax=ax[1], label='havest')
尝试3 以此为基础,从https://gist.github.com/jakevdp/91077b0cae40f8f8244a借用以简化离散的颜色图。
def discrete_cmap(N, base_cmap=None):
"""Create an N-bin discrete colormap from the specified input map"""
# from https://gist.github.com/jakevdp/91077b0cae40f8f8244a
base = plt.cm.get_cmap(base_cmap)
color_list = base(np.linspace(0, 1, N))
cmap_name = base.name + str(N)
return base.from_list(cmap_name, color_list, N)
cmap_disc = discrete_cmap(n_colors, cmap_base)
mapper2 = mpl.cm.ScalarMappable(norm=norm, cmap=cmap_disc)
colors2 = [mapper2.to_rgba(x) for x in df.harvest]
df.plot.bar(ax=ax[2], x='fruit', y='cost', color=colors2, legend=False)
mapper2._A = []
cb = plt.colorbar(mapper2, ax=ax[2], label='havest')
cb.set_ticks(np.linspace(*cb.get_clim(), num=n_colors+1)) # indicate color boundaries
cb.set_ticklabels(["{:.0f}".format(t) for t in cb.get_ticks()]) # without too much precision
最后,尝试4 允许在一个绘图中尝试3d并分为2部分。
sns.barplot(ax=ax[4], x='fruit', y='cost', data=df, color='C0')
ax[4].set_xticklabels(ax[4].get_xticklabels(), rotation=90)
sns.regplot(x='harvest', y='cost', data=df, ax=ax[5])
(1)无法使用-我显然没有按预期使用。 (2)在10个系列中是可以的,但是,例如,更多的系列则很难分辨给定的样本是否高于/低于平均值。 (3)相当不错,可以缩放到50 bar,但是它离“开箱即用”还很远,也涉及到快速分析的问题。此外,sm._A = []
似乎很容易破解,但是如果没有它,代码将失败。也许在(4)中几行中的解决方案是更好的方法。
再次回到问题:是否可以轻松生成显示3d数据的条形图?我专注于在第3维上使用少量颜色,以便于识别趋势,但我愿意接受其他建议。
我也发布了一个解决方案,该解决方案使用大量自定义代码来实现我无法真正相信的不是在某些python图形库中构建的解决方案。
编辑:
以下代码,使用R的ggplot
,可以通过内置命令合理地近似(2)。
ggplot(data = df, aes(x =reorder(fruit, +cost), y = cost, fill=harvest)) +
geom_bar(data=df, aes(fill=harvest), stat='identity') +
scale_fill_gradientn(colours=rev(brewer.pal(7,"RdBu")))
前2行或多或少是barplot的最小代码,而第三行更改了调色板。
因此,如果可以在python中获得这种便利,那么我很想知道!
答案 0 :(得分:1)
我发布的答案确实解决了我的目标,即在使用时变得简单 ,仍可用于约100条,并利用PySAL端的Fisher-Jenks 1d分类器可以很好地处理异常值(发布有关d3 coloring的信息)
-但总体上涉及很多(BinnedColorScaler
类中的50多行,显示在底部)。
# set up the color binner
quantizer = BinnedColorScaler(df.harvest, k=5, cmap='coolwarm' )
# and plot dataframe with it.
df.plot.bar(ax=ax, x='fruit', y='cost',
color=df.harvest.map(quantizer.map_by_class))
quantizer.add_legend(ax, title='harvest') # show meaning of bins in legend
使用下面的类,该类使用来自PySAL的漂亮的1d分类器,并借鉴了geoplot / geopandas库的思想。
from pysal.esda.mapclassify import Fisher_Jenks
class BinnedColorScaler(object):
'''
give this an array-like data set, a bin count, and a colormap name, and it
- quantizes the data
- provides a bin lookup and a color mapper that can be used by pandas for selecting artist colors
- provides a method for a legend to display the colors and bin ranges
'''
def __init__(self, values, k=5, cmap='coolwarm'):
self.base_cmap = plt.cm.get_cmap(cmap) # can be None, text, or a cmap instane
self.bin_colors = self.base_cmap(np.linspace(0, 1, k)) # evenly-spaced colors
# produce bins - see _discrete_colorize in geoplot.geoplot.py:2372
self.binning = Fisher_Jenks(np.array(values), k)
self.bin_edges = np.array([self.binning.yb.min()] + self.binning.bins.tolist())
# some text for the legend (as per geopandas approx)
self.categories = [
'{0:.2f} - {1:.2f}'.format(self.bin_edges[i], self.bin_edges[i + 1])
for i in xrange(len(self.bin_edges) - 1)]
def map_by_class(self, val):
''' return a color for a given data value '''
#bin_id = self.binning.find_bin(val)
bin_id = self.find_bin(val)
return self.bin_colors[bin_id]
def find_bin(self, x):
''' unfortunately the pysal implementation seems to fail on bin edge
cases :(. So reimplement with the way we expect here.
'''
# wow, subtle. just <= instead of < in the uptos
x = np.asarray(x).flatten()
uptos = [np.where(value <= self.binning.bins)[0] for value in x]
bins = [v.min() if v.size > 0 else len(self.bins)-1 for v in uptos] #bail upwards
bins = np.asarray(bins)
if len(bins) == 1:
return bins[0]
else:
return bins
def add_legend(self, ax, title=None, **kwargs):
''' add legend showing the discrete colors and the corresponding data range '''
# following the geoplot._paint_hue_legend functionality, approx.
# generate a patch for each color in the set
artists, labels = [], []
for i in xrange(len(self.bin_colors)):
labels.append(self.categories[i])
artists.append(mpl.lines.Line2D(
(0,0), (1,0), mfc='none', marker='None', ls='-', lw=10,
color=self.bin_colors[i]))
return ax.legend(artists, labels, fancybox=True, title=title, **kwargs)