Skip to content

Commit

Permalink
Merge branch 'dev-v0.0.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
lanecodes committed Mar 14, 2019
2 parents 4250c70 + fea68e6 commit 58b3e6a
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 31 deletions.
2 changes: 2 additions & 0 deletions cymod/__init__.py
Expand Up @@ -40,6 +40,8 @@
"""
from __future__ import print_function

__version__ = "0.0.5"

from cymod.load import ServerGraphLoader
from cymod.load import EmbeddedGraphLoader
from cymod.params import read_params_file
Expand Down
9 changes: 6 additions & 3 deletions cymod/load.py
Expand Up @@ -55,7 +55,7 @@ def load_cypher(self, root_dir, cypher_file_suffix=None,
self._load_job_queue.append(cff)

def load_tabular(self, df, start_state_col, end_state_col, labels=None,
global_params=None):
global_params=None, state_alias_translator=None):
"""Generate Cypher queries based data in a :obj:`pandas.DataFrame`.
Args:
Expand All @@ -66,10 +66,13 @@ def load_tabular(self, df, start_state_col, end_state_col, labels=None,
end_state_col (str): Name of the column specifying the end state of
the transition described by each row.
global_params (dict, optional): property name/ value pairs which
will be added as parameters to every query.
will be added as parameters to every query.
state_alias_translator (:obj:`EnvrStateAliasTranslator`): Container
for translations from codes to human readable values.
"""
tabular_src = TransTableProcessor(df, start_state_col, end_state_col,
labels=labels, global_params=global_params)
labels=labels, global_params=global_params,
state_alias_translator=state_alias_translator)
self._load_job_queue.append(tabular_src)

def iterqueries(self):
Expand Down
112 changes: 94 additions & 18 deletions cymod/tabproc.py
Expand Up @@ -9,6 +9,7 @@
import re
import json
import collections
import warnings

import six
import pandas as pd
Expand All @@ -17,6 +18,59 @@
from cymod.cybase import CypherQuery, CypherQuerySource
from cymod.customise import NodeLabels

class EnvrStateAliasTranslator(object):
"""Container for translations from codes to human readable values.
Sometimes it is useful to specify a transition table using codes rather
than human readable aliases. This class contains data which is used by a
:obj:`TransTableProcessor` to perform this translation.
Attrs:
state_aliases (dict): Mapping from codes identifying states to names.
cond_aliases (:obj:`collections.OrderedDict`): Mappings from conditions
and their codes to corresponding values.
"""

def __init__(self):
self.state_aliases = {}
self.cond_aliases = collections.OrderedDict()

def add_cond_aliases(self, cond_name, trans_dict):
"""Add code-to-name translation for a state condition.
Args:
cond_name (str): Name for the environmental condition.
trans_dict (dict): Dictionary containing mappings from codes to
values for this envrionmental condition.
"""
self.cond_aliases[cond_name] = trans_dict

@property
def all_conds(self):
return list(self.cond_aliases.keys())

def state_alias(self, state_code):
"""Return the name of the state with the given code."""
try:
return self.state_aliases[state_code]
except KeyError:
raise ValueError("No alias specified for state with code '{0}'."\
.format(state_code))

def cond_alias(self, cond_name, cond_code):
"""Return the name of the state condition value with the given code."""
try:
self.cond_aliases[cond_name]
except KeyError:
raise ValueError("No aliases specified for condition '{0}'."\
.format(cond_name))

try:
return self.cond_aliases[cond_name][cond_code]
except KeyError:
raise ValueError(("No alias specified for condition '{0}' with "
+ "value '{1}'.").format(cond_name, cond_code))

class TransTableProcessor(object):
"""Processes a :obj:`pandas.DataFrame` and produces Cypher queries.
Expand All @@ -32,7 +86,7 @@ class TransTableProcessor(object):
"""

def __init__(self, df, start_state_col, end_state_col, labels=None,
global_params=None):
global_params=None, state_alias_translator=None):
"""
Args:
df (:obj:`pandas.DataFrame`): Table containing data which will be
Expand All @@ -49,12 +103,18 @@ def __init__(self, df, start_state_col, end_state_col, labels=None,
Condition nodes are labelled in the generated graph.
global_params (dict, optional): property name/ value pairs which
will be added as parameters to every query.
state_alias_translator (:obj:`EnvrStateAliasTranslator`): Container
for translations from codes to human readable values.
"""
self.df = df
self.start_state_col = start_state_col
self.end_state_col = end_state_col
self.global_params = global_params

if state_alias_translator:
self.df = self._aliased_df_from_codes(df, state_alias_translator)
else:
self.df = df

if labels:
self.labels = labels
else:
Expand Down Expand Up @@ -86,6 +146,37 @@ def _dict_to_cypher_properties(self, dict):
string = re.sub(r"(:)( )([\d\"\'])", r"\1\3", string)
return string[1:-1]

def _aliased_df_from_codes(self, df, translator):
"""Convert codes to aliases in the transition table.
Args:
df (:obj:`pd.DataFrame`): Transition table data.
translator (:obj:`EnvrStateAliasTranslator`): Used to convert codes
in the transition table to human readable aliases.
Returns:
:obj:`pd.DataFrame`: A version of the transition table with state
and condition codes replaced with corresponding names.
"""
aliased_df = df.copy()

# Replace state codes with their names
if translator.state_aliases:
for state_col in [self.start_state_col, self.end_state_col]:
aliased_df[state_col] = aliased_df[state_col]\
.apply(translator.state_alias)

# Replace condition codes with their names
for cond_col in translator.all_conds:
if cond_col not in aliased_df.columns:
warnings.warn(("'{0}' given in translator but not"
+ " found in transition table.").format(cond_col))
else:
aliased_df[cond_col] = aliased_df[cond_col]\
.apply(lambda x: translator.cond_alias(cond_col, x))

return aliased_df

def _add_global_params_to_query_string(self, query_str, global_params):
"""Modify a query string specifying node creation, add parameters.
Expand Down Expand Up @@ -177,19 +268,4 @@ def _row_to_cypher_query(self, row_index, row):

def iterqueries(self):
for i, row in self.df.iterrows():
yield self._row_to_cypher_query(i, row)















yield self._row_to_cypher_query(i, row)
12 changes: 9 additions & 3 deletions docs/cymod.org
Expand Up @@ -243,6 +243,9 @@ gl.commit()


**** UC7: Load model specified by a coded succession transition table into Neo4j server
:PROPERTIES:
:CUSTOM_ID: cymod-UC7
:END:
***** Use case description
Jenny has the same table to load into Neo4j server as Dave (of [[#UC4-trans-table][UC4]] fame) did
but, being of a quantitative bent, Jenny has encoded the data using numerical
Expand All @@ -262,15 +265,18 @@ These codes have the following meanings:
The ~trans_time~ column contains numerical data as was the case in UC4.

***** API usage
:PROPERTIES:
:CUSTOM_ID: cymod-UC7-API
:END:
#+BEGIN_SRC python
import pandas as pd
from cymod import ServerGraphLoader, EnvrStateAliasTranslator

# Set up EnvrStateAliasTranslator and configure with relevant model-specific data
trans = EnvrStateAliasTranslator()
trans.set_state_aliases({0: "state1", 1: "state2", 2: "state3"})
trans.add_condition_aliases("cond1", {0: False, 1: True})
trans.add_condition_aliases("cond2", {0: "low", 1: "high"})
trans.state_aliases = {0: "state1", 1: "state2", 2: "state3"}
trans.add_cond_aliases("cond1", {0: False, 1: True})
trans.add_cond_aliases("cond2", {0: "low", 1: "high"})

# Initialise ServerGraphLoader object
gl = ServerGraphLoader(user="username", password="password")
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -18,7 +18,7 @@
EMAIL = 'ajlane50@gmail.com'
AUTHOR = 'Andrew Lane'
REQUIRES_PYTHON = '==2.7'
VERSION = '0.0.4'
VERSION = '0.0.5'

# What packages are required for this module to be executed?
REQUIRED = [
Expand Down
19 changes: 19 additions & 0 deletions tests/test_load.py
Expand Up @@ -15,6 +15,7 @@
from cymod.cybase import CypherQuery
from cymod.load import GraphLoader, EmbeddedGraphLoader
from cymod.customise import NodeLabels
from cymod.tabproc import EnvrStateAliasTranslator

def touch(path):
"""Immitate *nix `touch` behaviour, creating directories as required."""
Expand Down Expand Up @@ -55,6 +56,14 @@ def setUp(self):
"cond": ["low", "high"]
})

self.demo_coded_table = pd.DataFrame({
"start": [0, 1],
"end": [1, 2],
"cond1": [0, 1],
"cond2": [2, 3],
"cond3": [1, 0]
})

def tearDown(self):
# Remove the temp directory after the test
shutil.rmtree(self.test_dir)
Expand Down Expand Up @@ -213,6 +222,16 @@ def test_custom_labels_applied_for_tabular(self):
self.assertEqual(query_iter.next().statement, query2.statement)
self.assertRaises(StopIteration, query_iter.next)

def test_coded_transition_table_can_be_used(self):
trans = EnvrStateAliasTranslator()

gl = GraphLoader()
try:
gl.load_tabular(self.demo_coded_table, "start", "end",
state_alias_translator=trans)
except Exception:
self.fail("Could not use state_alias_translator in load_tabular.")


class EmbeddedGraphLoaderTestCase(unittest.TestCase):

Expand Down

0 comments on commit 58b3e6a

Please sign in to comment.