From 3aa7074c22592645055f2373b6191e5eb9d05348 Mon Sep 17 00:00:00 2001 From: mmcky Date: Thu, 28 Mar 2024 15:42:07 +1100 Subject: [PATCH] MAINT: remove code-book --- .github/workflows/ci.yml | 105 +- .../downloads => book}/networks-book.pdf | Bin code_book/_config.yml | 49 - code_book/_toc.yml | 14 - code_book/appendix.md | 547 ------- code_book/ch_fpms.md | 178 --- code_book/ch_intro.md | 788 ---------- code_book/ch_mcs.md | 476 ------ code_book/ch_opt.md | 416 ----- code_book/ch_opt_julia.md | 118 -- code_book/ch_production.md | 416 ----- code_book/img/betweenness_centrality_1.png | Bin 64051 -> 0 bytes code_book/img/code.png | Bin 29941 -> 0 bytes code_book/intro.md | 15 - code_book/pkg_funcs.md | 359 ----- code_book/status.md | 17 - ipynb/appendix.ipynb | 776 +++++++++ ipynb/ch_fpms.ipynb | 273 ++++ ipynb/ch_intro.ipynb | 1399 +++++++++++++++++ ipynb/ch_mcs.ipynb | 799 ++++++++++ ipynb/ch_opt.ipynb | 728 +++++++++ ipynb/ch_opt_julia.ipynb | 209 +++ ipynb/ch_production.ipynb | 737 +++++++++ .../figures/benhabib_mobility_dists.pdf | Bin ipynb/pkg_funcs.ipynb | 582 +++++++ {frontpage => website}/LICENSE.txt | 0 {frontpage => website}/README.txt | 0 .../assets/css/fontawesome-all.min.css | 0 {frontpage => website}/assets/css/main.css | 4 +- .../assets/js/breakpoints.min.js | 0 .../assets/js/browser.min.js | 0 .../assets/js/jquery.min.js | 0 .../assets/js/jquery.scrolly.min.js | 0 {frontpage => website}/assets/js/main.js | 0 {frontpage => website}/assets/js/util.js | 0 .../assets/sass/base/_page.scss | 0 .../assets/sass/base/_reset.scss | 0 .../assets/sass/base/_typography.scss | 0 .../assets/sass/components/_actions.scss | 0 .../assets/sass/components/_arrow.scss | 0 .../assets/sass/components/_box.scss | 0 .../assets/sass/components/_button.scss | 0 .../sass/components/_feature-icons.scss | 0 .../assets/sass/components/_form.scss | 0 .../assets/sass/components/_gallery.scss | 0 .../assets/sass/components/_icon.scss | 0 .../assets/sass/components/_icons.scss | 0 .../assets/sass/components/_image.scss | 0 .../assets/sass/components/_list.scss | 0 .../assets/sass/components/_row.scss | 0 .../assets/sass/components/_table.scss | 0 .../assets/sass/layout/_wrapper.scss | 0 .../assets/sass/libs/_breakpoints.scss | 0 .../assets/sass/libs/_functions.scss | 0 .../assets/sass/libs/_html-grid.scss | 0 .../assets/sass/libs/_mixins.scss | 0 .../assets/sass/libs/_vars.scss | 0 .../assets/sass/libs/_vendor.scss | 0 {frontpage => website}/assets/sass/main.scss | 0 .../assets/webfonts/fa-brands-400.eot | Bin .../assets/webfonts/fa-brands-400.svg | 0 .../assets/webfonts/fa-brands-400.ttf | Bin .../assets/webfonts/fa-brands-400.woff | Bin .../assets/webfonts/fa-brands-400.woff2 | Bin .../assets/webfonts/fa-regular-400.eot | Bin .../assets/webfonts/fa-regular-400.svg | 0 .../assets/webfonts/fa-regular-400.ttf | Bin .../assets/webfonts/fa-regular-400.woff | Bin .../assets/webfonts/fa-regular-400.woff2 | Bin .../assets/webfonts/fa-solid-900.eot | Bin .../assets/webfonts/fa-solid-900.svg | 0 .../assets/webfonts/fa-solid-900.ttf | Bin .../assets/webfonts/fa-solid-900.woff | Bin .../assets/webfonts/fa-solid-900.woff2 | Bin .../images/gallery/fulls/01.jpg | Bin .../images/gallery/fulls/02.jpg | Bin .../images/gallery/fulls/03.jpg | Bin .../images/gallery/fulls/04.jpg | Bin .../images/gallery/fulls/05.jpg | Bin .../images/gallery/fulls/06.jpg | Bin .../images/gallery/fulls/07.jpg | Bin .../images/gallery/fulls/08.jpg | Bin .../images/gallery/fulls/09.jpg | Bin .../images/gallery/fulls/10.jpg | Bin .../images/gallery/thumbs/01.jpg | Bin .../images/gallery/thumbs/02.jpg | Bin .../images/gallery/thumbs/03.jpg | Bin .../images/gallery/thumbs/04.jpg | Bin .../images/gallery/thumbs/05.jpg | Bin .../images/gallery/thumbs/06.jpg | Bin .../images/gallery/thumbs/07.jpg | Bin .../images/gallery/thumbs/08.jpg | Bin .../images/gallery/thumbs/09.jpg | Bin .../images/gallery/thumbs/10.jpg | Bin {frontpage => website}/images/github-mark.png | Bin {frontpage => website}/images/logo-square.svg | 0 {frontpage => website}/images/mail.png | Bin {frontpage => website}/images/network3.jpg | Bin {frontpage => website}/images/pic01.jpg | Bin {frontpage => website}/images/pic02.jpg | Bin .../images/qe-logo-small.png | Bin {frontpage => website}/index.html | 6 +- website/notebooks.html | 85 + 103 files changed, 5597 insertions(+), 3499 deletions(-) rename {code_book/downloads => book}/networks-book.pdf (100%) delete mode 100644 code_book/_config.yml delete mode 100644 code_book/_toc.yml delete mode 100644 code_book/appendix.md delete mode 100644 code_book/ch_fpms.md delete mode 100644 code_book/ch_intro.md delete mode 100644 code_book/ch_mcs.md delete mode 100644 code_book/ch_opt.md delete mode 100644 code_book/ch_opt_julia.md delete mode 100644 code_book/ch_production.md delete mode 100644 code_book/img/betweenness_centrality_1.png delete mode 100644 code_book/img/code.png delete mode 100644 code_book/intro.md delete mode 100644 code_book/pkg_funcs.md delete mode 100644 code_book/status.md create mode 100644 ipynb/appendix.ipynb create mode 100644 ipynb/ch_fpms.ipynb create mode 100644 ipynb/ch_intro.ipynb create mode 100644 ipynb/ch_mcs.ipynb create mode 100644 ipynb/ch_opt.ipynb create mode 100644 ipynb/ch_opt_julia.ipynb create mode 100644 ipynb/ch_production.ipynb rename {code_book => ipynb}/figures/benhabib_mobility_dists.pdf (100%) create mode 100644 ipynb/pkg_funcs.ipynb rename {frontpage => website}/LICENSE.txt (100%) rename {frontpage => website}/README.txt (100%) rename {frontpage => website}/assets/css/fontawesome-all.min.css (100%) rename {frontpage => website}/assets/css/main.css (99%) rename {frontpage => website}/assets/js/breakpoints.min.js (100%) rename {frontpage => website}/assets/js/browser.min.js (100%) rename {frontpage => website}/assets/js/jquery.min.js (100%) rename {frontpage => website}/assets/js/jquery.scrolly.min.js (100%) rename {frontpage => website}/assets/js/main.js (100%) rename {frontpage => website}/assets/js/util.js (100%) rename {frontpage => website}/assets/sass/base/_page.scss (100%) rename {frontpage => website}/assets/sass/base/_reset.scss (100%) rename {frontpage => website}/assets/sass/base/_typography.scss (100%) rename {frontpage => website}/assets/sass/components/_actions.scss (100%) rename {frontpage => website}/assets/sass/components/_arrow.scss (100%) rename {frontpage => website}/assets/sass/components/_box.scss (100%) rename {frontpage => website}/assets/sass/components/_button.scss (100%) rename {frontpage => website}/assets/sass/components/_feature-icons.scss (100%) rename {frontpage => website}/assets/sass/components/_form.scss (100%) rename {frontpage => website}/assets/sass/components/_gallery.scss (100%) rename {frontpage => website}/assets/sass/components/_icon.scss (100%) rename {frontpage => website}/assets/sass/components/_icons.scss (100%) rename {frontpage => website}/assets/sass/components/_image.scss (100%) rename {frontpage => website}/assets/sass/components/_list.scss (100%) rename {frontpage => website}/assets/sass/components/_row.scss (100%) rename {frontpage => website}/assets/sass/components/_table.scss (100%) rename {frontpage => website}/assets/sass/layout/_wrapper.scss (100%) rename {frontpage => website}/assets/sass/libs/_breakpoints.scss (100%) rename {frontpage => website}/assets/sass/libs/_functions.scss (100%) rename {frontpage => website}/assets/sass/libs/_html-grid.scss (100%) rename {frontpage => website}/assets/sass/libs/_mixins.scss (100%) rename {frontpage => website}/assets/sass/libs/_vars.scss (100%) rename {frontpage => website}/assets/sass/libs/_vendor.scss (100%) rename {frontpage => website}/assets/sass/main.scss (100%) rename {frontpage => website}/assets/webfonts/fa-brands-400.eot (100%) rename {frontpage => website}/assets/webfonts/fa-brands-400.svg (100%) rename {frontpage => website}/assets/webfonts/fa-brands-400.ttf (100%) rename {frontpage => website}/assets/webfonts/fa-brands-400.woff (100%) rename {frontpage => website}/assets/webfonts/fa-brands-400.woff2 (100%) rename {frontpage => website}/assets/webfonts/fa-regular-400.eot (100%) rename {frontpage => website}/assets/webfonts/fa-regular-400.svg (100%) rename {frontpage => website}/assets/webfonts/fa-regular-400.ttf (100%) rename {frontpage => website}/assets/webfonts/fa-regular-400.woff (100%) rename {frontpage => website}/assets/webfonts/fa-regular-400.woff2 (100%) rename {frontpage => website}/assets/webfonts/fa-solid-900.eot (100%) rename {frontpage => website}/assets/webfonts/fa-solid-900.svg (100%) rename {frontpage => website}/assets/webfonts/fa-solid-900.ttf (100%) rename {frontpage => website}/assets/webfonts/fa-solid-900.woff (100%) rename {frontpage => website}/assets/webfonts/fa-solid-900.woff2 (100%) rename {frontpage => website}/images/gallery/fulls/01.jpg (100%) rename {frontpage => website}/images/gallery/fulls/02.jpg (100%) rename {frontpage => website}/images/gallery/fulls/03.jpg (100%) rename {frontpage => website}/images/gallery/fulls/04.jpg (100%) rename {frontpage => website}/images/gallery/fulls/05.jpg (100%) rename {frontpage => website}/images/gallery/fulls/06.jpg (100%) rename {frontpage => website}/images/gallery/fulls/07.jpg (100%) rename {frontpage => website}/images/gallery/fulls/08.jpg (100%) rename {frontpage => website}/images/gallery/fulls/09.jpg (100%) rename {frontpage => website}/images/gallery/fulls/10.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/01.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/02.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/03.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/04.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/05.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/06.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/07.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/08.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/09.jpg (100%) rename {frontpage => website}/images/gallery/thumbs/10.jpg (100%) rename {frontpage => website}/images/github-mark.png (100%) rename {frontpage => website}/images/logo-square.svg (100%) rename {frontpage => website}/images/mail.png (100%) rename {frontpage => website}/images/network3.jpg (100%) rename {frontpage => website}/images/pic01.jpg (100%) rename {frontpage => website}/images/pic02.jpg (100%) rename {frontpage => website}/images/qe-logo-small.png (100%) rename {frontpage => website}/index.html (88%) create mode 100644 website/notebooks.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff69dcb..429b04a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,110 +5,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 - - name: Setup Anaconda - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - auto-activate-base: true - miniconda-version: 'latest' - python-version: 3.11 - environment-file: environment.yml - activate-environment: networks - - name: Install latex dependencies - run: | - sudo apt-get -qq update - sudo apt-get install -y \ - texlive-latex-recommended \ - texlive-latex-extra \ - texlive-fonts-recommended \ - texlive-fonts-extra \ - texlive-xetex \ - latexmk \ - xindy \ - dvipng \ - cm-super \ - msttcorefonts - - name: Set up Julia - uses: julia-actions/setup-julia@v1 - with: - version: 1.9 - - name: Install IJulia and Setup Project - shell: bash - run: | - julia -e 'using Pkg; ENV["PYTHON"]="/usr/share/miniconda3"; Pkg.add(["JuMP","GLPK","PyPlot","IJulia"]); using PyPlot' - # - name: Install latex dependencies - # run: | - # sudo apt-get -qq update - # sudo apt-get install -y \ - # texlive-latex-recommended \ - # texlive-latex-extra \ - # texlive-fonts-recommended \ - # texlive-fonts-extra \ - # texlive-xetex \ - # latexmk \ - # xindy \ - # dvipng \ - # cm-super \ - # msttcorefonts - # - name: Display Conda Environment Versions - # shell: bash -l {0} - # run: conda list - # - name: Display Pip Versions - # shell: bash -l {0} - # run: pip list - # - name: Download "build" folder (cache) - # uses: dawidd6/action-download-artifact@v2 - # with: - # workflow: cache.yml - # branch: main - # name: build-cache - # path: _build - # # Build Assets (Download Notebooks and PDF via LaTeX) - # - name: Build PDF from LaTeX - # shell: bash -l {0} - # run: | - # jb build lectures --builder pdflatex --path-output ./ -n --keep-going - # mkdir -p _build/html/_pdf - # cp -u _build/latex/*.pdf _build/html/_pdf - # - name: Build Download Notebooks (sphinx-tojupyter) - # shell: bash -l {0} - # run: | - # jb build lectures --path-output ./ --builder=custom --custom-builder=jupyter - # mkdir -p _build/html/_notebooks - # cp -u _build/jupyter/*.ipynb _build/html/_notebooks - # Build HTML (Website) - - name: Build HTML - shell: bash -l {0} - run: | - jb build code_book --path-output ./ - - name: Upload Execution Reports - uses: actions/upload-artifact@v2 - if: failure() - with: - name: execution-reports - path: _build/html/reports - - name: Custom Front Page - shell: bash -l {0} - run: | - cp -r frontpage/assets _build/html/ - cp -r frontpage/images _build/html/ - cp frontpage/index.html _build/html/ - - name: Book PDF - shell: bash -l {0} - run: | - mkdir -p _build/html/_downloads - cp pdf/networks.pdf _build/html/_downloads/ - cp pdf/networks_chinese.pdf _build/html/_downloads/ - - name: Save Build as Artifact - uses: actions/upload-artifact@v1 - with: - name: _build - path: _build + uses: actions/checkout@v4 - name: Preview Deploy to Netlify - uses: nwtgck/actions-netlify@v1.1 + uses: nwtgck/actions-netlify@v2 with: - publish-dir: '_build/html/' + publish-dir: 'website' production-branch: main github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: "Preview Deploy from GitHub Actions" diff --git a/code_book/downloads/networks-book.pdf b/book/networks-book.pdf similarity index 100% rename from code_book/downloads/networks-book.pdf rename to book/networks-book.pdf diff --git a/code_book/_config.yml b/code_book/_config.yml deleted file mode 100644 index 0e923b4..0000000 --- a/code_book/_config.yml +++ /dev/null @@ -1,49 +0,0 @@ -# Book settings -title: | - Economic Networks - Theory and Computation -author: John Stachurski and Thomas J. Sargent -only_build_toc_files: true - -execute: - timeout: 100 - -sphinx: - extra_extensions: ["sphinx_design"] - config: - html_js_files: - - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js - html_theme: quantecon_book_theme - html_theme_options: - header_organisation_url: https://quantecon.org - header_organisation: QuantEcon - repository_url: https://github.com/quantecon/book-networks - twitter: quantecon - twitter_logo_url: https://assets.quantecon.org/img/qe-twitter-logo.png - og_logo_url: https://assets.quantecon.org/img/qe-og-logo.png - description: | - This website is the companion site for Economic Networks: Theory and Computation written by John Stachurski and Thomas J. Sargent - keywords: Python, QuantEcon, Quantitative Economics, Economics, Networks, Tom J. Sargent, John Stachurski - # google_analytics_id: UA-54984338-10 - mathjax3_config: - tex: - macros: - "Exp" : ["\\operatorname{Exp}"] - "Binomial" : ["\\operatorname{Binomial}"] - "Poisson" : ["\\operatorname{Poisson}"] - "BB" : ["\\mathbb{B}"] - "EE" : ["\\mathbb{E}"] - "PP" : ["\\mathbb{P}"] - "RR" : ["\\mathbb{R}"] - "NN" : ["\\mathbb{N}"] - "ZZ" : ["\\mathbb{Z}"] - "dD" : ["\\mathcal{D}"] - "fF" : ["\\mathcal{F}"] - "lL" : ["\\mathcal{L}"] - "linop" : ["\\mathcal{L}(\\mathbb{B})"] - "linopell" : ["\\mathcal{L}(\\ell_1)"] - -latex: - latex_engine: "xelatex" - latex_documents: - targetname: book.tex \ No newline at end of file diff --git a/code_book/_toc.yml b/code_book/_toc.yml deleted file mode 100644 index 1405efd..0000000 --- a/code_book/_toc.yml +++ /dev/null @@ -1,14 +0,0 @@ -format: jb-book -root: intro -chapters: -- url: https://networks.quantecon.org - title: Home -- file: ch_intro -- file: ch_production -- file: ch_opt -- file: ch_opt_julia -- file: ch_mcs -- file: ch_fpms -- file: appendix -- file: pkg_funcs -- file: status diff --git a/code_book/appendix.md b/code_book/appendix.md deleted file mode 100644 index 9e4909d..0000000 --- a/code_book/appendix.md +++ /dev/null @@ -1,547 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.10.3 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Appendix Code - -We begin with some imports - -```{code-cell} -import numpy as np -import matplotlib.pyplot as plt -import quantecon_book_networks -from mpl_toolkits.mplot3d.axes3d import Axes3D, proj3d -from matplotlib import cm -export_figures = False -quantecon_book_networks.config("matplotlib") -``` - -## Functions - -### One-to-one and onto functions on $(0,1)$ - -We start by defining the domain and our one-to-one and onto function examples. - - -```{code-cell} -x = np.linspace(0, 1, 100) -titles = 'one-to-one', 'onto' -labels = '$f(x)=1/2 + x/2$', '$f(x)=4x(1-x)$' -funcs = lambda x: 1/2 + x/2, lambda x: 4 * x * (1-x) -``` - -The figure can now be produced as follows. - -```{code-cell} -fig, axes = plt.subplots(1, 2, figsize=(10, 4)) -for f, ax, lb, ti in zip(funcs, axes, labels, titles): - ax.set_xlim(0, 1) - ax.set_ylim(0, 1.01) - ax.plot(x, f(x), label=lb) - ax.set_title(ti, fontsize=12) - ax.legend(loc='lower center', fontsize=12, frameon=False) - ax.set_xticks((0, 1)) - ax.set_yticks((0, 1)) - -if export_figures: - plt.savefig("figures/func_types_1.pdf") -plt.show() -``` - -### Some functions are bijections - -This figure can be produced in a similar manner to 6.1. - -```{code-cell} -x = np.linspace(0, 1, 100) -titles = 'constant', 'bijection' -labels = '$f(x)=1/2$', '$f(x)=1-x$' -funcs = lambda x: 1/2 + 0 * x, lambda x: 1-x - -fig, axes = plt.subplots(1, 2, figsize=(10, 4)) -for f, ax, lb, ti in zip(funcs, axes, labels, titles): - ax.set_xlim(0, 1) - ax.set_ylim(0, 1.01) - ax.plot(x, f(x), label=lb) - ax.set_title(ti, fontsize=12) - ax.legend(loc='lower center', fontsize=12, frameon=False) - ax.set_xticks((0, 1)) - ax.set_yticks((0, 1)) - -if export_figures: - plt.savefig("figures/func_types_2.pdf") -plt.show() -``` - -## Fixed Points - -### Graph and fixed points of $G \colon x \mapsto 2.125/(1 + x^{-4})$ - -We begin by defining the domain and the function. - -```{code-cell} -xmin, xmax = 0.0000001, 2 -xgrid = np.linspace(xmin, xmax, 200) -g = lambda x: 2.125 / (1 + x**(-4)) -``` - -Next we define our fixed points. - -```{code-cell} -fps_labels = ('$x_\ell$', '$x_m$', '$x_h$' ) -fps = (0.01, 0.94, 1.98) -coords = ((40, 80), (40, -40), (-40, -80)) -``` - -Finally we can produce the figure. - -```{code-cell} -fig, ax = plt.subplots(figsize=(6.5, 6)) - -ax.set_xlim(xmin, xmax) -ax.set_ylim(xmin, xmax) - -ax.plot(xgrid, g(xgrid), 'b-', lw=2, alpha=0.6, label='$G$') -ax.plot(xgrid, xgrid, 'k-', lw=1, alpha=0.7, label='$45^o$') - -ax.legend(fontsize=14) - -ax.plot(fps, fps, 'ro', ms=8, alpha=0.6) - -for (fp, lb, coord) in zip(fps, fps_labels, coords): - ax.annotate(lb, - xy=(fp, fp), - xycoords='data', - xytext=coord, - textcoords='offset points', - fontsize=16, - arrowprops=dict(arrowstyle="->")) - -if export_figures: - plt.savefig("figures/three_fixed_points.pdf") -plt.show() -``` - -## Complex Numbers - -### The complex number $(a, b) = r e^{i \phi}$ - -We start by abbreviating some useful values and functions. - -```{code-cell} -π = np.pi -zeros = np.zeros -ones = np.ones -fs = 18 -``` - -Next we set our parameters. - -```{code-cell} -r = 2 -φ = π/3 -x = r * np.cos(φ) -x_range = np.linspace(0, x, 1000) -φ_range = np.linspace(0, φ, 1000) -``` - -Finally we produce the plot. - -```{code-cell} -fig = plt.figure(figsize=(7, 7)) -ax = plt.subplot(111, projection='polar') - -ax.plot((0, φ), (0, r), marker='o', color='b', alpha=0.5) # Plot r -ax.plot(zeros(x_range.shape), x_range, color='b', alpha=0.5) # Plot x -ax.plot(φ_range, x / np.cos(φ_range), color='b', alpha=0.5) # Plot y -ax.plot(φ_range, ones(φ_range.shape) * 0.15, color='green') # Plot φ - -ax.margins(0) # Let the plot starts at origin - -ax.set_rmax(2) -ax.set_rticks((1, 2)) # Less radial ticks -ax.set_rlabel_position(-88.5) # Get radial labels away from plotted line - -ax.text(φ, r+0.04 , r'$(a, b) = (1, \sqrt{3})$', fontsize=fs) # Label z -ax.text(φ+0.4, 1 , '$r = 2$', fontsize=fs) # Label r -ax.text(0-0.4, 0.5, '$1$', fontsize=fs) # Label x -ax.text(0.5, 1.2, '$\sqrt{3}$', fontsize=fs) # Label y -ax.text(0.3, 0.25, '$\\varphi = \\pi/3$', fontsize=fs) # Label θ - -xT=plt.xticks()[0] -xL=['0', - r'$\frac{\pi}{4}$', - r'$\frac{\pi}{2}$', - r'$\frac{3\pi}{4}$', - r'$\pi$', - r'$\frac{5\pi}{4}$', - r'$\frac{3\pi}{2}$', - r'$\frac{7\pi}{4}$'] - -plt.xticks(xT, xL, fontsize=fs+2) -ax.grid(True) - -if export_figures: - plt.savefig("figures/complex_number.pdf") -plt.show() -``` - -## Convergence - -### Convergence of a sequence to the origin in $\mathbb{R}^3$ - -We define our transformation matrix, initial point, and number of iterations. - -```{code-cell} -θ = 0.1 -A = ((np.cos(θ), - np.sin(θ), 0.0001), - (np.sin(θ), np.cos(θ), 0.001), - (np.sin(θ), np.cos(θ), 1)) - -A = 0.98 * np.array(A) -p = np.array((1, 1, 1)) -n = 200 -``` - -Now we can produce the plot by repeatedly transforming our point with the transformation matrix and plotting each resulting point. - -```{code-cell} -fig = plt.figure(figsize=(6, 6)) -ax = fig.add_subplot(111, projection='3d') -ax.view_init(elev=20, azim=-40) - -ax.set_xlim((-1.5, 1.5)) -ax.set_ylim((-1.5, 1.5)) -ax.set_xticks((-1,0,1)) -ax.set_yticks((-1,0,1)) - -for i in range(n): - x, y, z = p - ax.plot([x], [y], [z], 'o', ms=4, color=cm.jet_r(i / n)) - p = A @ p - -if export_figures: - plt.savefig("figures/euclidean_convergence_1.pdf") -plt.show() -``` - -## Linear Algebra - -### The span of vectors $u$, $v$, $w$ in $\mathbb{R}$ - -We begin by importing the `FancyArrowPatch` class and extending it. - -```{code-cell} -from matplotlib.patches import FancyArrowPatch - -class Arrow3D(FancyArrowPatch): - def __init__(self, xs, ys, zs, *args, **kwargs): - FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs) - self._verts3d = xs, ys, zs - - def do_3d_projection(self, renderer=None): - xs3d, ys3d, zs3d = self._verts3d - xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) - self.set_positions((xs[0],ys[0]),(xs[1],ys[1])) - - return np.min(zs) - - def draw(self, renderer): - xs3d, ys3d, zs3d = self._verts3d - xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) - self.set_positions((xs[0],ys[0]),(xs[1],ys[1])) - FancyArrowPatch.draw(self, renderer) -``` - -Next we generate our vectors $u$, $v$, $w$, ensuring linear dependence. - -```{code-cell} -α, β = 0.2, 0.1 -def f(x, y): - return α * x + β * y - -# Vector locations, by coordinate -x_coords = np.array((3, 3, -3.5)) -y_coords = np.array((4, -4, 3.0)) -z_coords = f(x_coords, y_coords) - -vecs = [np.array((x, y, z)) for x, y, z in zip(x_coords, y_coords, z_coords)] -``` - -Next we define the spanning plane. - -```{code-cell} -x_min, x_max = -5, 5 -y_min, y_max = -5, 5 - -grid_size = 20 -xr2 = np.linspace(x_min, x_max, grid_size) -yr2 = np.linspace(y_min, y_max, grid_size) -x2, y2 = np.meshgrid(xr2, yr2) -z2 = f(x2, y2) -``` - -Finally we generate the plot. - -```{code-cell} -fig = plt.figure(figsize=(12, 7)) -ax = plt.axes(projection ='3d') -ax.view_init(elev=10., azim=-80) - -ax.set(xlim=(x_min, x_max), - ylim=(x_min, x_max), - zlim=(x_min, x_max), - xticks=(0,), yticks=(0,), zticks=(0,)) - -# Draw the axes -gs = 3 -z = np.linspace(x_min, x_max, gs) -x = np.zeros(gs) -y = np.zeros(gs) -ax.plot(x, y, z, 'k-', lw=2, alpha=0.5) -ax.plot(z, x, y, 'k-', lw=2, alpha=0.5) -ax.plot(y, z, x, 'k-', lw=2, alpha=0.5) - -# Draw the vectors -for v in vecs: - a = Arrow3D([0, v[0]], - [0, v[1]], - [0, v[2]], - mutation_scale=20, - lw=1, - arrowstyle="-|>", - color="b") - ax.add_artist(a) - - -for v, label in zip(vecs, ('u', 'v', 'w')): - v = v * 1.1 - ax.text(v[0], v[1], v[2], - f'${label}$', - fontsize=14) - -# Draw the plane -grid_size = 20 -xr2 = np.linspace(x_min, x_max, grid_size) -yr2 = np.linspace(y_min, y_max, grid_size) -x2, y2 = np.meshgrid(xr2, yr2) -z2 = f(x2, y2) - -ax.plot_surface(x2, y2, z2, rstride=1, cstride=1, cmap=cm.jet, - linewidth=0, antialiased=True, alpha=0.2) - - -if export_figures: - plt.savefig("figures/span1.pdf") -plt.show() -``` - -## Linear Maps Are Matrices - -### Equivalence of the onto and one-to-one properties (for linear maps) - -This plot is produced similarly to Figures 6.1 and 6.2. - -```{code-cell} -fig, axes = plt.subplots(1, 2, figsize=(10, 4)) - -x = np.linspace(-2, 2, 10) - -titles = 'non-bijection', 'bijection' -labels = '$f(x)=0 x$', '$f(x)=0.5 x$' -funcs = lambda x: 0*x, lambda x: 0.5 * x - -for ax, f, lb, ti in zip(axes, funcs, labels, titles): - - # Set the axes through the origin - for spine in ['left', 'bottom']: - ax.spines[spine].set_position('zero') - for spine in ['right', 'top']: - ax.spines[spine].set_color('none') - ax.set_yticks((-1, 1)) - ax.set_ylim((-1, 1)) - ax.set_xlim((-1, 1)) - ax.set_xticks((-1, 1)) - y = f(x) - ax.plot(x, y, '-', linewidth=4, label=lb, alpha=0.6) - ax.text(-0.8, 0.5, ti, fontsize=14) - ax.legend(loc='lower right', fontsize=12) - -if export_figures: - plt.savefig("figures/func_types_3.pdf") -plt.show() -``` - -## Convexity and Polyhedra - -### A polyhedron $P$ represented as intersecting halfspaces - -Inequalities are of the form - -$$ a x + b y \leq c $$ - -To plot the halfspace we plot the line - -$$ y = c/b - a/b x $$ - -and then fill in the halfspace using `fill_between` on points $x, y, \hat y$, -where $\hat y$ is either `y_min` or `y_max`. - -```{code-cell} -fig, ax = plt.subplots() -plt.axis('off') - -x = np.linspace(-10, 14, 200) -y_min, y_max = -2, 3 - -a1, b1, c1 = 1.0, 8.0, -5.0 -y = c1 / b1 - (a1 / b1) * x -ax.plot(x, y, label='$a_1 x_1 + b_1 x_2 = c_1$') -ax.fill_between(x, y, y_max, alpha=0.2) - - -a2, b2, c2 = 0.5, 0.75, 1.5 -y = c2 / b2 - (a2 / b2) * x -ax.plot(x, y, label='$a_2 x_1 + b_2 x_2 = c_2$') -ax.fill_between(x, y, y_min, alpha=0.2) - - -a3, b3, c3 = -1.0, 1.0, 1.5 -y = c3 / b3 - (a3 / b3) * x -ax.plot(x, y, label='$a_3 x_1 + b_3 x_2 = c_3$') -ax.fill_between(x, y, y_min, alpha=0.2) - -ax.plot((0.23,), (1.82,), 'ko') -ax.plot((-1.95,), (-0.4,), 'ko') -ax.plot((4.8,), (-1.2,), 'ko') - - -ax.annotate('$P$', xy=(0, 0), fontsize=12) - -ax.set_ylim(y_min, y_max) -if export_figures: - plt.savefig("figures/polyhedron1.pdf") -plt.show() -``` - -## Saddle Points and Duality - -```{code-cell} -fig = plt.figure(figsize=(8.5, 6)) - -## Top left Plot - -ax = fig.add_subplot(221, projection='3d') - -plot_args = {'rstride': 1, 'cstride': 1, 'cmap':"viridis", - 'linewidth': 0.4, 'antialiased': True, "alpha":0.75, - 'vmin': -1, 'vmax': 1} - -x, y = np.mgrid[-1:1:31j, -1:1:31j] -z = x**2 - y**2 - -ax.plot_surface(x, y, z, **plot_args) - -ax.view_init(azim=245, elev=20) - -ax.set_xlim(-1, 1) -ax.set_ylim(-1, 1) -ax.set_zlim(-1, 1) - -ax.set_xticks([0]) -ax.set_xticklabels([r"$x^*$"], fontsize=16) -ax.set_yticks([0]) -ax.set_yticklabels([r"$\theta^*$"], fontsize=16) - - -ax.set_zticks([]) -ax.set_zticklabels([]) - -ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) -ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) -ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) - -ax.set_zlabel("$L(x,\\theta)$", fontsize=14) - -## Top Right Plot - -ax = fig.add_subplot(222) - - -plot_args = {'cmap':"viridis", 'antialiased': True, "alpha":0.6, - 'vmin': -1, 'vmax': 1} - -x, y = np.mgrid[-1:1:31j, -1:1:31j] -z = x**2 - y**2 - -ax.contourf(x, y, z, **plot_args) - -ax.plot([0], [0], 'ko') - -ax.set_xlim(-1, 1) -ax.set_ylim(-1, 1) - -plt.xticks([ 0], - [r"$x^*$"], fontsize=16) - -plt.yticks([0], - [r"$\theta^*$"], fontsize=16) - -ax.hlines(0, -1, 1, color='k', ls='-', lw=1) -ax.vlines(0, -1, 1, color='k', ls='-', lw=1) - -coords=(-35, 30) -ax.annotate(r'$L(x, \theta^*)$', - xy=(-0.5, 0), - xycoords="data", - xytext=coords, - textcoords="offset points", - fontsize=12, - arrowprops={"arrowstyle" : "->"}) - -coords=(35, 30) -ax.annotate(r'$L(x^*, \theta)$', - xy=(0, 0.25), - xycoords="data", - xytext=coords, - textcoords="offset points", - fontsize=12, - arrowprops={"arrowstyle" : "->"}) - -## Bottom Left Plot - -ax = fig.add_subplot(223) - -x = np.linspace(-1, 1, 100) -ax.plot(x, -x**2, label='$\\theta \mapsto L(x^*, \\theta)$') -ax.set_ylim((-1, 1)) -ax.legend(fontsize=14) -ax.set_xticks([]) -ax.set_yticks([]) - -## Bottom Right Plot - -ax = fig.add_subplot(224) - -x = np.linspace(-1, 1, 100) -ax.plot(x, x**2, label='$x \mapsto L(x, \\theta^*)$') -ax.set_ylim((-1, 1)) -ax.legend(fontsize=14, loc='lower right') -ax.set_xticks([]) -ax.set_yticks([]) - -if export_figures: - plt.savefig("figures/saddle_1.pdf") -plt.show() -``` diff --git a/code_book/ch_fpms.md b/code_book/ch_fpms.md deleted file mode 100644 index d033658..0000000 --- a/code_book/ch_fpms.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Chapter 5 - Nonlinear Interactions (Python Code) - -```{code-cell} -:tags: [hide-output] - -! pip install --upgrade quantecon_book_networks -``` - -We begin with some imports - -```{code-cell} -import quantecon as qe -import quantecon_book_networks -import quantecon_book_networks.input_output as qbn_io -import quantecon_book_networks.plotting as qbn_plt -import quantecon_book_networks.data as qbn_data -export_figures = False -``` - -```{code-cell} -import numpy as np -import networkx as nx -import matplotlib.pyplot as plt -from matplotlib import cm -quantecon_book_networks.config("matplotlib") -``` - -## Financial Networks - -### Equity-Cross Holdings - -Here we define a class for modelling a financial network where firms are linked by share cross-holdings, -and there are failure costs as described by [Elliott et al. (2014)](https://www.aeaweb.org/articles?id=10.1257/aer.104.10.3115). - -```{code-cell} -class FinNet: - - def __init__(self, n=100, c=0.72, d=1, θ=0.5, β=1.0, seed=1234): - - self.n, self.c, self.d, self.θ, self.β = n, c, d, θ, β - np.random.seed(seed) - - self.e = np.ones(n) - self.C, self.C_hat = self.generate_primitives() - self.A = self.C_hat @ np.linalg.inv(np.identity(n) - self.C) - self.v_bar = self.A @ self.e - self.t = np.full(n, θ) - - def generate_primitives(self): - - n, c, d = self.n, self.c, self.d - B = np.zeros((n, n)) - C = np.zeros_like(B) - - for i in range(n): - for j in range(n): - if i != j and np.random.rand() < d/(n-1): - B[i,j] = 1 - - for i in range(n): - for j in range(n): - k = np.sum(B[:,j]) - if k > 0: - C[i,j] = c * B[i,j] / k - - C_hat = np.identity(n) * (1 - c) - - return C, C_hat - - def T(self, v): - Tv = self.A @ (self.e - self.β * np.where(v < self.t, 1, 0)) - return Tv - - def compute_equilibrium(self): - i = 0 - v = self.v_bar - error = 1 - while error > 1e-10: - print(f"number of failing firms is ", np.sum(v < self.θ)) - new_v = self.T(v) - error = np.max(np.abs(new_v - v)) - v = new_v - i = i+1 - - print(f"Terminated after {i} iterations") - return v - - def map_values_to_colors(self, v, j): - cols = cm.plasma(qbn_io.to_zero_one(v)) - if j != 0: - for i in range(len(v)): - if v[i] < self.t[i]: - cols[i] = 0.0 - return cols -``` - -Now we create a financial network. - -```{code-cell} -fn = FinNet(n=100, c=0.72, d=1, θ=0.3, β=1.0) -``` - -And compute its equilibrium. - -```{code-cell} -fn.compute_equilibrium() -``` - -### Waves of bankruptcies in a financial network - -Now we visualise the network after different numbers of iterations. - -For convenience we will first define a function to plot the graphs of the financial network. - -```{code-cell} -def plot_fin_graph(G, ax, node_color_list): - - n = G.number_of_nodes() - - node_pos_dict = nx.spring_layout(G, k=1.1) - edge_colors = [] - - for i in range(n): - for j in range(n): - edge_colors.append(node_color_list[i]) - - - nx.draw_networkx_nodes(G, - node_pos_dict, - node_color=node_color_list, - edgecolors='grey', - node_size=100, - linewidths=2, - alpha=0.8, - ax=ax) - - nx.draw_networkx_edges(G, - node_pos_dict, - edge_color=edge_colors, - alpha=0.4, - ax=ax) -``` - -Now we will iterate by applying the operator $T$ to the vector of firm values $v$ and produce the plots. - -```{code-cell} -G = nx.from_numpy_array(np.array(fn.C), create_using=nx.DiGraph) -v = fn.v_bar - -k = 15 -d = 3 -fig, axes = plt.subplots(int(k/d), 1, figsize=(10, 12)) - -for i in range(k): - if i % d == 0: - ax = axes[int(i/d)] - ax.set_title(f"iteration {i}") - - plot_fin_graph(G, ax, fn.map_values_to_colors(v, i)) - v = fn.T(v) -if export_figures: - plt.savefig("figures/fin_network_sims_1.pdf") -plt.show() -``` - diff --git a/code_book/ch_intro.md b/code_book/ch_intro.md deleted file mode 100644 index 775b781..0000000 --- a/code_book/ch_intro.md +++ /dev/null @@ -1,788 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.15.1 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Chapter 1 - Introduction (Python Code) - -```{code-cell} ipython3 -:tags: [hide-output] -! pip install --upgrade quantecon_book_networks kaleido -``` - -We begin by importing the `quantecon` package as well as some functions and data that have been packaged for release with this text. - -```{code-cell} ipython3 -import quantecon as qe -import quantecon_book_networks -import quantecon_book_networks.input_output as qbn_io -import quantecon_book_networks.data as qbn_data -import quantecon_book_networks.plotting as qbn_plot -ch1_data = qbn_data.introduction() -default_figsize = (6, 4) -export_figures = False -``` - -Next we import some common python libraries. - -```{code-cell} ipython3 -import numpy as np -import pandas as pd -import networkx as nx -import statsmodels.api as sm -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import matplotlib.ticker as ticker -from mpl_toolkits.mplot3d.art3d import Poly3DCollection -import matplotlib.patches as mpatches -import plotly.graph_objects as go -quantecon_book_networks.config("matplotlib") -``` - -## Motivation - -### International trade in crude oil 2021 - -We begin by loading a `NetworkX` directed graph object that represents international trade in crude oil. - -```{code-cell} ipython3 -DG = ch1_data["crude_oil"] -``` - -Next we transform the data to prepare it for display as a Sankey diagram. - -```{code-cell} ipython3 -nodeid = {} -for ix,nd in enumerate(DG.nodes()): - nodeid[nd] = ix - -# Links -source = [] -target = [] -value = [] -for src,tgt in DG.edges(): - source.append(nodeid[src]) - target.append(nodeid[tgt]) - value.append(DG[src][tgt]['weight']) -``` - -Finally we produce our plot. - -```{code-cell} ipython3 -fig = go.Figure(data=[go.Sankey( - node = dict( - pad = 15, - thickness = 20, - line = dict(color = "black", width = 0.5), - label = list(nodeid.keys()), - color = "blue" - ), - link = dict( - source = source, - target = target, - value = value - ))]) - - -fig.update_layout(title_text="Crude Oil", font_size=10, width=600, height=800) -if export_figures: - fig.write_image("figures/crude_oil_2021.pdf") -fig.show(renderer='svg') -``` - -### International trade in commercial aircraft during 2019 - -For this plot we will use a cleaned dataset from -[Harvard, CID Dataverse](https://dataverse.harvard.edu/dataverse/atlas). - -```{code-cell} ipython3 -DG = ch1_data['aircraft_network'] -pos = ch1_data['aircraft_network_pos'] -``` - -We begin by calculating some features of our graph using the `NetworkX` and -the `quantecon_book_networks` packages. - -```{code-cell} ipython3 -centrality = nx.eigenvector_centrality(DG) -node_total_exports = qbn_io.node_total_exports(DG) -edge_weights = qbn_io.edge_weights(DG) -``` - -Now we convert our graph features to plot features. - -```{code-cell} ipython3 -node_pos_dict = pos - -node_sizes = qbn_io.normalise_weights(node_total_exports,10000) -edge_widths = qbn_io.normalise_weights(edge_weights,10) - -node_colors = qbn_io.colorise_weights(list(centrality.values()),color_palette=cm.viridis) -node_to_color = dict(zip(DG.nodes,node_colors)) -edge_colors = [] -for src,_ in DG.edges: - edge_colors.append(node_to_color[src]) -``` - -Finally we produce the plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(10, 10)) -ax.axis('off') - -nx.draw_networkx_nodes(DG, - node_pos_dict, - node_color=node_colors, - node_size=node_sizes, - linewidths=2, - alpha=0.6, - ax=ax) - -nx.draw_networkx_labels(DG, - node_pos_dict, - ax=ax) - -nx.draw_networkx_edges(DG, - node_pos_dict, - edge_color=edge_colors, - width=edge_widths, - arrows=True, - arrowsize=20, - ax=ax, - node_size=node_sizes, - connectionstyle='arc3,rad=0.15') - -if export_figures: - plt.savefig("figures/commercial_aircraft_2019_1.pdf") -plt.show() -``` - -## Spectral Theory - -### Spectral Radii - -Here we provide code for computing the spectral radius of a matrix. - -```{code-cell} ipython3 -def spec_rad(M): - """ - Compute the spectral radius of M. - """ - return np.max(np.abs(np.linalg.eigvals(M))) -``` - -```{code-cell} ipython3 -M = np.array([[1,2],[2,1]]) -spec_rad(M) -``` - -This function is available in the `quantecon_book_networks` package, along with -several other functions for used repeatedly in the text. Source code for -these functions can be seen [here](pkg_funcs). - -```{code-cell} ipython3 -qbn_io.spec_rad(M) -``` - -## Probability - -### The unit simplex in $\mathbb{R}^3$ - -Here we define a function for plotting the unit simplex. - -```{code-cell} ipython3 -def unit_simplex(angle): - - fig = plt.figure(figsize=(10, 8)) - ax = fig.add_subplot(111, projection='3d') - - vtx = [[0, 0, 1], - [0, 1, 0], - [1, 0, 0]] - - tri = Poly3DCollection([vtx], color='darkblue', alpha=0.3) - tri.set_facecolor([0.5, 0.5, 1]) - ax.add_collection3d(tri) - - ax.set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), - xticks=(1,), yticks=(1,), zticks=(1,)) - - ax.set_xticklabels(['$(1, 0, 0)$'], fontsize=16) - ax.set_yticklabels([f'$(0, 1, 0)$'], fontsize=16) - ax.set_zticklabels([f'$(0, 0, 1)$'], fontsize=16) - - ax.xaxis.majorTicks[0].set_pad(15) - ax.yaxis.majorTicks[0].set_pad(15) - ax.zaxis.majorTicks[0].set_pad(35) - - ax.view_init(30, angle) - - # Move axis to origin - ax.xaxis._axinfo['juggled'] = (0, 0, 0) - ax.yaxis._axinfo['juggled'] = (1, 1, 1) - ax.zaxis._axinfo['juggled'] = (2, 2, 0) - - ax.grid(False) - - return ax -``` - -We can now produce the plot. - -```{code-cell} ipython3 -unit_simplex(50) -if export_figures: - plt.savefig("figures/simplex_1.pdf") -plt.show() -``` - -### Independent draws from Student’s t and Normal distributions - -Here we illustrate the occurrence of "extreme" events in heavy tailed distributions. -We start by generating 1,000 samples from a normal distribution and a Student's t distribution. - -```{code-cell} ipython3 -from scipy.stats import t -n = 1000 -np.random.seed(123) - -s = 2 -n_data = np.random.randn(n) * s - -t_dist = t(df=1.5) -t_data = t_dist.rvs(n) -``` - -When we plot our samples, we see the Student's t distribution frequently -generates samples many standard deviations from the mean. - -```{code-cell} ipython3 -fig, axes = plt.subplots(1, 2, figsize=(8, 3.4)) - -for ax in axes: - ax.set_ylim((-50, 50)) - ax.plot((0, n), (0, 0), 'k-', lw=0.3) - -ax = axes[0] -ax.plot(list(range(n)), t_data, linestyle='', marker='o', alpha=0.5, ms=4) -ax.vlines(list(range(n)), 0, t_data, 'k', lw=0.2) -ax.get_xaxis().set_major_formatter( - ticker.FuncFormatter(lambda x, p: format(int(x), ','))) -ax.set_title(f"Student t draws", fontsize=11) - -ax = axes[1] -ax.plot(list(range(n)), n_data, linestyle='', marker='o', alpha=0.5, ms=4) -ax.vlines(list(range(n)), 0, n_data, lw=0.2) -ax.get_xaxis().set_major_formatter( - ticker.FuncFormatter(lambda x, p: format(int(x), ','))) -ax.set_title(f"$N(0, \sigma^2)$ with $\sigma = {s}$", fontsize=11) - -plt.tight_layout() -if export_figures: - plt.savefig("figures/heavy_tailed_draws.pdf") -plt.show() -``` - -### CCDF plots for the Pareto and Exponential distributions - -When the Pareto tail property holds, the CCDF is eventually log linear. Here -we illustrates this using a Pareto distribution. For comparison, an exponential -distribution is also shown. First we define our domain and the Pareto and -Exponential distributions. - -```{code-cell} ipython3 -x = np.linspace(1, 10, 500) -``` - -```{code-cell} ipython3 -α = 1.5 -def Gp(x): - return x**(-α) -``` - -```{code-cell} ipython3 -λ = 1.0 -def Ge(x): - return np.exp(-λ * x) -``` - -We then plot our distribution on a log-log scale. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) - -ax.plot(np.log(x), np.log(Gp(x)), label="Pareto") -ax.plot(np.log(x), np.log(Ge(x)), label="Exponential") - -ax.legend(fontsize=12, frameon=False, loc="lower left") -ax.set_xlabel("$\ln x$", fontsize=12) -ax.set_ylabel("$\ln G(x)$", fontsize=12) - -if export_figures: - plt.savefig("figures/ccdf_comparison_1.pdf") -plt.show() -``` - -### Empirical CCDF plots for largest firms (Forbes) - -Here we show that the distribution of firm sizes has a Pareto tail. We start -by loading the `forbes_global_2000` dataset. - -```{code-cell} ipython3 -dfff = ch1_data['forbes_global_2000'] -``` - -We calculate values of the empirical CCDF. - -```{code-cell} ipython3 -data = np.asarray(dfff['Market Value'])[0:500] -y_vals = np.empty_like(data, dtype='float64') -n = len(data) -for i, d in enumerate(data): - # record fraction of sample above d - y_vals[i] = np.sum(data >= d) / n -``` - -Now we fit a linear trend line (on the log-log scale). - -```{code-cell} ipython3 -x, y = np.log(data), np.log(y_vals) -results = sm.OLS(y, sm.add_constant(x)).fit() -b, a = results.params -``` - -Finally we produce our plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(7.3, 4)) - -ax.scatter(x, y, alpha=0.3, label="firm size (market value)") -ax.plot(x, x * a + b, 'k-', alpha=0.6, label=f"slope = ${a: 1.2f}$") - -ax.set_xlabel('log value', fontsize=12) -ax.set_ylabel("log prob.", fontsize=12) -ax.legend(loc='lower left', fontsize=12) - -if export_figures: - plt.savefig("figures/empirical_powerlaw_plots_firms_forbes.pdf") -plt.show() -``` - -## Graph Theory - -### Zeta and Pareto distributions - -We begin by defining the Zeta and Pareto distributions. - -```{code-cell} ipython3 -γ = 2.0 -α = γ - 1 -``` - -```{code-cell} ipython3 -def z(k, c=2.0): - return c * k**(-γ) - -k_grid = np.arange(1, 10+1) -``` - -```{code-cell} ipython3 -def p(x, c=2.0): - return c * x**(-γ) - -x_grid = np.linspace(1, 10, 200) -``` - -Then we can produce our plot - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) -ax.plot(k_grid, z(k_grid), '-o', label='zeta distribution with $\gamma=2$') -ax.plot(x_grid, p(x_grid), label='density of Pareto with tail index $\\alpha$') -ax.legend(fontsize=12) -ax.set_yticks((0, 1, 2)) -if export_figures: - plt.savefig("figures/zeta_1.pdf") -plt.show() -``` - -### NetworkX digraph plot - -We start by creating a graph object and populating it with edges. - -```{code-cell} ipython3 -G_p = nx.DiGraph() - -edge_list = [ - ('p', 'p'), - ('m', 'p'), ('m', 'm'), ('m', 'r'), - ('r', 'p'), ('r', 'm'), ('r', 'r') -] - -for e in edge_list: - u, v = e - G_p.add_edge(u, v) -``` - -Now we can plot our graph. - -```{code-cell} ipython3 -fig, ax = plt.subplots() -nx.spring_layout(G_p, seed=4) -nx.draw_spring(G_p, ax=ax, node_size=500, with_labels=True, - font_weight='bold', arrows=True, alpha=0.8, - connectionstyle='arc3,rad=0.25', arrowsize=20) -if export_figures: - plt.savefig("figures/networkx_basics_1.pdf") -plt.show() -``` - -The `DiGraph` object has methods that calculate in-degree and out-degree of vertices. - -```{code-cell} ipython3 -G_p.in_degree('p') -``` - -```{code-cell} ipython3 -G_p.out_degree('p') -``` - -Additionally, the `NetworkX` package supplies functions for testing -communication and strong connectedness, as well as to compute strongly -connected components. - -```{code-cell} ipython3 -G = nx.DiGraph() -G.add_edge(1, 1) -G.add_edge(2, 1) -G.add_edge(2, 3) -G.add_edge(3, 2) -list(nx.strongly_connected_components(G)) -``` - -Like `NetworkX`, the Python library `quantecon` -provides access to some graph-theoretic algorithms. - -In the case of QuantEcon's `DiGraph` object, an instance is created via the adjacency matrix. - -```{code-cell} ipython3 -A = ((1, 0, 0), - (1, 1, 1), - (1, 1, 1)) -A = np.array(A) # Convert to NumPy array -G = qe.DiGraph(A) - -G.strongly_connected_components -``` - -### International private credit flows by country - -We begin by loading an adjacency matrix of international private credit flows -(in the form of a NumPy array and a list of country labels). - -```{code-cell} ipython3 -Z = ch1_data["adjacency_matrix"]["Z"] -Z_visual= ch1_data["adjacency_matrix"]["Z_visual"] -countries = ch1_data["adjacency_matrix"]["countries"] -``` - -To calculate our graph's properties, we use hub-based eigenvector -centrality as our centrality measure for this plot. - -```{code-cell} ipython3 -centrality = qbn_io.eigenvector_centrality(Z_visual, authority=False) -``` - -Now we convert our graph features to plot features. - -```{code-cell} ipython3 -node_colors = cm.plasma(qbn_io.to_zero_one_beta(centrality)) -``` - -Finally we produce the plot. - -```{code-cell} ipython3 -X = qbn_io.to_zero_one_beta(Z.sum(axis=1)) - -fig, ax = plt.subplots(figsize=(8, 10)) -plt.axis("off") - -qbn_plot.plot_graph(Z_visual, X, ax, countries, - layout_type='spring', - layout_seed=1234, - node_size_multiple=3000, - edge_size_multiple=0.000006, - tol=0.0, - node_color_list=node_colors) - -if export_figures: - plt.savefig("figures/financial_network_analysis_visualization.pdf") -plt.show() -``` - -### Centrality measures for the credit network - -This figure looks at six different centrality measures. - -We begin by defining a function for calculating eigenvector centrality. - -Hub-based centrality is calculated by default, although authority-based centrality -can be calculated by setting `authority=True`. - -```{code-cell} ipython3 -def eigenvector_centrality(A, k=40, authority=False): - """ - Computes the dominant eigenvector of A. Assumes A is - primitive and uses the power method. - - """ - A_temp = A.T if authority else A - n = len(A_temp) - r = spec_rad(A_temp) - e = r**(-k) * (np.linalg.matrix_power(A_temp, k) @ np.ones(n)) - return e / np.sum(e) -``` - -Here a similar function is defined for calculating Katz centrality. - -```{code-cell} ipython3 -def katz_centrality(A, b=1, authority=False): - """ - Computes the Katz centrality of A, defined as the x solving - - x = 1 + b A x (1 = vector of ones) - - Assumes that A is square. - - If authority=True, then A is replaced by its transpose. - """ - n = len(A) - I = np.identity(n) - C = I - b * A.T if authority else I - b * A - return np.linalg.solve(C, np.ones(n)) -``` - -Now we generate an unweighted version of our matrix to help calculate in-degree and out-degree. - -```{code-cell} ipython3 -D = qbn_io.build_unweighted_matrix(Z) -``` - -We now use the above to calculate the six centrality measures. - -```{code-cell} ipython3 -outdegree = D.sum(axis=1) -ecentral_hub = eigenvector_centrality(Z, authority=False) -kcentral_hub = katz_centrality(Z, b=1/1_700_000) - -indegree = D.sum(axis=0) -ecentral_authority = eigenvector_centrality(Z, authority=True) -kcentral_authority = katz_centrality(Z, b=1/1_700_000, authority=True) -``` - -Here we provide a helper function that returns a DataFrame for each measure. -The DataFrame is ordered by that measure and contains color information. - -```{code-cell} ipython3 -def centrality_plot_data(countries, centrality_measures): - df = pd.DataFrame({'code': countries, - 'centrality':centrality_measures, - 'color': qbn_io.colorise_weights(centrality_measures).tolist() - }) - return df.sort_values('centrality') -``` - -Finally, we plot the various centrality measures. - -```{code-cell} ipython3 -centrality_measures = [outdegree, indegree, - ecentral_hub, ecentral_authority, - kcentral_hub, kcentral_authority] - -ylabels = ['out degree', 'in degree', - 'eigenvector hub','eigenvector authority', - 'Katz hub', 'Katz authority'] - -ylims = [(0, 20), (0, 20), - None, None, - None, None] - - -fig, axes = plt.subplots(3, 2, figsize=(10, 12)) - -axes = axes.flatten() - -for i, ax in enumerate(axes): - df = centrality_plot_data(countries, centrality_measures[i]) - - ax.bar('code', 'centrality', data=df, color=df["color"], alpha=0.6) - - patch = mpatches.Patch(color=None, label=ylabels[i], visible=False) - ax.legend(handles=[patch], fontsize=12, loc="upper left", handlelength=0, frameon=False) - - if ylims[i] is not None: - ax.set_ylim(ylims[i]) - -if export_figures: - plt.savefig("figures/financial_network_analysis_centrality.pdf") -plt.show() -``` - -### Computing in and out degree distributions - -The in-degree distribution evaluated at $k$ is the fraction of nodes in a -network that have in-degree $k$. The in-degree distribution of a `NetworkX` -DiGraph can be calculated using the code below. - -```{code-cell} ipython3 -def in_degree_dist(G): - n = G.number_of_nodes() - iG = np.array([G.in_degree(v) for v in G.nodes()]) - d = [np.mean(iG == k) for k in range(n+1)] - return d -``` - -The out-degree distribution is defined analogously. - -```{code-cell} ipython3 -def out_degree_dist(G): - n = G.number_of_nodes() - oG = np.array([G.out_degree(v) for v in G.nodes()]) - d = [np.mean(oG == k) for k in range(n+1)] - return d -``` - -### Degree distribution for international aircraft trade - -Here we illustrate that the commercial aircraft international trade network is -approximately scale-free by plotting the degree distribution alongside -$f(x)=cx-\gamma$ with $c=0.2$ and $\gamma=1.1$. - -In this calculation of the degree distribution, performed by the NetworkX -function `degree_histogram`, directions are ignored and the network is treated -as an undirected graph. - -```{code-cell} ipython3 -def plot_degree_dist(G, ax, loglog=True, label=None): - "Plot the degree distribution of a graph G on axis ax." - dd = [x for x in nx.degree_histogram(G) if x > 0] - dd = np.array(dd) / np.sum(dd) # normalize - if loglog: - ax.loglog(dd, '-o', lw=0.5, label=label) - else: - ax.plot(dd, '-o', lw=0.5, label=label) -``` - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) - -plot_degree_dist(DG, ax, loglog=False, label='degree distribution') - -xg = np.linspace(0.5, 25, 250) -ax.plot(xg, 0.2 * xg**(-1.1), label='power law') -ax.set_xlim(0.9, 22) -ax.set_ylim(0, 0.25) -ax.legend() -if export_figures: - plt.savefig("figures/commercial_aircraft_2019_2.pdf") -plt.show() -``` - -### Random graphs - -The code to produce the Erdos-Renyi random graph, used below, applies the -combinations function from the `itertools` library. The function -`combinations(A, k)` returns a list of all subsets of $A$ of size $k$. For -example: - -```{code-cell} ipython3 -import itertools -letters = 'a', 'b', 'c' -list(itertools.combinations(letters, 2)) -``` - -Below we generate random graphs using the Erdos-Renyi and Barabasi-Albert -algorithms. Here, for convenience, we will define a function to plot these -graphs. - -```{code-cell} ipython3 -def plot_random_graph(RG,ax): - node_pos_dict = nx.spring_layout(RG, k=1.1) - - centrality = nx.degree_centrality(RG) - node_color_list = qbn_io.colorise_weights(list(centrality.values())) - - edge_color_list = [] - for i in range(n): - for j in range(n): - edge_color_list.append(node_color_list[i]) - - nx.draw_networkx_nodes(RG, - node_pos_dict, - node_color=node_color_list, - edgecolors='grey', - node_size=100, - linewidths=2, - alpha=0.8, - ax=ax) - - nx.draw_networkx_edges(RG, - node_pos_dict, - edge_color=edge_colors, - alpha=0.4, - ax=ax) -``` - -### An instance of an Erdos–Renyi random graph - -```{code-cell} ipython3 -n = 100 -p = 0.05 -G_er = qbn_io.erdos_renyi_graph(n, p, seed=1234) -``` - -```{code-cell} ipython3 -fig, axes = plt.subplots(1, 2, figsize=(10, 3.2)) - -axes[0].set_title("Graph visualization") -plot_random_graph(G_er,axes[0]) - -axes[1].set_title("Degree distribution") -plot_degree_dist(G_er, axes[1], loglog=False) - -if export_figures: - plt.savefig("figures/rand_graph_experiments_1.pdf") -plt.show() -``` - -### An instance of a preferential attachment random graph - -```{code-cell} ipython3 -n = 100 -m = 5 -G_ba = nx.generators.random_graphs.barabasi_albert_graph(n, m, seed=123) -``` - -```{code-cell} ipython3 -fig, axes = plt.subplots(1, 2, figsize=(10, 3.2)) - -axes[0].set_title("Graph visualization") -plot_random_graph(G_ba, axes[0]) - -axes[1].set_title("Degree distribution") -plot_degree_dist(G_ba, axes[1], loglog=False) - -if export_figures: - plt.savefig("figures/rand_graph_experiments_2.pdf") -plt.show() -``` diff --git a/code_book/ch_mcs.md b/code_book/ch_mcs.md deleted file mode 100644 index 198d542..0000000 --- a/code_book/ch_mcs.md +++ /dev/null @@ -1,476 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.10.3 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Chapter 4 - Markov Chains and Networks (Python Code) - -```{code-cell} ---- -tags: [hide-output] ---- -! pip install --upgrade quantecon_book_networks -``` - -We begin with some imports. - -```{code-cell} -import quantecon as qe -import quantecon_book_networks -import quantecon_book_networks.input_output as qbn_io -import quantecon_book_networks.plotting as qbn_plt -import quantecon_book_networks.data as qbn_data -ch4_data = qbn_data.markov_chains_and_networks() -export_figures = False -``` - -```{code-cell} ipython3 -import numpy as np -import pandas as pd -import networkx as nx -import matplotlib.pyplot as plt -from matplotlib import cm -from mpl_toolkits.mplot3d import Axes3D -from mpl_toolkits.mplot3d.art3d import Poly3DCollection -quantecon_book_networks.config("matplotlib") -``` - -## Example transition matrices - -In this chapter two transition matrices are used. - -First, a Markov model is estimated in the international growth dynamics study -of [Quah (1993)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.142.5504&rep=rep1&type=pdf). - -The state is real GDP per capita in a given country relative to the world -average. - -Quah discretizes the possible values to 0–1/4, 1/4–1/2, 1/2–1, 1–2 -and 2–inf, calling these states 1 to 5 respectively. The transitions are over -a one year period. - -```{code-cell} -P_Q = [ - [0.97, 0.03, 0, 0, 0 ], - [0.05, 0.92, 0.03, 0, 0 ], - [0, 0.04, 0.92, 0.04, 0 ], - [0, 0, 0.04, 0.94, 0.02], - [0, 0, 0, 0.01, 0.99] -] -P_Q = np.array(P_Q) -codes_Q = ('1', '2', '3', '4', '5') -``` - -Second, [Benhabib et al. (2015)](https://www.economicdynamics.org/meetpapers/2015/paper_364.pdf) estimate the following transition matrix for intergenerational social mobility. - -The states are percentiles of the wealth distribution. - -In particular, states 1, 2,..., 8, correspond to the percentiles 0-20%, 20-40%, 40-60%, 60-80%, 80-90%, 90-95%, 95-99%, 99-100%. - -```{code-cell} -P_B = [ - [0.222, 0.222, 0.215, 0.187, 0.081, 0.038, 0.029, 0.006], - [0.221, 0.22, 0.215, 0.188, 0.082, 0.039, 0.029, 0.006], - [0.207, 0.209, 0.21, 0.194, 0.09, 0.046, 0.036, 0.008], - [0.198, 0.201, 0.207, 0.198, 0.095, 0.052, 0.04, 0.009], - [0.175, 0.178, 0.197, 0.207, 0.11, 0.067, 0.054, 0.012], - [0.182, 0.184, 0.2, 0.205, 0.106, 0.062, 0.05, 0.011], - [0.123, 0.125, 0.166, 0.216, 0.141, 0.114, 0.094, 0.021], - [0.084, 0.084, 0.142, 0.228, 0.17, 0.143, 0.121, 0.028] - ] - -P_B = np.array(P_B) -codes_B = ('1', '2', '3', '4', '5', '6', '7', '8') -``` - - -## Markov Chains as Digraphs - -### Contour plot of a transition matrix - -Here we define a function for producing contour plots of matrices. - -```{code-cell} -def plot_matrices(matrix, - codes, - ax, - font_size=12, - alpha=0.6, - colormap=cm.viridis, - color45d=None, - xlabel='sector $j$', - ylabel='sector $i$'): - - ticks = range(len(matrix)) - - levels = np.sqrt(np.linspace(0, 0.75, 100)) - - - if color45d != None: - co = ax.contourf(ticks, - ticks, - matrix, - alpha=alpha, cmap=colormap) - ax.plot(ticks, ticks, color=color45d) - else: - co = ax.contourf(ticks, - ticks, - matrix, - levels, - alpha=alpha, cmap=colormap) - - ax.set_xlabel(xlabel, fontsize=font_size) - ax.set_ylabel(ylabel, fontsize=font_size) - ax.set_yticks(ticks) - ax.set_yticklabels(codes_B) - ax.set_xticks(ticks) - ax.set_xticklabels(codes_B) - -``` - -Now we use our function to produce a plot of the transition matrix for -intergenerational social mobility, $P_B$. - -```{code-cell} -fig, ax = plt.subplots(figsize=(6,6)) -plot_matrices(P_B.transpose(), codes_B, ax, alpha=0.75, - colormap=cm.viridis, color45d='black', - xlabel='state at time $t$', ylabel='state at time $t+1$') - -if export_figures: - plt.savefig("figures/markov_matrix_visualization.pdf") -plt.show() -``` - - -### Wealth percentile over time - -Here we compare the mixing of the transition matrix for intergenerational -social mobility $P_B$ and the transition matrix for international growth -dynamics $P_Q$. - -We begin by creating `quantecon` `MarkovChain` objects with each of our transition -matrices. - -```{code-cell} -mc_B = qe.MarkovChain(P_B, state_values=range(1, 9)) -mc_Q = qe.MarkovChain(P_Q, state_values=range(1, 6)) -``` - -Next we define a function to plot simulations of Markov chains. - -Two simulations will be run for each `MarkovChain`, one starting at the -minimum initial value and one at the maximum. - -```{code-cell} -def sim_fig(ax, mc, T=100, seed=14, title=None): - X1 = mc.simulate(T, init=1, random_state=seed) - X2 = mc.simulate(T, init=max(mc.state_values), random_state=seed+1) - ax.plot(X1) - ax.plot(X2) - ax.set_xlabel("time") - ax.set_ylabel("state") - ax.set_title(title, fontsize=12) -``` - -Finally, we produce the figure. - -```{code-cell} -fig, axes = plt.subplots(2, 1, figsize=(6, 4)) -ax = axes[0] -sim_fig(axes[0], mc_B, title="$P_B$") -sim_fig(axes[1], mc_Q, title="$P_Q$") -axes[1].set_yticks((1, 2, 3, 4, 5)) - -plt.tight_layout() -if export_figures: - plt.savefig("figures/benhabib_mobility_mixing.pdf") -plt.show() -``` - - -### Predicted vs realized cross-country income distributions for 2019 - -Here we load a `pandas` `DataFrame` of GDP per capita data for countries compared to the global average. - -```{code-cell} -gdppc_df = ch4_data['gdppc_df'] -gdppc_df.head() -``` - -Now we assign countries bins, as per Quah (1993). - -```{code-cell} -q = [0, 0.25, 0.5, 1.0, 2.0, np.inf] -l = [0, 1, 2, 3, 4] - -x = pd.cut(gdppc_df.gdppc_r, bins=q, labels=l) -gdppc_df['interval'] = x - -gdppc_df = gdppc_df.reset_index() -gdppc_df['interval'] = gdppc_df['interval'].astype(float) -gdppc_df['year'] = gdppc_df['year'].astype(float) -``` - -Here we define a function for calculating the cross-country income -distributions for a given date range. - -```{code-cell} -def gdp_dist_estimate(df, l, yr=(1960, 2019)): - Y = np.zeros(len(l)) - for i in l: - Y[i] = df[ - (df['interval'] == i) & - (df['year'] <= yr[1]) & - (df['year'] >= yr[0]) - ].count()[0] - - return Y / Y.sum() -``` - -We calculate the true distribution for 1985. - -```{code-cell} -ψ_1985 = gdp_dist_estimate(gdppc_df,l,yr=(1985, 1985)) -``` - -Now we use the transition matrix to update the 1985 distribution $t = 2019 - 1985 = 34$ -times to get our predicted 2019 distribution. - -```{code-cell} -ψ_2019_predicted = ψ_1985 @ np.linalg.matrix_power(P_Q, 2019-1985) -``` - -Now, calculate the true 2019 distribution. - -```{code-cell} -ψ_2019 = gdp_dist_estimate(gdppc_df,l,yr=(2019, 2019)) -``` - -Finally we produce the plot. - -```{code-cell} -states = np.arange(0, 5) -ticks = range(5) -codes_S = ('1', '2', '3', '4', '5') - -fig, ax = plt.subplots(figsize=(6, 4)) -width = 0.4 -ax.plot(states, ψ_2019_predicted, '-o', alpha=0.7, label='predicted') -ax.plot(states, ψ_2019, '-o', alpha=0.7, label='realized') -ax.set_xlabel("state") -ax.set_ylabel("probability") -ax.set_yticks((0.15, 0.2, 0.25, 0.3)) -ax.set_xticks(ticks) -ax.set_xticklabels(codes_S) - -ax.legend(loc='upper center', fontsize=12) -if export_figures: - plt.savefig("figures/quah_gdppc_prediction.pdf") -plt.show() - -``` - -### Distribution dynamics - -Here we define a function for plotting the convergence of marginal -distributions $\psi$ under a transition matrix $P$ on the unit simplex. - -```{code-cell} -def convergence_plot(ψ, P, n=14, angle=50): - - ax = qbn_plt.unit_simplex(angle) - - # Convergence plot - - P = np.array(P) - - ψ = ψ # Initial condition - - x_vals, y_vals, z_vals = [], [], [] - for t in range(n): - x_vals.append(ψ[0]) - y_vals.append(ψ[1]) - z_vals.append(ψ[2]) - ψ = ψ @ P - - ax.scatter(x_vals, y_vals, z_vals, c='darkred', s=80, alpha=0.7, depthshade=False) - - mc = qe.MarkovChain(P) - ψ_star = mc.stationary_distributions[0] - ax.scatter(ψ_star[0], ψ_star[1], ψ_star[2], c='k', s=80) - - return ψ - -``` - -Now we define P. - -```{code-cell} -P = ( - (0.9, 0.1, 0.0), - (0.4, 0.4, 0.2), - (0.1, 0.1, 0.8) - ) -``` - -#### A trajectory from $\psi_0 = (0, 0, 1)$ - -Here we see the sequence of marginals appears to converge. - -```{code-cell} -ψ_0 = (0, 0, 1) -ψ = convergence_plot(ψ_0, P) -if export_figures: - plt.savefig("figures/simplex_2.pdf") -plt.show() -``` - -#### A trajectory from $\psi_0 = (0, 1/2, 1/2)$ - -Here we see again that the sequence of marginals appears to converge, and the -limit appears not to depend on the initial distribution. - -```{code-cell} -ψ_0 = (0, 1/2, 1/2) -ψ = convergence_plot(ψ_0, P, n=12) -if export_figures: - plt.savefig("figures/simplex_3.pdf") -plt.show() -``` - - -### Distribution projections from $P_B$ - -Here we define a function for plotting $\psi$ after $n$ iterations of the -transition matrix $P$. The distribution $\psi_0$ is taken as the uniform -distribution over the -state space. - -```{code-cell} -def transition(P, n, ax=None): - - P = np.array(P) - nstates = P.shape[1] - s0 = np.ones(8) * 1/nstates - s = s0 - - for i in range(n): - s = s @ P - - if ax is None: - fig, ax = plt.subplots() - - ax.plot(range(1, nstates+1), s, '-o', alpha=0.6) - ax.set(ylim=(0, 0.25), - xticks=((1, nstates))) - ax.set_title(f"$t = {n}$") - - return ax -``` - -We now generate the marginal distributions after 0, 1, 2, and 100 iterations for $P_B$. - -```{code-cell} -ns = (0, 1, 2, 100) -fig, axes = plt.subplots(1, len(ns), figsize=(6, 4)) - -for n, ax in zip(ns, axes): - ax = transition(P_B, n, ax=ax) - ax.set_xlabel("Quantile") - -plt.tight_layout() -if export_figures: - plt.savefig("figures/benhabib_mobility_dists.pdf") -plt.show() -``` - - - -## Asymptotics - -### Convergence of the empirical distribution to $\psi^*$ - -We begin by creating a `MarkovChain` object, taking $P_B$ as the transition matrix. - -```{code-cell} -mc = qe.MarkovChain(P_B) -``` - -Next we use the `quantecon` package to calculate the true stationary distribution. - -```{code-cell} -stationary = mc.stationary_distributions[0] -n = len(mc.P) -``` - -Now we define a function to simulate the Markov chain. - -```{code-cell} -def simulate_distribution(mc, T=100): - # Simulate path - n = len(mc.P) - path = mc.simulate_indices(ts_length=T, random_state=1) - distribution = np.empty(n) - for i in range(n): - distribution[i] = np.mean(path==i) - return distribution - -``` - -We run simulations of length 10, 100, 1,000 and 10,000. - -```{code-cell} -lengths = [10, 100, 1_000, 10_000] -dists = [] - -for t in lengths: - dists.append(simulate_distribution(mc, t)) -``` - -Now we produce the plots. - -We see that the simulated distribution starts to approach the true stationary distribution. - -```{code-cell} -fig, axes = plt.subplots(2, 2, figsize=(9, 6), sharex='all')#, sharey='all') - -axes = axes.flatten() - -for dist, ax, t in zip(dists, axes, lengths): - - ax.plot(np.arange(n)+1 + .25, - stationary, - '-o', - #width = 0.25, - label='$\\psi^*$', - alpha=0.75) - - ax.plot(np.arange(n)+1, - dist, - '-o', - #width = 0.25, - label=f'$\\hat \\psi_k$ with $k={t}$', - alpha=0.75) - - - ax.set_xlabel("state", fontsize=12) - ax.set_ylabel("prob.", fontsize=12) - ax.set_xticks(np.arange(n)+1) - ax.legend(loc='upper right', fontsize=12, frameon=False) - ax.set_ylim(0, 0.5) - -if export_figures: - plt.savefig("figures/benhabib_ergodicity_1.pdf") -plt.show() -``` diff --git a/code_book/ch_opt.md b/code_book/ch_opt.md deleted file mode 100644 index 2b0dce1..0000000 --- a/code_book/ch_opt.md +++ /dev/null @@ -1,416 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.10.3 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Chapter 3 - Optimal Flows (Python Code) - -```{code-cell} ---- -tags: [hide-output] ---- -! pip install --upgrade quantecon_book_networks -``` - -We begin with some imports - -```{code-cell} -import quantecon as qe -import quantecon_book_networks -import quantecon_book_networks.input_output as qbn_io -import quantecon_book_networks.plotting as qbn_plt -import quantecon_book_networks.data as qbn_data -ch3_data = qbn_data.optimal_flows() -export_figures = False -``` - -```{code-cell} -import numpy as np -from scipy.optimize import linprog -import networkx as nx -import ot -import matplotlib.pyplot as plt -from matplotlib import cm -from matplotlib.patches import Polygon -from matplotlib.artist import Artist -quantecon_book_networks.config("matplotlib") -``` - -## Linear Programming and Duality - -### Betweenness centrality (by color and node size) for the Florentine families - -We load the Florentine Families data from the NetworkX package. -```{code-cell} -G = nx.florentine_families_graph() -``` - -Next we calculate betweenness centrality. - -```{code-cell} -bc_dict = nx.betweenness_centrality(G) -``` - -And we produce the plot. - -```{code-cell} -fig, ax = plt.subplots(figsize=(9.4, 9.4)) - -plt.axis("off") -nx.draw_networkx( - G, - ax=ax, - pos=nx.spring_layout(G, seed=1234), - with_labels=True, - alpha=.8, - arrowsize=15, - connectionstyle="arc3,rad=0.1", - node_size=[10_000*(size+0.1) for size in bc_dict.values()], - node_color=[cm.plasma(bc+0.4) for bc in bc_dict.values()], -) -if export_figures: - plt.savefig("figures/betweenness_centrality_1.pdf") -``` - -### Revenue maximizing quantities and a Python implementation of linear programming - -First we specify our linear program. - -```{code-cell} -A = ((2, 5), - (4, 2)) -b = (30, 20) -c = (-3, -4) # minus in order to minimize -``` - -And now we use SciPy's linear programing module to solve our linear program. - -```{code-cell} -from scipy.optimize import linprog -result = linprog(c, A_ub=A, b_ub=b) -print(result.x) -``` - -Here we produce a visualization of what is being done. - -```{code-cell} -fig, ax = plt.subplots(figsize=(8, 4.5)) -plt.rcParams['font.size'] = '14' - -# Draw constraint lines - -ax.plot(np.linspace(-1, 17.5, 100), 6-0.4*np.linspace(-1, 17.5, 100)) -ax.plot(np.linspace(-1, 5.5, 100), 10-2*np.linspace(-1, 5.5, 100)) -ax.text(10, 2.5, "$2q_1 + 5q_2 \leq 30$") -ax.text(1.5, 8, "$4q_1 + 2q_2 \leq 20$") -ax.text(-2, 2, "$q_2 \geq 0$") -ax.text(2.5, -0.7, "$q_1 \geq 0$") - -# Draw the feasible region -feasible_set = Polygon(np.array([[0, 0], - [0, 6], - [2.5, 5], - [5, 0]])) -ax.add_artist(feasible_set) -Artist.set_alpha(feasible_set, 0.2) - -# Draw the objective function -ax.plot(np.linspace(-1, 5.5, 100), 3.875-0.75*np.linspace(-1, 5.5, 100), 'g-') -ax.plot(np.linspace(-1, 5.5, 100), 5.375-0.75*np.linspace(-1, 5.5, 100), 'g-') -ax.plot(np.linspace(-1, 5.5, 100), 6.875-0.75*np.linspace(-1, 5.5, 100), 'g-') -ax.text(5.8, 1, "revenue $ = 3q_1 + 4q_2$") - -# Draw the optimal solution -ax.plot(2.5, 5, "o", color="black") -ax.text(2.7, 5.2, "optimal solution") - -for spine in ['right', 'top']: - ax.spines[spine].set_color('none') - -ax.set_xticks(()) -ax.set_yticks(()) - -for spine in ['left', 'bottom']: - ax.spines[spine].set_position('zero') - -ax.set_ylim(-1, 8) - -if export_figures: - plt.savefig("figures/linear_programming_1.pdf") -plt.show() -``` - -## Optimal Transport - - -### Transforming one distribution into another - -Below we provide code to produce a visualization of transforming one -distribution into another in one dimension. - -```{code-cell} -σ = 0.1 - -def ϕ(z): - return (1 / np.sqrt(2 * σ**2 * np.pi)) * np.exp(-z**2 / (2 * σ**2)) - -def v(x, a=0.4, b=0.6, s=1.0, t=1.4): - return a * ϕ(x - s) + b * ϕ(x - t) - -fig, ax = plt.subplots(figsize=(10, 4)) - -x = np.linspace(0.2, 4, 1000) -ax.plot(x, v(x), label="$\\phi$") -ax.plot(x, v(x, s=3.0, t=3.3, a=0.6), label="$\\psi$") - -ax.legend(loc='upper left', fontsize=12, frameon=False) - -ax.arrow(1.8, 1.6, 0.8, 0.0, width=0.01, head_width=0.08) -ax.annotate('transform', xy=(1.9, 1.9), fontsize=12) -if export_figures: - plt.savefig("figures/ot_figs_1.pdf") -plt.show() -``` - -### Function to solve a transport problem via linear programming - -Here we define a function to solve optimal transport problems using linear programming. - -```{code-cell} -def ot_solver(phi, psi, c, method='highs-ipm'): - """ - Solve the OT problem associated with distributions phi, psi - and cost matrix c. - Parameters - ---------- - phi : 1-D array - Distribution over the source locations. - psi : 1-D array - Distribution over the target locations. - c : 2-D array - Cost matrix. - """ - n, m = len(phi), len(psi) - - # vectorize c - c_vec = c.reshape((m * n, 1), order='F') - - # Construct A and b - A1 = np.kron(np.ones((1, m)), np.identity(n)) - A2 = np.kron(np.identity(m), np.ones((1, n))) - A = np.vstack((A1, A2)) - b = np.hstack((phi, psi)) - - # Call sover - res = linprog(c_vec, A_eq=A, b_eq=b, method=method) - - # Invert the vec operation to get the solution as a matrix - pi = res.x.reshape((n, m), order='F') - return pi -``` - -Now we can set up a simple optimal transport problem. - -```{code-cell} -phi = np.array((0.5, 0.5)) -psi = np.array((1, 0)) -c = np.ones((2, 2)) -``` - -Next we solve using the above function. - -```{code-cell} -ot_solver(phi, psi, c) -``` - -We see we get the same result as when using the Python optimal transport -package. - -```{code-cell} -ot.emd(phi, psi, c) -``` - -### An optimal transport problem solved by linear programming - -Here we demonstrate a more detailed optimal transport problem. We begin by defining a node class. - -```{code-cell} -class Node: - - def __init__(self, x, y, mass, group, name): - - self.x, self.y = x, y - self.mass, self.group = mass, group - self.name = name -``` - -Now we define a function for randomly generating nodes. - -```{code-cell} -from scipy.stats import betabinom - -def build_nodes_of_one_type(group='phi', n=100, seed=123): - - nodes = [] - np.random.seed(seed) - - for i in range(n): - - if group == 'phi': - m = 1/n - x = np.random.uniform(-2, 2) - y = np.random.uniform(-2, 2) - else: - m = betabinom.pmf(i, n-1, 2, 2) - x = 0.6 * np.random.uniform(-1.5, 1.5) - y = 0.6 * np.random.uniform(-1.5, 1.5) - - name = group + str(i) - nodes.append(Node(x, y, m, group, name)) - - return nodes -``` - -We now generate our source and target nodes. -```{code-cell} -n_phi = 32 -n_psi = 32 - -phi_list = build_nodes_of_one_type(group='phi', n=n_phi) -psi_list = build_nodes_of_one_type(group='psi', n=n_psi) - -phi_probs = [phi.mass for phi in phi_list] -psi_probs = [psi.mass for psi in psi_list] -``` - -Now we define our transport costs. -```{code-cell} -c = np.empty((n_phi, n_psi)) -for i in range(n_phi): - for j in range(n_psi): - x0, y0 = phi_list[i].x, phi_list[i].y - x1, y1 = psi_list[j].x, psi_list[j].y - c[i, j] = np.sqrt((x0-x1)**2 + (y0-y1)**2) -``` - -We solve our optimal transport problem using the Python optimal transport package. - -```{code-cell} -pi = ot.emd(phi_probs, psi_probs, c) -``` - -Finally we produce a graph of our sources, targets, and optimal transport plan. - -```{code-cell} -g = nx.DiGraph() -g.add_nodes_from([phi.name for phi in phi_list]) -g.add_nodes_from([psi.name for psi in psi_list]) - -for i in range(n_phi): - for j in range(n_psi): - if pi[i, j] > 0: - g.add_edge(phi_list[i].name, psi_list[j].name, weight=pi[i, j]) - -node_pos_dict={} -for phi in phi_list: - node_pos_dict[phi.name] = (phi.x, phi.y) -for psi in psi_list: - node_pos_dict[psi.name] = (psi.x, psi.y) - -node_color_list = [] -node_size_list = [] -scale = 8_000 -for phi in phi_list: - node_color_list.append('blue') - node_size_list.append(phi.mass * scale) -for psi in psi_list: - node_color_list.append('red') - node_size_list.append(psi.mass * scale) - -fig, ax = plt.subplots(figsize=(7, 10)) -plt.axis('off') - -nx.draw_networkx_nodes(g, - node_pos_dict, - node_color=node_color_list, - node_size=node_size_list, - edgecolors='grey', - linewidths=1, - alpha=0.5, - ax=ax) - -nx.draw_networkx_edges(g, - node_pos_dict, - arrows=True, - connectionstyle='arc3,rad=0.1', - alpha=0.6) -if export_figures: - plt.savefig("figures/ot_large_scale_1.pdf") -plt.show() -``` - -### Solving linear assignment as an optimal transport problem - -Here we set up a linear assignment problem (matching $n$ workers to $n$ jobs). - -```{code-cell} -n = 4 -phi = np.ones(n) -psi = np.ones(n) -``` - -We generate our cost matrix (the cost of training the $i$th worker for the $j$th job) - -```{code-cell} -c = np.random.uniform(size=(n, n)) -``` - -Finally, we solve our linear assignment problem as a special case of optimal transport. - -```{code-cell} -ot.emd(phi, psi, c) -``` - -### Python Spatial Analysis library - -Readers interested in computational optimal transport should also consider -PySAL, the [Python Spatial Analysis library](https://pysal.org/). See, for -example, https://pysal.org/spaghetti/notebooks/transportation-problem.html. - -### The General Flow Problem - -Here we solve a simple network flow problem as a linear program. We begin by -defining the node-edge incidence matrix. - -```{code-cell} -A = ( -( 1, 1, 0, 0), -(-1, 0, 1, 0), -( 0, 0, -1, 1), -( 0, -1, 0, -1) -) -``` - -Now we define exogenous supply and transport costs. - -```{code-cell} -b = (10, 0, 0, -10) -c = (1, 4, 1, 1) -``` - -Finally we solve as a linear program. - -```{code-cell} -result = linprog(c, A_eq=A, b_eq=b, method='highs-ipm') -print(result.x) -``` diff --git a/code_book/ch_opt_julia.md b/code_book/ch_opt_julia.md deleted file mode 100644 index 6cd231f..0000000 --- a/code_book/ch_opt_julia.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.10.3 -kernelspec: - display_name: Julia - language: Julia - name: julia-1.9 ---- - - -# Chapter 3 - Optimal Flows (Julia Code) - -## Bellman’s Method - -Here we demonstrate solving a shortest path problem using Bellman's method. - -Our first step is to set up the cost function, which we store as an array -called `c`. Note that we set `c[i, j] = Inf` when no edge exists from `i` to -`j`. - -```{code-cell} -:tags: ["remove-output"] - -c = fill(Inf, (7, 7)) -c[1, 2], c[1, 3], c[1, 4] = 1, 5, 3 -c[2, 4], c[2, 5] = 9, 6 -c[3, 6] = 2 -c[4, 6] = 4 -c[5, 7] = 4 -c[6, 7] = 1 -c[7, 7] = 0 -``` - -Next we define the Bellman operator. - -```{code-cell} -:tags: ["remove-output"] - -function T(q) - Tq = similar(q) - n = length(q) - for x in 1:n - Tq[x] = minimum(c[x, :] + q[:]) - end - return Tq -end -``` - -Now we arbitrarily set $q \equiv 0$, generate the sequence of iterates $T_q$, -$T^2_q$, $T^3_q$ and plot them. By $T^3_q$ has already converged on $q^∗$. - -```{code-cell} -using PyPlot -export_figures = false -fig, ax = plt.subplots(figsize=(6, 4)) - -n = 7 -q = zeros(n) -ax.plot(1:n, q) -ax.set_xlabel("cost-to-go") -ax.set_ylabel("nodes") - -for i in 1:3 - new_q = T(q) - ax.plot(1:n, new_q, "-o", alpha=0.7, label="iterate $i") - q = new_q -end - -ax.legend() -if export_figures == true - plt.savefig("figures/shortest_path_iter_1.pdf") -end -``` - -## Linear programming - -When solving linear programs, one option is to use a domain specific modeling -language to set out the objective and constraints in the optimization problem. -Here we demonstrate the Julia package `JuMP`. - -```{code-cell} -using JuMP -using GLPK -``` - -We create our model object and select our solver. - -```{code-cell} -m = Model() -set_optimizer(m, GLPK.Optimizer) -``` - -Now we add variables, constraints and an objective to our model. - -```{code-cell} -:tags: ["remove-output"] - -@variable(m, q1 >= 0) -@variable(m, q2 >= 0) -@constraint(m, 2q1 + 5q2 <= 30) -@constraint(m, 4q1 + 2q2 <= 20) -@objective(m, Max, 3q1 + 4q2) -``` - -Finally we solve our linear program. - -```{code-cell} -optimize!(m) - -println(value.(q1)) -println(value.(q2)) -``` diff --git a/code_book/ch_production.md b/code_book/ch_production.md deleted file mode 100644 index c6d7b4a..0000000 --- a/code_book/ch_production.md +++ /dev/null @@ -1,416 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.11.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Chapter 2 - Production (Python Code) - -```{code-cell} ipython3 ---- -tags: [hide-output] ---- -! pip install --upgrade quantecon_book_networks -``` - -We begin with some imports. - -```{code-cell} ipython3 -import quantecon as qe -import quantecon_book_networks -import quantecon_book_networks.input_output as qbn_io -import quantecon_book_networks.plotting as qbn_plt -import quantecon_book_networks.data as qbn_data -ch2_data = qbn_data.production() -default_figsize = (6, 4) -export_figures = False -``` - -```{code-cell} ipython3 -import numpy as np -import pandas as pd -import networkx as nx -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import matplotlib.colors as plc -from matplotlib import cm -quantecon_book_networks.config("matplotlib") -``` - -## Multisector Models - -We start by loading a graph of linkages between 15 US sectors in 2021. - -Our graph comes as a list of sector codes, an adjacency matrix of sales between -the sectors, and a list the total sales of each sector. - -In particular, `Z[i,j]` is the sales from industry `i` to industry `j`, and `X[i]` is the the total sales -of each sector `i`. - -```{code-cell} ipython3 -codes = ch2_data["us_sectors_15"]["codes"] -Z = ch2_data["us_sectors_15"]["adjacency_matrix"] -X = ch2_data["us_sectors_15"]["total_industry_sales"] -``` - -Now we define a function to build coefficient matrices. - -Two coefficient matrices are returned. The backward linkage case, where sales -between sector `i` and `j` are given as a fraction of total sales of sector -`j`. The forward linkage case, where sales between sector `i` and `j` are -given as a fraction of total sales of sector `i`. - -```{code-cell} ipython3 -def build_coefficient_matrices(Z, X): - """ - Build coefficient matrices A and F from Z and X via - - A[i, j] = Z[i, j] / X[j] - F[i, j] = Z[i, j] / X[i] - - """ - A, F = np.empty_like(Z), np.empty_like(Z) - n = A.shape[0] - for i in range(n): - for j in range(n): - A[i, j] = Z[i, j] / X[j] - F[i, j] = Z[i, j] / X[i] - - return A, F - -A, F = build_coefficient_matrices(Z, X) -``` - -### Backward linkages for 15 US sectors in 2021 - -Here we calculate the hub-based eigenvector centrality of our backward linkage coefficient matrix. - -```{code-cell} ipython3 -centrality = qbn_io.eigenvector_centrality(A) -``` - -Now we use the `quantecon_book_networks` package to produce our plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(8, 10)) -plt.axis("off") -color_list = qbn_io.colorise_weights(centrality, beta=False) -# Remove self-loops -A1 = A.copy() -for i in range(A1.shape[0]): - A1[i][i] = 0 -qbn_plt.plot_graph(A1, X, ax, codes, - layout_type='spring', - layout_seed=5432167, - tol=0.0, - node_color_list=color_list) - -if export_figures: - plt.savefig("figures/input_output_analysis_15.pdf") -plt.show() -``` - -### Eigenvector centrality of across US industrial sectors - -Now we plot a bar chart of hub-based eigenvector centrality by sector. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) -ax.bar(codes, centrality, color=color_list, alpha=0.6) -ax.set_ylabel("eigenvector centrality", fontsize=12) -if export_figures: - plt.savefig("figures/input_output_analysis_15_ec.pdf") -plt.show() -``` - -### Output multipliers across 15 US industrial sectors - -Output multipliers are equal to the authority-based Katz centrality measure of -the backward linkage coefficient matrix. - -Here we calculate authority-based -Katz centrality using the `quantecon_book_networks` package. - -```{code-cell} ipython3 -omult = qbn_io.katz_centrality(A, authority=True) -``` - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) -omult_color_list = qbn_io.colorise_weights(omult,beta=False) -ax.bar(codes, omult, color=omult_color_list, alpha=0.6) -ax.set_ylabel("Output multipliers", fontsize=12) -if export_figures: - plt.savefig("figures/input_output_analysis_15_omult.pdf") -plt.show() -``` - -### Forward linkages and upstreamness over US industrial sectors - -Upstreamness is the hub-based Katz centrality of the forward linkage -coefficient matrix. - -Here we calculate hub-based Katz centrality. - -```{code-cell} ipython3 -upstreamness = qbn_io.katz_centrality(F) -``` - -Now we plot the network. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(8, 10)) -plt.axis("off") -upstreamness_color_list = qbn_io.colorise_weights(upstreamness,beta=False) -# Remove self-loops -for i in range(F.shape[0]): - F[i][i] = 0 -qbn_plt.plot_graph(F, X, ax, codes, - layout_type='spring', # alternative layouts: spring, circular, random, spiral - layout_seed=5432167, - tol=0.0, - node_color_list=upstreamness_color_list) - -if export_figures: - plt.savefig("figures/input_output_analysis_15_fwd.pdf") -plt.show() -``` - -### Relative upstreamness of US industrial sectors - -Here we produce a barplot of upstreamness. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) -ax.bar(codes, upstreamness, color=upstreamness_color_list, alpha=0.6) -ax.set_ylabel("upstreamness", fontsize=12) -if export_figures: - plt.savefig("figures/input_output_analysis_15_up.pdf") -plt.show() -``` - -### Hub-based Katz centrality of across 15 US industrial sectors - -Next we plot the hub-based Katz centrality of the backward linkage coefficient matrix. - -```{code-cell} ipython3 -kcentral = qbn_io.katz_centrality(A) -``` - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=default_figsize) -kcentral_color_list = qbn_io.colorise_weights(kcentral,beta=False) -ax.bar(codes, kcentral, color=kcentral_color_list, alpha=0.6) -ax.set_ylabel("Katz hub centrality", fontsize=12) -if export_figures: - plt.savefig("figures/input_output_analysis_15_katz.pdf") -plt.show() -``` - -### The Leontief inverse 𝐿 (hot colors are larger values) - -We construct the Leontief inverse matrix from 15 sector adjacency matrix. - -```{code-cell} ipython3 -I = np.identity(len(A)) -L = np.linalg.inv(I - A) -``` - -Now we produce the plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(6.5, 5.5)) - -ticks = range(len(L)) - -levels = np.sqrt(np.linspace(0, 0.75, 100)) - -co = ax.contourf(ticks, - ticks, - L, - levels, - alpha=0.85, cmap=cm.plasma) - -ax.set_xlabel('sector $j$', fontsize=12) -ax.set_ylabel('sector $i$', fontsize=12) -ax.set_yticks(ticks) -ax.set_yticklabels(codes) -ax.set_xticks(ticks) -ax.set_xticklabels(codes) - -if export_figures: - plt.savefig("figures/input_output_analysis_15_leo.pdf") -plt.show() -``` - -### Propagation of demand shocks via backward linkages - -We begin by generating a demand shock vector $d$. - -```{code-cell} ipython3 -N = len(A) -np.random.seed(1234) -d = np.random.rand(N) -d[6] = 1 # positive shock to agriculture -``` - -Now we simulate the demand shock propagating through the economy. - -```{code-cell} ipython3 -sim_length = 11 -x = d -x_vecs = [] -for i in range(sim_length): - if i % 2 ==0: - x_vecs.append(x) - x = A @ x -``` - -Finally, we plot the shock propagating through the economy. - -```{code-cell} ipython3 -fig, axes = plt.subplots(3, 2, figsize=(8, 10)) -axes = axes.flatten() - -for ax, x_vec, i in zip(axes, x_vecs, range(sim_length)): - if i % 2 != 0: - pass - ax.set_title(f"round {i*2}") - x_vec_cols = qbn_io.colorise_weights(x_vec,beta=False) - # remove self-loops - for i in range(len(A)): - A[i][i] = 0 - qbn_plt.plot_graph(A, X, ax, codes, - layout_type='spring', - layout_seed=342156, - node_color_list=x_vec_cols, - node_size_multiple=0.00028, - edge_size_multiple=0.8) - -plt.tight_layout() -if export_figures: - plt.savefig("figures/input_output_analysis_15_shocks.pdf") -plt.show() -``` - -### Network for 71 US sectors in 2021 - -We start by loading a graph of linkages between 71 US sectors in 2021. - -```{code-cell} ipython3 -codes_71 = ch2_data['us_sectors_71']['codes'] -A_71 = ch2_data['us_sectors_71']['adjacency_matrix'] -X_71 = ch2_data['us_sectors_71']['total_industry_sales'] -``` - -Next we calculate our graph's properties. - -We use hub-based eigenvector centrality as our centrality measure for this plot. - -```{code-cell} ipython3 -centrality_71 = qbn_io.eigenvector_centrality(A_71) -color_list_71 = qbn_io.colorise_weights(centrality_71,beta=False) -``` - -Finally we produce the plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(10, 12)) -plt.axis("off") -# Remove self-loops -for i in range(A_71.shape[0]): - A_71[i][i] = 0 -qbn_plt.plot_graph(A_71, X_71, ax, codes_71, - node_size_multiple=0.0005, - edge_size_multiple=4.0, - layout_type='spring', - layout_seed=5432167, - tol=0.01, - node_color_list=color_list_71) - -if export_figures: - plt.savefig("figures/input_output_analysis_71.pdf") -plt.show() -``` - -### Network for 114 Australian industry sectors in 2020 - -Next we load a graph of linkages between 114 Australian sectors in 2020. - -```{code-cell} ipython3 -codes_114 = ch2_data['au_sectors_114']['codes'] -A_114 = ch2_data['au_sectors_114']['adjacency_matrix'] -X_114 = ch2_data['au_sectors_114']['total_industry_sales'] -``` - -Next we calculate our graph's properties. - -We use hub-based eigenvector centrality as our centrality measure for this plot. - -```{code-cell} ipython3 -centrality_114 = qbn_io.eigenvector_centrality(A_114) -color_list_114 = qbn_io.colorise_weights(centrality_114,beta=False) -``` - -Finally we produce the plot. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(11, 13.2)) -plt.axis("off") -# Remove self-loops -for i in range(A_114.shape[0]): - A_114[i][i] = 0 -qbn_plt.plot_graph(A_114, X_114, ax, codes_114, - node_size_multiple=0.008, - edge_size_multiple=5.0, - layout_type='spring', - layout_seed=5432167, - tol=0.03, - node_color_list=color_list_114) - -if export_figures: - plt.savefig("figures/input_output_analysis_aus_114.pdf") -plt.show() -``` - -### GDP growth rates and std. deviations (in parentheses) for 8 countries - -Here we load a `pandas` DataFrame of GDP growth rates. - -```{code-cell} ipython3 -gdp_df = ch2_data['gdp_df'] -gdp_df.head() -``` - -Now we plot the growth rates and calculate their standard deviations. - -```{code-cell} ipython3 -fig, axes = plt.subplots(5, 2, figsize=(8, 9)) -axes = axes.flatten() - -countries = gdp_df.columns -t = np.asarray(gdp_df.index.astype(float)) -series = [np.asarray(gdp_df[country].astype(float)) for country in countries] - - -for ax, country, gdp_data in zip(axes, countries, series): - - ax.plot(t, gdp_data) - ax.set_title(f'{country} (${gdp_data.std():1.2f}$\%)' ) - ax.set_ylabel('\%') - ax.set_ylim((-12, 14)) - -plt.tight_layout() -if export_figures: - plt.savefig("figures/gdp_growth.pdf") -plt.show() -``` diff --git a/code_book/img/betweenness_centrality_1.png b/code_book/img/betweenness_centrality_1.png deleted file mode 100644 index 6ec8f49ae4fb82b1484e19dfb2015a72259483a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64051 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 diff --git a/code_book/img/code.png b/code_book/img/code.png deleted file mode 100644 index e1820ad84b6a4427322937fd0b8a0dd9cf3b184c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29941 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

>>akgDjkWQ5*gf(626p>}3?;Of`O1R{hGcVNPP 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

3q7X)NY^C@T7z64lr_X;heW5-p}( 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 - ``` ---- -# Economic Networks {{ br }} Theory and Computation (Code Book) - -**Authors**: [John Stachurski](https://johnstachurski.net/) and [Thomas J. Sargent](http://www.tomsargent.com/) - -```{tableofcontents} -``` \ No newline at end of file diff --git a/code_book/pkg_funcs.md b/code_book/pkg_funcs.md deleted file mode 100644 index 2ed0e5d..0000000 --- a/code_book/pkg_funcs.md +++ /dev/null @@ -1,359 +0,0 @@ ---- -jupytext: - cell_metadata_filter: -all - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.10.3 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# quantecon_book_networks - -## input_output - -### node_total_exports -```{code-cell} -def node_total_exports(G): - node_exports = [] - for node1 in G.nodes(): - total_export = 0 - for node2 in G[node1]: - total_export += G[node1][node2]['weight'] - node_exports.append(total_export) - return node_exports -``` - -### node_total_imports -```{code-cell} -def node_total_imports(G): - node_imports = [] - for node1 in G.nodes(): - total_import = 0 - for node2 in G[node1]: - total_import += G[node2][node1]['weight'] - node_imports.append(total_import) - return node_imports -``` - -### edge_weights -```{code-cell} -def edge_weights(G): - edge_weights = [G[u][v]['weight'] for u,v in G.edges()] - return edge_weights -``` - -### normalise_weights -```{code-cell} -def normalise_weights(weights,scalar=1): - max_value = np.max(weights) - return [scalar * (weight / max_value) for weight in weights] -``` - -### to_zero_one -```{code-cell} -def to_zero_one(x): - "Map vector x to the zero one interval." - x = np.array(x) - x_min, x_max = x.min(), x.max() - return (x - x_min)/(x_max - x_min) -``` - -### to_zero_one_beta -```{code-cell} -def to_zero_one_beta(x, - qrange=[0.25, 0.75], - beta_para=[0.5, 0.5]): - - """ - Nonlinearly map vector x to the zero one interval with beta distribution. - https://en.wikipedia.org/wiki/Beta_distribution - """ - x = np.array(x) - x_min, x_max = x.min(), x.max() - if beta_para != None: - a, b = beta_para - return beta.cdf((x - x_min) /(x_max - x_min), a, b) - else: - q1, q2 = qrange - return (x - x_min) * (q2 - q1) /(x_max - x_min) + q1 -``` - -### colorise_weights -```{code-cell} -import matplotlib.cm as cm -def colorise_weights(weights,beta=True,color_palette=cm.plasma): - if beta: - cp = color_palette(to_zero_one_beta(weights)) - else: - cp = color_palette(to_zero_one(weights)) - return cp -``` - -### spec_rad -```{code-cell} -def spec_rad(M): - """ - Compute the spectral radius of M. - """ - return np.max(np.abs(np.linalg.eigvals(M))) -``` - -### adjacency_matrix_to_graph -```{code-cell} -def adjacency_matrix_to_graph(A, - codes, - tol=0.0): # clip entries below tol - """ - Build a networkx graph object given an adjacency matrix - """ - G = nx.DiGraph() - N = len(A) - - # Add nodes - for i, code in enumerate(codes): - G.add_node(code, name=code) - - # Add the edges - for i in range(N): - for j in range(N): - a = A[i, j] - if a > tol: - G.add_edge(codes[i], codes[j], weight=a) - - return G -``` - -### eigenvector_centrality -```{code-cell} -def eigenvector_centrality(A, k=40, authority=False): - """ - Computes the dominant eigenvector of A. Assumes A is - primitive and uses the power method. - """ - A_temp = A.T if authority else A - n = len(A_temp) - r = spec_rad(A_temp) - e = r**(-k) * (np.linalg.matrix_power(A_temp, k) @ np.ones(n)) - return e / np.sum(e) -``` - -### katz_centrality -```{code-cell} -def katz_centrality(A, b=1, authority=False): - """ - Computes the Katz centrality of A, defined as the x solving - - x = 1 + b A x (1 = vector of ones) - - Assumes that A is square. - - If authority=True, then A is replaced by its transpose. - """ - n = len(A) - I = np.identity(n) - C = I - b * A.T if authority else I - b * A - return np.linalg.solve(C, np.ones(n)) -``` - -### build_unweighted_matrix -```{code-cell} -def build_unweighted_matrix(Z, tol=1e-5): - """ - return a unweighted adjacency matrix - """ - return 1*(Z>tol) -``` - -### erdos_renyi_graph -```{code-cell} -def erdos_renyi_graph(n=100, p=0.5, seed=1234): - "Returns an Erdős-Rényi random graph." - - np.random.seed(seed) - edges = itertools.combinations(range(n), 2) - G = nx.Graph() - - for e in edges: - if np.random.rand() < p: - G.add_edge(*e) - return G -``` - -### build_coefficient_matrices -```{code-cell} -def build_coefficient_matrices(Z, X): - """ - Build coefficient matrices A and F from Z and X via - - A[i, j] = Z[i, j] / X[j] - F[i, j] = Z[i, j] / X[i] - - """ - A, F = np.empty_like(Z), np.empty_like(Z) - n = A.shape[0] - for i in range(n): - for j in range(n): - A[i, j] = Z[i, j] / X[j] - F[i, j] = Z[i, j] / X[i] - - return A, F -``` - -## plotting - -### plot_graph -```{code-cell} -def plot_graph(A, - X, - ax, - codes, - node_color_list=None, - node_size_multiple=0.0005, - edge_size_multiple=14, - layout_type='circular', - layout_seed=1234, - tol=0.03): # clip entries below tol - - G = nx.DiGraph() - N = len(A) - - # Add nodes, with weights by sales of the sector - for i, w in enumerate(X): - G.add_node(codes[i], weight=w, name=codes[i]) - - node_sizes = X * node_size_multiple - - # Position the nodes - if layout_type == 'circular': - node_pos_dict = nx.circular_layout(G) - elif layout_type == 'spring': - node_pos_dict = nx.spring_layout(G, seed=layout_seed) - elif layout_type == 'random': - node_pos_dict = nx.random_layout(G, seed=layout_seed) - elif layout_type == 'spiral': - node_pos_dict = nx.spiral_layout(G) - - # Add the edges, along with their colors and widths - edge_colors = [] - edge_widths = [] - for i in range(N): - for j in range(N): - a = A[i, j] - if a > tol: - G.add_edge(codes[i], codes[j]) - edge_colors.append(node_color_list[i]) - width = a * edge_size_multiple - edge_widths.append(width) - - # Plot the networks - nx.draw_networkx_nodes(G, - node_pos_dict, - node_color=node_color_list, - node_size=node_sizes, - edgecolors='grey', - linewidths=2, - alpha=0.6, - ax=ax) - - nx.draw_networkx_labels(G, - node_pos_dict, - font_size=10, - ax=ax) - - nx.draw_networkx_edges(G, - node_pos_dict, - edge_color=edge_colors, - width=edge_widths, - arrows=True, - arrowsize=20, - alpha=0.6, - ax=ax, - arrowstyle='->', - node_size=node_sizes, - connectionstyle='arc3,rad=0.15') -``` - -### plot_matrices -```{code-cell} -def plot_matrices(matrix, - codes, - ax, - font_size=12, - alpha=0.6, - colormap=cm.viridis, - color45d=None, - xlabel='sector $j$', - ylabel='sector $i$'): - - ticks = range(len(matrix)) - - levels = np.sqrt(np.linspace(0, 0.75, 100)) - - - if color45d != None: - co = ax.contourf(ticks, - ticks, - matrix, -# levels, - alpha=alpha, cmap=colormap) - ax.plot(ticks, ticks, color=color45d) - else: - co = ax.contourf(ticks, - ticks, - matrix, - levels, - alpha=alpha, cmap=colormap) - - #plt.colorbar(co) - - ax.set_xlabel(xlabel, fontsize=font_size) - ax.set_ylabel(ylabel, fontsize=font_size) - ax.set_yticks(ticks) - ax.set_yticklabels(codes) - ax.set_xticks(ticks) - ax.set_xticklabels(codes) -``` - -### unit_simplex -```{code-cell} -def unit_simplex(angle): - - fig = plt.figure(figsize=(10, 8)) - ax = fig.add_subplot(111, projection='3d') - - vtx = [[0, 0, 1], - [0, 1, 0], - [1, 0, 0]] - - tri = Poly3DCollection([vtx], color='darkblue', alpha=0.3) - tri.set_facecolor([0.5, 0.5, 1]) - ax.add_collection3d(tri) - - ax.set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), - xticks=(1,), yticks=(1,), zticks=(1,)) - - ax.set_xticklabels(['$(1, 0, 0)$'], fontsize=16) - ax.set_yticklabels([f'$(0, 1, 0)$'], fontsize=16) - ax.set_zticklabels([f'$(0, 0, 1)$'], fontsize=16) - - ax.xaxis.majorTicks[0].set_pad(15) - ax.yaxis.majorTicks[0].set_pad(15) - ax.zaxis.majorTicks[0].set_pad(35) - - ax.view_init(30, angle) - - # Move axis to origin - ax.xaxis._axinfo['juggled'] = (0, 0, 0) - ax.yaxis._axinfo['juggled'] = (1, 1, 1) - ax.zaxis._axinfo['juggled'] = (2, 2, 0) - - ax.grid(False) - - return ax -``` \ No newline at end of file diff --git a/code_book/status.md b/code_book/status.md deleted file mode 100644 index 56b9450..0000000 --- a/code_book/status.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Execution Statistics - -This table contains the latest execution statistics. - -```{nb-exec-table} -``` diff --git a/ipynb/appendix.ipynb b/ipynb/appendix.ipynb new file mode 100644 index 0000000..316c944 --- /dev/null +++ b/ipynb/appendix.ipynb @@ -0,0 +1,776 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "35da8276", + "metadata": {}, + "source": [ + "# Appendix Code\n", + "\n", + "We begin with some imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46328226", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np \n", + "import matplotlib.pyplot as plt \n", + "import quantecon_book_networks \n", + "from mpl_toolkits.mplot3d.axes3d import Axes3D, proj3d \n", + "from matplotlib import cm \n", + "export_figures = False\n", + "quantecon_book_networks.config(\"matplotlib\") " + ] + }, + { + "cell_type": "markdown", + "id": "483e031e", + "metadata": {}, + "source": [ + "## Functions\n", + "\n", + "### One-to-one and onto functions on $(0,1)$\n", + "\n", + "We start by defining the domain and our one-to-one and onto function examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b617e66", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 1, 100)\n", + "titles = 'one-to-one', 'onto'\n", + "labels = '$f(x)=1/2 + x/2$', '$f(x)=4x(1-x)$'\n", + "funcs = lambda x: 1/2 + x/2, lambda x: 4 * x * (1-x)" + ] + }, + { + "cell_type": "markdown", + "id": "40b1bb2d", + "metadata": {}, + "source": [ + "The figure can now be produced as follows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12ba8e7b", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n", + "for f, ax, lb, ti in zip(funcs, axes, labels, titles):\n", + " ax.set_xlim(0, 1)\n", + " ax.set_ylim(0, 1.01)\n", + " ax.plot(x, f(x), label=lb)\n", + " ax.set_title(ti, fontsize=12)\n", + " ax.legend(loc='lower center', fontsize=12, frameon=False)\n", + " ax.set_xticks((0, 1))\n", + " ax.set_yticks((0, 1))\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/func_types_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "fe7452ac", + "metadata": {}, + "source": [ + "### Some functions are bijections\n", + "\n", + "This figure can be produced in a similar manner to 6.1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5907691a", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 1, 100)\n", + "titles = 'constant', 'bijection'\n", + "labels = '$f(x)=1/2$', '$f(x)=1-x$'\n", + "funcs = lambda x: 1/2 + 0 * x, lambda x: 1-x\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n", + "for f, ax, lb, ti in zip(funcs, axes, labels, titles):\n", + " ax.set_xlim(0, 1)\n", + " ax.set_ylim(0, 1.01)\n", + " ax.plot(x, f(x), label=lb)\n", + " ax.set_title(ti, fontsize=12)\n", + " ax.legend(loc='lower center', fontsize=12, frameon=False)\n", + " ax.set_xticks((0, 1))\n", + " ax.set_yticks((0, 1))\n", + " \n", + "if export_figures:\n", + " plt.savefig(\"figures/func_types_2.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bbfde515", + "metadata": {}, + "source": [ + "## Fixed Points\n", + "\n", + "### Graph and fixed points of $G \\colon x \\mapsto 2.125/(1 + x^{-4})$\n", + "\n", + "We begin by defining the domain and the function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3110e5cd", + "metadata": {}, + "outputs": [], + "source": [ + "xmin, xmax = 0.0000001, 2\n", + "xgrid = np.linspace(xmin, xmax, 200)\n", + "g = lambda x: 2.125 / (1 + x**(-4))" + ] + }, + { + "cell_type": "markdown", + "id": "6532632d", + "metadata": {}, + "source": [ + "Next we define our fixed points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b9ef820", + "metadata": {}, + "outputs": [], + "source": [ + "fps_labels = ('$x_\\ell$', '$x_m$', '$x_h$' )\n", + "fps = (0.01, 0.94, 1.98)\n", + "coords = ((40, 80), (40, -40), (-40, -80))" + ] + }, + { + "cell_type": "markdown", + "id": "87f4c5c2", + "metadata": {}, + "source": [ + "Finally we can produce the figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8f4567d", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(6.5, 6))\n", + "\n", + "ax.set_xlim(xmin, xmax)\n", + "ax.set_ylim(xmin, xmax)\n", + "\n", + "ax.plot(xgrid, g(xgrid), 'b-', lw=2, alpha=0.6, label='$G$')\n", + "ax.plot(xgrid, xgrid, 'k-', lw=1, alpha=0.7, label='$45^o$')\n", + "\n", + "ax.legend(fontsize=14)\n", + "\n", + "ax.plot(fps, fps, 'ro', ms=8, alpha=0.6)\n", + "\n", + "for (fp, lb, coord) in zip(fps, fps_labels, coords):\n", + " ax.annotate(lb, \n", + " xy=(fp, fp),\n", + " xycoords='data',\n", + " xytext=coord,\n", + " textcoords='offset points',\n", + " fontsize=16,\n", + " arrowprops=dict(arrowstyle=\"->\"))\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/three_fixed_points.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b6b47191", + "metadata": {}, + "source": [ + "## Complex Numbers\n", + "\n", + "### The complex number $(a, b) = r e^{i \\phi}$\n", + "\n", + "We start by abbreviating some useful values and functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee845f66", + "metadata": {}, + "outputs": [], + "source": [ + "π = np.pi\n", + "zeros = np.zeros\n", + "ones = np.ones\n", + "fs = 18" + ] + }, + { + "cell_type": "markdown", + "id": "4a40f91e", + "metadata": {}, + "source": [ + "Next we set our parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa26afd6", + "metadata": {}, + "outputs": [], + "source": [ + "r = 2\n", + "φ = π/3\n", + "x = r * np.cos(φ)\n", + "x_range = np.linspace(0, x, 1000)\n", + "φ_range = np.linspace(0, φ, 1000)" + ] + }, + { + "cell_type": "markdown", + "id": "d55d19ec", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7036969e", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(7, 7))\n", + "ax = plt.subplot(111, projection='polar')\n", + "\n", + "ax.plot((0, φ), (0, r), marker='o', color='b', alpha=0.5) # Plot r\n", + "ax.plot(zeros(x_range.shape), x_range, color='b', alpha=0.5) # Plot x\n", + "ax.plot(φ_range, x / np.cos(φ_range), color='b', alpha=0.5) # Plot y\n", + "ax.plot(φ_range, ones(φ_range.shape) * 0.15, color='green') # Plot φ\n", + "\n", + "ax.margins(0) # Let the plot starts at origin\n", + "\n", + "ax.set_rmax(2)\n", + "ax.set_rticks((1, 2)) # Less radial ticks\n", + "ax.set_rlabel_position(-88.5) # Get radial labels away from plotted line\n", + "\n", + "ax.text(φ, r+0.04 , r'$(a, b) = (1, \\sqrt{3})$', fontsize=fs) # Label z\n", + "ax.text(φ+0.4, 1 , '$r = 2$', fontsize=fs) # Label r\n", + "ax.text(0-0.4, 0.5, '$1$', fontsize=fs) # Label x\n", + "ax.text(0.5, 1.2, '$\\sqrt{3}$', fontsize=fs) # Label y\n", + "ax.text(0.3, 0.25, '$\\\\varphi = \\\\pi/3$', fontsize=fs) # Label θ\n", + "\n", + "xT=plt.xticks()[0]\n", + "xL=['0',\n", + " r'$\\frac{\\pi}{4}$',\n", + " r'$\\frac{\\pi}{2}$',\n", + " r'$\\frac{3\\pi}{4}$',\n", + " r'$\\pi$',\n", + " r'$\\frac{5\\pi}{4}$',\n", + " r'$\\frac{3\\pi}{2}$',\n", + " r'$\\frac{7\\pi}{4}$']\n", + "\n", + "plt.xticks(xT, xL, fontsize=fs+2)\n", + "ax.grid(True)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/complex_number.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "418ac96b", + "metadata": {}, + "source": [ + "## Convergence\n", + "\n", + "### Convergence of a sequence to the origin in $\\mathbb{R}^3$\n", + "\n", + "We define our transformation matrix, initial point, and number of iterations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebbf6747", + "metadata": {}, + "outputs": [], + "source": [ + "θ = 0.1\n", + "A = ((np.cos(θ), - np.sin(θ), 0.0001),\n", + " (np.sin(θ), np.cos(θ), 0.001),\n", + " (np.sin(θ), np.cos(θ), 1))\n", + "\n", + "A = 0.98 * np.array(A)\n", + "p = np.array((1, 1, 1))\n", + "n = 200" + ] + }, + { + "cell_type": "markdown", + "id": "815fed7d", + "metadata": {}, + "source": [ + "Now we can produce the plot by repeatedly transforming our point with the transformation matrix and plotting each resulting point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14e5a9a7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(6, 6))\n", + "ax = fig.add_subplot(111, projection='3d')\n", + "ax.view_init(elev=20, azim=-40)\n", + "\n", + "ax.set_xlim((-1.5, 1.5))\n", + "ax.set_ylim((-1.5, 1.5))\n", + "ax.set_xticks((-1,0,1))\n", + "ax.set_yticks((-1,0,1))\n", + "\n", + "for i in range(n):\n", + " x, y, z = p\n", + " ax.plot([x], [y], [z], 'o', ms=4, color=cm.jet_r(i / n))\n", + " p = A @ p\n", + " \n", + "if export_figures:\n", + " plt.savefig(\"figures/euclidean_convergence_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8804299c", + "metadata": {}, + "source": [ + "## Linear Algebra\n", + "\n", + "### The span of vectors $u$, $v$, $w$ in $\\mathbb{R}$\n", + "\n", + "We begin by importing the `FancyArrowPatch` class and extending it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07b47dce", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib.patches import FancyArrowPatch\n", + "\n", + "class Arrow3D(FancyArrowPatch):\n", + " def __init__(self, xs, ys, zs, *args, **kwargs):\n", + " FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs)\n", + " self._verts3d = xs, ys, zs\n", + "\n", + " def do_3d_projection(self, renderer=None):\n", + " xs3d, ys3d, zs3d = self._verts3d\n", + " xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)\n", + " self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))\n", + "\n", + " return np.min(zs)\n", + "\n", + " def draw(self, renderer):\n", + " xs3d, ys3d, zs3d = self._verts3d\n", + " xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)\n", + " self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))\n", + " FancyArrowPatch.draw(self, renderer)" + ] + }, + { + "cell_type": "markdown", + "id": "2a087a52", + "metadata": {}, + "source": [ + "Next we generate our vectors $u$, $v$, $w$, ensuring linear dependence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ced11ab1", + "metadata": {}, + "outputs": [], + "source": [ + "α, β = 0.2, 0.1\n", + "def f(x, y):\n", + " return α * x + β * y\n", + "\n", + "# Vector locations, by coordinate\n", + "x_coords = np.array((3, 3, -3.5))\n", + "y_coords = np.array((4, -4, 3.0))\n", + "z_coords = f(x_coords, y_coords)\n", + "\n", + "vecs = [np.array((x, y, z)) for x, y, z in zip(x_coords, y_coords, z_coords)]" + ] + }, + { + "cell_type": "markdown", + "id": "165912ee", + "metadata": {}, + "source": [ + "Next we define the spanning plane." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97870984", + "metadata": {}, + "outputs": [], + "source": [ + "x_min, x_max = -5, 5\n", + "y_min, y_max = -5, 5\n", + "\n", + "grid_size = 20\n", + "xr2 = np.linspace(x_min, x_max, grid_size)\n", + "yr2 = np.linspace(y_min, y_max, grid_size)\n", + "x2, y2 = np.meshgrid(xr2, yr2)\n", + "z2 = f(x2, y2)" + ] + }, + { + "cell_type": "markdown", + "id": "be62269c", + "metadata": {}, + "source": [ + "Finally we generate the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49b4eff0", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(12, 7))\n", + "ax = plt.axes(projection ='3d')\n", + "ax.view_init(elev=10., azim=-80)\n", + "\n", + "ax.set(xlim=(x_min, x_max), \n", + " ylim=(x_min, x_max), \n", + " zlim=(x_min, x_max),\n", + " xticks=(0,), yticks=(0,), zticks=(0,))\n", + "\n", + "# Draw the axes\n", + "gs = 3\n", + "z = np.linspace(x_min, x_max, gs)\n", + "x = np.zeros(gs)\n", + "y = np.zeros(gs)\n", + "ax.plot(x, y, z, 'k-', lw=2, alpha=0.5)\n", + "ax.plot(z, x, y, 'k-', lw=2, alpha=0.5)\n", + "ax.plot(y, z, x, 'k-', lw=2, alpha=0.5)\n", + "\n", + "# Draw the vectors\n", + "for v in vecs:\n", + " a = Arrow3D([0, v[0]], \n", + " [0, v[1]], \n", + " [0, v[2]], \n", + " mutation_scale=20, \n", + " lw=1, \n", + " arrowstyle=\"-|>\", \n", + " color=\"b\")\n", + " ax.add_artist(a)\n", + "\n", + "\n", + "for v, label in zip(vecs, ('u', 'v', 'w')):\n", + " v = v * 1.1\n", + " ax.text(v[0], v[1], v[2], \n", + " f'${label}$', \n", + " fontsize=14)\n", + "\n", + "# Draw the plane\n", + "grid_size = 20\n", + "xr2 = np.linspace(x_min, x_max, grid_size)\n", + "yr2 = np.linspace(y_min, y_max, grid_size)\n", + "x2, y2 = np.meshgrid(xr2, yr2)\n", + "z2 = f(x2, y2)\n", + "\n", + "ax.plot_surface(x2, y2, z2, rstride=1, cstride=1, cmap=cm.jet,\n", + " linewidth=0, antialiased=True, alpha=0.2)\n", + "\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/span1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b87b7088", + "metadata": {}, + "source": [ + "## Linear Maps Are Matrices\n", + "\n", + "### Equivalence of the onto and one-to-one properties (for linear maps)\n", + "\n", + "This plot is produced similarly to Figures 6.1 and 6.2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a243b6e1", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n", + "\n", + "x = np.linspace(-2, 2, 10)\n", + "\n", + "titles = 'non-bijection', 'bijection'\n", + "labels = '$f(x)=0 x$', '$f(x)=0.5 x$'\n", + "funcs = lambda x: 0*x, lambda x: 0.5 * x\n", + "\n", + "for ax, f, lb, ti in zip(axes, funcs, labels, titles):\n", + "\n", + " # Set the axes through the origin\n", + " for spine in ['left', 'bottom']:\n", + " ax.spines[spine].set_position('zero')\n", + " for spine in ['right', 'top']:\n", + " ax.spines[spine].set_color('none')\n", + " ax.set_yticks((-1, 1))\n", + " ax.set_ylim((-1, 1))\n", + " ax.set_xlim((-1, 1))\n", + " ax.set_xticks((-1, 1))\n", + " y = f(x)\n", + " ax.plot(x, y, '-', linewidth=4, label=lb, alpha=0.6)\n", + " ax.text(-0.8, 0.5, ti, fontsize=14)\n", + " ax.legend(loc='lower right', fontsize=12)\n", + " \n", + "if export_figures:\n", + " plt.savefig(\"figures/func_types_3.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e3aa06c3", + "metadata": {}, + "source": [ + "## Convexity and Polyhedra\n", + "\n", + "### A polyhedron $P$ represented as intersecting halfspaces\n", + "\n", + "Inequalities are of the form\n", + "\n", + "$$ a x + b y \\leq c $$\n", + "\n", + "To plot the halfspace we plot the line\n", + "\n", + "$$ y = c/b - a/b x $$\n", + "\n", + "and then fill in the halfspace using `fill_between` on points $x, y, \\hat y$,\n", + "where $\\hat y$ is either `y_min` or `y_max`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae4e8b4d", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "plt.axis('off')\n", + "\n", + "x = np.linspace(-10, 14, 200)\n", + "y_min, y_max = -2, 3\n", + "\n", + "a1, b1, c1 = 1.0, 8.0, -5.0\n", + "y = c1 / b1 - (a1 / b1) * x\n", + "ax.plot(x, y, label='$a_1 x_1 + b_1 x_2 = c_1$')\n", + "ax.fill_between(x, y, y_max, alpha=0.2)\n", + "\n", + "\n", + "a2, b2, c2 = 0.5, 0.75, 1.5\n", + "y = c2 / b2 - (a2 / b2) * x\n", + "ax.plot(x, y, label='$a_2 x_1 + b_2 x_2 = c_2$')\n", + "ax.fill_between(x, y, y_min, alpha=0.2)\n", + "\n", + "\n", + "a3, b3, c3 = -1.0, 1.0, 1.5\n", + "y = c3 / b3 - (a3 / b3) * x\n", + "ax.plot(x, y, label='$a_3 x_1 + b_3 x_2 = c_3$')\n", + "ax.fill_between(x, y, y_min, alpha=0.2)\n", + "\n", + "ax.plot((0.23,), (1.82,), 'ko')\n", + "ax.plot((-1.95,), (-0.4,), 'ko')\n", + "ax.plot((4.8,), (-1.2,), 'ko')\n", + "\n", + "\n", + "ax.annotate('$P$', xy=(0, 0), fontsize=12)\n", + "\n", + "ax.set_ylim(y_min, y_max)\n", + "if export_figures:\n", + " plt.savefig(\"figures/polyhedron1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "45b13eed", + "metadata": {}, + "source": [ + "## Saddle Points and Duality" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53bdce43", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(8.5, 6))\n", + "\n", + "## Top left Plot\n", + "\n", + "ax = fig.add_subplot(221, projection='3d')\n", + "\n", + "plot_args = {'rstride': 1, 'cstride': 1, 'cmap':\"viridis\",\n", + " 'linewidth': 0.4, 'antialiased': True, \"alpha\":0.75,\n", + " 'vmin': -1, 'vmax': 1}\n", + "\n", + "x, y = np.mgrid[-1:1:31j, -1:1:31j]\n", + "z = x**2 - y**2\n", + "\n", + "ax.plot_surface(x, y, z, **plot_args)\n", + "\n", + "ax.view_init(azim=245, elev=20)\n", + "\n", + "ax.set_xlim(-1, 1)\n", + "ax.set_ylim(-1, 1)\n", + "ax.set_zlim(-1, 1)\n", + "\n", + "ax.set_xticks([0])\n", + "ax.set_xticklabels([r\"$x^*$\"], fontsize=16)\n", + "ax.set_yticks([0])\n", + "ax.set_yticklabels([r\"$\\theta^*$\"], fontsize=16)\n", + "\n", + "\n", + "ax.set_zticks([])\n", + "ax.set_zticklabels([])\n", + "\n", + "ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) \n", + "ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) \n", + "ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))\n", + "\n", + "ax.set_zlabel(\"$L(x,\\\\theta)$\", fontsize=14)\n", + "\n", + "## Top Right Plot\n", + "\n", + "ax = fig.add_subplot(222)\n", + "\n", + "\n", + "plot_args = {'cmap':\"viridis\", 'antialiased': True, \"alpha\":0.6,\n", + " 'vmin': -1, 'vmax': 1}\n", + "\n", + "x, y = np.mgrid[-1:1:31j, -1:1:31j]\n", + "z = x**2 - y**2\n", + "\n", + "ax.contourf(x, y, z, **plot_args)\n", + "\n", + "ax.plot([0], [0], 'ko')\n", + "\n", + "ax.set_xlim(-1, 1)\n", + "ax.set_ylim(-1, 1)\n", + "\n", + "plt.xticks([ 0],\n", + " [r\"$x^*$\"], fontsize=16)\n", + "\n", + "plt.yticks([0],\n", + " [r\"$\\theta^*$\"], fontsize=16)\n", + "\n", + "ax.hlines(0, -1, 1, color='k', ls='-', lw=1)\n", + "ax.vlines(0, -1, 1, color='k', ls='-', lw=1)\n", + "\n", + "coords=(-35, 30)\n", + "ax.annotate(r'$L(x, \\theta^*)$', \n", + " xy=(-0.5, 0), \n", + " xycoords=\"data\",\n", + " xytext=coords,\n", + " textcoords=\"offset points\",\n", + " fontsize=12,\n", + " arrowprops={\"arrowstyle\" : \"->\"})\n", + "\n", + "coords=(35, 30)\n", + "ax.annotate(r'$L(x^*, \\theta)$', \n", + " xy=(0, 0.25), \n", + " xycoords=\"data\",\n", + " xytext=coords,\n", + " textcoords=\"offset points\",\n", + " fontsize=12,\n", + " arrowprops={\"arrowstyle\" : \"->\"})\n", + "\n", + "## Bottom Left Plot\n", + "\n", + "ax = fig.add_subplot(223)\n", + "\n", + "x = np.linspace(-1, 1, 100)\n", + "ax.plot(x, -x**2, label='$\\\\theta \\mapsto L(x^*, \\\\theta)$')\n", + "ax.set_ylim((-1, 1))\n", + "ax.legend(fontsize=14)\n", + "ax.set_xticks([])\n", + "ax.set_yticks([])\n", + "\n", + "## Bottom Right Plot\n", + "\n", + "ax = fig.add_subplot(224)\n", + "\n", + "x = np.linspace(-1, 1, 100)\n", + "ax.plot(x, x**2, label='$x \\mapsto L(x, \\\\theta^*)$')\n", + "ax.set_ylim((-1, 1))\n", + "ax.legend(fontsize=14, loc='lower right')\n", + "ax.set_xticks([])\n", + "ax.set_yticks([])\n", + "\n", + "if export_figures:\n", + "\tplt.savefig(\"figures/saddle_1.pdf\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_fpms.ipynb b/ipynb/ch_fpms.ipynb new file mode 100644 index 0000000..8327586 --- /dev/null +++ b/ipynb/ch_fpms.ipynb @@ -0,0 +1,273 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ab032fde", + "metadata": {}, + "source": [ + "# Chapter 5 - Nonlinear Interactions (Python Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0b02209", + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [], + "source": [ + "! pip install --upgrade quantecon_book_networks" + ] + }, + { + "cell_type": "markdown", + "id": "7d82d322", + "metadata": {}, + "source": [ + "We begin with some imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be38b591", + "metadata": {}, + "outputs": [], + "source": [ + "import quantecon as qe\n", + "import quantecon_book_networks\n", + "import quantecon_book_networks.input_output as qbn_io\n", + "import quantecon_book_networks.plotting as qbn_plt\n", + "import quantecon_book_networks.data as qbn_data\n", + "export_figures = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e481b4fb", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "quantecon_book_networks.config(\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "7f027495", + "metadata": {}, + "source": [ + "## Financial Networks\n", + "\n", + "### Equity-Cross Holdings\n", + "\n", + "Here we define a class for modelling a financial network where firms are linked by share cross-holdings,\n", + "and there are failure costs as described by [Elliott et al. (2014)](https://www.aeaweb.org/articles?id=10.1257/aer.104.10.3115)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58e02f09", + "metadata": {}, + "outputs": [], + "source": [ + "class FinNet:\n", + " \n", + " def __init__(self, n=100, c=0.72, d=1, θ=0.5, β=1.0, seed=1234):\n", + " \n", + " self.n, self.c, self.d, self.θ, self.β = n, c, d, θ, β\n", + " np.random.seed(seed)\n", + " \n", + " self.e = np.ones(n)\n", + " self.C, self.C_hat = self.generate_primitives()\n", + " self.A = self.C_hat @ np.linalg.inv(np.identity(n) - self.C)\n", + " self.v_bar = self.A @ self.e\n", + " self.t = np.full(n, θ)\n", + " \n", + " def generate_primitives(self):\n", + " \n", + " n, c, d = self.n, self.c, self.d\n", + " B = np.zeros((n, n))\n", + " C = np.zeros_like(B)\n", + "\n", + " for i in range(n):\n", + " for j in range(n):\n", + " if i != j and np.random.rand() < d/(n-1):\n", + " B[i,j] = 1\n", + " \n", + " for i in range(n):\n", + " for j in range(n):\n", + " k = np.sum(B[:,j])\n", + " if k > 0:\n", + " C[i,j] = c * B[i,j] / k\n", + " \n", + " C_hat = np.identity(n) * (1 - c)\n", + " \n", + " return C, C_hat\n", + " \n", + " def T(self, v):\n", + " Tv = self.A @ (self.e - self.β * np.where(v < self.t, 1, 0))\n", + " return Tv\n", + " \n", + " def compute_equilibrium(self):\n", + " i = 0\n", + " v = self.v_bar\n", + " error = 1\n", + " while error > 1e-10:\n", + " print(f\"number of failing firms is \", np.sum(v < self.θ))\n", + " new_v = self.T(v)\n", + " error = np.max(np.abs(new_v - v))\n", + " v = new_v\n", + " i = i+1\n", + " \n", + " print(f\"Terminated after {i} iterations\")\n", + " return v\n", + " \n", + " def map_values_to_colors(self, v, j):\n", + " cols = cm.plasma(qbn_io.to_zero_one(v))\n", + " if j != 0:\n", + " for i in range(len(v)):\n", + " if v[i] < self.t[i]:\n", + " cols[i] = 0.0\n", + " return cols" + ] + }, + { + "cell_type": "markdown", + "id": "dbf9b681", + "metadata": {}, + "source": [ + "Now we create a financial network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "663e7992", + "metadata": {}, + "outputs": [], + "source": [ + "fn = FinNet(n=100, c=0.72, d=1, θ=0.3, β=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "c89db6a0", + "metadata": {}, + "source": [ + "And compute its equilibrium." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81efad24", + "metadata": {}, + "outputs": [], + "source": [ + "fn.compute_equilibrium()" + ] + }, + { + "cell_type": "markdown", + "id": "25a95eb9", + "metadata": {}, + "source": [ + "### Waves of bankruptcies in a financial network\n", + "\n", + "Now we visualise the network after different numbers of iterations. \n", + "\n", + "For convenience we will first define a function to plot the graphs of the financial network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff6c8d54", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_fin_graph(G, ax, node_color_list):\n", + " \n", + " n = G.number_of_nodes()\n", + "\n", + " node_pos_dict = nx.spring_layout(G, k=1.1)\n", + " edge_colors = []\n", + "\n", + " for i in range(n):\n", + " for j in range(n):\n", + " edge_colors.append(node_color_list[i])\n", + "\n", + " \n", + " nx.draw_networkx_nodes(G, \n", + " node_pos_dict, \n", + " node_color=node_color_list, \n", + " edgecolors='grey', \n", + " node_size=100,\n", + " linewidths=2, \n", + " alpha=0.8, \n", + " ax=ax)\n", + "\n", + " nx.draw_networkx_edges(G, \n", + " node_pos_dict, \n", + " edge_color=edge_colors, \n", + " alpha=0.4, \n", + " ax=ax)" + ] + }, + { + "cell_type": "markdown", + "id": "bd11cd19", + "metadata": {}, + "source": [ + "Now we will iterate by applying the operator $T$ to the vector of firm values $v$ and produce the plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4e15ff6", + "metadata": {}, + "outputs": [], + "source": [ + "G = nx.from_numpy_array(np.array(fn.C), create_using=nx.DiGraph)\n", + "v = fn.v_bar\n", + "\n", + "k = 15\n", + "d = 3\n", + "fig, axes = plt.subplots(int(k/d), 1, figsize=(10, 12))\n", + "\n", + "for i in range(k):\n", + " if i % d == 0:\n", + " ax = axes[int(i/d)]\n", + " ax.set_title(f\"iteration {i}\")\n", + "\n", + " plot_fin_graph(G, ax, fn.map_values_to_colors(v, i))\n", + " v = fn.T(v)\n", + "if export_figures:\n", + " plt.savefig(\"figures/fin_network_sims_1.pdf\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_intro.ipynb b/ipynb/ch_intro.ipynb new file mode 100644 index 0000000..a796306 --- /dev/null +++ b/ipynb/ch_intro.ipynb @@ -0,0 +1,1399 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5175177c", + "metadata": {}, + "source": [ + "# Chapter 1 - Introduction (Python Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e0179da", + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [], + "source": [ + "! pip install --upgrade quantecon_book_networks kaleido" + ] + }, + { + "cell_type": "markdown", + "id": "36c17bdf", + "metadata": {}, + "source": [ + "We begin by importing the `quantecon` package as well as some functions and data that have been packaged for release with this text." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf584c62", + "metadata": {}, + "outputs": [], + "source": [ + "import quantecon as qe\n", + "import quantecon_book_networks\n", + "import quantecon_book_networks.input_output as qbn_io\n", + "import quantecon_book_networks.data as qbn_data\n", + "import quantecon_book_networks.plotting as qbn_plot\n", + "ch1_data = qbn_data.introduction()\n", + "default_figsize = (6, 4)\n", + "export_figures = False" + ] + }, + { + "cell_type": "markdown", + "id": "858f6c0d", + "metadata": {}, + "source": [ + "Next we import some common python libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "622fff13", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import networkx as nx\n", + "import statsmodels.api as sm\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.cm as cm\n", + "import matplotlib.ticker as ticker\n", + "from mpl_toolkits.mplot3d.art3d import Poly3DCollection\n", + "import matplotlib.patches as mpatches\n", + "import plotly.graph_objects as go\n", + "quantecon_book_networks.config(\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ee79c32", + "metadata": {}, + "source": [ + "## Motivation\n", + "\n", + "### International trade in crude oil 2021\n", + "\n", + "We begin by loading a `NetworkX` directed graph object that represents international trade in crude oil." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e495966", + "metadata": {}, + "outputs": [], + "source": [ + "DG = ch1_data[\"crude_oil\"]" + ] + }, + { + "cell_type": "markdown", + "id": "284d4d87", + "metadata": {}, + "source": [ + "Next we transform the data to prepare it for display as a Sankey diagram." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c944e03", + "metadata": {}, + "outputs": [], + "source": [ + "nodeid = {}\n", + "for ix,nd in enumerate(DG.nodes()):\n", + " nodeid[nd] = ix\n", + "\n", + "# Links\n", + "source = []\n", + "target = []\n", + "value = []\n", + "for src,tgt in DG.edges():\n", + " source.append(nodeid[src])\n", + " target.append(nodeid[tgt])\n", + " value.append(DG[src][tgt]['weight'])" + ] + }, + { + "cell_type": "markdown", + "id": "2cd4537f", + "metadata": {}, + "source": [ + "Finally we produce our plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6a9a856", + "metadata": {}, + "outputs": [], + "source": [ + "fig = go.Figure(data=[go.Sankey(\n", + " node = dict(\n", + " pad = 15,\n", + " thickness = 20,\n", + " line = dict(color = \"black\", width = 0.5),\n", + " label = list(nodeid.keys()),\n", + " color = \"blue\"\n", + " ),\n", + " link = dict(\n", + " source = source,\n", + " target = target,\n", + " value = value\n", + " ))])\n", + "\n", + "\n", + "fig.update_layout(title_text=\"Crude Oil\", font_size=10, width=600, height=800)\n", + "if export_figures:\n", + " fig.write_image(\"figures/crude_oil_2021.pdf\")\n", + "fig.show(renderer='svg')" + ] + }, + { + "cell_type": "markdown", + "id": "663c1374", + "metadata": {}, + "source": [ + "### International trade in commercial aircraft during 2019\n", + "\n", + "For this plot we will use a cleaned dataset from \n", + "[Harvard, CID Dataverse](https://dataverse.harvard.edu/dataverse/atlas)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31cf9ca7", + "metadata": {}, + "outputs": [], + "source": [ + "DG = ch1_data['aircraft_network']\n", + "pos = ch1_data['aircraft_network_pos']" + ] + }, + { + "cell_type": "markdown", + "id": "990418cb", + "metadata": {}, + "source": [ + "We begin by calculating some features of our graph using the `NetworkX` and\n", + "the `quantecon_book_networks` packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4da4ce46", + "metadata": {}, + "outputs": [], + "source": [ + "centrality = nx.eigenvector_centrality(DG)\n", + "node_total_exports = qbn_io.node_total_exports(DG)\n", + "edge_weights = qbn_io.edge_weights(DG)" + ] + }, + { + "cell_type": "markdown", + "id": "ead51019", + "metadata": {}, + "source": [ + "Now we convert our graph features to plot features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9be3605", + "metadata": {}, + "outputs": [], + "source": [ + "node_pos_dict = pos\n", + "\n", + "node_sizes = qbn_io.normalise_weights(node_total_exports,10000)\n", + "edge_widths = qbn_io.normalise_weights(edge_weights,10)\n", + "\n", + "node_colors = qbn_io.colorise_weights(list(centrality.values()),color_palette=cm.viridis)\n", + "node_to_color = dict(zip(DG.nodes,node_colors))\n", + "edge_colors = []\n", + "for src,_ in DG.edges:\n", + " edge_colors.append(node_to_color[src])" + ] + }, + { + "cell_type": "markdown", + "id": "b44e9249", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5dfccd9c", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 10))\n", + "ax.axis('off')\n", + "\n", + "nx.draw_networkx_nodes(DG, \n", + " node_pos_dict, \n", + " node_color=node_colors, \n", + " node_size=node_sizes, \n", + " linewidths=2, \n", + " alpha=0.6, \n", + " ax=ax)\n", + "\n", + "nx.draw_networkx_labels(DG, \n", + " node_pos_dict, \n", + " ax=ax)\n", + "\n", + "nx.draw_networkx_edges(DG, \n", + " node_pos_dict, \n", + " edge_color=edge_colors, \n", + " width=edge_widths, \n", + " arrows=True, \n", + " arrowsize=20, \n", + " ax=ax,\n", + " node_size=node_sizes, \n", + " connectionstyle='arc3,rad=0.15')\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/commercial_aircraft_2019_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "70e419f0", + "metadata": {}, + "source": [ + "## Spectral Theory\n", + "\n", + "### Spectral Radii\n", + "\n", + "Here we provide code for computing the spectral radius of a matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0ac0f17", + "metadata": {}, + "outputs": [], + "source": [ + "def spec_rad(M):\n", + " \"\"\"\n", + " Compute the spectral radius of M.\n", + " \"\"\"\n", + " return np.max(np.abs(np.linalg.eigvals(M)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bdca499", + "metadata": {}, + "outputs": [], + "source": [ + "M = np.array([[1,2],[2,1]])\n", + "spec_rad(M)" + ] + }, + { + "cell_type": "markdown", + "id": "f4fa90a5", + "metadata": {}, + "source": [ + "This function is available in the `quantecon_book_networks` package, along with \n", + "several other functions for used repeatedly in the text. Source code for\n", + "these functions can be seen [here](pkg_funcs)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a1e4cba", + "metadata": {}, + "outputs": [], + "source": [ + "qbn_io.spec_rad(M)" + ] + }, + { + "cell_type": "markdown", + "id": "9ac85796", + "metadata": {}, + "source": [ + "## Probability\n", + "\n", + "### The unit simplex in $\\mathbb{R}^3$\n", + "\n", + "Here we define a function for plotting the unit simplex." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a661d596", + "metadata": {}, + "outputs": [], + "source": [ + "def unit_simplex(angle):\n", + " \n", + " fig = plt.figure(figsize=(10, 8))\n", + " ax = fig.add_subplot(111, projection='3d')\n", + "\n", + " vtx = [[0, 0, 1],\n", + " [0, 1, 0], \n", + " [1, 0, 0]]\n", + " \n", + " tri = Poly3DCollection([vtx], color='darkblue', alpha=0.3)\n", + " tri.set_facecolor([0.5, 0.5, 1])\n", + " ax.add_collection3d(tri)\n", + "\n", + " ax.set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), \n", + " xticks=(1,), yticks=(1,), zticks=(1,))\n", + "\n", + " ax.set_xticklabels(['$(1, 0, 0)$'], fontsize=16)\n", + " ax.set_yticklabels([f'$(0, 1, 0)$'], fontsize=16)\n", + " ax.set_zticklabels([f'$(0, 0, 1)$'], fontsize=16)\n", + "\n", + " ax.xaxis.majorTicks[0].set_pad(15)\n", + " ax.yaxis.majorTicks[0].set_pad(15)\n", + " ax.zaxis.majorTicks[0].set_pad(35)\n", + "\n", + " ax.view_init(30, angle)\n", + "\n", + " # Move axis to origin\n", + " ax.xaxis._axinfo['juggled'] = (0, 0, 0)\n", + " ax.yaxis._axinfo['juggled'] = (1, 1, 1)\n", + " ax.zaxis._axinfo['juggled'] = (2, 2, 0)\n", + " \n", + " ax.grid(False) \n", + " \n", + " return ax" + ] + }, + { + "cell_type": "markdown", + "id": "33c4089d", + "metadata": {}, + "source": [ + "We can now produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "337d3792", + "metadata": {}, + "outputs": [], + "source": [ + "unit_simplex(50)\n", + "if export_figures:\n", + " plt.savefig(\"figures/simplex_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8086d97f", + "metadata": {}, + "source": [ + "### Independent draws from Student’s t and Normal distributions\n", + "\n", + "Here we illustrate the occurrence of \"extreme\" events in heavy tailed distributions. \n", + "We start by generating 1,000 samples from a normal distribution and a Student's t distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7be0e011", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.stats import t\n", + "n = 1000\n", + "np.random.seed(123)\n", + "\n", + "s = 2\n", + "n_data = np.random.randn(n) * s\n", + "\n", + "t_dist = t(df=1.5)\n", + "t_data = t_dist.rvs(n)" + ] + }, + { + "cell_type": "markdown", + "id": "37b4b6cb", + "metadata": {}, + "source": [ + "When we plot our samples, we see the Student's t distribution frequently\n", + "generates samples many standard deviations from the mean." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90cd4891", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(8, 3.4))\n", + "\n", + "for ax in axes:\n", + " ax.set_ylim((-50, 50))\n", + " ax.plot((0, n), (0, 0), 'k-', lw=0.3)\n", + "\n", + "ax = axes[0]\n", + "ax.plot(list(range(n)), t_data, linestyle='', marker='o', alpha=0.5, ms=4)\n", + "ax.vlines(list(range(n)), 0, t_data, 'k', lw=0.2)\n", + "ax.get_xaxis().set_major_formatter(\n", + " ticker.FuncFormatter(lambda x, p: format(int(x), ',')))\n", + "ax.set_title(f\"Student t draws\", fontsize=11)\n", + "\n", + "ax = axes[1]\n", + "ax.plot(list(range(n)), n_data, linestyle='', marker='o', alpha=0.5, ms=4)\n", + "ax.vlines(list(range(n)), 0, n_data, lw=0.2)\n", + "ax.get_xaxis().set_major_formatter(\n", + " ticker.FuncFormatter(lambda x, p: format(int(x), ',')))\n", + "ax.set_title(f\"$N(0, \\sigma^2)$ with $\\sigma = {s}$\", fontsize=11)\n", + "\n", + "plt.tight_layout()\n", + "if export_figures:\n", + " plt.savefig(\"figures/heavy_tailed_draws.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ca8d8028", + "metadata": {}, + "source": [ + "### CCDF plots for the Pareto and Exponential distributions\n", + "\n", + "When the Pareto tail property holds, the CCDF is eventually log linear. Here\n", + "we illustrates this using a Pareto distribution. For comparison, an exponential\n", + "distribution is also shown. First we define our domain and the Pareto and\n", + "Exponential distributions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a79084d", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(1, 10, 500)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89d0baa5", + "metadata": {}, + "outputs": [], + "source": [ + "α = 1.5\n", + "def Gp(x):\n", + " return x**(-α)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb412bf5", + "metadata": {}, + "outputs": [], + "source": [ + "λ = 1.0\n", + "def Ge(x):\n", + " return np.exp(-λ * x)" + ] + }, + { + "cell_type": "markdown", + "id": "90785fac", + "metadata": {}, + "source": [ + "We then plot our distribution on a log-log scale." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6b2f844", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "\n", + "ax.plot(np.log(x), np.log(Gp(x)), label=\"Pareto\")\n", + "ax.plot(np.log(x), np.log(Ge(x)), label=\"Exponential\")\n", + "\n", + "ax.legend(fontsize=12, frameon=False, loc=\"lower left\")\n", + "ax.set_xlabel(\"$\\ln x$\", fontsize=12)\n", + "ax.set_ylabel(\"$\\ln G(x)$\", fontsize=12)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/ccdf_comparison_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "63906e68", + "metadata": {}, + "source": [ + "### Empirical CCDF plots for largest firms (Forbes)\n", + "\n", + "Here we show that the distribution of firm sizes has a Pareto tail. We start\n", + "by loading the `forbes_global_2000` dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b9e33a0", + "metadata": {}, + "outputs": [], + "source": [ + "dfff = ch1_data['forbes_global_2000']" + ] + }, + { + "cell_type": "markdown", + "id": "9266e970", + "metadata": {}, + "source": [ + "We calculate values of the empirical CCDF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a342d5a", + "metadata": {}, + "outputs": [], + "source": [ + "data = np.asarray(dfff['Market Value'])[0:500]\n", + "y_vals = np.empty_like(data, dtype='float64')\n", + "n = len(data)\n", + "for i, d in enumerate(data):\n", + " # record fraction of sample above d\n", + " y_vals[i] = np.sum(data >= d) / n" + ] + }, + { + "cell_type": "markdown", + "id": "690fa754", + "metadata": {}, + "source": [ + "Now we fit a linear trend line (on the log-log scale)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "956c019d", + "metadata": {}, + "outputs": [], + "source": [ + "x, y = np.log(data), np.log(y_vals)\n", + "results = sm.OLS(y, sm.add_constant(x)).fit()\n", + "b, a = results.params" + ] + }, + { + "cell_type": "markdown", + "id": "c90d836b", + "metadata": {}, + "source": [ + "Finally we produce our plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a13d4b05", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(7.3, 4))\n", + "\n", + "ax.scatter(x, y, alpha=0.3, label=\"firm size (market value)\")\n", + "ax.plot(x, x * a + b, 'k-', alpha=0.6, label=f\"slope = ${a: 1.2f}$\")\n", + "\n", + "ax.set_xlabel('log value', fontsize=12)\n", + "ax.set_ylabel(\"log prob.\", fontsize=12)\n", + "ax.legend(loc='lower left', fontsize=12)\n", + " \n", + "if export_figures:\n", + " plt.savefig(\"figures/empirical_powerlaw_plots_firms_forbes.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d80c5a0a", + "metadata": {}, + "source": [ + "## Graph Theory\n", + "\n", + "### Zeta and Pareto distributions\n", + "\n", + "We begin by defining the Zeta and Pareto distributions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19032324", + "metadata": {}, + "outputs": [], + "source": [ + "γ = 2.0\n", + "α = γ - 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abccb3b0", + "metadata": {}, + "outputs": [], + "source": [ + "def z(k, c=2.0):\n", + " return c * k**(-γ)\n", + "\n", + "k_grid = np.arange(1, 10+1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb68c8bc", + "metadata": {}, + "outputs": [], + "source": [ + "def p(x, c=2.0):\n", + " return c * x**(-γ)\n", + "\n", + "x_grid = np.linspace(1, 10, 200)" + ] + }, + { + "cell_type": "markdown", + "id": "f605c4dc", + "metadata": {}, + "source": [ + "Then we can produce our plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3661175", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "ax.plot(k_grid, z(k_grid), '-o', label='zeta distribution with $\\gamma=2$')\n", + "ax.plot(x_grid, p(x_grid), label='density of Pareto with tail index $\\\\alpha$')\n", + "ax.legend(fontsize=12)\n", + "ax.set_yticks((0, 1, 2))\n", + "if export_figures:\n", + " plt.savefig(\"figures/zeta_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "35f01cce", + "metadata": {}, + "source": [ + "### NetworkX digraph plot\n", + "\n", + "We start by creating a graph object and populating it with edges." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8fae62", + "metadata": {}, + "outputs": [], + "source": [ + "G_p = nx.DiGraph()\n", + "\n", + "edge_list = [\n", + " ('p', 'p'),\n", + " ('m', 'p'), ('m', 'm'), ('m', 'r'),\n", + " ('r', 'p'), ('r', 'm'), ('r', 'r')\n", + "]\n", + "\n", + "for e in edge_list:\n", + " u, v = e\n", + " G_p.add_edge(u, v)" + ] + }, + { + "cell_type": "markdown", + "id": "77e32349", + "metadata": {}, + "source": [ + "Now we can plot our graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "892bc151", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "nx.spring_layout(G_p, seed=4)\n", + "nx.draw_spring(G_p, ax=ax, node_size=500, with_labels=True, \n", + " font_weight='bold', arrows=True, alpha=0.8,\n", + " connectionstyle='arc3,rad=0.25', arrowsize=20)\n", + "if export_figures:\n", + " plt.savefig(\"figures/networkx_basics_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f089c248", + "metadata": {}, + "source": [ + "The `DiGraph` object has methods that calculate in-degree and out-degree of vertices." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3e1adc6", + "metadata": {}, + "outputs": [], + "source": [ + "G_p.in_degree('p')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a502c57a", + "metadata": {}, + "outputs": [], + "source": [ + "G_p.out_degree('p')" + ] + }, + { + "cell_type": "markdown", + "id": "84636840", + "metadata": {}, + "source": [ + "Additionally, the `NetworkX` package supplies functions for testing\n", + "communication and strong connectedness, as well as to compute strongly\n", + "connected components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b41f9be", + "metadata": {}, + "outputs": [], + "source": [ + "G = nx.DiGraph()\n", + "G.add_edge(1, 1)\n", + "G.add_edge(2, 1)\n", + "G.add_edge(2, 3)\n", + "G.add_edge(3, 2)\n", + "list(nx.strongly_connected_components(G))" + ] + }, + { + "cell_type": "markdown", + "id": "79d4b29c", + "metadata": {}, + "source": [ + "Like `NetworkX`, the Python library `quantecon` \n", + "provides access to some graph-theoretic algorithms. \n", + "\n", + "In the case of QuantEcon's `DiGraph` object, an instance is created via the adjacency matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbd43d1a", + "metadata": {}, + "outputs": [], + "source": [ + "A = ((1, 0, 0),\n", + " (1, 1, 1),\n", + " (1, 1, 1))\n", + "A = np.array(A) # Convert to NumPy array\n", + "G = qe.DiGraph(A)\n", + "\n", + "G.strongly_connected_components" + ] + }, + { + "cell_type": "markdown", + "id": "0c670e09", + "metadata": {}, + "source": [ + "### International private credit flows by country\n", + "\n", + "We begin by loading an adjacency matrix of international private credit flows\n", + "(in the form of a NumPy array and a list of country labels)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd6f046d", + "metadata": {}, + "outputs": [], + "source": [ + "Z = ch1_data[\"adjacency_matrix\"][\"Z\"]\n", + "Z_visual= ch1_data[\"adjacency_matrix\"][\"Z_visual\"]\n", + "countries = ch1_data[\"adjacency_matrix\"][\"countries\"]" + ] + }, + { + "cell_type": "markdown", + "id": "ade862b8", + "metadata": {}, + "source": [ + "To calculate our graph's properties, we use hub-based eigenvector\n", + "centrality as our centrality measure for this plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a89769e", + "metadata": {}, + "outputs": [], + "source": [ + "centrality = qbn_io.eigenvector_centrality(Z_visual, authority=False)" + ] + }, + { + "cell_type": "markdown", + "id": "7be043ae", + "metadata": {}, + "source": [ + "Now we convert our graph features to plot features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce2696fd", + "metadata": {}, + "outputs": [], + "source": [ + "node_colors = cm.plasma(qbn_io.to_zero_one_beta(centrality))" + ] + }, + { + "cell_type": "markdown", + "id": "8ba53d05", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f802d18", + "metadata": {}, + "outputs": [], + "source": [ + "X = qbn_io.to_zero_one_beta(Z.sum(axis=1))\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 10))\n", + "plt.axis(\"off\")\n", + "\n", + "qbn_plot.plot_graph(Z_visual, X, ax, countries,\n", + " layout_type='spring',\n", + " layout_seed=1234,\n", + " node_size_multiple=3000,\n", + " edge_size_multiple=0.000006,\n", + " tol=0.0,\n", + " node_color_list=node_colors) \n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/financial_network_analysis_visualization.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "88e9c96e", + "metadata": {}, + "source": [ + "### Centrality measures for the credit network\n", + "\n", + "This figure looks at six different centrality measures.\n", + "\n", + "We begin by defining a function for calculating eigenvector centrality.\n", + "\n", + "Hub-based centrality is calculated by default, although authority-based centrality\n", + "can be calculated by setting `authority=True`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "577372e6", + "metadata": {}, + "outputs": [], + "source": [ + "def eigenvector_centrality(A, k=40, authority=False):\n", + " \"\"\"\n", + " Computes the dominant eigenvector of A. Assumes A is \n", + " primitive and uses the power method. \n", + " \n", + " \"\"\"\n", + " A_temp = A.T if authority else A\n", + " n = len(A_temp)\n", + " r = spec_rad(A_temp)\n", + " e = r**(-k) * (np.linalg.matrix_power(A_temp, k) @ np.ones(n))\n", + " return e / np.sum(e)" + ] + }, + { + "cell_type": "markdown", + "id": "c16682dc", + "metadata": {}, + "source": [ + "Here a similar function is defined for calculating Katz centrality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d7fbee4", + "metadata": {}, + "outputs": [], + "source": [ + "def katz_centrality(A, b=1, authority=False):\n", + " \"\"\"\n", + " Computes the Katz centrality of A, defined as the x solving\n", + "\n", + " x = 1 + b A x (1 = vector of ones)\n", + "\n", + " Assumes that A is square.\n", + "\n", + " If authority=True, then A is replaced by its transpose.\n", + " \"\"\"\n", + " n = len(A)\n", + " I = np.identity(n)\n", + " C = I - b * A.T if authority else I - b * A\n", + " return np.linalg.solve(C, np.ones(n))" + ] + }, + { + "cell_type": "markdown", + "id": "c0812961", + "metadata": {}, + "source": [ + "Now we generate an unweighted version of our matrix to help calculate in-degree and out-degree." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d665a8f", + "metadata": {}, + "outputs": [], + "source": [ + "D = qbn_io.build_unweighted_matrix(Z)" + ] + }, + { + "cell_type": "markdown", + "id": "947d8b01", + "metadata": {}, + "source": [ + "We now use the above to calculate the six centrality measures." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "413f7029", + "metadata": {}, + "outputs": [], + "source": [ + "outdegree = D.sum(axis=1)\n", + "ecentral_hub = eigenvector_centrality(Z, authority=False)\n", + "kcentral_hub = katz_centrality(Z, b=1/1_700_000)\n", + "\n", + "indegree = D.sum(axis=0)\n", + "ecentral_authority = eigenvector_centrality(Z, authority=True)\n", + "kcentral_authority = katz_centrality(Z, b=1/1_700_000, authority=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a14bf02e", + "metadata": {}, + "source": [ + "Here we provide a helper function that returns a DataFrame for each measure.\n", + "The DataFrame is ordered by that measure and contains color information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bcb86db", + "metadata": {}, + "outputs": [], + "source": [ + "def centrality_plot_data(countries, centrality_measures):\n", + " df = pd.DataFrame({'code': countries,\n", + " 'centrality':centrality_measures, \n", + " 'color': qbn_io.colorise_weights(centrality_measures).tolist()\n", + " })\n", + " return df.sort_values('centrality')" + ] + }, + { + "cell_type": "markdown", + "id": "c5777a24", + "metadata": {}, + "source": [ + "Finally, we plot the various centrality measures." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47130789", + "metadata": {}, + "outputs": [], + "source": [ + "centrality_measures = [outdegree, indegree, \n", + " ecentral_hub, ecentral_authority, \n", + " kcentral_hub, kcentral_authority]\n", + "\n", + "ylabels = ['out degree', 'in degree',\n", + " 'eigenvector hub','eigenvector authority', \n", + " 'Katz hub', 'Katz authority']\n", + "\n", + "ylims = [(0, 20), (0, 20), \n", + " None, None, \n", + " None, None]\n", + "\n", + "\n", + "fig, axes = plt.subplots(3, 2, figsize=(10, 12))\n", + "\n", + "axes = axes.flatten()\n", + "\n", + "for i, ax in enumerate(axes):\n", + " df = centrality_plot_data(countries, centrality_measures[i])\n", + " \n", + " ax.bar('code', 'centrality', data=df, color=df[\"color\"], alpha=0.6)\n", + " \n", + " patch = mpatches.Patch(color=None, label=ylabels[i], visible=False)\n", + " ax.legend(handles=[patch], fontsize=12, loc=\"upper left\", handlelength=0, frameon=False)\n", + " \n", + " if ylims[i] is not None:\n", + " ax.set_ylim(ylims[i])\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/financial_network_analysis_centrality.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "11204e33", + "metadata": {}, + "source": [ + "### Computing in and out degree distributions\n", + "\n", + "The in-degree distribution evaluated at $k$ is the fraction of nodes in a\n", + "network that have in-degree $k$. The in-degree distribution of a `NetworkX`\n", + "DiGraph can be calculated using the code below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61512e1d", + "metadata": {}, + "outputs": [], + "source": [ + "def in_degree_dist(G):\n", + " n = G.number_of_nodes()\n", + " iG = np.array([G.in_degree(v) for v in G.nodes()])\n", + " d = [np.mean(iG == k) for k in range(n+1)]\n", + " return d" + ] + }, + { + "cell_type": "markdown", + "id": "8121ffff", + "metadata": {}, + "source": [ + "The out-degree distribution is defined analogously." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc3e776b", + "metadata": {}, + "outputs": [], + "source": [ + "def out_degree_dist(G):\n", + " n = G.number_of_nodes()\n", + " oG = np.array([G.out_degree(v) for v in G.nodes()])\n", + " d = [np.mean(oG == k) for k in range(n+1)]\n", + " return d" + ] + }, + { + "cell_type": "markdown", + "id": "840131af", + "metadata": {}, + "source": [ + "### Degree distribution for international aircraft trade\n", + "\n", + "Here we illustrate that the commercial aircraft international trade network is\n", + "approximately scale-free by plotting the degree distribution alongside\n", + "$f(x)=cx-\\gamma$ with $c=0.2$ and $\\gamma=1.1$. \n", + "\n", + "In this calculation of the degree distribution, performed by the NetworkX\n", + "function `degree_histogram`, directions are ignored and the network is treated\n", + "as an undirected graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecc6928e", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_degree_dist(G, ax, loglog=True, label=None):\n", + " \"Plot the degree distribution of a graph G on axis ax.\"\n", + " dd = [x for x in nx.degree_histogram(G) if x > 0]\n", + " dd = np.array(dd) / np.sum(dd) # normalize\n", + " if loglog:\n", + " ax.loglog(dd, '-o', lw=0.5, label=label)\n", + " else:\n", + " ax.plot(dd, '-o', lw=0.5, label=label)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5ec0060", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "\n", + "plot_degree_dist(DG, ax, loglog=False, label='degree distribution')\n", + "\n", + "xg = np.linspace(0.5, 25, 250)\n", + "ax.plot(xg, 0.2 * xg**(-1.1), label='power law')\n", + "ax.set_xlim(0.9, 22)\n", + "ax.set_ylim(0, 0.25)\n", + "ax.legend()\n", + "if export_figures:\n", + " plt.savefig(\"figures/commercial_aircraft_2019_2.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6c8cb9ba", + "metadata": {}, + "source": [ + "### Random graphs\n", + "\n", + "The code to produce the Erdos-Renyi random graph, used below, applies the\n", + "combinations function from the `itertools` library. The function\n", + "`combinations(A, k)` returns a list of all subsets of $A$ of size $k$. For\n", + "example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "875a6ae1", + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "letters = 'a', 'b', 'c'\n", + "list(itertools.combinations(letters, 2))" + ] + }, + { + "cell_type": "markdown", + "id": "fe83b981", + "metadata": {}, + "source": [ + "Below we generate random graphs using the Erdos-Renyi and Barabasi-Albert\n", + "algorithms. Here, for convenience, we will define a function to plot these\n", + "graphs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc9858c8", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_random_graph(RG,ax):\n", + " node_pos_dict = nx.spring_layout(RG, k=1.1)\n", + "\n", + " centrality = nx.degree_centrality(RG)\n", + " node_color_list = qbn_io.colorise_weights(list(centrality.values()))\n", + "\n", + " edge_color_list = []\n", + " for i in range(n):\n", + " for j in range(n):\n", + " edge_color_list.append(node_color_list[i])\n", + "\n", + " nx.draw_networkx_nodes(RG, \n", + " node_pos_dict, \n", + " node_color=node_color_list, \n", + " edgecolors='grey', \n", + " node_size=100,\n", + " linewidths=2, \n", + " alpha=0.8, \n", + " ax=ax)\n", + "\n", + " nx.draw_networkx_edges(RG, \n", + " node_pos_dict, \n", + " edge_color=edge_colors, \n", + " alpha=0.4, \n", + " ax=ax)" + ] + }, + { + "cell_type": "markdown", + "id": "968c2f98", + "metadata": {}, + "source": [ + "### An instance of an Erdos–Renyi random graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "027cd2dc", + "metadata": {}, + "outputs": [], + "source": [ + "n = 100\n", + "p = 0.05\n", + "G_er = qbn_io.erdos_renyi_graph(n, p, seed=1234)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86c89612", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 3.2))\n", + "\n", + "axes[0].set_title(\"Graph visualization\")\n", + "plot_random_graph(G_er,axes[0])\n", + "\n", + "axes[1].set_title(\"Degree distribution\")\n", + "plot_degree_dist(G_er, axes[1], loglog=False)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/rand_graph_experiments_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1c2f0411", + "metadata": {}, + "source": [ + "### An instance of a preferential attachment random graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fcae0d0", + "metadata": {}, + "outputs": [], + "source": [ + "n = 100\n", + "m = 5\n", + "G_ba = nx.generators.random_graphs.barabasi_albert_graph(n, m, seed=123)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b513178", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(10, 3.2))\n", + "\n", + "axes[0].set_title(\"Graph visualization\")\n", + "plot_random_graph(G_ba, axes[0])\n", + "\n", + "axes[1].set_title(\"Degree distribution\")\n", + "plot_degree_dist(G_ba, axes[1], loglog=False)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/rand_graph_experiments_2.pdf\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_mcs.ipynb b/ipynb/ch_mcs.ipynb new file mode 100644 index 0000000..f8690e2 --- /dev/null +++ b/ipynb/ch_mcs.ipynb @@ -0,0 +1,799 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9eba78b3", + "metadata": {}, + "source": [ + "# Chapter 4 - Markov Chains and Networks (Python Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c5585b0", + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [], + "source": [ + "! pip install --upgrade quantecon_book_networks" + ] + }, + { + "cell_type": "markdown", + "id": "03d19647", + "metadata": {}, + "source": [ + "We begin with some imports." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48b566bb", + "metadata": {}, + "outputs": [], + "source": [ + "import quantecon as qe\n", + "import quantecon_book_networks\n", + "import quantecon_book_networks.input_output as qbn_io\n", + "import quantecon_book_networks.plotting as qbn_plt\n", + "import quantecon_book_networks.data as qbn_data\n", + "ch4_data = qbn_data.markov_chains_and_networks()\n", + "export_figures = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d76606ee", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from mpl_toolkits.mplot3d.art3d import Poly3DCollection\n", + "quantecon_book_networks.config(\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "da06eb17", + "metadata": {}, + "source": [ + "## Example transition matrices\n", + "\n", + "In this chapter two transition matrices are used.\n", + "\n", + "First, a Markov model is estimated in the international growth dynamics study\n", + "of [Quah (1993)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.142.5504&rep=rep1&type=pdf).\n", + "\n", + "The state is real GDP per capita in a given country relative to the world\n", + "average. \n", + "\n", + "Quah discretizes the possible values to 0–1/4, 1/4–1/2, 1/2–1, 1–2\n", + "and 2–inf, calling these states 1 to 5 respectively. The transitions are over\n", + "a one year period." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1d19891", + "metadata": {}, + "outputs": [], + "source": [ + "P_Q = [\n", + " [0.97, 0.03, 0, 0, 0 ],\n", + " [0.05, 0.92, 0.03, 0, 0 ],\n", + " [0, 0.04, 0.92, 0.04, 0 ],\n", + " [0, 0, 0.04, 0.94, 0.02],\n", + " [0, 0, 0, 0.01, 0.99]\n", + "]\n", + "P_Q = np.array(P_Q)\n", + "codes_Q = ('1', '2', '3', '4', '5')" + ] + }, + { + "cell_type": "markdown", + "id": "c9d3f18b", + "metadata": {}, + "source": [ + "Second, [Benhabib et al. (2015)](https://www.economicdynamics.org/meetpapers/2015/paper_364.pdf) estimate the following transition matrix for intergenerational social mobility.\n", + "\n", + "The states are percentiles of the wealth distribution. \n", + "\n", + "In particular, states 1, 2,..., 8, correspond to the percentiles 0-20%, 20-40%, 40-60%, 60-80%, 80-90%, 90-95%, 95-99%, 99-100%." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a2ff2d3", + "metadata": {}, + "outputs": [], + "source": [ + "P_B = [\n", + " [0.222, 0.222, 0.215, 0.187, 0.081, 0.038, 0.029, 0.006],\n", + " [0.221, 0.22, 0.215, 0.188, 0.082, 0.039, 0.029, 0.006],\n", + " [0.207, 0.209, 0.21, 0.194, 0.09, 0.046, 0.036, 0.008],\n", + " [0.198, 0.201, 0.207, 0.198, 0.095, 0.052, 0.04, 0.009],\n", + " [0.175, 0.178, 0.197, 0.207, 0.11, 0.067, 0.054, 0.012],\n", + " [0.182, 0.184, 0.2, 0.205, 0.106, 0.062, 0.05, 0.011],\n", + " [0.123, 0.125, 0.166, 0.216, 0.141, 0.114, 0.094, 0.021],\n", + " [0.084, 0.084, 0.142, 0.228, 0.17, 0.143, 0.121, 0.028]\n", + " ]\n", + "\n", + "P_B = np.array(P_B)\n", + "codes_B = ('1', '2', '3', '4', '5', '6', '7', '8')" + ] + }, + { + "cell_type": "markdown", + "id": "53bd61df", + "metadata": {}, + "source": [ + "## Markov Chains as Digraphs\n", + "\n", + "### Contour plot of a transition matrix \n", + "\n", + "Here we define a function for producing contour plots of matrices." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a95d7285", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_matrices(matrix,\n", + " codes,\n", + " ax,\n", + " font_size=12,\n", + " alpha=0.6, \n", + " colormap=cm.viridis, \n", + " color45d=None, \n", + " xlabel='sector $j$', \n", + " ylabel='sector $i$'):\n", + " \n", + " ticks = range(len(matrix))\n", + "\n", + " levels = np.sqrt(np.linspace(0, 0.75, 100))\n", + " \n", + " \n", + " if color45d != None:\n", + " co = ax.contourf(ticks, \n", + " ticks,\n", + " matrix,\n", + " alpha=alpha, cmap=colormap)\n", + " ax.plot(ticks, ticks, color=color45d)\n", + " else:\n", + " co = ax.contourf(ticks, \n", + " ticks,\n", + " matrix,\n", + " levels,\n", + " alpha=alpha, cmap=colormap)\n", + "\n", + " ax.set_xlabel(xlabel, fontsize=font_size)\n", + " ax.set_ylabel(ylabel, fontsize=font_size)\n", + " ax.set_yticks(ticks)\n", + " ax.set_yticklabels(codes_B)\n", + " ax.set_xticks(ticks)\n", + " ax.set_xticklabels(codes_B)\n" + ] + }, + { + "cell_type": "markdown", + "id": "15a6af09", + "metadata": {}, + "source": [ + "Now we use our function to produce a plot of the transition matrix for\n", + "intergenerational social mobility, $P_B$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dc01df7", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(6,6))\n", + "plot_matrices(P_B.transpose(), codes_B, ax, alpha=0.75, \n", + " colormap=cm.viridis, color45d='black',\n", + " xlabel='state at time $t$', ylabel='state at time $t+1$')\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/markov_matrix_visualization.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b2476ede", + "metadata": {}, + "source": [ + "### Wealth percentile over time\n", + "\n", + "Here we compare the mixing of the transition matrix for intergenerational\n", + "social mobility $P_B$ and the transition matrix for international growth\n", + "dynamics $P_Q$. \n", + "\n", + "We begin by creating `quantecon` `MarkovChain` objects with each of our transition\n", + "matrices." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f13e25bb", + "metadata": {}, + "outputs": [], + "source": [ + "mc_B = qe.MarkovChain(P_B, state_values=range(1, 9))\n", + "mc_Q = qe.MarkovChain(P_Q, state_values=range(1, 6))" + ] + }, + { + "cell_type": "markdown", + "id": "df645ec9", + "metadata": {}, + "source": [ + "Next we define a function to plot simulations of Markov chains. \n", + "\n", + "Two simulations will be run for each `MarkovChain`, one starting at the\n", + "minimum initial value and one at the maximum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "174ec3fc", + "metadata": {}, + "outputs": [], + "source": [ + "def sim_fig(ax, mc, T=100, seed=14, title=None):\n", + " X1 = mc.simulate(T, init=1, random_state=seed)\n", + " X2 = mc.simulate(T, init=max(mc.state_values), random_state=seed+1)\n", + " ax.plot(X1)\n", + " ax.plot(X2)\n", + " ax.set_xlabel(\"time\")\n", + " ax.set_ylabel(\"state\")\n", + " ax.set_title(title, fontsize=12)" + ] + }, + { + "cell_type": "markdown", + "id": "05dc505f", + "metadata": {}, + "source": [ + "Finally, we produce the figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c98817b", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(2, 1, figsize=(6, 4))\n", + "ax = axes[0]\n", + "sim_fig(axes[0], mc_B, title=\"$P_B$\")\n", + "sim_fig(axes[1], mc_Q, title=\"$P_Q$\")\n", + "axes[1].set_yticks((1, 2, 3, 4, 5))\n", + "\n", + "plt.tight_layout()\n", + "if export_figures:\n", + " plt.savefig(\"figures/benhabib_mobility_mixing.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "2bf229d0", + "metadata": {}, + "source": [ + "### Predicted vs realized cross-country income distributions for 2019\n", + "\n", + "Here we load a `pandas` `DataFrame` of GDP per capita data for countries compared to the global average." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44dc111f", + "metadata": {}, + "outputs": [], + "source": [ + "gdppc_df = ch4_data['gdppc_df']\n", + "gdppc_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "d3a50768", + "metadata": {}, + "source": [ + "Now we assign countries bins, as per Quah (1993)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09775bab", + "metadata": {}, + "outputs": [], + "source": [ + "q = [0, 0.25, 0.5, 1.0, 2.0, np.inf]\n", + "l = [0, 1, 2, 3, 4]\n", + "\n", + "x = pd.cut(gdppc_df.gdppc_r, bins=q, labels=l)\n", + "gdppc_df['interval'] = x\n", + "\n", + "gdppc_df = gdppc_df.reset_index()\n", + "gdppc_df['interval'] = gdppc_df['interval'].astype(float)\n", + "gdppc_df['year'] = gdppc_df['year'].astype(float)" + ] + }, + { + "cell_type": "markdown", + "id": "8c91bb21", + "metadata": {}, + "source": [ + "Here we define a function for calculating the cross-country income\n", + "distributions for a given date range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7e86382", + "metadata": {}, + "outputs": [], + "source": [ + "def gdp_dist_estimate(df, l, yr=(1960, 2019)):\n", + " Y = np.zeros(len(l))\n", + " for i in l:\n", + " Y[i] = df[\n", + " (df['interval'] == i) & \n", + " (df['year'] <= yr[1]) & \n", + " (df['year'] >= yr[0])\n", + " ].count()[0]\n", + " \n", + " return Y / Y.sum()" + ] + }, + { + "cell_type": "markdown", + "id": "ad6d54d7", + "metadata": {}, + "source": [ + "We calculate the true distribution for 1985." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "130b145a", + "metadata": {}, + "outputs": [], + "source": [ + "ψ_1985 = gdp_dist_estimate(gdppc_df,l,yr=(1985, 1985))" + ] + }, + { + "cell_type": "markdown", + "id": "b3b24523", + "metadata": {}, + "source": [ + "Now we use the transition matrix to update the 1985 distribution $t = 2019 - 1985 = 34$\n", + "times to get our predicted 2019 distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de8e360b", + "metadata": {}, + "outputs": [], + "source": [ + "ψ_2019_predicted = ψ_1985 @ np.linalg.matrix_power(P_Q, 2019-1985)" + ] + }, + { + "cell_type": "markdown", + "id": "3e8be48f", + "metadata": {}, + "source": [ + "Now, calculate the true 2019 distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48653d34", + "metadata": {}, + "outputs": [], + "source": [ + "ψ_2019 = gdp_dist_estimate(gdppc_df,l,yr=(2019, 2019))" + ] + }, + { + "cell_type": "markdown", + "id": "886af82b", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d6c8426", + "metadata": {}, + "outputs": [], + "source": [ + "states = np.arange(0, 5)\n", + "ticks = range(5)\n", + "codes_S = ('1', '2', '3', '4', '5')\n", + "\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "width = 0.4\n", + "ax.plot(states, ψ_2019_predicted, '-o', alpha=0.7, label='predicted')\n", + "ax.plot(states, ψ_2019, '-o', alpha=0.7, label='realized')\n", + "ax.set_xlabel(\"state\")\n", + "ax.set_ylabel(\"probability\")\n", + "ax.set_yticks((0.15, 0.2, 0.25, 0.3))\n", + "ax.set_xticks(ticks)\n", + "ax.set_xticklabels(codes_S)\n", + "\n", + "ax.legend(loc='upper center', fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/quah_gdppc_prediction.pdf\")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "ce838e58", + "metadata": {}, + "source": [ + "### Distribution dynamics\n", + "\n", + "Here we define a function for plotting the convergence of marginal\n", + "distributions $\\psi$ under a transition matrix $P$ on the unit simplex." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6eaa5db8", + "metadata": {}, + "outputs": [], + "source": [ + "def convergence_plot(ψ, P, n=14, angle=50):\n", + "\n", + " ax = qbn_plt.unit_simplex(angle)\n", + "\n", + " # Convergence plot\n", + " \n", + " P = np.array(P)\n", + "\n", + " ψ = ψ # Initial condition\n", + "\n", + " x_vals, y_vals, z_vals = [], [], []\n", + " for t in range(n):\n", + " x_vals.append(ψ[0])\n", + " y_vals.append(ψ[1])\n", + " z_vals.append(ψ[2])\n", + " ψ = ψ @ P\n", + "\n", + " ax.scatter(x_vals, y_vals, z_vals, c='darkred', s=80, alpha=0.7, depthshade=False)\n", + "\n", + " mc = qe.MarkovChain(P)\n", + " ψ_star = mc.stationary_distributions[0]\n", + " ax.scatter(ψ_star[0], ψ_star[1], ψ_star[2], c='k', s=80)\n", + "\n", + " return ψ\n" + ] + }, + { + "cell_type": "markdown", + "id": "5bc093e3", + "metadata": {}, + "source": [ + "Now we define P." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64d602c8", + "metadata": {}, + "outputs": [], + "source": [ + "P = (\n", + " (0.9, 0.1, 0.0),\n", + " (0.4, 0.4, 0.2),\n", + " (0.1, 0.1, 0.8)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "a38c94cd", + "metadata": {}, + "source": [ + "#### A trajectory from $\\psi_0 = (0, 0, 1)$\n", + "\n", + "Here we see the sequence of marginals appears to converge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39d90e91", + "metadata": {}, + "outputs": [], + "source": [ + "ψ_0 = (0, 0, 1)\n", + "ψ = convergence_plot(ψ_0, P)\n", + "if export_figures:\n", + " plt.savefig(\"figures/simplex_2.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "da3337df", + "metadata": {}, + "source": [ + "#### A trajectory from $\\psi_0 = (0, 1/2, 1/2)$\n", + "\n", + "Here we see again that the sequence of marginals appears to converge, and the\n", + "limit appears not to depend on the initial distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4ba2e7b", + "metadata": {}, + "outputs": [], + "source": [ + "ψ_0 = (0, 1/2, 1/2)\n", + "ψ = convergence_plot(ψ_0, P, n=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/simplex_3.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7491cf30", + "metadata": {}, + "source": [ + "### Distribution projections from $P_B$\n", + "\n", + "Here we define a function for plotting $\\psi$ after $n$ iterations of the\n", + "transition matrix $P$. The distribution $\\psi_0$ is taken as the uniform \n", + "distribution over the\n", + "state space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dd25c98", + "metadata": {}, + "outputs": [], + "source": [ + "def transition(P, n, ax=None):\n", + " \n", + " P = np.array(P)\n", + " nstates = P.shape[1]\n", + " s0 = np.ones(8) * 1/nstates\n", + " s = s0\n", + " \n", + " for i in range(n):\n", + " s = s @ P\n", + " \n", + " if ax is None:\n", + " fig, ax = plt.subplots()\n", + " \n", + " ax.plot(range(1, nstates+1), s, '-o', alpha=0.6)\n", + " ax.set(ylim=(0, 0.25), \n", + " xticks=((1, nstates)))\n", + " ax.set_title(f\"$t = {n}$\")\n", + " \n", + " return ax" + ] + }, + { + "cell_type": "markdown", + "id": "659480d1", + "metadata": {}, + "source": [ + "We now generate the marginal distributions after 0, 1, 2, and 100 iterations for $P_B$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1bbb0cd", + "metadata": {}, + "outputs": [], + "source": [ + "ns = (0, 1, 2, 100)\n", + "fig, axes = plt.subplots(1, len(ns), figsize=(6, 4))\n", + "\n", + "for n, ax in zip(ns, axes):\n", + " ax = transition(P_B, n, ax=ax)\n", + " ax.set_xlabel(\"Quantile\")\n", + "\n", + "plt.tight_layout()\n", + "if export_figures:\n", + " plt.savefig(\"figures/benhabib_mobility_dists.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e7ee0c57", + "metadata": {}, + "source": [ + "## Asymptotics\n", + "\n", + "### Convergence of the empirical distribution to $\\psi^*$\n", + "\n", + "We begin by creating a `MarkovChain` object, taking $P_B$ as the transition matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f911e34", + "metadata": {}, + "outputs": [], + "source": [ + "mc = qe.MarkovChain(P_B)" + ] + }, + { + "cell_type": "markdown", + "id": "7ca9220f", + "metadata": {}, + "source": [ + "Next we use the `quantecon` package to calculate the true stationary distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50947355", + "metadata": {}, + "outputs": [], + "source": [ + "stationary = mc.stationary_distributions[0]\n", + "n = len(mc.P)" + ] + }, + { + "cell_type": "markdown", + "id": "0ea1a5bf", + "metadata": {}, + "source": [ + "Now we define a function to simulate the Markov chain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d98416ff", + "metadata": {}, + "outputs": [], + "source": [ + "def simulate_distribution(mc, T=100):\n", + " # Simulate path \n", + " n = len(mc.P)\n", + " path = mc.simulate_indices(ts_length=T, random_state=1)\n", + " distribution = np.empty(n)\n", + " for i in range(n):\n", + " distribution[i] = np.mean(path==i)\n", + " return distribution\n" + ] + }, + { + "cell_type": "markdown", + "id": "7bd6bdaa", + "metadata": {}, + "source": [ + "We run simulations of length 10, 100, 1,000 and 10,000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef22ec7", + "metadata": {}, + "outputs": [], + "source": [ + "lengths = [10, 100, 1_000, 10_000]\n", + "dists = []\n", + "\n", + "for t in lengths:\n", + " dists.append(simulate_distribution(mc, t))" + ] + }, + { + "cell_type": "markdown", + "id": "a185b18b", + "metadata": {}, + "source": [ + "Now we produce the plots. \n", + "\n", + "We see that the simulated distribution starts to approach the true stationary distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ccd5bdf", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(2, 2, figsize=(9, 6), sharex='all')#, sharey='all')\n", + "\n", + "axes = axes.flatten()\n", + "\n", + "for dist, ax, t in zip(dists, axes, lengths):\n", + " \n", + " ax.plot(np.arange(n)+1 + .25, \n", + " stationary, \n", + " '-o',\n", + " #width = 0.25, \n", + " label='$\\\\psi^*$', \n", + " alpha=0.75)\n", + " \n", + " ax.plot(np.arange(n)+1, \n", + " dist, \n", + " '-o',\n", + " #width = 0.25, \n", + " label=f'$\\\\hat \\\\psi_k$ with $k={t}$', \n", + " alpha=0.75)\n", + "\n", + "\n", + " ax.set_xlabel(\"state\", fontsize=12)\n", + " ax.set_ylabel(\"prob.\", fontsize=12)\n", + " ax.set_xticks(np.arange(n)+1)\n", + " ax.legend(loc='upper right', fontsize=12, frameon=False)\n", + " ax.set_ylim(0, 0.5)\n", + " \n", + "if export_figures:\n", + " plt.savefig(\"figures/benhabib_ergodicity_1.pdf\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_opt.ipynb b/ipynb/ch_opt.ipynb new file mode 100644 index 0000000..f18ddc2 --- /dev/null +++ b/ipynb/ch_opt.ipynb @@ -0,0 +1,728 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "461d8d77", + "metadata": {}, + "source": [ + "# Chapter 3 - Optimal Flows (Python Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e85fdcd6", + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [], + "source": [ + "! pip install --upgrade quantecon_book_networks" + ] + }, + { + "cell_type": "markdown", + "id": "d3d5fcb1", + "metadata": {}, + "source": [ + "We begin with some imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a00ff7f", + "metadata": {}, + "outputs": [], + "source": [ + "import quantecon as qe\n", + "import quantecon_book_networks\n", + "import quantecon_book_networks.input_output as qbn_io\n", + "import quantecon_book_networks.plotting as qbn_plt\n", + "import quantecon_book_networks.data as qbn_data\n", + "ch3_data = qbn_data.optimal_flows()\n", + "export_figures = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ed711ad", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.optimize import linprog\n", + "import networkx as nx\n", + "import ot\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "from matplotlib.patches import Polygon\n", + "from matplotlib.artist import Artist \n", + "quantecon_book_networks.config(\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "7a5e9cd1", + "metadata": {}, + "source": [ + "## Linear Programming and Duality\n", + "\n", + "### Betweenness centrality (by color and node size) for the Florentine families\n", + "\n", + "We load the Florentine Families data from the NetworkX package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e14e416", + "metadata": {}, + "outputs": [], + "source": [ + "G = nx.florentine_families_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "5199cc62", + "metadata": {}, + "source": [ + "Next we calculate betweenness centrality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dab1b1d9", + "metadata": {}, + "outputs": [], + "source": [ + "bc_dict = nx.betweenness_centrality(G)" + ] + }, + { + "cell_type": "markdown", + "id": "700349de", + "metadata": {}, + "source": [ + "And we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33557ff6", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(9.4, 9.4))\n", + "\n", + "plt.axis(\"off\")\n", + "nx.draw_networkx(\n", + " G, \n", + " ax=ax,\n", + " pos=nx.spring_layout(G, seed=1234), \n", + " with_labels=True,\n", + " alpha=.8,\n", + " arrowsize=15,\n", + " connectionstyle=\"arc3,rad=0.1\",\n", + " node_size=[10_000*(size+0.1) for size in bc_dict.values()], \n", + " node_color=[cm.plasma(bc+0.4) for bc in bc_dict.values()],\n", + ")\n", + "if export_figures:\n", + " plt.savefig(\"figures/betweenness_centrality_1.pdf\")" + ] + }, + { + "cell_type": "markdown", + "id": "0fa794a1", + "metadata": {}, + "source": [ + "### Revenue maximizing quantities and a Python implementation of linear programming\n", + "\n", + "First we specify our linear program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59dd01db", + "metadata": {}, + "outputs": [], + "source": [ + "A = ((2, 5),\n", + " (4, 2))\n", + "b = (30, 20)\n", + "c = (-3, -4) # minus in order to minimize" + ] + }, + { + "cell_type": "markdown", + "id": "f64d9118", + "metadata": {}, + "source": [ + "And now we use SciPy's linear programing module to solve our linear program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0bf9ba9", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.optimize import linprog\n", + "result = linprog(c, A_ub=A, b_ub=b)\n", + "print(result.x)" + ] + }, + { + "cell_type": "markdown", + "id": "b1c7bfbb", + "metadata": {}, + "source": [ + "Here we produce a visualization of what is being done." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41077bbd", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(8, 4.5))\n", + "plt.rcParams['font.size'] = '14'\n", + "\n", + "# Draw constraint lines\n", + "\n", + "ax.plot(np.linspace(-1, 17.5, 100), 6-0.4*np.linspace(-1, 17.5, 100))\n", + "ax.plot(np.linspace(-1, 5.5, 100), 10-2*np.linspace(-1, 5.5, 100))\n", + "ax.text(10, 2.5, \"$2q_1 + 5q_2 \\leq 30$\")\n", + "ax.text(1.5, 8, \"$4q_1 + 2q_2 \\leq 20$\")\n", + "ax.text(-2, 2, \"$q_2 \\geq 0$\")\n", + "ax.text(2.5, -0.7, \"$q_1 \\geq 0$\")\n", + "\n", + "# Draw the feasible region\n", + "feasible_set = Polygon(np.array([[0, 0], \n", + " [0, 6], \n", + " [2.5, 5], \n", + " [5, 0]]))\n", + "ax.add_artist(feasible_set)\n", + "Artist.set_alpha(feasible_set, 0.2) \n", + "\n", + "# Draw the objective function\n", + "ax.plot(np.linspace(-1, 5.5, 100), 3.875-0.75*np.linspace(-1, 5.5, 100), 'g-')\n", + "ax.plot(np.linspace(-1, 5.5, 100), 5.375-0.75*np.linspace(-1, 5.5, 100), 'g-')\n", + "ax.plot(np.linspace(-1, 5.5, 100), 6.875-0.75*np.linspace(-1, 5.5, 100), 'g-')\n", + "ax.text(5.8, 1, \"revenue $ = 3q_1 + 4q_2$\")\n", + "\n", + "# Draw the optimal solution\n", + "ax.plot(2.5, 5, \"o\", color=\"black\")\n", + "ax.text(2.7, 5.2, \"optimal solution\")\n", + "\n", + "for spine in ['right', 'top']:\n", + " ax.spines[spine].set_color('none')\n", + " \n", + "ax.set_xticks(())\n", + "ax.set_yticks(())\n", + "\n", + "for spine in ['left', 'bottom']:\n", + " ax.spines[spine].set_position('zero')\n", + " \n", + "ax.set_ylim(-1, 8)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/linear_programming_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b1c4f25b", + "metadata": {}, + "source": [ + "## Optimal Transport\n", + " \n", + "\n", + "### Transforming one distribution into another\n", + "\n", + "Below we provide code to produce a visualization of transforming one\n", + "distribution into another in one dimension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92b7150b", + "metadata": {}, + "outputs": [], + "source": [ + "σ = 0.1\n", + "\n", + "def ϕ(z):\n", + " return (1 / np.sqrt(2 * σ**2 * np.pi)) * np.exp(-z**2 / (2 * σ**2))\n", + "\n", + "def v(x, a=0.4, b=0.6, s=1.0, t=1.4):\n", + " return a * ϕ(x - s) + b * ϕ(x - t)\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "\n", + "x = np.linspace(0.2, 4, 1000)\n", + "ax.plot(x, v(x), label=\"$\\\\phi$\")\n", + "ax.plot(x, v(x, s=3.0, t=3.3, a=0.6), label=\"$\\\\psi$\")\n", + "\n", + "ax.legend(loc='upper left', fontsize=12, frameon=False)\n", + "\n", + "ax.arrow(1.8, 1.6, 0.8, 0.0, width=0.01, head_width=0.08)\n", + "ax.annotate('transform', xy=(1.9, 1.9), fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/ot_figs_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7464f721", + "metadata": {}, + "source": [ + "### Function to solve a transport problem via linear programming\n", + "\n", + "Here we define a function to solve optimal transport problems using linear programming." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d94e547a", + "metadata": {}, + "outputs": [], + "source": [ + "def ot_solver(phi, psi, c, method='highs-ipm'):\n", + " \"\"\"\n", + " Solve the OT problem associated with distributions phi, psi\n", + " and cost matrix c.\n", + " Parameters\n", + " ----------\n", + " phi : 1-D array\n", + " Distribution over the source locations.\n", + " psi : 1-D array\n", + " Distribution over the target locations.\n", + " c : 2-D array\n", + " Cost matrix.\n", + " \"\"\"\n", + " n, m = len(phi), len(psi)\n", + "\n", + " # vectorize c\n", + " c_vec = c.reshape((m * n, 1), order='F')\n", + "\n", + " # Construct A and b\n", + " A1 = np.kron(np.ones((1, m)), np.identity(n))\n", + " A2 = np.kron(np.identity(m), np.ones((1, n)))\n", + " A = np.vstack((A1, A2))\n", + " b = np.hstack((phi, psi))\n", + "\n", + " # Call sover\n", + " res = linprog(c_vec, A_eq=A, b_eq=b, method=method)\n", + "\n", + " # Invert the vec operation to get the solution as a matrix\n", + " pi = res.x.reshape((n, m), order='F')\n", + " return pi" + ] + }, + { + "cell_type": "markdown", + "id": "dfdeb358", + "metadata": {}, + "source": [ + "Now we can set up a simple optimal transport problem." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54e87d72", + "metadata": {}, + "outputs": [], + "source": [ + "phi = np.array((0.5, 0.5))\n", + "psi = np.array((1, 0))\n", + "c = np.ones((2, 2))" + ] + }, + { + "cell_type": "markdown", + "id": "b81830ff", + "metadata": {}, + "source": [ + "Next we solve using the above function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e044c8c", + "metadata": {}, + "outputs": [], + "source": [ + "ot_solver(phi, psi, c)" + ] + }, + { + "cell_type": "markdown", + "id": "f44e6ab3", + "metadata": {}, + "source": [ + "We see we get the same result as when using the Python optimal transport\n", + "package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82d43df5", + "metadata": {}, + "outputs": [], + "source": [ + "ot.emd(phi, psi, c) " + ] + }, + { + "cell_type": "markdown", + "id": "642f85ba", + "metadata": {}, + "source": [ + "### An optimal transport problem solved by linear programming\n", + "\n", + "Here we demonstrate a more detailed optimal transport problem. We begin by defining a node class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bcd7819", + "metadata": {}, + "outputs": [], + "source": [ + "class Node:\n", + "\n", + " def __init__(self, x, y, mass, group, name):\n", + "\n", + " self.x, self.y = x, y\n", + " self.mass, self.group = mass, group\n", + " self.name = name" + ] + }, + { + "cell_type": "markdown", + "id": "ad91c7e4", + "metadata": {}, + "source": [ + "Now we define a function for randomly generating nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d1bfd36", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.stats import betabinom\n", + "\n", + "def build_nodes_of_one_type(group='phi', n=100, seed=123):\n", + "\n", + " nodes = []\n", + " np.random.seed(seed)\n", + "\n", + " for i in range(n):\n", + " \n", + " if group == 'phi':\n", + " m = 1/n\n", + " x = np.random.uniform(-2, 2)\n", + " y = np.random.uniform(-2, 2)\n", + " else:\n", + " m = betabinom.pmf(i, n-1, 2, 2)\n", + " x = 0.6 * np.random.uniform(-1.5, 1.5)\n", + " y = 0.6 * np.random.uniform(-1.5, 1.5)\n", + " \n", + " name = group + str(i)\n", + " nodes.append(Node(x, y, m, group, name))\n", + "\n", + " return nodes" + ] + }, + { + "cell_type": "markdown", + "id": "45b701ab", + "metadata": {}, + "source": [ + "We now generate our source and target nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efa68979", + "metadata": {}, + "outputs": [], + "source": [ + "n_phi = 32\n", + "n_psi = 32\n", + "\n", + "phi_list = build_nodes_of_one_type(group='phi', n=n_phi)\n", + "psi_list = build_nodes_of_one_type(group='psi', n=n_psi)\n", + "\n", + "phi_probs = [phi.mass for phi in phi_list]\n", + "psi_probs = [psi.mass for psi in psi_list]" + ] + }, + { + "cell_type": "markdown", + "id": "61f09811", + "metadata": {}, + "source": [ + "Now we define our transport costs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38af4ff7", + "metadata": {}, + "outputs": [], + "source": [ + "c = np.empty((n_phi, n_psi))\n", + "for i in range(n_phi):\n", + " for j in range(n_psi):\n", + " x0, y0 = phi_list[i].x, phi_list[i].y\n", + " x1, y1 = psi_list[j].x, psi_list[j].y\n", + " c[i, j] = np.sqrt((x0-x1)**2 + (y0-y1)**2)" + ] + }, + { + "cell_type": "markdown", + "id": "cbe78a44", + "metadata": {}, + "source": [ + "We solve our optimal transport problem using the Python optimal transport package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8cbef04", + "metadata": {}, + "outputs": [], + "source": [ + "pi = ot.emd(phi_probs, psi_probs, c)" + ] + }, + { + "cell_type": "markdown", + "id": "3fd92511", + "metadata": {}, + "source": [ + "Finally we produce a graph of our sources, targets, and optimal transport plan." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa85e02e", + "metadata": {}, + "outputs": [], + "source": [ + "g = nx.DiGraph()\n", + "g.add_nodes_from([phi.name for phi in phi_list])\n", + "g.add_nodes_from([psi.name for psi in psi_list])\n", + "\n", + "for i in range(n_phi):\n", + " for j in range(n_psi):\n", + " if pi[i, j] > 0:\n", + " g.add_edge(phi_list[i].name, psi_list[j].name, weight=pi[i, j])\n", + "\n", + "node_pos_dict={}\n", + "for phi in phi_list:\n", + " node_pos_dict[phi.name] = (phi.x, phi.y)\n", + "for psi in psi_list:\n", + " node_pos_dict[psi.name] = (psi.x, psi.y)\n", + "\n", + "node_color_list = []\n", + "node_size_list = []\n", + "scale = 8_000\n", + "for phi in phi_list:\n", + " node_color_list.append('blue')\n", + " node_size_list.append(phi.mass * scale)\n", + "for psi in psi_list:\n", + " node_color_list.append('red')\n", + " node_size_list.append(psi.mass * scale)\n", + "\n", + "fig, ax = plt.subplots(figsize=(7, 10))\n", + "plt.axis('off')\n", + "\n", + "nx.draw_networkx_nodes(g, \n", + " node_pos_dict, \n", + " node_color=node_color_list,\n", + " node_size=node_size_list,\n", + " edgecolors='grey',\n", + " linewidths=1,\n", + " alpha=0.5,\n", + " ax=ax)\n", + "\n", + "nx.draw_networkx_edges(g, \n", + " node_pos_dict, \n", + " arrows=True,\n", + " connectionstyle='arc3,rad=0.1',\n", + " alpha=0.6)\n", + "if export_figures:\n", + " plt.savefig(\"figures/ot_large_scale_1.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4bd33217", + "metadata": {}, + "source": [ + "### Solving linear assignment as an optimal transport problem\n", + "\n", + "Here we set up a linear assignment problem (matching $n$ workers to $n$ jobs)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f26dc4c", + "metadata": {}, + "outputs": [], + "source": [ + "n = 4\n", + "phi = np.ones(n)\n", + "psi = np.ones(n)" + ] + }, + { + "cell_type": "markdown", + "id": "756aa046", + "metadata": {}, + "source": [ + "We generate our cost matrix (the cost of training the $i$th worker for the $j$th job)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "437274b1", + "metadata": {}, + "outputs": [], + "source": [ + "c = np.random.uniform(size=(n, n))" + ] + }, + { + "cell_type": "markdown", + "id": "b6a66222", + "metadata": {}, + "source": [ + "Finally, we solve our linear assignment problem as a special case of optimal transport." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69565581", + "metadata": {}, + "outputs": [], + "source": [ + "ot.emd(phi, psi, c)" + ] + }, + { + "cell_type": "markdown", + "id": "ef9fc68a", + "metadata": {}, + "source": [ + "### Python Spatial Analysis library\n", + "\n", + "Readers interested in computational optimal transport should also consider\n", + "PySAL, the [Python Spatial Analysis library](https://pysal.org/). See, for\n", + "example, https://pysal.org/spaghetti/notebooks/transportation-problem.html.\n", + "\n", + "### The General Flow Problem\n", + "\n", + "Here we solve a simple network flow problem as a linear program. We begin by\n", + "defining the node-edge incidence matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e7fc46b", + "metadata": {}, + "outputs": [], + "source": [ + "A = (\n", + "( 1, 1, 0, 0),\n", + "(-1, 0, 1, 0),\n", + "( 0, 0, -1, 1),\n", + "( 0, -1, 0, -1)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "23fac21d", + "metadata": {}, + "source": [ + "Now we define exogenous supply and transport costs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8c5644d", + "metadata": {}, + "outputs": [], + "source": [ + "b = (10, 0, 0, -10)\n", + "c = (1, 4, 1, 1)" + ] + }, + { + "cell_type": "markdown", + "id": "3fe351ee", + "metadata": {}, + "source": [ + "Finally we solve as a linear program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a257b5e1", + "metadata": {}, + "outputs": [], + "source": [ + "result = linprog(c, A_eq=A, b_eq=b, method='highs-ipm')\n", + "print(result.x)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_opt_julia.ipynb b/ipynb/ch_opt_julia.ipynb new file mode 100644 index 0000000..b88726d --- /dev/null +++ b/ipynb/ch_opt_julia.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "736e5076", + "metadata": {}, + "source": [ + "# Chapter 3 - Optimal Flows (Julia Code)\n", + "\n", + "## Bellman’s Method\n", + "\n", + "Here we demonstrate solving a shortest path problem using Bellman's method.\n", + "\n", + "Our first step is to set up the cost function, which we store as an array\n", + "called `c`. Note that we set `c[i, j] = Inf` when no edge exists from `i` to\n", + "`j`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c36f47c4", + "metadata": { + "tags": [ + "remove-output" + ] + }, + "outputs": [], + "source": [ + "c = fill(Inf, (7, 7))\n", + "c[1, 2], c[1, 3], c[1, 4] = 1, 5, 3\n", + "c[2, 4], c[2, 5] = 9, 6\n", + "c[3, 6] = 2\n", + "c[4, 6] = 4\n", + "c[5, 7] = 4\n", + "c[6, 7] = 1\n", + "c[7, 7] = 0" + ] + }, + { + "cell_type": "markdown", + "id": "2b8fa3ad", + "metadata": {}, + "source": [ + "Next we define the Bellman operator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2bc51e6", + "metadata": { + "tags": [ + "remove-output" + ] + }, + "outputs": [], + "source": [ + "function T(q)\n", + " Tq = similar(q)\n", + " n = length(q)\n", + " for x in 1:n\n", + " Tq[x] = minimum(c[x, :] + q[:])\n", + " end\n", + " return Tq\n", + "end" + ] + }, + { + "cell_type": "markdown", + "id": "9857bf8a", + "metadata": {}, + "source": [ + "Now we arbitrarily set $q \\equiv 0$, generate the sequence of iterates $T_q$,\n", + "$T^2_q$, $T^3_q$ and plot them. By $T^3_q$ has already converged on $q^∗$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "757cadc8", + "metadata": {}, + "outputs": [], + "source": [ + "using PyPlot\n", + "export_figures = false\n", + "fig, ax = plt.subplots(figsize=(6, 4))\n", + "\n", + "n = 7\n", + "q = zeros(n)\n", + "ax.plot(1:n, q)\n", + "ax.set_xlabel(\"cost-to-go\")\n", + "ax.set_ylabel(\"nodes\")\n", + "\n", + "for i in 1:3\n", + " new_q = T(q)\n", + " ax.plot(1:n, new_q, \"-o\", alpha=0.7, label=\"iterate $i\")\n", + " q = new_q\n", + "end\n", + "\n", + "ax.legend()\n", + "if export_figures == true\n", + " plt.savefig(\"figures/shortest_path_iter_1.pdf\")\n", + "end" + ] + }, + { + "cell_type": "markdown", + "id": "d31abe43", + "metadata": {}, + "source": [ + "## Linear programming\n", + "\n", + "When solving linear programs, one option is to use a domain specific modeling\n", + "language to set out the objective and constraints in the optimization problem.\n", + "Here we demonstrate the Julia package `JuMP`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67361f35", + "metadata": {}, + "outputs": [], + "source": [ + "using JuMP\n", + "using GLPK" + ] + }, + { + "cell_type": "markdown", + "id": "9bd4aef4", + "metadata": {}, + "source": [ + "We create our model object and select our solver." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "856ef0ee", + "metadata": {}, + "outputs": [], + "source": [ + "m = Model()\n", + "set_optimizer(m, GLPK.Optimizer)" + ] + }, + { + "cell_type": "markdown", + "id": "fe817ce7", + "metadata": {}, + "source": [ + "Now we add variables, constraints and an objective to our model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07228cb0", + "metadata": { + "tags": [ + "remove-output" + ] + }, + "outputs": [], + "source": [ + "@variable(m, q1 >= 0)\n", + "@variable(m, q2 >= 0)\n", + "@constraint(m, 2q1 + 5q2 <= 30)\n", + "@constraint(m, 4q1 + 2q2 <= 20)\n", + "@objective(m, Max, 3q1 + 4q2)" + ] + }, + { + "cell_type": "markdown", + "id": "842781b0", + "metadata": {}, + "source": [ + "Finally we solve our linear program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83c0d928", + "metadata": {}, + "outputs": [], + "source": [ + "optimize!(m)\n", + "\n", + "println(value.(q1)) \n", + "println(value.(q2))" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Julia", + "language": "Julia", + "name": "julia-1.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/ch_production.ipynb b/ipynb/ch_production.ipynb new file mode 100644 index 0000000..8f51490 --- /dev/null +++ b/ipynb/ch_production.ipynb @@ -0,0 +1,737 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7e970772", + "metadata": {}, + "source": [ + "# Chapter 2 - Production (Python Code)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd1d49bf", + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [], + "source": [ + "! pip install --upgrade quantecon_book_networks" + ] + }, + { + "cell_type": "markdown", + "id": "5bb0e40f", + "metadata": {}, + "source": [ + "We begin with some imports." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b06f1ff", + "metadata": {}, + "outputs": [], + "source": [ + "import quantecon as qe\n", + "import quantecon_book_networks\n", + "import quantecon_book_networks.input_output as qbn_io\n", + "import quantecon_book_networks.plotting as qbn_plt\n", + "import quantecon_book_networks.data as qbn_data\n", + "ch2_data = qbn_data.production()\n", + "default_figsize = (6, 4)\n", + "export_figures = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67d26617", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.cm as cm\n", + "import matplotlib.colors as plc\n", + "from matplotlib import cm\n", + "quantecon_book_networks.config(\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "aa98eb15", + "metadata": {}, + "source": [ + "## Multisector Models\n", + "\n", + "We start by loading a graph of linkages between 15 US sectors in 2021. \n", + "\n", + "Our graph comes as a list of sector codes, an adjacency matrix of sales between\n", + "the sectors, and a list the total sales of each sector. \n", + "\n", + "In particular, `Z[i,j]` is the sales from industry `i` to industry `j`, and `X[i]` is the the total sales\n", + "of each sector `i`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bac5b180", + "metadata": {}, + "outputs": [], + "source": [ + "codes = ch2_data[\"us_sectors_15\"][\"codes\"]\n", + "Z = ch2_data[\"us_sectors_15\"][\"adjacency_matrix\"]\n", + "X = ch2_data[\"us_sectors_15\"][\"total_industry_sales\"]" + ] + }, + { + "cell_type": "markdown", + "id": "c3a07fd9", + "metadata": {}, + "source": [ + "Now we define a function to build coefficient matrices. \n", + "\n", + "Two coefficient matrices are returned. The backward linkage case, where sales\n", + "between sector `i` and `j` are given as a fraction of total sales of sector\n", + "`j`. The forward linkage case, where sales between sector `i` and `j` are\n", + "given as a fraction of total sales of sector `i`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa4c9d05", + "metadata": {}, + "outputs": [], + "source": [ + "def build_coefficient_matrices(Z, X):\n", + " \"\"\"\n", + " Build coefficient matrices A and F from Z and X via \n", + " \n", + " A[i, j] = Z[i, j] / X[j] \n", + " F[i, j] = Z[i, j] / X[i]\n", + " \n", + " \"\"\"\n", + " A, F = np.empty_like(Z), np.empty_like(Z)\n", + " n = A.shape[0]\n", + " for i in range(n):\n", + " for j in range(n):\n", + " A[i, j] = Z[i, j] / X[j]\n", + " F[i, j] = Z[i, j] / X[i]\n", + "\n", + " return A, F\n", + "\n", + "A, F = build_coefficient_matrices(Z, X)" + ] + }, + { + "cell_type": "markdown", + "id": "66ecec7b", + "metadata": {}, + "source": [ + "### Backward linkages for 15 US sectors in 2021\n", + "\n", + "Here we calculate the hub-based eigenvector centrality of our backward linkage coefficient matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1e25af6", + "metadata": {}, + "outputs": [], + "source": [ + "centrality = qbn_io.eigenvector_centrality(A)" + ] + }, + { + "cell_type": "markdown", + "id": "99a5def5", + "metadata": {}, + "source": [ + "Now we use the `quantecon_book_networks` package to produce our plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c9a1c8e", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(8, 10))\n", + "plt.axis(\"off\")\n", + "color_list = qbn_io.colorise_weights(centrality, beta=False)\n", + "# Remove self-loops\n", + "A1 = A.copy()\n", + "for i in range(A1.shape[0]):\n", + " A1[i][i] = 0\n", + "qbn_plt.plot_graph(A1, X, ax, codes, \n", + " layout_type='spring',\n", + " layout_seed=5432167,\n", + " tol=0.0,\n", + " node_color_list=color_list) \n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "30667357", + "metadata": {}, + "source": [ + "### Eigenvector centrality of across US industrial sectors\n", + "\n", + "Now we plot a bar chart of hub-based eigenvector centrality by sector." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60e3a77c", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "ax.bar(codes, centrality, color=color_list, alpha=0.6)\n", + "ax.set_ylabel(\"eigenvector centrality\", fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_ec.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f0556d61", + "metadata": {}, + "source": [ + "### Output multipliers across 15 US industrial sectors\n", + "\n", + "Output multipliers are equal to the authority-based Katz centrality measure of\n", + "the backward linkage coefficient matrix. \n", + "\n", + "Here we calculate authority-based\n", + "Katz centrality using the `quantecon_book_networks` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1358ad2", + "metadata": {}, + "outputs": [], + "source": [ + "omult = qbn_io.katz_centrality(A, authority=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d45c83f2", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "omult_color_list = qbn_io.colorise_weights(omult,beta=False)\n", + "ax.bar(codes, omult, color=omult_color_list, alpha=0.6)\n", + "ax.set_ylabel(\"Output multipliers\", fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_omult.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d07a87b3", + "metadata": {}, + "source": [ + "### Forward linkages and upstreamness over US industrial sectors\n", + "\n", + "Upstreamness is the hub-based Katz centrality of the forward linkage\n", + "coefficient matrix. \n", + "\n", + "Here we calculate hub-based Katz centrality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2df63367", + "metadata": {}, + "outputs": [], + "source": [ + "upstreamness = qbn_io.katz_centrality(F)" + ] + }, + { + "cell_type": "markdown", + "id": "cff33a99", + "metadata": {}, + "source": [ + "Now we plot the network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ac45508", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(8, 10))\n", + "plt.axis(\"off\")\n", + "upstreamness_color_list = qbn_io.colorise_weights(upstreamness,beta=False)\n", + "# Remove self-loops\n", + "for i in range(F.shape[0]):\n", + " F[i][i] = 0\n", + "qbn_plt.plot_graph(F, X, ax, codes, \n", + " layout_type='spring', # alternative layouts: spring, circular, random, spiral\n", + " layout_seed=5432167,\n", + " tol=0.0,\n", + " node_color_list=upstreamness_color_list) \n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_fwd.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e9cbc94b", + "metadata": {}, + "source": [ + "### Relative upstreamness of US industrial sectors\n", + "\n", + "Here we produce a barplot of upstreamness." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abc67147", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "ax.bar(codes, upstreamness, color=upstreamness_color_list, alpha=0.6)\n", + "ax.set_ylabel(\"upstreamness\", fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_up.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f9223fd5", + "metadata": {}, + "source": [ + "### Hub-based Katz centrality of across 15 US industrial sectors\n", + "\n", + "Next we plot the hub-based Katz centrality of the backward linkage coefficient matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19bce52b", + "metadata": {}, + "outputs": [], + "source": [ + "kcentral = qbn_io.katz_centrality(A)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7563381", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=default_figsize)\n", + "kcentral_color_list = qbn_io.colorise_weights(kcentral,beta=False)\n", + "ax.bar(codes, kcentral, color=kcentral_color_list, alpha=0.6)\n", + "ax.set_ylabel(\"Katz hub centrality\", fontsize=12)\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_katz.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bf61ce72", + "metadata": {}, + "source": [ + "### The Leontief inverse 𝐿 (hot colors are larger values)\n", + "\n", + "We construct the Leontief inverse matrix from 15 sector adjacency matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87c25e09", + "metadata": {}, + "outputs": [], + "source": [ + "I = np.identity(len(A))\n", + "L = np.linalg.inv(I - A)" + ] + }, + { + "cell_type": "markdown", + "id": "a37cada4", + "metadata": {}, + "source": [ + "Now we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d590c0e1", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(6.5, 5.5))\n", + "\n", + "ticks = range(len(L))\n", + "\n", + "levels = np.sqrt(np.linspace(0, 0.75, 100))\n", + "\n", + "co = ax.contourf(ticks, \n", + " ticks,\n", + " L,\n", + " levels,\n", + " alpha=0.85, cmap=cm.plasma)\n", + "\n", + "ax.set_xlabel('sector $j$', fontsize=12)\n", + "ax.set_ylabel('sector $i$', fontsize=12)\n", + "ax.set_yticks(ticks)\n", + "ax.set_yticklabels(codes)\n", + "ax.set_xticks(ticks)\n", + "ax.set_xticklabels(codes)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_leo.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1a9a7d40", + "metadata": {}, + "source": [ + "### Propagation of demand shocks via backward linkages\n", + "\n", + "We begin by generating a demand shock vector $d$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0861b2f5", + "metadata": {}, + "outputs": [], + "source": [ + "N = len(A)\n", + "np.random.seed(1234)\n", + "d = np.random.rand(N) \n", + "d[6] = 1 # positive shock to agriculture" + ] + }, + { + "cell_type": "markdown", + "id": "7fa2840f", + "metadata": {}, + "source": [ + "Now we simulate the demand shock propagating through the economy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9089f879", + "metadata": {}, + "outputs": [], + "source": [ + "sim_length = 11\n", + "x = d\n", + "x_vecs = []\n", + "for i in range(sim_length):\n", + " if i % 2 ==0:\n", + " x_vecs.append(x)\n", + " x = A @ x" + ] + }, + { + "cell_type": "markdown", + "id": "4a51906c", + "metadata": {}, + "source": [ + "Finally, we plot the shock propagating through the economy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d777b13", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(3, 2, figsize=(8, 10))\n", + "axes = axes.flatten()\n", + "\n", + "for ax, x_vec, i in zip(axes, x_vecs, range(sim_length)):\n", + " if i % 2 != 0:\n", + " pass\n", + " ax.set_title(f\"round {i*2}\")\n", + " x_vec_cols = qbn_io.colorise_weights(x_vec,beta=False)\n", + " # remove self-loops\n", + " for i in range(len(A)):\n", + " A[i][i] = 0\n", + " qbn_plt.plot_graph(A, X, ax, codes,\n", + " layout_type='spring',\n", + " layout_seed=342156,\n", + " node_color_list=x_vec_cols,\n", + " node_size_multiple=0.00028,\n", + " edge_size_multiple=0.8)\n", + "\n", + "plt.tight_layout()\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_15_shocks.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6ccd313f", + "metadata": {}, + "source": [ + "### Network for 71 US sectors in 2021\n", + "\n", + "We start by loading a graph of linkages between 71 US sectors in 2021." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24d32882", + "metadata": {}, + "outputs": [], + "source": [ + "codes_71 = ch2_data['us_sectors_71']['codes']\n", + "A_71 = ch2_data['us_sectors_71']['adjacency_matrix']\n", + "X_71 = ch2_data['us_sectors_71']['total_industry_sales']" + ] + }, + { + "cell_type": "markdown", + "id": "e4efe0f9", + "metadata": {}, + "source": [ + "Next we calculate our graph's properties. \n", + "\n", + "We use hub-based eigenvector centrality as our centrality measure for this plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c468f74b", + "metadata": {}, + "outputs": [], + "source": [ + "centrality_71 = qbn_io.eigenvector_centrality(A_71)\n", + "color_list_71 = qbn_io.colorise_weights(centrality_71,beta=False)" + ] + }, + { + "cell_type": "markdown", + "id": "37c499e0", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d920aca", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 12))\n", + "plt.axis(\"off\")\n", + "# Remove self-loops\n", + "for i in range(A_71.shape[0]):\n", + " A_71[i][i] = 0\n", + "qbn_plt.plot_graph(A_71, X_71, ax, codes_71,\n", + " node_size_multiple=0.0005,\n", + " edge_size_multiple=4.0,\n", + " layout_type='spring',\n", + " layout_seed=5432167,\n", + " tol=0.01,\n", + " node_color_list=color_list_71)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_71.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "419a96fa", + "metadata": {}, + "source": [ + "### Network for 114 Australian industry sectors in 2020\n", + "\n", + "Next we load a graph of linkages between 114 Australian sectors in 2020." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52309e87", + "metadata": {}, + "outputs": [], + "source": [ + "codes_114 = ch2_data['au_sectors_114']['codes']\n", + "A_114 = ch2_data['au_sectors_114']['adjacency_matrix']\n", + "X_114 = ch2_data['au_sectors_114']['total_industry_sales']" + ] + }, + { + "cell_type": "markdown", + "id": "b5e46835", + "metadata": {}, + "source": [ + "Next we calculate our graph's properties. \n", + "\n", + "We use hub-based eigenvector centrality as our centrality measure for this plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9555ffa8", + "metadata": {}, + "outputs": [], + "source": [ + "centrality_114 = qbn_io.eigenvector_centrality(A_114)\n", + "color_list_114 = qbn_io.colorise_weights(centrality_114,beta=False)" + ] + }, + { + "cell_type": "markdown", + "id": "1b6801cd", + "metadata": {}, + "source": [ + "Finally we produce the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1af80023", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(11, 13.2))\n", + "plt.axis(\"off\")\n", + "# Remove self-loops\n", + "for i in range(A_114.shape[0]):\n", + " A_114[i][i] = 0\n", + "qbn_plt.plot_graph(A_114, X_114, ax, codes_114,\n", + " node_size_multiple=0.008,\n", + " edge_size_multiple=5.0,\n", + " layout_type='spring',\n", + " layout_seed=5432167,\n", + " tol=0.03,\n", + " node_color_list=color_list_114)\n", + "\n", + "if export_figures:\n", + " plt.savefig(\"figures/input_output_analysis_aus_114.pdf\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a3095a50", + "metadata": {}, + "source": [ + "### GDP growth rates and std. deviations (in parentheses) for 8 countries\n", + "\n", + "Here we load a `pandas` DataFrame of GDP growth rates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7aa80e5", + "metadata": {}, + "outputs": [], + "source": [ + "gdp_df = ch2_data['gdp_df']\n", + "gdp_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "c82df9d4", + "metadata": {}, + "source": [ + "Now we plot the growth rates and calculate their standard deviations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "394a86ea", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(5, 2, figsize=(8, 9))\n", + "axes = axes.flatten()\n", + "\n", + "countries = gdp_df.columns\n", + "t = np.asarray(gdp_df.index.astype(float))\n", + "series = [np.asarray(gdp_df[country].astype(float)) for country in countries]\n", + "\n", + "\n", + "for ax, country, gdp_data in zip(axes, countries, series):\n", + " \n", + " ax.plot(t, gdp_data)\n", + " ax.set_title(f'{country} (${gdp_data.std():1.2f}$\\%)' )\n", + " ax.set_ylabel('\\%')\n", + " ax.set_ylim((-12, 14))\n", + "\n", + "plt.tight_layout()\n", + "if export_figures:\n", + " plt.savefig(\"figures/gdp_growth.pdf\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/code_book/figures/benhabib_mobility_dists.pdf b/ipynb/figures/benhabib_mobility_dists.pdf similarity index 100% rename from code_book/figures/benhabib_mobility_dists.pdf rename to ipynb/figures/benhabib_mobility_dists.pdf diff --git a/ipynb/pkg_funcs.ipynb b/ipynb/pkg_funcs.ipynb new file mode 100644 index 0000000..886138c --- /dev/null +++ b/ipynb/pkg_funcs.ipynb @@ -0,0 +1,582 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "83f7d689", + "metadata": {}, + "source": [ + "# quantecon_book_networks\n", + "\n", + "## input_output\n", + "\n", + "### node_total_exports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e1c0e01", + "metadata": {}, + "outputs": [], + "source": [ + "def node_total_exports(G):\n", + " node_exports = []\n", + " for node1 in G.nodes():\n", + " total_export = 0\n", + " for node2 in G[node1]:\n", + " total_export += G[node1][node2]['weight']\n", + " node_exports.append(total_export)\n", + " return node_exports" + ] + }, + { + "cell_type": "markdown", + "id": "d91edba5", + "metadata": {}, + "source": [ + "### node_total_imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7942abf", + "metadata": {}, + "outputs": [], + "source": [ + "def node_total_imports(G):\n", + " node_imports = []\n", + " for node1 in G.nodes():\n", + " total_import = 0\n", + " for node2 in G[node1]:\n", + " total_import += G[node2][node1]['weight']\n", + " node_imports.append(total_import)\n", + " return node_imports" + ] + }, + { + "cell_type": "markdown", + "id": "3dd8089f", + "metadata": {}, + "source": [ + "### edge_weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdf563f5", + "metadata": {}, + "outputs": [], + "source": [ + "def edge_weights(G):\n", + " edge_weights = [G[u][v]['weight'] for u,v in G.edges()]\n", + " return edge_weights" + ] + }, + { + "cell_type": "markdown", + "id": "055dcff7", + "metadata": {}, + "source": [ + "### normalise_weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54211ff8", + "metadata": {}, + "outputs": [], + "source": [ + "def normalise_weights(weights,scalar=1):\n", + " max_value = np.max(weights)\n", + " return [scalar * (weight / max_value) for weight in weights]" + ] + }, + { + "cell_type": "markdown", + "id": "bfaedc3f", + "metadata": {}, + "source": [ + "### to_zero_one" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed795d06", + "metadata": {}, + "outputs": [], + "source": [ + "def to_zero_one(x):\n", + " \"Map vector x to the zero one interval.\"\n", + " x = np.array(x)\n", + " x_min, x_max = x.min(), x.max()\n", + " return (x - x_min)/(x_max - x_min)" + ] + }, + { + "cell_type": "markdown", + "id": "6ce4a5bd", + "metadata": {}, + "source": [ + "### to_zero_one_beta" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2d51900", + "metadata": {}, + "outputs": [], + "source": [ + "def to_zero_one_beta(x, \n", + " qrange=[0.25, 0.75], \n", + " beta_para=[0.5, 0.5]):\n", + " \n", + " \"\"\"\n", + " Nonlinearly map vector x to the zero one interval with beta distribution.\n", + " https://en.wikipedia.org/wiki/Beta_distribution\n", + " \"\"\"\n", + " x = np.array(x)\n", + " x_min, x_max = x.min(), x.max()\n", + " if beta_para != None:\n", + " a, b = beta_para\n", + " return beta.cdf((x - x_min) /(x_max - x_min), a, b)\n", + " else:\n", + " q1, q2 = qrange\n", + " return (x - x_min) * (q2 - q1) /(x_max - x_min) + q1" + ] + }, + { + "cell_type": "markdown", + "id": "677a3b25", + "metadata": {}, + "source": [ + "### colorise_weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "650554ba", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.cm as cm\n", + "def colorise_weights(weights,beta=True,color_palette=cm.plasma):\n", + " if beta:\n", + " cp = color_palette(to_zero_one_beta(weights))\n", + " else:\n", + " cp = color_palette(to_zero_one(weights))\n", + " return cp " + ] + }, + { + "cell_type": "markdown", + "id": "13e770e7", + "metadata": {}, + "source": [ + "### spec_rad" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d31e595", + "metadata": {}, + "outputs": [], + "source": [ + "def spec_rad(M):\n", + " \"\"\"\n", + " Compute the spectral radius of M.\n", + " \"\"\"\n", + " return np.max(np.abs(np.linalg.eigvals(M)))" + ] + }, + { + "cell_type": "markdown", + "id": "8b997a2b", + "metadata": {}, + "source": [ + "### adjacency_matrix_to_graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b8660e3", + "metadata": {}, + "outputs": [], + "source": [ + "def adjacency_matrix_to_graph(A, \n", + " codes,\n", + " tol=0.0): # clip entries below tol\n", + " \"\"\"\n", + " Build a networkx graph object given an adjacency matrix\n", + " \"\"\"\n", + " G = nx.DiGraph()\n", + " N = len(A)\n", + "\n", + " # Add nodes\n", + " for i, code in enumerate(codes):\n", + " G.add_node(code, name=code)\n", + "\n", + " # Add the edges\n", + " for i in range(N):\n", + " for j in range(N):\n", + " a = A[i, j]\n", + " if a > tol:\n", + " G.add_edge(codes[i], codes[j], weight=a)\n", + "\n", + " return G" + ] + }, + { + "cell_type": "markdown", + "id": "e673657f", + "metadata": {}, + "source": [ + "### eigenvector_centrality" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90190208", + "metadata": {}, + "outputs": [], + "source": [ + "def eigenvector_centrality(A, k=40, authority=False):\n", + " \"\"\"\n", + " Computes the dominant eigenvector of A. Assumes A is \n", + " primitive and uses the power method. \n", + " \"\"\"\n", + " A_temp = A.T if authority else A\n", + " n = len(A_temp)\n", + " r = spec_rad(A_temp)\n", + " e = r**(-k) * (np.linalg.matrix_power(A_temp, k) @ np.ones(n))\n", + " return e / np.sum(e)" + ] + }, + { + "cell_type": "markdown", + "id": "78f99a4d", + "metadata": {}, + "source": [ + "### katz_centrality" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "842342f6", + "metadata": {}, + "outputs": [], + "source": [ + "def katz_centrality(A, b=1, authority=False):\n", + " \"\"\"\n", + " Computes the Katz centrality of A, defined as the x solving\n", + "\n", + " x = 1 + b A x (1 = vector of ones)\n", + "\n", + " Assumes that A is square.\n", + "\n", + " If authority=True, then A is replaced by its transpose.\n", + " \"\"\"\n", + " n = len(A)\n", + " I = np.identity(n)\n", + " C = I - b * A.T if authority else I - b * A\n", + " return np.linalg.solve(C, np.ones(n))" + ] + }, + { + "cell_type": "markdown", + "id": "76431864", + "metadata": {}, + "source": [ + "### build_unweighted_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e638e6a", + "metadata": {}, + "outputs": [], + "source": [ + "def build_unweighted_matrix(Z, tol=1e-5):\n", + " \"\"\"\n", + " return a unweighted adjacency matrix\n", + " \"\"\"\n", + " return 1*(Z>tol)" + ] + }, + { + "cell_type": "markdown", + "id": "db7270d9", + "metadata": {}, + "source": [ + "### erdos_renyi_graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a74e9db", + "metadata": {}, + "outputs": [], + "source": [ + "def erdos_renyi_graph(n=100, p=0.5, seed=1234):\n", + " \"Returns an Erdős-Rényi random graph.\"\n", + " \n", + " np.random.seed(seed)\n", + " edges = itertools.combinations(range(n), 2)\n", + " G = nx.Graph()\n", + " \n", + " for e in edges:\n", + " if np.random.rand() < p:\n", + " G.add_edge(*e)\n", + " return G" + ] + }, + { + "cell_type": "markdown", + "id": "02443320", + "metadata": {}, + "source": [ + "### build_coefficient_matrices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9316d715", + "metadata": {}, + "outputs": [], + "source": [ + "def build_coefficient_matrices(Z, X):\n", + " \"\"\"\n", + " Build coefficient matrices A and F from Z and X via \n", + " \n", + " A[i, j] = Z[i, j] / X[j] \n", + " F[i, j] = Z[i, j] / X[i]\n", + " \n", + " \"\"\"\n", + " A, F = np.empty_like(Z), np.empty_like(Z)\n", + " n = A.shape[0]\n", + " for i in range(n):\n", + " for j in range(n):\n", + " A[i, j] = Z[i, j] / X[j]\n", + " F[i, j] = Z[i, j] / X[i]\n", + "\n", + " return A, F" + ] + }, + { + "cell_type": "markdown", + "id": "54010bf3", + "metadata": {}, + "source": [ + "## plotting\n", + "\n", + "### plot_graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4439b087", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_graph(A, \n", + " X,\n", + " ax,\n", + " codes,\n", + " node_color_list=None,\n", + " node_size_multiple=0.0005, \n", + " edge_size_multiple=14,\n", + " layout_type='circular',\n", + " layout_seed=1234,\n", + " tol=0.03): # clip entries below tol\n", + "\n", + " G = nx.DiGraph()\n", + " N = len(A)\n", + "\n", + " # Add nodes, with weights by sales of the sector\n", + " for i, w in enumerate(X):\n", + " G.add_node(codes[i], weight=w, name=codes[i])\n", + "\n", + " node_sizes = X * node_size_multiple\n", + "\n", + " # Position the nodes\n", + " if layout_type == 'circular':\n", + " node_pos_dict = nx.circular_layout(G)\n", + " elif layout_type == 'spring':\n", + " node_pos_dict = nx.spring_layout(G, seed=layout_seed)\n", + " elif layout_type == 'random':\n", + " node_pos_dict = nx.random_layout(G, seed=layout_seed)\n", + " elif layout_type == 'spiral':\n", + " node_pos_dict = nx.spiral_layout(G)\n", + "\n", + " # Add the edges, along with their colors and widths\n", + " edge_colors = []\n", + " edge_widths = []\n", + " for i in range(N):\n", + " for j in range(N):\n", + " a = A[i, j]\n", + " if a > tol:\n", + " G.add_edge(codes[i], codes[j])\n", + " edge_colors.append(node_color_list[i])\n", + " width = a * edge_size_multiple\n", + " edge_widths.append(width)\n", + " \n", + " # Plot the networks\n", + " nx.draw_networkx_nodes(G, \n", + " node_pos_dict, \n", + " node_color=node_color_list, \n", + " node_size=node_sizes, \n", + " edgecolors='grey', \n", + " linewidths=2, \n", + " alpha=0.6, \n", + " ax=ax)\n", + "\n", + " nx.draw_networkx_labels(G, \n", + " node_pos_dict, \n", + " font_size=10, \n", + " ax=ax)\n", + "\n", + " nx.draw_networkx_edges(G, \n", + " node_pos_dict, \n", + " edge_color=edge_colors, \n", + " width=edge_widths, \n", + " arrows=True, \n", + " arrowsize=20, \n", + " alpha=0.6, \n", + " ax=ax, \n", + " arrowstyle='->', \n", + " node_size=node_sizes, \n", + " connectionstyle='arc3,rad=0.15')" + ] + }, + { + "cell_type": "markdown", + "id": "f879578e", + "metadata": {}, + "source": [ + "### plot_matrices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e619a96", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_matrices(matrix,\n", + " codes,\n", + " ax,\n", + " font_size=12,\n", + " alpha=0.6, \n", + " colormap=cm.viridis, \n", + " color45d=None, \n", + " xlabel='sector $j$', \n", + " ylabel='sector $i$'):\n", + " \n", + " ticks = range(len(matrix))\n", + "\n", + " levels = np.sqrt(np.linspace(0, 0.75, 100))\n", + " \n", + " \n", + " if color45d != None:\n", + " co = ax.contourf(ticks, \n", + " ticks,\n", + " matrix,\n", + "# levels,\n", + " alpha=alpha, cmap=colormap)\n", + " ax.plot(ticks, ticks, color=color45d)\n", + " else:\n", + " co = ax.contourf(ticks, \n", + " ticks,\n", + " matrix,\n", + " levels,\n", + " alpha=alpha, cmap=colormap)\n", + "\n", + " #plt.colorbar(co)\n", + "\n", + " ax.set_xlabel(xlabel, fontsize=font_size)\n", + " ax.set_ylabel(ylabel, fontsize=font_size)\n", + " ax.set_yticks(ticks)\n", + " ax.set_yticklabels(codes)\n", + " ax.set_xticks(ticks)\n", + " ax.set_xticklabels(codes)" + ] + }, + { + "cell_type": "markdown", + "id": "88631141", + "metadata": {}, + "source": [ + "### unit_simplex" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f4de6b0", + "metadata": {}, + "outputs": [], + "source": [ + "def unit_simplex(angle):\n", + " \n", + " fig = plt.figure(figsize=(10, 8))\n", + " ax = fig.add_subplot(111, projection='3d')\n", + "\n", + " vtx = [[0, 0, 1],\n", + " [0, 1, 0], \n", + " [1, 0, 0]]\n", + " \n", + " tri = Poly3DCollection([vtx], color='darkblue', alpha=0.3)\n", + " tri.set_facecolor([0.5, 0.5, 1])\n", + " ax.add_collection3d(tri)\n", + "\n", + " ax.set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), \n", + " xticks=(1,), yticks=(1,), zticks=(1,))\n", + "\n", + " ax.set_xticklabels(['$(1, 0, 0)$'], fontsize=16)\n", + " ax.set_yticklabels([f'$(0, 1, 0)$'], fontsize=16)\n", + " ax.set_zticklabels([f'$(0, 0, 1)$'], fontsize=16)\n", + "\n", + " ax.xaxis.majorTicks[0].set_pad(15)\n", + " ax.yaxis.majorTicks[0].set_pad(15)\n", + " ax.zaxis.majorTicks[0].set_pad(35)\n", + "\n", + " ax.view_init(30, angle)\n", + "\n", + " # Move axis to origin\n", + " ax.xaxis._axinfo['juggled'] = (0, 0, 0)\n", + " ax.yaxis._axinfo['juggled'] = (1, 1, 1)\n", + " ax.zaxis._axinfo['juggled'] = (2, 2, 0)\n", + " \n", + " ax.grid(False)\n", + " \n", + " return ax" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/frontpage/LICENSE.txt b/website/LICENSE.txt similarity index 100% rename from frontpage/LICENSE.txt rename to website/LICENSE.txt diff --git a/frontpage/README.txt b/website/README.txt similarity index 100% rename from frontpage/README.txt rename to website/README.txt diff --git a/frontpage/assets/css/fontawesome-all.min.css b/website/assets/css/fontawesome-all.min.css similarity index 100% rename from frontpage/assets/css/fontawesome-all.min.css rename to website/assets/css/fontawesome-all.min.css diff --git a/frontpage/assets/css/main.css b/website/assets/css/main.css similarity index 99% rename from frontpage/assets/css/main.css rename to website/assets/css/main.css index 1ddf66f..c8e02a0 100644 --- a/frontpage/assets/css/main.css +++ b/website/assets/css/main.css @@ -3879,4 +3879,6 @@ input, select, textarea { padding: 4rem; } - } \ No newline at end of file + } + + diff --git a/frontpage/assets/js/breakpoints.min.js b/website/assets/js/breakpoints.min.js similarity index 100% rename from frontpage/assets/js/breakpoints.min.js rename to website/assets/js/breakpoints.min.js diff --git a/frontpage/assets/js/browser.min.js b/website/assets/js/browser.min.js similarity index 100% rename from frontpage/assets/js/browser.min.js rename to website/assets/js/browser.min.js diff --git a/frontpage/assets/js/jquery.min.js b/website/assets/js/jquery.min.js similarity index 100% rename from frontpage/assets/js/jquery.min.js rename to website/assets/js/jquery.min.js diff --git a/frontpage/assets/js/jquery.scrolly.min.js b/website/assets/js/jquery.scrolly.min.js similarity index 100% rename from frontpage/assets/js/jquery.scrolly.min.js rename to website/assets/js/jquery.scrolly.min.js diff --git a/frontpage/assets/js/main.js b/website/assets/js/main.js similarity index 100% rename from frontpage/assets/js/main.js rename to website/assets/js/main.js diff --git a/frontpage/assets/js/util.js b/website/assets/js/util.js similarity index 100% rename from frontpage/assets/js/util.js rename to website/assets/js/util.js diff --git a/frontpage/assets/sass/base/_page.scss b/website/assets/sass/base/_page.scss similarity index 100% rename from frontpage/assets/sass/base/_page.scss rename to website/assets/sass/base/_page.scss diff --git a/frontpage/assets/sass/base/_reset.scss b/website/assets/sass/base/_reset.scss similarity index 100% rename from frontpage/assets/sass/base/_reset.scss rename to website/assets/sass/base/_reset.scss diff --git a/frontpage/assets/sass/base/_typography.scss b/website/assets/sass/base/_typography.scss similarity index 100% rename from frontpage/assets/sass/base/_typography.scss rename to website/assets/sass/base/_typography.scss diff --git a/frontpage/assets/sass/components/_actions.scss b/website/assets/sass/components/_actions.scss similarity index 100% rename from frontpage/assets/sass/components/_actions.scss rename to website/assets/sass/components/_actions.scss diff --git a/frontpage/assets/sass/components/_arrow.scss b/website/assets/sass/components/_arrow.scss similarity index 100% rename from frontpage/assets/sass/components/_arrow.scss rename to website/assets/sass/components/_arrow.scss diff --git a/frontpage/assets/sass/components/_box.scss b/website/assets/sass/components/_box.scss similarity index 100% rename from frontpage/assets/sass/components/_box.scss rename to website/assets/sass/components/_box.scss diff --git a/frontpage/assets/sass/components/_button.scss b/website/assets/sass/components/_button.scss similarity index 100% rename from frontpage/assets/sass/components/_button.scss rename to website/assets/sass/components/_button.scss diff --git a/frontpage/assets/sass/components/_feature-icons.scss b/website/assets/sass/components/_feature-icons.scss similarity index 100% rename from frontpage/assets/sass/components/_feature-icons.scss rename to website/assets/sass/components/_feature-icons.scss diff --git a/frontpage/assets/sass/components/_form.scss b/website/assets/sass/components/_form.scss similarity index 100% rename from frontpage/assets/sass/components/_form.scss rename to website/assets/sass/components/_form.scss diff --git a/frontpage/assets/sass/components/_gallery.scss b/website/assets/sass/components/_gallery.scss similarity index 100% rename from frontpage/assets/sass/components/_gallery.scss rename to website/assets/sass/components/_gallery.scss diff --git a/frontpage/assets/sass/components/_icon.scss b/website/assets/sass/components/_icon.scss similarity index 100% rename from frontpage/assets/sass/components/_icon.scss rename to website/assets/sass/components/_icon.scss diff --git a/frontpage/assets/sass/components/_icons.scss b/website/assets/sass/components/_icons.scss similarity index 100% rename from frontpage/assets/sass/components/_icons.scss rename to website/assets/sass/components/_icons.scss diff --git a/frontpage/assets/sass/components/_image.scss b/website/assets/sass/components/_image.scss similarity index 100% rename from frontpage/assets/sass/components/_image.scss rename to website/assets/sass/components/_image.scss diff --git a/frontpage/assets/sass/components/_list.scss b/website/assets/sass/components/_list.scss similarity index 100% rename from frontpage/assets/sass/components/_list.scss rename to website/assets/sass/components/_list.scss diff --git a/frontpage/assets/sass/components/_row.scss b/website/assets/sass/components/_row.scss similarity index 100% rename from frontpage/assets/sass/components/_row.scss rename to website/assets/sass/components/_row.scss diff --git a/frontpage/assets/sass/components/_table.scss b/website/assets/sass/components/_table.scss similarity index 100% rename from frontpage/assets/sass/components/_table.scss rename to website/assets/sass/components/_table.scss diff --git a/frontpage/assets/sass/layout/_wrapper.scss b/website/assets/sass/layout/_wrapper.scss similarity index 100% rename from frontpage/assets/sass/layout/_wrapper.scss rename to website/assets/sass/layout/_wrapper.scss diff --git a/frontpage/assets/sass/libs/_breakpoints.scss b/website/assets/sass/libs/_breakpoints.scss similarity index 100% rename from frontpage/assets/sass/libs/_breakpoints.scss rename to website/assets/sass/libs/_breakpoints.scss diff --git a/frontpage/assets/sass/libs/_functions.scss b/website/assets/sass/libs/_functions.scss similarity index 100% rename from frontpage/assets/sass/libs/_functions.scss rename to website/assets/sass/libs/_functions.scss diff --git a/frontpage/assets/sass/libs/_html-grid.scss b/website/assets/sass/libs/_html-grid.scss similarity index 100% rename from frontpage/assets/sass/libs/_html-grid.scss rename to website/assets/sass/libs/_html-grid.scss diff --git a/frontpage/assets/sass/libs/_mixins.scss b/website/assets/sass/libs/_mixins.scss similarity index 100% rename from frontpage/assets/sass/libs/_mixins.scss rename to website/assets/sass/libs/_mixins.scss diff --git a/frontpage/assets/sass/libs/_vars.scss b/website/assets/sass/libs/_vars.scss similarity index 100% rename from frontpage/assets/sass/libs/_vars.scss rename to website/assets/sass/libs/_vars.scss diff --git a/frontpage/assets/sass/libs/_vendor.scss b/website/assets/sass/libs/_vendor.scss similarity index 100% rename from frontpage/assets/sass/libs/_vendor.scss rename to website/assets/sass/libs/_vendor.scss diff --git a/frontpage/assets/sass/main.scss b/website/assets/sass/main.scss similarity index 100% rename from frontpage/assets/sass/main.scss rename to website/assets/sass/main.scss diff --git a/frontpage/assets/webfonts/fa-brands-400.eot b/website/assets/webfonts/fa-brands-400.eot similarity index 100% rename from frontpage/assets/webfonts/fa-brands-400.eot rename to website/assets/webfonts/fa-brands-400.eot diff --git a/frontpage/assets/webfonts/fa-brands-400.svg b/website/assets/webfonts/fa-brands-400.svg similarity index 100% rename from frontpage/assets/webfonts/fa-brands-400.svg rename to website/assets/webfonts/fa-brands-400.svg diff --git a/frontpage/assets/webfonts/fa-brands-400.ttf b/website/assets/webfonts/fa-brands-400.ttf similarity index 100% rename from frontpage/assets/webfonts/fa-brands-400.ttf rename to website/assets/webfonts/fa-brands-400.ttf diff --git a/frontpage/assets/webfonts/fa-brands-400.woff b/website/assets/webfonts/fa-brands-400.woff similarity index 100% rename from frontpage/assets/webfonts/fa-brands-400.woff rename to website/assets/webfonts/fa-brands-400.woff diff --git a/frontpage/assets/webfonts/fa-brands-400.woff2 b/website/assets/webfonts/fa-brands-400.woff2 similarity index 100% rename from frontpage/assets/webfonts/fa-brands-400.woff2 rename to website/assets/webfonts/fa-brands-400.woff2 diff --git a/frontpage/assets/webfonts/fa-regular-400.eot b/website/assets/webfonts/fa-regular-400.eot similarity index 100% rename from frontpage/assets/webfonts/fa-regular-400.eot rename to website/assets/webfonts/fa-regular-400.eot diff --git a/frontpage/assets/webfonts/fa-regular-400.svg b/website/assets/webfonts/fa-regular-400.svg similarity index 100% rename from frontpage/assets/webfonts/fa-regular-400.svg rename to website/assets/webfonts/fa-regular-400.svg diff --git a/frontpage/assets/webfonts/fa-regular-400.ttf b/website/assets/webfonts/fa-regular-400.ttf similarity index 100% rename from frontpage/assets/webfonts/fa-regular-400.ttf rename to website/assets/webfonts/fa-regular-400.ttf diff --git a/frontpage/assets/webfonts/fa-regular-400.woff b/website/assets/webfonts/fa-regular-400.woff similarity index 100% rename from frontpage/assets/webfonts/fa-regular-400.woff rename to website/assets/webfonts/fa-regular-400.woff diff --git a/frontpage/assets/webfonts/fa-regular-400.woff2 b/website/assets/webfonts/fa-regular-400.woff2 similarity index 100% rename from frontpage/assets/webfonts/fa-regular-400.woff2 rename to website/assets/webfonts/fa-regular-400.woff2 diff --git a/frontpage/assets/webfonts/fa-solid-900.eot b/website/assets/webfonts/fa-solid-900.eot similarity index 100% rename from frontpage/assets/webfonts/fa-solid-900.eot rename to website/assets/webfonts/fa-solid-900.eot diff --git a/frontpage/assets/webfonts/fa-solid-900.svg b/website/assets/webfonts/fa-solid-900.svg similarity index 100% rename from frontpage/assets/webfonts/fa-solid-900.svg rename to website/assets/webfonts/fa-solid-900.svg diff --git a/frontpage/assets/webfonts/fa-solid-900.ttf b/website/assets/webfonts/fa-solid-900.ttf similarity index 100% rename from frontpage/assets/webfonts/fa-solid-900.ttf rename to website/assets/webfonts/fa-solid-900.ttf diff --git a/frontpage/assets/webfonts/fa-solid-900.woff b/website/assets/webfonts/fa-solid-900.woff similarity index 100% rename from frontpage/assets/webfonts/fa-solid-900.woff rename to website/assets/webfonts/fa-solid-900.woff diff --git a/frontpage/assets/webfonts/fa-solid-900.woff2 b/website/assets/webfonts/fa-solid-900.woff2 similarity index 100% rename from frontpage/assets/webfonts/fa-solid-900.woff2 rename to website/assets/webfonts/fa-solid-900.woff2 diff --git a/frontpage/images/gallery/fulls/01.jpg b/website/images/gallery/fulls/01.jpg similarity index 100% rename from frontpage/images/gallery/fulls/01.jpg rename to website/images/gallery/fulls/01.jpg diff --git a/frontpage/images/gallery/fulls/02.jpg b/website/images/gallery/fulls/02.jpg similarity index 100% rename from frontpage/images/gallery/fulls/02.jpg rename to website/images/gallery/fulls/02.jpg diff --git a/frontpage/images/gallery/fulls/03.jpg b/website/images/gallery/fulls/03.jpg similarity index 100% rename from frontpage/images/gallery/fulls/03.jpg rename to website/images/gallery/fulls/03.jpg diff --git a/frontpage/images/gallery/fulls/04.jpg b/website/images/gallery/fulls/04.jpg similarity index 100% rename from frontpage/images/gallery/fulls/04.jpg rename to website/images/gallery/fulls/04.jpg diff --git a/frontpage/images/gallery/fulls/05.jpg b/website/images/gallery/fulls/05.jpg similarity index 100% rename from frontpage/images/gallery/fulls/05.jpg rename to website/images/gallery/fulls/05.jpg diff --git a/frontpage/images/gallery/fulls/06.jpg b/website/images/gallery/fulls/06.jpg similarity index 100% rename from frontpage/images/gallery/fulls/06.jpg rename to website/images/gallery/fulls/06.jpg diff --git a/frontpage/images/gallery/fulls/07.jpg b/website/images/gallery/fulls/07.jpg similarity index 100% rename from frontpage/images/gallery/fulls/07.jpg rename to website/images/gallery/fulls/07.jpg diff --git a/frontpage/images/gallery/fulls/08.jpg b/website/images/gallery/fulls/08.jpg similarity index 100% rename from frontpage/images/gallery/fulls/08.jpg rename to website/images/gallery/fulls/08.jpg diff --git a/frontpage/images/gallery/fulls/09.jpg b/website/images/gallery/fulls/09.jpg similarity index 100% rename from frontpage/images/gallery/fulls/09.jpg rename to website/images/gallery/fulls/09.jpg diff --git a/frontpage/images/gallery/fulls/10.jpg b/website/images/gallery/fulls/10.jpg similarity index 100% rename from frontpage/images/gallery/fulls/10.jpg rename to website/images/gallery/fulls/10.jpg diff --git a/frontpage/images/gallery/thumbs/01.jpg b/website/images/gallery/thumbs/01.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/01.jpg rename to website/images/gallery/thumbs/01.jpg diff --git a/frontpage/images/gallery/thumbs/02.jpg b/website/images/gallery/thumbs/02.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/02.jpg rename to website/images/gallery/thumbs/02.jpg diff --git a/frontpage/images/gallery/thumbs/03.jpg b/website/images/gallery/thumbs/03.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/03.jpg rename to website/images/gallery/thumbs/03.jpg diff --git a/frontpage/images/gallery/thumbs/04.jpg b/website/images/gallery/thumbs/04.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/04.jpg rename to website/images/gallery/thumbs/04.jpg diff --git a/frontpage/images/gallery/thumbs/05.jpg b/website/images/gallery/thumbs/05.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/05.jpg rename to website/images/gallery/thumbs/05.jpg diff --git a/frontpage/images/gallery/thumbs/06.jpg b/website/images/gallery/thumbs/06.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/06.jpg rename to website/images/gallery/thumbs/06.jpg diff --git a/frontpage/images/gallery/thumbs/07.jpg b/website/images/gallery/thumbs/07.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/07.jpg rename to website/images/gallery/thumbs/07.jpg diff --git a/frontpage/images/gallery/thumbs/08.jpg b/website/images/gallery/thumbs/08.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/08.jpg rename to website/images/gallery/thumbs/08.jpg diff --git a/frontpage/images/gallery/thumbs/09.jpg b/website/images/gallery/thumbs/09.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/09.jpg rename to website/images/gallery/thumbs/09.jpg diff --git a/frontpage/images/gallery/thumbs/10.jpg b/website/images/gallery/thumbs/10.jpg similarity index 100% rename from frontpage/images/gallery/thumbs/10.jpg rename to website/images/gallery/thumbs/10.jpg diff --git a/frontpage/images/github-mark.png b/website/images/github-mark.png similarity index 100% rename from frontpage/images/github-mark.png rename to website/images/github-mark.png diff --git a/frontpage/images/logo-square.svg b/website/images/logo-square.svg similarity index 100% rename from frontpage/images/logo-square.svg rename to website/images/logo-square.svg diff --git a/frontpage/images/mail.png b/website/images/mail.png similarity index 100% rename from frontpage/images/mail.png rename to website/images/mail.png diff --git a/frontpage/images/network3.jpg b/website/images/network3.jpg similarity index 100% rename from frontpage/images/network3.jpg rename to website/images/network3.jpg diff --git a/frontpage/images/pic01.jpg b/website/images/pic01.jpg similarity index 100% rename from frontpage/images/pic01.jpg rename to website/images/pic01.jpg diff --git a/frontpage/images/pic02.jpg b/website/images/pic02.jpg similarity index 100% rename from frontpage/images/pic02.jpg rename to website/images/pic02.jpg diff --git a/frontpage/images/qe-logo-small.png b/website/images/qe-logo-small.png similarity index 100% rename from frontpage/images/qe-logo-small.png rename to website/images/qe-logo-small.png diff --git a/frontpage/index.html b/website/index.html similarity index 88% rename from frontpage/index.html rename to website/index.html index 7c61998..1c82b78 100644 --- a/frontpage/index.html +++ b/website/index.html @@ -24,8 +24,8 @@

Economic Networks

Theory and Computation

-

Download the textbook here -
你可以点击 这里 下载电子版教材 +

Download the textbook here +
你可以点击 这里 下载电子版教材

  • Next
  • @@ -92,7 +92,7 @@

    Links

    • - Code Book + Notebooks
    • GitHub diff --git a/website/notebooks.html b/website/notebooks.html new file mode 100644 index 0000000..c70638d --- /dev/null +++ b/website/notebooks.html @@ -0,0 +1,85 @@ + + + + + + Notebooks + + + + + + + + + + + +
      + +
      + + + + +
      + + + +
      +
      Design: HTML5 UP +
      +
      + +
      + + + + + + + + + + + + \ No newline at end of file

Thomas J. Sargent and John Stachurski