Как улучшить размещение меток для точечной диаграммы matplotlib (код, алгоритм, советы)?

Я использую matplotlib для построения точечной диаграммы:

введите здесь описание изображения

И пометьте пузырь, используя прозрачную рамку, в соответствии с советом по адресу arrow">matplotlib: как аннотировать точку на автоматически размещенной стрелке разброса?

Вот код:

if show_annote:
    for i in range(len(x)):
        annote_text = annotes[i][0][0]  # STK_ID
        ax.annotate(annote_text, xy=(x[i], y[i]), xytext=(-10,3),
            textcoords='offset points', ha='center', va='bottom',
            bbox=dict(boxstyle='round,pad=0.2', fc='yellow', alpha=0.2),
            fontproperties=ANNOTE_FONT) 

и получившийся график: введите здесь описание изображения

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

  1. динамически изменять смещение поля метки в зависимости от плотности его соседства
  2. динамически разместить поле метки удаленно и добавить линию со стрелкой между пузырьком и полем метки
  3. немного изменить ориентацию этикетки
  4. Перекрывающийся пузырь label_box лучше, чем label_box, перекрывающий label_box?

Я просто хочу сделать диаграмму легкой для понимания человеческим глазом, поэтому некоторое перекрытие допустимо, а не такое жесткое ограничение, как http://en.wikipedia.org/wiki/Automatic_label_placement предлагает. И количество пузырьков на графике в большинстве случаев меньше 150.

Я считаю, что так называемый Force-based label placement http://bl.ocks.org/MoritzStefaner/1377729 вполне интересно. Я не знаю, есть ли какой-либо код/пакет python для реализации алгоритма.

Я не академик и не ищу оптимального решения, и мои коды Python должны маркировать множество диаграмм, поэтому скорость/память находятся в поле зрения.

Я ищу быстрое и эффективное решение. Любая помощь (код, алгоритм, советы, мысли) по этому вопросу? Спасибо.


person bigbug    schedule 18.02.2013    source источник
comment
Бьюсь об заклад, вы могли бы сделать что-нибудь классное с networkx и его механизмом компоновки.   -  person tacaswell    schedule 07.04.2013


Ответы (4)


Другой вариант с использованием моей библиотеки adjustText, написанной специально для этой цели (https://github.com/Phlya/adjustText).

from adjustText import adjust_text
np.random.seed(2016)

N = 50
scatter_data = np.random.rand(N, 3)
fig, ax = plt.subplots()
ax.scatter(scatter_data[:, 0], scatter_data[:, 1],
           c=scatter_data[:, 2], s=scatter_data[:, 2] * 150)
labels = ['ano_{}'.format(i) for i in range(N)]
texts = []
for x, y, text in zip(scatter_data[:, 0], scatter_data[:, 1], labels):
    texts.append(ax.text(x, y, text))
plt.show()

введите описание изображения здесь

np.random.seed(2016)

N = 50
scatter_data = np.random.rand(N, 3)
fig, ax = plt.subplots()
ax.scatter(scatter_data[:, 0], scatter_data[:, 1],
           c=scatter_data[:, 2], s=scatter_data[:, 2] * 150)
labels = ['ano_{}'.format(i) for i in range(N)]
texts = []
for x, y, text in zip(scatter_data[:, 0], scatter_data[:, 1], labels):
    texts.append(ax.text(x, y, text))
adjust_text(texts, force_text=0.05, arrowprops=dict(arrowstyle="-|>",
                                                    color='r', alpha=0.5))
plt.show()

введите описание изображения здесь

От пузырьков не отталкивается, только от их центров и других надписей.

person Phlya    schedule 07.01.2017
comment
именно то, что я искал! Спасибо за библиотеку. - person volodymyr; 07.06.2018
comment
@volodymyr спасибо, рад, что это работает для вас! Не стесняйтесь обращаться, если у вас есть какие-либо вопросы или проблемы! - person Phlya; 07.06.2018
comment
Я продолжаю возвращаться к этому вопросу на протяжении многих лет и продолжаю использовать этот пакет. это замечательно!! - person Alex Spangher; 26.06.2018
comment
@AlexSpangher спасибо, рад слышать, что это так полезно для вас! - person Phlya; 28.06.2018
comment
Это намного проще, чем networkx, но это заняло некоторое время. Хотя стоит подождать! - person lagrange103; 07.07.2018
comment
@surelyourejoking Рад, что это помогло вам! Это медленно для сложных сюжетов, я начал делать небольшой рефакторинг для повышения скорости, но столкнулся с некоторыми проблемами, поэтому это еще не сделано... - person Phlya; 07.07.2018
comment
@surelyourejoking, но также возможно, что в вашем конкретном случае это можно сделать быстрее, просто изменив некоторые параметры, такие как силы или точность, - зависит от качества результата, который вы хотите получить. Не стесняйтесь связаться, если вы хотите, чтобы я взял на это. - person Phlya; 07.07.2018
comment
Кого это может касаться... У меня был график с примерно 200 метками, и настройки по умолчанию приводили к длительному времени рендеринга. Установите параметр lim=20, т.е. для быстрой итерации (по умолчанию 500). Кстати, суперкрутой инструмент! Большое спасибо за то, что сделали это доступным. - person petezurich; 30.09.2018

Следующее основано на ответе tcaswell.

Методы компоновки Networkx, такие как nx.spring_layout, изменяют масштаб позиций, чтобы все они помещались в единичный квадрат (по умолчанию). Даже положение фиксированного data_nodes масштабируется. Таким образом, чтобы применить pos к исходному scatter_data, необходимо выполнить несдвиг и масштабирование.

Также обратите внимание, что nx.spring_layout имеет параметр k, который управляет оптимальным расстоянием между узлами. По мере увеличения k увеличивается и расстояние аннотаций от точек данных.

import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
np.random.seed(2016)

N = 20
scatter_data = np.random.rand(N, 3)*10


def repel_labels(ax, x, y, labels, k=0.01):
    G = nx.DiGraph()
    data_nodes = []
    init_pos = {}
    for xi, yi, label in zip(x, y, labels):
        data_str = 'data_{0}'.format(label)
        G.add_node(data_str)
        G.add_node(label)
        G.add_edge(label, data_str)
        data_nodes.append(data_str)
        init_pos[data_str] = (xi, yi)
        init_pos[label] = (xi, yi)

    pos = nx.spring_layout(G, pos=init_pos, fixed=data_nodes, k=k)

    # undo spring_layout's rescaling
    pos_after = np.vstack([pos[d] for d in data_nodes])
    pos_before = np.vstack([init_pos[d] for d in data_nodes])
    scale, shift_x = np.polyfit(pos_after[:,0], pos_before[:,0], 1)
    scale, shift_y = np.polyfit(pos_after[:,1], pos_before[:,1], 1)
    shift = np.array([shift_x, shift_y])
    for key, val in pos.items():
        pos[key] = (val*scale) + shift

    for label, data_str in G.edges():
        ax.annotate(label,
                    xy=pos[data_str], xycoords='data',
                    xytext=pos[label], textcoords='data',
                    arrowprops=dict(arrowstyle="->",
                                    shrinkA=0, shrinkB=0,
                                    connectionstyle="arc3", 
                                    color='red'), )
    # expand limits
    all_pos = np.vstack(pos.values())
    x_span, y_span = np.ptp(all_pos, axis=0)
    mins = np.min(all_pos-x_span*0.15, 0)
    maxs = np.max(all_pos+y_span*0.15, 0)
    ax.set_xlim([mins[0], maxs[0]])
    ax.set_ylim([mins[1], maxs[1]])

fig, ax = plt.subplots()
ax.scatter(scatter_data[:, 0], scatter_data[:, 1],
           c=scatter_data[:, 2], s=scatter_data[:, 2] * 150)
labels = ['ano_{}'.format(i) for i in range(N)]
repel_labels(ax, scatter_data[:, 0], scatter_data[:, 1], labels, k=0.008)

plt.show()

с k=0.011 урожайностью

введите описание изображения здесь и с k=0.008 дает введите здесь описание изображения

person unutbu    schedule 09.01.2016
comment
Мне пришлось изменить pos.iteritems() в цикле for на pos.items(). Я использую Python 3.5.2 и networkx v1.11. - person equant; 20.08.2016
comment
Я получаю FutureWarning: arrays to stack must be passed as a "sequence" type such as list or tuple. Support for non-sequence iterables such as generators is deprecated as of NumPy 1.16 and will raise an error in the future. all_pos = np.vstack(pos.values()) Не могли бы вы посоветовать, как это можно решить? - person Slartibartfast; 05.07.2020

Это немного грубо по краям (я не могу понять, как масштабировать относительную силу пружинной сети по сравнению с силой отталкивания, а ограничивающая рамка немного испорчена), но это достойное начало:

import networkx as nx

N = 15
scatter_data = rand(3, N)
G=nx.Graph()

data_nodes = []
init_pos = {}
for j, b in enumerate(scatter_data.T):
    x, y, _ = b
    data_str = 'data_{0}'.format(j)
    ano_str = 'ano_{0}'.format(j)
    G.add_node(data_str)
    G.add_node(ano_str)
    G.add_edge(data_str, ano_str)
    data_nodes.append(data_str)
    init_pos[data_str] = (x, y)
    init_pos[ano_str] = (x, y)

pos = nx.spring_layout(G, pos=init_pos, fixed=data_nodes)
ax = gca()
ax.scatter(scatter_data[0], scatter_data[1], c=scatter_data[2], s=scatter_data[2]*150)

for j in range(N):
    data_str = 'data_{0}'.format(j)
    ano_str = 'ano_{0}'.format(j)
    ax.annotate(ano_str,
                xy=pos[data_str], xycoords='data',
                xytext=pos[ano_str], textcoords='data',
                arrowprops=dict(arrowstyle="->",
                                connectionstyle="arc3"))

all_pos = np.vstack(pos.values())
mins = np.min(all_pos, 0)
maxs = np.max(all_pos, 0)

ax.set_xlim([mins[0], maxs[0]])
ax.set_ylim([mins[1], maxs[1]])

draw()

пример изображения

Насколько хорошо это работает, немного зависит от того, как ваши данные сгруппированы.

person tacaswell    schedule 07.04.2013
comment
Да, это достойное начало, выглядит улучшение результата отображения. Я посмотрю на networkx. Спасибо, - person bigbug; 07.04.2013

Мы можем использовать plotly для этого. Но мы не можем не разместить перекрытие правильно, если данных много. Вместо этого мы можем увеличивать и уменьшать масштаб.

import plotly.express as px
df = px.data.tips()

df = px.data.gapminder().query("year==2007 and continent=='Americas'")


fig = px.scatter(df, x="gdpPercap", y="lifeExp", text="country", log_x=True, size_max=100, color="lifeExp",
                 title="Life Expectency")
fig.update_traces(textposition='top center')

fig.show()

Выход:

введите здесь описание изображения

person bigbounty    schedule 14.07.2020