根据标签值垂直重新排列 Sankey 图

san*_*juh 7 python plotly sankey-diagram

我正在尝试在Sankey 图中绘制 3 个集群之间的患者流量。我有一个counts带有 from-to 值的 pd.DataFrame ,见下文。为了重现这个 DF,这里counts应该加载到 pd.DataFrame的dict(这是visualize_cluster_flow_counts 函数的输入)。

    from    to      value
0   C1_1    C1_2    867
1   C1_1    C2_2    405
2   C1_1    C0_2    2
3   C2_1    C1_2    46
4   C2_1    C2_2    458
... ... ... ...
175 C0_20   C0_21   130
176 C0_20   C2_21   1
177 C2_20   C1_21   12
178 C2_20   C0_21   0
179 C2_20   C2_21   96
Run Code Online (Sandbox Code Playgroud)

DataFrame 中的fromto值表示集群编号(0、1 或 2)和 x 轴的天数(介于 1 和 21 之间)。如果我用这些值绘制桑基图,结果如下: 桑基情节

代码:

import plotly.graph_objects as go

def visualize_cluster_flow_counts(counts):
    all_sources = list(set(counts['from'].values.tolist() + counts['to'].values.tolist()))            
    
    froms, tos, vals, labs = [], [], [], []
    for index, row in counts.iterrows():
        froms.append(all_sources.index(row.values[0]))
        tos.append(all_sources.index(row.values[1]))
        vals.append(row[2])
        labs.append(row[3])
                
    fig = go.Figure(data=[go.Sankey(
        arrangement='snap',
        node = dict(
          pad = 15,
          thickness = 5,
          line = dict(color = "black", width = 0.1),
          label = all_sources,
          color = "blue"
        ),
        link = dict(
          source = froms,
          target = tos,
          value = vals,
          label = labs
      ))])

    fig.update_layout(title_text="Patient flow between clusters over time: 48h (2 days) - 504h (21 days)", font_size=10)
    fig.show()

visualize_cluster_flow_counts(counts)
Run Code Online (Sandbox Code Playgroud)

但是,我想对条形进行垂直排序,以便 C0始终在顶部,C1始终在中间,而 C2始终在底部(或相反,无关紧要)。我知道,我们可以设置node.xnode.y手动指定的坐标。因此,我将 x 值设置为天数 *(1/天数范围),增量为 +- 0.045。我根据集群值设置 y 值:0、0.5 或 1。然后我获得了下面的图像。垂直顺序很好,但条形之间的垂直边距明显偏离;它们应该与第一个结果相似。

在此处输入图片说明

产生这个的代码是:

import plotly.graph_objects as go

def find_node_coordinates(sources):
    x_nodes, y_nodes = [], []
    
    for s in sources:
        # Shift each x with +- 0.045
        x = float(s.split("_")[-1]) * (1/21)
        x_nodes.append(x)
        
        # Choose either 0, 0.5 or 1 for the y-value
        cluster_number = s[1]
        if cluster_number == "0": y = 1
        elif cluster_number == "1": y = 0.5
        else: y = 1e-09
        
        y_nodes.append(y)
                
    return x_nodes, y_nodes


def visualize_cluster_flow_counts(counts):
    all_sources = list(set(counts['from'].values.tolist() + counts['to'].values.tolist()))    
        
    node_x, node_y = find_node_coordinates(all_sources)
    
    froms, tos, vals, labs = [], [], [], []
    for index, row in counts.iterrows():
        froms.append(all_sources.index(row.values[0]))
        tos.append(all_sources.index(row.values[1]))
        vals.append(row[2])
        labs.append(row[3])
                
    fig = go.Figure(data=[go.Sankey(
        arrangement='snap',
        node = dict(
          pad = 15,
          thickness = 5,
          line = dict(color = "black", width = 0.1),
          label = all_sources,
          color = "blue",
          x = node_x,
          y = node_y,
        ),
        link = dict(
          source = froms,
          target = tos,
          value = vals,
          label = labs
      ))])

    fig.update_layout(title_text="Patient flow between clusters over time: 48h (2 days) - 504h (21 days)", font_size=10)
    fig.show()
    
    
visualize_cluster_flow_counts(counts)
Run Code Online (Sandbox Code Playgroud)

问题:如何修复条形的边距,使结果看起来像第一个结果?因此,为了清楚起见:应该将条形推到底部。或者还有另一种方法可以让桑基图根据标签值自动对条形图进行垂直重新排序?

Par*_*ano 1

首先,我认为当前公开的API没有办法顺利实现你的目标,你可以在这里查看源代码。

尝试find_node_coordinates按如下方式更改您的函数(请注意,您应该将计数传递给DataFrame):

counts = pd.DataFrame(counts_dict) 
def find_node_coordinates(sources, counts):
    x_nodes, y_nodes = [], []

    flat_on_top = False
    range = 1 # The y range
    total_margin_width = 0.15
    y_range = 1 - total_margin_width 
    margin = total_margin_width / 2 # From number of  Cs
    srcs = counts['from'].values.tolist()
    dsts = counts['to'].values.tolist() 
    values = counts['value'].values.tolist() 
    max_acc = 0

    def _calc_day_flux(d=1):
        _max_acc = 0 
        for i in [0,1,2]:
            # The first ones
            from_source = 'C{}_{}'.format(i,d) 
            indices = [i for i, val in enumerate(srcs) if val == from_source]
            for j in indices: 
                _max_acc += values[j]
        
        return _max_acc

    def _calc_node_io_flux(node_str): 
        c,d = int(node_str.split('_')[0][-1]), int(node_str.split('_')[1])
        _flux_src = 0 
        _flux_dst = 0 

        indices_src = [i for i, val in enumerate(srcs) if val == node_str]
        indices_dst = [j for j, val in enumerate(dsts) if val == node_str]
        for j in indices_src: 
            _flux_src += values[j]
        for j in indices_dst: 
            _flux_dst += values[j]

        return max(_flux_dst, _flux_src) 

    max_acc = _calc_day_flux() 
    graph_unit_per_val = y_range / max_acc
    print("Graph Unit per Acc Val", graph_unit_per_val) 
 
    
    for s in sources:
        # Shift each x with +- 0.045
        d = int(s.split("_")[-1])
        x = float(d) * (1/21)
        x_nodes.append(x)
        
        print(s, _calc_node_io_flux(s))
        # Choose either 0, 0.5 or 1 for the y-v alue
        cluster_number = s[1]

        
        # Flat on Top
        if flat_on_top: 
            if cluster_number == "0": 
              y = _calc_node_io_flux('C{}_{}'.format(2, d))*graph_unit_per_val + margin + _calc_node_io_flux('C{}_{}'.format(1, d))*graph_unit_per_val + margin +  _calc_node_io_flux('C{}_{}'.format(0, d))*graph_unit_per_val/2
            elif cluster_number == "1": y = _calc_node_io_flux('C{}_{}'.format(2, d))*graph_unit_per_val + margin +  _calc_node_io_flux('C{}_{}'.format(1, d))*graph_unit_per_val/2
            else: y = 1e-09
        # Flat On Bottom
        else: 
            if cluster_number == "0": y = 1 - (_calc_node_io_flux('C{}_{}'.format(0,d))*graph_unit_per_val / 2)
            elif cluster_number == "1": y = 1 - (_calc_node_io_flux('C{}_{}'.format(0,d))*graph_unit_per_val + margin + _calc_node_io_flux('C{}_{}'.format(1,d)) * graph_unit_per_val /2 )
            elif cluster_number == "2": y = 1 - (_calc_node_io_flux('C{}_{}'.format(0,d))*graph_unit_per_val + margin + _calc_node_io_flux('C{}_{}'.format(1,d)) * graph_unit_per_val + margin + _calc_node_io_flux('C{}_{}'.format(2,d)) * graph_unit_per_val /2 )
            
        y_nodes.append(y)
                
    return x_nodes, y_nodes

Run Code Online (Sandbox Code Playgroud)

桑基图应该通过相应的标准化值来衡量连接宽度,对吗?这里我也做了同样的事情,首先,它计算每个节点的通量,然后通过计算每个节点的中心根据其通量计算的归一化坐标。

这是带有修改后的函数的代码的示例输出,请注意,我尝试尽可能地遵循您的代码,因此它有点未优化(例如,可以存储每个指定源节点上方的节点值,以避免其通量重新计算)。

带旗帜flat_on_top = True 在此输入图像描述

带旗帜flat_on_top = False 在此输入图像描述

版本中存在一些不一致,flat_on_bottom我认为这是由 Plotly API 的填充或其他内部来源引起的。