Updated: Jul 12, 2026
| 20 min

Sweet Bonanza 1000 RTP Analysis: 60,000 Spins

Python analysis of 60,000 Sweet Bonanza 1000 demo spins, covering RTP, dead spins, bonus frequency, payout concentration, and logging errors.

Cover image for the blog series "Expected Riches and Expensive Lessons"

What happened across 60,000 paid demo spins of Sweet Bonanza 1000?

After reconstructing every bonus and reducing the log to one row per paid wager, the sample produced an observed RTP of 109.05%, a zero-return paid-round rate of 56.64%, and 131 bonus triggers. The published 96.53% RTP remained inside both the paid-round and session-cluster bootstrap intervals.

Those results do not prove that the game has a positive expected value or certify its fairness. The sample return was dominated by rare outcomes, including one bonus worth more than 5,000× the triggering wager.

This chapter uses Python to answer five questions:

  1. How often did a paid round return zero?
  2. How concentrated was the observed return?
  3. How frequently did the bonus trigger, and what did it pay?
  4. What do the recorded multiplier fields actually measure?
  5. Was the sample RTP statistically compatible with the published RTP?

Responsible-gambling note: RTP is a long-run mathematical expectation, not a promise for one session. A sample can return more than 100% even when the underlying expected value is below 100%.

Key Results from the Supplied Dataset

MetricRecalculated result
Raw logged events61,455
Paid spins60,000
Recorded free-spin rows1,455
Collection sessions60
Bonus sessions131
Zero-return paid rounds33,981
Dead-spin rate56.635%
95% Wilson interval for dead-spin rate56.238%–57.031%
Bonus-trigger rate0.2183%
Approximate mean paid spins per trigger458
Median bonus return10.41 currency units / 52.05× bet
Largest 10% of winning paid rounds: share of return57.401%
95% bootstrap interval for top-10% share50.826%–64.767%
Total wager12,000.00
Normalized total return13,086.14
Observed RTP109.051%
95% paid-round bootstrap interval for RTP94.318%–132.566%
95% session-cluster bootstrap interval for RTP93.881%–131.662%

The supplied file contains only Regular-mode observations:

is_ante = False for all 61,455 rows
bet = 0.20 for all rows

No Regular-versus-Ante comparison can be made from this dataset.

Corrections to the Earlier Results

Recalculating the article from the supplied CSV changed several results.

Earlier figureRecalculated figureReason
56.74% dead spins56.635%The earlier value counted zero-base-return bonus triggers as dead, even though their paid rounds later returned a bonus payout.
73.486% from the largest 10% of wins57.401%The earlier calculation treated repeated cumulative bonus totals as separate winning observations.
59.77% ineffective multipliersNot a valid game-mechanic estimateAll 87 supposed base-game multiplier rows occurred immediately after a bonus and appear to be collector-state leakage.
108.81% observed RTP109.051%Reconstructing all 131 cumulative bonus sessions and attaching each final value to its triggering paid spin produces 109.051%.
93.99%–130.76% RTP interval94.318%–132.566%Recalculated with 20,000 paid-round bootstrap samples and seed 42.

The earlier 108.81% RTP could not be reproduced exactly from the supplied file. The normalized calculation is fully accounted for:

Base-game return:       9,292.22
Final bonus returns:    3,793.92
Total return:          13,086.14
Total wager:           12,000.00
Observed RTP:            109.051%

Table of Contents

Official Sweet Bonanza 1000 Information

Pragmatic Play’s public Sweet Bonanza 1000 page lists:

  • a 6×5 layout;
  • wins when eight or more matching fruit or candy symbols land;
  • tumbling wins;
  • a maximum advertised win of 25,000×;
  • 10 free spins from at least four lollipop symbols;
  • candy-bomb multipliers up to 1,000× in the bonus game;
  • five additional free spins from at least three lollipops during the feature;
  • a published RTP of 96.53%.

Official source: Pragmatic Play — Sweet Bonanza 1000, checked July 11, 2026.

The exact RTP and rules shown in the tested demo build should still be archived. Public product pages do not prove that every market or operator uses one identical configuration.

How Sweet Bonanza 1000 Works

Anywhere-Pays Wins

A payout occurs when at least eight matching symbols appear anywhere on the 6×5 grid. They do not need to occupy adjacent positions or a conventional payline.

Tumbles

After a win:

  1. the winning symbols disappear;
  2. new symbols fall into the empty positions;
  3. another win may form;
  4. the sequence continues until no further win occurs.

One paid spin can therefore contain several dependent tumbles. The paid round—not an individual tumble—is the correct unit for RTP.

Bonus Multipliers

The public rules place candy-bomb multipliers in the bonus game. During a tumble, multiplier values remain visible and are added before being applied to the qualifying win.

A reliable multiplier study requires tumble-level fields showing:

  • which tumble displayed the multiplier;
  • whether that same tumble produced a qualifying win;
  • the multiplier value applied;
  • the tumble payout.

The supplied dataset contains only one aggregate multiplier_total field per logged row. It cannot independently reconstruct the full multiplier mechanism.

This Is a Statistical Analysis, Not a Fairness Certification

The dataset contains outputs collected from demo play. It does not provide:

  • source code;
  • certified probability tables;
  • RNG output or seeds;
  • regulator-controlled testing;
  • every available RTP configuration;
  • proof that demo and real-money deployments are identical.

The analysis can describe the supplied sample, expose logging problems, quantify uncertainty, and test reproducible statistical claims. It cannot certify the game or predict future spins.

What the Dataset Contains

The supplied CSV has seven columns:

ColumnMeaning in this analysis
round_idEvent counter that restarts at the beginning of each collection session
betRecorded wager; always 0.20 in this file
winBase return on paid rows and cumulative feature return on bonus rows
cascade_depthRecorded number of tumbles or cascades
multiplier_totalAggregate multiplier field recorded by the collector
is_bonusWhether the row belongs to a free-spin sequence
is_anteAnte flag; always False in this file

The round_id resets reveal 60 collection sessions. Every session contains exactly 1,000 paid rows plus any free-spin rows generated during that session.

from __future__ import annotations

import numpy as np
import pandas as pd

raw = pd.read_csv("dataset.csv")

raw["session_id"] = (
    raw["round_id"]
    .diff()
    .fillna(1)
    .le(0)
    .cumsum()
)

print(raw.shape)
print(raw["session_id"].nunique())
print((~raw["is_bonus"]).sum())
print(raw["is_bonus"].sum())

Expected output:

Rows:              61,455
Sessions:               60
Paid rows:          60,000
Bonus rows:          1,455

How the Raw Events Were Normalized

One Row per Paid Wager

For RTP, dead-spin rate, payout concentration, and bonus frequency, the observational unit is one paid wager:

paid-round return
    = base-game return
    + final cumulative bonus return caused by that wager

The bonus rows cannot be summed because their win values are cumulative.

For example, one recorded feature progressed as follows:

0.60, 0.60, 0.60, 9.18, 9.28, 9.28, 9.28, 9.28, 9.28, 9.28

Its feature return is 9.28, not the sum of all ten rows.

Every one of the 131 bonus sequences in the file has a non-decreasing win series, supporting the cumulative interpretation.

Reconstruct the Bonus Sessions

raw = raw.reset_index(names="event_index")

previous_bonus_state = (
    raw.groupby("session_id", observed=True)["is_bonus"]
    .shift(fill_value=False)
)

raw["bonus_start"] = (
    raw["is_bonus"]
    & ~previous_bonus_state
)

raw["bonus_id"] = (
    raw["bonus_start"]
    .cumsum()
    .where(raw["is_bonus"])
)

bonus_rows = raw[raw["is_bonus"]].copy()

bonus_monotonic = (
    bonus_rows.groupby("bonus_id", observed=True)["win"]
    .apply(lambda values: values.is_monotonic_increasing)
)

if not bonus_monotonic.all():
    raise ValueError(
        "At least one bonus sequence is not cumulative."
    )

bonuses = (
    bonus_rows.groupby("bonus_id", observed=True)
    .agg(
        session_id=("session_id", "first"),
        first_event=("event_index", "min"),
        last_event=("event_index", "max"),
        free_spin_rows=("event_index", "size"),
        bonus_return=("win", "last"),
    )
    .reset_index()
)

bonuses["trigger_event"] = (
    bonuses["first_event"] - 1
)

Attach Each Bonus to Its Triggering Paid Spin

paid_rounds = raw.loc[
    ~raw["is_bonus"],
    [
        "event_index",
        "session_id",
        "round_id",
        "bet",
        "win",
    ],
].copy()

paid_rounds = paid_rounds.rename(
    columns={
        "bet": "wager",
        "win": "base_return",
    }
)

bonus_map = bonuses.set_index(
    "trigger_event"
)["bonus_return"]

paid_rounds["bonus_return"] = (
    paid_rounds["event_index"]
    .map(bonus_map)
    .fillna(0.0)
)

paid_rounds["bonus_triggered"] = (
    paid_rounds["bonus_return"].gt(0)
)

paid_rounds["total_return"] = (
    paid_rounds["base_return"]
    + paid_rounds["bonus_return"]
)

paid_rounds["paid_round_id"] = np.arange(
    len(paid_rounds)
)

Validate the Reconstruction

assert len(raw) == 61_455
assert len(paid_rounds) == 60_000
assert len(bonuses) == 131
assert raw["session_id"].nunique() == 60
assert paid_rounds["wager"].eq(0.20).all()
assert not raw["is_ante"].any()
assert paid_rounds["paid_round_id"].is_unique
assert paid_rounds["total_return"].ge(0).all()

assert np.isclose(
    paid_rounds["base_return"].sum(),
    9_292.22,
)

assert np.isclose(
    bonuses["bonus_return"].sum(),
    3_793.92,
)

assert np.isclose(
    paid_rounds["total_return"].sum(),
    13_086.14,
)

Statistical Assumptions

The following analyses assume:

Independent Paid Rounds

Paid rounds are treated as independent observations. Tumbles and free spins inside one paid round are not treated as independent wagers.

Stable Configuration

The game build, denomination, RTP setting, collection logic, and feature rules are assumed not to have changed during collection.

Complete Logging

The collector is assumed to have captured zero-return rounds, bonus starts, retriggers, large wins, and session resets without selective omissions.

Representative Empirical Distribution

The bootstrap treats the observed rounds or sessions as representative. It cannot generate rare outcomes absent from the sample.

Question 1: How Common Were Dead Spins?

A dead spin is defined here as a normalized paid round whose complete attributable return equals zero:

total_return = 0

This definition prevents a zero-base-return spin that triggers a paying bonus from being classified as dead.

paid_rounds["dead_spin"] = (
    paid_rounds["total_return"].eq(0)
)

number_dead = int(
    paid_rounds["dead_spin"].sum()
)

number_paid = len(paid_rounds)

dead_rate = number_dead / number_paid

The supplied data produced:

Dead paid rounds: 33,981 / 60,000
Dead-spin rate:    56.635%

Wilson Confidence Interval

from statsmodels.stats.proportion import (
    proportion_confint,
)

ci_low, ci_high = proportion_confint(
    count=number_dead,
    nobs=number_paid,
    alpha=0.05,
    method="wilson",
)

print(f"Rate: {dead_rate:.4%}")
print(f"95% Wilson CI: {ci_low:.4%}{ci_high:.4%}")

Output:

Rate: 56.6350%
95% Wilson CI: 56.2380%–57.0311%

Why the Earlier Result Was 56.74%

The raw paid rows contain 34,046 observations with base_return = 0, or 56.7433%.

However, 65 of those rows triggered a bonus and therefore had a positive normalized total_return. Counting them as dead mixes the base-spin outcome with the complete paid-round outcome.

Both statistics can be reported, but they answer different questions:

DefinitionEstimate
Base-game return equals zero56.743%
Complete paid-round return equals zero56.635%

Test Against a 50% Benchmark

A one-sided exact binomial test gives:

from scipy.stats import binomtest

dead_test = binomtest(
    k=number_dead,
    n=number_paid,
    p=0.50,
    alternative="greater",
)

print(dead_test.pvalue)
p = 1.10 × 10^-232

The result is unsurprising with 60,000 observations. Estimation and practical interpretation are more useful than testing an arbitrary 50% threshold.

Session-Level Variation

session_dead_rates = (
    paid_rounds.groupby(
        "session_id",
        observed=True,
    )["dead_spin"]
    .mean()
)

print(session_dead_rates.describe())

Across the 60 sessions, the dead-spin rate ranged from:

Minimum: 51.9%
Median:  56.7%
Maximum: 60.2%

The pooled interval is narrow because it contains 60,000 paid rounds. It does not imply that every 1,000-spin session will have nearly the same rate.

Question 2: How Concentrated Was the Return?

Return concentration measures how much of the observed payout came from the largest winning paid rounds.

The correct input is the normalized total_return, not every raw bonus snapshot.

winning_rounds = (
    paid_rounds.loc[
        paid_rounds["total_return"].gt(0),
        "total_return",
    ]
    .sort_values(ascending=False)
    .reset_index(drop=True)
)

number_top = max(
    1,
    int(np.ceil(0.10 * len(winning_rounds))),
)

top_share = (
    winning_rounds.iloc[:number_top].sum()
    / winning_rounds.sum()
)

print(len(winning_rounds))
print(number_top)
print(top_share)

Output:

Winning paid rounds:             26,019
Largest 10% of winning rounds:    2,602
Share of normalized return:      57.401%

Why the Earlier Figure Was 73.486%

The 73.486% result is reproducible only when all positive raw win rows are treated as independent observations.

That method includes multiple cumulative values from the same bonus:

0.60, 0.60, 0.60, 9.18, 9.28, 9.28, ...

Those are not separate paid-round returns. They repeatedly count one evolving feature total and inflate the apparent concentration.

Bootstrap Interval for the Top-10% Share

def bootstrap_top_share(
    returns: np.ndarray,
    fraction: float = 0.10,
    n_bootstrap: int = 20_000,
    seed: int = 42,
    batch_size: int = 1_000,
) -> np.ndarray:
    if not 0 < fraction <= 1:
        raise ValueError(
            "fraction must be between 0 and 1."
        )

    values, counts = np.unique(
        returns,
        return_counts=True,
    )

    order = np.argsort(values)[::-1]
    values = values[order]
    probabilities = counts[order] / counts.sum()

    positive_mask = values > 0
    positive_values = values[positive_mask]

    if len(positive_values) == 0:
        raise ValueError(
            "At least one positive return is required."
        )

    rng = np.random.default_rng(seed)
    estimates = np.empty(n_bootstrap)
    number_rounds = len(returns)

    for start in range(0, n_bootstrap, batch_size):
        stop = min(
            start + batch_size,
            n_bootstrap,
        )

        sampled_counts = rng.multinomial(
            number_rounds,
            probabilities,
            size=stop - start,
        )

        positive_counts = sampled_counts[
            :, positive_mask
        ]

        number_positive = positive_counts.sum(axis=1)
        number_top = np.ceil(
            fraction * number_positive
        ).astype(int)

        cumulative_counts = np.cumsum(
            positive_counts,
            axis=1,
        )

        cumulative_returns = np.cumsum(
            positive_counts * positive_values,
            axis=1,
        )

        cutoff = (
            cumulative_counts >= number_top[:, None]
        ).argmax(axis=1)

        row_index = np.arange(stop - start)

        previous_count = np.where(
            cutoff > 0,
            cumulative_counts[
                row_index,
                np.maximum(cutoff - 1, 0),
            ],
            0,
        )

        previous_return = np.where(
            cutoff > 0,
            cumulative_returns[
                row_index,
                np.maximum(cutoff - 1, 0),
            ],
            0.0,
        )

        top_return = (
            previous_return
            + (number_top - previous_count)
            * positive_values[cutoff]
        )

        total_return = (
            positive_counts * positive_values
        ).sum(axis=1)

        estimates[start:stop] = (
            top_return / total_return
        )

    return estimates

share_samples = bootstrap_top_share(
    paid_rounds["total_return"].to_numpy()
)

share_ci = np.percentile(
    share_samples,
    [2.5, 97.5],
)

Using 20,000 resamples and seed 42:

Observed share:   57.401%
95% bootstrap CI: 50.826%–64.767%

Plot the Concentration Curve

import matplotlib.pyplot as plt

ordered = winning_rounds.to_numpy()

cumulative_share = (
    np.cumsum(ordered)
    / ordered.sum()
)

fraction_of_winners = (
    np.arange(1, len(ordered) + 1)
    / len(ordered)
)

plt.figure(figsize=(10, 6))
plt.plot(
    fraction_of_winners,
    cumulative_share,
)
plt.axvline(
    0.10,
    linestyle="--",
    label="Largest 10% of winning paid rounds",
)
plt.axhline(
    top_share,
    linestyle="--",
    label=f"Return share: {top_share:.1%}",
)
plt.xlabel("Fraction of winning paid rounds")
plt.ylabel("Fraction of normalized return")
plt.title(
    "Sweet Bonanza 1000 Paid-Round Return Concentration"
)
plt.legend()
plt.tight_layout()
plt.show()

Sweet Bonanza 1000 paid-round return concentration curve

The result shows substantial concentration, but less than the raw-row calculation suggested. A small subset of wins still had a large influence on the sample mean.

Question 3: How Often Did the Bonus Trigger?

The normalized dataset contains 131 bonus sessions from 60,000 paid spins.

bonus_count = int(
    paid_rounds["bonus_triggered"].sum()
)

paid_count = len(paid_rounds)

bonus_rate = bonus_count / paid_count

bonus_ci = proportion_confint(
    count=bonus_count,
    nobs=paid_count,
    alpha=0.05,
    method="wilson",
)

Results:

Bonus triggers:              131
Trigger rate:                0.2183%
95% Wilson interval:         0.1840%–0.2590%
Mean paid spins per trigger: 458.0
Gross wager per trigger:     91.60

The mean waiting estimate is:

1 / 0.0021833 = 458.0 paid spins

This is an average, not a deadline. It does not mean the feature becomes due after 458 spins.

Bonus Length and Retriggers

The 131 recorded bonus sequences contained:

Recorded free-spin rowsNumber of bonus sessions
10107
1520
203
251

This pattern is consistent with initial 10-spin features and five-spin retriggers, assuming one bonus row represents one free spin.

Bonus-Payout Distribution

bonuses["bonus_return_multiple"] = (
    bonuses["bonus_return"] / 0.20
)

bonus_summary = bonuses[
    ["bonus_return", "bonus_return_multiple"]
].describe(
    percentiles=[0.10, 0.25, 0.50, 0.75, 0.90]
)

print(bonus_summary)
StatisticCurrency unitsBet multiple
Minimum0.703.50×
10th percentile2.4712.35×
25th percentile5.63528.175×
Median10.4152.05×
Mean28.961144.806×
75th percentile23.465117.325×
90th percentile44.78223.90×
Maximum1,096.365,481.80×

The mean is almost three times the median because a small number of large features pull it upward.

Additional descriptive results:

Bonuses below 50× bet:  46.56%
Bonuses below 100× bet: 69.47%
Bonus-only share of all normalized return: 28.99%

Why 91.60 Is Not a Bonus Price

The observed gross wager per trigger is:

12,000 total wager / 131 triggers = 91.60

It is not:

  • a fixed feature-purchase price;
  • the net amount lost before the bonus;
  • the expected cost of one feature;
  • directly comparable with a median feature payout as a profitability test.

The paid spins before each trigger also produced base-game returns.

The earlier sign-test framing can be reproduced: 127 of 131 bonus payouts were below 91.60, producing a one-sided exact binomial p-value of approximately 4.44 × 10^-33. That test does not establish that the bonus is unprofitable because the comparison threshold is an estimated gross-wager average and excludes base returns earned during the wait.

Better Bonus-Economics Measures

More useful descriptive statistics include:

  • trigger probability;
  • median and mean bonus return in wager multiples;
  • feature-return quantiles;
  • bonus contribution to total return;
  • complete trigger-to-trigger episode returns.

A trigger-to-trigger episode should include every wager and every return between two observed triggers in the same uninterrupted collection session.

Question 4: What Do the Multiplier Records Mean?

The earlier analysis identified 87 non-bonus rows where:

multiplier_total > 0

Of those rows, 52 had win = 0, producing:

52 / 87 = 59.770%

A normal-approximation one-sided test gives the earlier p-value of approximately 0.0342. An exact binomial test gives:

One-sided exact p-value: 0.04284
95% exact interval:      48.71%–70.15%
95% Wilson interval:     49.26%–69.45%

However, the structural inspection is more important than the p-value.

All 87 Rows Followed a Bonus Immediately

suspected_rows = raw.loc[
    ~raw["is_bonus"]
    & raw["multiplier_total"].gt(0)
].copy()

previous_row_was_bonus = (
    raw["is_bonus"]
    .shift(fill_value=False)
    .loc[suspected_rows.index]
    .to_numpy()
)

print(len(suspected_rows))
print(previous_row_was_bonus.sum())

Output:

Non-bonus rows with multiplier_total > 0: 87
Those immediately following a bonus row: 87

Every supposed base-game multiplier event occurred on the first paid row after a bonus sequence.

The official public rules place candy-bomb multipliers in the bonus game. The pattern strongly suggests that multiplier_total was not cleared immediately when the collector left bonus mode.

Therefore, 59.77% should be treated as a logging-state diagnostic, not an estimate of how often real multiplier symbols have no economic effect.

What Would Be Needed for a Valid Multiplier Test?

A valid study requires one row per tumble with fields such as:

paid_round_id
bonus_id
free_spin_index
tumble_index
symbol_win
multiplier_landed
multiplier_value
multiplier_applied
tumble_return

Without those fields, the dataset cannot establish whether a multiplier landed on the same tumble as a qualifying win.

Question 5: Was the Observed RTP Compatible with 96.53%?

RTP is calculated from normalized paid rounds:

RTP = total return / total wager
def calculate_rtp(
    group: pd.DataFrame,
) -> float:
    total_wager = group["wager"].sum()
    total_return = group["total_return"].sum()

    if total_wager <= 0:
        raise ValueError(
            "Total wager must be positive."
        )

    return 100 * total_return / total_wager

observed_rtp = calculate_rtp(paid_rounds)

The supplied dataset produced:

Total wager:  12,000.00
Total return: 13,086.14
Observed RTP: 109.051%

The sample finished in profit. That does not imply a positive long-run expectation.

The nonparametric bootstrap resamples normalized paid rounds with replacement.

def bootstrap_rtp_constant_wager(
    group: pd.DataFrame,
    n_bootstrap: int = 20_000,
    seed: int = 42,
    batch_size: int = 1_000,
) -> np.ndarray:
    wagers = group["wager"].to_numpy(dtype=float)
    returns = group["total_return"].to_numpy(dtype=float)

    if len(group) == 0:
        raise ValueError(
            "Cannot bootstrap an empty dataset."
        )

    if not np.allclose(wagers, wagers[0]):
        raise ValueError(
            "This efficient implementation requires "
            "a constant wager."
        )

    values, counts = np.unique(
        returns,
        return_counts=True,
    )

    probabilities = counts / counts.sum()
    number_rounds = len(group)
    total_wager = number_rounds * wagers[0]

    rng = np.random.default_rng(seed)
    estimates = np.empty(n_bootstrap)

    for start in range(0, n_bootstrap, batch_size):
        stop = min(
            start + batch_size,
            n_bootstrap,
        )

        sampled_counts = rng.multinomial(
            number_rounds,
            probabilities,
            size=stop - start,
        )

        sampled_return = sampled_counts @ values

        estimates[start:stop] = (
            100 * sampled_return / total_wager
        )

    return estimates

rtp_samples = bootstrap_rtp_constant_wager(
    paid_rounds
)

row_ci = np.percentile(
    rtp_samples,
    [2.5, 97.5],
)

Using 20,000 resamples and seed 42:

Observed RTP: 109.051%
95% interval: 94.318%–132.566%

The published 96.53% value lies inside this interval.

Under this empirical bootstrap model, the sample does not provide strong evidence against 96.53%. The interval is also wide enough to include many other values, so this is not confirmation of the theoretical RTP.

Session-Cluster Bootstrap

Paid rounds from one browser session may share collection conditions. A sensitivity analysis can resample complete sessions instead of individual rounds.

def cluster_bootstrap_rtp(
    group: pd.DataFrame,
    n_bootstrap: int = 20_000,
    seed: int = 42,
) -> np.ndarray:
    sessions = (
        group.groupby(
            "session_id",
            observed=True,
        )
        .agg(
            wager=("wager", "sum"),
            total_return=("total_return", "sum"),
        )
        .reset_index(drop=True)
    )

    if len(sessions) < 2:
        raise ValueError(
            "At least two sessions are required."
        )

    rng = np.random.default_rng(seed)

    indices = rng.integers(
        low=0,
        high=len(sessions),
        size=(n_bootstrap, len(sessions)),
    )

    wagers = sessions["wager"].to_numpy()
    returns = sessions["total_return"].to_numpy()

    return (
        100
        * returns[indices].sum(axis=1)
        / wagers[indices].sum(axis=1)
    )

cluster_samples = cluster_bootstrap_rtp(
    paid_rounds
)

cluster_ci = np.percentile(
    cluster_samples,
    [2.5, 97.5],
)

Result:

95% session-cluster interval: 93.881%–131.662%

The similarity between the row and session intervals suggests that session-level resampling does not materially change the broad conclusion.

Session RTP Was Highly Variable

Each session contained 1,000 paid spins. Their observed RTP values ranged from:

Minimum: 71.455%
Median:  91.343%
Maximum: 640.735%

The maximum came from a session containing an exceptionally large bonus. This illustrates how one rare outcome can dominate a limited sample.

Why the Bootstrap Is Still Limited

The empirical bootstrap cannot create a payout that never occurred in the dataset.

If the true game includes extremely rare high-impact outcomes absent from the sample, the bootstrap may understate long-run uncertainty. It also cannot correct:

  • missing events;
  • duplicated events;
  • an incorrect wager denominator;
  • changing game configurations;
  • incorrect bonus reconstruction;
  • non-representative collection sessions.

Mean Return vs. Typical Experience

The observed mean determines RTP:

mean paid-round return = 0.218102
wager per paid round   = 0.20

The median paid-round return was zero.

That is not a contradiction. A large number of zero and small returns can coexist with a high mean when a few outcomes are very large.

The sample’s largest normalized paid-round return was approximately:

1,096.97 currency units

One outcome of that size has a visible effect on the average of 60,000 spins.

What 60,000 Spins Can and Cannot Tell Us

The Dataset Can Estimate

  • the observed paid-round dead-spin rate;
  • the observed bonus-trigger rate;
  • feature-payout quantiles;
  • return concentration;
  • sample RTP;
  • session-to-session variation;
  • collector anomalies visible in the logged fields.

The Dataset Cannot Prove

  • the internal RNG is fair;
  • the exact theoretical RTP;
  • the probability of unobserved rare outcomes;
  • demo and real-money equivalence;
  • every operator uses the same configuration;
  • the next spin’s result;
  • the true multiplier mechanism without tumble-level records.

Main Limitations

Demo Environment

The data came from demo play. The article does not independently verify that every real-money deployment uses the same configuration.

No Ante Sample

All is_ante values are false. The dataset cannot estimate the effect of an Ante or double-chance mode.

Inferred Sessions

Session identifiers were reconstructed from round_id resets rather than supplied explicitly.

Cumulative Bonus Field

Bonus win values are cumulative. The normalization uses each feature’s final row, which is strongly supported by the non-decreasing sequences but should still be documented by the collector’s author.

Aggregate Multiplier Field

The dataset lacks tumble-level multiplier data. The 87 non-bonus multiplier rows appear to be transition-state leakage.

Rare Outcomes

A 25,000× advertised maximum is far above most observed outcomes. Sixty thousand spins can still miss important low-probability events.

Bootstrap Assumptions

The bootstrap assumes the observed rounds or sessions are representative and exchangeable. It cannot reconstruct missing tail outcomes.

No Independent Certification

The analysis does not inspect source code, certified game mathematics, RNG output, or regulator reports.

Final Conclusions

1. More Than Half of Paid Rounds Returned Zero

The normalized dead-spin rate was 56.635%, with a 95% Wilson interval of 56.238%–57.031%.

The earlier 56.74% figure measured zero base returns and included 65 rounds that later returned a bonus payout.

2. Return Was Concentrated, but Less Than Originally Reported

The largest 10% of winning paid rounds generated 57.401% of normalized return.

The earlier 73.486% result was inflated by repeated cumulative bonus snapshots.

3. The Sample Recorded 131 Bonus Triggers

The observed trigger rate was 0.2183%, or approximately one trigger per 458 paid spins.

The median feature returned 52.05× the wager, while the mean was 144.81× because rare large features pulled it upward.

4. The Multiplier Result Was a Logging Artifact

The earlier 59.77% estimate can be reproduced numerically, but every included row occurred immediately after a bonus sequence.

It should not be interpreted as evidence about the actual multiplier mechanic.

5. The Sample RTP Was 109.051%

The normalized sample returned 13,086.14 from 12,000.00 wagered.

Both the paid-round and session-cluster bootstrap intervals included the published 96.53% RTP. The sample therefore did not reject that value, but its uncertainty was too wide to verify it precisely.

Frequently Asked Questions

What was the observed Sweet Bonanza 1000 RTP in this dataset?

The normalized observed RTP was 109.051%, based on 13,086.14 returned from 12,000.00 wagered.

Why is this different from the earlier 108.81% result?

The earlier figure could not be reproduced exactly. Using all 60,000 paid wagers and the final cumulative return from each of the 131 bonus sessions produces 109.051%.

Does an RTP above 100% mean the game is profitable?

No. It means this limited sample returned more than it wagered. Negative-expectation games can produce profitable sessions or samples through random variation.

What is the published Sweet Bonanza 1000 RTP?

Pragmatic Play’s public page lists 96.53%. The exact in-game value from the tested build should also be recorded.

Did the sample contradict 96.53% RTP?

No. The published value lies inside both recalculated bootstrap intervals. That means the sample did not distinguish it from other values inside those wide ranges.

What was the dead-spin rate?

Using the complete paid-round return, 56.635% of paid spins returned zero.

Why was the previous dead-spin result 56.74%?

That figure used only the paid row’s base return. It classified 65 zero-base-return bonus triggers as dead even though those paid rounds later returned a feature payout.

How often did the bonus trigger?

The file contains 131 bonus triggers in 60,000 paid spins, an observed rate of 0.2183%, or approximately one per 458 spins.

What was the typical bonus payout?

The median was 10.41 currency units. With a 0.20 wager, that equals 52.05× the bet.

Did 73.486% of return come from the largest 10% of wins?

Not after proper paid-round normalization. The corrected figure is 57.401%. The earlier result counted repeated cumulative bonus totals as separate wins.

Did 59.77% of multipliers have no effect?

That percentage is reproducible from 87 non-bonus rows, but all 87 immediately followed a bonus. The pattern indicates a logging-state problem, not a valid estimate of multiplier behavior.

Is 60,000 spins enough to verify theoretical RTP?

It is enough to estimate sample RTP, but not necessarily enough to verify a high-volatility game’s theoretical RTP precisely. Rare payouts cause slow convergence.

Can this analysis prove the game is fair?

No. Output analysis cannot replace certified game mathematics, source-code inspection, RNG testing, or regulatory verification.

Can the data predict the next spin?

No. Historical sample statistics do not reveal the next random outcome.

Key Takeaways

  1. Reduce the event log to one row per paid wager before calculating RTP.
  2. Treat bonus win values as cumulative in this dataset and use only each sequence’s final value.
  3. Attach the final bonus return to the paid spin that triggered it.
  4. Distinguish zero base return from zero complete paid-round return.
  5. Use the normalized paid-round table for return concentration.
  6. Do not count cumulative bonus snapshots as separate wins.
  7. Report the supplied dataset as Regular mode only; it contains no Ante observations.
  8. Treat the non-bonus multiplier rows as a collector-state anomaly.
  9. Express bonus payouts in wager multiples as well as currency units.
  10. Do not interpret gross wager per trigger as a feature price or net cost.
  11. Compare the published RTP with the bootstrap interval, not with the observed estimate alone.
  12. Use a session-cluster bootstrap as a sensitivity check.
  13. Report uncertainty and data-quality problems alongside headline estimates.
  14. Do not describe observational demo analysis as a certified fairness audit.
  15. Publish the raw data, normalization code, build details, and in-game RTP screen for reproducibility.

The central lesson is the difference between an average and a typical result.

Most paid rounds in this sample returned zero, while a small number of large wins and bonuses raised the overall return above 100%. That pattern is compatible with a highly volatile payout distribution and explains why short sessions can differ dramatically from a long-run theoretical RTP.

Series: Expected Riches and Expensive Lessons

2 Chapters