<jupyter_start><jupyter_text>Analysis of IMDB data using the ArangoDB Netoworkx adapter  This notebook will illustrate how the Networkx adapter can be used to perform graph analytics tasks on graph data stored in ArangoDB. For this example we will use the IMDB movie review dataset. The dataset provides reviews of various movies by viewers. The details of the dataset are available [here](https://www.imdb.com/interfaces/). If we use a relational representation to analyze this problem, then we will need to know what we want to look in the data and then seek it from the data. For example we may be interested in the average rating for a particular genre of movies. The point here is that we need to know the questions that are important in the analysis of this problem apriori. When we use a graph representation of this data, then we can leverage the body of ideas used in network analysis (or social network analysis) to extract insights from the data. The body of ideas are the standard questions we seek answers to when analyzing any network. The answers to these questions will provide us insights about the data and the problem. In other words, we have a framework for extracting insights from the data. In the example provided in this notebook, we will use ideas from the analysis of bipartite graphs to illustrate this point. Of course, if we have a specific question that we are seeking the answer for, then that is available easily from the graph as well. Therefore, going with a graph represenation makes it easier to extract insights from the data.tThe advantages of using a graph representation for the data extend beyond mining and learning from the data. Running adhoc queries for analytic purposes on the data is more efficient with a graph representation. With a relational representation, we can only optimize query performance for queries that are known before hand (with indexes). Adhoc queries could require multiple joins and have poor performance. Most graphs, have the so called _small world effect_. In such graphs, most nodes can be reached from most other nodes with a small number of edge traversals. An adhoc query starting at any node can reach the node of interest in a few hops.The illustration in this notebook is organized as follows:1. Loading the data for the example.2. Creating the Networkx graph from the AranogoDB graph using the Networkx Adapter.3. Analysis of the graph using Networkx.4. Using the Networkx graph to obtain a graph embedding using _Node2vec_.5. Using Arangopipe to store metadata about this experiment.  Loading the Data into ArangoDB We will use Oasis, ArangoDB's managed service offering, for this example. We will start with installing the required libraries to run this example. We will then connect to Oasis, obtain a connection and create the database.After creating the database, we will load the data into ArangoDB. The next few cells illustrate these steps.  Install Required Libraries<jupyter_code>%%capture
!git clone -b narrative_addition https://github.com/arangoml/networkx-adapter.git
!rsync -av networkx-adapter/examples/ ./ --exclude=.git
!pip3 install networkx
!pip3 install matplotlib
!pip3 install --index-url https://test.pypi.org/simple/ adbnx-adapter==0.0.0.2.5.3
!pip3 install pyarango
!pip3 install python-arango
!pip install node2vec
!pip install seaborn
!pip install arangopipe==0.0.6.9.3
!rm creds.dat
!touch creds.dat<jupyter_output><empty_output><jupyter_text>Get a Oasis Connection __Oasis__, the managed database service offering from ArangoDB, will be used for this exercise. This eliminates the need for setting up and configuring an instance of a database.<jupyter_code>from adbnx_adapter.imdb_arangoDB_networkx_adapter import IMDBArangoDB_Networkx_Adapter
import oasis
con = oasis.getTempCredentials()

print()
print("https://{}:{}".format(con["hostname"], con["port"]))
print("Username: " + con["username"])
print("Password: " + con["password"])
print("Database: " + con["dbName"])


ma = IMDBArangoDB_Networkx_Adapter(conn=con)<jupyter_output><empty_output><jupyter_text>Create the Collections for the Database<jupyter_code># Connect to the temp database
conn = oasis.connect(con)
db = conn[con["dbName"]]
import csv
import json
import requests
import sys
import oasis


from pyArango.connection import *
from pyArango.collection import Collection, Edges, Field
from pyArango.graph import Graph, EdgeDefinition
from pyArango.collection import BulkOperation as BulkOperation
from pyArango.collection import Collection, Field
from pyArango.graph import Graph, EdgeDefinition


class Users(Collection):
    _fields = {
        "user_id": Field(),
        "age": Field(),
        "gender": Field()
    }
    
class Movies(Collection):
    _fields = {
        "movie_id": Field(),
        "movie_title": Field(),
        "release_data": Field()
    }

class Ratings(Edges): 
    _fields = {
        #user_id and item_id are encoded by _from, _to 
        "rating": Field(),
        "timestamp": Field()
    }

class IMDBGraph(Graph) :
    _edgeDefinitions = [EdgeDefinition("Ratings", fromCollections=["Users"], toCollections=["Movies"])]
    _orphanedCollections = []

db.createCollection("Users")
db.createCollection("Movies")
db.createCollection("Ratings")
iMDBGraph = db.createGraph("IMDBGraph")

print("Collection/Graph Setup done.")<jupyter_output><empty_output><jupyter_text>Import the Data<jupyter_code>collection = db["Users"]
with BulkOperation(collection, batchSize=100) as col:
    with open('data/users.csv', newline='') as csvfile:
        reader = csv.reader(csvfile, delimiter=',', quotechar='|')
        # Skip header
        next(reader)
        for row in reader:
            user_id, age, gender, occupation, zip = tuple(row)
            doc = col.createDocument()
            doc["_key"] = user_id
#             doc["age"] = age
#             doc["gender"] = gender
            doc.save()

collection = db["Movies"]
with BulkOperation(collection, batchSize=100) as col:
    with open('data/movies.csv', newline='') as csvfile:
        reader = csv.reader(csvfile, delimiter=',', quotechar='|')
        # Skip header
        next(reader)
        for row in reader:
            movie_id, movie_title, release_date, video_release_date, url, unknown, action, adventure, animation, childrens, comedy, crime, documentary, drama, fantasy, noir, horror, musical, mystery, romance, scifi, thriller, war, western = tuple(
                row)
            doc = col.createDocument()
            doc["_key"] = movie_id
#             doc["movie_title"] = movie_title
#             doc["release_date"] = release_date
            doc.save()

collection = db["Ratings"]
with BulkOperation(collection, batchSize=1000) as col:
    with open('data/ratings.csv', newline='') as csvfile:
        reader = csv.reader(csvfile, delimiter=',', quotechar='|')
        # Skip header
        next(reader)
        for row in reader:
            user_id, movie_id, rating, timestamp = tuple(row)
            doc = col.createDocument()
            doc["_from"] = "Users/"+user_id
            doc["_to"] = "Movies/"+movie_id
            doc["ratings"] = rating
#             doc["timestamp"] = timestamp
            doc.save()

print("Import Done")<jupyter_output><empty_output><jupyter_text>Use the Networkx-Adapter to create a Networkx Graph Now that we have loaded the data into ArangoDB, we can use the __Networkx-Adapter__ to create _Networkx_ graphs from the ArangoDB graph. To do so, we'll have to provide a graph descriptor that describes the graph structure to the __Networkx-Adapter__. These steps are illustrated below.   Specify the Graph Structure  To use the IMDB Networkx Adapter, we need to specify the structure of the graph that we want to create. This is done with a simple dictionary.<jupyter_code>imdb_attributes = {'vertexCollections': {'Users': {},
                                         'Movies': {}},
                   'edgeCollections': {'Ratings': {'_from', '_to', 'ratings'}}}<jupyter_output><empty_output><jupyter_text>Obtain the networkx graph<jupyter_code>g = ma.create_networkx_graph(
    graph_name='IMDBGraph',  graph_attributes=imdb_attributes)<jupyter_output><empty_output><jupyter_text>__Done!__, we now have a _Networkx_ graph representation that we can use for analysis!  Analysis of the IMDB reviews dataset with NetworkxWe just created a networkx graph for this problem. In this notebook we will use a small set of ideas from the standard body of ideas used in network analysis to extract insights from the movie review dataset. Networkx provides an implementation of these ideas. These will be discussed next.As mentioned earlier, one of the advantages of using a graph representation is that we can leverage the standard body of ideas used to analyze networks to extract information about this dataset. In what follows, we will call out such facts as we identify them.  Get the user and movie nodes<jupyter_code>user_nodes = [n for n in g.nodes() if n.startswith("Users")]
movie_nodes = [n for n in g.nodes() if n.startswith("Movies")]<jupyter_output><empty_output><jupyter_text>Structural Property Introspection: Number of Nodes and Edges<jupyter_code>print("Number of Users are %d" % (len(user_nodes)))
print("Number of Movies are %d" % (len(movie_nodes)))
print("Number of Ratings are %d" % (len(list(g.edges()))))<jupyter_output><empty_output><jupyter_text>Convert the graph obtained from the interface to a bi-partite graph<jupyter_code>import networkx as nx
B = nx.Graph()
B.add_nodes_from(user_nodes, bipartite=0)
B.add_nodes_from(movie_nodes, bipartite=1)
B.add_edges_from(list(g.edges()))<jupyter_output><empty_output><jupyter_text>Identified fact:_The graph has two kinds of edges. It has 943 users and and 1682 Movies. A user may watch multiple movies. 65499 movie ratings are available. This information is obtained by simply identifying the structure of the graph._<jupyter_code>from networkx.algorithms import bipartite
bipartite.is_bipartite(B)<jupyter_output><empty_output><jupyter_text>Is the graph connected?If the graph is connected then there is a path between any two nodes in the graph. If not, then some nodes are not connected. In the context of this application, a disconnected graph implies that there exist users in the graph who may not have rated a movie in common. If it is true that any given pair of users have rated at least one movie in common, then there is a path connecting these users through the movie they have rated in common.  Identified fact:_In the user community, if we pick any two users, then it is possible that they may not have rated a movie in common._<jupyter_code>nx.is_connected(B)
users = [n for n in B.nodes if B.nodes[n]['bipartite'] == 0]
movies = [n for n in B.nodes if B.nodes[n]['bipartite'] == 1]<jupyter_output><empty_output><jupyter_text>How many movies does a User rate?This is a distribution. A review of the plot below will show the familiar long tailed distribution. Most viewers rate less than say 100 movies. There are some serious reviewers out there though. These are on the tail of the distribution.<jupyter_code>degu, degm = bipartite.degrees(B, movies)
du = [v for k,v in degu]
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [10, 5]
ax = plt.hist(du)
plt.title("Histogram of the Number of Movies Viewed")
plt.xlabel("Movies Viewed")
plt.grid(True)<jupyter_output><empty_output><jupyter_text>Identified fact:_Inspecting the degree distribution of vertices is an activity we commonly perform to understand some basic characteristics of the network. In this application, this activity shows that most user's rate less than a 100 movies. The graph above is an example of "long tailed distribution". This is commonly seen in social networks._  How many users rate a movie?This is also a distribution that also has the long tailed behavior. Most mvoies are rated by less than 100 users. There are some movies that are rated by many viewers though.<jupyter_code>dm = [v for k,v in degm]
plt.rcParams['figure.figsize'] = [10, 5]
ax = plt.hist(dm)
plt.title("Histogram of the Number of Viewers Rating a Movie")
plt.xlabel("Number of Viewers")
plt.grid(True)<jupyter_output><empty_output><jupyter_text>Identified fact:_We can repeat the exploration of degree distribution with the movie nodes. The behavior observed with the movie nodes is similar to what is observed with the user nodes. We observe the same "long tailed" distribution and most movies are rated by less than a 100 users._  Centrality MeasuresA centrality measure identifies influential nodes in a network. How do we formalize the observations about movie importance and the user's rating behavior in the given network? The notion of centrality measure is useful in this regard. Many measures of node centrality are used, see [Chapter 3 of the text book by Zafarani et.al](http://dmml.asu.edu/smm/) for details. We will use degree centraility in this example (see the [documentation](https://networkx.github.io/documentation/stable/reference/algorithms/generated/networkx.algorithms.bipartite.centrality.degree_centrality.html) for details). In the illustrations below, we will capture the top 10 users and movies in terms of importance as determined by this measure.<jupyter_code>dc= bipartite.degree_centrality(B, users)
sdcu = {}
sdcm = {}
for k, v in sorted(dc.items(),reverse=True, key=lambda item: item[1]):
    if k.startswith("Users"):
        sdcu[k] = v
    else:
        sdcm[k] = v

del dc<jupyter_output><empty_output><jupyter_text>List the top 10 users in terms of degree centralityThese guys rate a lot of movies<jupyter_code>list(sdcu.keys())[:10]<jupyter_output><empty_output><jupyter_text>List the top 10 movies in terms of degree centralityThese movies are rated by a lot of people<jupyter_code>list(sdcm.keys())[:10]<jupyter_output><empty_output><jupyter_text>How does this importance measure vary over the user community?We can get a sense of this by viewing the distribution of this measure.<jupyter_code>import seaborn as sns
import numpy as np
npvals = np.fromiter(sdcu.values(), dtype=float)
sns.set(rc={'figure.figsize':(11.7,8.27)})
ax = sns.distplot(npvals, hist = False)
plt.title("Distribution of User Degree Centrality")
plt.xlabel("User Degree Centraility")<jupyter_output><empty_output><jupyter_text>How does this importance measure vary over the movie community?<jupyter_code>import seaborn as sns
import numpy as np
npvals = np.fromiter(sdcm.values(), dtype=float)
sns.set(rc={'figure.figsize':(11.7,8.27)})
ax = sns.distplot(npvals, hist = False)
plt.title("Distribution of Movie Degree Centrality")
plt.xlabel("Movie Degree Centraility")<jupyter_output><empty_output><jupyter_text>Identified fact:_We have a measure to capture the importance of a movie and a user on the basis of the rating activity performed on them. We have identified important users and movies in the network on the basis of this measure. We have also have information about how this importance measure varies over the users and movies in the data._<jupyter_code>m4v = [t[0] for t in g.in_edges('Movies/4')]<jupyter_output><empty_output><jupyter_text>Similarity of nodesJust like measures exist to quantify the importance of nodes in a network, there exist measures to quantify the similarity of nodes in a network. We will pick a random node in the network and characterize the similarity of nodes connected to it.  How similar are viewers of the movie 'Get Shorty'?In this example, we will use the __Jaccard__ similarity as a measure of node similarity. We first get all pairs of users who have seen the movie and then compute the __Jaccard__ similarity between them. The details are shown below. Networkx provides an API for this purpose that we will use.<jupyter_code>from itertools import combinations
m4vucmb = list(combinations(m4v, 2))
import networkx as nx
gp = g.to_undirected()
jcp = nx.jaccard_coefficient(gp, m4vucmb)
jcpv = []
for u, v, p in jcp:
    jcpv.append(p)
    #print('(%s, %s) -> %.8f' % (u, v, p))<jupyter_output><empty_output><jupyter_text>What does the distribution of User Similarity look like?<jupyter_code>import seaborn as sns
import numpy as np
sns.set(rc={'figure.figsize':(11.7,8.27)})
ax = sns.distplot(jcpv, hist = False)
plt.title("Distribution of Jaccard Similarity between Raters of Get Shorty")
plt.xlabel("Jaccard Similarity")<jupyter_output><empty_output><jupyter_text>Identified fact:_Picking a random node in the graph, we characterized the similarity of nodes connected to it. In this dataset, we can use this idea to get a sense of how similar user's rating a movie tend to be. A review of the above figure shows that a range of similarities are observed._  Bipartite ClusteringThe next idea that we will investigate on the graph comes from [Latapy et.al](https://arxiv.org/abs/cond-mat/0611631). In this work, the authors extend the notion of clustering coefficients associated with a node to the case of bi-partite graphs. Since our graph is a bipartite graph, we will use this metric. See section VIII of the paper for a detailed explanation of the idea. Briefly, the clustering coefficient for a pair of nodes $u, v$, is defined as follows:$$\begin{equation*}cc(u,v) = \frac{N(u)\cap N(v)}{N(u) \cup N(v)},\end{equation*}$$where:* N(u) is the neighborhood of node $u$. These are the nodes connected to $u$. For example, the movies user $u$ rates.* N(v) is the neighborhood of node $v$.Reviewing the above equation, it is evident that the clustering coefficient captures the shared neighborhood for a pair of nodes. Nodes associated with high clustering coefficient have many common neighbors. In the context of this problem, a pair of users with a high clustering coefficient suggests that these users have rated many movies in common. Similarly, a pair of movies with a high clustering coefficient suggests that these movies there are many common raters(users) for these movies. Networkx provides an implementation of this idea. The details of computing the clustering coefficients for the users and movies are shown below.To obtain the clustering coefficient for a node $u$, we simply average the clustering coefficient of $u$ with other nodes that it shares common neighbors with. It is defined as follows:$$\begin{equation*}cc(u) = \frac{\sum_{v \in N(u)} cc(u,v)}{\left| N(N(u) \right|},\end{equation*}$$<jupyter_code>cr = bipartite.clustering(B)
cu = {}
cm = {}
for k, v in sorted(cr.items(),reverse=True, key=lambda item: item[1]):
    if k.startswith("Users"):
        cu[k] = v
    else:
        cm[k] = v

del cr<jupyter_output><empty_output><jupyter_text>List the top 10 users in terms of clustering tendencies These users share a high degree of common movie rating activity with other users. If rating is used as a proxy for the act of liking or disliking a movie, then user's with high clustering values share rating activity with<jupyter_code>list(cu.keys())[:10]<jupyter_output><empty_output><jupyter_text>List the top 10 movies in terms of clustering tendencies These movies have a high degree of common users rating them.<jupyter_code>list(cm.keys())[:10]<jupyter_output><empty_output><jupyter_text>How does user clustering tendency vary over the community?<jupyter_code>import seaborn as sns
import numpy as np
npvals = np.fromiter(cu.values(), dtype=float)
sns.set(rc={'figure.figsize':(11.7,8.27)})
ax = sns.distplot(npvals, hist = False)
plt.title("Distribution of User Clustering Coefficients")
plt.xlabel("User Clustering Coefficients")<jupyter_output><empty_output><jupyter_text>Note:Note that the distribution of user clustering coefficients has two humps (bi-modal). This suggests that there are two user communities. This is borne out when we visualize a sample of the user community using a _tSNE_ plot later in this notebook.  How does movie clustering tendency vary over the community?<jupyter_code>import seaborn as sns
import numpy as np
npvals = np.fromiter(cm.values(), dtype=float)
sns.set(rc={'figure.figsize':(11.7,8.27)})
ax = sns.distplot(npvals, hist = False)
plt.title("Distribution of Movie Clustering Coefficients")
plt.xlabel("Movie Clustering Coefficients")<jupyter_output><empty_output><jupyter_text>Note:Note that the distribution of user clustering coefficients has three humps. This suggests that there are three movie clusters.   Identified fact:_We have identified users and movies that tend to have have a lot of common rating activity. We have identified that users and movies have clustering tendencies._  Using Node2vec to obtain a graph embedding Machine learning techniques can be applied on graphs to determine vector representations of graph elements such as nodes and edges. Such a representation is called an _embedding_. We can use the Networkx representation of the graph associated with the 10 most clustered users and determine its embedding. We can use a technique called [_t Stochastic Neighbor Embedding_](https://www.youtube.com/watch?v=RJVL80Gg3lA&list=UUtXKDgv1AVoG88PLl8nGXmw) to plot a two dimensional representation of this subgraph. The details of doing this is provided below. Earlier we noted that the user clustering tendencies were bi-modal. A review of the embedding of the sub-graph associated with the 10 most clustered users shows that there are two clusters in the data.  Extract the sub-graph associated with the 10 most clustered users<jupyter_code>t10cu = list(cu.keys())[:10]
proj_user = nx.bipartite.projected_graph(B, t10cu)
len(list(proj_user.edges()))
len(proj_user.nodes())<jupyter_output><empty_output><jupyter_text>Embed the sub-graph using Node2vec<jupyter_code>from node2vec import Node2Vec
node2vec = Node2Vec(proj_user, dimensions=64, walk_length=100, num_walks=100, workers=4)
model = node2vec.fit(window=10, min_count=1, batch_words=4)
t10cu[2]<jupyter_output><empty_output><jupyter_text>Apply the modelWe can ask questions like "Who is most similar to (a particular user) Users/64?"<jupyter_code>model.wv.most_similar(t10cu[2])<jupyter_output><empty_output><jupyter_text>Embed the graph with tSNE<jupyter_code>t10cu_emb = { n: list(model.wv.get_vector(n)) for n in proj_user.nodes()}
import numpy as np
from matplotlib  import cm
from sklearn.manifold import TSNE
fig = plt.figure(figsize=(11.7,8.27))
plt.grid(True)
plt.xlabel("Comp 1")
plt.ylabel("Comp 2")
plt.title("tSNE plot of subgraph of top 10 most clustered users")
t10cu_embedded = TSNE(n_components=2).fit_transform(list(t10cu_emb.values()))
sns.scatterplot(t10cu_embedded[:,0], t10cu_embedded[:,1])<jupyter_output><empty_output><jupyter_text>__Using the Networkx adapter we have been able to perform a range of analysis tasks using the Networkx API. We were also able use the Networkx graph to obtain an embedding of a graph using Node2vec.__  Store Embeddings in ArangoDB<jupyter_code>%time
collection = db["Users"]
with BulkOperation(collection, batchSize=500) as col:
    #user_col = db.collections["Users"]
    for u, e in t10cu_emb.items():
        the_key = u.split('/')[1]
        the_user = collection[the_key]
        the_user["n2v_emb"] = e
        the_user.patch()<jupyter_output><empty_output><jupyter_text>Store metadata about these experiments using Arangopipe<jupyter_code>from arangopipe.arangopipe_storage.arangopipe_api import ArangoPipe
from arangopipe.arangopipe_storage.arangopipe_admin_api import ArangoPipeAdmin
from arangopipe.arangopipe_storage.arangopipe_config import ArangoPipeConfig
from arangopipe.arangopipe_storage.managed_service_conn_parameters import ManagedServiceConnParam
mdb_config = ArangoPipeConfig()
msc = ManagedServiceConnParam()
conn_params = { msc.DB_SERVICE_HOST : "arangoml.arangodb.cloud", \
                        msc.DB_SERVICE_END_POINT : "createDB",\
                        msc.DB_SERVICE_NAME : "createDB",\
                        msc.DB_SERVICE_PORT : 8529,\
                        msc.DB_CONN_PROTOCOL : 'https'}
        
mdb_config = mdb_config.create_connection_config(conn_params)
admin = ArangoPipeAdmin(reuse_connection = False, config = mdb_config)
ap_config = admin.get_config()
ap = ArangoPipe(config = ap_config)
proj_info = {"name": "IMDB_Movie_Reviews"}
proj_reg = admin.register_project(proj_info)
import io
import requests
url = ('https://raw.githubusercontent.com/arangoml/networkx-adapter/master/examples/IMDB_Networkx_Adapter.ipynb')
nbjson = requests.get(url).text
model_info = {"name": "Exploratory Data Analysis IMDB",  "task": "multiple", 'notebook': nbjson}
model_reg = ap.register_model(model_info, project = "IMDB_Movie_Reviews")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08263385217178713, 'fraction_numerical': 0.008760492217423192, 'mean_word_length': 4.915401301518438, 'pattern_counts': {'":': 11, '<': 129, '<?xml version=': 0, '>': 130, 'https://': 8, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '32', 'dir': 'jupyter-structured-clean-dedup', 'id': '403898', 'license': 'Apache-2.0', 'n_tokens_mistral': 7280, 'n_tokens_neox': 6955, 'n_words': 2814, 'path': 'examples/IMDB_Networkx_Adapter.ipynb', 'repo_name': 'cw00dw0rd/networkx-adapter', 'size': '24542'}