Lab rats biggest issue when it comes to similarity is the face and hair styles.Huh. Storing the character data in SQLite seems ... surmountable. Hell, you could even finagle a pretty fast rollback system, depending on how Ren'Py implements it. But would that be worth it? If you have too many NPCs, you get procedural soup. You stop seeing differences and start seeing similarities. Lab Rat's thirty or so is pretty much there already.
The real solution is not more NPCs, but fewer. There is no way for NPCs to permanently leave the game, and there should be. Be it death, emigration, or human trafficking, there needs to be a way to GTFO and make way for new arrivals.
Those two area's are were we as humans generally look at people and make associations.
You don't have different noses, mouths,eyes or ears and you only have a few hair styles.
You don't have changes in head shape, hair line, cheek bones...
You also don't have a lot of range in body types. In real life we have people with different length legs, torsos, wastes, butt shapes and sizes, and a lot more...
Then of course you have visual aging markers, and other characteristics that make them look different, tats, scars, ...
If the characters are 3D you can make all those chances a lot easier. If they are rendered images as they are it would take a lot of images to store all that information. The most simplistic method would use gigabytes of storage. Making better use of file space can reduce it some.
There are other options such as converting the rendered image to an SVG format. Then you can also store the offset changes for the different body parts and types. You can achieve photo realism with an SVG.
You could also store a modified version of the character model and clothing. Instead of rendering the objects normally create projection images of each character off the model. It would be using an old game programming style of rendering the image from back in the DOS days. You could however get lighting and so on. Texturing is fairly simple for it. You could generate textures at the start of the game the first time it is played. After that they would be stored. If a new character is generated it could create the few initial images needed then add to create others in the background. Unfortunately cpython has the GIL so running it on a separate thread isn't possible. But can be run when other loads aren't so heavy. If you wonder if there is enough performance to do it we were doing it on 33Mhz systems and now we are over 3Ghz. Even doing it with python vs C/C++ then you should still be running several times faster than we were then.
You can look the method up in books like the blackart of 3D game programming. Given it isn't needing to be rendered in real time you have more time to do stuff like add lighting and shadows in which is easy since it is projection.
Being that it would now have a 3D model you could change any of the features.