본문 바로가기

Programing Language/Python

Networkx: 효과적인 node position with hierarchy (editing)

반응형

Visualization: e(연결선 시각화의 모든 것):editing

Python에서 plot을 하다 보면 가끔 연결선을 시각화해야하는 경우가 있다.

바로 다음과 같다.

  • Network를 그릴 때
  • Hierarchy를 표현할 때
  • diagram 또는 관계도를 그릴 때
  • 그외에 기타 등등

이런 경우에 시각화를 하다보면 맘에 드는 경우도 있고, 마음에 들지 않는 경우도 있다.

나는 첫 번째와 두 번째 경우가 모두 해당되는 경우였다. Louvain Method를 통해 군집에 대한 Hierarchy structure를 얻었고, 이를 시각화하기 위해 graphviz_layoutprog='dot'옵션을 사용해서 다음과 같은 이미지를 얻었는데, graphviz_layout에는 우선 두 가지 문제점이 있었다.

 

  1. position에 node size가 고려되지 않아 겹치는 부분이 발생했다.
  2. 상위 노드들이 너무 가운데 몰려있어서 군집 구조가 한눈에 들어오지 않는다.

이를 해결하기 위해 두 가지를 고려해야 한다.

  1. graphviz_layout(prog='dot')에서 밀집된 node를 펼쳐야 한다.
    1. 이 때 군집별로 domain_wall 같은 것이 있으면 더욱 좋을 것 같다.

먼저 graphviz_layout으로 얻었던 pos를 기반으로 가장 node가 많은 층을 중심으로, 위로 올라가면서 상위노드의 위치를 하위노드의 평균 위치로 설정해주는 코드를 작성했다.

 

최종 결과물은 다음과 같다.

 

Ref

edge option

Plot

fig = plt.figure(figsize=(24,8), dpi=200)
plt.title(f'Food clustering with Louvain Algorithm, THRESHOLD_N={THRESHOLD_N}')

nodes = G.nodes()
sizes = dict(G.nodes('size'))
colors = dict(G.nodes('dm_rate'))

# ec = nx.draw_networkx_edges(G, pos, alpha=0.4, arrows=False, 
#                             width=.5, edge_color='k', style='solid', arrowsize=5,
#                             connectionstyle="angle3,angleA=90,angleB=0")

for edge in edges:
    _from = pos2[edge[0]]
    _to = pos2[edge[1]]
    plt.annotate("",
                xy=_from, xycoords='data',
                xytext=_to, textcoords='data',
                arrowprops=dict(arrowstyle="<-", color="0.5",
                                shrinkA=5, shrinkB=5,
                                patchA=None, patchB=None,
                                connectionstyle="arc,angleA=90,angleB=-90,armA=40,armB=40,rad=10",
#                                 connectionstyle="arc,angleA=0,angleB=0,armA=20,armB=0,rad=0",
                                ),
                 zorder=-10
                )

nc = nx.draw_networkx_nodes(G, pos2, nodelist=nodes, 
#                             node_color = [v/100. for v in range(len(list(G.nodes)))],
                            node_color = [v for v in colors.values()],
#                             with_labels=False, 
#                             node_size=[v*100 for v in range(len(list(G.nodes)))]
                            node_size=[v for v in sizes.values()],
#                             alpha = [min(v/100., 1.) for v in sizes.values()], 
                            label=list(G.nodes), 
                            cmap=plt.cm.turbo, vmin = 0, vmax =.2,)
# nx.draw_networkx_labels(G, pos, font_size = [12 - 2*int(x[2]) for x in list(G.nodes)])
for node, (x, y) in pos2.items():
#     plt.text(x, y, f'{node.split(":")[-1]}', fontsize=12 - 2*int(node[2]), ha='center', va='center')
    plt.text(x, y, f'{node}', fontsize=12 - 2*int(node[2]), ha='center', va='center')
# plt.edgecolor('none')
nc.set_edgecolor('none')
plt.colorbar(nc, label='DM prevalence')
# plt.clim(0,1)
# plt.savefig('nx_test.png')
# plt.xlim(left=-20,right=7100)
plt.show()

Get position

d_depth = 55
min_gap = 70
gap_ratio = .8
max_depth = 6
THRESHOLD_N = 30

SIZE_CONST = .7
WEIGHT_CONST = 1
THRESHOLD_N = 30

new_pos = {}

G = nx.DiGraph()
G.add_node("lv0:0", size=6127*SIZE_CONST, dm_rate = 0.127795)

edges = []
for stt_lv in range(6):
# stt_lv = 0
    col_from = f'lv{stt_lv}'
    col_to = f'lv{stt_lv+1}'
    _from = df2[col_from]
    _to = df2[col_to]

    cnt = Counter([(f'{col_from}:{_from[i]}', f'{col_to}:{_to[i]}') for i in df.index])
    edge_list = cnt.most_common(1000000)
    dm_rate = df2.groupby(col_to)['DM'].mean()

    i = 0
    others = 0
    for (f, t), n in edge_list:
        if n > THRESHOLD_N:
            G.add_node(t, size=n*SIZE_CONST, dm_rate = dm_rate[i])
            G.add_edge(f, t, weight=n*WEIGHT_CONST)
            edges.append([f, t])
            i+=1

pos=graphviz_layout(G, prog='dot')

# Set Max_lv (main lavel)
_edges, _x, _df = get_mainlv_pos(edges, max_lv, min_gap, gap_ratio)
for i, edge in enumerate(_edges):
    new_pos[edge[1]] = (_x[i], d_depth*(max_depth - max_lv))

# Upper nodes
for lv in np.arange(max_lv)[::-1]:
    _edges, _x, _df = get_upperlv_pos(_df, edges, lv, min_gap, gap_ratio)
    for i, edge in enumerate(_edges):
        new_pos[edge[1]] = (_x[i], d_depth*(max_depth - lv))

# Mother node
lv = -1
_temp = _df.groupby('head').mean()['x_shift']
new_pos[_temp.index[0]] = (_temp.values[0], d_depth*(max_depth - lv))

# bottom nodes


# Get new pos
pos2 = pos.copy()
for node in new_pos.keys():
    pos2[node] = new_pos[node]

Related Function

def get_edges(edges, _from, option = 'all'):
    if option == 'from':
        return [e1 for e1, e2 in edges if (e1[2] == f'{_from}') & (e2[2] == f'{_from+1}')]
    elif option == 'to':
        return [e2 for e1, e2 in edges if (e1[2] == f'{_from}') & (e2[2] == f'{_from+1}')]
    elif option == 'all':
        return [(e1, e2) for e1, e2 in edges if (e1[2] == f'{_from}') & (e2[2] == f'{_from+1}')]

##### Def max_lv pos ####
def get_mainlv_pos(edges, lv, min_gap = 60 , gap_ratio = .2):
    '''
    min_gap = 80 #min_size +20
    gap_gr = min_gap*gap_ratio
    '''
    # Set Params
    gap_gr = min_gap*gap_ratio

    _edges = get_edges(edges, lv)
    _edges = np.array(_edges)

    xs = []
    for edge in _edges:
        x = pos[edge[1]]
        xs.append(x[0])

    df = pd.DataFrame()
    df['edge'] = get_edges(edges, lv)
    df['head'] = _edges[:,0]
    df['x'] = xs
    df = df.sort_values('x')
    _edges = df['edge'].values

    # Get new pos with domain-wall
    _xs = []
    _head_bf = 0
    _x = min_gap
    for i in range(len(df)):
        _head = int(df['head'].values[i][-1])
        if _head_bf != _head:
            _head_bf = _head
            _x+=gap_gr
        _xs.append(_x)
        _x+=min_gap

    # df['x_shift'] = np.arange(len(df))*min_gap + min_gap
    df['x_shift'] = _xs
    return _edges, _xs, df

def get_upperlv_pos(df, edges, lv, min_gap = 80 , gap_ratio = .4):

    # Set Params
    gap_gr = min_gap*gap_ratio
    df_gr = df.groupby('head').mean().sort_values('x_shift')

    _edges = get_edges(edges, lv)
    _edges = np.array(_edges)

    xs = []
    for edge in _edges:
        x = pos[edge[1]]
        xs.append(x[0])


    df = pd.DataFrame()
    df['edge'] = get_edges(edges, lv)
    df['head'] = _edges[:,0]
    df['offs'] = _edges[:,1]
    df['x'] = xs
    df = df.sort_values('x')
    _edges = df['edge'].values

    _xs = []
    for offs in df['offs'].values:
        try:
            _xs.append(df_gr.loc[offs]['x_shift'])
        except:
            _xs.append(-1) #do not have any offspring

    # Fix pos which do not have any offspring
    x_new = -1
    for i, x in enumerate(_xs):
        if x < 0:
            _xs[i] = x_new
        else:
            x_new = x
        x_new += min_gap

    df['x_shift'] = _xs

    return _edges, _xs, df
반응형