The chart is always visible. View Data Simulator and View Plot Code expand and collapse independently. This lets a beginner focus on the picture first and inspect reproducible Python when ready.
TipWhat you will learn
Distribution, comparison, relationship, time-series, experimentation, multivariate, model-diagnostic, geographic and interactive visualization using Matplotlib, Seaborn, Plotly and Bokeh, plus guidance on modern alternatives.
1. Visualization Starts with the Question
🔍 Distribution Histogram · KDE · ECDF · Box · Violin
⚖️ Comparison Bar · Box · Error bars · Small multiples
Matplotlib is the low-level foundation and is commonly static-first. Seaborn builds statistical graphics on Matplotlib. Plotly and Bokeh make browser interaction—hover, zoom, selection and widgets—a central strength. Matplotlib can also use interactive backends, so “Matplotlib is never interactive” is not technically correct.
2.1. Matplotlib — Precise Figure Control
Company question: How is weekly revenue changing?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(1)mat_df=pd.DataFrame({"week":np.arange(1,21)})mat_df["revenue"]=85000+1800*mat_df["week"]+rng.normal(0,4500,20)
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(8,4))ax.plot(mat_df["week"],mat_df["revenue"],marker="o")ax.set(title="Weekly Revenue",xlabel="Week",ylabel="Revenue ($)")fig.tight_layout(); plt.show()
Best for: custom static analytical figures, reports, annotations and fine control.
2.2. Seaborn — Statistical Visualization
Company question: How does revenue differ across segments?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(2)seg=rng.choice(["Consumer","SMB","Enterprise"],900,p=[.55,.3,.15])base=pd.Series(seg).map({"Consumer":70,"SMB":125,"Enterprise":220}).to_numpy()sns_df=pd.DataFrame({"segment":seg,"revenue":np.maximum(5,base+rng.normal(0,32,900))})
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(7.5,4))sns.boxplot(data=sns_df,x="segment",y="revenue",ax=ax)ax.set(title="Monthly Revenue by Segment",ylabel="Revenue ($)")fig.tight_layout(); plt.show()
Best for: EDA, grouped distributions, statistical comparisons and regression views.
2.3. Plotly — Interactive Exploration
Company question: Which individual customers sit behind the overall pattern?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(3); n=650px_df=pd.DataFrame({"customer_id":np.arange(10001,10001+n),"segment":rng.choice(["Consumer","SMB","Enterprise"],n,p=[.55,.3,.15]),"sessions":rng.poisson(9,n)+1,"tickets":rng.poisson(1.5,n)})px_df["revenue"]=25+5.5*px_df["sessions"]+px_df["segment"].map({"Consumer":0,"SMB":40,"Enterprise":110})+rng.normal(0,25,n)
View Plot Code
import plotly.express as pxfig=px.scatter(px_df,x="sessions",y="revenue",color="segment",size="tickets",hover_data=["customer_id"],title="Interactive Customer Explorer")fig.show()
Hover, zoom, pan, select and toggle legend groups directly in the rendered chart.
2.4. Bokeh — Interactive Browser Plots
Company question: Can an analyst inspect and zoom into customers?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(4); n=500bk_df=pd.DataFrame({"customer_id":np.arange(1,n+1),"sessions":rng.poisson(10,n)+1})bk_df["revenue"]=(35+6*bk_df["sessions"]+rng.normal(0,30,n)).clip(5)
View Plot Code
from bokeh.plotting import figure,showfrom bokeh.models import ColumnDataSource,HoverToolsource=ColumnDataSource(bk_df)p=figure(width=800,height=380,title="Bokeh: Revenue vs Sessions",tools="pan,wheel_zoom,box_zoom,reset")p.scatter(x="sessions",y="revenue",source=source,size=7,alpha=.45)p.add_tools(HoverTool(tooltips=[("Customer","@customer_id"),("Revenue","@revenue{0.0}")]))show(p)
Bokeh is especially useful for linked selections, widgets and browser applications.
3. Histogram — Distribution Shape
Company question: What does monthly customer revenue look like?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(10)hist_df=pd.DataFrame({"revenue":rng.lognormal(4.35,.62,2500)})
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(8,4)); ax.hist(hist_df["revenue"],bins=38,edgecolor="white")ax.set(title="Distribution of Monthly Revenue",xlabel="Revenue ($)",ylabel="Customers"); fig.tight_layout(); plt.show()
Bin width matters. Too few bins hide structure; too many emphasize noise.
4. KDE — Smooth Distribution Comparison
Company question: Do segments have differently shaped revenue distributions?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(11)kde_df=pd.concat([pd.DataFrame({"segment":s,"revenue":rng.lognormal(mu,sd,700)}) for s,mu,sd in [("Consumer",4,.55),("SMB",4.55,.48),("Enterprise",5.25,.40)]],ignore_index=True)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(8,4)); sns.kdeplot(data=kde_df,x="revenue",hue="segment",common_norm=False,ax=ax)ax.set(title="Revenue Density by Segment",xlabel="Revenue ($)"); fig.tight_layout(); plt.show()
KDE is estimated smoothness. Do not interpret every bump as a real population mode.
5. ECDF — Cumulative Distribution
Company question: What fraction of each segment falls below a revenue threshold?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(12)ecdf_df=pd.concat([pd.DataFrame({"segment":s,"revenue":np.maximum(5,rng.normal(c,c*.28,600))}) for s,c in [("Consumer",65),("SMB",125),("Enterprise",225)]],ignore_index=True)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(8,4)); sns.ecdfplot(data=ecdf_df,x="revenue",hue="segment",ax=ax)ax.set(title="ECDF of Revenue by Segment",xlabel="Revenue ($)",ylabel="Fraction ≤ x"); fig.tight_layout(); plt.show()
The ECDF has no histogram-bin choice and makes percentile questions direct.
6. Box Plot — Compact Group Comparison
Company question: Which segment has the widest revenue distribution?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(13)box_df=pd.DataFrame({"segment":np.repeat(["Consumer","SMB","Enterprise"],500)})c=box_df["segment"].map({"Consumer":70,"SMB":130,"Enterprise":230}).to_numpy(); box_df["revenue"]=np.maximum(5,c+rng.normal(0,c*.25))
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(7.5,4)); sns.boxplot(data=box_df,x="segment",y="revenue",ax=ax)ax.set(title="Revenue by Segment",ylabel="Revenue ($)"); fig.tight_layout(); plt.show()
Box plots summarize median, quartiles, spread and potential outliers.
7. Violin + Strip — Shape and Observations
Company question: Do plans differ in the whole engagement distribution?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(14)violin_df=pd.DataFrame({"plan":np.repeat(["Standard","Premium"],220),"engagement":np.r_[rng.beta(2.2,4,220),rng.beta(4,2.4,220)]})
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(7,4)); sns.violinplot(data=violin_df,x="plan",y="engagement",inner=None,ax=ax); sns.stripplot(data=violin_df,x="plan",y="engagement",alpha=.28,size=2.5,ax=ax)ax.set_title("Engagement Distribution by Plan"); fig.tight_layout(); plt.show()
Use violin plots when distribution shape itself matters.
8. Bar Plot — Categorical Summary
Company question: Which segment has the highest churn rate?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(15); n=2400seg=rng.choice(["Consumer","SMB","Enterprise"],n,p=[.55,.3,.15]); prob=pd.Series(seg).map({"Consumer":.19,"SMB":.13,"Enterprise":.08}).to_numpy(); raw=pd.DataFrame({"segment":seg,"churn":rng.binomial(1,prob)}); bar_df=raw.groupby("segment",as_index=False).agg(churn_rate=("churn","mean"))
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(7,4)); ax.bar(bar_df["segment"],bar_df["churn_rate"]); ax.set(title="Churn Rate by Segment",ylabel="Churn rate",ylim=(0,bar_df["churn_rate"].max()*1.25)); fig.tight_layout(); plt.show()
Bar lengths need an honest baseline. Truncating the y-axis can exaggerate differences.
9. Count Plot — Group Size
Company question: How many customers use each device?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(16)count_df=pd.DataFrame({"device":rng.choice(["Web","iOS","Android"],1800,p=[.46,.28,.26]),"segment":rng.choice(["Consumer","SMB","Enterprise"],1800,p=[.56,.3,.14])})
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(8,4)); sns.countplot(data=count_df,x="device",hue="segment",ax=ax); ax.set(title="Customer Count by Device and Segment",ylabel="Customers"); fig.tight_layout(); plt.show()
Inspect sample size before interpreting subgroup metrics.
10. Scatter Plot — Numerical Relationship
Company question: Do more sessions correspond to more revenue?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(17); n=1300scatter_df=pd.DataFrame({"sessions":rng.poisson(10,n)+1}); scatter_df["revenue"]=(30+6.2*scatter_df["sessions"]+rng.normal(0,32,n)).clip(5)
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(8,4.5)); ax.scatter(scatter_df["sessions"],scatter_df["revenue"],alpha=.25,s=18); ax.set(title="Sessions vs Monthly Revenue",xlabel="Sessions",ylabel="Revenue ($)"); fig.tight_layout(); plt.show()
Look for direction, curvature, clusters, changing variance and outliers.
11. Regression Plot — Average Trend
Company question: What average relationship is visible?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(18); n=900reg_df=pd.DataFrame({"sessions":rng.uniform(1,25,n)}); reg_df["revenue"]=25+5.8*reg_df["sessions"]+rng.normal(0,28,n)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(8,4.5)); sns.regplot(data=reg_df,x="sessions",y="revenue",scatter_kws={"alpha":.18,"s":18},ax=ax); ax.set_title("Average Revenue Trend with Sessions"); fig.tight_layout(); plt.show()
Warning
A fitted trend summarizes association, not causation.
12. Hexbin — Dense Relationships
Company question: How do two telemetry measures relate across 75,000 events?
View Data Simulator
import numpy as nprng=np.random.default_rng(19); n=75000hex_x=rng.normal(size=n); hex_y=.65*hex_x+rng.normal(0,.75,n)
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(7,5)); hb=ax.hexbin(hex_x,hex_y,gridsize=45,mincnt=1); fig.colorbar(hb,ax=ax,label="Observations"); ax.set(title="Density Hidden by Overplotting",xlabel="Feature X",ylabel="Feature Y"); fig.tight_layout(); plt.show()
Use hexbin, density, sampling or rasterization when scatter points become an opaque cloud.
13. Line Plot — Change Over Time
Company question: Is revenue rising beneath daily noise?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(20); dates=pd.date_range("2026-01-01",periods=220,freq="D"); t=np.arange(220)line_df=pd.DataFrame({"date":dates,"revenue":120000+180*t+10000*np.sin(2*np.pi*t/7)+rng.normal(0,6500,220)}); line_df["rolling_14d"]=line_df["revenue"].rolling(14).mean()
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(10,4)); ax.plot(line_df["date"],line_df["revenue"],alpha=.35,label="Daily"); ax.plot(line_df["date"],line_df["rolling_14d"],linewidth=2,label="14-day mean"); ax.set(title="Revenue: Noise vs Trend",ylabel="Revenue ($)"); ax.legend(); fig.tight_layout(); plt.show()
Chronology is information. Keep time on an ordered axis.
14. Uncertainty Band — Show What We Do Not Know
Company question: Is a conversion trend larger than its uncertainty?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(21); w=np.arange(1,21); est=.12+.002*w+rng.normal(0,.004,20); se=np.linspace(.012,.006,20)band_df=pd.DataFrame({"week":w,"estimate":est,"lower":est-1.96*se,"upper":est+1.96*se})
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(8,4)); ax.plot(band_df["week"],band_df["estimate"],linewidth=2); ax.fill_between(band_df["week"],band_df["lower"],band_df["upper"],alpha=.2); ax.set(title="Conversion Estimate with 95% Band",xlabel="Week",ylabel="Conversion rate"); fig.tight_layout(); plt.show()
Use uncertainty bands for forecasts, experiments and estimated quantities.
15. Error Bars — Experiment Estimates
Company question: How do A/B variants compare with uncertainty?
View Data Simulator
import numpy as npimport pandas as pdexp_df=pd.DataFrame({"variant":["Control","Treatment A","Treatment B"],"conversion":[.100,.112,.107],"n":[12000,11850,12120]}); exp_df["se"]=np.sqrt(exp_df["conversion"]*(1-exp_df["conversion"])/exp_df["n"]); exp_df["ci95"]=1.96*exp_df["se"]
Effect magnitude and uncertainty belong in the same comparison.
16. Correlation Heatmap — Matrix Pattern
Company question: Which numerical variables move together?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(22); n=1200heat_df=pd.DataFrame({"sessions":rng.poisson(10,n)+1,"tenure":rng.integers(1,72,n),"tickets":rng.poisson(1.5,n)}); heat_df["revenue"]=25+6*heat_df["sessions"]+.6*heat_df["tenure"]+rng.normal(0,30,n); heat_df["satisfaction"]=np.clip(4.5-.28*heat_df["tickets"]+rng.normal(0,.5,n),1,5); corr=heat_df.corr(numeric_only=True)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsfig,ax=plt.subplots(figsize=(7,5)); sns.heatmap(corr,annot=True,fmt=".2f",center=0,ax=ax); ax.set_title("Correlation Matrix"); fig.tight_layout(); plt.show()
Correlation is a screening tool, not a causal map.
17. Pair Plot — Multivariate First Look
Company question: How do several customer measures relate at once?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(23); n=450pair_df=pd.DataFrame({"segment":rng.choice(["Consumer","SMB","Enterprise"],n,p=[.55,.3,.15]),"sessions":rng.poisson(9,n)+1,"tenure":rng.integers(1,72,n),"satisfaction":np.clip(rng.normal(4,.65,n),1,5)}); pair_df["revenue"]=30+5.5*pair_df["sessions"]+pair_df["segment"].map({"Consumer":0,"SMB":40,"Enterprise":105})+rng.normal(0,25,n)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsg=sns.pairplot(pair_df,vars=["sessions","revenue","tenure","satisfaction"],hue="segment",corner=True); g.fig.suptitle("Multivariate Customer Overview",y=1.02); plt.show()
Pair plots are excellent for early EDA but become unwieldy as the number of variables grows.
18. Faceting — Small Multiples
Company question: Does a relationship change by segment?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(24); n=1000facet_df=pd.DataFrame({"segment":rng.choice(["Consumer","SMB","Enterprise"],n,p=[.55,.3,.15]),"device":rng.choice(["Web","iOS","Android"],n),"sessions":rng.poisson(9,n)+1}); facet_df["revenue"]=25+5.8*facet_df["sessions"]+facet_df["segment"].map({"Consumer":0,"SMB":45,"Enterprise":110})+rng.normal(0,30,n)
View Plot Code
import matplotlib.pyplot as pltimport seaborn as snsg=sns.relplot(data=facet_df,x="sessions",y="revenue",col="segment",hue="device",kind="scatter",alpha=.45,height=3.3); g.fig.suptitle("Sessions vs Revenue Across Segments",y=1.05); plt.show()
Small multiples often beat one overloaded plot with too many visual encodings.
19. Interactive Time Series — Zoom into Events
Company question: Can an analyst inspect a long timeline around an incident?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(27); dates=pd.date_range("2025-01-01",periods=420,freq="D"); t=np.arange(420)its_df=pd.DataFrame({"date":dates,"revenue":100000+120*t+8000*np.sin(2*np.pi*t/7)+rng.normal(0,5500,420)}); its_df["rolling_21d"]=its_df["revenue"].rolling(21).mean()
View Plot Code
import plotly.express as pxfig=px.line(its_df,x="date",y=["revenue","rolling_21d"],title="Interactive Revenue Timeline"); fig.update_xaxes(rangeslider_visible=True); fig.show()
The range slider and zoom controls are valuable for campaigns, incidents and long monitoring windows.
20. Confusion Matrix — Classification Errors
Company question: Is the churn model missing positives or generating false alarms?
View Data Simulator
import numpy as nprng=np.random.default_rng(30); n=2500cm_y=rng.binomial(1,.18,n); cm_score=np.clip(.12+.55*cm_y+rng.normal(0,.22,n),0,1); cm_pred=(cm_score>=.5).astype(int)
Ranking quality and probability calibration are different properties.
24. Residual Plot — Regression Diagnostics
Company question: Does prediction error change systematically?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(34); n=1200actual=rng.normal(100,25,n); predicted=actual+.10*(actual-100)+rng.normal(0,12,n); resid_df=pd.DataFrame({"predicted":predicted,"residual":actual-predicted})
Residuals can reveal bias, nonlinearity and changing variance hidden by a single RMSE.
25. Feature Importance — What Does the Model Rely On?
Company question: Which inputs contribute most under this importance definition?
View Data Simulator
import pandas as pdimp_df=pd.DataFrame({"feature":["last_login_days","support_tickets","tenure","monthly_spend","sessions"],"importance":[.31,.25,.19,.14,.11]}).sort_values("importance")
View Plot Code
import matplotlib.pyplot as pltfig,ax=plt.subplots(figsize=(7,4)); ax.barh(imp_df["feature"],imp_df["importance"]); ax.set(title="Model Feature Importance",xlabel="Importance"); fig.tight_layout(); plt.show()
Importance is not causality. A predictive feature can be a proxy, consequence, correlated signal or leakage.
26. Funnel — Where Do Users Drop Out?
Company question: At which purchase stage is the largest loss?
View Data Simulator
import pandas as pdfunnel_df=pd.DataFrame({"stage":["Visitors","Product View","Cart","Checkout","Purchase"],"users":[100000,62000,21000,12000,9200]})
View Plot Code
import plotly.express as pxfig=px.funnel(funnel_df,x="users",y="stage",title="Purchase Funnel"); fig.show()
Common in acquisition, onboarding, checkout and application workflows.
27. Geographic Plot — When Location Matters
Company question: Where are orders concentrated around a service area?
View Data Simulator
import numpy as npimport pandas as pdrng=np.random.default_rng(35); n=500geo_df=pd.DataFrame({"latitude":30.27+rng.normal(0,.055,n),"longitude":-97.74+rng.normal(0,.065,n),"orders":rng.poisson(5,n)+1})
View Plot Code
import plotly.express as pxfig=px.scatter_map(geo_df,lat="latitude",lon="longitude",size="orders",hover_data=["orders"],zoom=9,height=500,center={"lat": 30.2672, "lon": -97.7431},title="Interactive Geographic Distribution of Orders"); fig.show()
Use a map when spatial relationships matter. If the question is only “which region is largest?”, a sorted bar chart may be clearer.
28. Other Modern Visualization Libraries
Altair — declarative grammar-of-graphics and concise interactive statistical charts.
hvPlot / HoloViews — high-level interactive plotting for DataFrame/xarray workflows.
Datashader — rasterizes/aggregates huge datasets rather than drawing every point.
The state of the art is not “use the newest library.” Match the tool to data volume + analytical question + audience + interaction + deployment environment.
29. Which Library Should I Reach For?
Need
Good starting choice
Custom static figure
Matplotlib
Statistical EDA
Seaborn
Publication/report control
Matplotlib
Interactive exploration
Plotly
Browser widgets / linked interaction
Bokeh
Declarative grammar
Altair
Huge point clouds
Datashader ecosystem
Interactive geospatial work
Plotly / PyDeck
30. Visualization Mistakes That Mislead
Truncated bar axes can exaggerate small differences.
Too many categories turn legends into puzzles.
Dual y-axes can manufacture apparent relationships through scaling.
Many-slice pie charts make precise comparison difficult.
Rainbow scales can create false perceptual boundaries.
Lines between unrelated categories imply continuity that does not exist.
Means without uncertainty/sample size can hide noise.
Overplotting calls for transparency, sampling, hexbin, density or rasterization.
31. Exploratory vs Explanatory Visualization
Exploratory: “What is happening?” → many views, interaction, diagnostics.
Explanatory: “Here is what matters.” → fewer views, intentional ordering, annotation, uncertainty, restrained color.
EDA → DISCOVERY → ANALYSIS → DECISION → COMMUNICATION
32. A Data Scientist’s Plot Selector
ONE NUMERIC → Histogram / KDE / ECDF / Box
NUMERIC + CATEGORY → Box / Violin / Bar + CI
TWO NUMERIC → Scatter / Regression / Hexbin
TIME → Line / Rolling trend / Uncertainty band
MATRIX → Heatmap
CLASSIFIER → Confusion / ROC / PR / Calibration
REGRESSION → Actual-vs-predicted / Residuals
LOCATION → Map only when spatial structure matters
33. A Company Visualization Workflow
If Product asks “Why did conversion fall last month?”:
Line plot → when did it begin?
Facets → which device/segment/region?
Funnel → which stage?
Distributions → did population mix change?
Event annotations → what changed around that date?
Statistical analysis → is the change beyond noise?
✨ Choose the question first. Choose the plot second. Choose the library third.
A sophisticated interactive chart cannot rescue the wrong visual encoding. A simple plot can reveal the entire story when it represents the right question.