barplot: между хаосом и ясностью

barplot: между хаосом и ясностью


Maxim.ML

Почему одни графики мгновенно доносят суть, а другие заставляют зрителя страдать? Ответ кроется в деталях. Возьмем пример двух барплотов, которые я недавно встретил в аналитике рынка труда. Оба пытались показать распределение мнений респондентов по категориям, ни один не справился с задачей.


ещё больше интересного в моём telegram-канале: https://t.me/ml_maxim


Первый график: потенциал, который не раскрыли

Автор оригинала попытался добавить ценность через сортировку данных — это похвально, так как сортировка помогает выделить закономерности (например, ранжирование от наибольшего к наименьшему). Однако критичных ошибок избежать не удалось:

1. Цвет как белый шум. Использование случайных оттенков вместо семантической палитры (например, красный для негатива, зеленый для позитива) лишило график контекста. Цвет — это не украшение, а визуальная метафора.

2. Отсутствие системы координат. Без базовой линии или разделителя (например, вертикальной черты на 50%) сложно оценить соотношение категорий. Добавление такой линии превращает график в карту сравнения: зритель сразу видит, какие значения выше или ниже критической отметки.

Попробуем исправить ситуацию:

Исправленная версия демонстрирует силу простых приемов:

  • Контрастная палитра (красный/зеленый) кодирует эмоциональный подтекст.
  • Центральнаялиния разделяет зоны «проблем» и «достижений», превращая график в историю о дисбалансах.

Второй график: как алфавитный порядок убивает смысл

Здесь автор допустил классическую ошибку: расположил категории в алфавитном порядке. Это удобно для поиска, но бесполезно для анализа.

Почему это проблема?

  • Алфавитная сортировка не учитывает вес категорий. Например, если «Удовлетворенность зарплатой» занимает 70% негативных ответов, но находится в середине списка, ее значимость теряется.
  • Такой подход игнорирует принципы гештальт-психологии: группировка по смыслу или величине помогает увидеть целое, а не набор разрозненных столбцов.

Решение: Сортировка по значению (например, по убыванию доли негативных ответов) превращает график в иерархию проблем. Теперь зритель сразу видит, на чем стоит сфокусироваться.

Три правила идеального барплота

  1. Сортировка — ваш союзник. Ранжируйте категории по ключевой метрике, если только порядок не несет самостоятельной ценности (например, временные периоды).
  2. Цвет как язык. Используйте палитры с семантикой (теплые/холодные тона) и учитывайте доступность: 8% мужчин имеют дальтонизм, поэтому избегайте комбинаций вроде красный/зеленый.
  3. Контекст через аннотации. Добавьте подписи, линии среднего или доверительные интервалы — они превращают данные в историю.

Зачем это нужно?

Эффективная визуализация — это не про эстетику. Это про влияние. Разные исследования показывают: решения, подкрепленные грамотными графиками, принимаются на 28% быстрее и воспринимаются как более обоснованные. Ваш барплот может стать тем самым триггером, который убедит инвесторов, изменит стратегию компании или поможет коллегам избежать ошибок.


Код можно найти в моём github или скопировать отсюда

import os
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
# Configuration
CONFIG = {
  "xlabel": "% компаний",
  "img_name": "img_2.png", 
  "colormaps": {
    "green": ["#2D5F2E", "#8CB369"],
    "red": ["#C14953", "#6D1A1A"]
  },
  "bar_width": 0.65,
  "left_ticks": 3,
  "font": "Arial",
  "grid_color": "#EAEAEA",
  "text_colors": {
    "green": "#FFFFFF",
    "red": "#FFFFFF"
  },
  "title": "Как прошёл 2024 год в вашей компании?",
  "ticks": [
    "Лучше, чем 2023",

    "Лучше, чем 2023, но хуже 2022", 
    "Лучше, чем 2022, но хуже 2020",
    "Так же, как и прошлый", 
    "Хуже, чем 2023"
  ],
  "data": {
    "В целом": [31, 5, 2, 19, 43],
    "Тяжелая промышленность": [40, 4, 0, 18, 38],
    "ИТ": [34, 6, 4, 20, 36],
    "Легкая промышленность": [33, 6, 0, 19, 42],
    "Финансовые технологии": [31, 3, 4, 14, 48],
    "Розничная торговля": [31, 4, 3, 16, 46],
    "Строительство, недвижимость": [30, 5, 0, 22, 43],
    "Услуги для бизнеса и населения": [30, 5, 2, 22, 41],
    "Добыча и переработка ископаемых": [28, 5, 1, 20, 46],
    "Гостиницы, рестораны": [27, 2, 2, 15, 54],
    "Логистика": [23, 7, 2, 18, 50]
  },
  "xlim": [-65, 75]
}

# Sort the data
CONFIG['data'] = dict(sorted(CONFIG['data'].items(), key=lambda x:
sum(x[1][: CONFIG['left_ticks']]), reverse=False))
def create_colormaps():
  """Create LinearSegmentedColormap objects from the CONFIG
colormaps."""
  cmaps = {}
  for name, colors in CONFIG["colormaps"].items():
    cmaps[name] = mcolors.LinearSegmentedColormap.from_list(name,
colors)
  return cmaps


def add_labels(ax, bars, values, color_type):
  """Add value labels to the bars with appropriate text color."""
  text_color = CONFIG["text_colors"][color_type]
  for bar, value in zip(bars, values):
    if value == 0:
      continue
    width = bar.get_width()
    x = bar.get_x() + width / 2
    y = bar.get_y() + bar.get_height() / 2
    ax.text(x, y, f"{int(value)}", 
        ha='center', va='center',
        color=text_color, fontsize=13)


def add_style(ax):
  """Apply professional styling elements."""
  plt.rcParams['font.family'] = CONFIG["font"]
  ax.xaxis.grid(True, color=CONFIG["grid_color"], linestyle='--')
  ax.set_axisbelow(True)
  for spine in ax.spines.values():
    spine.set_visible(False)
  ax.axvline(0, color='#404040', linewidth=0.8, alpha=0.8)


def plot_segments(ax, data, cmaps):
  """Plot the horizontal bar segments with correct colors."""
  categories = list(data.keys())
  params = [
    {'direction': 'left', 'cmap': cmaps['green'], 'color': 'green',
'indices': range(CONFIG["left_ticks"]), 'edgecolor': '#404040'},
    {'direction': 'right', 'cmap': cmaps['red'], 'color': 'red',
'indices': range(CONFIG["left_ticks"], len(CONFIG["ticks"])), 'edgecolor': '#404040'}
  ]
  for param in params:
    base = np.zeros(len(categories))
    for idx, data_idx in enumerate(param['indices']):
      values = np.array([v[data_idx] for v in data.values()])
      width = -values if param['direction'] == 'left' else values
      norm_idx = idx / (len(param['indices']) - 0.8)
      color = param['cmap'](norm_idx)
      bars = ax.barh(
        np.arange(len(categories)), width,
        left=base, height=CONFIG["bar_width"],
        color=color, edgecolor=param['edgecolor'],
        linewidth=0.5, alpha=0.95
      )
      add_labels(ax, bars, values, param['color'])
      base += width


def create_plot(data):
  """Create the final plot with correct legend colors."""
  plt.rcParams['font.size'] = 12
  fig, ax = plt.subplots(figsize=(15, 8))
   
  ax.set_title(CONFIG["title"], fontsize=18, pad=20, color='#2D2D2D', weight='semibold', y=1.2)
  ax.set_yticks(np.arange(len(data)))
  ax.set_yticklabels(data.keys(), color='#404040', fontsize=11)
  ax.set_xlim(*CONFIG['xlim'])
  ax.set_xticks([])
   
  cmaps = create_colormaps()
  plot_segments(ax, data, cmaps)
  add_style(ax)   
  # Generate correct legend colors
  tick_colors = []
  for i in range(len(CONFIG["ticks"])):
    if i < CONFIG["left_ticks"]:
      cmap = cmaps['green']
      pos = i / (CONFIG["left_ticks"] - 0.8)
    else:
      cmap = cmaps['red']
      idx = i - CONFIG["left_ticks"]
      pos = idx / ((len(CONFIG["ticks"]) - CONFIG["left_ticks"]) - 0.8)
    tick_colors.append(cmap(pos))
  # Create legend handles
  legend_handles = [plt.Rectangle((0,0), 1, 1, color=color) for color in tick_colors]     
# Add legend
  legend = ax.legend(
    handles=legend_handles,
    labels=CONFIG["ticks"],
    ncol=2, frameon=False,
    loc=(0.13, 1.05), fontsize=12
  )
  for text in legend.get_texts():
    text.set_color('#606060')
     
  plt.subplots_adjust(top=0.88, bottom=0.15)
  plt.xlabel(CONFIG['xlabel'])
  plt.tight_layout()
  plt.savefig(CONFIG['img_name'], format='png', dpi=120)


if __name__ == "__main__":
  create_plot(CONFIG['data'])



ключевые слова:

Визуализация данных, интерпретация данных, анализ данных, barplot, data science, analytics, big data, dashboard


Report Page