GraphGist: Finding Influencers in a Social Network

Use Case(s)

Introduction

For this Graph Gist I am going to be analysing the interaction between users of a social network, such as Twitter or Facebook.

Many examples using social networks focus on friends and recommendations, I will instead focus on how people use the network and try to establish different types of behaviours and find influencers. This is important as the owner of a social network you want to know who the power users are, or as a user to know who might be a good person to follow. You could also you this to identify shoppers on an e-commerce site who have influence through their reviews.

Model

The model for my graph looks like this:

Figure 1. How popular is Alice?

Nodes

User: A user of a social network.

Message: A message is SENT between User`s and have `FORWARD and REPLY_TO relationships with other `Message`s.

Relationships

FOLLOWS: `User`s follow other `User`s.

SENT: `User`s send `Message`s to other `User`s

FORWARD: A Mesage may be a forwarded version of another Message

REPLY_TO: A Message may be a reply to another Message

Setup

CREATE (nAlice:User {id:'Alice', name:'Node Alice'})
,(nBridget:User {id:'Bridget', name:'Node Bridget'})
,(nCharles:User {id:'Charles', name:'Node Charles'})
,(nDoug:User {id:'Doug', name:'Node Doug'})
,(nMark:User {id:'Mark', name:'Node Mark'})
,(nNancy:User {id:'Nancy', name:'Node Nancy'})
,(nOliver:User {id:'Oliver', name:'Node Oliver'})

CREATE (nAlice)-[:FOLLOWS]->(nBridget)
,(nAlice)-[:FOLLOWS]->(nCharles)
,(nAlice)-[:FOLLOWS]->(nDoug)
,(nBridget)-[:FOLLOWS]->(nAlice)
,(nBridget)-[:FOLLOWS]->(nCharles)
,(nBridget)-[:FOLLOWS]->(nDoug)
,(nCharles)-[:FOLLOWS]->(nAlice)
,(nCharles)-[:FOLLOWS]->(nBridget)
,(nCharles)-[:FOLLOWS]->(nDoug)
,(nDoug)-[:FOLLOWS]->(nAlice)
,(nDoug)-[:FOLLOWS]->(nBridget)
,(nDoug)-[:FOLLOWS]->(nCharles)
,(nMark)-[:FOLLOWS]->(nNancy)
,(nMark)-[:FOLLOWS]->(nOliver)
,(nNancy)-[:FOLLOWS]->(nMark)
,(nNancy)-[:FOLLOWS]->(nOliver)
,(nOliver)-[:FOLLOWS]->(nMark)
,(nOliver)-[:FOLLOWS]->(nNancy)
,(nCharles)-[:FOLLOWS]->(nMark)
,(nMark)-[:FOLLOWS]->(nCharles)

CREATE (msg1:Message {id:'1', contents:'This is a message', day_sent: 'Monday'})
,(msg2:Message {id:'2', contents:'This is a message', day_sent: 'Monday'})
,(msg3:Message {id:'3', contents:'This is a message', day_sent: 'Monday'})
,(msg4:Message {id:'4', contents:'This is a message', day_sent: 'Monday'})
,(msg5:Message {id:'5', contents:'This is a message', day_sent: 'Monday'})
,(msg6:Message {id:'6', contents:'This is a message', day_sent: 'Monday'})
,(msg7:Message {id:'7', contents:'This is a message', day_sent: 'Monday'})
,(msg8:Message {id:'8', contents:'This is a message', day_sent: 'Monday'})
,(msg9:Message {id:'9', contents:'This is a message', day_sent: 'Tuesday'})
,(msg10:Message {id:'10', contents:'This is a message', day_sent: 'Tuesday'})
,(msg11:Message {id:'11', contents:'This is a message', day_sent: 'Tuesday'})
,(msg12:Message {id:'12', contents:'This is a message', day_sent: 'Tuesday'})
,(msg13:Message {id:'13', contents:'This is a message', day_sent: 'Tuesday'})
,(msg14:Message {id:'14', contents:'This is a message', day_sent: 'Monday'})
,(msg15:Message {id:'15', contents:'This is a message', day_sent: 'Thursday'})
,(msg16:Message {id:'16', contents:'This is a message', day_sent: 'Thursday'})
,(msg17:Message {id:'17', contents:'This is a message', day_sent: 'Thursday'})
,(msg18:Message {id:'18', contents:'This is a message', day_sent: 'Thursday'})
,(msg19:Message {id:'19', contents:'This is a message', day_sent: 'Thursday'})
,(msg20:Message {id:'20', contents:'Conversation Starter', day_sent: 'Friday'})
,(msg21:Message {id:'21', contents:'Conversation Starter', day_sent: 'Friday'})
,(msg22:Message {id:'22', contents:'Conversation Starter', day_sent: 'Friday'})
,(msg23:Message {id:'23', contents:'Conversation Starter', day_sent: 'Friday'})
,(msg24:Message {id:'24', contents:'Reply', day_sent: 'Friday'})
,(msg25:Message {id:'25', contents:'Reply', day_sent: 'Friday'})
,(msg26:Message {id:'26', contents:'Reply', day_sent: 'Friday'})
,(msg27:Message {id:'27', contents:'Reply', day_sent: 'Friday'})
,(msg28:Message {id:'28', contents:'Reply', day_sent: 'Friday'})
,(msg29:Message {id:'29', contents:'Reply', day_sent: 'Friday'})
,(msg30:Message {id:'30', contents:'Reply', day_sent: 'Friday'})
,(msg31:Message {id:'31', contents:'Reply', day_sent: 'Friday'})
,(msg32:Message {id:'32', contents:'Reply', day_sent: 'Friday'})
,(msg33:Message {id:'33', contents:'Reply', day_sent: 'Friday'})
,(msg34:Message {id:'34', contents:'Reply', day_sent: 'Friday'})
,(msg35:Message {id:'35', contents:'Conversation Starter', day_sent: 'Saturday'})
,(msg36:Message {id:'36', contents:'Conversation Starter', day_sent: 'Saturday'})
,(msg37:Message {id:'37', contents:'Reply', day_sent: 'Saturday'})
,(msg38:Message {id:'38', contents:'Reply', day_sent: 'Saturday'})
,(msg39:Message {id:'39', contents:'Reply', day_sent: 'Saturday'})
,(msg40:Message {id:'40', contents:'Reply', day_sent: 'Saturday'})
,(msg41:Message {id:'41', contents:'Reply', day_sent: 'Saturday'})
,(msg42:Message {id:'42', contents:'Reply', day_sent: 'Saturday'})
,(msg43:Message {id:'43', contents:'Reply', day_sent: 'Saturday'})
,(msg44:Message {id:'44', contents:'Reply', day_sent: 'Saturday'})
,(msg45:Message {id:'45', contents:'Reply', day_sent: 'Saturday'})
,(msg46:Message {id:'46', contents:'Reply', day_sent: 'Saturday'})
,(msg47:Message {id:'47', contents:'Reply', day_sent: 'Saturday'})
,(msg48:Message {id:'48', contents:'Reply', day_sent: 'Saturday'})
,(msg49:Message {id:'49', contents:'Reply', day_sent: 'Sunday'})
,(msg50:Message {id:'50', contents:'Reply', day_sent: 'Sunday'})
,(msg51:Message {id:'51', contents:'Reply', day_sent: 'Sunday'})
,(msg52:Message {id:'52', contents:'Reply', day_sent: 'Sunday'})
,(msg53:Message {id:'53', contents:'Reply', day_sent: 'Sunday'})
,(msg54:Message {id:'54', contents:'Conversation Starter', day_sent: 'Sunday'})
,(msg55:Message {id:'55', contents:'Conversation Starter', day_sent: 'Sunday'})
,(msg56:Message {id:'56', contents:'Conversation Starter', day_sent: 'Sunday'})
,(msg57:Message {id:'57', contents:'Reply', day_sent: 'Sunday'})
,(msg58:Message {id:'58', contents:'Reply', day_sent: 'Sunday'})
,(msg59:Message {id:'59', contents:'Reply', day_sent: 'Sunday'})
,(msg60:Message {id:'60', contents:'Reply', day_sent: 'Sunday'})
,(msg61:Message {id:'61', contents:'Reply', day_sent: 'Sunday'})
,(msg62:Message {id:'62', contents:'Reply', day_sent: 'Sunday'})
,(msg63:Message {id:'63', contents:'Reply', day_sent: 'Sunday'})
,(msg64:Message {id:'64', contents:'Reply', day_sent: 'Sunday'})
,(msg65:Message {id:'65', contents:'Reply', day_sent: 'Sunday'})
,(msg66:Message {id:'66', contents:'Reply', day_sent: 'Sunday'})
,(msg67:Message {id:'67', contents:'Reply', day_sent: 'Sunday'})
,(msg68:Message {id:'68', contents:'Reply', day_sent: 'Sunday'})
,(msg69:Message {id:'69', contents:'Reply', day_sent: 'Sunday'})
,(nAlice)-[:SENT]->(msg2)
,(nBridget)-[:SENT]->(msg3)
,(nBridget)-[:SENT]->(msg4)
,(nBridget)-[:SENT]->(msg5)
,(nCharles)-[:SENT]->(msg6)
,(nDoug)-[:SENT]->(msg7)
,(nDoug)-[:SENT]->(msg8)
,(nAlice)-[:SENT]->(msg9)
,(nBridget)-[:SENT]->(msg10)
,(nCharles)-[:SENT]->(msg11)
,(nDoug)-[:SENT]->(msg12)
,(nDoug)-[:SENT]->(msg13)
,(nAlice)-[:SENT]->(msg14)
,(nAlice)-[:SENT]->(msg15)
,(nAlice)-[:SENT]->(msg16)
,(nAlice)-[:SENT]->(msg17)
,(nBridget)-[:SENT]->(msg18)
,(nCharles)-[:SENT]->(msg19)
,(nBridget)-[:SENT]->(msg20)
,(nBridget)-[:SENT]->(msg21)
,(nNancy)-[:SENT]->(msg22)
,(nMark)-[:SENT]->(msg23)
,(nAlice)-[:SENT]->(msg24)
,(nBridget)-[:SENT]->(msg25)
,(nAlice)-[:SENT]->(msg26)
,(nBridget)-[:SENT]->(msg27)
,(nAlice)-[:SENT]->(msg28)
,(nNancy)-[:SENT]->(msg29)
,(nMark)-[:SENT]->(msg30)
,(nAlice)-[:SENT]->(msg31)
,(nBridget)-[:SENT]->(msg32)
,(nBridget)-[:SENT]->(msg33)
,(nMark)-[:SENT]->(msg34)
,(nMark)-[:SENT]->(msg35)
,(nAlice)-[:SENT]->(msg36)
,(nAlice)-[:SENT]->(msg37)
,(nBridget)-[:SENT]->(msg38)
,(nMark)-[:SENT]->(msg39)
,(nMark)-[:SENT]->(msg40)
,(nBridget)-[:SENT]->(msg41)
,(nCharles)-[:SENT]->(msg42)
,(nBridget)-[:SENT]->(msg43)
,(nAlice )-[:SENT]->(msg44)
,(nCharles)-[:SENT]->(msg45)
,(nDoug)-[:SENT]->(msg46)
,(nDoug)-[:SENT]->(msg47)
,(nMark)-[:SENT]->(msg48)
,(nAlice)-[:SENT]->(msg49)
,(nMark)-[:SENT]->(msg50)
,(nAlice)-[:SENT]->(msg51)
,(nBridget)-[:SENT]->(msg52)
,(nAlice)-[:SENT]->(msg53)
,(nAlice)-[:SENT]->(msg54)
,(nAlice)-[:SENT]->(msg55)
,(nAlice)-[:SENT]->(msg56)
,(nCharles)-[:SENT]->(msg57)
,(nAlice)-[:SENT]->(msg58)
,(nCharles)-[:SENT]->(msg59)
,(nAlice)-[:SENT]->(msg60)
,(nCharles)-[:SENT]->(msg61)
,(nCharles)-[:SENT]->(msg62)
,(nBridget)-[:SENT]->(msg63)
,(nCharles)-[:SENT]->(msg64)
,(nMark)-[:SENT]->(msg65)
,(nMark)-[:SENT]->(msg66)
,(nCharles)-[:SENT]->(msg67)
,(nBridget)-[:SENT]->(msg68)
,(nCharles)-[:SENT]->(msg69)
CREATE (msg5)-[:FORWARD]->(msg2)
,(msg6)-[:FORWARD]->(msg2)
,(msg7)-[:FORWARD]->(msg3)
,(msg8)-[:FORWARD]->(msg1)
,(msg11)-[:FORWARD]->(msg10)
,(msg12)-[:FORWARD]->(msg10)
,(msg13)-[:FORWARD]->(msg11)
,(msg14)-[:FORWARD]->(msg3)
,(msg15)-[:FORWARD]->(msg4)
,(msg16)-[:FORWARD]->(msg5)
,(msg17)-[:FORWARD]->(msg10)
,(msg18)-[:FORWARD]->(msg6)
,(msg46)-[:FORWARD]->(msg39)
,(msg47)-[:FORWARD]->(msg40)

CREATE (msg24)-[:REPLY_TO]->(msg20)
,(msg25)-[:REPLY_TO]->(msg24)
,(msg26)-[:REPLY_TO]->(msg25)
,(msg27)-[:REPLY_TO]->(msg26)
,(msg28)-[:REPLY_TO]->(msg27)
,(msg29)-[:REPLY_TO]->(msg21)
,(msg30)-[:REPLY_TO]->(msg21)
,(msg31)-[:REPLY_TO]->(msg21)
,(msg32)-[:REPLY_TO]->(msg28)
,(msg33)-[:REPLY_TO]->(msg30)
,(msg34)-[:REPLY_TO]->(msg33)
,(msg37)-[:REPLY_TO]->(msg35)
,(msg38)-[:REPLY_TO]->(msg35)
,(msg39)-[:REPLY_TO]->(msg37)
,(msg40)-[:REPLY_TO]->(msg38)
,(msg41)-[:REPLY_TO]->(msg38)
,(msg42)-[:REPLY_TO]->(msg37)
,(msg43)-[:REPLY_TO]->(msg37)
,(msg44)-[:REPLY_TO]->(msg43)
,(msg45)-[:REPLY_TO]->(msg42)
,(msg48)-[:REPLY_TO]->(msg36)
,(msg49)-[:REPLY_TO]->(msg48)
,(msg50)-[:REPLY_TO]->(msg49)
,(msg51)-[:REPLY_TO]->(msg41)
,(msg52)-[:REPLY_TO]->(msg50)
,(msg53)-[:REPLY_TO]->(msg52)
,(msg57)-[:REPLY_TO]->(msg54)
,(msg58)-[:REPLY_TO]->(msg57)
,(msg59)-[:REPLY_TO]->(msg58)
,(msg60)-[:REPLY_TO]->(msg59)
,(msg61)-[:REPLY_TO]->(msg60)
,(msg62)-[:REPLY_TO]->(msg55)
,(msg63)-[:REPLY_TO]->(msg62)
,(msg64)-[:REPLY_TO]->(msg63)
,(msg65)-[:REPLY_TO]->(msg64)
,(msg66)-[:REPLY_TO]->(msg56)
,(msg67)-[:REPLY_TO]->(msg66)
,(msg68)-[:REPLY_TO]->(msg67)
,(msg69)-[:REPLY_TO]->(msg68)

Use Cases

For our analysis, let’s begin simple by listing all of the `User`s and `Message`s:

MATCH path=(User)-[:SENT]->(Message)
RETURN path
Loading graph...

As you can see, it is hard to spot any patterns from this view.

Finding User Counts

Remember our goal here is to find the influencers in the network, we could start with the most simple measure which is the number of people who follow a user.

MATCH (follower:User)-[:FOLLOWS]->(targetUser:User)-[:FOLLOWS]->(following:User)
RETURN targetUser AS User, COUNT(distinct follower) AS Followers, COUNT(distinct following) AS Following

For a bit more information we could provide the names of all of the followers:

MATCH (p:User)-[f:FOLLOWS]->(p1:User)
RETURN p.id AS User, COLLECT(p1.id) AS Following

While this is interesting, it doesn’t tell us much about the actions of a user. The user they may be inactive, or they may send multiple messages a day.

We can easily see how active users are with the following query:

MATCH (p:User)-[:SENT]->(tweet:Message)
RETURN p.id AS User, COUNT(tweet) AS Tweets

We can now get an idea of how active a user is, but let us dive deeper and see what sort of activity they have.

Looking at forwarded messages

One measure of influence is how often a message from a user gets forwarded throughout the network, so let’s find the most forwarded messages:

MATCH (retweet:Message)-[r:FORWARD]->(tweet:Message)
RETURN tweet, COUNT(r)
ORDER BY COUNT(r) DESC

we can restrict to a certain day by limiting the messages we look at:

MATCH (retweet:Message)-[r:FORWARD]->(tweet:Message {day_sent:'Monday'})
RETURN tweet, COUNT(r)
ORDER BY COUNT(r) DESC

Remember that we are trying to find the influencers, so we need to know who sent those messages:

MATCH (retweet:Message)-[r:FORWARD]->(tweet:Message)<-[:SENT]-(p:User)
RETURN p.id AS User, COUNT(r) AS `Messages Retweeted`
ORDER BY COUNT(r)
DESC LIMIT 5
Loading table...

From this we can see that Bridget gets lots of her messages forwarded, but Mark’s message got more forwards.

If you are a user of Twitter or a similar social network, you will be aware that there are lots of bots on Twitter that simply forward messages. We want to remove these bots from our analysis.

MATCH (p:User)-[s:SENT]->(tweet:Message)-[retweet:FORWARD]->(tweet1:Message), (p:User)-[s2:SENT]->(tweet2:Message)
WITH p, COUNT(DISTINCT tweet) AS forwards, COUNT(DISTINCT tweet2) AS messages
WHERE (forwards*1.00)/messages > 0.8
RETURN p.id AS `Potential Bot`, (forwards*1.00)/messages*100 AS `Percent of Messages that are Retweets`
ORDER BY `Percent Retweeted` DESC

As you can see, Doug only forwards messages so is probably a bot. To get a better idea of influence we need to remove him and any other bots from the analysis:

MATCH (p:User)-[s:SENT]->(tweet:Message)-[retweet:FORWARD]->(tweet1:Message), (p:User)-[s2:SENT]->(tweet2:Message)
WITH p, COUNT(DISTINCT tweet) AS forwards, COUNT(DISTINCT tweet2) AS messages
WHERE (forwards*1.00)/messages < 0.8
WITH p
MATCH (p)-[s:SENT]->(tweet:Message)-[rt:FORWARD]->(tweet1:Message)<-[:SENT]-(p1:User)
RETURN p1.id as User, COUNT(tweet) as `Retweeted Messages`
ORDER BY COUNT(tweet)
DESC LIMIT 15
Loading table...

Note we now look for users for whom forwards make up LESS THAN 80% of their messages.

As you can see this shows a slightly different picture, as Mark only had messages forwarded by bots. The reason I want to remove the forwarders from the analysis is that a human forwarding will do some filtering and only forward things they like.

We now have a couple of measures of influence, based on follower count and how many forwards a user gets.

There is a third measure that I want to investigate which is how often a user starts a conversation or discussion on Twitter and amongst how many people.

Finding conversations

Finding conversations is a good measure of influence a it shows people want to engage with that user.

To begin this analysis, let’s start by getting a list of conversations, note that I have restricted the length of the conversation path, you may want to consider extending for your use case.

MATCH p=(tweet:Message)-[r:REPLY_TO*1..10]->(conversation:Message)
RETURN p
Loading graph...

We can restrict this to a single conversation:

MATCH (tweet:Message {id:'20'})<-[r:REPLY_TO*0..10]-(conversation:Message)
RETURN DISTINCT(conversation) AS Conversation
ORDER BY conversation.date_sent

Note the DISTINCT(conversation), which will ensure we only get one of each message in our response.

Now that we have a list of our conversations, let us dive deeper.

Get a list of messages that start a conversation, that is messages that someone has replied to:

MATCH (tweet:Message)-[r:REPLY_TO]->(conversation:Message)
WHERE NOT (conversation)-[:REPLY_TO]->()
RETURN DISTINCT conversation

and find out who sent the messages that started the conversation:

MATCH (tweet:Message)-[r:REPLY_TO]->(conversation:Message)<-[s:SENT]-(p:User)
WHERE NOT (conversation)-[:REPLY_TO]->()
RETURN DISTINCT conversation, p.id AS `Conversation Starter`

Build on this to get a list of the users that a user will engage with and respond to as this shows that there is more than a shallow 'Follow' relationship.

MATCH conv=(b:User)-[:SENT]->(tweet:Message)-[:REPLY_TO]->(tweet1:Message)<-[:SENT]-(a:User)-[:SENT]->(tweet2:Message)-[:REPLY_TO]->(tweet)
RETURN a.id AS `Conversationalist 1`, b.id AS `Conversationalist 2`, COUNT(DISTINCT conv) AS Conversations
ORDER BY a.id ASC

We also want to get an idea of how large the conversations are and how many people are involved in them, a long conversation involving lots of people shows more signs of influence than a short conversation with a couple of people.

MATCH (tweet:Message)-[r:REPLY_TO]->(conversation:Message)<-[s:SENT]-(p:User)
WHERE NOT (conversation)-[:REPLY_TO]->()
WITH DISTINCT conversation,p
MATCH conv=(participant:User)-[:SENT]->(tweet:Message)-[r:REPLY_TO*0..10]->(conversation)
RETURN conversation, p, AS `Distinct Tweets`, COUNT(DISTINCT participant) AS `Distinct Participants`

Finally modify the query again to add the number of conversations started by the user.

MATCH (tweet:Message)-[r:REPLY_TO]->(conversation:Message)<-[s:SENT]-(p:User)
WHERE NOT (conversation)-[:REPLY_TO]->()
WITH DISTINCT conversation,p
MATCH conv=(participant:User)-[:SENT]->(tweet:Message)-[r:REPLY_TO*0..10]->(conversation)
WITH conversation, p, COUNT(DISTINCT tweet) AS messageCount, COUNT(DISTINCT participant) AS participantCount
WHERE participantCount > 2
RETURN p.id, COUNT(p) AS `Conversations`, AVG(messageCount) AS `Average Length`, AVG(participantCount) AS `Average Participants`
Loading table...

As you can see, Alice starts more conversations, but the conversation Mark started had more engagement. You will need to determine yourself which of these has greater influence in your network.

Conclusion

As you can see, Neo4j is a powerful tool for analysing social networks and you can use some of the values above to observe who the influencers are in your network.

Running queries, preparing the console!

Run
Table
Graph
Table!
Graph!
Error!
Loading