-
Notifications
You must be signed in to change notification settings - Fork 29
Add Covering Edges #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
80062c8
ba43c25
c438316
139c3fc
3d8eca6
43351b9
078790a
6aba883
7209db3
ecb99ad
bb037e9
f72bcad
166c677
20fa77f
8907323
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
"""Covering Edges | ||
|
||
cf. Carpineto, Claudio, and Giovanni Romano. | ||
Concept data analysis: Theory and applications. | ||
John Wiley & Sons, 2004. | ||
""" | ||
|
||
import multiprocessing | ||
import itertools | ||
import collections | ||
|
||
from .fcbo import fast_generate_from | ||
|
||
|
||
def covering_edges(concept_list, context, concept_index=None): | ||
"""Yield mapping edge as ``((extent, intent), (lower_extent, lower_intent))`` | ||
pairs (concept and it's lower neighbor) from ``context`` and ``concept_list`` | ||
|
||
Example: | ||
>>> from concepts import make_context, ConceptList | ||
>>> from concepts._common import Concept | ||
mikulatomas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
>>> context = make_context(''' | ||
xflr6 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
... |0|1|2|3|4|5| | ||
... A|X|X|X| | | | | ||
... B|X| |X|X|X|X| | ||
... C|X|X| | |X| | | ||
... D| |X|X| | | |''') | ||
|
||
>>> concepts = [('ABCD', ''), | ||
... ('ABC', '0'), | ||
... ('AC', '01'), | ||
... ('A', '012'), | ||
... ('', '012345'), | ||
... ('C', '014'), | ||
... ('AB', '02'), | ||
... ('B', '02345'), | ||
... ('BC', '04'), | ||
... ('ACD', '1'), | ||
... ('AD', '12'), | ||
... ('ABD', '2')] | ||
|
||
>>> concept_list = ConceptList.frompairs( | ||
... map(lambda c: (context._Objects.frommembers(c[0]), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use of private API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any tips how to avoid that? Problem is that this whole function is probably also for internal use only. Maybe the whole example can be defined other way, originally wanted to avoid using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am confused: Can't the function just use the output of a concept generator like
See the other discussion on avoiding materialization. :) Although, I might be missing something and need to read the paper first.
In case a test fiddles with internals or otherwise has a complex setup, state requirement, or assertions, it should probably be done in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option might be to create the input from scratch in the test (creating the two There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reasons why context is needed are the prime operators and bitset classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right, that is a clever solution, thanks. I will add that and remove context. PS: So do we want to force There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have checked the comments once again. So, do you agree, that class ConceptMappingInfo(typing.NamedTuple):
concept: concepts._common.Concept
lower_neighbors: typing.Tuple[concepts._common.Concept, ...] I am still confused what is the purpose of Other option is (hope I got typing for bitset right): class ConceptMappingInfo(typing.NamedTuple):
extent: bitsets.bases.BitSet
intent: bitsets.bases.BitSet
lower_neighbors: typing.Tuple[typing.Tuple[bitsets.bases.BitSet, bitsets.bases.BitSet], ...] As for input for def covering_edges(extent_intent_pairs: typing.Iterator[typing.Tuple[bitsets.bases.BitSet,
bitsets.bases.BitSet]]):
pass In that case, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea mentioned in #15 (comment) is interesting, but I am worried it would be better to build full lattice from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quick notes: Still nesting (yield quadruples, see earlier discussion)? class ConceptMappingInfo(typing.NamedTuple):
concept: concepts._common.Concept IIUC this differs from lower_neighbors: typing.Tuple[concepts._common.Concept, ...] Same here: lower_neighbors: typing.Tuple[typing.Tuple[bitsets.bases.BitSet, bitsets.bases.BitSet], ...] nit:
We might need to do some more to express the requirement that the bitset pairs need to be ones that where for prime in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You right, that both extent/intent are not required for storing the relationship, so probably something like: class ConceptMappingInfo(typing.NamedTuple):
extent: bitsets.bases.BitSet
intent: bitsets.bases.BitSet
lower_neighbors_extents: typing.Tuple[bitsets.bases.BitSet, ...] Agree with Do you agree that adding Then we can finish the whole code. |
||
... context._Properties.frommembers(c[1])), | ||
... concepts)) | ||
|
||
>>> edges = covering_edges(concept_list, context) | ||
|
||
>>> [(''.join(concept[0].members()), # doctest: +NORMALIZE_WHITESPACE | ||
mikulatomas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
... ''.join(lower[0].members())) | ||
... for concept, lower in edges] | ||
[('ABCD', 'ABC'), | ||
('ABCD', 'ACD'), | ||
('ABCD', 'ABD'), | ||
('ABC', 'AC'), | ||
('ABC', 'AB'), | ||
('ABC', 'BC'), | ||
('AC', 'A'), | ||
('AC', 'C'), | ||
('A', ''), | ||
('C', ''), | ||
('AB', 'A'), | ||
('AB', 'B'), | ||
('B', ''), | ||
('BC', 'C'), | ||
('BC', 'B'), | ||
('ACD', 'AC'), | ||
('ACD', 'AD'), | ||
('AD', 'A'), | ||
('ABD', 'AB'), | ||
('ABD', 'AD')] | ||
""" | ||
Objects = context._Objects | ||
Properties = context._Properties | ||
|
||
if not concept_index: | ||
concept_index = dict(concept_list) | ||
|
||
for extent, intent in concept_list: | ||
candidate_counter = collections.Counter() | ||
|
||
property_candidates = Properties.fromint(Properties.supremum & ~intent) | ||
|
||
for atom in property_candidates.atoms(): | ||
extent_candidate = Objects.fromint(extent & atom.prime()) | ||
intent_candidate = concept_index[extent_candidate] | ||
candidate_counter[extent_candidate] += 1 | ||
|
||
if (intent_candidate.count() - intent.count()) == candidate_counter[extent_candidate]: | ||
yield (extent, intent), (extent_candidate, intent_candidate) | ||
|
||
|
||
def _return_edges(batch, concept_index, context): | ||
return list(covering_edges(batch, concept_index, context)) | ||
|
||
|
||
def lattice_fcbo(context, process_count=1): | ||
"""Returns tuple of tuples in form of ``(extent, intent, upper, lower)`` in short lexicographic order.""" | ||
concepts = list(fast_generate_from(context)) | ||
concepts.sort(key=lambda concept: concept[0].shortlex()) | ||
concept_index = dict(concepts) | ||
|
||
if process_count == 1: | ||
edges = covering_edges(concepts, context, concept_index=concept_index) | ||
else: | ||
batches = [concepts[i::process_count] for i in range(0, process_count)] | ||
|
||
with multiprocessing.Pool(process_count) as p: | ||
results = [p.apply_async(_return_edges, (batch, context, concept_index)) for batch in batches] | ||
edges = itertools.chain.from_iterable([result.get() for result in results]) | ||
|
||
mapping = dict([(extent, (extent, intent, [], [])) for extent, intent in concepts]) | ||
|
||
for concept, lower_neighbor in edges: | ||
extent, _ = concept | ||
lower_extent, _ = lower_neighbor | ||
|
||
mapping[extent][3].append(lower_extent) | ||
mapping[lower_extent][2].append(extent) | ||
|
||
return tuple(mapping.values()) |
Uh oh!
There was an error while loading. Please reload this page.