Mathew K Analytics

Lesson 56 · Market Research Analytics in Python

End-to-End Market Research Analytics Project

In this lesson, we will walk through a complete market research analytics project using real customer and survey data. You will learn to analyze customer…

⬇ 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

End-to-End Market Research Analytics Project#

  • In this lesson, we will walk through a complete market research analytics project using real customer and survey data.
  • You will learn to analyze customer satisfaction, survey responses, campaign results, and open-ended feedback.
  • These skills help businesses understand what customers think, segment people into groups, measure loyalty, and make data-driven decisions.
  • At the end, you will be able to create actionable insights and reports from real-world datasets.
import pandas as pd
import numpy as np
import openml
from collections import Counter
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

Understanding Market Research Data#

  • Market research data includes customer surveys, transactions, feedback, and responses from different groups.
  • Datasets can have columns like age, gender, satisfaction scores, feedback comments, or campaign response indicators.
  • Each row usually represents a survey participant or customer.
  • It is important to know if values mean ratings, Yes/No outcomes, or open-ended text.
  • Beginners often group or average data incorrectly, or treat missing data the wrong way.
  • Always check what each column and value mean before analyzing.
# Load a real customer satisfaction survey dataset from OpenML
dataset = openml.datasets.get_dataset(42178)
df_csat, _, _, _ = dataset.get_data(dataset_format='dataframe')
print('Shape:', df_csat.shape)
print(df_csat.head(3))
Shape: (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  
# Calculate basic gender proportions in the satisfaction survey
gender_counts = df_csat['gender'].value_counts(normalize=True)
print('Gender distribution (fraction):\n', gender_counts)
Gender distribution (fraction):
 gender
Male      0.504756
Female    0.495244
Name: proportion, dtype: float64
# Find the percentage of customers who churned (a key business driver)
churn_rate = df_csat['Churn'].value_counts(normalize=True)['Yes'] * 100
print(f'Churn rate: {churn_rate:.2f}%')
Churn rate: 26.54%
# Load a real marketing campaign dataset from OpenML (Bank Marketing)
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('Shape:', df_campaign.shape)
print(df_campaign.head(3))
Shape: (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  
# Calculate overall marketing response rate
response_rate = df_campaign['response'].value_counts(normalize=True)['2'] * 100
print(f'Response rate: {response_rate:.2f}%')
Response rate: 11.70%
# Visualize campaign response by marital status
sns.barplot(x='marital', y='response', data=df_campaign.groupby('marital')['response'].apply(lambda x: (x=='2').mean()).reset_index())
plt.ylabel('Response Rate')
plt.title('Marketing Response Rate by Marital Status')
plt.show()
No description has been provided for this image
# Load a Net Promoter Score (NPS) survey dataset (synthetic)
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('Shape:', df_nps.shape)
print(df_nps.head(3))
Shape: (500, 4)
   CustomerID  Age Region  NPS_Score
0           1   56   West          2
1           2   69  North          0
2           3   46   East          4
# Classify each row as Detractor, Passive, or Promoter (standard NPS categories)
def nps_group(score):
    if score <= 6:
        return 'Detractor'
    elif score <= 8:
        return 'Passive'
    else:
        return 'Promoter'
df_nps['NPS_Type'] = df_nps['NPS_Score'].apply(nps_group)
counts = df_nps['NPS_Type'].value_counts()
print('NPS breakdown:\n', counts)
NPS breakdown:
 NPS_Type
Detractor    323
Passive       92
Promoter      85
Name: count, dtype: int64
# Calculate the official NPS metric: % Promoters - % Detractors
promoter_pct = (df_nps['NPS_Type']=='Promoter').mean()*100
detractor_pct = (df_nps['NPS_Type']=='Detractor').mean()*100
nps_score = promoter_pct - detractor_pct
print(f'NPS = {nps_score:.1f} (Promoters: {promoter_pct:.1f}%, Detractors: {detractor_pct:.1f}%)')
NPS = -47.6 (Promoters: 17.0%, Detractors: 64.6%)
# Load a real online retail customer transaction dataset
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('Shape:', df_retail.shape)
print(df_retail.head(3))
Shape: (541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Find top 5 countries by transaction count (excluding UK, the source country)
country_counts = df_retail['Country'].value_counts()
top_countries = country_counts.drop('United Kingdom').head(5)
print('Top 5 non-UK countries by transactions:\n', top_countries)
Top 5 non-UK countries by transactions:
 Country
Germany        9495
France         8558
EIRE           8196
Spain          2533
Netherlands    2371
Name: count, dtype: int64
# Segment retail customers by how much they spend per transaction
df_retail['Total'] = df_retail['Quantity'] * df_retail['Price']
spending_bins = pd.cut(df_retail['Total'], bins=[-1,10,50,100,1e6], labels=['Low','Medium','High','Very High'])
df_retail['Spending_Segment'] = spending_bins
print(df_retail[['Customer ID','Total','Spending_Segment']].dropna().head(3))
   Customer ID  Total Spending_Segment
0      17850.0  15.30           Medium
1      17850.0  20.34           Medium
2      17850.0  22.00           Medium
# Load and analyze open-ended customer feedback
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))
   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
# Count the most common words in feedback (simple sentiment signal)
all_words = ' '.join(df_feedback['Feedback']).lower().split()
word_counts = Counter(all_words)
print('Most common words:', word_counts.most_common(5))
Most common words: [('and', 2), ('was', 2), ('great', 1), ('service', 1), ('friendly', 1)]
# Create Net Promoter Score segment breakdown by region
region_nps = df_nps.groupby('Region')['NPS_Type'].value_counts(normalize=True).unstack().fillna(0)
print('NPS type percentage by region:\n', (region_nps*100).round(1))
NPS type percentage by region:
 NPS_Type  Detractor  Passive  Promoter
Region                                
East           67.3     17.7      15.0
North          57.7     25.5      16.8
South          69.4     15.7      14.9
West           65.8     12.8      21.4
# Cross-tab: Churn rate by contract type in satisfaction data
churn_contract = pd.crosstab(df_csat['Contract'], df_csat['Churn'], normalize='index')
print('Churn rate by contract type:\n', churn_contract['Yes'].round(2))
Churn rate by contract type:
 Contract
Month-to-month    0.43
One year          0.11
Two year          0.03
Name: Yes, dtype: float64
# Time trend: Churned customers over customer tenure (experience in months)
df_csat['tenure_group'] = pd.cut(df_csat['tenure'], bins=[0,12,24,48,72], labels=['<1yr','1-2yr','2-4yr','4-6yr'])
churn_trend = df_csat.groupby('tenure_group')['Churn'].value_counts(normalize=True).unstack()['Yes']
churn_trend.plot(kind='bar', color='orange')
plt.ylabel('Churn Rate')
plt.title('Churn Rate by Tenure Group')
plt.show()
No description has been provided for this image
# Handle missing survey responses in the NPS data
df_nps_missing = df_nps.copy()
df_nps_missing.loc[df_nps_missing.sample(frac=0.05, random_state=42).index, 'NPS_Score'] = np.nan
missing_n = df_nps_missing['NPS_Score'].isna().sum()
print(f'Missing NPS responses: {missing_n}')
Missing NPS responses: 25
# Remove or replace missing NPS scores before analysis
df_nps_dropna = df_nps_missing.dropna(subset=['NPS_Score'])
print('After removing missing:', df_nps_dropna.shape[0], 'responses left')
After removing missing: 475 responses left
# Example: Incorrect grouping can hide or distort insights
# Suppose we forget to group by region when calculating NPS
wrong_nps = (df_nps_dropna['NPS_Score'] >= 9).mean()*100 - (df_nps_dropna['NPS_Score'] <= 6).mean()*100
print(f'Incorrect NPS calculation (ignores groups): {wrong_nps:.1f}')
Incorrect NPS calculation (ignores groups): -46.9
# Misinterpreting text scales (e.g., Likert or NPS) can mislead decision makers
example = pd.Series(['Strongly disagree','Disagree','Agree','Strongly agree'])
# Convert to numeric for averaging (arbitrary mapping, must match survey meaning)
likert_map = {'Strongly disagree':1, 'Disagree':2, 'Neutral':3, 'Agree':4, 'Strongly agree':5}
numbers = example.map(likert_map)
print(numbers)
0    1
1    2
2    4
3    5
dtype: int64

Best Practices for Market Research Analytics#

  • Always validate what each data column means before analyzing.
  • Use segmentation to compare groups (e.g., by region, product, age).
  • Use cross-tabulation for categorical comparisons (e.g., churn by contract type).
  • Construct indexes like NPS or satisfaction scores with proper mapping.
  • Plot time trends to see how key metrics change over time.
  • Always check for missing data and document any changes or cleanups.
  • Share visualizations and summaries with clear business impact statements.
# END-TO-END EXAMPLE: From raw survey to business insight
# 1. Load customer satisfaction data, 2. Calculate churn and NPS, 3. Segment by contract, 4. Recommend action
dataset = openml.datasets.get_dataset(42178)
df = dataset.get_data(dataset_format='dataframe')[0].copy()
df['tenure_group'] = pd.cut(df['tenure'],[0,12,24,48,72],labels=['<1yr','1-2yr','2-4yr','4-6yr'])
contract_churn = pd.crosstab(df['Contract'],df['Churn'],normalize='index').get('Yes',pd.Series(0))
avg_monthly = df.groupby('Contract')['MonthlyCharges'].mean().round(2)
insight = f"Highest churn: {contract_churn.idxmax()} ({contract_churn.max():.2%}), lowest churn: {contract_churn.idxmin()} ({contract_churn.min():.2%})"
print(insight)
print('Average monthly charges by contract:\n', avg_monthly)
if contract_churn.idxmax() == 'Month-to-month':
    print('Recommendation: Target month-to-month customers for retention with better offers.')
else:
    print('Recommendation: Investigate drivers for churn in highest risk contract segment.')
Highest churn: Month-to-month (42.71%), lowest churn: Two year (2.83%)
Average monthly charges by contract:
 Contract
Month-to-month    66.40
One year          65.05
Two year          60.77
Name: MonthlyCharges, dtype: float64
Recommendation: Target month-to-month customers for retention with better offers.

Next Steps and Practice Prompts#

  • Try creating your own index (like NPS or satisfaction) using a real or synthetic survey dataset.
  • Segment major customer groups and present the results using a simple table or visualization.
  • Practice: Download more OpenML market research datasets to deepen your skills.
  • For more hands-on walkthroughs, search YouTube for "market research analytics Python".

Found this useful?

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