DataGenerator — pandas DataFrames from a RandSpec¶
DataGenerator turns a RandSpec (a dict, or a callable returning one) into a pandas DataFrame,
an infinite stream of dicts, files, or one SQLite output table. Every Python block in this guide runs in CI, in order.
from rand_engine import DataGenerator
spec = {
"user_id": {"method": "int_zfilled", "kwargs": {"length": 8}},
"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}},
}
df = DataGenerator(spec, seed=42).size(1_000).get_df()
assert list(df.columns) == ["user_id", "age"]
assert df["age"].between(18, 80).all()
API¶
| Call | Does |
|---|---|
DataGenerator(spec, seed=None) |
validates the spec at once (SpecValidationError); owns one np.random.default_rng(seed) |
.size(n) |
rows per get_df, total rows in one batch write, or rows per stream microbatch; n may be a callable returning an int |
.transformers([f, ...]) |
functions DataFrame -> DataFrame, applied in order after generation |
.get_df() |
one DataFrame of size rows |
.stream_dict(min_throughput=1, max_throughput=10) |
an infinite generator of dicts, sleeping 1/uniform(min, max) s after each |
.write / .writeStream |
file writers, see 3_WRITING_FILES.md |
Calling get_df without .size raises RandEngineError.
Seeds¶
A seed fixes the output: the same spec, seed and size give the same frame. Each generator owns
its own numpy.random.Generator (PCG64), so generators never share or reset NumPy's global state.
Calls on one generator continue its sequence; they do not repeat it.
a = DataGenerator(spec, seed=7).size(100).get_df()
b = DataGenerator(spec, seed=7).size(100).get_df()
assert a.equals(b)
gen = DataGenerator(spec, seed=7).size(100)
assert not gen.get_df().equals(gen.get_df())
Methods¶
Each column is {"method": <name>, "kwargs": {...}}. Bounds of integers are inclusive. A method that generates text gives a string[pyarrow] column (pd.ArrowDtype(pa.string())) whose nulls are pd.NA; numeric, boolean and datetime columns keep their pandas dtype. An embedded transformer's output dtype is pandas' inference of what the transformer returns.
| Method | Required kwargs | Optional kwargs | Column fields |
|---|---|---|---|
integers |
min, max |
int_type (default int32) |
— |
int_zfilled |
length (integer 1…18) |
— | — |
floats |
min, max |
decimals (default 2) |
— |
floats_normal |
mean, std |
decimals (default 2) |
— |
booleans |
— | true_prob (default 0.5) |
— |
distincts |
distincts (list) |
— | — |
distincts_prop |
distincts (dict value → integer weight) |
— | — |
unix_timestamps |
start, end |
date_format (default %Y-%m-%d) |
— |
dates |
start, end |
date_format (default %Y-%m-%d) |
— |
uuid4 |
— | — | — |
exponential |
— | scale (default 1.0), decimals (default 2) |
— |
lognormal |
— | mean (default 0.0), std (default 1.0), decimals (default 2) |
— |
poisson |
— | lam (default 1.0) |
— |
zipf |
— | a (default 2.0) |
— |
constant |
value (a scalar) |
— | — |
distincts_map |
distincts (dict key → list of values) |
— | cols (exactly 2 names) |
distincts_map_prop |
distincts (dict key → [[value, weight], ...]) |
— | cols (exactly 2 names) |
distincts_multi_map |
distincts (dict key → one list per level) |
— | cols (levels + 1 names) |
complex_distincts |
pattern, replacement, templates |
— | — |
pk |
— | style, start, step, domain, key, format; see 4_CONSTRAINTS.md |
— |
fk |
parent, parent_size |
skew, born_before; see 4_CONSTRAINTS.md |
— |
Column fields are required keys beside kwargs, not optional kwargs. Only the three map
methods require cols; the global optional modifiers are documented separately below.
dates and unix_timestamps parse start and end with date_format. A dates value is a
string drawn at whole-second resolution in UTC; its date_format may use only the directives
%Y %m %d %H %M %S %f (%f renders 000000). Any other directive fails validation on both
engines, naming the supported set.
from rand_engine.validators.exceptions import SpecValidationError
for length in (1, 18):
DataGenerator({"id": {"method": "int_zfilled", "kwargs": {"length": length}}})
for length in (0, 19):
try:
DataGenerator({"id": {"method": "int_zfilled", "kwargs": {"length": length}}})
except SpecValidationError:
pass
else:
raise AssertionError("expected int_zfilled length refusal")
spec = {
"price": {"method": "floats", "kwargs": {"min": 1, "max": 100, "decimals": 2}},
"status": {"method": "distincts_prop", "kwargs": {"distincts": {"paid": 9, "refunded": 1}}},
"created_at": {"method": "dates", "kwargs": {
"start": "2024-01-01 00:00:00", "end": "2024-12-31 23:59:59",
"date_format": "%Y-%m-%d %H:%M:%S"}},
"epoch": {"method": "unix_timestamps", "kwargs": {"start": "2024-01-01", "end": "2024-12-31"}},
}
df = DataGenerator(spec, seed=1).size(1_000).get_df()
assert df["created_at"].str.match(r"^2024-\d\d-\d\d \d\d:\d\d:\d\d$").all() # .str.* predicates return bool[pyarrow]
text_df = DataGenerator({"status": {"method": "distincts", "kwargs": {"distincts": ["paid", "refunded"]}}}, seed=1).size(10).get_df()
assert str(text_df["status"].dtype) == "string[pyarrow]"
exponential, lognormal, poisson, zipf and constant run on the NumPy engine only.
from rand_engine.validators.exceptions import SpecValidationError
spec = {
"wait_s": {"method": "exponential", "kwargs": {"scale": 2.5}},
"amount": {"method": "lognormal", "kwargs": {"mean": 3.0, "std": 0.5}},
"visits": {"method": "poisson", "kwargs": {"lam": 4}},
"rank": {"method": "zipf", "kwargs": {"a": 1.5}},
"source": {"method": "constant", "kwargs": {"value": "web"}},
}
df = DataGenerator(spec, seed=1).size(1_000).get_df()
assert (df["wait_s"] > 0).all() and (df["rank"] >= 1).all()
assert (df["source"] == "web").all()
Float rules¶
decimalsis an integer with |decimals| ≤ 308 for every float method; beyond that the spec fails validation.exponentialandlognormaltake onlydecimals ≥ 0.floatsdraws on thedecimalsgrid betweenminandmax. A grid of more than 2^53 steps, or a grid end (bound times 10^decimals) beyond int64, draws uniformly within [min,max] instead.- Bounds are compared in float64: an integer bound above 2^53 that a double cannot hold may round to the nearest double.
- Rounding never turns a finite draw into
inf. Parameters near the float64 limit can still drawinffrom the distribution itself; that is accepted. poissonlamis capped at 10^6.
for bad in ({"method": "floats", "kwargs": {"min": 0, "max": 1, "decimals": 309}},
{"method": "poisson", "kwargs": {"lam": 2_000_000}}):
try:
DataGenerator({"x": bad})
except SpecValidationError:
pass
else:
raise AssertionError("expected SpecValidationError")
Null and anomaly rates¶
Any single-column NumPy-engine method may carry null_rate (share of rows set to null) and anomaly_rate (share
replaced by a draw from anomaly_values, a non-empty list of scalars required when the rate is
above 0). Both are in [0, 1]; anomalies apply first, then nulls. pk, fk and the multi-column methods
(distincts_map, distincts_map_prop, distincts_multi_map, complex_distincts) refuse them, and so does SparkGenerator.
spec = {
"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}, "null_rate": 0.1},
"price": {"method": "floats", "kwargs": {"min": 1, "max": 100},
"anomaly_rate": 0.05, "anomaly_values": [-1.0, 9999.0]},
}
df = DataGenerator(spec, seed=5).size(2_000).get_df()
assert df["age"].isna().any() and df["price"].isin([-1.0, 9999.0]).any()
for bad in ({"id": {"method": "pk", "kwargs": {"start": 1}, "null_rate": 0.1}},
{"d": {"method": "distincts_map", "cols": ["device", "os"], "null_rate": 0.1,
"kwargs": {"distincts": {"phone": ["android", "ios"]}}}},
{"x": {"method": "exponential", "kwargs": {"scale": 2, "decimals": -1}}}):
try:
DataGenerator(bad)
except SpecValidationError:
pass
else:
raise AssertionError("expected SpecValidationError")
Multi-column methods (NumPy engine only)¶
These take a cols list naming the columns they produce.
| Method | distincts shape |
Columns |
|---|---|---|
distincts_map |
{key: [value, ...]} |
cols[0] = the key, cols[1] = a value |
distincts_map_prop |
{key: [(value, weight), ...]} |
cols[0] = the key, cols[1] = a value |
distincts_multi_map |
{key: [[a, ...], [b, ...]]} |
the key, then one column per inner list (cartesian product); cols must name levels + 1 columns, any other count fails validation |
complex_distincts fills each replacement character of pattern with one template column.
spec = {
"device_os": {"method": "distincts_map", "cols": ["device", "os"],
"kwargs": {"distincts": {"phone": ["android", "ios"], "desktop": ["linux", "windows"]}}},
"company": {"method": "distincts_multi_map", "cols": ["sector", "sub_sector", "size"],
"kwargs": {"distincts": {"tech": [["software", "hardware"], ["small", "large"]]}}},
"ip": {"method": "complex_distincts", "kwargs": {
"pattern": "x.x.x.x", "replacement": "x",
"templates": [
{"method": "distincts", "kwargs": {"distincts": ["10", "192"]}},
{"method": "integers", "kwargs": {"min": 0, "max": 255}},
{"method": "integers", "kwargs": {"min": 0, "max": 255}},
{"method": "integers", "kwargs": {"min": 1, "max": 254}},
]}},
}
df = DataGenerator(spec, seed=1).size(500).get_df()
assert list(df.columns) == ["device", "os", "sector", "sub_sector", "size", "ip"]
assert set(zip(df["device"], df["os"])) <= {("phone", "android"), ("phone", "ios"), ("desktop", "linux"), ("desktop", "windows")}
Migration to kwargs¶
kwargs is the only parameter key. A column carrying args, splitable, sep or pk as a key
raises SpecValidationError (pk stays a method, see 4_CONSTRAINTS.md).
for key in ("args", "splitable", "sep", "pk"):
try:
DataGenerator({"age": {"method": "integers", "kwargs": {"min": 0, "max": 9}, key: 1}})
except SpecValidationError:
pass
else:
raise AssertionError(key)
Transformers¶
A column may carry "transformers": functions applied value by value. .transformers([...]) takes
whole-frame functions, run after the column ones.
import pandas as pd
spec = {
"name": {"method": "distincts", "kwargs": {"distincts": ["ana", "bia"]},
"transformers": [str.upper]},
"age": {"method": "integers", "kwargs": {"min": 10, "max": 30}},
}
def age_group(df):
df["adult"] = df["age"] >= 18
return df
df = DataGenerator(spec, seed=3).size(100).transformers([age_group]).get_df()
assert set(df["name"]) <= {"ANA", "BIA"}
assert (df["adult"] == (df["age"] >= 18)).all()
assert df["name"].dtype == pd.Series([str.upper("ana")]).dtype # a transformer's output dtype is pandas' inference of its plain Python result
Validation¶
An invalid spec raises SpecValidationError (a RandEngineError) on construction, listing every
error with a corrected example.
try:
DataGenerator({"age": {"method": "integers", "kwargs": {"min": 0}}})
except SpecValidationError as error:
assert "max" in str(error)
else:
raise AssertionError("expected SpecValidationError")
Streaming dicts¶
stream_dict never ends: it generates size rows per microbatch, converts datetime columns to
strings and adds timestamp_created (epoch seconds) to each record. Stop it yourself.
After that unchanged normalization, every microbatch uses pandas records, preserving public Python values for scalar, nested, mixed, custom and extension values, and non-string labels.
from itertools import islice
spec = {"user_id": {"method": "int_zfilled", "kwargs": {"length": 8}}}
stream = DataGenerator(spec, seed=1).size(10).stream_dict(min_throughput=500, max_throughput=1000)
records = list(islice(stream, 25))
assert len(records) == 25 and set(records[0]) == {"user_id", "timestamp_created"}
scalar_records = list(islice(
DataGenerator({"value": {"method": "integers", "kwargs": {"min": 1, "max": 3}}}, seed=3)
.size(2).stream_dict(500, 1000), 2
))
nested_records = list(islice(
DataGenerator({"value": {"method": "distincts", "kwargs": {"distincts": [{"nested": [1, 2]}]}}}, seed=3)
.size(1).stream_dict(500, 1000), 1
))
assert isinstance(nested_records[0]["value"], dict)