BCG growth-share matrix
Strategy — Grunfeld investment data
Example from the compendium of canonical charts
Python Code
"""Strategy — BCG growth-share matrix (Grunfeld investment data)."""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
URL = "https://vincentarelbundock.github.io/Rdatasets/csv/AER/Grunfeld.csv"
COLORS = [VIOLET, GREEN, ORANGE, PINK, TEAL, "#e67e22", "#2980b9", "#8e44ad", "#27ae60", "#c0392b"]
def generate():
print("fetching Grunfeld from Rdatasets …")
df = fetch_csv(URL)
print("columns:", df.columns.tolist())
print(df.head(3))
df.columns = [c.strip().lower() for c in df.columns]
# Cols: firm, year, value, invest, capital
# Compute for each firm: mean value, invest CAGR, mean invest
firms = df["firm"].unique()
records = []
for firm in firms:
sub = df[df["firm"] == firm].sort_values("year")
mean_val = sub["value"].mean()
mean_inv = sub["invest"].mean()
y0 = sub["invest"].iloc[0]
y1 = sub["invest"].iloc[-1]
n = len(sub) - 1
cagr = (y1 / y0) ** (1 / n) - 1 if y0 > 0 and n > 0 else 0.0
records.append({"firm": firm, "mean_val": mean_val,
"mean_inv": mean_inv, "invest_cagr": cagr})
res = pd.DataFrame(records)
total_val = res["mean_val"].sum()
res["rel_share"] = res["mean_val"] / total_val
res["log_share"] = np.log10(res["rel_share"])
mean_growth = res["invest_cagr"].mean()
log_mid = np.log10(1 / len(firms)) # divider at equal share
# Quadrant labels
def quadrant(row):
if row["log_share"] >= log_mid and row["invest_cagr"] >= mean_growth:
return "Star"
elif row["log_share"] >= log_mid:
return "Cash Cow"
elif row["invest_cagr"] >= mean_growth:
return "Question Mark"
else:
return "Dog"
res["quadrant"] = res.apply(quadrant, axis=1)
x_min, x_max = res["log_share"].min() - 0.1, res["log_share"].max() + 0.1
y_min, y_max = res["invest_cagr"].min() - 0.01, res["invest_cagr"].max() + 0.01
fig = go.Figure()
# Quadrant background rectangles
quad_styles = [
(log_mid, x_max, mean_growth, y_max, "Stars", "rgba(85,182,133,0.10)"),
(x_min, log_mid, mean_growth, y_max, "Question Marks", "rgba(233,162,59,0.10)"),
(log_mid, x_max, y_min, mean_growth, "Cash Cows", "rgba(132,94,238,0.10)"),
(x_min, log_mid, y_min, mean_growth, "Dogs", "rgba(218,85,151,0.10)"),
]
for x0, x1, y0, y1, label, color in quad_styles:
fig.add_shape(type="rect", x0=x0, x1=x1, y0=y0, y1=y1,
fillcolor=color, line=dict(width=0), layer="below")
fig.add_annotation(x=(x0+x1)/2, y=(y0+y1)/2,
text=f"<b>{label}</b>",
showarrow=False, font=dict(size=13, color=MUTED),
opacity=0.7)
# Divider lines
fig.add_shape(type="line", x0=log_mid, x1=log_mid, y0=y_min, y1=y_max,
line=dict(color=MUTED, width=1.2, dash="dash"))
fig.add_shape(type="line", x0=x_min, x1=x_max, y0=mean_growth, y1=mean_growth,
line=dict(color=MUTED, width=1.2, dash="dash"))
# Bubble scatter
for i, row in res.iterrows():
fig.add_trace(go.Scatter(
x=[row["log_share"]],
y=[row["invest_cagr"]],
mode="markers+text",
marker=dict(size=row["mean_val"] / res["mean_val"].max() * 50 + 12,
color=COLORS[i % len(COLORS)], opacity=0.8,
line=dict(width=1, color="white")),
text=[str(row["firm"])],
textposition="top center",
textfont=dict(size=9),
name=str(row["firm"]),
hovertemplate=(f"<b>Firm {row['firm']}</b><br>"
f"Rel Share: {row['rel_share']:.2%}<br>"
f"Invest CAGR: {row['invest_cagr']:.1%}<extra></extra>"),
))
fig.update_layout(
xaxis=dict(title="log₁₀(Relative Market Share)", range=[x_min, x_max]),
yaxis=dict(title="Investment CAGR", tickformat=".1%"),
showlegend=False,
margin=dict(t=50, b=60, l=80, r=40),
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly