"""A3. Word-internal structure: positional glyph distributions, the q-o rule, word-final set, gallows, 'iin' groups, and a simple slot grammar coverage test. Outputs: out/tables/glyph_positions.csv, out/tables/wordgrammar.csv, out/figs/glyph_positions.pdf, out/tables/common_prefixes_suffixes.csv """ import os, sys _here = os.path.dirname(os.path.abspath(__file__)) # data/analysis, or a subfolder of it sys.path.insert(0, _here if os.path.exists(os.path.join(_here, "paths.py")) else os.path.dirname(_here)) from paths import * # noqa: repository locations import collections, csv, re, os import matplotlib; matplotlib.use("Agg") import matplotlib.pyplot as plt from corpus import Corpus, tokenize_glyphs C = Corpus() W = C.running_text() types = collections.Counter(W) N = len(W) # positional distribution per glyph unit pos = collections.defaultdict(lambda: collections.Counter()) for w in W: g = tokenize_glyphs(w) for i, u in enumerate(g): if len(g) == 1: p = "only" elif i == 0: p = "initial" elif i == len(g) - 1: p = "final" else: p = "medial" pos[u][p] += 1 rows = [] for u, c in sorted(pos.items(), key=lambda kv: -sum(kv[1].values())): tot = sum(c.values()) rows.append(dict(glyph=u, total=tot, initial=c["initial"] / tot, medial=c["medial"] / tot, final=c["final"] / tot, only=c["only"] / tot)) with open("out/tables/glyph_positions.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=rows[0].keys()); w.writeheader(); w.writerows(rows) for r in rows[:24]: print(f"{r['glyph']:4s} {r['total']:6d} init {r['initial']:.2f} med {r['medial']:.2f} fin {r['final']:.2f}") # figure: stacked bars for the 20 most frequent units fig, ax = plt.subplots(figsize=(5.6, 3.2)) top = [r for r in rows if r["total"] > 300][:20] xs = range(len(top)); bottom = [0] * len(top) for key, col in [("initial", "#222222"), ("medial", "#888888"), ("final", "#cccccc"), ("only", "#ffffff")]: vals = [r[key] for r in top] ax.bar(xs, vals, bottom=bottom, color=col, edgecolor="#222222", linewidth=0.4, label=key) bottom = [b + v for b, v in zip(bottom, vals)] ax.set_xticks(list(xs)); ax.set_xticklabels([r["glyph"] for r in top], fontsize=8) ax.set_ylabel("share of occurrences"); ax.legend(fontsize=7, frameon=False, ncol=4, loc="upper center", bbox_to_anchor=(0.5, 1.15)) ax.spines[["top", "right"]].set_visible(False) fig.tight_layout(); fig.savefig("out/figs/glyph_positions.pdf") # --- specific rules, counted stats = {} q_words = [w for w in W if "q" in w] stats["q_tokens"] = len(q_words) stats["q_initial"] = sum(w.startswith("q") for w in q_words) / len(q_words) stats["q_followed_by_o"] = sum(w.startswith("qo") for w in q_words) / len(q_words) finals = collections.Counter(w[-1] for w in W) stats["final_y_n_l_r_s_m_share"] = sum(finals[c] for c in "ynlrsm") / N stats["final_top"] = ",".join(f"{c}:{n / N:.2f}" for c, n in finals.most_common(8)) inits = collections.Counter(w[0] for w in W) stats["initial_top"] = ",".join(f"{c}:{n / N:.2f}" for c, n in inits.most_common(8)) gall = re.compile(r"[ktpf]") stats["words_with_gallows"] = sum(bool(gall.search(w)) for w in W) / N stats["words_with_two_plus_gallows"] = sum(len(gall.findall(w)) >= 2 for w in W) / N stats["words_with_iin_group"] = sum("iin" in w or "in" in w for w in W) / N stats["in_group_final"] = sum(bool(re.search(r"i*n$", w)) and "n" in w for w in W) / sum("n" in w for w in W) stats["words_with_ed"] = sum("ed" in w for w in W) / N with open("out/tables/wordrules.csv", "w", newline="") as f: w = csv.writer(f); w.writerow(["rule", "value"]) for k, v in stats.items(): w.writerow([k, v]) print(stats) # --- slot grammar coverage: how much of the text is covered by a simple # prefix + core + suffix template (an operationalisation of Tiltman/Stolfi) PRE = r"(q?o|d|s|ch|sh|y|a|o|l|r|t|k|p|f|cth|ckh|cph|cfh|)" GALL = r"(cth|ckh|cph|cfh|t|k|p|f|)" MID = r"(ch|sh|e{0,3}|ee|eo|o|a|)" SUF = r"(d?y|ain|aiin|aiiin|an|ar|al|or|ol|am|dy|ey|eey|edy|eedy|ey|s|l|r|n|m|d|o|a|e|)" GRAM = re.compile(f"^{PRE}{GALL}{MID}{SUF}$") GRAM2 = re.compile(f"^{PRE}{GALL}{MID}(d|l|r|s|)?{SUF}$") cov = sum(types[w] for w in types if GRAM.match(w)) / N cov2 = sum(types[w] for w in types if GRAM2.match(w)) / N covt = sum(1 for w in types if GRAM2.match(w)) / len(types) print("slot grammar token coverage:", round(cov, 3), round(cov2, 3), "type coverage", round(covt, 3)) with open("out/tables/wordgrammar.csv", "w", newline="") as f: w = csv.writer(f); w.writerow(["grammar", "token_coverage", "type_coverage"]) w.writerow(["PRE GALL MID SUF", cov, sum(1 for w in types if GRAM.match(w)) / len(types)]) w.writerow(["PRE GALL MID (d|l|r|s) SUF", cov2, covt]) # most frequent unmatched unm = collections.Counter({w: c for w, c in types.items() if not GRAM2.match(w)}) print("top unmatched:", unm.most_common(25)) # --- common word-initial and word-final 2-3 letter strings pre2 = collections.Counter(w[:2] for w in W if len(w) > 2); suf3 = collections.Counter(w[-3:] for w in W if len(w) > 3) with open("out/tables/common_prefixes_suffixes.csv", "w", newline="") as f: w = csv.writer(f); w.writerow(["kind", "string", "count", "share"]) for s, c in pre2.most_common(15): w.writerow(["initial-2", s, c, c / N]) for s, c in suf3.most_common(15): w.writerow(["final-3", s, c, c / N]) print(pre2.most_common(12)); print(suf3.most_common(12))