Skip to content

Instantly share code, notes, and snippets.

@paw-lu
Last active June 7, 2019 23:28
Show Gist options
  • Save paw-lu/a3c2a3443eddfaf68d5dbd7430c351be to your computer and use it in GitHub Desktop.
Save paw-lu/a3c2a3443eddfaf68d5dbd7430c351be to your computer and use it in GitHub Desktop.
import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output
import pandas as pd
import numpy as np
import pyodbc
import json
import sys
import os
def get_relations(group_id, non_hh=False, related_hh=False, generations=2):
group_ids = [int(group_id)]
relations = pd.DataFrame(
{
"begin_date": [
20141231,
20141231,
20141231,
20150131,
20151031,
20150131,
20151031,
20141231,
20141231,
20141231,
20141231,
20150131,
20141231,
20141231,
20150131,
20151031,
20150131,
20151031,
20150131,
],
"end_date": [
20151030,
20151030,
20151030,
20151030,
99999999,
20151030,
99999999,
20151030,
20151030,
20150529,
20151030,
20151030,
20151030,
20151030,
20151030,
99999999,
20151030,
99999999,
20151030,
],
"id1": [2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 8, 8, 3, 3, 5, 5, 5],
"id2": [0, 4, 6, 3, 3, 5, 5, 1, 8, 9, 7, 7, 0, 7, 2, 2, 2, 2, 6],
"king_id": [0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0],
"1_name": [
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
"saefei jsoiefj",
],
"2_name": [
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
"oierei eriuie",
],
"edge_name": [
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
"siriejf",
],
"group_id": [
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
],
"royalty": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
}
)
return relations
def slice_relations(relation_df, steps=5):
time_slices = []
dates = pd.to_datetime(
relation_df["begin_date"].unique(), format="%Y%m%d"
).sort_values()
formatted_dates = [
date.strftime("%b") + " " + str(date.day) + ", " + str(date.year)
for date in dates
]
unix_steps = (dates.astype(np.int64) // 10 ** 9 // 1e6).astype(np.int32)
# unix_steps = [i for i in range(len(dates))]
step_number = [i for i in range(len(dates))]
formatted_dates = dict(zip(unix_steps, formatted_dates))
step_keys = dict(zip(unix_steps, step_number))
int_dates = [
int(date.strftime("%Y") + date.strftime("%m") + date.strftime("%d"))
for date in dates
]
household_heads = []
for time_step in int_dates:
if time_step == int_dates[-1]:
time_slice = relation_df[
(
(relation_df["begin_date"] <= time_step)
& (time_step < relation_df["end_date"])
)
| (
relation_df["end_date"] == time_step
) # This is a temp hotfix until we change how quantdb time windows work
]
else:
time_slice = relation_df[
(relation_df["begin_date"] <= time_step)
& (time_step < relation_df["end_date"])
]
time_slices.append(time_slice)
household_heads.append(time_slice["king_id"].unique().tolist())
return formatted_dates, household_heads, time_slices, int_dates, step_keys
def get_names(relation_df):
real_names = pd.Series(
relation_df["1_name"].str.title().values,
index=relation_df["id1"],
).to_dict()
real_names.update(
pd.Series(
relation_df["2_name"].str.title().values,
index=relation_df["id2"],
).to_dict()
)
return real_names
# This is to deal with the bizzare issue where numpy int64s fuck up when dumping to JSON
# Hopefully fixed one day
def default(o):
if isinstance(o, np.int64):
return int(o)
raise TypeError
# Things to change: mulitple household heads, add edge names, wealth maybe?
# Dash stuff
external_stylesheets = [
"https://fonts.googleapis.com/css?family=Roboto:300,400,500",
"https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css",
"https://fonts.googleapis.com/icon?family=Material+Icons",
]
app = dash.Dash(
__name__,
external_stylesheets=external_stylesheets,
routes_pathname_prefix="/hh-networks/",
)
app.title = "Household networks"
app.layout = html.Div(
className="mdc-typography",
children=[
html.H2(
className="mdc-typography--headline2",
children="Evolving network",
),
html.H6(
className="mdc-typography--button",
children="BETA",
style={
"background-color": "#177d6b",
"width": "7%",
"font-size": "16px",
"border-radius": "10px",
"color": "#ffffff",
"text-align": "center",
},
),
html.Div(
children=[
html.Div(
className="mdc-text-field mdc-text-field--outlined mdc-text-field--no-label",
children=[
dcc.Input(
className="mdc-text-field__input",
id="cust-id-textbox",
placeholder="Customer key",
type="text",
),
html.Div(
className="mdc-notched-outline",
children=[
html.Div(className="mdc-notched-outline__leading"),
html.Div(className="mdc-notched-outline__trailing"),
],
),
],
)
]
),
html.Br(),
html.Div(
children=[
html.Div(
className="mdc-text-field mdc-text-field--outlined mdc-text-field--no-label",
children=[
dcc.Input(
className="mdc-text-field__input",
id="group_id-textbox",
placeholder="Household key",
type="text",
value="1508",
),
html.Div(
className="mdc-notched-outline",
children=[
html.Div(className="mdc-notched-outline__leading"),
html.Div(className="mdc-notched-outline__trailing"),
],
),
],
),
html.Div(
className="mdc-text-field-helper-line",
children=[
html.Div(
"quantdb household key",
className="mdc-text-field-helper-text mdc-text-field-helper-text--persistent",
)
],
),
]
),
html.Br(),
dcc.Checklist(
id="hh-checkbox-options",
className="mdc-typography--button",
options=[
{"label": "Show non-royalty links", "value": "non_hh"},
{"label": "Show relatives", "value": "related_hh"},
],
values=[],
labelStyle={"display": "block"},
),
html.Br(),
dcc.Slider(id="date-slider", className="mdc-typography--button", step=None),
html.Br(),
html.Br(),
html.Br(),
html.H6(
className="mdc-typography--headline6",
id="rel-type-headline",
children="Relationship",
),
html.P(className="mdc-typography", id="rel-type"),
cyto.Cytoscape(
id="household-network",
# elements=elements,
layout={"name": "breadthfirst", "animate": True},
style={"width": "100%", "height": "500px"},
stylesheet=[
{
"selector": "node",
"style": {
"content": "data(label)",
"text-opacity": 0.87,
"font-size": 20,
"font-family": "Roboto, HelveticaNeue, Helvetica Neue, Helvetica, Segoe UI, sans-serif",
"text-valign": "bottom",
"background-color": "#616161",
"width": 35,
"height": 35,
},
},
{
"selector": ".parent",
"style": {"background-color": "#2d907f", "text-valign": "top"},
},
{
"selector": ".edge",
"style": {
"line-color": "#afd6cf",
"width": "5px",
"curve-style": "unbundled-bezier",
},
},
{
"selector": ".edge-non-hh",
"style": {
"line-color": "#e0e0e0",
"width": "5px",
"curve-style": "unbundled-bezier",
},
},
],
),
# Hidden div inside the app that stores the intermediate value
html.Div(id="intermediate-value", style={"display": "none"}),
],
style={"margin": "auto", "width": "1200px"},
)
@app.callback(
Output("intermediate-value", "children"),
[Input("group_id-textbox", "value"), Input("hh-checkbox-options", "values")],
)
def update_household(group_id, hh_options):
non_hh = False
related_hh = False
related_family = False
if "non_hh" in hh_options:
non_hh = True
if "related_hh" in hh_options:
related_hh = True
group_id = int(group_id)
relations = get_relations(group_id, non_hh, related_hh)
formatted_dates, household_heads, time_slices, int_dates, step_keys = slice_relations(
relations
)
# networks = make_networks(time_slices, 'id1', 'id2')
real_names = get_names(relations)
household_network = {}
household_network["formatted_dates"] = formatted_dates
household_network["int_dates"] = int_dates
household_network["step_keys"] = step_keys
time_networks = []
for i, rel in enumerate(time_slices):
parents = household_heads[i]
cust_ids = pd.unique(
rel[["id2", "id1"]].values.ravel("K")
).tolist() # Using method 'K' for speed
nodes = [
{
"data": {"id": cust_id, "label": real_names[cust_id]},
# 'position': {'x': positions[cust_id][0]*300, 'y': positions[cust_id][1]*300},
"classes": "child",
}
if cust_id not in parents
else {
"data": {"id": cust_id, "label": real_names[cust_id]},
# 'position': {'x': positions[cust_id][0]*300, 'y': positions[cust_id][1]*300},
"classes": "parent",
}
for cust_id in cust_ids
]
edges = [
{
"data": {"source": source, "target": target, "label": edge_desc},
"classes": "edge",
}
if edge_type == 1
else {
"data": {"source": source, "target": target, "label": edge_desc},
"classes": "edge-non-hh",
}
for source, target, edge_type, edge_desc in rel[
[
"id2",
"id1",
"royalty",
"edge_name",
]
].values.tolist()
]
elements = nodes + edges
time_networks.append(elements)
household_network["time_networks"] = time_networks
return json.dumps(household_network, default=default)
# Make slider with dates from household
# Create slider marks
@app.callback(Output("date-slider", "marks"), [Input("intermediate-value", "children")])
def make_slider(household_network):
if household_network:
formatted_dates = {
int(k): {"label": v, "style": {"line-height": "24px"}}
for k, v in json.loads(household_network)["formatted_dates"].items()
}
return formatted_dates
# Set slider max
@app.callback(Output("date-slider", "max"), [Input("intermediate-value", "children")])
def make_slider(household_network):
if household_network:
formatted_dates = {
int(k): v
for k, v in json.loads(household_network)["formatted_dates"].items()
}
return max(formatted_dates.keys())
@app.callback(Output("date-slider", "min"), [Input("intermediate-value", "children")])
def make_slider(household_network):
if household_network:
formatted_dates = {
int(k): v
for k, v in json.loads(household_network)["formatted_dates"].items()
}
return min(formatted_dates.keys())
# Set slider to default to that max date
@app.callback(Output("date-slider", "value"), [Input("intermediate-value", "children")])
def make_slider(household_network):
if household_network:
formatted_dates = {
int(k): v
for k, v in json.loads(household_network)["formatted_dates"].items()
}
return max(formatted_dates.keys())
# Display network as determined by slider positions
@app.callback(
Output("household-network", "elements"),
[Input("intermediate-value", "children"), Input("date-slider", "value")],
)
def update_date(household_network, date):
household_network = json.loads(household_network)
time_networks = household_network["time_networks"]
step_keys = household_network["step_keys"]
step = int(step_keys[str(date)])
return time_networks[step]
if __name__ == "__main__":
app.run_server(debug=True)
# app.run_server(host="0.0.0.0", debug=False, port=8080)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment