Data Visualization & Interactive Dashboard Specialization
How does an economic recession fundamentally restructure the automobile market? This project, completed for the IBM Data Visualization with Python certification, is a two-part study designed to answer that question.
Instead of looking at aggregate declines, I engineered an interactive dashboard to investigate how recessions shift consumer preferences, advertising strategies, and segment-level resilience across four decades of historical data.
| # | Period |
|---|---|
| 1 | 1980 |
| 2 | 1981 – 1982 |
| 3 | 1991 |
| 4 | 2000 – 2001 |
| 5 | End 2007 – Mid 2009 (Global Financial Crisis) |
| 6 | Feb – Apr 2020 (COVID-19 Impact) |
| Library | Role |
|---|---|
pandas |
Data loading, filtering, groupby aggregations |
numpy |
Mathematical operations |
plotly.express |
Interactive chart generation (line, bar, pie) |
dash |
Web framework for the interactive dashboard |
dash.dependencies |
Callback wiring (Input/Output reactivity) |
matplotlib |
Core plotting engine for Part 1 EDA |
seaborn |
Enhanced statistical visualizations — barplots, lineplots, scatterplots |
folium |
Interactive geospatial choropleth map |
pip install pandas numpy matplotlib seaborn folium requests dash plotlyimport numpy as np
import pandas as pd
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import foliumimport requests
import io
URL = "https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/d51iMGfp_t0QpO30Lym-dw/automobile-sales.csv"
response = requests.get(URL)
response.raise_for_status()
csv_content = io.StringIO(response.text)
df = pd.read_csv(csv_content)
print('Data downloaded and read into a dataframe!')
print(df.head())| Column | Description |
|---|---|
Date |
Month-end date of sales observation |
Recession |
Binary flag: 1 = recession, 0 = normal |
Automobile_Sales |
Number of vehicles sold during the period |
GDP |
Per capita GDP value in USD |
unemployment_rate |
Monthly unemployment rate |
Consumer_Confidence |
Synthetic index representing consumer sentiment |
Seasonality_Weight |
Seasonal effect multiplier (>1 = above average, <1 = below average) |
Price |
Average vehicle price during the period |
Advertising_Expenditure |
Company advertising spend |
Vehicle_Type |
Segment: SuperMiniCar, SmallFamilyCar, MediumFamilyCar, ExecutiveCar, Sports |
Competition |
Market competition measure |
Month |
Month extracted from Date |
Year |
Year extracted from Date |
City |
Company office/sales location |
Before building the interactive dashboard, I conducted a deep-dive EDA to establish the "Stress Test" patterns of the automobile industry.
| Question | Technical Approach | Key Insight |
|---|---|---|
| Sales Volatility | df.groupby('Year')['Automobile_Sales'].mean() |
Identified clear sales troughs perfectly aligned with 1982, 1991, and 2008 recessions |
| Segment Resilience | Grouped bar charts (Recession vs. Normal) | Executive & Sports vehicles collapsed by ~75%, while SuperMiniCars showed the highest demand retention |
| Ad Strategy | Pie chart budget allocation | Manufacturers concentrated ad spend on economy family segments during downturns to maximize ROI |
| Macro Stress | Subplotting GDP & Unemployment | Sales remain stable until unemployment hits a 4–5% threshold, after which premium segments decline sharply |
Question: How have average automobile sales fluctuated from year to year?
w, h = 9, 6
# Basic line chart
plt.figure(figsize=(w, h))
avg_auto_sales = df.groupby("Year")["Automobile_Sales"].mean()
avg_auto_sales.plot(x=df.index.values)
plt.title("Automobile Sales during Recession")
plt.xlabel("Year")
plt.ylabel("Automobile Sales")
plt.show()# Enhanced version with year ticks and recession annotations
plt.figure(figsize=(w, h))
avg_auto_sales = df.groupby("Year")["Automobile_Sales"].mean()
avg_auto_sales.plot()
plt.title("Automobile Sales during Recession")
plt.xticks(list(range(1980, 2024)), rotation=75)
plt.xlabel("Year")
plt.ylabel("Automobile Sales")
plt.text(1982, 650, '1981-82 Recession')
plt.show()💡 Insight: The long-run trend line reveals clear, cyclical sales dips aligned with known recession years. Annotating the 1981–82 recession visually confirms the dataset's integrity — flagged recession years correspond directly to visible sales troughs. The overall sales volume grows over decades, making recession dips increasingly visible relative to the growing baseline.
Question: How do advertising trends correlate with automobile sales outside of recession years?
# Filter to non-recession periods only
df_non_recess = df[df["Recession"] == 0]
# Aggregate average sales and ad spend per year
df_trends = df_non_recess.groupby("Year", as_index=False).agg(
avg_sales=("Automobile_Sales", "mean"),
avg_ads=("Advertising_Expenditure", "mean")
)
# Dual-line plot
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_trends, x='Year', y='avg_sales',
marker='o', linestyle='-', color='green', label='Automobile Sales')
sns.lineplot(data=df_trends, x='Year', y='avg_ads',
marker='s', linestyle='--', color='blue', label='Advertising Expenditure')
plt.title("Automobile Sales During Outside Recession Period")
plt.xlabel("Year")
plt.ylabel("Value")
plt.legend(["Automobile Sales", "Advertising Expenditure"])
plt.tight_layout()
plt.show()💡 Insight: During non-recession periods, automobile sales are significantly more volatile than advertising expenditure. While there are moments of alignment between the two lines, sales often spike independently — suggesting other factors such as market demand cycles, new model launches, and macroeconomic growth also play major roles. The correlation between advertising spend and sales is inconsistent over time, indicating that advertising alone does not drive sales volume in stable economic conditions.
Question: Which vehicle types are most affected during recessions compared to normal periods?
# Aggregate average sales by recession status and vehicle type
df_sales_vehicle_type = df.groupby(
["Recession", "Vehicle_Type"], as_index=False
)["Automobile_Sales"].mean()
print(df_sales_vehicle_type)Data snapshot — average sales by segment:
| Period | Vehicle Type | Avg Sales |
|---|---|---|
| Non-Recession | ExecutiveCar | 3,332 |
| Non-Recession | MediumFamilyCar | 3,853 |
| Non-Recession | SmallFamilyCar | 4,022 |
| Non-Recession | Sports | 3,431 |
| Non-Recession | SuperMiniCar | 3,754 |
| Recession | ExecutiveCar | 842 |
| Recession | MediumFamilyCar | 1,673 |
| Recession | SmallFamilyCar | 1,663 |
| Recession | Sports | 815 |
| Recession | SuperMiniCar | 2,019 |
# Grouped bar chart — recession vs. non-recession by vehicle type
plt.figure(figsize=(w, h))
sns.barplot(x="Recession", y="Automobile_Sales",
hue="Vehicle_Type", data=df_sales_vehicle_type)
plt.xticks(ticks=[0, 1], labels=["Non-Recession", "Recession"])
plt.title('Average Automobile Sales during Recession and Non-Recession')
plt.xlabel('Period')
plt.ylabel('Automobile Sales')
plt.show()# Vehicle-wise detailed comparison
plt.figure(figsize=(w, h))
df_veh_type = df.groupby(
["Recession", "Vehicle_Type"], as_index=False
)["Automobile_Sales"].mean()
sns.barplot(x="Recession", y="Automobile_Sales",
hue="Vehicle_Type", data=df_veh_type)
plt.title("Vehicle Sales During Recession and Non-Recession Periods")
plt.xlabel("Period")
plt.ylabel("Automobile Sales")
plt.show()💡 Insight: There is a drastic decline in automobile sales across all segments during recessions. However, the impact is structurally unequal. ExecutiveCar and Sports vehicles suffer the most severe drops — losing roughly 75% of their average non-recession volume — confirming their discretionary-purchase nature. SuperMiniCar shows the strongest recession resilience, consistent with its role as a necessity-grade vehicle for cost-conscious consumers.
Question: How did GDP behave differently during recession and non-recession periods?
# Partition dataset
df_gdp_recess = df[df["Recession"] == 1]
df_gdp_nrecess = df[df["Recession"] == 0]
# Side-by-side subplots
fig = plt.figure(figsize=(12, 8))
ax0 = fig.add_subplot(1, 2, 1)
ax1 = fig.add_subplot(1, 2, 2)
sns.lineplot(x="Year", y="GDP", data=df_gdp_recess,
label="Recession", color="red", ax=ax0)
ax0.set_title("GDP During Recession Period")
ax0.set_xlabel("Year")
ax0.set_ylabel("GDP")
sns.lineplot(x="Year", y="GDP", data=df_gdp_nrecess,
label="Non-Recession", ax=ax1)
ax1.set_title("GDP During Non-Recession Period")
ax1.set_xlabel("Year")
ax1.set_ylabel("GDP")
plt.tight_layout()
plt.show()💡 Insight: GDP tends to be lower and far more volatile during recession periods, with significant fluctuations and sharp uncertainty. Non-recessionary periods show relatively higher GDP levels with a more gradual, upward trend. The juxtaposition of both subplots makes the economic contrast immediately legible: recession GDP is choppy and compressed, while non-recession GDP trends directionally upward over decades.
Question: In which months does seasonality drive higher or lower automobile sales outside of recession periods?
plt.figure(figsize=(w, h))
df_nrecess = df[df["Recession"] == 0]
size = df_nrecess["Seasonality_Weight"] # Bubble size encodes seasonality strength
sns.scatterplot(
x="Month", y="Automobile_Sales",
hue="Seasonality_Weight", legend=False,
data=df_nrecess, size=size
)
plt.title("Seasonality Impact on Automobile Sales")
plt.xlabel("Month")
plt.ylabel("Automobile Sales")
plt.show()💡 Insight: Seasonality alone does not create a consistent, linear effect on automobile sales. However, two months stand out clearly — April and December show notably higher sales volumes, driven by the spring buying season and year-end promotions respectively. The bubble size variation (Seasonality_Weight) confirms that months with larger seasonal weights correspond to elevated sales activity, but the relationship is not perfectly uniform across all non-recession years.
Question: How do consumer confidence levels and vehicle price relate to automobile sales during recessions?
# Plot 1: Consumer Confidence vs. Sales
plt.figure(figsize=(w, h))
df_recess = df[df["Recession"] == 1]
sns.scatterplot(data=df_recess,
x="Consumer_Confidence", y="Automobile_Sales")
plt.title("Consumer Confidence and Automobile Sales during Recessions")
plt.xlabel("Consumer Confidence")
plt.ylabel("Automobile Sales")
plt.show()# Plot 2: Vehicle Price vs. Sales
plt.figure(figsize=(w, h))
df_recess = df[df["Recession"] == 1]
sns.scatterplot(data=df_recess,
x="Price", y="Automobile_Sales")
plt.title("Relationship between Vehicle Price and Sales during Recessions")
plt.xlabel("Vehicle Price")
plt.ylabel("Automobile Sales")
plt.show()💡 Insight: The two scatter plots together tell a clear story about recession consumer behaviour. Higher consumer confidence is positively associated with automobile sales — as sentiment improves, purchase willingness recovers. Conversely, higher vehicle price generally corresponds to lower sales volumes, confirming that affordability is a primary driver during economic downturns. Both psychological (confidence) and economic (price) factors independently influence purchasing decisions and must both be considered in any recession-period sales strategy.
Question: How much of XYZAutomotives' total advertising budget was spent during recession versus non-recession periods?
plt.figure(figsize=(w, h))
df_recess = df[df["Recession"] == 1]
df_nrecess = df[df["Recession"] == 0]
ads_recess = df_recess["Advertising_Expenditure"].sum()
ads_nrecess = df_nrecess["Advertising_Expenditure"].sum()
plt.pie([ads_recess, ads_nrecess], autopct="%1.1f%%", startangle=90)
plt.title("Advertising Expenditure during Recession and Non-Recession Periods")
plt.axis("off")
plt.legend(["Recession", "Non-Recession"], loc="lower right")
plt.show()💡 Insight: XYZAutomotives spent significantly more on advertising during non-recession periods than during recessions — a rational budget decision reflecting the reduced return on ad investment during economic downturns. When consumers are financially constrained, advertising spend has a lower conversion rate, making budget concentration in growth periods the more defensible strategy.
Question: During recessions, which vehicle segments received the most advertising investment?
plt.figure(figsize=(8, 9))
df_recess = df[df["Recession"] == 1]
veh_type_ads = df_recess.groupby(
"Vehicle_Type", as_index=False
)["Advertising_Expenditure"].sum()
sizes = veh_type_ads["Advertising_Expenditure"]
labels = veh_type_ads["Vehicle_Type"]
plt.pie(sizes, autopct="%1.1f%%", startangle=90)
plt.legend(labels)
plt.title("Advertisement Expenditure by Vehicle Type During Recession")
plt.axis("off")
plt.show()💡 Insight: During recession periods, advertising expenditure was concentrated in lower-price-range vehicle segments — particularly SuperMiniCars and SmallFamilyCars — rather than being spread equally across the portfolio. Manufacturers redirected budget toward segments with recession-resilient demand rather than continuing to advertise premium products to financially stressed consumers. The pie chart reveals a company that adjusts its marketing strategy in direct response to macroeconomic signals.
Question: How does rising unemployment during recessions affect automobile sales across different vehicle segments?
plt.figure(figsize=(8, 9))
df_recess = df[df["Recession"] == 1]
sns.lineplot(
data=df_recess,
y="Automobile_Sales",
x="unemployment_rate",
hue="Vehicle_Type",
marker="o"
)
plt.title('Effect of Unemployment Rate on Vehicle Type and Sales')
plt.legend(title="Vehicle Type")
plt.xlabel("Unemployment Rate")
plt.ylabel("Automobile Sales")
plt.tight_layout()
plt.show()💡 Insight: Automobile sales decline broadly as unemployment rates rise during recessions, but the pattern is not uniform. SuperMiniCars, SmallFamilyCars, and MediumFamilyCars exhibit high volatility — their sales fluctuate sharply as unemployment climbs, reflecting the sensitivity of middle-income buyers to job insecurity. ExecutiveCars and Sports vehicles maintain consistently low volumes throughout all unemployment levels, suggesting their buyers had already largely exited the market before unemployment became a notable factor. Beyond approximately 3% unemployment, most segments show accelerating decline.
The EDA findings from Part 1 were translated into a two-mode interactive dashboard built with Plotly Dash, allowing stakeholders to switch contexts between long-run recession impacts and specific yearly performance.
Recession Period Statistics View
Yearly Statistics View
The system uses a UX Guardian callback system — two Dash callbacks working together:
- Recession Mode: Aggregates data across all recession years (1980–2023). The year selector is intelligently disabled to prevent logically invalid queries.
- Yearly Mode: Filters the entire dashboard to a specific year for micro-level analysis, unlocking the year dropdown.
# Callback 1: UX Guardian — controls year dropdown availability
@app.callback(
Output(component_id='select-year', component_property='disabled'),
Input(component_id='dropdown-statistics', component_property='value')
)
def update_input_container(selected_statistics):
if selected_statistics == 'Yearly Statistics':
return False # Enable year selector
else:
return True # Disable during recession analysis
# Callback 2: Analytical engine — renders 4 charts per valid input state
@app.callback(
Output(component_id='output-container', component_property='children'),
[Input(component_id='dropdown-statistics', component_property='value'),
Input(component_id='select-year', component_property='value')]
)
def update_output_container(selected_statistics, input_year):
...| # | Chart | Type | Mode | Strategic Purpose |
|---|---|---|---|---|
| 1 | Average Sales Fluctuation | Line | Recession | Visualizes the "shape of decline" across recession years |
| 2 | Average Vehicles Sold by Type | Bar | Recession | Identifies segment winners and losers per downturn |
| 3 | Total Advertising Expenditure Share | Pie | Recession | Reveals manufacturer investment priorities during stress |
| 4 | Effect of Unemployment Rate | Grouped Bar | Recession | Quantifies economic stress sensitivity per segment |
| 5 | Yearly Average Sales (Full Period) | Line | Yearly | Places selected year in the 40-year historical story |
| 6 | Total Monthly Automobile Sales | Line | Yearly | Surfaces within-year demand peaks (April & December) |
| 7 | Average Vehicles Sold per Type | Bar | Yearly | Snapshot of vehicle segment dominance for the year |
| 8 | Total Advertisement Expenditure | Pie | Yearly | Compares ad-spend philosophy vs. actual sales mix |
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import pandas as pd
import plotly.express as px
# Load the dataset
data = pd.read_csv('https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/IBMDeveloperSkillsNetwork-DV0101EN-SkillsNetwork/Data%20Files/historical_automobile_sales.csv')
# Initialize the Dash app
app = dash.Dash(__name__)
app.layout = html.Div([
html.H1("Automobile Sales Statistics Dashboard",
style={'textAlign': 'center', 'color': '#503D36', 'font-size': 24}),
html.Div([
html.Label("Select Statistics:"),
dcc.Dropdown(
id='dropdown-statistics',
options=[
{'label': 'Yearly Statistics', 'value': 'Yearly Statistics'},
{'label': 'Recession Period Statistics', 'value': 'Recession Period Statistics'}
],
placeholder='Select a report type',
value='Select Statistics',
style={'width': '80%', 'padding': '3px', 'font-size': '20px', 'text-align-last': 'center'}
)
]),
html.Div([
dcc.Dropdown(
id='select-year',
options=[{'label': i, 'value': i} for i in range(1980, 2024)],
placeholder='Select-year',
value='Select-year'
)
]),
html.Div(id='output-container', className='chart-grid', style={'display': 'flex'})
])
@app.callback(
Output(component_id='select-year', component_property='disabled'),
Input(component_id='dropdown-statistics', component_property='value')
)
def update_input_container(selected_statistics):
if selected_statistics == 'Yearly Statistics':
return False
else:
return True
@app.callback(
Output(component_id='output-container', component_property='children'),
[Input(component_id='dropdown-statistics', component_property='value'),
Input(component_id='select-year', component_property='value')]
)
def update_output_container(selected_statistics, input_year):
if selected_statistics == 'Recession Period Statistics':
recession_data = data[data['Recession'] == 1]
yearly_rec = recession_data.groupby('Year')['Automobile_Sales'].mean().reset_index()
R_chart1 = dcc.Graph(figure=px.line(yearly_rec, x='Year', y='Automobile_Sales',
title="Average Automobile Sales Fluctuation"))
average_sales = recession_data.groupby('Vehicle_Type')['Automobile_Sales'].mean().reset_index()
R_chart2 = dcc.Graph(figure=px.bar(average_sales, x='Vehicle_Type', y='Automobile_Sales',
title="Average Vehicles Sold by Type"))
exp_rec = recession_data.groupby('Vehicle_Type')['Advertising_Expenditure'].sum().reset_index()
R_chart3 = dcc.Graph(figure=px.pie(exp_rec, values='Advertising_Expenditure', names='Vehicle_Type',
title="Total Expenditure Share"))
unemp_data = recession_data.groupby(
['unemployment_rate', 'Vehicle_Type'])['Automobile_Sales'].mean().reset_index()
R_chart4 = dcc.Graph(figure=px.bar(unemp_data, x='unemployment_rate', y='Automobile_Sales',
color='Vehicle_Type', title='Effect of Unemployment Rate'))
return [
html.Div(className='chart-item', children=[R_chart1, R_chart2], style={'display': 'flex'}),
html.Div(className='chart-item', children=[R_chart3, R_chart4], style={'display': 'flex'})
]
elif (input_year and selected_statistics == 'Yearly Statistics'):
yearly_data = data[data['Year'] == int(input_year)]
yas = data.groupby('Year')['Automobile_Sales'].mean().reset_index()
Y_chart1 = dcc.Graph(figure=px.line(yas, x='Year', y='Automobile_Sales',
title='Yearly Average Automobile Sales'))
mas = yearly_data.groupby('Month')['Automobile_Sales'].sum().reset_index()
Y_chart2 = dcc.Graph(figure=px.line(mas, x='Month', y='Automobile_Sales',
title='Total Monthly Automobile Sales'))
avr_vdata = yearly_data.groupby('Vehicle_Type')['Automobile_Sales'].mean().reset_index()
Y_chart3 = dcc.Graph(figure=px.bar(avr_vdata, x='Vehicle_Type', y='Automobile_Sales',
title='Average Vehicles Sold per Type'))
exp_data = yearly_data.groupby('Vehicle_Type')['Advertising_Expenditure'].sum().reset_index()
Y_chart4 = dcc.Graph(figure=px.pie(exp_data, values='Advertising_Expenditure', names='Vehicle_Type',
title='Total Advertisement Expenditure'))
return [
html.Div(className='chart-item', children=[Y_chart1, Y_chart2], style={'display': 'flex'}),
html.Div(className='chart-item', children=[Y_chart3, Y_chart4], style={'display': 'flex'})
]
else:
return None
if __name__ == '__main__':
app.run(debug=True)# Install dependencies
pip install -r requirements.txt
# Run the app
python Automobile_Sales_Dashboard.py
# Open in browser
# http://127.0.0.1:8050/| # | Finding |
|---|---|
| 1 | Structural, Not Proportional Shifts — Recessions don't just reduce sales; they restructure what people buy. Discretionary segments (Sports/Executive) fail first, while utility segments (SmallFamily/SuperMini) act as staple goods |
| 2 | ExecutiveCar and Sports suffered the most severe recession declines (~75% volume drop vs. non-recession averages) |
| 3 | SuperMiniCar was the most recession-resilient segment, maintaining relatively stable demand throughout downturns |
| 4 | Advertising Follows the Money — Manufacturers strategically redirected resources toward segments with inelastic demand during recessions |
| 5 | Consumer confidence positively correlates with sales during recessions; vehicle price negatively correlates |
| 6 | April and December are the two peak seasonality months for automobile sales in non-recession periods |
| 7 | Unemployment above 3–5% triggers accelerating sales declines across most segments, with premium vehicles failing earliest |
| 8 | GDP volatility during recession periods is markedly higher than during non-recession years, validating the macroeconomic context of the sales data |
| 9 | Ad spend during non-recession periods is disproportionately higher — the company correctly reduces marketing investment when consumer responsiveness is at its lowest |
Automobile-Sales-Analytics/
│
├── DV0101EN-Final-Assign-Part1.ipynb # Part 1: Static EDA Notebook
├── Automobile_Sales_Dashboard.py # Part 2: Interactive Dash App
├── requirements.txt # Python dependencies
├── images/
│ ├── task1_1_sales_trend.png # Task 1.1 — Sales trend line chart
│ ├── task1_2_ads_vs_sales.png # Task 1.2 — Advertising vs. sales dual line
│ ├── task1_3_vehicle_type_sales.png # Task 1.3 — Vehicle type bar chart
│ ├── task1_4_gdp_variation.png # Task 1.4 — GDP subplots
│ ├── task1_5_seasonality.png # Task 1.5 — Seasonality bubble plot
│ ├── task1_6_consumer_confidence.png # Task 1.6 — Consumer confidence scatter
│ ├── task1_6_price_vs_sales.png # Task 1.6 — Price vs. sales scatter
│ ├── task1_7_ad_split.png # Task 1.7 — Ad spend pie chart
│ ├── task1_8_ad_by_vehicle.png # Task 1.8 — Ad spend by vehicle type pie
│ ├── task1_9_unemployment_effect.png # Task 1.9 — Unemployment effect line plot
│ ├── RecessionReportgraphs.png # Part 2 — Recession dashboard screenshot
│ └── YearlyReportgraphs.png # Part 2 — Yearly dashboard screenshot
└── README.md
requirements.txt
pandas
numpy
matplotlib
seaborn
folium
requests
dash
plotly
- LinkedIn: linkedin.com/in/emycodes
- Portfolio: EmyCodes GitHub
"Data is most powerful when it serves as a clear, honest bridge between raw numbers and strategic growth."
©️ EmyCodes | 2026












