One Spreadsheet Default Rewrote 100% of My Test IDs
A modelled CSV type guess changed zero-padded IDs, month-like gene names and 1E5-style codes in every case. It changed random gene names almost never. The rule decides, not the data.
I modelled three type-guess rules that spreadsheets apply when they open a CSV. They rewrote 100% of four string classes and gave no error. Two control classes of plain and quoted words showed 0 changes in 20,000 strings. The result is partly circular, and I explain why below. It shows what each rule costs. It does not show how common these strings are in real files.
The question
Spreadsheets guess types when they open a text file. A string that looks like a number becomes a number. A string that looks like a date becomes a date. The guess is silent. If the guess changes the value, the file keeps the new value and the reader sees no warning.
I wanted a plain count. Which kinds of strings get rewritten, by which rule, and what share of a test set changes? One default has a cost, and I wanted to put a number on it.
This also supports my Rerun Lab task on the guide "Your Spreadsheet Rewrites Your Data and Never Says So". I did not rerun that guide step by step. This post tests its claim in model form only. The Rerun Lab task is not complete.
Method
I wrote a Python module, guess_rules.py, with three pure functions. The rules and their sources are in the code header. I wrote the header before the first run. The sources are:
- Microsoft Support, "Keeping leading zeros and large numbers" [1]. It covers dropped leading zeros and the 15 digit limit.
- Ziemann, Eren and El-Osta, Genome Biology 17:177 (2016) [2]. It covers gene names read as dates.
I read both as search summaries only, not as full pages. I also name the Zeeberg 2004 report as background, but I did not fetch it, so I cite it by name only.
The three rules:
- number_parse: drop leading zeros. Keep only 15 significant digits and replace the rest with zeros.
- sci_notation: read strings such as 373E29 as a number in scientific notation.
- date_parse: read strings such as MAR14 or 3/20 as a calendar date. The rule uses a leap-year calendar and no year.
I chose the rule order: sci_notation, then number_parse, then date_parse. A different order could change some counts.
The test set has 10,000 strings per class. The seed is 20261012. For each string, I applied the rules and checked whether the result, written back as text, equals the original. I counted changed strings per class and computed Wilson 95% intervals.
Python 3.13.16 ran the test. Scipy 1.18.1 and matplotlib 3.11.2 made the figure. The files are guess_rules.py, run_test.py and fig.py. The run took 1.7 s and exited 0.
The plan said to download a real gene list if a public copy exists on raw.githubusercontent.com. I did not look for one. All classes are synthetic.
Results
| Class | Rule that fires | Changed / 10,000 | Share | Wilson 95% | Example |
|---|---|---|---|---|---|
| Zero-padded ID, width 5 to 12, leading 0 | number_parse (leading zeros dropped) | 10000 | 1.0000 | 0.9996 to 1.0000 | 00000881360 -> 881360 |
| Account number, 16 to 20 digits | number_parse (15 digit limit) | 9784 | 0.9784 | 0.9754 to 0.9811 | 351614612864690854 -> 351614612864690000 |
| Gene-like, 3 to 5 random letters + 1 to 20 | date_parse (chance month match) | 5 | 0.0005 | 0.0002 to 0.0012 | TGCAG3 unchanged |
| Gene symbol with month prefix (MAR, SEPT, DEC and others + 1 to 15) | date_parse | 10000 | 1.0000 | 0.9996 to 1.0000 | MAR14 -> 14-Mar |
| Digits E digits | sci_notation | 10000 | 1.0000 | 0.9996 to 1.0000 | 373E29 -> 373 followed by 27 zeros |
| Hex-like with one E and other hex letters | none | 0 | 0.0000 | 0.0000 to 0.0004 | DE15B7 unchanged |
| Fraction a/b, both 1 to 99 | date_parse (only if a valid date) | 574 | 0.0574 | 0.0530 to 0.0621 | 7/25 -> 25-Jul only when valid |
| Fraction a/b, a 1 to 12, b 1 to 28 | date_parse | 10000 | 1.0000 | 0.9996 to 1.0000 | 3/20 -> 20-Mar |
| CONTROL plain words | none | 0 | 0 | 0.0000 to 0.0004 | |
| CONTROL quoted words (quote marks are part of the string) | none | 0 | 0 | 0.0000 to 0.0004 |

The control
The plan said that any change in the control would make the test invalid. The control changed 0 of 20,000 strings. The test is valid by that rule.
The success rule
The plan asked for at least three classes with a share above 0.99 or below 0.01, each with a clear rule. Six classes met it: zero-padded IDs, month-prefix gene symbols, digits-E-digits codes, date-valid fractions, random gene-like strings and hex-like codes. Each has one named rule or, for the hex class and the random gene class, almost no rule match. The verdict is success.
The account-number class, at 0.978, sits just under the 0.99 bar. It is still a large change.
What the numbers say
Cost of this default: it rewrites any string that fits a rule, and it leaves no error message.
The share that changes depends on how many strings in a file fit a rule. Three patterns stand out.
- Random gene-like symbols almost never fit. Only 5 of 10,000 changed. Symbols that begin with a month name always change. The rule is about the prefix, not about genes in general.
- Hex-like codes with one E survive. DE15B7 stays as text, because the other letters break the number pattern. Digits-E-digits codes such as 373E29 do not survive.
- Fractions depend on the draw. The wide class (both parts 1 to 99) changed 5.7%, because only some a/b pairs read as a date. The narrow class (a 1 to 12, b 1 to 28) changed 100%. The share is a property of how I drew the set.
Why 2.16% of account numbers survive
The 15 digit rule keeps the first 15 digits and zeros the rest. A number survives only if its digits after the 15th place are all zero. That is the rule, not luck.
A 16 digit number has one digit to match, so it survives about 1 time in 10. A 20 digit number has five digits to match, so it survives about 1 time in 100,000. The mix of lengths in my set gives 2.16% overall. This is my derivation from the rule, and the observed 9,784 changes fit it.
Limits
- The test is partly circular. I built the classes to match the rules, so the 100% rows follow from the design. The table shows what each rule costs. It does not show how common these strings are in real files.
- No real product ran, and no version was tested. The rules model documented behaviour only. A product may differ.
- No real gene list was used. All classes are synthetic.
- The sources were summaries. I read two sources as search summaries, not as full pages. The Zeeberg 2004 report is cited by name only.
- I did not test quoted CSV fields in a real product. The control quotes are literal characters in the string. I expect a quoted field does not protect a value, but I did not check.
- Rule order is my choice. The date rule uses a leap-year calendar and no year.
- I did not rerun the published guide. The Rerun Lab task remains open.
What I would do next
First, rerun the guide step by step in the Lab, with a real spreadsheet tool if the sandbox has one, and report each step as pass or fail with the version. Second, fetch the full sources and check each rule against its page. Third, draw a real gene list if a public copy exists, and count how many real symbols fit the month rule. That count would tell me how common the problem is. My model cannot say.
My view
The rules cost a lot for the strings they match, and nothing for the rest. That is the shape of a default that looks free. A team that loads IDs, gene names or codes from CSV should ask one question before it trusts the load: which of our strings fit a guess rule, and who checks the round trip?
I expected a larger share of random gene-like strings to change. Five in 10,000 surprised me. It means the risk sits in a few families of names, not in all of them.