Mathew K Analytics

Lesson 15 · Market Research Analytics in Python

Assessing Data Quality in Market Research

In this lesson, we will solve a real-world problem: How do we assess and improve data quality in customer surveys and market research datasets? Many…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Assessing Data Quality in Market Research#

  • In this lesson, we will solve a real-world problem: How do we assess and improve data quality in customer surveys and market research datasets?
  • Many business decisions rely on survey or customer data; poor data quality can lead to wrong conclusions.
  • You will learn how to explore, diagnose, and handle common data quality issues using Python.
  • By the end, you will produce actionable insights by ensuring your data is reliable before analysis.
import warnings
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import openml
warnings.filterwarnings('ignore')

What is market research data, and why does quality matter?#

  • Market research data comes from customer surveys, feedback, sales, or marketing experiments.
  • Most datasets include multiple columns: respondent demographics, product ratings, open-ended feedback, or behavioral signals.
  • Beginners often forget to check for mistakes like missing data, inconsistent codes, or survey fatigue, which can lead to misleading analysis.
  • Good data quality is the foundation for accurate, actionable business insights.
# Beginner: Load a real customer satisfaction survey dataset
dataset = openml.datasets.get_dataset(42178)
df = dataset.get_data(dataset_format='dataframe')[0]
print(df.shape)
print(df.head(3))
(7043, 20)
   gender  SeniorCitizen Partner Dependents  tenure PhoneService  \
0  Female              0     Yes         No       1           No   
1    Male              0      No         No      34          Yes   
2    Male              0      No         No       2          Yes   

      MultipleLines InternetService OnlineSecurity OnlineBackup  \
0  No phone service             DSL             No          Yes   
1                No             DSL            Yes           No   
2                No             DSL            Yes          Yes   

  DeviceProtection TechSupport StreamingTV StreamingMovies        Contract  \
0               No          No          No              No  Month-to-month   
1              Yes          No          No              No        One year   
2               No          No          No              No  Month-to-month   

  PaperlessBilling     PaymentMethod  MonthlyCharges TotalCharges Churn  
0              Yes  Electronic check           29.85        29.85    No  
1               No      Mailed check           56.95       1889.5    No  
2              Yes      Mailed check           53.85       108.15   Yes  

Beginner: Which columns and customer features are in the data?#

  • The customer satisfaction survey includes demographic and service columns such as gender, SeniorCitizen, tenure, PaymentMethod, and Churn.
  • Each row represents a unique customer record; columns represent survey responses or customer attributes.
  • Identifying the variables in your data is the first step to understanding data quality.
# Beginner: Check for missing values in the survey data
missing_counts = df.isnull().sum()
print(missing_counts[missing_counts > 0])
Series([], dtype: int64)
# Beginner: Count unique values in Churn column
print('Unique churn responses:', df['Churn'].unique())
Unique churn responses: ['No' 'Yes']
# Check and visualize missing values safely
missing = df.isnull().sum().sort_values(ascending=False)
missing_nonzero = missing[missing > 0]
if len(missing_nonzero) > 0:
    plt.figure(figsize=(10,4))
    missing_nonzero.plot(kind='bar', color='salmon')
    plt.title('Missing Value Count by Column')
    plt.xlabel('Column')
    plt.ylabel('Count')
    plt.show()
else:
    print('No missing values found in the dataset.')
No missing values found in the dataset.
# Beginner: Load a marketing campaign dataset
dataset = openml.datasets.get_dataset(1461)
df_marketing, _, _, _ = dataset.get_data(dataset_format='dataframe')
df_marketing.columns = ['age','job','marital','education','default','balance','housing','loan','contact','day','month','duration','campaign','pdays','previous','poutcome','response']
print(df_marketing.shape)
print(df_marketing.head(3))
(45211, 17)
   age           job  marital  education default  balance housing loan  \
0   58    management  married   tertiary      no   2143.0     yes   no   
1   44    technician   single  secondary      no     29.0     yes   no   
2   33  entrepreneur  married  secondary      no      2.0     yes  yes   

   contact  day month  duration  campaign  pdays  previous poutcome response  
0  unknown    5   may     261.0         1   -1.0       0.0  unknown        1  
1  unknown    5   may     151.0         1   -1.0       0.0  unknown        1  
2  unknown    5   may      76.0         1   -1.0       0.0  unknown        1  
# Intermediate: Look for duplicate records in the marketing dataset
duplicates = df_marketing.duplicated().sum()
print(f'Duplicate records found: {duplicates}')
Duplicate records found: 0
# Intermediate: Assess data type consistency in each column
print(df_marketing.dtypes)
age             uint8
job          category
marital      category
education    category
default      category
balance       float64
housing      category
loan         category
contact      category
day             uint8
month        category
duration      float64
campaign        uint8
pdays         float64
previous      float64
poutcome     category
response     category
dtype: object
# Intermediate: Frequency of each unique value in the 'response' column
response_counts = df_marketing['response'].value_counts(dropna=False)
print(response_counts)
response
1    39922
2     5289
Name: count, dtype: int64
# Intermediate: Spot potential outliers in 'age' and 'balance' fields
print('Age range:', df_marketing['age'].min(), '-', df_marketing['age'].max())
print('Balance range:', df_marketing['balance'].min(), '-', df_marketing['balance'].max())
Age range: 18 - 95
Balance range: -8019.0 - 102127.0
# Intermediate: Visualize 'age' distribution to spot unusual spikes or cuts
plt.figure(figsize=(8,4))
df_marketing['age'].hist(bins=30, color='skyblue', edgecolor='black')
plt.title('Customer Age Distribution')
plt.xlabel('Age')
plt.ylabel('Count')
plt.show()
No description has been provided for this image
# Advanced: Explore data quality in open feedback text
df_feedback = pd.DataFrame({'CustomerID':[1,2,3,4,5], 'Feedback':['Great service and friendly staff','Delivery was slow and packaging was poor','Excellent quality, will buy again','Customer support needs improvement','Good value for money']})
print(df_feedback.head(3))
print('Feedback length:', df_feedback['Feedback'].apply(len))
   CustomerID                                  Feedback
0           1          Great service and friendly staff
1           2  Delivery was slow and packaging was poor
2           3         Excellent quality, will buy again
Feedback length: 0    32
1    40
2    33
3    34
4    20
Name: Feedback, dtype: int64
# Advanced: Check for non-English or gibberish feedback (simple heuristic)
def is_english(text):
    try:
        text.encode(encoding='utf-8').decode('ascii')
    except UnicodeDecodeError:
        return False
    return True
non_english = df_feedback[~df_feedback['Feedback'].apply(is_english)]
print('Non-English or gibberish feedback:')
print(non_english)
Non-English or gibberish feedback:
Empty DataFrame
Columns: [CustomerID, Feedback]
Index: []
# Advanced: Load Net Promoter Score (NPS) survey and check for invalid scores
np.random.seed(42)
df_nps = pd.DataFrame({'CustomerID': range(1,501), 'Age': np.random.randint(18,70,500), 'Region': np.random.choice(['North','South','East','West'],500), 'NPS_Score': np.random.randint(0,11,500)})
print(df_nps.head(3))
invalid_nps = df_nps[~df_nps['NPS_Score'].between(0,10)]
print('Invalid NPS records:', invalid_nps.shape[0])
   CustomerID  Age Region  NPS_Score
0           1   56   West          2
1           2   69  North          0
2           3   46   East          4
Invalid NPS records: 0
# Advanced: Visualize NPS score distribution to spot survey response artifacts
plt.figure(figsize=(7,3))
df_nps['NPS_Score'].hist(bins=11, color='green', rwidth=0.8)
plt.xticks(range(0,11))
plt.title('NPS Survey Score Distribution')
plt.xlabel('NPS Score')
plt.ylabel('Number of Responses')
plt.show()
No description has been provided for this image
# Error Handling: Simulate missing survey responses in key columns
df_missing = df.copy()
df_missing.loc[df_missing.sample(frac=0.1, random_state=42).index, 'MonthlyCharges'] = np.nan
print('Missing MonthlyCharges:', df_missing['MonthlyCharges'].isnull().sum())
Missing MonthlyCharges: 704
# Error Handling: Attempt incorrect grouping on a continuous variable
try:
    df.groupby('MonthlyCharges').size()
except Exception as e:
    print('Grouping error:', e)
# Error Handling: Misinterpret Likert or NPS scales (e.g., average as a metric)
mean_nps = df_nps['NPS_Score'].mean()
print('Average NPS (should not be used as standard NPS):', mean_nps)
Average NPS (should not be used as standard NPS): 4.89

Best Practices: Segmentation, cross-tabs, index construction, trend analysis#

  • Segmenting by customer age, tenure, or region uncovers actionable subgroups.
  • Cross-tabulation helps reveal relationships between two categorical fields, such as churn and contract type.
  • Building custom indexes (such as satisfaction scores) and running trends over time is critical for decision support.
  • Always start with data quality before segmentation, as bad data can hide or create fake patterns!
# Pattern: Segment NPS by Region
nps_by_region = df_nps.groupby('Region')['NPS_Score'].mean()
print(nps_by_region)
Region
East     4.504425
North    5.214765
South    4.719008
West     5.025641
Name: NPS_Score, dtype: float64
# Pattern: Build a cross-tab between Churn and Contract type
churn_contract = pd.crosstab(df['Churn'], df['Contract'])
print(churn_contract)
Contract  Month-to-month  One year  Two year
Churn                                       
No                  2220      1307      1647
Yes                 1655       166        48
# Pattern: Build a custom satisfaction index from multiple fields
for col in ['OnlineSecurity', 'OnlineBackup', 'TechSupport']:
    df[col] = df[col].map({'Yes':1, 'No':0})
df['Satisfaction_Index'] = df[['OnlineSecurity','OnlineBackup','TechSupport']].mean(axis=1)
print(df['Satisfaction_Index'].head(5))
0    0.333333
1    0.333333
2    0.666667
3    0.666667
4    0.000000
Name: Satisfaction_Index, dtype: float64
# Pattern: Investigate churn trends over customer tenure
churned_by_tenure = df.groupby('tenure')['Churn'].value_counts(normalize=True).unstack().fillna(0)
churned_by_tenure.plot(kind='line', figsize=(10,4))
plt.ylabel('Proportion of Churn')
plt.title('Churn Proportion by Customer Tenure')
plt.show()
No description has been provided for this image
# End-to-end: From raw survey data to usable business insight
# Step 1: Load customer satisfaction data
df_end = df.copy()
# Step 2: Clean Churn column (remove whitespaces, fix typos if any)
df_end['Churn'] = df_end['Churn'].str.strip().str.title()
# Step 3: Impute missing MonthlyCharges using median
df_end['MonthlyCharges'] = df_end['MonthlyCharges'].fillna(df_end['MonthlyCharges'].median())
# Step 4: Compute the churn rate
churn_rate = (df_end['Churn'] == 'Yes').mean()
print(f'Cleaned dataset churn rate: {churn_rate:.2%}')
Cleaned dataset churn rate: 26.54%
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.