From ca3053d9b49759bd70ee08dc7c73486ee1616ab6 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 13 Jul 2026 13:36:37 -0400 Subject: [PATCH 1/4] Add precision option to float_field() --- pointblank/field.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pointblank/field.py b/pointblank/field.py index a38c784fd..67fd00814 100644 --- a/pointblank/field.py +++ b/pointblank/field.py @@ -490,6 +490,7 @@ class FloatField(Field): min_val: float | None = None max_val: float | None = None allowed: list[float] | None = field(default=None) + precision: int | None = None # Override dtype with default dtype: str = "Float64" @@ -517,6 +518,10 @@ def _validate(self) -> None: if len(self.allowed) == 0: raise ValueError("allowed list cannot be empty") + # Validate precision + if self.precision is not None and self.precision < 0: + raise ValueError(f"precision ({self.precision}) must be a non-negative integer") + def has_allowed_values(self) -> bool: """Check if this field has a set of allowed values.""" return self.allowed is not None @@ -526,6 +531,7 @@ def float_field( min_val: float | None = None, max_val: float | None = None, allowed: list[float] | None = None, + precision: int | None = None, nullable: bool = False, null_probability: float = 0.0, unique: bool = False, @@ -555,6 +561,9 @@ def float_field( allowed List of allowed values (categorical constraint). When provided, values are sampled from this list. Cannot be combined with `min_val=`/`max_val=`. + precision + Number of decimal places to round generated values to. Default is `None` (no rounding). + Must be a non-negative integer. Has no effect when `allowed=` or `generator=` is used. nullable Whether the column can contain null values. Default is `False`. null_probability @@ -578,8 +587,8 @@ def float_field( ------ ValueError If `min_val` is greater than `max_val`, if `allowed` is an empty list, if - `null_probability` is not between `0.0` and `1.0`, or if `dtype` is not a valid - float type. + `null_probability` is not between `0.0` and `1.0`, if `precision` is negative, + or if `dtype` is not a valid float type. Examples -------- @@ -620,7 +629,20 @@ def float_field( calibration=pb.float_field(min_val=0.9, max_val=1.1), ) - pb.preview(pb.generate_dataset(schema, n=30, seed=7)) + pb.preview(pb.generate_dataset(schema, n=30, seed=23)) + ``` + + Use `precision=` to round generated values to a fixed number of decimal places. This is useful + for prices, scores, or any measurement where full floating-point precision is unwanted: + + ```{python} + schema = pb.Schema( + price=pb.float_field(min_val=1.0, max_val=200.0, precision=2), + score=pb.float_field(min_val=0.0, max_val=100.0, precision=1), + probability=pb.float_field(min_val=0.0, max_val=1.0, precision=4), + ) + + pb.preview(pb.generate_dataset(schema, n=20, seed=23)) ``` Setting `dtype="Float32"` gives reduced precision, and a custom `generator=` provides @@ -636,13 +658,14 @@ def float_field( log_value=pb.float_field(generator=lambda: math.log(rng.uniform(1, 1000))), ) - pb.preview(pb.generate_dataset(schema, n=20, seed=99)) + pb.preview(pb.generate_dataset(schema, n=20, seed=23)) ``` """ return FloatField( min_val=min_val, max_val=max_val, allowed=allowed, + precision=precision, nullable=nullable, null_probability=null_probability, unique=unique, From 0896d445b6aa5d50f7a477fb6458d86885c6184f Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 13 Jul 2026 13:36:50 -0400 Subject: [PATCH 2/4] Respect float precision in value generation --- pointblank/generate/generators.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pointblank/generate/generators.py b/pointblank/generate/generators.py index e0cbc7036..b3dcdf248 100644 --- a/pointblank/generate/generators.py +++ b/pointblank/generate/generators.py @@ -64,11 +64,17 @@ def _generate_float(field: Field, rng: random.Random, generator: Any | None = No """Generate a random float value respecting field constraints.""" min_val = getattr(field, "min_val", None) max_val = getattr(field, "max_val", None) + precision = getattr(field, "precision", None) min_val = min_val if min_val is not None else -1e10 max_val = max_val if max_val is not None else 1e10 - return rng.uniform(float(min_val), float(max_val)) + value = rng.uniform(float(min_val), float(max_val)) + + if precision is not None: + value = round(value, precision) + + return value def _generate_string( From c8f855b905924de92a077a9b117b94855822b408 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 13 Jul 2026 13:37:39 -0400 Subject: [PATCH 3/4] Add float_field() precision validation tests --- tests/test_field.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_field.py b/tests/test_field.py index b0ec39d89..a3f6b224e 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -121,6 +121,26 @@ def test_float_field_is_numeric(self): assert field.is_float() is True assert field.is_integer() is False + def test_float_field_with_precision(self): + """Test FloatField with precision parameter.""" + field = float_field(min_val=0.0, max_val=100.0, precision=2) + assert field.precision == 2 + + def test_float_field_precision_none_by_default(self): + """Test that precision defaults to None.""" + field = float_field() + assert field.precision is None + + def test_float_field_precision_zero_is_valid(self): + """Test that `precision=0` is accepted (rounds to integer).""" + field = float_field(min_val=0.0, max_val=10.0, precision=0) + assert field.precision == 0 + + def test_float_field_negative_precision_raises_error(self): + """Test that negative precision raises ValueError.""" + with pytest.raises(ValueError, match="precision"): + float_field(precision=-1) + class TestStringField: """Tests for StringField and string_field().""" From 2734bad06981a63c8411ecaf126d906bb6c42132 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Mon, 13 Jul 2026 13:37:49 -0400 Subject: [PATCH 4/4] Add float precision generator tests --- tests/test_generate.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_generate.py b/tests/test_generate.py index 9c2f13149..712e5fd25 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -105,6 +105,52 @@ def test_generate_float64_with_constraints(self): assert all(0.0 <= v <= 1.0 for v in values) + def test_generate_float_with_precision(self): + """Test that precision rounds generated float values.""" + field = float_field(min_val=0.0, max_val=100.0, precision=2) + config = GeneratorConfig(n=50, seed=23) + values = generate_column(field, config) + + assert all(isinstance(v, float) for v in values) + assert all(round(v, 2) == v for v in values) + + def test_generate_float_precision_zero(self): + """Test that `precision=0` rounds values to whole numbers.""" + field = float_field(min_val=0.0, max_val=100.0, precision=0) + config = GeneratorConfig(n=20, seed=7) + values = generate_column(field, config) + + assert all(v == round(v, 0) for v in values) + + def test_generate_float_precision_none_produces_full_precision(self): + """Test that `precision=None` does not round values.""" + field = float_field(min_val=0.0, max_val=1.0) + config = GeneratorConfig(n=20, seed=23) + values = generate_column(field, config) + + # Full-precision floats are very unlikely to already be rounded to 2 decimals + assert not all(round(v, 2) == v for v in values) + + def test_generate_float_precision_with_unique(self): + """Test that precision composes correctly with unique=True.""" + field = float_field(min_val=1.0, max_val=200.0, precision=2, unique=True) + config = GeneratorConfig(n=20, seed=23) + values = generate_column(field, config) + + assert len(values) == len(set(values)) + assert all(round(v, 2) == v for v in values) + + def test_generate_float_precision_with_nullable(self): + """Test that precision composes correctly with nullable.""" + field = float_field( + min_val=0.0, max_val=10.0, precision=1, nullable=True, null_probability=0.3 + ) + config = GeneratorConfig(n=50, seed=99) + values = generate_column(field, config) + + non_null = [v for v in values if v is not None] + assert all(round(v, 1) == v for v in non_null) + class TestGenerateColumnString: """Tests for string column generation."""