import numpy as np, pandas as pd

SEED = 123456789            # replace: draw a fresh number once, e.g. python -c "import secrets; print(secrets.randbelow(10**9))"
rng = np.random.default_rng(SEED)
groups = ["vehicle", "low", "mid", "high"]
animals = pd.DataFrame({                              # or: animals = pd.read_csv("my_animals.csv"), columns tag, cage
    "tag": [f"T{i:03d}" for i in range(1, 25)],
    "cage": np.repeat(range(1, 7), 4)})
parts = []
for cage, a in animals.groupby("cage"):               # every group once per cage
    assert len(a) == len(groups), f"cage {cage}: needs {len(groups)} animals"
    parts.append(a.assign(group=rng.permutation(groups)))
key = pd.concat(parts)
key["code"] = rng.permutation([f"A{i:02d}" for i in range(1, len(key) + 1)])   # codes reveal nothing
key.to_csv("allocation_key.csv", index=False)         # move to the key-holder's own storage at once
key[["cage", "tag", "code"]].to_csv("coded_sheet.csv", index=False)   # for treatment and measurement
print(pd.crosstab(key.cage, key.group))               # key-holder's check: one of each group per cage
