---
title: Revision exercises — Solutions
jupyter: python3
---
## Exercise 1: Customising Plot Styles
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2 * np.pi, 50)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(2 * x)
fig, ax = plt.subplots()
ax.plot(x, y1, color="red", linestyle="--", marker="o", label="sin(x)")
ax.plot(x, y2, color="midnightblue", linestyle="-", label="cos(x)")
ax.plot(x, y3, color="0.5", linestyle=":", marker="x", label="sin(2x)")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("Wave signals")
ax.legend()
plt.show()
```
## Exercise 2: Saving Figures
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(1, 11)
y = np.sqrt(x)
fig, ax = plt.subplots()
ax.scatter(x, y)
ax.set_xlabel("x")
ax.set_ylabel("sqrt(x)")
ax.set_title("Square root function")
plt.show()
fig.savefig("sqrt_plot.png")
fig.savefig("sqrt_plot.pdf")
```
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
# Approach 1: create fig, ax first, pass ax to df.plot()
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({"prime": [2, 3, 5, 7, 11, 13], "non-prime": [1, 4, 6, 8, 9, 10]})
fig, ax = plt.subplots()
df.plot(ax=ax)
fig.savefig("primes_approach1.png")
```
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
# Approach 2: capture the Axes returned by df.plot(), get Figure from it
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({"prime": [2, 3, 5, 7, 11, 13], "non-prime": [1, 4, 6, 8, 9, 10]})
ax = df.plot()
fig = ax.get_figure()
fig.savefig("primes_approach2.png")
```
## Exercise 3: Index-based Scatter Plot with Pandas
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import pandas as pd
df_weather = pd.DataFrame(
{"temperature": [3, 4, 8, 12, 16, 20, 22, 21, 17, 12, 7, 4],
"rainfall": [52, 40, 47, 50, 55, 44, 47, 50, 54, 65, 58, 55]},
index=["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
)
# Scatter-like: markers only, no connecting line
df_weather.plot.line(y="temperature", marker="o", linestyle="None")
```
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
# Without linestyle="None" — both markers and line appear
df_weather.plot.line(y="temperature", marker="o")
```
## Exercise 4: Dual Y-axis Plots with `twinx()`
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import numpy as np
import matplotlib.pyplot as plt
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
x = np.arange(len(months))
temperature = [3, 4, 8, 12, 16, 20, 22, 21, 17, 12, 7, 4]
rainfall = [52, 40, 47, 50, 55, 44, 47, 50, 54, 65, 58, 55]
fig, ax1 = plt.subplots(figsize=(10, 5))
color1 = "C0"
ax1.plot(x, temperature, color=color1)
ax1.set_ylabel("Temperature (°C)", color=color1)
ax1.tick_params(axis="y", labelcolor=color1)
ax2 = ax1.twinx()
color2 = "C1"
ax2.bar(x, rainfall, color=color2, alpha=0.6)
ax2.set_ylabel("Rainfall (mm)", color=color2)
ax2.tick_params(axis="y", labelcolor=color2)
ax1.set_xticks(x)
ax1.set_xticklabels(months)
ax1.set_title("Monthly Temperature and Rainfall")
plt.tight_layout()
plt.show()
```
## Exercise 5: Random Number Generation with NumPy
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
from numpy import random
rng = random.default_rng(seed=99)
random_ints = rng.integers(1, 100, size=10)
print(random_ints)
moves = rng.choice(["rock", "paper", "scissors"], size=5, replace=True)
print(moves)
my_list = [1, 2, 3, 4, 5]
print("Before:", my_list)
rng.shuffle(my_list)
print("After: ", my_list)
```
## Exercise 6: DatetimeIndex and Filtering by Month
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import pandas as pd
import numpy as np
dates = pd.date_range("2020-01-01", "2023-12-01", freq="MS")
values = np.random.default_rng(0).uniform(10, 30, size=len(dates))
df = pd.DataFrame({"temperature": values}, index=dates)
# 1. Confirm DatetimeIndex
print(df.index)
# 2. Filter to March only
march_df = df[df.index.month == 3]
print(march_df)
# 3. Mean temperature per calendar month
monthly_means = df.groupby(df.index.month).mean()
print(monthly_means)
```
## Exercise 7: NaN Propagation
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import pandas as pd
import numpy as np
df_nan = pd.DataFrame({
"a": [1.0, 2.0, 3.0, 4.0, 5.0],
"b": [10.0, np.nan, 30.0, np.nan, 50.0],
})
# 1. Adding two columns — NaN propagates
print(df_nan["a"] + df_nan["b"])
# 2. Adding a scalar — NaN still propagates
print(df_nan["b"] + 100)
# 3. .mean() skips NaN by default
print("mean of b:", df_nan["b"].mean())
# 4. Fill NaN with 0 first, then add 100
print(df_nan["b"].fillna(0) + 100)
```
## Exercise 8: Finding a Minimum with `scipy.optimize`
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize_scalar
def f(x):
return np.sin(x) + 0.1 * x**2
result = minimize_scalar(f, bounds=(0, 10), method="bounded")
print(f"Minimum value f(x) = {result.fun:.4f} at x = {result.x:.4f}")
x = np.linspace(0, 10, 300)
fig, ax = plt.subplots()
ax.plot(x, f(x), label="f(x) = sin(x) + 0.1x²")
ax.scatter(result.x, result.fun, color="red", zorder=5, label=f"minimum at x={result.x:.2f}")
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
ax.legend()
plt.show()
```
## Exercise 9: Sampling from Distributions with `scipy.stats`
```{pyodide}
#| caption: "▶ Ctrl/Cmd+Enter | ⇥ Ctrl/Cmd+] | ⇤ Ctrl/Cmd+["
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
dist = stats.norm(loc=10, scale=3)
samples = dist.rvs(size=500, random_state=42)
x = np.linspace(dist.mean() - 4*dist.std(), dist.mean() + 4*dist.std(), 300)
fig, ax = plt.subplots()
ax.hist(samples, bins=30, density=True, alpha=0.6, label="samples")
ax.plot(x, dist.pdf(x), color="C1", linewidth=2, label="theoretical PDF")
ax.set_xlabel("x")
ax.set_ylabel("density")
ax.set_title("Normal distribution: samples vs PDF")
ax.legend()
plt.show()
print(f"Theoretical mean: {dist.mean():.2f}, sample mean: {samples.mean():.2f}")
print(f"Theoretical std: {dist.std():.2f}, sample std: {samples.std():.2f}")
```