Data Science
Plotly professional workflow: subplots and an analytical handoff
Compose an offline two-panel sales report with definitions, source data and a clear business conclusion.
What you will learn
Compose an offline two-panel sales report with definitions, source data and a clear business conclusion.
Before you start
You need basic variables, lists and function calls, not advanced Python algorithms. If these are new, visit Python foundations first.
- Create a local environment:
python -m venv .venv. Activate it for your OS and installpython -m pip install jupyterlab numpy pandas matplotlib seaborn plotly. - Download the 12-row sales CSV into your notebook folder. Keep its filename unchanged.
- Start Jupyter with
jupyter lab. Run the setup below in a fresh notebook before every workshop.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
raw = pd.read_csv('data-science-sales.csv', parse_dates=['date'])
df = raw.copy()
df['revenue'] = df['units'] * df['unit_price']
net = df.loc[df['returned'].eq(0)].copy()
Data dictionary: each row is one order for one product; units is a positive count; unit_price is INR per unit; returned is 0 or 1. Revenue is units × unit_price. For this workshop, net sales exclude the entire returned order (not partial returns). The CSV has 12 rows, 4950 gross revenue, 4450 net revenue and 35 net units. This small fictional sample teaches technique; it does not justify population or causal claims.
Save your notebook and written interpretation. There is no browser code judge and no automatic grading of visual design.
Learn the idea
Use graph_objects when you need explicit trace and subplot control. make_subplots assigns traces to numbered rows and columns. Give each axis its own units and avoid unexplained dual axes. Responsiveness is about the figure container too: avoid fixed layout widths and test exported HTML where you will share it. HTML charts need a text or CSV alternative; a colour-only legend is not sufficient communication. A professional handoff includes the data contract, code and caveats, not just the chart.
Follow a worked example
from plotly.subplots import make_subplots
import plotly.graph_objects as go
r=net.groupby('region')['revenue'].sum()
p=net.groupby('product')['revenue'].sum()
fig=make_subplots(rows=1,cols=2,subplot_titles=['Region','Product'])
fig.add_trace(go.Bar(x=r.index,y=r.values,name='Region',text=r.values,textposition='auto'),row=1,col=1)
fig.add_trace(go.Bar(x=p.index,y=p.values,name='Product',text=p.values,textposition='auto'),row=1,col=2)
fig.update_yaxes(title_text='Net revenue (INR)')
fig.update_layout(title='Q1 2026 — returns excluded',showlegend=False,autosize=True)
fig.write_html('sales-dashboard.html',include_plotlyjs=True,config={'responsive':True})
Expected result
An offline HTML report with region and product panels. Each panel reconciles independently to 4450; adding both totals would double-count revenue.
Skill-development workshop
These are practical assignments, not MCQs or code-judge puzzles. Work in your own notebook or MySQL client. Complete the levels in order; if a task feels difficult, follow the hint and compare with the reference only after making an attempt. Keep your result and a short explanation of what it means.
Easy: Prepare separate region and product summaries.
Success criteria: 3 regions and 4 products; each total is 4450.
Need a starting point? Show the hint
These are two views of the same data, not additive tables.
Reference solution — open after trying
r=net.groupby('region')['revenue'].sum(); p=net.groupby('product')['revenue'].sum()
assert len(r)==3 and len(p)==4 and r.sum()==p.sum()==4450
Medium: Add two explicitly located traces to a subplot figure.
Success criteria: 2 traces, region on left, product on right, both axes labelled INR.
Need a starting point? Show the hint
Use make_subplots, add_trace(row=1,col=...) and update_yaxes.
Reference solution — open after trying
from plotly.subplots import make_subplots
import plotly.graph_objects as go
r=net.groupby('region')['revenue'].sum(); p=net.groupby('product')['revenue'].sum()
fig=make_subplots(rows=1,cols=2,subplot_titles=['Region','Product'])
fig.add_trace(go.Bar(x=r.index,y=r.values),row=1,col=1)
fig.add_trace(go.Bar(x=p.index,y=p.values),row=1,col=2)
fig.update_yaxes(title_text='Net revenue (INR)'); assert len(fig.data)==2
Hard: Create a compact mobile-friendly alternative to the wide report.
Success criteria: One chart per row rather than squeezing two side by side; no fixed width; keep all labels.
Need a starting point? Show the hint
Use a 2×1 subplot grid and set responsive=True in the exported config.
Reference solution — open after trying
from plotly.subplots import make_subplots
import plotly.graph_objects as go
r=net.groupby('region')['revenue'].sum(); p=net.groupby('product')['revenue'].sum()
fig=make_subplots(rows=2,cols=1,subplot_titles=['Region','Product'])
fig.add_trace(go.Bar(x=r.index,y=r.values),row=1,col=1); fig.add_trace(go.Bar(x=p.index,y=p.values),row=2,col=1)
fig.update_yaxes(title_text='Net revenue (INR)'); fig.update_layout(height=700,showlegend=False,autosize=True)
fig.write_html('sales-mobile.html',include_plotlyjs=True,config={'responsive':True})
Professional: Package a complete sales-analysis handoff.
Success criteria: Notebook runs top-to-bottom; includes CSV, offline chart, source filename, net rule, 4450 reconciliation and limitations. State that South leads region revenue and Lamp leads product revenue, without causal claims.
Need a starting point? Show the hint
Check every table against net, then write a short report distinguishing evidence from recommendation.
Reference solution — open after trying
assert len(raw)==12 and len(net)==11 and net['revenue'].sum()==4450
net.to_csv('analysis-source.csv',index=False)
summary='Q1 2026 sample: net revenue INR 4450, excluding one fully returned order. South leads regional revenue (1700); Lamp leads products (2400). Only 11 non-returned orders; no causal or population claim. Review a larger date range before making inventory decisions.'
from pathlib import Path
Path('analysis-readme.txt').write_text(summary,encoding='utf-8')
print(summary)
Reflection and submission
Save your queries/notebook and outputs. Write three sentences: what you changed, how you checked it, and one limitation. A correct chart or query without a clear interpretation is not a completed professional assignment.
from plotly.subplots import make_subplots
import plotly.graph_objects as go
r=net.groupby('region')['revenue'].sum()
p=net.groupby('product')['revenue'].sum()
fig=make_subplots(rows=1,cols=2,subplot_titles=['Region','Product'])
fig.add_trace(go.Bar(x=r.index,y=r.values,name='Region',text=r.values,textposition='auto'),row=1,col=1)
fig.add_trace(go.Bar(x=p.index,y=p.values,name='Product',text=p.values,textposition='auto'),row=1,col=2)
fig.update_yaxes(title_text='Net revenue (INR)')
fig.update_layout(title='Q1 2026 — returns excluded',showlegend=False,autosize=True)
fig.write_html('sales-dashboard.html',include_plotlyjs=True,config={'responsive':True})Lesson Code (python)
from plotly.subplots import make_subplots
import plotly.graph_objects as go
r=net.groupby('region')['revenue'].sum()
p=net.groupby('product')['revenue'].sum()
fig=make_subplots(rows=1,cols=2,subplot_titles=['Region','Product'])
fig.add_trace(go.Bar(x=r.index,y=r.values,name='Region',text=r.values,textposition='auto'),row=1,col=1)
fig.add_trace(go.Bar(x=p.index,y=p.values,name='Product',text=p.values,textposition='auto'),row=1,col=2)
fig.update_yaxes(title_text='Net revenue (INR)')
fig.update_layout(title='Q1 2026 — returns excluded',showlegend=False,autosize=True)
fig.write_html('sales-dashboard.html',include_plotlyjs=True,config={'responsive':True})Console Output
An offline HTML report with region and product panels. Each panel reconciles independently to 4450; adding both totals would double-count revenue.Up next · Continue learning
The Python Data Science Ecosystem
Jupyter setup, virtual environments, variables, data structures, and list comprehensions.