Canadian Motor Vehicle Industry - Employment & Earnings Analysis

A comprehensive analysis of Canada’s motor vehicle industry employment and earnings trends based on the Survey of Employment, Payrolls and Hours (SEPH). The analysis covers 11 automotive-related industries across the value chain, from manufacturing to retail and services.
analysis
visualization
Published

December 19, 2025

Data Overview

About the Dataset

The SEPH (Survey of Employment, Payrolls and Hours) provides a monthly portrait of earnings, job counts (occupied positions), and hours worked by detailed induustry at the national, provincial, and territorial levels. This data serves as: - A key input for GDP calculations - representing more than 17% of total economic activity - A source for policy decisions - used by the Canada Revenue Agency (CRA) for pension and RRSP contribution limits - An economic indicator - used by the private sector for contract escalations and wage rate determinations

Dataset Description

This analysis focuses on 11 motor vehicle-related industries classified under the NAICS (North American Industry Classification System). These industries span the complete automotive value chain:

Industry Segment NAICS Codes Description
Manufacturing 3361, 3362, 3363 Vehicle assembly, body/trailer, and parts manufacturing
Wholesale Trade 4151, 4152, 4153 Vehicle and parts wholesale distribution
Retail Trade 4411, 4412, 4413 Vehicle dealers and parts retailers
Services 5321, 8111 Equipment rental and automotive repair
Click to expand/collapse code
# Import libraries

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
from datetime import datetime

# Suppress warnings for cleaner output
warnings.filterwarnings('ignore')

# Configure pandas display options for better readability
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.float_format', '{:,.2f}'.format)

import plotly.io as pio
# Set to 'notebook' or 'notebook_connected' for Quarto compatibility
pio.renderers.default = "notebook" 


class style:
    BOLD = '\033[1m'
    END = '\033[0m'
Click to expand/collapse code
# Load the main dataset
df_raw = pd.read_csv('../../data/CanadaMotorEmplData.csv')

# Display basic information about the dataset
print(f"\nTotal Records: {len(df_raw):,}")
print(f"Date Range: {df_raw['REF_DATE'].min()} to {df_raw['REF_DATE'].max()}")
print(f"Number of Industries: {df_raw['North American Industry Classification System (NAICS)'].nunique()}")
print(f"Estimate Types: {df_raw['Estimate'].nunique()}")
print(f"\nColumn Names:")
for i, col in enumerate(df_raw.columns, 1):
    print(f"   {i:2}. {col}")


# Sample of the data
print("\nSample of the data:")
df_raw.head(10)

Total Records: 2,838
Date Range: 2015-01 to 2025-09
Number of Industries: 11
Estimate Types: 2

Column Names:
    1. REF_DATE
    2. GEO
    3. DGUID
    4. Estimate
    5. North American Industry Classification System (NAICS)
    6. UOM
    7. UOM_ID
    8. SCALAR_FACTOR
    9. SCALAR_ID
   10. VECTOR
   11. COORDINATE
   12. VALUE
   13. STATUS
   14. SYMBOL
   15. TERMINATED
   16. DECIMALS

Sample of the data:
REF_DATE GEO DGUID Estimate North American Industry Classification System (NAICS) UOM UOM_ID SCALAR_FACTOR SCALAR_ID VECTOR COORDINATE VALUE STATUS SYMBOL TERMINATED DECIMALS
0 2015-01 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 42,565.00 A NaN NaN 0
1 2015-02 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 42,517.00 A NaN NaN 0
2 2015-03 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 39,145.00 B NaN NaN 0
3 2015-04 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 39,193.00 C NaN NaN 0
4 2015-05 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 41,597.00 B NaN NaN 0
5 2015-06 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 43,411.00 A NaN NaN 0
6 2015-07 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 43,259.00 A NaN NaN 0
7 2015-08 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 42,685.00 A NaN NaN 0
8 2015-09 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 42,816.00 A NaN NaN 0
9 2015-10 Canada 2021A000011124 Employment for all employees Motor vehicle manufacturing [3361] Persons 249 units 0 v54026580 1.1.132 42,680.00 B NaN NaN 0
Click to expand/collapse code
print(style.BOLD + "\nIndustries in the dataset:" + style.END)

industries = df_raw['North American Industry Classification System (NAICS)'].unique()

# Categorize industries
manufacturing = [i for i in industries if '336' in i]
wholesale = [i for i in industries if '415' in i]
retail = [i for i in industries if '441' in i]
services = [i for i in industries if '532' in i or '811' in i]

print("\nMANUFACTURING (NAICS 336x):")
for ind in manufacturing:
    print(f"   • {ind}")

print("\nWHOLESALE TRADE (NAICS 415x):")
for ind in wholesale:
    print(f"   • {ind}")

print("\nRETAIL TRADE (NAICS 441x):")
for ind in retail:
    print(f"   • {ind}")

print("\nSERVICES (NAICS 532x, 811x):")
for ind in services:
    print(f"   • {ind}")


Industries in the dataset:



MANUFACTURING (NAICS 336x):

   • Motor vehicle manufacturing [3361]

   • Motor vehicle body and trailer manufacturing [3362]

   • Motor vehicle parts manufacturing [3363]



WHOLESALE TRADE (NAICS 415x):

   • Motor vehicle merchant wholesalers [4151]

   • New motor vehicle parts and accessories merchant wholesalers [4152]

   • Used motor vehicle parts and accessories merchant wholesalers [4153]



RETAIL TRADE (NAICS 441x):

   • Automobile dealers [4411]

   • Other motor vehicle dealers [4412]

   • Automotive parts, accessories and tire retailers [4413]



SERVICES (NAICS 532x, 811x):

   • Automotive equipment rental and leasing [5321]

   • Automotive repair and maintenance [8111]

Data Validation

We’ll perform rigorous data quality checks to ensure the integrity of our analysis. We examine data types, missing values, duplicate records, and value ranges.

Click to expand/collapse code
df_raw.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2838 entries, 0 to 2837
Data columns (total 16 columns):
 #   Column                                                 Non-Null Count  Dtype  
---  ------                                                 --------------  -----  
 0   REF_DATE                                               2838 non-null   object 
 1   GEO                                                    2838 non-null   object 
 2   DGUID                                                  2838 non-null   object 
 3   Estimate                                               2838 non-null   object 
 4   North American Industry Classification System (NAICS)  2838 non-null   object 
 5   UOM                                                    2838 non-null   object 
 6   UOM_ID                                                 2838 non-null   int64  
 7   SCALAR_FACTOR                                          2838 non-null   object 
 8   SCALAR_ID                                              2838 non-null   int64  
 9   VECTOR                                                 2838 non-null   object 
 10  COORDINATE                                             2838 non-null   object 
 11  VALUE                                                  2755 non-null   float64
 12  STATUS                                                 2838 non-null   object 
 13  SYMBOL                                                 0 non-null      float64
 14  TERMINATED                                             0 non-null      float64
 15  DECIMALS                                               2838 non-null   int64  
dtypes: float64(3), int64(3), object(10)
memory usage: 354.9+ KB

Missing Values

Missing values can significantly impact analysis quality. We need to identify where gaps exist and understand why.

Click to expand/collapse code
missing_summary = pd.DataFrame({
    'Column': df_raw.columns,
    'Missing Count': df_raw.isnull().sum().values,
    'Missing %': (df_raw.isnull().sum().values / len(df_raw) * 100).round(2),
    'Non-Missing Count': df_raw.notnull().sum().values
})

# Show only columns with missing values or important columns
print("\nColumns with missing values:")
missing_cols = missing_summary[missing_summary['Missing Count'] > 0]
if len(missing_cols) > 0:
    print(missing_cols.to_string(index=False))
else:
    print("   No columns have missing values (except empty columns).")

# Check VALUE column specifically - this is our main data column
print(f"\nVALUE Column Statistics:")
print(f"   - Total records: {len(df_raw):,}")
print(f"   - Missing VALUES: {df_raw['VALUE'].isnull().sum():,}")
print(f"   - Valid VALUES: {df_raw['VALUE'].notnull().sum():,}")

Columns with missing values:
    Column  Missing Count  Missing %  Non-Missing Count
     VALUE             83       2.92               2755
    SYMBOL           2838     100.00                  0
TERMINATED           2838     100.00                  0

VALUE Column Statistics:
   - Total records: 2,838
   - Missing VALUES: 83
   - Valid VALUES: 2,755

Data Quality Indicators

Statistics Canada provides quality indicators (STATUS) column for each data point:

A = Excellent
B = Very Good
C = Good
D = Acceptable
E = Use with Caution
F = Too Unreliable to Publish

Click to expand/collapse code
print("\nQuality grades indicate the reliability of each data point.")
print("Statistics Canada quality codes:")
print("   A = Excellent | B = Very Good | C = Good | D = Acceptable")
print("   E = Use with Caution | F = Too Unreliable to Publish")

quality_counts = df_raw['STATUS'].value_counts().sort_index()
quality_pct = (quality_counts / len(df_raw) * 100).round(1)

print("\n" + "-"*40)
print(f"{'Grade':<10} {'Count':<15} {'Percentage':<15}")
print("-"*40)
for grade in quality_counts.index:
    print(f"{grade:<10} {quality_counts[grade]:>10,}     {quality_pct[grade]:>8.1f}%")
print("-"*40)

# Calculate high-quality percentage (A, B, C)
high_quality = df_raw[df_raw['STATUS'].isin(['A', 'B', 'C'])].shape[0]
print(f"\nHigh-quality data (A, B, C): {high_quality:,} records ({high_quality/len(df_raw)*100:.1f}%)")
print(f"Data requiring caution (D, E, F): {len(df_raw)-high_quality:,} records ({(len(df_raw)-high_quality)/len(df_raw)*100:.1f}%)")

Quality grades indicate the reliability of each data point.
Statistics Canada quality codes:
   A = Excellent | B = Very Good | C = Good | D = Acceptable
   E = Use with Caution | F = Too Unreliable to Publish

----------------------------------------
Grade      Count           Percentage     
----------------------------------------
A               1,715         60.4%
B                 843         29.7%
C                 142          5.0%
D                  36          1.3%
E                  19          0.7%
F                  83          2.9%
----------------------------------------

High-quality data (A, B, C): 2,700 records (95.1%)
Data requiring caution (D, E, F): 138 records (4.9%)

Duplicate Records

Duplicate records could skew our analysis. We check for exact duplicates and logical duplicates (same date/industry/estimate).

Click to expand/collapse code
# Check for exact duplicates
exact_dupes = df_raw.duplicated().sum()
print(f"\n   Exact duplicate rows: {exact_dupes}")

# Check for logical duplicates (same date, estimate, industry)
key_cols = ['REF_DATE', 'Estimate', 'North American Industry Classification System (NAICS)']
logical_dupes = df_raw.duplicated(subset=key_cols).sum()
print(f"   Logical duplicates (same date/estimate/industry): {logical_dupes}")

if exact_dupes == 0 and logical_dupes == 0:
    print("\nNo duplicate records found - data integrity verified!")
else:
    print("\nDuplicates detected - will be handled in data cleaning.")

   Exact duplicate rows: 0
   Logical duplicates (same date/estimate/industry): 0

No duplicate records found - data integrity verified!

Value Range Validation

We check whether values fall within expected ranges.
Employment should be positive integers, earnings should be positive decimals.

Click to expand/collapse code
# Split by estimate type
employment_data = df_raw[df_raw['Estimate'] == 'Employment for all employees']['VALUE']
earnings_data = df_raw[df_raw['Estimate'] == 'Average weekly earnings including overtime for all employees']['VALUE']

print("\nEMPLOYMENT (Number of Persons):")
print(f"   - Minimum: {employment_data.min():,.0f}")
print(f"   - Maximum: {employment_data.max():,.0f}")
print(f"   - Mean: {employment_data.mean():,.0f}")
print(f"   - Median: {employment_data.median():,.0f}")

print("\nAVERAGE WEEKLY EARNINGS (CAD):")
print(f"   - Minimum: ${earnings_data.min():,.2f}")

EMPLOYMENT (Number of Persons):
   - Minimum: 1,948
   - Maximum: 162,858
   - Mean: 48,794
   - Median: 35,398

AVERAGE WEEKLY EARNINGS (CAD):
   - Minimum: $688.55

Data Cleaning & Preparation

This section transforms the raw data into a clean, analysis-ready format. We will: - convert date strings to proper datetime format - handle missing values appropriately - create derived variables for analysis - restructure data for easier visualization

Click to expand/collapse code
# Make a copy to preserve original data
df = df_raw.copy()

# Step 1: Convert REF_DATE to datetime format
# The format is 'YYYY-MM' - we'll convert to the first day of each month
df['Date'] = pd.to_datetime(df['REF_DATE'] + '-01')
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['Month_Name'] = df['Date'].dt.strftime('%b')
df['YearMonth'] = df['Date'].dt.strftime('%Y-%m')

# Step 2: Create short industry names for easier visualization
industry_short_names = {
    'Motor vehicle manufacturing [3361]': 'Vehicle Manufacturing',
    'Motor vehicle body and trailer manufacturing [3362]': 'Body & Trailer Mfg',
    'Motor vehicle parts manufacturing [3363]': 'Parts Manufacturing',
    'Motor vehicle merchant wholesalers [4151]': 'Vehicle Wholesalers',
    'New motor vehicle parts and accessories merchant wholesalers [4152]': 'New Parts Wholesalers',
    'Used motor vehicle parts and accessories merchant wholesalers [4153]': 'Used Parts Wholesalers',
    'Automobile dealers [4411]': 'Auto Dealers',
    'Other motor vehicle dealers [4412]': 'Other Vehicle Dealers',
    'Automotive parts, accessories and tire retailers [4413]': 'Parts & Tire Retailers',
    'Automotive equipment rental and leasing [5321]': 'Vehicle Rental',
    'Automotive repair and maintenance [8111]': 'Auto Repair & Service'
}

df['Industry_Short'] = df['North American Industry Classification System (NAICS)'].map(industry_short_names)

# Step 3: Create sector categories for grouping
def categorize_sector(naics):
    if '336' in naics:
        return 'Manufacturing'
    elif '415' in naics:
        return 'Wholesale'
    elif '441' in naics:
        return 'Retail'
    else:
        return 'Services'

df['Sector'] = df['North American Industry Classification System (NAICS)'].apply(categorize_sector)

# Step 4: Create short estimate names
df['Estimate_Short'] = df['Estimate'].map({
    'Employment for all employees': 'Employment',
    'Average weekly earnings including overtime for all employees': 'Avg Weekly Earnings'
})

print("\nDATA CLEANING COMPLETED")

print("\nNew columns added:")
print("   • Date - Proper datetime format")
print("   • Year, Month, Month_Name - Date components")
print("   • Industry_Short - Abbreviated industry names")
print("   • Sector - Industry sector category")
print("   • Estimate_Short - Abbreviated estimate names")

DATA CLEANING COMPLETED

New columns added:
   • Date - Proper datetime format
   • Year, Month, Month_Name - Date components
   • Industry_Short - Abbreviated industry names
   • Sector - Industry sector category
   • Estimate_Short - Abbreviated estimate names

Handling missing values

Missing values (F status) represent data too unreliable to publish. We’ll flag these but keep them as NaN to maintain data integrity.

Click to expand/collapse code
# Count missing values by estimate type and industry
missing_by_estimate = df[df['VALUE'].isnull()].groupby('Estimate_Short').size()
print("\nMissing values by estimate type:")
print(missing_by_estimate.to_string())

missing_by_industry = df[df['VALUE'].isnull()].groupby('Industry_Short').size()
print("\nMissing values by industry:")
print(missing_by_industry.to_string())

# Create a clean dataset excluding unreliable data (F status)
# But keep the original for reference
df_clean = df[df['STATUS'] != 'F'].copy()

print(f"\nDataset Summary:")
print(f"   - Original records: {len(df):,}")
print(f"   - Clean records (excluding F status): {len(df_clean):,}")
print(f"   - Records removed: {len(df) - len(df_clean):,}")

Missing values by estimate type:
Estimate_Short
Avg Weekly Earnings    82
Employment              1

Missing values by industry:
Industry_Short
Body & Trailer Mfg         1
New Parts Wholesalers      1
Used Parts Wholesalers    79
Vehicle Manufacturing      1
Vehicle Rental             1

Dataset Summary:
   - Original records: 2,838
   - Clean records (excluding F status): 2,755
   - Records removed: 83

Create Pivot Tables

We’ll restructure data into analysis-friendly formats.

Click to expand/collapse code
# Employment data pivot
df_employment = df_clean[df_clean['Estimate_Short'] == 'Employment'].pivot_table(
    index='Date',
    columns='Industry_Short',
    values='VALUE',
    aggfunc='first'
).reset_index()

# Earnings data pivot
df_earnings = df_clean[df_clean['Estimate_Short'] == 'Avg Weekly Earnings'].pivot_table(
    index='Date',
    columns='Industry_Short',
    values='VALUE',
    aggfunc='first'
).reset_index()

# Sector-level aggregations
df_sector = df_clean.groupby(['Date', 'Sector', 'Estimate_Short'])['VALUE'].sum().reset_index()

print("\nPIVOT TABLES CREATED")

print(f"   • Employment matrix: {df_employment.shape[0]} time periods × {df_employment.shape[1]-1} industries")
print(f"   • Earnings matrix: {df_earnings.shape[0]} time periods × {df_earnings.shape[1]-1} industries")
print(f"   • Sector aggregation: {len(df_sector):,} records")

PIVOT TABLES CREATED
   • Employment matrix: 129 time periods × 11 industries
   • Earnings matrix: 129 time periods × 11 industries
   • Sector aggregation: 1,032 records
Click to expand/collapse code
# Save the main cleaned dataset
df_clean.to_csv('CanadaMotorEmplData_cleaned.csv', index=False)

print("\nCLEANED DATA EXPORTED")

print("   • File: CanadaMotorEmplData_cleaned.csv")
print(f"   • Records: {len(df_clean):,}")
print(f"   • Columns: {len(df_clean.columns)}")

CLEANED DATA EXPORTED
   • File: CanadaMotorEmplData_cleaned.csv
   • Records: 2,755
   • Columns: 24

Exploratory Data Analysis

This section provides comprehensive statistical summaries and identifies patterns, trends and anomalies in the data.

Click to expand/collapse code
# Employment statistics by industry
emp_stats = df_clean[df_clean['Estimate_Short'] == 'Employment'].groupby('Industry_Short')['VALUE'].agg([
    ('Mean', 'mean'),
    ('Median', 'median'),
    ('Std Dev', 'std'),
    ('Min', 'min'),
    ('Max', 'max')
]).round(0)

print("\nEMPLOYMENT STATISTICS BY INDUSTRY (Number of Employees)")
print(emp_stats.to_string())

print("\n")

# Earnings statistics by industry
earn_stats = df_clean[df_clean['Estimate_Short'] == 'Avg Weekly Earnings'].groupby('Industry_Short')['VALUE'].agg([
    ('Mean', 'mean'),
    ('Median', 'median'),
    ('Std Dev', 'std'),
    ('Min', 'min'),
    ('Max', 'max')
]).round(2)

print("\nAVERAGE WEEKLY EARNINGS BY INDUSTRY (CAD)")
print(earn_stats.to_string())

EMPLOYMENT STATISTICS BY INDUSTRY (Number of Employees)
                             Mean     Median  Std Dev       Min        Max
Industry_Short                                                            
Auto Dealers           152,491.00 152,487.00 9,027.00 99,924.00 162,858.00
Auto Repair & Service  111,295.00 110,659.00 7,464.00 85,974.00 124,960.00
Body & Trailer Mfg      15,570.00  16,110.00 2,022.00 11,852.00  18,495.00
New Parts Wholesalers   36,021.00  36,527.00 2,128.00 30,838.00  40,572.00
Other Vehicle Dealers   23,104.00  23,342.00 1,418.00 14,623.00  25,194.00
Parts & Tire Retailers  40,503.00  40,374.00 2,262.00 33,895.00  43,891.00
Parts Manufacturing     71,783.00  72,211.00 3,519.00 53,021.00  77,171.00
Used Parts Wholesalers   2,506.00   2,544.00   224.00  1,948.00   2,942.00
Vehicle Manufacturing   40,159.00  39,102.00 3,793.00 30,937.00  46,345.00
Vehicle Rental          19,751.00  20,119.00 2,430.00 12,225.00  22,648.00
Vehicle Wholesalers     23,486.00  23,602.00   939.00 20,866.00  25,166.00



AVERAGE WEEKLY EARNINGS BY INDUSTRY (CAD)
                           Mean   Median  Std Dev      Min      Max
Industry_Short                                                     
Auto Dealers           1,189.60 1,137.68   138.17   994.90 1,454.46
Auto Repair & Service    951.84   939.04   111.75   769.10 1,176.74
Body & Trailer Mfg     1,090.47 1,084.04   135.19   822.44 1,427.20
New Parts Wholesalers  1,116.58 1,087.86   114.05   898.60 1,412.20
Other Vehicle Dealers  1,004.85   964.22   124.91   821.13 1,234.90
Parts & Tire Retailers   905.00   894.65   103.35   732.11 1,117.18
Parts Manufacturing    1,165.53 1,154.33   132.06   929.79 1,433.61
Used Parts Wholesalers   953.89   956.46   175.16   688.55 1,308.32
Vehicle Manufacturing  1,528.40 1,470.52   211.35 1,035.84 2,364.18
Vehicle Rental           969.55   955.44   161.19   691.71 1,280.97
Vehicle Wholesalers    1,366.15 1,360.35   115.65 1,100.25 1,652.65
Click to expand/collapse code
# Specific analysis of the pandemic period (2020-2021).

print("\nCOVID-19 IMPACT ANALYSIS")

# Pre-COVID baseline (2019 average)
pre_covid = df_clean[(df_clean['Year'] == 2019) & (df_clean['Estimate_Short'] == 'Employment')].groupby('Industry_Short')['VALUE'].mean()

# COVID low point (April-May 2020 average)
covid_low = df_clean[(df_clean['Date'] >= '2020-04-01') & (df_clean['Date'] <= '2020-05-31') & 
                     (df_clean['Estimate_Short'] == 'Employment')].groupby('Industry_Short')['VALUE'].mean()

# Latest period (2025)
latest = df_clean[(df_clean['Year'] == 2025) & (df_clean['Estimate_Short'] == 'Employment')].groupby('Industry_Short')['VALUE'].mean()

# Create comparison table
covid_impact = pd.DataFrame({
    'Pre-COVID (2019)': pre_covid,
    'COVID Low (Apr-May 2020)': covid_low,
    'Latest (2025)': latest
})
covid_impact['COVID Drop %'] = ((covid_impact['COVID Low (Apr-May 2020)'] - covid_impact['Pre-COVID (2019)']) / 
                                 covid_impact['Pre-COVID (2019)'] * 100).round(1)
covid_impact['Recovery to 2019 %'] = ((covid_impact['Latest (2025)'] / covid_impact['Pre-COVID (2019)'] - 1) * 100).round(1)

print("\nEmployment Impact by Industry:")
print(covid_impact.round(0).to_string())

COVID-19 IMPACT ANALYSIS

Employment Impact by Industry:
                        Pre-COVID (2019)  COVID Low (Apr-May 2020)  Latest (2025)  COVID Drop %  Recovery to 2019 %
Industry_Short                                                                                                     
Auto Dealers                  161,842.00                102,254.00     159,939.00        -37.00               -1.00
Auto Repair & Service         113,787.00                 86,388.00     124,255.00        -24.00                9.00
Body & Trailer Mfg             16,058.00                 12,659.00      18,279.00        -21.00               14.00
New Parts Wholesalers          37,603.00                 30,966.00      33,668.00        -18.00              -10.00
Other Vehicle Dealers          23,593.00                 15,644.00      23,021.00        -34.00               -2.00
Parts & Tire Retailers         42,148.00                 34,116.00      43,661.00        -19.00                4.00
Parts Manufacturing            74,297.00                 53,682.00      68,623.00        -28.00               -8.00
Used Parts Wholesalers          2,615.00                  2,028.00       2,398.00        -22.00               -8.00
Vehicle Manufacturing          44,177.00                 32,385.00      35,525.00        -27.00              -20.00
Vehicle Rental                 22,110.00                 13,034.00      19,887.00        -41.00              -10.00
Vehicle Wholesalers            24,817.00                 21,288.00      23,945.00        -14.00               -4.00

Key Metrics & Business KPIs

This section presents the most important performance indicators to understand the health of Canada’s motor vehicle industry.

Click to expand/collapse code
print("\nKEY PERFORMANCE INDICATORS")

# KPI 1: Total Industry Employment (Latest Month)
latest_date = df_clean['Date'].max()
latest_employment = df_clean[(df_clean['Date'] == latest_date) & 
                              (df_clean['Estimate_Short'] == 'Employment')]['VALUE'].sum()
print(f"\n1️⃣ TOTAL INDUSTRY EMPLOYMENT ({latest_date.strftime('%B %Y')})")
print(f"   {latest_employment:,.0f} employees across all motor vehicle industries")

# KPI 2: Year-over-Year Employment Change
yoy_date = latest_date - pd.DateOffset(years=1)
yoy_employment = df_clean[(df_clean['Date'] == yoy_date) & 
                           (df_clean['Estimate_Short'] == 'Employment')]['VALUE'].sum()
yoy_change = ((latest_employment - yoy_employment) / yoy_employment * 100)
print(f"\n2️⃣ YEAR-OVER-YEAR EMPLOYMENT CHANGE")
print(f"   {yoy_change:+.1f}% compared to {yoy_date.strftime('%B %Y')}")

# KPI 3: Average Weekly Earnings (Latest Month)
latest_earnings = df_clean[(df_clean['Date'] == latest_date) & 
                            (df_clean['Estimate_Short'] == 'Avg Weekly Earnings')]['VALUE'].mean()
print(f"\n3️⃣ AVERAGE WEEKLY EARNINGS ({latest_date.strftime('%B %Y')})")
print(f"   ${latest_earnings:,.2f} per week (industry average)")

# KPI 4: Wage Growth Since 2015
first_date = df_clean['Date'].min()
first_earnings = df_clean[(df_clean['Date'] == first_date) & 
                           (df_clean['Estimate_Short'] == 'Avg Weekly Earnings')]['VALUE'].mean()
wage_growth = ((latest_earnings - first_earnings) / first_earnings * 100)
print(f"\n4️⃣ WAGE GROWTH SINCE 2015")
print(f"   {wage_growth:+.1f}% cumulative growth in average weekly earnings")

# KPI 5: Largest Employer
largest_employer = df_clean[(df_clean['Date'] == latest_date) & 
                             (df_clean['Estimate_Short'] == 'Employment')].nlargest(1, 'VALUE')
print(f"\n5️⃣ LARGEST EMPLOYER")
print(f"   {largest_employer['Industry_Short'].values[0]}: {largest_employer['VALUE'].values[0]:,.0f} employees")

# KPI 6: Highest Paying Industry
highest_paying = df_clean[(df_clean['Date'] == latest_date) & 
                           (df_clean['Estimate_Short'] == 'Avg Weekly Earnings')].nlargest(1, 'VALUE')
print(f"\n6️⃣ HIGHEST PAYING INDUSTRY")
print(f"   {highest_paying['Industry_Short'].values[0]}: ${highest_paying['VALUE'].values[0]:,.2f}/week")

KEY PERFORMANCE INDICATORS

1️⃣ TOTAL INDUSTRY EMPLOYMENT (September 2025)
   550,951 employees across all motor vehicle industries

2️⃣ YEAR-OVER-YEAR EMPLOYMENT CHANGE
   -0.4% compared to September 2024

3️⃣ AVERAGE WEEKLY EARNINGS (September 2025)
   $1,348.17 per week (industry average)

4️⃣ WAGE GROWTH SINCE 2015
   +40.0% cumulative growth in average weekly earnings

5️⃣ LARGEST EMPLOYER
   Auto Dealers: 159,984 employees

6️⃣ HIGHEST PAYING INDUSTRY
   Vehicle Manufacturing: $1,880.36/week
Click to expand/collapse code
print("\nSECTOR-LEVEL PERFORMANCE")


sector_kpis = df_clean[df_clean['Date'] == latest_date].groupby(['Sector', 'Estimate_Short'])['VALUE'].sum().unstack()
sector_kpis.columns = ['Employment', 'Avg Weekly Earnings']
sector_kpis['Employment Share %'] = (sector_kpis['Employment'] / sector_kpis['Employment'].sum() * 100).round(1)

print(f"\nSector Distribution ({latest_date.strftime('%B %Y')}):\n")
print(sector_kpis.to_string())

SECTOR-LEVEL PERFORMANCE

Sector Distribution (September 2025):

               Employment  Avg Weekly Earnings  Employment Share %
Sector                                                            
Manufacturing    4,637.08           120,031.00               34.40
Retail           3,711.66           226,769.00               27.50
Services         2,329.37           144,398.00               17.30
Wholesale        2,803.64            59,753.00               20.80

Visual Insights

Click to expand/collapse code
# This chart shows the evolution of employment across all industries over time.
# Key events like COVID-19 are highlighted.

# Prepare data for time series
emp_time_series = df_clean[df_clean['Estimate_Short'] == 'Employment'].copy()

# Create the figure
fig1 = px.line(
    emp_time_series,
    x='Date',
    y='VALUE',
    color='Industry_Short',
    title='<b>Employment Trends in Canada\'s Motor Vehicle Industry</b><br><sup>Monthly employment by industry segment (2015-2025)<br>👆 Click legend items to show/hide | Double-click to isolate</sup>',
    labels={'VALUE': 'Number of Employees', 'Date': '', 'Industry_Short': 'Industry'},
    template='plotly_white'
)

# Add COVID-19 annotation
fig1.add_vrect(
    x0='2020-03-01', x1='2020-06-01',
    fillcolor='red', opacity=0.1,
    layer='below', line_width=0,
    annotation_text='COVID-19 Lockdowns',
    annotation_position='top left'
)

# Add quick filter buttons
industries = sorted(emp_time_series['Industry_Short'].unique())
buttons = [
    dict(label="✓ All", method="restyle", args=["visible", [True] * len(industries)]),
    dict(label="✗ None", method="restyle", args=["visible", ["legendonly"] * len(industries)]),
]

# Mobile-friendly layout
fig1.update_layout(
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            buttons=buttons,
            pad={"r": 10, "t": 50},
            showactive=False,
            x=0.01,
            xanchor="left",
            y=1.15,
            yanchor="top",
            bgcolor="#F0F0F0",
            bordercolor="#CCCCCC",
            borderwidth=1
        )
    ],
    height=600,
    margin=dict(l=20, r=20, t=100, b=20),
    legend=dict(
        orientation='h',
        yanchor='bottom',
        y=-0.4,
        xanchor='center',
        x=0.5,
        font=dict(size=10),
        itemclick="toggle",
        itemdoubleclick="toggleothers",
        # Make legend look like checkboxes
        bgcolor="rgba(255,255,255,0.8)",
        bordercolor="#CCCCCC",
        borderwidth=1
    ),
    hovermode='x unified'
)

fig1.update_traces(line=dict(width=2))
fig1.update_yaxes(tickformat=',')

fig1.show()

Key Insights for Earnings Growth

  • Vehicle Manufacturing offers the highest wages in the sector, with average weekly earnings reaching almost $1,900 by 2025. This reflects the skilled nature of assembly line work and strong union representation. The peak in earnings was in Dec 2020, likely due to hazard pay during the pandemic.

  • Most industries show upward wage trends, with accelerated growth since 2022 driven by inflation and labour market tightening.

  • The Wholesale industry has erratic wage patterns, with fluctuations that do not align with broader economic trends. This volatility likely reflects commission-based compensation structures tied to vehicle sales volumes. Despite volatility, the overall trend remains upward.

  • The Parts and Tire Retailers and Auto Repair and Service industries currently offer the lowest wages, but have seen consistent growth over the past decade.

Click to expand/collapse code
# Side-by-side comparison of employment and earnings by sector.

# Prepare latest data
latest_data = df_clean[df_clean['Date'] == latest_date].copy()

# Employment by industry
emp_latest = latest_data[latest_data['Estimate_Short'] == 'Employment'].sort_values('VALUE', ascending=True)

fig3 = px.bar(
    emp_latest,
    x='VALUE',
    y='Industry_Short',
    color='Sector',
    orientation='h',
    title=f'<b>Employment by Industry</b><br><sup>{latest_date.strftime("%B %Y")} - Total Employees</sup>',
    labels={'VALUE': 'Number of Employees', 'Industry_Short': ''},
    template='plotly_white',
    color_discrete_map={
        'Manufacturing': '#1f77b4',
        'Wholesale': '#ff7f0e',
        'Retail': '#2ca02c',
        'Services': '#d62728'
    }
)

fig3.update_layout(
    height=500,
    margin=dict(l=20, r=20, t=80, b=20),
    legend=dict(
        orientation='h',
        yanchor='bottom',
        y=-0.2,
        xanchor='center',
        x=0.5
    ),
    xaxis=dict(tickformat=',')
)

fig3.show()

Key Insights for Employment Numbers for September 2025

  • Retail dominates total employment and accounts for the largest share. auto dealers alone employ approximately 160,000 workers. Combined retail employment totals over 225,000, representing roughly 40% of the entire sector.

  • Services sector is a major job provider. It is the second-largest employer at approximately 125,000 workers. This after segment provides essential ongoing vehicle maintenance jobs that are less susceptible to manufacturing automation. Combined with Vehicle Rental, the services sector employs close to 145,000 Canadians.

  • Despite it’s high profile, total manufacturing employment is close to 120,000 workers. Parts manufacturing employs nearly twice as many workers as Vehicle manufacturing, reflecting that vehicle assembly might be highly automated, while parts production remains more labour-intensive.

  • Wholesale is the smallest segment, employing only about 60,000 workers.

Click to expand/collapse code
earn_latest = latest_data[latest_data['Estimate_Short'] == 'Avg Weekly Earnings'].sort_values('VALUE', ascending=True)

fig4 = px.bar(
    earn_latest,
    x='VALUE',
    y='Industry_Short',
    color='Sector',
    orientation='h',
    title=f'<b>Average Weekly Earnings by Industry</b><br><sup>{latest_date.strftime("%B %Y")} - Including Overtime</sup>',
    labels={'VALUE': 'Avg Weekly Earnings (CAD)', 'Industry_Short': ''},
    template='plotly_white',
    color_discrete_map={
        'Manufacturing': '#1f77b4',
        'Wholesale': '#ff7f0e',
        'Retail': '#2ca02c',
        'Services': '#d62728'
    }
)

fig4.update_layout(
    height=500,
    margin=dict(l=20, r=20, t=80, b=20),
    legend=dict(
        orientation='h',
        yanchor='bottom',
        y=-0.2,
        xanchor='center',
        x=0.5
    ),
    xaxis=dict(tickprefix='$', tickformat=',')
)

fig4.show()

Key Insights for Weekly Wages for September 2025

  • Manufacturing commands premium wages. The sector occupies the top three positions for highest earnings.

  • Significant wage gap across sectors. The gap between the highest and lowest paying industries is over $800/week. Vehicle Manufacturing workers earn nearly 75% more than Parts & Tire Retailers workers. This wage disparity highlights the “two Canadas” within the same industry ecosystem — those in production vs. those in retail.

  • Retail shows internal wage variation. Auto dealer wages are elevated by commission-based compensation for sales staff.

  • Wholesale Punches Above Its Weight. Vehicle Wholesalers earn over $1,500/week — higher than most retail positions. This is the second-highest paying non-manufacturing industry, reflecting specialized knowledge requirements for B2B vehicle transactions.

Click to expand/collapse code
# Shows employment as a percentage of pre-COVID levels.

# Calculate index (2019 average = 100)
baseline_2019 = df_clean[(df_clean['Year'] == 2019) & 
                          (df_clean['Estimate_Short'] == 'Employment')].groupby('Industry_Short')['VALUE'].mean()

# Monthly data indexed to 2019
emp_indexed = df_clean[df_clean['Estimate_Short'] == 'Employment'].copy()
emp_indexed['Index_Value'] = emp_indexed.apply(
    lambda x: (x['VALUE'] / baseline_2019.get(x['Industry_Short'], 1)) * 100 
    if pd.notna(x['VALUE']) else np.nan, axis=1
)

fig5 = px.line(
    emp_indexed,
    x='Date',
    y='Index_Value',
    color='Industry_Short',
    title='<b>COVID-19 Recovery Index</b><br><sup>Employment as % of 2019 baseline (100 = 2019 average)<br>👆 Click legend items to show/hide | Double-click to isolate</sup>',
    labels={'Index_Value': 'Employment Index', 'Date': '', 'Industry_Short': 'Industry'},
    template='plotly_white'
)

# Add baseline reference line
fig5.add_hline(y=100, line_dash='dash', line_color='gray', 
               annotation_text='2019 Baseline', annotation_position='bottom right')

# Add COVID shading
fig5.add_vrect(
    x0='2020-03-01', x1='2020-06-01',
    fillcolor='red', opacity=0.1,
    layer='below', line_width=0
)

# Add quick filter buttons
industries = sorted(emp_indexed['Industry_Short'].unique())
buttons = [
    dict(label="✓ All", method="restyle", args=["visible", [True] * len(industries)]),
    dict(label="✗ None", method="restyle", args=["visible", ["legendonly"] * len(industries)]),
]

fig5.update_layout(
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            buttons=buttons,
            pad={"r": 10, "t": 45},
            showactive=False,
            x=0.01,
            xanchor="left",
            y=1.15,
            yanchor="top",
            bgcolor="#F0F0F0",
            bordercolor="#CCCCCC",
            borderwidth=1
        )
    ],
    height=600,
    margin=dict(l=20, r=20, t=100, b=20),
    legend=dict(
        orientation='h',
        yanchor='bottom',
        y=-0.4,
        xanchor='center',
        x=0.5,
        font=dict(size=10),
        itemclick="toggle",
        itemdoubleclick="toggleothers",
        # Make legend look like checkboxes
        bgcolor="rgba(255,255,255,0.8)",
        bordercolor="#CCCCCC",
        borderwidth=1
    ),
    hovermode='x unified'
)

fig5.update_traces(line=dict(width=2))

fig5.show()

Key Insights for COVID-19 Recovery

  • Recovery is deeply uneven since some industries thrive above 2019 levels while others remain permanently diminished. Body & Trailer manufacturing has grown significantly while all other manufacturing segments have seen only partial recovery. Vehicle manufacturing remains the furthest from pre-pandemic employment levels with a sharp decline.

  • The COVID crash was historic in depth. During the April-May 2020 lockdowns, several industries plunged to 55-65% of their 2019 workforce. This represents a 35-45% workforce reduction in mere weeks — unprecedented in modern Canadian economic history. The synchronized nature of the collapse demonstrates the sector’s vulnerability to supply chain disruptions.

  • Manufacturing has seen permanent job losses with the industry facing major job losses post pandemic. This could worsen due to the Trump tariffs and supply chain shifts.

  • Vehicle Rental remains structurally impaired and is among the worst performers. This reflects permanent changes in business travel and car-sharing behavior post-pandemic. The industry may have reached a “new normal” with a smaller workforce footprint going forward.

Click to expand/collapse code
# Year-over-year percentage changes by industry.

# Calculate year-over-year changes
annual_avg = df_clean[df_clean['Estimate_Short'] == 'Employment'].groupby(
    ['Year', 'Industry_Short'])['VALUE'].mean().unstack()
yoy_pct_change = annual_avg.pct_change() * 100

fig6 = px.imshow(
    yoy_pct_change.T.round(1),
    labels=dict(x='Year', y='Industry', color='YoY Change %'),
    title='<b>Year-over-Year Employment Change (%)</b><br><sup>Annual average employment change by industry</sup>',
    color_continuous_scale='RdYlGn',
    color_continuous_midpoint=0,
    aspect='auto',
    text_auto=True
)

fig6.update_layout(
    height=500,
    margin=dict(l=20, r=20, t=80, b=20)
)

fig6.show()

Key Insights for YoY Change

  • 2020 was universally devastating, showing negative growth across all 11 industries.

  • Vehicle rental suffered the worst impact with a -27.6% decline in 2020, followed by Vehicle Manufacturing and Used Parts Wholesalers. The rental collapse reflects the immediate halt of travel and tourism - an industry with zero buffer against demand shocks.

  • 2021 rebound was strong but uneven. Body & Trailer Manufacturing and Other Vehicle Dealers were standout performers.

  • Warning Signs in 2024-2025 as several industries show renewed decline in 2024-2025 with new headwinds such as automation and tariffs.

Click to expand/collapse code
# Distribution of employment by sector.

sector_emp = latest_data[latest_data['Estimate_Short'] == 'Employment'].groupby('Sector')['VALUE'].sum().reset_index()

fig7 = px.pie(
    sector_emp,
    values='VALUE',
    names='Sector',
    title=f'<b>Employment Distribution by Sector</b><br><sup>{latest_date.strftime("%B %Y")}</sup>',
    template='plotly_white',
    color='Sector',
    color_discrete_map={
        'Manufacturing': '#1f77b4',
        'Wholesale': '#ff7f0e',
        'Retail': '#2ca02c',
        'Services': '#d62728'
    },
    hole=0.4
)

fig7.update_traces(
    textposition='outside',
    textinfo='percent+label',
    hovertemplate='<b>%{label}</b><br>Employees: %{value:,.0f}<br>Share: %{percent}<extra></extra>'
)

fig7.update_layout(
    height=500,
    margin=dict(l=20, r=20, t=80, b=20),
    annotations=[dict(text=f'{sector_emp["VALUE"].sum():,.0f}<br>Total', x=0.5, y=0.5, font_size=14, showarrow=False)]
)

fig7.show()

Key Insights for Employment Distribution by Sector

  • Retail is the dominant employer, accounting for 41.2% of all motor vehicle industry employment (~227,000 workers). This challenges the common perception that the “auto industry” is primarily about manufacturing. For every one manufacturing worker, there are nearly two retail workers in Canada’s motor vehicle ecosystem.

  • Manufacturing represents less than a quarter of the jobs, accounting for only 21.8% of sector employment. Despite receiving significant policy attention and investment incentives, manufacturing provides fewer jobs than services. This reflects decades of automation, productivity gains, and global supply chain restructuring.

  • Service sector is a quiet giant. It is the second-largest employer with approximately 144,000 workers. This includes auto-repair technicians, service advisors, and rental agents - roles that are essential and difficult to automate or offshore. services employment tends to be more geographically distributed across Canada, unlike manufacturing concentrated in Ontario.

  • Wholesale is Lean and Efficient. Wholesale represents only 10.8% of employment (~60,000 workers). This small share reflects the efficiency of modern distribution - moving billions in inventory with minimal workforce.

Click to expand/collapse code
# Comparing wage growth rates across industries since 2015.

# Calculate wage growth from 2015 to latest
first_earnings = df_clean[(df_clean['Date'] == first_date) & 
                           (df_clean['Estimate_Short'] == 'Avg Weekly Earnings')].set_index('Industry_Short')['VALUE']
latest_earnings_ind = df_clean[(df_clean['Date'] == latest_date) & 
                                (df_clean['Estimate_Short'] == 'Avg Weekly Earnings')].set_index('Industry_Short')['VALUE']

wage_growth_pct = ((latest_earnings_ind - first_earnings) / first_earnings * 100).sort_values(ascending=True)

fig8 = go.Figure()

fig8.add_trace(go.Bar(
    x=wage_growth_pct.values,
    y=wage_growth_pct.index,
    orientation='h',
    marker=dict(
        color=wage_growth_pct.values,
        colorscale='Greens',
        showscale=True,
        colorbar=dict(title='Growth %')
    ),
    text=[f'{v:.1f}%' for v in wage_growth_pct.values],
    textposition='outside'
))

fig8.update_layout(
    title=f'<b>Cumulative Wage Growth Since 2015</b><br><sup>Percentage change in average weekly earnings ({first_date.strftime("%b %Y")} to {latest_date.strftime("%b %Y")})</sup>',
    template='plotly_white',
    height=500,
    margin=dict(l=20, r=100, t=80, b=20),
    xaxis=dict(title='Cumulative Wage Growth (%)', ticksuffix='%'),
    yaxis=dict(title='')
)

fig8.show()

Key Insights for Cumulative Wage Growth (2015-2025)

  • Vehicle Rental leads in Wage Growth despite employment struggles. Vehicle rental shows the highest wage growth at 61.9% - a remarkable finding given this sector has the worst employment recovery. This paradox suggests that the fewer remaining workers are being paid significantly more, likely due to labour shortages and retention challenges. Workers who stayed in vehicle rental have benefited substantially from tight labour markets.

  • Parts manufacturing shows strong wage growth amid job losses, achieving 47% wage growth - the second highest in the sector. Combined with showing earlier data showing 15% permanent employment decline, this indicates surviving jobs are higher-skilled and better compensated. The industry apppears to be shedding lower-wage positions while retaining/creating higher-value roles.

  • Wholesale sector shows weakest wage growth. Vehicle Wholesalers (17%) and New Parts Wholesalers (25.7%) show the lowest wage growth. This may reflect already-high base wages, commission-based structures with less inflation adjustment, or competitive pricing pressures.

  • Lower wage industries saw faster growth, signifying wage compression in the sector. Auto Repair & Service and Parts & Tire Retailers — traditionally lower-wage industries — showed above-average growth. Meanwhile, Vehicle Manufacturing — the highest-paying industry — saw more modest growth. This indicates wage compression: the gap between highest and lowest paid workers is narrowing, driven by minimum wage increases and competitive hiring at the lower end.