Skip to content
>GLB_
Go back

Building a data quality dashboard on top of Tuva's DQI mart

After running the Tuva Project on 167k synthetic Medicare claims, the DQI (Data Quality Indicator) mart is one of the first places worth looking. It tells you not whether your pipeline ran, but whether the data you fed into the pipeline is actually usable.

What the DQI mart contains

The DQI mart is structured around several types of output:

Fill rate tables — for each field in the input tables (medical claims, eligibility), the DQI mart calculates what percentage of rows have a non-null, non-empty value. Fields like claim_id, patient_id, and claim_start_date should be at 100%. Fields like attending_provider_npi or discharge_disposition_code are often partially populated depending on claim type.

Claim type distribution — breakdown of claims by claim_type (institutional, professional) and sub-type. This catches common data issues like all claims landing in a single claim type due to a bad mapping in the source extract.

Date range checks — validates that service dates fall within eligibility periods, that claim end dates are after start dates, and that dates aren’t unreasonably far in the past or future.

Code validity flags — checks whether diagnosis codes, procedure codes, and revenue codes appear in the relevant reference tables. A high invalid-code rate usually means the code format is wrong (codes without decimal points when they should have them, or ICD-9 codes in an ICD-10 field).

The Streamlit page structure

The data quality page is one of four pages in the app, using the st.navigation() API:

# app.py
import streamlit as st

pg = st.navigation([
    st.Page("tuva_explorer.py", title="Tuva Explorer"),
    st.Page("data_quality_explorer.py", title="Data Quality"),
    st.Page("predictions.py", title="Risk Predictions"),
])
pg.run()

The data_quality_explorer.py page connects to DuckDB in read-only mode:

import duckdb
import streamlit as st
import pandas as pd

@st.cache_data
def load_fill_rates():
    con = duckdb.connect("tuva.duckdb", read_only=True)
    df = con.execute("""
        SELECT
            source_table,
            field_name,
            fill_rate,
            total_count,
            null_count
        FROM dqi.field_level_fill_rates
        ORDER BY fill_rate ASC
    """).df()
    con.close()
    return df

Visualizing fill rates

The most useful visualization is a horizontal bar chart grouped by source table, with low fill rates highlighted:

import plotly.express as px

df = load_fill_rates()
df["flag"] = df["fill_rate"].apply(lambda x: "Low" if x < 0.8 else "OK")

fig = px.bar(
    df,
    x="fill_rate",
    y="field_name",
    color="flag",
    color_discrete_map={"Low": "#e74c3c", "OK": "#2ecc71"},
    orientation="h",
    facet_col="source_table",
    title="Field-level fill rates by source table",
)
st.plotly_chart(fig, use_container_width=True)

On 167k claims with clean synthetic data, most fields are at or near 100%. In real-world Medicare claims, you typically see lower fill rates on:

The key distinction is “low fill rate because the field doesn’t apply to this claim type” versus “low fill rate because the data is actually missing.” The DQI mart doesn’t always make this distinction automatically — that interpretation requires domain knowledge.

Claim type distribution

A pie or bar chart of claim counts by claim_type is a quick sanity check:

@st.cache_data
def load_claim_type_dist():
    con = duckdb.connect("tuva.duckdb", read_only=True)
    return con.execute("""
        SELECT claim_type, COUNT(*) as claim_count
        FROM core__medical_claim
        GROUP BY claim_type
    """).df()

dist_df = load_claim_type_dist()
st.bar_chart(dist_df.set_index("claim_type"))

Medicare FFS should be roughly 60% professional and 40% institutional by claim count. If everything lands in one category, something is wrong with the claim_type mapping in the source data.

What to look at first

When opening a new dataset in the DQI dashboard, the useful inspection order is:

  1. Claim type distribution — make sure the mix looks reasonable
  2. Fill rates on identity fields (patient_id, claim_id, line_number) — these must be 100% or records can’t be linked
  3. Date validity — service dates outside enrollment periods indicate eligibility data gaps or a join problem upstream
  4. Code validity — invalid ICD-10 or CPT codes usually point to format issues in the source extract

On the synthetic dataset, all of these pass. On real payer data, steps 3 and 4 typically surface issues that require investigation before the downstream marts produce meaningful results.


Share this post:

Related Posts


Previous Post
improve-codebase-architecture: review that knows your domain
Next Post
Building a multipage Streamlit app with st.navigation() — the modern way