Skip to content

Casino & Gaming Analytics

Tribal Casino & Gaming Operations Analytics on Azure

This use case covers the ingestion, transformation, and analysis of tribal casino gaming operations data — slot machine telemetry, table game events, player tracking, F&B point-of-sale, hotel PMS, and loyalty programs — using Azure Cloud Scale Analytics patterns. The implementation combines real-time slot and table game event streaming with batch ingestion from operational systems to produce floor performance dashboards, player lifetime value models, comp optimization analysis, and regulatory compliance reports for NIGC and Title 31 AML requirements.

Reference Implementation

The complete working code for this domain lives in examples/casino-analytics/. This page explains the architecture, data sources, and step-by-step build process.

Synthetic Data Only

ALL data in this implementation is fully synthetic. No real player data, gaming results, or financial transactions are used. Player names, loyalty IDs, transaction amounts, and all other personally identifiable information are generated using Faker and random distributions. This dataset is designed for architectural demonstration only and must not be used for actual gaming operations or regulatory filings.


Architecture Overview

The platform follows a dual-path ingestion model: a hot path for sub-second slot telemetry and table game events via Event Hub and Azure Data Explorer, and a cold/batch path for player tracking, F&B, hotel, and loyalty data via Azure Data Factory into ADLS Gen2 with dbt-driven medallion transformations.

graph LR
    subgraph Sources
        SLOT[Slot Machine Telemetry<br/>SAS/G2S Protocol Events]
        TBL[Table Game Events<br/>RFID Chip Tracking]
        PLR[Player Tracking<br/>Card-In/Card-Out]
        FB[F&B Point-of-Sale<br/>Comps & Cash]
        HTL[Hotel PMS<br/>Reservations & Stays]
        LOY[Loyalty Program<br/>Tier & Rewards]
        VID[Video Analytics<br/>Floor Camera Feeds]
    end

    subgraph "Hot Path"
        EH[Azure Event Hub<br/>slot-events / table-events]
        ADX[Azure Data Explorer<br/>Streaming Ingestion]
    end

    subgraph "Batch Path"
        ADF[Azure Data Factory]
        ADLS[ADLS Gen2<br/>Bronze Layer]
    end

    subgraph "Medallion Architecture"
        BRZ[Bronze<br/>Raw Events & Extracts]
        SLV[Silver<br/>Player 360 · Gaming Events<br/>F&B Patterns · Stay History]
        GLD[Gold<br/>Floor Optimization<br/>Player Value · Comp ROI<br/>AML Alerts · NIGC Reports]
    end

    subgraph "Consumption"
        PBI[Power BI<br/>Operations Dashboards]
        ADXD[ADX Dashboards<br/>Real-Time Floor Performance]
        AVA[Azure Video Analyzer<br/>Floor Density Heatmaps]
    end

    SLOT -->|Real-time stream| EH
    TBL -->|Real-time stream| EH
    EH --> ADX
    ADX --> ADXD

    PLR --> ADF
    FB --> ADF
    HTL --> ADF
    LOY --> ADF
    VID --> AVA

    ADF --> ADLS
    ADLS --> BRZ
    BRZ --> SLV
    SLV --> GLD
    GLD --> PBI
    ADX --> GLD

Data Sources

All data sources listed below are synthetically generated for demonstration purposes.

Source Protocol / Format Frequency Volume (Synthetic) Key Fields
Slot Machine Telemetry SAS/G2S → JSON Sub-second ~50M events/day machine_id, denomination, coin_in, coin_out, jackpot, game_cycle_ms
Table Game Events RFID Chip Tracking → JSON Per-hand/round ~5M events/day table_id, game_type, buy_in, cash_out, avg_bet, hands_played
Player Tracking Card-In/Card-Out CSV Session-level ~200K sessions/day player_id, card_in_ts, card_out_ts, machine_id, theo_win, actual_win
F&B Point-of-Sale POS Export CSV Batch (hourly) ~80K transactions/day check_id, player_id, outlet, items, total, comp_amount, payment_type
Hotel PMS PMS Export CSV Batch (daily) ~5K reservations/day reservation_id, player_id, room_type, rate, nights, comp_flag, amenities
Loyalty Program CRM Extract CSV Batch (daily) ~500K member records player_id, tier, points_balance, lifetime_theo, lifetime_actual, signup_date
Video Floor Analytics Azure Video Analyzer Continuous ~200 camera feeds camera_id, zone, person_count, density_pct, timestamp

Step-by-Step Implementation

Step 1: Real-Time Slot Telemetry Streaming

Slot machines emit events on every game cycle — coin-in, coin-out, bonus triggers, and jackpot hits. These events stream through Event Hub into Azure Data Explorer for sub-second floor monitoring.

Slot Event Producer (Python)

"""Synthetic slot machine telemetry producer for Event Hub."""

import asyncio
import json
import random
import uuid
from datetime import datetime, timezone

from azure.eventhub.aio import EventHubProducerClient
from azure.eventhub import EventData

CONNECTION_STR = "Endpoint=sb://<namespace>.servicebus.windows.net/;SharedAccessKeyName=send;SharedAccessKey=<key>"
EVENTHUB_NAME = "slot-events"

DENOMINATIONS = [0.01, 0.05, 0.25, 1.00, 5.00]
MACHINE_IDS = [f"SLT-{i:04d}" for i in range(1, 501)]
GAME_TYPES = ["video-slots", "progressive", "video-poker", "mechanical-reel"]


def generate_slot_event() -> dict:
    """Generate a single synthetic slot machine event."""
    machine_id = random.choice(MACHINE_IDS)
    denomination = random.choice(DENOMINATIONS)
    credits_wagered = random.choice([1, 2, 3, 5, 10, 20])
    coin_in = denomination * credits_wagered

    # House edge simulation: ~8-12% hold depending on game type
    hold_pct = random.uniform(0.06, 0.14)
    if random.random() < 0.35:  # ~35% of spins pay something
        payout_multiplier = random.choices(
            [1, 2, 5, 10, 50, 500],
            weights=[50, 25, 12, 8, 4, 1],
            k=1
        )[0]
        coin_out = coin_in * payout_multiplier
    else:
        coin_out = 0.0

    return {
        "event_id": str(uuid.uuid4()),
        "machine_id": machine_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "denomination": denomination,
        "game_type": random.choice(GAME_TYPES),
        "credits_wagered": credits_wagered,
        "coin_in": round(coin_in, 2),
        "coin_out": round(coin_out, 2),
        "jackpot_flag": coin_out > coin_in * 100,
        "game_cycle_ms": random.randint(2800, 5500),
        "zone": f"ZONE-{random.choice(['A','B','C','D','E'])}",
        "floor_section": random.choice(["high-limit", "main-floor", "non-smoking", "vip"]),
    }


async def produce_events(batch_size: int = 100, interval_sec: float = 1.0):
    """Stream synthetic slot events to Event Hub."""
    producer = EventHubProducerClient.from_connection_string(
        conn_str=CONNECTION_STR, eventhub_name=EVENTHUB_NAME
    )
    async with producer:
        while True:
            batch = await producer.create_batch()
            for _ in range(batch_size):
                event = generate_slot_event()
                batch.add(EventData(json.dumps(event)))
            await producer.send_batch(batch)
            await asyncio.sleep(interval_sec)


if __name__ == "__main__":
    asyncio.run(produce_events())

ADX Ingestion Setup

// Create target table for slot telemetry
.create table SlotEvents (
    event_id: string,
    machine_id: string,
    timestamp: datetime,
    denomination: real,
    game_type: string,
    credits_wagered: int,
    coin_in: real,
    coin_out: real,
    jackpot_flag: bool,
    game_cycle_ms: int,
    zone: string,
    floor_section: string
)

// Create JSON ingestion mapping
.create table SlotEvents ingestion json mapping 'SlotEventsMapping'
    '[{"column":"event_id","path":"$.event_id"},'
    ' {"column":"machine_id","path":"$.machine_id"},'
    ' {"column":"timestamp","path":"$.timestamp"},'
    ' {"column":"denomination","path":"$.denomination"},'
    ' {"column":"game_type","path":"$.game_type"},'
    ' {"column":"credits_wagered","path":"$.credits_wagered"},'
    ' {"column":"coin_in","path":"$.coin_in"},'
    ' {"column":"coin_out","path":"$.coin_out"},'
    ' {"column":"jackpot_flag","path":"$.jackpot_flag"},'
    ' {"column":"game_cycle_ms","path":"$.game_cycle_ms"},'
    ' {"column":"zone","path":"$.zone"},'
    ' {"column":"floor_section","path":"$.floor_section"}]'

// Connect Event Hub data connection via Azure portal or ARM template

Step 2: Player Engagement Scoring and Lifecycle Tracking

The Silver layer builds a unified player 360 profile by joining gaming sessions, F&B activity, hotel stays, and loyalty data.

dbt Model: Player 360 Silver (models/silver/slv_player_360.sql)

-- models/silver/slv_player_360.sql
-- Unified player profile combining gaming, F&B, hotel, and loyalty data

{{ config(
    materialized='incremental',
    unique_key='player_id',
    schema='silver'
) }}

with gaming_summary as (
    select
        player_id,
        count(distinct session_id)          as total_sessions,
        sum(coin_in)                        as lifetime_coin_in,
        sum(coin_out)                       as lifetime_coin_out,
        sum(theo_win)                       as lifetime_theo_win,
        sum(actual_win)                     as lifetime_actual_win,
        avg(session_duration_min)           as avg_session_duration_min,
        max(session_end_ts)                 as last_gaming_activity,
        min(session_start_ts)              as first_gaming_activity,
        count(distinct machine_id)          as unique_machines_played,
        mode() within group (order by floor_section)  as preferred_floor_section,
        mode() within group (order by denomination)   as preferred_denomination
    from {{ ref('brz_player_sessions') }}
    group by player_id
),

fb_summary as (
    select
        player_id,
        count(distinct check_id)    as total_fb_visits,
        sum(total)                  as lifetime_fb_spend,
        sum(comp_amount)            as lifetime_fb_comps,
        mode() within group (order by outlet)  as favorite_outlet,
        max(transaction_ts)         as last_fb_visit
    from {{ ref('brz_fb_transactions') }}
    where player_id is not null
    group by player_id
),

hotel_summary as (
    select
        player_id,
        count(distinct reservation_id)  as total_stays,
        sum(nights)                     as lifetime_room_nights,
        sum(case when comp_flag then 1 else 0 end)  as comp_room_nights,
        avg(rate)                       as avg_daily_rate,
        max(checkout_date)              as last_hotel_stay
    from {{ ref('brz_hotel_reservations') }}
    where player_id is not null
    group by player_id
),

loyalty as (
    select
        player_id,
        tier,
        points_balance,
        signup_date,
        lifetime_theo    as loyalty_lifetime_theo,
        lifetime_actual  as loyalty_lifetime_actual
    from {{ ref('brz_loyalty_members') }}
)

select
    coalesce(g.player_id, f.player_id, h.player_id, l.player_id)  as player_id,
    l.tier                          as loyalty_tier,
    l.points_balance,
    l.signup_date,

    -- Gaming metrics
    g.total_sessions,
    g.lifetime_coin_in,
    g.lifetime_coin_out,
    g.lifetime_theo_win,
    g.lifetime_actual_win,
    g.avg_session_duration_min,
    g.preferred_floor_section,
    g.preferred_denomination,
    g.unique_machines_played,
    g.first_gaming_activity,
    g.last_gaming_activity,

    -- F&B metrics
    f.total_fb_visits,
    f.lifetime_fb_spend,
    f.lifetime_fb_comps,
    f.favorite_outlet,
    f.last_fb_visit,

    -- Hotel metrics
    h.total_stays,
    h.lifetime_room_nights,
    h.comp_room_nights,
    h.avg_daily_rate,
    h.last_hotel_stay,

    -- Derived: days since last visit (any touchpoint)
    datediff(
        day,
        greatest(
            coalesce(g.last_gaming_activity, '1900-01-01'),
            coalesce(f.last_fb_visit, '1900-01-01'),
            coalesce(h.last_hotel_stay, '1900-01-01')
        ),
        current_date
    ) as days_since_last_visit,

    -- Derived: engagement score (0-100)
    least(100, (
        coalesce(g.total_sessions, 0) * 0.3
        + coalesce(f.total_fb_visits, 0) * 2.0
        + coalesce(h.total_stays, 0) * 5.0
        + case when l.tier = 'Platinum' then 20
               when l.tier = 'Gold' then 15
               when l.tier = 'Silver' then 10
               else 5 end
    )) as engagement_score,

    current_timestamp as _loaded_at

from gaming_summary g
full outer join fb_summary f on g.player_id = f.player_id
full outer join hotel_summary h on g.player_id = h.player_id
full outer join loyalty l on g.player_id = l.player_id

Step 3: Floor Layout Optimization with Heat Mapping

KQL queries against the ADX real-time store produce floor performance metrics for heat map visualizations. Zone-level coin-in, hold percentages, and machine utilization drive layout decisions.

KQL: Real-Time Floor Performance

// Floor performance by zone — rolling 15-minute windows
SlotEvents
| where timestamp > ago(15m)
| summarize
    total_coin_in = sum(coin_in),
    total_coin_out = sum(coin_out),
    hold_pct = round((sum(coin_in) - sum(coin_out)) / sum(coin_in) * 100, 2),
    active_machines = dcount(machine_id),
    total_spins = count(),
    jackpot_count = countif(jackpot_flag),
    avg_cycle_ms = avg(game_cycle_ms)
    by zone, floor_section
| extend revenue = total_coin_in - total_coin_out
| order by revenue desc

// Machine-level utilization for repositioning analysis
SlotEvents
| where timestamp > ago(1h)
| summarize
    spins = count(),
    coin_in = sum(coin_in),
    hold = round((sum(coin_in) - sum(coin_out)) / sum(coin_in) * 100, 2),
    idle_pct = round(
        (3600 - sum(game_cycle_ms) / 1000.0) / 3600 * 100, 2
    )
    by machine_id, zone, denomination
| where idle_pct > 80
| order by idle_pct desc
| project machine_id, zone, denomination, spins, coin_in, hold, idle_pct

Step 4: F&B Preference Analysis and Comp Optimization

Comp ROI analysis links complimentary F&B and hotel spending back to incremental gaming revenue, enabling data-driven reinvestment decisions.

-- Gold layer: Comp ROI by loyalty tier
-- models/gold/gld_comp_roi_analysis.sql

{{ config(materialized='table', schema='gold') }}

select
    p.loyalty_tier,
    count(distinct p.player_id)                         as player_count,
    round(avg(p.lifetime_theo_win), 2)                  as avg_theo_win,
    round(sum(p.lifetime_fb_comps +
        p.comp_room_nights * p.avg_daily_rate), 2)      as total_comp_cost,
    round(sum(p.lifetime_coin_in - p.lifetime_coin_out), 2)  as total_gaming_revenue,
    round(
        sum(p.lifetime_coin_in - p.lifetime_coin_out) /
        nullif(sum(p.lifetime_fb_comps +
            p.comp_room_nights * p.avg_daily_rate), 0),
        2
    )                                                    as comp_roi_ratio,
    round(avg(p.engagement_score), 1)                   as avg_engagement_score,
    round(avg(p.days_since_last_visit), 0)              as avg_days_since_visit
from {{ ref('slv_player_360') }} p
group by p.loyalty_tier
order by comp_roi_ratio desc

Step 5: Title 31 AML Compliance Monitoring

The Bank Secrecy Act (Title 31) requires casinos to file Currency Transaction Reports (CTRs) for cash transactions exceeding $10,000 in a gaming day and Suspicious Activity Reports (SARs) for structuring patterns.

SQL: AML Threshold Monitoring

-- models/gold/gld_aml_ctr_candidates.sql
-- Identify CTR-reportable transactions: aggregate cash activity > $10,000 per gaming day

{{ config(materialized='incremental', schema='gold', unique_key='player_gaming_day') }}

with daily_cash_activity as (
    select
        s.player_id,
        cast(s.session_start_ts as date)      as gaming_day,
        sum(s.cash_buy_in)                    as total_buy_in,
        sum(s.cash_out)                       as total_cash_out,
        sum(s.cash_buy_in) + sum(s.cash_out)  as total_cash_activity,
        count(distinct s.session_id)          as session_count,
        array_agg(distinct s.machine_id)      as machines_used
    from {{ ref('brz_player_sessions') }} s
    group by s.player_id, cast(s.session_start_ts as date)
),

structuring_detection as (
    -- Flag transactions just below $10,000 threshold (potential structuring)
    select
        player_id,
        gaming_day,
        total_cash_activity,
        session_count,
        case
            when total_cash_activity between 8000 and 9999
                 and session_count >= 3
            then true
            else false
        end as structuring_flag
    from daily_cash_activity
)

select
    d.player_id || '-' || d.gaming_day  as player_gaming_day,
    d.player_id,
    d.gaming_day,
    d.total_buy_in,
    d.total_cash_out,
    d.total_cash_activity,
    d.session_count,
    d.machines_used,
    case
        when d.total_cash_activity >= 10000 then 'CTR_REQUIRED'
        when s.structuring_flag then 'SAR_REVIEW'
        else 'BELOW_THRESHOLD'
    end as compliance_action,
    current_timestamp as _evaluated_at
from daily_cash_activity d
left join structuring_detection s
    on d.player_id = s.player_id and d.gaming_day = s.gaming_day
where d.total_cash_activity >= 8000
   or s.structuring_flag

{% if is_incremental() %}
    and d.gaming_day > (select max(gaming_day) from {{ this }})
{% endif %}

Step 6: NIGC Regulatory Reporting

The National Indian Gaming Commission (NIGC) requires periodic reporting on gaming revenue, machine counts, and operational metrics. Gold-layer aggregations feed standardized report outputs.

-- models/gold/gld_nigc_revenue_report.sql
-- Monthly revenue summary formatted for NIGC regulatory submission

{{ config(materialized='table', schema='gold') }}

select
    date_trunc('month', gaming_day)          as report_month,
    game_category,
    count(distinct machine_id)               as machine_count,
    sum(coin_in)                             as total_coin_in,
    sum(coin_out)                            as total_coin_out,
    sum(coin_in) - sum(coin_out)             as gross_gaming_revenue,
    round((sum(coin_in) - sum(coin_out)) / nullif(sum(coin_in), 0) * 100, 2)
                                              as hold_percentage,
    sum(jackpot_amount)                      as total_jackpots_paid,
    count(distinct player_id)                as unique_players
from {{ ref('slv_gaming_events_daily') }}
group by date_trunc('month', gaming_day), game_category
order by report_month desc, game_category

Step 7: Floor Density Analytics with Azure Video Analyzer

Azure Video Analyzer with Custom Vision models processes floor camera feeds to estimate zone-level occupancy and density, enabling real-time staffing and safety decisions.

"""Query Azure Video Analyzer for floor density metrics."""

from azure.media.videoanalyzer import VideoAnalyzerClient
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
client = VideoAnalyzerClient(
    account_name="<video-analyzer-account>",
    resource_group="<resource-group>",
    subscription_id="<subscription-id>",
    credential=credential,
)

# Floor density is computed by Custom Vision person-detection model
# deployed as a spatial analysis pipeline on each camera feed.
# Results are emitted to Event Hub and queryable in ADX:

FLOOR_DENSITY_KQL = """
FloorDensityEvents
| where timestamp > ago(30m)
| summarize
    avg_person_count = avg(person_count),
    max_person_count = max(person_count),
    avg_density_pct = round(avg(density_pct), 1)
    by zone, bin(timestamp, 5m)
| order by timestamp desc, zone asc
"""

Regulatory Compliance Reference

Regulation Authority Requirement Implementation
Title 31 / BSA FinCEN CTR filing for cash transactions ≥ $10,000 per gaming day gld_aml_ctr_candidates model with daily aggregation
Title 31 / BSA FinCEN SAR filing for suspicious structuring patterns Structuring detection logic (multiple transactions near threshold)
NIGC Regulations NIGC Monthly/quarterly revenue reporting by game category gld_nigc_revenue_report model
NIGC MICS NIGC Minimum Internal Control Standards for slot/table operations Audit trail via Bronze-layer raw event retention
Tribal-State Compact State Gaming Commission Compact-specific revenue sharing and reporting Configurable per-compact reporting templates

Tribal Sovereignty

Tribal gaming operations are regulated under the Indian Gaming Regulatory Act (IGRA). Specific reporting requirements vary by tribal-state compact. The models provided here implement common Title 31 and NIGC requirements; compact-specific customizations should be configured per deployment.


Power BI Dashboard Layers

Dashboard Refresh Key Metrics Audience
Real-Time Floor Monitor Sub-minute (ADX Direct Query) Active machines, coin-in/out by zone, jackpot alerts Floor managers, surveillance
Player Value & Segmentation Daily Player 360 scores, tier distribution, churn risk Marketing, player development
Comp ROI Analysis Weekly Comp cost vs. incremental revenue by tier Finance, marketing directors
AML Compliance Daily CTR candidates, structuring alerts, SAR queue Compliance officers
NIGC Revenue Summary Monthly Gross gaming revenue, hold %, machine counts Tribal gaming commission, NIGC
Floor Density Near real-time (ADX) Zone occupancy, peak hours, staffing recommendations Operations, security

Cross-References


Deployment

# Deploy infrastructure
cd examples/casino-analytics/deploy
az deployment group create \
    --resource-group rg-casino-analytics \
    --template-file main.bicep \
    --parameters @parameters.json

# Start synthetic data producers
cd ../streaming
python slot_event_producer.py &
python table_event_producer.py &

# Run dbt transformations
cd ../
dbt run --project-dir . --profiles-dir . --target dev
dbt test --project-dir . --profiles-dir . --target dev

Sources