DS-0005
Factory-reset classifier set
A teaching set for supervised learning. Each row is an invented Android phone described by 22 features of the kind an examiner reads, and the question is yes or no: was it factory reset in the last 30 days? There are train and test files, a hand-written baseline, an evaluation script and a teaching guide. Every row is synthetic.
At a glance
What the set is, how it was made, and what it is for.
Plain CSV, so a spreadsheet, a SQLite browser or a text editor opens it. Try the challenge: Train and doubt a reset classifier (Practitioner).
- Task
- Binary classification:
reset_30dis 1 if the phone's most recent factory reset was 30 days or less before acquisition - Device class
- None (generated table of Android-style artifact features)
- OS
- Not applicable
- Evidence type
- Synthetic table
- Made on
- A script (generate.py, Python standard library) with seed 20261106. No device, no case data and no real identifiers. The same seed gives the same bytes.
- Contents
- 2,000 rows in the main set: 1,400 to train on and 600 to test on, with 600 positives (30 percent). A variant with rare positives has 2,000 more rows and 100 positives (5 percent).
- Built-in traps
- A leakage column, a class-imbalance variant, clock-tampered rows, look-alike phones and blanks that mean different things
- Size
- 179 KB as a zip (182,901 bytes), 17 files
- Version
- 1.0
- Licence
- Data: CC BY 4.0. Code: MIT.
- Maintainer
- Daniel Addai, SectraTEK Lab
What is inside
One zip. The test labels are in a separate file so you can predict blind and score afterwards.
factory-reset-classifier/
README.txt
LICENCE.txt
DATA_DICTIONARY.md
data_dictionary.csv
TEACHING_GUIDE.md
train.csv
test.csv
test_answers.csv
train_reference.csv
variants/imbalanced/
train.csv
test.csv
test_answers.csv
train_reference.csv
code/baseline_stdlib.py
code/baseline_sklearn.py
code/evaluate.py
generator/generate.pytrain.csvandtest.csv: 1,400 and 600 rows,device_idplus the 22 features; train.csv also has the labelreset_30d.test_answers.csv: the test labels, kept apart, with the profile, true reset age and clock columns for slicing.train_reference.csvhas the same extra columns for the train rows.variants/imbalanced/: the same four files with about 5 percent positives.DATA_DICTIONARY.md,data_dictionary.csv,TEACHING_GUIDE.md,README.txt,LICENCE.txt: documentation and licences.
The baseline in code/baseline_stdlib.py is a logistic regression and a decision stump written by hand with the Python standard library, so nothing needs installing. code/baseline_sklearn.py is optional and needs scikit-learn; it was checked for syntax only. code/evaluate.py prints accuracy, precision, recall, F1, balanced accuracy and the confusion matrix, for all rows or sliced by a column of the answer file.
The 22 features
Ages are days before the acquisition time, as the phone's own clock reports them. A blank cell means the artifact was not there or could not be read. The full dictionary is in DATA_DICTIONARY.md.
| Column | Kind | Blank when | What it is |
|---|---|---|---|
| recovery_file_present | flag 0/1 | never | A recovery history file was found on the extraction. Absent on about one phone in four, as on a whole device generation in LAB-TR-0004. |
| recovery_entries_total | count, entries | blank when recovery_file_present is 0 | Entries in the recovery history, of any kind (wipes, updates, region changes). |
| recovery_wipe_entries | count, entries | blank when recovery_file_present is 0 | Entries in the recovery history that are wipes (a --wipe_data command). Counts reach back before the 30-day window, so a high count is not a recent reset. |
| last_recovery_wipe_age_days | number, days before acquisition | blank when there is no file or no wipe entry | Age of the most recent wipe entry, from the recovery clock as printed. On about one wipe-bearing phone in eight the recovery clock prints a year near 1970, so the age reads about 20,500 days. See recovery_clock_implausible. |
| last_recovery_entry_age_days | number, days before acquisition | blank when there is no file or no entry | Age of the most recent entry of any kind, from the recovery clock as printed. Same implausible-clock problem as the wipe age. |
| recovery_clock_implausible | flag 0/1 | blank when recovery_file_present is 0 | 1 when the recovery clock printed a year before 2010 on the wipe-bearing history. |
| setup_complete_age_days | number, days before acquisition | about 4 percent blank | Age of the setup-wizard-completed flag, from its file time. A reset sets it again; so can a system update. Overlap: updated phones and new phones also have a recent value. |
| setup_flag_changes_90d | count, changes | never | Changes to the setup flags in the 90 days before acquisition. Setup flags cycle around updates as well as resets (LAB-TR-0004). |
| boot_count_since_setup | count, boots | never | Boots counted from the setup flag time to acquisition. Heavy right tail: try a log scale. |
| boot_reason_history_rows | count, rows | never | Rows in the boot-reason history. Written to carry no signal about resets; the real history never held a factory-reset entry in the Lab's study. A noise column on purpose: does your model give it weight? |
| user_app_count | count, apps | never | User-installed apps present at acquisition. |
| earliest_app_install_age_days | number, days before acquisition | never | Age of the oldest first-install time among user apps. A restore from backup or an app kept across the reset can push this back. A new phone also has a small value. |
| app_install_spread_days | number, days | never | Newest first-install time minus oldest, among user apps. Small on a restored phone, on a new phone and on a phone reset a few days ago. |
| apps_installed_30d_fraction | number, fraction 0 to 1 | never | Share of user apps whose first-install time is within 30 days of acquisition. |
| account_added_age_days | number, days before acquisition | about 3 percent blank | Age of the primary account being added, from the account database. People re-add accounts, so an old phone can show a recent value. |
| account_count | count, accounts | never | Accounts on the phone. |
| battery_stats_age_days | number, days before acquisition | never | Age of the start of the battery statistics. Weak: these statistics restart on charging and reboots, not only on resets. |
| storage_used_pct | number, percent | never | Share of user storage in use. Weak and overlapping between the classes. |
| epoch_zero_file_time | flag 0/1 | never | At least one file time within a day of the Unix epoch (a clock check). Fires on about nine phones in ten whatever the label (LAB-TR-0022 reports 41 of 46). Not informative. |
| future_file_time_48h | flag 0/1 | never | At least one file time more than 48 hours after acquisition (a clock check). Fires on about one phone in three whatever the label. Not informative. |
| clock_set_since_boot | flag 0/1 | never | The phone's own record says its clock was set since boot. Only a partial pointer to the clock-tampered rows: it misses some and fires on some untouched phones. |
| reviewer_reset_note | flag 0/1 | never | A note a human reviewer typed after reading the recovery log: reset seen. LEAKAGE TRAP. Written after the question was answered, so it would not exist at triage time. Drop it before you train. |
The feature ideas come from the reset-indicator study (LAB-TR-0004: recovery history, setup flags, boot-reason history, clocks that print an impossible year) and the clock-anomaly study (LAB-TR-0022: simple checks that fire on most ordinary phones). How the features relate to the label is invented.
Reference answers and traps
The label is exact by construction: each phone has a known reset age, and reset_30d is 1 when it is 30 days or less. test_answers.csv holds the label for every test row, and train_reference.csv and test_answers.csv add the columns an examiner would not have: the phone's profile, the true reset age (blank if never reset), whether its clock was tampered with, and the clock offset.
| Profile | Main set | Imbalanced variant | Label | Description |
|---|---|---|---|---|
| In use | 720 | 1,120 | 0 | Phone in ordinary use, not reset in the window; a few were reset long ago |
| Updated | 240 | 400 | 0 | Took a system update in the last month; setup flags and recovery runs move; no reset |
| New device | 160 | 180 | 0 | First set up in the last month; looks young; not reset |
| Reset, older | 280 | 200 | 0 | Reset 31 to 450 days before acquisition (many just past the 30-day line) |
| Reset, recent | 600 | 100 | 1 | Reset 0 to 30 days before acquisition |
Traps built in
Each one is described in the teaching guide, with the numbers. Try to find them yourself first.
| Trap | Where | What it teaches |
|---|---|---|
| Leakage column | reviewer_reset_note, in train.csv and test.csv | It is 1 on 93 percent of resets and 3 percent of the rest, because a reviewer typed it after reading the log. Scores rise with it and nothing warns you. It would not exist at triage time. |
| Class imbalance | variants/imbalanced/ | About 5 percent positives (66 of 1,400 train rows, 34 of 600 test rows). Accuracy looks high while most resets are missed; thresholds and class weights trade precision for recall. |
| Clock-tampered rows | clock_tampered in the answer files; 157 of 2,000 rows | Ages stamped after a clock change are shifted by 3 to 400 days, so a reset reads older or in the future. The same model scores clearly lower on these rows. |
| Look-alikes | Profiles new_device, updated, some in_use | Recent setup flags and young apps without any reset. "Recent setup" is not "reset". |
| Blanks that differ | Recovery columns; 536 rows have no recovery file | No file, a file with no wipe entry, and a clock that prints a year near 1970 (88 rows) are three different situations. |
| Flags that fire on everyone | epoch_zero_file_time, future_file_time_48h | On about 89 and 36 percent of rows whatever the label. A feature that is nearly always on says little. |
What the included baseline scores
600 test rows, 175 of them positive. These are printed by code/evaluate.py. The score of the default logistic regression is not printed here, because question 4 of the challenge asks for it: run the baseline and read it off.
| Model | Accuracy | Precision | Recall |
|---|---|---|---|
| Always predict "not reset" | 0.708 | not defined | 0.000 |
| Decision stump (one rule) | 0.857 | 0.989 | 0.514 |
| Logistic regression with the leakage column | 0.977 | 0.960 | 0.960 |
| Imbalanced variant, logistic regression | 0.965 | 0.933 | 0.412 |
| Imbalanced variant, class weights balanced | 0.945 | 0.508 | 0.941 |
Two limits. The accuracy of any model here is higher than it would be on real phones, because the labels are exact, the features come from a short generator and train and test are the same process; the teaching guide lists the reasons. And a score is an indicator of how a method behaves on this table. It is consistent with the method working, and it does not prove that it works on a device.
Three rows, read side by side
Three phones from train.csv. A rule that says "the setup flag is under 30 days old, so it was reset" calls all three resets and is right once.
| Column | D17064 | D60032 | D16967 |
|---|---|---|---|
| reset_30d | 1 | 0 | 0 |
| recovery_file_present | 1 | 1 | 0 |
| recovery_wipe_entries | 1 | 0 | blank |
| last_recovery_wipe_age_days | 9.33 | blank | blank |
| setup_complete_age_days | 9.32 | 24.18 | 19.07 |
| account_added_age_days | 0.78 | 24.19 | 0.70 |
| earliest_app_install_age_days | 9.25 | 24.23 | 1112.38 |
| apps_installed_30d_fraction | 1.000 | 1.000 | 0.676 |
| boot_count_since_setup | 2 | 16 | 13 |
| reviewer_reset_note | 1 | 0 | 0 |
| Profile (reference file) | Reset, recent (9.33 days) | New device | In use |
D17064 has a wipe entry 9.33 days old, a setup flag 9.32 days old, and its oldest app installed 9.25 days ago. Three sources point to one moment, which is consistent with a reset.
D60032 was set up 24 days ago and every app arrived after that, but its recovery history holds no wipe. It is a new phone, so the young app times are consistent with a first setup and say nothing about a reset.
D16967 has no recovery file, a recent setup flag, a recently added account and two thirds of its apps installed lately, but its oldest app is 1,112 days old. The recent activity is real; the reset is not. Only the oldest app and the profile column in the reference file tell you so.
D17064 is also the only one of the three with reviewer_reset_note = 1: the note a reviewer wrote after reading the recovery log. That is the leakage trap in miniature. All values are invented.
Try it with free tools
Python 3 and a text editor are enough. A spreadsheet or a SQLite browser helps for the first look.
- Unzip and look. Open
README.txtandDATA_DICTIONARY.mdin a text editor, thentrain.csvin a spreadsheet. Count the blank cells in each column. - Run the baseline. In the folder, run
python code/baseline_stdlib.py --train train.csv --test test.csv --out predictions.csv. It trains in a few seconds. - Score it. Run
python code/evaluate.py predictions.csv test_answers.csv. It prints accuracy, precision and recall. Write them down. - Break it on purpose. Add
--keep-leakand score again (accuracy 0.977, as in the table above). Then run the same two commands insidevariants/imbalancedand read the recall. - Slice it. Add
--by clock_tampered,profileto the score command. Which slice fails first? - Query it. Import
train.csvinto a SQLite browser as a table namedtrainand try the queries below. Then openTEACHING_GUIDE.mdfor ten things to try and the pitfalls.
-- the leakage column: 0.030 for not reset, 0.929 for reset SELECT reset_30d, COUNT(*) AS n, ROUND(AVG(reviewer_reset_note), 3) AS note_rate FROM train GROUP BY reset_30d; -- a clock check that fires on nearly everyone: 0.887 in both classes SELECT reset_30d, ROUND(AVG(epoch_zero_file_time), 3) FROM train GROUP BY reset_30d; -- one trustworthy rule: a recent, plausible wipe entry is right 266 times in 271 SELECT COUNT(*), SUM(reset_30d) FROM train WHERE last_recovery_wipe_age_days <= 30 AND recovery_clock_implausible = 0;
The third query finds only the phones that have a recovery file, so it misses the rest of the positives. Try the challenge: Train and doubt a reset classifier (Practitioner).
Get the dataset
One zip, 182,901 bytes. It holds 17 files: the tables, the answers, the guide, the code and the generator. Check it against the hash below.
Download factory-reset-classifier-1.0.zip (179 KB)
Served from this site, versioned. The manifest and the hashes are linked below.
SHA-256 of the zip
The loose files, manifest.json and SHA256SUMS.txt, sit beside the zip on this site. To rebuild every byte yourself, run python generate.py from the generator folder (Python 3, standard library only); change SEED for a different draw.
Licence
Data: Creative Commons Attribution 4.0 (CC BY 4.0). You may copy, share and adapt the tables, for teaching, research, casework and commercial work. You must give credit with the citation below, link the licence and say if you changed the files. The full text is at creativecommons.org/licenses/by/4.0 and in LICENCE.txt inside the zip.
Code: MIT. generate.py, the baselines and the evaluation script. Keep the copyright notice with copies.
Please keep the hashes beside any copy you share, and say that the files are synthetic. Do not present them as taken from real phones or real cases. The dataset is provided as is, without warranty.
Hashes
Check every file after download. The set hash is the SHA-256 of manifest.json, which lists the size and hash of every other file and the counts quoted on this page.
Set hash (SHA-256, manifest.json)
| File | Content | Size | SHA-256 |
|---|---|---|---|
| factory-reset-classifier-1.0.zip | Archive | 182,901 bytes | d1a9b3421d6c32f1553ace253e7515906018b858685164f91e719f781427f308 |
| train.csv | Train rows with labels | 118,747 bytes | 0a76b2e04b0c6cd54da59abbf258916a8d4dd83068865dd0aedeecd32b7dd622 |
| test.csv | Test rows, no labels | 49,447 bytes | 442bcd1f2c157c70207b20e168acb6a430ea2cd00d78df30d2aafe222e2a0987 |
| test_answers.csv | Test labels and slice columns | 15,335 bytes | cdca422480cd23c26cbf5319bfba778e73dbf45983fab122444f7d72173ab312 |
| train_reference.csv | Slice columns for train rows | 32,876 bytes | b3e91dfc2c871f9df4a886e385ee88518f826ddb9d3324035a4daaf60860af7e |
| variants/imbalanced/train.csv | Imbalanced variant: train.csv | 118,267 bytes | 9e3cca5476312ae082fb280900da024b45a71aac51e22b80b4fa988d551d9da4 |
| variants/imbalanced/test.csv | Imbalanced variant: test.csv | 49,698 bytes | 7cf83f8913ee3d1972f65775e69225a8ee3e01d31166b5dd947ae17da5b20fc2 |
| variants/imbalanced/test_answers.csv | Imbalanced variant: test_answers.csv | 13,694 bytes | c84f9115b5fd376ed14f011090cc1012e3014432d7d7fc67bb6efcf7ea619bca |
| variants/imbalanced/train_reference.csv | Imbalanced variant: train_reference.csv | 28,914 bytes | 7225f3ad3ff4ae2d12610bbd1fc4c369ead2000d120de91e4d3d67998a0e0075 |
| DATA_DICTIONARY.md | Data dictionary | 6,126 bytes | 543985b4567d458bf00068aeed811840ca7591b8eb135dcf67b33d55d5220826 |
| data_dictionary.csv | Data dictionary (table) | 4,327 bytes | e47b13b0f31e4e060c9d398c92a82cc126f9b53e6f72ca06bb4bc747612f8202 |
| TEACHING_GUIDE.md | Teaching guide | 11,282 bytes | c627047f6d794284d167b99fc358c3d096620527069d60b46b0050d48092ac73 |
| README.txt | Notes | 2,216 bytes | ae42e313e267c6ef98f24b84763c5709b98a174a200e1c1c1a4b8be3ac593a2a |
| LICENCE.txt | Licences | 1,518 bytes | 7b0fda7772b65d4b9fe0a2253cefc3c90cf4e9cc3b6e6864b678db0e8bfb594b |
| code/baseline_stdlib.py | Baseline (standard library) | 8,623 bytes | abf5a50e4eda20adcee604dc4e2bddc92e02134fe881488845caeac780be53cc |
| code/baseline_sklearn.py | Baseline (scikit-learn, optional) | 4,426 bytes | 9132e0ae957e12f0c22963281eb2e0c569dfe77ccafe4527bca02438da5c0b3e |
| code/evaluate.py | Evaluation script | 3,530 bytes | 68695af9bb090e1965b0aef3d98699113fa2a7bcc8c7f0114f4cb40bce11d94e |
| generator/generate.py | Generator (MIT) | 30,063 bytes | 0765e08baeb01f82df12316ce334d5e6548e16a6e2e0934ea5d66d307d272954 |
| manifest.json | Manifest (file, bytes, SHA-256, summary counts) | 5,875 bytes | 22621655320ddf47b86af1d9963b100976ef4032f3c630586c5c9b9ac5140911 |
| SHA256SUMS.txt | The same hashes in sha256sum format | 1,661 bytes | 77e180708e5c29ef77eecba6f7648ee256e2cd7ed48295ab661bbc8c8d1a35f7 |
The zip holds the same files as the folder (the zip itself, manifest.json and SHA256SUMS.txt excepted). Together those files are 498,966 bytes before compression. Hashes and sizes on this page are read from manifest.json.
Cite this dataset
Cite as
Addai, D., SectraTEK Lab (2026). Factory-reset classifier set (DS-0005), version 1.0. SectraTEK Lab Datasets. https://lab.sectratek.com/datasets/factory-reset-classifier/. SHA-256 of manifest.json 22621655320ddf47b86af1d9963b100976ef4032f3c630586c5c9b9ac5140911
@dataset{sectratek-lab-ds-0005,
author = {Addai, Daniel},
title = {Factory-reset classifier set},
year = {2026},
version = {1.0},
publisher = {SectraTEK Lab},
url = {https://lab.sectratek.com/datasets/factory-reset-classifier/},
note = {DS-0005, SHA-256 of manifest.json 22621655320ddf47b86af1d9963b100976ef4032f3c630586c5c9b9ac5140911}
}Related
- Report: Android reset indicators and their clocksThe recovery, boot-reason and setup-flag sources behind the features
- Report: Base rates of clock anomaliesWhy simple clock checks fire on most ordinary phones
- Dataset: Synthetic recovery-history setGenerated recovery files with reference answers
Datasets carry their own licence. This one is CC BY 4.0 for data and MIT for code. Articles are CC BY 4.0. All licences.