Convex Lattices#

Components#

Characteristics are derived from raw data using components defined by a JMESPath expression. The find method returns an iterator of values extracted from the raw datum, representing partial imprecision. If the datum cannot be processed, an None value is returned, indicating complete imprecision (i.e., any value within the domain is possible).

[1]:
from galactic.algebras.concept.core import Item
from galactic.algebras.convex.characteristics.core import Component
[2]:
component = Component(expr="value")
component
[2]:
Component(expr='value')
[3]:
dataset = [
    {"value": 5},
    {"value": 10},
    {"value": 12},
    {"value": "15"},
    {},
]
[4]:
component.find(Item(value=dataset[0]))
[4]:
[5]
[5]:
component.find(Item(value=dataset[1]))
[5]:
[10]
[6]:
component.find(Item(value=dataset[2]))
[6]:
[12]
[7]:
component.find(Item(value=dataset[3]))
[7]:
['15']
[8]:
component.find(Item(value=dataset[4]))

Characteristics#

Characteristics are creating using several components and return for each data value a python object computed using the component raw values and some given parameters.

[10]:
characteristic = Integer(components=(component,))
[11]:
<galactic.algebras.convex.characteristics.core.Integer object at 0x7a87dd47ef80>
[12]:
[characteristic(Item(value=value)) for value in dataset]
[12]:
[[5], [10], [12], [15], None]

Predicates#

Predicates are creating using several characteristics and return for each data value either TernaryBool.TRUE, TernaryBool.FALSE or TernaryBool.UNKNOWN (representing undecidability).

[14]:
dataset = [
    12,
    13,
    18,
    True,
    "abc",
]
predicate = Multiple(space=(Integer(components=(Component(),)),), number=4)
[15]:
[15]:
<galactic.algebras.convex.predicates.examples.arithmetic.core.Multiple object at 0x7a87dd47e080>
[16]:
[16]:
'M(int(@),4)'
[17]:
[predicate(Item(value=data)) for data in dataset]
[17]:
[TernaryBool.TRUE,
 TernaryBool.FALSE,
 TernaryBool.FALSE,
 TernaryBool.FALSE,
 TernaryBool.UNKNOWN]

Description spaces#

Description spaces allow for the precise characterization of a dataset using predicates derived from a convex geometry. Except for certain degenerate cases (such as an empty dataset to describe), the set of descriptors is unique.

[19]:
description = DivisorDescription(space=(Integer(components=(Component(),)),))
[20]:
[20]:
<galactic.algebras.convex.descriptions.examples.arithmetic.core.DivisorDescription object at 0x7a87dd47e440>
[21]:
dataset = [
    12,
    13,
    18,
    True,
    "abc",
]
[22]:
[
    str(predicate)
    for predicate in description(
        [
            Item(value=dataset[0]),
            Item(value=dataset[1]),
            Item(value=dataset[2]),
            Item(value=dataset[3]),
            Item(value=dataset[4]),
        ],
    )
]
[22]:
[]
[23]:
[
    str(predicate)
    for predicate in description(
        [
            Item(value=dataset[0]),
            Item(value=dataset[2]),
        ],
    )
]
[23]:
['D(int(@),36)']
[24]:
[str(predicate) for predicate in description([Item(value=dataset[0])])]
[24]:
['D(int(@),12)']
[25]:
[str(predicate) for predicate in description([Item(value=dataset[2])])]
[25]:
['D(int(@),18)']

Concepts#

Convex concepts consist of a pair (extent, intent), where the extent is the maximal set of elements satisfying all the predicates contained in the intent (and conversely, the intent is the maximal set of predicates satisfied by all the elements contained in the extent).

Generally, in the case of convex concepts, the set of predicates contained in the intent can be infinite, and therefore it cannot be traversed, and its length cannot be calculated. Convex concepts are then described by a minimal set of predicates, called the borders, which are necessary and sufficient to uniquely describe the data of the elements in the extent. Some degenerate cases may occur, such as the case of the empty set; these are handled by appropriate predicates.

[27]:
characteristic = Integer(components=(Component(),))
descriptions = (
    DivisorDescription(space=(characteristic,)),
    MultipleDescription(space=(characteristic,)),
)
dataset = [36, 48, 16, 32]
context = Context(
    ItemUniverse(dataset),
    PredicateUniverse(descriptions=descriptions),
)
connection = GaloisConnection(context)
[28]:
top = Concept(connection=connection)
top
[28]:
<galactic.algebras.convex.descriptions.core.Concept object at 0x7a87dd3f66c0>
[29]:
top.extent
[29]:
<galactic.algebras.convex.descriptions.core.Extent object at 0x7a87dd461340>
[30]:
top.intent
[30]:
<galactic.algebras.convex.descriptions.core.Intent object at 0x7a87dd3de680>
[31]:
[item.value for item in top.extent]
[31]:
[36, 48, 16, 32]
[32]:
[str(predicate) for predicate in top.intent]
[32]:
['D(int(@),288)', 'M(int(@),4)']
[33]:
bottom = Concept(connection=connection, items=[])
bottom
[33]:
<galactic.algebras.convex.descriptions.core.Concept object at 0x7a87dd47ef30>
[34]:
[item.value for item in bottom.extent]
[34]:
[]
[35]:
[str(predicate) for predicate in bottom.intent]
[35]:
['⊥', '⊥']
[36]:
concept1 = Concept(
    connection=connection,
    items=[context.domain[1], context.domain[2]],
)
[37]:
[item.value for item in concept1.extent]
[37]:
[48, 16]
[38]:
[str(predicate) for predicate in concept1.intent]
[38]:
['D(int(@),48)', 'M(int(@),16)']
[39]:
Multiple(space=descriptions[1].space, number=4) in concept1.intent
[39]:
True
[40]:
Multiple(space=descriptions[1].space, number=3) in concept1.intent
[40]:
False
[41]:
concept2 = Concept(connection=connection, items=[context.domain[0]])
[42]:
meet = concept1 & concept2
[43]:
[item.value for item in meet.extent]
[43]:
[]
[44]:
[str(predicate) for predicate in meet.intent]
[44]:
['⊥', '⊥']
[45]:
join = concept1 | concept2
[46]:
[item.value for item in join.extent]
[46]:
[36, 48, 16]
[47]:
[str(predicate) for predicate in join.intent]
[47]:
['D(int(@),144)', 'M(int(@),4)']

Convex Lattices#

Convex lattices are a collection of convex concepts that form a lattice structure, where each concept can be combined with others to form new concepts through meet and join operations. The lattice is defined by the Galois connection between the extent and intent of the concepts.

[48]:
from galactic.algebras.concept.core import ExtensibleLattice
from galactic.algebras.concept.renderer import (
    ConceptRenderer,
    ConceptHasseDiagramDrawer,
)
from galactic.algebras.convex.descriptions.examples.arithmetic.core import (
    DivisorDescription,
    MultipleDescription,
)
[49]:
characteristic = Integer(components=(Component(),))
descriptions = (
    DivisorDescription(space=(characteristic,)),
    MultipleDescription(space=(characteristic,)),
)
dataset = {str(n): n for n in (36, 48, 16, 32, 12, 24, 18, 30, 42, 60)}
context = Context(
    ItemUniverse(dataset),
    PredicateUniverse(descriptions=descriptions),
)
connection = GaloisConnection(context)
lattice = ExtensibleLattice()
drawer = ConceptHasseDiagramDrawer(domain_renderer=ConceptRenderer())
[50]:
drawer.draw(lattice)
[50]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_55_0.svg
[51]:
lattice.extend([Concept(connection=connection)])
drawer.draw(lattice)
[51]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_56_0.svg
[52]:
lattice.extend([Concept(connection=connection, items=[context.domain[0]])])
drawer.draw(lattice)
[52]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_57_0.svg
[53]:
lattice.extend([Concept(connection=connection, items=[context.domain[1]])])
drawer.draw(lattice)
[53]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_58_0.svg
[54]:
lattice.extend([Concept(connection=connection, items=[context.domain[2]])])
drawer.draw(lattice)
[54]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_59_0.svg
[55]:
lattice.extend([Concept(connection=connection, items=[context.domain[3]])])
drawer.draw(lattice)
[55]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_60_0.svg
[56]:
lattice.extend([Concept(connection=connection, items=[context.domain[4]])])
drawer.draw(lattice)
[56]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_61_0.svg
[57]:
lattice.extend([Concept(connection=connection, items=[context.domain[5]])])
drawer.draw(lattice)
[57]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_62_0.svg
[58]:
lattice.extend([Concept(connection=connection, items=[context.domain[6]])])
drawer.draw(lattice)
[58]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_63_0.svg
[59]:
lattice.extend([Concept(connection=connection, items=[context.domain[7]])])
drawer.draw(lattice)
[59]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_64_0.svg
[60]:
lattice.extend([Concept(connection=connection, items=[context.domain[8]])])
drawer.draw(lattice)
[60]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_65_0.svg
[61]:
lattice.extend([Concept(connection=connection, items=[context.domain[9]])])
drawer.draw(lattice)
[61]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_66_0.svg

Strategies#

Strategies are algorithms for constructing convex lattices. They take as input a concept and return an iterable of sub-concepts that are related to the input concept.

By iteratively applying a strategy to the concepts in the lattice, we can construct the entire lattice.

[62]:
from galactic.algebras.convex.strategies.core import NaiveStrategy, RecursiveStrategy

lattice = ExtensibleLattice()
drawer = ConceptHasseDiagramDrawer(domain_renderer=ConceptRenderer())
drawer.draw(lattice)
[62]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_68_0.svg
[63]:
top = Concept(connection=connection)
lattice.extend([top])
drawer.draw(lattice)
[63]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_69_0.svg
[64]:
strategy = NaiveStrategy(descriptions=descriptions)
lattice.extend(list(strategy(top)))
drawer.draw(lattice)
[64]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_70_0.svg
[65]:
strategy = RecursiveStrategy(strategies=(strategy,))
lattice.extend(list(strategy(top)))
drawer.draw(lattice)
[65]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_71_0.svg

Measures#

The SelectiveStrategy is a measure-driven strategy that applies an inner strategy and keeps only sub-concepts whose measure is at least ratio * best_measure, where best_measure is the highest measure value among all generated sub-concepts.

This is useful when you want to explore only the most relevant branches of the lattice, based on a quality criterion such as support.

With ratio=1, only sub-concepts reaching the maximum support are kept:

[67]:
selective_best = SelectiveStrategy(
    strategies=(NaiveStrategy(descriptions=descriptions),),
    measure=support,
    ratio=1,
)
top = Concept(connection=connection)
selected_best = list(selective_best(top))
display(f"ratio=1: {len(selected_best)} concepts kept (only the best)")
for concept in selected_best:
    display(f"  support={support(concept, top):.0f}", end="")
    display(f"  items={[item.value for item in concept.extent]}")
'ratio=1: 2 concepts kept (only the best)'
'  support=9'
'  items=[36, 48, 16, 12, 24, 18, 30, 42, 60]'
'  support=9'
'  items=[36, 48, 16, 32, 12, 24, 18, 30, 60]'

With ratio=0.8, sub-concepts within 80% of the best support are kept:

[68]:
selective_80 = SelectiveStrategy(
    strategies=(NaiveStrategy(descriptions=descriptions),),
    measure=support,
    ratio=0.8,
)
selected_80 = list(selective_80(top))
display(f"ratio=0.8: {len(selected_80)} concepts kept")
for concept in selected_80:
    display(f"  support={support(concept, top):.0f}", end="")
    display(f"  items={[item.value for item in concept.extent]}")
'ratio=0.8: 4 concepts kept'
'  support=9'
'  items=[36, 48, 16, 12, 24, 18, 30, 42, 60]'
'  support=9'
'  items=[36, 48, 16, 32, 12, 24, 18, 30, 60]'
'  support=8'
'  items=[48, 16, 32, 12, 24, 30, 42, 60]'
'  support=8'
'  items=[36, 48, 16, 32, 12, 24, 18, 42]'

With ratio=0, all sub-concepts are kept regardless of their measure, yielding the same result as NaiveStrategy:

[69]:
selective_all = SelectiveStrategy(
    strategies=(NaiveStrategy(descriptions=descriptions),),
    measure=support,
    ratio=0,
)
selected_all = list(selective_all(top))
display(f"ratio=0: {len(selected_all)} concepts kept (all, like NaiveStrategy)")
for concept in selected_all:
    display(f"  support={support(concept, top):.0f}", end="")
    display(f"  items={[item.value for item in concept.extent]}")
'ratio=0: 4 concepts kept (all, like NaiveStrategy)'
'  support=8'
'  items=[48, 16, 32, 12, 24, 30, 42, 60]'
'  support=9'
'  items=[36, 48, 16, 12, 24, 18, 30, 42, 60]'
'  support=8'
'  items=[36, 48, 16, 32, 12, 24, 18, 42]'
'  support=9'
'  items=[36, 48, 16, 32, 12, 24, 18, 30, 60]'

We can also combine SelectiveStrategy with RecursiveStrategy to traverse the full lattice while pruning low-quality branches:

[70]:
selective_recursive = RecursiveStrategy(
    strategies=(
        SelectiveStrategy(
            strategies=(NaiveStrategy(descriptions=descriptions),),
            measure=support,
            ratio=1,
        ),
    ),
)
lattice2 = ExtensibleLattice()
top = Concept(connection=connection)
all_selective = list(selective_recursive(top))
lattice2.extend([top, *all_selective])
display(f"Selective recursive lattice: {len(lattice2)} concepts")
drawer.draw(lattice2)
'Selective recursive lattice: 24 concepts'
[70]:
../../../../../_images/share_galactic_algebras_convex_notebooks_notebook_81_1.svg