diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4bc5e30d3..494565c95 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
- python-version: [3.8, 3.9, '3.10', 3.11]
+ python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
steps:
@@ -71,7 +71,7 @@ jobs:
strategy:
matrix:
- python-version: [3.8, 3.9, '3.10', 3.11]
+ python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
steps:
@@ -93,7 +93,7 @@ jobs:
python -m venv pygraphistry
source pygraphistry/bin/activate
python -m pip install --upgrade pip
- python -m pip install -e .[docs,test,build,bolt,igraph,networkx,gremlin,nodexl,jupyter]
+ python -m pip install -e .[test,build,bolt,igraph,networkx,gremlin,nodexl,jupyter]
- name: Lint
run: |
@@ -110,6 +110,47 @@ jobs:
source pygraphistry/bin/activate
./bin/test.sh
+ test-graphviz:
+
+ needs: [ test-minimal-python ]
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
+
+ steps:
+
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ with:
+ lfs: true
+
+ - name: Checkout LFS objects
+ run: git lfs pull
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install test dependencies
+ run: |
+ python -m venv pygraphistry
+ source pygraphistry/bin/activate
+ sudo apt-get install graphviz graphviz-dev
+ python -m pip install --upgrade pip
+ python -m pip install -e .[test,pygraphviz]
+
+ - name: Type check
+ run: |
+ source pygraphistry/bin/activate
+ ./bin/typecheck.sh
+
+ - name: Graphviz tests
+ run: |
+ source pygraphistry/bin/activate
+ ./bin/test-graphviz.sh
test-core-umap:
@@ -118,6 +159,7 @@ jobs:
strategy:
matrix:
+ #python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
python-version: [3.8, 3.9]
steps:
@@ -165,6 +207,10 @@ jobs:
strategy:
matrix:
python-version: [3.8, 3.9]
+ #python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
+ #include:
+ # - python-version: 3.12
+ # continue-on-error: true
steps:
@@ -284,7 +330,7 @@ jobs:
- name: Test building docs
run: |
- cd docs && ./docker.sh
+ cd docs && ./ci.sh
test-readme:
diff --git a/.gitignore b/.gitignore
index f8a1ee954..104d69b49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,6 +61,8 @@ coverage.xml
# Sphinx documentation
docs/_build/
+docs/doctrees/
+docs/source/demos/
# PyBuilder
target/
@@ -87,3 +89,4 @@ demos/data/BIOGRID-IDENTIFIERS-3.3.123.tab.txt
# local jupyter dev
jupyter_dev/
+docs/source/demos
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 609e875f7..e037f6cd8 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -5,24 +5,53 @@
# Required
version: 2
-# Build documentation in the docs/ directory with Sphinx
-sphinx:
- configuration: docs/source/conf.py
-
build:
os: ubuntu-22.04
tools:
- python: "3.8"
+ python: "3.12"
+ apt_packages:
+ # More closely mirror https://github.com/sphinx-doc/sphinx-docker-images
+ - graphviz
+ - imagemagick
+ - make
+ - pandoc
+ - texlive-latex-base
+ - texlive-latex-recommended
+ - texlive-latex-extra
+ - texlive-fonts-recommended
+ commands:
+
+ # setup
+ - pip install ".[docs]"
+ - cp -r demos docs/source/demos
+ - cp README.md docs/source/README.md
+ - cp ARCHITECTURE.md docs/source/ARCHITECTURE.md
+ - cp CONTRIBUTE.md docs/source/CONTRIBUTE.md
+ - cp DEVELOP.md docs/source/DEVELOP.md
+
+ # build html
+ - sphinx-build -b html -d docs/doctrees docs/source $READTHEDOCS_OUTPUT/html/
+
+ # build epub
+ - sphinx-build -b epub -d docs/doctrees docs/source docs/_build/latexpdf
+ - mkdir -p $READTHEDOCS_OUTPUT/epub
+ - cp docs/_build/latexpdf/PyGraphistry.epub $READTHEDOCS_OUTPUT/epub/PyGraphistry.epub
-# Optionally build your docs in additional formats such as PDF
+ # build pdf
+ - sphinx-build -b latex -d docs/doctrees docs/source docs/_build/latexpdf
+ - cd docs/_build/latexpdf && pdflatex -file-line-error -interaction=nonstopmode PyGraphistry.tex && pdflatex -file-line-error -interaction=nonstopmode PyGraphistry.tex && echo ok || { echo fail && exit 1 ; }
+ - mkdir -p $READTHEDOCS_OUTPUT/pdf
+ - cp docs/_build/latexpdf/PyGraphistry.pdf $READTHEDOCS_OUTPUT/pdf/PyGraphistry.pdf
+
+#for nav links?
formats:
- pdf
- - htmlzip
- epub
+ - htmlzip
python:
install:
- method: pip
path: .
extra_requirements:
- - dev
\ No newline at end of file
+ - docs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e735dff9..a7cf750fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,155 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Development]
+## [0.34.16 - 2024-10-13]
+
+### Docs
+
+* Update and streamline readme.md
+* Add quicksheet for overall
+* More crosslinking
+
+### Infra
+
+* Add markdown support to docsite
+* ReadTheDocs homepage reuses github README.md
+* Docs pip install caches
+* Drop SVGs and external images during latexpdf generation
+
+### Changed
+
+* Treemap import `squarify` deferred to use to allow core import without squarify installed, such as in `--no-deps`
+
+## [0.34.15 - 2024-10-11]
+
+### Docs
+
+* Improve GFQL translation doc
+* Add examples and API links: Shaping, Hypergraphs, AI & ML
+* Add performance docs
+* Add AI examples
+
+## [0.34.14 - 2024-10-09]
+
+### Added
+
+* HTTP responses with error status codes log an `logging.ERROR`-level message of the status code and response body
+
+## [0.34.13 - 2024-10-07]
+
+### Docs
+
+* Add more GFQL cross-references
+
+## [0.34.12 - 2024-10-07]
+
+### Docs
+
+* Fix ipynb examples in ReadTheDocs distribution
+
+## [0.34.11 - 2024-10-07]
+
+### Fix
+
+* Types
+
+### Infra
+
+* Enable more Python version checks
+
+## [0.34.10 - 2024-10-07]
+
+### Fix
+
+* Docs: Notebook builds
+
+### Docs
+
+* More links, especially around plugins
+* Update color theme to match Graphistry branding
+
+## [0.34.9 - 2024-10-07]
+
+### Fix
+
+* Docs: 10 Minutes to PyGraphistry links
+
+## [0.34.8 - 2024-10-06]
+
+### Fix
+
+* Docs: PDF support
+* Docs: Links
+
+### Docs
+
+* More accessible theme
+
+## [0.34.7 - 2024-10-06]
+
+### Docs
+
+* RTD: Added notebook tutorials
+* RTD: Added various guides
+* RTD: Added cross-references
+* RTD: Cleaner navigation
+
+### Infra
+
+* Python: Add Python 12 to CI and document support
+* Docs: Udated dependencies - Sphinx 8, Python 12, and various related
+* Docs: Added nbsphinx - hub url grounding, ...
+* Docs: Redo as a docker compose flow with incremental builds (docker, sphinx)
+* Docs: Updated instructions for new flow
+
+### Fix
+
+* Docs: 2024
+* Notebooks: Compatibility with nbsphinx - exactly one title heading, no uncommented `!`, correct references, ...
+
+## [0.34.6 - 2024-10-04]
+
+### Added
+
+* Plugins: graphviz bindings, such as `g.layout_graphviz("dot")`
+
+### Docs
+
+* Reorganized readthedocs
+* Added intro tutorials: `10 Minutes to PyGraphistry`, `10 Minutes to GFQL`, `Login and Sharing`
+
+## [0.34.5 - 2024-09-23]
+
+### Fixed
+
+* GFQL: Fix `chain()` regression around an incorrectly disabled check manifesting as https://github.com/graphistry/pygraphistry/issues/583
+* GFQL: Fix `chain()`, `hop()` traverse filtering logic for a multi-hop edge scenarios
+* GFQL: Fix `hop()` predicate handling in multihop scenarios
+
+### Infra
+
+* GFQL: Expand test suite around multihop edge predicates in `hop()` and `chain()`
+
+## [0.34.4 - 2024-09-20]
+
+### Added
+
+* UMAP: Optional kwargs passthrough to umap library constructor, fit, and transform methods: `g.umap(..., umap_kwargs={...}, umap_fit_kwargs={...}, umap_transform_kwargs={...})`
+* Additional GPU support in featurize paths
+
+### Changed
+
+* Replace `verbose` with `logging`
+
+### Refactor
+
+* Narrow `use_scaler` and `use_scaler_target` typing to `ScalerType` (`Literal[...]`) vs `str`
+* Rename `featurize_or_get_nodes_dataframe_if_X_is_None` (and edges variant) as non-private due to being shared
+
+### Fixed
+
+* get_indegrees: Fix warning https://github.com/graphistry/pygraphistry/issues/587
+
## [0.34.3 - 2024-08-03]
### Added
diff --git a/README.md b/README.md
index a50e260f7..d5dd94b92 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# PyGraphistry: Explore Relationships
+# PyGraphistry: Leverage the power of graphs & GPUs to visualize, analyze, and scale your data
![Build Status](https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg)
[![CodeQL](https://github.com/graphistry/pygraphistry/workflows/CodeQL/badge.svg)](https://github.com/graphistry/pygraphistry/actions?query=workflow%3ACodeQL)
@@ -11,1606 +11,201 @@
[![Uptime Robot status](https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com)](https://status.graphistry.com/) [ ](https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g)
[![Twitter Follow](https://img.shields.io/twitter/follow/graphistry)](https://twitter.com/graphistry)
-**PyGraphistry is a dataframe-native Python visual graph AI library to extract, query, transform, analyze, model, and visualize big graphs, and especially alongside [Graphistry](https://www.graphistry.com) end-to-end GPU server sessions.** The GFQL query language supports running a large subset of the Cypher property graph query language without requiring external software and adds optional GPU acceleration. Installing PyGraphistry with the optional `graphistry[ai]` dependencies adds **graph autoML**, including automatic feature engineering, UMAP, and graph neural net support. Combined, PyGraphistry reduces your **time to graph** for going from raw data to visualizations and AI models down to three lines of code.
-
-The optional visual engine, Graphistry, gets used on problems like visually mapping the behavior of devices and users, investigating fraud, analyzing machine learning results, and starting in graph AI. It provides point-and-click features like timebars, search, filtering, clustering, coloring, sharing, and more. Graphistry is the only tool built ground-up for large graphs. The client's custom WebGL rendering engine renders up to 8MM nodes + edges at a time, and most older client GPUs smoothly support somewhere between 100K and 2MM elements. The serverside GPU analytics engine supports even bigger graphs. It smoothes graph workflows over the PyData ecosystem including Pandas/Spark/Dask dataframes, Nvidia RAPIDS GPU dataframes & GPU graphs, DGL/PyTorch graph neural networks, and various data connectors.
-
-The PyGraphistry Python client helps several kinds of usage modes:
-
-* **Data scientists**: Go from data to accelerated visual explorations in a couple lines, share live results, build up more advanced views over time, and do it all from notebook environments like Jupyter and Google Colab
-* **Developers**: Quickly prototype stunning Python solutions with PyGraphistry, embed in a language-neutral way with the [REST APIs](https://hub.graphistry.com/docs/api/), and go deep on customizations like colors, icons, layouts, JavaScript, and more
-* **Analysts**: Every Graphistry session is a point-and-click environment with interactive search, filters, timebars, histograms, and more
-* **Dashboarding**: Embed into your favorite framework. Additionally, see our sister project [Graph-App-Kit](https://github.com/graphistry/graph-app-kit) for quickly building interactive graph dashboards by launching a stack built on PyGraphistry, StreamLit, Docker, and ready recipes for integrating with common graph libraries
-
-PyGraphistry is a friendly and optimized PyData-native interface to the language-neutral [Graphistry REST APIs](https://hub.graphistry.com/docs/api/).
-You can use PyGraphistry with traditional Python data sources like CSVs, SQL, Neo4j, Splunk, and more (see below). Wrangle data however you want, and with especially good support for Pandas dataframes, Apache Arrow tables, Nvidia RAPIDS cuDF dataframes & cuGraph graphs, and DGL/PyTorch graph neural networks.
-
-1. [Interactive Demo](#demo-of-friendship-communities-on-facebook)
-2. [Graph Gallery](#gallery)
-3. [Install](#install)
-4. [Tutorial](#tutorial-les-misérables)
-5. [Next Steps](#next-steps)
-6. [Resources](#resources)
-
-## Demo of Friendship Communities on Facebook
-## **PyGraphistry is:**
-
-* **Fast & gorgeous:** Interactively cluster, filter, inspect large amounts of data, and zip through timebars. It clusters large graphs with a descendant of the gorgeous ForceAtlas2 layout algorithm introduced in Gephi. Our data explorer connects to Graphistry's GPU cluster to layout and render hundreds of thousand of nodes+edges in your browser at unparalleled speeds.
-
-* **Easy to install:** `pip install` the client in your notebook or web app, and then connect to a [free Graphistry Hub account](https://www.graphistry.com/get-started) or [launch your own private GPU server](https://www.graphistry.com/get-started)
-
- ```python
- # pip install --user graphistry # minimal
- # pip install --user graphistry[bolt,gremlin,nodexl,igraph,networkx] # data plugins
- # AI modules: Python 3.8+ with scikit-learn 1.0+:
- # pip install --user graphistry[umap-learn] # Lightweight: UMAP autoML (without text support); scikit-learn 1.0+
- # pip install --user graphistry[ai] # Heavy: Full UMAP + GNN autoML, including sentence transformers (1GB+)
-
- import graphistry
- graphistry.register(api=3, username='abc', password='xyz') # Free: hub.graphistry.com
- #graphistry.register(..., personal_key_id='pkey_id', personal_key_secret='pkey_secret') # Key instead of username+password+org_name
- #graphistry.register(..., is_sso_login=True) # SSO instead of password
- #graphistry.register(..., org_name='my-org') # Upload into an organization account vs personal
- #graphistry.register(..., protocol='https', server='my.site.ngo') # Use with a self-hosted server
- # ... and if client (browser) URLs are different than python server<> graphistry server uploads
- #graphistry.register(..., client_protocol_hostname='https://public.acme.co')
- ```
-
-* **Notebook-friendly:** PyGraphistry plays well with interactive notebooks like [Jupyter](http://ipython.org), [Zeppelin](https://zeppelin.incubator.apache.org/), and [Databricks](http://databricks.com). Process, visualize, and drill into with graphs directly within your notebooks:
-
- ```python
- graphistry.edges(pd.read_csv('rows.csv'), 'col_a', 'col_b').plot()
- ```
-
-* **Great for events, CSVs, and more:** Not sure if your data is graph-friendly? PyGraphistry's `hypergraph` transform helps turn any sample data like CSVs, SQL results, and event data into a graph for pattern analysis:
-
- ```python
- rows = pandas.read_csv('transactions.csv')[:1000]
- graphistry.hypergraph(rows)['graph'].plot()
- ```
-
-* **Embeddable:** Drop live views into your web dashboards and apps (and go further with [JS/React](https://hub.graphistry.com/docs)):
-
- ```python
- iframe_url = g.plot(render=False)
- print(f'')
- ```
-
-* **Configurable:** In-tool or via the declarative APIs, use the powerful encodings systems for tasks like coloring by time, sizing by score, clustering by weight, show icons by type, and more.
-
-* **Shareable:** Share live links, configure who has access, and more! [(Notebook tutorial)](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/sharing_tutorial.ipynb)
-
-* **Graph AI that is fast & easy:** In oneines of code, turn messy data into feature vectors for modeling, GNNs for training pipelines, lower dimensional embeddings, and visualizations:
-
- ```python
- df = pandas.read_csv('accounts.csv')
-
- # UMAP dimensionality reduction with automatic feature engineering
- g1 = graphistry.nodes(df).umap()
-
- # Automatically shows top inferred similarity edges g1._edges
- g1.plot()
-
- # Optional: Use subset of columns, supervised learning target, & more
- g2.umap(X=['name', 'description', 'amount'], y=['label_col_1']).plot()
- ```
-
-### Explore any data as a graph
-
-It is easy to turn arbitrary data into insightful graphs. PyGraphistry comes with many built-in connectors, and by supporting Python dataframes (Pandas, Arrow, RAPIDS), it's easy to bring standard Python data libraries. If the data comes as a table instead of a graph, PyGraphistry will help you extract and explore the relationships.
-
-* [Pandas](http://pandas.pydata.org)
-
- ```python
- edges = pd.read_csv('facebook_combined.txt', sep=' ', names=['src', 'dst'])
- graphistry.edges(edges, 'src', 'dst').plot()
- ```
-
- ```python
- table_rows = pd.read_csv('honeypot.csv')
- graphistry.hypergraph(table_rows, ['attackerIP', 'victimIP', 'victimPort', 'vulnName'])['graph'].plot()
- ```
-
- ```python
- graphistry.hypergraph(table_rows, ['attackerIP', 'victimIP', 'victimPort', 'vulnName'],
- direct=True,
- opts={'EDGES': {
- 'attackerIP': ['victimIP', 'victimPort', 'vulnName'],
- 'victimIP': ['victimPort', 'vulnName'],
- 'victimPort': ['vulnName']
- }})['graph'].plot()
- ```
-
- ```python
- ### Override smart defaults with custom settings
- g1 = graphistry.bind(source='src', destination='dst').edges(edges)
- g2 = g1.nodes(nodes).bind(node='col2')
- g3 = g2.bind(point_color='col3')
- g4 = g3.settings(url_params={'edgeInfluence': 1.0, play: 2000})
- url = g4.plot(render=False)
- ```
-
- ```python
- ### Read back data and create modified variants
- enriched_edges = my_function1(g1._edges)
- enriched_nodes = my_function2(g1._nodes)
- g2 = g1.edges(enriched_edges).nodes(enriched_nodes)
- g2.plot()
- ```
-
-* GFQL: Cypher-style graph pattern mining queries on dataframes with optional GPU acceleration ([ipynb demo](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb), [benchmark](https://github.com/graphistry/pygraphistry/blob/master/demos/gfql/benchmark_hops_cpu_gpu.ipynb))
-
- Run Cypher-style graph queries natively on dataframes without going to a database or Java with GFQL:
-
- ```python
- from graphistry import n, e_undirected, is_in
-
- g2 = g1.chain([
- n({'user': 'Biden'}),
- e_undirected(),
- n(name='bridge'),
- e_undirected(),
- n({'user': is_in(['Trump', 'Obama'])})
- ])
-
- print('# bridges', len(g2._nodes[g2._nodes.bridge]))
- g2.plot()
- ```
-
- Enable GFQL's optional automatic GPU acceleration for 43X+ speedups:
-
- ```python
- # Switch from Pandas CPU dataframes to RAPIDS GPU dataframes
- import cudf
- g2 = g1.edges(lambda g: cudf.DataFrame(g._edges))
- # GFQL will automaticallly run on a GPU
- g3 = g2.chain([n(), e(hops=3), n()])
- g3.plot()
- ```
-
-* [Spark](https://spark.apache.org/)/[Databricks](https://databricks.com/) ([ipynb demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.ipynb), [dbc demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.dbc))
-
- ```python
- #optional but recommended
- spark.conf.set("spark.sql.execution.arrow.enabled", "true")
-
- edges_df = (
- spark.read.format('json').
- load('/databricks-datasets/iot/iot_devices.json')
- .sample(fraction=0.1)
- )
- g = graphistry.edges(edges_df, 'device_name', 'cn')
-
- #notebook
- displayHTML(g.plot())
-
- #dashboard: pick size of choice
- displayHTML(
- g.settings(url_params={'splashAfter': 'false'})
- .plot(override_html_style="""
- width: 50em;
- height: 50em;
- """)
- )
- ```
-
-* GPU [RAPIDS.ai](https://www.rapids.ai) cudf
-
- ```python
- edges = cudf.read_csv('facebook_combined.txt', sep=' ', names=['src', 'dst'])
- graphistry.edges(edges, 'src', 'dst').plot()
- ```
-
-* GPU [RAPIDS.ai](https://www.rapids.ai) cuML
-
- ```python
- g = graphistry.nodes(cudf.read_csv('rows.csv'))
- g = graphistry.nodes(G)
- g.umap(engine='cuml',metric='euclidean').plot()
- ```
-
-* GPU [RAPIDS.ai](https://www.rapids.ai) cugraph ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/gpu_rapids/cugraph.ipynb))
-
- ```python
- g = graphistry.from_cugraph(G)
- g2 = g.compute_cugraph('pagerank')
- g3 = g2.layout_cugraph('force_atlas2')
- g3.plot()
- G3 = g.to_cugraph()
- ```
-
-* [Apache Arrow](https://arrow.apache.org/)
-
- ```python
- edges = pa.Table.from_pandas(pd.read_csv('facebook_combined.txt', sep=' ', names=['src', 'dst']))
- graphistry.edges(edges, 'src', 'dst').plot()
- ```
-
-* [Neo4j](http://neo4j.com) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/neo4j/official/graphistry_bolt_tutorial_public.ipynb))
-
- ```python
- NEO4J_CREDS = {'uri': 'bolt://my.site.ngo:7687', 'auth': ('neo4j', 'mypwd')}
- graphistry.register(bolt=NEO4J_CREDS)
- graphistry.cypher("MATCH (n1)-[r1]->(n2) RETURN n1, r1, n2 LIMIT 1000").plot()
- ```
-
- ```python
- graphistry.cypher("CALL db.schema()").plot()
- ```
-
- ```python
- from neo4j import GraphDatabase, Driver
- graphistry.register(bolt=GraphDatabase.driver(**NEO4J_CREDS))
- g = graphistry.cypher("""
- MATCH (a)-[p:PAYMENT]->(b)
- WHERE p.USD > 7000 AND p.USD < 10000
- RETURN a, p, b
- LIMIT 100000""")
- print(g._edges.columns)
- g.plot()
- ```
-
-* [Memgraph](https://memgraph.com/) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/memgraph/visualizing_iam_dataset.ipynb))
-
- ```python
- from neo4j import GraphDatabase
- MEMGRAPH = {
- 'uri': "bolt://localhost:7687",
- 'auth': (" ", " ")
- }
- graphistry.register(bolt=MEMGRAPH)
- ```
-
- ```python
- driver = GraphDatabase.driver(**MEMGRAPH)
- with driver.session() as session:
- session.run("""
- CREATE (per1:Person {id: 1, name: "Julie"})
- CREATE (fil2:File {id: 2, name: "welcome_to_memgraph.txt"})
- CREATE (per1)-[:HAS_ACCESS_TO]->(fil2) """)
- g = graphistry.cypher("""
- MATCH (node1)-[connection]-(node2)
- RETURN node1, connection, node2;""")
- g.plot()
- ```
-
-* [Azure Cosmos DB (Gremlin)](https://azure.microsoft.com/en-us/services/cosmos-db/)
-
- ```python
- # pip install --user gremlinpython
- # Options in help(graphistry.cosmos)
- g = graphistry.cosmos(
- COSMOS_ACCOUNT='',
- COSMOS_DB='',
- COSMOS_CONTAINER='',
- COSMOS_PRIMARY_KEY=''
- )
- g2 = g.gremlin('g.E().sample(10000)').fetch_nodes()
- g2.plot()
- ```
-
-* [Amazon Neptune (Gremlin)](https://aws.amazon.com/neptune/) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/neptune/neptune_tutorial.ipynb), [dashboarding demo](https://aws.amazon.com/blogs/database/enabling-low-code-graph-data-apps-with-amazon-neptune-and-graphistry/))
-
- ```python
- # pip install --user gremlinpython==3.4.10
- # - Deploy tips: https://github.com/graphistry/graph-app-kit/blob/master/docs/neptune.md
- # - Versioning tips: https://gist.github.com/lmeyerov/459f6f0360abea787909c7c8c8f04cee
- # - Login options in help(graphistry.neptune)
- g = graphistry.neptune(endpoint='wss://zzz:8182/gremlin')
- g2 = g.gremlin('g.E().limit(100)').fetch_nodes()
- g2.plot()
- ```
-
-* [TigerGraph](https://tigergraph.com) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/tigergraph/tigergraph_pygraphistry_bindings.ipynb))
-
- ```python
- g = graphistry.tigergraph(protocol='https', ...)
- g2 = g.gsql("...", {'edges': '@@eList'})
- g2.plot()
- print('# edges', len(g2._edges))
- ```
-
- ```python
- g.endpoint('my_fn', {'arg': 'val'}, {'edges': '@@eList'}).plot()
- ```
-
-* [igraph](http://igraph.org)
-
- ```python
- edges = pd.read_csv('facebook_combined.txt', sep=' ', names=['src', 'dst'])
- g_a = graphistry.edges(edges, 'src', 'dst')
- g_b = g_a.layout_igraph('sugiyama', directed=True) # directed: for to_igraph
- g_b.compute_igraph('pagerank', params={'damping': 0.85}).plot() #params: for layout
-
- ig = igraph.read('facebook_combined.txt', format='edgelist', directed=False)
- g = graphistry.from_igraph(ig) # full conversion
- g.plot()
-
- ig2 = g.to_igraph()
- ig2.vs['spinglass'] = ig2.community_spinglass(spins=3).membership
- # selective column updates: preserve g._edges; merge 1 attribute from ig into g._nodes
- g2 = g.from_igraph(ig2, load_edges=False, node_attributes=[g._node, 'spinglass'])
- ```
-
-* [NetworkX](https://networkx.github.io) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/networkx/networkx.ipynb))
-
- ```python
- graph = networkx.read_edgelist('facebook_combined.txt')
- graphistry.bind(source='src', destination='dst', node='nodeid').plot(graph)
- ```
-
-* [HyperNetX](https://github.com/pnnl/HyperNetX) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/hypernetx/hypernetx.ipynb))
-
- ```python
- hg.hypernetx_to_graphistry_nodes(H).plot()
- ```
-
- ```python
- hg.hypernetx_to_graphistry_bipartite(H.dual()).plot()
- ```
-
-* [Splunk](https://www.splunk.com) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/splunk/splunk_demo_public.ipynb))
-
- ```python
- df = splunkToPandas("index=netflow bytes > 100000 | head 100000", {})
- graphistry.edges(df, 'src_ip', 'dest_ip').plot()
- ```
-
-* [NodeXL](https://www.nodexl.com) ([notebook demo](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_databases_apis/nodexl/official/nodexl_graphistry.ipynb))
-
- ```python
- graphistry.nodexl('/my/file.xls').plot()
- ```
-
- ```python
- graphistry.nodexl('https://file.xls').plot()
- ```
-
- ```python
- graphistry.nodexl('https://file.xls', 'twitter').plot()
- graphistry.nodexl('https://file.xls', verbose=True).plot()
- graphistry.nodexl('https://file.xls', engine='xlsxwriter').plot()
- graphistry.nodexl('https://file.xls')._nodes
- ```
-
-## Graph AI in a single line of code
-
-Graph autoML features including:
-
-### Generate features from raw data
-
-Automatically and intelligently transform text, numbers, booleans, and other formats to AI-ready representations:
-
-* Featurization
-
- ```python
- g = graphistry.nodes(df).featurize(kind='nodes', X=['col_1', ..., 'col_n'], y=['label', ..., 'other_targets'], ...)
-
- print('X', g._node_features)
- print('y', g._node_target)
- ```
+PyGraphistry is an open source Python library for data scientists and developers to leverage the power of graph visualization, analytics, AI, including with native GPU acceleration:
-* Set `kind='edges'` to featurize edges:
+* [**Python dataframe-native graph processing:**](https://pygraphistry.readthedocs.io/en/latest/10min.html) Quickly ingest & prepare data in many formats, shapes, and scales as graphs. Use tools like Pandas, Spark, [RAPIDS (GPU)](https://www.rapids.ai), and [Apache Arrow](https://arrow.apache.org/).
- ```python
- g = graphistry.edges(df, src, dst).featurize(kind='edges', X=['col_1', ..., 'col_n'], y=['label', ..., 'other_targets'], ...)
- ```
+* [**Integrations:**](https://pygraphistry.readthedocs.io/en/latest/plugins.html) Plug into [Amazon Neptune](https://docs.aws.amazon.com/neptune/latest/userguide/visualization-graphistry.html) ([notebook](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/neptune/neptune_cypher_viz_using_bolt.html)), [cuGraph](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/gpu_rapids/cugraph.html), [Databricks](https://www.databricks.com/solutions/accelerators/incident-investigation-using-graphistry) ([notebook](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.html)), [graphviz](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/graphviz/graphviz.html), [Neo4j](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/neo4j/official/graphistry_bolt_tutorial_public.html), [Splunk](https://www.splunk.com/en_us/blog/security/supercharge-cybersecurity-investigations-with-splunk-and-graphistry-a-powerful-combination-for-interactive-graph-exploration.html) ([notebook](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/splunk/splunk_demo_public.html)), [TigerGraph](https://pygraphistry.readthedocs.io/en/latest/demos/demos_databases_apis/tigergraph/tigergraph_pygraphistry_bindings.html), and many more in the [notebook data provider demo gallery](https://pygraphistry.readthedocs.io/en/latest/notebooks/plugins.connectors.html).
-* Use generated features with both Graphistry and external libraries:
- ```python
- # graphistry
- g = g.umap() # UMAP, GNNs, use features if already provided, otherwise will compute
+* [**Prototype locally and deploy remotely:**](https://www.graphistry.com/get-started) Prototype from notebooks like Jupyter and Databricks using local CPUs & GPUs, and then power production dashboards & pipelines with Graphistry Hub and your own self-hosted servers.
- # other pydata libraries
- X = g._node_features # g._get_feature('nodes') or g.get_matrix()
- y = g._node_target # g._get_target('nodes') or g.get_matrix(target=True)
- from sklearn.ensemble import RandomForestRegressor
- model = RandomForestRegressor().fit(X, y) # assumes train/test split
- new_df = pandas.read_csv(...) # mini batch
- X_new, _ = g.transform(new_df, None, kind='nodes', return_graph=False)
- preds = model.predict(X_new)
- ```
+* [**Query graphs with GFQL:**](https://pygraphistry.readthedocs.io/en/latest/gfql/index.html) Use GFQL, the first dataframe-native graph query language, to ask relationship questions that are difficult for tabular tools and without requiring a database.
-* Encode model definitions and compare models against each other
+* [**graphistry[ai]:**](https://pygraphistry.readthedocs.io/en/latest/gfql/combo.html#) Call streamlined graph ML & AI methods to benefit from clustering, UMAP embeddings, graph neural networks, automatic feature engineering, and more.
- ```python
- # graphistry
- from graphistry.features import search_model, topic_model, ngrams_model, ModelDict, default_featurize_parameters, default_umap_parameters
+* [**Visualize & explore large graphs:**](https://pygraphistry.readthedocs.io/en/latest/visualization/10min.html#) In just a few minutes, create stunning interactive visualizations with millions of edges and many point-and-click built-ins like drilldowns, timebars, and filtering. When ready, customize with Python, JavaScript, and REST APIs.
- g = graphistry.nodes(df)
- g2 = g.umap(X=[..], y=[..], **search_model)
+* [**Columnar & GPU acceleration:**](https://pygraphistry.readthedocs.io/en/latest/performance.html) CPU-mode ingestion and wrangling is fast due to native use of Apache Arrow and columnar analytics, and the optional RAPIDS-based GPU mode delivers 100X+ speedups.
- # set custom encoding model with any feature/umap/dbscan kwargs
- new_model = ModelDict(message='encoding new model parameters is easy', **default_featurize_parameters)
- new_model.update(dict(
- y=[...],
- kind='edges',
- model_name='sbert/cool_transformer_model',
- use_scaler_target='kbins',
- n_bins=11,
- strategy='normal'))
- print(new_model)
- g3 = g.umap(X=[..], **new_model)
- # compare g2 vs g3 or add to different pipelines
- ```
+From global 10 banks, manufacturers, news agencies, and government agencies, to startups, game companies, scientists, biotechs, and NGOs, many teams are tackling their graph workloads with Graphistry.
-See `help(g.featurize)` for more options
-### [sklearn-based UMAP](https://umap-learn.readthedocs.io/en/latest/), [cuML-based UMAP](https://docs.rapids.ai/api/cuml/stable/api.html?highlight=umap#cuml.UMAP)
-* Reduce dimensionality by plotting a similarity graph from feature vectors:
-
- ```python
- # automatic feature engineering, UMAP
- g = graphistry.nodes(df).umap()
-
- # plot the similarity graph without any explicit edge_dataframe passed in -- it is created during UMAP.
- g.plot()
- ```
-
-* Apply a trained model to new data:
-
- ```python
- new_df = pd.read_csv(...)
- embeddings, X_new, _ = g.transform_umap(new_df, None, kind='nodes', return_graph=False)
- ```
-
-* Infer a new graph from new data using the old umap coordinates to run inference without having to train a new umap model.
-
- ```python
- new_df = pd.read_csv(...)
- g2 = g.transform_umap(new_df, return_graph=True) # return_graph=True is default
- g2.plot() #
-
- # or if you want the new minibatch to cluster to closest points in previous fit:
- g3 = g.transform_umap(new_df, return_graph=True, merge_policy=True)
- g3.plot() # useful to see how new data connects to old -- play with `sample` and `n_neighbors` to control how much of old to include
- ```
-
-* UMAP supports many options, such as supervised mode, working on a subset of columns, and passing arguments to underlying `featurize()` and UMAP implementations (see `help(g.umap)`):
-
- ```python
- g.umap(kind='nodes', X=['col_1', ..., 'col_n'], y=['label', ..., 'other_targets'], ...)
- ```
-
-* `umap(engine="...")` supports multiple implementations. It defaults to using the GPU-accelerated `engine="cuml"` when a GPU is available, resulting in orders-of-magnitude speedups, and falls back to CPU processing via `engine="umap_learn"`.:
-
- ```python
- g.umap(engine='cuml')
- ```
-
-You can also featurize edges and UMAP them as we did above.
-
-UMAP support is rapidly evolving, please contact the team directly or on Slack for additional discussions
-
-See `help(g.umap)` for more options
-
-### [GNN models](https://docs.dgl.ai/en/0.6.x/index.html)
-
-* Graphistry adds bindings and automation to working with popular GNN models, currently focusing on DGL/PyTorch:
-
- ```python
- g = (graphistry
- .nodes(ndf)
- .edges(edf, src, dst)
- .build_gnn(
- X_nodes=['col_1', ..., 'col_n'], #columns from nodes_dataframe
- y_nodes=['label', ..., 'other_targets'],
- X_edges=['col_1_edge', ..., 'col_n_edge'], #columns from edges_dataframe
- y_edges=['label_edge', ..., 'other_targets_edge'],
- ...)
- )
- G = g.DGL_graph
-
- from [your_training_pipeline] import train, model
- # Train
- g = graphistry.nodes(df).build_gnn(y_nodes=`target`)
- G = g.DGL_graph
- train(G, model)
- # predict on new data
- X_new, _ = g.transform(new_df, None, kind='nodes' or 'edges', return_graph=False) # no targets
- predictions = model.predict(G_new, X_new)
- ```
-
-Like `g.umap()`, GNN layers automate feature engineering (`.featurize()`)
-
-See `help(g.build_gnn)` for options.
-
-GNN support is rapidly evolving, please contact the team directly or on Slack for additional discussions
-
-### [Semantic Search](https://www.sbert.net/examples/applications/semantic-search/README.html)
-
-* Search textual data semantically and see the resulting graph:
-
- ```python
- ndf = pd.read_csv(nodes.csv)
- edf = pd.read_csv(edges.csv)
-
- g = graphistry.nodes(ndf, 'node').edges(edf, 'src', 'dst')
-
- g2 = g.featurize(X = ['text_col_1', .., 'text_col_n'], kind='nodes',
- min_words = 0, # forces all named columns as textual ones
- #encode text as paraphrase embeddings, supports any sbert model
- model_name = "paraphrase-MiniLM-L6-v2")
-
- # or use convienence `ModelDict` to store parameters
-
- from graphistry.features import search_model
- g2 = g.featurize(X = ['text_col_1', .., 'text_col_n'], kind='nodes', **search_model)
-
- # query using the power of transformers to find richly relevant results
-
- results_df, query_vector = g2.search('my natural language query', ...)
-
- print(results_df[['_distance', 'text_col', ..]]) #sorted by relevancy
-
- # or see graph of matching entities and original edges
-
- g2.search_graph('my natural language query', ...).plot()
-
- ```
-
-* If edges are not given, `g.umap(..)` will supply them:
-
- ```python
- ndf = pd.read_csv(nodes.csv)
- g = graphistry.nodes(ndf)
- g2 = g.umap(X = ['text_col_1', .., 'text_col_n'], min_words=0, ...)
-
- g2.search_graph('my natural language query', ...).plot()
- ```
-
-See `help(g.search_graph)` for options
-
-### Knowledge Graph Embeddings
-
-* Train a RGCN model and predict:
-
- ```python
- edf = pd.read_csv(edges.csv)
- g = graphistry.edges(edf, src, dst)
- g2 = g.embed(relation='relationship_column_of_interest', **kwargs)
-
- # predict links over all nodes
- g3 = g2.predict_links_all(threshold=0.95) # score high confidence predicted edges
- g3.plot()
-
- # predict over any set of entities and/or relations.
- # Set any `source`, `destination` or `relation` to `None` to predict over all of them.
- # if all are None, it is better to use `g.predict_links_all` for speed.
- g4 = g2.predict_links(source=['entity_k'],
- relation=['relationship_1', 'relationship_4', ..],
- destination=['entity_l', 'entity_m', ..],
- threshold=0.9, # score threshold
- return_dataframe=False) # set to `True` to return dataframe, or just access via `g4._edges`
- ```
-
-* Detect Anamolous Behavior (example use cases such as Cyber, Fraud, etc)
-
- ```python
- # Score anomolous edges by setting the flag `anomalous` to True and set confidence threshold low
- g5 = g.predict_links_all(threshold=0.05, anomalous=True) # score low confidence predicted edges
- g5.plot()
-
- g6 = g.predict_links(source=['ip_address_1', 'user_id_3'],
- relation=['attempt_logon', 'phishing', ..],
- destination=['user_id_1', 'active_directory', ..],
- anomalous=True,
- threshold=0.05)
- g6.plot()
- ```
-
-* Train a RGCN model including auto-featurized node embeddings
-
- ```python
- edf = pd.read_csv(edges.csv)
- ndf = pd.read_csv(nodes.csv) # adding node dataframe
-
- g = graphistry.edges(edf, src, dst).nodes(ndf, node_column)
-
- # inherets all the featurization `kwargs` from `g.featurize`
- g2 = g.embed(relation='relationship_column_of_interest', use_feat=True, **kwargs)
- g2.predict_links_all(threshold=0.95).plot()
- ```
-
-See `help(g.embed)`, `help(g.predict_links)` , or `help(g.predict_links_all)` for options
-
-### DBSCAN
-
-* Enrich UMAP embeddings or featurization dataframe with GPU or CPU DBSCAN
-
- ```python
- g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node')
-
- # cluster by UMAP embeddings
- kind = 'nodes' | 'edges'
- g2 = g.umap(kind=kind).dbscan(kind=kind)
- print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan'])
-
- # dbscan in `umap` or `featurize` via flag
- g2 = g.umap(dbscan=True, min_dist=0.2, min_samples=1)
-
- # or via chaining,
- g2 = g.umap().dbscan(min_dist=1.2, min_samples=2, **kwargs)
-
- # cluster by feature embeddings
- g2 = g.featurize().dbscan(**kwargs)
-
- # cluster by a given set of feature column attributes, inhereted from `g.get_matrix(cols)`
- g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs)
-
- # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings)
- g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs)
- g2.plot() # color by `_dbscan`
-
- new_df = pd.read_csv(..)
- # transform on new data according to fit dbscan model
- g3 = g2.transform_dbscan(new_df)
- ```
-
-See `help(g.dbscan)` or `help(g.transform_dbscan)` for options
-
-### Quickly configurable
-
-Set visual attributes through [quick data bindings](https://hub.graphistry.com/docs/api/2/rest/upload/#createdataset2) and set [all sorts of URL options](https://hub.graphistry.com/docs/api/1/rest/url/). Check out the tutorials on [colors](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/encodings-colors.ipynb), [sizes](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/encodings-sizes.ipynb), [icons](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/encodings-icons.ipynb), [badges](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/encodings-badges.ipynb), [weighted clustering](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/edge-weights.ipynb) and [sharing controls](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/sharing_tutorial.ipynb):
+## Gallery
- ```python
- g
- .privacy(mode='private', invited_users=[{'email': 'friend1@site.ngo', 'action': '10'}], notify=False)
- .edges(df, 'col_a', 'col_b')
- .edges(my_transform1(g._edges))
- .nodes(df, 'col_c')
- .nodes(my_transform2(g._nodes))
- .bind(source='col_a', destination='col_b', node='col_c')
- .bind(
- point_color='col_a',
- point_size='col_b',
- point_title='col_c',
- point_x='col_d',
- point_y='col_e')
- .bind(
- edge_color='col_m',
- edge_weight='col_n',
- edge_title='col_o')
- .encode_edge_color('timestamp', ["blue", "yellow", "red"], as_continuous=True)
- .encode_point_icon('device_type', categorical_mapping={'macbook': 'laptop', ...})
- .encode_point_badge('passport', 'TopRight', categorical_mapping={'Canada': 'flag-icon-ca', ...})
- .encode_point_color('score', ['black', 'white'])
- .addStyle(bg={'color': 'red'}, fg={}, page={'title': 'My Graph'}, logo={})
- .settings(url_params={
- 'play': 2000,
- 'menu': True, 'info': True,
- 'showArrows': True,
- 'pointSize': 2.0, 'edgeCurvature': 0.5,
- 'edgeOpacity': 1.0, 'pointOpacity': 1.0,
- 'lockedX': False, 'lockedY': False, 'lockedR': False,
- 'linLog': False, 'strongGravity': False, 'dissuadeHubs': False,
- 'edgeInfluence': 1.0, 'precisionVsSpeed': 1.0, 'gravity': 1.0, 'scalingRatio': 1.0,
- 'showLabels': True, 'showLabelOnHover': True,
- 'showPointsOfInterest': True, 'showPointsOfInterestLabel': True, 'showLabelPropertiesOnHover': True,
- 'pointsOfInterestMax': 5
- })
- .plot()
- ```
-### Gallery
+The [notebook demo gallery](https://pygraphistry.readthedocs.io/en/latest/demos/for_analysis.html) shares many more live visualizations, demos, and integration examples
-## Install
-
-### Get
-
-You need to install the PyGraphistry Python client and connect it to a Graphistry GPU server of your choice:
-
-1. Graphistry server account:
- * Create a free [Graphistry Hub account](https://www.graphistry.com/get-started) for open data, or [one-click launch your own private AWS/Azure instance](https://www.graphistry.com/get-started)
- * Later, [setup and manage](https://github.com/graphistry/graphistry-cli) your own private Docker instance ([contact](https://www.graphistry.com/demo-request))
-
-2. PyGraphistry Python client:
- * `pip install --user graphistry` (Python 3.8+) or [directly call the HTTP API](https://hub.graphistry.com/docs/api/)
- * Use `pip install --user graphistry[all]` for optional dependencies such as Neo4j drivers
- * To use from a notebook environment, run your own [Jupyter](https://jupyter.org/) server ([one-click launch your own private AWS/Azure GPU instance](https://www.graphistry.com/get-started)) or another such as [Google Colab](https://colab.research.google.com)
- * See immediately following `configure` section for how to connect
-
-### Configure
-
-Most users connect to a Graphistry GPU server account via:
-
-* `graphistry.register(api=3, username='abc', password='xyz')`: personal hub.graphistry.com account
-* `graphistry.register(api=3, username='abc', password='xyz', org_name='optional_org')`: team hub.graphistry.com account
-* `graphistry.register(api=3, username='abc', password='xyz', org_name='optiona_org', protocol='http', server='my.private_server.org')`: private server
-
-For more advanced configuration, read on for:
-
-* Version: Use protocol `api=3`, which will soon become the default, or a legacy version
-
-* JWT Tokens: Connect to a GPU server by providing a `username='abc'`/`password='xyz'`, or for advanced long-running service account software, a refresh loop using 1-hour-only JWT tokens
-
-* Organizations: Optionally use `org_name` to set a specific organization
-
-* Private servers: PyGraphistry defaults to using the free [Graphistry Hub](https://hub.graphistry.com) public API
-
- * Connect to a [private Graphistry server](https://www.graphistry.com/get-started) and provide optional settings specific to it via `protocol`, `server`, and in some cases, `client_protocol_hostname`
-
-Non-Python users may want to explore the underlying language-neutral [authentication REST API docs](https://hub.graphistry.com/docs/api/1/rest/auth/).
-
-#### Advanced Login
-
-* **For people:** Provide your account username/password:
-
-```python
-import graphistry
-graphistry.register(api=3, username='username', password='your password')
-```
-
-* **For service accounts**: Long-running services may prefer to use 1-hour JWT tokens:
-
-```python
-import graphistry
-graphistry.register(api=3, username='username', password='your password')
-initial_one_hour_token = graphistry.api_token()
-graphistry.register(api=3, token=initial_one_hour_token)
-
-# must run every 59min
-graphistry.refresh()
-fresh_token = graphistry.api_token()
-assert initial_one_hour_token != fresh_token
-```
-
-Refreshes exhaust their limit every day/month. An upcoming Personal Key feature enables non-expiring use.
-
-Alternatively, you can rerun `graphistry.register(api=3, username='username', password='your password')`, which will also fetch a fresh token.
-
-#### Advanced: Private servers - server uploads
-
-Specify which Graphistry server to reach for Python uploads:
-
-```python
-graphistry.register(protocol='https', server='hub.graphistry.com')
-```
-
-Private Graphistry notebook environments are preconfigured to fill in this data for you:
-
-```python
-graphistry.register(protocol='http', server='nginx', client_protocol_hostname='')
-```
-
-Using `'http'`/`'nginx'` ensures uploads stay within the Docker network (vs. going more slowly through an outside network), and client protocol `''` ensures the browser URLs do not show `http://nginx/`, and instead use the server's name. (See immediately following **Switch client URL** section.)
-
-#### Advanced: Private servers - switch client URL for browser views
-
-In cases such as when the notebook server is the same as the Graphistry server, you may want your Python code to *upload* to a known local Graphistry address without going outside the network (e.g., `http://nginx` or `http://localhost`), but for web viewing, generate and embed URLs to a different public address (e.g., `https://graphistry.acme.ngo/`). In this case, explicitly set a client (browser) location different from `protocol` / `server`:
-
-```python
-graphistry.register(
- ### fast local notebook<>graphistry upload
- protocol='http', server='nginx',
-
- ### shareable public URL for browsers
- client_protocol_hostname='https://graphistry.acme.ngo'
-)
-```
-
-Prebuilt Graphistry servers are already setup to do this out-of-the-box.
-
-#### Advanced: Sharing controls
-
-Graphistry supports flexible sharing permissions that are similar to Google documents and Dropbox links
-
-By default, visualizations are publicly viewable by anyone with the URL (that is unguessable & unlisted), and only editable by their owner.
-* Private-only: You can globally default uploads to private:
-```python
-graphistry.privacy() # graphistry.privacy(mode='private')
-```
-
-* Organizations: You can login with an organization and share only within it
-
-```python
-graphistry.register(api=3, username='...', password='...', org_name='my-org123')
-graphistry.privacy(mode='organization')
-```
-
-* Invitees: You can share access to specify users, and optionally, even email them invites
-
-```python
-VIEW = "10"
-EDIT = "20"
-graphistry.privacy(
- mode='private',
- invited_users=[
- {"email": "friend1@site1.com", "action": VIEW},
- {"email": "friend2@site2.com", "action": EDIT}
- ],
- notify=True)
-```
+## Install
-* Per-visualization: You can choose different rules for global defaults vs. for specific visualizations
+Common configurations:
-```python
-graphistry.privacy(invited_users=[...])
-g = graphistry.hypergraph(pd.read_csv('...'))['graph']
-g.privacy(notify=True).plot()
-```
+* **Minimal core**
-See additional examples in the [sharing tutorial](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/sharing_tutorial.ipynb)
+ Includes: The GFQL dataframe-native graph query language, built-in layouts, Graphistry visualization server client
-## Tutorial: Les Misérables
+ ```python
+ pip install graphistry
+ ```
-Let's visualize relationships between the characters in [Les Misérables](http://en.wikipedia.org/wiki/Les_Misérables).
-For this example, we'll choose [Pandas](http://pandas.pydata.org) to wrangle data and [igraph](http://igraph.org) to run a community detection algorithm. You can [view](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/simple/MarvelTutorial.ipynb) the Jupyter notebook containing this example.
+ Does not include `graphistry[ai]`, plugins
-Our [dataset is a CSV file](https://raw.githubusercontent.com/graphistry/pygraphistry/master/demos/data/lesmiserables.csv) that looks like this:
+* **No dependencies and user-level**
-| source | target | value |
-| ------------- |:-------------:| ------:|
-| Cravatte | Myriel | 1
-| Valjean | Mme.Magloire | 3
-| Valjean | Mlle.Baptistine | 3
+ ```python
+ pip install --no-deps --user graphistry
+ ```
-*Source* and *target* are character names, and the *value* column counts the number of time they meet. Parsing is a one-liner with Pandas:
+* **GPU acceleration** - Optional
-```python
-import pandas
-links = pandas.read_csv('./lesmiserables.csv')
-```
+ Local GPU: Install [RAPIDS](https://www.rapids.ai) and/or deploy a GPU-ready [Graphistry server](https://www.graphistry.com/get-started)
+
+ Remote GPU: Use the [remote endpoints](https://www.graphistry.com/blog/graphistry-2-41-3).
-### Quick Visualization
+For further options, see the [installation guides](https://pygraphistry.readthedocs.io/en/latest/install/index.html)
-If you already have graph-like data, use this step. Otherwise, try the [Hypergraph Transform](https://github.com/graphistry/pygraphistry/blob/master/demos/demos_by_use_case/logs/malware-hypergraph/Malware%20Hypergraph.ipynb) for creating graphs from rows of data (logs, samples, records, ...).
-PyGraphistry can plot graphs directly from Pandas data frames, Arrow tables, cuGraph GPU data frames, igraph graphs, or NetworkX graphs. Calling *plot* uploads the data to our visualization servers and return an URL to an embeddable webpage containing the visualization.
+## Visualization quickstart
-To define the graph, we `bind` *source* and *destination* to the columns indicating the start and end nodes of each edges:
+Quickly go from raw data to a styled and interactive Graphistry graph visualization:
```python
import graphistry
-graphistry.register(api=3, username='YOUR_ACCOUNT_HERE', password='YOUR_PASSWORD_HERE')
-
-g = graphistry.bind(source="source", destination="target")
-g.edges(links).plot()
-```
-
-You should see a beautiful graph like this one:
-![Graph of Miserables](http://i.imgur.com/dRHHTyK.png)
-
-### Adding Labels
-
-Let's add labels to edges in order to show how many times each pair of characters met. We create a new column called *label* in edge table *links* that contains the text of the label and we bind *edge_label* to it.
-
-```python
-links["label"] = links.value.map(lambda v: "#Meetings: %d" % v)
-g = g.bind(edge_title="label")
-g.edges(links).plot()
-```
-
-### Controlling Node Title, Size, Color, and Position
-
-Let's size nodes based on their [PageRank](http://en.wikipedia.org/wiki/PageRank) score and color them using their [community](https://en.wikipedia.org/wiki/Community_structure).
-
-#### Warmup: igraph for computing statistics
-
-[igraph](http://igraph.org/python/) already has these algorithms implemented for us for small graphs. (See our cuGraph examples for big graphs.) If igraph is not already installed, fetch it with `pip install igraph`.
-
-We start by converting our edge dateframe into an igraph. The plotter can do the conversion for us using the *source* and *destination* bindings. Then we compute two new node attributes (*pagerank* & *community*).
-
-```python
-g = g.compute_igraph('pagerank', directed=True, params={'damping': 0.85}).compute_igraph('community_infomap')
-```
-
-The algorithm names `'pagerank'` and `'community_infomap'` correspond to method names of [igraph.Graph](https://igraph.org/python/api/latest/igraph.Graph.html). Likewise, optional `params={...}` allow specifying additional parameters.
-
-#### Bind node data to visual node attributes
-
-We can then bind the node `community` and `pagerank` columns to visualization attributes:
-
-```python
-g.bind(point_color='community', point_size='pagerank').plot()
-```
-
-See the [color palette documentation](https://hub.graphistry.com/docs/api/2/rest/upload/colors/#extendedpalette2) for specifying color values by using built-in ColorBrewer palettes (`int32`) or custom RGB values (`int64`).
-
-To control the position, we can add `.bind(point_x='colA', point_y='colB').settings(url_params={'play': 0})` ([see demos](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/external_layout) and [additional url parameters](https://hub.graphistry.com/docs/api/1/rest/url/#urloptions)]). In `api=1`, you created columns named `x` and `y`.
-
-You may also want to bind `point_title`: `.bind(point_title='colA')`.
-
-For more in-depth examples, check out the tutorials on [colors](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-colors.ipynb) and [sizes](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-sizes.ipynb).
-
-![Second Graph of Miserables](http://i.imgur.com/P7fm5sn.png)
-
-### Add edge colors and weights
-
-By default, edges get colored as a gradient between their source/destination node colors. You can override this by setting `.bind(edge_color='colA')`, similar to how node colors function. ([See color documentation](https://hub.graphistry.com/docs/api/2/rest/upload/colors/#extendedpalette2).)
-
-Similarly, you can bind the edge weight, where higher weights cause nodes to cluster closer together: `.bind(edge_weight='colA')`. [See tutorial](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/edge-weights.ipynb).
-
-For more in-depth examples, check out the tutorials on [colors](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-colors.ipynb) and [weighted clustering](demos/more_examples/graphistry_features/edge-weights.ipynb).
-
-### More advanced color and size controls
-
-You may want more controls like using gradients or maping specific values:
-
-```python
-g.encode_edge_color('int_col') # int32 or int64
-g.encode_edge_color('time_col', ["blue", "red"], as_continuous=True)
-g.encode_edge_color('type', as_categorical=True,
- categorical_mapping={"cat": "red", "sheep": "blue"}, default_mapping='#CCC')
-g.encode_edge_color('brand',
- categorical_mapping={'toyota': 'red', 'ford': 'blue'},
- default_mapping='#CCC')
-g.encode_point_size('numeric_col')
-g.encode_point_size('criticality',
- categorical_mapping={'critical': 200, 'ok': 100},
- default_mapping=50)
-g.encode_point_color('int_col') # int32 or int64
-g.encode_point_color('time_col', ["blue", "red"], as_continuous=True)
-g.encode_point_color('type', as_categorical=True,
- categorical_mapping={"cat": "red", "sheep": "blue"}, default_mapping='#CCC')
-```
-
-For more in-depth examples, check out the tutorials on [colors](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-colors.ipynb).
-
-### Custom icons and badges
-
-You can add a main icon and multiple peripherary badges to provide more visual information. Use column `type` for the icon type to appear visually in the legend. The glyph system supports text, icons, flags, and images, as well as multiple mapping and style controls.
-
-#### Main icon
-
-```python
-g.encode_point_icon(
- 'some_column',
- shape="circle", #clip excess
- categorical_mapping={
- 'macbook': 'laptop', #https://fontawesome.com/v4.7.0/icons/
- 'Canada': 'flag-icon-ca', #ISO3611-Alpha-2: https://github.com/datasets/country-codes/blob/master/data/country-codes.csv
- 'embedded_smile': 'data:svg...',
- 'external_logo': 'http://..../img.png'
- },
- default_mapping="question")
-g.encode_point_icon(
- 'another_column',
- continuous_binning=[
- [20, 'info'],
- [80, 'exclamation-circle'],
- [None, 'exclamation-triangle']
- ]
-)
-g.encode_point_icon(
- 'another_column',
- as_text=True,
- categorical_mapping={
- 'Canada': 'CA',
- 'United States': 'US'
- }
-)
-```
-
-For more in-depth examples, check out the tutorials on [icons](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-icons.ipynb).
-
-#### Badges
-
-```python
-# see icons examples for mappings and glyphs
-g.encode_point_badge('another_column', 'TopRight', categorical_mapping=...)
-
-g.encode_point_badge('another_column', 'TopRight', categorical_mapping=...,
- shape="circle",
- border={'width': 2, 'color': 'white', 'stroke': 'solid'},
- color={'mapping': {'categorical': {'fixed': {}, 'other': 'white'}}},
- bg={'color': {'mapping': {'continuous': {'bins': [], 'other': 'black'}}}})
-```
+import pandas as pd
-For more in-depth examples, check out the tutorials on [badges](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/encodings-badges.ipynb).
-
-#### Axes
-
-For more automated use, see the section on radial layouts below.
-
-Radial axes support three coloring types (`'external'`, `'internal'`, and `'space'`) and optional labels:
-
-```python
- g.encode_axis([
- {'r': 14, 'external': True, "label": "outermost"},
- {'r': 12, 'external': True},
- {'r': 10, 'space': True},
- {'r': 8, 'space': True},
- {'r': 6, 'internal': True},
- {'r': 4, 'space': True},
- {'r': 2, 'space': True, "label": "innermost"}
-])
-```
-
-Horizontal axis support optional labels and ranges:
-
-```python
-g.encode_axis([
- {"label": "a", "y": 2, "internal": True },
- {"label": "b", "y": 40, "external": True,
- "width": 20, "bounds": {"min": 40, "max": 400}},
-])
-```
-
-Radial axis are generally used with radial positioning:
-
-```python
-g2 = (g
- .nodes(
- g._nodes.assign(
- x = 1 + (g._nodes['ring']) * g._nodes['n'].apply(math.cos),
- y = 1 + (g._nodes['ring']) * g._nodes['n'].apply(math.sin)
- )).settings(url_params={'lockedR': 'true', 'play': 1000})
-```
-
-Horizontal axis are often used with pinned y and free x positions:
-
-```python
-g2 = (g
- .nodes(
- g._nodes.assign(
- y = 50 * g._nodes['level'])
- )).settings(url_params={'lockedY': 'true', 'play': 1000})
-```
-
-### Theming
-
-You can customize several style options to match your theme:
-
-```python
-g.addStyle(bg={'color': 'red'})
-g.addStyle(bg={
- 'color': '#333',
- 'gradient': {
- 'kind': 'radial',
- 'stops': [ ["rgba(255,255,255, 0.1)", "10%", "rgba(0,0,0,0)", "20%"] ]}})
-g.addStyle(bg={'image': {'url': 'http://site.com/cool.png', 'blendMode': 'multiply'}})
-g.addStyle(fg={'blendMode': 'color-burn'})
-g.addStyle(page={'title': 'My site'})
-g.addStyle(page={'favicon': 'http://site.com/favicon.ico'})
-g.addStyle(logo={'url': 'http://www.site.com/transparent_logo.png'})
-g.addStyle(logo={
- 'url': 'http://www.site.com/transparent_logo.png',
- 'dimensions': {'maxHeight': 200, 'maxWidth': 200},
- 'style': {'opacity': 0.5}
+# Raw data as Pandas CPU dataframes, cuDF GPU dataframes, Spark, ...
+df = pd.DataFrame({
+ 'src': ['Alice', 'Bob', 'Carol'],
+ 'dst': ['Bob', 'Carol', 'Alice'],
+ 'friendship': [0.3, 0.95, 0.8]
})
-```
-### Transforms
+# Bind
+g1 = graphistry.edges(df, 'src', 'dst')
-The below methods let you quickly manipulate graphs directly and with dataframe methods: Search, pattern mine, transform, and more:
+# Override styling defaults
+g1_styled = g1.encode_edge_color('friendship', as_continuous=True, ['blue', 'red'])
-```python
-from graphistry import n, e_forward, e_reverse, e_undirected, is_in
-g = (graphistry
- .edges(pd.DataFrame({
- 's': ['a', 'b'],
- 'd': ['b', 'c'],
- 'k1': ['x', 'y']
- }))
- .nodes(pd.DataFrame({
- 'n': ['a', 'b', 'c'],
- 'k2': [0, 2, 4, 6]
- })
-)
-
-g2 = graphistry.hypergraph(g._edges, ['s', 'd', 'k1'])['graph']
-g2.plot() # nodes are values from cols s, d, k1
-
-(g
- .materialize_nodes()
- .get_degrees()
- .get_indegrees()
- .get_outdegrees()
- .pipe(lambda g2: g2.nodes(g2._nodes.assign(t=x))) # transform
- .filter_edges_by_dict({"k1": "x"})
- .filter_nodes_by_dict({"k2": 4})
- .prune_self_edges()
- .hop( # filter to subgraph
- #almost all optional
- direction='forward', # 'reverse', 'undirected'
- hops=2, # number (1..n hops, inclusive) or None if to_fixed_point
- to_fixed_point=False,
-
- #every edge source node must match these
- source_node_match={"k2": 0, "k3": is_in(['a', 'b', 3, 4])},
- source_node_query='k2 == 0',
-
- #every edge must match these
- edge_match={"k1": "x"},
- edge_query='k1 == "x"',
-
- #every edge destination node must match these
- destination_node_match={"k2": 2},
- destination_node_query='k2 == 2 or k2 == 4',
- )
- .chain([ # filter to subgraph with Cypher-style GFQL
- n(),
- n({'k2': 0, "m": 'ok'}), #specific values
- n({'type': is_in(["type1", "type2"])}), #multiple valid values
- n(query='k2 == 0 or k2 == 4'), #dataframe query
- n(name="start"), # add column 'start':bool
- e_forward({'k1': 'x'}, hops=1), # same API as hop()
- e_undirected(name='second_edge'),
- e_reverse(
- {'k1': 'x'}, # edge property match
- hops=2, # 1 to 2 hops
- #same API as hop()
- source_node_match={"k2": 2},
- source_node_query='k2 == 2 or k2 == 4',
- edge_match={"k1": "x"},
- edge_query='k1 == "x"',
- destination_node_match={"k2": 0},
- destination_node_query='k2 == 0')
- ])
- # replace as one node the node w/ given id + transitively connected nodes w/ col=attr
- .collapse(node='some_id', column='some_col', attribute='some val')
-```
-
-Both `hop()` and `chain()` (GFQL) match dictionary expressions support dataframe series *predicates*. The above examples show `is_in([x, y, z, ...])`. Additional predicates include:
-
-* categorical: is_in, duplicated
-* temporal: is_month_start, is_month_end, is_quarter_start, is_quarter_end, is_year_start, is_year_end
-* numeric: gt, lt, ge, le, eq, ne, between, isna, notna
-* string: contains, startswith, endswith, match, isnumeric, isalpha, isdigit, islower, isupper, isspace, isalnum, isdecimal, istitle, isnull, notnull
-
-Both `hop()` and `chain()` will run on GPUs when passing in RAPIDS dataframes. Specify parameter `engine='cudf'` to be sure.
+# Connect: Free GPU accounts and self-hosting @ graphistry.com/get-started
+graphistry.register(api=3, username='your_username', password='your_password')
-#### Table to graph
-
-```python
-df = pd.read_csv('events.csv')
-hg = graphistry.hypergraph(df, ['user', 'email', 'org'], direct=True)
-g = hg['graph'] # g._edges: | src, dst, user, email, org, time, ... |
-g.plot()
+# Upload for GPU server visualization session
+g1_styled.plot()
```
-```python
-hg = graphistry.hypergraph(
- df,
- ['from_user', 'to_user', 'email', 'org'],
- direct=True,
- opts={
-
- # when direct=True, can define src -> [ dst1, dst2, ...] edges
- 'EDGES': {
- 'org': ['from_user'], # org->from_user
- 'from_user': ['email', 'to_user'], #from_user->email, from_user->to_user
- },
-
- 'CATEGORIES': {
- # determine which columns share the same namespace for node generation:
- # - if user 'louie' is both a from_user and to_user, show as 1 node
- # - if a user & org are both named 'louie', they will appear as 2 different nodes
- 'user': ['from_user', 'to_user']
- }
-})
-g = hg['graph']
-g.plot()
-```
+Explore [10 Minutes to Graphistry Visualization](https://pygraphistry.readthedocs.io/en/latest/visualization/10min.html) for more visualization examples and options
-#### Generate node table
-```python
-g = graphistry.edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}))
-g2 = g.materialize_nodes()
-g2._nodes # pd.DataFrame({'id': ['a', 'b', 'c']})
-```
+## PyGraphistry[AI] & GFQL quickstart - CPU & GPU
-#### Compute degrees
+**CPU graph pipeline** combining graph ML, AI, mining, and visualization:
```python
-g = graphistry.edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}))
-g2 = g.get_degrees()
-g2._nodes # pd.DataFrame({
- # 'id': ['a', 'b', 'c'],
- # 'degree_in': [0, 1, 1],
- # 'degree_out': [1, 1, 0],
- # 'degree': [1, 1, 1]
- #})
-```
-
-See also `get_indegrees()` and `get_outdegrees()`
+from graphistry import n, e, e_forward, e_reverse
-#### Use igraph (CPU) and cugraph (GPU) compute
-
-Install the plugin of choice and then:
-
-```python
-g2 = g.compute_igraph('pagerank')
+# Graph analytics
+g2 = g1.compute_igraph('pagerank')
assert 'pagerank' in g2._nodes.columns
-g3 = g.compute_cugraph('pagerank')
-assert 'pagerank' in g2._nodes.columns
-```
-
-#### Graph pattern matching
-
-PyGraphistry supports GFQL, its PyData-native variant of the popular Cypher graph query language, meaning you can do graph pattern matching directly from Pandas dataframes without installing a database or Java
-
-See also [graph pattern matching tutorial](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb) and the CPU/GPU [benchmark](https://github.com/graphistry/pygraphistry/tree/master/demos/gfql/benchmark_hops_cpu_gpu.ipynb)
-
-Traverse within a graph, or expand one graph against another
-
-Simple node and edge filtering via `filter_edges_by_dict()` and `filter_nodes_by_dict()`:
-
-```python
-g = graphistry.edges(pd.read_csv('data.csv'), 's', 'd')
-g2 = g.materialize_nodes()
+# Graph ML/AI
+g3 = g2.umap()
+assert ('x' in g3._nodes.columns) and ('y' in g3._nodes.columns)
-g3 = g.filter_edges_by_dict({"v": 1, "b": True})
-g4 = g.filter_nodes_by_dict({"v2": 1, "b2": True})
-```
-
-Method `.hop()` enables slightly more complicated edge filters:
-
-```python
-
-from graphistry import is_in, gt
-
-# (a)-[{"v": 1, "type": "z"}]->(b) based on g
-g2b = g2.hop(
- source_node_match={g2._node: "a"},
- edge_match={"v": 1, "type": "z"},
- destination_node_match={g2._node: "b"})
-g2b = g2.hop(
- source_node_query='n == "a"',
- edge_query='v == 1 and type == "z"',
- destination_node_query='n == "b"')
-
-# (a {x in [1,2] and y > 3})-[e]->(b) based on g
-g2c = g2.hop(
- source_node_match={
- g2._node: "a",
- "x": is_in([1,2]),
- "y": gt(3)
- },
- destination_node_match={g2._node: "b"})
-)
-
-# (a or b)-[1 to 8 hops]->(anynode), based on graph g2
-g3 = g2.hop(pd.DataFrame({g2._node: ['a', 'b']}), hops=8)
-
-# (a or b)-[1 to 8 hops]->(anynode), based on graph g2
-g3 = g2.hop(pd.DataFrame({g2._node: is_in(['a', 'b'])}), hops=8)
-
-# (c)<-[any number of hops]-(any node), based on graph g3
-# Note multihop matches check source/destination/edge match/query predicates
-# against every encountered edge for it to be included
-g4 = g3.hop(source_node_match={"node": "c"}, direction='reverse', to_fixed_point=True)
-
-# (c)-[incoming or outgoing edge]-(any node),
-# for c in g4 with expansions against nodes/edges in g2
-g5 = g2.hop(pd.DataFrame({g4._node: g4[g4._node]}), hops=1, direction='undirected')
-
-g5.plot()
-```
-
-Rich compound patterns are enabled via `.chain()`:
-
-```python
-from graphistry import n, e_forward, e_reverse, e_undirected, is_in
-
-g2.chain([ n() ])
-g2.chain([ n({"x": 1, "y": True}) ]),
-g2.chain([ n(query='x == 1 and y == True') ]),
-g2.chain([ n({"z": is_in([1,2,4,'z'])}) ]), # multiple valid values
-g2.chain([ e_forward({"type": "x"}, hops=2) ]) # simple multi-hop
-g3 = g2.chain([
- n(name="start"), # tag node matches
- e_forward(hops=3),
- e_forward(name="final_edge"), # tag edge matches
- n(name="end")
+# Graph querying with GFQL
+g4 = g3.chain([
+ n(query='pagerank > 0.1'), e_forward(), n(query='pagerank > 0.1')
])
-g2.chain(n(), e_forward(), n(), e_reverse(), n()]) # rich shapes
-print('# end nodes: ', len(g3._nodes[ g3._nodes.end ]))
-print('# end edges: ', len(g3._edges[ g3._edges.final_edge ]))
-```
-
-See table above for more predicates like `is_in()` and `gt()`
-
-Queries can be serialized and deserialized, such as for saving and remote execution:
+assert (g4._nodes.pagerank > 0.1).all()
-```python
-from graphistry.compute.chain import Chain
-
-pattern = Chain([n(), e(), n()])
-pattern_json = pattern.to_json()
-pattern2 = Chain.from_json(pattern_json)
-g.chain(pattern2).plot()
+# Upload for GPU server visualization session
+g4.plot()
```
-Benefit from automatic GPU acceleration by passing in GPU dataframes:
+The **automatic GPU modes** require almost no code changes:
```python
import cudf
+from graphistry import n, e, e_forward, e_reverse
-g1 = graphistry.edges(cudf.read_csv('data.csv'), 's', 'd')
-g2 = g1.chain(..., engine='cudf')
-```
-
-The parameter `engine` is optional, defaulting to `'auto'`.
+# Modified -- Rebind data as a GPU dataframe and swap in a GPU plugin call
+g1_gpu = g1.edges(cudf.from_pandas(df))
+g2 = g1_gpu.compute_cugraph('pagerank')
-#### Pipelining
-
-```python
-def capitalize(df, col):
- df2 = df.copy()
- df2[col] df[col].str.capitalize()
- return df2
-
-g
- .cypher('MATCH (a)-[e]->(b) RETURN a, e, b')
- .nodes(lambda g: capitalize(g._nodes, 'nTitle'))
- .edges(capitalize, None, None, 'eTitle'),
- .pipe(lambda g: g.nodes(g._nodes.pipe(capitalize, 'nTitle')))
-```
-
-#### Removing nodes
-
-```python
-g = graphistry.edges(pd.DataFrame({'s': ['a', 'b', 'c'], 'd': ['b', 'c', 'a']}))
-g2 = g.drop_nodes(['c']) # drops node c, edge c->a, edge b->c,
-```
-
-#### Keeping nodes
-
-```python
-# keep nodes [a,b,c] and edges [(a,b),(b,c)]
-g2 = g.keep_nodes(['a, b, c'])
-g2 = g.keep_nodes(pd.Series(['a, b, c']))
-g2 = g.keep_nodes(cudf.Series(['a, b, c']))
-```
-
-#### Collapsing adjacent nodes with specific k=v matches
-
-One col/val pair:
-
-```python
-g2 = g.collapse(
- node='root_node_id', # rooted traversal beginning
- column='some_col', # column to inspect
- attribute='some val' # value match to collapse on if hit
-)
-assert len(g2._nodes) <= len(g._nodes)
-```
-
-Collapse for all possible vals in a column, and assuming a stable root node id:
-
-```python
-g3 = g
-for v in g._nodes['some_col'].unique():
- g3 = g3.collapse(node='root_node_id', column='some_col', attribute=v)
-```
-
-### Hierarchical layouts: Tree and radial
-
-A hierachical view via horizontal or vertical trees, or radial. Graph data may also be presented using these layouts.
-
-#### Tree
-
-```python
-g = graphistry.edges(pd.DataFrame({'s': ['a', 'b', 'b'], 'd': ['b', 'c', 'd']}))
-
-g2a = g.tree_layout()
-g2b = g2.tree_layout(allow_cycles=False, remove_self_loops=False, vertical=False)
-g2c = g2.tree_layout(ascending=False, level_align='center')
-g2d = g2.tree_layout(level_sort_values_by=['type', 'degree'], level_sort_values_by_ascending=False)
-
-g3a = g2a.layout_settings(locked_r=True, play=1000)
-g3b = g2a.layout_settings(locked_y=True, play=0)
-g3c = g2a.layout_settings(locked_x=True)
-
-g4 = g2.tree_layout().rotate(90)
-```
-
-To use with non-tree data, e.g., graphs with cycles, we recommend computing a tree such as via a minimum spanning tree, and then using that achieved layout with this algorithm. Alternatively, the radial layouts may more naturally support your graph.
-
-#### Radial
-
-A hierarchical view via radial rings that may be more space-efficient and aesthetic than the equivalent tree layout
-
-Supports time-based, continuous, and categorical modes:
-
-##### Radial: Time-based
-
-Use when the value column defining the ring order is a time column. See [(Notebook tutorial)](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/layout_time_ring.ipynb)
-
-```python
-g.time_ring_layout().plot() # finds a time column and infers all settings
-
-g.time_ring_layout(
- time_col='my_node_time_col',
- num_rings=20,
- time_start=np.datetime64('2014-01-22'),
- time_end=np.datetime64('2015-01-22'),
- time_unit= 'Y', # s, m, h, D, W, M, Y, C
- min_r=100.0, # smallest ring radius
- max_r=1000.0, # biggest ring radius
- reverse=False,
- #format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None,
- #format_label: Optional[Callable[[np.datetime64, int, np.timedelta64], str]] = None,
- #play_ms: int = 2000,
- #engine='auto' # 'auto', 'pandas', 'cudf'
-).plot()
-```
-
-#### Continuous
-
-Use when the value column defining the ring order is a continuous number, like distance or amount. See [(Notebook tutorial)](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/layout_continuous_ring.ipynb)
-
-```python
-g.ring_continuous_layout() # find a numeric column and infers all settings
-
-g.ring_continuous_layout(
- ring_col='my_numeric_col',
- #v_start= # first ring at this value
- #v_end= # last ring at this value
- #v_step= # distance between rings in the value domain
- min_r=100.0, # smallest ring radius
- max_r=1000.0, # biggest ring radius
- normalize_ring_col=True, # remap [v_start,v_end] to [min_r,max_r]
- num_rings=20,
- ring_step=100,
-
- #Control axis labels and styles
- #axis: Optional[Union[Dict[float,str],List[str]]] = None,
- #format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None,
- #format_labels: Optional[Callable[[float, int, float], str]] = None,
-
- reverse=False,
- play_ms=0,
- #engine='auto', # 'auto', 'pandas', 'cudf'
-)
-```
-
-#### Categorical
-
-Use when the value column defining the ring order is a categorical value, such as a name or ID. See [(Notebook tutorial)](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/layout_categorical_ring.ipynb)
-
-```python
-g.ring_categorical_layout('my_categorical_col') # infers all settings
-
-g.ring_categorical_layout(
- ring_col='my_numeric_col',
- order=['col1', 'my_col2'],
- drop_empty=True, # remove unpopulated rings
- combine_unhandled=False, # Put values not covered by order into one ring Other vs a ring per unique value
- append_unhandled=True, # Append vs prepend
- min_r=100.0, # smallest ring radius
- max_r=1000.0, # biggest ring radius
-
- #Control axis labels and styles
- #axis: Optional[Dict[Any,str]] = None,
- #format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None,
- #format_labels: Optional[Callable[[Any, int, float], str]] = None,
-
- reverse=False,
- play_ms=0,
- #engine='auto', # 'auto', 'pandas', 'cudf'
-)
-```
-
-### Layout: Modularity weighted
-
-Weight edges by community membership to emphasize community structure. See [(Notebook tutorial)](https://github.com/graphistry/pygraphistry/blob/master/demos/more_examples/graphistry_features/layout_modularity_weighted.ipynb)
-
-```python
-g.modularity_weighted_layout().plot()
-g.modularity_weighted_layout('my_community_col').plot()
-g.modularity_weighted_layout(
- community_alg='louvain',
- engine='cudf',
- same_community_weight=2.0,
- cross_community_weight=0.3,
- edge_influence=2.0
-).plot()
-```
-
-### Plugin: igraph
-
-With `pip install graphistry[igraph]`, you can also use [`igraph` layouts](https://igraph.org/python/doc/api/igraph.Graph.html#layout):
-
-```python
-g.layout_igraph('sugiyama').plot()
-g.layout_igraph('sugiyama', directed=True, params={}).plot()
+# Unmodified -- Automatic GPU mode for all ML, AI, GFQL queries, & visualization APIs
+g3 = g2.umap()
+g4 = g3.chain([
+ n(query='pagerank > 0.1'), e_forward(), n(query='pagerank > 0.1')
+])
+g4.plot()
```
-See list [`layout_algs`](https://github.com/graphistry/pygraphistry/blob/master/graphistry/plugins/igraph.py#L365)
+Explore [10 Minutes to PyGraphistry](https://pygraphistry.readthedocs.io/en/latest/10min.html) for a wider variety of graph processing.
-### Plugin: cugraph
-With [Nvidia RAPIDS cuGraph](https://www.rapids.ai) install:
+## PyGraphistry documentation
-```python
-g.layout_cugraph('force_atlas2').plot()
-help(g.layout_cugraph)
-```
+* [Main PyGraphistry documentation](https://pygraphistry.readthedocs.io/en/latest/)
+* 10 Minutes to: [PyGraphistry](https://pygraphistry.readthedocs.io/en/latest/10min.html), [Visualization](https://pygraphistry.readthedocs.io/en/latest/visualization/10min.html), [GFQL](https://pygraphistry.readthedocs.io/en/latest/gfql/about.html)
+* Get started: [Install](https://pygraphistry.readthedocs.io/en/latest/install/index.html), [UI Guide](https://hub.graphistry.com/docs/ui/index/), [Notebooks](https://pygraphistry.readthedocs.io/en/latest/demos/for_analysis.html)
+* Performance: [PyGraphistry CPU+GPU](https://pygraphistry.readthedocs.io/en/latest/performance.html) & [GFQL CPU+GPU](https://pygraphistry.readthedocs.io/en/latest/gfql/performance.html)
+* API References
+ - [PyGraphistry API Reference](https://pygraphistry.readthedocs.io/en/latest/api/index.html): [Visualization & Compute](https://pygraphistry.readthedocs.io/en/latest/visualization/index.html), [PyGraphistry Cheatsheet](https://pygraphistry.readthedocs.io/en/latest/cheatsheet.html)
+ - [GFQL Documentation](https://pygraphistry.readthedocs.io/en/latest/gfql/index.html): [GFQL Cheatsheet](https://pygraphistry.readthedocs.io/en/latest/gfql/quick.html) and [GFQL Operator Cheatsheet](https://pygraphistry.readthedocs.io/en/latest/gfql/predicates/quick.html)
+ - [Plugins](https://pygraphistry.readthedocs.io/en/latest/plugins.html): Databricks, Splunk, Neptune, Neo4j, RAPIDS, and more
+ - Web: [iframe](https://hub.graphistry.com/docs/api/1/rest/url/#urloptions), [JavaScript](https://hub.graphistry.com/static/js-docs/index.html?path=/docs/introduction--docs), [REST](https://hub.graphistry.com/docs/api/1/rest/auth/)
-See list [`layout_algs`](https://github.com/graphistry/pygraphistry/blob/master/graphistry/plugins/cugraph.py#L315)
+## Graphistry ecosystem
-#### Group-in-a-box layout
+- **Graphistry server:**
+ - Launch - [Graphistry Hub, Graphistry cloud marketplaces, and self-hosting](https://www.graphistry.com/get-started)
+ - Self-hosting: [Administration (including Docker)](https://github.com/graphistry/graphistry-cli) & [Kubernetes](https://github.com/graphistry/graphistry-helm)
-[Group-in-a-box layout](https://ieeexplore.ieee.org/document/6113135) with igraph/pandas and cugraph/cudf implementations:
+- **Graphistry client APIs:**
+ - Web: [iframe](https://hub.graphistry.com/docs/api/1/rest/url/#urloptions), [JavaScript](https://hub.graphistry.com/static/js-docs/index.html?path=/docs/introduction--docs), [REST](https://hub.graphistry.com/docs/api/1/rest/auth/)
+ - [PyGraphistry](https://pygraphistry.readthedocs.io/en/latest/index.html)
+ - [Graphistry for Microsoft PowerBI](https://hub.graphistry.com/docs/powerbi/pbi/)
-```python
-g.group_in_a_box_layout().plot()
-g.group_in_a_box_layout(
- partition_alg='ecg', # see igraph/cugraph algs
- #partition_key='some_col', # use existing col
- #layout_alg='circle', # see igraph/cugraph algs
- #x, y, w, h
- #encode_colors=False,
- #colors=['#FFF', '#FF0', ...]
- engine='cudf'
-).plot()
-```
+- **Additional projects**:
+ - [Louie.ai](https://louie.ai/): GenAI-native notebooks & dashboards to talk to your databases & Graphistry
+ - [graph-app-kit](https://github.com/graphistry/graph-app-kit): Streamlit Python dashboards with batteries-include graph packages
+ - [cu-cat](https://chat.openai.com/chat): Automatic GPU feature engineering
-### Control render settings
-```python
-g = graphistry.edges(pd.DataFrame({'s': ['a', 'b', 'b'], 'd': ['b', 'c', 'd']}))
-g2 = g.scene_settings(
- #hide menus
- menu=False,
- info=False,
- #tweak graph
- show_arrows=False,
- point_size=1.0,
- edge_curvature=0.0,
- edge_opacity=0.5,
- point_opacity=0.9
-).plot()
+## Community and support
-```
+- [Blog](https://www.graphistry.com/blog) for tutorials, case studies, and updates
+- [Slack](https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g): Join the Graphistry Community Slack for discussions and support
+- [Twitter](https://twitter.com/graphistry) & [LinkedIn](https://www.linkedin.com/company/graphistry): Follow for updates
+- [GitHub Issues](https://github.com/graphistry/pygraphistry/issues) open source support
+- [Graphistry ZenDesk](https://graphistry.zendesk.com/) dedicated enterprise support
-With `pip install graphistry[igraph]`, you can also use [`igraph` layouts](https://igraph.org/python/doc/api/igraph.Graph.html#layout):
+## Contribute
-```python
-g.layout_igraph('sugiyama').plot()
-g.layout_igraph('sugiyama', directed=True, params={}).plot()
-```
+See [CONTRIBUTE](https://pygraphistry.readthedocs.io/en/latest/CONTRIBUTE.html) and [DEVELOP](https://pygraphistry.readthedocs.io/en/latest/DEVELOP.html) for participating in PyGraphistry development, or reach out to our team
-## Next Steps
-
-1. Create a free public data [Graphistry Hub](https://www.graphistry.com/get-started) account or [one-click launch a private Graphistry instance in AWS](https://www.graphistry.com/get-started)
-2. Check out the [analyst](https://github.com/graphistry/pygraphistry/tree/master/demos/for_analysis.ipynb) and [developer](https://github.com/graphistry/pygraphistry/tree/master/demos/for_developers.ipynb) introductions, or [try your own CSV](https://github.com/graphistry/pygraphistry/tree/master/demos/upload_csv_miniapp.ipynb)
-3. Explore the [demos folder](https://github.com/graphistry/pygraphistry/tree/master/demos) for your favorite [file format, database, API](https://github.com/graphistry/pygraphistry/tree/master/demos/demos_databases_apis), use case domain, kind of analysis, and [visual analytics feature](https://github.com/graphistry/pygraphistry/tree/master/demos/more_examples/graphistry_features)
-
-## Resources
-
-* Graphistry [In-Tool UI Guide](https://hub.graphistry.com/docs/ui/index/)
-* [General and REST API docs](https://hub.graphistry.com/docs/api/):
- * [URL settings](https://hub.graphistry.com/docs/api/1/rest/url/#urloptions)
- * [Authentication](https://hub.graphistry.com/docs/api/1/rest/auth/)
- * [Uploading](https://hub.graphistry.com/docs/api/2/rest/upload/#createdataset2), including multiple file formats and settings
- * [Color bindings](https://hub.graphistry.com/docs/api/2/rest/upload/colors/#extendedpalette2) and [color palettes](https://hub.graphistry.com/docs/api/api-color-palettes/) (ColorBrewer)
- * Bindings and colors, REST API, embedding URLs and URL parameters, dynamic JS API, and more
- * JavaScript and more!
-* Python-specific
- * [Python API ReadTheDocs](http://pygraphistry.readthedocs.org/en/latest/)
- * Within a notebook, you can always run `help(graphistry)`, `help(graphistry.hypergraph)`, etc.
-* [Administration docs](https://github.com/graphistry/graphistry-cli) for sizing, installing, configuring, managing, and updating Graphistry servers
-* [Graph-App-Kit Dashboarding](https://github.com/graphistry/graph-app-kit) dashboarding
diff --git a/bin/lint.sh b/bin/lint.sh
index 8c4d2f3c2..22c986570 100755
--- a/bin/lint.sh
+++ b/bin/lint.sh
@@ -19,7 +19,7 @@ flake8 \
graphistry \
--exclude graphistry/graph_vector_pb2.py,graphistry/_version.py \
--count \
- --ignore=C901,E121,E122,E123,E124,E125,E128,E131,E144,E201,E202,E203,E231,E251,E265,E301,E302,E303,E401,E501,E722,F401,W291,W293 \
+ --ignore=C901,E121,E122,E123,E124,E125,E128,E131,E144,E201,E202,E203,E231,E251,E265,E301,E302,E303,E401,E501,E722,F401,W291,W293,W503 \
--max-complexity=10 \
--max-line-length=127 \
--statistics
diff --git a/bin/test-graphviz.sh b/bin/test-graphviz.sh
new file mode 100755
index 000000000..765cc8577
--- /dev/null
+++ b/bin/test-graphviz.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -ex
+
+# Run from project root
+# - Args get passed to pytest phase
+# Non-zero exit code on fail
+
+# Assume [pygraphviz,test], apt-get install graphviz graphviz-dev
+
+python -m pytest --version
+
+python -B -m pytest -vv \
+ graphistry/tests/plugins/test_graphviz.py
diff --git a/demos/ai/cyber/CyberSecurity-Slim.ipynb b/demos/ai/cyber/CyberSecurity-Slim.ipynb
index 9b6cfdb7a..d8ee1c916 100644
--- a/demos/ai/cyber/CyberSecurity-Slim.ipynb
+++ b/demos/ai/cyber/CyberSecurity-Slim.ipynb
@@ -190,7 +190,7 @@
"id": "125f6ef0",
"metadata": {},
"source": [
- "# Fast Incident Response\n",
+ "## Fast Incident Response\n",
"An Incident Responder needs to quickly find which IP is the attacker.\n",
"\n",
"If, say, a predictive model enriched the data, responders could repeat the pipeline on new data\n",
@@ -285,7 +285,7 @@
"id": "caf504e5",
"metadata": {},
"source": [
- "# Do we have a predictive model?\n",
+ "## Do we have a predictive model?\n",
"\n",
"Using the x, y's we get from autofeaturization, we fit two RandomForest models"
]
@@ -378,7 +378,7 @@
"id": "671557b5",
"metadata": {},
"source": [
- "# Let's remove edges and see if there is a model of just 'common features' (ie no ip addresses)\n",
+ "## Let's remove edges and see if there is a model of just 'common features' (ie no ip addresses)\n",
"\n",
"Given learnings, we want to see if there is a model that does not use edge information (ie, no IP addresses, only connection metadata)"
]
@@ -525,7 +525,7 @@
"id": "71166b62",
"metadata": {},
"source": [
- "# Hence we see that including just common features clusters botnet traffic together under featurization and UMAP"
+ "## Hence we see that including just common features clusters botnet traffic together under featurization and UMAP"
]
},
{
@@ -557,7 +557,7 @@
"id": "762b80ed",
"metadata": {},
"source": [
- "# Now we dive deeper\n",
+ "## Now we dive deeper\n",
"-----------------------------------------"
]
},
@@ -566,7 +566,7 @@
"id": "2bac394b",
"metadata": {},
"source": [
- "# Let's encode the graph as a DGL graph for use in Machine Learning"
+ "## Let's encode the graph as a DGL graph for use in Machine Learning"
]
},
{
@@ -757,7 +757,7 @@
"id": "00751e3b",
"metadata": {},
"source": [
- "# Contributions\n",
+ "## Contributions\n",
"\n",
"Now we know how to take raw data and turn them into actionable features and models using the Graphistry[ai] API.\n",
"\n",
diff --git a/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.ipynb b/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.ipynb
old mode 100644
new mode 100755
index ab4126ce8..515a27057
--- a/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.ipynb
+++ b/demos/demos_databases_apis/databricks_pyspark/graphistry-notebook-dashboard.ipynb
@@ -39,128 +39,122 @@
}
},
"source": [
- "## Install & connect"
+ "## Install & authenticate with graphistry server"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "eaf03d3c-d046-4f96-825e-5db2355af383",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Requirement already satisfied: graphistry in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (0.28.5)\r\n",
- "Requirement already satisfied: numpy in /databricks/python3/lib/python3.9/site-packages (from graphistry) (1.20.3)\r\n",
- "Requirement already satisfied: pandas>=0.17.0 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (1.3.4)\r\n",
- "Requirement already satisfied: packaging>=20.1 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (21.0)\r\n",
- "Requirement already satisfied: squarify in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (from graphistry) (0.4.3)\r\n",
- "Requirement already satisfied: palettable>=3.0 in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (from graphistry) (3.3.0)\r\n",
- "Requirement already satisfied: typing-extensions in /databricks/python3/lib/python3.9/site-packages (from graphistry) (3.10.0.2)\r\n",
- "Requirement already satisfied: pyarrow>=0.15.0 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (7.0.0)\r\n",
- "Requirement already satisfied: requests in /databricks/python3/lib/python3.9/site-packages (from graphistry) (2.26.0)\r\n",
- "Requirement already satisfied: pyparsing>=2.0.2 in /databricks/python3/lib/python3.9/site-packages (from packaging>=20.1->graphistry) (3.0.4)\r\n",
- "Requirement already satisfied: python-dateutil>=2.7.3 in /databricks/python3/lib/python3.9/site-packages (from pandas>=0.17.0->graphistry) (2.8.2)\r\n",
- "Requirement already satisfied: pytz>=2017.3 in /databricks/python3/lib/python3.9/site-packages (from pandas>=0.17.0->graphistry) (2021.3)\r\n",
- "Requirement already satisfied: six>=1.5 in /databricks/python3/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas>=0.17.0->graphistry) (1.16.0)\r\n",
- "Requirement already satisfied: idna<4,>=2.5 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (3.2)\r\n",
- "Requirement already satisfied: charset-normalizer~=2.0.0 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (2.0.4)\r\n",
- "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (1.26.7)\r\n",
- "Requirement already satisfied: certifi>=2017.4.17 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (2021.10.8)\r\n",
- "\u001b[33mWARNING: You are using pip version 21.2.4; however, version 22.3.1 is available.\r\n",
- "You should consider upgrading via the '/local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/bin/python -m pip install --upgrade pip' command.\u001b[0m\r\n"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "Requirement already satisfied: graphistry in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (0.28.5)\r\nRequirement already satisfied: numpy in /databricks/python3/lib/python3.9/site-packages (from graphistry) (1.20.3)\r\nRequirement already satisfied: pandas>=0.17.0 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (1.3.4)\r\nRequirement already satisfied: packaging>=20.1 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (21.0)\r\nRequirement already satisfied: squarify in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (from graphistry) (0.4.3)\r\nRequirement already satisfied: palettable>=3.0 in /local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/lib/python3.9/site-packages (from graphistry) (3.3.0)\r\nRequirement already satisfied: typing-extensions in /databricks/python3/lib/python3.9/site-packages (from graphistry) (3.10.0.2)\r\nRequirement already satisfied: pyarrow>=0.15.0 in /databricks/python3/lib/python3.9/site-packages (from graphistry) (7.0.0)\r\nRequirement already satisfied: requests in /databricks/python3/lib/python3.9/site-packages (from graphistry) (2.26.0)\r\nRequirement already satisfied: pyparsing>=2.0.2 in /databricks/python3/lib/python3.9/site-packages (from packaging>=20.1->graphistry) (3.0.4)\r\nRequirement already satisfied: python-dateutil>=2.7.3 in /databricks/python3/lib/python3.9/site-packages (from pandas>=0.17.0->graphistry) (2.8.2)\r\nRequirement already satisfied: pytz>=2017.3 in /databricks/python3/lib/python3.9/site-packages (from pandas>=0.17.0->graphistry) (2021.3)\r\nRequirement already satisfied: six>=1.5 in /databricks/python3/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas>=0.17.0->graphistry) (1.16.0)\r\nRequirement already satisfied: idna<4,>=2.5 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (3.2)\r\nRequirement already satisfied: charset-normalizer~=2.0.0 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (2.0.4)\r\nRequirement already satisfied: urllib3<1.27,>=1.21.1 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (1.26.7)\r\nRequirement already satisfied: certifi>=2017.4.17 in /databricks/python3/lib/python3.9/site-packages (from requests->graphistry) (2021.10.8)\r\n\u001b[33mWARNING: You are using pip version 21.2.4; however, version 22.3.1 is available.\r\nYou should consider upgrading via the '/local_disk0/.ephemeral_nfs/envs/pythonEnv-969db892-92cf-4b34-a5cf-61642fa76e77/bin/python -m pip install --upgrade pip' command.\u001b[0m\r\n",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "type": "ansi"
- }
+ "outputs": [],
+ "source": [
+ "# Uncomment and run first time or\n",
+ "# have databricks admin install graphistry python library: \n",
+ "# https://docs.databricks.com/en/libraries/package-repositories.html#pypi-package\n",
+ "\n",
+ "#%pip install graphistry\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 0,
+ "metadata": {
+ "application/vnd.databricks.v1+cell": {
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
},
- "output_type": "display_data"
+ "inputWidgets": {},
+ "nuid": "8ad9b072-f037-4d4a-a1fa-ca2c14bd639f",
+ "showTitle": false,
+ "title": ""
}
- ],
+ },
+ "outputs": [],
"source": [
- "# Uncomment and run first time\n",
- "! pip install graphistry\n",
- "#! pip install git+https://github.com/graphistry/pygraphistry.git@dev/databricks\n",
- " \n",
- "# Can sometimes help:\n",
- "#dbutils.library.restartPython()"
+ "# Required to run after pip install to pick up new python package: \n",
+ "dbutils.library.restartPython()"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
- "nuid": "9e649f0e-fca5-4be6-8ad6-fa781bbb81d6",
+ "nuid": "cfd253ba-c647-4c45-8048-58b0ca427569",
"showTitle": false,
"title": ""
}
},
"outputs": [],
"source": [
- "#Optional: Uncomment - We find this speeds up calls 10%+ on some datasets\n",
- "#spark.conf.set(\"spark.sql.execution.arrow.enabled\", \"true\")"
+ "import graphistry # if not yet available, install pygraphistry and/or restart Python kernel using the cells above\n",
+ "graphistry.__version__"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"application/vnd.databricks.v1+cell": {
"cellMetadata": {},
"inputWidgets": {},
- "nuid": "cfd253ba-c647-4c45-8048-58b0ca427569",
+ "nuid": "55e30c26-3a8c-46dc-8eff-bd730d3c7798",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Out[12]: '0.28.5'"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "Out[12]: '0.28.5'",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "type": "ansi"
- }
+ "source": [
+ "### Use databricks secrets to retrieve graphistry creds and pass to register "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 0,
+ "metadata": {
+ "application/vnd.databricks.v1+cell": {
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
},
- "output_type": "display_data"
+ "inputWidgets": {},
+ "nuid": "b5496fa5-525a-48c9-ad46-0ce17ebdc4f8",
+ "showTitle": false,
+ "title": ""
}
- ],
+ },
+ "outputs": [],
"source": [
- "import graphistry # if not yet available, install and/or restart Python kernel using the above\n",
"\n",
- "# To specify Graphistry account & server, use:\n",
- "# graphistry.register(api=3, username='...', password='...', protocol='https', server='hub.graphistry.com')\n",
- "# For more options, see https://github.com/graphistry/pygraphistry#configure\n",
+ "# As a best practice, use databricks secrets to store graphistry personal key (access token)\n",
+ "# create databricks secrets: https://docs.databricks.com/en/security/secrets/index.html \n",
+ "# create graphistry personal key: https://hub.graphistry.com/account/tokens\n",
"\n",
- "graphistry.__version__"
+ "graphistry.register(api=3, \n",
+ " personal_key_id=dbutils.secrets.get(scope=\"my-secret-scope\", key=\"graphistry-personal_key_id\"), \n",
+ " personal_key_secret=dbutils.secrets.get(scope=\"my-secret-scope\", key=\"graphistry-personal_key_secret\"), \n",
+ " protocol='https',\n",
+ " server='hub.graphistry.com')\n",
+ "\n",
+ "# Alternatively, use username and password: \n",
+ "# graphistry.register(api=3, username='...', password='...', protocol='https', server='hub.graphistry.com')\n",
+ "# For more options, see https://github.com/graphistry/pygraphistry#configure"
]
},
{
@@ -188,337 +182,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "c187c650-01c2-4e48-b8e0-803e937cdb11",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "type: \n"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "type: \n",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "type": "ansi"
- }
- },
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "battery_level c02_level cca2 cca3 cn device_id device_name humidity ip latitude lcd longitude scale temp timestamp 8 868 US USA United States 1 meter-gauge-1xbYRYcj 51 68.161.225.1 38.0 green -97.0 Celsius 34 1458444054093 7 1473 NO NOR Norway 2 sensor-pad-2n2Pea 70 213.161.254.1 62.47 red 6.15 Celsius 11 1458444054119 2 1556 IT ITA Italy 3 device-mac-36TWSKiT 44 88.36.5.1 42.83 red 12.83 Celsius 19 1458444054120 6 1080 US USA United States 4 sensor-pad-4mzWkz 32 66.39.173.154 44.06 yellow -121.32 Celsius 28 1458444054121 4 931 PH PHL Philippines 5 therm-stick-5gimpUrBB 62 203.82.41.9 14.58 green 120.97 Celsius 25 1458444054122 3 1210 US USA United States 6 sensor-pad-6al7RTAobR 51 204.116.105.67 35.93 yellow -85.46 Celsius 27 1458444054122 3 1129 CN CHN China 7 meter-gauge-7GeDoanM 26 220.173.179.1 22.82 yellow 108.32 Celsius 18 1458444054123 0 1536 JP JPN Japan 8 sensor-pad-8xUD6pzsQI 35 210.173.177.1 35.69 red 139.69 Celsius 27 1458444054123 3 807 JP JPN Japan 9 device-mac-9GcjZ2pw 85 118.23.68.227 35.69 green 139.69 Celsius 13 1458444054124 7 1470 US USA United States 10 sensor-pad-10BsywSYUF 56 208.109.163.218 33.61 red -111.89 Celsius 26 1458444054125
"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "aggData": [],
- "aggError": "",
- "aggOverflow": false,
- "aggSchema": [],
- "aggSeriesLimitReached": false,
- "aggType": "",
- "arguments": {},
- "columnCustomDisplayInfos": {},
- "data": [
- [
- 8,
- 868,
- "US",
- "USA",
- "United States",
- 1,
- "meter-gauge-1xbYRYcj",
- 51,
- "68.161.225.1",
- 38,
- "green",
- -97,
- "Celsius",
- 34,
- 1458444054093
- ],
- [
- 7,
- 1473,
- "NO",
- "NOR",
- "Norway",
- 2,
- "sensor-pad-2n2Pea",
- 70,
- "213.161.254.1",
- 62.47,
- "red",
- 6.15,
- "Celsius",
- 11,
- 1458444054119
- ],
- [
- 2,
- 1556,
- "IT",
- "ITA",
- "Italy",
- 3,
- "device-mac-36TWSKiT",
- 44,
- "88.36.5.1",
- 42.83,
- "red",
- 12.83,
- "Celsius",
- 19,
- 1458444054120
- ],
- [
- 6,
- 1080,
- "US",
- "USA",
- "United States",
- 4,
- "sensor-pad-4mzWkz",
- 32,
- "66.39.173.154",
- 44.06,
- "yellow",
- -121.32,
- "Celsius",
- 28,
- 1458444054121
- ],
- [
- 4,
- 931,
- "PH",
- "PHL",
- "Philippines",
- 5,
- "therm-stick-5gimpUrBB",
- 62,
- "203.82.41.9",
- 14.58,
- "green",
- 120.97,
- "Celsius",
- 25,
- 1458444054122
- ],
- [
- 3,
- 1210,
- "US",
- "USA",
- "United States",
- 6,
- "sensor-pad-6al7RTAobR",
- 51,
- "204.116.105.67",
- 35.93,
- "yellow",
- -85.46,
- "Celsius",
- 27,
- 1458444054122
- ],
- [
- 3,
- 1129,
- "CN",
- "CHN",
- "China",
- 7,
- "meter-gauge-7GeDoanM",
- 26,
- "220.173.179.1",
- 22.82,
- "yellow",
- 108.32,
- "Celsius",
- 18,
- 1458444054123
- ],
- [
- 0,
- 1536,
- "JP",
- "JPN",
- "Japan",
- 8,
- "sensor-pad-8xUD6pzsQI",
- 35,
- "210.173.177.1",
- 35.69,
- "red",
- 139.69,
- "Celsius",
- 27,
- 1458444054123
- ],
- [
- 3,
- 807,
- "JP",
- "JPN",
- "Japan",
- 9,
- "device-mac-9GcjZ2pw",
- 85,
- "118.23.68.227",
- 35.69,
- "green",
- 139.69,
- "Celsius",
- 13,
- 1458444054124
- ],
- [
- 7,
- 1470,
- "US",
- "USA",
- "United States",
- 10,
- "sensor-pad-10BsywSYUF",
- 56,
- "208.109.163.218",
- 33.61,
- "red",
- -111.89,
- "Celsius",
- 26,
- 1458444054125
- ]
- ],
- "datasetInfos": [],
- "dbfsResultPath": null,
- "isJsonSchema": true,
- "metadata": {},
- "overflow": false,
- "plotOptions": {
- "customPlotOptions": {},
- "displayType": "table",
- "pivotAggregation": null,
- "pivotColumns": [],
- "xColumns": [],
- "yColumns": []
- },
- "removedWidgets": [],
- "schema": [
- {
- "metadata": "{}",
- "name": "battery_level",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "c02_level",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "cca2",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "cca3",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "cn",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "device_id",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "device_name",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "humidity",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "ip",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "latitude",
- "type": "\"double\""
- },
- {
- "metadata": "{}",
- "name": "lcd",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "longitude",
- "type": "\"double\""
- },
- {
- "metadata": "{}",
- "name": "scale",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "temp",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "timestamp",
- "type": "\"long\""
- }
- ],
- "type": "table"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"# Load the data from its source.\n",
"devices = spark.read \\\n",
@@ -532,393 +209,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "c69b91ed-c172-47b7-9bb7-27532202179a",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/html": [
- "device_id cca2 cca3 cn device_name ip location_rounded1 location_rounded2 battery_level_min c02_level_min humidity_min timestamp_min battery_level_max c02_level_max humidity_max timestamp_max battery_level_avg c02_level_avg humidity_avg timestamp_avg 1 US USA United States meter-gauge-1xbYRYcj 68.161.225.1 38_-97 40_-100 8 868 51 1458444054093 8 868 51 1458444054093 8.0 868.0 51.0 1.458444054093E12 2 NO NOR Norway sensor-pad-2n2Pea 213.161.254.1 62_6 60_10 7 1473 70 1458444054119 7 1473 70 1458444054119 7.0 1473.0 70.0 1.458444054119E12 3 IT ITA Italy device-mac-36TWSKiT 88.36.5.1 43_13 40_10 2 1556 44 1458444054120 2 1556 44 1458444054120 2.0 1556.0 44.0 1.45844405412E12 4 US USA United States sensor-pad-4mzWkz 66.39.173.154 44_-121 40_-120 6 1080 32 1458444054121 6 1080 32 1458444054121 6.0 1080.0 32.0 1.458444054121E12 5 PH PHL Philippines therm-stick-5gimpUrBB 203.82.41.9 15_121 10_120 4 931 62 1458444054122 4 931 62 1458444054122 4.0 931.0 62.0 1.458444054122E12 6 US USA United States sensor-pad-6al7RTAobR 204.116.105.67 36_-85 40_-90 3 1210 51 1458444054122 3 1210 51 1458444054122 3.0 1210.0 51.0 1.458444054122E12 7 CN CHN China meter-gauge-7GeDoanM 220.173.179.1 23_108 20_110 3 1129 26 1458444054123 3 1129 26 1458444054123 3.0 1129.0 26.0 1.458444054123E12 8 JP JPN Japan sensor-pad-8xUD6pzsQI 210.173.177.1 36_140 40_140 0 1536 35 1458444054123 0 1536 35 1458444054123 0.0 1536.0 35.0 1.458444054123E12 9 JP JPN Japan device-mac-9GcjZ2pw 118.23.68.227 36_140 40_140 3 807 85 1458444054124 3 807 85 1458444054124 3.0 807.0 85.0 1.458444054124E12 10 US USA United States sensor-pad-10BsywSYUF 208.109.163.218 34_-112 30_-110 7 1470 56 1458444054125 7 1470 56 1458444054125 7.0 1470.0 56.0 1.458444054125E12
"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "aggData": [],
- "aggError": "",
- "aggOverflow": false,
- "aggSchema": [],
- "aggSeriesLimitReached": false,
- "aggType": "",
- "arguments": {},
- "columnCustomDisplayInfos": {},
- "data": [
- [
- 1,
- "US",
- "USA",
- "United States",
- "meter-gauge-1xbYRYcj",
- "68.161.225.1",
- "38_-97",
- "40_-100",
- 8,
- 868,
- 51,
- 1458444054093,
- 8,
- 868,
- 51,
- 1458444054093,
- 8,
- 868,
- 51,
- 1458444054093
- ],
- [
- 2,
- "NO",
- "NOR",
- "Norway",
- "sensor-pad-2n2Pea",
- "213.161.254.1",
- "62_6",
- "60_10",
- 7,
- 1473,
- 70,
- 1458444054119,
- 7,
- 1473,
- 70,
- 1458444054119,
- 7,
- 1473,
- 70,
- 1458444054119
- ],
- [
- 3,
- "IT",
- "ITA",
- "Italy",
- "device-mac-36TWSKiT",
- "88.36.5.1",
- "43_13",
- "40_10",
- 2,
- 1556,
- 44,
- 1458444054120,
- 2,
- 1556,
- 44,
- 1458444054120,
- 2,
- 1556,
- 44,
- 1458444054120
- ],
- [
- 4,
- "US",
- "USA",
- "United States",
- "sensor-pad-4mzWkz",
- "66.39.173.154",
- "44_-121",
- "40_-120",
- 6,
- 1080,
- 32,
- 1458444054121,
- 6,
- 1080,
- 32,
- 1458444054121,
- 6,
- 1080,
- 32,
- 1458444054121
- ],
- [
- 5,
- "PH",
- "PHL",
- "Philippines",
- "therm-stick-5gimpUrBB",
- "203.82.41.9",
- "15_121",
- "10_120",
- 4,
- 931,
- 62,
- 1458444054122,
- 4,
- 931,
- 62,
- 1458444054122,
- 4,
- 931,
- 62,
- 1458444054122
- ],
- [
- 6,
- "US",
- "USA",
- "United States",
- "sensor-pad-6al7RTAobR",
- "204.116.105.67",
- "36_-85",
- "40_-90",
- 3,
- 1210,
- 51,
- 1458444054122,
- 3,
- 1210,
- 51,
- 1458444054122,
- 3,
- 1210,
- 51,
- 1458444054122
- ],
- [
- 7,
- "CN",
- "CHN",
- "China",
- "meter-gauge-7GeDoanM",
- "220.173.179.1",
- "23_108",
- "20_110",
- 3,
- 1129,
- 26,
- 1458444054123,
- 3,
- 1129,
- 26,
- 1458444054123,
- 3,
- 1129,
- 26,
- 1458444054123
- ],
- [
- 8,
- "JP",
- "JPN",
- "Japan",
- "sensor-pad-8xUD6pzsQI",
- "210.173.177.1",
- "36_140",
- "40_140",
- 0,
- 1536,
- 35,
- 1458444054123,
- 0,
- 1536,
- 35,
- 1458444054123,
- 0,
- 1536,
- 35,
- 1458444054123
- ],
- [
- 9,
- "JP",
- "JPN",
- "Japan",
- "device-mac-9GcjZ2pw",
- "118.23.68.227",
- "36_140",
- "40_140",
- 3,
- 807,
- 85,
- 1458444054124,
- 3,
- 807,
- 85,
- 1458444054124,
- 3,
- 807,
- 85,
- 1458444054124
- ],
- [
- 10,
- "US",
- "USA",
- "United States",
- "sensor-pad-10BsywSYUF",
- "208.109.163.218",
- "34_-112",
- "30_-110",
- 7,
- 1470,
- 56,
- 1458444054125,
- 7,
- 1470,
- 56,
- 1458444054125,
- 7,
- 1470,
- 56,
- 1458444054125
- ]
- ],
- "datasetInfos": [],
- "dbfsResultPath": null,
- "isJsonSchema": true,
- "metadata": {},
- "overflow": false,
- "plotOptions": {
- "customPlotOptions": {},
- "displayType": "table",
- "pivotAggregation": null,
- "pivotColumns": [],
- "xColumns": [],
- "yColumns": []
- },
- "removedWidgets": [],
- "schema": [
- {
- "metadata": "{}",
- "name": "device_id",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "cca2",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "cca3",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "cn",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "device_name",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "ip",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "location_rounded1",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "location_rounded2",
- "type": "\"string\""
- },
- {
- "metadata": "{}",
- "name": "battery_level_min",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "c02_level_min",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "humidity_min",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "timestamp_min",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "battery_level_max",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "c02_level_max",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "humidity_max",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "timestamp_max",
- "type": "\"long\""
- },
- {
- "metadata": "{}",
- "name": "battery_level_avg",
- "type": "\"double\""
- },
- {
- "metadata": "{}",
- "name": "c02_level_avg",
- "type": "\"double\""
- },
- {
- "metadata": "{}",
- "name": "humidity_avg",
- "type": "\"double\""
- },
- {
- "metadata": "{}",
- "name": "timestamp_avg",
- "type": "\"double\""
- }
- ],
- "type": "table"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"from pyspark.sql import functions as F\n",
"from pyspark.sql.functions import concat_ws, col, round\n",
@@ -985,56 +289,24 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "8028c3a6-308a-43ec-8988-0b51d9f1826d",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- " \n",
- " \n",
- " \n",
- " "
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "\n \n \n \n ",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "textData": null,
- "type": "htmlSandbox"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"(\n",
" graphistry \n",
- " .edges(devices.sample(fraction=0.1), 'device_name', 'cca3') \\\n",
+ " .edges(devices.sample(fraction=0.1).toPandas(), 'device_name', 'cca3') \\\n",
" .settings(url_params={'strongGravity': 'true'}) \\\n",
" .plot()\n",
")"
@@ -1042,73 +314,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "852b48fe-61af-4953-858f-52680bf07fd2",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "# links 79200\n",
- "# events 19800\n",
- "# attrib entities 41197\n"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "# links 79200\n# events 19800\n# attrib entities 41197\n",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "type": "ansi"
- }
- },
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "\n",
- " \n",
- " \n",
- " \n",
- " "
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "\n \n \n \n ",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "textData": null,
- "type": "htmlSandbox"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"hg = graphistry.hypergraph(\n",
" devices_with_rounded_locations.sample(fraction=0.1).toPandas(),\n",
@@ -1150,55 +369,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "4e327ad1-169b-4bb6-95c0-8fc0cf452625",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- " \n",
- " \n",
- " \n",
- " "
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "\n \n \n \n ",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "textData": null,
- "type": "htmlSandbox"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"(\n",
" g\n",
@@ -1227,37 +411,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
- "cellMetadata": {},
+ "cellMetadata": {
+ "byteLimit": 2048000,
+ "rowLimit": 10000
+ },
"inputWidgets": {},
"nuid": "a0e6bd79-1172-4cfe-ac6d-83b187d48747",
"showTitle": false,
"title": ""
}
},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Out[18]: 'https://hub.graphistry.com/graph/graph.html?dataset=187d97493ce54498b820f727877eda4b&type=arrow&viztoken=b3106e8a-cbe9-4802-8519-97e1d0d539c3&usertag=50d9aebe-pygraphistry-0.28.5&splashAfter=1669270570&info=true&strongGravity=true'"
- ]
- },
- "metadata": {
- "application/vnd.databricks.v1+output": {
- "addedWidgets": {},
- "arguments": {},
- "data": "Out[18]: 'https://hub.graphistry.com/graph/graph.html?dataset=187d97493ce54498b820f727877eda4b&type=arrow&viztoken=b3106e8a-cbe9-4802-8519-97e1d0d539c3&usertag=50d9aebe-pygraphistry-0.28.5&splashAfter=1669270570&info=true&strongGravity=true'",
- "datasetInfos": [],
- "metadata": {},
- "removedWidgets": [],
- "type": "ansi"
- }
- },
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"url = g.plot(render=False)\n",
"url"
@@ -1265,12 +432,12 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
"cellMetadata": {},
"inputWidgets": {},
- "nuid": "ed683717-2c64-43b0-9a7e-bbe2115ba880",
+ "nuid": "cd326621-1224-4b91-890d-9285f7755ad2",
"showTitle": false,
"title": ""
}
@@ -1282,12 +449,12 @@
"metadata": {
"application/vnd.databricks.v1+notebook": {
"dashboards": [],
+ "environmentMetadata": null,
"language": "python",
"notebookMetadata": {
"pythonIndentUnit": 4
},
- "notebookName": "graphistry-notebook-dashboard",
- "notebookOrigID": 382244341032212,
+ "notebookName": "graphistry-notebook-dashboard (1)",
"widgets": {}
},
"kernelspec": {
@@ -1309,5 +476,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 0
}
diff --git a/demos/demos_databases_apis/gpu_rapids/part_i_cpu_pandas.ipynb b/demos/demos_databases_apis/gpu_rapids/part_i_cpu_pandas.ipynb
index 956f05499..931eaad73 100644
--- a/demos/demos_databases_apis/gpu_rapids/part_i_cpu_pandas.ipynb
+++ b/demos/demos_databases_apis/gpu_rapids/part_i_cpu_pandas.ipynb
@@ -14,8 +14,10 @@
"This tutorial series visually analyzes Zeek/Bro network connection logs using different compute engines:\n",
"\n",
"* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
- "* Part II: [GPU Dataframse with RAPIDS Python cudf bindings](./part_ii_gpu_cudf)\n",
- "\n",
+ "* Part II: [GPU Dataframes with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb)\n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n",
"\n",
"**Part I Contents:**\n",
"\n",
@@ -81,9 +83,9 @@
"source": [
"%%time\n",
"# download data \n",
- "!if [ ! -f conn.log ]; then \\\n",
- " curl https://www.secrepo.com/maccdc2012/conn.log.gz | gzip -d > conn.log; \\\n",
- "fi"
+ "#!if [ ! -f conn.log ]; then \\\n",
+ "# curl https://www.secrepo.com/maccdc2012/conn.log.gz | gzip -d > conn.log; \\\n",
+ "#fi"
]
},
{
@@ -92,7 +94,7 @@
"metadata": {},
"outputs": [],
"source": [
- "!head -n 3 conn.log"
+ "#!head -n 3 conn.log"
]
},
{
@@ -291,7 +293,10 @@
"## Next Steps\n",
"\n",
"* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
- "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)"
+ "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb) \n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n"
]
},
{
diff --git a/demos/demos_databases_apis/gpu_rapids/part_ii_gpu_cudf.ipynb b/demos/demos_databases_apis/gpu_rapids/part_ii_gpu_cudf.ipynb
index 294356e76..b81141f1b 100644
--- a/demos/demos_databases_apis/gpu_rapids/part_ii_gpu_cudf.ipynb
+++ b/demos/demos_databases_apis/gpu_rapids/part_ii_gpu_cudf.ipynb
@@ -11,8 +11,10 @@
"This tutorial series visually analyzes Zeek/Bro network connection logs using different compute engines:\n",
"\n",
"* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
- "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf)\n",
- "\n",
+ "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb)\n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n",
"\n",
"**Part II Contents:**\n",
"\n",
@@ -114,9 +116,9 @@
"source": [
"%%time\n",
"# download data \n",
- "!if [ ! -f conn.log ]; then \\\n",
- " curl https://www.secrepo.com/maccdc2012/conn.log.gz | gzip -d > conn.log; \\\n",
- "fi"
+ "#!if [ ! -f conn.log ]; then \\\n",
+ "# curl https://www.secrepo.com/maccdc2012/conn.log.gz | gzip -d > conn.log; \\\n",
+ "#fi"
]
},
{
@@ -736,7 +738,10 @@
"## Next Steps\n",
"\n",
"* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
- "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf)"
+ "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb)\n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n"
]
},
{
diff --git a/demos/demos_databases_apis/gpu_rapids/part_iii_gpu_blazingsql.ipynb b/demos/demos_databases_apis/gpu_rapids/part_iii_gpu_blazingsql.ipynb
index a7b8e0794..5910cd629 100644
--- a/demos/demos_databases_apis/gpu_rapids/part_iii_gpu_blazingsql.ipynb
+++ b/demos/demos_databases_apis/gpu_rapids/part_iii_gpu_blazingsql.ipynb
@@ -6,7 +6,9 @@
"source": [
"# BlazingSQL + Graphistry: Netflow analysis\n",
"\n",
- "This tutorial shows running BlazingSQL (GPU-accelerated SQL) on raw parquet files and visually analyzing the result with Graphistry"
+ "This tutorial shows running BlazingSQL (GPU-accelerated SQL) on raw parquet files and visually analyzing the result with Graphistry\n",
+ "\n",
+ "**WARNING: Deprecated as BlazingSQL is no longer maintained, see dask-sql instead**"
]
},
{
diff --git a/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb b/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb
index 849887d4b..41a9a5c23 100644
--- a/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb
+++ b/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb
@@ -4,14 +4,25 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "UMAP is a popular method of dimensionality reduction, a helpful technique for meaningful analysis of large, complex datasets\n",
+ "# GPU UMAP\n",
+ "\n",
+ "UMAP is a popular method of dimensionality reduction, a helpful technique for meaningful analysis of large, complex datasets. Graphistry provides convenient bindings for working with `cuml.UMAP`.\n",
+ "\n",
"UMAP is:\n",
" * interested in the number of nearest numbers\n",
" * non-linear, unlike longstanding methods such as PCA\n",
" * non-scaling, which keep calculation fast\n",
" * stochastic and thus non-deterministic -- and different libraries handle this differently as you will see in this notebook\n",
" * `umap-learn` states that [\"variance between runs will exist, however small\"](https://umap-learn.readthedocs.io/en/latest/reproducibility.html)\n",
- " * `cuml` currently uses [\"exact kNN\"](https://docs.rapids.ai/api/cuml/stable/api.html?highlight=umap#cuml.UMAP). This may chance in [future releases](https://github.com/rapidsai/cuml/issues/1653#issuecomment-584357155)\n"
+ " * `cuml` currently uses [\"exact kNN\"](https://docs.rapids.ai/api/cuml/stable/api.html?highlight=umap#cuml.UMAP). This may chance in [future releases](https://github.com/rapidsai/cuml/issues/1653#issuecomment-584357155)\n",
+ "\n",
+ "Further reading:\n",
+ "\n",
+ "* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
+ "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb)\n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n"
]
},
{
@@ -24,11 +35,7 @@
{
"cell_type": "code",
"execution_count": 9,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stdout",
@@ -64,11 +71,7 @@
{
"cell_type": "code",
"execution_count": 2,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"data": {
@@ -237,11 +240,7 @@
{
"cell_type": "code",
"execution_count": 3,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -278,11 +277,7 @@
{
"cell_type": "code",
"execution_count": 4,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -312,11 +307,7 @@
{
"cell_type": "code",
"execution_count": 5,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -353,11 +344,7 @@
{
"cell_type": "code",
"execution_count": 6,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -394,11 +381,7 @@
{
"cell_type": "code",
"execution_count": 7,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -427,11 +410,7 @@
{
"cell_type": "code",
"execution_count": 8,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -467,11 +446,7 @@
{
"cell_type": "code",
"execution_count": 12,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stderr",
@@ -502,11 +477,7 @@
{
"cell_type": "code",
"execution_count": 13,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [
{
"name": "stdout",
@@ -608,15 +579,29 @@
{
"cell_type": "code",
"execution_count": 16,
- "metadata": {
- "vscode": {
- "languageId": "python"
- }
- },
+ "metadata": {},
"outputs": [],
"source": [
"#g3.plot()"
]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Next steps\n",
+ "\n",
+ "* Part I: [CPU Baseline in Python Pandas](./part_i_cpu_pandas.ipynb)\n",
+ "* Part II: [GPU Dataframe with RAPIDS Python cudf bindings](./part_ii_gpu_cudf.ipynb)\n",
+ "* Part III: GPU SQL - deprecated as Dask-SQL replaced BlazingSQL in the RAPIDS ecosystem\n",
+ "* Part IV: [GPU ML with RAPIDS cuML UMAP and PyGraphistry](./part_iv_gpu_cuml.ipynb)\n",
+ "* [Graphistry cuGraph bindings](./cugraph.ipynb)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": []
}
],
"metadata": {
diff --git a/demos/demos_databases_apis/graphviz/graphviz.ipynb b/demos/demos_databases_apis/graphviz/graphviz.ipynb
new file mode 100644
index 000000000..14fe015a9
--- /dev/null
+++ b/demos/demos_databases_apis/graphviz/graphviz.ipynb
@@ -0,0 +1,1430 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "fEjoJ5eBnuKZ"
+ },
+ "source": [
+ "# Graphistry <> graphviz integration quickstart\n",
+ "\n",
+ "The [graphviz engine](https://graphviz.org/) is popular for layout of small graphs and rendering to static images. The Graphistry Python bindings to graphviz enable using pygraphistry as usual for quickly loading and manipulating your data, and then benefiting from graphviz for layout, and optionally, rendering.\n",
+ "\n",
+ "The example below shows laying out and rendering company ownership data that is in a tree and benefits from graphviz's high-quality layout engine."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "0BF6FBhDpLas"
+ },
+ "source": [
+ "## Setup\n",
+ "\n",
+ "* graphviz: Install the graphviz engine and the pygraphviz bindings, see below (official [tutorial](https://pygraphviz.github.io/documentation/stable/install.html) )\n",
+ "* Graphistry: Install PyGraphistry below, and [get a free GPU account on Graphistry Hub](https://www.graphistry.com/get-started) or run your own server\n",
+ "\n",
+ "Notes:\n",
+ "\n",
+ "* You must install the graphviz engine, as well as its pygraphviz Python bindings and pygraphistry\n",
+ "* graphviz is most known for its `\"dot\"` layout engine, and it includes others as well\n",
+ "* graphviz is generally not recommended for layout of graphs over 10,000 nodes and edges"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "3XMNgAvIM9Ep",
+ "outputId": "b391eb13-0650-433b-bd2b-c905cdef9e18"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
+ " Building wheel for graphistry (setup.py) ... \u001b[?25l\u001b[?25hdone\n"
+ ]
+ }
+ ],
+ "source": [
+ "#!apt-get install graphviz graphviz-dev\n",
+ "\n",
+ "#!pip install -q graphistry[pygraphviz]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "s40Iw_3vqQZy"
+ },
+ "source": [
+ "## Imports"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 102,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 35
+ },
+ "id": "Cnhc-A4_M2Ad",
+ "outputId": "0f2fb73f-72a2-4fae-9b28-cea26b85d0ad"
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.google.colaboratory.intrinsic+json": {
+ "type": "string"
+ },
+ "text/plain": [
+ "'0.34.5+12.g4dba3e6'"
+ ]
+ },
+ "execution_count": 102,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from typing import Any, Dict, Literal, Optional\n",
+ "import logging\n",
+ "try:\n",
+ " import pygraphviz as pgv\n",
+ "except (ImportError, ModuleNotFoundError):\n",
+ " logging.error(\"ImportError: Did you install pygraphviz and the supporting native packages?\")\n",
+ " raise\n",
+ "\n",
+ "import pandas as pd\n",
+ "import graphistry\n",
+ "from graphistry import Plottable\n",
+ "graphistry.register(api=3, username=FILL_ME_IN, password=FILL_ME_IN)\n",
+ "\n",
+ "graphistry.__version__"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "wwl3XdQLqf5k"
+ },
+ "source": [
+ "### Sample graph: HSBC Beneficial ownership graph\n",
+ "\n",
+ "Sample data from [openownership.org](https://openownership.org/). Corporate ownership graphs often have deeply tree structure, and for bigger conglomerates with numerous subsidaries, officers, board officers, suppliers, and lenders, can greatly benefit from higher-quality tree layouts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "8-7OAzDml0RV"
+ },
+ "outputs": [],
+ "source": [
+ "companies_df = pd.DataFrame([{'label': 'Hsbc Finance (Netherlands)', 'n': '1862294673469042014'},\n",
+ " {'label': 'Hsbc Holdings Plc', 'n': '7622088245850069747'},\n",
+ " {'label': 'Unknown person(s)', 'n': '7622088245850069747-unknown'},\n",
+ " {'label': 'HSBC PROPERTY (UK) LIMITED', 'n': '16634236373777089526'},\n",
+ " {'label': 'HSBC ALTERNATIVE INVESTMENTS LIMITED',\n",
+ " 'n': '18011320449780894329'},\n",
+ " {'label': 'HSBC INVESTMENT COMPANY LIMITED', 'n': '9134577322728469115'},\n",
+ " {'label': 'HSBC IM PENSION TRUST LIMITED', 'n': '1446072728533515665'},\n",
+ " {'label': 'MERCANTILE COMPANY LIMITED', 'n': '6904185395252167658'},\n",
+ " {'label': 'Mp Payments Group Limited', 'n': '13630126251685975826'},\n",
+ " {'label': 'MP PAYMENTS OPERATIONS LIMITED', 'n': '11514603667851101425'},\n",
+ " {'label': 'MP PAYMENTS UK LIMITED', 'n': '13417892994160273884'},\n",
+ " {'label': 'Hsbc Asia Pacific Holdings (Uk) Limited',\n",
+ " 'n': '2173486047275631423'},\n",
+ " {'label': 'HSBC SECURITIES (JAPAN) LIMITED', 'n': '18045747820524565803'}])\n",
+ "\n",
+ "ownership_df = pd.DataFrame([{'s': '7622088245850069747', 'd': '1862294673469042014'},\n",
+ " {'s': '7622088245850069747-unknown', 'd': '7622088245850069747'},\n",
+ " {'s': '1862294673469042014', 'd': '16634236373777089526'},\n",
+ " {'s': '1862294673469042014', 'd': '18011320449780894329'},\n",
+ " {'s': '1862294673469042014', 'd': '9134577322728469115'},\n",
+ " {'s': '9134577322728469115', 'd': '1446072728533515665'},\n",
+ " {'s': '9134577322728469115', 'd': '6904185395252167658'},\n",
+ " {'s': '9134577322728469115', 'd': '13630126251685975826'},\n",
+ " {'s': '13630126251685975826', 'd': '11514603667851101425'},\n",
+ " {'s': '13630126251685975826', 'd': '13417892994160273884'},\n",
+ " {'s': '9134577322728469115', 'd': '2173486047275631423'},\n",
+ " {'s': '2173486047275631423', 'd': '18045747820524565803'},\n",
+ " {'s': '9134577322728469115', 'd': '16634236373777089526'}])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {
+ "id": "7TmvBE5iI8Tu"
+ },
+ "outputs": [],
+ "source": [
+ "g = graphistry.edges(ownership_df, 's', 'd').nodes(companies_df, 'n').bind(point_title='label')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {
+ "id": "y7eC5hOCwfE1"
+ },
+ "outputs": [],
+ "source": [
+ "g = g.nodes(g._nodes.assign(sz=1)).encode_point_size('sz')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "gnhwn1v_r3kD"
+ },
+ "source": [
+ "## Minimal tree layout and graphviz layout engines\n",
+ "\n",
+ "Graphviz provides 15+ layout engines you can use. General guidance is to use for graphs up to 10,000 nodes and engines.\n",
+ "\n",
+ "The `\"dot\"` layout engine is best known due to its beautiful hierarchical layouts for directed acycle graphs like trees."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 543
+ },
+ "id": "KiOxkJR_YKrh",
+ "outputId": "2b9af5e5-b199-452a-839b-d8cc7cc0ea50"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 35,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g2 = g.layout_graphviz('dot')\n",
+ "g2.plot()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "dTEjXkhisCui"
+ },
+ "source": [
+ "Additional layout engines beyond `\"dot\"` are below. See also the [graphviz layout engines documents](https://graphviz.org/docs/layouts/). The same documentation, and the below section on global graph attributes, describe options you can pass in to different layout engines."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "_x28pxBUr7e_",
+ "outputId": "dd209734-f4cf-425b-dc3c-7cec0d9fac74"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['acyclic',\n",
+ " 'ccomps',\n",
+ " 'circo',\n",
+ " 'dot',\n",
+ " 'fdp',\n",
+ " 'gc',\n",
+ " 'gvcolor',\n",
+ " 'gvpr',\n",
+ " 'neato',\n",
+ " 'nop',\n",
+ " 'osage',\n",
+ " 'patchwork',\n",
+ " 'sccmap',\n",
+ " 'sfdp',\n",
+ " 'tred',\n",
+ " 'twopi',\n",
+ " 'unflatten']"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from graphistry.plugins_types.graphviz_types import PROGS\n",
+ "PROGS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 543
+ },
+ "id": "VD5ezMLss9Dw",
+ "outputId": "553250c2-26a8-4820-f01d-fe138280bcf0"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 36,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g2b = g.layout_graphviz('neato')\n",
+ "g2b.plot()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from graphistry.plugins_types.graphviz_types import PROGS"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "oF9m9a_WuPjN"
+ },
+ "source": [
+ "### Global attributes\n",
+ "\n",
+ "You can set global attributes. Parameter [`graph_attr`](https://graphviz.org/docs/graph/) generally refers to layout engine options, while [`edge_attr`](https://graphviz.org/docs/edges/) and [`node_attr`](https://graphviz.org/docs/nodes/) are generally for default colors, sizes, shapes, etc."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 543
+ },
+ "id": "ACoYzOgCE7Pt",
+ "outputId": "597b8129-dc9f-4f1a-e086-912e7206b103"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g2b = g.layout_graphviz(\n",
+ " 'dot',\n",
+ " graph_attr={'ratio': 10},\n",
+ " edge_attr={},\n",
+ " node_attr={}\n",
+ ")\n",
+ "g2b.plot()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['_background',\n",
+ " 'bb',\n",
+ " 'beautify',\n",
+ " 'bgcolor',\n",
+ " 'center',\n",
+ " 'charset',\n",
+ " 'class',\n",
+ " 'clusterrank',\n",
+ " 'colorscheme',\n",
+ " 'comment',\n",
+ " 'compound',\n",
+ " 'concentrate',\n",
+ " 'Damping',\n",
+ " 'defaultdist',\n",
+ " 'dim',\n",
+ " 'dimen',\n",
+ " 'diredgeconstraints',\n",
+ " 'dpi',\n",
+ " 'epsilon',\n",
+ " 'esep',\n",
+ " 'fontcolor',\n",
+ " 'fontname',\n",
+ " 'fontnames',\n",
+ " 'fontpath',\n",
+ " 'fontsize',\n",
+ " 'forcelabels',\n",
+ " 'gradientangle',\n",
+ " 'href',\n",
+ " 'id',\n",
+ " 'imagepath',\n",
+ " 'inputscale',\n",
+ " 'K',\n",
+ " 'label',\n",
+ " 'label_scheme',\n",
+ " 'labeljust',\n",
+ " 'labelloc',\n",
+ " 'landscape',\n",
+ " 'layerlistsep',\n",
+ " 'layers',\n",
+ " 'layerselect',\n",
+ " 'layersep',\n",
+ " 'layout',\n",
+ " 'levels',\n",
+ " 'levelsgap',\n",
+ " 'lheight',\n",
+ " 'linelength',\n",
+ " 'lp',\n",
+ " 'lwidth',\n",
+ " 'margin',\n",
+ " 'maxiter',\n",
+ " 'mclimit',\n",
+ " 'mindist',\n",
+ " 'mode',\n",
+ " 'model',\n",
+ " 'newrank',\n",
+ " 'nodesep',\n",
+ " 'nojustify',\n",
+ " 'normalize',\n",
+ " 'notranslate',\n",
+ " 'nslimit',\n",
+ " 'nslimit1',\n",
+ " 'oneblock',\n",
+ " 'ordering',\n",
+ " 'orientation',\n",
+ " 'outputorder',\n",
+ " 'overlap',\n",
+ " 'overlap_scaling',\n",
+ " 'overlap_shrink',\n",
+ " 'pack',\n",
+ " 'packmode',\n",
+ " 'pad',\n",
+ " 'page',\n",
+ " 'pagedir',\n",
+ " 'quadtree',\n",
+ " 'quantum',\n",
+ " 'rankdir',\n",
+ " 'ranksep',\n",
+ " 'ratio',\n",
+ " 'remincross',\n",
+ " 'repulsiveforce',\n",
+ " 'resolution',\n",
+ " 'root',\n",
+ " 'rotate',\n",
+ " 'rotation',\n",
+ " 'scale',\n",
+ " 'searchsize',\n",
+ " 'sep',\n",
+ " 'showboxes',\n",
+ " 'size',\n",
+ " 'smoothing',\n",
+ " 'sortv',\n",
+ " 'splines',\n",
+ " 'start',\n",
+ " 'style',\n",
+ " 'stylesheet',\n",
+ " 'target',\n",
+ " 'TBbalance',\n",
+ " 'tooltip',\n",
+ " 'truecolor',\n",
+ " 'URL',\n",
+ " 'viewport',\n",
+ " 'voro_margin',\n",
+ " 'xdotversion']"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\n",
+ "from graphistry.plugins_types.graphviz_types import GRAPH_ATTRS\n",
+ "GRAPH_ATTRS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['arrowhead',\n",
+ " 'arrowsize',\n",
+ " 'arrowtail',\n",
+ " 'class',\n",
+ " 'color',\n",
+ " 'colorscheme',\n",
+ " 'comment',\n",
+ " 'constraint',\n",
+ " 'decorate',\n",
+ " 'dir',\n",
+ " 'edgehref',\n",
+ " 'edgetarget',\n",
+ " 'edgetooltip',\n",
+ " 'edgeURL',\n",
+ " 'fillcolor',\n",
+ " 'fontcolor',\n",
+ " 'fontname',\n",
+ " 'fontsize',\n",
+ " 'head_lp',\n",
+ " 'headclip',\n",
+ " 'headhref',\n",
+ " 'headlabel',\n",
+ " 'headport',\n",
+ " 'headtarget',\n",
+ " 'headtooltip',\n",
+ " 'headURL',\n",
+ " 'href',\n",
+ " 'id',\n",
+ " 'label',\n",
+ " 'labelangle',\n",
+ " 'labeldistance',\n",
+ " 'labelfloat',\n",
+ " 'labelfontcolor',\n",
+ " 'labelfontname',\n",
+ " 'labelfontsize',\n",
+ " 'labelhref',\n",
+ " 'labeltarget',\n",
+ " 'labeltooltip',\n",
+ " 'labelURL',\n",
+ " 'layer',\n",
+ " 'len',\n",
+ " 'lhead',\n",
+ " 'lp',\n",
+ " 'ltail',\n",
+ " 'minlen',\n",
+ " 'nojustify',\n",
+ " 'penwidth',\n",
+ " 'pos',\n",
+ " 'samehead',\n",
+ " 'sametail',\n",
+ " 'showboxes',\n",
+ " 'style',\n",
+ " 'tail_lp',\n",
+ " 'tailclip',\n",
+ " 'tailhref',\n",
+ " 'taillabel',\n",
+ " 'tailport',\n",
+ " 'tailtarget',\n",
+ " 'tailtooltip',\n",
+ " 'tailURL',\n",
+ " 'target',\n",
+ " 'tooltip',\n",
+ " 'URL',\n",
+ " 'weight',\n",
+ " 'xlabel',\n",
+ " 'xlp']"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\n",
+ "from graphistry.plugins_types.graphviz_types import EDGE_ATTRS\n",
+ "EDGE_ATTRS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['area',\n",
+ " 'class',\n",
+ " 'color',\n",
+ " 'colorscheme',\n",
+ " 'comment',\n",
+ " 'distortion',\n",
+ " 'fillcolor',\n",
+ " 'fixedsize',\n",
+ " 'fontcolor',\n",
+ " 'fontname',\n",
+ " 'fontsize',\n",
+ " 'gradientangle',\n",
+ " 'group',\n",
+ " 'height',\n",
+ " 'href',\n",
+ " 'id',\n",
+ " 'image',\n",
+ " 'imagepos',\n",
+ " 'imagescale',\n",
+ " 'label',\n",
+ " 'labelloc',\n",
+ " 'layer',\n",
+ " 'margin',\n",
+ " 'nojustify',\n",
+ " 'ordering',\n",
+ " 'orientation',\n",
+ " 'penwidth',\n",
+ " 'peripheries',\n",
+ " 'pin',\n",
+ " 'pos',\n",
+ " 'rects',\n",
+ " 'regular',\n",
+ " 'root',\n",
+ " 'samplepoints',\n",
+ " 'shape',\n",
+ " 'shapefile',\n",
+ " 'showboxes',\n",
+ " 'sides',\n",
+ " 'skew',\n",
+ " 'sortv',\n",
+ " 'style',\n",
+ " 'target',\n",
+ " 'tooltip',\n",
+ " 'URL',\n",
+ " 'vertices',\n",
+ " 'width',\n",
+ " 'xlabel',\n",
+ " 'xlp',\n",
+ " 'z']"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\n",
+ "from graphistry.plugins_types.graphviz_types import NODE_ATTRS\n",
+ "NODE_ATTRS"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "Fslt0bjvuyv0"
+ },
+ "source": [
+ "## Static image rendering and entity-level attributes\n",
+ "\n",
+ "graphviz suports rendering to a static file in various image formats such as png.\n",
+ "\n",
+ "You can add graphviz-specific columns to your node and edge dataframes that configure per-row render settings. These use the same names as in the above global attribute guidance, such as `color`, `shape`, and `label`.\n",
+ "\n",
+ "Adding a column for an attribute will typically disable the global attribute. For example, creating setting node column `\"shape\"` with values `\"star\"` and `None`, and global node attribute `\"shape\"` with value value `\"box\"`. All the nodes with `shape == \"star\"` will render as a star in the static image, and the rows with value `None` will not default to the global node attribute `\"box\"`, but to graphviz's general default of an oval."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 76,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 492
+ },
+ "id": "-3e_fH-80bhE",
+ "outputId": "015cb98e-f1eb-4fbe-e29b-18a821b3b3d3"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " 1862294673469042014 \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " 7622088245850069747 \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " 7622088245850069747-unknown \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " 16634236373777089526 \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " 18011320449780894329 \n",
+ " \n",
+ " \n",
+ " 5 \n",
+ " 9134577322728469115 \n",
+ " \n",
+ " \n",
+ " 6 \n",
+ " 1446072728533515665 \n",
+ " \n",
+ " \n",
+ " 7 \n",
+ " 6904185395252167658 \n",
+ " \n",
+ " \n",
+ " 8 \n",
+ " 13630126251685975826 \n",
+ " \n",
+ " \n",
+ " 9 \n",
+ " 11514603667851101425 \n",
+ " \n",
+ " \n",
+ " 10 \n",
+ " 13417892994160273884 \n",
+ " \n",
+ " \n",
+ " 11 \n",
+ " 2173486047275631423 \n",
+ " \n",
+ " \n",
+ " 12 \n",
+ " 18045747820524565803 \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
dtype: object "
+ ],
+ "text/plain": [
+ "0 1862294673469042014\n",
+ "1 7622088245850069747\n",
+ "2 7622088245850069747-unknown\n",
+ "3 16634236373777089526\n",
+ "4 18011320449780894329\n",
+ "5 9134577322728469115\n",
+ "6 1446072728533515665\n",
+ "7 6904185395252167658\n",
+ "8 13630126251685975826\n",
+ "9 11514603667851101425\n",
+ "10 13417892994160273884\n",
+ "11 2173486047275631423\n",
+ "12 18045747820524565803\n",
+ "dtype: object"
+ ]
+ },
+ "execution_count": 76,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g._nodes.apply(lambda row: row['n'], axis=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 68,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 717
+ },
+ "id": "newXWAjEzo5F",
+ "outputId": "ff234aa4-5811-42ff-b289-d67e4e5c11b7"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "row 1862294673469042014\n",
+ "row 7622088245850069747\n",
+ "row 7622088245850069747-unknown\n",
+ "row 16634236373777089526\n",
+ "row 18011320449780894329\n",
+ "row 9134577322728469115\n",
+ "row 1446072728533515665\n",
+ "row 6904185395252167658\n",
+ "row 13630126251685975826\n",
+ "row 11514603667851101425\n",
+ "row 13417892994160273884\n",
+ "row 2173486047275631423\n",
+ "row 18045747820524565803\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 5 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 6 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 7 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 8 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 9 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 10 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 11 \n",
+ " None \n",
+ " \n",
+ " \n",
+ " 12 \n",
+ " None \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
dtype: object "
+ ],
+ "text/plain": [
+ "0 None\n",
+ "1 None\n",
+ "2 None\n",
+ "3 None\n",
+ "4 None\n",
+ "5 None\n",
+ "6 None\n",
+ "7 None\n",
+ "8 None\n",
+ "9 None\n",
+ "10 None\n",
+ "11 None\n",
+ "12 None\n",
+ "dtype: object"
+ ]
+ },
+ "execution_count": 68,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g._nodes.apply(lambda row: print('row', row['n']), 1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 99,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 206
+ },
+ "id": "72YquJpavfLL",
+ "outputId": "40739d8a-dbe2-4437-fcc7-1a45c92a5b73"
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.google.colaboratory.intrinsic+json": {
+ "repr_error": "Out of range float values are not JSON compliant: nan",
+ "type": "dataframe"
+ },
+ "text/html": [
+ "\n",
+ " \n",
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " n \n",
+ " x \n",
+ " y \n",
+ " label \n",
+ " sz \n",
+ " shape \n",
+ " color \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 \n",
+ " 1862294673469042014 \n",
+ " 381.39 \n",
+ " 234.0 \n",
+ " Hsbc Finance (Netherlands) \n",
+ " 1 \n",
+ " None \n",
+ " red \n",
+ " \n",
+ " \n",
+ " 1 \n",
+ " 16634236373777089526 \n",
+ " 140.39 \n",
+ " 90.0 \n",
+ " HSBC PROPERTY (UK) LIMITED \n",
+ " 1 \n",
+ " None \n",
+ " red \n",
+ " \n",
+ " \n",
+ " 2 \n",
+ " 18011320449780894329 \n",
+ " 381.39 \n",
+ " 162.0 \n",
+ " HSBC ALTERNATIVE INVESTMENTS LIMITED \n",
+ " 1 \n",
+ " None \n",
+ " red \n",
+ " \n",
+ " \n",
+ " 3 \n",
+ " 9134577322728469115 \n",
+ " 778.39 \n",
+ " 162.0 \n",
+ " HSBC INVESTMENT COMPANY LIMITED \n",
+ " 1 \n",
+ " None \n",
+ " red \n",
+ " \n",
+ " \n",
+ " 4 \n",
+ " 1446072728533515665 \n",
+ " 454.39 \n",
+ " 90.0 \n",
+ " HSBC IM PENSION TRUST LIMITED \n",
+ " 1 \n",
+ " None \n",
+ " red \n",
+ " \n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
\n"
+ ],
+ "text/plain": [
+ " n x y label \\\n",
+ "0 1862294673469042014 381.39 234.0 Hsbc Finance (Netherlands) \n",
+ "1 16634236373777089526 140.39 90.0 HSBC PROPERTY (UK) LIMITED \n",
+ "2 18011320449780894329 381.39 162.0 HSBC ALTERNATIVE INVESTMENTS LIMITED \n",
+ "3 9134577322728469115 778.39 162.0 HSBC INVESTMENT COMPANY LIMITED \n",
+ "4 1446072728533515665 454.39 90.0 HSBC IM PENSION TRUST LIMITED \n",
+ "\n",
+ " sz shape color \n",
+ "0 1 None red \n",
+ "1 1 None red \n",
+ "2 1 None red \n",
+ "3 1 None red \n",
+ "4 1 None red "
+ ]
+ },
+ "execution_count": 99,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# row-level attrs\n",
+ "\n",
+ "root_id = '7622088245850069747-unknown'\n",
+ "\n",
+ "g2c = g.nodes(g._nodes.assign(\n",
+ " label=g._nodes.apply(lambda row: \"ROOT: Unknown person(s)\" if row['n'] == root_id else row['label'], axis=1),\n",
+ " shape=g._nodes.n.apply(lambda n: \"box\" if n == root_id else None),\n",
+ " color=g._nodes.n.apply(lambda n: \"blue\" if n == root_id else 'red')\n",
+ ")).edges(g._edges.assign(\n",
+ " color=g._edges[g._source].apply(lambda n: 'blue' if n == root_id else None)\n",
+ "))\n",
+ "\n",
+ "\n",
+ "# Save a static graphviz render\n",
+ "g2c_positioned = g2c.layout_graphviz(\n",
+ " \"dot\",\n",
+ " render_to_disk=True,\n",
+ " path=f'./graph.png',\n",
+ " graph_attr={},\n",
+ " edge_attr={},\n",
+ " node_attr={'color': 'green'}, # ignored due to g2c._nodes.color\n",
+ " format='png'\n",
+ ")\n",
+ "\n",
+ "g2c_positioned._nodes.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 98,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 353
+ },
+ "id": "Orvnc-p4vwyd",
+ "outputId": "59f03155-128b-4d55-f216-58b16ce7f914"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAACAkAAAIbCAYAAACD01XsAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOzde5hWdb03/vcNDAyHmWEURpSDSIolgpZo5mGb5inNQ2rqo3jOtPKnj1nbQ2Xm4yF3Ptp277h8LLN2PdsHC8x0W9vMyDTTsEzTQEQREAUPnGGEmbl/f6yGAQUFBG7kfr2u63ute9b9Xev+rHXPKNf1fa/vt1Qul8sBAAAAAAAAADZ34zpVugIAAAAAAAAAYOMQEgAAAAAAAACAKiEkAAAAAAAAAABVokulCwDg/e2OOypdAVSv44+vdAUAAAAAALzflMrlcrnSRQDw/lUqVboCqF7+FQcAAAAAwFoaZyYBAN6zMWM80Qwb0x13JCecUOkqAAAAAAB4P+pU6QIAAAAAAAAAgI1DSAAAAAAAAAAAqoSQAAAAAAAAAABUCSEBAAAAAAAAAKgSQgIAAAAAAAAAUCWEBAAAAAAAAACgSggJAAAAAAAAAECVEBIAAAAAAAAAgCohJAAAAAAAAAAAVUJIAAAAAAAAAACqhJAAAAAAAAAAAFQJIQEAAAAAAAAAqBJCAgAAAAAAAABQJYQEAAAAAAAAAKBKCAkAsNGMHTs2Q4YMSalUWqnV1tZmu+22y5lnnpkXXnhhlcfef//9Oe644zJw4MB069YtvXr1yrBhw3LhhRfmxRdffE/HnHjiiW+raXXtnnvuedfr/Nd//ddss802KZVK6dSpU4YOHZr7779/pT6f+tSnUl9fn06dOuWDH/xgHn744Xc977XXXpuGhoaUSqU88cQT79qfyrj//vtz6aWXvmOfX/ziF7nuuuvS2tq6kaoCAAAAAICCkAAAG82xxx6b559/Ph/4wAfS0NCQcrmc1tbWTJs2LVdeeWXGjBmTPffcM6+//vpKx11yySU56KCDUl9fn7vvvjvz5s3LzJkzc8MNN+T3v/99RowYkQceeOA9HXPfffdl7ty5WbZsWV5++eUkyZFHHpmlS5dm0aJFmT17ds4+++w1us4LLrggM2fOTJLsscceefbZZ3PggQeu1Oeee+7JL37xixxwwAGZOHFi9t5773c976WXXpr/83/+zxrVQGV84xvfyE033ZTLLrvsHfsdeeSRqa2tzSc+8YnMnTt3I1UHAAAAAABCAgBUWKdOndLU1JRTTjkl5513XmbPnr3SU/d33XVXrrvuunzuc5/LD37wg+y6666pra1NfX19DjnkkIwfPz79+vXL8ccfvzxcsLbHlEql7L333mloaEiXLl2Wf3apVEpNTU169OiRvn37Zrfddtvo94f3j29961v5f//v/+WOO+5IXV3du/a/4IILsssuu+Swww5LS0vLRqgQAAAAAACEBADYhGy//fZJkldeeWX5vuuvvz5J8rWvfW2Vx/Tq1Stf+tKX8vrrr+f73//+Oh1z++23p0ePHu9a3znnnJNPfepTa35BVI3nnnsuX//61/PNb34ztbW1a3zcFVdckSeeeCLf+c53NmB1AAAAAADQQUgAgE3G5MmTkyS77LJLkmTx4sX54x//mEGDBmXgwIGrPe5jH/tYkuTXv/71Oh2zNn71q1+lvr4+V1999Vod925Gjx6dnj17pkePHrnrrrvyyU9+MvX19RkwYEBuv/321R43a9asDB48OF26dMmhhx66xucpl8u54YYb8qEPfSjdunVLY2Njjj766EycODFJMnLkyJRKpZRKpYwYMSLTp09f5edfccUV2WKLLVJbW5ttttlmna7hpptuSm1tbZqamnLuuedm6623Tm1tbfbaa688+uijy/u1trbm8ssvz6BBg9K9e/eMGDEiY8aMSZL8y7/8S3r06JG6urrMnj07F110Ufr3759Jkybld7/7XfbYY4/06NEj9fX1GT58eObPn79G92FN7+dNN92UcrmcI488cqVre6fPTpLGxsbst99++c53vpNyubzaewQAAAAAAOuLkAAAFTd37tz86Ec/yne/+90cfvjh+fjHP54kmTZtWlpaWtLU1PSOx/fr1y9J8vzzz6/TMWujtbU1SdLW1rZWx72bL3zhC7nwwguzZMmS1NXVZcyYMZkyZUqGDBmSs88+O8uWLVvlcVtssUVGjhyZcePG5Ve/+tUan+eKK67IpZdemq9+9auZPXt2HnzwwUyfPj377rtvZs2alQkTJmTvvffOwIED89e//nV54OKee+7JjjvuuPzzr7jiinzzm9/M5ZdfnpkzZ67TNZx//vk5/fTTs3jx4lxwwQWZOnVq/vznP6elpSUHHXTQ8oDCJZdckn/5l3/JjTfemJdffjlHHHFETjrppEyYMCH//M//nC996UtZuHBhrrnmmmy33XbZc889s3Dhwhx55JE57rjj8sYbb2Ty5MkZOnRoli5dukb3YU3v53/9139lxx13XGlGikWLFr3jZ7f78Ic/nJdeeil//etf1+VXBwAAAAAA1oqQAAAVMW/evOVPqjc2NuaMM87IZZddljvvvHN5n4ULFyZJ6uvr3/FcvXv3TpIsWLBgnY5ZG4cffnjmz5+fr3/962t13NrYa6+9Ul9fn759++bEE0/MokWLMm3atLf1a2lpyWmnnZbPfvazb3uC/Z3Os2TJktxwww055phjMmrUqDQ0NGT48OG5+eab89prr+WWW25Jkpx++umZPn16xo8fv/ycP/rRj/Lss8/mkUceWb5vzJgxGTVq1Dpdw4q6dOmy/In+nXbaKaNHj86CBQty2223pbm5OaNHj86nP/3pHHvssendu3e+9rWvpaamJrfddttK5/nWt76V8847L2PHjk1tbW3mz5+fYcOGpba2NltttVXGjh2bPn36rPF9eLdrWrRoUV544YV84AMfWKn/1KlTV/vZK9phhx2SJE899dQ73h8AAAAAAFgfhAQAqIiGhoaUy+WUy+V85StfSblcTkNDQ2pqapb3qaurS1LMNPBO3njjjSRFMGBdjtmUde3aNUne9hR+a2trTjrppDQ1NeXQQw9dq/M8/fTTWbhwYUaOHLlSn9133z1du3ZdPsX/CSeckB49euQ//uM/kiRz5szJlClT0q1bt+X7pk6dmq5du2bQoEFrfQ3vZuTIkenRo0cmTpyYSZMmZfHixdl5552Xv9+9e/f069dv+dIAqzJkyJA0NTVl1KhRueKKKzJ16tTl763pfXi3a5o9e3bK5fJKswi822evqP24WbNmrfbzAAAAAABgfRESAKDivv71r6dfv3657LLLlk8tnyTbbrttampq3nXw9JVXXklSPJG9LsdsSO+0LEFra+tKoYi1cd5552Xy5Mm5+eab88wzz6zVse0Bil69er3tvd69ey+fXaGuri7HHHNMxo4dm8WLF+f222/PWWedlSOOOCJjxozJm2++mdtvv/1tswisT926dcurr76aRYsWJUm+9rWvLZ+BolQq5cUXX8zixYtXe3z37t3zwAMPZJ999snVV1+dIUOG5MQTT8ySJUvW+D68m+bm5uW1rulnv7XfiucBAAAAAIANSUgAgIqrq6vLt771rSxYsCBf+MIXlu+vra3Nvvvum5deeikvvPDCao9/6KGHkiSHHHLIOh2zoWyxxRaZOXPmat9/4YUXMnDgwHU69/HHH59f//rX6d27d0499dS0tLSs8bHvtNTC3LlzM2DAgOU/n3HGGVmwYEHuvPPO3H777TnxxBNzxhlnZM6cObnnnnvy85//PMcdd9w6XcO7WbZs2fJ6+vbtmyS58cYbl89A0d5WXPpgVYYNG5a77747M2fOzMUXX5wxY8bk+uuvX6v78E7aB/lbW1vX+LNXtHTp0pXOAwAAAAAAG5KQAACbhFNPPTUf/ehHc8899+SOO+5Yvv+SSy5Jklx55ZWrPG7+/Pm58cYb09TUlDPPPHOdj9kQDjjggLz00kv5wx/+8Lb3yuVyfvjDH+ajH/3oOp17//33T58+fXLLLbfk8ccfz1VXXbXGx+68887p1atXJkyYsNL+Rx99NEuXLs1uu+220udsu+22ueqqq9LU1JQtt9wyhxxySLbeeut84xvfyHbbbbfBlmwYP358yuVy9txzzwwcODC1tbV54okn1uocM2fOXD7TQt++fXPttdfmIx/5SJ555pm1ug/vpKmpKaVSKfPmzVvjz15R+3FbbbXVWl0bAAAAAACsCyEBADYJpVIpN910U0qlUs4///zMmTMnSXLQQQfl2muvzY9+9KOcfvrp+etf/5rm5ubMnz8/9913X/bff//MmTMnP/vZz9LQ0LDOx6ypX/7yl6mvr8/VV1/9rn2vuuqq9O7dO5/5zGdy5513ZtGiRXnzzTfz17/+NSeddFJaWlpyyimnrP3NWsGRRx6Z008/PVdffXUef/zxNTqmtrY2F110UcaNG5ef/OQnmT9/fp566ql8/vOfz9Zbb51zzjlned9SqZTTTjstEydOzGmnnZYk6dy5c0455ZQ8/fTT77n+FbW1tWXOnDlpaWnJk08+mf/5P/9nBg0alNNPPz21tbU544wzcvvtt2f06NGZP39+WltbM2PGjLz88surPefMmTNz7rnnZuLEiVm6dGn+8pe/5MUXX8yee+65VvfhnfTo0SNDhgzJjBkz1vizV9R+3PDhw9fyjgEAAAAAwDooA8B7kJTLY8asWd+HH364PHTo0HKScpLyNttsUz733HNX6nP66aeXk5R79+5dvvbaa5fvf+SRR8onnXRSedCgQeWuXbuWe/bsWd55553LF110UXnGjBmr/Ly1PWb+/Pnlf/qnfypvscUW5STlTp06lbfffvvy1VdfvbzPvffeW66rqytfddVVa3TNL7zwQvnss88ub7fdduWuXbuWu3fvXt5pp53Kl19+eXnhwoXL+333u98t9+jRo5ykvMMOO5SnTJlSvuWWW8r19fXlJOVtt922/K1vfavc2NhYTlIePHhwefbs2eX58+eXBw4cWE5S7tWrV3nPPfd81/M8++yz5ba2tvK3v/3t8g477FCuqakpNzY2lj/96U+XJ02a9LZreP7558tNTU3lpUuXLt/397//vdzU1FRetmzZWl3Ds88+u8r7dM4555RramrK/fv3L3fp0qVcX19fPvroo8tTpkxZ3ufNN98sX3zxxeVBgwaVu3TpUu7bt2/52GOPLT/99NPl6667rty9e/dykvLAgQPLP/7xj8vlcrk8derU8l577VVubGwsd+7cubzNNtuUv/rVr5ZbWlrK5XL5Xe/Dml7T+eefX66pqSkvXrx4eb3v9tntDj/88HL//v3LbW1ta/Q7VS4Xf3P+FQcAAAAAwDoYWyqXy+UKZBMA2EyUSsmYMcnxx1e6Et7Pzj333Pz0pz/N66+/XulS1slzzz2XD33oQ7ntttsyatSoNT7u9ddfz4ABA3LVVVfloosuWuPj7rgjOeGExL/iAAAAAABYS+MsNwAAbBJaW1srXcI623777XPllVfmyiuvzMKFC9f4uCuuuCK77rprzj///A1YHQAAAAAAdBASAABYDy699NJ85jOfyYknnph58+a9a/8bbrghTzzxRO69997U1NRshAoBAAAAAEBIAACosMsuuyy33XZb5s2bl+222y4/+9nPKl3SOrv66qtz/vnn59prr33HfnfddVfefPPNjB8/Po2NjRupOgAAAAAASErlstVsAVh3pVIyZkxy/PGVrgSqxx13JCeckPhXHAAAAAAAa2mcmQQAAAAAAAAAoEoICQAAAAAAAABAlRASAAAAAAAAAIAqISQAAAAAAAAAAFVCSAAAAAAAAAAAqoSQAAAAAAAAAABUCSEBAAAAAAAAAKgSQgIAAAAAAAAAUCWEBAAAAAAAAACgSggJAAAAAAAAAECVEBIAAAAAAAAAgCohJAAAAAAAAAAAVUJIAAAAAAAAAACqRJdKFwDA+98jj1S6Aqgu/uYAAAAAAFhXpXK5XK50EQC8f5VKla4Aqpd/xQEAAAAAsJbGmUkAgPfEIOX71/HHF9s77qhs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9bwbRUNDctddRUjgU58qZhYAAAAAAIANQEgAgA2rrS056aRkhx2Sn/wk6dKl0hWttd/97nfZY4890qNHj9TX12f48OGZP3/+8vefe+65fPCDH0zPnj3TvXv37LvvvnnooYdWOsePf/zjjBw5MrW1tenZs2cGDx6c//W//td6rbNcLueGG27Ihz70oXTr1i2NjY05+uijM3HixHc97tvf/nZ23HHHdOvWLQ0NDfnKV76y/P2HHnoogwYNSqlUyr//+78nSUaPHp2ePXumR48eueuuu/LJT34y9fX1GTBgQG6//fblx7a2tuaaa67JjjvumO7du6dPnz7Zbrvtcs011+T4449P8u73d021tLQkSbp16/aO/TbGd7FOamqS//zPZMiQ5OSTk01kiQcAAAAAADYvQgIAbFg/+1ny5z8nP/xhUltb6WrW2qJFi3LkkUfmuOOOyxtvvJHJkydn6NChWbp06fI+jY2N+dWvfpV58+ZlwoQJWbZsWQ466KBMnjw5SfKd73wnp556ao477rjMnDkzM2bMyGWXXZZJkyat11qvuOKKXHrppfnqV7+a2bNn58EHH8z06dOz7777ZtasWas97utf/3ouvvjinHPOOZk1a1ZeeeWVXHLJJcvf32efffKHP/xhpWO+8IUv5MILL8ySJUtSV1eXMWPGZMqUKRkyZEjOPvvsLFu2LEly3XXX5fLLL8+3v/3tvPHGG7nvvvvS3Nyc3r17p3fv3mt0f9fUgw8+mCTZZZddVttnY30X66y2NrnttmTChGTs2EpXAwAAAADAZkhIAIAN6z/+Izn88GTYsEpXspJ58+alVCq9rX3sYx9bqd/UqVMzf/78DBs2LLW1tdlqq60yduzY9OnTZ3mfurq6DB48OF26dMmwYcPyve99L83NzbnllluybNmyfPOb38z++++fSy65JFtssUUaGxtz1llnZffdd19vdS5ZsiQ33HBDjjnmmIwaNSoNDQ0ZPnx4br755rz22murndp/yZIlufHGG3PggQfmS1/6Unr37p3u3btniy22WON7uddee6W+vj59+/bNiSeemEWLFmXatGlJkp///OfZbbfdcuSRR6Z79+75yEc+kqOOOioPPvhgli5dukb3990sWrQoY8eOzZe//OU0NTXlggsuWGW/9/pdbDQjRiSf/GTy4x9XuhIAAAAAADZDQgIAbFgTJiSf+ESlq3ibhoaGlMvlt7VHHnlkpX5DhgxJU1NTRo0alSuuuCJTp05913MPHz48DQ0NefLJJ/Pkk09m7ty5OeSQQ1bq07lz59UOZq9LnU8//XQWLlyYkSNHrrR/9913T9euXfPoo4+u8vzPPfdcFi9enE+sp++oa9euSbJ8JoHm5uaU3zJtfmtra2pqatK5c+d1ur/t2nOraW4AACAASURBVAMUDQ0NueCCC3LYYYflscceS//+/VfZ/71+FxvVgQcmjz1W6SoAAAAAANgMCQkAsGHNm5c0Nla6inXWvXv3PPDAA9lnn31y9dVXZ8iQITnxxBOzZMmSdzyupqYmy5Yty/z585MkvXv33qB1zp07N0nSq1evt73Xu3fvLFiwYJXHzZgxI0nSt2/fDVLXYYcdlscffzx33XVXlixZkgkTJuTnP/95PvWpT6Vz587rfH+TjgBFS0tLZsyYkR/84AfZdtttV9t/Y30X60VjY/KP7xQAAAAAANYnIQEANqz+/ZPnn690Fe/JsGHDcvfdd2fmzJm5+OKLM2bMmFx//fWr7d/S0pI33ngjgwYNyjbbbJMkee211zZoje0D36sKA8ydOzcDBgxY5XG1tbVJkjfffHOD1HXFFVfkgAMOyOmnn576+vocc8wxOf744/O9731veZ+1vb/ramN9F+vFlCnJar4zAAAAAAB4L4QEANiwDj44GTs2ecuU8+8XM2fOzDPPPJOkeNr+2muvzUc+8pHl+1blt7/9bdra2vKRj3wkgwcPzhZbbJH77rtvg9a58847p1evXpkwYcJK+x999NEsXbo0u+2222qP69SpU373u99tkLqefvrpTJkyJa+++mqWLVuWadOmZfTo0Wn8x+wS63J/19XG+i7es7a24m/mLcsiAAAAAADA+iAkAMCG9YUvJM88k/z4x5WuZJ3MnDkz5557biZOnJilS5fmL3/5S1588cXsueeey/ssXbo08+bNS0tLS/785z/n/PPPz7bbbpvTTz893bp1y2WXXZYHH3ww559/fl566aW0tbVlwYIF63UgvLa2NhdddFHGjRuXn/zkJ5k/f36eeuqpfP7zn8/WW2+dc845Z5XH9e3bN8cdd1x+9rOf5dZbb838+fPz5JNP5pZbblkvdZ133nkZNGhQFi5cuMr31+T+ri8b67t4z/7jP5KJE5PPf77SlQAAAAAAsBkSEgBgw9p55yIo8P/9f8XAZwXdeeed2X777TNlypTMmzcvgwcPzg9/+MPl73/uc5/LYYcdliQZPXp0Ro4cmebm5rS2tmavvfZKjx498qlPfSrnnntuzjvvvCTJqFGj0q9fvwwdOjTdu3fPwQcfnA9/+MN58MEHU19fnyS56KKL8u///u8ZP358tt9++/Ts2TP77bdfxo8fv97qnD59er7xjW/kmmuuyZVXXpk+ffpkv/32y+DBgzN+/Pj07NkzN9xwQ/bZZ58kyZe//OUce+yxSZJbb701Z555Zi6++OL0798/X/ziF7PvvvsmSY444oh87nOfy+67754kufjii3PUUUdl9OjRufHGG5MkI0aMyPPPP5/vfe97ueiii5Ikhx56aCZPnpxrrrkmf/vb39LY2JhSqZRSqZSuXbtmp512yrhx49K3b993vL9v9Yc//CE77rjj8nuzzTbb5Pjjj39bv9Vd69p+Fxvd3/+enH9+8fcybFilqwEAAAAAYDNUKpffp/M/A/D+8eabyQEHJC++mPz2t8kOO1S6IjaS0aNHZ/LkycsDBUkx88Ill1yS0aNHZ86cOenevXsFK9yETJpU/J0MGZL85jdJ166VrggAAAAAgM3PODMJALDhdeuW3HtvMmBA8rGPJQ88UOmK2AheeeWVnH/++TnrrLNW2t+1a9cMGjQoy5Yty7JlyypU3Sbm/vuTvfZKtt02+a//EhAAAAAAAGCDERIAYONoaEjGj08OOyw56KDknHOSBQsqXRUbUPfu3VNTU5Nbb701s2bNyrJlyzJz5sx8//vfz+WXX54TTzxx+ZIMVWvx4uSSS5JDDy3+Lu6/P6n2ewIAAAAAwAZluQEANr6f/jT54heTmprk3/4tOeaYSlfEBvL73/8+V155ZR577LEsWrQovXr1yrBhw3LyySfnc5/7XLp06VLpEivnv/4r+cIXirDMt76VnH12UipVuioAAAAAADZv44QEAKiMV19NLrww+b//Nzn44OSrX03+6Z8qXRVseL/7XXL11cmvf52MGpXceGPSp0+lqwIAAAAAoDqMs9wAAJXRt2/yk58kv/lN0tyc7LdfERL47/+udGWwYfzyl8m++yYf/3iydGnywAPJj38sIAAAAAAAwEYlJABAZR1wQPFk9YMPJj17Fmuz77prsQzBG29Uujp4b15/PbnppmSXXZLD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fvazZMSI5Nhji6nD77svmTgxufTSigcExo4dmyFDhqRUKqVUKmXgwIG59dZbl79/1llnpbGxMaVSKTU1Nfnwhz+cadOmrfZ8u+++ezp37pxdd911g9a5qjZ48OAkyb333puGhobcfffd67WGTcH999+fSy+99G3345RTTnlb34MPPjh1dXXp3Llzhg0blj//+c9r/XlnnHFGamtrUyqV0tzcvD4uYbnrr78+TU1NKZVKufnmm9frud/qs5/9bOrq6lIqlfLEE0+8Y99f/OIXue6669La2rpBa1prQ4YU/82YNCn57/9O6uuTo49Odt01GTu20tUBAAD8/+zdd3gUVdsG8Hs3WdI7CaElIUgoIXSkShPwU4qFqtgAEUWkIyh2UFBQQYq+KEZEBFFARKwIitJEOgQCBJIAgYSEFNJI2fP9cZzsbrJJdtMmm9y/65prNjvtmbOzM5M9z5xDREREROXEJAEioprg2DH5tO+oUTJJ4MQJ4LvvgAEDqs2Tv8OGDcPFixfRtGlTeHh44PLlyxg/fnzB9DVr1uCnn34CADz33HM4evQoAgICil3foUOH0Ldv30qPUwgBIQTy8vKQmZmJ+Ph4ODs7AwBqamM8r732Gj788EO89NJLJuXh4+ODL7/8Ejt27DCZ/9dff8U333yDIUOG4PTp0+jQoYPV2wwPD8esWbMqahdMzJo1C/v27auUdRf26aef4pNPPrFo3qFDh8LR0RF33303UlJSKjmyMtBogIEDge+/B44fB1q1AkaMAPr0kX8TERERERERERERkU1ikgARkS3T64F33wW6dJGvDx4E1q+vVX2Ia6ooCcLOzg5OTk7w8/NDSEgIAGDQoEFITU3FkCFDqiSGqrBo0SJs3LgRmzZtgpubm8m0Dz/8EFqtFhMnTkRqamq5tpOVlYXu3buXax01wdSpU9G2bVvcd999yMvLUzuc4oWFARs2AAcOALdvA3feCSxZIs87RERERERERERERGRTmCRARGSrcnKAMWOAefNk0+B79gCdOqkdVZXT6XRVvs3vvvuuyrdZFS5cuIBXXnkFb7zxBhwdHYtM7969O6ZNm4arV6+W+6n/NWvWICEhwey0qkr8qCzWxv/666/j2LFjWLp0aSVFVIHuvBPYt08mJ82bBwwfDlRw9xBEREREREREREREVLmYJEBEZIuEAEaPBn76Cdi5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mmKSABFVrZiYGLz55ptYt24d/P39MXPmTDz11FNwcXFROzSiWodJAkTlk5ycXKTVAXZZQERERERERERERNUckwSIqGrExcXh7bffxieffIJGjRrh5ZdfxqOPPgqdTqd2aES1FpMEiCqeNV0WKIkDd955J+rVq6d26ERERERERERERFQ7MEmAiCpXUlISFi9ejA8//BA+Pj6YNWsWnnnmGTg4OKgdGlGtxyQBoqpz69YtnDt3DqdPny5IHDh06BDi4+MBmO+yoEOHDnB2dlY5ciIiIiIiIiIiIqphttirHQER1Uy3bt3CqlWrsHDhQuh0Orz22muYMmUK+2cmIqJayc3NDR07dkTHjh1N3i/cZcHhw4fx6aefIjMzk10WEBERERERERERUaVgSwJEVKEyMjKwbNkyLFmyBBqNBrNnz8bzzz8PFxcXtUMjokLYkgBR9cQuC4iIiIiIiIiIiKgSsbsBIqoYQgh8++23mD17NpKSkvDcc89h7ty58PT0VDs0IioGkwSIbAu7LCAiIiIiIiIiIqIKwO4GiKj8/vnnH0ybNg0HDx7EmDFjsHjxYj7NSEREVMHYZQERERERERERERFVBCYJEFGZXb58GfPmzcOXX36Jvn374ujRo2jTpo3aYREREdUqXl5e6NmzJ3r27FnwnrkuC7755hvMnz8fer0ederUwR133GGSONC5c2f4+/uruCdERERERERERERUFZgkQERWy8jIwOLFi/HOO++gcePG+PrrrzFixAi1wyIiIqL/2NvbIzg4GMHBwRgyZEjB+zk5OTh//nxB4sDp06exdOlSXL9+HQC7LCAiIiIiIiIiIqoNmCRARBYTQiA8PBzz5s3D7du3sXDhQjz33HPQ6XRqh0ZEREQWqFOnDkJDQxEaGmryfkldFtjZ2SEwMBCtWrUqSBzo2LEjuywgIiIiIiIiIiKyURohhFA7CCKq/k6dOoVnnnkGBw8exDPPPIPXX38dPj4+aodFROUwcuRIAMCmTZtUjoSIqqP8/HzExMTg9OnTBa0OHD58GGfPnmWXBURERERERERERLZrC1sSIKISZWVl4Z133sGiRYvQqlUr7Nu3D507d1Y7LCIiIqpkdnZ2FdZlQfv27eHi4qLWrhAREREREREREZERJgkQUbH++OMPPPPMM7hy5QreeOMNzJo1C3Z2dmqHRURERCpilwVERERERERERES2jd0NEFER8fHxmD17NtatW4fBgwdj1apVaNy4sdphEVEFY3cDRFTZytJlQadOnVC/fn21QyciIiIiIiIiIqqp2N0AERkIIbB69Wq8+OKLcHd3x/bt2zF48GC1wyIiIiIbxS4LiIiIiIiIiIiIqh8mCRARACA2NhZjx47Fnj17MGnSJCxYsABubm5qh0VEREQ1kDVdFqxZswYZGRkAgPr165skDnTs2BEtWrRgd0hERERERERERERWYHcDRIR169bh+eefR/369bFu3Tp06tRJ7ZCIqAqwuwEishVxcXEmrQ4cPnwYkZGRyM/PZ5cFRERERERERERE1mF3A0S1WUpKCiZPnoyvvvoKEyZMwPvvv89mfImIiKjaadCgARo0aFBqlwXLli3DtWvXABi6LDBudYBdFhAREREREREREbG7AaJa65dffsH48eOh1+vxww8/4L777lM7JCIiIiKLldZlgXGrA+vXr2eXBURERERERERERP9hdwNEtUxWVhbmzp2L5cuXY/jw4fj444/h7e2tdlhEVMnWrl2LDz74APn5+QXvKU/bGjfJbWdnh+nTp+OJJ56o8hiJiCqTtV0WhIaGIjg4WO2wiYiIiIiIiIiIKtoWJgkQ1SIHDx7EmDFjkJqaio8++gjDhw9XOyQiqiLnzp1D8+bNLZo3MjISISEhlRwREZH6zHVZcPjwYXZZQERERERERERENRmTBIhqiw8//BCzZ89Gv379EB4eDn9/f7VDIqIq1rZtW5w8eRLFXfo1Gg3CwsJw/PjxKo6MiKh6MddlwbFjx9hlARERERERERER1QRMEiCq6W7duoUJEybg22+/xcsvv4xXX30VWq1W7bCISAXvvfce5s6di7y8PLPTdTodFi1ahBkzZlRxZEREtoFdFhARERERERERUQ3AJAGimuzMmTMYPnw4bty4gfXr12PAgAFqh0REKoqLi0Pjxo2h1+vNTtdoNLh8+TIaNmxYxZEREdkuc10WRERE4OLFiwDMd1nQrl07uLq6qhw5ERERERERERHVUkwSIKqpvvjiCzz77LPo1KkTNmzYgAYNGqgdEhFVA3fddRf27dtXJFFAq9Wie/fu+Ouvv1SKjIioZjHXZcHx48eRnp4OgF0WEBERERERERGRapgkQFTTZGdnY+rUqfjkk0/w/PPPY8mSJdDpdGqHRUTVxOrVq/Hss88WSRKws7PDRx99hAkTJqgUGRFR7VBSlwU6nQ7NmjUzSRxglwVERERERERERFTBmCRAVJNcvHgRDz74IK5cuYK1a9di8ODBaodERNVMcnIy/Pz8kJeXZ/K+nZ0d4uPj4ePjo1JkRES1l9JlgXHigHGXBZ6enggNDWWXBUREREREREREVBGYJEBUU+zduxcPPvggGjVqhC1btiAoKEjtkIiomho0aBB++eUX5OfnA5AJAv/3f/+HH374QeXIiIjImLVdFoSGhiIsLAx16tRROXIiIiIiIiIiIqrGmCRAVBNs2rQJTz75JPr06YONGzfC3d1d7ZCIqBrbsGEDxowZA+UWQKvVYv369Rg9erTKkRERkSWqW5cFMTExWLduHWbMmAFnZ+dK2w4REREREREREVUIJgkQ2TIhBN599128+OKLeP755/HBBx9Aq9WqHRYRVXOZmZnw8fFBdnY2AMDR0RE3btxgs9VERDYsNzcX586dU6XLgm3btuGBBx5AvXr1sHjxYowZM4b3pERERERERERE1ReTBIhs1e3bt/HUU09h48aNWLZsGSZNmqR2SERkQ0aPHo0tW7ZACIHhw4djw4YNaodERESVICUlBadOnbKqy4LWrVvDwcHB4m0sWLAAb775JvLy8gAAbdq0wYcffohevXpVyj4REREREREREVG5MEmAyBYlJibioYcewqlTp/Dtt9+iX79+aodERDZm+/btuP/++wHIJ0CHDBmickRERFSVCndZEBERgVOnTuH27dvFdlnQpEkTaDSaIusaMWIEtmzZAr1eDwCws7NDfn4++vXrh+XLl6NVq1ZVvXtERERERERERFQ8JgkQ2ZpTp05hyJAhsLe3xw8//IDmzZurHRIRFSc7G8jKMn0vIwPIyTF9Ly0NyM83v470dCA3t2zbz82Vy5ublJ+PuuPHA0Lgxpo1qGNvb34drq6ATle27et0cnlz7OwAd3fT9+rUAVxcTN9zcgIcHcu2fSIiskpZuixo27Yt2rZti0uXLhVZn06ng16vx7hx4/DWW2/B19e3qnepZklONv278D1CTo68zzAnJQWw9l9/vR5ITbVuGYW9PeDmZv1yDg6As7P5aV5epn+7uMh7B4W5+wgiIiIiIiIiModJAkS25NChQ/i///s/hIaGYuvWrfDx8VE7JKKqo1SuK2Plh3FlnJcH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lWj79u345ZdfsHTpUmi1KnzdPD2B1auBd98FXn8dyM+v+hhqqNu3byMiIqLI+5GRkf/P3r2HxVUe+AP/AsNAgOF+GxiuScAQcyUJMUBsbLSr29bVbm11e3Nrq9vWPtZru2vVmt1Wn67VbrVut3305z5r19pqq7XtPopVkyHmRkiiIeTGnQECyXCHuf/+eH3PnDmcIZAQDpfv53nOM2dmzsA7MHPec973e94XALB69eop/Zy6ujoAQGlpKQDgkUceQUxMDO6+++6wVxx9+OGHMJlMus8FAgG89NJL+OY3vznhuZSUFHz2s5+Fz+fDr3/96ymVT5YxPj5+yttPRVdXF2699dZJt/nSl76EiooKvPHGG3j55Zen9HN37NiByy67DPX19SGPX4q/y3R885vfhM/nw2OPPTbl0IPWVP5m0xXu76U10+Wfqe/P+ciy/u53v8N3vvOdi/55U/0fTPXvOplL+Te60O/0TP89Z8J0vhfd3d148sknYbPZ8I8c0j48rxd46CEREviv/wJmK9T04INAby9wyy3AR3OW09TN1eOSC8X6aXpms37SM93PyXe/+10AwKOPPqq77eDgIJ588klkZmZOur++FPXSTB8zXqq/+VSxzmedDwA4fBj47GeBT30K+OQnjS4NERERERHNEQwJEF0iHo8H9913H2666SZs3brVuILccgvw7LPA448DW7eKKwhoRtxwww14+eWX0d/fj4GBAbz22mv47ne/i+uvv163EXlsbAx+vx+BQAAOhwPPP/88HnzwQaSnpyuNb2vXrsWLL76IDz/8ENXV1fjzn/+MgYEBeDweNDc345e//CW++tWvIjo6WrdMu3fvRmJiIiorK3Wfl3P1TmWYVI/Hg56eHrz77rszFhIIBAIYGxvDK6+8ct75QSMiIvAf//EfiIiIwLe//e0pzQMsh3aNiooKeXwm/y4X4pZbbkFqaioqKyun3cEwnb/ZdIX7e2ldivJP9/tzIW666Sakp6fjhhtuQHFx8QX/nOn+D6b6dz2fmf4bXex3eqb+njNhsv9JIBDA8PCwsr/t7e3Fb37zG1RWViIqKgp/+MMfOD9xOO+/D1RXA//+78AvfgHcfPPs/e68POAPfwB27xajCrz//uz97gViLh6XXCjWT1NjVP2kNd3PydVXX40f/ehHeOGFF/CVr3wFhw8fxvj4OAYHB/Hmm29i27ZtcDqd+N3vfjfp6BCXol6a6WPGS/U3Px/W+azzAYjg3zPPAFu2ACUlwP/7f0BEhNGlIiIiIiKiuSJARJfET3/600BsbGygubnZ6KIIH3wQCFRWBgJAIHDddYHAX/8aCPj9RpfKcK+++mpg6dKlAQABAIGCgoLA888/rzz/ta99LZCSkhIAEIiOjg6Ul5cH2traAm+++Wbgc5/7XGDp0qWBmJiYgNlsDpSWlgYeeeSRwPj4uO7PVi8xMTGB5cuXB77xjW8E2traJpSrra0tcO+99wZWrVoVSEhICERFRQWSk5MD69atC3z1q18N1NbWTnjNV7/61UB8fHzAZDIF1qxZEzh48GDI8zt27AhYrValDLm5uYHt27eHLaN6efXVVwOBQCAwODgY2Lp1ayA1NTUAIBAZGRlYtmxZ4N/+7d/C/k3DLQ8++GCgtrY2UFJSojyWk5MTuOOOO0LK/ZWvfCUAIJCcnBzYtGmT8rPT09MD3/rWt3T/r/fdd1/g+uuvv+C/y2OPPXbe97lnz57AypUrA5GRkQEAgezs7JDn9cr061//Wve56fzN9IQri/rnTvXvNRvln8r3R5ruZ077Pu+///7A7t27lfsPPp8Sm7EAACAASURBVPhgIDs7W/lZZWVlgV27dk3rPVzM33Uq72e6f6ML+V+8+uqrF/S5merf83zfj5nel7z++uuB1atXB+Li4gJms1n5vREREcq+49FHHw2cPXs27N9u0fL7A4G33w4Err1WHCNUVwcCR48aV562tkDgqqsCgYiIQODznw8EPvzQuLIYYCEdlwQCrJ8WYv00mel+Tt5///3ALbfcEsjPzw+YzeZAfHx84PLLLw/cc889gY6Ojhn9O2qPOa1Wa+Czn/1sSHku9lj6Qj/LV199dSAzM1OptwoLCwP33Xffef/erPNZ54fl9QYCr74aCJSVBQJmcyDwL/8SCHg8RpeKiIiIiIjmllciAoFAAEQ0o0ZGRrB06VJ88YtfxI9//GOjixPqzTeBRx8FamuBZcuAr35VXClYUGB0yYiIiGi2NDcD//u/wHPPAadPA1VVYpqBq682umTC668D3/secOwYsH07cOedwN/8DTDDV6wTEREtGH19wH//txg9oKUFuPFG4LHHgKVLjS4ZERERERHNPa8yJEB0CTz++OP413/9V5w+fRqZmZlGF0ffhx8Cv/wl8D//A5w7B6xfLxoRPv1pYNUqo0tHREREM+3IEeC114Df/x6orwfS0oAvfAH42teAlSuNLt1EgQDwf/8HPPUU8NZboryf+5wIN15xBRDJmdOIiGiRGx4Wwbpf/1pcEBAbC3zlKyJct3y50aUjIiIiIqK5iyEBopk2PDyM4uJi3H777dixY4fRxTk/txt45x3RYfCHPwA9PYDVKq7au/pq4OMfB3JyjC4lERERTVdnJ/D226KD/e23ga4uUcdff70IBn7sY/PnyvyWFtEB8utfA0ePApmZwHXXAZ/8pDheWcxzThMR0eLS3Ay88YZY3nsP8PuBa64BbrlF1PHx8UaXkIiIiIiI5j6GBIhm2g9+8AM8+eSTaGpqQmpqqtHFmR6/HzhwQHQm1NQAu3eLEMHSpUBlZXApKwMiIowuLREREUl+vxia324XUwrV1gJNTUBMDLBlSzD8V14+/6/AP3oU+OMfRefInj3imGTjRuCqq8RyxRXAkiVGl5KIiGhm9PSIYP9f/yqW06eBpCTgE58QYbnrrhOj7RAREREREU0dQwJEM8npdKKoqAj33HMPvv/97xtdnIs3MhLsaLDbgX37xHCGSUnAmjXA2rViWbMGuPxywGw2usREREQLn9stpg06dEgshw+LZWAASEgAKiqAqqpguC8uzugSXzp9fSLY+Ne/ig6UU6dEMGLNGmDTJhEe2LgRKC2d/+EIIiJa+EZHxZRA+/eLZd8+UbdFR4t6TQbiKivnz2hAREREREQ0FzEkQDSTduzYgaeeegotLS2wWCxGF2fmeb2iE2LfvmDHxAcfAGNjooGirCw0OLB2LZCSYnSpiYiI5i+nM1jnykBAQwPg8YjO/8svF/XtunWiM3zNGsBkMrrUxmlrE0Mvy46VQ4cAl0tMR7BhQ2hwIC/P6NISEdFi5vWK0XFknbV/vwgBer1AenqwzqqoAKqrRRCQiIiIiIhoZjAkQDRTRkdHUVhYiDvuuAOPPvqo0cWZPT4fcPy46LQ4dEhc9XD4MHDmjHi+oAAoKQkupaXA8uXi8agoY8tOREQ0F/h8QGsrcOKEWI4fD663tYltMjODQTy5lJSwLj0fj0ccl6g7YBobxd88KwtYtUqEHFeuDN4y4EhERDMpEABaWsS0QB9+KG6PHhXL6CgQHw+sXx8MBWzaBBQVGV1qIiIiIiJa2BgSIJopzzzzDO699140NzcjOzvb6OIYz+EIjjSg7vTo7RXPx8QAS5eK0EBJiQgOyPXMTGPLTkREdCmcORMaADh5Utw/dUpMIQCIOlDWhyUlohN7zRogJ8fYsi8kQ0NAXZ1YGhqCHTZDQ+J5q3VicKCsDEhNNbbcREQ0t8kwQEODCADI22PHxFR+AGCzAStWiLpl1SoRCigrY+iPiIiIiIhmG0MCRDPB5/PhsssuwzXXXINnnnnG6OLMbf39+ldKnjwZbDhJTgYKC4H8fHFbUCCW/HxxyxABERHNRT094sr/1tbgbUtL8HZgQGwXHx8MyMlRdmQoIDnZyHewuLW2Bq/ybGwUQcfGRmBwUDyflSUCjnIpLg7eMiBKRLQ4eDyivmhqAk6fDr09dSo0DKANnK1YwXqeiIiIiIjmCoYEiGbCSy+9hC984QtobGzEsmXLjC7O/NXeHgwMtLYGl5YWoLsb8PvFdkuWBEME6gCBDBRYrYt7PmYiIpp5Xi/Q1RXa8S+DAHIZGxPbRkaKukjWS7KOkqEAm83AN0LT1tYmwgPHjoV2CLW0AC6X2CY+PjQ0oL7NzxcjKBER0fzQ3y/29XpBgPZ2cUwAiOlptPt8GQpISjL2PRAREREREU2OIQGimbBx40YUFRXh5ZdfNrooC5fHI6Yq6OoKNtjIxeEQDfWjo8HtU1JEB01OTvjbvDwgOtqwt0RERHOE0ynqkq6u8LetrWIee0DUHenpoi4pLg5dZDggPt7Qt0SzxOmceFyiXiT1cYn8nKjXCwqAhATj3gcR0WIxNhY8p5R1vHr99GkREpBkECDcQkREREREND8xJEB0sd5//31s2bIFu3fvxhVXXGF0cRYvv1806rS0AJ2doR076nWnM/iayEgxdUF2NpCbG3prtYoOoPR0sQ3nISYiml/OnhXhsr4+sXR1iVFpOjtDb3t6xBzC0mQhs5wcEQCwWkUdQjQZeSVqW5tYOjrE506uOxyA2x3cXh6H2Gxi9IHcXLFYreK5jAxOuUREFI7bDZw5I+p2uXR0iCv/OzrE0tYGDA8HX2OxiOB4Xp7Y38p1mw0oKhJ1vtls2FsiIiIiIiK6hBgSILpYX/jCF9DQ0ICDBw8aXRSaCnnliPYKUe1j6jABIKYvSE8XDfTp6WJeYrmudz8jA4iIMOY9EhEtNH5/sLO/r090/vf0BNf7+sR9dShADgUspaaKztbzjTITG2vMe6TFJxAQnVjt7SI80N4+cd3hEKMpSSaTCApkZYnP7Edhx7MWCwbi4lC8bp04BsnOZsCRiOY/r1d0/Pf0iPM0dQigt1fsI+XzZ8+GvjYhQQSubDYRANBb55QARERERES0eDEkQHQxent7kZeXh2effRa33nqr0cWhmeR2BzuazpwRi959dYeUWmRkMDCQmiqWlBSxqNe191NTRQcAEdFC5PGIEJZczp2bfP3cueA+1u8P/Vnq4JZeWCszUyzyPq8EpHnKefIkHA0N6Dp5Ek3HjsHR2Ymunh409fTAMTSEjrExDPp8WBYRgZPqUzuzORgoSE8H0tImLvL7Ie9zmgwiupRkvX72bMjyYWMj9p44gXyfD/kuFwpGRxHb2yvOudTi4sQ+LTtb7N+sVnE/M1ME/WRIKjub+zMiIiIiIqLJvcqeKKKL8Itf/AJxcXH43Oc+Z3RRaKaZzcGhpafC5wsNDKivaJWdXX19wIkToR1k6mGGJYtFPzygXrdYxJKYKG6Tk8V6YiI7wojo0nG7gcFBsfT3A0NDYl3enq/zf2ho4s80m/X3dUVF4jYjIzjMujoYEBU1+++faAa53W50dXWho6MDHR0d6OzsRHt7Ozo7O5X17u5ueFQjCWRkZCAnJwd5eXlYWlGB6pwcFBQUICcnB/l5eeI709sbvNJWXmErO+Xa2kI753y+0ELFxoaGCOT3Td5PShLHHOrblBSxzhGUiBYHl0scAwwMhN46neJWJwQQdp+zZAmQloZ9ERG4y+HAsOr5LIsFeUuXIt9mQ35hIQrLypC/fDny8/ORn5+PjIyMWX7jRERERERECwtHEiC6QF6vF8XFxbj55pvx+OOPG10cmq+Gh8/fqaZ3f3AwdPhhtZiYYHggJSU0TCCDBMnJoY/LoEFcnOggSE4WjXYcdpto/hsbA8bHxb5jfFzc13bwDw0FG/rlffmc3OcMDYmOAT1mc3CfM5URU9TPJSTM7t+DaBaMj4/D4XDA4XCgq6tLuW1qalLWW1tb4VN1iKWkpKC4uBhWqxU5OTnKrXwsPz8fFotlZgsqQ4zhOvR6e0PvDwwAIyP6P0seX+gFCdS36iUuTlztm5Qk9iEcTYno0hodFd/hoSHxfR4d1e/wVy/a58bH9X92Sor4Lmdk6I9cojd6SVxcyI8YGxtT9pVyUe8/1fvNmJgY5Obmhuw3i4uLlftFRUWI0/x8IiIiIiIiUnC6AaIL9cYbb+D666/HqVOnUFRUZHRxaDEaHw925PX3h17Nq76qV9vhNzAgFnl/bGzy35OSIsICS5aIBv3YWNGgl5Qk1uPjRcdAbKzo7FOvWyxi3WIBoqPFY/LWZBKPEy12Q0Nizt3hYRH+kbdDQ8Hv+fCw+K7K9fFx8R0eGRHrsqF/fFzsD9TBgMksWRIMDCUlBTvq1MEiOVLJZI8zUESLiLoTK1wIoLu7G/I0y2w2Iy0tLaTDXxsCyM/Ph2m+dJB7PKGdhvLqYfVj2k5F7fbhxMSIY4yUFHF8ER8vjhmSkoL3Pwo89vh8iEpIQLrNJraRz8fFiZ8jQwcpKbP3tyGaKW63qONlfS6PFZxO8fjIiDgekMGdkZHgSD8yCNDfL7aRz/f3A+Gaf8zm8IGe8wV/5O2s/Fnc6OvrCxskOHHiBIZUoxbJ8FW4IEHOVEeNIyIiIiIiWngYEiC6UDfeeCMGBwdRU1NjdFGILo7sjNR2LMorjrVXH4+NBdfHx0VD5MCAeEyuj4+Hv9JQS4YFwt1GRYkOgXC3kZGicRIIhhkA0VEgp16Q2wPBIZHVr2NgYfGRje1AsNE8EBDrgHhONjLLhnog+B0BxGfd7w/eDg6KYXTD3crfqb2divh48fmWHWVyPS5OfOblunokEBnskaOCqAM/cpSR+dIpSTRLnE6n7lX/8vbUqVMYGBhQto+NjdW96l/9WGFhISIjIw18V3OQDAzIzks5isnIiDiW0HSEes6dw5Hubti7u1HndKJuaAjH3G78a0wM/jncCCdq8pjBbBb7ULlflPvB5GSxTVLSxG1kwFE+DgRfpz5+kMcd6uMLjso0v8n6W31MIIN8sv4HxGdWfg5lCGZkJDhFj88nHpc/x+USr5GvUwcAPjpuGADwcQBVAKo/WjLVZZOf6cTEYEBGBv3k/eTk0ABNcnJw3WIJHlPIY4gFwul0ThiBQH2/ublZCXHJfXi4IMG8CnARERERERFND0MCRBfi7NmzyMnJwXPPPYd/+Id/MLo4RHPX4KDoVB0ZCTaIam9lB2y4W/WV1Xq36oZb+bqLIa9iBEIb92UngSQbZ9XUHQiSXgeB9mcBooF2sk6kiIgLv0pLHZiYKnXH+HRNdqUaENqwLsn/p5oMp6jp/Y9lA7zez1L/DHUj/oXS6ySSt+qRMvRu5f8h3K28+lXeJiSIz472c0ZE0+bxeNDb2xt26H+Hw4GWlhaMjo4qr1F3Hk0WAqCZd+LECezbtw979+7F3r17cejQIXg8HqSnp6OiogKbNm1CRUUFNm/ejKSkpODV06OjwWOEgQFxjDAwEKwXZJ0g65KBAVF/9PcHjye028g6Rq9Omg45ooH6OENb96tDjUBo+BEIDSBM9TVaso6ZjukeR8j/wXScLzgnA3mSOrQHBEN503mN+n8qt72Y4x8g+D/VhkySksSxQlJS8NhA/q/k3zcpSfwvk5PRMzyMH/3xj9j54Yc4cvo0fH4/Llu6FNVbtqD6Yx/D1o9/HAUFBRdezkXM5XKhs7MzbJBAXRdER0cjPT09bJBg+fLlSORxGhERERERzU8MCRBdiJ/85Cd49NFH4XA4OM8h0Vyld6W4unNaHS6QgQUgtAFZ3cit7YjWa0TWNj5rf4akbbRWX8EezoUGINTvc7ou9EpzvbCElhzRQdLr+JCN6Gp6nR/azgt1gEP9M9SvVXeSqN+nLJc6lMGRJojmNNnhE27o/66urpB5rAExBLVex79cz8/Ph4Xf+1kxODiII0eOoLa2Fna7HXv37kVvby9MJhNKSkpQVVWFyspKlJeXo6ysDBHqusMoeh3KF3PVuaS9r6379Y49tK+ZrINeL6R3Pnqd7x/pAfABgO3aJ/SClOdzvrpWW9frHWtop5bQhjK1r7nY0SH0gh+XwPDwMPbs2QO73a58T8bHx2G1WpXvR1VVFdavXz83vh8LgByNYLIRCSRtfaKtUziaDBERERERzVEMCRBdiDVr1uCKK67Af/7nfxpdFCIiIqIFa2xsLOzQ//Kx7u5uZehos9mMtLS0sFMAFBcXIy8vD9Ha0VxoVni9Xhw/fhx1dXVKZ2djYyP8fj+sVivKy8tDQgFLJrsingz3+9//HjfeeCPcbje/U7PI4/HgyJEjqKmpgd1uh91uR39/PzIyMlBRUaF8hyoqKvh/uUTUdZNekKCtrQ3ej8LKZrMZNpstbJCgtLQUCdpQLBERERER0aXHkADRdNXV1WHDhg3Ys2cPKioqjC4OERER0bzkdDrDDv3f1dWFU6dOYUB11bMc/n+yof8LCgoQpR56nQzlcDhQV1enhAJ2796N0dFRWCwWrF69WgkFbN26FVlZWUYXl6bpzTffxCc+8Qk4nU4kX+iUSHTRfD4fGhsbUVtbi5qaGrzzzjvo6+tDQkICNm/erIw0UFVVhVjtFFh0SainuNELEpw8eRKDqhE6UlJSJgTa1GECq9XKUSKIiIiIiGimMSRANF133303/vjHP+LkyZNGF4WIiIhoTpJDNYcLAbS2tmJENWy6DADoDf0vQwDsJJnbhoeHcejQISUUsGvXLrS0tCAqKgqlpaUoLy9XQgHr1q3j8NsLQG1tLaqqqtDR0YHc3Fyji0MqTU1NykgD8rtoMpmwZs0abN++HZWVlaiurma4w0DaoJy2zmxpaYH/o+nJwtWRcsnPz4fpQqYIIyIiIiKixYwhAaLp8Pv9yM/Px2233YZHHnnE6OIQERERzSqXy4WzZ89O6PhXr7e2tsLn8ymv0c7XrF3Py8tD4nTnLydDySuX1aMEHDp0CD6fT5k2QC7siFy4Dh06hHXr1uH48eMoKSkxujg0CYfDoYw0YLfbcezYMURGRqK0tBRVVVXYvn07tm3bhvT0dKOLSh9xu93o6OiYECSQ9a02bCdHI9ALEixdupT7YSIiIiIi0mJIgGg63n33XWzbtg1Hjx5FWVmZ0cUhIiIimjFyjmW9jn+53t3dDXn6YDabkZaWFnYKgOLiYuTl5XFO7AWgu7sb+/fvDwkFOJ1OxMfHY+3atSGjBBQXFxtdXJolJ0+eRElJCQ4ePIh169YZXRyahp6eHuzbt08JDtTX18Pv96O4uFiZnuCaa65BYWGh0UWlSeiN2qMOEqjrbPVoBHphAk7XQ0RERES06DAkQDQdt99+O/bt24f6+nqji0JEREQ0ZdphjdVD/3d1deH06dPo7+9XtpedCerh/rUhAHYoLEwejwdHjhyB3W5XQgENDQ0AoHQgylDApk2bYDabDS4xGcXhcCA3Nxe7du1CVVWV0cWhizA0NIS9e/fCbrejtrYWu3btgsvlgtVqRVVVlRIcWL9+Pad9mUfGx8fhcDjCBgna2trg9XoBhAb/9IIEJSUlsFgsBr8jIiIiIiKaQQwJEE2Vx+OB1WrF/fffj/vvv9/o4hAREREB0L+SUB0CaGtrw/DwsLK93tzG2hCA1WplR9AiIYchl6GAAwcOwOVyISkpCRs3blRCAVu2bEFaWprRxaU5ZGBgAMnJyfi///s/fOITnzC6ODSDRkdHcfDgQWXfsHPnTgwODiIrKwsbN25UpihYt24dIiMjjS4uXQR5DKEegUAeS5w6dQoDAwPKttrpg7RBgsLCQn4eiIiIiIjmD4YEiKbqzTffxCc+8Qk0Nzdz2EUiIiK65FwuF86ePRt26P+urq6QqwCBiQ342hDAsmXLkJSUZOC7IiMNDg7iyJEjSsffnj170NfXB5PJhJKSEuWK4fLycpSVlTEoQpPyer2Ijo7GK6+8ghtvvNHo4tAl5PV6cfjwYWWkgbfffhvnzp1DQkICNm/erIw0UF1djZiYGKOLSzNITkUULkjQ2toKn88HAIiJiUFubu6EqQzk/aKiIsTFxRn8joiIiIiI6CMMCRBN1Te+8Q3s3bsXdXV1RheFiIiI5jnZ6K7X8S/Xe3p64Pf7AQDR0dFIT08PO/R/cXEx8vLyEB0dbfA7o7nC6/Xi+PHjIaMEHDt2DIFAAFarFeXl5UooYMOGDYiNjTW6yDQPxcTE4Fe/+hW++MUvGl0UmmVNTU2oqamB3W7He++9h7a2NsTFxWHdunXKvmXr1q0Mpi1wbrcbfX19YYMEJ06cwNDQkLJ9SkrKhBEItPeJiIiIiGhWMCRANBWBQAD5+fn4+te/ju9///tGF4eIiIjmMKfTGTLcvzYEcPr0afT39yvbyyvvwg39n5OTg4KCAkRFRRn4rmiuczgcqKurU0IBBw8exNjYGCwWC1avXq102lVUVCAzM9Po4tICkZqaih/+8Ie44447jC4KGaypqUkZacBut6OhoQEmkwlr1qxRRhq46qqrOG3JIqQ3LZL6fnNzM2TTpHpKJL0gQX5+Pkwmk8HviIiIiIhoQWBIgGgq9uzZgyuuuAJHjhzBqlWrjC4OERERGUTb0K0NAbS1tWF4eFjZXt3YHS4EYLVaOaw7Tcvw8DAOHTqkhAJ27tyJnp4eREVFobS0NGSUgBUrVnCOaLpk8vLycNddd+Gee+4xuig0x3R1dcFutyvBgfr6evj9fhQXF2P79u2orKzElVdeiYKCAqOLSgZzuVzo7OwMGyRoaWnB6OgogNCRlfSCBMuXL0diYqLB74iIiIiIaF5gSIBoKr73ve/hpZdeQnNzs9FFISIiokvA5XLh7NmzE676VzdWt7W1wev1Kq9JSUnRvepf3i5btozDLNNF8/l8aGxsRF1dnRIKkJ1tctoAGQrYsmUL53umWXXZZZfhlltuwUMPPWR0UWiOGxwcxL59+5QpCvbv3w+32w2r1YqqqiolOLBy5Uqji0pzkAxp6gUJmpqa4HQ6lW21x2faIEFhYSHDc0REREREDAkQTU1ZWRmuvfZaPPHEE0YXhYiIiKZpbGws7ND/8ralpQV+v195jZwzN1wIoLCwEPHx8Qa+K1qourq6cODAASUUYLfb0d/fj/j4eKxdu1YJBVRXV6OoqMjo4tIiV15eju3bt+Pxxx83uig0z4yMjKC+vh61tbWoqalBbW0txsbGkJ2djerqamWKgnXr1rFDl85LHuuFCxK0trbC5/MBAMxmM2w224QggTzW4zEeERERES0SDAkQnU9raysKCwvx9ttv46qrrjK6OERERKTidDrDDv0v19VXl8XExCA1NXVCx796vaCgAFFRUQa+K1osZCeZDATU1dWhoaEBAFBcXIzKykolFLBp0yaYzWaDS0wUauvWrVi9ejWefvppo4tC85zX68Xhw4eV6QlqamrgdDphsVhQUVGhjDTAfSFdCI/Hg97e3rBBgpMnT2JwcFDZXi8sqg4ScKooIiIiIloAGBIgOp9f/vKXuOuuu3Du3DnExMQYXRwiIqJFQw4tGy4E0N7ejqGhIWX72NjYkOH+9UIAbNQlIzU1NcFutyuBADncdnZ2NjZs2KAEAiorK5Gammp0cYnO69prr0V2djaef/55o4tCC4ycakUGBt5991309vYqo6qopyhYsmSJ0cWlBUAdPFVPZaA36pQ85gwXJMjPz4fJZDL4HRERERERTYohAaLzuemmmzAyMoI//elPRheFiIhoQXC73ejr6ws79H9TUxPa2trg9XqV12jnl1UP/Z+Tk4OlS5ciOTnZwHdFFGpgYAD79+9XQgHvv/8+zp49i+joaKxevTpklICysjKGV2he+vu//3tERkbi5ZdfNrootAjIoFVtbS3eeustNDc3w2QyYc2aNcr0BB//+McZsqJLwu12o6OjI2yQoLW1FSMjI8r2cjQC9QgE8jiWx61ERERENAcwJEA0GZ/Ph8zMTHz/+9/HXXfdZXRxiIiI5rzx8XE4HA7dof/1rsQC9Id0VYcACgoKkJCQYOC7Ipqc1+vF8ePHUVtbq4QCjh07hkAgAKvViqqqKiUUsGHDBsTGxhpdZKIZ8eUvfxl9fX0MVJMhHA6Hst+tra3FwYMHERkZidLSUmW/u23bNuTl5RldVFoktKNgaYME3d3dkM2w6tEI9IIEnP6KiIiIiC4xhgSIJrN3715s3rwZR48eRVlZmdHFISIiMtTY2NiEjn+9EIAUExOD1NTUsEP/5+TkcDhWmpccDgfq6upCQgHj4+NITEzEqlWrlM6pzZs3IyMjw+jiEl0y3/jGN9DQ0IB3333X6KIQ4cyZM9i7d6+yb963bx88Hg+Ki4uVkQYqKyuxcuVKo4tKi5QM02qnMpD31SNpmc1mpKWlhQ0SlJSUwGKxGPyOiIiIiGgeY0iAaDI7duzAL37xC3R0dBhdFCIioktKOw+rdr29vR1DQ0PK9vLqJ72Of7menZ2NyMhIA98V0cUbGhrC4cOHlVDAe++9hzNnzsBkMqGkpATl5eVKx9OKFSv4madF5b777sO7776L/fv3G10UogmGh4exZ88eZaQBu92O8fHxkBFeqqqqsH79ek75QnOGHI1AL0hw6tQpDAwMKNvqjcalvl9UVMTPNhERERGFw5AA0WS2bt2KZcuW4bnnnjO6KERERBfE6/Wip6cHra2tcDgc6OjoQHt7u9LxL+dW9Xg8ymsyMjKQm5sLm80Gm82G3Nxc5OXlITc3V1nn8P+0EPl8PjQ2NoaMEtDY2Ai/3w+r1Yry8vKQUMCSJUuMLjKRoR555BG8/PLLaGhoMLooROfl8Xhw5MgR1NTUwG63w263o7+/HxkZGaioqFD27RUVFYiOjja6uES61CN76QUJWltb4fP5AIhRvXJzc8MGCYqKihAXF2fwOyIiIiIiojqnvgAAIABJREFUgzAkQBTO0NAQ0tLS8MILL+Dmm282ujhEREQTuN1u9PX1hR32XztsKSCuOJrs6v/8/HwOXUqLhpw2QIYCdu/ejdHRUSQkJGDNmjVKKODKK69EQUGB0cUlmnN+/OMf4+mnn0Zra6vRRSGaNhkMq62tRU1NDd555x309fUhISEBmzdvVkYaqKqqQmxsrNHFJZoS7fmBNkhw/PhxDA8PK9vL0QgmG5GAiIiIiBYkhgSIwnnttddw4403oqurC5mZmUYXh4iIFhmXy4XOzk7djn+5rr5SCNAfclSu82ohWuxGRkZQX1+vhALsdjuam5sRFRWF0tLSkFEC1q1bx2kDiKbg5z//OR566CH09fUZXRSiGdHU1KSMNLBr1y60tLTAZDJhzZo12L59OyorK1FdXY3k5GSji0p0weSUBurzC/X95uZmyOZiOcVYuCBBfn4+TCaTwe+IiIiIiC4AQwJE4XzrW9/Cnj17cODAAaOLQkREC4wcJlSv41+ud3d3K41zZrMZaWlpuh3/8jE20BGFampqgt1uV0IB+/fvh9vtDpk2QIYCUlJSjC4u0bz0wgsv4I477sDY2JjRRSG6JBwOhzLSgN1ux7FjxxAZGYnS0lJUVVVh+/bt2LZtG9LT040uKtGMkWHlcEGClpYWjI6OAgCio6ORnp4eNkiwfPlyJCYmGvyOiIiIiEgHQwJE4Vx22WW44YYb8KMf/cjoohAR0TyinidULwQgbyU5V6hex79cLygoQFRUlIHvimhu6+/vx4EDB5RQwO7du3Hu3DlER0dj9erVqKysVEIBK1euNLq4RAvG7373O3z2s5+F1+tlPUWLQk9PD/bt26cEB+rr6+H3+1FcXKxMT3DNNdegsLDQ6KISXVJyNAK9IEFTUxOcTqeyrXa6M22QoLCwkCM4EREREc0+hgSI9PT19SEzMxN/+tOfcO211xpdHCIimiOcTuekHf8nT57E4OCgsr0cnlOv41+uW61WREREGPiuiOYXj8eDEydOoLa2VgkFHDt2DIFAAFarFVVVVUooYOPGjYiJiTG6yEQL1l/+8hdcd911GBwchMViMbo4RLNuaGgIe/fuhd1uR21tLXbt2gWXy6XUR3KKgrKyMh7v0aKiDk7rBQnU06aZzWbYbLYJQQJ5vlRYWIj4+HiD3xERERHRgsOQAJGe119/HX/3d3+Hvr4+pKamGl0cIiKaBdq5ObVhgLa2NgwPDyvbq+fnDBcC4BDmRBdPDvWsnjpgfHwciYmJWLVqlRIKuOKKKzjcM9Es27lzJ6688kp0dXUhOzvb6OIQGW50dBQHDx5U6q2dO3dicHAQWVlZ2LhxoxIcWLduHa+cpkXN4/Ggt7c3bJBAG75OSUmZcK6lDhIweE1EREQ0bQwJEOn53ve+h9dffx1Hjx41uihERHSR1A1QeqMANDU1ob29HR6PR3mNdkhM7VQAnFuT6NIYHBzEkSNHlM6VvXv3ore3FyaTCSUlJSGjBPCqTCLj1dXVYcOGDTh9+jSKi4uNLg7RnOP1enH48GFlpIG3334b586dQ0JCAjZv3qxMUVBdXc2Rb4g0tKO4acMELS0t8Pv9AMIHuOWSn58Pk8lk8DsiIiIimlMYEiDSc+WVV6KkpAS//OUvjS4KERFNwuVy4ezZs7od/3JdPZQlMPEqFG0YoKCgAAkJCQa+K6LFwev14vjx46irq1NCAY2NjfD7/bBarSgvLw8JBSxZssToIhORRmNjI1asWIEjR45g1apVRheHaM7z+/04duyYUu+99957aGtrQ1xcHNatW6fUe1u3bkVSUpLRxSWa01wuFzo7O8MGCVpbWzEyMqJsL88D1SMQyPPAZcuW8TtHREREiw1DAkRaHo8HycnJ+NnPfoZ//Md/NLo4RESL1vj4OBwOh27Hv97VI9HR0UhPT9ft+JfrvIKEyDgOh0OZLqC2tha7d+/G6OgoLBYLVq9erYQCtm7diqysLKOLS0RT0N7ejvz8fLz//vvYvHmz0cUhmpeampqUkQbsdjsaGhpgMpmwZs0aZaSBq666CmlpaUYXlWje0U4ppzcigaQejUAvSFBQUICoqCgD3w0RERHRjGJIgEhr//792LRpExoaGrBixQqji0NEtCCNjY2F7fiXj3V3d0MepsTExCA1NVW341+us9GGaO4YHh7GoUOHlFDArl270NLSgqioKJSWlqK8vFwJBXBeZqL569y5c0hLS0NNTQ0+/vGPG10cogWhq6sLdrtdCQ7U19fD7/ejuLgY27dvR2VlJT72sY8hPz/f6KISzXsymK4NDsj7bW1t8Hq9AACz2Yy0tLSwQYKSkhJYLBaD3xERERHRlDEkQKT105/+FD/4wQ/Q19fHBmsiogugnjtSbxSA06dPo7+/X9leXrGh1/EvbwsLC7lPJpqjfD4fGhsblUBAXV0d9u3bB4/Ho0wbIJfq6mokJycbXWQimiEulwuxsbF47bXX8OlPf9ro4hAtSIODg9i3bx9qampgt9uxf/9+uN1uWK1WVFVVKcGBlStXGl1UogVJjkagFyQ4deoUBgYGlG21U9tpgwRFRUWIiIgw8N0QERERKRgSINL6/Oc/j+HhYbzxxhtGF4WIaM5RBwC0DSQOhwPt7e0YGhpStlcP2ajt+JePWa1WNpQQzSPd3d3Yv39/yNQBTqcT8fHxWLt2bUgogB0WRAtfdHQ0/vu//xs333yz0UUhWhRGRkZQX1+P2tpa1NTUoLa2FmNjY8jOzkZ1dbUyRQFH6iGaHepR8vSCBK2trfD5fADECHm5ublhgwRFRUWIi4sz+B0RERHRIsGQAC1uL7zwAoqKirB69WrlqraCggJ8/etfx7/8y78YXDoiotnj8/nQ3d2NtrY2dHZ2oqOjA+3t7SHrXV1d8Hg8ymsyMzORk5MDm82GvLw85OTkID8/H7m5ucrw/2zgIJrfPB4Pjhw5ArvdroQCGhoaAADFxcWorKxUAgGbNm2C2Ww2uMRENNuSkpLwxBNP4LbbbjO6KESLktfrxeHDh5XpCWpqauB0OmGxWFBRUaGMNMB6msgYbrcbHR0dE6YykPePHz+O4eFhZXs5GoFekKC4uBgpKSkGvhsiIiJaQBgSoMVt/fr1qK+vBwBkZ2dj5cqVqK2txT333IPPf/7zKC0tRXR0tMGlJCK6OF6vF11dXUoAoLOzM2S9tbUV3d3dytUNERERyM7Ohs1mQ25uLvLy8mCz2UJCALm5uYiNjTX4nRHRTHM4HKitrVVCAQcOHIDL5UJSUhI2btyohAK2bNmCtLQ0o4tLRLPsZz/7GT788EP09/djbGwMg4ODOHDgAJKSkhAREYGxsTG43W4MDw/jwIEDKC8vN7rIRIuOnAZIBgbeffdd9Pb2KiP+qKcoWLJkidHFJSIEpzQIFyRobm6GbMJXj9anFyQoKChAVFSUwe+IiIiI5gGGBGhx+9KXvoQXX3wRfr8fgOgYM5vN8Hg88Pv9MJlMWL58ObZu3Ypnn32Ww2ET0Zzj8XjQ29uLrq6usNMAtLW1wev1Kq/RzpOoHf6/sLAQ8fHxBr4rIpoNg4ODOHLkiBIK2LNnD/r6+mAymVBSUoKqqiolFFBWVsbjICLC008/jTvvvBMRERGYrCkhMzMT3d3d3G8QzRFNTU3KSANvvfUWmpubYTKZsGbNGmV6gu3bt/MKZaI5yuVyobOzUzdI0NTUhPb2dmXUv+joaKSnp4cNEixfvhyJiYkGvyMiIiKaAxgSoMXtsccew8MPPwy32z3pdo888ggefvjhWSoVEZHgdrvR19en2/Ev19XzGwITAwDa9by8PI6QQrQIeb1eHD9+HHV1dUoo4NixYwgEArBarSgvL1dCARs2bOBIIUSka2BgAFlZWXC5XGG3iY6Oxm233Yaf//zns1gyIpoO9chBtbW1OHjwICIjI1FaWqocD2zbtg15eXlGF5WIpkiORqAXJDh9+jT6+/uVbVNSUnSnMpCPFRYWIjIy0sB3Q0RERLOAIQFa3F577TXccMMNYa+CiYiIQEZGBpqamnhVLRHNqEsRAMjPz4fJZDLwXRHRXOFwOEICAQcPHsTY2BgsFgtWr16thAKuvPJKZGZmGl1cIppHvvzlL+N///d/lSsW9fz5z3/GtddeO4ulIqKLcebMGezdu1c5bti3bx88Hg+Ki4uVkQYqKyuxcuVKo4tKRBdobGxMdyoDeV/d/hATE4Pc3NywQQKOPkhERLQgMCRAi9uJEydQWloa9vnIyEg8//zz+NKXvjSLpSKi+U4OBShPuvUCAC0tLcpUJ9rhAPWmAWAAgIjCGR4exqFDh5RQwM6dO9HT04OoqCiUlpaGjBKwYsUKXhVERBeltrYWVVVVYZ+PjY2F0+nkiCRE89jw8DD27NmjjDRgt9sxPj4Oq9WqHFNUVVVh/fr1nFaEaIFQT2WonspAtmGcPHkSg4ODyvZ6FzGogwRWq5X7ByIiormNIQFa3LxeL+Li4nSvgomKisKKFStw+PBhNqYTkWJ8fBwOh0O341+ud3d3KyOUmM1mpKWl6V75L9cLCgoQFRVl8DsjovnA5/OhsbERdXV1Siigvr4efr9fmTZAhgK2bNmCuLg4o4tMRAtQaWkpTp48OWFEtqioKHzyk5/EH/7wB4NKRkSXgsfjwZEjR1BTUwO73Q673Y7+/n5kZmZi06ZNSnCgoqKCU5sRLWBOp3PCCAThLoaIjY0NCQ9owwS8EIKIiMhwDAkQlZSU4OTJk7rPvffee9i6dessl4iIjDLdAEBMTAxSU1MZACCiS6arqwsHDhxQQgGyUT4+Ph5r165VQgHV1dUoKioyurhEtEg89dRTuPfee0OmRQJESOBXv/oVvvKVrxhTMCKaFT6fD4cOHVJGGnjnnXfQ19eHhIQEbN68WRlpoKqqiqOKEC0i2lEVtUGC1tZWjIyMKNvL0Qj0ggTLli1DUlKSge+GiIhowWNIgOimm27CK6+8oiRdATH093XXXccrYIgWEDn/3mQBgK6uLmV77Rx8egGAwsJCjjRCRDNGXqVnt9uVUEBDQwMAKHMCy1DApk2bYDabDS4xES1W/f39yM7OhsvlCnk8IiICXV1dyMrKMqhkRGSUpqYmZaSBnTt3orW1FSaTCWvWrMH27dtRWVmJ6upqJCcnG11UIjKQ0+mcMAKB9r6kHY1AGyTgRRlEREQXhSEBokceeQSPPfZYSAOXyWRCQ0MDli9fbmDJiGiqZAAgXMd/U1MTnE6nsr080dTr+Of8eUSkVV9fj7i4OJSWls7oz21qagoJBBw4cAAulwvJycnYsGGDEgqorKxEamrqjP5uIqKL9YUvfAEvv/yyMnVbREQENm7ciL179xpcMiKaCxwOB2pra5XgwLFjxxAZGYnS0lJUVVVh+/bt2LZtG9LT040uKhHNIXKEx3BBgra2Nni9XgBiekebzTZhKgN5v7S0FAkJCQa/IyIiojmLIQGi3/zmN7j55puV4cOjo6Px7W9/G//+7/9ucMmICDh/AODUqVMYGBhQtmcAgIhmyuDgIB588EE888wz+PGPf4y77777gn/WwMAA9u/fr4QC3n//fZw9exbR0dFYvXp1yCgBZWVl3EcR0Zy3c+dOXHnllcr96Oho7NixAw888ICBpSKiuaqnpwf79u1TggP19fXw+/3KaElVVVW45pprUFhYeFG/59y5c7j11lvxzDPPwGazzUzhiWhOUY9GoG0r0rYRySkNwgUJioqKeO5FRESLFUMCRB988AFWr16t3LdYLGhpaeEVe0SzwOl0KidzegGAkydPYnBwUNlePdTcZNMAEBFdrN/85je488474XQ64fP58JnPfAa//e1vp/Rar9eL48ePo7a2VgkFHDt2DIFAAFarFVVVVUooYMOGDZyrl4jmrZKSEpw6dUoJXDc0NGDFihUGl4qI5oOhoSHs3bsXdrsdtbW12LVrF1wul3KsJKcomG548vXXX8f1118Pi8WCp556Crfeeis7AIkWGfXFJnpBgpaWFmXaWTnVZLggQVFREeLi4gx+R0RERJcEQwJELpcL8fHx8Pl8iIyMxFNPPYU777zT6GIRzXvqAIDeKADt7e0YGhpSttcLAKjXly5dyvkrieiSO336NP7pn/4Jb731FiIjI5XGo+zs7JD5MdUcDgfq6upCQgHj4+NITEzEqlWrlFDA5s2bkZGRMZtvh4joknriiSfwwAMPwOfzIS8vD21tbUYXiYjmqdHRURw8eFA5ntq5cycGBweRlZWFjRs3KsGBdevWITIyMuzPue+++/DTn/4UHo8HERERqK6uxvPPP4/i4uJZfDdENJe53W50dHRMmMpA3j9+/DiGh4eV7eVoBHpBguLiYqSkpBj4boiIiC4YQwJEAFBQUIC2tjYUFxejsbER0dHRRheJaE47XwCgra0t5ITqfAGAZcuWISkpycB3RESL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mDFGlYmSITQ01Mqz0LF//374+flZ/TvlwF27di2eeeYZm49vTyx5hhRJSUlYsmQJGjRoYPdnuVJQXMylkj09gYcfdtxxJkwA6tYF/u//gAoKBlZFqos+qUzvvaXv+/z589GqVSscPHhQ7/Py3hN760tTcjoCc7rUWh1mr2e7vIguq6a6zJAjR4Bnn+XyanXrOu44r74KbNoELFvmuGNUcuz9bpSnn2+triMirFy5Ek899VSp7YKDgzF69GgUFRXhf//7n0WyV2T7bIg1bVtZ19ja62gvPeeIdrY63WNAdFiN0WE5OTrbacIExx3noYeAtDTg3/923DGcgD2e9z/++ANEhB49epRLlqNHj6KgoABTp05F06ZN4e3tDRcj07YGBwfjgQcewIYNG/D2229jypQp5TquomHDhvD29sahQ4fssj/FlStXSvr19erVw6JFi9CpUyeToxcBYN68efDw8MDzzz9vdXlhLe3atYO/vz/27dun9/nu3buRn5+Pzn8n1ri7u9t0HHvce1vbantR1n1v164dXF1d8eeff1awZA6msBB45RXg9tudV+nroYe4Qsu4cYAdqlpUR2xpP7Zt24bi4mJ06tQJjRs3Ru3atbFlyxaLj2ltf9BaGS1t622hrH3bcj2swdH7V1jathv7nSPbs+PHj+PcuXO4fv06CgoKkJiYiOXLl5ckIFr7rDgqVjRgwADUrVsXH3/8Mfbv348FCxaU2saZsSF7UWP1my288w7w44/AvHl22Z3xutBLlgA+PsCAAcDfzt2azsiRI7F69WqkpqYiLS0NGzduxJw5c3DPPfcYdXzn5OSguLgYRIQrV67g888/x8svv4y6deuWGDIdO3bEN998g2PHjqFv37746aefkJaWhoKCAly4cAGffPIJJk+ebLe5go3h4uKC9957Dy4uLpg+fXrJnMFDhgzBokWL8MUXX2DixIk4fPgwcnNzkZ6eji1btmDAgAFISUnB2rVrERgYaBdZiAg5OTlYt25dmXMRmpK7IvH19UXTpk1LSp0YsmDBAgQFBWH06NH47rvvkJWVhby8PBw+fBjjxo1DYWFhyQjLsnjyySfx4IMPIjk5GaNHjy5lDCkZ1OhiaygoKEBycjL++OMPmxTBmDFjULduXYwcORJNmza1+vf2xNwzRETIzMwseS+vX7+OVatWoXfv3nBzc8OGDRuq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ILKGrYpYBbVaUVFUlCpKVSq4V31deIttrVvLW2ttqW2lqBWlSFWqVdnLDiK7hASSkJB9J8uc94+nJ/fOZGYyk0wyWX7fz+d+Zrszc+bOzDn3nuc55z6L733ve5wlgIiIiFpEYmIisrOzfV0Mok6NSQJEREREREREREQdkKtZAhISEjB16lT8z//8D1JSUjBx4kQEBgb6ushERETUCSQmJnImASIfY5IAERERERERERFRB1BaWoqDBw/WJwRs374dBQUFCAsLw5gxY+pnCZg5cyZiY2N9XVwiIiLqpJgkQOR7TBIgIiIiIiIiIiJqh06fPm1z2oD9+/fDarXWzxLwzDPPICUlBRMmTEBQUJCvi0tEREQEQJIEDh065OtiEHVqTBIgIiIiIiIiIiJq4xqbJSA1NRWPPfYYZsyYgbi4OF8Xl4iIiMiphIQEziRA5GNMEiAiIiIiIiIiImpjOEsAERERdVSJiYm4cOECqquruR9D5CNMEiAiIiIiIiIiIvKhsrIyHDhwoH6WgB07diA/Px+BgYEYPXp0/SwB06dPR48ePXxdXCIiIqJmSUxMhFIK58+fR+/evX1dHKJOiUkCRERERERERERErcjVLAEpKSl45JFHkJqaivHjxyM4ONjXxSUiIiLyqsTERABAVlYWkwSIfIRJAkRERERERERERC3EfpaAnTt3Ii8vz2aWgAcffBAzZsxAnz59fF1cIiIiohaXmJgIi8WCrKwsXxeFqNNikgAREREREREREZGXZGVl1ScE7N27F7t27UJNTU39LAEPP/wwZwkgIiKiTi0oKAgxMTFMEiDyISYJEBERERERERERNUF5eTn2799ff9qAr776ChcuXLCZJeCee+7B9OnT0bdvX18Xl4iIiKjNSExMRHZ2tq+LQdRpMUmAiIiIiIiIiIjIDfazBOzevRvV1dX1swQsX74cqampuOSSSxASEuLr4hIRERG1WYmJiZxJgMiHmCRARERERERERERkx36WgK+//hq5ubkICAhAcnJy/SwB06ZNQ79+/XxdXCIiIqJ2JTExEWfPnvV1MYg6LSYJEBERERERERFRp9fYLAH33Xcfpk6ditTUVHTp0sXXxSUiIiJq1xITE/Gf//zH18Ug6rSYJEBERERERERERJ1KRUUF9u3b53CWgMGDB2Pq1Km45557kJKSghEjRvi6uEREREQdTkJCAk83QORDTBIgIiIiIiIiIqIOLSsrqz4hYOvWrdizZw8uXryI+Ph4XHLJJZwlgIiIiKiVJSYmoqCgAJWVldz/IvIBJgkQEREREREREVGHUVO0E5PFAAAgAElEQVRTg0OHDtWfNmDz5s1IT0/nLAFEREREbUhiYiIAIDs7G/379/dxaYg6HyYJEBERERERERFRu9XYLAELFy7E1KlTMWXKFISGhvq6uEREREQEI0kgKyuLSQJEPsAkASIiIiIiIiIiahfsZwnYsmULzpw5A39/fwwZMsRmloDhw4fDYrH4ushERERE5EB8fDz8/f2RlZXl66IQdUpMEiAiIiIiIiIiojbJfpaAvXv3oqqqCpGRkRg/fjzuuOMOzhJARERE1A75+/sjLi6OSQJEPsIkASIiIiIiIiIi8rna2locPHiwPhlg69atSEtLq58lICUlBbfffjtSU1M5SwARERFRB5CYmMgkASIfYZIAERERERERERG1usZmCbj99tuRkpKCadOmISoqytfFJSIiIiIvY5IAke8wSYCIiIiIiIiIiFpUbW0tjh8/Xp8QsG3bNpw+fZqzBBARERF1YomJiThx4oSvi0HUKTFJgIiIiIiIiIiIvCo7Oxt79uypnylg27ZtqKysREREBCZMmIDbbrsNKSkpmDp1KqKjo31dXCIiIiLygYSEBHz11Ve+LgZRp8QkASIiIiIiIiIiajL7WQL27t2LI0eO2MwS8Oqrr3KWACIiIiKykZiYiMzMzPrb5eXlOHv2LIqLizFx4kQfloyo42OSABERERERERERue38+fPYvXu301kC5s2bx1kCiDqYnJwcXHnllaipqam/r6ysDEFBQRg7dqzNuqNHj8Y777zT2kUkIqI2rqKiArt378a5c+eQk5ODc+fO4cCBA6irq0Pfvn2Rk5ODqqoqAMCll16KL774wsclJurYmCRAREREREREREQOOZslAAD69++P1NRUrFy5ElOnTsXYsWPh5+fn4xITUUvo0aMH6urqcPjw4QaP2d83b9681ioWERG1IwEBAViwYAEyMzMRGBgIQPY1rVYr0tPTbdabPn26r4pJ1GkwSYCIiIiIiIiIiADIaOFdu3bVzxKwfft2VFRUIDw8HKNHj8bVV1+NF198EampqejWrZuvi0tErej222/H448/jtraWpfrzZ8/v5VKRERE7UlQUBCWLVuGRx55BNXV1U7Xq62tZZIAUSuwKKWUrwtBREREREREREStq66uDseOHbOZJeDo0aNQStXPEqBPG8BZAogoKysLvXr1gtVqdfi4xWJBcnIy9u/f38olIyKi9qK8vByJiYkoKSlxuk5AQACKi4sRGhraiiUj6nTWcSYBIiIiIiIiIqJOoLi4GLt3765PCNiyZQuKi4sbzBIwZcoUdO/e3dfFJaI2JjExEVOmTMH27dsdJgr4+/vjjjvu8EHJiIiovQgLC8PSpUuxcuVKpzPTjBs3jgkCRK2AMwkQEREREREREXUwepYAfdqArVu3cpYAImq2N998E0uWLEFdXV2DxywWC86ePYukpCQflIyIiNqL/Px89OzZE1VVVQ0eCwoKwvLly/Gzn/3MByUj6lTWMUmAiIiIiIiIiKidKykpwa5du+pnCdi6dSuKiorQtWtXJCcnY+rUqUhNTcXkyZMRExPj6+ISUTtVWFiIuLi4BqM//fz8kJqais2bN/uoZERE1J7cf//9ePPNN1FTU9PgsX/+85+48sorfVAqok6FSQJERERERERERC1BKQWLxeL113U1S0BCQkJ9QgBnCSCilnDVVVdh06ZNNrMJ+Pv744033sCPfvQjH5aMiIjai7S0NAwcOLDB6Wv8/PyQn5+PqKgoH5WMqNNgkgARERERERERkTdVVFTg+eefR2xsLJYvX97s17OfJWDbtm0oLCysnyVAnzZg5syZiI2N9cInICJybs2aNViwYAHM3coBAQHIyclBt27dfFgyIiJqT+bPn48PPvjAZjaBkSNH4ptvvvFhqYg6DSYJEBERERERERF5y0cffYQlS5YgOzsbc+fOxYYNGzx6vqNZAo4dOwar1WozS0BKSgomTpyIwMDAFvokRESOVVRUoHv37vXnkg4ICMD3v/99j+s7IiLq3A4dOoQxY8bUJ50FBQVhyZIleOWVV3xcMqJOYV2Ar0tARERERERERNTeZWRkYMmSJdi4cWP99P7btm1r9HmlpaU4ePBgfULA9u3bUVBQgLCwMIwZMwazZs3Cs88+y1kCiKjNCA0NxbXXXot169ahpqYGdXV1WLBgga+LRURE7czo0aNx+eWX48svv0RNTQ1qamowbdo0XxeLqNPgTAJERERERERERE1UW1uLVatW4cknn6zv3DQ7deoUBgwYUH/79OnTNqcN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Wrp0qUpLS1OVlZXq+PHj6tFHH3VYbkfbB4C688471f79+1VlZaUqLi5Wn376qUpJSVERERHqiy++cPrZXX2Pnm4jvW5dXZ3KyclRf/7zn1VoaKiKi4tTeXl5Tj+Ds9+Jp9tSKaWuuOIKBUAVFhZ6ZTu5+70rpZr9Xdpvb1ecfU5v/DfsX7sp30NTfztWq1UVFhaqL7/8Ui1cuFBZLBaVkJCgdu/e7dZ2aTeKi5Vau1apm25SqmtXpYKDlZo3T6lPPlHKavV16Qzr1imVnKyUn59S11yj1Gefta3yucD2smW2g16/PbaXANTEiROdPv7ll182eB1n9bLValU/+tGP1D333KOsbvwnWL93kPq9tFSp3/5WqREjpF686Saljh71dalESYlSr74qZQOUGjVKqRUrlDp+3Ncl8xpX/6MdO3YoAGrp0qX193lSZ9i/dkFBgfr888/V0KFDFQC1du1am/XfeustFRgYqKZPn64+/fRTVVhYqKqqqtR3332n1q5dq6ZMmaJ+97vfOf0szSmbq/+cp+Vqbr3ozj6/N+odV/WiO9umue2HUko988wz6uqrr1YlJSU29+/evVsBUD/+8Y+dPrexNt7R5/HGvoP5dT05pnn11VfVjBkzGmxvd8rdGLZx7dSJE0r97GdyXAAoNXy4Uq+8Isc07vuAMwkQEREREZH37NoF/PCHwPDhwIcfAvfeKyMbN28GHnoI6NfP1yVsESkpKTh//jw++ugjh49/8MEHSEpKcuu1wsLCcPXVV+O1115DWVkZfv3rX7tdjsaem5+fjyNHjuC5554DAPz5z39u8Bp9+vTB888/j6qqKixZsqTB46dOncI777yDF154ocFj3twOAODv748uXbogLi4OgwcPbvD466+/Dj8/PyxevBjFxcVOX8disSA1NRWRkZEICAiwuT8wMBChoaGIjY1FSkoKAOCll15CVFQUXn31VfTt2xchISH1o0C7dOnisszr16/Hz3/+c9xzzz344x//iDFjxiAkJAQRERG44oor8NVXXyE+Ph433ngj8vPzHb6GJ7+BxraR5ufnh7i4ONx22224//77kZub22B0kubqd+LptmyJ7eTu9w4077v0Jm//N7zxPbj727FYLIiKisLMmTOxevVqvPfee8jJycGcOXMa3f7tSkQEcNNNMm1/drZM4Z+bK9N5JifL7AO1tb4r33ffAd//vrSxgwcDBw7IbAKzZwMWi+/K5SG2l4LtpXdZrVYsWrQIgYGBeOONN2Bpxf8E63cf69pVjjcOHZLR+EePymkInnhCzu/sC8XFwAsvyJTQTz8t55LetUvK+NOfSh3eSTWnzoiOjsZll12G1157DQBsZm/ZuXMnFi9ejGnTpuGLL77AFVdcgaioKAQHB6N///646aab8Mwzz6C6urpFyubsP+eNcnnKk31+zVv794442zbNbT9efIERYGoAACAASURBVPFFrF27Fu+99x7Cw8M9KpM7bbwj3th3MPPkmGbp0qVITk7GVVddhdpW3B9lG9eGDRokbd2BA8Du3XK6mmeekbbnueeAoiK3XoZJAkRERERE1HzZ2XJKgUmTZArQ1auBkyeB//kfOUjp4HRw4Le//a3Dx19++WUsX77co9fUUyMePnzY4/I4e+57772HOXPm4JprrkFISAj+8pe/QCnV4PlLly7FuHHjsGnTJqxdu7bBY8899xyioqIaPK8ltoPmqGNiypQp+MlPfoLMzEw8/PDDTp/7t7/9DaGhoY2+x+LFizF37lzk5+ejuLgYBXbTjQcFBWHDhg0uX+Oll14CADz11FMOH+/atSuWLVuG/Px8vPXWWy5fy9PfgLPOG3sDBw4EAJw/f97h465+J55uS2eas53c/d4BNOu79CZv/ze89T1o7v52AOCGG27AwoULkZubizfeeMPt57UrXbsCCxcCX30FHDwo0z8vWgSMHAls2tT65Vm/HkhJkbZ2yxYJhI0a1frl8AK2l4LtpfdYrVbcddddCA0Nxf/93/+1aoIAwPq9zfDzkySq/fuBt94Cfv97YNw4SRpoTRs2yGlgfvlL4O67gTNngN/9Dhg/vnXL0UZ5o87Q04sXmQJgK1asQF1dHVauXGkTeDS74oorcP/997do2QDb/5w3ytUcje3za96ud5wxb5vmbO9Tp07h6aefxnPPPYeQkBCPy+FuG+9Mc/cdNE+OaQDg2WefxYEDB/Dqq6+6XdbmYhvXTlxyiSQ4Z2YCjz0GvP46MGAA8OabcpoyF5gkQEREREREzfPxx8DQoXJetPXrgX37JGGgHZxP01suvfRSDBs2DF9++SWOHz9u89i2bdtQUVGB2bNne/SaeoRAcHCwx+Vx9tw1a9bg+uuvR3h4OGbPno0zZ85gy5YtDZ7v7++P3//+9/D398dDDz1Un2m/ceNG5Ofn484773T4vi2xHRqzYsUKDB48GG+99Vajo2TcNX78eJSXl+PSSy/Ftm3b3H5eRUUFdu7cid69e6NXr15O15s8eTIA4F//+pfL12vOb8CVkydPAgCSk5MdPu7u76SpvLGd3P3em/pdepsv/hstaeHChQCATz75xLcFaQ2jRgF//rOc63rgQBnNv2wZ4MZ5Yb3itdeA664D7rgD2LNHRqS2Y2wvRWdvL73FarVi4cKFiIyMxG9+85tWf3+A9Xub4+cnxyH79wORkcCUKZLs1dIuXgRuvRX4wQ+AuXOB9HTgxReBbt1a/r3bEW/UGYcOHQIAzJgxAwBQXV2NL774At27d3d6/vnWKpuZt8rVHI3t8/tSc7b366+/DqUUrrnmmia9d3OPdZq772DmSdscHR2NGTNm4NVXX/UoqaE52Ma1M+HhkiTw3XfAzTcD990HzJ8vbZQTTBIgIiIiIqKm++tfpTPsppuAw4eBq6/2dYl85t577wWABlnpv/rVr7Bs2TKPX2/z5s0Amtap4+i5GRkZOH78OKZPnw4AmDdvHgDn0yCOGzcODz74IM6fP48nnngC1dXVWL58OX7zm9+4HKXn7e3wk5/8xOXo0C5duuBPf/oT/Pz8cPfdd6O8vNzj97D36KOP4pJLLsHBgwcxdepUjBgxAr/4xS8ajHSxl5GRgdraWsTFxblcLz4+HgBw+vRpl+u5+xtobBtpRUVFePvtt7Fq1SrMmTMHM2fObLCOp7+TpvDGdnL3e2/qd9kSvP3f8AZ3fzv2xowZA6Dx33CHMnCgJMX99a8yUufmmxsdmdNsGzdKQsIvfynJAoGBLft+rYTtpejM7aU31NXV4fbbb8fatWvx4x//uMXfzxXW721Qr15yurOUFDltjJNTPHmF1Qpce63U2Z99Jm1EdHTLvV871pw6o7KyEp9++ikefvhhzJ49u370cnp6OqqqqjBo0CCflQ1o+J/zVrmawp19/tbkqD5qzvbeuHEjhgwZ4taIdHveONbxxr6D5mnbPHbsWGRmZuJgayQ//RfbuHYoKgpYtQr44guZBW3OHKcJzkwSICIiIiKipjl9WjKTH3pIpjFrwlR/Hckdd9yBsLAwvP3226isrAQgB5+7d+/GLbfc4vbrlJeX44MPPsDDDz+MuLg4LF261CvPXbNmDebOnQv//87wcM011yA4OBjvv/9+fXntvfDCC+jTpw9+97vf4Y477sCMGTNwySWXuCxDc7dDcXExLBZL/aLPO+rK5MmT8dBDD+HMmTN4/PHHG12/MV26dMH27dvx2muvYejQoThy5Agee+wxDBs2DF9//bXT55WVlQEAIiIiXL6+nnq6tLTU4eON/QY82UbmdaOjo3HnnXfiySefxIcffuhw/ab8Tjzlre3kzvfe1O+yJXirjmiOpvy/HAkPD4fFYnH63XRot9wiQaD16yUQ1FLq6oDlyyUZoYnTzrdVbC9FZ24vm8tqteK2227D0aNHUVNTg+uvv94rSQ9Nxfq9jQoOBtatk4SulStb7n1+/Ws5Pc2//w3MmtVy79NG2f/29KJnhDLztM4wv3ZoaCiuvPJKRERE4NZbb0XgfxPnSkpKAMipqpqjOWVz9J/zVrnc5ek+f2uVxVl91NT2o7y8HGlpaRgwYECTytacYx1v7ztonrTNOunkm2++afSzegvbuHZs5kzgyy+BbduAV15xuAqTBIiIiIiIqGnefRfo3h342c98XZI2ITIyErfccgsKCwvrz0v8yiuvYMmSJQgKCmr0+frANzIyEkuXLsVVV12FXbt2ISkpySvP1dMfahEREZg9ezZKSkqwfv16h68bFhaGVatWwWq14rPPPsPP3Pium7sdIiMjoZSqX9wN+qxYsQJDhgzBqlWrsHXrVree40pgYCAefPBBHD16FDt37sQPfvAD5ObmYt68eSgsLHT4nPDwcAC250h1RI+QsQ+Su/sb8GQbmdd95JFHoJRCZGRkfceqvab8TjzV3O1k5s733pTvsiU097/hrTI05f9lr7y8HEqpRhM9Oqxp0yRJ7q23Wu49jh4Fjh8HvBDIbWvYXorO3F42V0VFBWbMmIG9e/fiuuuuw7fffosf/ehHLfJe7mD93oZFREh97cF5qz329tvyHuPGtdx7tGH2vz297Nixw+H6ntQZ5teuqanBuXPn8NBDD+HBBx/E6NGjkZeXVx+Er6ioaPZnaWrZHP3nvFkud3i6z99aZXFVHzWl/cjNzYVSqkmzCABNa+Nbat/BzN22WX/unJycRl/TW9jGtXPJycADD0hb5QCTBIiIiIiIqGnS0oBBgzrM9MfesGTJEgAyFV9RURHef//9+un5GqMPfGtra3Hu3Dn88Y9/RJ8+fbzy3MOHD+Obb77B1VdfbZOBv2HDBgCup0G84oorAACDBw9G9+7d3SpPc7aDvVdffRUjR45sdL2QkBCsXr0aFosFd911l9dGvQPAxIkT8eGHH+K+++7DhQsX8OWXXzpcr0+fPggMDGy00+b8+fMA0GD60ab+BtzdRk8//TTi4+Px5JNP4uzZsw0eb87vxBPN3U5mnn7v7n6XLcWb/w1vcPe3Y+/EiRMAgKFDh3q7SO3H8OHSDraU7Gy57Nmz5d7Dh9heis7aXppZrVanj9XV1TkMcHXt2hWLFy8GAKxevRr9+/fH2rVr8YqTUXKtgfV7G9a7N5CV1XKvn5EBDB7ccq/fgXlSZwQEBCApKQl33nknXnrpJRw/fhwrV65E3759ERISUv/b9UXZgIb/ueaUqyn1ollj+/ytzZ36yN3tXVVVBQAIDg5u9H3tT/nT1Da+JfcdNHfb5i5dugAwtkNrYRvXzg0dCpw54/AhJgkQEREREVHTTJwI7NwpHWMEQM5xN2nSJOzatQuLFy/GvHnzEN0Gzkn6zjvvYP78+Q1G+BQUFKBLly7YtGlTfUDWG3y1HSZPnoxly5bh5MmTeOGFF5r8Oj/84Q9RW1vb4P7bbrsNgPMRQSEhIZg2bRoyMzOR5iJ4qEeH6IBSawkPD8eLL76I0tLS+o4es9b6nXh7O7n63pv6Xbpj8+bNHgek2mod4alPP/0UAHDllVf6uCQ+ohTw/vvApEkt9x7DhwMWC+CFkd5tUVv9L7C99Exz69hu3bohy0XwNi0tDb169XL5GpGRkXj//fcRHByMRx99tP5c0c3B+r2D1e9btgBNCCa5LSVFTkFDjfLWftmoUaMAAEeOHEFwcDCuuOIK5OXlYdu2bU6fU1BQ4HLGEW/vMza1XN6oFxvb528Lmrq9dZC8zsn51c3sE/Zaqo331uu60zZXV1cDMLZDU7CN62BtnDvWrQPGj3f4EJMEiIiIiIioaRYsAPr1A264AcjN9XVp2gzdEfP3v/8dDz30kI9LAyilsHbtWvz4xz9u8Fh0dDTmzZuHuro6rFmzxqvv6+3tkJ2djTvvvLPR9V544QUMHToU+/fvb/J7Xbx4EUeOHGlw//HjxwEAo0ePdvpcfR7J559/3uHjJSUleOWVVxAXF4e77rqryWV0xJ1tdPvtt2PixIn4+OOP8d5779Xf39q/E29vJ2ffe3O+y8bs3bsXYWFhHj+vrdURgPv/L0BmeHjllVfQs2dPr/+G2wWrFXj0UQk6rVjRcu+TlARcd52cbuC/5zXuaNraf4HtpeeaW8deeumlyMzMxPbt2xs8ppTCn/70J0ycOLHRcowbNw6vvPIKamtrceONN7oMsLmD9XsHqt937pQplh94oOXe47nngM8/b9k2oYPw1n7Z3r17AQBDhgwBADz77LMIDg7GsmXLnI7APnz4MAICAlq8bOb/XFPK5a160dk+vy+Zt01Tt3dcXBwsFguKi4tt7r/33nuRmZlZ/9r2r9FSbby3X7extll/7h49enhUTjO2cR2ojXPHz38OfPIJ4OS4m0kCRERERETUNMHBwAcfAAUFMpry6699XaI24cYbb0RMTAyuu+469O/f39fFwfbt2xEREYHU1FSHj993330AvDeVvOat7aCUQmVlJT744AO3zh2op2r09/dv8nsCwHXXXYf33nsPRUVFKC4uxvr16/H444/j2muvddlJePnll2PlypV4++23sXDhQhw8eBBVVVUoKSnBpk2b8L3vfQ+FhYX4+9//jsjIyGaVUfNkG1ksFrz++uuwWCx48MEH68/32dq/E29vJ1ffe1O/S2dqamqQk5ODr776qkkdbG2pjnD121FKoaysDFarFUopXLhwAe+++y5SU1Ph7++Pjz76qHOdzxMAMjOBH/wA+PWvgdWrW/780y+/DBQVyXsWFbXse/lAW/ovAGwvm6o5deyKFSsQFRWFefPm4cMPP0R5eTkuXryIgwcP4pZbbkFtbW39qNLG3HfffZg/fz5ycnIwb9481NTUePxZWL93sPp9717g6quBK68Ebr215d5n0iTgN78Bnn0WuPtuoLS05d6rA/C0zqisrKz/rWZlZWH16tV46qmnEBMTUx+oHDNmDN555x0cPnwY06ZNwz//+U8UFxejpqYGaWlp+P3vf49FixY1Ok1/c+ozR/+5ppTLW/Wis31+X3BWHzVle4eGhqJ///44d+5cg8fWrFmDsrIyrF27FoMGDcLcuXPrH2upNt7br9tY26w/t55NwxNs4zpYG9eYsjLg3nuBJ58EXnsNmDrV8XqKiIiIiIioOfLylJo7VymLRan585U6ccLXJWoV69atUwMGDFAAVExMjLr//vvrH3v00UfV9u3b628/9dRTKj4+XgFQfn5+avjw4WrLli1q27ZtavDgwQqAAqASEhLUvHnz3Hp/d567aNEiFRYWpgICAlRycrLat2+fzeMvvPCCSkhIqH+NpKQktWrVqvrHL7/8chUXF6cAKIvFovr27aseeeQRr28H82u4Wp566imX72f2yCOPqGuvvdbmvpKSEjV9+nTVrVu3+jIMHDhQ/e///q/Neps2bVI33XSTGjBggAoODlZBQUFqyJAh6tlnn1VVVVVufDtK7dixQ91yyy2qd+/eKigoSIWFhamRI0eq5cuXq3PnztWv5+5vwJNtZP+aiYmJ6t5777V5vYULFyoAKioqSgHw+Hfy4osvNrotd+7cqUaMGKH8/PwUABUfH99gW7u7nZr6vTf1u3R3e69bt87p5/TGf6OxbejOb9qT384//vEPNXr0aBUaGqqCgoLq39disaioqCg1YcIE9fzzz6v8/Hyn265DKilR6sUXlQoPV6p/f6W2bm299z54UKmkJKUGDVJqz57We18vYnvpve3QEdvLtLQ0dffdd6t+/fqpoKAg1aVLFzV8+HD1zDPPqLKyMofbD4Dq2bOnevLJJ21eq7S0VA0ZMkQBUHFxceoPf/hDg/dj/d4J6nerVak331QqJESpK65QqqKidd53/XqlYmOV6tlTqTVrlKqra5339QH7316fPn3U6tWr6x+/++67VXR0tAKgAgMDVUpKisrIyHCrznD1uw4ODlaDBg1SS5YsURkZGQ3KlZGRoR5++GE1atQo1bVrV+Xv76+ioqLU2LFj1aJFi9S2bducfqbmls3+P9eccrlbL3q6z/+DH/yg2fWOO/WiO9umOe3Hgw8+qAIDA1WF6b/90ksvqaioKBUcHKy+//3vq9OnT9c/1pQ2/tZbb22RfYdZs2Y1uW1WSqk5c+aopKQkZbVaHT6PbVwnaOMaU1en1LvvKtWrl1Lduyu1bp2rtT+wKKUUiIiIiIiImmvDBmD5cuC774Drrwd+8hPASUY9ERFRm3f2LPDb38pSVwc8/LCcaiAkpHXLkZMD3HKLzNizZImMVu3WrXXLQETUHhw4IKcW2LEDeOwxmV65mbNleCQvT9qJt98GhgwBnngCuPFGmYGNiLzi1KlTGDZsGFavXo0FCxb4ujitJj8/Hz179sSKFSuwfPlyXxeH2pqLF4H33wdWrgSOHZPTg770EhAb6+pZ65gkQERERERE3mO1yikIfvELYM8e6Ry74w45QOnVy9elIyIicq28HFi/HvjTn4AvvpCOtQcfBO67D4iO9l25lJKg06OPAlVVEgR76CEgJsZ3ZSIiaisOHJCEgI8+AiZMkOSusWN9V57jx4Gf/QxYswaIjARuuw1YtAgYOdJ3ZSLqQFauXInVq1dj37596Nq1q6+L0yoeeOAB7NmzB5s3b2701BnUiRw5AvzhD8Cf/yynJ5s/XxLUhg1z59lMEiAiIiIiohayd68ENNasAQoKgJQU4Lrr5NzKw4f7unREREQiL09mw/noI+Bf/wJqa+Uc1gsXAnPmAEFBvi6hoawMWLUK+NWv5LzXN90kswtMmODrkhERta7qamDdOuD//g/YsgUYNw545hngmmsAi8XXpRPZ2cDq1cBbbwFpacCIEcC8ecANN8h1Imqyn/70pzh48CDeeecdREZG+ro4Lerll1/Ghx9+iH/84x+I9mXSKrUNR44Af/+7zBxw+DDQt68kot11F5CY6MkrMUmAiIiIiIhaWHW1jMb86CMZnZmTA/TpA8yeDVx+OXDZZZw2mYiIWk91tUxFvWmTJAXs3SuJALNmSSLbtde2/RH6FRXAO+9IcOzAAQk2LVggpyXo3dvXpSMiajk7dgB//Svw7rtAcbEkBSxZAlx6adtJDrBntQJbt0pQ54MPgKwsoH9/SUi76ipg5kwgNNTXpSRqdzZt2oR///vfePHFF31dlBazfv16HDlyBI8++ij8W/P0KdR2VFYCX30F/POfwCefyCk+ExLkNJ/z5gHTpgF+fk15ZSYJEBERERFRK7JagZ07gc8+k8DMrl1y//DhwOTJskyaJKcpaKudfERE1L7k5Ejbs3OnBJf27JHTCvTvLwlrOmmtvU5Xu3Mn8Je/SMCssFBm7pkzR5aUFLanRNS+VVQAX34JbNwoAZL0dEmMuvVWmca/Z09fl9Az+nho40YJ9hw4IIlq48cDqanA1KnAlClMoiYi6qwKC4Ft24xl927g4kUgOdlILpsypamJAWZMEiAiIiIiIh8qKgK+/loOfHbulMBNZaWc93nSJFkmTwYmTgQiInxdWiIiautqayXgYk4KOH1aOtGGDjXale99DxgwwNel9a7qapkdYcMGCaSdOwfEx0tH4lVXSSIE21Iiag/OnJF67OOPZfRkVZWcTuCqq+T0ZWPH+rqE3pOdLcnTW7fKMdHRo3L/8OFG0kBqqiS2ERFRx5OWZrQBW7dKO6CUHLvoduDyyz09lYA7mCRARERERERtSE0NcPCgBHV0cCctTYI7gwcDo0YBo0cDI0fK9X79vJE9TURE7VFeHnDoEPDNN3I+zkOH5LKiAoiKMpLNJk2SZLOoKF+XuHUdPChBto0bpU318wMmTLAdqdq9u69LSUQEnDxpBEe2bgWOHwfCw2Wml6uukpGTCQm+LmXryM8Htm83AkZ79sgI0oQEYMwYGUk6dqxcDhwIcPpxIqL2oa4OOHVK9tEPHJDL/fslWSw4WGYAM++nt/zpz5gkQEREREREbZyeJnrvXiMIlJYmU3WGhcl0ozpxYORIuR4b6+tSExGRt1RWAt9+2zAZ4Px5eTwmRur+UaMkaDJpkoy84TT7hvx8mWXg668l8HTkiNxvHqGUmioBJyKillRTA+zbZyQFbN8u+/tdusiU+1OnApdeKudYDgrydWl9r6pKEgV27jSCSseOycw5oaFG26cTCEaNkgQLIiLynbIyOWY5eNBICjh8WE55FhAgp9jUdffkycAllwAhIa1dSiYJEBERERFRO1RWJgEO+xGkeXnyeFyczDwwcCAwaJBc6uvsNCMiantqaiQB7NQpGVF68qRcP3VKpp2uq5MA0vDhEgAxL/Hxvi59++PoXKdVVdJ+jh0rHZb6ctAgztpDRE1TWSn76gcOyGjJAwdkn72iQpJ6p0wxkpRSUpgU4K6LF+X4Rwef9FJUJPV1375yLDRkiFzqpVcvJtAREXmLUsDZs3LccuKEzIJz/LhcP3NGBrZERkoygF7GjJGBLq2fEOAIkwSIiIiIiKgDOX9eOiK//dYIMJ08CWRkSIAJAHr0kICHffLAgAE8VzMRUUuqrgbS023rZ309PV1GRQISqDYneQ0dKskAnFa55VRX245UPXBAzodaWyuz9oweLZ2aOnlg5EhJ2iAi0i5cMKZO1vXI8eOyDx4ebgRHxo2TUZNDh/q6xB3PmTPyHXz7rRGwOnECKCiQx0NDjYSBQYPkO9DXo6N9WnQiojarsNA2EeDECWOpqJB1oqON5KwhQySxOTlZTpHZdjFJgIiIiIiIOoGaGsnwPn3adtEdaDowFRICJCYC/fvLkpBge7t3b5kajoiIGqqslHNqmuvZrCzjvvR0I2ErOtqoW/WiZwmIjPTt5yBRUyNt5N69shw5IlOE62BTQoKMhNLfnb7erx9HqhJ1ZFlZUh/ofWl9/fRpeTw6WuqElBRjGTaMM5L4UmGh7fGP/s6OHjUCXPbHQfbHQ6zbiaijcnQMY14KC2W9wECZkcV+31cv7Q+TBIiIiIiIqJOrrpYprr/7TmYcOHtWLtPT5TIz00giCAgAkpLkwLBvX0ka6NVL7ouPlw60uDg5eCQi6kjy8mS2luxsWdLTpb7UdeaZM0agAZC6UNeRvXtLnanrzkGDOHNLe2W1Snv5zTcykurYMVmOHweKi2WdqCgZQTV0qCyDBklwqW9foFs3nxafiNxgtUoiwJkz8n/XIyePHZORlNXVsl7PnsaIyWHDjPMrx8X5tPjkgbo6Y4afM2fkuvkyO1um0wZkNog+faQu79tXrvfuLcc/SUmSTNA2ps8mIjJUVUldlpUli+7rOXPGWEpLjfUTEmzrOX194EC57FizmjFJgIiIiIiIyKW6OiMglpFhJBKkpxtBsqIiY32LRTpH4+Kk8zQuTjrOevSQTrT4eCOhIDTUd5+LiKi2FsjJMQL/589L51lOjiRI5ebKZU6OERQCgOBgI/hvnzTVu7csnIq+88nONhIGzMkD6elGkCky0kgYMF/qJSzMl5+AqPPIzZUk2TNnGl6mp8s57wGp7/W09ObknyFDJGhMHdvFi8Yxjzl5QC/Z2ZJUonXvLgG2nj3leMfRZY8enJmNiJpPH8ecOyfHMM4u8/KM51gsUkfpfVCdBGC+DA720QfyCSYJEBERERERNVtFhW2QzVmwLSfHCJQA0rmalATExkqnWmysJBXExMhifx+TCojIlZoa6QjLz5fL3Fw5R7Sj+3Jy5Lq5ToqIkASmHj2kbnKW5NS9u+8+I7U/OshkDkSar1+4YKwbGyudtAkJkmyig01JSfIb7NmTgUmixuTkyD5oZqYsWVmS1JqdLZdpacbML/7+8r9ylLTTr5/87zjFPDnTlCCdn5/sV/ToIfsUsbFynBMXJ/fp2z16yH1MHiPqPMrLjX6TvDzZR7S/nZMj9UpOjuMkJT2ziU5K6tWLSUrOMUmAiIiIiIio1dTWykFuVpbttN15eY4DenoUlxYaKge/cXFGB5pOJoiJkXPARkU1vOTpD4jan6IiOf+lo0tnwX/zrCaABHZ0PWGfeNSjh3SgxcfLZUICE5HIN8rKbJMH0tOlnTx3zgh0mtvD8HDp6NVTXPfsKb9fc4BJt5EMblJHUV0t9f6FC7IPqa+fPWv8T86dk/1K88wvkZG2/5PevY0Rk/36SfCE+4nU0uyn+87Kktvm/RmdvFhebvvcLl2kTtdJAzExcjs+Xo51unWTS/N1JhYQ+V55uRy7FBTIpfm6bsd04F/frqy0fY2wMOP/rv/7cXHSniUmGgtPd9JUTBIgIiIiIiJqs0pLXY8E1rf1UlRkm02vde3qOHnA2aVePyJCrvOAm8gzdXVASYmco72sTBZnAX9Hl4WFjl83PFz+p+YgqF4c3de9u4zYI2rvLlwwRkGbR0hnZ8tpgHJyZB1zN6e/v/G/sE8g0KNXY2Ntg0sMllJr0cETHTTRAVJHiQA5OQ2TwIKDpZ7v1UuCI716GcESnUTTqxeTRxB42wAABYhJREFUv6j9qay0/Q+Yg4j6uv5fFBTIPpa9oCAjYcBREoH9feHhskREyDEQE8yIZJ+qqEiOaUpLZXEU8Hd0X2FhwwEPgPQtdOvWMKnTPKuIOSmAbVhLY5IAERERERFRh1JcbBtwdDcoWVRkTD1rLyBAOs50AoFeoqLkfn07IkJGrHXtatwfFSUd2aGhcjswUDrkiNqSsjKZqr+wUGb8KC2VAI4O8BcVyX36tjkBQN9vXsd+FIwWEOBeso6zx/z9W3e7ELUnVqsRPHIUZLUPwJaWNnwN3XndWGBJX9q3gdR5WK3SDpSUGG2BfcDEVTDFPnji52eb8GVOZLGfhj0+Xva3iMjYf3MWqHT1f3QUxASMOl0nD+jkaZ1IYE4oMCcY6PYgOFj+oyEhMhMCUWuprJSZO4qLZVYZfWxSWmoE++0D/yUlsuhjGfPiSHCw8/0kV8k40dFMxmx7mCRARERERERE/3XxonQONBYU1R0NrtZx1umm6cSDLl2kAy0iQjoNIiONpIKwMBkJpE+ZYF4/MFA64QB5rr+/8TzASEQIDZX7/f0ZwGkPqqqkc0uPXAEkWF9dbQTvAfmt1dXJ76yiwhi5r2+bg/41NXK7okIeLymR+4qLjfVd8fOT36W589fdpBnzOvo6EbUNVVUyKtXdoK75urPuVJ0op5foaLkMC2tYT3TpItcDAowkoMhIo33TwSXdFlLT6PaguFjaiqIio82orpY2RgdVdMDfvJ+jb7ubDGYewdxYsMR8X2wsZ34ham0VFVKn64Cpo0Cpvs8cVC0tlTpFJwrV1Lh+n4gIqRsiIoxjk6gouU+3F/qYRx/P6PZBtwFBQXLdYpH1AGMd3V7weKft0ccsup0xH88UFcn+hD7W0W2SXkcfuxQVyWO6Laqulvv0cUxpqXGM44o+ntZJL+ZEGHPSi6sEmehoju7vWJgkQERERERERC1AB2YLC40OD90BUlQkj5eW2nbM6058HSguLZX7dMeI+TXcCe46ozvUdGcbYDu1qLnzTbOf/SAy0rYzX7+mpmdNcEQnSHjCk9kXdEDEXXp7O2M/9X1xse1pLUpLpTPL2fvrDi779fV36ug13aWDafo709+pqwQTRwkp5lku9GuGhcnz2BFGRPZ00kBzk+k8qft0Qlx0tG0gyNym6KQmoGFbZm7n9GuZuRtcamwqbnfaZz0K3565PTK3EXqfAjAC/Pav4yzw7w5zW6CTOMyzI9knfejFnEAWHi5Bf54LnajzqaoykgfKy43Arq7TdCKrOfBrPkYqLZXrxcW2I8Gt1ob72e7Sxyo6KcGcYA0Y92v2xy727URjx0pmnhy3mI/HGqOPA93l7PRdgBGgd7SufRtlDuwDtu0TYBz76PbPWRvXGN2e67ZcJ39ERsp2Cg83jnGio20TSoKD5TvTxzvmJBQd8OcpBKkhJgkQERERERFRO6c7dXSnmzmAoDttzAEG3enmaNQ60LAjyPx6QMP1zWXQXAV+vB3Et9eUJARXQR/7TkL7TkT7TkbdOaWZZ3gwr28OSLlK3NCvZw5+uUrCICJqT3SbotseHWTQbZoOEOkR8MXFtu2UOSjvLJhu3245CpzYJ3Q5Yn59V9wJENkn1wG2bYCnyQ/6fv083fboNke3G5GR0v6Y1yUiauv08YC5btfHG7r+NtfRup63D2BreoS7Zn/s4knQ3FE53WVfDlc8nSnB/pjEzL4Nsk8Ad5UUYV8OR4kYuh3UbY9uk8ztmT7GclVOopbFJAEiIiIiIiIiIiIiIiIiIqJOYh1PdERERERERERERERERERERNRJMEmAiIiIiIiIiIiIiIiIiIiok2CSABERERERERERERERERERUScRAOB9XxeCiIiIiIiIiIiIiIiIiIiIWtyu/wcTXv/sbDrNwQAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 98,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from IPython.display import Image\n",
+ "Image(filename='./graph.png')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 101,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 543
+ },
+ "id": "X_e8slJ4LO2z",
+ "outputId": "c36ff578-dd8b-4626-8e52-7096bc5f0cfb"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 101,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "g2d = g.layout_graphviz('circo')\n",
+ "g2d.plot()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "pDEzL2UlZGz_"
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/demos/demos_databases_apis/gremlin-tinkerpop/TitanDemo.ipynb b/demos/demos_databases_apis/gremlin-tinkerpop/TitanDemo.ipynb
index f54add017..411c1184f 100644
--- a/demos/demos_databases_apis/gremlin-tinkerpop/TitanDemo.ipynb
+++ b/demos/demos_databases_apis/gremlin-tinkerpop/TitanDemo.ipynb
@@ -4,7 +4,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# In this notebook, we demonstrate how to create and modify a Titan graph in python, and then visualize the result using Graphistry's visual graph explorer. "
+ "# PyGraphistry <> Titan graph\n",
+ "\n",
+ "In this notebook, we demonstrate how to create and modify a Titan graph in python, and then visualize the result using Graphistry's visual graph explorer. "
]
},
{
@@ -12,12 +14,12 @@
"metadata": {},
"source": [
"### We assume the gremlin server for our Titan graph is hosted locally on port 8182\n",
- " - This notebook utilizes the python modules aiogremlin and asyncio.\n",
- " - The GremlinClient class of aiogremlin communicates asynchronously with the gremlin server using websockets via asyncio coroutines.\n",
- " - This implementation allows you to submit additional requests to the server before any responses are recieved, which is much faster than synchronous request / response cycles. \n",
- " - For more information about these modules, please visit:\n",
- " - aiogremlin: http://aiogremlin.readthedocs.org/en/latest/index.html\n",
- " - asyncio: https://pypi.python.org/pypi/asyncio"
+ "- This notebook utilizes the python modules aiogremlin and asyncio.\n",
+ "- The GremlinClient class of aiogremlin communicates asynchronously with the gremlin server using websockets via asyncio coroutines.\n",
+ "- This implementation allows you to submit additional requests to the server before any responses are recieved, which is much faster than synchronous request / response cycles. \n",
+ "- For more information about these modules, please visit:\n",
+ " - aiogremlin: http://aiogremlin.readthedocs.org/en/latest/index.html\n",
+ " - asyncio: https://pypi.python.org/pypi/asyncio"
]
},
{
diff --git a/demos/demos_databases_apis/neo4j/contributed/Neo4jTwitter.ipynb b/demos/demos_databases_apis/neo4j/contributed/Neo4jTwitter.ipynb
index 6621db721..47c707a08 100644
--- a/demos/demos_databases_apis/neo4j/contributed/Neo4jTwitter.ipynb
+++ b/demos/demos_databases_apis/neo4j/contributed/Neo4jTwitter.ipynb
@@ -65,9 +65,7 @@
"source": [
"## Connect To Neo4j\n",
"\n",
- "If you haven't already, create an instance of the Russian Twitter Trolls sandbox on [Neo4j Sandbox.](https://neo4j.com/sandbox-v2/) We'll use the [Python driver for Neo4j](https://github.com/neo4j/neo4j-python-driver) to fetch data from Neo4j. To do this we'll need to instantiate a `Driver` object, passing in the credentials for our Neo4j instance. If using Neo4j Sandbox you can find the credentials for your Neo4j instance in the \"Details\" tab. Specifically we need the IP address, bolt port, username, and password. Bolt is the binary protocol used by the Neo4j drivers so a typical database URL string takes the form `bolt://:`\n",
- "\n",
- "![](./img/sandbox.png)"
+ "If you haven't already, create an instance of the Russian Twitter Trolls sandbox on [Neo4j Sandbox.](https://neo4j.com/sandbox-v2/) We'll use the [Python driver for Neo4j](https://github.com/neo4j/neo4j-python-driver) to fetch data from Neo4j. To do this we'll need to instantiate a `Driver` object, passing in the credentials for our Neo4j instance. If using Neo4j Sandbox you can find the credentials for your Neo4j instance in the \"Details\" tab. Specifically we need the IP address, bolt port, username, and password. Bolt is the binary protocol used by the Neo4j drivers so a typical database URL string takes the form `bolt://:`\n"
]
},
{
@@ -118,7 +116,6 @@
"source": [
"If we inspect the datamodel in Neo4j we can see that we have inormation about Tweets and specifically Users mentioned in tweets.\n",
"\n",
- "![](./img/datamodel.png)\n",
"\n",
"Let's use Graphistry to visualize User-User Tweet mention interactions. We'll do this by querying Neo4j for all tweets that mention users."
]
@@ -371,8 +368,6 @@
"source": [
"After running the above Python cell you should see an interactive Graphistry visualization like this:\n",
"\n",
- "![](./img/graphistry1.png)\n",
- "\n",
"Known Troll user nodes are colored red, regular users colored blue. By default, the size of the nodes is proportional to the degree of the node (number of relationships). We'll see in the next section how we can use graph algorithms such as PageRank and visualize the results of those algorithms in Graphistry."
]
},
@@ -549,8 +544,6 @@
"source": [
"Now when we render the Graphistry visualization, node size is proprtional to the node's PageRank score. This results in a different set of nodes that are identified as most important. \n",
"\n",
- "![](./img/graphistry2.png)\n",
- "\n",
"By binding node size to the results of graph algorithms we are able to draw insight from the data at a glance and further explore the interactive visualization.\n"
]
},
diff --git a/demos/demos_databases_apis/neptune/neptune_cypher_viz_using_bolt.ipynb b/demos/demos_databases_apis/neptune/neptune_cypher_viz_using_bolt.ipynb
index 49cf8e156..c9d6f3c1d 100755
--- a/demos/demos_databases_apis/neptune/neptune_cypher_viz_using_bolt.ipynb
+++ b/demos/demos_databases_apis/neptune/neptune_cypher_viz_using_bolt.ipynb
@@ -5,7 +5,7 @@
"id": "10436f61-3f82-4316-b9be-b6a70746d4f7",
"metadata": {},
"source": [
- "## Graphistry for Neptune using pygraphistry bolt connector \n",
+ "# Graphistry for Neptune using pygraphistry bolt connector \n",
"\n",
"#### This example uses pygraphistry bolt helper class to run queries against AWS Neptune and retrieve query results as graph, then the bolt helper function extracts all the nodes and edges into the dataframes automatically. Then visualize the resulting datasets using Graphistry. \n",
"\n"
diff --git a/demos/demos_databases_apis/neptune/neptune_tutorial.ipynb b/demos/demos_databases_apis/neptune/neptune_tutorial.ipynb
index 5a6ee1101..da07191e6 100644
--- a/demos/demos_databases_apis/neptune/neptune_tutorial.ipynb
+++ b/demos/demos_databases_apis/neptune/neptune_tutorial.ipynb
@@ -698,7 +698,7 @@
"id": "removed-blair",
"metadata": {},
"source": [
- "# Next steps\n",
+ "## Next steps\n",
"\n",
"* Go deeper with [PyGraphistry](https://github.com/graphistry/pygraphistry): Examples for customization, GPU graph analytics, and more\n",
"* Explore [gremlinpython](https://pypi.org/project/gremlinpython/)\n",
diff --git a/demos/demos_databases_apis/networkx/networkx.ipynb b/demos/demos_databases_apis/networkx/networkx.ipynb
index 0426751bf..510cc6413 100644
--- a/demos/demos_databases_apis/networkx/networkx.ipynb
+++ b/demos/demos_databases_apis/networkx/networkx.ipynb
@@ -1,5 +1,14 @@
{
"cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# NetworkX\n",
+ "\n",
+ "NetworkX is an early graph manipulation library with a variety of algorithms and layouts."
+ ]
+ },
{
"cell_type": "code",
"execution_count": 1,
@@ -14,7 +23,8 @@
"# graphistry.register(api=3, username='...', password='...', protocol='https', server='hub.graphistry.com')\n",
"# For more options, see https://github.com/graphistry/pygraphistry#configure\n",
"\n",
- "import networkx as nx"
+ "import networkx as nx\n",
+ "import pandas as pd"
]
},
{
@@ -49,7 +59,7 @@
}
],
"source": [
- "G=nx.Graph()\n",
+ "G = nx.Graph()\n",
"G.add_nodes_from([\n",
" (1, {\"v\": \"one\"}), \n",
" (2, {\"v\": \"two\"}), \n",
@@ -64,14 +74,26 @@
"graphistry.bind(source='src', destination='dst', node='nodeid').plot(G)"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When manipulating the graph, this form is even easier, as you can then use PyGraphistry methods for tasks like filtering, algorithmic enrichment, GFQL queries, etc:"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
- "source": []
+ "source": [
+ "g = graphistry.bind().from_networkx(G)\n",
+ "\n",
+ "assert isinstance(g._edges, pd.DataFrame)\n",
+ "assert isinstance(g._nodes, pd.DataFrame)\n",
+ "\n",
+ "g._edges"
+ ]
}
],
"metadata": {
@@ -90,7 +112,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.11"
+ "version": "3.9.16"
}
},
"nbformat": 4,
diff --git a/demos/demos_databases_apis/nodexl/official/nodexl_graphistry.ipynb b/demos/demos_databases_apis/nodexl/official/nodexl_graphistry.ipynb
index b11acdc56..f30220175 100644
--- a/demos/demos_databases_apis/nodexl/official/nodexl_graphistry.ipynb
+++ b/demos/demos_databases_apis/nodexl/official/nodexl_graphistry.ipynb
@@ -43,7 +43,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -56,7 +56,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -80,7 +80,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -95,7 +95,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -115,12 +115,12 @@
"id": "jK9AXFTjAyDD"
},
"source": [
- "# Sample use"
+ "## Sample use"
]
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -134,7 +134,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -148,7 +148,7 @@
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -166,7 +166,7 @@
"id": "a7erX6jnKhHj"
},
"source": [
- "# Twitter Demos"
+ "### Twitter Demos"
]
},
{
@@ -176,12 +176,12 @@
"id": "uGuj40xkxtMh"
},
"source": [
- "## Debate Warren"
+ "### Debate Warren"
]
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -199,12 +199,12 @@
"id": "UrnlAwkryE10"
},
"source": [
- "## CES Samsung"
+ "### CES Samsung"
]
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -222,12 +222,12 @@
"id": "2a4TOajvC4sb"
},
"source": [
- "## Larger Graph"
+ "### Larger Graph"
]
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
@@ -245,7 +245,7 @@
"id": "wY9KhWEgDHzn"
},
"source": [
- "# MediaWiki Demos"
+ "## MediaWiki Demos"
]
},
{
@@ -255,12 +255,12 @@
"id": "t4Im6padK7Ze"
},
"source": [
- "## Demo 1"
+ "### Demo 1"
]
},
{
"cell_type": "code",
- "execution_count": 0,
+ "execution_count": null,
"metadata": {
"colab": {},
"colab_type": "code",
diff --git a/demos/demos_databases_apis/splunk/splunk_demo_public.ipynb b/demos/demos_databases_apis/splunk/splunk_demo_public.ipynb
index b6d84abee..281bb88f0 100644
--- a/demos/demos_databases_apis/splunk/splunk_demo_public.ipynb
+++ b/demos/demos_databases_apis/splunk/splunk_demo_public.ipynb
@@ -151,34 +151,10 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "colab": {
- "base_uri": "https://localhost:8080/",
- "height": 188
- },
- "colab_type": "code",
- "id": "XPK5n5Yrvjb5",
- "outputId": "04e436c6-5a8b-4148-cd31-874421e6967e"
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Collecting splunk-sdk\n",
- "\u001b[?25l Downloading https://files.pythonhosted.org/packages/d4/bb/408c504f4307fcf4a89909cc85bc912d8529c9ca88200682f94a31a06186/splunk-sdk-1.6.5.tar.gz (103kB)\n",
- "\u001b[K 100% |████████████████████████████████| 112kB 2.6MB/s \n",
- "\u001b[?25hBuilding wheels for collected packages: splunk-sdk\n",
- " Running setup.py bdist_wheel for splunk-sdk ... \u001b[?25l-\b \bdone\n",
- "\u001b[?25h Stored in directory: /root/.cache/pip/wheels/87/83/8f/5f78fbc79322715add8f39ba8adc97511f27297852eb4dc270\n",
- "Successfully built splunk-sdk\n",
- "Installing collected packages: splunk-sdk\n",
- "Successfully installed splunk-sdk-1.6.5\n"
- ]
- }
- ],
+ "metadata": {},
+ "outputs": [],
"source": [
- "!pip install splunk-sdk\n",
+ "# !pip install splunk-sdk\n",
"\n",
"import splunklib"
]
diff --git a/demos/demos_databases_apis/sql/postgres.ipynb b/demos/demos_databases_apis/sql/postgres.ipynb
index 1422625d8..219e1a101 100644
--- a/demos/demos_databases_apis/sql/postgres.ipynb
+++ b/demos/demos_databases_apis/sql/postgres.ipynb
@@ -13,10 +13,9 @@
"* Shows several viz modes + a convenience function for sql->interactive viz\n",
"* Try: Modify the indicated lines to change to visualize any other table\n",
"\n",
- "Further docs\n",
+ "Further reading:\n",
" - [UI Guide](https://hub.graphistry.com/docs/ui/index/)\n",
- " - [More demos: database connectors, ...](/notebook/tree/demos/demos_databases_apis)\n",
- " - [CSV upload notebook app](/notebook/tree/demos/upload_csv_miniapp.ipynb)"
+ " - [CSV upload notebook app](../../upload_csv_miniapp.ipynb)"
]
},
{
@@ -357,8 +356,7 @@
"source": [
"## Further docs\n",
" - [UI Guide](https://hub.graphistry.com/docs/ui/index/)\n",
- " - [More demos: database connectors, ...](/notebook/tree/demos/demos_databases_apis)\n",
- " - [CSV upload notebook app](/notebook/tree/demos/upload_csv_miniapp.ipynb)"
+ " - [CSV upload notebook app](../..//upload_csv_miniapp.ipynb)"
]
},
{
diff --git a/demos/demos_databases_apis/tigergraph/fraud_raw_REST_calls.ipynb b/demos/demos_databases_apis/tigergraph/fraud_raw_REST_calls.ipynb
index dce85185a..c067b6563 100644
--- a/demos/demos_databases_apis/tigergraph/fraud_raw_REST_calls.ipynb
+++ b/demos/demos_databases_apis/tigergraph/fraud_raw_REST_calls.ipynb
@@ -104,7 +104,7 @@
"id": "LUEA1fmFOjCD"
},
"source": [
- "# 1. Fraud"
+ "## 1. Fraud"
]
},
{
@@ -114,7 +114,7 @@
"id": "rY8Ip6WcOnPl"
},
"source": [
- "## 1.a circleDetection"
+ "### 1.a circleDetection"
]
},
{
@@ -152,7 +152,7 @@
"id": "mXT2bD2UOp3o"
},
"source": [
- "## 1.b fraudConnectivity"
+ "### 1.b fraudConnectivity"
]
},
{
@@ -190,7 +190,7 @@
"id": "SKepDGbKZLGI"
},
"source": [
- "## Combined"
+ "### Combined"
]
},
{
diff --git a/demos/for_analysis.ipynb b/demos/for_analysis.ipynb
index adf548691..675722246 100644
--- a/demos/for_analysis.ipynb
+++ b/demos/for_analysis.ipynb
@@ -14,10 +14,10 @@
"3. Advanced plotting\n",
"4. Further reading\n",
" - [PyGraphistry](https://github.com/graphistry/pygraphistry)\n",
- " - [PyGraphistry demos: database connectors, ...](demos_databases_apis)\n",
+ " - [PyGraphistry demos: database connectors, ...](https://github.com/graphistry/pygraphistry/tree/master/demos/demos_databases_apis)\n",
" - [graph-app-kit: Streamlit graph dashboarding](https://github.com/graphistry/graph-app-kit)\n",
" - [UI Guide](https://hub.graphistry.com/docs/ui/index/)\n",
- " - [CSV upload notebook app](upload_csv_miniapp.ipynb)\n",
+ " - [CSV upload notebook app](https://github.com/graphistry/pygraphistry/tree/master/demos/upload_csv_miniapp.ipynb)\n",
" \n",
"## 1. Register\n"
]
@@ -896,10 +896,10 @@
"source": [
"## Further reading:\n",
" - [PyGraphistry](https://github.com/graphistry/pygraphistry)\n",
- " - [PyGraphistry demos: database connectors, ...](demos_databases_apis)\n",
+ " - [PyGraphistry demos: database connectors, ...](https://github.com/graphistry/pygraphistry/demos/demos_databases_apis)\n",
" - [graph-app-kit: Streamlit graph dashboarding](https://github.com/graphistry/graph-app-kit)\n",
" - [UI Guide](https://hub.graphistry.com/docs/ui/index/)\n",
- " - [CSV upload notebook app](upload_csv_miniapp.ipynb)"
+ " - [CSV upload notebook app](https://github.com/graphistry/pygraphistry/demos/upload_csv_miniapp.ipynb)"
]
}
],
diff --git a/demos/for_developers.ipynb b/demos/for_developers.ipynb
index 8d2187a10..25d07d1b7 100644
--- a/demos/for_developers.ipynb
+++ b/demos/for_developers.ipynb
@@ -7,7 +7,7 @@
"# Tutorial: Graphistry for Developers\n",
"\n",
"\n",
- "**Start by generating interactive graphs in the [Analysis tutorial](for_analysis.ipynb)**\n",
+ "**Start by generating interactive graphs in the [Analysis tutorial](https://github.com/graphistry/pygraphistry/demos/for_analysis.ipynb)**\n",
"\n",
"\n",
"**Graphistry is a client/server system:**\n",
@@ -48,7 +48,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# 1. Backend APIs\n",
+ "## 1. Backend APIs\n",
"\n",
"Graphistry provides a REST upload API, and you can reuse the Python client for more conveniently using it."
]
@@ -57,8 +57,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Python\n",
- "* Use the PyGraphistry API as in the [Analysis tutorial](for_analysis.ipynb)\n",
+ "### Python\n",
+ "* Use the PyGraphistry API as in the [Analysis tutorial](https://github.com/graphistry/pygraphistry/demos/for_analysis.ipynb)\n",
"* Instead of plotting, get the plot URL for embedding\n"
]
},
@@ -173,7 +173,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## iframe"
+ "### iframe"
]
},
{
@@ -207,7 +207,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## JavaScript - Browser vanilla JS\n",
+ "### JavaScript - Browser vanilla JS\n",
"* [npm](https://www.npmjs.com/package/@graphistry/client-api)\n",
"* `npm install --save \"@graphistry/client-api\"`\n",
"* See [vanilla js examples](https://hub.graphistry.com/static/js-docs/examples/toggles.html)]\n",
diff --git a/demos/gfql/benchmark_hops_cpu_gpu.ipynb b/demos/gfql/benchmark_hops_cpu_gpu.ipynb
index bf17b630e..cafd90815 100644
--- a/demos/gfql/benchmark_hops_cpu_gpu.ipynb
+++ b/demos/gfql/benchmark_hops_cpu_gpu.ipynb
@@ -1,23 +1,10 @@
{
- "nbformat": 4,
- "nbformat_minor": 0,
- "metadata": {
- "colab": {
- "provenance": [],
- "gpuType": "T4"
- },
- "kernelspec": {
- "name": "python3",
- "display_name": "Python 3"
- },
- "language_info": {
- "name": "python"
- },
- "accelerator": "GPU"
- },
"cells": [
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "GZxoiU8sQDk_"
+ },
"source": [
"# GFQL CPU, GPU Benchmark\n",
"\n",
@@ -73,33 +60,27 @@
"| **Orkut** | N/A | N/A | 41.50 | N/A | 711.4 |\n",
"| **AVG** | 22X | 0.41 | 14.4 | 41.1 | 246.8\n",
"| **MAX** | 42X | 0.50 | 41.50 | 50.2 | 711.4\n"
- ],
- "metadata": {
- "id": "GZxoiU8sQDk_"
- }
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "## Optional: GPU setup - Google Colab"
- ],
"metadata": {
"id": "SAj8lhREEOwS"
- }
+ },
+ "source": [
+ "## Optional: GPU setup - Google Colab"
+ ]
},
{
"cell_type": "markdown",
- "source": [],
"metadata": {
"id": "4hrEEAAm7DTO"
- }
+ },
+ "source": []
},
{
"cell_type": "code",
- "source": [
- "# Report GPU used when GPU benchmarking\n",
- "! nvidia-smi"
- ],
+ "execution_count": 1,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -107,11 +88,10 @@
"id": "W2MF6ZsjDv3B",
"outputId": "46088cbc-2db9-4529-f724-dc57ed85dfb7"
},
- "execution_count": 1,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:50:30 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -135,27 +115,28 @@
"+---------------------------------------------------------------------------------------+\n"
]
}
+ ],
+ "source": [
+ "# Report GPU used when GPU benchmarking\n",
+ "# ! nvidia-smi"
]
},
{
"cell_type": "code",
- "source": [
- "# if in google colab\n",
- "!git clone https://github.com/rapidsai/rapidsai-csp-utils.git\n",
- "!python rapidsai-csp-utils/colab/pip-install.py"
- ],
+ "execution_count": 8,
"metadata": {
"id": "Aikh0x4ID_wK"
},
- "execution_count": 8,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "# if in google colab\n",
+ "#!git clone https://github.com/rapidsai/rapidsai-csp-utils.git\n",
+ "#!python rapidsai-csp-utils/colab/pip-install.py"
+ ]
},
{
"cell_type": "code",
- "source": [
- "import cudf\n",
- "cudf.__version__"
- ],
+ "execution_count": 3,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -164,160 +145,155 @@
"id": "Lwekdei1dH3N",
"outputId": "71f5b01d-7917-4283-8338-969167d6e1e8"
},
- "execution_count": 3,
"outputs": [
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- "'23.12.01'"
- ],
"application/vnd.google.colaboratory.intrinsic+json": {
"type": "string"
- }
+ },
+ "text/plain": [
+ "'23.12.01'"
+ ]
},
+ "execution_count": 3,
"metadata": {},
- "execution_count": 3
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "import cudf\n",
+ "cudf.__version__"
]
},
{
"cell_type": "markdown",
- "source": [
- "# 1. Install & configure"
- ],
"metadata": {
"id": "QQpsrtwBT7sa"
- }
+ },
+ "source": [
+ "## 1. Install & configure"
+ ]
},
{
"cell_type": "code",
- "source": [
- "#! pip install graphistry[igraph]\n",
- "\n",
- "!pip install -q igraph\n",
- "#!pip install -q git+https://github.com/graphistry/pygraphistry.git@dev/cugfql\n",
- "!pip install -q graphistry\n"
- ],
+ "execution_count": 2,
"metadata": {
- "id": "cYjRbgkU9Sx8",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "cYjRbgkU9Sx8",
"outputId": "2cf25531-9b8b-4715-ccc7-e79094d84ebd"
},
- "execution_count": 2,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
" Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n"
]
}
+ ],
+ "source": [
+ "#! pip install graphistry[igraph]"
]
},
{
"cell_type": "markdown",
- "source": [
- "## Imports"
- ],
"metadata": {
"id": "Ff6Tt9DhkePl"
- }
+ },
+ "source": [
+ "### Imports"
+ ]
},
{
"cell_type": "code",
- "source": [
- "import pandas as pd\n",
- "\n",
- "import graphistry\n",
- "\n",
- "from graphistry import (\n",
- "\n",
- " # graph operators\n",
- " n, e_undirected, e_forward, e_reverse,\n",
- "\n",
- " # attribute predicates\n",
- " is_in, ge, startswith, contains, match as match_re\n",
- ")\n",
- "graphistry.__version__"
- ],
+ "execution_count": 3,
"metadata": {
- "id": "S5_y0CbLkjft",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 35
},
+ "id": "S5_y0CbLkjft",
"outputId": "a68a9c4b-c9c5-4b8b-ea4f-7bf1e4ddf315"
},
- "execution_count": 3,
"outputs": [
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- "'0.32.0+12.g72e778c'"
- ],
"application/vnd.google.colaboratory.intrinsic+json": {
"type": "string"
- }
+ },
+ "text/plain": [
+ "'0.32.0+12.g72e778c'"
+ ]
},
+ "execution_count": 3,
"metadata": {},
- "execution_count": 3
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "import graphistry\n",
+ "\n",
+ "from graphistry import (\n",
+ "\n",
+ " # graph operators\n",
+ " n, e_undirected, e_forward, e_reverse,\n",
+ "\n",
+ " # attribute predicates\n",
+ " is_in, ge, startswith, contains, match as match_re\n",
+ ")\n",
+ "graphistry.__version__"
]
},
{
"cell_type": "code",
- "source": [
- "import cudf"
- ],
+ "execution_count": 6,
"metadata": {
"id": "I7Fg75jsG4co"
},
- "execution_count": 6,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "import cudf"
+ ]
},
{
"cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "id": "uLZKph2-a5M4"
+ },
+ "outputs": [],
"source": [
"#work around google colab shell encoding bugs\n",
"\n",
"import locale\n",
"locale.getpreferredencoding = lambda: \"UTF-8\""
- ],
- "metadata": {
- "id": "uLZKph2-a5M4"
- },
- "execution_count": 7,
- "outputs": []
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "# 2. Perf benchmarks"
- ],
"metadata": {
"id": "eU9SyauNUHtR"
- }
+ },
+ "source": [
+ "## 2. Perf benchmarks"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "### Facebook: 88K edges"
- ],
"metadata": {
"id": "NA0Ym11fkB8j"
- }
+ },
+ "source": [
+ "### Facebook: 88K edges"
+ ]
},
{
"cell_type": "code",
- "source": [
- "df = pd.read_csv('https://raw.githubusercontent.com/graphistry/pygraphistry/master/demos/data/facebook_combined.txt', sep=' ', names=['s', 'd'])\n",
- "print(df.shape)\n",
- "df.head(5)"
- ],
+ "execution_count": 10,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -326,26 +302,16 @@
"id": "vXuQogHekClJ",
"outputId": "64db92c0-2704-438b-d0e4-25865acbb5e9"
},
- "execution_count": 10,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(88234, 2)\n"
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " s d\n",
- "0 0 1\n",
- "1 0 2\n",
- "2 0 3\n",
- "3 0 4\n",
- "4 0 5"
- ],
"text/html": [
"\n",
" \n",
@@ -608,20 +574,30 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " s d\n",
+ "0 0 1\n",
+ "1 0 2\n",
+ "2 0 3\n",
+ "3 0 4\n",
+ "4 0 5"
]
},
+ "execution_count": 10,
"metadata": {},
- "execution_count": 10
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "df = pd.read_csv('https://raw.githubusercontent.com/graphistry/pygraphistry/master/demos/data/facebook_combined.txt', sep=' ', names=['s', 'd'])\n",
+ "print(df.shape)\n",
+ "df.head(5)"
]
},
{
"cell_type": "code",
- "source": [
- "fg = graphistry.edges(df, 's', 'd').materialize_nodes()\n",
- "print(fg._nodes.shape, fg._edges.shape)\n",
- "fg._nodes.head(5)"
- ],
+ "execution_count": 11,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -630,26 +606,16 @@
"id": "jEma7hvvkzkN",
"outputId": "dbf21342-6b80-429c-bd3f-b1494c6854c7"
},
- "execution_count": 11,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(4039, 1) (88234, 2)\n"
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " id\n",
- "0 0\n",
- "1 1\n",
- "2 2\n",
- "3 3\n",
- "4 4"
- ],
"text/html": [
"\n",
" \n",
@@ -906,20 +872,30 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " id\n",
+ "0 0\n",
+ "1 1\n",
+ "2 2\n",
+ "3 3\n",
+ "4 4"
]
},
+ "execution_count": 11,
"metadata": {},
- "execution_count": 11
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "fg = graphistry.edges(df, 's', 'd').materialize_nodes()\n",
+ "print(fg._nodes.shape, fg._edges.shape)\n",
+ "fg._nodes.head(5)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(100):\n",
- " fg2 = fg.chain([n({'id': 0}), e_forward(hops=2)])"
- ],
+ "execution_count": 12,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -927,30 +903,25 @@
"id": "5lEdCBw9lzd7",
"outputId": "ed7451e0-401e-4edc-c8de-79c5afd0c95b"
},
- "execution_count": 12,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 13.6 s, sys: 2.08 s, total: 15.7 s\n",
"Wall time: 18 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "for i in range(100):\n",
+ " fg2 = fg.chain([n({'id': 0}), e_forward(hops=2)])"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "for i in range(100):\n",
- " fg2 = fg_gdf.chain([n({'id': 0}), e_forward(hops=2)])\n",
- "print(fg._nodes.shape, fg._edges.shape)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)\n",
- "del fg_gdf\n",
- "del fg2"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -958,11 +929,10 @@
"id": "JFKIBa8mJCvJ",
"outputId": "c22022f0-b33d-483a-db64-29992c5161e8"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(4039, 1) (88234, 2)\n",
"(1519, 1) (4060, 2)\n",
@@ -970,17 +940,21 @@
"Wall time: 11.9 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(100):\n",
+ " fg2 = fg_gdf.chain([n({'id': 0}), e_forward(hops=2)])\n",
+ "print(fg._nodes.shape, fg._edges.shape)\n",
+ "print(fg2._nodes.shape, fg2._edges.shape)\n",
+ "del fg_gdf\n",
+ "del fg2"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(50):\n",
- " fg2 = fg.chain([n({'id': 0}), e_forward(hops=5)])\n",
- "print(fg._nodes.shape, fg._edges.shape)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -988,11 +962,10 @@
"id": "-KBGLexek5tS",
"outputId": "2f462e6c-578a-4fa1-ec29-91bae753f4c5"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(4039, 1) (88234, 2)\n",
"(3829, 1) (86074, 2)\n",
@@ -1000,20 +973,18 @@
"Wall time: 16.2 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
"for i in range(50):\n",
- " fg2 = fg_gdf.chain([n({'id': 0}), e_forward(hops=5)])\n",
+ " fg2 = fg.chain([n({'id': 0}), e_forward(hops=5)])\n",
"print(fg._nodes.shape, fg._edges.shape)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)\n",
- "del fg_gdf\n",
- "del fg2"
- ],
+ "print(fg2._nodes.shape, fg2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1021,11 +992,10 @@
"id": "CVpcbhpdHFEF",
"outputId": "aba04ee1-781e-4226-b593-b42415a55fc4"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(4039, 1) (88234, 2)\n",
"(3829, 1) (86074, 2)\n",
@@ -1033,47 +1003,47 @@
"Wall time: 10.1 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(50):\n",
+ " fg2 = fg_gdf.chain([n({'id': 0}), e_forward(hops=5)])\n",
+ "print(fg._nodes.shape, fg._edges.shape)\n",
+ "print(fg2._nodes.shape, fg2._edges.shape)\n",
+ "del fg_gdf\n",
+ "del fg2"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(100):\n",
- " fg2 = fg.chain([e_forward(source_node_match={'id': 0}, hops=5)])"
- ],
+ "execution_count": null,
"metadata": {
- "id": "1cFIyJF9pLjE",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "1cFIyJF9pLjE",
"outputId": "107329af-8e4b-428c-8b03-77ed00bdf5bf"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 11.8 s, sys: 377 ms, total: 12.1 s\n",
"Wall time: 13.1 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "for i in range(100):\n",
+ " fg2 = fg.chain([e_forward(source_node_match={'id': 0}, hops=5)])"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "for i in range(100):\n",
- " fg2 = fg_gdf.chain([e_forward(source_node_match={'id': 0}, hops=5)])\n",
- "print(fg._nodes.shape, fg._edges.shape)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)\n",
- "del fg_gdf\n",
- "del fg2\n",
- "\n"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1081,11 +1051,10 @@
"id": "M5uRiD6uJVNW",
"outputId": "5e938a19-2992-4280-80c2-784382d40113"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(4039, 1) (88234, 2)\n",
"(348, 1) (347, 2)\n",
@@ -1093,20 +1062,22 @@
"Wall time: 14.2 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "fg_gdf = fg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(100):\n",
+ " fg2 = fg_gdf.chain([e_forward(source_node_match={'id': 0}, hops=5)])\n",
+ "print(fg._nodes.shape, fg._edges.shape)\n",
+ "print(fg2._nodes.shape, fg2._edges.shape)\n",
+ "del fg_gdf\n",
+ "del fg2\n",
+ "\n"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "start_nodes = pd.DataFrame({fg._node: [0]})\n",
- "for i in range(100):\n",
- " fg2 = fg.hop(\n",
- " nodes=start_nodes,\n",
- " direction='forward',\n",
- " hops=2)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)"
- ],
+ "execution_count": 17,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1114,35 +1085,31 @@
"id": "Y9vgzfT69x41",
"outputId": "6882c1ce-0df8-4087-dda4-0a105a8617e1"
},
- "execution_count": 17,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(1519, 1) (4060, 2)\n",
"CPU times: user 4.5 s, sys: 1.35 s, total: 5.85 s\n",
"Wall time: 6.09 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({fg._node: [0]})\n",
- "fg_gdf = fg.nodes(cudf.from_pandas(fg._nodes)).edges(cudf.from_pandas(fg._edges))\n",
+ "start_nodes = pd.DataFrame({fg._node: [0]})\n",
"for i in range(100):\n",
- " fg2 = fg_gdf.hop(\n",
+ " fg2 = fg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=2)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)\n",
- "del start_nodes\n",
- "del fg_gdf\n",
- "del fg2"
- ],
+ "print(fg2._nodes.shape, fg2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1150,31 +1117,35 @@
"id": "c7ybJqjc-T31",
"outputId": "37ccc1fb-6460-4193-8aa7-22837ff06d0a"
},
- "execution_count": 18,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(1519, 1) (4060, 2)\n",
"CPU times: user 2.58 s, sys: 6.75 ms, total: 2.59 s\n",
"Wall time: 2.58 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({fg._node: [0]})\n",
+ "start_nodes = cudf.DataFrame({fg._node: [0]})\n",
+ "fg_gdf = fg.nodes(cudf.from_pandas(fg._nodes)).edges(cudf.from_pandas(fg._edges))\n",
"for i in range(100):\n",
- " fg2 = fg.hop(\n",
+ " fg2 = fg_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=5)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)"
- ],
+ " hops=2)\n",
+ "print(fg2._nodes.shape, fg2._edges.shape)\n",
+ "del start_nodes\n",
+ "del fg_gdf\n",
+ "del fg2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1182,35 +1153,31 @@
"id": "Dy7a4zDZ-7_G",
"outputId": "077b5d9c-c9ae-411a-8228-3c026b07a910"
},
- "execution_count": 19,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(3829, 1) (86074, 2)\n",
"CPU times: user 13.2 s, sys: 2 s, total: 15.2 s\n",
"Wall time: 18.3 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({fg._node: [0]})\n",
- "fg_gdf = fg.nodes(cudf.from_pandas(fg._nodes)).edges(cudf.from_pandas(fg._edges))\n",
+ "start_nodes = pd.DataFrame({fg._node: [0]})\n",
"for i in range(100):\n",
- " fg2 = fg_gdf.hop(\n",
+ " fg2 = fg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=5)\n",
- "print(fg2._nodes.shape, fg2._edges.shape)\n",
- "del start_nodes\n",
- "del fg_gdf\n",
- "del fg2"
- ],
+ "print(fg2._nodes.shape, fg2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1218,49 +1185,58 @@
"id": "N5aUtF1a--ML",
"outputId": "0c2b67b8-fac6-45b3-dfbe-8002b5506e91"
},
- "execution_count": 20,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(3829, 1) (86074, 2)\n",
"CPU times: user 5.72 s, sys: 159 ms, total: 5.88 s\n",
"Wall time: 5.86 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "start_nodes = cudf.DataFrame({fg._node: [0]})\n",
+ "fg_gdf = fg.nodes(cudf.from_pandas(fg._nodes)).edges(cudf.from_pandas(fg._edges))\n",
+ "for i in range(100):\n",
+ " fg2 = fg_gdf.hop(\n",
+ " nodes=start_nodes,\n",
+ " direction='forward',\n",
+ " hops=5)\n",
+ "print(fg2._nodes.shape, fg2._edges.shape)\n",
+ "del start_nodes\n",
+ "del fg_gdf\n",
+ "del fg2"
]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "KrJKjXy2KLos"
+ },
"source": [
"## Twitter\n",
"\n",
"- edges: 2420766\n",
"- nodes: 81306"
- ],
- "metadata": {
- "id": "KrJKjXy2KLos"
- }
+ ]
},
{
"cell_type": "code",
- "source": [
- "! wget 'https://snap.stanford.edu/data/twitter_combined.txt.gz'\n",
- "#! curl -L 'https://snap.stanford.edu/data/twitter_combined.txt.gz' -o twitter_combined.txt.gz"
- ],
+ "execution_count": 21,
"metadata": {
- "id": "fO2qasGqpubr",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "fO2qasGqpubr",
"outputId": "d41a110e-9f7c-4710-9ce3-3f4906ab02ae"
},
- "execution_count": 21,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"--2023-12-25 21:58:27-- https://snap.stanford.edu/data/twitter_combined.txt.gz\n",
"Resolving snap.stanford.edu (snap.stanford.edu)... 171.64.75.80\n",
@@ -1275,24 +1251,25 @@
"\n"
]
}
+ ],
+ "source": [
+ "#! wget 'https://snap.stanford.edu/data/twitter_combined.txt.gz'"
]
},
{
"cell_type": "code",
- "source": [
- "! gunzip twitter_combined.txt.gz"
- ],
+ "execution_count": 22,
"metadata": {
"id": "fn7zeA3SGlEo"
},
- "execution_count": 22,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "#! gunzip twitter_combined.txt.gz"
+ ]
},
{
"cell_type": "code",
- "source": [
- "! head -n 5 twitter_combined.txt"
- ],
+ "execution_count": 24,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1300,11 +1277,10 @@
"id": "68TAZkhLGz9g",
"outputId": "8ba7c23d-267f-4b59-d6c6-b3f66caec9cf"
},
- "execution_count": 24,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"214328887 34428380\n",
"17116707 28465635\n",
@@ -1313,15 +1289,14 @@
"107830991 17868918\n"
]
}
+ ],
+ "source": [
+ "#! head -n 5 twitter_combined.txt"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "te_df = pd.read_csv('twitter_combined.txt', sep=' ', names=['s', 'd'])\n",
- "te_df.shape"
- ],
+ "execution_count": 25,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1329,46 +1304,46 @@
"id": "QU2wNeGXG2GC",
"outputId": "349ac9c0-6f6c-4ce6-fec0-8bae75fca635"
},
- "execution_count": 25,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 474 ms, sys: 61.9 ms, total: 536 ms\n",
"Wall time: 534 ms\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"(2420766, 2)"
]
},
+ "execution_count": 25,
"metadata": {},
- "execution_count": 25
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "te_df = pd.read_csv('twitter_combined.txt', sep=' ', names=['s', 'd'])\n",
+ "te_df.shape"
]
},
{
"cell_type": "code",
- "source": [
- "import graphistry"
- ],
+ "execution_count": 26,
"metadata": {
"id": "EK5gQH2iG5UU"
},
- "execution_count": 26,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "import graphistry"
+ ]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "g = graphistry.edges(te_df, 's', 'd').materialize_nodes()\n",
- "g._nodes.shape"
- ],
+ "execution_count": 27,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1376,36 +1351,35 @@
"id": "ZtIW-eFGG_R4",
"outputId": "0686e9b3-b684-4b93-da03-289244394338"
},
- "execution_count": 27,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 86.4 ms, sys: 106 ms, total: 193 ms\n",
"Wall time: 191 ms\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"(81306, 1)"
]
},
+ "execution_count": 27,
"metadata": {},
- "execution_count": 27
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "g = graphistry.edges(te_df, 's', 'd').materialize_nodes()\n",
+ "g._nodes.shape"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(10):\n",
- " g2 = g.chain([n({'id': 17116707}), e_forward(hops=1)])\n",
- "g2._nodes.shape, g2._edges.shape"
- ],
+ "execution_count": 29,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1413,39 +1387,36 @@
"id": "yUaRfw4FHGMb",
"outputId": "3945cc5a-c36c-451b-ac95-8af992a3546f"
},
- "execution_count": 29,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 11.8 s, sys: 8.4 s, total: 20.2 s\n",
"Wall time: 23 s\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"((140, 1), (615, 2))"
]
},
+ "execution_count": 29,
"metadata": {},
- "execution_count": 29
+ "output_type": "execute_result"
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
"for i in range(10):\n",
- " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=1)])._nodes\n",
- "print(out.shape)\n",
- "del g_gdf\n",
- "del out"
- ],
+ " g2 = g.chain([n({'id': 17116707}), e_forward(hops=1)])\n",
+ "g2._nodes.shape, g2._edges.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1453,27 +1424,30 @@
"id": "5hM4NBu2_eks",
"outputId": "54505262-4871-44ee-e5e4-ad7ab32c13c2"
},
- "execution_count": 30,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(140, 1)\n",
"CPU times: user 1.33 s, sys: 46.6 ms, total: 1.38 s\n",
"Wall time: 1.63 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(10):\n",
+ " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=1)])._nodes\n",
+ "print(out.shape)\n",
+ "del g_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(10):\n",
- " out = g.chain([n({'id': 17116707}), e_forward(hops=2)])\n",
- "print(out._nodes.shape, out._edges.shape)"
- ],
+ "execution_count": 31,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1481,30 +1455,27 @@
"id": "m2-MxD5lHX6u",
"outputId": "e89b9d4b-6c04-45c7-9e7f-cbdbbe0a4730"
},
- "execution_count": 31,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(2345, 1) (68536, 2)\n",
"CPU times: user 13.3 s, sys: 8.05 s, total: 21.4 s\n",
"Wall time: 21.6 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "for i in range(10):\n",
+ " out = g.chain([n({'id': 17116707}), e_forward(hops=2)])\n",
+ "print(out._nodes.shape, out._edges.shape)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "for i in range(10):\n",
- " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=2)])._nodes\n",
- "print(out.shape)\n",
- "del g_gdf\n",
- "del out"
- ],
+ "execution_count": 36,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1512,27 +1483,30 @@
"id": "7EQSRbIqLaGw",
"outputId": "60c00a03-9e7b-46b5-fce3-f4f567a09430"
},
- "execution_count": 36,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(2345, 1)\n",
"CPU times: user 1.67 s, sys: 55.8 ms, total: 1.72 s\n",
"Wall time: 1.75 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(10):\n",
+ " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=2)])._nodes\n",
+ "print(out.shape)\n",
+ "del g_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "for i in range(10):\n",
- " out = g.chain([n({'id': 17116707}), e_forward(hops=8)])\n",
- "print(out._nodes.shape, out._edges.shape)"
- ],
+ "execution_count": 37,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1540,30 +1514,27 @@
"id": "hh6WnjI3ITpB",
"outputId": "33138efe-a581-49ed-b2b4-247f8e9bdc09"
},
- "execution_count": 37,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(81304, 1) (2417796, 2)\n",
"CPU times: user 1min 56s, sys: 17.1 s, total: 2min 13s\n",
"Wall time: 2min 22s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "for i in range(10):\n",
+ " out = g.chain([n({'id': 17116707}), e_forward(hops=8)])\n",
+ "print(out._nodes.shape, out._edges.shape)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "for i in range(10):\n",
- " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=8)])._nodes\n",
- "print(out.shape)\n",
- "del g_gdf\n",
- "del out"
- ],
+ "execution_count": 38,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1571,31 +1542,30 @@
"id": "7jFFVUenM87j",
"outputId": "2cceb720-9de3-488e-8b74-b820fd06e6c1"
},
- "execution_count": 38,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(81304, 1)\n",
"CPU times: user 5.3 s, sys: 1.48 s, total: 6.78 s\n",
"Wall time: 7.89 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "g_gdf = g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "for i in range(10):\n",
+ " out = g_gdf.chain([n({'id': 17116707}), e_forward(hops=8)])._nodes\n",
+ "print(out.shape)\n",
+ "del g_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
- "for i in range(10):\n",
- " g2 = g.hop(\n",
- " nodes=start_nodes,\n",
- " direction='forward',\n",
- " hops=1)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ "execution_count": 39,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1603,35 +1573,31 @@
"id": "_5LD0bZB_lU4",
"outputId": "bc31bd03-e79f-46d2-ea8f-3b01d9ef39a2"
},
- "execution_count": 39,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(0, 1) (0, 2)\n",
"CPU times: user 2.58 s, sys: 1.59 s, total: 4.17 s\n",
"Wall time: 6.02 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
- "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
+ "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
"for i in range(10):\n",
- " g2 = g_gdf.hop(\n",
+ " g2 = g.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=5)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del g_gdf\n",
- "del g2"
- ],
+ " hops=1)\n",
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1639,31 +1605,35 @@
"id": "M_rHjqtvACQw",
"outputId": "8d3e308e-b1e2-452b-f402-573be0dd5b58"
},
- "execution_count": 44,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(61827, 1) (1473599, 2)\n",
"CPU times: user 822 ms, sys: 179 ms, total: 1 s\n",
"Wall time: 997 ms\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
+ "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
+ "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
"for i in range(10):\n",
- " g2 = g.hop(\n",
+ " g2 = g_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=2)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=5)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del g_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1671,35 +1641,31 @@
"id": "0zEIucaCAbj_",
"outputId": "83e64b0f-2b3a-4e4b-d189-3e6a8ef78f53"
},
- "execution_count": 40,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(2345, 1) (68536, 2)\n",
"CPU times: user 8.93 s, sys: 5.92 s, total: 14.9 s\n",
"Wall time: 15.8 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
- "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
+ "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
"for i in range(10):\n",
- " g2 = g_gdf.hop(\n",
+ " g2 = g.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=2)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del g_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1707,31 +1673,35 @@
"id": "LKJh5gRtAdIj",
"outputId": "e3c7883d-74c0-4d55-b238-88457296c6bc"
},
- "execution_count": 41,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(2345, 1) (68536, 2)\n",
"CPU times: user 374 ms, sys: 6.92 ms, total: 381 ms\n",
"Wall time: 379 ms\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
+ "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
+ "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
"for i in range(10):\n",
- " g2 = g.hop(\n",
+ " g2 = g_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=8)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=2)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del g_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1739,35 +1709,31 @@
"id": "JZwxdofNAfmb",
"outputId": "2731be4c-75d9-47f4-8602-4f2d6cb2ddac"
},
- "execution_count": 42,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(81304, 1) (2417796, 2)\n",
"CPU times: user 38.8 s, sys: 8.7 s, total: 47.5 s\n",
"Wall time: 48.2 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
- "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
+ "start_nodes = pd.DataFrame({g._node: [17116707]})\n",
"for i in range(10):\n",
- " g2 = g_gdf.hop(\n",
+ " g2 = g.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=8)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del g_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1775,36 +1741,47 @@
"id": "9o_og8bSAhe3",
"outputId": "dd3e4f8f-f426-4705-98c4-60f1912ba28a"
},
- "execution_count": 43,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(81304, 1) (2417796, 2)\n",
"CPU times: user 1.8 s, sys: 506 ms, total: 2.3 s\n",
"Wall time: 2.3 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "start_nodes = cudf.DataFrame({g._node: [17116707]})\n",
+ "g_gdf = g.nodes(cudf.from_pandas(g._nodes)).edges(cudf.from_pandas(g._edges))\n",
+ "for i in range(10):\n",
+ " g2 = g_gdf.hop(\n",
+ " nodes=start_nodes,\n",
+ " direction='forward',\n",
+ " hops=8)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del g_gdf\n",
+ "del g2"
]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "9dZzAAVONCD2"
+ },
"source": [
- "### GPlus\n",
+ "## GPlus\n",
"\n",
"- edges: 30494866\n",
"- nodes: 107614"
- ],
- "metadata": {
- "id": "9dZzAAVONCD2"
- }
+ ]
},
{
"cell_type": "code",
- "source": [
- "! wget https://snap.stanford.edu/data/gplus_combined.txt.gz"
- ],
+ "execution_count": 4,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -1812,11 +1789,10 @@
"id": "-nhWGNekKpcZ",
"outputId": "e2175290-337c-4faa-e5d8-4bc401583326"
},
- "execution_count": 4,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"--2023-12-26 18:36:29-- https://snap.stanford.edu/data/gplus_combined.txt.gz\n",
"Resolving snap.stanford.edu (snap.stanford.edu)... 171.64.75.80\n",
@@ -1831,27 +1807,25 @@
"\n"
]
}
+ ],
+ "source": [
+ "#! wget https://snap.stanford.edu/data/gplus_combined.txt.gz"
]
},
{
"cell_type": "code",
- "source": [
- "! gunzip gplus_combined.txt.gz"
- ],
+ "execution_count": 5,
"metadata": {
"id": "g5wgA_c2KqwJ"
},
- "execution_count": 5,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "#! gunzip gplus_combined.txt.gz"
+ ]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "ge_df = pd.read_csv('gplus_combined.txt', sep=' ', names=['s', 'd'])\n",
- "print(ge_df.shape)\n",
- "ge_df.head(5)"
- ],
+ "execution_count": 6,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -1860,11 +1834,10 @@
"id": "52hgDbr0Kti6",
"outputId": "217203fc-7095-4784-c4c4-d46ee9c78808"
},
- "execution_count": 6,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(30494866, 2)\n",
"CPU times: user 16 s, sys: 1.45 s, total: 17.5 s\n",
@@ -1872,16 +1845,7 @@
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " s d\n",
- "0 116374117927631468606 101765416973555767821\n",
- "1 112188647432305746617 107727150903234299458\n",
- "2 116719211656774388392 100432456209427807893\n",
- "3 117421021456205115327 101096322838605097368\n",
- "4 116407635616074189669 113556266482860931616"
- ],
"text/html": [
"\n",
" \n",
@@ -2144,22 +2108,31 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " s d\n",
+ "0 116374117927631468606 101765416973555767821\n",
+ "1 112188647432305746617 107727150903234299458\n",
+ "2 116719211656774388392 100432456209427807893\n",
+ "3 117421021456205115327 101096322838605097368\n",
+ "4 116407635616074189669 113556266482860931616"
]
},
+ "execution_count": 6,
"metadata": {},
- "execution_count": 6
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "ge_df = pd.read_csv('gplus_combined.txt', sep=' ', names=['s', 'd'])\n",
+ "print(ge_df.shape)\n",
+ "ge_df.head(5)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg = graphistry.edges(ge_df, 's', 'd').materialize_nodes()\n",
- "gg = graphistry.edges(ge_df, 's', 'd').nodes(gg._nodes, 'id')\n",
- "print(gg._edges.shape, gg._nodes.shape)\n",
- "gg._nodes.head(5)"
- ],
+ "execution_count": 7,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -2168,11 +2141,10 @@
"id": "w5YkN-nLK6UV",
"outputId": "dc98380d-54c2-4b36-c56e-5e8401c4ffa4"
},
- "execution_count": 7,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(30494866, 2) (107614, 1)\n",
"CPU times: user 4.49 s, sys: 1.25 s, total: 5.74 s\n",
@@ -2180,16 +2152,7 @@
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " id\n",
- "0 116374117927631468606\n",
- "1 112188647432305746617\n",
- "2 116719211656774388392\n",
- "3 117421021456205115327\n",
- "4 116407635616074189669"
- ],
"text/html": [
"\n",
" \n",
@@ -2446,19 +2409,32 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " id\n",
+ "0 116374117927631468606\n",
+ "1 112188647432305746617\n",
+ "2 116719211656774388392\n",
+ "3 117421021456205115327\n",
+ "4 116407635616074189669"
]
},
+ "execution_count": 7,
"metadata": {},
- "execution_count": 7
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg = graphistry.edges(ge_df, 's', 'd').materialize_nodes()\n",
+ "gg = graphistry.edges(ge_df, 's', 'd').nodes(gg._nodes, 'id')\n",
+ "print(gg._edges.shape, gg._nodes.shape)\n",
+ "gg._nodes.head(5)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg.chain([ n({'id': '116374117927631468606'})])._nodes"
- ],
+ "execution_count": 49,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -2467,23 +2443,17 @@
"id": "NKtz54uELX-8",
"outputId": "5d8f3eef-893d-47cc-e7a9-c5cbfec8270c"
},
- "execution_count": 49,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 534 ms, sys: 598 ms, total: 1.13 s\n",
"Wall time: 1.65 s\n"
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " id\n",
- "0 116374117927631468606"
- ],
"text/html": [
"\n",
" \n",
@@ -2597,20 +2567,25 @@
"\n",
"
\n",
" \n"
+ ],
+ "text/plain": [
+ " id\n",
+ "0 116374117927631468606"
]
},
+ "execution_count": 49,
"metadata": {},
- "execution_count": 49
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg.chain([ n({'id': '116374117927631468606'})])._nodes"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=1)])._nodes\n",
- "out.shape"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2618,38 +2593,35 @@
"id": "iNWdi00VLmZG",
"outputId": "ecfb56a6-c564-4bf6-f43f-2c95a103f4be"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 27.5 s, sys: 11.1 s, total: 38.5 s\n",
"Wall time: 39.5 s\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"(1473, 1)"
]
},
+ "execution_count": 75,
"metadata": {},
- "execution_count": 75
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=1)])._nodes\n",
+ "out.shape"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=1)])\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del gg_gdf\n",
- "del out"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2657,26 +2629,29 @@
"id": "Q6p3h6uCOABh",
"outputId": "817fc80f-ef5d-4070-eb48-a12344be709c"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(1473, 1) (13375, 2)\n",
"CPU times: user 4.57 s, sys: 2.11 s, total: 6.68 s\n",
"Wall time: 7.63 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=1)])\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del gg_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=2)])._nodes\n",
- "out.shape"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2684,38 +2659,35 @@
"id": "6UdCcMdqLw-P",
"outputId": "70742c79-b22b-4db2-c548-cb1e25d572eb"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 45.8 s, sys: 17 s, total: 1min 2s\n",
"Wall time: 1min 5s\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"(44073, 1)"
]
},
+ "execution_count": 77,
"metadata": {},
- "execution_count": 77
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=2)])._nodes\n",
+ "out.shape"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=2)])\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del gg_gdf\n",
- "del out"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2723,26 +2695,29 @@
"id": "QElqatDyNYCS",
"outputId": "0e15bd3e-d2d9-4965-df7d-c8856d036680"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(44073, 1) (2069325, 2)\n",
"CPU times: user 4.97 s, sys: 2.36 s, total: 7.34 s\n",
"Wall time: 10.6 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=2)])\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del gg_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=3)])._nodes\n",
- "out.shape"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2750,38 +2725,35 @@
"id": "3HJOItZ4MQMG",
"outputId": "f5be7bb4-7f09-4f80-c549-e703e99f5067"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 3min 45s, sys: 1min 5s, total: 4min 50s\n",
"Wall time: 4min 52s\n"
]
},
{
- "output_type": "execute_result",
"data": {
"text/plain": [
"(102414, 1)"
]
},
+ "execution_count": 79,
"metadata": {},
- "execution_count": 79
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=3)])._nodes\n",
+ "out.shape"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=3)])\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del gg_gdf\n",
- "del out"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2789,26 +2761,29 @@
"id": "G32t_xthOUle",
"outputId": "7721741f-9c86-41aa-eb0b-2c8f0db2ed54"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(102414, 1) (24851333, 2)\n",
"CPU times: user 6.95 s, sys: 2.63 s, total: 9.57 s\n",
"Wall time: 9.84 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=3)])\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del gg_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=4)])\n",
- "print(out._nodes.shape, out._edges.shape)"
- ],
+ "execution_count": 8,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2816,29 +2791,26 @@
"id": "bXy2yyJsMsEG",
"outputId": "911f2680-067c-44f2-9ba2-7f27d3c9bc6b"
},
- "execution_count": 8,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105479, 1) (30450354, 2)\n",
"CPU times: user 4min 36s, sys: 1min 25s, total: 6min 2s\n",
"Wall time: 6min 4s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=4)])\n",
+ "print(out._nodes.shape, out._edges.shape)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=4)])\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del gg_gdf\n",
- "del out"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2846,26 +2818,29 @@
"id": "Vt8hhjWDP_W_",
"outputId": "824ae644-e1cf-4239-bda9-84aecde52ad8"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105479, 1) (30450354, 2)\n",
"CPU times: user 7.44 s, sys: 2.45 s, total: 9.88 s\n",
"Wall time: 9.9 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=4)])\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del gg_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=5)])\n",
- "print(out._nodes.shape, out._edges.shape)"
- ],
+ "execution_count": 9,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2873,61 +2848,56 @@
"id": "_z4KpNZaOH8t",
"outputId": "2417f78b-e1b7-452d-8e26-7df259620c88"
},
- "execution_count": 9,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105604, 1) (30468335, 2)\n",
"CPU times: user 5min 36s, sys: 1min 39s, total: 7min 16s\n",
"Wall time: 7min 15s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "out = gg.chain([ n({'id': '116374117927631468606'}), e_forward(hops=5)])\n",
+ "print(out._nodes.shape, out._edges.shape)"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=5)])\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del gg_gdf\n",
- "del out"
- ],
+ "execution_count": null,
"metadata": {
- "id": "spUBH9EHSz2O",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "spUBH9EHSz2O",
"outputId": "22340ce3-e8d4-4a72-b485-9839c667b965"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105604, 1) (30468335, 2)\n",
"CPU times: user 8.82 s, sys: 2.71 s, total: 11.5 s\n",
"Wall time: 11.9 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "gg_gdf = gg.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "out = gg_gdf.chain([ n({'id': '116374117927631468606'}), e_forward(hops=5)])\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del gg_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
- "for i in range(1):\n",
- " g2 = gg.hop(\n",
- " nodes=start_nodes,\n",
- " direction='forward',\n",
- " hops=1)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ "execution_count": 50,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2935,35 +2905,31 @@
"id": "vCsdmc62A7OM",
"outputId": "adc05d29-c628-49ed-cd6d-8921c6dcd206"
},
- "execution_count": 50,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(1473, 1) (13375, 2)\n",
"CPU times: user 19.9 s, sys: 9.36 s, total: 29.2 s\n",
"Wall time: 41.8 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
- "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
"for i in range(1):\n",
- " g2 = gg_gdf.hop(\n",
+ " g2 = gg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=1)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del gg_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 52,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -2971,31 +2937,35 @@
"id": "J3kV8NBYBQdW",
"outputId": "76073248-43e1-4c3c-c004-67324cc1d312"
},
- "execution_count": 52,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(1473, 1) (13375, 2)\n",
"CPU times: user 3.71 s, sys: 2.09 s, total: 5.8 s\n",
"Wall time: 6.05 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
"for i in range(1):\n",
- " g2 = gg.hop(\n",
+ " g2 = gg_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=2)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=1)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del gg_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 53,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3003,35 +2973,31 @@
"id": "ONv1RQeWBeeK",
"outputId": "58d57fa4-be72-45bc-abfa-5de9d1102f55"
},
- "execution_count": 53,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(44073, 1) (2069325, 2)\n",
"CPU times: user 27.8 s, sys: 13.2 s, total: 41 s\n",
"Wall time: 43.9 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
- "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
"for i in range(1):\n",
- " g2 = gg_gdf.hop(\n",
+ " g2 = gg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=2)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del gg_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 54,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3039,31 +3005,35 @@
"id": "ke5SZZ01BgqR",
"outputId": "4173fd28-a11b-4300-d28b-6fdb87e8e9f3"
},
- "execution_count": 54,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(44073, 1) (2069325, 2)\n",
"CPU times: user 4.26 s, sys: 2.37 s, total: 6.63 s\n",
"Wall time: 7.91 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
"for i in range(1):\n",
- " g2 = gg.hop(\n",
+ " g2 = gg_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=3)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=2)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del gg_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 55,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3071,35 +3041,31 @@
"id": "U795pIBUBiZV",
"outputId": "d499433c-cc0c-4bbf-c69f-36b5d55402d9"
},
- "execution_count": 55,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(102414, 1) (24851333, 2)\n",
"CPU times: user 1min 3s, sys: 22.7 s, total: 1min 26s\n",
"Wall time: 1min 35s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
- "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
"for i in range(1):\n",
- " g2 = gg_gdf.hop(\n",
+ " g2 = gg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=3)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del gg_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 56,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3107,31 +3073,35 @@
"id": "kIZYwSe1Bj2e",
"outputId": "b7e1ed9f-47d1-412e-9593-ecc436ac1486"
},
- "execution_count": 56,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(102414, 1) (24851333, 2)\n",
"CPU times: user 3.96 s, sys: 2.11 s, total: 6.07 s\n",
"Wall time: 6.05 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
"for i in range(1):\n",
- " g2 = gg.hop(\n",
+ " g2 = gg_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=4)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=3)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del gg_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3139,35 +3109,31 @@
"id": "YTI5sD6YBpYL",
"outputId": "b37bf2df-07dc-404c-8a83-a83f28e38bf6"
},
- "execution_count": 57,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105479, 1) (30450354, 2)\n",
"CPU times: user 1min 34s, sys: 30.6 s, total: 2min 5s\n",
"Wall time: 2min 5s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
- "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
"for i in range(1):\n",
- " g2 = gg_gdf.hop(\n",
+ " g2 = gg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=4)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del gg_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 58,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3175,31 +3141,35 @@
"id": "d5WBazICBrSz",
"outputId": "ef95e893-3a0f-4d47-ede4-bd8a6faebf98"
},
- "execution_count": 58,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105479, 1) (30450354, 2)\n",
"CPU times: user 5.25 s, sys: 2.41 s, total: 7.67 s\n",
"Wall time: 7.69 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
"for i in range(1):\n",
- " g2 = gg.hop(\n",
+ " g2 = gg_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=5)\n",
- "print(g2._nodes.shape, g2._edges.shape)"
- ],
+ " hops=4)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del gg_gdf\n",
+ "del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 59,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3207,35 +3177,31 @@
"id": "ozQlRPaFBtPD",
"outputId": "4f1655c4-38fd-47f9-942d-836585e0d866"
},
- "execution_count": 59,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105604, 1) (30468335, 2)\n",
"CPU times: user 2min 16s, sys: 39.1 s, total: 2min 55s\n",
"Wall time: 2min 58s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
- "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "start_nodes = pd.DataFrame({gg._node: ['116374117927631468606']})\n",
"for i in range(1):\n",
- " g2 = gg_gdf.hop(\n",
+ " g2 = gg.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=5)\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del gg_gdf\n",
- "del g2"
- ],
+ "print(g2._nodes.shape, g2._edges.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 60,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3243,35 +3209,46 @@
"id": "-ACkMG20B6HM",
"outputId": "f26c03a9-9f25-4f93-c7d3-0e8676694040"
},
- "execution_count": 60,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(105604, 1) (30468335, 2)\n",
"CPU times: user 5.79 s, sys: 2.51 s, total: 8.3 s\n",
"Wall time: 8.29 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "start_nodes = cudf.DataFrame({gg._node: ['116374117927631468606']})\n",
+ "gg_gdf = gg.nodes(cudf.from_pandas(gg._nodes)).edges(cudf.from_pandas(gg._edges))\n",
+ "for i in range(1):\n",
+ " g2 = gg_gdf.hop(\n",
+ " nodes=start_nodes,\n",
+ " direction='forward',\n",
+ " hops=5)\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del gg_gdf\n",
+ "del g2"
]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "R03M_swxarKC"
+ },
"source": [
- "### Orkut\n",
+ "## Orkut\n",
"- 117M edges\n",
"- 3M nodes"
- ],
- "metadata": {
- "id": "R03M_swxarKC"
- }
+ ]
},
{
"cell_type": "code",
- "source": [
- "! wget https://snap.stanford.edu/data/bigdata/communities/com-orkut.ungraph.txt.gz"
- ],
+ "execution_count": 8,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3279,11 +3256,10 @@
"id": "QoabYR2maxPo",
"outputId": "2bb6275d-46bb-42da-ec05-d0e5a58b1f77"
},
- "execution_count": 8,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"--2023-12-26 00:55:52-- https://snap.stanford.edu/data/bigdata/communities/com-orkut.ungraph.txt.gz\n",
"Resolving snap.stanford.edu (snap.stanford.edu)... 171.64.75.80\n",
@@ -3298,24 +3274,25 @@
"\n"
]
}
+ ],
+ "source": [
+ "#! wget https://snap.stanford.edu/data/bigdata/communities/com-orkut.ungraph.txt.gz"
]
},
{
"cell_type": "code",
- "source": [
- "! gunzip com-orkut.ungraph.txt.gz"
- ],
+ "execution_count": 9,
"metadata": {
"id": "BvvfFPKWbAVJ"
},
- "execution_count": 9,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "#! gunzip com-orkut.ungraph.txt.gz"
+ ]
},
{
"cell_type": "code",
- "source": [
- "! head -n 7 com-orkut.ungraph.txt"
- ],
+ "execution_count": 10,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3323,11 +3300,10 @@
"id": "YsWwRoPqbPIb",
"outputId": "2eb4f862-b4e1-42bf-ff5d-eec10b27cedc"
},
- "execution_count": 10,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"# Undirected graph: ../../data/output/orkut.txt\n",
"# Orkut\n",
@@ -3338,10 +3314,33 @@
"1\t4\n"
]
}
+ ],
+ "source": [
+ "#! head -n 7 com-orkut.ungraph.txt"
]
},
{
"cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "cbMC8r2ldjbW",
+ "outputId": "82688d53-7d56-4563-d65e-7c5cd32ac14e"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "('23.12.01', '0.32.0+12.g72e778c')"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"import pandas as pd\n",
"\n",
@@ -3363,33 +3362,11 @@
"locale.getpreferredencoding = lambda: \"UTF-8\"\n",
"\n",
"cudf.__version__, graphistry.__version__"
- ],
- "metadata": {
- "colab": {
- "base_uri": "https://localhost:8080/"
- },
- "id": "cbMC8r2ldjbW",
- "outputId": "82688d53-7d56-4563-d65e-7c5cd32ac14e"
- },
- "execution_count": 11,
- "outputs": [
- {
- "output_type": "execute_result",
- "data": {
- "text/plain": [
- "('23.12.01', '0.32.0+12.g72e778c')"
- ]
- },
- "metadata": {},
- "execution_count": 11
- }
]
},
{
"cell_type": "code",
- "source": [
- "! nvidia-smi"
- ],
+ "execution_count": 12,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3397,11 +3374,10 @@
"id": "TopFxAvnh_Cv",
"outputId": "cc9d9dc9-e594-4190-fe84-3f1b6dce8a1a"
},
- "execution_count": 12,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:27 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -3424,18 +3400,14 @@
"+---------------------------------------------------------------------------------------+\n"
]
}
+ ],
+ "source": [
+ "#! nvidia-smi"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "co_df = cudf.read_csv('com-orkut.ungraph.txt', sep='\\t', names=['s', 'd'], skiprows=5).to_pandas()\n",
- "print(co_df.shape)\n",
- "print(co_df.head(5))\n",
- "print(co_df.dtypes)\n",
- "#del co_df"
- ],
+ "execution_count": 13,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3443,11 +3415,10 @@
"id": "Oczs87ITbJgw",
"outputId": "ac203ddd-e684-4eb9-a586-f6a49fd1625d"
},
- "execution_count": 13,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(117185082, 2)\n",
" s d\n",
@@ -3463,17 +3434,19 @@
"Wall time: 6.76 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "co_df = cudf.read_csv('com-orkut.ungraph.txt', sep='\\t', names=['s', 'd'], skiprows=5).to_pandas()\n",
+ "print(co_df.shape)\n",
+ "print(co_df.head(5))\n",
+ "print(co_df.dtypes)\n",
+ "#del co_df"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "co_g = graphistry.edges(cudf.DataFrame(co_df), 's', 'd').materialize_nodes(engine='cudf')\n",
- "co_g = co_g.nodes(lambda g: g._nodes.to_pandas()).edges(lambda g: g._edges.to_pandas())\n",
- "print(co_g._nodes.shape, co_g._edges.shape)\n",
- "co_g._nodes.head(5)"
- ],
+ "execution_count": 14,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -3482,11 +3455,10 @@
"id": "gGSDjTtveFAT",
"outputId": "e7b38f4f-dc07-4f35-9bab-9c80a80bbf0b"
},
- "execution_count": 14,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(3072441, 1) (117185082, 2)\n",
"CPU times: user 1.96 s, sys: 2.95 s, total: 4.91 s\n",
@@ -3494,16 +3466,7 @@
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " id\n",
- "0 1\n",
- "1 2\n",
- "2 3\n",
- "3 4\n",
- "4 5"
- ],
"text/html": [
"\n",
" \n",
@@ -3760,18 +3723,32 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " id\n",
+ "0 1\n",
+ "1 2\n",
+ "2 3\n",
+ "3 4\n",
+ "4 5"
]
},
+ "execution_count": 14,
"metadata": {},
- "execution_count": 14
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "%%time\n",
+ "co_g = graphistry.edges(cudf.DataFrame(co_df), 's', 'd').materialize_nodes(engine='cudf')\n",
+ "co_g = co_g.nodes(lambda g: g._nodes.to_pandas()).edges(lambda g: g._edges.to_pandas())\n",
+ "print(co_g._nodes.shape, co_g._edges.shape)\n",
+ "co_g._nodes.head(5)"
]
},
{
"cell_type": "code",
- "source": [
- "! nvidia-smi"
- ],
+ "execution_count": 15,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3779,11 +3756,10 @@
"id": "V5qL8K7-dqIZ",
"outputId": "e08319fc-74d3-4f33-df0f-f98950dc8c99"
},
- "execution_count": 15,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:39 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -3806,18 +3782,14 @@
"+---------------------------------------------------------------------------------------+\n"
]
}
+ ],
+ "source": [
+ "#! nvidia-smi"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "# crashes\n",
- "if False:\n",
- " out = co_g.chain([ n({'id': 1}), e_forward(hops=1)])._nodes\n",
- " print(out.shape)\n",
- " del out"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3825,31 +3797,28 @@
"id": "hCbxZ8UmhRLp",
"outputId": "519aed6c-733d-41f4-d462-e57f5e32b131"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"CPU times: user 4 µs, sys: 1 µs, total: 5 µs\n",
"Wall time: 47.7 µs\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "# crashes\n",
+ "if False:\n",
+ " out = co_g.chain([ n({'id': 1}), e_forward(hops=1)])._nodes\n",
+ " print(out.shape)\n",
+ " del out"
]
},
{
- "cell_type": "code",
- "source": [
- "%%time\n",
- "co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
- "for i in range(10):\n",
- " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=1)])\n",
- "! nvidia-smi\n",
- "print(out._nodes.shape, out._edges.shape)\n",
- "del co_gdf\n",
- "del out"
- ],
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3857,11 +3826,10 @@
"id": "Q682scC_eC-S",
"outputId": "7ff5f829-0de7-4a6c-a77d-e2857896a8a5"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Mon Dec 25 06:23:46 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -3906,21 +3874,22 @@
"Wall time: 4.42 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
- " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=2)])\n",
- "! nvidia-smi\n",
+ " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=1)])\n",
+ "#! nvidia-smi\n",
"print(out._nodes.shape, out._edges.shape)\n",
"del co_gdf\n",
"del out"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3928,11 +3897,10 @@
"id": "i0AXhfqVbVsm",
"outputId": "8271f469-a73f-48e3-e1a9-3077026ab8ec"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Mon Dec 25 06:24:52 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -3977,21 +3945,22 @@
"Wall time: 6.13 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
- " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=3)])\n",
- "! nvidia-smi\n",
+ " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=2)])\n",
+ "#! nvidia-smi\n",
"print(out._nodes.shape, out._edges.shape)\n",
"del co_gdf\n",
"del out"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -3999,11 +3968,10 @@
"id": "Hid0-iPKhpOd",
"outputId": "ecaeb534-d4d7-48fa-d4e1-c80b22626afe"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Mon Dec 25 06:25:25 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4048,21 +4016,22 @@
"Wall time: 6.37 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
- " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=4)])\n",
- "! nvidia-smi\n",
+ " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=3)])\n",
+ "#! nvidia-smi\n",
"print(out._nodes.shape, out._edges.shape)\n",
"del co_gdf\n",
"del out"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4070,11 +4039,10 @@
"id": "buutj-ZjhrEe",
"outputId": "ae11addd-6bea-44e9-81c0-b431e1db8089"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Mon Dec 25 06:26:04 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4119,21 +4087,22 @@
"Wall time: 9.84 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
- " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=5)])\n",
- "! nvidia-smi\n",
+ " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=4)])\n",
+ "#! nvidia-smi\n",
"print(out._nodes.shape, out._edges.shape)\n",
"del co_gdf\n",
"del out"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4141,11 +4110,10 @@
"id": "bK4C9Ly0hso-",
"outputId": "8a9a32ab-03e2-42b4-8b71-2bcf797b31b1"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Mon Dec 25 06:27:18 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4190,10 +4158,26 @@
"Wall time: 39.2 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "#! nvidia-smi\n",
+ "for i in range(10):\n",
+ " out = co_gdf.chain([ n({'id': 1}), e_forward(hops=5)])\n",
+ "#! nvidia-smi\n",
+ "print(out._nodes.shape, out._edges.shape)\n",
+ "del co_gdf\n",
+ "del out"
]
},
{
"cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "qrga-la0hwhh"
+ },
+ "outputs": [],
"source": [
"%%time\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
@@ -4201,18 +4185,11 @@
"print(out.shape)\n",
"del co_gdf\n",
"del out"
- ],
- "metadata": {
- "id": "qrga-la0hwhh"
- },
- "execution_count": null,
- "outputs": []
+ ]
},
{
"cell_type": "code",
- "source": [
- "!lscpu\n"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4220,11 +4197,10 @@
"id": "eiXFImxF-rzw",
"outputId": "b807cc3d-ed1a-4bef-c6e0-bfc2df7356ff"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Architecture: x86_64\n",
" CPU op-mode(s): 32-bit, 64-bit\n",
@@ -4276,13 +4252,14 @@
" Tsx async abort: Vulnerable\n"
]
}
+ ],
+ "source": [
+ "#!lscpu\n"
]
},
{
"cell_type": "code",
- "source": [
- "!free -h\n"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4290,36 +4267,24 @@
"id": "wJohLi58-sN5",
"outputId": "c3e144f6-c19a-4c68-e867-f5e7fa2e9df4"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
" total used free shared buff/cache available\n",
"Mem: 12Gi 717Mi 8.0Gi 1.0Mi 3.9Gi 11Gi\n",
"Swap: 0B 0B 0B\n"
]
}
+ ],
+ "source": [
+ "#!free -h\n"
]
},
{
"cell_type": "code",
- "source": [
- "%%time\n",
- "start_nodes = pd.DataFrame({'id': [1]})\n",
- "! nvidia-smi\n",
- "for i in range(1):\n",
- " g2 = co_g.hop(\n",
- " nodes=start_nodes,\n",
- " direction='forward',\n",
- " hops=1)\n",
- "! nvidia-smi\n",
- "print(g2._nodes.shape, g2._edges.shape)\n",
- "#del start_nodes\n",
- "#del co_gdf\n",
- "#del g2"
- ],
+ "execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4327,11 +4292,10 @@
"id": "zak4Inhco5il",
"outputId": "30bcf2bc-853e-4e5e-8c57-ba0cd9429554"
},
- "execution_count": null,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 01:01:43 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4354,38 +4318,37 @@
"+---------------------------------------------------------------------------------------+\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
- "start_nodes = cudf.DataFrame({'id': [1]})\n",
- "co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
- "for i in range(10):\n",
- " g2 = co_gdf.hop(\n",
+ "start_nodes = pd.DataFrame({'id': [1]})\n",
+ "#! nvidia-smi\n",
+ "for i in range(1):\n",
+ " g2 = co_g.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
" hops=1)\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
- "del start_nodes\n",
- "del co_gdf\n",
- "del g2"
- ],
+ "#del start_nodes\n",
+ "#del co_gdf\n",
+ "#del g2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
"metadata": {
- "id": "-SmFlCBS_Bgx",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "-SmFlCBS_Bgx",
"outputId": "d2326cf7-3ea6-4f99-9548-f2e98ece59a4"
},
- "execution_count": 16,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:45 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4430,26 +4393,27 @@
"Wall time: 1.84 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"start_nodes = cudf.DataFrame({'id': [1]})\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
" g2 = co_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=2)\n",
- "! nvidia-smi\n",
+ " hops=1)\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
"del start_nodes\n",
"del co_gdf\n",
"del g2"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4457,11 +4421,10 @@
"id": "fjjt3YnYnabv",
"outputId": "05762f50-bfe1-4d23-9153-31431418c8e5"
},
- "execution_count": 17,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:47 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4506,26 +4469,27 @@
"Wall time: 2.51 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"start_nodes = cudf.DataFrame({'id': [1]})\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
" g2 = co_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=3)\n",
- "! nvidia-smi\n",
+ " hops=2)\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
"del start_nodes\n",
"del co_gdf\n",
"del g2"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4533,11 +4497,10 @@
"id": "oIouuORgnbcY",
"outputId": "f07abe4c-5137-4ee3-935a-afbb2c5eaa1e"
},
- "execution_count": 18,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:50 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4582,26 +4545,27 @@
"Wall time: 3.25 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"start_nodes = cudf.DataFrame({'id': [1]})\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
" g2 = co_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=4)\n",
- "! nvidia-smi\n",
+ " hops=3)\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
"del start_nodes\n",
"del co_gdf\n",
"del g2"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4609,11 +4573,10 @@
"id": "oNLZGjwInc85",
"outputId": "534097cf-4022-48cc-9419-a00c135f69e1"
},
- "execution_count": 19,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:53 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4658,26 +4621,27 @@
"Wall time: 5.02 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"start_nodes = cudf.DataFrame({'id': [1]})\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
" g2 = co_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=5)\n",
- "! nvidia-smi\n",
+ " hops=4)\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
"del start_nodes\n",
"del co_gdf\n",
"del g2"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4685,11 +4649,10 @@
"id": "ePqaeujMneX8",
"outputId": "ffd88fff-016e-4ac0-ecb9-fa06baca60f8"
},
- "execution_count": 20,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:56:58 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4734,26 +4697,27 @@
"Wall time: 12 s\n"
]
}
- ]
- },
- {
- "cell_type": "code",
+ ],
"source": [
"%%time\n",
"start_nodes = cudf.DataFrame({'id': [1]})\n",
"co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
- "! nvidia-smi\n",
+ "#! nvidia-smi\n",
"for i in range(10):\n",
" g2 = co_gdf.hop(\n",
" nodes=start_nodes,\n",
" direction='forward',\n",
- " hops=6)\n",
- "! nvidia-smi\n",
+ " hops=5)\n",
+ "#! nvidia-smi\n",
"print(g2._nodes.shape, g2._edges.shape)\n",
"del start_nodes\n",
"del co_gdf\n",
"del g2"
- ],
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@@ -4761,11 +4725,10 @@
"id": "PTBkoIVHnfzK",
"outputId": "5615ecd7-47ea-46ab-fd36-13bce4b3c787"
},
- "execution_count": 21,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"Tue Dec 26 00:57:10 2023 \n",
"+---------------------------------------------------------------------------------------+\n",
@@ -4810,16 +4773,48 @@
"Wall time: 28.2 s\n"
]
}
+ ],
+ "source": [
+ "%%time\n",
+ "start_nodes = cudf.DataFrame({'id': [1]})\n",
+ "co_gdf = co_g.nodes(lambda g: cudf.DataFrame(g._nodes)).edges(lambda g: cudf.DataFrame(g._edges))\n",
+ "#! nvidia-smi\n",
+ "for i in range(10):\n",
+ " g2 = co_gdf.hop(\n",
+ " nodes=start_nodes,\n",
+ " direction='forward',\n",
+ " hops=6)\n",
+ "#! nvidia-smi\n",
+ "print(g2._nodes.shape, g2._edges.shape)\n",
+ "del start_nodes\n",
+ "del co_gdf\n",
+ "del g2"
]
},
{
"cell_type": "code",
- "source": [],
+ "execution_count": null,
"metadata": {
"id": "Ygc2nrkznlCu"
},
- "execution_count": null,
- "outputs": []
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "accelerator": "GPU",
+ "colab": {
+ "gpuType": "T4",
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
}
- ]
-}
\ No newline at end of file
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/demos/more_examples/graphistry_features/embed/simple-ssh-logs-rgcn-anomaly-detector.ipynb b/demos/more_examples/graphistry_features/embed/simple-ssh-logs-rgcn-anomaly-detector.ipynb
index e79c81cc6..3cb5b0dd6 100644
--- a/demos/more_examples/graphistry_features/embed/simple-ssh-logs-rgcn-anomaly-detector.ipynb
+++ b/demos/more_examples/graphistry_features/embed/simple-ssh-logs-rgcn-anomaly-detector.ipynb
@@ -12,10 +12,8 @@
"* Unsupervised graph neural network: RGCN\n",
"* Runs on both CPU + GPU: Toggle `is_gpu`\n",
"\n",
- "See also:\n",
- "* Other pygraphistry[ai] gnn notebooks for more advanced modes like incorporating node features\n",
- "* Intro to RGCNs - [intro-story.ipynb](intro-story.md)\n",
- "* In-depth RGCN - [advanced-identity-protection-40m.ipynb](advanced-identity-protection-40m.ipynb)\n"
+ "For background, so the RGCN intro: [intro-story.ipynb](../../../talks/infosec_jupyterthon2022/rgcn_login_anomaly_detection/advanced-identity-protection-40m.ipynb)\n",
+ "\n"
]
},
{
@@ -353,189 +351,10 @@
},
{
"cell_type": "code",
- "execution_count": 6,
- "id": "dbf488b3-2a98-4c19-aa4f-4aef63943412",
- "metadata": {
- "execution": {
- "iopub.execute_input": "2022-12-02T20:52:04.712354Z",
- "iopub.status.busy": "2022-12-02T20:52:04.712254Z",
- "iopub.status.idle": "2022-12-02T20:52:34.396563Z",
- "shell.execute_reply": "2022-12-02T20:52:34.396305Z",
- "shell.execute_reply.started": "2022-12-02T20:52:04.712343Z"
- },
- "tags": []
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Preprocessing embedding data\n",
- "--Splitting data\n",
- "--num_nodes: 97, num_relationships: 20\n",
- "Training embedding\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 1, loss: 0.4165, score: 0.0000%: 0%| | 0/10 [00:03, ?it/s]/home/graphistry/.local/lib/python3.8/site-packages/graphistry/embed_utils.py:459: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n",
- " emb = torch.tensor(self._embeddings)\n",
- "epoch: 2, loss: 0.4542, score: 84.8845%: 10%|█ | 1/10 [00:03<00:28, 3.11s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 3, loss: 0.3594, score: 88.4535%: 20%|██ | 2/10 [00:06<00:24, 3.02s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 4, loss: 0.3348, score: 82.7852%: 30%|███ | 3/10 [00:09<00:21, 3.00s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 5, loss: 0.3213, score: 82.3653%: 40%|████ | 4/10 [00:12<00:17, 2.98s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 6, loss: 0.3318, score: 84.2547%: 50%|█████ | 5/10 [00:15<00:14, 2.93s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 7, loss: 0.2965, score: 80.7558%: 60%|██████ | 6/10 [00:17<00:11, 2.94s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 8, loss: 0.2893, score: 74.9475%: 70%|███████ | 7/10 [00:20<00:08, 2.96s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 9, loss: 0.2932, score: 70.3289%: 80%|████████ | 8/10 [00:24<00:05, 2.98s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 10, loss: 0.3061, score: 65.8502%: 90%|█████████ | 9/10 [00:26<00:02, 2.96s/it]"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "epoch: 10, loss: 0.2931, score: 67.88%: 100%|██████████| 10/10 [00:29<00:00, 2.97s/it] "
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Evaluating...\n",
- "--took 0.00 minutes to evaluate\n"
- ]
- },
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "\n"
- ]
- }
- ],
+ "execution_count": null,
+ "id": "018a64cf",
+ "metadata": {},
+ "outputs": [],
"source": [
"g2 = g.embed( # rerun until happy with quality\n",
" device=dev0,\n",
@@ -841,8 +660,9 @@
"source": [
"## Next steps\n",
"\n",
- "- RGCN intro: [intro-story.ipynb](../../talks/infosec_jupyterthon2022/intro-story.ipynb)\n",
- "- In-depth RGCN: [advanced-identity-protection-40m.ipynb](../../talks/infosec_jupyterthon2022/advanced-identity-protection-40m.ipynb)\n",
+ "- RGCN intro: [intro-story.ipynb](../../../talks/infosec_jupyterthon2022/rgcn_login_anomaly_detection/intro-story.ipynb)\n",
+ "- In-depth RGCN: [advanced-identity-protection-40m.ipynb](../../../talks/infosec_jupyterthon2022/rgcn_login_anomaly_detection/advanced-identity-protection-40m.ipynb\n",
+ ")\n",
"- UMAP demo for 97% alert volume reduction & alert correlation\n",
"- [PyGraphistry](http://github.com/graphistry/pygraphistryhttp://github.com/graphistry/pygraphistry) (py, oss) + [Graphistry Hub](https://hub.graphistry.com/https://hub.graphistry.com/) (free)\n",
" - Dashboarding with [graph-app-kit (containerized, gpu, graph Streamlit)](https://github.com/graphistry/graph-app-kithttps://github.com/graphistry/graph-app-kit)\n",
@@ -874,7 +694,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.9"
+ "version": "3.8.10"
},
"vscode": {
"interpreter": {
diff --git a/demos/more_examples/graphistry_features/encodings-badges.ipynb b/demos/more_examples/graphistry_features/encodings-badges.ipynb
index f334b6264..449262ec4 100644
--- a/demos/more_examples/graphistry_features/encodings-badges.ipynb
+++ b/demos/more_examples/graphistry_features/encodings-badges.ipynb
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Badge encodings tutorial\n",
+ "# Badge encodings tutorial\n",
"\n",
"See the examples below for common ways to map data to node badges in Graphistry. Icons appear in the main area of a node, while badges circle them (`TopRight`, `BottomLeft`, `Right`, etc.). They can be used together.\n",
"\n",
@@ -17,7 +17,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Setup\n",
+ "### Setup\n",
"\n",
"Mode `api=3` is recommended. It is required for `complex_encodings` (ex: `.encode_point_size(...)`). Mode `api=1` works with the simpler `.bind(point_size='col_a')` form."
]
diff --git a/demos/more_examples/graphistry_features/encodings-colors.ipynb b/demos/more_examples/graphistry_features/encodings-colors.ipynb
index 193770d7c..fcf0d6b93 100644
--- a/demos/more_examples/graphistry_features/encodings-colors.ipynb
+++ b/demos/more_examples/graphistry_features/encodings-colors.ipynb
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Color encodings tutorial\n",
+ "# Color encodings tutorial\n",
"\n",
"See the examples below for common ways to map data to node/edge color in Graphistry.\n",
"\n",
diff --git a/demos/more_examples/graphistry_features/encodings-icons.ipynb b/demos/more_examples/graphistry_features/encodings-icons.ipynb
index fe99afd4d..07a1872df 100644
--- a/demos/more_examples/graphistry_features/encodings-icons.ipynb
+++ b/demos/more_examples/graphistry_features/encodings-icons.ipynb
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Icons encodings tutorial\n",
+ "# Icons encodings tutorial\n",
"\n",
"See the examples below for common ways to map data to node icon in Graphistry.\n",
"\n",
diff --git a/demos/more_examples/graphistry_features/encodings-sizes.ipynb b/demos/more_examples/graphistry_features/encodings-sizes.ipynb
index 8ccefd501..16fa5832e 100644
--- a/demos/more_examples/graphistry_features/encodings-sizes.ipynb
+++ b/demos/more_examples/graphistry_features/encodings-sizes.ipynb
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Size encodings tutorial\n",
+ "# Size encodings tutorial\n",
"\n",
"See the examples below for common ways to map data to node size in Graphistry.\n",
"\n",
diff --git a/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb b/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb
index 98cc207cd..98063ac1e 100644
--- a/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb
+++ b/demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb
@@ -1,23 +1,12 @@
{
- "nbformat": 4,
- "nbformat_minor": 0,
- "metadata": {
- "colab": {
- "provenance": []
- },
- "kernelspec": {
- "name": "python3",
- "display_name": "Python 3"
- },
- "language_info": {
- "name": "python"
- }
- },
"cells": [
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "GZxoiU8sQDk_"
+ },
"source": [
- "# Hop-and-chain: PyGraphistry Cypher-style graph pattern matching on dataframes\n",
+ "# GFQL: Hop-and-chain - PyGraphistry Cypher-style graph pattern matching on dataframes\n",
"\n",
"PyGraphistry supports a rich subset of the popular Cypher graph query language, which you can run on dataframes without needing to install a database nor native libraries. It is natively integrated with dataframes and thus has a Python-native syntax rather than the traditional string syntax.\n",
"\n",
@@ -35,7 +24,7 @@
"\n",
"---\n",
"\n",
- "# Tutorial:\n",
+ "**Tutorial**\n",
"\n",
"1. Install & configure\n",
"1. Load & enrich a US congress twitter interaction dataset\n",
@@ -44,42 +33,44 @@
"1. Advanced filter predicates\n",
"1. Result labeling\n",
"\n"
- ],
- "metadata": {
- "id": "GZxoiU8sQDk_"
- }
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "# 1. Install & configure"
- ],
"metadata": {
"id": "QQpsrtwBT7sa"
- }
+ },
+ "source": [
+ "## 1. Install & configure"
+ ]
},
{
"cell_type": "code",
- "source": [
- "#! pip install graphistry[igraph]"
- ],
+ "execution_count": null,
"metadata": {
"id": "cYjRbgkU9Sx8"
},
- "execution_count": null,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "# ! pip install graphistry[igraph]"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "## Imports"
- ],
"metadata": {
"id": "Ff6Tt9DhkePl"
- }
+ },
+ "source": [
+ "### Imports"
+ ]
},
{
"cell_type": "code",
+ "execution_count": 141,
+ "metadata": {
+ "id": "S5_y0CbLkjft"
+ },
+ "outputs": [],
"source": [
"import pandas as pd\n",
"\n",
@@ -93,66 +84,57 @@
" # attribute predicates\n",
" is_in, ge, startswith, contains, match as match_re\n",
")"
- ],
- "metadata": {
- "id": "S5_y0CbLkjft"
- },
- "execution_count": 141,
- "outputs": []
+ ]
},
{
"cell_type": "code",
- "source": [
- "graphistry.register(api=3, username='...', password='...')"
- ],
+ "execution_count": null,
"metadata": {
"id": "GQ83i-sKUaw9"
},
- "execution_count": null,
- "outputs": []
+ "outputs": [],
+ "source": [
+ "graphistry.register(api=3, username='...', password='...')"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "# 2. Load & enrich a US congress twitter interaction dataset"
- ],
"metadata": {
"id": "eU9SyauNUHtR"
- }
+ },
+ "source": [
+ "## 2. Load & enrich a US congress twitter interaction dataset"
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "AM9JhnaQkRd3"
+ },
"source": [
- "## Data\n",
+ "### Data\n",
"\n",
"* Download\n",
"* Turn json into a Pandas edges dataframe\n",
"* Turn edges dataframe into a PyGraphistry graph\n",
"* Enrich nodes and edges with some useful graph metrics\n",
"* Visualize full graph to test"
- ],
- "metadata": {
- "id": "AM9JhnaQkRd3"
- }
+ ]
},
{
"cell_type": "code",
- "source": [
- "! wget -q https://snap.stanford.edu/data/congress_network.zip\n",
- "! unzip congress_network.zip\n"
- ],
+ "execution_count": 9,
"metadata": {
- "id": "55xeNAyDXhAm",
"colab": {
"base_uri": "https://localhost:8080/"
},
+ "id": "55xeNAyDXhAm",
"outputId": "287758f0-0df2-49ff-ecdc-283313f7e07a"
},
- "execution_count": 9,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"total 1.2M\n",
"drwxr-xr-x 1 root root 4.0K Dec 4 03:56 .\n",
@@ -168,29 +150,15 @@
"-rw-r--r-- 1 root root 299K May 9 2017 vertex2aid\n"
]
}
+ ],
+ "source": [
+ "# ! wget -q https://snap.stanford.edu/data/congress_network.zip\n",
+ "# ! unzip congress_network.zip\n"
]
},
{
"cell_type": "code",
- "source": [
- "import json\n",
- "\n",
- "with open('congress_network/congress_network_data.json', 'r') as file:\n",
- " data = json.load(file)\n",
- "\n",
- "edges = []\n",
- "for i, name in enumerate(data[0]['usernameList']):\n",
- " for ii, j in enumerate(data[0]['outList'][i]):\n",
- " edges.append({\n",
- " 'from': name,\n",
- " 'to': data[0]['usernameList'][j],\n",
- " 'weight': data[0]['outWeight'][i][ii]\n",
- " })\n",
- "edges_df = pd.DataFrame(edges)\n",
- "\n",
- "print(edges_df.shape)\n",
- "edges_df.sample(5)"
- ],
+ "execution_count": 40,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -199,26 +167,16 @@
"id": "6CmULn4N-8oh",
"outputId": "61a1a4cf-dfe1-4260-a427-46009f4e4aaf"
},
- "execution_count": 40,
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
+ "output_type": "stream",
"text": [
"(13289, 3)\n"
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " from to weight\n",
- "11112 RepBobbyRush janschakowsky 0.034364\n",
- "3836 RepCori Ilhan 0.015936\n",
- "5282 RepTedDeutch RepDWStweets 0.003268\n",
- "12352 BennieGThompson RepStricklandWA 0.006849\n",
- "9358 RepCarolMiller RepTroyNehls 0.005291"
- ],
"text/html": [
"\n",
" \n",
@@ -487,55 +445,57 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " from to weight\n",
+ "11112 RepBobbyRush janschakowsky 0.034364\n",
+ "3836 RepCori Ilhan 0.015936\n",
+ "5282 RepTedDeutch RepDWStweets 0.003268\n",
+ "12352 BennieGThompson RepStricklandWA 0.006849\n",
+ "9358 RepCarolMiller RepTroyNehls 0.005291"
]
},
+ "execution_count": 40,
"metadata": {},
- "execution_count": 40
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "import json\n",
+ "\n",
+ "with open('congress_network/congress_network_data.json', 'r') as file:\n",
+ " data = json.load(file)\n",
+ "\n",
+ "edges = []\n",
+ "for i, name in enumerate(data[0]['usernameList']):\n",
+ " for ii, j in enumerate(data[0]['outList'][i]):\n",
+ " edges.append({\n",
+ " 'from': name,\n",
+ " 'to': data[0]['usernameList'][j],\n",
+ " 'weight': data[0]['outWeight'][i][ii]\n",
+ " })\n",
+ "edges_df = pd.DataFrame(edges)\n",
+ "\n",
+ "print(edges_df.shape)\n",
+ "edges_df.sample(5)"
]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "id": "XLFTgDTEDSeA"
+ },
"source": [
- "## Load dataframe as a PyGraphistry graph\n",
+ "### Load dataframe as a PyGraphistry graph\n",
"\n",
"Turn into a graph and precompute some useful graph metrics\n",
"\n",
"Recall that a `g` object, underneath, is essentially just two dataframes, `g._edges` and `g._nodes`, and with many useful graph methods:"
- ],
- "metadata": {
- "id": "XLFTgDTEDSeA"
- }
+ ]
},
{
"cell_type": "code",
- "source": [
- "# Shape\n",
- "g = graphistry.edges(edges_df, 'from', 'to')\n",
- "\n",
- "# Enrich & style\n",
- "# Tip: Switch from compute_igraph to compute_cugraph when GPUs are available\n",
- "g2 = (g\n",
- " .materialize_nodes()\n",
- " .nodes(lambda g: g._nodes.assign(title=g._nodes.id))\n",
- " .edges(lambda g: g._edges.assign(weight2=g._edges.weight))\n",
- " .bind(point_title='title')\n",
- " .compute_igraph('community_infomap')\n",
- " .compute_igraph('pagerank')\n",
- " .get_degrees()\n",
- " .encode_point_color(\n",
- " 'community_infomap',\n",
- " as_categorical=True,\n",
- " categorical_mapping={\n",
- " 0: '#32a9a2', # vibrant teal\n",
- " 1: '#ff6b6b', # soft coral\n",
- " 2: '#f9d342', # muted yellow\n",
- " }\n",
- " )\n",
- ")\n",
- "\n",
- "g2._nodes"
- ],
+ "execution_count": 77,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -544,47 +504,16 @@
"id": "aB1U7e0HXmHh",
"outputId": "53b9fa91-0caf-4866-c5a9-d9cf80e3c9ac"
},
- "execution_count": 77,
"outputs": [
{
- "output_type": "stream",
"name": "stderr",
+ "output_type": "stream",
"text": [
"WARNING:root:edge index g._edge not set so using edge index as ID; set g._edge via g.edges(), or change merge_if_existing to FalseWARNING:root:edge index g._edge __edge_index__ missing as attribute in ig; using ig edge order for IDsWARNING:root:edge index g._edge not set so using edge index as ID; set g._edge via g.edges(), or change merge_if_existing to FalseWARNING:root:edge index g._edge __edge_index__ missing as attribute in ig; using ig edge order for IDs"
]
},
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- " id title community_infomap pagerank degree_in \\\n",
- "0 SenatorBaldwin SenatorBaldwin 0 0.001422 26 \n",
- "1 SenJohnBarrasso SenJohnBarrasso 0 0.001179 22 \n",
- "2 SenatorBennet SenatorBennet 0 0.001995 33 \n",
- "3 MarshaBlackburn MarshaBlackburn 0 0.001331 18 \n",
- "4 SenBlumenthal SenBlumenthal 0 0.001672 30 \n",
- ".. ... ... ... ... ... \n",
- "470 RepJoeWilson RepJoeWilson 1 0.001780 21 \n",
- "471 RobWittman RobWittman 1 0.001017 13 \n",
- "472 rep_stevewomack rep_stevewomack 1 0.002637 35 \n",
- "473 RepJohnYarmuth RepJohnYarmuth 2 0.000555 5 \n",
- "474 RepLeeZeldin RepLeeZeldin 1 0.000511 3 \n",
- "\n",
- " degree_out degree \n",
- "0 20 46 \n",
- "1 19 41 \n",
- "2 22 55 \n",
- "3 38 56 \n",
- "4 35 65 \n",
- ".. ... ... \n",
- "470 38 59 \n",
- "471 19 32 \n",
- "472 19 54 \n",
- "473 20 25 \n",
- "474 25 28 \n",
- "\n",
- "[475 rows x 7 columns]"
- ],
"text/html": [
"\n",
" \n",
@@ -938,18 +867,73 @@
"
\n",
" \n",
" \n"
+ ],
+ "text/plain": [
+ " id title community_infomap pagerank degree_in \\\n",
+ "0 SenatorBaldwin SenatorBaldwin 0 0.001422 26 \n",
+ "1 SenJohnBarrasso SenJohnBarrasso 0 0.001179 22 \n",
+ "2 SenatorBennet SenatorBennet 0 0.001995 33 \n",
+ "3 MarshaBlackburn MarshaBlackburn 0 0.001331 18 \n",
+ "4 SenBlumenthal SenBlumenthal 0 0.001672 30 \n",
+ ".. ... ... ... ... ... \n",
+ "470 RepJoeWilson RepJoeWilson 1 0.001780 21 \n",
+ "471 RobWittman RobWittman 1 0.001017 13 \n",
+ "472 rep_stevewomack rep_stevewomack 1 0.002637 35 \n",
+ "473 RepJohnYarmuth RepJohnYarmuth 2 0.000555 5 \n",
+ "474 RepLeeZeldin RepLeeZeldin 1 0.000511 3 \n",
+ "\n",
+ " degree_out degree \n",
+ "0 20 46 \n",
+ "1 19 41 \n",
+ "2 22 55 \n",
+ "3 38 56 \n",
+ "4 35 65 \n",
+ ".. ... ... \n",
+ "470 38 59 \n",
+ "471 19 32 \n",
+ "472 19 54 \n",
+ "473 20 25 \n",
+ "474 25 28 \n",
+ "\n",
+ "[475 rows x 7 columns]"
]
},
+ "execution_count": 77,
"metadata": {},
- "execution_count": 77
+ "output_type": "execute_result"
}
+ ],
+ "source": [
+ "# Shape\n",
+ "g = graphistry.edges(edges_df, 'from', 'to')\n",
+ "\n",
+ "# Enrich & style\n",
+ "# Tip: Switch from compute_igraph to compute_cugraph when GPUs are available\n",
+ "g2 = (g\n",
+ " .materialize_nodes()\n",
+ " .nodes(lambda g: g._nodes.assign(title=g._nodes.id))\n",
+ " .edges(lambda g: g._edges.assign(weight2=g._edges.weight))\n",
+ " .bind(point_title='title')\n",
+ " .compute_igraph('community_infomap')\n",
+ " .compute_igraph('pagerank')\n",
+ " .get_degrees()\n",
+ " .encode_point_color(\n",
+ " 'community_infomap',\n",
+ " as_categorical=True,\n",
+ " categorical_mapping={\n",
+ " 0: '#32a9a2', # vibrant teal\n",
+ " 1: '#ff6b6b', # soft coral\n",
+ " 2: '#f9d342', # muted yellow\n",
+ " }\n",
+ " )\n",
+ ")\n",
+ "\n",
+ "g2._nodes"
]
},
{
"cell_type": "code",
- "source": [
- "g2.plot()"
- ],
+ "execution_count": 79,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@@ -958,14 +942,9 @@
"id": "GY9Q7KyqBMq8",
"outputId": "5b4b277e-17fd-4201-9518-25168b927c6f"
},
- "execution_count": 79,
"outputs": [
{
- "output_type": "execute_result",
"data": {
- "text/plain": [
- ""
- ],
"text/html": [
"\n",
"