[tools] Sankey

plotly Sankey 不太好用,好多跟0有关的奇怪bug,但目前也没有替代的,只能先凑合用了。 import import plotly.graph objects as go import plotly.io as pio 关于data df 如果放在一个df里,然后就是很简单的连接(同一行就连),那node的数量就是len df x column

plotly Sankey 不太好用,好多跟0有关的奇怪bug,但目前也没有替代的,只能先凑合用了。

import

import plotly.graph_objects as go
import plotly.io as pio

关于data df

如果放在一个df里,然后就是很简单的连接(同一行就连),那node的数量就是len(df) x column number,连接的话source就是range(0到len(df)),target就是range(len(df) , len(df)x2)

画图

Sankey里主要定义三个东西:arrangement, node, link

arrangement

里面有三个选项,perpendicular是只能垂直拉node,freeform随便拽,fixed是固定的。

node

pad

pad普通的话就定15,如果你想让link变得很细,在node很多的情况下,可以设置pad=100,再高了也不会有什么变化。

thickness=10就可以。

label (node label)

label是node的label,是list,从第一个node到最后一个node,如果不需要label,就用‘’放在list里。

比如:

label=['']*len(df) + df[gene_col].tolist()

color (node的颜色)

是list,可以是['red','blue' ....]就是不透明的,也可以用透明的,那就用rgba,rgba的第四个value代表transparency。

比如:

df['rgba_colors'] = df.direction.map({'up':"rgba(255, 99, 71, 0.5)",'down':"rgba(38, 101, 255, 0.5)"})

# in Sankey function
color=df.rgba_colors.tolist()

x (node 横坐标)

这是这里面最无语的地方,首先它的value必须设置在0-1之间,如果你有两列,在左边和右边,左边是0右边是1的话,左边0的那个y坐标就会没用,但右边1的那个y坐标却可以用。所以不能设置0,设置成0.01,右边不知道有没有什么奇怪的bug,干脆设置成0.99

比如:

x=[0.01] * len(df) + [0.99] * len(df)

y(node 纵坐标)

value range从0到1,需要画图前normalize,最炒蛋的地方是,它也不识别0!!

所以把里面的0都替换成1e-13即可。1没什么问题。

另外,记得invert value,因为它用的html语言,0是最高的,1是最低的!!日

normalize的方法如下

  # Combine control and experiment values into a single array for joint normalization
  all_values = pd.concat([df[col1], df[col2]])

  # Normalize all values together
  normalized_values = (all_values - all_values.min()) / (all_values.max() - all_values.min())

  normalized_values = (1-normalized_values)+1e-13

  # Split back into control and experiment
  df[f'{col1}_norm'] = normalized_values[:len(df)]
  df[f'{col2}_norm'] = normalized_values[len(df):]

customdata 和 hovertemplate (鼠标移到node可显示的value)

customdata的格式list in list, 类似numpy,shape是 [[node1_value1, node1_value2],[node2_value1, node2_value2]]

hovertemplate的格式是str,里面可以放{label}, 可以放{customdata[value_number]},分行就用
是html语言。

比如:

hovertemplate='Gene: %{label}<br>Value: %{customdata[1]:.2f}'

source和target (node之间的连法)

source, list of integer,比如[0,1,2,3,4]

target, list of integer, 比如[5,6,7,8,9]

上面的例子就是第一个连接是从node 0到node 5,第二个连接是从node 1 到node 6。

value (连接的宽度)

list of values,会自动normalize到0到1,所以如果全部设置成0.1或者1其实没有区别,但是如果一些设置成小的数,一些设置成大的数,就能看出区别。

color (连接的颜色)

一般用透明的,那就用rgba,rgba的第四个value代表transparency。

比如:

df['rgba_colors'] = df.direction.map({'up':"rgba(255, 99, 71, 0.5)",'down':"rgba(38, 101, 255, 0.5)"})

# in Sankey function
color=df.rgba_colors.tolist()

layout

font_size (node label的字体大小)

width&height 大概在(1000,500)

margin是个dictionary , 用的是html里的t (top), b(buttom), l(left), r(right)

比如:

margin=dict(r=300)

例子

def plot_sankey(df, gene_col, col1, col2,figsize=(1000,600),write=False,link_colors=None,link_values=None):
  df = df.copy()

  # Combine control and experiment values into a single array for joint normalization
  all_values = pd.concat([df[col1], df[col2]])

  # Normalize all values together
  normalized_values = (all_values - all_values.min()) / (all_values.max() - all_values.min())

  normalized_values = (1-normalized_values)+1e-13

  # Split back into control and experiment
  df[f'{col1}_norm'] = normalized_values[:len(df)]
  df[f'{col2}_norm'] = normalized_values[len(df):]

  # Define the source (control) and target (experiment) nodes
  sources = list(range(len(df)))  # Control nodes (one per gene)
  targets = list(range(len(df), 2 * len(df)))  # Experiment nodes (one per gene)

  customdata = [[0, orig_y, inv_y] for orig_y, inv_y in zip(df[col1], df[f'{col1}_norm'])] + \
              [[0.5, orig_y, inv_y] for orig_y, inv_y in zip(df[col2], df[f'{col2}_norm'])]

  # Create the Sankey diagram with y-axis positions reflecting joint-normalized values and uniform height
  fig = go.Figure(go.Sankey(
      arrangement = "perpendicular",
      node=dict(
          pad=15,
          thickness=10,  # Uniform thickness for all nodes
          line=dict(color="black", width=0.5),
          label=['']*len(df) + df[gene_col].tolist(),  # Labels for control and experiment nodes
          color=["blue"] * len(df)*2 if not link_colors else link_colors*2,  # Blue for control, green for experiment
          x=[0.01] * len(df) + [0.99] * len(df),  # Left (x=0) for control, right (x=1) for experiment
          y=df[f'{col1}_norm'].tolist() + df[f'{col2}_norm'].tolist(),  # Y positions based on joint-normalized values
          customdata=customdata,
          hovertemplate='Gene: %{label}<br>Value: %{customdata[1]:.2f}'

      ),
      link=dict(
          source=sources,  # Link control nodes to experiment nodes
          target=targets,
          value=[0.01]*(len(df)) if not link_values else link_values,  # the expression is relative, changing 1 to 0.1 to all would not change as no variation
          color="rgba(0, 150, 255, 0.5)" if not link_colors else link_colors # Color of the links
      )
  ))

  # Update layout with titles
  fig.update_layout(
      title_text="Gene Expression Changes",
      font_size=10,
      autosize=False,
      width=figsize[0],
      height=figsize[1],
      margin=dict(r=300)
  )
  
  # Save the figure as an HTML file
  if write:
    pio.write_html(fig, file="sankey_diagram.html", auto_open=False)
  # Show the figure
  fig.show()