7维韦恩图 7-set Venn Diagram

知乎的界面简洁,而且代码的显示功能不错。

因此我选择了一些写得还行文章,并做了修改,搬到这边。


写在前面

看系统进化的论文时,我看到一种特殊的韦恩图,用于表示集合之间的关系:

  • 当平面内集合数量为1的时候,这个空间被分成两个部分,即集合内和集合外
  • 当平面内集合数量增加的时候,集合划分情况愈加复杂
↑我随手用PS画的

n维的Venn图(n维,即Venn图内有n个集合)包含了对这个集合从属状态的描述,每个元素对于每个集合都有两种状态——「包含」和「不包含」,如果用0、1表示的话,那么在n个集合Venn图内,所有元素的状态可以用一个n位二进制数字表示

↑也是我随手用PS画的

易得,n维Venn图,里面的元素会被n个集合划分成2^n 个区域,随集合数的增加,被划分出来的区域数量呈指数级增加,图形愈发复杂。

我之前在论文里面看到的Venn图,是种群对比数据的一种可视化方法,而5维以上的Venn图的可视化效果已经大打折扣了,所以网上5维以上的Venn图就比较少了。

↑来自 Draft genome of the wheat A-genome progenitor Triticum urartu

讲小麦A基因的,2013年


而我之前比较傻,以为触及到人类的未知领域了,于是很开心地开始画5维以上的Venn图,画完Google后才知道,别人都已经画到11维的Venn图了(2012年),而7维的Venn图在我出生前就被人画出来了。

ヽ(#`Д´)ノ

Logic blooms with new 11-set Venn diagram

↑毫无可读性可言的11维的Venn图


尽管如此,我还是把7维的画出来了,这里放上思路以及代码,并以及解释为何大家画的都是奇数维度的Venn图。


问题简化:

奇数维度的Venn图拥有更高的对称度,以及更明显的规律

原问题「如何绘制n维的Venn图」

  • 奇数维度的Venn图可以绘成旋转对称图形

问题简化为「如何绘制奇数维度的Venn图」

  • 偶数2k维度的Venn图是容易得到的:奇数2k+1维度的Venn图,删去一个集合后可得
  • 通过将一个图形旋转n次,分别代表这n个集合,可以得到n维Venn图

问题简化为「如何绘制这个旋转图形」

  • 先简化朴素的3维5维Venn图,得到如下:

可以看到,n维Venn图其实是由 3个内接多边形构成的,5维Venn图多了一个五角星。

每个内接n边形都有1/n 的区域作为初始图形的一部分参与旋转

右图是7维的Venn图,可以说是很复杂了

由于按照内接n边形的划分方法,会导致区域大小划分不均衡,图案缺乏美感,同时不便于填入数字,所以需要调整其边界。

采用极坐标记录每个点的位置

为了可视化这个过程,我顺手用Python3写了绘图程序,这段代码包含了上图每个点的位置信息(以展示为主,其效果是下面的gif图)

调整后的7维Venn图,旋转的过程是很好看的
import matplotlib.pyplot as plt
import numpy as np

gap = 2 ** 2
side = 2 ** 9
base = np.zeros((side, side, 3), dtype=np.uint8)

center = side // 2
radii = center * 0.9

plotMODE = plt.imshow(base, interpolation='quadric')
# plotMODE = plt.imshow(base, interpolation='none')
plt.pause(9)

def get_area(r):
    # Polar Coordinates [p_radii, 0_angle]
    # return point list of 7-set Venn diagram area
    p0 = [1, 0]
    p1 = [90 / 100, 1 / 14]

    p15 = [85 / 100, 0]

    p6 = [72 / 100, +1 / 30]
    p2 = [72 / 100, -1 / 30]
    p7 = [65 / 100, -1 / 20]
    p14 = [65 / 100, 1 / 20]
    p3 = [60 / 100, -1 / 65]
    p5 = [60 / 100, 1 / 65]

    p4 = [50 / 100, 0]

    p16 = [58 / 100, 1 / 14]
    p8 = [45 / 100, -1 / 20]
    p13 = [45 / 100, 1 / 20]

    p9 = [35 / 100, -1 / 25]
    p10 = [35 / 100, 1 / 25]

    p11 = [25 / 100, 0]

    p12 = [15 / 100, 1 / 14]

    npy = np.array((
        p0, p1,
        p2, p3, p4, p5, p6, p1,
        p7, p8, p9, p4, p10, p11, p12,
        p12,
        p11, p9,
        p10, p13, p14, p1, p15, p6, p14, p16,
        p8, p3, p5, p13, p16,
        p7, p2, p15, p1,
        p0
    ), dtype=np.float)

    npy[0:2, 1] += 0 / 7
    npy[2:8, 1] += 1 / 7
    npy[8:15, 1] += 2 / 7
    npy[15:16, 1] += 3 / 7  # signal
    npy[16:18, 1] += 4 / 7
    npy[18:22, 1] += 3 / 7
    npy[22:26, 1] += 4 / 7
    npy[26:31, 1] += 5 / 7
    npy[31:35, 1] += 6 / 7
    npy[35:36, 1] += 7 / 7

    npy[:, 0] *= r
    npy[:, 1] *= 2 * np.pi
    return npy

def get_ploygon(r, n=3):
    """
    :param r: radii
    :param n: the side number of ploygon
    :return: polar coordinate == [p_radii, 0_angle]
    """
    mx = np.array((n+1, 2))
    mx[:, 0] = np.arange(0, n+1, 1)
    mx[:, 0] *= 2*np.pi / n
    mx[:, 1] = np.zeros(n+1)
    mx[:, 1] += r
    return mx

def get_guide_line(r, n=3):
    mx = np.empty((n * 2, 2), dtype=np.float)
    for i in range(n):
        mx[i * 2] = np.array((r, 1 / 7 * i))
        mx[i * 2 + 1] = np.array((0, 0))
    mx[:, 1] += 1 / 14
    mx[:, 1] *= 2 * np.pi
    return mx

def trans_p_to_r(mx0, xc, yc):
    """
    transform Polar Coordinate into Rectangular
    :param mx0: the polar_point matrix(p_radii, 0_theta)
    :param xc: the location_x of the center point
    :param yc: the location_y of the center point
    :return: Rectangular coordinate == [x, y]
    """
    mx1 = np.empty((np.shape(mx0)))
    mx1[:, 0] = mx0[:, 0] * np.cos(mx0[:, 1]) + xc
    mx1[:, 1] = mx0[:, 0] * np.sin(mx0[:, 1]) + yc
    return mx1

def draw_line(plot, canv, color='#ffffff', size=1):
    """
    draw the line in the base canvas with plot
    :param color: color of line
    :param plot: plot point list, [x, y]
    :param canv: base canvas
    :param size: size of line
    :return: canvas for imshow
    """
    color_rbg = np.array((int(color[1:3], 16),
                          int(color[3:5], 16),
                          int(color[5:7], 16)), dtype=np.uint8)
    mx = np.empty(np.shape(canv))
    mx[:, :, :] = canv[:, :, :]
    # mx = canv
    for i5 in range(len(plot) - 1):
        xi, yi = plot[i5]
        xl, yl = plot[i5 + 1] - plot[i5]
        rl = max(abs(xl), abs(yl))
        dx = xl / rl
        dy = yl / rl

        for i2 in range(int(rl)):
            xx = int(xi + dx * i2)
            yy = int(yi + dy * i2)
            mx[xx:xx+size, yy:yy+size] = color_rbg
    return mx

# dynamic imshow
plot0 = base
for i0 in range(7):
    area_p = get_area(radii)  # Polar Coordinate
    area_p[:, 1] += np.pi * 2 / 7 * i0  # rotate 1/7 * 2 * pi
    area_r = trans_p_to_r(area_p, center, center)

    plot0 = draw_line(area_r, plot0, size=2)
    plotMODE.set_data(plot0)
    plt.title('%d - 0' % i0)
    plt.pause(0.01)

    area_p1 = area_p
    for i1 in range(1, gap):
        area_p1[:, 1] += np.pi * 2 / 7 / gap
        area_r1 = trans_p_to_r(area_p1, center, center)

        plot1 = draw_line(area_r1, plot0, size=1)
        plotMODE.set_data(plot1)
        plt.title('%d - %d' % (i0, i1))
        plt.pause(0.01)

plotMODE.set_data(plot0)
plt.pause(0.01)
plt.title('7-set Venn Diagram Finish')
plt.pause(0.01)
# plt.show()


另外,不管以何种方案画出来的n维Venn图,它最小区域的最大「宽度」都不会超过360°/(n*(n-2)) 当n=<2的时候,不会超过 360°/n

想要欣赏比较数学的证明方式,可以去看我上面提及的文章(在2013年画出了11维Venn图的↓)

Logic blooms with new 11-set Venn diagram

发布于 2017-12-26