diff --git a/stan/fit.py b/stan/fit.py
index 69db205..98fcb8a 100644
--- a/stan/fit.py
+++ b/stan/fit.py
@@ -1,5 +1,6 @@
 import collections
 import json
+from math import ceil
 from typing import Generator, Tuple, cast
 
 import numpy as np
@@ -41,6 +42,8 @@ class Fit(collections.abc.Mapping):
             constrained_param_names,
         )
         self.num_warmup, self.num_samples = num_warmup, num_samples
+        if not isinstance(num_thin, int):
+            raise ValueError(f"{type(num_thin)} object cannot be interpreted as an integer: num_thin={num_thin}")
         self.num_thin, self.save_warmup = num_thin, save_warmup
 
         # `self.sample_and_sampler_param_names` collects the sample and sampler param names.
@@ -51,7 +54,9 @@ class Fit(collections.abc.Mapping):
 
         num_flat_params = sum(np.product(dims_ or 1) for dims_ in dims)  # if dims == [] then it is a scalar
         assert num_flat_params == len(constrained_param_names)
-        num_samples_saved = (self.num_samples + self.num_warmup * self.save_warmup) // self.num_thin
+        num_samples_saved = ceil(self.num_samples / self.num_thin) + ceil(
+            (self.num_warmup * self.save_warmup) / self.num_thin
+        )
 
         # self._draws holds all the draws. We cannot allocate it before looking at the draws
         # because we do not know how many sampler-specific parameters are present. Later in this
@@ -124,7 +129,7 @@ class Fit(collections.abc.Mapping):
         param_indexes = self._parameter_indexes(param)
         param_dim = [] if param in self.sample_and_sampler_param_names else self.dims[self.param_names.index(param)]
         # fmt: off
-        num_samples_saved = (self.num_samples + self.num_warmup * self.save_warmup) // self.num_thin
+        num_samples_saved = ceil(self.num_samples / self.num_thin) + ceil((self.num_warmup * self.save_warmup) / self.num_thin)
         assert self._draws.shape == (len(self.sample_and_sampler_param_names) + len(self.constrained_param_names), num_samples_saved, self.num_chains)
         # fmt: on
         if not len(param_indexes):

diff --git a/tests/test_basic_bernoulli.py b/tests/test_basic_bernoulli.py
index 735ae90..c467531 100644
--- a/tests/test_basic_bernoulli.py
+++ b/tests/test_basic_bernoulli.py
@@ -42,8 +42,8 @@ def test_bernoulli_fixed_param(posterior):
 
 
 def test_bernoulli_sampling_invalid_argument(posterior):
-    with pytest.raises(TypeError, match=r"'float' object cannot be interpreted as an integer"):
-        posterior.sample(num_thin=2.0)
+    with pytest.raises(ValueError, match="<class 'float'> object cannot be interpreted as an integer"):
+        posterior.sample(num_thin=2.5)
 
 
 def test_bernoulli_sampling(fit):
diff --git a/tests/test_normal.py b/tests/test_normal.py
index 07bcbb4..5520b6a 100644
--- a/tests/test_normal.py
+++ b/tests/test_normal.py
@@ -1,3 +1,5 @@
+from math import ceil
+
 import stan
 
 
@@ -41,7 +43,7 @@ def test_normal_sample_args():
     program_code = "parameters {real y;} model {y ~ normal(0,1);}"
     posterior = stan.build(program_code, random_seed=1)
     assert posterior is not None
-    fit = posterior.sample(num_samples=350, num_thin=2)
+    fit = posterior.sample(num_samples=350, num_warmup=350, num_thin=3, save_warmup=True)
     df = fit.to_frame()
-    assert len(df["y"]) == 350 * 4 // 2
+    assert len(df["y"]) == ceil(350 / 3) * 2 * 4
     assert -5 < df["y"].mean() < 5

Error in formula for `num_samples_save` in `fit.py`
#### Describe the bug
Currently defined formula is right only on even number of samples.

    num_samples_saved = (self.num_samples + self.num_warmup * self.save_warmup) // self.num_thin

In `stan` C++ the samples are created with a `for` -loop in [`stan/services/util/generate_transitions.hpp`](https://github.com/stan-dev/stan/blob/develop/src/stan/services/util/generate_transitions.hpp) for both `num_warmup` and `num_samples` as `num_iterations`

```
  for (int m = 0; m < num_iterations; ++m) {
    ...

    if (save && ((m % num_thin) == 0)) {
      ...
    }
  }
```

the correct formula is

    from math import ceil
    ...
    num_samples_saved = ceil(self.num_samples / self.num_thin) + ceil((self.num_warmup * self.save_warmup) / self.num_thin)




#### Steps/Code to Reproduce

```python
import stan

schools_code = """
data {
  int<lower=0> J;         // number of schools
  real y[J];              // estimated treatment effects
  real<lower=0> sigma[J]; // standard error of effect estimates
}
parameters {
  real mu;                // population treatment effect
  real<lower=0> tau;      // standard deviation in treatment effects
  vector[J] eta;          // unscaled deviation from mu by school
}
transformed parameters {
  vector[J] theta = mu + tau * eta;        // school treatment effects
}
model {
  target += normal_lpdf(eta | 0, 1);       // prior log-density
  target += normal_lpdf(y | theta, sigma); // log-likelihood
}
"""

schools_data = {"J": 8,
                "y": [28,  8, -3,  7, -1,  1, 18, 12],
                "sigma": [15, 10, 16, 11,  9, 11, 10, 18]}

posterior = stan.build(schools_code, data=schools_data)
fit = posterior.sample(num_chains=1, num_samples=100, num_warmup=100, num_thin=3, save_warmup=True)
eta = fit["eta"]  # array with shape (8, 68) <- original formula gives (8, 66)  and raises IndexError
```
tests/test_basic_bernoulli.py::test_bernoulli_sampling_invalid_argument
tests/test_basic_bernoulli.py::test_bernoulli_sampling_thin tests/test_basic_bernoulli.py::test_bernoulli_fixed_param tests/test_basic_bernoulli.py::test_bernoulli_sampling tests/test_basic_bernoulli.py::test_bernoulli_get_item tests/test_basic_bernoulli.py::test_bernoulli_random_seed tests/test_basic_bernoulli.py::test_bernoulli_random_seed_same tests/test_basic_bernoulli.py::test_bernoulli_random_seed_different tests/test_basic_bernoulli.py::test_bernoulli_different_chains tests/test_normal.py::test_normal_build