diff --git a/microdf/__init__.py b/microdf/__init__.py
index 993a7dd..5033ad2 100644
--- a/microdf/__init__.py
+++ b/microdf/__init__.py
@@ -6,6 +6,7 @@ from .custom_taxes import *
 from .income_measures import *
 from .inequality import *
 from .style import *
+from .tax import *
 from .ubi import *
 from .utils import *
 
diff --git a/microdf/tax.py b/microdf/tax.py
new file mode 100644
index 0000000..5979a58
--- /dev/null
+++ b/microdf/tax.py
@@ -0,0 +1,21 @@
+import pandas as pd
+
+
+def tax_from_mtrs(val, brackets, rates):
+    # Calculates tax liability based on a marginal tax rate schedule.
+    #
+    # Args:
+    #     val: Value to assess tax on, e.g. wealth or income (list or Series).
+    #     brackets: Left side of each bracket (list or Series).
+    #     rates: Rate corresponding to each bracket.
+    #
+    # Returns:
+    #     Series of tax liabilities with the same size as val.
+    df_tax = pd.DataFrame({'brackets': brackets, 'rates': rates})
+    df_tax['base_tax'] = df_tax.brackets.\
+        sub(df_tax.brackets.shift(fill_value=0)).\
+        mul(df_tax.rates.shift(fill_value=0)).cumsum()
+    rows = df_tax.brackets.searchsorted(val, side='right') - 1
+    income_bracket_df = df_tax.loc[rows].reset_index(drop=True)
+    return pd.Series(val).sub(income_bracket_df.brackets).\
+        mul(income_bracket_df.rates).add(income_bracket_df.base_tax)

Add function to calculate tax from a MTR schedule
Basic tax calculation:
```
def tax_from_mtrs(val, brackets, rates):
    # Args:
    #     val: Value to assess tax on, e.g. wealth or income (list or Series).
    #     brackets: Left side of each bracket (list or Series).
    #     rates: Rate corresponding to each bracket.
    df_tax = pd.DataFrame({'brackets': brackets, 'rates': rates})
    df_tax['base_tax'] = df_tax.brackets.\
        sub(df_tax.brackets.shift(fill_value=0)).\
        mul(df_tax.rates.shift(fill_value=0)).cumsum()
    rows = df_tax.brackets.searchsorted(val, side='right') - 1
    income_bracket_df = df_tax.loc[rows].reset_index(drop=True)
    return pd.Series(val).sub(income_bracket_df.brackets).\
        mul(income_bracket_df.rates).add(income_bracket_df.base_tax)
```
diff --git a/microdf/tests/test_tax.py b/microdf/tests/test_tax.py
new file mode 100644
index 0000000..f4cec43
--- /dev/null
+++ b/microdf/tests/test_tax.py
@@ -0,0 +1,13 @@
+import pytest
+import pandas as pd
+import microdf as mdf
+
+
+def test_tax():
+    # Consider a MTR schedule of 0% up to 10,000, then 10% after that.
+    BRACKETS = [0, 10e3]
+    RATES = [0, 0.1]
+    INCOME = [0, 5e3, 10e3, 10e3 + 1, 20e3]
+    EXPECTED = [0, 0, 0, 0.1, 1e3]
+    res = mdf.tax_from_mtrs(INCOME, BRACKETS, RATES)
+    pd.testing.assert_series_equal(res, pd.Series(EXPECTED))

environment.yml
pip install -e .[taxcalc]
parse_log_pytest
environment.yml
pytest
apt-get update
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohttp @ file:///home/conda/feedstock_root/build_artifacts/aiohttp_1649013154501/work
aiosignal @ file:///home/conda/feedstock_root/build_artifacts/aiosignal_1734342155601/work
async-timeout @ file:///home/conda/feedstock_root/build_artifacts/async-timeout_1691763562544/work
attrs @ file:///home/conda/feedstock_root/build_artifacts/attrs_1741918516150/work
bokeh @ file:///home/conda/feedstock_root/build_artifacts/bokeh_1706215790147/work
Brotli @ file:///home/conda/feedstock_root/build_artifacts/brotli-split_1648883617327/work
certifi @ file:///home/conda/feedstock_root/build_artifacts/certifi_1739515848642/work/certifi
cffi @ file:///home/conda/feedstock_root/build_artifacts/cffi_1636046055389/work
charset-normalizer @ file:///home/conda/feedstock_root/build_artifacts/charset-normalizer_1661170624537/work
colorama @ file:///home/conda/feedstock_root/build_artifacts/colorama_1733218098505/work
contourpy @ file:///home/conda/feedstock_root/build_artifacts/contourpy_1652954513473/work
cycler @ file:///home/conda/feedstock_root/build_artifacts/cycler_1733332471406/work
exceptiongroup @ file:///home/conda/feedstock_root/build_artifacts/exceptiongroup_1733208806608/work
fonttools @ file:///home/conda/feedstock_root/build_artifacts/fonttools_1651017733844/work
frozenlist @ file:///home/conda/feedstock_root/build_artifacts/frozenlist_1648771673823/work
fsspec @ file:///home/conda/feedstock_root/build_artifacts/fsspec_1743361113926/work
h2 @ file:///home/conda/feedstock_root/build_artifacts/h2_1738578511449/work
hpack @ file:///home/conda/feedstock_root/build_artifacts/hpack_1737618293087/work
hyperframe @ file:///home/conda/feedstock_root/build_artifacts/hyperframe_1737618333194/work
idna @ file:///home/conda/feedstock_root/build_artifacts/idna_1733211830134/work
importlib_resources @ file:///home/conda/feedstock_root/build_artifacts/importlib_resources_1736252299705/work
iniconfig @ file:///home/conda/feedstock_root/build_artifacts/iniconfig_1733223141826/work
Jinja2 @ file:///home/conda/feedstock_root/build_artifacts/jinja2_1741263328855/work
kiwisolver @ file:///home/conda/feedstock_root/build_artifacts/kiwisolver_1648854392795/work
llvmlite @ file:///croot/llvmlite_1736366675558/work
MarkupSafe @ file:///home/conda/feedstock_root/build_artifacts/markupsafe_1648737556467/work
marshmallow @ file:///home/conda/feedstock_root/build_artifacts/marshmallow_1738612362469/work
matplotlib @ file:///croot/matplotlib-suite_1713336378214/work
matplotlib-label-lines==0.8.0
-e git+https://github.com/MaxGhenis/microdf.git@2e3330a96328a0ac293771bc1b46349689a02e00#egg=microdf
more-itertools==10.6.0
multidict @ file:///home/conda/feedstock_root/build_artifacts/multidict_1648882420423/work
munkres==1.1.4
numba @ file:///croot/numba_1738606613869/work
numpy @ file:///home/conda/feedstock_root/build_artifacts/numpy_1651020388495/work
olefile @ file:///home/conda/feedstock_root/build_artifacts/olefile_1734769783178/work
packaging @ file:///home/conda/feedstock_root/build_artifacts/packaging_1733203243479/work
pandas==1.4.2
paramtools @ file:///home/conda/feedstock_root/build_artifacts/paramtools_1736456544755/work
patsy @ file:///home/conda/feedstock_root/build_artifacts/patsy_1733792384640/work
Pillow @ file:///home/conda/feedstock_root/build_artifacts/pillow_1630696601414/work
pluggy @ file:///home/conda/feedstock_root/build_artifacts/pluggy_1733222765875/work
pycparser @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_pycparser_1733195786/work
pyparsing @ file:///home/conda/feedstock_root/build_artifacts/pyparsing_1652235407899/work
PyQt5==5.15.4
PyQt5-sip==12.9.0
PySocks @ file:///home/conda/feedstock_root/build_artifacts/pysocks_1733217236728/work
pytest @ file:///home/conda/feedstock_root/build_artifacts/pytest_1740946542080/work
python-dateutil @ file:///home/conda/feedstock_root/build_artifacts/python-dateutil_1733215673016/work
pytz @ file:///home/conda/feedstock_root/build_artifacts/pytz_1742920838005/work
PyYAML @ file:///home/conda/feedstock_root/build_artifacts/pyyaml_1648757097602/work
requests @ file:///home/conda/feedstock_root/build_artifacts/requests_1733217035951/work
scipy @ file:///home/conda/feedstock_root/build_artifacts/scipy_1653073865807/work
seaborn @ file:///home/conda/feedstock_root/build_artifacts/seaborn-split_1733730015268/work
sip @ file:///home/conda/feedstock_root/build_artifacts/sip_1646101340685/work
six @ file:///home/conda/feedstock_root/build_artifacts/six_1733380938961/work
sortedcontainers @ file:///home/conda/feedstock_root/build_artifacts/sortedcontainers_1738440353519/work
statsmodels @ file:///home/conda/feedstock_root/build_artifacts/statsmodels_1644535581977/work
taxcalc @ file:///home/conda/feedstock_root/build_artifacts/taxcalc_1727883798934/work
toml @ file:///home/conda/feedstock_root/build_artifacts/toml_1734091811753/work
tomli @ file:///home/conda/feedstock_root/build_artifacts/tomli_1733256695513/work
tornado @ file:///home/conda/feedstock_root/build_artifacts/tornado_1648827245914/work
typing_extensions @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_typing_extensions_1743201626/work
unicodedata2 @ file:///home/conda/feedstock_root/build_artifacts/unicodedata2_1649111919389/work
urllib3 @ file:///home/conda/feedstock_root/build_artifacts/urllib3_1734859416348/work
xyzservices @ file:///home/conda/feedstock_root/build_artifacts/xyzservices_1737234886776/work
yarl @ file:///home/conda/feedstock_root/build_artifacts/yarl_1648966524636/work
zipp @ file:///home/conda/feedstock_root/build_artifacts/zipp_1732827521216/work
zstandard @ file:///croot/zstandard_1731356346222/work

name: microdf
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - aiohttp=3.8.1=py39hb9d737c_1
  - aiosignal=1.3.2=pyhd8ed1ab_0
  - async-timeout=4.0.3=pyhd8ed1ab_0
  - attrs=25.3.0=pyh71513ae_0
  - bokeh=3.3.4=pyhd8ed1ab_0
  - brotli=1.0.9=h166bdaf_7
  - brotli-bin=1.0.9=h166bdaf_7
  - brotli-python=1.0.9=py39h5a03fae_7
  - bzip2=1.0.8=h7f98852_4
  - c-ares=1.19.1=h5eee18b_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2025.1.31=pyhd8ed1ab_0
  - cffi=1.15.0=py39h4bc2ebd_0
  - charset-normalizer=2.1.1=pyhd8ed1ab_0
  - colorama=0.4.6=pyhd8ed1ab_1
  - contourpy=1.0.2=py39hf939315_2
  - cycler=0.12.1=pyhd8ed1ab_1
  - cyrus-sasl=2.1.28=h52b45da_1
  - dbus=1.13.18=hb2f20db_0
  - exceptiongroup=1.2.2=pyhd8ed1ab_1
  - expat=2.6.4=h6a678d5_0
  - fontconfig=2.14.1=h55d465d_3
  - fonttools=4.33.3=py39hb9d737c_0
  - freetype=2.10.4=h0708190_1
  - frozenlist=1.3.0=py39hb9d737c_1
  - fsspec=2025.3.1=pyhd8ed1ab_0
  - glib=2.78.4=h6a678d5_0
  - glib-tools=2.78.4=h6a678d5_0
  - gst-plugins-base=1.14.1=h6a678d5_1
  - gstreamer=1.14.1=h5eee18b_1
  - h2=4.2.0=pyhd8ed1ab_0
  - hpack=4.1.0=pyhd8ed1ab_0
  - hyperframe=6.1.0=pyhd8ed1ab_0
  - icu=73.1=h6a678d5_0
  - idna=3.10=pyhd8ed1ab_1
  - importlib_resources=6.5.2=pyhd8ed1ab_0
  - iniconfig=2.0.0=pyhd8ed1ab_1
  - jbig=2.1=h7f98852_2003
  - jinja2=3.1.6=pyhd8ed1ab_0
  - jpeg=9e=h166bdaf_1
  - kiwisolver=1.4.2=py39hf939315_1
  - krb5=1.20.1=h143b758_1
  - lcms2=2.12=hddcbb42_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - lerc=2.2.1=h9c3ff4c_0
  - libabseil=20250127.0=cxx17_h6a678d5_0
  - libblas=3.9.0=16_linux64_openblas
  - libbrotlicommon=1.0.9=h166bdaf_7
  - libbrotlidec=1.0.9=h166bdaf_7
  - libbrotlienc=1.0.9=h166bdaf_7
  - libcblas=3.9.0=16_linux64_openblas
  - libclang=14.0.6=default_hc6dbbc7_2
  - libclang13=14.0.6=default_he11475f_2
  - libcups=2.4.2=h2d74bed_1
  - libcurl=8.12.1=hc9e6f67_0
  - libdeflate=1.7=h7f98852_5
  - libedit=3.1.20230828=h5eee18b_0
  - libev=4.33=h516909a_1
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgfortran-ng=13.2.0=h69a702a_0
  - libgfortran5=13.2.0=ha4646dd_0
  - libglib=2.78.4=hdc74915_0
  - libgomp=11.2.0=h1234567_1
  - libiconv=1.17=h166bdaf_0
  - liblapack=3.9.0=16_linux64_openblas
  - libllvm14=14.0.6=hecde1de_4
  - libnghttp2=1.57.0=h2d74bed_0
  - libopenblas=0.3.21=h043d6bf_0
  - libpng=1.6.39=h5eee18b_0
  - libpq=17.4=hdbd6064_0
  - libprotobuf=5.29.3=hc99497a_0
  - libssh2=1.11.1=h251f7ec_0
  - libstdcxx-ng=11.2.0=h1234567_1
  - libtiff=4.3.0=hf544144_1
  - libuuid=1.41.5=h5eee18b_0
  - libwebp-base=1.2.2=h7f98852_1
  - libxcb=1.15=h7f8727e_0
  - libxkbcommon=1.0.3=he3ba5ed_0
  - libxml2=2.13.5=hfdd30dd_0
  - llvmlite=0.43.0=py39h6a678d5_1
  - lz4-c=1.9.4=h6a678d5_1
  - markupsafe=2.1.1=py39hb9d737c_1
  - marshmallow=3.26.1=pyhd8ed1ab_0
  - matplotlib=3.8.4=py39hf3d152e_2
  - matplotlib-base=3.8.4=py39h1128e8f_0
  - multidict=6.0.2=py39hb9d737c_1
  - munkres=1.1.4=pyh9f0ad1d_0
  - mysql=8.4.0=h721767e_2
  - ncurses=6.4=h6a678d5_0
  - numba=0.60.0=py39h6a678d5_1
  - numpy=1.22.3=py39hc58783e_2
  - olefile=0.47=pyhd8ed1ab_1
  - openjpeg=2.4.0=hb52868f_1
  - openldap=2.6.4=h42fbc30_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=pyhd8ed1ab_2
  - pandas=1.4.2=py39h1832856_1
  - paramtools=0.19.0=pyhd8ed1ab_1
  - patsy=1.0.1=pyhd8ed1ab_1
  - pcre2=10.42=hebb0a14_1
  - pillow=8.3.2=py39ha612740_0
  - pip=25.0.1=pyh8b19718_0
  - pluggy=1.5.0=pyhd8ed1ab_1
  - pycparser=2.22=pyh29332c3_1
  - pyparsing=3.0.9=pyhd8ed1ab_0
  - pyqt=5.15.4=py39h5a03fae_0
  - pyqt5-sip=12.9.0=py39h5a03fae_0
  - pysocks=1.7.1=pyha55dd90_7
  - pytest=8.3.5=pyhd8ed1ab_0
  - python=3.9.21=he870216_1
  - python-dateutil=2.9.0.post0=pyhff2d567_1
  - python_abi=3.9=2_cp39
  - pytz=2025.2=pyhd8ed1ab_0
  - pyyaml=6.0=py39hb9d737c_4
  - qt-main=5.15.2=hb6262e9_12
  - readline=8.2=h5eee18b_0
  - requests=2.32.3=pyhd8ed1ab_1
  - scipy=1.8.1=py39he49c0e8_0
  - seaborn=0.13.2=hd8ed1ab_3
  - seaborn-base=0.13.2=pyhd8ed1ab_3
  - setuptools=75.8.0=py39h06a4308_0
  - sip=6.5.1=py39he80948d_2
  - six=1.17.0=pyhd8ed1ab_0
  - sortedcontainers=2.4.0=pyhd8ed1ab_1
  - sqlite=3.45.3=h5eee18b_0
  - statsmodels=0.13.2=py39hce5d2b2_0
  - taxcalc=4.3.0=pyhd8ed1ab_0
  - tbb=2021.8.0=hdb19cb5_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd8ed1ab_1
  - tomli=2.2.1=pyhd8ed1ab_1
  - tornado=6.1=py39hb9d737c_3
  - typing-extensions=4.13.0=h9fa5a19_1
  - typing_extensions=4.13.0=pyh29332c3_1
  - tzdata=2025a=h04d1e81_0
  - unicodedata2=14.0.0=py39hb9d737c_1
  - urllib3=2.3.0=pyhd8ed1ab_0
  - wheel=0.45.1=py39h06a4308_0
  - xyzservices=2025.1.0=pyhd8ed1ab_0
  - xz=5.6.4=h5eee18b_1
  - yaml=0.2.5=h7f98852_2
  - yarl=1.7.2=py39hb9d737c_2
  - zipp=3.21.0=pyhd8ed1ab_1
  - zlib=1.2.13=h5eee18b_1
  - zstandard=0.23.0=py39h2c38b39_1
  - zstd=1.5.6=hc292b87_0
  - pip:
      - matplotlib-label-lines==0.8.0
      - more-itertools==10.6.0
prefix: /opt/conda/envs/microdf

microdf/tests/test_tax.py::test_tax
microdf/tax.py microdf/__init__.py
microdf/tax.py microdf/__init__.py