Spaces:
Sleeping
Sleeping
File size: 17,581 Bytes
0e7a159 9729dcb 0e7a159 3b5acef 0e7a159 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | # Reverse Etymology Atlas
## Purpose
Ordinary etymology asks "where did this word come from?" Reverse etymology asks the opposite: **given a base word, what was derived from it?**
This tool turns the [lukeslp/etymology-atlas](https://huggingface.co/datasets/lukeslp/etymology-atlas) graph (4.17 million relationships across 19,401 languages) into an interactive, mobile-capable explorer. A user picks an etymon such as Latin *mater* or Proto-Indo-European *meh2ter* and immediately sees the descendant cloud: inherited daughters, later borrowings, compounds, clippings, and (optionally) cognates.
The product is a local web application: a FastAPI service over an in-memory compressed graph, plus a canvas-rendered frontend. It is designed to feel like a living atlas, not a spreadsheet with circles.
## Data sources
The Hugging Face dataset is the system of record. Five parquet tables are ingested, then enriched with two CLDF catalogs so that empty fields in the published atlas (notably `lang*_family` and most ISO codes) do not cripple filtering.
| Source | Role |
| --- | --- |
| `etymologies.parquet` (4,173,462 rows) | Directed word graph |
| `languages.parquet` (19,401 rows) | Glottolog skeleton: coordinates, macroarea, vitality, phoneme counts |
| `cognate_sets.parquet` (4,981 IE-CoR sets) | Expert cognate groups, shown on node detail |
| `phonemes.parquet` (105,484 rows) | PHOIBLE inventories by glottocode |
| `linguistic_features.parquet` (76,475 rows) | WALS typological values |
| Glottolog CLDF `languages.csv` | Family names, ISO 639-3, coordinates, language vs family vs dialect |
| WALS CLDF `languages.csv` + `codes.csv` | Join WALS codes to glottocodes and human-readable feature values |
License of the atlas and Wiktionary-derived etymology-db content is CC BY-SA 3.0. The UI cites Steuber 2026 and the upstream sources.
## Graph semantics
### Edge direction
Each etymology row is:
```
(term1, lang1) relationship_type, confidence, source (term2, lang2)
```
Empirical inspection of the atlas matches Wiktionary / etymology-db convention:
- **term1** is the derived or attested form.
- **term2** is the etymon, stem, or related form.
Examples:
- English *portmanteau* `borrowed` Middle French *portemanteau*
- English *mother* `inherited` Middle English *moder*, Old English *modor*, Proto-Germanic *moder*, PIE *meh2ter*
**Reverse etymology** therefore walks edges **from term2 toward term1**: "everything that lists this node as a source."
Forward etymology (etymons of the current node) is still stored and shown in the inspector, but it is not the primary layout.
### Node identity
A node is the pair `(term, lang)` after trimming whitespace and dropping empty terms (a few dozen nulls exist in the parquet file). `lang` is the atlas language key: a Wiktionary language name (`latin`, `proto-indo-european`) for etymology-db rows, or an ISO 639-3 code (`ben`, `hin`) for Lexibank IE-CoR rows.
Display names prefer, in order: atlas `lang*_name` when non-empty, Glottolog `Name`, then title-cased `lang`.
### Relation taxonomy
The atlas already normalizes Wiktionary's ~31 templates into 11 types:
| Type | Count | Default in reverse walk | Notes |
| --- | --- | --- | --- |
| `inherited` | 327,261 | on | Unbroken descent. Highest-signal descendants. |
| `derived` | 325,776 | on | Derivation that is not specifically inherited/borrowed. |
| `borrowed` | 231,814 | on | Loanwords. |
| `calque` | 10,318 | on | Loan translations. |
| `clipping` | 2,482 | on | Spoken shortening. |
| `back_formation` | 2,173 | on | Affix removal. |
| `abbreviation` | 404 | on | Written shortening. |
| `compound` | 304,470 | on | `term1` contains `term2` as a part. Useful for affix/root exploration; noisier for strict lexical descent — uncheck if a root gets bushy. |
| `blend` | 15,796 | on | Portmanteau parts. |
| `other` | 2,162,894 | on | Catch-all (prefixes, mentions, mixed Wiktionary templates). |
| `cognate` | 790,074 | on | Lateral, not descent. Includes Lexibank pairwise cognates; can expand into cliques — uncheck for stricter trees. |
Global edge filters (allowed types + minimum confidence) apply **before** traversal. They define which graph is being searched, not which nodes survive afterwards.
Confidence in the atlas is discrete: 0.70 (`other`), 0.75, 0.85, 0.90, 0.92 (`inherited`), 0.95 (`derived`), 1.00 (Lexibank). Default floor is 0.70 (everything). A slider exposes the steps.
### Cycles, multi-parents, and depth
The graph is not a tree. A word can list several etymons; walking reverse edges can revisit nodes through different parents. Traversal is a bounded BFS:
1. Start at the selected root.
2. Follow reverse edges whose type/confidence pass the global edge filters.
3. Ignore edges back to an already-seen node (cycle cut).
4. Record the **BFS parent** so the visible structure is a tree even when the underlying graph is a DAG with extra links.
5. Stop at `max_depth` (default 3, max 8) or `max_visit` (50,000 nodes).
Extra parents that were not chosen as the BFS parent are attached as `also_from` on the node payload so the inspector can show them without breaking layout.
## Filtering model (the core requirement)
Filters are **not** a single global predicate applied to every visible node. That would be wrong for reverse etymology. A user who asks "English words that came from Latin *caput* by way of French" wants:
- **Leaves** (terminal descendants, or nodes at max depth treated as terminals for this query) to be English.
- **Some intermediate hop** on the unique BFS path to be French.
- The Latin root itself to remain visible even though it is not English.
Three independent layers are applied in order.
### Layer A — graph construction (edges)
Applies to every hop, including those that will later be pruned.
- Allowed `relationship_type` set.
- `min_confidence`.
- Optional `max_depth`.
### Layer B — leaf predicates
When **no** leaf constraints are set, candidate leaves are structural sinks under the construction graph (or nodes at `max_depth`). When leaf constraints **are** set, every visited non-root node that matches those constraints is a candidate — including productive words that still have further descendants (Spanish *verde* under Latin *viridis* must survive a Spanish leaf filter even though Tagalog *berde* was borrowed from it). A candidate is kept only if it matches **all** specified leaf constraints:
- Term contains / regex (case-insensitive, Unicode-aware).
- Language key or display-name set (multi-select).
- Language family (Glottolog `Family_ID` resolved to a name such as Indo-European).
- Macroarea (Africa, Eurasia, Papunesia, Australia, North America, South America).
- Vitality / Glottolog endangerment when known.
- Geographic: has coordinates; optional lat/lon bounding box.
- WALS: one or more `(feature_id, allowed_values)` clauses, AND-combined. Example: 81A in `{SOV, SVO}`.
- Phonology: inventory **contains** these IPA segments; inventory **lacks** these segments; has tone; segment class present (click, implosive approximated via IPA).
- Source dataset: etymology-db, lexibank, or both (inherited from incident edges).
- Minimum descendant fan-out of the leaf itself (rarely used; more often a search ranking feature).
Unknown metadata: by default a positive constraint **fails closed** (a language with no WALS 81A value does not count as SOV). A per-layer toggle `keep_unknown` lets sparse typological data through.
### Layer C — path / intermediate predicates
Let `path(leaf) = [root, hop1, hop2, ..., leaf]`.
**Intermediates** are `path[1:-1]` (root and leaf excluded). If the path has length 1 (the root is also a leaf), intermediate filters do not apply unless the user opts into `apply_path_to_root`.
Each intermediate constraint set is evaluated with an explicit **quantifier**:
| Quantifier | Meaning |
| --- | --- |
| `any` (default) | At least one intermediate matches. "Passed through French." |
| `all` | Every intermediate matches. "Never left Romance." |
| `none` | No intermediate matches. "Did not pass through English." |
| `exactly` | Exactly `k` intermediates match. |
Path constraints may include the same trait vocabulary as leaves (language, family, macroarea, WALS, phonemes, term pattern) plus two edge-aware clauses evaluated on the path's edges rather than nodes:
- `path_relations_any`: at least one hop has this relation (e.g. a borrowing occurred somewhere).
- `path_relations_none`: no hop has this relation (e.g. no compounds).
### Pruning
After BFS:
1. Evaluate every candidate leaf against Layer B.
2. Evaluate `path(leaf)` against Layer C.
3. Keep a leaf only if both pass.
4. Rebuild the visible tree as the union of surviving root-to-leaf paths.
5. Drop nodes that are not on any surviving path (except the root, always kept).
6. Cluster remaining high-degree sibling lists for display (see Layout).
This is the only definition of "filter correctly ... for the leaf words and intermediate nodes along the path." Applying leaf language = English to every node would hide the French and Latin ancestors the user needs in order to understand the path.
### Worked example
Root: Latin *caput*.
- Layer A: relations `{inherited, borrowed, derived}`, confidence >= 0.9, depth 4.
- Layer B: leaf language = English.
- Layer C: `any` intermediate language = French.
Kept: English *chief*, *chef*, *capital* when their BFS paths include a French (or Middle French) hop. Dropped: English *head* if it arrived only through Germanic, even though it is English; dropped French *chef* because it is not an English leaf.
## Search and ranking
Autocomplete queries SQLite FTS5 on `term` with a Unicode61 tokenizer, plus a normalized shadow column (`nfc`, lowercased, leading `*` stripped, `h2`/`h₂` folded) so PIE reconstructions remain findable.
Ranking for prefix `q`:
1. Exact term match.
2. Prefix match.
3. Substring / FTS rank.
4. Tie-break: `child_count` descending (productive etymons float up), then language name.
Each hit returns `{term, lang, display_lang, family, child_count, sample_children}`. Homographs are separate rows (Latin *mater* vs English *mater*).
A curated example list is shipped for first-run exploration: PIE *meh2ter*, Latin *mater*, Latin *caput*, Latin *habere*, English *computer*, Arabic *kitab*, Sanskrit *cakra*, etc.
## Visualization
### Views
1. **Descent tree** (default). Left-to-right tidy tree: etymon on the left, descendants growing right. This matches the reverse-etymology metaphor.
2. **Radial**. Root at center, depth as radius. Better on tablets for bushy trees.
3. **Map**. Surviving leaves (and optionally intermediates) plotted by language coordinates. Marker size = number of surviving words in that language; color = family. Clicking a marker lists those words and can isolate the language in the tree.
4. **Table**. Virtualized, sortable: term, language, family, relation from parent, confidence, depth, full path as a breadcrumb. CSV export.
5. **Stats**. Relation mix, family mix, depth histogram, borrowing vs inheritance ratio, macroarea breakdown for the current filtered tree.
### Canvas renderer
SVG is too slow above a few hundred labeled nodes. The tree and radial views use a single high-DPI canvas:
- Layout via `d3-hierarchy` (`tree` / `cluster`) computed on a web worker when the node count exceeds 800.
- Camera: pan, wheel zoom, pinch zoom, double-tap zoom, keyboard arrows.
- Hit testing: uniform grid hashed by screen cell, rebuilt on camera change.
- Level of detail: labels fade in by zoom and by node importance (`child_count`, depth, selection).
- Minimap in the corner on desktop; hidden on narrow screens unless toggled.
- Selection: click a node to open the inspector; hover previews the BFS path back to the root in a brighter stroke.
- Re-root: "descend from here" makes the node the new query without leaving the UI.
### Clustering (speed and legibility)
Unfiltered reverse walks of productive affixes are huge (`ly` English has 13,422 direct children). After filtering, if a node still has more than `cluster_threshold` (default 28) children:
1. Group by language.
2. If a single language still exceeds the threshold, group by initial letter.
3. Emit a cluster stub `{kind: "cluster", label, count, relation_mix}` that expands on click.
The server never ships more than `max_payload_nodes` (2,500) in one response. Expansion requests pass `expand` keys so only the requested cluster is unpacked, still subject to the same filters.
### Color and encoding
User-selectable color mode:
- Relation from parent (default): inherited teal, borrowed copper, derived moss, calque violet, clipping/abbreviation slate, compound/blend ochre, other ink-wash gray, cognate muted magenta.
- Language family (hashed qualitative palette, stable per family name).
- Confidence (sequential).
- Depth (sequential).
- Macroarea.
Stroke width encodes `child_count` on the visible subtree. A legend is always present.
### Inspector
Slide-over on desktop, bottom sheet on mobile:
- Term, language, family, coordinates, vitality.
- Incident reverse children (counts by relation).
- Forward etymons ("this word comes from...").
- IE-CoR cognate sets containing the form, when any.
- WALS highlights (word order, case, tone).
- Phoneme inventory summary (counts by class, tone yes/no, searchable list).
- Wiktionary deep link for etymology-db nodes.
- Actions: re-root, isolate this language, copy path, exclude this language from leaves / from path.
## Mobile
"Viewable on mobile" is a first-class layout, not a shrunk desktop.
- Responsive shell: below 840px the filter panel and inspector become bottom sheets; the canvas is full-bleed under a compact chrome (search + view switcher + filter chip count).
- Touch: one-finger pan, pinch zoom, tap select, two-finger rotate disabled (it fights tree reading).
- Filter chips summarize active leaf vs path constraints so a phone user can see the query without opening the sheet.
- PWA manifest + service worker cache the app shell (not the 4M-edge index). Add-to-home-screen works when served over HTTPS or localhost.
- The API binds `0.0.0.0` so a phone on the same network can open `http://<lan-ip>:8000`. A "Open on phone" dialog shows that origin as a QR code.
- Shareable URLs encode the full query (root, layers A/B/C, view, color mode). Opening the URL on a phone restores the same tree.
## Architecture
```
parquet + CLDF CSVs
|
v
pipeline/build_index.py
|
+--> data/index/atlas.sqlite (nodes, FTS, languages, WALS, phonemes, cognates)
+--> data/index/graph.npz (CSR: offsets, targets, rel_code, conf_code, src_code)
+--> data/index/meta.json (vocabularies, color maps, examples)
|
v
FastAPI --loads CSR into RAM once-- React canvas SPA
```
Why this split:
- SQLite FTS is the right tool for autocomplete and inspector joins.
- Pure SQL recursive CTEs cannot apply Layer C quantifiers quickly enough on 13k-way affixes. An in-memory CSR BFS in Python over int32 arrays does this in tens of milliseconds.
- The SPA never downloads the full graph. It only receives the pruned, clustered payload for the current query.
Estimated RAM after load: well under 500 MB (CSR ~30 MB, interned trait arrays ~20 MB, SQLite mmap, Python overhead).
## API
All JSON. CORS open. No auth.
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/api/health` | Index stats, load time |
| GET | `/api/suggest?q=&limit=` | Autocomplete |
| GET | `/api/meta` | Vocabularies for filter widgets |
| POST | `/api/tree` | Filtered descendant tree |
| GET | `/api/node?term=&lang=` | Inspector payload |
| GET | `/api/cognates?term=&lang=` | IE-CoR sets |
| POST | `/api/export` | Same tree as CSV or JSON download |
| GET | `/api/examples` | Curated roots |
`POST /api/tree` body is the serialized filter document (see Implementation plan). Response:
```json
{
"root": {"id": 0, "term": "mater", "lang": "latin", "...": ""},
"nodes": [{"id": 1, "parent": 0, "kind": "word"|"cluster", "...": ""}],
"stats": {"visited": 1800, "kept_leaves": 42, "elapsed_ms": 18},
"truncated": false
}
```
## Performance budget
| Action | Target |
| --- | --- |
| Process start including mmap load | < 3 s on a laptop |
| Autocomplete | < 30 ms |
| Typical tree (depth 3, < 2k visit) | < 80 ms |
| Worst productive affix, filtered | < 400 ms |
| Canvas pan/zoom | 60 fps at 2,500 nodes |
| Mobile first interaction | usable within one round trip |
Techniques: CSR int32, BFS with typed arrays, cluster stubs, canvas LOD, SQLite covering indexes, gzip JSON, immutable filter objects so React does not relayout on every keystroke (search input is debounced 120 ms).
## Accessibility and i18n
- Canvas is supplemented by the table view (keyboard reachable, screen-reader friendly).
- Focus states on all controls; `/` focuses search; `f` toggles filters; `1-4` switch views.
- Color is never the only encoding (relation also appears as a short label and in the inspector).
- Terms are rendered as stored (IPA, Greek, reconstructions). UI chrome is English.
## Non-goals (this version)
- Editing or crowdsourcing etymologies.
- Full phylogenetic tree inference.
- Offline copies of Wiktionary prose.
- Account/sync in the cloud.
## Visual language
The UI should read as a night atlas: deep ink navy, warm parchment text, copper and sea-teal relation colors, serif display for headwords, sans for chrome. No generic dashboard cards, no rainbow force-directed hairball as the default.
|