Mathew K Analytics

Lesson 46 · Market Research Analytics in Python

Introduction to Trend Analysis in Market Research

In this lesson, we will learn how to analyze and visualize trends in real-world customer and market research data. Understanding trends helps businesses…

⬇ 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

Introduction to Trend Analysis in Market Research#

  • In this lesson, we will learn how to analyze and visualize trends in real-world customer and market research data.
  • Understanding trends helps businesses identify emerging needs, opportunities, and potential problems.
  • By the end, you will be able to extract, plot, and interpret trends from key market research datasets, and translate these trends into actionable business insights.
  • This knowledge is essential for making informed decisions and staying competitive in your field.
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import openml

Core Concepts: What is Trend Analysis in Market Research?#

  • Market research datasets may contain customer demographics, sales records, survey responses, or feedback over time.
  • Trends help us understand how customer attitudes, satisfaction, or behaviors change, revealing patterns, seasonality, or shifts in perception.
  • Responses are usually structured as numbers (like satisfaction scores), choices (like NPS or churn), or text (feedback).
  • Beginners often misinterpret time-based data by ignoring units, missing missing values, or using the wrong aggregation level (daily vs monthly).
  • Accurate trend analysis depends on using dates/times correctly and understanding what each column means.
# Load customer satisfaction survey data from OpenML
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 values in key trend columns
missing = df_cs[['tenure', 'MonthlyCharges', 'Churn']].isnull().sum()
print(missing)
tenure            0
MonthlyCharges    0
Churn             0
dtype: int64
# Beginner trend: Average monthly charges by customer tenure
df_cs_grouped = df_cs.groupby('tenure')['MonthlyCharges'].mean().reset_index()
plt.plot(df_cs_grouped['tenure'], df_cs_grouped['MonthlyCharges'])
plt.xlabel('Tenure (months)')
plt.ylabel('Average Monthly Charges')
plt.title('Trend: Avg Monthly Charges vs. Tenure')
plt.show()
No description has been provided for this image
# Load online retail behavior data (real transactions)
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.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 trend: Number of transactions per month
df_retail['Month'] = df_retail['InvoiceDate'].dt.to_period('M')
monthly_counts = df_retail.groupby('Month').size()
monthly_counts.plot(kind='line', marker='o')
plt.xlabel('Month')
plt.ylabel('Number of Transactions')
plt.title('Number of Transactions per Month')
plt.show()
No description has been provided for this image
# Intermediate: Trend of total sales value per month
df_retail['TotalSales'] = df_retail['Quantity'] * df_retail['Price']
monthly_sales = df_retail.groupby('Month')['TotalSales'].sum()
monthly_sales.plot(kind='bar', color='skyblue')
plt.ylabel('Total Sales Value')
plt.title('Total Sales Value per Month')
plt.xticks(rotation=45)
plt.show()
No description has been provided for this image
# Load marketing campaign performance data
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.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 trend: Campaign response rates by month
response_by_month = df_mkt.groupby('month')['response'].value_counts().unstack().fillna(0)
response_by_month['response_rate'] = response_by_month['2'] / (response_by_month['2'] + response_by_month['1'])
response_by_month['response_rate'].plot(kind='bar', color='green')
plt.ylabel('Response Rate')
plt.xlabel('Campaign Month')
plt.title('Marketing Campaign Response Rates by Month')
plt.show()
No description has been provided for this image
# Intermediate: Trend of average customer satisfaction by churn status
satisfaction_churn = df_cs.groupby('Churn')['MonthlyCharges'].mean()
satisfaction_churn.plot(kind='bar', color=['orange','gray'])
plt.ylabel('Average Monthly Charges')
plt.title('Avg Monthly Charges: Churned vs. Retained Customers')
plt.xticks(rotation=0)
plt.show()
No description has been provided for this image
# Advanced: Calculate rolling trend of transactions (moving average)
transactions_daily = df_retail.resample('D', on='InvoiceDate').size()
rolling_tx = transactions_daily.rolling(window=14, min_periods=1).mean()
plt.plot(rolling_tx)
plt.xlabel('Date')
plt.ylabel('14-day Moving Avg: Transactions')
plt.title('Smoothed Trend of Daily Transactions')
plt.show()
No description has been provided for this image
# Advanced: Segment trend analysis by country
countries = ['United Kingdom', 'Germany', 'France', 'Netherlands']
for country in countries:
    sales = df_retail[df_retail['Country'] == country].groupby('Month')['TotalSales'].sum()
    plt.plot(sales.index.astype(str), sales, label=country)
plt.legend()
plt.xlabel('Month')
plt.ylabel('Total Sales Value')
plt.title('Monthly Sales Trend by Country')
plt.xticks(rotation=45)
plt.show()
No description has been provided for this image
# Error example: What happens with missing values in grouping?
df_missing = df_cs.copy()
df_missing.loc[0:5, 'MonthlyCharges'] = np.nan
grouped = df_missing.groupby('tenure')['MonthlyCharges'].mean()
print(grouped.head(10))
tenure
0    41.418182
1    50.519526
2    57.163347
3    58.015000
4    57.432670
5    61.003759
6    56.589091
7    59.642366
8    56.897541
9    62.564706
Name: MonthlyCharges, dtype: float64
# Debug: Identifying missing and correcting with imputation
imputed = df_missing['MonthlyCharges'].fillna(df_missing['MonthlyCharges'].mean())
print('Original mean:', df_missing['MonthlyCharges'].mean())
print('Imputed mean:', imputed.mean())
Original mean: 64.76670456160296
Imputed mean: 64.76670456160295
# Error: Misinterpreting NPS scaleload and plot synthetic NPS data
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)})
df_nps['Month'] = np.random.choice(list(range(1, 13)), size=500)
monthly_nps = df_nps.groupby('Month')['NPS_Score'].mean()
plt.plot(monthly_nps.index, monthly_nps.values, marker='o', linestyle='-')
plt.xlabel('Month')
plt.ylabel('Avg NPS Score')
plt.title('Average NPS Score by Month')
plt.show()
No description has been provided for this image

Best Practices: How to Analyze Trends for Market Research#

  • Segment your analysis by customer type, location, or time period for deeper insights.
  • Cross-tabulation lets you see how trends differ between groupslike churned vs. loyal customers.
  • Use rolling averages to smooth out noisy data and highlight true patterns.
  • Build composite indexes (like satisfaction or loyalty scores) from multiple variables rather than one measure.
  • Communicate findings using clear charts and business language, not just statistics.
# Best practice demo: Cross-tab trend by customer contract type
df_cs_contract = df_cs.groupby(['tenure','Contract'])['MonthlyCharges'].mean().unstack()
df_cs_contract.plot(figsize=(8,5))
plt.xlabel('Tenure (months)')
plt.ylabel('Average Monthly Charges')
plt.title('Trend: Monthly Charges by Contract Type Over Tenure')
plt.show()
No description has been provided for this image
# Pattern: Construct a composite satisfaction index
df_cs['SatIndex'] = (
    df_cs['OnlineSecurity'].map({'No': 0, 'Yes': 1}) +
    df_cs['OnlineBackup'].map({'No': 0, 'Yes': 1}) +
    df_cs['TechSupport'].map({'No': 0, 'Yes': 1})
) / 3
monthly_sat = df_cs.groupby('tenure')['SatIndex'].mean()
plt.plot(monthly_sat.index, monthly_sat.values)
plt.xlabel('Tenure (months)')
plt.ylabel('Satisfaction Index (0-1)')
plt.title('Composite Satisfaction Index by Tenure')
plt.show()
No description has been provided for this image
# End-to-end: Find evidence for customer churn risk over time
df_cs_trend = df_cs.groupby('tenure')['Churn'].value_counts(normalize=True).unstack().fillna(0)
plt.plot(df_cs_trend.index, df_cs_trend['Yes'], label='Churned')
plt.plot(df_cs_trend.index, df_cs_trend['No'], label='Retained')
plt.xlabel('Tenure (months)')
plt.ylabel('Fraction of Customers')
plt.title('Customer Churn vs. Tenure Trend')
plt.legend()
plt.show()
churn_tipping_point = df_cs_trend['Yes'].idxmax()
print(f'Business insight: Churn risk peaks at {churn_tipping_point} months tenure.')
No description has been provided for this image
Business insight: Churn risk peaks at 1 months tenure.
 

Found this useful?

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