#!/usr/bin/env python
import enum
from enum import Enum
import functools
import logging
from typing import Any, cast, Dict, Optional
import pandas as pd
from pandas.api.types import CategoricalDtype
logger = logging.getLogger(__name__)
[docs]@functools.total_ordering
@enum.unique
class CompassBearing(Enum):
NORTH = 0 # noqa: E221
NORTH_EAST = 1
EAST = 2 # noqa: E221
SOUTH_EAST = 3
SOUTH = 4 # noqa: E221
SOUTH_WEST = 5
WEST = 6 # noqa: E221
NORTH_WEST = 7
[docs] @staticmethod
def parse(value: Any) -> Optional['CompassBearing']:
if value is None:
return None
if type(value) == CompassBearing: # pylint: disable=C0123
return cast(CompassBearing, value)
if not isinstance(value, str):
raise ValueError(f'Failed to parse {CompassBearing.__name__}: "{value}"')
value = value.upper()
for bearing in CompassBearing:
if value == bearing.name:
return bearing
if value == bearing.to_short_code():
return bearing
raise ValueError(f'Failed to parse {CompassBearing.__name__}: "{value}"')
[docs] def to_short_code(self):
return ''.join([p[0] for p in str(self.name).split('_')])
def __lt__(self, other):
if other is None:
return False
if isinstance(other, int):
other = CompassBearing(other)
if type(other) != type(self): # pylint: disable=C0123
logger.warning('Attempt to compare: %s with %s', self, other)
raise NotImplementedError
return (self.value < other.value) # pylint: disable=C0325,W0143
def __repr__(self):
return f'{self.__class__.__name__}: {self}'
def __str__(self):
return self.name
[docs] @staticmethod
def dtype():
return CategoricalDtype(categories=[s.name for s in CompassBearing], ordered=True)
[docs]@functools.total_ordering
@enum.unique
class Present(Enum):
NOT_PRESENT = 0
PRESENT = 1 # noqa: E221
[docs] @staticmethod
def parse(value: Any) -> Optional['Present']:
if value is None:
return None
if isinstance(value, str) and value.upper() == 'NA':
return None
if type(value) == Present: # pylint: disable=C0123
return cast(Present, value)
if isinstance(value, bool):
return Present.PRESENT if value else Present.NOT_PRESENT
value = float(value)
if value == 0.0:
return Present.NOT_PRESENT
if value > 0.0:
return Present.PRESENT
raise ValueError(f'Failed to parse {Present.__name__}: "{value}"')
def __lt__(self, other):
if other is None:
return False
if isinstance(other, int):
other = Present(other)
if type(other) != type(self): # pylint: disable=C0123
logger.warning('Attempt to compare: %s with %s', self, other)
raise NotImplementedError
return (self.value < other.value) # pylint: disable=C0325,W0143
def __repr__(self):
return f'{self.__class__.__name__}: {self}'
def __str__(self):
return self.name
[docs] @staticmethod
def dtype():
return CategoricalDtype(categories=[s.name for s in Present], ordered=True)
[docs]@functools.total_ordering
@enum.unique
class BodyPosition(Enum):
SUPINE = 0 # noqa: E221
SUPINE_FLEXED_LEGS = 1 # noqa: E221
CROUCHED = 2 # noqa: E221
CROUCHED_LEFT_SIDE = 3 # noqa: E221
CROUCHED_RIGHT_SIDE = 4
STOMACH = 5 # noqa: E221
[docs] @staticmethod
def parse(value: Any) -> Optional['BodyPosition']:
if value is None:
return None
if isinstance(value, str) and value.upper() == 'NA':
return None
if type(value) == BodyPosition: # pylint: disable=C0123
return cast(BodyPosition, value)
if isinstance(value, str):
value = value.upper()
for position in BodyPosition:
if value == position.name:
return position
if value == position.value:
return position
raise ValueError(f'Failed to parse {BodyPosition.__name__}: "{value}"')
def __lt__(self, other):
if other is None:
return False
if isinstance(other, int):
other = BodyPosition(other)
if type(other) != type(self): # pylint: disable=C0123
logger.warning('Attempt to compare: %s with %s', self, other)
raise NotImplementedError
return (self.value < other.value) # pylint: disable=C0325,W0143
def __repr__(self):
return f'{self.__class__.__name__}: {self}'
def __str__(self):
return self.name
[docs] @staticmethod
def dtype():
return CategoricalDtype(categories=[s.name for s in BodyPosition], ordered=True)
KNOWN_GROUPS = {
'utilitarian' : set(['knife', 'whetstone', 'awl', 'scissors', 'vessel', 'pot_sherd', 'flint', 'flakes', 'flint_flakes']), # noqa: E203
'textile' : set(['textile', 'needle', 'spindle_whorl']), # noqa: E203
'equestrian' : set(['equestrian', 'horse_equipment']), # noqa: E203
'economic' : set(['coins', 'scales']), # noqa: E203
'organic_material': set(['animal_remains', 'charcoal', 'flower', 'shell', 'burned_bones']),
'appearance' : set(['brooch', 'cloak ring', 'bracelet', 'beads', 'comb', 'buckle', 'mounts', 'cloak_fastener', 'iron_ring']), # noqa: E203
'burial_container': set(['coffin', 'coffin_substitute']),
'weapons' : set(['sword', 'axe', 'shield_boss', 'spear']), # noqa: E203
'iron_fragment': set(['iron frafments', 'nails', 'iron_object', 'iron_nail/rivets']),
'miscellaneous': set(['lock', 'keys', 'thors_hammer', 'bronze_item', 'bronze_disk', 'quartz', 'unidentified_bronze', 'iron_pole']),
}
[docs]class Context(object):
"""docstring for Context"""
def __init__(self, body_position: Optional[BodyPosition], body_orientation: Optional[CompassBearing], disturbed: Optional[Present], decapitation: Optional[Present], double_grave: Optional[Present], stone_layer: Optional[Present], grave_goods: Dict[str, Optional[Any]]):
if body_position is not None and not isinstance(body_position, BodyPosition):
raise ValueError(f'Invalid body_position: "{body_position}"')
self.body_position = body_position
if body_orientation is not None and not isinstance(body_orientation, CompassBearing):
raise ValueError(f'Invalid body_orientation: "{body_orientation}"')
self.body_orientation = body_orientation
self.disturbed = disturbed
self.decapitation = decapitation
self.double_grave = double_grave
self.stone_layer = stone_layer
self.grave_goods = {k.lower(): Present.parse(v) for k, v in grave_goods.items()}
countable_goods = [float(v) for v in grave_goods.values() if Present.parse(v) is not None] # type: ignore
self.grave_goods_total = sum(countable_goods) if len(countable_goods) > 0 else None
[docs] @staticmethod
def empty():
return Context(None, None, None, None, None, None, {})
[docs] @staticmethod
def group(value):
value = value.lower()
groups = set()
for group_name, group in KNOWN_GROUPS.items():
if value in group:
groups.add(group_name)
return groups
[docs] def to_pd_data_frame(self, index):
data = {
'id': pd.Series([index]),
}
data['body_position_cat'] = pd.Series([self.body_position.name if self.body_position else None], copy=True, dtype=BodyPosition.dtype())
data['body_position_val'] = pd.Series([self.body_position.value if self.body_position else None], copy=True, dtype='Int64')
data['body_orientation_cat'] = pd.Series([self.body_orientation.name if self.body_orientation else None], copy=True, dtype=CompassBearing.dtype())
data['body_orientation_val'] = pd.Series([self.body_orientation.value if self.body_orientation else None], copy=True, dtype='Int64')
data['disturbed_cat'] = pd.Series([self.disturbed.name if self.disturbed else None], copy=True, dtype=Present.dtype())
data['decapitation_cat'] = pd.Series([self.decapitation.name if self.decapitation else None], copy=True, dtype=Present.dtype())
data['double_grave_cat'] = pd.Series([self.double_grave.name if self.double_grave else None], copy=True, dtype=Present.dtype())
data['stone_layer_cat'] = pd.Series([self.stone_layer.name if self.stone_layer else None], copy=True, dtype=Present.dtype())
for key, value in self.grave_goods.items():
data[f'all_{key}_cat'] = pd.Series([value.name if value else None], copy=True, dtype=Present.dtype())
data[f'all_{key}_val'] = pd.Series([value.value if value else None], copy=True, dtype='Int64')
per_group_count = None
for group_name, group in KNOWN_GROUPS.items():
status_set = {v for k, v in self.grave_goods.items() if k in group and v}
if Present.PRESENT in status_set:
present = Present.PRESENT
per_group_count = per_group_count + 1 if per_group_count else 1
elif Present.NOT_PRESENT in status_set:
present = Present.NOT_PRESENT
per_group_count = per_group_count + 0 if per_group_count else 0
else:
present = None
data[f'{group_name}_cat'] = pd.Series([present.name if present else None], copy=True, dtype=Present.dtype())
data[f'{group_name}_val'] = pd.Series([present.value if present else None], copy=True, dtype='Int64')
data['total_grave_goods'] = pd.Series([self.grave_goods_total], copy=True, dtype='Int64')
data['total_grave_goods_indicator'] = pd.Series([per_group_count], copy=True, dtype='Int64')
return pd.DataFrame.from_dict(data).set_index('id')
if __name__ == "__main__":
raise RuntimeError('No main available')