In Auriel Geron's book, there is a short description of the approach:
you could compute a hash of each instance’s identifier, keep only the last byte of the hash, and put
the instance in the test set if this value is lower or equal to 51 (~20% of 256). This ensures
that the test set will remain consistent across multiple runs, even if you refresh the dataset.
The new test set will contain 20% of the new instances, but it will not contain any instance
that was previously in the training set.
While a full explanation of what exactly happens and why is probably best placed on StackOverflow, I can try to answer your questions, first with some background info.
The method uses a cyclic redundancy check, which is a method of checking that the raw blocks of memory have not been damaged/changed. It is a way to ensure data integrity, e.g. in network traffic - checking if a message way altered between being sent and received.
For train/test splits, it is checking the unique identifier of each sample. We have a column that gives each sample an ID - this should never be changed! Don't delete rows, only append to the end with new unique IDs.
In this part: test_ratio * 2**32
, the part $2^{32}$ represents the largest integer of a 32-bit system.
0xFFFFFFFF is a large number; it's the hexadecimal representation of $2^{32}-1$
To answer your questions:
- What is the 3rd line doing:
crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32
Based on the information I gave above, we see the crc32
function finds the checksum value in memory (of the unique identifier). If we know the unique ID has never changed, then We ensure that crc32(np.int64(identifier)) & 0xffffffff
will always return exactly the same numeric value, across all Python versions and platforms.
Imagine we give IDs in the range 0-80 for train, and 81-100 for test. No we want to make sure a sample'd s ID falls in the first bucket. We check its ID is simple less that 81, right? Well the numeric value we made above is checked to be less than our test_ratio * 2**32
, where 2**32
is the largest 32-bit number. It checks that the sample's ID is within the range of train data, not in the test bucket:: > test_ratio * 2**32
.
- What is the anonymous function doing in the 2nd to last line?
lambda id_: test_set_check(id_, test_ratio)
This simply applies our test_set_check
function to each sample's unique identifiers. Using the apply
methd on a Pandas Series object (here it is one column of a Pandas DataFrame).
- In practice, do you commonly split datasets by ID in this manner?
Not really... Scikit-Learn's train_test_split
is often good enough. I think there are many other ways to remove bias and errors from your models before worrying too much about the impact of random splits.
For example, the snoop bias, whereby you analyse the entire dataset yourself before deciding on a model architecture/pipeline, thereby incorporating knowledge of the entuire distribution, which is inherently biasing our model.
There is also bias in overfitting e.g. in sequential imaging data (think frames of videos) such that the background is consistent, even though the objects you might want to detect are not. Your model will learn what to expect based on the background, which is not robust! Here you might look into using a geographical split (not random at all).
On a side note, there is also a slightly robuster way of setting random seeds in Python (instead of using NumPy's random seed generator). Have a look here for some differences.
Helpful resources:
- https://stackoverflow.com/questions/36819849/detect-int32-overflow-using-0xffffffff-masking-in-python
- https://pynative.com/python-random-module/
- https://stackoverflow.com/questions/30092226/how-to-calculate-crc32-with-python-to-match-online-results
- https://stackoverflow.com/questions/49331030/bitwise-xor-0xffffffff/49332291#49332291