使用 Python 绘制包含标签的分类 XY 数据(例如 BCG 矩阵)

Plotting catecorigal XY data including labels using Python (e. g. BCG matrices)

我喜欢画 2x2 / BCG 矩阵。这次我有一个相当大的数据集(超过 50 个主题和多个值,例如 A 和 B)。我想知道如何使用 Python?

来绘制它

结果应该与此类似:

我发现了几个关于散点图的问题,但是 none 其中确实很好地处理了例如两个具有相同值的主题(参见图中的主题 3、2、L、J、...)。

ID要显示在图中,同一组值的ID不能重叠,而是要靠得很近。

有办法吗?如果没有Python,我也很乐意提供其他建议。

这是一个示例数据集:

ID  Name        value_A     value_B
A   topic_1     2           4
B   topic_2     4           2
C   topic_3     3           3
D   topic_4     3           5
E   topic_5     3           4
F   topic_6     5           1
G   topic_7     4           5
H   topic_8     1           2
I   topic_9     4           1
J   topic_10    3           3
K   topic_11    5           5
L   topic_12    5           3
M   topic_13    3           5
N   topic_14    1           5
O   topic_15    4           1
P   topic_16    4           2
Q   topic_17    1           5
R   topic_18    2           3
S   topic_19    1           2
T   topic_20    5           1
U   topic_21    3           4
V   topic_22    2           5
W   topic_23    1           3
X   topic_24    3           3
Y   topic_25    4           1
Z   topic_26    2           4
1   topic_27    2           4
2   topic_28    5           4
3   topic_29    3           3
4   topic_30    4           4
5   topic_31    3           2
6   topic_32    4           2
7   topic_33    2           3
8   topic_34    2           3
9   topic_35    2           5
10  topic_36    4           2

我认为下面的代码应该非常接近您要查找的内容。基本思想是将聚集在一个位置的每组点放置在以该位置为中心的圆圈中。我以一种 临时 的方式定义了圆的半径,只是为了让它看起来适合我遇到的尺寸,但您可能需要根据您的特定任务稍微改变一下.

首先,这只是将您的 copy/paste 个值放入列表中。

values = ['ID  Name        value_A     value_B',
          'A   topic_1     2           4',
          'B   topic_2     4           2',
          'C   topic_3     3           3',
          'D   topic_4     3           5',
          'E   topic_5     3           4',
          'F   topic_6     5           1',
          'G   topic_7     4           5',
          'H   topic_8     1           2',
          'I   topic_9     4           1',
          'J   topic_10    3           3',
          'K   topic_11    5           5',
          'L   topic_12    5           3',
          'M   topic_13    3           5',
          'N   topic_14    1           5',
          'O   topic_15    4           1',
          'P   topic_16    4           2',
          'Q   topic_17    1           5',
          'R   topic_18    2           3',
          'S   topic_19    1           2',
          'T   topic_20    5           1',
          'U   topic_21    3           4',
          'V   topic_22    2           5',
          'W   topic_23    1           3',
          'X   topic_24    3           3',
          'Y   topic_25    4           1',
          'Z   topic_26    2           4',
          '1   topic_27    2           4',
          '2   topic_28    5           4',
          '3   topic_29    3           3',
          '4   topic_30    4           4',
          '5   topic_31    3           2',
          '6   topic_32    4           2',
          '7   topic_33    2           3',
          '8   topic_34    2           3',
          '9   topic_35    2           5',
          '10  topic_36    4           2']

接下来,获取您在上面提供的数据,并将其组织成一个 ID 列表以及 A 和 B 的另一个值列表。

import re
values = [re.split(r'\s+', v) for v in values][1:]
points = [[int(v[2]), int(v[3])] for v in values]
labels = [v[0] for v in values]

现在我们需要找到唯一的 AB 对及其 ID。有很多方法可以从您的原始列表中得出这一点,其他人可能会根据您的原始数据结构和效率考虑因素提出改进建议。

unique_points = []
n_labels = []

for i in range(len(points)):
    if points[i] not in unique_points:
        unique_points.append(points[i])
        n_labels.append([labels[i],])
    else:
        n_labels[unique_points.index(points[i])] += [labels[i],]

对于我的另一个项目,我设计这个 class 来做一些与您正在尝试做的事情非常相似的事情,所以我在这里重新实施它有几个变体。基本上每个独特的点和伴随的 ID 进入他们自己的对象,这使您可以轻松地将这些点绘制在以独特点为中心的圆圈中。

import numpy as np
from matplotlib import pyplot as plt


class clique():
    def __init__(self, center, labels, r):
        self.n = len(labels)
        self.x = center[0]
        self.y = center[1]
        self.labels = labels
        self.r = r
        
        # The random addition below just spins the points about 
        # the circle so groups of the same size look different
        self.theta = np.arange(0, 2 * np.pi, 2 * np.pi / self.n) + np.random.rand() * 2 * np.pi
        
        if self.n == 1: 
            self.nodes_x = [self.x,]
            self.nodes_y = [self.y,]
        else: 
            self.nodes_x = self.x + r * np.cos(self.theta)
            self.nodes_y = self.y + r * np.sin(self.theta)
            
    def draw_nodes(self, shape = 'o', color = 'k', markersize = 12):
        for i in range(len(self.nodes_x)):
            plt.plot(self.nodes_x[i], self.nodes_y[i], shape, color = color,
                     markersize = markersize)
    def label_nodes(self, color = 'w', fs = 10):
        for i in range(len(self.nodes_x)):
            plt.text(self.nodes_x[i], self.nodes_y[i], self.labels[i],
                     va = 'center', ha = 'center', color = color, fontsize = fs)

现在,为每个点簇创建 clique 对象并绘制它。

for i in range(len(unique_points)):
    radius = 0.05 + 0.2 / 5 * len(n_labels[i])
    G = clique(unique_points[i], n_labels[i], radius)
    G.draw_nodes()
    G.label_nodes()

最后,稍微清理一下情节。

plt.axis('equal') # This ensures things look circular on the
                  # figure. If you want non-equal axes and a circular
                  # look, you'll need to work out the equation for
                  # plotting in "clique" as ellipses based on the
                  # figure dimensions

A = np.array([u[0] for u in unique_points])
B = np.array([u[1] for u in unique_points])
plt.xticks([min(A), max(A)], ['Low', 'High'])
plt.yticks([min(B), max(B)], ['Low', 'High'])
plt.xlabel('A')
plt.ylabel('B')