Mathew K Analytics

Lesson 31 · Market Research Analytics in Python

Statistical Thinking for Market Insights: Training for Data-Driven Decision Making

In this lesson, we will solve real market research problems using customer and survey data. Understanding your customers' preferences, satisfaction, and…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Statistical Thinking for Market Insights#

  • In this lesson, we will solve real market research problems using customer and survey data.
  • Understanding your customers' preferences, satisfaction, and loyalty is crucial for making data-driven decisions.
  • You will practice analyzing customer survey responses, building segments, and finding actionable insights in real datasets.
  • We focus on turning data into business recommendations with practical, statistical tools.
import openml
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

Core Market Research Data Structures#

  • Market research datasets often contain customer demographics, ratings, and survey responses.
  • Responses may be Yes/No, multiple choice, Likert scales, or open text.
  • Demographics give business context for segmentation, such as age, region, or tenure.
  • A common mistake is treating all fields as numeric or ignoring missing values.
  • Always start by understanding variable types and their business meaning.
dataset = openml.datasets.get_dataset(42178)
csat_df, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(csat_df.shape)
print(csat_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  
np.random.seed(42)
nps_df = 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(nps_df.shape)
print(nps_df.head(3))
(500, 4)
   CustomerID  Age Region  NPS_Score
0           1   56   West          2
1           2   69  North          0
2           3   46   East          4
dataset = openml.datasets.get_dataset(1461)
mkt_df, _, _, _ = dataset.get_data(dataset_format='dataframe')
mkt_df.columns = ['age','job','marital','education','default','balance','housing','loan','contact','day','month','duration','campaign','pdays','previous','poutcome','response']
print(mkt_df.shape)
print(mkt_df.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  
retail_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
retail_df = pd.read_excel(retail_url, sheet_name='Year 2010-2011')
retail_df['InvoiceDate'] = pd.to_datetime(retail_df['InvoiceDate'])
print(retail_df.shape)
print(retail_df.head(3))
(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  

Beginner Example 1: Customer Satisfaction Score Distribution#

  • We start by plotting the distribution of customer satisfaction scores.
  • This shows where most customers' experiences fall.
if 'Satisfaction' in csat_df.columns:
    plt.figure(figsize=(6,4))
    csat_df['Satisfaction'].hist(bins=10, color='skyblue', edgecolor='black')
    plt.xlabel('Satisfaction Score')
    plt.ylabel('Number of Customers')
    plt.title('Distribution of Customer Satisfaction Scores')
    plt.show()
else:
    print('The Satisfaction column is not present in the dataset.')
The Satisfaction column is not present in the dataset.

Beginner Example 2: Grouping NPS by Region#

  • We analyze how NPS scores vary across different business regions.
  • Grouping by region reveals differences in customer loyalty.
nps_by_region = nps_df.groupby('Region')['NPS_Score'].mean()
print(nps_by_region)
nps_by_region.plot(kind='bar', color='orange')
plt.title('Average NPS by Region')
plt.ylabel('NPS Score')
plt.show()
Region
East     4.504425
North    5.214765
South    4.719008
West     5.025641
Name: NPS_Score, dtype: float64
No description has been provided for this image

Beginner Example 3: Simple Customer Segments#

  • We create basic customer segments based on age groups.
  • Segmenting by age helps find unique opportunities and risks.
nps_df['Age_Group'] = pd.cut(nps_df['Age'], bins=[17,29,44,59,70], labels=['18-29','30-44','45-59','60-70'])
segment_counts = nps_df['Age_Group'].value_counts().sort_index()
print(segment_counts)
segment_counts.plot(kind='bar', color='purple')
plt.xlabel('Age Group')
plt.ylabel('Customers in Group')
plt.title('Customer Count by Age Segment')
plt.show()
Age_Group
18-29    107
30-44    139
45-59    158
60-70     96
Name: count, dtype: int64
No description has been provided for this image

Intermediate Example 1: Churn vs. Satisfaction#

  • We compare average satisfaction for customers who left vs stayed.
  • This helps link satisfaction data to actual business outcomes.
if 'Satisfaction' in csat_df.columns and 'Churn' in csat_df.columns:
    churn_means = csat_df.groupby('Churn')['Satisfaction'].mean()
    print(churn_means)
    churn_means.plot(kind='bar', color='teal')
    plt.title('Avg Satisfaction by Customer Churn Status')
    plt.ylabel('Average Satisfaction Score')
    plt.show()
else:
    print('Required columns for Churn and Satisfaction are missing.')
Required columns for Churn and Satisfaction are missing.

Intermediate Example 2: Campaign Response Rate Calculation#

  • We calculate the response rate for a marketing campaign.
  • This metric shows how effective a campaign was at engaging potential customers.
response_counts = mkt_df['response'].value_counts()
print(response_counts)
rate = response_counts.get('yes', 0) / response_counts.sum() if response_counts.sum() > 0 else 0
print(f'Campaign Response Rate: {rate:.2%}')
response
1    39922
2     5289
Name: count, dtype: int64
Campaign Response Rate: 0.00%

Intermediate Example 3: Trend Analysis Over Time#

  • We analyze how customer activity changes month to month.
  • This approach is used for seasonality and retention insights.
np.random.seed(0)
dates = pd.date_range('2021-01-01', periods=24, freq='ME')
cohort_df = pd.DataFrame({
    'CustomerID': np.random.randint(1000,2000,len(dates)),
    'Signup_Month': dates,
    'Active_Users': np.random.randint(50,300,len(dates))
})
plt.figure(figsize=(8,4))
plt.plot(cohort_df['Signup_Month'], cohort_df['Active_Users'], marker='o')
plt.title('Customer Activity Over Time')
plt.xlabel('Month')
plt.ylabel('Active Users')
plt.grid(True)
plt.show()
No description has been provided for this image

Advanced Example 1: Cross-Tabulating Satisfaction and Contract Type#

  • We use cross-tabulation to understand satisfaction scores across contract types.
  • This can reveal which offerings create better customer experiences.
if 'Contract' in csat_df.columns and 'Satisfaction' in csat_df.columns:
    ctab = pd.crosstab(csat_df['Contract'], csat_df['Satisfaction'])
    print(ctab.head())
    plt.figure(figsize=(8,4))
    sns.heatmap(ctab, annot=False, cmap='Blues')
    plt.title('Satisfaction Scores vs Contract Type')
    plt.xlabel('Satisfaction Score')
    plt.ylabel('Contract Type')
    plt.show()
else:
    print('Required columns for Contract and Satisfaction are missing.')
Required columns for Contract and Satisfaction are missing.

Advanced Example 2: Building a Composite Loyalty Index#

  • We build a simple loyalty index combining NPS and campaign response.
  • Composite scores provide a more holistic view of customer commitment.
# Merge NPS and marketing response on random customer IDs
merged = nps_df.copy()
merged['response'] = np.where(np.random.rand(len(merged)) < 0.3, 'yes', 'no')
merged['Loyalty_Index'] = merged['NPS_Score'] * (merged['response'] == 'yes').astype(int)
print(merged[['Age','Region','NPS_Score','response','Loyalty_Index']].head(5))
print('Highest Loyalty Index:', merged['Loyalty_Index'].max())
   Age Region  NPS_Score response  Loyalty_Index
0   56   West          2       no              0
1   69  North          0       no              0
2   46   East          4       no              0
3   32   West          3       no              0
4   60   East          9      yes              9
Highest Loyalty Index: 10

Advanced Example 3: Identifying Outliers in Customer Purchases#

  • Outliers can point to high-value or risky transaction customers.
  • We use z-scores to spot purchases outside typical customer range.
if 'Price' in retail_df.columns and 'Quantity' in retail_df.columns:
    retail_df['Total'] = retail_df['Price'] * retail_df['Quantity']
    total_mean = retail_df['Total'].mean()
    total_std = retail_df['Total'].std()
    z_scores = (retail_df['Total'] - total_mean) / total_std
    outliers = retail_df[(z_scores > 3) | (z_scores < -3)]
    print(outliers[['Invoice','Quantity','Price','Total']].head())
    print(f'Outlier count: {len(outliers)}')
else:
    print('Price or Quantity column missing.')
      Invoice  Quantity  Price   Total
870    536477       480   3.39  1627.2
4505   536785       144  10.95  1576.8
4946   536830      1400   1.06  1484.0
6607   536970       120  10.95  1314.0
10205  537235       156   8.50  1326.0
Outlier count: 403

Error Handling Example 1: Missing Survey Responses#

  • Surveys often have missing or skipped questions.
  • We check for missing satisfaction or NPS responses.
if 'Satisfaction' in csat_df.columns:
    missing = csat_df['Satisfaction'].isnull().sum()
    print(f'Missing Satisfaction Responses: {missing}')
if 'NPS_Score' in nps_df.columns:
    missing_nps = nps_df['NPS_Score'].isnull().sum()
    print(f'Missing NPS Responses: {missing_nps}')
Missing NPS Responses: 0

Error Handling Example 2: Incorrect Aggregation by Customer#

  • Aggregation errors are common with large datasets.
  • We verify our groupby logic by double-checking unique customer counts.
unique_customers = nps_df['CustomerID'].nunique()
agg_counts = nps_df.groupby('CustomerID').size().max()
if agg_counts > 1:
    print(f'Warning: Multiple records for same customer detected!')
else:
    print(f'Customer groupings are unique.')
Customer groupings are unique.

Error Handling Example 3: Misinterpreting Likert or NPS Scale#

  • Mistakes in reporting can come from reversing scales or misreading NPS ratings.
  • We demonstrate correct calculation of a Net Promoter Score.
detractors = ((nps_df['NPS_Score'] >= 0) & (nps_df['NPS_Score'] <= 6)).sum()
passives = ((nps_df['NPS_Score'] >= 7) & (nps_df['NPS_Score'] <= 8)).sum()
promoters = ((nps_df['NPS_Score'] >= 9) & (nps_df['NPS_Score'] <= 10)).sum()
n_total = len(nps_df)
nps_result = ((promoters - detractors) / n_total) * 100 if n_total else 0
print(f'NPS Calculation: {nps_result:.1f}')
NPS Calculation: -47.6

Best Practices: Market Segmentation and Index Building#

  • Segment your data by strategic categories (age, region, tenure).
  • Use cross-tabs to identify underperforming or winning groups.
  • Create composite scores, such as loyalty indexes, to simplify complex behaviors into one metric.
  • Always visualize trends over time for product or service strategy.
loyalty_by_age = merged.groupby('Age_Group')['Loyalty_Index'].mean()
print(loyalty_by_age)
loyalty_by_age.plot(kind='bar', color='darkgreen')
plt.title('Average Loyalty Index by Age Group')
plt.xlabel('Age Group')
plt.ylabel('Mean Loyalty Index')
plt.show()
Age_Group
18-29    1.728972
30-44    1.035971
45-59    1.120253
60-70    1.427083
Name: Loyalty_Index, dtype: float64
No description has been provided for this image

Tiny End-to-End Market Research Problem#

  • Scenario: Management wants to know which region needs the most NPS loyalty improvement.
  • Steps:
    1. Calculate average NPS by region.
    1. Find the lowest average.
    1. Recommend which regional manager to support with targeted programs.
nps_by_region = nps_df.groupby('Region')['NPS_Score'].mean()
target_region = nps_by_region.idxmin()
min_score = nps_by_region.min()
print('Average NPS by Region:')
print(nps_by_region)
print(f'Lowest NPS region: {target_region} (Score: {min_score:.2f})')
print(f'Recommendation: Focus loyalty initiatives in {target_region} to close the gap.')
Average NPS by Region:
Region
East     4.504425
North    5.214765
South    4.719008
West     5.025641
Name: NPS_Score, dtype: float64
Lowest NPS region: East (Score: 4.50)
Recommendation: Focus loyalty initiatives in East to close the gap.
 

Found this useful?

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