Assigning multiple attributes to nodes

Viewed 711

I would like to assign to my nodes an attribute. Currently I am creating a network using the following sample of data:

Attribute   Source       Target Weight  Label
    87.5    Heisenberg   Pauli  66.3    1
    12.5    Beckham      Messi  38.1    0
    12.5    Beckham      Maradona 12    0
    43.5    water        melon  33.6    1

Label should give the colour of nodes (1=yellow, 0=blue).

Code for network:

 G = nx.from_pandas_edgelist(df, source='Source', target='Target', edge_attr='Weight') 

    collist = df.drop('Weight', axis=1).melt('Label').dropna() # I need this for the below lines of code because I want to draw nodes - their size - based on their degree

    degrees=[]
    for x in collist['value']:
        deg=G.degree[x]  
        degrees.append(100*deg)

    
    pos=nx.spring_layout(G)

    nx.draw_networkx_labels(G, pos, font_size=10)
    nx.draw_networkx_nodes(G, pos, nodelist=collist['value'], node_size = degrees, node_color=collist['Label'])
    nx.draw_networkx_edges(G, pos)

What this code is supposed to do is the following: the nodes should have size equal their degree (this explains degrees and collist in my code). Edges should have thickness equal to Weight. Attribute should be assigned (and updated) as in this link: (Changing attributes of nodes). Currently, my code does not include the assignment as in the link mentioned, where it was added and updated as follows:

G = nx.Graph()
G.add_node(0, weight=8)
G.add_node(1, weight=5)
G.add_node(2, weight=3)
G.add_node(3, weight=2)

nx.add_path(G, [2,5])
nx.add_path(G, [2,3])


labels = {
    n: str(n) + '\nweight=' + str(G.nodes[n]['weight']) if 'weight' in G.nodes[n] else str(n)
    for n in G.nodes
}

newWeights = \
    [
        sum( # summ for averaging
            [G.nodes[neighbor]['weight'] for neighbor in G.neighbors(node)] # weight of every neighbor
            + [G.nodes[i]['weight']] # adds the node itsself to the average
        ) / (len(list(G.neighbors(node)))+1) # average over number of neighbours+1
        if len(list(G.neighbors(node))) > 0 # if there are no neighbours
        else G.nodes[i]['weight'] # weight stays the same if no neighbours
    for i,node in enumerate(G.nodes) # do the above for every node
    ]
print(newWeights) 
for i, node in enumerate(G.nodes):
    G.nodes[i]['weight'] = newWeights[i] # writes new weights after it calculated them all.

Please note that I have more than 100 nodes so I cannot do it manually. I tried to include the Attribute in my code as follows:

G = nx.from_pandas_edgelist(df_net, source='Source', target='Target', edge_attr=['Weight'])
    nx.set_node_attributes(G, pd.Series(nodes.Attribute, index=nodes.node).to_dict(), 'Attribute')

However, I have got the error:

----> 1 network(df)

<ipython-input-72-f68985d20046> in network(dataset)
     24     degrees=[]
     25     for x in collist['value']:
---> 26         deg=G.degree[x]
     27         degrees.append(100*deg)
     28 

~/opt/anaconda3/lib/python3.8/site-packages/networkx/classes/reportviews.py in __getitem__(self, n)
    445     def __getitem__(self, n):
    446         weight = self._weight
--> 447         nbrs = self._succ[n]
    448         if weight is None:
    449             return len(nbrs) + (n in nbrs)

KeyError: 87.5

What I would like to have as expected output is a network where nodes are in the Source column and their neighbors are within the Target column. Edges have thickness based on Weight. Label gives the colour of the source, while Attribute value should be added as label and updated as in the question/answer on this link: Changing attributes of nodes .

Please see below a visual example of the type of net that I am trying to build. The attribute value in the figure is meant before the update (newWeights), and this explains why some nodes have missing value. Attribute is related to Source only, which is colored based on Label. The thickness of the edge is given by Weight.

enter image description here

3 Answers
import networkx as nx
import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame({"Attribute": [87.5, 12.5, 12.5, 43.5], "Source": ["Heisenberg", "Beckham", "Messi", "water"], "Target" : ["Pauli", "Messi", "Maradona", "melon"], "Weight" : [66.3, 38.1, 12, 33.6], "Label" : [1, 0, 0,1]})

G = nx.from_pandas_edgelist(df, source='Source', target='Target', edge_attr='Weight')

source_attrs = {df.Source[i]: {"Attribute": df.Attribute[i]} for i in range(len(df.Attribute))}

target_attrs = {df.Target[i]: {"Attribute": df.Attribute[i]} for i in range(len(df.Attribute))}



nx.set_node_attributes(G, source_attrs)
nx.set_node_attributes(G, target_attrs)

degrees=[100*G.degree[i] for i in G.nodes()]
weights = [G[u][v]['Weight']/10 for u,v in G.edges()]
colors = []
for node in G.nodes():
    if node in source_attrs.keys():
        colors.append('yellow')
    else:
        colors.append('blue')

pos=nx.spring_layout(G)

pos_attrs = {}
for node, coords in pos.items():
    pos_attrs[node] = (coords[0], coords[1] + 0.08)

labels = nx.get_node_attributes(G, "Attribute")

custom_node_attrs = {}
for node, attr in labels.items():
    custom_node_attrs[node] = str(node) + str(attr)

nx.draw_networkx_labels(G, pos_attrs, labels=custom_node_attrs, font_size=10)

nx.draw_networkx_nodes(G, pos, nodelist=G.nodes(), node_size = degrees, node_color=colors)

nx.draw_networkx_edges(G,pos, width=weights)

plt.show()


enter image description here

From your given example data frame and the desired output image, I created the following solution:


import pandas as pd
import networkx as nx
import matplotlib.pylab as pl

df = pd.DataFrame(
    data=[[87.5, "Heisenberg", "Pauli", 66.3, 1, ],
          [12.5, "Beckham", "Messi", 38.1, 0, ],
          [12.5, "Beckham", "Maradona", 12, 0, ],
          [43.5, "water", "melon", 33.6, 1, ]],
    columns=["Attribute", "Source", "Target", "Weight", "Label"]
)

# 1 Creating the graph
G = nx.from_pandas_edgelist(df, source='Source', target='Target', edge_attr='Weight')

# 2 Adding the node attributes for the source nodes
nx.set_node_attributes(G, {node: df.Attribute[i] for i, node in enumerate(df.Source)}, 'Attribute')
nx.set_node_attributes(G, {node: df.Label[i] for i, node in enumerate(df.Source)}, 'Label')

# (optional) checking the created data 
print(G.nodes(data=True))
# [('Heisenberg', {'Attribute': 87.5, 'Label': 1}), ('Pauli', {}), ('Beckham', {'Attribute': 12.5, 'Label': 0}), ('Messi', {}), ('Maradona', {}), ('water', {'Attribute': 43.5, 'Label': 1}), ('melon', {})]
print(G.edges(data=True))
# [('Heisenberg', 'Pauli', {'Weight': 66.3}), ('Beckham', 'Messi', {'Weight': 38.1}), ('Beckham', 'Maradona', {'Weight': 12.0}), ('water', 'melon', {'Weight': 33.6})]

# 3 fine tuning the visualisation 
degrees = [100 * G.degree[i] for i in G.nodes()]
# not sure what should be the color if no label is available
color_dict = {0: "blue", 1: "yellow", "default": "yellow"}
node_colors = []
labels = {}
for node in G:
    label = node + "\n Attribute="
    if "Attribute" in G.nodes[node]:
        label += str(G.nodes[node]["Attribute"])
    labels[node] = label

    if "Label" in G.nodes[node]:
        node_colors.append(color_dict[G.nodes[node]["Label"]])
    else:
        node_colors.append(color_dict["default"])

# you can use any other layout e.g spring_layout
pos = nx.circular_layout(G)
nx.draw_networkx(G,
                 pos,
                 node_color=node_colors,
                 node_size=degrees,
                 width=[edge_info[2]/10 for edge_info in G.edges(data="Weight")],
                 labels=labels,
                 )

# 4 Adjustments for node labels partially cut
axis = pl.gca()
# zoom out
# maybe smaller factors work as well, but 1.3 works fine for this minimal example
axis.set_xlim([1.3*x for x in axis.get_xlim()])
axis.set_ylim([1.3*y for y in axis.get_ylim()])
# turn off frame
pl.axis("off")
pl.show()

With the following result

Output Figure

Explanation

The main step for creating the network are the following:

  1. The easiest part is the creation of the basic network with nx.from_pandas_edgelist, which already added edge weights.
  2. Afterwards, adding the node attributes using nx.set_node_attributes.
  3. Then, the graph is completely created and all following code should only operate on the graph, e.g. G.nodes(). Hence, for adjusting the visualisation only loops over G.
  4. Finally, I fine tuned the created matplotlib figure to avoid cutting of the labels.

edge_attr argument of from_pandas_edgelist() can accept a list

G = nx.from_pandas_edgelist(df, source='Source', target='Target', edge_attr=['Weight', 'Attribute']) 

What's the purpose of your melt? If you want to see label of each node, you can use

df['Node'] = df['Source'].str.cat(df['Target'], sep=' ').str.split(' ')
df = df.explode('Node')
print(df)

   Attribute      Source    Target  Weight  Label        Node
0       87.5  Heisenberg     Pauli    66.3      1  Heisenberg
0       87.5  Heisenberg     Pauli    66.3      1       Pauli
1       12.5     Beckham     Messi    38.1      1     Beckham
1       12.5     Beckham     Messi    38.1      1       Messi
2       23.5     Beckham  Maradona    12.0      0     Beckham
2       23.5     Beckham  Maradona    12.0      0    Maradona
3       43.5       water     melon    33.6      1       water
3       43.5       water     melon    33.6      1       melon

But there are duplicate nodes having different label, you need to choose which to keep.

Related