【问题标题】:Parallel Coordinates plot in MatplotlibMatplotlib 中的平行坐标图
【发布时间】:2012-01-04 01:31:02
【问题描述】:

使用传统绘图类型可以相对直接地查看二维和三维数据。即使是四维数据,我们也经常能找到一种显示数据的方法。然而,超过四个的尺寸变得越来越难以显示。幸运的是,parallel coordinates plots 提供了一种查看更高维度结果的机制。

几个绘图包提供平行坐标图,例如MatlabRVTK type 1VTK type 2,但我不知道如何使用 Matplotlib 创建一个。

  1. Matplotlib 中是否有内置的平行坐标图?我当然没有看到in the gallery
  2. 如果没有内置类型,是否可以使用 Matplotlib 的标准功能构建平行坐标图?

编辑

基于下面Zhenya提供的答案,我开发了以下支持任意数量轴的泛化。按照我在上面的原始问题中发布的示例的绘图样式,每个轴都有自己的比例。我通过对每个轴点的数据进行归一化并使轴的范围为 0 到 1 来实现这一点。然后我返回并将标签应用于每个刻度线,在该截距处给出正确的值。

该函数通过接受可迭代的数据集来工作。每个数据集被认为是一组点,其中每个点位于不同的轴上。 __main__ 中的示例在两组 30 行中为每个轴获取随机数。线条在导致线条聚集的范围内是随机的;我想验证的行为。

此解决方案不如内置解决方案,因为您有奇怪的鼠标行为,而且我通过标签伪造数据范围,但在 Matplotlib 添加内置解决方案之前,它是可以接受的。

#!/usr/bin/python
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

def parallel_coordinates(data_sets, style=None):

    dims = len(data_sets[0])
    x    = range(dims)
    fig, axes = plt.subplots(1, dims-1, sharey=False)

    if style is None:
        style = ['r-']*len(data_sets)

    # Calculate the limits on the data
    min_max_range = list()
    for m in zip(*data_sets):
        mn = min(m)
        mx = max(m)
        if mn == mx:
            mn -= 0.5
            mx = mn + 1.
        r  = float(mx - mn)
        min_max_range.append((mn, mx, r))

    # Normalize the data sets
    norm_data_sets = list()
    for ds in data_sets:
        nds = [(value - min_max_range[dimension][0]) / 
                min_max_range[dimension][2] 
                for dimension,value in enumerate(ds)]
        norm_data_sets.append(nds)
    data_sets = norm_data_sets

    # Plot the datasets on all the subplots
    for i, ax in enumerate(axes):
        for dsi, d in enumerate(data_sets):
            ax.plot(x, d, style[dsi])
        ax.set_xlim([x[i], x[i+1]])

    # Set the x axis ticks 
    for dimension, (axx,xx) in enumerate(zip(axes, x[:-1])):
        axx.xaxis.set_major_locator(ticker.FixedLocator([xx]))
        ticks = len(axx.get_yticklabels())
        labels = list()
        step = min_max_range[dimension][2] / (ticks - 1)
        mn   = min_max_range[dimension][0]
        for i in xrange(ticks):
            v = mn + i*step
            labels.append('%4.2f' % v)
        axx.set_yticklabels(labels)


    # Move the final axis' ticks to the right-hand side
    axx = plt.twinx(axes[-1])
    dimension += 1
    axx.xaxis.set_major_locator(ticker.FixedLocator([x[-2], x[-1]]))
    ticks = len(axx.get_yticklabels())
    step = min_max_range[dimension][2] / (ticks - 1)
    mn   = min_max_range[dimension][0]
    labels = ['%4.2f' % (mn + i*step) for i in xrange(ticks)]
    axx.set_yticklabels(labels)

    # Stack the subplots 
    plt.subplots_adjust(wspace=0)

    return plt


if __name__ == '__main__':
    import random
    base  = [0,   0,  5,   5,  0]
    scale = [1.5, 2., 1.0, 2., 2.]
    data = [[base[x] + random.uniform(0., 1.)*scale[x]
            for x in xrange(5)] for y in xrange(30)]
    colors = ['r'] * 30

    base  = [3,   6,  0,   1,  3]
    scale = [1.5, 2., 2.5, 2., 2.]
    data.extend([[base[x] + random.uniform(0., 1.)*scale[x]
                 for x in xrange(5)] for y in xrange(30)])
    colors.extend(['b'] * 30)

    parallel_coordinates(data, style=colors).show()

编辑 2:

以下是绘制Fisher's Iris data 时上述代码的示例。它不如 Wikipedia 中的参考图像那么好,但如果你只有 Matplotlib 并且你需要多维绘图,它是可以接受的。

【问题讨论】:

  • +1 好问题!我确信 #2 的答案是肯定的,但我不知道它是容易还是困难。
  • 获取线路将很简单。获得坐标轴可能会更困难。
  • 不错的功能。我用我的离散数据集进行了尝试,但是轴上的标签在奇怪的位置上浮动,就像你的情节一样,为什么会这样?

标签: python matplotlib parallel-coordinates


【解决方案1】:

我确信有更好的方法,但这里有一个快速和肮脏的方法(一个非常肮脏的方法):

#!/usr/bin/python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

#vectors to plot: 4D for this example
y1=[1,2.3,8.0,2.5]
y2=[1.5,1.7,2.2,2.9]

x=[1,2,3,8] # spines

fig,(ax,ax2,ax3) = plt.subplots(1, 3, sharey=False)

# plot the same on all the subplots
ax.plot(x,y1,'r-', x,y2,'b-')
ax2.plot(x,y1,'r-', x,y2,'b-')
ax3.plot(x,y1,'r-', x,y2,'b-')

# now zoom in each of the subplots 
ax.set_xlim([ x[0],x[1]])
ax2.set_xlim([ x[1],x[2]])
ax3.set_xlim([ x[2],x[3]])

# set the x axis ticks 
for axx,xx in zip([ax,ax2,ax3],x[:-1]):
  axx.xaxis.set_major_locator(ticker.FixedLocator([xx]))
ax3.xaxis.set_major_locator(ticker.FixedLocator([x[-2],x[-1]]))  # the last one

# EDIT: add the labels to the rightmost spine
for tick in ax3.yaxis.get_major_ticks():
  tick.label2On=True

# stack the subplots together
plt.subplots_adjust(wspace=0)

plt.show()

这基本上是基于 Joe Kingon 的(更好的)一个,Python/Matplotlib - Is there a way to make a discontinuous axis?。您可能还想查看同一问题的其他答案。

在此示例中,我什至不尝试缩放垂直比例,因为这取决于您想要实现的具体目标。

编辑:这是结果

【讨论】:

  • @Simon: 是的,你需要dim-1 subplots
  • 不错的方法。在我的显示器上,最后一个轴上没有任何单位。您如何将单位添加到最后一个轴?我还有另外两个问题,但它们可能超出了这个问题的范围。 1)你能标记轴吗? 2) 交互式绘图上的“移动”工具允许每个轴独立于其他轴移动。你能限制它们一起移动并且只在 y 维度上移动吗?
  • @Nathan:我已经编辑了答案,在最右边的轴上添加了刻度线。有多种方法可以标记轴——参见例如这个,stackoverflow.com/questions/6236238/…。关于你的第二个问题,我不知道,所以如果你把它作为一个单独的问题,那么更有知识的人有机会看到它。
  • @Zhenya 这是一个很棒的起点!我正在努力完善细节,并将在接下来的几天内发布完整的解决方案。我现在的挂断是处理垂直轴。为了匹配我在问题中引用的数字,每个轴都必须有一个独立的比例。但是,子图方法在整个子图中具有独立的比例。为了获得与轴相关的比例,我对每个轴上的数据进行了标准化,然后将自定义标签应用于每个轴。最后一个子图我还需要 twinx,所以最后两个轴是独立的。
【解决方案2】:

pandas 有一个平行坐标包装器:

import pandas
import matplotlib.pyplot as plt
from pandas.tools.plotting import parallel_coordinates

data = pandas.read_csv(r'C:\Python27\Lib\site-packages\pandas\tests\data\iris.csv', sep=',')
parallel_coordinates(data, 'Name')
plt.show()

源代码,他们是如何做到的:plotting.py#L494

【讨论】:

  • 每个轴可以独立缩放吗?如果我有多个轴的比例非常不同(比如 0 到 1 和 0 到 1e6),则统一轴缩放会导致不可读的图。
  • 有没有办法把它变成交互式工具?
  • 例如,通过从数据中划分出一些常数并记下图例中的常数,可能会破坏每个轴的缩放比例。
  • @gradi3nt 这在实践中并没有真正起作用,因为,至少在上图中,只有一个轴上有单位。您还需要以某种方式表示其他轴上的单位,以使缩放成为一种实用的解决方案。
  • from pandas.tools.plotting import parallel_coordinates 现已弃用,弃用警告建议使用 from pandas.plotting import parallel_coordinates 代替(但仍然完全一样)。
【解决方案3】:

使用 pandas 时(如 theta 建议的那样),无法独立缩放轴。

你找不到不同的垂直轴的原因是因为没有。我们的平行坐标只是通过绘制一条垂直线和一些标签来“伪造”其他两个轴。

https://github.com/pydata/pandas/issues/7083#issuecomment-74253671

【讨论】:

    【解决方案4】:

    到目前为止我见过的最好的例子就是这个

    https://python.g-node.org/python-summerschool-2013/_media/wiki/datavis/olympics_vis.py

    参见 normalised_coordinates 函数。不是超级快,但我已经尝试过。

    normalised_coordinates(['VAL_1', 'VAL_2', 'VAL_3'], np.array([[1230.23, 1500000, 12453.03], [930.23, 140000, 12453.03], [130.23, 120000, 1243.03]]), [1, 2, 1])
    

    【讨论】:

      【解决方案5】:

      离完美还很远,但它可以工作并且相对较短:

      import numpy as np
      
      import matplotlib.pyplot as plt
      
      def plot_parallel(data,labels):
      
          data=np.array(data)
          x=list(range(len(data[0])))
          fig, axis = plt.subplots(1, len(data[0])-1, sharey=False)
      
      
          for d in data:
              for i, a in enumerate(axis):
                  temp=d[i:i+2].copy()
                  temp[1]=(temp[1]-np.min(data[:,i+1]))*(np.max(data[:,i])-np.min(data[:,i]))/(np.max(data[:,i+1])-np.min(data[:,i+1]))+np.min(data[:,i])
                  a.plot(x[i:i+2], temp)
      
      
          for i, a in enumerate(axis):
              a.set_xlim([x[i], x[i+1]])
              a.set_xticks([x[i], x[i+1]])
              a.set_xticklabels([labels[i], labels[i+1]], minor=False, rotation=45)
              a.set_ylim([np.min(data[:,i]),np.max(data[:,i])])
      
      
          plt.subplots_adjust(wspace=0)
      
          plt.show()
      

      【讨论】:

        【解决方案6】:

        在回答相关问题时,我制定了一个版本,仅使用一个子图(因此它可以很容易地与其他图结合在一起),并且可以选择使用三次贝塞尔曲线来连接点。绘图会自行调整到所需的轴数。

        import matplotlib.pyplot as plt
        from matplotlib.path import Path
        import matplotlib.patches as patches
        import numpy as np
        
        fig, host = plt.subplots()
        
        # create some dummy data
        ynames = ['P1', 'P2', 'P3', 'P4', 'P5']
        N1, N2, N3 = 10, 5, 8
        N = N1 + N2 + N3
        category = np.concatenate([np.full(N1, 1), np.full(N2, 2), np.full(N3, 3)])
        y1 = np.random.uniform(0, 10, N) + 7 * category
        y2 = np.sin(np.random.uniform(0, np.pi, N)) ** category
        y3 = np.random.binomial(300, 1 - category / 10, N)
        y4 = np.random.binomial(200, (category / 6) ** 1/3, N)
        y5 = np.random.uniform(0, 800, N)
        
        # organize the data
        ys = np.dstack([y1, y2, y3, y4, y5])[0]
        ymins = ys.min(axis=0)
        ymaxs = ys.max(axis=0)
        dys = ymaxs - ymins
        ymins -= dys * 0.05  # add 5% padding below and above
        ymaxs += dys * 0.05
        dys = ymaxs - ymins
        
        # transform all data to be compatible with the main axis
        zs = np.zeros_like(ys)
        zs[:, 0] = ys[:, 0]
        zs[:, 1:] = (ys[:, 1:] - ymins[1:]) / dys[1:] * dys[0] + ymins[0]
        
        
        axes = [host] + [host.twinx() for i in range(ys.shape[1] - 1)]
        for i, ax in enumerate(axes):
            ax.set_ylim(ymins[i], ymaxs[i])
            ax.spines['top'].set_visible(False)
            ax.spines['bottom'].set_visible(False)
            if ax != host:
                ax.spines['left'].set_visible(False)
                ax.yaxis.set_ticks_position('right')
                ax.spines["right"].set_position(("axes", i / (ys.shape[1] - 1)))
        
        host.set_xlim(0, ys.shape[1] - 1)
        host.set_xticks(range(ys.shape[1]))
        host.set_xticklabels(ynames, fontsize=14)
        host.tick_params(axis='x', which='major', pad=7)
        host.spines['right'].set_visible(False)
        host.xaxis.tick_top()
        host.set_title('Parallel Coordinates Plot', fontsize=18)
        
        colors = plt.cm.tab10.colors
        for j in range(N):
            # to just draw straight lines between the axes:
            # host.plot(range(ys.shape[1]), zs[j,:], c=colors[(category[j] - 1) % len(colors) ])
        
            # create bezier curves
            # for each axis, there will a control vertex at the point itself, one at 1/3rd towards the previous and one
            #   at one third towards the next axis; the first and last axis have one less control vertex
            # x-coordinate of the control vertices: at each integer (for the axes) and two inbetween
            # y-coordinate: repeat every point three times, except the first and last only twice
            verts = list(zip([x for x in np.linspace(0, len(ys) - 1, len(ys) * 3 - 2, endpoint=True)],
                             np.repeat(zs[j, :], 3)[1:-1]))
            # for x,y in verts: host.plot(x, y, 'go') # to show the control points of the beziers
            codes = [Path.MOVETO] + [Path.CURVE4 for _ in range(len(verts) - 1)]
            path = Path(verts, codes)
            patch = patches.PathPatch(path, facecolor='none', lw=1, edgecolor=colors[category[j] - 1])
            host.add_patch(patch)
        plt.tight_layout()
        plt.show()
        

        这里是 iris 数据集的类似代码。第二个轴被反转以避免一些交叉线。

        import matplotlib.pyplot as plt
        from matplotlib.path import Path
        import matplotlib.patches as patches
        import numpy as np
        from sklearn import datasets
        
        iris = datasets.load_iris()
        ynames = iris.feature_names
        ys = iris.data
        ymins = ys.min(axis=0)
        ymaxs = ys.max(axis=0)
        dys = ymaxs - ymins
        ymins -= dys * 0.05  # add 5% padding below and above
        ymaxs += dys * 0.05
        
        ymaxs[1], ymins[1] = ymins[1], ymaxs[1]  # reverse axis 1 to have less crossings
        dys = ymaxs - ymins
        
        # transform all data to be compatible with the main axis
        zs = np.zeros_like(ys)
        zs[:, 0] = ys[:, 0]
        zs[:, 1:] = (ys[:, 1:] - ymins[1:]) / dys[1:] * dys[0] + ymins[0]
        
        fig, host = plt.subplots(figsize=(10,4))
        
        axes = [host] + [host.twinx() for i in range(ys.shape[1] - 1)]
        for i, ax in enumerate(axes):
            ax.set_ylim(ymins[i], ymaxs[i])
            ax.spines['top'].set_visible(False)
            ax.spines['bottom'].set_visible(False)
            if ax != host:
                ax.spines['left'].set_visible(False)
                ax.yaxis.set_ticks_position('right')
                ax.spines["right"].set_position(("axes", i / (ys.shape[1] - 1)))
        
        host.set_xlim(0, ys.shape[1] - 1)
        host.set_xticks(range(ys.shape[1]))
        host.set_xticklabels(ynames, fontsize=14)
        host.tick_params(axis='x', which='major', pad=7)
        host.spines['right'].set_visible(False)
        host.xaxis.tick_top()
        host.set_title('Parallel Coordinates Plot — Iris', fontsize=18, pad=12)
        
        colors = plt.cm.Set2.colors
        legend_handles = [None for _ in iris.target_names]
        for j in range(ys.shape[0]):
            # create bezier curves
            verts = list(zip([x for x in np.linspace(0, len(ys) - 1, len(ys) * 3 - 2, endpoint=True)],
                             np.repeat(zs[j, :], 3)[1:-1]))
            codes = [Path.MOVETO] + [Path.CURVE4 for _ in range(len(verts) - 1)]
            path = Path(verts, codes)
            patch = patches.PathPatch(path, facecolor='none', lw=2, alpha=0.7, edgecolor=colors[iris.target[j]])
            legend_handles[iris.target[j]] = patch
            host.add_patch(patch)
        host.legend(legend_handles, iris.target_names,
                    loc='lower center', bbox_to_anchor=(0.5, -0.18),
                    ncol=len(iris.target_names), fancybox=True, shadow=True)
        plt.tight_layout()
        plt.show()
        

        【讨论】:

        • 这看起来很棒!它所需要的只是一个函数接口,使其更易于使用。
        【解决方案7】:

        plotly 有一个很好的交互式解决方案,名为 parallel_coordinates,效果很好:

        import plotly.express as px
        df = px.data.iris()
        fig = px.parallel_coordinates(df, color="species_id", labels={"species_id": "Species",
                        "sepal_width": "Sepal Width", "sepal_length": "Sepal Length",
                        "petal_width": "Petal Width", "petal_length": "Petal Length", },
                                     color_continuous_scale=px.colors.diverging.Tealrose,
                                     color_continuous_midpoint=2)
        fig.show()
        

        【讨论】:

          【解决方案8】:

          我已将 @JohanC 代码改编为 pandas 数据框,并将其扩展为也可以处理分类变量。代码需要更多改进,比如能够将数字变量作为数据框中的第一个变量,但我认为现在还不错。

          
          # Paths:
          path_data = "data/"
          
          # Packages:
          import numpy as np
          import pandas as pd
          import matplotlib.pyplot as plt
          from matplotlib.colors import LinearSegmentedColormap
          from matplotlib.path import Path
          import matplotlib.patches as patches
          from functools import reduce
          
          # Display options:
          pd.set_option("display.width", 1200)
          pd.set_option("display.max_columns", 300)
          pd.set_option("display.max_rows", 300)
          
          # Dataset:
          df = pd.read_csv(path_data + "nasa_exoplanets.csv")
          df_varnames = pd.read_csv(path_data + "nasa_exoplanets_var_names.csv")
          
          # Variables (the first variable must be categoric):
          my_vars = ["discoverymethod", "pl_orbper", "st_teff", "disc_locale", "sy_gaiamag"]
          my_vars_names = reduce(pd.DataFrame.append,
                                 map(lambda i: df_varnames[df_varnames["var"] == i], my_vars))
          my_vars_names = my_vars_names["var_name"].values.tolist()
          
          # Adapt the data:
          df = df.loc[df["pl_letter"] == "d"]
          df_plot = df[my_vars]
          df_plot = df_plot.dropna()
          df_plot = df_plot.reset_index(drop = True)
          
          # Convert to numeric matrix:
          ym = []
          dics_vars = []
          for v, var in enumerate(my_vars):
              if df_plot[var].dtype.kind not in ["i", "u", "f"]:
                  dic_var = dict([(val, c) for c, val in enumerate(df_plot[var].unique())])
                  dics_vars += [dic_var]
                  ym += [[dic_var[i] for i in df_plot[var].tolist()]]
              else:
                  ym += [df_plot[var].tolist()]
          ym = np.array(ym).T
          
          # Padding:
          ymins = ym.min(axis = 0)
          ymaxs = ym.max(axis = 0)
          dys = ymaxs - ymins
          ymins -= dys*0.05
          ymaxs += dys*0.05
          
          # Reverse some axes for better visual:
          axes_to_reverse = [0, 1]
          for a in axes_to_reverse:
              ymaxs[a], ymins[a] = ymins[a], ymaxs[a]
          dys = ymaxs - ymins
          
          # Adjust to the main axis:
          zs = np.zeros_like(ym)
          zs[:, 0] = ym[:, 0]
          zs[:, 1:] = (ym[:, 1:] - ymins[1:])/dys[1:]*dys[0] + ymins[0]
          
          # Colors:
          n_levels = len(dics_vars[0])
          my_colors = ["#F41E1E", "#F4951E", "#F4F01E", "#4EF41E", "#1EF4DC", "#1E3CF4", "#F41EF3"]
          cmap = LinearSegmentedColormap.from_list("my_palette", my_colors)
          my_palette = [cmap(i/n_levels) for i in np.array(range(n_levels))]
          
          # Plot:
          fig, host_ax = plt.subplots(
              figsize = (20, 10),
              tight_layout = True
          )
          
          # Make the axes:
          axes = [host_ax] + [host_ax.twinx() for i in range(ym.shape[1] - 1)]
          dic_count = 0
          for i, ax in enumerate(axes):
              ax.set_ylim(
                  bottom = ymins[i],
                  top = ymaxs[i]
              )
              ax.spines.top.set_visible(False)
              ax.spines.bottom.set_visible(False)
              ax.ticklabel_format(style = 'plain')
              if ax != host_ax:
                  ax.spines.left.set_visible(False)
                  ax.yaxis.set_ticks_position("right")
                  ax.spines.right.set_position(
                      (
                          "axes",
                           i/(ym.shape[1] - 1)
                       )
                  )
              if df_plot.iloc[:, i].dtype.kind not in ["i", "u", "f"]:
                  dic_var_i = dics_vars[dic_count]
                  ax.set_yticks(
                      range(len(dic_var_i))
                  )
                  ax.set_yticklabels(
                      [key_val for key_val in dics_vars[dic_count].keys()]
                  )
                  dic_count += 1
          host_ax.set_xlim(
              left = 0,
              right = ym.shape[1] - 1
          )
          host_ax.set_xticks(
              range(ym.shape[1])
          )
          host_ax.set_xticklabels(
              my_vars_names,
              fontsize = 14
          )
          host_ax.tick_params(
              axis = "x",
              which = "major",
              pad = 7
          )
          
          # Make the curves:
          host_ax.spines.right.set_visible(False)
          host_ax.xaxis.tick_top()
          for j in range(ym.shape[0]):
              verts = list(zip([x for x in np.linspace(0, len(ym) - 1, len(ym)*3 - 2, 
                                                       endpoint = True)],
                           np.repeat(zs[j, :], 3)[1: -1]))
              codes = [Path.MOVETO] + [Path.CURVE4 for _ in range(len(verts) - 1)]
              path = Path(verts, codes)
              color_first_cat_var = my_palette[dics_vars[0][df_plot.iloc[j, 0]]]
              patch = patches.PathPatch(
                  path,
                  facecolor = "none",
                  lw = 2,
                  alpha = 0.7,
                  edgecolor = color_first_cat_var
              )
              host_ax.add_patch(patch)
          
          

          【讨论】:

          • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-27
          • 2017-07-22
          • 1970-01-01
          • 2014-01-28
          相关资源
          最近更新 更多