The criteria for searching exhibits the usual balance between precision and recall. Recall can be increased by making the search wide and the use of arbitrary combinations… That approach when hit with the giant khipu AS069, slows search to somewhere between crawl and stop. High recall, but not in a reasonable time. The approach that works, is to limit indexing to contiguous groups, a constraint which seems reasonable given both the evidence in the Ascher equations, and to our intuitive sense of user-interface. At least two summands are required (i.e no identity matches).
There are many trivial sums. The filtering constraints include:
Setting the minimum cord sum to 5 removes trivial 1 sums and 0 sums
Removing sums which are multiples of 10, when the total value is < 100
Removing sums which are multiples of 100, when the total value is < 1000
Off-by-one matching (i.e. 436==336 or 446==436 or 446==447) is turned off for this search.
Code
################################################################# SUM COMBINATION CORD funcs################################################################def search_cords_that_sum(self, aKhipu): left_sum_cord_tuples = [] right_sum_cord_tuples = []# Search over all groups and cordsfor aGroup in aKhipu.cord_groups(): non_zero_sum_cords = [aCord for aCord in aGroup.pendant_cords() if aCord.knotted_value() >5]for aCord in non_zero_sum_cords: (left_cords, right_cords) =self.indexed_pendant_cords(aKhipu, aGroup, aCord) non_zero_left_cords = [aCord for aCord in left_cords if aCord.knotted_value() >0] non_zero_right_cords = [aCord for aCord in right_cords if aCord.knotted_value() >0] left_sum_matches =self.find_sum_matches(aCord, non_zero_left_cords) right_sum_matches =self.find_sum_matches(aCord, non_zero_right_cords)iflen(left_sum_matches) >0: left_sum_cord_tuples += left_sum_matchesiflen(right_sum_matches) >0: right_sum_cord_tuples += right_sum_matchesreturn (left_sum_cord_tuples, right_sum_cord_tuples)def find_sum_matches(self, aCord, test_cords):""" This is the one that does all the hard work of finding matches. It builds all the contiguous combinations, and then tests the combinations for equality. Note use of off-by-one equality test """ match_group = aCord.pendant_group() match_sum = aCord.knotted_value() good_matches = [] all_matches =self.contiguous_combinations(test_cords)for test_match in all_matches: test_sum =sum([aCord.knotted_value() for aCord in test_match])ifself.obo_equal(match_sum, test_sum): # match_list = "{" + ", ".join([f"{aCord.pendant_group().position}:{aCord.cord_ordinal}" for aCord in test_match]) + "}"# print(f"match_sum {match_sum} - group:cord {match_group.position()}:{aCord.cord_ordinal} = {match_list}") cord_match_tuple = (aCord, test_match)# Filter trival sums....if (aCord.knotted_value() >1) or (len(cord_match_tuple[1]) >1): good_matches.append(cord_match_tuple)return good_matchesdef contiguous_combinations(self, aList): """" from [1,2,3,4] return [[1,2],[2,3],[3,4],[1,2,3],[2,3,4],[1,2,3,4]] """def take_n(aList,n):return [aList[i:i+n] for i inrange(0,len(aList)-n+1)] combos = [] for i inrange(2, len(aList)+1): combos += take_n(aList,i) return combos def left_right_pendant_groups(self, aKhipu, aGroup):""" Returns list of groups to the left and to the right of given group """ group_position = aGroup.position() pendant_groups = [aGroup for aGroup in aKhipu.cord_groups() if aGroup.is_pendant_group()] left_groups = [aGroup for aGroup in pendant_groups if aGroup.position() < group_position] right_groups = [aGroup for aGroup in pendant_groups if aGroup.position() > group_position]return (left_groups, right_groups)def indexed_pendant_cords(self, aKhipu, aGroup, aCord):""" Get cords, indexed at aCord.position from left and right groups. Return tuple of list of cords to the left, and list of cords to the right """ cord_index = aCord.position() (left_groups, right_groups) =self.left_right_pendant_groups(aKhipu, aGroup) left_cords = [aGroup.pendant_cords()[cord_index] for aGroup in left_groups if aGroup.num_pendant_cords() > cord_index] right_cords = [aGroup.pendant_cords()[cord_index] for aGroup in right_groups if aGroup.num_pendant_cords() > cord_index]return (left_cords, right_cords)
3. Significance Criteria:
This is a common fieldmark. So we limit significant khipus to those having occurences of the sum relationship that are greater than the mean (7).
# Initialize plotlyimport plotlyplotly.offline.init_notebook_mode(connected =False);# Read in the Fieldmark and its associated dataframe and match dictionaryfrom fieldmark_ascher_indexed_pendant_sum import FieldmarkIndexedPendantSumaFieldmark = FieldmarkIndexedPendantSum()fieldmark_dataframe = aFieldmark.dataframes[0].dataframeraw_match_dict = aFieldmark.raw_match_dict()
Code
# Plot Matching khipumatching_khipus = aFieldmark.matching_khipus() matching_values = [raw_match_dict[aKhipuName] for aKhipuName in matching_khipus]matching_df = pd.DataFrame(list(zip(matching_khipus, matching_values)), columns =['KhipuName', 'Value'])fig = px.bar(matching_df, x='KhipuName', y='Value', labels={"KhipuName": "Khipu Name", "Value": "Number of Indexed Pendant Sum Cords", }, title=f"Matching Khipu ({len(matching_khipus)}) for Number of Indexed Pendant Sum Cords", width=944, height=450).update_layout(showlegend=True).show()
Code
# Plot Significant khipusignificant_khipus = aFieldmark.significant_khipus()significant_values = [raw_match_dict[aKhipuName] for aKhipuName in significant_khipus]significant_df = pd.DataFrame(list(zip(significant_khipus, significant_values)), columns =['KhipuName', 'Value'])fig = px.bar(significant_df, x='KhipuName', y='Value', labels={"KhipuName": "Khipu Name", "Value": "Number of Indexed Pendant Sum Cords", }, title=f"Significant Khipu ({len(significant_khipus)}) for Number of Indexed Pendant Sum Cords", width=944, height=450).update_layout(showlegend=True).show()
6. Exploratory Data Analysis
Surprisingly, AS069 occurs high on this list. Had the Indexed Pendant Sum fieldmark not been investigated, I had thought that there was something very unique (ie. possibly linguistic?) about AS069. Now we see that despite it’s size, AS069 has a lot of sums involved and is largely an accounting of something…
6.1 Position
A look at how the indexes distribute by position, sum value, and number of summands:
Code
import plotly.express as pximport utils_khipu as ukhipusum_cord_df = aFieldmark.dataframes[1].dataframesum_cord_df['group_position'] = [ukhipu.group_index_from_cord_rep(aString) for aString in sum_cord_df.cord_position.values]fig = (px.scatter(sum_cord_df, x="cord_position_index", y="cord_value", log_y=True, size="num_summands", color="num_summands", labels={"num_summands":"# Summands", "group_position":"Group Index (0 based)", "cord_position_index": "Cord Index (0 Based)", "cord_value": "Cord Value"}, hover_name='name', hover_data=['group_position', 'cord_position_index', 'cord_value', 'num_summands'], title="<b>Indexed Pendant Sums:</b> Cord Value vs. Position - <i>Hover Over Circles to View Khipu/Cord Info</i>", width=944, height=944) .update_layout(showlegend=True) .show() )
In the graphic below, note how the deep orange AS069 has a sum of 29 contiguous summands totaling 585.
Code
fig = (px.scatter(sum_cord_df, y="cord_value", x="num_summands", log_y=True,#size="num_summands", color="cord_position_index", labels={"num_summands":"# Summands", "group_position":"Group Index (0 based)", "cord_position_index": "Cord Index (0 Based)", "cord_value": "Cord Value"}, hover_name='name', hover_data=['group_position', 'cord_position_index', 'cord_value', 'num_summands'], title="<b>Indexed Pendant Sums:</b> Cord Value vs Number Summands - <i>Hover Over Circles to View Khipu/Cord Info</i>", width=944, height=944) .update_layout(showlegend=True) .show() )
right_sum_cord_position_df = sum_cord_df[sum_cord_df.handedness >=0]left_sum_cord_position_df = sum_cord_df[sum_cord_df.handedness <0]num_left_sums =len(left_sum_cord_position_df) # sum(fieldmark_dataframe.num_right_sums.values.tolist())num_right_sums =len(right_sum_cord_position_df) # sum(fieldmark_dataframe.num_left_sums.values.tolist())num_total_sums = num_left_sums + num_right_sumsleft_pct =round(100.0*float(num_left_sums)/float(num_total_sums))right_pct =round(100.0*float(num_right_sums)/float(num_total_sums))print(f"Out of {num_total_sums} indexed sums, there are {num_right_sums}({right_pct}%) right_handed sums and {num_left_sums}({left_pct}%) left-handed sums")
Out of 4686 indexed sums, there are 2682(57%) right_handed sums and 2004(43%) left-handed sums
Code
# Zoom in on thatmake_heatmap_array(right_sum_cord_position_df, max_num_groups=30, max_num_cords=50, title="<b>Top occurrences</b> of RIGHT-HANDED</b> Indexed Pendant Sum Positions", is_right_handed=True)make_heatmap_array(left_sum_cord_position_df, max_num_groups=30, max_num_cords=50, title="<b>Top occurrences</b> of <b>LEFT-HANDED</b> Indexed Pendant Sum Positions (from the right)", is_right_handed=False)make_heatmap_array(left_sum_cord_position_df, max_num_groups=30, max_num_cords=50, title="<b>Top occurrences</b> of <b>LEFT-HANDED</b> Indexed Pendant Sum Positions (from the left)", is_right_handed=True)
That’s unusual and unexpected. With indexed-pendant sums, the cord value’s decrease somewhat with increasing number of summands.
6.2 Handedness
Do the summands exhibit “handedness”/moment_arm? do the khipus exhibit a “handedness”?
Code
import utils_loom as uloomdef is_right_handed(x): return x <0num_right_handed_pendants =sum([is_right_handed(x) for x in sum_cord_df.handedness.values])num_left_handed_pendants =sum([not is_right_handed(x) for x in sum_cord_df.handedness.values])num_total_pendant_sums = num_right_handed_pendants+num_left_handed_pendantsprint(f"Number of left handed pendants: {num_left_handed_pendants}(~{uloom.as_percent_string(num_left_handed_pendants,num_total_pendant_sums)})")print(f"Number of right handed pendants: {num_right_handed_pendants}(~{uloom.as_percent_string(num_right_handed_pendants,num_total_pendant_sums)})")print(f"Total number of sum pendants = {num_total_pendant_sums}")def handed_color(x): return0.0if x <0else1.0sum_cord_df['handed_color'] = [handed_color(x) for x in sum_cord_df.handedness.values]
Number of left handed pendants: 2682(~57%)
Number of right handed pendants: 2004(~43%)
Total number of sum pendants = 4686
Code
from statistics import meanhandedness_mean = mean(list(sum_cord_df.handedness.values))fig = (px.scatter(sum_cord_df, x="handedness", y="cord_value", log_y=True, size="num_summands", color='handed_color', color_continuous_scale=['#3c3fff', '#ff5030',], labels={"handedness": f"Handedness - Mean({handedness_mean})", "cord_value": "Cord Value (Log(y) scale)", }, hover_data=['name'], title="<b>Handedness of Indexed Pendant Sums</b> - <i>Hover Over Circles to View Khipu/Cord Info</i>", width=944, height=944) .add_vline(x=handedness_mean) .update_layout(showlegend=False).update(layout_coloraxis_showscale=False).show() )
In our analysis of Pendant Pendant Sums it was noted that AS069 appears to be in a strange order. Let’s drop AS069 from the list of Indexed Pendant sums and see what happens:
Code
not_as069_df = sum_cord_df[sum_cord_df.name !="AS069"]not_as069_handedness_mean = mean(list(not_as069_df.handedness.values))fig = (px.scatter(not_as069_df, x="handedness", y="cord_value", log_y=True, size="num_summands", color='handed_color', color_continuous_scale=['#3c3fff', '#ff5030',], labels={"handedness": f"Handedness - Mean({not_as069_handedness_mean})", "cord_value": "Cord Value (Log(y) scale)", }, hover_data=['name'], title="<b>Handedness of Indexed Pendant Sums</b> - <i>Hover Over Circles to View Khipu/Cord Info</i>", width=944, height=944) .add_vline(x=not_as069_handedness_mean) .update_layout(showlegend=False).update(layout_coloraxis_showscale=False).show() )
WOW
By dropping AS069, the long-range handedness drops from a range of -400 to 400 to -80 to 60. A MUCH more believable number. Clearly AS069 has something odd about it.
Dropping AS069 also clarify’s other observations:
Right-handed indexed-pendant sums are usually between 1 and 40 cords away. There is a curious triangle in the above scatterplot.
Left-handed indexed-pendant sums are usually between 1 and two cords away.
Left-handed indexed-pendant sums stop appearing around 2000 (with one outlier).
7. Conclusions:
Once again, we see the classic 60/40 split between right and left-handed sums. This consistent 60/40 split across all four ascher sum relations is astonishing.
Dropping AS069 gives us more insight into the distance between sums and summands. It’s not that large. When the outlier AS069 is dropped, the distance between summands and sum cord follows a roughly triangular distribution - Large sum cord values are close to their summands, and small sum cord values can be farther away (1-40 cords) from their summands.
Indexed -pendant sums follow the same overall location distribution that pendant-pendant sums and pendant-color sums follow.
With indexed-pendant sums, the cord value’s decrease, somewhat, with an increasing number of summands.