High School Friendship Networks
Table of Contents
The Departure
This looks at data provided by SocioPatterns that looks a the interactions between students at a High School in Marseilles, France, during the month of December in 2013. In particular, I'm going to look at the Friendship Networks data - wherein the students reported who they were friends with.
Imports
From Python
from argparse import Namespace
from collections import Counter
from functools import partial
from pathlib import Path
import os
From PyPi
from bokeh.models import HoverTool, TapTool
from dotenv import load_dotenv
from holoviews import dim, opts
from holoviews.operation.datashader import bundle_graph
import holoviews
import hvplot.pandas
import networkx
import pandas as pandas
My Stuff
from graeae.timers import Timer
from graeae.tables import CountPercentage
from graeae.visualization import EmbedHoloview
Load the Dotenv
load_dotenv(".env")
Build the Timer
TIMER = Timer()
Setup The Plotting
holoviews.extension("bokeh")
SLUG = "high-school-friendship-networks/"
output = Path("../../files/posts/networks/" + SLUG)
Embed = partial(EmbedHoloview, folder_path=output)
Plot = Namespace(
width = 1000,
height = 800,
graph_width = 800,
graph_height = 800,
small = 550,
fontsize = 18,
edge_color = "RoyalBlue",
padding = dict(x=(-1.1, 1.1),
y=(-1.1, 1.1)),
)
The Initiation
Meta-Data
Let's take a look at the meta-data before loading it into pandas.
HIGH_SCHOOL = Path(os.environ.get("HIGH_SCHOOL")).expanduser()
assert HIGH_SCHOOL.is_dir()
#+begin_src python :session highschool :results none
class Files:
metadata = "metadata_2013.txt"
contact_diaries = "Contact-diaries-network_data_2013.csv"
facebook = "Facebook-known-pairs_data_2013.csv"
friendship = "Friendship-network_data_2013.csv"
high_school = "High-School_data_2013.csv"
metadata_path = HIGH_SCHOOL.joinpath(Files.metadata)
assert metadata_path.is_file()
with metadata_path.open() as reader:
for line in range(5):
print(reader.readline(), end="")
650 2BIO1 F 498 2BIO1 F 627 2BIO1 F 857 2BIO1 F 487 2BIO1 F
This file has the meta-data for the students. The three columns are the student's ID, class, and gender. I don't know what class refers to here. From an American perspective this could mean a particular subject, e.g. Biology 101, or it could mean the number of years that the student has been in attendance, e.g. Freshman. The names suggest that they are subject related, but I don't know this for certain.
meta_data = pandas.read_csv(metadata_path, sep="\t",
names=["id", "class", "gender"])
meta_data.loc[:, "class"] = meta_data["class"].astype("category")
meta_data.loc[:, "gender"] = meta_data.gender.astype("category")
print(len(meta_data))
print(len(meta_data.id.unique()))
329 329
So there's one entry for each student, meaning the student's can't belong to more than one class.
Classes
First a table of the counts.
table = CountPercentage(meta_data["class"]).holoviews_table.opts(
width=Plot.width,
height=256,
fontsize=Plot.fontsize,
)
Embed(plot=table, file_name="class_table", height_in_pixels=270)()
The fact that there are nine classes makes it seem like it's not likely they are 'classes' in the sense of freshman, sophomores, etc. At the same time, since each student only has one class, it doesn't seem like they are 'classes' in the sense of "Biology 101".
Now a bar-plot to look at how the classes are distributed.
grouped = meta_data.groupby(["class", "gender"]).agg(
{"class": "count", "gender": "count"})
grouped.columns = ["class_count", "gender_count"]
grouped = grouped.reset_index()
grouped.loc[:, "class"]= grouped["class"].astype(str)
plot = grouped.hvplot.bar("class", "class_count", title="Class Counts by Gender",
stacked=True,
by="gender", height=Plot.height,
width=Plot.width,
ylabel="Count",
xlabel="Class",
tools=["hover"],
fontsize=Plot.fontsize).opts(xrotation=90)
Embed(plot=plot, file_name="gender_counts_stacked", height_in_pixels=Plot.height)()
This is a look at the same thing except not stacked.
plot = grouped.hvplot.bar(title="Class Counts by Gender", x="class",
y="class_count",
xlabel="Class",
ylabel="Count",
by="gender", height=Plot.height, width=Plot.width,
tools=["hover"],
fontsize=Plot.fontsize).opts(xrotation=90)
Embed(plot=plot, file_name="gender_counts", height_in_pixels=Plot.height)()
Strangely, the classes that start with 2BIO
are more female while the others are more male.
Gender
A stacked bar plot to get a sense of not just the distribution among genders but among classes.
plot = grouped.hvplot.bar(title="Gender Counts", x="gender", y="gender_count",
stacked=True,
by="class",
xlabel="Count",
ylabel="Gender",
fontsize=Plot.fontsize,
width=Plot.width,
height=Plot.height).opts(
legend_position="top_right",
xrotation=90,
xlabel="Gender and Class")
Embed(plot=plot, file_name="class_counts_stacked", height_in_pixels=Plot.height)()
A non-stacked bar plot to get a better sense of how the genders fill the different classes.
plot = grouped.hvplot.bar(title="Gender Counts", x="gender", y="gender_count",
xlabel="Gender",
ylabel="Count",
by="class",
height=Plot.height,
width=Plot.width,
fontsize=Plot.fontsize).opts(
xrotation=90, xlabel="Gender and Class")
Embed(plot=plot, file_name="class_counts", height_in_pixels=Plot.height)()
It looks like there were a little more males than females, but not a whole lot more.
The Friendship Network
This is a dataset that shows whether a student identified another student as their friend.
friendship_path = HIGH_SCHOOL.joinpath(Files.friendship)
assert friendship_path.is_file()
with friendship_path.open() as reader:
for line in range(5):
print(reader.readline(), end="")
1 55 1 205 1 272 1 494 1 779
The first column is the person who reported who his or her friends were and the second column is the person that was identified as a friend.
friendship_data = pandas.read_csv(friendship_path, delimiter=" ",
names=["reporter", "friend"])
friendship_data = friendship_data.dropna()
Looking at the Friendship Network
with TIMER:
friendship_graph = networkx.convert_matrix.from_pandas_edgelist(
friendship_data, "reporter", "friend",
create_using=networkx.DiGraph)
Started: 2019-04-29 12:04:26.556414 Ended: 2019-04-29 12:04:26.558285 Elapsed: 0:00:00.001871
genders = dict(zip(meta_data.id, meta_data.gender))
classes = dict(zip(meta_data.id, meta_data["class"]))
for node in friendship_graph.nodes:
friendship_graph.nodes[node]["gender"] = genders[node]
friendship_graph.nodes[node]["class"] = classes[node]
Plotting
Friendship Network Circular
By Gender
hover = HoverTool(
tooltips = [
("Student Number", "@index"),
("Gender", "@gender"),
("Class", "@class"),
],
)
plot = holoviews.Graph.from_networkx(friendship_graph,
networkx.circular_layout).redim.range(**Plot.padding).options(
node_color=dim("gender"), cmap="Set1",
tools=[hover, TapTool()],
fontsize=Plot.fontsize,
width=Plot.graph_width,
height=Plot.graph_height,
edge_line_color=Plot.edge_color,
title="Friendship Network by Gender",
xaxis=None,
yaxis=None,
directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_circular")()
It's a little hard to see what's going on here, other than to note that you can see some people are more popular than others. The red nodes are male, the green nodes are female, and the blue is "unknown". Strangely, when I did the distributions earlier there were seven "unknown" but there's only one here…
print(meta_data[meta_data.gender=="Unknown"])
id class gender 320 34 MP Unknown 321 41 MP Unknown 322 243 MP Unknown 323 420 MP Unknown 324 58 PC* Unknown 325 209 PC* Unknown 326 979 2BIO2 Unknown
There are seven id's, so there are really are seven unknowns, but for some reason the circle graph doesn't expose any other than the first (student 34). Maybe not all the students are in the data?
students = set(meta_data.id.unique())
reporters = set(friendship_data.reporter.unique())
print(f"Number of students in the meta-data: {len(students)}")
print(f"Number of students who reported who their friends were: {len(reporters)}")
Number of students in the meta-data: 329 Number of students who reported who their friends were: 133
So, it looks like not everyone took part in the survey.
reported = set(friendship_data.friend.unique())
print(f"Students not in graph: {len(students - (reporters & reported))}")
Students not in graph: 199
Okay, so not all the students are part of the study.
By Class
hover = HoverTool(
tooltips = [
("Gender", "@gender"),
("Class", "@class"),
],
)
plot = holoviews.Graph.from_networkx(friendship_graph,
networkx.circular_layout).opts(
node_color=dim("class"), cmap="Set1",
tools=[hover],
fontsize=Plot.fontsize,
width=800,
height=800,
edge_line_color=Plot.edge_color,
title="Friendship Network by Class",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_circular_class")()
Unfortunately there's a bug in HoloViews so I can't show a legend with Graphs, but I suppose since I don't know what the classes are, that doesn't really mean anything here.
Spring Layout
Class
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout, ).opts(
node_color=dim("class"), cmap="Set1",
tools=["hover"],
width=800,
height=800,
edge_line_color=Plot.edge_color,
title="Friendship Network By Class",
xaxis=None, yaxis=None, directed=True,
legend_position="right"
).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_class_spring", height_in_pixels=810)()
Unlike the circular plot, this plot shows that there are disconnected neighborhoods within the network and there seems to be a clustering by class.
Gender
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout, ).opts(
node_color=dim("gender"), cmap="Set1",
tools=["hover"],
width=800,
height=800,
edge_line_color=Plot.edge_color,
title="Friendship Network By Gender",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_gender_spring", height_in_pixels=810)()
Interestingly, this view seems to show that there is also some clustering by gender.
Kawada Kamai Layout
Class
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout, ).opts(
node_color=dim("class"), cmap="Set1",
tools=["hover", TapTool()],
width=Plot.graph_width,
height=Plot.graph_height,
edge_line_color=Plot.edge_color,
title="Friendship Network By Class (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True,
legend_position="right"
).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_class_kawada_kamai", height_in_pixels=810)()
This has more space between the nodes so it's a little easier to see the groups. Strangely, there's no isolated neighborhoods the way there is with the spring layout.
Gender
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout, ).opts(
node_color=dim("gender"), cmap="Set1",
tools=["hover"],
width=800,
height=800,
edge_line_color=Plot.edge_color,
title="Friendship Network By Gender (Kamada Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_gender_kamada_kawai", height_in_pixels=810)()
Gender and Class Side-By-Side
graph = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
class_plot = graph.opts(
node_color=dim("class"), cmap="Set1",
tools=["hover"],
width=Plot.small,
height=Plot.small,
edge_color=Plot.edge_color,
title="Class",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
gender = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
gender = gender.opts(
node_color=dim("gender"), cmap="Set1",
tools=["hover"],
width=Plot.small,
height=Plot.small,
title="Gender",
edge_color=Plot.edge_color,
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
layout = (class_plot + gender).opts(title="Friendship Network")
Embed(plot=layout, file_name="friendship_network_class_vs_gender", height_in_pixels=Plot.small+200)()
It looks like the Class might be more significant in clustering than the Gender.
Degree Distribution
Total Degrees
degree_sequence = sorted((degree for node, degree in friendship_graph.degree()))
degree_counts = Counter(degree_sequence)
degrees, counts = zip(*degree_counts.items())
table = holoviews.Table({"Degrees": degrees, "Count": counts}, ["Degrees"], ["Count"])
plot = table.to.bars(kdims=["Degrees"], vdims=["Count"]).opts(
width=Plot.width,
height=Plot.height,
fontsize=Plot.fontsize,
title="Degree Distribution",
tools=["hover"],
)
Embed(plot=plot, file_name="degree_distribution")()
The Total Degrees for a node is the number of edges attached to it (either coming in or going out).
In-Degree Distribution
in_degree_sequence = sorted((degree for node, degree in friendship_graph.in_degree))
in_degree_counts = Counter(in_degree_sequence)
in_degrees, in_counts = zip(*in_degree_counts.items())
in_data = pandas.DataFrame.from_dict({"Degrees": in_degrees, "Count": in_counts})
in_data["Direction"] = "in"
plot = in_data.hvplot.bar(x="Degrees", y="Count").opts(
width=Plot.width,
height=Plot.height,
fontsize=Plot.fontsize,
title="In-Degree Distribution",
tools=["hover"],
)
Embed(plot=plot, file_name="in_degree_distribution")()
The in-degree represents the number of times a student (the node) was identified by someone else as a friend. Three people weren't identified as friends at all and the most common count was 2, although someone was identified 15 times.
Out-Degree Distribution
out_degree_sequence = sorted((degree for node, degree in friendship_graph.out_degree))
out_degree_counts = Counter(out_degree_sequence)
out_degrees, out_counts = zip(*out_degree_counts.items())
out_data = pandas.DataFrame.from_dict({"Degrees": out_degrees, "Count": out_counts})
out_data["Direction"] = "out"
# table = holoviews.Table(, ["Degrees"], ["Count"])
plot = out_data.hvplot.bar(x="Degrees", y="Count").opts(
width=Plot.width,
height=Plot.height,
fontsize=Plot.fontsize,
title="Out-Degree Distribution",
tools=["hover"],
)
Embed(plot=plot, file_name="out_degree_distribution")()
The out-degree is the number of other students a student identified as a friend.
In and Out Degrees
in_out = pandas.concat([in_data,
out_data]).sort_values(by="Degrees")
plot = in_out.hvplot.bar(x="Degrees", y="Count", by="Direction").opts(
width=Plot.width,
height=Plot.height,
fontsize=Plot.fontsize,
title="In and Out-Degree Distribution",
tools=["hover"],
)
Embed(plot=plot, file_name="in_and_out_degree_distribution")()
Despite the fact that I sorted the data by degrees, the actual plot seems to have also sort it by degrees but treating them as strings instead of integers. I'm going to add the in-degree, out-degree and in-degree - out-degree (in minus out) as data for the nodes so that they'll be available in the plots.
for node in friendship_graph.nodes:
friendship_graph.nodes[node]["In-Degree"] = friendship_graph.in_degree[node]
friendship_graph.nodes[node]["Out-Degree"] = friendship_graph.out_degree[node]
friendship_graph.nodes[node]["In-Out"] = friendship_graph.in_degree[node] - friendship_graph.out_degree[node]
Bokeh seemed to indicate that you could set the thickness of the edges using weights, but this doesn't seem to work, HoloViews appears to have changed something and I couldn't figure out how to make it work.
for start, end in friendship_graph.edges:
friendship_graph[start][end]["in_weight"] = friendship_graph.in_degree[end]
friendship_graph[start][end]["out_weight"] = friendship_graph.out_degree[start]
friendship_graph[start][end]["weight"] = friendship_graph.in_degree[end]
Popularity
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout)
plot = plot.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="In-Degree",
edge_cmap="Spectral",
title="Friendship Network In-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_degree_spring", height_in_pixels=810)()
The color of the nodes is related to the number of in-degrees it has (which represents the number of other students that stated a node was their friend). If it is dark purple then there are fewer in-degrees. If it is yellow than there are many in-degrees. So the yellow nodes are popular and the dark purple nodes not so much.
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout))
plot = plot.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="In-Degree",
edge_cmap="Spectral",
title="Friendship Network In-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_degree_bundled_spring", height_in_pixels=810)()
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.circular_layout)
plot = plot.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="In-Degree",
edge_cmap="Spectral",
title="Friendship Network In-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_degree_circular", height_in_pixels=810)()
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.circular_layout))
plot = plot.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="In-Degree",
edge_cmap="Spectral",
title="Friendship Network In-Degree",
directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_degree_bundled_circular", height_in_pixels=810)()
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout))
plot = plot.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="In-Degree",
edge_cmap="Spectral",
title="Friendship Network In-Degree (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_degree_bundled_kamada_kawai", height_in_pixels=810)()
Gregariousness
The out-degree is the number of times a student identified other students as friends. I'll interpret this as gregariousness (or maybe neediness).
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout)
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_spring", height_in_pixels=810)()
The color of the nodes is related to the number of out-degrees it has (which represents the number of students that a stated node identified as their friend). If it is dark purple than there are fewer out-degrees (loners?). If it is yellow than there are many out-degrees (the consider many to be their friends).
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout))
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_bundled_spring", height_in_pixels=810)()
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.circular_layout)
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_alpha=0.25,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_circular", height_in_pixels=810)()
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.circular_layout))
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_bundled_circular", height_in_pixels=810)()
plot = bundle_graph(holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout))
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_bundled_kamada_kawai", height_in_pixels=810)()
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
plot = plot.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network Out-Degree (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_out_degree_unbundled_kamada_kawai", height_in_pixels=810)()
In Vs Out
out = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
out = out.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
edge_color=Plot.edge_color,
title="Out Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
in_degree = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
in_degree = in_degree.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
title="In Degree",
edge_color=Plot.edge_color,
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
layout = (in_degree + out).opts(title="Friendship Network")
Embed(plot=layout, file_name="friendship_network_degrees_with_in_vs_out", height_in_pixels=Plot.small+200)()
It looks like the student with the highest in-degree isn't the student with the highest out-degree. It might be coincidental, but the student with the highest in-degree was female while the student with the highest out-degree was male.
Perception
The In Degree - Out Degree tells us how a student's perception of how many friends she has compares to how many people really think she's their friend. If it's negative than she thinks she has more friends than she has (delusional? optimistic?) and if it's positive than she has more friends than she thinks she does (modest? low self-esteem?).
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout)
plot = plot.opts(
node_color=dim("In-Out"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network In Degree - Out Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_minus_out_spring", height_in_pixels=810)()
The dark-purple nodes have the most out-degrees compared to their in-degrees and the yellow-nodes have the most in-degrees compared to out-degrees.
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
plot = plot.opts(
node_color=dim("In-Out"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color_index="Out-Degree",
edge_cmap="Spectral",
title="Friendship Network In Minus Out (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_in_minus_out_kamada_kawac", height_in_pixels=810)()
Just Degrees
for node in friendship_graph.nodes:
friendship_graph.nodes[node]["Degree"] = friendship_graph.degree[node]
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
plot = plot.opts(
node_color=dim("Degree"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
edge_color="RoyalBlue",
title="Friendship Network Degrees",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_degrees", height_in_pixels=810)()
In and Out Groups
I'll add another synthetic metric - this time I'm going to color the nodes based on the total degree plus the in-degree minus the out-degree. If a student says they have more friends than was indicated by the other students this will reduce the degree and if they say they have fewer then this will make the degree higher.
for node in friendship_graph.nodes:
friendship_graph.nodes[node]["In+Out"] = (
friendship_graph.degree[node] + (
friendship_graph.in_degree[node]
- friendship_graph.out_degree[node]))
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
plot = plot.opts(
node_color=dim("In+Out"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
title="Friendship Network Degrees + (In - Out) (Kamada-Kawai)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_degrees_plus_in_minus_out_kamada_kawai", height_in_pixels=810)()
The more yellow a node is, the higher the total degree (minus the over-counting of friends), indicating a larger friendship network, and the bluer the node is, the lower the total degree indicating fewer friends.
in_out = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
in_out = in_out.opts(
node_color=dim("In+Out"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
edge_color=Plot.edge_color,
title="Degree + (In - Out)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
degree = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
degree = degree.opts(
node_color=dim("Degree"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
title="Degree",
edge_color=Plot.edge_color,
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
layout = (in_out + degree).opts(title="Friendship Network")
Embed(plot=layout, file_name="friendship_network_degrees_with_in_out", height_in_pixels=Plot.small+200)()
It looks like adding the penalty (which penalizes over-reporting friends and rewards under-reporting) reduced the brightness of some of the nodes and made one degree look the most sociable. With the penalty, the brightest node is the female student that had the highest in-degree, without the penalty the brightest node is the male student who had the highest out-degree.
plot = holoviews.Graph.from_networkx(friendship_graph, networkx.spring_layout)
plot = plot.opts(
node_color=dim("In+Out"), cmap="Plasma",
tools=["hover"],
width=800,
height=800,
title="Friendship Network Degrees + (In - Out) (Spring Layout)",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
Embed(plot=plot, file_name="friendship_network_degrees_plus_in_minus_out_spring", height_in_pixels=810)()
The Return
The Submission
I think the single most-interesting plot is the Degrees plus (in-degree - out-degree), but that might just be because I like the metric I came up with. A more informative plot might just be to plot the in and out degrees side by side.
out = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
out = out.opts(
node_color=dim("Out-Degree"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
edge_color=Plot.edge_color,
title="Out Degree",
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
in_degree = holoviews.Graph.from_networkx(friendship_graph, networkx.kamada_kawai_layout)
in_degree = in_degree.opts(
node_color=dim("In-Degree"), cmap="Plasma",
tools=["hover"],
width=Plot.small,
height=Plot.small,
title="In Degree",
edge_color=Plot.edge_color,
xaxis=None, yaxis=None, directed=True).redim.range(**Plot.padding)
layout = (in_degree + out).opts(title="Friendship Network")
Embed(plot=layout, file_name="friendship_network_degrees_with_in_vs_out",
height_in_pixels=Plot.small+200,
add_link=True)()
Citations
- R. Mastrandrea, J. Fournet, A. Barrat,
Contact patterns in a high school: a comparison between data collected using wearable sensors, contact diaries and friendship surveys. PLoS ONE 10(9): e0136497 (2015)