AI & Machine Learning

Data Quality Checks Every Data Scientist Should Know

Python Rust Data Science Statistics Feature Engineering
1,429 words Includes Code

Data Quality Checks Every Data Scientist Should Know

Key Takeaway: Data quality is not optional — it is the foundation of every reliable analysis and model. Seven essential checks (missing, duplicates, outliers, schema, types, range, uniqueness) catch problems before they corrupt your results. Build a validator, run it on every dataset, and make quality a habit.

A model trained on data with 500 null values, 20 duplicates, and 15 outliers will produce unreliable results. The problem is not the algorithm — it is the data. And data quality problems are invisible until you look for them.

This article covers the seven data quality checks every data scientist should run, explains what each catches, and provides a complete Python validator you can use immediately.

Featured image: articles/084/featured-image.svg

Quality checks matrix: articles/084/quality-checks-matrix.svg

The Seven Essential Checks

#CheckWhat It CatchesSeverity
1Missingnull / NaN / empty valuescritical
2Duplicatesidentical rows, key collisionserror
3Outliersextreme values, anomalieswarning
4Schemamissing / extra columnscritical
5Typeswrong dtype, mixed typeserror
6Rangevalues outside expected boundserror
7Uniquenessduplicate primary keyscritical

Let's examine each one.

1. Missing Values

Null values are the most common data quality problem. They cause NaN propagation, silent type coercion, and model failures.

def check_missing(df, required_columns=None):
    """Check for null/NaN values."""
    total_nulls = df.isna().sum().sum()
    col_nulls = df.isna().sum()
    cols_with_nulls = col_nulls[col_nulls > 0]
    
    if required_columns:
        missing_required = {c: df[c].isna().sum() 
                          for c in required_columns if df[c].isna().sum() > 0}
        if missing_required:
            return {'passed': False, 'critical': True,
                    'message': f'Required columns have nulls: {missing_required}'}
    
    if total_nulls == 0:
        return {'passed': True, 'message': 'No null values'}
    
    return {'passed': False, 'message': 
            f'{total_nulls} nulls across {len(cols_with_nulls)} columns'}

Why it matters:

  • df.mean() silently ignores NaN — your mean is wrong
  • df.merge() produces unexpected row counts with null keys
  • ML models either crash or silently ignore rows with NaN

2. Duplicates

Duplicate rows bias statistics, inflate counts, and create false patterns.

def check_duplicates(df, subset=None):
    """Check for duplicate rows."""
    if subset:
        dupes = df.duplicated(subset=subset).sum()
        label = f' (subset: {subset})'
    else:
        dupes = df.duplicated().sum()
        label = ''
    
    if dupes == 0:
        return {'passed': True, 'message': 'No duplicates'}
    
    return {'passed': False, 'message': 
            f'{dupes} duplicate rows{label}'}

Types of duplicates:

TypeDetectionImpact
Exact row copiesdf.duplicated()Inflated statistics
Key collisionsdf.duplicated(subset=['id'])Join explosions
Near-duplicatesFuzzy matchingPhantom records

3. Outliers

Outliers are extreme values that distort statistics and models. Detection depends on context — what is an outlier in one dataset is normal in another.

def check_outliers(df, columns, method='iqr', threshold=1.5):
    """Detect outliers using IQR method."""
    outlier_report = {}
    for col in columns:
        series = df[col].dropna()
        q1, q3 = series.quantile(0.25), series.quantile(0.75)
        iqr = q3 - q1
        lower, upper = q1 - threshold * iqr, q3 + threshold * iqr
        outliers = ((series < lower) | (series > upper)).sum()
        if outliers > 0:
            outlier_report[col] = outliers
    return outlier_report

Two detection methods:

MethodHow It WorksBest For
IQR1.5x interquartile rangeMost numerical data
Z-score3 standard deviationsNormally distributed data

Important: do not automatically remove outliers. Investigate them first — they may be real, important data points.

4. Schema Validation

Schema checks verify that expected columns exist. This catches upstream pipeline changes, file format mismatches, and API changes.

def check_schema(df, expected_columns):
    """Validate column existence."""
    missing = [c for c in expected_columns if c not in df.columns]
    extra = [c for c in df.columns if c not in expected_columns]
    
    if missing:
        return {'passed': False, 'critical': True,
                'message': f'Missing columns: {missing}'}
    if extra:
        return {'passed': True, 'warning': f'Extra columns: {extra}'}
    return {'passed': True, 'message': 'All expected columns present'}

Schema validation is a critical check — missing columns cause immediate crashes downstream.

5. Type Validation

Wrong data types cause silent failures. A numeric column loaded as string, or a date loaded as object, breaks downstream operations.

def check_types(df, expected_types):
    """Validate column data types."""
    mismatches = {}
    for col, expected in expected_types.items():
        if col not in df.columns:
            continue
        actual = str(df[col].dtype)
        if expected.lower() not in actual.lower():
            mismatches[col] = {'expected': expected, 'actual': actual}
    return mismatches

Common type problems:

ProblemSymptomFix
Number loaded as stringmean() failspd.to_numeric()
Date loaded as objectDate operations failpd.to_datetime()
Integer loaded as floatNaN in columnHandle nulls first
Mixed types in columnUnexpected dtypeInvestigate source

6. Range Validation

Range checks verify that values fall within expected bounds. A negative quantity, a price of -100, or an age of 500 are range violations.

def check_range(df, ranges):
    """Validate values within expected ranges."""
    issues = {}
    for col, (low, high) in ranges.items():
        if col not in df.columns:
            continue
        series = df[col].dropna()
        below = (series < low).sum()
        above = (series > high).sum()
        if below > 0 or above > 0:
            issues[col] = {'below': below, 'above': above}
    return issues

Define ranges based on domain knowledge:

FieldExpected RangeWhy
quantity[1, 10000]Cannot order 0 or negative items
unit_price[0.01, 100000]Prices must be positive
age[0, 120]Human age bounds
percentage[0, 100]Cannot exceed 100%

7. Uniqueness Validation

Uniqueness checks verify that primary key columns contain unique values. Duplicate keys break joins, cause row explosions, and corrupt aggregations.

def check_uniqueness(df, columns):
    """Validate primary key uniqueness."""
    issues = {}
    for col in columns:
        if col not in df.columns:
            continue
        total = len(df)
        unique = df[col].nunique()
        dupes = total - unique
        if dupes > 0:
            issues[col] = dupes
    return issues

The test is simple: nunique() should equal len() for primary key columns.

Running All 7 Checks

Here is the complete validator class that runs all checks and generates a quality report:

class DataQualityValidator:
    def __init__(self, df, name='dataset'):
        self.df = df
        self.name = name
        self.results = []
    
    def check_missing(self, required_columns=None):
        # ... (see full code above)
        pass
    
    def check_duplicates(self, subset=None):
        # ... (see full code above)
        pass
    
    def check_outliers(self, columns=None, method='iqr'):
        # ... (see full code above)
        pass
    
    def check_schema(self, expected_columns):
        # ... (see full code above)
        pass
    
    def check_types(self, expected_types):
        # ... (see full code above)
        pass
    
    def check_range(self, ranges):
        # ... (see full code above)
        pass
    
    def check_uniqueness(self, columns):
        # ... (see full code above)
        pass
    
    def summary(self):
        total = len(self.results)
        passed = sum(1 for r in self.results if r['passed'])
        score = (passed / total * 100) if total > 0 else 0
        grade = 'A' if score >= 90 else 'B' if score >= 80 else 'C' if score >= 70 else 'F'
        
        print(f'Quality score: {score:.0f}% (Grade: {grade})')
        return {'score': score, 'grade': grade, 'passed': passed, 'total': total}

Real Validator Output

When we ran the validator on 5,001 synthetic sales rows with injected issues:

CheckResultDetails
MISSINGFAIL490 nulls in required column 'product'
DUPLICATESFAIL5 rows with duplicate customer+date
OUTLIERSFAILquantity=99999 detected via IQR
SCHEMAPASSAll 9 expected columns present
TYPESFAILorder_date: expected object, got str
RANGEFAILquantity: 1 below, 1 above [1, 1000]
UNIQUENESSFAILorder_id: 1 duplicate

Quality Score: 12% (Grade: F) — 2 critical, 2 error, 3 warning. Total validation time: 19.0 ms.

When to Run Each Check

CheckWhen to RunFrequency
MissingAfter every extractionEvery run
DuplicatesAfter extraction + after joinsEvery run
OutliersBefore model trainingEach training cycle
SchemaFirst time + when sources changeOn schema change
TypesAfter every extractionEvery run
RangeAfter transformationEvery run
UniquenessBefore joins and aggregationsBefore critical ops

Common Mistakes

  • Ignoring missing values — NaN propagates silently through calculations
  • Auto-removing outliers — investigate before deleting
  • Skipping schema validation — crashes downstream with unhelpful errors
  • Trusting inferred types — always verify dtypes explicitly
  • No quality report — if you don't measure it, you can't improve it

Best Practices

Data Quality Best Practices:
  • Run quality checks before every analysis and model training
  • Set severity levels — know what must stop the pipeline vs. what is a warning
  • Generate a quality report — store it with your outputs
  • Define expected ranges using domain knowledge, not arbitrary numbers
  • Treat quality checks as code — version them, test them, maintain them
  • Log quality metrics over time — track whether data quality is improving or degrading

Further Reading

Conclusion

Data quality is not a one-time task — it is a discipline. Every dataset has problems. The question is whether you find them before they affect your results, or after.

Seven checks cover the most common issues: missing values, duplicates, outliers, schema, types, range, and uniqueness. Running them takes milliseconds. The insights they provide are worth far more.

Try it yourself: Take any CSV you work with, run the seven checks from this article, and generate a quality report. You will almost certainly find issues you did not know existed — and that is the first step to fixing them.

💬 Discuss on BestWordz Community

Join the conversation about Python, Rust, Data Science on the BestWordz Community forum.

Visit Forum →