Skip to content

Commit

Permalink
flesh out example/full
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlambson committed Sep 4, 2024
1 parent 44054f4 commit f84694f
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 16 deletions.
101 changes: 94 additions & 7 deletions examples/full/analysis/elasticity.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,98 @@
import matplotlib.figure as mplfig
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from boredcharts import FigureRouter

figures = FigureRouter()


@figures.chart("price_vs_quantity")
async def price_vs_quantity(margin: float) -> mplfig.Figure:
"""plot the profitable frontier of price vs quantity for a given margin"""

# experiment results
results = pd.DataFrame(
[
(+0.05, -0.100),
(-0.03, +0.075),
(-0.07, +0.175),
(-0.10, +0.200),
],
columns=["price", "qty"], # pyright: ignore[reportArgumentType]
)

# simulation
n = 1000
price_range = (-0.14, 0.14 + 1 / n)
qty_range = (-0.4, 0.8 + 1 / n)
price_values = np.linspace(*price_range, n)
qty_values = np.linspace(*qty_range, n)

P, Q = np.meshgrid(price_values, qty_values)

RHS = -P * (1 / Q + 1)

fig, ax = plt.subplots(figsize=(10, 6))
mask = np.where(Q >= 0, margin > RHS, margin < RHS) # pyright: ignore[reportOptionalOperand]
ax.contourf(P, Q, mask, levels=[0.5, 1], alpha=0.5)
ax.contour(P, Q, mask, levels=[0], colors="black", linewidths=0.5)
ax.plot(results.price, results.qty, "rx", markersize=10, markeredgewidth=2)

ax.set_title(f"Profitable region given change in price & qty for {margin=:.0%}")
ax.set_xlabel("Change in Price")
ax.set_ylabel("Change in Quantity")
xticks = np.arange(*price_range, 0.02)
yticks = np.arange(*qty_range, 0.1)
ax.set_xticks(xticks, [f"{x:.0%}" for x in xticks])
ax.set_yticks(yticks, [f"{y:.0%}" for y in yticks])
ax.grid(True)
ax.axhline(0, color="black", linewidth=0.5)
ax.axvline(0, color="black", linewidth=0.5)

return fig


@figures.chart("profit_at_price_drop")
async def profit_at_price_drop(drop: float) -> mplfig.Figure:
"""sensitivity of profit to elasticity for a given price drop"""

base_profit = 760_000
n = 1000
elasticity_deviation_range = (-0.10, 0.10 + 1 / n)
elasticity_deviation = np.linspace(*elasticity_deviation_range, n)
# pretend we worked out this function and it's true
profit_values = (
elasticity_deviation * (10 * drop) * base_profit + base_profit * 1.05
)

fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(elasticity_deviation, profit_values, linestyle="-")
ax.axhline(base_profit, color="black", linestyle="--")
ax.text(0.04, base_profit, "Current profit", va="bottom")
ax.fill_between(
elasticity_deviation_range,
700_000,
base_profit,
color="gray",
alpha=0.5,
)

ax.set_title(f"Profit sensitivity to elasticity for price {drop=:.0%}")
ax.set_xlabel("Elasticity deviation from expected (%)")
ax.set_ylabel("Profit ($)")
xticks = np.arange(*elasticity_deviation_range, 0.02)
ax.set_xticks(xticks, [f"{x:.0%}" for x in xticks])
ax.set_xlim(*elasticity_deviation_range)
ax.set_ylim(700000, 850000)
ax.yaxis.set_major_formatter("${x:,.0f}")
ax.axvline(0, color="black", linewidth=0.5)
ax.grid(True)
fig.tight_layout()

return fig


@figures.chart("elasticity_vs_profit")
async def elasticity_vs_profit(
report_name: str, margin: float | None = None
Expand Down Expand Up @@ -50,15 +137,15 @@ async def elasticity_vs_profit(
use_clabeltext=True,
)

ax.set_title("Profitable regions given change in price & qty for various margins")
ax.set_xlabel("Change in Price")
ax.set_ylabel("Change in Quantity")
ax.set_xticks(
np.arange(*price_range, 0.1), [f"{x:.0%}" for x in np.arange(*price_range, 0.1)]
)
ax.set_yticks(
np.arange(*qty_range, 1), [f"{y:.0%}" for y in np.arange(*qty_range, 1)]
)
ax.set_title("Profitable regions given change in price & qty for set margin")
xticks = np.arange(*price_range, 0.1)
yticks = np.arange(*qty_range, 1)
ax.set_xticks(xticks, [f"{x:.0%}" for x in xticks])
ax.set_yticks(yticks, [f"{y:.0%}" for y in yticks])
ax.hlines(0, *price_range, color="black", linewidth=0.5)
ax.vlines(0, *qty_range, color="black", linewidth=0.5)
ax.grid(True)

return fig
32 changes: 24 additions & 8 deletions examples/full/pages/price-elasticity.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
August 2024

## Price elasticity
## Price decrease recommendation

Here we did some more fiddly analysis and wanted to use matplotlib for whatever reason,
no problem—it's exactly the same thing (except that these aren't interactive, just images):
We should drop the price of our widgets by 7% because we'll sell 18% more,
meaning we make more total profit. We observed a price elasticity of 2.5
in the experiment cell where we dropped our price by 7%. With our current margin
of 60%, this would increase our profit by $40,000.

<pre>
{%- raw %}
{{ figure("elasticity_vs_profit") }}
{% endraw -%}
</pre>
{{ figure("price_vs_quantity", margin=0.6, css_class="") }}

We looked at other price drops of 3% and 10%. The 3% drop had the same
elasticity as the 7% drop, which means it just wouldn't make us as much absolute
profit. The 10% drop had a lower elasticity, meaning we'd make the same absolute
profit as the 7% drop, but we'd have to sell more widgets to do so. I consider
this unpreferable since it would be less capital-efficient, but if there is some
strategic reason to flood the market with our widgets it wouldn't hurt our
bottom line to do so:

{{
row(
figure("profit_at_price_drop", drop=0.07, css_class=""),
figure("profit_at_price_drop", drop=0.10, css_class=""),
)
}}

We can generalise the elasticity relationship, but the results are kind of intuitive:
the higher your margin, the more you can play with your pricing.

{{ figure("elasticity_vs_profit") }}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dev-dependencies = [
"ruff>=0.5.7",
"types-markdown>=3.6.0.20240316",
"httpx>=0.27.0",
"pandas-stubs>=2.2.2.240807",
]

[tool.uv.sources]
Expand Down
25 changes: 24 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f84694f

Please sign in to comment.