From 97f5d380a40e2f6fb228a992b17f6e54ffaed310 Mon Sep 17 00:00:00 2001 From: agp8x Date: Mon, 7 Aug 2017 12:54:24 +0200 Subject: [PATCH] add locomotion-action analyzer --- analyzer/__init__.py | 1 + analyzer/analyzer.py | 58 +++++++++++++++++++--- analyzer/biogames.py | 36 ++++++++++++-- analyzer/locomotion_action.py | 93 +++++++++++++++++++++++++++++++++++ biogames2.json | 30 +++++++++-- log_analyzer.py | 27 +++++++--- util/__init__.py | 1 + util/iter.py | 18 +++++++ 8 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 analyzer/locomotion_action.py create mode 100644 util/__init__.py create mode 100644 util/iter.py diff --git a/analyzer/__init__.py b/analyzer/__init__.py index 826acb7..9bec4e6 100644 --- a/analyzer/__init__.py +++ b/analyzer/__init__.py @@ -1,2 +1,3 @@ from .analyzer import * from .biogames import * +from .locomotion_action import * diff --git a/analyzer/analyzer.py b/analyzer/analyzer.py index 25ce8e5..dab3ad8 100644 --- a/analyzer/analyzer.py +++ b/analyzer/analyzer.py @@ -1,5 +1,5 @@ import json -from collections import defaultdict +from collections import defaultdict, Iterable from log_analyzer import LogSettings @@ -11,21 +11,31 @@ class Analyzer: def process(self, entry: dict) -> bool: raise NotImplementedError() - def result(self) -> object: + def result(self) -> Iterable: raise NotImplementedError() + def name(self): + return self.__name__ + class LocationAnalyzer(Analyzer): + """ + store spatial log entries + """ entries = [] + __name__ = "Location" + + class Formats: + geojson = 0 def __init__(self, settings: LogSettings): super().__init__(settings) - def result(self) -> object: + def result(self) -> list: return self.entries - def render(self, format="geojson"): - if format is "geojson": + def render(self, format: int = Formats.geojson): + if format is self.Formats.geojson: return json.dumps([entry['location']['coordinates'] for entry in self.entries]) raise NotImplementedError() @@ -36,7 +46,12 @@ class LocationAnalyzer(Analyzer): class LogEntryCountAnalyzer(Analyzer): - def result(self) -> object: + """ + count occurrences of log entry types + """ + __name__ = "LogEntryCount" + + def result(self) -> defaultdict: return self.store def process(self, entry: dict) -> bool: @@ -48,3 +63,34 @@ class LogEntryCountAnalyzer(Analyzer): self.store = defaultdict(lambda: 0) +class LogEntrySequenceAnalyzer(Analyzer): + """ + store sequence of all log entry types + """ + __name__ = "LogEntrySequence" + + def result(self) -> list: + return self.store + + def process(self, entry: dict) -> bool: + entry_type = entry[self.settings.entry_type] + self.store.append(entry_type) + return False + + def __init__(self, settings: LogSettings): + super().__init__(settings) + self.store = [] + + +class ActionSequenceAnalyzer(LogEntrySequenceAnalyzer): + """ + find sequence of non-spatial log entry types + """ + __name__ = "ActionSequenceAnalyzer" + + def process(self, entry: dict) -> bool: + entry_type = entry[self.settings.entry_type] + if entry_type in self.settings.spatials: + return False + self.store.append(entry_type) + return False diff --git a/analyzer/biogames.py b/analyzer/biogames.py index 44e7f94..e97a4e8 100644 --- a/analyzer/biogames.py +++ b/analyzer/biogames.py @@ -6,13 +6,41 @@ from .analyzer import Analyzer class BoardDurationAnalyzer(Analyzer): - def result(self) -> object: - return self.store + """ + calculate display duration of boards + """ + __name__ = "BoardDuration" + + def render(self) -> str: + return "\n".join(["{}\t{}".format(entry["active"], entry["id"]) for entry in self.result()]) + + def result(self) -> list: + result = [] + last_timestamp = None + last_board = None + for board in self.store: + board_id, timestamp = board["id"], board["timestamp"] + + if not last_timestamp is None: + result.append(self.save_entry(last_board, last_timestamp, timestamp - last_timestamp)) + last_timestamp = timestamp + last_board = board_id + # TODO: last board? + return result def process(self, entry: dict) -> bool: - self.store[entry[self.settings.entry_type]] += 1 + entry_type = entry[self.settings.entry_type] + if entry_type in self.settings.boards: + self.store.append(self.save_entry(entry["board_id"], entry["timestamp"])) # TODO: make configurable return False + def save_entry(self, board_id: str, timestamp: int, active: int = None): + entry = {"id": board_id, "timestamp": timestamp} + if not active is None: + entry["active"] = active + return entry + def __init__(self, settings: LogSettings): super().__init__(settings) - self.store = defaultdict(lambda: 0) + self.store = [] + self.last = {} diff --git a/analyzer/locomotion_action.py b/analyzer/locomotion_action.py new file mode 100644 index 0000000..b2e3fab --- /dev/null +++ b/analyzer/locomotion_action.py @@ -0,0 +1,93 @@ +from collections import defaultdict + +from log_analyzer import LogSettings + +from .analyzer import Analyzer +from util import combinate + + +def init_filter(settings: LogSettings, state: str) -> callable: + # this implies OR for lists; AND for dicts + if type(settings.sequences[state]) in (str, list): + return lambda entry: entry[settings.entry_type] in settings.sequences[state] + else: + return lambda entry: combinate(settings.sequences[state], entry) + + +class LocomotionActionAnalyzer(Analyzer): # TODO + """ + calculate locomation/action times and ratio + + Anything between LogEntryCache and CacheEnableAction + is counted as ActionTime, the rest as LocomotionTime. + """ + __name__ = "LocomotionAction" + + def process(self, entry: dict) -> bool: + self.last_timestamp = entry["timestamp"] + if self.instance_start is None: + self.instance_start = self.last_timestamp + self.last = self.last_timestamp + if self.cache_time is None: + self.cache_time = self.last_timestamp + offset = self.last_timestamp - self.cache_time + + if self.current_cache is None and self.filter_start(entry): + if entry['cache'] is None: + return False + self.current_cache = entry['cache']['@id'] + self.cache_time = self.last_timestamp + self.locomotion.append(offset) + self.last = None + elif self.current_cache and self.filter_end(entry): + self.actions.append(offset) + self.cache_time = self.last_timestamp + self.current_cache = None + self.last = None + + def result(self) -> dict: + if self.last is not None: + if self.current_cache is None: + self.locomotion.append(self.last - self.cache_time) + else: + self.actions.append(self.last - self.cache_time) + locomotion = sum(self.locomotion) + action = sum(self.actions) + return { + 'loco_sum': locomotion, + 'action_sum': action, + 'loco': self.locomotion, + 'act': self.actions, + 'dur': (self.last_timestamp - self.instance_start) + } + + def __init__(self, settings: LogSettings): + super().__init__(settings) + self.filter_start = init_filter(settings, "start") + self.filter_end = init_filter(settings, "end") + self.current_cache = None + self.cache_time = None + self.locomotion = [] + self.actions = [] + self.instance_start = None + self.last_timestamp = None + self.last = None + + +class CacheSequenceAnalyzer(Analyzer): # TODO + __name__ = "CacheSequence" + + def process(self, entry: dict) -> bool: + if self.filter(entry): + if entry['cache']: + self.store.append((entry['timestamp'],entry['cache']['@id'])) + else: + self.store.append((entry['timestamp'],entry['cache'])) + + def result(self) -> list: + return self.store + + def __init__(self, settings: LogSettings): + super().__init__(settings) + self.store = [] + self.filter = init_filter(settings, "start") diff --git a/biogames2.json b/biogames2.json index 1943eaa..e1f3f3f 100644 --- a/biogames2.json +++ b/biogames2.json @@ -1,14 +1,36 @@ { "logFormat": "sqlite", "entryType": "@class", - "spatials":["de.findevielfalt.games.game2.instance.log.entry.LogEntryLocation"], - "actions":["...QuestionAnswerEvent", "...SimuAnswerEvent"], + "spatials": [ + "de.findevielfalt.games.game2.instance.log.entry.LogEntryLocation" + ], + "actions": [ + "...QuestionAnswerEvent", + "...SimuAnswerEvent" + ], + "boards": [ + "de.findevielfalt.games.game2.instance.log.entry.ShowBoardLogEntry" + ], "analyzers": { "analyzer": [ - "LocationAnalyzer" - ], + "LocationAnalyzer", + "LogEntryCountAnalyzer", + "LogEntrySequenceAnalyzer", + "ActionSequenceAnalyzer" + ], "analyzer.biogames": [ "BoardDurationAnalyzer" + ], + "analyzer.locomotion_action": [ + "LocomotionActionAnalyzer", + "CacheSequenceAnalyzer" ] + }, + "sequences": { + "start": "de.findevielfalt.games.game2.instance.log.entry.LogEntryCache", + "end": { + "@class": "de.findevielfalt.games.game2.instance.log.entry.LogEntryInstanceAction", + "action.@class": "de.findevielfalt.games.game2.instance.action.CacheEnableAction" + } } } \ No newline at end of file diff --git a/log_analyzer.py b/log_analyzer.py index 0e80f02..cb26911 100644 --- a/log_analyzer.py +++ b/log_analyzer.py @@ -10,15 +10,19 @@ class LogSettings: spatials = None actions = None analyzers = [] + boards = None + sequences = None def __init__(self, json_dict): self.log_format = json_dict['logFormat'] self.entry_type = json_dict['entryType'] self.spatials = json_dict['spatials'] self.actions = json_dict['actions'] + self.boards = json_dict['boards'] for mod in json_dict['analyzers']: for name in json_dict['analyzers'][mod]: self.analyzers.append(getattr(sys.modules[mod], name)) + self.sequences = json_dict['sequences'] def __repr__(self): return str({ @@ -26,11 +30,13 @@ class LogSettings: "entryType": self.entry_type, "spatials": self.spatials, "actions": self.actions, - "analyzers": self.analyzers + "analyzers": self.analyzers, + "boards": self.boards, + "sequences": self.sequences, }) -def load_settings(file:str) -> LogSettings: +def load_settings(file: str) -> LogSettings: return LogSettings(json.load(open(file))) @@ -51,8 +57,17 @@ if __name__ == '__main__': if analyzer.process(entry): break for analyzer in analyzers: - print("* Result for " + str(type(analyzer))) + print("* Result for " + analyzer.name()) print(analyzer.result()) - coords = analyzers[0].render() - with open("test.js", "w") as out: - out.write("coords = "+coords) \ No newline at end of file + #for analyzer in analyzers: + # if analyzer.name() in ["LogEntryCount", "ActionSequenceAnalyzer"]: + # print(json.dumps(analyzer.result(), indent=2)) + + #for analyzer in analyzers: + # if analyzer.name() in ["BoardDuration"]: + # print(json.dumps(analyzer.result(), indent=2)) + # print(analyzer.render()) + + # coords = analyzers[1].render() + # with open("test.js", "w") as out: + # out.write("coords = "+coords) diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..3a55a2f --- /dev/null +++ b/util/__init__.py @@ -0,0 +1 @@ +from .iter import * \ No newline at end of file diff --git a/util/iter.py b/util/iter.py new file mode 100644 index 0000000..9ad787a --- /dev/null +++ b/util/iter.py @@ -0,0 +1,18 @@ +def json_path(obj: dict, key: str): + """Query a nested dict with a dot-separated path""" + if not type(obj) is dict: + return None + if "." not in key: + return obj[key] + child_key = key.split(".") + if child_key[0] not in obj: + return None + return json_path(obj[child_key[0]], ".".join(child_key[1:])) + + +def combinate(settings: dict, entry:dict)-> bool: + """combine all settings {: } with entry using AND""" + result = True + for key, value in settings.items(): + result = result and json_path(entry, key) == value + return result \ No newline at end of file