diff --git a/python-matplotlib-guide/README.md b/python-matplotlib-guide/README.md new file mode 100644 index 0000000000..ef1a56d9ac --- /dev/null +++ b/python-matplotlib-guide/README.md @@ -0,0 +1,46 @@ +# Python Plotting With Matplotlib (Guide) + +This folder provides the code examples for the Real Python tutorial [Python Plotting With Matplotlib (Guide)](https://realpython.com/python-matplotlib-guide/). + +The tutorial works through its examples in the REPL. Here, each section of the tutorial is collected into a script you can run end to end, so you can see every result at once and then change the code to see what happens. Lines that the tutorial shows as REPL output are wrapped in `print()` calls, and the canonical `import matplotlib.pyplot as plt` plus `np.random.seed(444)` from the tutorial's opening sit at the top of each script that needs them. + +## Setup + +Create and activate a virtual environment, then install the requirements: + +```bash +$ python -m venv venv +$ source venv/bin/activate +(venv) $ python -m pip install -r requirements.txt +``` + +`requirements.txt` pins the versions the tutorial's examples were verified against. Newer releases will usually work too, but matplotlib changes the repr of its objects and the list of bundled style sheets between releases, so output from other versions won't match the tutorial exactly. + +## Usage + +After creating and activating your virtual environment, and installing the dependencies, you should be able to run each individual file normally: + +```bash +(venv) $ python subplots_notation.py +``` + +Each script prints its results to the terminal and opens a Matplotlib window for each figure it builds — close each window to move on to the next. + +| File | Tutorial section | +| --- | --- | +| `object_hierarchy.py` | The Matplotlib Object Hierarchy | +| `subplots_notation.py` | Understanding `plt.subplots()` Notation | +| `gridspec_housing.py` | Understanding `plt.subplots()` Notation (the `subplot2grid()` and California housing examples) | +| `figures_behind_the_scenes.py` | The "Figures" Behind The Scenes | +| `imshow_and_matshow.py` | A Burst of Color: `imshow()` and `matshow()` | +| `plotting_in_pandas.py` | Plotting in Pandas | +| `appendix_a_configuration.py` | Appendix A: Configuration and Styling | +| `appendix_b_interactive_mode.py` | Appendix B: Interactive Mode | + +Two of the scripts download their data at runtime, so they need an internet connection: `gridspec_housing.py` pulls the California housing archive from figshare, and `plotting_in_pandas.py` pulls the CBOE VIX series from FRED. + +A few results can't match the tutorial byte for byte. The `id()` values in `figures_behind_the_scenes.py` are memory addresses, so only the comparisons between them are meaningful. And `appendix_b_interactive_mode.py` first prints `False` rather than the tutorial's `True`, because matplotlib starts a script with interactive mode off, while the tutorial's session already had it on. + +Two code blocks in the tutorial aren't shipped here, because there's nothing for you to run: the single-line canonical imports in "Pylab: What Is It, and Should I Use It?", and the abridged excerpt of matplotlib's own `pyplot.py` source in "Stateful Versus Stateless Approaches". + +You can find more information and context on the code in [Python Plotting With Matplotlib (Guide)](https://realpython.com/python-matplotlib-guide/). diff --git a/python-matplotlib-guide/appendix_a_configuration.py b/python-matplotlib-guide/appendix_a_configuration.py new file mode 100644 index 0000000000..7c31bb202d --- /dev/null +++ b/python-matplotlib-guide/appendix_a_configuration.py @@ -0,0 +1,27 @@ +"""Configure matplotlib styles and rc parameters. + +Covers "Appendix A: Configuration and Styling" of +https://realpython.com/python-matplotlib-guide/ +""" + +import matplotlib.pyplot as plt + + +def main(): + # All of the module objects starting with "rc" are a means to + # interact with your plot styles and settings. + print([attr for attr in dir(plt) if attr.startswith("rc")]) + + # These two syntaxes are equivalent for adjusting settings. + plt.rc("lines", linewidth=2, color="r") # Syntax 1 + + plt.rcParams["lines.linewidth"] = 2 # Syntax 2 + plt.rcParams["lines.color"] = "r" + + # A style is just a predefined cluster of custom settings. + print(plt.style.available) + plt.style.use("fivethirtyeight") + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/appendix_b_interactive_mode.py b/python-matplotlib-guide/appendix_b_interactive_mode.py new file mode 100644 index 0000000000..02ddd83431 --- /dev/null +++ b/python-matplotlib-guide/appendix_b_interactive_mode.py @@ -0,0 +1,37 @@ +"""Toggle matplotlib's interactive mode on and off. + +Covers "Appendix B: Interactive Mode" of +https://realpython.com/python-matplotlib-guide/ + +The tutorial runs these lines in an interactive session, where +interactive mode is already on, so the first value it prints is True. +Run as a script, matplotlib starts with interactive mode off, so the +first value below prints False. +""" + +import matplotlib.pyplot as plt +import numpy as np + + +def main(): + print(plt.rcParams["interactive"]) # or: plt.isinteractive() + + plt.ioff() + print(plt.rcParams["interactive"]) + + # With interactive mode off, plt.show() is what displays the figure. + x = np.arange(-4, 5) + y1 = x**2 + y2 = 10 / (x**2 + 1) + fig, ax = plt.subplots() + ax.plot(x, y1, "rx", x, y2, "b+", linestyle="solid") + ax.fill_between( + x, y1, y2, where=y2 > y1, interpolate=True, color="green", alpha=0.3 + ) + lgnd = ax.legend(["y1", "y2"], loc="upper center", shadow=True) + lgnd.get_frame().set_facecolor("#ffb19a") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/figures_behind_the_scenes.py b/python-matplotlib-guide/figures_behind_the_scenes.py new file mode 100644 index 0000000000..863208efaa --- /dev/null +++ b/python-matplotlib-guide/figures_behind_the_scenes.py @@ -0,0 +1,37 @@ +"""Inspect the Figures that matplotlib keeps around in memory. + +Covers the "The 'Figures' Behind The Scenes" section of +https://realpython.com/python-matplotlib-guide/ + +The id() values below are memory addresses, so your numbers will differ +from the ones printed in the tutorial. Only the comparisons match. +""" + +import matplotlib.pyplot as plt + + +def get_all_figures(): + return [plt.figure(i) for i in plt.get_fignums()] + + +def main(): + fig1, ax1 = plt.subplots() + + print(id(fig1)) + print(id(plt.gcf())) # `fig1` is the current figure. + + fig2, ax2 = plt.subplots() + print(id(fig2) == id(plt.gcf())) # The current figure is now `fig2`. + + # Both figures are still hanging around in memory, each with a + # corresponding ID number (1-indexed, in MATLAB style). + print(plt.get_fignums()) + print(get_all_figures()) + + # Close each figure after use to avoid a MemoryError. + plt.close("all") + print(get_all_figures()) + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/gridspec_housing.py b/python-matplotlib-guide/gridspec_housing.py new file mode 100644 index 0000000000..bdbdda24e3 --- /dev/null +++ b/python-matplotlib-guide/gridspec_housing.py @@ -0,0 +1,76 @@ +"""Lay out uneven subplots with gridspec and subplot2grid(). + +Covers the gridspec examples in the "Understanding plt.subplots() +Notation" section of https://realpython.com/python-matplotlib-guide/ + +The California housing data is downloaded from figshare at runtime, so +this script needs an internet connection. +""" + +from io import BytesIO +import tarfile +from urllib.request import urlopen + +import matplotlib.pyplot as plt +import numpy as np + + +def load_housing(): + """Pull the macroeconomic California housing data.""" + url = "https://ndownloader.figshare.com/files/5976036" + b = BytesIO(urlopen(url).read()) + fpath = "CaliforniaHousing/cal_housing.data" + + with tarfile.open(mode="r", fileobj=b) as archive: + housing = np.loadtxt(archive.extractfile(fpath), delimiter=",") + + return housing + + +def add_titlebox(ax, text): + """Place a text box inside a plot as an "in-plot title".""" + ax.text( + 0.55, + 0.8, + text, + horizontalalignment="center", + transform=ax.transAxes, + bbox=dict(facecolor="white", alpha=0.6), + fontsize=12.5, + ) + return ax + + +def main(): + housing = load_housing() + + # The "response" variable y is an area's average home value. pop + # and age are the area's population and average house age. + y = housing[:, -1] + pop, age = housing[:, [4, 7]].T + + # A 3x2 grid where ax1 spans two columns and two rows. + gridsize = (3, 2) + # The tutorial binds this Figure to `fig`, but never uses it. + plt.figure(figsize=(12, 8)) + ax1 = plt.subplot2grid(gridsize, (0, 0), colspan=2, rowspan=2) + ax2 = plt.subplot2grid(gridsize, (2, 0)) + ax3 = plt.subplot2grid(gridsize, (2, 1)) + + ax1.set_title( + "Home value as a function of home age & area population", + fontsize=14, + ) + sctr = ax1.scatter(x=age, y=pop, c=y, cmap="RdYlGn") + plt.colorbar(sctr, ax=ax1, format="$%d") + ax1.set_yscale("log") + ax2.hist(age, bins="auto") + ax3.hist(pop, bins="auto", log=True) + + add_titlebox(ax2, "Histogram: home age") + add_titlebox(ax3, "Histogram: area population (log scl.)") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/imshow_and_matshow.py b/python-matplotlib-guide/imshow_and_matshow.py new file mode 100644 index 0000000000..e4a32595ff --- /dev/null +++ b/python-matplotlib-guide/imshow_and_matshow.py @@ -0,0 +1,45 @@ +"""Visualize raw numerical arrays as colored grids. + +Covers the "A Burst of Color: imshow() and matshow()" section of +https://realpython.com/python-matplotlib-guide/ +""" + +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable +import numpy as np + +np.random.seed(444) + + +def main(): + # Two distinct grids, built with some fancy NumPy indexing. + x = np.diag(np.arange(2, 12))[::-1] + x[np.diag_indices_from(x[::-1])] = np.arange(2, 12) + x2 = np.arange(x.size).reshape(x.shape) + + # Toggle "off" all axis labels and ticks with a dict comprehension. + sides = ("left", "right", "top", "bottom") + nolabels = {s: False for s in sides} + nolabels.update({"label%s" % s: False for s in sides}) + print(nolabels) + + with plt.rc_context(rc={"axes.grid": False}): + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) + ax1.matshow(x) + img2 = ax2.matshow(x2, cmap="RdYlGn_r") + for ax in (ax1, ax2): + ax.tick_params(axis="both", which="both", **nolabels) + for i, j in zip(*x.nonzero()): + ax1.text(j, i, x[i, j], color="white", ha="center", va="center") + + # The colorbar needs a new Axes within `fig`. + divider = make_axes_locatable(ax2) + cax = divider.append_axes("right", size="5%", pad=0) + plt.colorbar(img2, cax=cax, ax=[ax1, ax2]) + fig.suptitle("Heatmaps with `Axes.matshow`", fontsize=16) + + plt.show() + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/object_hierarchy.py b/python-matplotlib-guide/object_hierarchy.py new file mode 100644 index 0000000000..a8db1a6294 --- /dev/null +++ b/python-matplotlib-guide/object_hierarchy.py @@ -0,0 +1,22 @@ +"""Traverse the nested objects that make up a matplotlib graphic. + +Covers the "The Matplotlib Object Hierarchy" section of +https://realpython.com/python-matplotlib-guide/ +""" + +import matplotlib.pyplot as plt + + +def main(): + # A Figure is the outermost container for a matplotlib graphic. + fig, _ = plt.subplots() + print(type(fig)) + + # Attribute notation walks down the hierarchy: Figure -> Axes -> + # yaxis -> major ticks. + one_tick = fig.axes[0].yaxis.get_major_ticks()[0] + print(type(one_tick)) + + +if __name__ == "__main__": + main() diff --git a/python-matplotlib-guide/plotting_in_pandas.py b/python-matplotlib-guide/plotting_in_pandas.py new file mode 100644 index 0000000000..e066a845d3 --- /dev/null +++ b/python-matplotlib-guide/plotting_in_pandas.py @@ -0,0 +1,79 @@ +"""Mix pandas plotting methods with traditional matplotlib calls. + +Covers the "Plotting in Pandas" section of +https://realpython.com/python-matplotlib-guide/ + +The VIX series is downloaded from FRED at runtime, so this script needs +an internet connection. +""" + +import matplotlib.pyplot as plt +import matplotlib.transforms as mtransforms +import numpy as np +import pandas as pd + +np.random.seed(444) + + +def series_introspection(): + """Show that pandas' plot() wraps the state-based plt.plot().""" + s = pd.Series(np.arange(5), index=list("abcde")) + ax = s.plot() + + print(type(ax)) + print(id(plt.gca()) == id(ax)) + + +def volatility_regime(): + """Plot the moving average of the VIX, colored by regime state.""" + url = "https://fred.stlouisfed.org/graph/fredgraph.csv?id=VIXCLS" + vix = ( + pd.read_csv(url, index_col=0, parse_dates=True, na_values=".") + .squeeze("columns") + .dropna() + ) + ma = vix.rolling("90D").mean() + state = pd.cut(ma, bins=[-np.inf, 14, 18, 24, np.inf], labels=range(4)) + + cmap = plt.get_cmap("RdYlGn_r") + ma.plot( + color="black", + linewidth=1.5, + marker="", + figsize=(8, 4), + label="VIX 90d MA", + ) + ax = plt.gca() # Get the current Axes that ma.plot() references + ax.set_xlabel("") + ax.set_ylabel("90d moving average: CBOE VIX") + ax.set_title("Volatility Regime State") + ax.grid(False) + ax.legend(loc="upper center") + ax.set_xlim(xmin=ma.index[0], xmax=ma.index[-1]) + + trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes) + for i, color in enumerate(cmap([0.2, 0.4, 0.6, 0.8])): + ax.fill_between( + ma.index, + 0, + 1, + where=state == i, + facecolor=color, + transform=trans, + ) + ax.axhline( + vix.mean(), + linestyle="dashed", + color="xkcd:dark grey", + alpha=0.6, + label="Full-period mean", + marker="", + ) + plt.show() + + +if __name__ == "__main__": + for section in (series_introspection, volatility_regime): + title = section.__name__.replace("_", " ").title() + print(f"\n{title}\n{'-' * len(title)}") + section() diff --git a/python-matplotlib-guide/requirements.txt b/python-matplotlib-guide/requirements.txt new file mode 100644 index 0000000000..6224196091 --- /dev/null +++ b/python-matplotlib-guide/requirements.txt @@ -0,0 +1,3 @@ +matplotlib==3.11.2 +numpy==2.5.3 +pandas==3.0.5 diff --git a/python-matplotlib-guide/subplots_notation.py b/python-matplotlib-guide/subplots_notation.py new file mode 100644 index 0000000000..0f1643ea71 --- /dev/null +++ b/python-matplotlib-guide/subplots_notation.py @@ -0,0 +1,74 @@ +"""Create Figures and Axes with plt.subplots(). + +Covers the "Understanding plt.subplots() Notation" section of +https://realpython.com/python-matplotlib-guide/ +""" + +import matplotlib.pyplot as plt +import numpy as np + +np.random.seed(444) + + +def single_axes(): + """Show that the default call returns one Figure and one Axes.""" + fig, ax = plt.subplots() + print(type(ax)) + + +def stacked_area(): + """Draw a stacked area graph of three random time series.""" + rng = np.arange(50) + rnd = np.random.randint(0, 10, size=(3, rng.size)) + yrs = 1950 + rng + + fig, ax = plt.subplots(figsize=(5, 3)) + ax.stackplot(yrs, rng + rnd, labels=["Eastasia", "Eurasia", "Oceania"]) + ax.set_title("Combined debt growth over time") + ax.legend(loc="upper left") + ax.set_ylabel("Total debt") + ax.set_xlim(xmin=yrs[0], xmax=yrs[-1]) + fig.tight_layout() + plt.show() + + +def two_subplots(): + """Put two correlated arrays into a 1x2 grid of Axes.""" + x = np.random.randint(low=1, high=11, size=50) + y = x + np.random.randint(1, 5, size=x.size) + data = np.column_stack((x, y)) + + fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8, 4)) + + ax1.scatter(x=x, y=y, marker="o", c="r", edgecolor="b") + ax1.set_title("Scatter: $x$ versus $y$") + ax1.set_xlabel("$x$") + ax1.set_ylabel("$y$") + + ax2.hist(data, bins=np.arange(data.min(), data.max()), label=("x", "y")) + ax2.legend(loc=(0.65, 0.8)) + ax2.set_title("Frequencies of $x$ and $y$") + ax2.yaxis.tick_right() + + # Multiple Axes can "belong to" a given Figure. + print((fig.axes[0] is ax1, fig.axes[1] is ax2)) + plt.show() + + +def grid_of_axes(): + """Show that a 2x2 call returns a NumPy array of Axes.""" + fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(7, 7)) + print(type(ax)) + print(repr(ax)) + print(ax.shape) + + fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(7, 7)) + ax1, ax2, ax3, ax4 = ax.flatten() # flatten a 2d NumPy array to 1d + print(ax1, ax2, ax3, ax4) + + +if __name__ == "__main__": + for section in (single_axes, stacked_area, two_subplots, grid_of_axes): + title = section.__name__.replace("_", " ").title() + print(f"\n{title}\n{'-' * len(title)}") + section()