Как улучшить размещение меток для диаграммы рассеяния matplotlib (код, алгоритм, подсказки)?
Я использую matplotlib для построения диаграммы рассеяния:
![enter image description here]()
И назовите пузырь, используя прозрачный ящик в соответствии с подсказкой в 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)
и полученный участок:
![enter image description here]()
Но есть еще возможности для улучшения, чтобы уменьшить перекрытие (например, смещение метки метки фиксировано как (-10,3)). Существуют ли алгоритмы, которые могут:
- динамически изменять смещение поля ярлыка в соответствии с переполненностью его окрестности.
- динамически помещать ящик меток удаленно и добавить строку стрелки в пузырь и поле ярлыка
- несколько меняют ориентацию метки
- 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 для реализации алгоритма.
Я не академический парень и не ищу оптимального решения, и мои коды на питоне должны обозначать много графиков, поэтому скорость/память находятся в сфере рассмотрения.
Я ищу быстрое и эффективное решение. Любая помощь (код, алгоритм, советы, мысли) на эту тему? Благодарю.
Ответы
Ответ 1
Немного грубо по краям (я не могу понять, как масштабировать относительные силы сети spring против силы отталкивания, а ограничивающая коробка немного прикручена), но это достойный старт:
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()
![sample image]()
Насколько хорошо он работает, немного зависит от того, как ваши данные кластеризованы.
Ответ 2
Следующее построено на tcaswell answer.
Методы компоновки Networkx, такие как nx.spring_layout
, масштабируют позиции так, чтобы все они соответствовали квадрату единицы (по умолчанию). Даже положение фиксированного data_nodes
масштабируется. Итак, чтобы применить pos
к оригиналу scatter_data
, необходимо выполнить непересекающееся и unscaling.
Обратите внимание, что 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 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
дает
![введите описание изображения здесь]()
Ответ 3
Еще одна опция, использующая мою библиотеку 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()
![введите описание изображения здесь]()
Он не отталкивается от пузырьков, только из их центров и других текстов.