Гистограмма с использованием цвета для представления третьего измерения - PullRequest
0 голосов
/ 20 ноября 2018

Моя цель - показать гистограмму с 3-мерными данными, x, категориальными и y1, y2 в виде непрерывных рядов; столбцы должны иметь высоту от y1 и цвет для обозначения y2.

Это не кажется мне особенно неясным, но я не нашел простого / встроенного способа использовать гистограмму для визуализации трех измерений - я думаю, в основном, для исследовательских целей, прежде чем исследовать отношения более формально.

Мне не хватает типа заговора в библиотеках? Есть ли хорошая альтернатива показу трехмерных данных?

В любом случае, вот некоторые вещи, которые я пробовал, которые не особенно удовлетворяют:

enter image description here

Некоторые данные для этих попыток

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, с непрерывным цветовым диапазоном, затем добавляет цветовую шкалу (необходимо скалярное отображение). Я позаимствовал некоторые техники из среднего поста среди других.

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 баров, но это далеко не «из коробки», слишком сложное для быстрого анализа. Более того, sm._A = [] выглядит как хак, но код не работает без него. Возможно, решение в нескольких строках в (4) - лучший путь.


Возвращаясь к вопросу еще раз: возможно ли легко составить гистограмму, которая отображает 3D данные? Я сосредоточился на использовании небольшого количества цветов для третьего измерения, чтобы упростить идентификацию тенденций, но я открыт для других предложений.

Я также опубликовал решение, которое использует много пользовательского кода для достижения того, во что я не могу поверить, не встроено в некоторую графическую библиотеку 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 строки являются более или менее минимальным кодом для барплота, а третья меняет цветовую палитру.

Так что, если бы эта легкость была доступна в python, я бы хотел узнать об этом!

1 Ответ

0 голосов
/ 20 ноября 2018

Я публикую ответ, который решает мои цели: просто в точке использования , все еще быть полезным с ~ 100 барами, и используя 1d классификатор Fisher-Jenks из PySAL, получается достаточно хорошо справляется с выбросами (сообщение о d3 раскраске ) - но в целом это довольно сложно (более 50 строк в классе BinnedColorScaler, размещенных внизу).

# 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

Использование следующего класса, который использует хороший 1d-классификатор из PySAL и заимствует идеи из библиотек геоплот / геопанд.

enter image description here

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...