Mathew K Analytics

Lesson 17 · Market Research Analytics in Python

Cleaning and Standardizing Market Research Data Using Python: Step-by-Step Tutorial

In this lesson, we will focus on preparing real market research data for analysis. Dirty or inconsistent data can lead to the wrong business conclusions.…

⬇ 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

Cleaning and Standardizing Market Research Data#

  • In this lesson, we will focus on preparing real market research data for analysis.
  • Dirty or inconsistent data can lead to the wrong business conclusions.
  • You will learn to spot and fix issues in surveys, customer characteristics, satisfaction scores, and campaign data.
  • Clean data gives accurate insights on retention, satisfaction, and marketing performance.
import pandas as pd
import numpy as np
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding Market Research Data#

  • Market research data includes survey answers, customer traits, satisfaction scores, and open feedback.
  • Responses can be numbers (ratings), text (feedback), or categories (gender, region).
  • Demographic columns help slice insights by customer type.
  • Beginners often assume the data is already clean and make mistakes like using wrong scales, missing hidden null values, or failing to standardize columns.
dataset = openml.datasets.get_dataset(42178)
df_satis, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df_satis.shape)
print(df_satis.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  
df_satis.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 20 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   gender            7043 non-null   object 
 1   SeniorCitizen     7043 non-null   uint8  
 2   Partner           7043 non-null   object 
 3   Dependents        7043 non-null   object 
 4   tenure            7043 non-null   uint8  
 5   PhoneService      7043 non-null   object 
 6   MultipleLines     7043 non-null   object 
 7   InternetService   7043 non-null   object 
 8   OnlineSecurity    7043 non-null   object 
 9   OnlineBackup      7043 non-null   object 
 10  DeviceProtection  7043 non-null   object 
 11  TechSupport       7043 non-null   object 
 12  StreamingTV       7043 non-null   object 
 13  StreamingMovies   7043 non-null   object 
 14  Contract          7043 non-null   object 
 15  PaperlessBilling  7043 non-null   object 
 16  PaymentMethod     7043 non-null   object 
 17  MonthlyCharges    7043 non-null   float64
 18  TotalCharges      7043 non-null   object 
 19  Churn             7043 non-null   object 
dtypes: float64(1), object(17), uint8(2)
memory usage: 1004.3+ KB
print(df_satis['TotalCharges'].sample(10, random_state=42))
185        24.8
2715     996.45
3825     1031.7
1807      76.35
132      3260.1
1263     6127.6
3732     1759.4
1672    5016.65
811     7250.15
2526       19.4
Name: TotalCharges, dtype: object
df_satis['TotalCharges'] = pd.to_numeric(df_satis['TotalCharges'], errors='coerce')
print(df_satis['TotalCharges'].isnull().sum())
11
df_satis['gender'] = df_satis['gender'].str.strip().str.lower()
print(df_satis['gender'].unique())
['female' 'male']
df_satis['SeniorCitizen'] = df_satis['SeniorCitizen'].replace({1: 'Yes', 0: 'No'})
print(df_satis['SeniorCitizen'].value_counts())
SeniorCitizen
No     5901
Yes    1142
Name: count, dtype: int64
dataset = openml.datasets.get_dataset(1461)
df_campaign, _, _, _ = dataset.get_data(dataset_format='dataframe')
df_campaign.columns = ['age','job','marital','education','default','balance','housing','loan','contact','day','month','duration','campaign','pdays','previous','poutcome','response']
print(df_campaign.head(3))
   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  
df_campaign['response'] = df_campaign['response'].str.lower().str.strip()
df_campaign['response'] = df_campaign['response'].replace({'yes':'Yes', 'no':'No'})
print(df_campaign['response'].value_counts())
response
1    39922
2     5289
Name: count, dtype: int64
print(df_campaign['job'].unique())
['management', 'technician', 'entrepreneur', 'blue-collar', 'unknown', ..., 'services', 'self-employed', 'unemployed', 'housemaid', 'student']
Length: 12
Categories (12, object): ['admin.' < 'blue-collar' < 'entrepreneur' < 'housemaid' ... 'student' < 'technician' < 'unemployed' < 'unknown']
df_campaign['job'] = df_campaign['job'].replace({'admin.':'admin','management ':'management'})
print(df_campaign['job'].unique())
['management', 'technician', 'entrepreneur', 'blue-collar', 'unknown', ..., 'services', 'self-employed', 'unemployed', 'housemaid', 'student']
Length: 12
Categories (12, object): ['admin' < 'blue-collar' < 'entrepreneur' < 'housemaid' ... 'student' < 'technician' < 'unemployed' < 'unknown']
df_campaign['balance'] = pd.to_numeric(df_campaign['balance'], errors='coerce')
missing_balance = df_campaign['balance'].isnull().sum()
print(f'Missing or non-numeric balances: {missing_balance}')
Missing or non-numeric balances: 0
n_missing = df_campaign.isnull().sum().sum()
print(f'Total missing entries in campaign data: {n_missing}')
Total missing entries in campaign data: 0
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df_retail = pd.read_excel(url, sheet_name='Year 2010-2011')
df_retail['InvoiceDate'] = pd.to_datetime(df_retail['InvoiceDate'])
print(df_retail.shape)
print(df_retail[['Invoice','StockCode','Description','Quantity','Price']].head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  Price
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   2.55
1  536365     71053                 WHITE METAL LANTERN         6   3.39
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   2.75
df_retail = df_retail.dropna(subset=['Description', 'Price'])
print('Rows after dropping missing Descriptions or Price:', len(df_retail))
Rows after dropping missing Descriptions or Price: 540456
df_retail = df_retail[df_retail['Quantity'] > 0]
df_retail = df_retail[df_retail['Price'] >= 0]
print(f'Shape after filtering for positive quantities and non-negative prices: {df_retail.shape}')
Shape after filtering for positive quantities and non-negative prices: (530692, 8)
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.sample(5, random_state=42))
     CustomerID  Age Region  NPS_Score
361         362   24  South          0
73           74   45   West          2
374         375   26  South          2
155         156   22  South          0
104         105   25   East          1
print(df_nps['NPS_Score'].unique())
[ 0  8  5  9  7  1  2 10  3  6  4]
invalid = df_nps[~df_nps['NPS_Score'].between(0,10)]
print('Number of invalid NPS scores:', len(invalid))
Number of invalid NPS scores: 0
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)
   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
3           4        Customer support needs improvement
4           5                      Good value for money
df_feedback['Feedback'] = df_feedback['Feedback'].str.strip().str.replace(' +', ' ', regex=True).str.lower()
print(df_feedback)
   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
3           4        customer support needs improvement
4           5                      good value for money
df_satis_clean = df_satis.drop_duplicates()
print(df_satis.shape[0] - df_satis_clean.shape[0], 'duplicate rows removed')
22 duplicate rows removed
n_missing = df_nps.isnull().sum().sum()
print(f'Total missing NPS entries: {n_missing}')
Total missing NPS entries: 0
n_invalid = df_nps[~df_nps['NPS_Score'].between(0,10)].shape[0]
mean_nps = df_nps[df_nps['NPS_Score'].between(0,10)]['NPS_Score'].mean()
print(f'Mean NPS (excluding invalid): {mean_nps:.2f}')
Mean NPS (excluding invalid): 5.21

Handling Missing and Mistyped Data#

  • In market research, missing survey answers can distort business recommendations, so always check for and communicate nulls.
  • Mistyped codes (like 'femaale' instead of 'female') cause wrong group totals; fix using string cleaning and mapping.
  • Bad groupings (for example, by unsanitized region) ruin trend or segmentation analysis.
df_satis['gender'] = df_satis['gender'].replace({'femaale':'female','Femle':'female'})
print(df_satis['gender'].value_counts())
gender
male      3555
female    3488
Name: count, dtype: int64

Best Practices: Analytics Patterns#

  • Segment customers or responses before analyzingmarket segments often have unique satisfaction or churn rates.
  • Use cross-tabulation to compare business metrics by two traits (like by contract or by age group).
  • Construct customer indexes or scores after the data is cleannever before.
  • Trend analysis requires clean and date-standardized time fields.
segment_counts = df_satis.groupby('Contract').size()
print(segment_counts)
Contract
Month-to-month    3875
One year          1473
Two year          1695
dtype: int64
cross = pd.crosstab(df_satis['gender'], df_satis['Churn'])
print(cross)
Churn     No  Yes
gender           
female  2549  939
male    2625  930
df_satis['TenureGroup'] = pd.cut(df_satis['tenure'], bins=[0,12,24,36,48,60,72], labels=['<1y','1-2y','2-3y','3-4y','4-5y','5-6y'])
grouped = df_satis.groupby('TenureGroup')['Churn'].value_counts().unstack().fillna(0)
print(grouped)
Churn          No   Yes
TenureGroup            
<1y          1138  1037
1-2y          730   294
2-3y          652   180
3-4y          617   145
4-5y          712   120
5-6y         1314    93
df_satis['MonthlyCharges'] = pd.to_numeric(df_satis['MonthlyCharges'], errors='coerce')
trend = df_satis.groupby('tenure')['MonthlyCharges'].mean()
print(trend.head(10))
tenure
0    41.418182
1    50.485808
2    57.206303
3    58.015000
4    57.432670
5    61.003759
6    56.589091
7    59.642366
8    57.245122
9    62.564706
Name: MonthlyCharges, dtype: float64
df_cohort = pd.DataFrame({'CustomerID': np.random.randint(1000,2000,24),
                         'Signup_Month': pd.date_range('2021-01-01', periods=24, freq='ME'),
                         'Active_Users': np.random.randint(50,300,24)})
print(df_cohort.head(3))
   CustomerID Signup_Month  Active_Users
0        1325   2021-01-31           172
1        1599   2021-02-28           142
2        1503   2021-03-31           203
monthly_active = df_cohort.groupby(df_cohort['Signup_Month'].dt.year).agg({'Active_Users':'mean'})
print(monthly_active)
              Active_Users
Signup_Month              
2021            170.333333
2022            185.250000

End-to-End Example: Cleaning a Satisfaction Dataset#

  • We will walk through cleaning, standardizing, and summarizing a real satisfaction survey dataset.
  • The business goal is to deliver a clean input to the analytics team for NPS and churn analysis.
  • Final insight: What percent of customers are likely to churn, and what is the mean monthly bill after cleaning?
df_satis_final = df_satis.copy()
df_satis_final['TotalCharges'] = pd.to_numeric(df_satis_final['TotalCharges'], errors='coerce')
df_satis_final['MonthlyCharges'] = pd.to_numeric(df_satis_final['MonthlyCharges'], errors='coerce')
df_satis_final = df_satis_final.dropna(subset=['TotalCharges','MonthlyCharges'])
df_satis_final['gender'] = df_satis_final['gender'].str.strip().str.lower().replace({'femaale':'female','Femle':'female'})
df_satis_final['SeniorCitizen'] = df_satis_final['SeniorCitizen'].replace({1:'Yes',0:'No'})
n_churn = df_satis_final[df_satis_final['Churn']=='Yes'].shape[0]
n_total = df_satis_final.shape[0]
mean_bill = df_satis_final['MonthlyCharges'].mean()
print(f'Churn rate: {100*n_churn/n_total:.1f}%, Mean monthly bill: ${mean_bill:.2f}')
Churn rate: 26.6%, Mean monthly bill: $64.80
 

Found this useful?

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