To get started: consult start
We spot the many similarities between lines in the corpus.
There are ca 50,000 lines in the corpus of which ca 35,000 with real content. To compare these requires more than half a billion comparisons. That is a costly operation. On this laptop it took 21 whole minutes.
The good news it that we have stored the outcome in an extra feature.
This feature is packaged in a TF data module, that we will automatically loaded with the DSS.
%load_ext autoreload
%autoreload 2
import collections
from tf.app import use
A = use("etcbc/dss", hoist=globals())
This is Text-Fabric 9.2.2 Api reference : https://annotation.github.io/text-fabric/tf/cheatsheet.html 67 features found and 1 ignored
The new feature is sim and it it an edge feature. It annotates pairs of lines $(l, m)$ where $l$ and $m$ have similar content. The degree of similarity is a percentage (between 60 and 100), and this value is annotated onto the edges.
Here is an example:
allLines = F.otype.s("line")
nLines = len(allLines)
exampleLine = allLines[0]
sisters = E.sim.b(exampleLine)
print(f"{len(sisters)} similar lines")
print("\n".join(f"{s[0]} with similarity {s[1]}" for s in sisters[0:10]))
A.table(tuple((s[0],) for s in ((exampleLine,), *sisters)), end=10)
1 similar lines 1563769 with similarity 69
n | p | line |
---|---|---|
1 | CD 1:1 | ועתה שמעו כל יודעי צדק ובינו במעשי |
2 | 4Q268 f1:9 | ועתה שמעו ל׳י כול יודעי צדק ובינו במעשי אל ׃ כי ריב |
Let's first find out the range of similarities:
minSim = None
maxSim = None
similarity = dict()
for ln in F.otype.s("line"):
sisters = E.sim.f(ln)
if not sisters:
continue
for (m, s) in sisters:
similarity[(ln, m)] = s
thisMin = min(s[1] for s in sisters)
thisMax = max(s[1] for s in sisters)
if minSim is None or thisMin < minSim:
minSim = thisMin
if maxSim is None or thisMax > maxSim:
maxSim = thisMax
print(f"minimum similarity is {minSim:>3}")
print(f"maximum similarity is {maxSim:>3}")
minimum similarity is 60 maximum similarity is 100
We give a few examples of the least similar lines.
We can use a search template to get the 90% lines.
query = """
line
-sim=60> line
"""
In words: find a line connected via a sim-edge with value 60 to an other line.
results = A.search(query)
0.14s 24546 results
A.table(results, start=1, end=10, withPassage="1 2")
n | line | line |
---|---|---|
1 | CD 3:9 בעדת׳ם ׃ ובני׳הם ב׳ו אבדו ומלכי׳הם ב׳ו נכרתו וגיבורי׳הם ב׳ו | 4Q269 f2:4 אבדו ומלכי׳הם ב׳ו נכרתו וגבורי׳הם ב׳ו אבדו וארצ׳ם ב׳ו שממה ׃ |
2 | CD 7:11 אשר אמר יבוא עלי׳ך ועל עמ׳ך ועל בית אבי׳ך ימים אשר לא | 4Q59 f2_3:1 יביא יהוה עלי׳ך ועל עמ׳ך ועל בית אבי׳ך ימים אשר לא באו למיום סור אפרים |
3 | CD 10:7 ועשרים שנה עד בני ששים שנה ׃ ואל יתיצב עוד מבן | 4Q266 f8iii:6 ובישודי הברית מבני חמש ועשרים שנה ועד בן ששים שנה ׃ ואל יתיצב |
4 | CD 10:16 רחוק מן השער מלוא׳ו ׃ כי הוא אשר אמר שמור את | 4Q270 f6v:2 מן העת אשר יהיה גלגל השמש רחוק מן השער מלוא׳ו כי הוא אשר אמר |
5 | CD 11:6 אם אלפים באמה ׃ אל ירם את יד׳ו להכות׳ה באגרוף אם | 4Q271 f5i:3 אל ירם איש את יד׳ו להכות׳ה באגרוף ׃ אם סוררת היא אל יוציא׳ה |
6 | CD 11:16 וכל נפש אדם אשר תפול אל מים מקום מים ואל מקום | 4Q270 f6v:19 הון ובצע בשבת ׃ וכל נפש אדם אשר תפול אל מקום מים ואל בור אל |
7 | CD 12:16 והעפר אשר יגואלו בטמאת האדם לגאולי שמן ב׳הם כפי | 4Q266 f9ii:3 יגואלו בטמאת האדם לגאולי שמן ב׳הם כפי טמאת׳ם יטמא |
8 | CD 12:17 טמאת׳ם יטמא הנוגע ב׳ם ׃ וכל כלי מסמר מסמר או יתד בכותל | 4Q266 f9ii:4 הנוגע ב׳ם ׃ וכול כלי מסמר ויתד בכותל אשר יהיו עם |
9 | CD 13:13 מבני המחנה להביא איש אל העדה זולת פי המבקר אשר למחנה ׃ | 4Q267 f9iv:10 ימשול איש מכול ? בני המחנה להביא איש אל העדה |
10 | CD 14:6 שלושת׳ם והגר רביע ׃ וכן ישבו וכן ישאלו לכל ׃ והכהן אשר יפקד | 4Q269 f10ii:11 ישראל שלשיים והגר רביע ׃ וכן ישבו וכן ישאלו לכול ׃ |
Or in full layout:
A.table(results, start=1, end=10, fmt="layout-orig-full", withPassage="1 2")
n | line | line |
---|---|---|
1 | CD 3:9 בעדת׳ם ׃ ובני׳הם ב׳ו אבדו ומלכי׳הם ב׳ו נכרתו וגיבורי׳הם ב׳ו | 4Q269 f2:4 אבדו ומלכי׳הם ב׳ו נכרתו וגבורי׳הם ב׳ו אבדו וארצ׳ם ב׳ו שממה ׃ |
2 | CD 7:11 אשר אמר יבוא עלי׳ך ועל עמ׳ך ועל בית אבי׳ך ימים אשר לא | 4Q59 f2_3:1 יביא יהוה עלי׳ך ועל עמ׳ך ועל בית אבי׳ך ימים אשר לא באו למיום סור אפרים |
3 | CD 10:7 ועשרים שנה עד בני ששים שנה ׃ ואל יתיצב עוד מבן | 4Q266 f8iii:6 ובישודי הברית מבני חמש ועשרים שנה ועד בן ששים שנה ׃ ואל יתיצב |
4 | CD 10:16 רחוק מן השער מלוא׳ו ׃ כי הוא אשר אמר שמור את | 4Q270 f6v:2 מן העת אשר יהיה גלגל השמש רחוק מן השער מלוא׳ו כי הוא אשר אמר |
5 | CD 11:6 אם אלפים באמה ׃ אל ירם את יד׳ו להכות׳ה באגרוף אם | 4Q271 f5i:3 אל ירם איש את יד׳ו להכות׳ה באגרוף ׃ אם סוררת היא אל יוציא׳ה |
6 | CD 11:16 וכל נפש אדם אשר תפול אל מים מקום מים ואל מקום | 4Q270 f6v:19 הון ובצע בשבת ׃ וכל נפש אדם אשר תפול אל מקום מים ואל בור אל |
7 | CD 12:16 והעפר אשר יגואלו בטמאת האדם לגאולי שמן ב׳הם כפי | 4Q266 f9ii:3 יגואלו בטמאת האדם לגאולי שמן ב׳הם כפי טמאת׳ם יטמא |
8 | CD 12:17 טמאת׳ם יטמא הנוגע ב׳ם ׃ וכל כלי מסמר מסמר או יתד בכותל | 4Q266 f9ii:4 הנוגע ב׳ם ׃ וכול כלי מסמר ויתד בכותל אשר יהיו עם |
9 | CD 13:13 מבני המחנה להביא איש אל העדה זולת פי המבקר אשר למחנה ׃ | 4Q267 f9iv:10 ימשול איש מכול ? בני המחנה להביא איש אל העדה |
10 | CD 14:6 שלושת׳ם והגר רביע ׃ וכן ישבו וכן ישאלו לכל ׃ והכהן אשר יפקד | 4Q269 f10ii:11 ישראל שלשיים והגר רביע ׃ וכן ישבו וכן ישאלו לכול ׃ |
Let's find out which lines have the most correspondences.
parallels = {}
for (ln, m) in similarity:
parallels.setdefault(ln, set()).add(m)
parallels.setdefault(m, set()).add(ln)
print(f"{len(parallels)} out of {nLines} lines have at least one similar line")
16114 out of 52895 lines have at least one similar line
rankedParallels = sorted(
parallels.items(),
key=lambda x: (-len(x[1]), x[0]),
)
for (ln, paras) in rankedParallels[0:10]:
print(
f'{len(paras):>4} siblings of {ln} = {T.text(ln)} = {T.text(ln, fmt="text-source-full", descend=True)}'
)
317 siblings of 1554667 = ε # ם והב # # ל # # # # # # # # # ε = -- \M whb\\l\\\ \\\\\\ -- 291 siblings of 1565610 = ε ותי׳כם ε = -- wty/kM -- 291 siblings of 1569619 = ε # ותי׳הם # ε ׃ = -- \wty/hM \ -- . 291 siblings of 1578909 = ε # # # ותי׳כה ε ׃ = -- \ \ \wty/kh -- . 291 siblings of 1579081 = ε # ותי׳נו ε = -- \wty/nw -- 190 siblings of 1555321 = ε ירים למ # ε = -- yryM lm\ -- 190 siblings of 1577062 = ε ות׳ם לה # ε = -- wt/M lh\ -- 190 siblings of 1582371 = ε # ין ל׳הון ε = -- \yN l/hwN -- 181 siblings of 1554556 = ε # # # # ם וכול # # # # = -- \\\\M wkwl \\ □\\ 181 siblings of 1559975 = ε ין וכל ε = -- yN wkl --
for (ln, paras) in rankedParallels[100:110]:
print(
f'{len(paras):>4} siblings of {T.text(ln)} = {T.text(ln, fmt="text-source-full", descend=True)}'
)
102 siblings of ε ם והפריח ε = -- M whpryj -- 102 siblings of וצואהוא # ε = wxwahwa \ -- 102 siblings of ε # כרם וה ׃ = -- \ krM wh □ . 102 siblings of יחדו וית # ε = yjdw wyt\ -- 102 siblings of וכ # ל ε ׳כה = wk\l -- /kh 102 siblings of ε ים ואיכה = -- yM waykh 102 siblings of ε # ותוצאת ε = -- \ wtwxat -- 102 siblings of וי # # חוץ ו # ε = wy\ \jwX w\ -- 102 siblings of ε י׳הם ודב ε = -- y/hM wdb -- 102 siblings of ε ת ומנינ # ε ׃ = -- t wmnyn\ -- .
for (ln, paras) in rankedParallels[500:510]:
print(
f'{len(paras):>4} siblings of {T.text(ln)} = {T.text(ln, fmt="text-source-full", descend=True)}'
)
45 siblings of ε ב׳כה ובתורה ε = -- b/kh wbtwrh -- 45 siblings of ε ים אשר ε = -- yM aCr -- 45 siblings of אלוהים לכול ε = alwhyM lkwl -- 45 siblings of ובבינת ε = wbbynt -- 45 siblings of ε ית׳כה אשר ε = -- yt/kh aCr -- 45 siblings of ε # י׳כה אשר ε = -- \y/kh aCr -- 45 siblings of ובעשרין ε = wboCryN -- 45 siblings of ε ים אשר ε ׃ ╱ = -- yM aCr -- . ╱ 44 siblings of ε ובעדת׳נו ε ׃ = -- wbodt/nw □ -- . 44 siblings of ε לכול עולמים ε ׃ = -- lkwl owlmyM -- .
And how many lines have just one correspondence?
We look at the tail of rankedParallels
.
pairs = [(x, list(paras)[0]) for (x, paras) in rankedParallels if len(paras) == 1]
print(f"There are {len(pairs)} exclusively parallel pairs of lines")
There are 7426 exclusively parallel pairs of lines
for (x, y) in pairs[0:10]:
A.dm("---\n")
print(f"similarity {similarity[(x,y)]}")
A.plain(x, fmt="layout-orig-full")
A.plain(y, fmt="layout-orig-full")
similarity 69
similarity 85
similarity 83
similarity 67
similarity 83
similarity 73
similarity 62
similarity 64
similarity 79
similarity 79
Why not make an overview of exactly how wide-spread parallel lines are?
We count how many lines have how many parallels.
parallelCount = collections.Counter()
buckets = (2, 10, 20, 50, 100)
bucketRep = {}
prevBucket = None
for bucket in buckets:
if prevBucket is None:
bucketRep[bucket] = f" n <= {bucket:>3}"
elif bucket == buckets[-1]:
bucketRep[bucket] = f" n > {bucket:>3}"
else:
bucketRep[bucket] = f"{prevBucket:>3} < n <= {bucket:>3}"
prevBucket = bucket
for (ln, paras) in rankedParallels:
clusterSize = len(paras) + 1
if clusterSize > buckets[-1]:
theBucket = buckets[-1]
else:
for bucket in buckets:
if clusterSize <= bucket:
theBucket = bucket
break
parallelCount[theBucket] += 1
for (bucket, amount) in sorted(
parallelCount.items(),
key=lambda x: (-x[0], x[1]),
):
print(f"{amount:>4} lines have {bucketRep[bucket]} sisters")
445 lines have n > 100 sisters 720 lines have 20 < n <= 50 sisters 1047 lines have 10 < n <= 20 sisters 6476 lines have 2 < n <= 10 sisters 7426 lines have n <= 2 sisters
Before we try to find them, let's see if we can cluster the similar lines in similar clusters.
From now on we forget about the level of similarity, and focus on whether two lines are just "similar", meaning that they have a high degree of similarity.
SIMILARITY_THRESHOLD = 0.8
CLUSTER_THRESHOLD = 0.4
def makeClusters():
# determine the domain
domain = set()
for ln in allLines:
ms = E.sim.f(ln)
for (m, s) in ms:
if s > SIMILARITY_THRESHOLD:
domain.add(s)
added = True
if added:
domain.add(m)
A.indent(reset=True)
chunkSize = 1000
b = 0
j = 0
clusters = []
for ln in domain:
j += 1
b += 1
if b == chunkSize:
b = 0
A.info(f"{j:>5} lines and {len(clusters):>5} clusters")
lSisters = {x[0] for x in E.sim.b(ln) if x[1] > SIMILARITY_THRESHOLD}
lAdded = False
for cl in clusters:
if len(cl & lSisters) > CLUSTER_THRESHOLD * len(cl):
cl.add(ln)
lAdded = True
break
if not lAdded:
clusters.append({ln})
A.info(f"{j:>5} lines and {len(clusters)} clusters")
return clusters
clusters = makeClusters()
0.08s 1000 lines and 811 clusters 0.27s 2000 lines and 1540 clusters 0.61s 3000 lines and 2432 clusters 1.09s 4000 lines and 3298 clusters 1.72s 5000 lines and 4114 clusters 2.23s 5736 lines and 4688 clusters
What is the distribution of the clusters, in terms of how many similar lines they contain? We count them.
clusterSizes = collections.Counter()
for cl in clusters:
clusterSizes[len(cl)] += 1
for (size, amount) in sorted(
clusterSizes.items(),
key=lambda x: (-x[0], x[1]),
):
print(f"clusters of size {size:>4}: {amount:>5}")
clusters of size 30: 1 clusters of size 21: 1 clusters of size 15: 1 clusters of size 14: 1 clusters of size 10: 2 clusters of size 8: 1 clusters of size 7: 3 clusters of size 6: 6 clusters of size 5: 12 clusters of size 4: 26 clusters of size 3: 183 clusters of size 2: 407 clusters of size 1: 4044
Exercise: investigate some interesting groups, that lie in some sweet spots.
All chapters:
See the cookbook for recipes for small, concrete tasks.
CC-BY Dirk Roorda