Mathew K Analytics

Lesson 32 · Market Research Analytics in Python

Hypothesis Testing in Market Research: Essential Methods and Practical Python Analysis

This lesson teaches you how to use real customer datasets and hypothesis tests to make better business decisions. We focus on comparing customer groups,…

⬇ 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

Hypothesis Testing in Market Research#

  • This lesson teaches you how to use real customer datasets and hypothesis tests to make better business decisions.
  • We focus on comparing customer groups, testing marketing campaigns, and understanding key drivers of satisfaction.
  • Hypothesis testing helps you turn raw survey or behavior data into actionable business recommendations.
  • You will analyze satisfaction surveys and campaign results step-by-step, using Python and real-world datasets.
import pandas as pd
import numpy as np
import openml
import seaborn as sns
import matplotlib.pyplot as plt
import scipy.stats as stats
import warnings
warnings.filterwarnings('ignore')

Market Research Hypothesis Testing: What, Why, and How#

  • In market research, a hypothesis is a business guess about differences or patterns among customers.
  • Survey data includes demographic facts, satisfaction ratings, and customer choices.
  • Each survey row is a customer and columns describe who they are and what they think or do.
  • Beginners often misuse Likert scales as numbers or forget to check group sample sizes.
  • Always check for missing values, data type mismatches, and survey weighting before starting your analysis.
# Load the Customer Satisfaction Survey dataset
dataset = openml.datasets.get_dataset(42178)
df_cs, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df_cs.shape)
print(df_cs.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  
# Check for missing survey responses
missing_counts = df_cs.isnull().sum()
print('Missing values per column:')
print(missing_counts[missing_counts > 0])
Missing values per column:
Series([], dtype: int64)
# What does 'Churn' look like among customers?
churn_counts = df_cs['Churn'].value_counts()
print('Customer churn breakdown:')
print(churn_counts)
Customer churn breakdown:
Churn
No     5174
Yes    1869
Name: count, dtype: int64
# Visualize Churn by Customer Gender
sns.countplot(data=df_cs, x='gender', hue='Churn')
plt.title('Churn by Gender')
plt.xlabel('Gender')
plt.ylabel('Number of Customers')
plt.show()
No description has been provided for this image
# Beginner Hypothesis Test: Is churn rate different by gender?
contingency_table = pd.crosstab(df_cs['gender'], df_cs['Churn'])
chi2, p, dof, expected = stats.chi2_contingency(contingency_table)
print('Chi-Square statistic:', chi2)
print('p-value:', p)
Chi-Square statistic: 0.4840828822091383
p-value: 0.48657873605618596
# Beginner: Test mean MonthlyCharges by churn group
monthly_charges_churn = df_cs[df_cs['Churn']=='Yes']['MonthlyCharges']
monthly_charges_no = df_cs[df_cs['Churn']=='No']['MonthlyCharges']
t_stat, p_val = stats.ttest_ind(monthly_charges_churn, monthly_charges_no, nan_policy='omit')
print('T-statistic:', t_stat)
print('p-value:', p_val)
print('Average charge (Churned):', monthly_charges_churn.mean())
print('Average charge (Retained):', monthly_charges_no.mean())
T-statistic: 16.536738015936308
p-value: 2.7066456068884154e-60
Average charge (Churned): 74.44133226324237
Average charge (Retained): 61.26512369540008
# Load marketing campaign dataset
dataset = openml.datasets.get_dataset(1461)
df_mkt, _, _, _ = dataset.get_data(dataset_format='dataframe')
df_mkt.columns = ['age','job','marital','education','default','balance','housing','loan','contact','day','month','duration','campaign','pdays','previous','poutcome','response']
print(df_mkt.shape)
print(df_mkt[['response','job','age']].head(3))
(45211, 17)
  response           job  age
0        1    management   58
1        1    technician   44
2        1  entrepreneur   33
# Check conversion rates by education level
conversion_by_edu = pd.crosstab(df_mkt['education'], df_mkt['response'], normalize='index')
print('Conversion rates by education:')
print(conversion_by_edu)
Conversion rates by education:
response          1         2
education                    
primary    0.913735  0.086265
secondary  0.894406  0.105594
tertiary   0.849936  0.150064
unknown    0.864297  0.135703
# Hypothesis test: Is response rate different for 'primary' vs 'tertiary' education?
primary = df_mkt[df_mkt['education'] == 'primary']['response'].apply(lambda x: 1 if x=='yes' else 0)
tertiary = df_mkt[df_mkt['education'] == 'tertiary']['response'].apply(lambda x: 1 if x=='yes' else 0)
t_stat, p_val = stats.ttest_ind(primary, tertiary)
print('T-statistic:', t_stat)
print('p-value:', p_val)
T-statistic: nan
p-value: nan
# Visualize age distribution among responders and non-responders
sns.histplot(df_mkt[df_mkt['response']=='yes']['age'], kde=True, color='green', label='Responded', bins=20)
sns.histplot(df_mkt[df_mkt['response']=='no']['age'], kde=True, color='red', label='Not Responded', bins=20)
plt.legend()
plt.title('Age Distribution by Campaign Response')
plt.xlabel('Age')
plt.ylabel('Number of Customers')
plt.show()
No description has been provided for this image
# Segment by age group and test if campaign response differs
df_mkt['age_group'] = pd.cut(df_mkt['age'], bins=[18, 30, 45, 65, 100], labels=['18-30','31-45','46-65','66+'])
res_age_table = pd.crosstab(df_mkt['age_group'], df_mkt['response'])
chi2, p, dof, expected = stats.chi2_contingency(res_age_table)
print('Chi-square statistic:', chi2)
print('p-value:', p)
print(res_age_table)
Chi-square statistic: 920.7882106375396
p-value: 2.740762383306473e-199
response       1     2
age_group             
18-30       5880  1138
31-45      21388  2345
46-65      12218  1479
66+          431   320
# Advanced: Load Net Promoter Score (NPS) synthetic survey
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))
   CustomerID  Age Region  NPS_Score
0           1   56   West          2
1           2   69  North          0
2           3   46   East          4
# Advanced: Hypothesis test, is mean NPS different by region?
mean_by_region = df_nps.groupby('Region')['NPS_Score'].mean()
f_stat, p_val = stats.f_oneway(*[df_nps[df_nps['Region']==region]['NPS_Score'] for region in df_nps['Region'].unique()])
print('Average NPS by region:')
print(mean_by_region)
print('One-way ANOVA F-statistic:', f_stat)
print('p-value:', p_val)
Average NPS by region:
Region
East     4.504425
North    5.214765
South    4.719008
West     5.025641
Name: NPS_Score, dtype: float64
One-way ANOVA F-statistic: 1.292073173941513
p-value: 0.2764111891684852
# Advanced: Proportion of promoters vs detractors by age group
df_nps['category'] = pd.cut(df_nps['NPS_Score'], bins=[-1,6,8,10], labels=['Detractor','Passive','Promoter'])
df_nps['age_group'] = pd.cut(df_nps['Age'], bins=[17,30,45,70], labels=['18-30','31-45','46-70'])
table = pd.crosstab(df_nps['age_group'], df_nps['category'], normalize='index')
print('NPS categories by age group:')
print(table)
NPS categories by age group:
category   Detractor   Passive  Promoter
age_group                               
18-30       0.669643  0.160714  0.169643
31-45       0.646259  0.204082  0.149660
46-70       0.634855  0.182573  0.182573
# Advanced: Compare NPS category distribution by region
nps_cat_table = pd.crosstab(df_nps['Region'], df_nps['category'])
chi2, p, dof, expected = stats.chi2_contingency(nps_cat_table)
print('Contingency table:')
print(nps_cat_table)
print('Chi-square statistic:', chi2)
print('p-value:', p)
Contingency table:
category  Detractor  Passive  Promoter
Region                                
East             76       20        17
North            86       38        25
South            84       19        18
West             77       15        25
Chi-square statistic: 10.144480861679082
p-value: 0.11869800121141394
# Error handling: Remove rows with missing NPS scores
n_missing = df_nps['NPS_Score'].isnull().sum()
df_nps_clean = df_nps.dropna(subset=['NPS_Score'])
print(f'Removed {n_missing} rows with missing NPS scores. Remaining: {df_nps_clean.shape[0]}')
Removed 0 rows with missing NPS scores. Remaining: 500
# Debugging: Check group sizes before running hypothesis tests
group_sizes = df_nps_clean.groupby('Region').size()
print('Survey responses per region:')
print(group_sizes)
Survey responses per region:
Region
East     113
North    149
South    121
West     117
dtype: int64
# Debugging: What happens if you misinterpret Likert as numeric?
likert = ['Strongly disagree','Disagree','Neutral','Agree','Strongly agree']
score_map = {'Strongly disagree':1, 'Disagree':2, 'Neutral':3, 'Agree':4, 'Strongly agree':5}
fake_likert = pd.Series(np.random.choice(likert, 30))
numeric_likert = fake_likert.map(score_map)
print('Fake Likert ratings:')
print(fake_likert.head())
print('Numeric encoding:')
print(numeric_likert.head())
print('Mean:', numeric_likert.mean())
Fake Likert ratings:
0              Neutral
1    Strongly disagree
2    Strongly disagree
3             Disagree
4       Strongly agree
dtype: object
Numeric encoding:
0    3
1    1
2    1
3    2
4    5
dtype: int64
Mean: 2.8666666666666667
# Segmentation: Create customer groups for deeper insights
df_cs['tenure_group'] = pd.cut(df_cs['tenure'], bins=[0,12,24,48,100], labels=['<1yr','1-2yr','2-4yr','4+yr'])
print(df_cs[['tenure','tenure_group']].head(6))
   tenure tenure_group
0       1         <1yr
1      34        2-4yr
2       2         <1yr
3      45        2-4yr
4       2         <1yr
5       8         <1yr
# Cross-tabulation: Churn by payment method
ct = pd.crosstab(df_cs['PaymentMethod'], df_cs['Churn'], normalize='index')
print('Churn rate per payment method:')
print(ct.head())
Churn rate per payment method:
Churn                            No       Yes
PaymentMethod                                
Bank transfer (automatic)  0.832902  0.167098
Credit card (automatic)    0.847569  0.152431
Electronic check           0.547146  0.452854
Mailed check               0.808933  0.191067
# Index and score construction: Build a composite satisfaction score
# (Example using available columns; adapt for your data situation)
satisfaction_cols = ['OnlineSecurity','TechSupport','StreamingTV']
df_cs['satisfaction_score'] = (df_cs[satisfaction_cols] == 'Yes').sum(axis=1)
print(df_cs[['satisfaction_score']].describe())
       satisfaction_score
count         7043.000000
mean             0.961238
std              0.996616
min              0.000000
25%              0.000000
50%              1.000000
75%              2.000000
max              3.000000
# Trend analysis: Did average monthly charges increase over tenure?
monthly_by_tenure = df_cs.groupby('tenure_group')['MonthlyCharges'].mean()
monthly_by_tenure.plot(kind='bar', color='dodgerblue')
plt.title('Average Monthly Charges by Tenure Group')
plt.xlabel('Tenure Group')
plt.ylabel('Average Monthly Charge')
plt.show()
No description has been provided for this image
# End-to-end: Test if satisfaction differs by churn outcome
df_cs['satisfaction_score'] = (df_cs[satisfaction_cols] == 'Yes').sum(axis=1)
churned = df_cs[df_cs['Churn']=='Yes']['satisfaction_score']
retained = df_cs[df_cs['Churn']=='No']['satisfaction_score']
t_stat, p_value = stats.ttest_ind(churned, retained, nan_policy='omit')
print('Satisfaction (churned):', churned.mean())
print('Satisfaction (retained):', retained.mean())
print('t-statistic:', t_stat)
print('p-value:', p_value)
if p_value < 0.05:
    print('Conclusion: Satisfaction is significantly different for churned versus retained customers.')
else:
    print('Conclusion: Satisfaction difference by churn is not statistically significant.')
Satisfaction (churned): 0.7592295345104334
Satisfaction (retained): 1.034209509083881
t-statistic: -10.299800075374966
p-value: 1.056585768579333e-24
Conclusion: Satisfaction is significantly different for churned versus retained customers.
# Watch a YouTube guide for more hypothesis test examples
print('For more hands-on examples, search YouTube for "hypothesis testing in customer analytics".')
For more hands-on examples, search YouTube for "hypothesis testing in customer analytics".
 

Found this useful?

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