Indexed Pendant Sum Cords


1. Number of Indexed Pendant Sum Cords

Pendant cords that are Sums of similarly indexed cords in contiguous groups to the right or left of the cord.

Ascher khipu AS038/KH0049, is the first of the many khipus that use pendant position indexing for summing. In AS038, Ascher notes:

\(P_{2i}=P_{4i}+P_{5i}+P_{6i}\;for\;i=(1,2,3...,12)\)
\(P_{2,13}=P_{5,13}+P_{6,13}\)
\(P_{2,i}=P_{4,i-1}+P_{5i}+P_{6i}\;for\;i=(14,15,...,18)\)
\(P_{1i}=P_{2i}+P_{3i}\;for\;i=(1,2,3...,18)\)

Additionally Ascher notes that subsidiaries also exhibit indexing - a property explored in the Indexed Subsidiary Sum Cords Fieldmark

2. Search Criteria:

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 cords
    for 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)

            if len(left_sum_matches) > 0: left_sum_cord_tuples += left_sum_matches
            if len(right_sum_matches) > 0: right_sum_cord_tuples += right_sum_matches

    return (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])
        if self.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_matches

def 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 in range(0,len(aList)-n+1)]     
    combos = [] 
    for i in range(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).

4. Summary Results:

Measure Result
Number of Khipus That Match 228 (35%)
Number of Significant Khipus 79 (12%)
Five most Significant Khipu AS069, UR110, UR039, UR004, UR055
XRay Image Quilt Khipu by Indexed-Pendant Sum Relationships
Database View Click here

5.Summary Charts:

Code
# Initialize plotly
import plotly
plotly.offline.init_notebook_mode(connected = False);

# Read in the Fieldmark and its associated dataframe and match dictionary
from fieldmark_ascher_indexed_pendant_sum import FieldmarkIndexedPendantSum
aFieldmark = FieldmarkIndexedPendantSum()
fieldmark_dataframe = aFieldmark.dataframes[0].dataframe
raw_match_dict = aFieldmark.raw_match_dict()
Code
# Plot Matching khipu
matching_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 khipu
significant_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 px
import utils_khipu as ukhipu
sum_cord_df = aFieldmark.dataframes[1].dataframe
sum_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()
      )
Code
import plotly.graph_objects as go
import numpy as np

def make_heatmap_array(aSumDF, 
                       title="Sum Count occurrence", 
                       max_num_groups=None, 
                       max_num_cords=None,
                       is_right_handed = True):
    group_positions = aSumDF.group_position.values
    cord_positions = aSumDF.cord_position_index.values
    num_groups = 1+max(aSumDF.group_position.values)
    num_cords = 1+max(aSumDF.cord_position_index.values)
    if max_num_groups is None: max_num_groups = num_groups 
    if max_num_cords is None: max_num_cords = num_cords 
    position_count_array = np.zeros(shape=(max_num_cords, max_num_groups), dtype=np.int64) 
    for index in range(len(aSumDF)):
        group_pos = group_positions[index]
        cord_pos = cord_positions[index]
        if group_pos < max_num_groups and cord_pos < max_num_cords:
            position_count_array[cord_pos, group_pos] += 1

    if not is_right_handed: position_count_array = position_count_array[:,1:max_num_groups]
    dir_x = 1 if is_right_handed else -1
    data = position_count_array.tolist()
    
    fig = (go.Figure(data=go.Heatmap(z=data, dx=dir_x, dy=-1))
                     .update_layout(width=944, height=944, title=title)
                     .show())

6.2 Handedness

Code
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_sums
left_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 that
make_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 uloom
def is_right_handed(x): return x < 0
num_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_pendants
print(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): return 0.0 if x < 0 else 1.0
sum_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 mean
handedness_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.