Figure 8 Knots - Inquiry 1: Types and Locations


The first inquiry is to understand where Figure-8-Knots occur. Specifically, the locational distribution of Figure-8-Knots on khipu cords, groups, and khipus.

As mentioned in the Figure-8-Knot Study Introduction, it is important to distinguish between Figure-8-Knots in Lockean vs Non-Lockean knot sequences on a cord. When a Figure-8-knot is the last knot of more than one knot on a cord (i.e. cord values > 9), it is likely to be a Lockean knot sequence. However, when a Figure-8-knot is the only knot of a cord, it may have both numerical and/or semiotic values. The former (Lockean) knot is called a Trailing Figure-8-Knot, and the latter a Sole Figure-8-Knot.

The investigation proceeds from the bottom to the top. We investigate locational distributions of Figure-8-Knots on khipu cords, groups, and finally khipus.

These are the questions we will answer:

  1. How often does 1 occur, compared to other digits?
  2. What is the locational distributions of Figure-8-Knots on khipu cords?
  3. What are common cord knot-sequences and what “names” do we give them?
  4. What is the locational distributions of Figure-8-Knots along cord groups?
  5. What is the locational distributions of Figure-8-Knots along khipus?

1. Is 1 Special? How often does it occur, and how often does it occur as the last digit?

An analysis of how often the number 1 occurs in the khipu corpus reveals the difference between Lockean and Non-Lockean Figure-8-Knots.

1.1 Sample Study: City Populations

First, let’s look at how 1 distributes in a typical “organic” data se. In a sense, this is a “Lockean” approach, reading the rightmost digit of a number. As a sample study, we can get 10,000 random cities from the US Census, and look at the distribution of the last, rightmost digit:

Code
```{python}
K_VERBOSE_MODE = False

import os
import numpy as np
from scipy import stats
import pandas as pd
from pandas import Series, DataFrame
from collections import Counter

import qollqa_chuspa as qc  # A Khipu Maker is known (in Quechua) as a Khipu Kamayuq
import utils_loom as uloom
import utils_khipu as ukhipu
import utils_pandas as upanda
import utils_kfg_locations as uloc
    
# Plotly
import plotly
from plotly.offline import iplot, init_notebook_mode
import plotly.graph_objects as go
import plotly.express as px
import plotly.figure_factory as ff
plotly.offline.init_notebook_mode(connected = False)
# Load all khipus
(khipu_dict, all_khipus) = qc.fetch_khipus()
KFG_Names = ukhipu.kfg_order(khipu_dict.keys()) 

if (K_VERBOSE_MODE):
    print(f"Loaded {len(all_khipus)} khipus")
```
Code
```{python}
census_cities = pd.read_csv(f"./data/CSV/us_cities_random_10000.csv")
census_cities.head()

last_digit_counts = Counter([int(str(x)[-1]) for x in census_cities['Population'].to_list()])
#Make a plotly bar chart of the last digit counts, show title and labels, and set max width to 1000p    x
# Use ETBookOT as the font  
fig = px.bar(x=last_digit_counts.keys(), y=last_digit_counts.values())
fig.update_layout(title='Last Digit Counts for 10,000 Random Cities', xaxis_title='Last Digit', yaxis_title='Count',font_family="ETBookOT", font_size=20)
fig.update_layout(width=950)   
fig.show()
```

It’s close to a flat/uniform distribution (each digit landing around ~1,000, i.e. ~10%), which is what you’d expect from real population counts — unlike digits earlier in a number (which tend to follow Benford’s Law), the last digit of a large, organically-generated count is essentially uniformly random.

1.2 Khipu Analysis

Let’s look in the Khipu Field Guide? I will make two crucial distinctions: What is the last digit of the value of the cord when it is greater than or equal to 10 (i.e. a Trailing “Lockean” knots)? And what is the last digit of the value of the cord when it is less than 10 (ie. a Sole knot on the cord)? These values are not the same.

Code
```{python}
# for all the khipus in the KFG, get the last digit of the value of the cord    
# Skip cords with value 0 (no knots)
pendant_cords = uloom.flatten_list([aKhipu.all_cords(include_bottom_cords=True, include_top_cords=True, include_subsidiaries=False) for aKhipu in all_khipus])
subsidiary_cords = uloom.flatten_list([aKhipu.all_cords(include_bottom_cords=False, include_top_cords=False, include_subsidiaries=True) for aKhipu in all_khipus])
all_cords = uloom.flatten_list([aKhipu.all_cords(include_bottom_cords=True, include_top_cords=True, include_subsidiaries=True) for aKhipu in all_khipus])

header_labels = ["Last Digit"] + [f"{i}" for i in range(10)]
row_labels = ["Digit Count"]

def make_markdown_table(header_labels, row_labels, row_values): 
    tablestr =  "| " + " | ".join(header_labels) + " |\n"
    tablestr += "|"  + "|".join([":------:" for _ in header_labels]) + "|\n"
    for row_label, row_value in zip(row_labels, row_values):
        row_count = sum(row_value)
        tablestr += f"| {row_label} | " + " | ".join([f"{uloom.percent_info(count, row_count, as_html=True)}" for count in row_value]) + " |\n"
    return tablestr

import sys
k_MAX_KNOTTED_VALUE = sys.maxsize # 9223372036854775807 Largest possible int value for a cord

def make_last_digit_counts(a_min_knotted_value=0, a_max_knotted_value=k_MAX_KNOTTED_VALUE, print_table=False):

    pendant_cord_last_digits = [int(str(aCord.knotted_value)[-1]) for aCord in pendant_cords if (aCord.knotted_value >= a_min_knotted_value) and (aCord.knotted_value <= a_max_knotted_value)]
    subsidiary_cord_last_digits = [int(str(aCord.knotted_value)[-1]) for aCord in subsidiary_cords if (aCord.knotted_value >= a_min_knotted_value) and (aCord.knotted_value <= a_max_knotted_value)]
    all_cord_last_digits = [int(str(aCord.knotted_value)[-1]) for aCord in all_cords if (aCord.knotted_value >= a_min_knotted_value) and (aCord.knotted_value <= a_max_knotted_value)]

    pendant_cord_last_digit_counts = Counter(pendant_cord_last_digits)
    subsidiary_cord_last_digit_counts = Counter(subsidiary_cord_last_digits)
    all_cord_last_digit_counts = Counter(all_cord_last_digits)

    #Make a plotly bar chart of the last digit counts, show title and labels, and set max width to 1000p    x
    fig = px.bar(x=pendant_cord_last_digit_counts.keys(), y=pendant_cord_last_digit_counts.values())
    the_title = f"Last Digit Counts for Pendant Cords ≥ {a_min_knotted_value}"
    if (a_max_knotted_value < k_MAX_KNOTTED_VALUE): the_title += f" and ≤ {a_max_knotted_value}"
    fig.update_layout(title=the_title, xaxis_title='Last Digit', yaxis_title='Count',font_family="ETBookOT", font_size=20)
    fig.update_layout(width=945)   
    fig.show()

    #Make a plotly bar chart of the last digit counts, show title and labels, and set max width to 1000p    x
    fig = px.bar(x=subsidiary_cord_last_digit_counts.keys(), y=subsidiary_cord_last_digit_counts.values())
    the_title = f"Last Digit Counts for Subsidiary Cords ≥ {a_min_knotted_value}"
    if (a_max_knotted_value < k_MAX_KNOTTED_VALUE): the_title += f" and ≤ {a_max_knotted_value}"
    fig.update_layout(title=the_title, xaxis_title='Last Digit', yaxis_title='Count',font_family="ETBookOT", font_size=20)
    fig.update_layout(width=945)   
    fig.show()

    if print_table:
        print(f"Pendant Cords Last Digit Counts >= {a_min_knotted_value}")
        row_values = [list(pendant_cord_last_digit_counts.values())]
        print(make_markdown_table(header_labels, row_labels, row_values))

        print(f"Subsidiary Cords Last Digit Counts >= {a_min_knotted_value}")
        row_values = [list(subsidiary_cord_last_digit_counts.values())]
        print(make_markdown_table(header_labels, row_labels, row_values))


make_last_digit_counts(a_min_knotted_value=0, a_max_knotted_value=k_MAX_KNOTTED_VALUE, print_table=False)
make_last_digit_counts(a_min_knotted_value=10, a_max_knotted_value=k_MAX_KNOTTED_VALUE, print_table=False)
make_last_digit_counts(a_min_knotted_value=1, a_max_knotted_value=9, print_table=False)

```


The graphs show that Trailing (Lockean-style)digits follow a typical uniform distribution, while Sole digits follow a non-uniform decreasing distribution, heavily centered on 1.

1.3 How often does 1 occur as a Trailing vs Sole Digit on Pendant vs Subsidiary Cords?

Let’s look at the distribution of 1’s as the last digit vs a sole digit on pendant cords vs subsidiary cords.

Code
```{python}
pendant_cord_trailing_ones = [aCord for aCord in pendant_cords if (aCord.knotted_value > 1) and (int(str(aCord.knotted_value)[-1]) == 1)]
pendant_cord_sole_ones = [aCord for aCord in pendant_cords if (aCord.knotted_value == 1)]

subsidiary_cord_trailing_ones = [aCord for aCord in subsidiary_cords  if (aCord.knotted_value > 1) and (int(str(aCord.knotted_value)[-1]) == 1)]
subsidiary_cord_sole_ones = [aCord for aCord in subsidiary_cords if (aCord.knotted_value == 1)]

# Print as a markdown table
total_pendant_cord_1s = len(pendant_cord_trailing_ones) + len(pendant_cord_sole_ones)
total_subsidiary_cord_1s = len(subsidiary_cord_trailing_ones) + len(subsidiary_cord_sole_ones)
do_print = False
if do_print:
    print(f"| Cord Type | Last Digit 1's (Trailing 1's) | Sole 1's |")
    print(f"|:------|------:|-------:|")
    print(f"| Pendant Cords | {uloom.percent_info(len(pendant_cord_trailing_ones), total_pendant_cord_1s, as_html=True)} | {uloom.percent_info(len(pendant_cord_sole_ones), total_pendant_cord_1s, as_html=True)} |")
    print(f"| Subsidiary Cords | {uloom.percent_info(len(subsidiary_cord_trailing_ones), total_subsidiary_cord_1s, as_html=True)} | {uloom.percent_info(len(subsidiary_cord_sole_ones), total_subsidiary_cord_1s, as_html=True)} |")
```
Cord Type Last Digit 1’s (Trailing 1’s) Sole 1’s
Pendant Cords 33% (1641 of 4996) 67% (3355 of 4996)
Subsidiary Cords 12% (412 of 3424) 88% (3012 of 3424)

2. Common Knot Sequences

With what other knot types do figure-8-knots associate?

Let’s look at all cords in the KFG database, and gather the most common knot type sequences, in conventional Lockean order (from the primary cord down):

2.1 Common Knot Sequences for All Khipu Cords

Code
```{python}
import os
import numpy as np
from scipy import stats
import pandas as pd
from pandas import Series, DataFrame
from collections import Counter

import qollqa_chuspa as qc  # A Khipu Maker is known (in Quechua) as a Khipu Kamayuq
import utils_loom as uloom
import utils_khipu as ukhipu
import utils_pandas as upanda
import utils_kfg_locations as uloc
    
# Plotly
import plotly
from plotly.offline import iplot, init_notebook_mode
import plotly.graph_objects as go
import plotly.express as px
import plotly.figure_factory as ff
plotly.offline.init_notebook_mode(connected = False)

# Load all khipus
(khipu_dict, all_khipus) = qc.fetch_khipus()
KFG_Names = ukhipu.kfg_order(khipu_dict.keys())  # pyright: ignore[reportArgumentType]

khipu_cords_dict = {KFG_Name: khipu_dict[KFG_Name].all_cords(include_bottom_cords=True, include_top_cords=True, include_subsidiaries=True) for KFG_Name in KFG_Names}
all_khipu_cords = uloom.flatten_list([khipu_cords_dict[KFG_Name] for KFG_Name in KFG_Names])
total_num_khipu_cords = len(all_khipu_cords)
```
Code
```{python}
kMAXMOSTCOMMON = 200

def knot_sequence(aCord):
    cord_knots = aCord.all_knots(include_subsidiaries=False)
    knot_types = [knot.knot_type for knot in cord_knots]
    sequence_string = ','.join(knot_types)
    return sequence_string if sequence_string != '' else 'No Knots'

def most_common_knot_sequences(cords):
    all_knot_sequences = [knot_sequence(aCord) for aCord in cords]
    knot_sequence_counts = Counter(all_knot_sequences)
    return [(the_knot_sequence, count) for the_knot_sequence, count in knot_sequence_counts.most_common() if count > kMAXMOSTCOMMON]

def knot_sequence_table(cords=None, print_common_sequences = False):
    if cords is None: cords = all_khipu_cords
    the_most_common_knot_sequences = most_common_knot_sequences(cords)
    the_most_common_pendant_knot_sequences = most_common_knot_sequences([aCord for aCord in cords if aCord.is_pendant_cord()])   
    the_most_common_pendant_knot_sequences_dict = {the_knot_sequence:count for (the_knot_sequence, count) in the_most_common_pendant_knot_sequences}

    if print_common_sequences:
        # print(f"Most common knot sequences {len(the_most_common_knot_sequences)} with more than {kMAXMOSTCOMMON} occurrences:")
        for (index, (the_knot_sequence, count)) in enumerate(the_most_common_knot_sequences):
            if the_knot_sequence.strip() == '': the_knot_sequence = 'No_Knots'
            the_num_pendant_knot_sequence = the_most_common_pendant_knot_sequences_dict.get(the_knot_sequence, 0)
            the_num_subsidiary_knot_sequence = count - the_num_pendant_knot_sequence    
            print(f"| {index} | {the_knot_sequence} | {uloom.percent_info(count, len(cords), as_html=True)} | {uloom.percent_info(the_num_pendant_knot_sequence, count, as_html=True)} | {uloom.percent_info(the_num_subsidiary_knot_sequence, count, as_html=True)} | ")

    return the_most_common_knot_sequences

do_print = False
if do_print:
    knot_sequence_table(print_common_sequences = do_print);

    pendant_cords = [aCord for aCord in all_khipu_cords if aCord.is_pendant_cord()]
    subsidiary_cords = [aCord for aCord in all_khipu_cords if aCord.is_subsidiary_cord()]
    print(f"# All Cords = {len([aCord for aCord in all_khipu_cords])}")
    print(f"# Pendant Cords = {uloom.percent_info(len(pendant_cords), len(all_khipu_cords), as_html=True)}")
    print(f"# Subsidiary Cords = {uloom.percent_info(len(subsidiary_cords), len(all_khipu_cords), as_html=True)}")
```

The top knot sequences in the KFG database with more than 200 occurrences (out of ~60,000 cords) are:

Index Knot Type Sequence # Cords # Pendant Cords # Subsidiary Cords
- - 59144 Cords 72% (42572 of 59144) 28% (16572 of 59144)
0 No Knots 30% (19058 of 62837) 74% (14195 of 19058) 26% (4863 of 19058)
1 L 21% (13000 of 62837) 64% (8317 of 13000) 36% (4683 of 13000)
2 S,L 14% (8509 of 62837) 80% (6801 of 8509) 20% (1708 of 8509)
3 E 10% (6212 of 62837) 52% (3253 of 6212) 48% (2959 of 6212)
4 S 9% (5963 of 62837) 72% (4293 of 5963) 28% (1670 of 5963)
5 S,S,L 4% (2617 of 62837) 89% (2332 of 2617) 11% (285 of 2617)
6 S,E 2% (1325 of 62837) 78% (1033 of 1325) 22% (292 of 1325)
7 S,S 2% (1127 of 62837) 84% (948 of 1127) 16% (179 of 1127)
8 L,L 1% (668 of 62837) 84% (558 of 668) 16% (110 of 668)
9 S,S,S,L 1% (586 of 62837) 94% (548 of 586) 6% (38 of 586)
10 L,L,L 1% (412 of 62837) 92% (380 of 412) 8% (32 of 412)
11 S,S,E 1% (338 of 62837) 89% (301 of 338) 11% (37 of 338)
12 L,E 0% (313 of 62837) 67% (211 of 313) 33% (102 of 313)
13 S,S,S 0% (250 of 62837) 82% (206 of 250) 18% (44 of 250)
14 S,L,E 0% (207 of 62837) 0% (0 of 207) 100% (207 of 207)

You might have expected the conventional Lockean set of Single, Single, Long knots as the typical sequence ie. S,S,L… Actually the most common is no knots. However this simple count reveals:

  1. The most common value range is 2-9, represented as an L knot, occupies 21% of the cords.

  2. The next most common value range 11-99, represented as S,L, occupies 14% of the cords.

  3. Finally, the value 1, represented as a Sole Figure-8-Knot E, occupies 10% of the cords.

  4. E Sole Eight Knots reside equally (52%/48%) on pendants vs subsidiaries. This is unlike any of the other knot sequences which are typically 3 to 1 or 4 to 1 ratios for pendants vs subsidiaries

  5. S,E, S,S,E, L,E all have a Trailing Figure-Eight-Knot.

  6. The value range 101-999, represented as S,S,L knot sequences are fifth on the list.

As we will see, Figure-8-Knots are present on 15% of all khipu cords, an unexpectedly high frequency in the knot sequences and a wholly unexpected result. They usually appear in the form of Sole Eight-Knots (i.e. E), and Trailing Eight-Knots (i.e. S,E and S,S,E, S,L,E).

2.2 Lockean vs Non-Lockean Knot Sequences:

How do Lockean vs Non-Lockean knot sequences distribute?

Code
```{python}
def knot_sequence(aCord): return ("".join([aKnot.knot_type for aKnot  in aCord.all_knots(include_subsidiaries=False)])).strip()
def is_lockean_sequence(aKnotSequence): 
    is_lockean = False
    knot_set = set(aKnotSequence)
    if (len(aKnotSequence)==0) or (knot_set == {}) or (knot_set == {'S'}):
        is_lockean = True
    elif knot_set == {'L'}:
        is_lockean = len(aKnotSequence) == 1
    elif (knot_set == {'S', 'L'}):
        is_lockean = aKnotSequence[-1] == "L" and (all([theChar == 'S' for theChar in aKnotSequence[:-1]]))
    return is_lockean

all_cords = uloom.flatten_list([aKhipu.all_cords(include_subsidiaries=True, include_top_cords=True) for aKhipu in all_khipus]) 
lockean_cords = [aCord for aCord in all_cords if is_lockean_sequence(knot_sequence(aCord))]
non_lockean_cords = [aCord for aCord in all_cords if not is_lockean_sequence(knot_sequence(aCord))]

print(f"{uloom.percent_info(len(lockean_cords), len(all_cords))} of all cords have Lockean knot-sequences\n")

print("20 Most Common Lockean Knot Sequences")
print(Counter([knot_sequence(aCord) for aCord in lockean_cords]).most_common(20))
print("20 Most Common Non Lockean Knot Sequences")
print(Counter([knot_sequence(aCord) for aCord in non_lockean_cords]).most_common(20))


lockean_counts = Counter([knot_sequence(aCord) for aCord in lockean_cords]).most_common(25)[::-1]
non_lockean_counts = Counter([knot_sequence(aCord) for aCord in non_lockean_cords]).most_common(25)[::-1]

fig = (go.Figure(go.Bar(
            x=[item[1] for item in lockean_counts], 
            y=[item[0] for item in lockean_counts], 
            orientation='h', 
            )))
fig.layout.update(width=950, height=400, title_text='Top 15 Lockean Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()

fig = (go.Figure(go.Bar(
            x=[item[1] for item in non_lockean_counts], 
            y=[item[0] for item in non_lockean_counts], 
            orientation='h', 
            )))
fig.layout.update(width=950, height=600, title_text='Top 25 Non Lockean Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```
82% (51225 of 62837) of all cords have Lockean knot-sequences

20 Most Common Lockean Knot Sequences
[('', 19058), ('L', 13000), ('SL', 8509), ('S', 5963), ('SSL', 2617), ('SS', 1127), ('SSSL', 586), ('SSS', 250), ('SSSSL', 69), ('SSSS', 39), ('SSSSS', 4), ('SSSSSSSSS', 1), ('SSSSSSSSSSS', 1), ('SSSSSSSS', 1)]
20 Most Common Non Lockean Knot Sequences
[('E', 6212), ('SE', 1325), ('LL', 678), ('LLL', 413), ('SSE', 338), ('LE', 313), ('SLE', 207), ('EE', 184), ('SLL', 157), ('LLLL', 131), ('SSLE', 102), ('LS', 101), ('LLS', 80), ('SSSE', 75), ('EL', 74), ('TF', 62), ('LLLLL', 57), ('SSSLE', 54), ('SLS', 52), ('LSL', 51)]

2.3 Common Knot Sequences with Figure-8-Knots

For cords with a Figure-8-knot, what are the common knot sequences?

Code
```{python}
eight_knot_cords = [aCord for aCord in all_cords if (not is_lockean_sequence(knot_sequence(aCord)) and ('E' in knot_sequence(aCord)))]
eight_knot_counts = Counter([knot_sequence(aCord) for aCord in eight_knot_cords]).most_common(25)[::-1]

fig = (go.Figure(go.Bar(
            x=[item[1] for item in eight_knot_counts], 
            y=[item[0] for item in eight_knot_counts], 
            orientation='h', 
            )))
fig.layout.update(width=950, height=600, title_text='Top 25 Eight Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3. Figure-Eight-Knot Sequence Types:

3.1 Figure-Eight-Knot Types

We start by building a dataset of Figure-Eight-Knot Cords of six types:

  • Cords that have a Sole Figure-Eight-Knot where there is only one knot on the cord (ie. no single knots, long knots, etc)
  • Cords that have Multiple Eight-Knots (where there is more than one knot, and every knot is an eight-knot)
  • Cords that have a Leading Figure-Eight-Knot, succeeded by other knots
  • Cords that have a Middle Figure-Eight-Knot, sandwiched between other knots
  • Cords that have a Trailing Figure-Eight-Knot, preceded by other knots
  • Cords that have Mixed Eight-Knots (i.e Leading + Middle, or Middle + Trailing or Leading + Middle + Trailing, but not Only Eight-Knots)
Code
```{python}
def has_eight_knot(aCord):
    """ Does at least one eight-knot exist on the cord? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    eight_knots = [aKnot for aKnot in search_knots if aKnot.is_eight_knot()]
    return len(eight_knots) > 0

def has_sole_eight_knot(aCord):
    """ Does the cord have only one knot, and that knot is an eight-knot """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    eight_knots = [aKnot for aKnot in search_knots if aKnot.is_eight_knot()]
    return (len(search_knots) == 1) and (len(eight_knots) == 1)

def has_mutiple_only_eight_knots(aCord):
    """ Does the cord have multiple eight-knots, and only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    is_only_eight_knots = all([aKnot.is_eight_knot() for aKnot in search_knots])
    return (len(search_knots) >= 2) and is_only_eight_knots

def has_leading_eight_knot(aCord):
    """ Does the cord have multiple knots, and the first knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 2) and (not has_mutiple_only_eight_knots(aCord)) and search_knots[0].is_eight_knot()

def has_middle_eight_knot(aCord):
    """ Does the cord have at least 3 knots, and at least one of the middle knots is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 3) and (not has_mutiple_only_eight_knots(aCord)) and any([aKnot.is_eight_knot() for aKnot in search_knots[1:-1]])
        
def has_trailing_eight_knot(aCord):
    """ Does the cord have multiple knots, and the last knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 2) and (not has_mutiple_only_eight_knots(aCord)) and search_knots[-1].is_eight_knot()

def has_mixed_eight_knot(aCord):
    """ **Mixed Eight-Knots** (i.e Leading + Middle, or Middle + Trailing or Leading + Middle + Trailing, but not Only Eight-Knots) """
    is_only_eight_knots = has_mutiple_only_eight_knots(aCord)
    has_leading_middle_trailing = (has_leading_eight_knot(aCord) and has_middle_eight_knot(aCord) and has_trailing_eight_knot(aCord))
    has_leading_middle = (has_leading_eight_knot(aCord) and has_middle_eight_knot(aCord))
    has_middle_trailing = (has_middle_eight_knot(aCord) and has_trailing_eight_knot(aCord))
    has_leading_trailing = (has_leading_eight_knot(aCord) and has_trailing_eight_knot(aCord))
    return (is_only_eight_knots and has_leading_middle_trailing or has_leading_middle or has_middle_trailing or has_leading_trailing) 

def tag_cord_knots(aCord):
    """ Tag the knots on a cord with an eight-knot type """
    tag = "None"
    if has_sole_eight_knot(aCord):
        tag = "Sole_Eight_Knot"
    elif has_mutiple_only_eight_knots(aCord):
        tag = "Multiple_Only_Eight_Knots"
    elif has_mixed_eight_knot(aCord):
        tag = "Mixed_Eight_Knot"
    elif has_leading_eight_knot(aCord):
        tag = "Leading_Eight_Knot"
    elif has_middle_eight_knot(aCord):
        tag = "Middle_Eight_Knot"
    elif has_trailing_eight_knot(aCord):
        tag = "Trailing_Eight_Knot"
    return tag

def tagged_kfg_cords(aKFG_Name, khipu_cords):
    """ Return a list of tagged cords tuples (cord, tag_name) for a given KFG_Name """
    tagged_eight_knot_cords = []
    for aCord in khipu_cords:
        tag = tag_cord_knots(aCord)
        if tag != "None":
            tagged_eight_knot_cords.append({'KFG_Name': aKFG_Name, 
                                            'cord_name': aCord.pendant_name, 
                                            'group_pendant_cord_index': aCord.find_pendant_cord().group_index(),
                                            'group_position': aCord.find_pendant_cord().cord_group.position(),
                                            'num_group_pendants': aCord.find_pendant_cord().cord_group.num_pendant_cords(),
                                            'tag': tag, 
                                            'is_pendant': aCord.is_pendant_cord()})
    return tagged_eight_knot_cords

def tag_eight_knot_cords(pendant_only=False, subsidiary_only=False):
    """ Return a dataframe of tagged eight_knot cords """
    tagged_eight_knot_cords = []
    for aKFG_Name in KFG_Names:
        khipu_cords = khipu_cords_dict[aKFG_Name]
        if pendant_only:
            khipu_cords = [aCord for aCord in khipu_cords if aCord.is_pendant_cord()]
        if subsidiary_only:
            khipu_cords = [aCord for aCord in khipu_cords if aCord.is_subsidiary_cord()]
        tagged_eight_knot_cords += tagged_kfg_cords(aKFG_Name, khipu_cords)

    return DataFrame(tagged_eight_knot_cords, columns=['KFG_Name', 'cord_name', 'group_pendant_cord_index', 'group_position', 'num_group_pendants', 'tag', 'is_pendant'])


eight_knot_cords_df = tag_eight_knot_cords()
eight_knot_cords_df.to_csv(f"{uloc.fieldmarks_data_dir()}/eight_knot_cords.csv", index=False)
pendant_eight_knot_cords_df = tag_eight_knot_cords(pendant_only=True)
subsidiary_eight_knot_cords_df = tag_eight_knot_cords(subsidiary_only=True)
eight_knot_cords_df.head()
```
KFG_Name cord_name group_pendant_cord_index group_position num_group_pendants tag is_pendant
0 CM009 p1 0 0 5 Sole_Eight_Knot True
1 CM009 p12 2 2 8 Sole_Eight_Knot True
2 CM009 p15 5 2 8 Sole_Eight_Knot True
3 CM009 p17 7 2 8 Sole_Eight_Knot True
4 CM009 p21 3 3 4 Sole_Eight_Knot True
Code
```{python}
def has_sole_eight_knot(aCord):
    """ Does the cord have only one knot, and that knot is an eight-knot """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    eight_knots = [aKnot for aKnot in search_knots if aKnot.is_eight_knot()]
    return (len(search_knots) == 1) and (len(eight_knots) == 1)

def has_mutiple_only_eight_knots(aCord):
    """ Does the cord have multiple eight-knots, and only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    is_only_eight_knots = all([aKnot.is_eight_knot() for aKnot in search_knots])
    return (len(search_knots) >= 2) and is_only_eight_knots

def has_leading_eight_knot(aCord):
    """ Does the cord have multiple knots, and the first knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 2) and (not has_mutiple_only_eight_knots(aCord)) and search_knots[0].is_eight_knot()

def has_middle_eight_knot(aCord):
    """ Does the cord have at least 3 knots, and at least one of the middle knots is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 3) and (not has_mutiple_only_eight_knots(aCord)) and any([aKnot.is_eight_knot() for aKnot in search_knots[1:-1]])
        
def has_trailing_eight_knot(aCord):
    """ Does the cord have multiple knots, and the last knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots(include_subsidiaries=False)
    return (len(search_knots) >= 2) and (not has_mutiple_only_eight_knots(aCord)) and search_knots[-1].is_eight_knot()
```
Code
```{python}
def num_eight_knot_cords(aKFG_Name):
    """ Number of eight-knot cords in a Khipu """
    return eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name].shape[0]

def num_pendant_eight_knot_cords(aKFG_Name):
    """ Number of ppendant_eight-knot cords in a Khipu """
    return eight_knot_cords_df[(eight_knot_cords_df['KFG_Name'] == aKFG_Name) & (eight_knot_cords_df['is_pendant'])].shape[0]

def num_subsidiary_eight_knot_cords(aKFG_Name):
    """ Number of ppendant_eight-knot cords in a Khipu """
    return eight_knot_cords_df[(eight_knot_cords_df['KFG_Name'] == aKFG_Name) & (eight_knot_cords_df['is_pendant']==False)].shape[0]

def num_sole_eight_knot_cords(aKFG_Name):
    """ Number of sole eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Sole_Eight_Knot'].shape[0]

def num_multiple_only_eight_knot_cords(aKFG_Name):
    """ Number of only eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Multiple_Only_Eight_Knots'].shape[0]

def num_mixed_eight_knot_cords(aKFG_Name):
    """ Number of mixed eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Mixed_Eight_Knot'].shape[0]

def num_leading_eight_knot_cords(aKFG_Name):
    """ Number of leading eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Leading_Eight_Knot'].shape[0]

def num_middle_eight_knot_cords(aKFG_Name):
    """ Number of middle eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Middle_Eight_Knot'].shape[0]
        
def num_trailing_eight_knot_cords(aKFG_Name):
    """ Number of trailing eight-knot cords in a Khipu """
    kfg_df = eight_knot_cords_df[eight_knot_cords_df['KFG_Name'] == aKFG_Name]
    return kfg_df[kfg_df['tag'] == 'Trailing_Eight_Knot'].shape[0]

sole_eight_knots_df= eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Sole_Eight_Knot']
multiple_only_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Multiple_Only_Eight_Knots']
mixed_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Mixed_Eight_Knot']
leading_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Leading_Eight_Knot']
middle_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Middle_Eight_Knot']
trailing_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Trailing_Eight_Knot']

num_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_eight_knot_cords(KFG_Name) > 0})
num_sole_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_sole_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_sole_eight_knot_cords(KFG_Name) > 0})
num_multiple_only_eight_knot_dict = uloom.sort_dict_by_values({KFG_Name: num_multiple_only_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_multiple_only_eight_knot_cords(KFG_Name) > 0})
num_mixed_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_mixed_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_mixed_eight_knot_cords(KFG_Name) > 0})
num_leading_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_leading_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_leading_eight_knot_cords(KFG_Name) > 0})
num_middle_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_middle_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_middle_eight_knot_cords(KFG_Name) > 0})
num_trailing_eight_knots_dict = uloom.sort_dict_by_values({KFG_Name: num_trailing_eight_knot_cords(KFG_Name) for KFG_Name in KFG_Names if num_trailing_eight_knot_cords(KFG_Name) > 0})
```

3.2 Sole Eight-Knot Khipus

Code
```{python}
the_num_sole_eight_knot_khipus = sole_eight_knots_df['KFG_Name'].nunique()
the_num_sole_eight_knot_cords = sole_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_sole_eight_knot_khipus, len(KFG_Names))} ({the_num_sole_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have cords with a Sole Eight-Knot")
print(f"{uloom.as_percent_string(the_num_sole_eight_knot_cords, total_num_khipu_cords)} ({the_num_sole_eight_knot_cords}/{total_num_khipu_cords}) of Cords have a Sole Eight-Knot\n")

print(f"Top 5 Khipus with the most Cords with a Sole Eight-Knot")
print(f"-------------------------------------------------------")
for KFG_Name in list(num_sole_eight_knots_dict.keys())[:5]:
    the_num_kfg_sole_eight_knot_cords = num_sole_eight_knots_dict[KFG_Name]
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_sole_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_sole_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Sole Eight-Knot")
```
56.4% (401/711) of Khipus have cords with a Sole Eight-Knot
10.0% (6284/62837) of Cords have a Sole Eight-Knot

Top 5 Khipus with the most Cords with a Sole Eight-Knot
-------------------------------------------------------
KH0239: 0.4% ( 278 of 62837) of its Cords have a Sole Eight-Knot
KH0242: 0.4% ( 222 of 62837) of its Cords have a Sole Eight-Knot
KH0329: 0.3% ( 183 of 62837) of its Cords have a Sole Eight-Knot
KH0323: 0.3% ( 173 of 62837) of its Cords have a Sole Eight-Knot
KH0034: 0.3% ( 170 of 62837) of its Cords have a Sole Eight-Knot

3.3 Trailing Eight-Knot Khipus

A Trailing Figure-8-Knot is a cord with at least two knots, for which the last knot is a figure-8-knot.

Code
```{python}
the_num_trailing_eight_knot_khipus = trailing_eight_knots_df['KFG_Name'].nunique()
the_num_trailing_eight_knot_cords = trailing_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_trailing_eight_knot_khipus, len(KFG_Names))} ({the_num_trailing_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have cords with Trailing Figure-Eight-Knots")
print(f"{uloom.as_percent_string(the_num_trailing_eight_knot_cords, total_num_khipu_cords)} ({the_num_trailing_eight_knot_cords}/{total_num_khipu_cords}) of Cords have Trailing Leading Eight-Knots\n")

print(f"Top 5 Khipus with the most Cords with Trailing-Eight-Knots")
print(f"----------------------------------------------------------")
for KFG_Name in list(num_trailing_eight_knots_dict.keys())[:5]:
    the_num_kfg_trailing_eight_knot_cords = num_trailing_eight_knot_cords(KFG_Name)
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_trailing_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_trailing_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Trailing Figure-Eight-Knots")
```
59.1% (420/711) of Khipus have cords with Trailing Figure-Eight-Knots
4.0% (2515/62837) of Cords have Trailing Leading Eight-Knots

Top 5 Khipus with the most Cords with Trailing-Eight-Knots
----------------------------------------------------------
KH0517: 0.1% (  72 of 62837) of its Cords have a Trailing Figure-Eight-Knots
KH0507: 0.1% (  68 of 62837) of its Cords have a Trailing Figure-Eight-Knots
KH0698: 0.1% (  48 of 62837) of its Cords have a Trailing Figure-Eight-Knots
KH0699: 0.1% (  41 of 62837) of its Cords have a Trailing Figure-Eight-Knots
KH0516: 0.1% (  38 of 62837) of its Cords have a Trailing Figure-Eight-Knots

What are the knot sequences for trailing-eight-knot cords?

Code
```{python}
trailing_8knot_cords = []
for index, row in trailing_eight_knots_df.iterrows():
    aCord = khipu_dict[row['KFG_Name']][row['cord_name']]
    trailing_8knot_cords.append(aCord)

trailing_8knot_cords_8knot__sequence_counts = Counter([aCord.knot_sequence() for aCord in trailing_8knot_cords])
most_common_trailing_eight_knot_sequences = trailing_8knot_cords_8knot__sequence_counts.most_common()

trailing_counts = [(index, count) for index, count in enumerate(trailing_8knot_cords_8knot__sequence_counts)][::-1]
fig = (go.Figure(go.Bar(
            x=[item[1] for item in trailing_counts], 
            y=[item[0] for item in trailing_counts], 
            orientation='v', 
            )))
fig.layout.update(width=950, height=600, title_text='Trailing Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3.4 Leading Eight-Knot Khipus

A Leading Figure-8-Knot is a cord with at least two knots, of which the first one is a figure-8-knot (to distinguish this from a sole figure-8-knot).

Code
```{python}
the_num_leading_eight_knot_khipus = leading_eight_knots_df['KFG_Name'].nunique()
the_num_leading_eight_knot_cords = leading_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_leading_eight_knot_khipus, len(KFG_Names))} ({the_num_leading_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have cords with Leading Figure-Eight-Knots")
print(f"{uloom.as_percent_string(the_num_leading_eight_knot_cords, total_num_khipu_cords)} ({the_num_leading_eight_knot_cords}/{total_num_khipu_cords}) of Cords have Mixed Leading Eight-Knots\n")

print(f"Top 5 Khipus with the most Cords with Leading Eight-Knots")
print(f"---------------------------------------------------------")
for KFG_Name in list(num_leading_eight_knots_dict.keys())[:5]:
    the_num_kfg_leading_eight_knot_cords = num_leading_eight_knots_dict[KFG_Name]
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_leading_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_leading_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Leading Figure-Eight-Knots")
```
8.3% (59/711) of Khipus have cords with Leading Figure-Eight-Knots
0.2% (155/62837) of Cords have Mixed Leading Eight-Knots

Top 5 Khipus with the most Cords with Leading Eight-Knots
---------------------------------------------------------
KH0441: 0.0% (  27 of 62837) of its Cords have a Leading Figure-Eight-Knots
KH0108: 0.0% (  20 of 62837) of its Cords have a Leading Figure-Eight-Knots
KH0225: 0.0% (   9 of 62837) of its Cords have a Leading Figure-Eight-Knots
KH0226: 0.0% (   9 of 62837) of its Cords have a Leading Figure-Eight-Knots
KH0090: 0.0% (   8 of 62837) of its Cords have a Leading Figure-Eight-Knots

What are the knot sequences for leading-eight-knot cords?

Code
```{python}
leading_8knot_cords = []
for index, row in leading_eight_knots_df.iterrows():
    aCord = khipu_dict[row['KFG_Name']][row['cord_name']]
    leading_8knot_cords.append(aCord)

leading_8knot_sequence_counts = Counter([aCord.knot_sequence() for aCord in leading_8knot_cords])
most_common_leading_eight_knot_sequences = leading_8knot_sequence_counts.most_common()

leading_8knot_counts = [(count, knot_sequence) for index, (knot_sequence, count) in enumerate(most_common_leading_eight_knot_sequences)]
fig = (go.Figure(go.Bar(
            x=[item[1] for item in leading_8knot_counts], 
            y=[item[0] for item in leading_8knot_counts], 
            orientation='v', 
            )))
fig.layout.update(width=950, height=600, title_text='Leading 8-Knot Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3.5 Middle Eight-Knot Khipus

A Middle Figure-8-Knot is a cord with at least three knots, of which any knot that is not the leading or trailing knot, is a figure-8-knot.

Code
```{python}
the_num_middle_eight_knot_khipus = middle_eight_knots_df['KFG_Name'].nunique()
the_num_middle_eight_knot_cords = middle_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_middle_eight_knot_khipus, len(KFG_Names))} ({the_num_middle_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have cords with Middle Figure-Eight-Knots")
print(f"{uloom.as_percent_string(the_num_middle_eight_knot_cords, total_num_khipu_cords)} ({the_num_middle_eight_knot_cords}/{total_num_khipu_cords}) of Cords have Middle Leading Eight-Knots\n")

print(f"Top 5 Khipus with the most Cords with Middle Eight-Knots")
print(f"--------------------------------------------------------")
for KFG_Name in list(num_middle_eight_knots_dict.keys())[:5]:
    the_num_kfg_middle_eight_knot_cords = num_middle_eight_knots_dict[KFG_Name]
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_middle_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_middle_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Middle Figure-Eight-Knots")
```
5.9% (42/711) of Khipus have cords with Middle Figure-Eight-Knots
0.2% (112/62837) of Cords have Middle Leading Eight-Knots

Top 5 Khipus with the most Cords with Middle Eight-Knots
--------------------------------------------------------
KH0676: 0.1% (  32 of 62837) of its Cords have a Middle Figure-Eight-Knots
KH0441: 0.0% (  19 of 62837) of its Cords have a Middle Figure-Eight-Knots
KH0103: 0.0% (   5 of 62837) of its Cords have a Middle Figure-Eight-Knots
KH0360: 0.0% (   4 of 62837) of its Cords have a Middle Figure-Eight-Knots
KH0519: 0.0% (   4 of 62837) of its Cords have a Middle Figure-Eight-Knots

What are the knot sequences for middle-eight-knot cords?

Code
```{python}
middle_8knot_cords = []
for index, row in middle_eight_knots_df.iterrows():
    aCord = khipu_dict[row['KFG_Name']][row['cord_name']]
    middle_8knot_cords.append(aCord)

middle_8knot_cords_8knot__sequence_counts = Counter([aCord.knot_sequence() for aCord in middle_8knot_cords])
most_common_middle_eight_knot_sequences = middle_8knot_cords_8knot__sequence_counts.most_common()
#for (index, (knot_sequence, count)) in enumerate(most_common_middle_eight_knot_sequences):
#       print(f"{index:02d}.  {knot_sequence} | {count}")

middle_8knot_counts = [(count, knot_sequence) for index, (knot_sequence, count) in enumerate(most_common_middle_eight_knot_sequences)]
fig = (go.Figure(go.Bar(
            x=[item[1] for item in middle_8knot_counts], 
            y=[item[0] for item in middle_8knot_counts], 
            orientation='v', 
            )))
fig.layout.update(width=950, height=600, title_text='Middle 8-Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3.6 Multiple Only Eight-Knot Khipus

A Multiple Only Figure-8-Knot cord is a cord with at least two knots (to distinguish this from a sole figure-8-knot), and all knots are figure-8-knots

Code
```{python}
the_num_multiple_only_eight_knot_khipus = multiple_only_eight_knots_df['KFG_Name'].nunique()
the_num_multiple_only_eight_knot_cords =  multiple_only_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_multiple_only_eight_knot_khipus, len(KFG_Names))} ({the_num_multiple_only_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have Cords with Multiple and Only Figure-Eight-Knots")
print(f"{uloom.as_percent_string(the_num_multiple_only_eight_knot_cords, total_num_khipu_cords)} ({the_num_multiple_only_eight_knot_cords}/{total_num_khipu_cords}) of Cords have Multiple and Only Figure-Eight-Knots\n")

print(f"Top 5 Khipus with the most Cords with Multiple Only Eight-Knots")
print(f"---------------------------------------------------------------")
for KFG_Name in list(num_multiple_only_eight_knot_dict.keys())[:5]:
    the_num_kfg_multiple_only_eight_knot_cords = num_multiple_only_eight_knot_dict[KFG_Name]
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_multiple_only_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_multiple_only_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Multiple Only Figure-Eight-Knots")
```
7.6% (54/711) of Khipus have Cords with Multiple and Only Figure-Eight-Knots
0.2% (144/62837) of Cords have Multiple and Only Figure-Eight-Knots

Top 5 Khipus with the most Cords with Multiple Only Eight-Knots
---------------------------------------------------------------
KH0702: 0.0% (  15 of 62837) of its Cords have a Multiple Only Figure-Eight-Knots
KH0619: 0.0% (  12 of 62837) of its Cords have a Multiple Only Figure-Eight-Knots
KH0108: 0.0% (   9 of 62837) of its Cords have a Multiple Only Figure-Eight-Knots
KH0419: 0.0% (   8 of 62837) of its Cords have a Multiple Only Figure-Eight-Knots
KH0239: 0.0% (   7 of 62837) of its Cords have a Multiple Only Figure-Eight-Knots
Code
```{python}
multiple_8knot_cords = []
for index, row in multiple_only_eight_knots_df.iterrows():
    aCord = khipu_dict[row['KFG_Name']][row['cord_name']]
    multiple_8knot_cords.append(aCord)

multiple_8knot_cords_8knot__sequence_counts = Counter([aCord.knot_sequence() for aCord in multiple_8knot_cords])
most_common_multiple_eight_knot_sequences = multiple_8knot_cords_8knot__sequence_counts.most_common()
#for (index, (knot_sequence, count)) in enumerate(most_common_multiple_eight_knot_sequences):
#       print(f"{index:02d}.  {knot_sequence} | {count}")

multiple_8knot_counts = [(count, knot_sequence) for index, (knot_sequence, count) in enumerate(most_common_multiple_eight_knot_sequences)]
fig = (go.Figure(go.Bar(
            x=[item[1] for item in multiple_8knot_counts], 
            y=[item[0] for item in multiple_8knot_counts], 
            orientation='v', 
            )))
fig.layout.update(width=950, height=600, title_text='Multiple 8-Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3.7 Mixed Eight-Knot Khipus

Cords that have Mixed Eight-Knots (i.e Leading + Middle, or Middle + Trailing or Leading + Trailing or Leading + Middle + Trailing, but not Only Eight-Knots)

Code
```{python}
the_num_mixed_eight_knot_khipus = mixed_eight_knots_df['KFG_Name'].nunique()
the_num_mixed_eight_knot_cords = mixed_eight_knots_df.shape[0]
print(f"{uloom.as_percent_string(the_num_mixed_eight_knot_khipus, len(KFG_Names))} ({the_num_mixed_eight_knot_khipus}/{len(KFG_Names)}) of Khipus have cords with Mixed Figure-Eight-Knots")
print(f"{uloom.as_percent_string(the_num_mixed_eight_knot_cords, total_num_khipu_cords)} ({the_num_mixed_eight_knot_cords}/{total_num_khipu_cords}) of Cords have Mixed Figure-Eight-Knots\n")

print(f"Top 5 Khipus with the most Cords with Mixed Eight-Knots")
print(f"-------------------------------------------------------")
for KFG_Name in list(num_mixed_eight_knots_dict.keys())[:5]:
    the_num_kfg_mixed_eight_knot_cords = num_mixed_eight_knots_dict[KFG_Name]
    frequency_str = f"{KFG_Name}: {uloom.as_percent_string(the_num_kfg_mixed_eight_knot_cords, total_num_khipu_cords)} ({the_num_kfg_mixed_eight_knot_cords:4d} of {total_num_khipu_cords:4d})"
    print(f"{frequency_str} of its Cords have a Mixed Figure-Eight-Knots")
```
6.5% (46/711) of Khipus have cords with Mixed Figure-Eight-Knots
0.2% (134/62837) of Cords have Mixed Figure-Eight-Knots

Top 5 Khipus with the most Cords with Mixed Eight-Knots
-------------------------------------------------------
KH0676: 0.0% (  17 of 62837) of its Cords have a Mixed Figure-Eight-Knots
KH0517: 0.0% (  13 of 62837) of its Cords have a Mixed Figure-Eight-Knots
KH0441: 0.0% (  10 of 62837) of its Cords have a Mixed Figure-Eight-Knots
KH0507: 0.0% (   9 of 62837) of its Cords have a Mixed Figure-Eight-Knots
KH0103: 0.0% (   6 of 62837) of its Cords have a Mixed Figure-Eight-Knots

What are the knot sequences for mixed-eight-knot cords?

Code
```{python}
mixed_8knot_cords = []
for index, row in mixed_eight_knots_df.iterrows():
    aCord = khipu_dict[row['KFG_Name']][row['cord_name']]
    mixed_8knot_cords.append(aCord)

mixed_8knot__sequence_counts = Counter([aCord.knot_sequence() for aCord in mixed_8knot_cords])
most_common_mixed_eight_knot_sequences = mixed_8knot__sequence_counts.most_common()
#for (index, (knot_sequence, count)) in enumerate(most_common_mixed_eight_knot_sequences):
#    print(f"{index:02d}.  {knot_sequence} | {count}")


mixed_8knot_counts = [(count, knot_sequence) for index, (knot_sequence, count) in enumerate(most_common_mixed_eight_knot_sequences)]
fig = (go.Figure(go.Bar(
            x=[item[1] for item in mixed_8knot_counts], 
            y=[item[0] for item in mixed_8knot_counts], 
            orientation='v', 
            )))
fig.layout.update(width=950, height=600, title_text='Mixed 8-Knot Sequence Frequency')
fig.update_layout(
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
)
fig.show()
```

3.8 Summary of Eight-Knots Cord Distribution

For this study, we move mixed figure-8-knots to the leading or trailing figure-8-knots category. We then get the following distribution for figure-8-knots based on cord knot-sequence type:

Code
```{python}
def has_leading_eight_knot(aCord):
    """ Does the cord have multiple knots, and the first knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots()
    return (len(search_knots) >= 2) and search_knots[0].is_eight_knot()

def has_middle_eight_knot(aCord):
    """ Does the cord have at least 3 knots, and at least one of the middle knots is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots()
    return (len(search_knots) >= 3) and any([aKnot.is_eight_knot() for aKnot in search_knots[1:-1]])
        
def has_trailing_eight_knot(aCord):
    """ Does the cord have multiple knots, and the last knot is an eight-knot and it's not only eight-knots? """
    search_knots = aCord.all_knots()
    return (len(search_knots) >= 2) and search_knots[-1].is_eight_knot()

def tag_cord_knots(aCord):
    """ Tag the knots on a cord with an eight-knot type """
    tag = "None"
    if has_sole_eight_knot(aCord):
        tag = "Sole_Eight_Knot"
    elif has_trailing_eight_knot(aCord):
        tag = "Trailing_Eight_Knot"
    elif has_leading_eight_knot(aCord):
        tag = "Leading_Eight_Knot"
    elif has_mutiple_only_eight_knots(aCord):
        tag = "Multiple_Only_Eight_Knots"
    elif has_middle_eight_knot(aCord):
        tag = "Middle_Eight_Knot"
    elif has_mixed_eight_knot(aCord):
        tag = "Mixed_Eight_Knot"
    return tag

eight_knot_cords_df = tag_eight_knot_cords()

sole_eight_knots_df= eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Sole_Eight_Knot']
trailing_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Trailing_Eight_Knot']
only_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Only_Eight_Knot']
leading_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Leading_Eight_Knot']
middle_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Middle_Eight_Knot']
mixed_eight_knots_df = eight_knot_cords_df[eight_knot_cords_df['tag'] == 'Mixed_Eight_Knot']

the_num_eight_knot_khipus = eight_knot_cords_df['KFG_Name'].nunique()
the_num_sole_eight_knot_khipus = sole_eight_knots_df['KFG_Name'].nunique()
the_num_trailing_eight_knot_khipus = trailing_eight_knots_df['KFG_Name'].nunique()
the_num_leading_eight_knot_khipus = leading_eight_knots_df['KFG_Name'].nunique()
the_num_middle_eight_knot_khipus = middle_eight_knots_df['KFG_Name'].nunique()
the_num_mixed_eight_knot_khipus = mixed_eight_knots_df['KFG_Name'].nunique()

the_num_eight_knot_cords = eight_knot_cords_df.shape[0]
the_num_trailing_eight_knot_cords = trailing_eight_knots_df.shape[0]
the_num_sole_eight_knot_cords = sole_eight_knots_df.shape[0]
the_num_multiple_only_eight_knot_cords = only_eight_knots_df.shape[0]
the_num_leading_eight_knot_cords = leading_eight_knots_df.shape[0]
the_num_middle_eight_knot_cords = middle_eight_knots_df.shape[0]
the_num_mixed_eight_knot_cords = mixed_eight_knots_df.shape[0]
```
Code
```{python}
# Error check - Should not fail..
tagged_eight_knot_cords = (the_num_sole_eight_knot_cords + the_num_multiple_only_eight_knot_cords + the_num_mixed_eight_knot_cords 
                           + the_num_leading_eight_knot_cords + the_num_middle_eight_knot_cords + the_num_trailing_eight_knot_cords)
if tagged_eight_knot_cords != the_num_eight_knot_cords:
    error_msg = "Error check failed - "
    error_msg += f"Tagged Eight-Knot Cords {tagged_eight_knot_cords} != Num Eight-Knot Cords {the_num_eight_knot_cords}"
    uloom.log_error(error_msg)
```
Code
```{python}
do_print = False
if do_print:
    print(f"| Knot Type | # Khipus | # Cords | ")
    print(f"|:------|--------:|--------:| ")
    print(f"| Eight-Knot Exists | {uloom.percent_info(the_num_eight_knot_khipus, len(all_khipus), as_html=True)} | {uloom.percent_info(the_num_eight_knot_cords, len(all_khipu_cords), as_html=True)} | ")
    print(f"|  Sole Eight-Knot | {uloom.percent_info(the_num_sole_eight_knot_khipus, len(all_khipus), as_html=True)} | {uloom.percent_info(the_num_sole_eight_knot_cords, the_num_eight_knot_cords, as_html=True)} | ")
    print(f"|  Trailing Eight-Knot | {uloom.percent_info(the_num_trailing_eight_knot_khipus, len(all_khipus), as_html=True)} | {uloom.percent_info(the_num_trailing_eight_knot_cords, the_num_eight_knot_cords, as_html=True)} | ")
    print(f"|  Leading Eight-Knot | {uloom.percent_info(the_num_leading_eight_knot_khipus, len(all_khipus), as_html=True)} | {uloom.percent_info(the_num_leading_eight_knot_cords, the_num_eight_knot_cords, as_html=True)} | ")
    print(f"|  Middle Eight-Knot | {uloom.percent_info(the_num_middle_eight_knot_khipus, len(all_khipus), as_html=True)} | {uloom.percent_info(the_num_middle_eight_knot_cords, the_num_eight_knot_cords, as_html=True)} | ")
```
Knot Type # Khipus # Cords
Eight-Knot Exists 77% (550 of 711) 15% (9344 of 62837)
Sole Eight-Knot 56% (401 of 711) 67% (6284 of 9344)
Trailing Eight-Knot 60% (428 of 711) 30% (2772 of 9344)
Leading Eight-Knot 8% (60 of 711) 2% (176 of 9344)
Middle Eight-Knot 6% (42 of 711) 1% (112 of 9344)

3.9 Figure-8-Knot Locations by Pendant/Subsidiary:

Code
```{python}
the_num_eight_knot_khipus = eight_knot_cords_df['KFG_Name'].nunique()
the_num_sole_eight_knot_khipus = sole_eight_knots_df['KFG_Name'].nunique()
the_num_trailing_eight_knot_khipus = trailing_eight_knots_df['KFG_Name'].nunique()
the_num_leading_eight_knot_khipus = leading_eight_knots_df['KFG_Name'].nunique()

num_figure_8_knot_cords = len(eight_knot_cords_df)
num_pendant_figure_8_knot_cords = len(pendant_eight_knot_cords_df)
num_subsidiary_figure_8_knot_cords = len(subsidiary_eight_knot_cords_df)

num_sole_eight_knot_cords = len(sole_eight_knots_df)
num_pendant_sole_eight_knot_cords = len(sole_eight_knots_df[sole_eight_knots_df['is_pendant']]) 
num_subsidiary_sole_eight_knot_cords = len(sole_eight_knots_df[sole_eight_knots_df['is_pendant']==False])

num_trailing_eight_knot_cords = len(trailing_eight_knots_df)
num_pendant_trailing_eight_knot_cords = len(trailing_eight_knots_df[trailing_eight_knots_df['is_pendant']])
num_subsidiary_trailing_eight_knot_cords = len(trailing_eight_knots_df[trailing_eight_knots_df['is_pendant']==False])

num_leading_eight_knot_cords = len(leading_eight_knots_df)
num_pendant_leading_eight_knot_cords = len(leading_eight_knots_df[leading_eight_knots_df['is_pendant']])
num_subsidiary_leading_eight_knot_cords = len(leading_eight_knots_df[leading_eight_knots_df['is_pendant']==False])

do_print = False
if (do_print):
    preInfo, postInfo = ("<i><small>", "</small></i>")
            
    print(f"| All Figure 8 Knot Cords | {uloom.percent_info(the_num_eight_knot_khipus, len(KFG_Names), as_html=True)} | {uloom.percent_info(num_figure_8_knot_cords, total_num_khipu_cords, as_html=True)} <br/><small>(# 8-knot cords vs # All khipu cords)</small> | {uloom.percent_info(num_pendant_figure_8_knot_cords, num_figure_8_knot_cords, as_html=True)} | {uloom.percent_info(num_subsidiary_figure_8_knot_cords, num_figure_8_knot_cords, as_html=True)} |")
    print(f"| Sole Figure 8 Knot Cords | {uloom.percent_info(the_num_sole_eight_knot_khipus, len(KFG_Names), as_html=True)} | {uloom.percent_info(num_sole_eight_knot_cords, num_figure_8_knot_cords, as_html=True)} | {uloom.percent_info(num_pendant_sole_eight_knot_cords, num_sole_eight_knot_cords, as_html=True)} | {uloom.percent_info(num_subsidiary_sole_eight_knot_cords, num_sole_eight_knot_cords, as_html=True)} |")
    print(f"| Trailing Figure 8 Knot Cords | {uloom.percent_info(the_num_trailing_eight_knot_khipus, len(KFG_Names), as_html=True)} | {uloom.percent_info(num_trailing_eight_knot_cords, num_figure_8_knot_cords, as_html=True)} | {uloom.percent_info(num_pendant_trailing_eight_knot_cords, num_trailing_eight_knot_cords, as_html=True)} | {uloom.percent_info(num_subsidiary_trailing_eight_knot_cords, num_trailing_eight_knot_cords, as_html=True)} |")
    print(f"| Leading Figure 8 Knot Cords | {uloom.percent_info(the_num_leading_eight_knot_khipus, len(KFG_Names),  as_html=True)} | {uloom.percent_info(num_leading_eight_knot_cords, num_figure_8_knot_cords, as_html=True)} | {uloom.percent_info(num_pendant_leading_eight_knot_cords, num_leading_eight_knot_cords, as_html=True)} | {uloom.percent_info(num_subsidiary_leading_eight_knot_cords, num_leading_eight_knot_cords, as_html=True)} |")
```
Figure-8-Knot Type % of Khipus # Cords of that Type % Of Pendant Cords % of Subsidiary Cords
All Figure 8 Knot Cords 77% (550 of 711) 15% (9344 of 62837)
(# 8-knot cords vs # All khipu cords)
61% (5655 of 9344) 39% (3689 of 9344)
Sole Figure 8 Knot Cords 56% (401 of 711) 67% (6284 of 9344) 52% (3294 of 6284) 48% (2990 of 6284)
Trailing Figure 8 Knot Cords 60% (428 of 711) 30% (2772 of 9344) 78% (2170 of 2772) 22% (602 of 2772)
Leading Figure 8 Knot Cords 8% (60 of 711) 2% (176 of 9344) 69% (122 of 176) 31% (54 of 176)

4 Figure-8-Knot Locations Over Groups

We now have an understanding of the distribution of Figure-8-Knots over khipu cords, and already see a distinction between “Lockean” and “Non-Lockean” knot sequences. Next, lets examine the location of cords with a Figure-8-Knot over khipu groups. We’ll look at cord locations across groups and then across khipus. If Figure-8-Knots play a “semiotic” role as a sign “marker”, then they should have a higher probability of occuring at the left and right edges of a group, and as might be expected, they do.

4.1 By Distance from Left or Right Edge of a Group:

Because khipus have different group sizes, we have to “sneak up” to our understanding of the probability of Figure-8-Knots occuring at the left/leading edge of a cord group vs the right/trailing edge of a cord group. First we simply look at the frequency of Figure-8-Knots at the left/leading edge of a cord group vs the right/trailing edge of a cord group. Then we “normalize” the locations by mapping the group size to a unit interval (0 to 1). For the purposes of this analysis, we’ll skip groups with only one cord.

4.1.1 By Distance from Leading/Trailing Edge

Let’s examine how often Figure-8-Knot Cords occur at the left/leading edge of a cord group vs the right/trailing edge of a cord group.

Code
```{python}
all_figure_8_knot_cords = [aCord for aCord in all_khipu_cords if aCord.has_eight_knot(include_pendants=True, include_subsidiaries=True)] #all_khipu_cords already includes subsidiaries

def in_left_half(test_index, num_items, include_middle=True):
    is_left=False
    if num_items == 0:
        is_left = False
    elif num_items == 1:
        is_left = include_middle
    else:
        mid_pt = float(num_items)/2.0 if include_middle else float(num_items-1)/2.0 
        is_left = test_index < mid_pt
    return is_left

assert in_left_half(0,0, include_middle=True) == False

assert in_left_half(0,1, include_middle=False) == False
assert in_left_half(0,1, include_middle=True) == True

assert in_left_half(0,2, include_middle=True) == True
assert in_left_half(1,2, include_middle=True) == False

assert in_left_half(1,3, include_middle=True) == True
assert in_left_half(2,3, include_middle=True) == False
assert in_left_half(1,3, include_middle=False) == False

def in_right_half(test_index, num_items, include_middle=True):
    is_right=False
    if num_items == 0:
        is_right = False
    elif num_items == 1:
        is_right = include_middle
    elif test_index >= num_items:
        is_right = False    
    else:
        mid_pt = float(num_items-1)/2.0 if include_middle else float(num_items)/2.0 
        is_right = test_index >= mid_pt
    return is_right

assert in_right_half(0,0, include_middle=True) == False

assert in_right_half(0,1, include_middle=False) == False
assert in_right_half(0,1, include_middle=True) == True

assert in_right_half(0,2, include_middle=True) == False
assert in_right_half(1,2, include_middle=True) == True
assert in_right_half(2,2, include_middle=True) == False

assert in_right_half(1,3, include_middle=True) == True
assert in_right_half(2,3, include_middle=True) == True
assert in_right_half(1,3, include_middle=False) == False
```
Code
```{python}
def is_left_half_cord(aCord):
    """ Is the cord in the left half of the khipu? """
    return in_left_half(aCord.find_pendant_cord().cord_group.position_index, aCord.find_pendant_cord().cord_group.num_pendant_cords(), include_middle=False)
    
figure_8_knot_cord_in_group_locs = [aCord.find_pendant_cord().group_index()+1 for aCord in all_figure_8_knot_cords if is_left_half_cord(aCord)] 
fig = (px.histogram(figure_8_knot_cord_in_group_locs, nbins=160, width=944, height=500, 
                    title='Histogram of Figure-8-Knot Left-Half Cord Locations by LEADING Position from Left in a Group',
                    labels={'value':'Locations by LEADING Cord Position (from Left) in a Group'},
                    )
        .update_layout(
            showlegend=False,
            font_family="Lucida Grande",
            font_color="black",
            font_size=12,
            title_font_family="Lucida Grande",
            title_font_color="black",
            legend_title_font_color="black")
        .show())               
```
Code
```{python}
def trailing_position(aCord):
    num_group_cords = aCord.find_pendant_cord().cord_group.num_pendant_cords()
    cord_index = aCord.find_pendant_cord().group_index() + 1
    the_position = -(num_group_cords - cord_index)
    return the_position
def is_right_half_cord(aCord):
    """ Is the cord in the left half of the khipu? """
    return in_right_half(aCord.find_pendant_cord().cord_group.position_index, aCord.find_pendant_cord().cord_group.num_pendant_cords(), include_middle=False)
    

figure_8_knot_cord_in_group_locs = [trailing_position(aCord) for aCord in all_figure_8_knot_cords if is_right_half_cord(aCord)] 
fig = (px.histogram(figure_8_knot_cord_in_group_locs, nbins=160, width=944, height=500, 
                    title='Histogram of Figure-8-Knot Right-Half Cord Locations by TRAILING Position from Right in a Group',
                    labels={'value':'Locations by Trailing Cord Position (from Right) in a Group'},
                    )
        .update_layout(
            showlegend=False,
            font_family="Lucida Grande",
            font_color="black",
            font_size=12,
            title_font_family="Lucida Grande",
            title_font_color="black",
            legend_title_font_color="black")
        .show())
```

4.1.2 By Normalized Location Within a Group

Code
```{python}
normalized_8knot_cord_in_group_locs = [float(aCord.find_pendant_cord().group_index())/float(aCord.cord_group.num_pendant_cords()-1) for aCord in all_figure_8_knot_cords if aCord.cord_group.num_pendant_cords() > 1] 

fig = (px.histogram(normalized_8knot_cord_in_group_locs, nbins=100, histnorm="probability",
                    width=944, height=500, 
                    title=f'Probability of Figure-8-Knot Normalized Cord Locations over a Group where #Pendants > 1')
        .update_layout(
            xaxis_title="Normalized Cord Location (0 to 1)",
            yaxis_title="Probability of Figure-8-Knot Cord",
            showlegend=False,
            font_family="Lucida Grande",
            font_color="black",
            font_size=12,
            title_font_family="Lucida Grande",
            title_font_color="black",
            legend_title_font_color="black")
        .show())
```

The probability distribution shows that, Figure-8-Knots congregate at the left, right, and middle locations of a group.

  • The first cord of a group has a high probability of containing a Figure-8-Knot
  • The last cord of a group has ~85% of the probability of the first cord containing a Figure-8-Knot
  • The middle cord has a medium probability it will contain a Figure-8-Knot.
  • Figure-8-Knot cord frequencies also spike at 1/3 and 2/3 of the way through a group.

4.2 By Group Size

Another understanding of how figure 8 knots locate is to graph the counts for each group size (2,3,4,…) for locations of Figure-8-Knot cords.

The first graph shows the frequency count of Figure-8-Knot cords for groups of different sizes. To see critical detail we clip the 5 outlier khipus with more than 80 pendant cords

Code
```{python}
max_size = 80

num_groups_by_size = [0] * (max_size+1)
for aCord in all_figure_8_knot_cords:
    cords_group_size = aCord.find_pendant_cord().cord_group.num_pendant_cords()
    if cords_group_size <= max_size:
        num_groups_by_size[cords_group_size] += 1

group_size_counts = [(index, count) for index, count in enumerate(num_groups_by_size) if count > 0] 
fig = (go.Figure(go.Bar(
            x=[item[1] for item in group_size_counts], 
            y=[item[0] for item in group_size_counts], 
            orientation='h', 
            )))
fig.update_layout(width=950, height=1200, 
    yaxis=dict(
        dtick=5,        # tick spacing
        tick0=0,        # where ticks start from
        range=[0, 80]),
    xaxis_title="Frequency of Figure-8-Knot Cords",
    yaxis_title="Group Size (# Pendant Cords)",
    title_text='Groups with Figure8KnotCords - Frequency of Group Size (# Pendant Cords)',
    font_family="Lucida Grande",
    font_color="black",
    title_font_family="Lucida Grande",
    title_font_color="black",
    legend_title_font_color="black"
    )
fig.show()
```
Code
```{python}
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

def make_figure8knot_by_group_size_df(max_size=None):
    if max_size is None:
        max_size = max([aCord.cord_group.num_pendant_cords() for aCord in all_figure_8_knot_cords])
            
    fig_8_knot_table = [[None] * (max_size+4) for i in range((max_size+1))]    
    for aCord in all_figure_8_knot_cords:
        row_index = aCord.cord_group.num_pendant_cords()
        col_index = aCord.find_pendant_cord().group_index()+1  
        in_max_bounds = (row_index < max_size) and (col_index < max_size)
        in_min_bounds = (row_index > 0) and (col_index > 0)
        if in_min_bounds and in_max_bounds:
            fig_8_knot_table[row_index][col_index] = fig_8_knot_table[row_index][col_index]+1 if (not(fig_8_knot_table[row_index][col_index]) is None) else 1
        
    df = pd.DataFrame(fig_8_knot_table)
    return df
    
max_size = 90
df = make_figure8knot_by_group_size_df(max_size)
df.to_csv(f"{uloc.fieldmarks_data_dir()}/figure8knot_by_group_size.csv")

max_demo_size = 17
demo_df = df.copy()
demo_df = demo_df[demo_df.columns[:(max_demo_size+1)]][:(max_demo_size+1)].fillna(0)
cols = demo_df.columns.tolist()
demo_df[cols] = demo_df[cols].applymap(np.int64)
demo_df=demo_df.replace(0,' ')
demo_df["# Groups of Size"] = [0]+ [num_groups for (x,num_groups) in group_size_counts[:(max_demo_size)]]
print(f"Top {max_demo_size} Figure-8-Knot Cord Counts, by Position, in Groups of up to {max_demo_size} Pendants in Size")
demo_df.head(max_demo_size+2)
```
Top 17 Figure-8-Knot Cord Counts, by Position, in Groups of up to 17 Pendants in Size
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Groups of Size
0 0
1 511 511
2 195 174 369
3 216 165 153 534
4 211 192 192 209 804
5 194 193 155 164 138 844
6 168 216 229 203 159 151 1126
7 67 100 105 92 114 88 46 612
8 80 115 109 109 97 79 64 85 738
9 114 80 78 82 86 83 74 84 93 774
10 80 70 68 65 67 59 73 71 69 48 670
11 27 37 38 29 38 41 37 39 47 34 29 396
12 40 37 43 52 40 27 31 37 41 40 30 46 464
13 6 2 5 6 4 9 7 15 11 12 10 5 10 102
14 17 17 19 18 15 17 16 15 13 17 16 17 23 9 229
15 12 12 20 12 10 12 14 18 15 10 13 11 11 15 31 216
16 13 24 17 14 18 13 12 14 14 14 13 13 17 11 8 18 233
17 8 12 10 8 9 19 14 14 17 6 17 12 9 9 11 10 4 189
Code
```{python}
def draw_figure8knot_by_group_size(max_size=None, df=None):
    if df is None: df = make_figure8knot_by_group_size_df(max_size)
    fig = (px.imshow(df.to_numpy().tolist(),
                    labels=dict(x="Cord Position From Left", y="# Group Pendant Cords", ),
                    x=df.columns,
                    y=df.index.tolist(), 
                    color_continuous_scale=px.colors.sequential.Viridis,
                    width=944, height=944, aspect="auto")
            .update_coloraxes(showscale=True)
            .update_layout(
                showlegend=False,
                font_family="Lucida Grande",
                font_size=14,
                font_color="black",
                title_font_family="Lucida Grande",
                title_font_color="black",
                legend_title_font_color="black")
            .update_layout(
                yaxis = dict(tickfont = dict(size=8)),
                xaxis= dict(tickangle=270),
                title=f"Figure-8-Knot Cord Counts, by Position, in Groups up to {max_size} Pendants in Size")
            .show())

draw_figure8knot_by_group_size(max_size)
```

5 Figure-8-Knot Locations over Khipus:

To compute locations of cords, across khipus of different sizes, we compute a “normalized” location. The normalized location is the cord’s location divided by the total number of (pendant) cords in the khipu.
Then we can examine the (binned) histogram/distribution…of normalized locations.

Code
```{python}
normalized_8knot_cord_locs = [float(aCord.pendant_index())/float(aCord.khipu.num_pendant_cords()-1) for aCord in all_figure_8_knot_cords if aCord.khipu.num_pendant_cords() > 1] 

fig = (px.histogram(normalized_8knot_cord_locs, nbins=100, width=944, height=600, 
                    histnorm="probability",
                    title='Figure-8-Knot Normalized Cord Locations over a Khipu')
        .update_layout(
            xaxis_title="Normalized Cord Location (0 to 1)",
            yaxis_title="Probability of Figure-8-Knot Cord",
            showlegend=False,
            font_family="Lucida Grande",
            font_color="black",
            title_font_family="Lucida Grande",
            title_font_color="black",
            legend_title_font_color="black")
        .show())
```

While Figure-8-Knots do spike in frequency at the beginning and end cord of a khipu, the above distribution seems closer to overall uniform noise.

Let’s broaden our scope to normalize by group rather than by cord. For all khipus with more than one cord group, let’s evaluate the cord group’s position:

Code
```{python}
normalized_8knot_cord_group_locs = [float(aCord.find_pendant_cord().cord_group.position_index)/float(aCord.khipu.num_cord_groups()-1) 
                                        for aCord in all_figure_8_knot_cords if aCord.khipu.num_cord_groups() > 1] 
fig = (px.histogram(normalized_8knot_cord_group_locs, nbins=100,  histnorm="probability",
                    width=944, height=500, 
                    title='Probability of Figure-8-Knot Normalized by Cord Group Locations over a Khipu')
        .update_layout(
            xaxis_title="Normalized Cord Group Location (0 to 1)",
            yaxis_title="Probability of 8-Knot in Group",
            showlegend=False,
            font_family="Lucida Grande",
            font_color="black",
            font_size=14,
            title_font_family="Lucida Grande",
            title_font_color="black",
            legend_title_font_color="black")
        .show())
```

Fascinating. Now we see a significant spike in the probability of a Figure-8-Knot occuring in the first and last group of a khipu. In Brezinne and Urton’s article on the Puruchuco khipus, they note that a figure-8-knot frequently occurs at the beginning of the Puruchuco khipus, and that may signify a toponymic presence. We clearly see evidence of Figure-8-Knots playing some kind of semiotic role in the first group of the KFG database.

6. Conclusions

  • In section 1.3 Trailing vs Sole Digits it was shown that there is indeed a distinction in distribution of Sole digits vs Trailing digits on pendant vs subsidiary cords.

  • In section 2.1 Common Knot Sequences for All Khipu Cords it was shown:

    • The most common value range is 2-9, represented as an L knot, occupies 21% of the cords.
    • The second most common value range 11-99, represented as S,L, occupies 14% of the cords.
    • The third most common value, the value 1, represented as a Sole Figure-8-Knot E, occupies 10% of the cords.
    • E Sole Eight Knots reside equally (52%/48%) on pendants vs subsidiaries. This is unlike any of the other knot sequences which are typically 3 to 1 or 4 to 1 ratios for pendants vs subsidiaries
    • S,E, S,S,E, L,E all have a Trailing Figure-Eight-Knot. and represent ~3% of the cords.
    • The value range 101-999, represented as S,S,L knot sequences are fifth on the list.
  • In section 4.1.2 Figure-8-Knot Cord Locations Within a Group it was shown that Figure-8-Knots are significantly more likely to occur in the first and last cord of a group.

    • The probability distribution shows that, Figure-8-Knots congregate at the left, right, and middle locations of a group.
    • The first cord of a group has a high probability of containing a Figure-8-Knot
    • The last cord of a group has ~85% of the probability of the first cord containing a Figure-8-Knot
    • The middle cord has a medium probability it will contain a Figure-8-Knot.
    • Figure-8-Knot cord frequencies also spike at 1/3 and 2/3 of the way through a group.

Finally, in section 5 Figure-8-Knot Groups Within a Khipu it was shown that groups in khipus with figure 8 knots have a similar to cord-in-group location distribution. Significantly we confirmed that the leftmost and/or rightmost groups of a khipu have a significantly higher than random likelihood of containing a Figure-8-Knot in their cords.

6.1 Summary of Knot Sequences and Figure-8 Knot Sequences

The top knot sequences in the KFG database with more than 200 occurrences (out of ~60,000 cords) are:

Index Knot Type Sequence # Cords # Pendant Cords # Subsidiary Cords
- - 59144 Cords 72% (42572 of 59144) 28% (16572 of 59144)
0 No Knots 30% (19058 of 62837) 74% (14195 of 19058) 26% (4863 of 19058)
1 L 21% (13000 of 62837) 64% (8317 of 13000) 36% (4683 of 13000)
2 S,L 14% (8509 of 62837) 80% (6801 of 8509) 20% (1708 of 8509)
3 E 10% (6212 of 62837) 52% (3253 of 6212) 48% (2959 of 6212)
4 S 9% (5963 of 62837) 72% (4293 of 5963) 28% (1670 of 5963)
5 S,S,L 4% (2617 of 62837) 89% (2332 of 2617) 11% (285 of 2617)
6 S,E 2% (1325 of 62837) 78% (1033 of 1325) 22% (292 of 1325)
7 S,S 2% (1127 of 62837) 84% (948 of 1127) 16% (179 of 1127)
8 L,L 1% (668 of 62837) 84% (558 of 668) 16% (110 of 668)
9 S,S,S,L 1% (586 of 62837) 94% (548 of 586) 6% (38 of 586)
10 L,L,L 1% (412 of 62837) 92% (380 of 412) 8% (32 of 412)
11 S,S,E 1% (338 of 62837) 89% (301 of 338) 11% (37 of 338)
12 L,E 0% (313 of 62837) 67% (211 of 313) 33% (102 of 313)
13 S,S,S 0% (250 of 62837) 82% (206 of 250) 18% (44 of 250)
14 S,L,E 0% (207 of 62837) 0% (0 of 207) 100% (207 of 207)

6.2 Summary of Eight-Knot Types

Knot Type # Khipus # Cords
Eight-Knot Exists 77% (550 of 711) 15% (9344 of 62837)
Sole Eight-Knot 56% (401 of 711) 67% (6284 of 9344)
Trailing Eight-Knot 60% (428 of 711) 30% (2772 of 9344)
Leading Eight-Knot 8% (60 of 711) 2% (176 of 9344)
Middle Eight-Knot 6% (42 of 711) 1% (112 of 9344)



From now on, we will focus on cases of either a cord with a trailing Figure-Eight-Knot, or a cord with a sole Figure-Eight-Knot. If it’s a mixed Figure-Eight-Knot, or multiple-only Figure-Eight-Knots it will be considered to be a leading and trailing Figure-Eight-Knot.

Accordingly, the new measures are:

Figure-8-Knot Type % of Khipus # Cords of that Type % Of Pendant Cords % of Subsidiary Cords
All Figure 8 Knot Cords 77% (550 of 711) 15% (9344 of 62837)
(# 8-knot cords vs # All khipu cords)
61% (5655 of 9344) 39% (3689 of 9344)
Sole Figure 8 Knot Cords 56% (401 of 711) 67% (6284 of 9344) 52% (3294 of 6284) 48% (2990 of 6284)
Trailing Figure 8 Knot Cords 60% (428 of 711) 30% (2772 of 9344) 78% (2170 of 2772) 22% (602 of 2772)
Leading Figure 8 Knot Cords 8% (60 of 711) 2% (176 of 9344) 69% (122 of 176) 31% (54 of 176)



The apportionment of knot types, pendant/subsidiaries, etc., is summarized in the Sankey Diagram below:

Significant points:

  • 77% of the khipus in the KFG have Figure-8-Knot cords
  • Only 15% of the cords in the KFG have Figure-8-Knot cords
  • There is a rough 2:1 ratio of Sole-8 Knot cords (i.e. a knot-sequence of ‘E’) to Trailing Figure-8-Knot cords (i.e. ‘S,E’, ‘S,L,E’, etc)
  • Sole-8-Knot cords occur roughly equally 1:1 on primaries and subsidiaries, but Trailing Figure-8-Knot cords have the more common 2:1 or 3:1 pendant to subsidiary frequency count.

Figure-8-Knot Sankey Diagram

6.3 Summary of 8 Knot Locations

At a khipu level, we see that:

  • Figure-8-Knot cords have a high probability of occuring in the first group or last group of a khipu.
  • The middle group has a medium probability it will contain a figure 8 knot cord.
  • Figure-8-Knot cord frequencies also spike at 1/4 and 3/4 of the way through the groups in the khipus.

Similarly by analogy, at a group level:

  • The first cord of a group has a high probability of containing a figure-8-Knot cords
  • The last cord of a group has ~3/4 of the probability of the first cord containing a figure-8-knot cord
  • The middle cord has a medium probability it will contain a figure 8 knot cord.
  • Figure-8-Knot cord frequencies also spike at 1/3 and 2/3 of the way through a group.

The following images layout the evidence for these claims:

Figure8Knot Locations by Normalized Group Position in Khipu

Figure8Knot Locations by Normalized Position in Group

6.4 : Summary of All Figure-8-Knot Khipus

6.4.1 Quick Results

Measure Result
Number of Khipus That Match 516 (78%)
Five most Interesting Khipus KH0239, KH0242, KH0323, KH0329, KH0349

6.4.2 Summary Chart:

Code
```{python}
# Read in the Fieldmark and its associated dataframe and match dictionary
from fieldmark_figure8knots import FieldmarkFigure8Knots
figure8knot_fieldmark = FieldmarkFigure8Knots()
figure8_knot_fieldmark_df = figure8knot_fieldmark.fieldmark_df().sort_values('num_8knot_cords', ascending=False)
figure8_knot_fieldmark_df.sort_values('num_8knot_cords', ascending=False, inplace=True)   
raw_match_dict = figure8knot_fieldmark.raw_match_dict()

# Plot Matching khipu
matching_khipus = figure8knot_fieldmark.matching_khipus()[:100]
matching_values = [raw_match_dict[aKFG_Name] for aKFG_Name in matching_khipus]
matching_df =  pd.DataFrame(list(zip(matching_khipus[::-1], matching_values[::-1])), columns =['KFG_Name', 'Value'])
fig = (px.bar(matching_df, x='Value', y='KFG_Name', labels={"KFG_Name": "Khipu Name", "Value": "# of Cords (Including Subsidiaries) with Figure-8-Knots", }, 
             title=f"Top 100 Matching Khipus  - # of Cords (Including Subsidiaries) w Figure-8-Knots",  
             width=944, height=1500)
         .update_layout(showlegend=True,
              yaxis = dict(tickfont = dict(size=8)),
              xaxis= dict(tickangle=270),
              font_family="Lucida Grande",
              font_color="black",
              title_font_family="Lucida Grande",
              title_font_color="black",
              legend_title_font_color="black")
        .show())
```