The problem in brief
You get 6 606 Peruvian court rulings in which every sensitive name was replaced by the token xxxxxxxxxx (23 131 replacements, 2 to 5 per document). You also get a "leak": for every document, the embedding of the original text (with the real names) and the embedding of the redacted text. A document embedding is the average of the unit-normalised Spanish fastText vectors of its words. Finally you get 800 candidate vectors: 300 are noisy copies of the true name vectors, 500 are decoys, all shuffled. For every xxxxxxxxxx you must output the hidden name as a word.
There are no labels at all, neither on Kaggle's side for us (test answers are private) nor for training. We never submitted anything. All scores below are self-consistency measurements, explained in the Results section.
How you are scored
where bag is the fraction of names recovered per document regardless of order (multiset intersection), and positional is the fraction of slots with exactly the right name.
What to aim for. The only public-leaderboard entry is the organisers' baseline, 0.211.
First look at the data
Two facts from the statement are the whole key. The embedding of a document with words is
with the unit word vectors and the vector of xxxxxxxxxx. Everything that is not a name cancels in the difference, so
Every document gives us the exact sum of its hidden name vectors. The task becomes: write 6 606 known vectors as sums of unknown "atoms" from a common dictionary of 300 names.
The candidates make this harder than it looks. Name vectors in fastText are strongly correlated (surnames live in one region of the space: the median cosine between two candidates is 0.30, the top 1 % above 0.73), and the candidates are noisy: once we recovered the true vectors, their cosine with the given candidates was only about 0.8.
A first baseline
Decode each with the 800 given candidates: greedily add the candidate that most reduces , times, then repeatedly try swapping each chosen atom for a better one. With the raw candidates the median residual is 1.09: no document is explained exactly, because the noise on each candidate adds up.
The key idea
Use the 6 606 documents to clean the dictionary. This is dictionary learning by alternating minimisation:
- Decode every document with the current atoms (greedy + swaps).
- Re-estimate every atom by least squares. Let be how many times atom was chosen in document ; then , and pools the evidence of every document that uses a name. Since each is an exact sum, a name used in a hundred documents is pinned down very precisely.
- Renormalise each atom to unit length (true word vectors have norm 1) and drop atoms that no document uses. Decoys are rarely the best choice once the true names sharpen, so they die out.

After four rounds, 86 % of the documents are explained exactly (residual below 0.05, against a typical norm of 2 to 4 for ); the process stabilises at 90 % of the documents, 88.5 % of the names. The right panel shows the strongest confirmation that the atoms are right: we streamed the 1.5 million alphabetic words of the fastText Spanish vocabulary and computed each vector's closest word. The given candidates match no word well (median cosine 0.77), while the re-estimated atoms match a word almost perfectly (median cosine 0.9998): we have recovered the exact vectors of names such as castro, chavez, trujillo, vargas.
Some atoms stay far from every vocabulary word (cosine 0.5 to 0.75). One of them is used 429 times. Its closest words are sequeira and jsequeiros: it is very likely the surname Sequeiros, whose lowercase form is not in the .vec vocabulary. fastText builds such vectors from character n-grams, which needs the full .bin model (7 GB) that we did not download. These names get the nearest vocabulary word, which is probably wrong.
The order is a second, separate problem. An average is blind to word order, so the embeddings say nothing about which name sits in which slot. Choosing a random order would give about 36 % positional agreement on our decoded bags. The text helps: legal documents repeat formulas ("el señor juez xxxxxxxxxx Saldarriaga", "Barrios xxxxxxxxxx") and the same people appear in many rulings. We fit a naive-Bayes context model on the previous and next one and two words, by hard EM: start with every bag name at every slot, fit the counts, give each document the permutation of its bag with the highest likelihood, refit, repeat. It converges in three rounds.
The full solution
- Compute for every document (float64).
- Start from all 800 candidates; 25 rounds of decode, least-squares refit, unit renormalisation and pruning; a final decoding with 10 swap sweeps. 435 atoms survive, some of them near-duplicates of the same word (four atoms map to castro), which is harmless because we merge them by name.
- Name each atom by its closest fastText word (lowercased, accents removed, as the scorer does).
- Order the names inside each document with the EM context model.
A, res = decode(S, k, V) # which atoms, for every document
V = refit(S, A, len(V))[use >= 2] # least squares + unit norm + drop unused atoms
Results
No ground truth is available, so we measure what we can.
| quantity | value |
|---|---|
| organisers' baseline (public leaderboard) | 0.211 |
| documents explained exactly with the raw candidates | 0 % |
| documents explained exactly after dictionary learning | 90.0 % |
| names in exactly explained documents | 88.5 % |
| of which the atom matches a vocabulary word (cosine > 0.99) | 79.8 % of all names |
| order: agreement with unredacted text evidence, EM context model | 79.9 % (11 480 slots) |
| order: same check for a random order | 33.7 % |
The bag score should lie between 0.80 (names that are both exactly reconstructed and exactly named) and about 0.88 (all names in exactly explained documents, if most out-of-vocabulary names still happen to be right). For the order, we used an independent check: in about half of the slots, the document itself contains the same neighbour word next to one of the bag names in clear text (for example "Prado Saldarriaga" written out elsewhere in the ruling). On those slots our EM order agrees with that evidence 80 % of the time, against 34 % for a random order. These slots are easier than average, so the true positional accuracy is probably lower. Our rough estimate of the final score is therefore around , nearly four times the baseline; this is an estimate, not a measured score.
What to remember
- Write down the formula of the embedding. The averaging made everything except the names cancel exactly. Algebra on the representation beat any learned model.
- An exact signal plus many observations means you can learn the dictionary. Least squares over thousands of documents turns noisy candidates into exact vectors; this is the alternating idea behind sparse coding, k-means and matrix factorisation.
- Use physical constraints. Unit norm for word vectors and integer counts for names kept the alternation stable.
- Check your answers against an independent source. A near-perfect match with fastText vocabulary words, and agreement with unredacted text, are strong evidence even without labels.
- Averaged embeddings leak. Removing a name from a document does not remove it from an embedding computed before redaction: a privacy lesson as much as an ML one.
Note on the code. The full
solution.pybelow reproduces the setup that produced the reported score, but it was not re-run end to end after it was assembled.