This vignette introduces the PTAXSIM package, including its: main function, primary inputs, basic abilities, and common uses. Advanced use cases are described in separate vignettes. Familiarity with the Cook County property tax system is useful, but not essential to follow this vignette.
In this vignette, we will:
- Start with the basics - what is PTAXSIM, its main function, the main function’s inputs;
- Then we will look at common scenarios - calculating a bill after a change in assessed value, getting the change in a bill’s proportions over time, etc.
1. Basics
What is PTAXSIM?
PTAXSIM is a software package to estimate Cook County property tax bills. It has two parts:
- A database file containing the historical data (>= 2006) necessary to estimate individual property tax bills
- A highly-optimized R package that utilizes the database to quickly and efficiently estimate tax bills
By default, PTAXSIM uses the data in the database to estimate historical tax bills. However, you can also provide counterfactual data in order to generate counterfactual bills.
Note that:
The R package cannot function without the separate database, as its functions query the database directly.
PTAXSIM’s output bills are estimates which may not be identical to real bills. This is due to differences in rounding, math, and data between PTAXSIM and the real tax calculation program.
All of PTAXSIM’s inputs and outputs are
data.table
s to facilitate efficient operation chaining and/or by-reference column updates. This enables fast and memory-efficient calculations, which are needed to perform large-scale counterfactual simulations.-
You must instantiate a database connection object with the name
ptaxsim_db_conn
before using PTAXSIM. Here is an example connection:
The main function - tax_bill()
PTAXSIM has a single primary function - tax_bill()
-
with two required arguments:
-
year_vec
- A numeric vector of tax years -
pin_vec
- A character vector of 14-digit Property Index Numbers (PINs), with no dashes or spaces
The output is a data.table
containing the tax amount
directed to each taxing district, by PIN and year. Let’s look at an
example using just one property and year:
bill <- tax_bill(year_vec = 2021, pin_vec = "13264290020000")
print(bill)
#> Key: <year, pin, agency_num>
#> year pin class tax_code av eav agency_num
#> <int> <char> <char> <char> <int> <int> <char>
#> 1: 2021 13264290020000 211 71163 62464 187561 010010000
#> 2: 2021 13264290020000 211 71163 62464 187561 010020000
#> 3: 2021 13264290020000 211 71163 62464 187561 030210000
#> 4: 2021 13264290020000 211 71163 62464 187561 030210001
#> 5: 2021 13264290020000 211 71163 62464 187561 030210002
#> 6: 2021 13264290020000 211 71163 62464 187561 043030000
#> 7: 2021 13264290020000 211 71163 62464 187561 044060000
#> 8: 2021 13264290020000 211 71163 62464 187561 050200000
#> 9: 2021 13264290020000 211 71163 62464 187561 050200001
#> 10: 2021 13264290020000 211 71163 62464 187561 080180000
#> 11: 2021 13264290020000 211 71163 62464 187561 140030000
#> agency_name agency_major_type agency_minor_type
#> <char> <char> <char>
#> 1: COUNTY OF COOK COOK COUNTY COOK
#> 2: FOREST PRESERVE DISTRICT ... COOK COUNTY COOK
#> 3: CITY OF CHICAGO MUNICIPALITY/TOWNSHIP MUNI
#> 4: CITY OF CHICAGO LIBRARY F... MUNICIPALITY/TOWNSHIP LIBRARY
#> 5: CITY OF CHICAGO SCHOOL BL... MUNICIPALITY/TOWNSHIP MISC
#> 6: CHICAGO COMMUNITY COLLEGE... SCHOOL COMM COLL
#> 7: BOARD OF EDUCATION SCHOOL UNIFIED
#> 8: CHICAGO PARK DISTRICT MISCELLANEOUS PARK
#> 9: CHICAGO PARK DISTRICT AQU... MISCELLANEOUS BOND
#> 10: METRO WATER RECLAMATION D... MISCELLANEOUS WATER
#> 11: LOGAN AVONDALE HERMOSA EX... MISCELLANEOUS HEALTH
#> agency_tax_rate final_tax
#> <num> <num>
#> 1: 0.00446 836.52
#> 2: 0.00058 108.79
#> 3: 0.01556 2918.45
#> 4: 0.00129 241.95
#> 5: 0.00153 286.97
#> 6: 0.00145 271.96
#> 7: 0.03517 6596.52
#> 8: 0.00311 583.31
#> 9: 0.00000 0.00
#> 10: 0.00382 716.48
#> 11: 0.00022 41.26
Output columns
Column Name | Description | Possible Valus |
---|---|---|
year |
Tax year of the bill. Note that bills are paid in arrears, so a 2021 bill is paid in 2022 | Must be >= 2006 |
pin |
Property Index Number of the property. This is the unique identifier of each parcel in Cook County | |
class |
The 3-digit Assessor minor class code. This identifies the property type/structure | See class dictionary |
tax_code |
The 5-digit tax code. Each tax code identifies the unique
combination of taxing districts that a PIN is within. Note that the
first 2 digits of tax_code are the Assessor township
code |
|
av |
Assessed value of the property. By default, this is the finalized AV after all appeals have been completed. See optional arguments below for more details | Must be >= 0 |
eav |
The av multiplied by the state equalization factor |
Must be >= 0 |
agency_num |
Unique numeric identifier for each taxing body/district. Each
pin is typically taxed by multiple taxing districts.
tax_bill() will output 1 row for each district for each PIN
and year. The PIN above is being taxed by 11 districts |
|
agency_name |
Name of the taxing district | |
agency_major_type |
Type of taxing district. Similar to the grouping used on real tax bills | COOK COUNTY, MISCELLANEOUS, MUNICIPALITY/TOWNSHIP, SCHOOL |
agency_minor_type |
Sub-type of taxing district. Much more fine-grained | BOND, COOK, COMM COLL, ELEMENTARY, FIRE, GEN ASST, HEALTH, INFRA, LIBRARY, MISC, MOSQUITO, MUNI, PARK, POLICE, SANITARY, SECONDARY, SSA, TIF, TOWNSHIP, UNIFIED, WATER |
agency_tax_rate |
The tax rate for the PIN and district, which is calculated from each district’s levy and total tax base | |
final_tax |
The final tax (in dollars) the PIN owes to the taxing district.
Equal to agency_tax_rate * eav in cases without
exemptions |
Multiple PINs or years
The tax_bill()
function can take multiple years in the
year_vec
argument:
bills <- tax_bill(2010:2021, "13264290020000")
print(bills, topn = 3)
#> Key: <year, pin, agency_num>
#> year pin class tax_code av eav agency_num
#> <int> <char> <char> <char> <int> <int> <char>
#> 1: 2010 13264290020000 211 71001 34882 115111 010010000
#> 2: 2010 13264290020000 211 71001 34882 115111 010020000
#> 3: 2010 13264290020000 211 71001 34882 115111 030210000
#> ---
#> 121: 2021 13264290020000 211 71163 62464 187561 050200001
#> 122: 2021 13264290020000 211 71163 62464 187561 080180000
#> 123: 2021 13264290020000 211 71163 62464 187561 140030000
#> agency_name agency_major_type agency_minor_type
#> <char> <char> <char>
#> 1: COUNTY OF COOK COOK COUNTY COOK
#> 2: FOREST PRESERVE DISTRICT ... COOK COUNTY COOK
#> 3: CITY OF CHICAGO MUNICIPALITY/TOWNSHIP MUNI
#> ---
#> 121: CHICAGO PARK DISTRICT AQU... MISCELLANEOUS BOND
#> 122: METRO WATER RECLAMATION D... MISCELLANEOUS WATER
#> 123: LOGAN AVONDALE HERMOSA EX... MISCELLANEOUS HEALTH
#> agency_tax_rate final_tax
#> <num> <num>
#> 1: 0.00423 282.30
#> 2: 0.00051 34.04
#> 3: 0.00914 609.97
#> ---
#> 121: 0.00000 0.00
#> 122: 0.00382 716.48
#> 123: 0.00022 41.26
And multiple PINs in the pin_vec
argument:
bills <- tax_bill(2021, c("13264290020000", "07101010391078", "10153080520000"))
print(bills, topn = 3)
#> Key: <year, pin, agency_num>
#> year pin class tax_code av eav agency_num
#> <int> <char> <char> <char> <int> <int> <char>
#> 1: 2021 07101010391078 299 35011 10738 32243 010010000
#> 2: 2021 07101010391078 299 35011 10738 32243 010010001
#> 3: 2021 07101010391078 299 35011 10738 32243 010020000
#> ---
#> 37: 2021 13264290020000 211 71163 62464 187561 050200001
#> 38: 2021 13264290020000 211 71163 62464 187561 080180000
#> 39: 2021 13264290020000 211 71163 62464 187561 140030000
#> agency_name agency_major_type agency_minor_type
#> <char> <char> <char>
#> 1: COUNTY OF COOK COOK COUNTY COOK
#> 2: CONSOLIDATED ELECTIONS COOK COUNTY COOK
#> 3: FOREST PRESERVE DISTRICT ... COOK COUNTY COOK
#> ---
#> 37: CHICAGO PARK DISTRICT AQU... MISCELLANEOUS BOND
#> 38: METRO WATER RECLAMATION D... MISCELLANEOUS WATER
#> 39: LOGAN AVONDALE HERMOSA EX... MISCELLANEOUS HEALTH
#> agency_tax_rate final_tax
#> <num> <num>
#> 1: 0.00446 99.20
#> 2: 0.00019 4.23
#> 3: 0.00058 12.90
#> ---
#> 37: 0.00000 0.00
#> 38: 0.00382 716.48
#> 39: 0.00022 41.26
Passing year_vec
and pin_vec
of different
lengths will yield the Cartesian product of those vectors:
bills <- tax_bill(
year_vec = 2006:2021,
pin_vec = c("13264290020000", "07101010391078", "10153080520000")
)
print(bills, topn = 3)
#> Key: <year, pin, agency_num>
#> year pin class tax_code av eav agency_num
#> <int> <char> <char> <char> <int> <int> <char>
#> 1: 2006 10153080520000 211 24023 54989 148888 010010000
#> 2: 2006 10153080520000 211 24023 54989 148888 010010001
#> 3: 2006 10153080520000 211 24023 54989 148888 010020000
#> ---
#> 584: 2021 13264290020000 211 71163 62464 187561 050200001
#> 585: 2021 13264290020000 211 71163 62464 187561 080180000
#> 586: 2021 13264290020000 211 71163 62464 187561 140030000
#> agency_name agency_major_type agency_minor_type
#> <char> <char> <char>
#> 1: COUNTY OF COOK COOK COUNTY COOK
#> 2: CONSOLIDATED ELECTIONS COOK COUNTY COOK
#> 3: FOREST PRESERVE DISTRICT ... COOK COUNTY COOK
#> ---
#> 584: CHICAGO PARK DISTRICT AQU... MISCELLANEOUS BOND
#> 585: METRO WATER RECLAMATION D... MISCELLANEOUS WATER
#> 586: LOGAN AVONDALE HERMOSA EX... MISCELLANEOUS HEALTH
#> agency_tax_rate final_tax
#> <num> <num>
#> 1: 0.00500 215.83
#> 2: 0.00000 0.00
#> 3: 0.00057 24.61
#> ---
#> 584: 0.00000 0.00
#> 585: 0.00382 716.48
#> 586: 0.00022 41.26
Passing year_vec
and pin_vec
of the same
length will match the vectors element-wise:
bills <- tax_bill(
year_vec = c(2012, 2006, 2021),
pin_vec = c("13264290020000", "07101010391078", "10153080520000")
)
print(bills, topn = 3)
#> Key: <year, pin, agency_num>
#> year pin class tax_code av eav agency_num
#> <int> <char> <char> <char> <int> <int> <char>
#> 1: 2012 13264290020000 211 71001 37280 104593 010010000
#> 2: 2012 13264290020000 211 71001 37280 104593 010020000
#> 3: 2012 13264290020000 211 71001 37280 104593 030210000
#> ---
#> 22: 2021 10153080520000 211 24023 42720 128275 050970000
#> 23: 2021 10153080520000 211 24023 42720 128275 080180000
#> 24: 2021 10153080520000 211 24023 42720 128275 090020000
#> agency_name agency_major_type agency_minor_type
#> <char> <char> <char>
#> 1: COUNTY OF COOK COOK COUNTY COOK
#> 2: FOREST PRESERVE DISTRICT ... COOK COUNTY COOK
#> 3: CITY OF CHICAGO MUNICIPALITY/TOWNSHIP MUNI
#> ---
#> 22: SKOKIE PARK DISTRICT MISCELLANEOUS PARK
#> 23: METRO WATER RECLAMATION D... MISCELLANEOUS WATER
#> 24: NORTH SHORE MOSQUITO ABAT... MISCELLANEOUS MOSQUITO
#> agency_tax_rate final_tax
#> <num> <num>
#> 1: 0.00531 518.22
#> 2: 0.00063 61.48
#> 3: 0.01151 1123.30
#> ---
#> 22: 0.00451 398.12
#> 23: 0.00382 337.21
#> 24: 0.00009 7.94
These basic arguments can even be used to calculate line-item bills for all PINs and years, assuming you have enough system memory:
# Query the PTAXSIM database directly to get all unique PINs
pins <- DBI::dbGetQuery(ptaxsim_db_conn, "SELECT DISTINCT pin FROM pin")
# Calculate all bills for all years (~350M rows, takes about 10 minutes)
bills <- tax_bill(2006:2021, pins$pin)
print(bills, topn = 3)
#> NOT RUN, takes too long on GitHub CI and requires ~90 GB of RAM
Optional arguments to tax_bill()
In addition to the two required arguments, tax_bill()
also has a number of optional arguments that can be used for
counterfactual or predictive analysis.
By default, these arguments are filled with historic data from the
PTAXSIM database. Each argument has a corresponding function, prefixed
with lookup_
, that retrieves data from the database and
puts it in the format expected by tax_bill()
. These
arguments and their corresponding lookup_
functions
are:
tax_code_vec
- Expects a character vector of Cook County tax codes. Must be the
same length as the combination of
year_vec
andpin_vec
, using the same recycling rules astax_bill()
- A tax code is a 5-digit number that identifies the unique overlap of taxing districts for a given PIN and year
- Tax codes can vary from year to year for the same PIN as taxing districts are created or destroyed
- Changing
tax_code_vec
“relocates” a PIN by changing the things that are taxing it. This can be useful for counterfactual analysis. For example, if you own property within a school district and want to know what your tax bill would be just outside the district, but otherwise within the same municipality, then you can find the tax code that represents that situation and plug it intotax_bill()
- Filled by
lookup_tax_code()
:-
Takes PINs and years as inputs and outputs a character vector of tax codes. Here is an example:
tax_code <- lookup_tax_code(2018:2021, "13264290020000") print(tax_code) #> [1] "71001" "71163" "71163" "71163"
-
Follows the same recycling rules as
tax_bill()
: input vectors of the same length return an element-wise output, input vectors of different lengths return the Cartesian product.tax_code <- lookup_tax_code( year = 2018:2021, pin = c("13264290020000", "07101010391078", "10153080520000") ) print(tax_code) #> [1] "71001" "35011" "24023" "71163" "35011" "24023" "71163" "35011" "24023" #> [10] "71163" "35011" "24023" tax_code <- lookup_tax_code( year = 2006:2021, pin = c("13264290020000", "07101010391078", "10153080520000") ) print(tax_code) #> [1] "71001" NA "24023" "71001" NA "24023" "71001" "35011" "24023" #> [10] "71001" "35011" "24023" "71001" "35011" "24023" "71001" "35011" "24023" #> [19] "71001" "35011" "24023" "71001" "35011" "24023" "71001" "35011" "24023" #> [28] "71001" "35011" "24023" "71001" "35011" "24023" "71001" "35011" "24023" #> [37] "71001" "35011" "24023" "71163" "35011" "24023" "71163" "35011" "24023" #> [46] "71163" "35011" "24023"
-
agency_dt
Column Name | Type | Key | Note |
---|---|---|---|
year |
int | 1 | See tax_bill() outputs |
tax_code |
varchar(5) | 2 | See tax_bill() outputs |
agency_num |
varchar(9) | 3 | See tax_bill() outputs |
agency_name |
varchar | See tax_bill() outputs | |
agency_major_type |
varchar | See tax_bill() outputs | |
agency_minor_type |
varchar | See tax_bill() outputs | |
agency_total_eav |
bigint | The total amount of EAV within the taxing district, otherwise known as the “base”. This is the denominator when calculating tax rates | |
agency_total_ext |
double | The total extension requested by the taxing district, otherwise known as the “levy”. This is the amount the district needs in tax revenue and is the numerator when calculating tax rates |
- Expects a
data.table
with the columns above: - Each row represents a taxing district (agency) that imposes a tax on
the specified
tax_code
. Differenttax_code
s can have different numbers of districts taxing them - The input
data.table
must be keyed on theyear
,tax_code
, andagency_num
columns and must be distinct, e.i. it must not have repeat rows - When joined to the PIN level (via
tax_code
) theagency_dt
table is used to determine which districts a PIN pays and how much. The basic calculation is: - Filled by
lookup_agency()
:-
Takes years and tax codes as inputs and outputs a keyed
data.table
of taxing districts, including their identifying information, extension, and base. Here is an example:tax_code <- lookup_tax_code(2021, "13264290020000") agency <- lookup_agency(2021, tax_code) print(agency, topn = 3) #> Key: <year, tax_code, agency_num> #> year tax_code agency_num agency_name #> <int> <char> <char> <char> #> 1: 2021 71163 010010000 COUNTY OF COOK #> 2: 2021 71163 010020000 FOREST PRESERVE DISTRICT ... #> 3: 2021 71163 030210000 CITY OF CHICAGO #> --- #> 9: 2021 71163 050200001 CHICAGO PARK DISTRICT AQU... #> 10: 2021 71163 080180000 METRO WATER RECLAMATION D... #> 11: 2021 71163 140030000 LOGAN AVONDALE HERMOSA EX... #> agency_major_type agency_minor_type agency_total_eav agency_total_ext #> <char> <char> <i64> <num> #> 1: COOK COUNTY COOK 175454836754 782528571.9 #> 2: COOK COUNTY COOK 175454836754 101763805.3 #> 3: MUNICIPALITY/TOWNSHIP MUNI 96913880556 1507979981.5 #> --- #> 9: MISCELLANEOUS BOND 96913880556 0.0 #> 10: MISCELLANEOUS WATER 172735189631 659848424.4 #> 11: MISCELLANEOUS HEALTH 4467629443 982878.5
-
Note that it returns a distinct set of districts, even if year and tax code are repeated:
tax_codes <- lookup_tax_code( year = 2021, pin = c("13264290020000", "13264290020000", "13264290020000") ) agency <- lookup_agency(c(2021, 2021, 2021), tax_codes) print(agency, topn = 3) #> Key: <year, tax_code, agency_num> #> year tax_code agency_num agency_name #> <int> <char> <char> <char> #> 1: 2021 71163 010010000 COUNTY OF COOK #> 2: 2021 71163 010020000 FOREST PRESERVE DISTRICT ... #> 3: 2021 71163 030210000 CITY OF CHICAGO #> --- #> 9: 2021 71163 050200001 CHICAGO PARK DISTRICT AQU... #> 10: 2021 71163 080180000 METRO WATER RECLAMATION D... #> 11: 2021 71163 140030000 LOGAN AVONDALE HERMOSA EX... #> agency_major_type agency_minor_type agency_total_eav agency_total_ext #> <char> <char> <i64> <num> #> 1: COOK COUNTY COOK 175454836754 782528571.9 #> 2: COOK COUNTY COOK 175454836754 101763805.3 #> 3: MUNICIPALITY/TOWNSHIP MUNI 96913880556 1507979981.5 #> --- #> 9: MISCELLANEOUS BOND 96913880556 0.0 #> 10: MISCELLANEOUS WATER 172735189631 659848424.4 #> 11: MISCELLANEOUS HEALTH 4467629443 982878.5
-
pin_dt
Column Name | Type | Key | Note |
---|---|---|---|
year |
int | 1 | See tax_bill() outputs |
pin |
varchar(14) | 2 | See tax_bill() outputs |
class |
varchar(3) | See tax_bill() outputs | |
av |
int | See tax_bill() outputs | |
eav |
int | See tax_bill() outputs | |
exe_homeowner |
int | Homeowner Exemption. All exemption amounts are in EAV, e.i. the Homeowner Exemption is typically an integer equal to 10000 | |
exe_senior |
int | Senior Exemption | |
exe_freeze |
int |
Senior
Freeze Exemption. Subtract from eav to get the frozen
amount |
|
exe_longtime_homeowner |
int | Longtime Homeowner Exemption | |
exe_disabled |
int | Persons with Disabilities Exemption | |
exe_vet_returning |
int | Returning Veterans Exemption | |
exe_vet_dis_lt50 |
int | Veterans with Disabilities Exemption. Level of disability < 50% | |
exe_vet_dis_50_69 |
int | Veterans with Disabilities Exemption. Level of disability >= 51% and <= 69% | |
exe_vet_dis_ge70 |
int | Veterans with Disabilities Exemption. Level of disability >= 70% | |
exe_abate |
int | Other tax abatements, exemptions, etc. |
- Expects a
data.table
with the columns above: - Each row represents the assessed value and exemptions for a specific PIN and year
- All exemptions are recorded in EAV. For example, per Illinois statute, the Homeowner Exemption is currently equal to $10,000 in EAV. This amount is multiplied by the local tax rate for the PIN to determine the final exemption amount. The higher the tax rate, the larger the exemption
- The input
data.table
must be keyed on theyear
andpin
columns and must be distinct, e.i. it must not have repeat rows - Filled by
lookup_pin()
:-
Takes years and PINs as inputs and outputs a keyed
data.table
of PINs, including their class, assessed value, and individual exemptions. Here is an example:pin <- lookup_pin(2021, "13264290020000") print(pin) #> Key: <year, pin> #> year pin class av eav exe_homeowner exe_senior exe_freeze #> <num> <char> <char> <int> <int> <int> <int> <int> #> 1: 2021 13264290020000 211 62464 187561 0 0 0 #> exe_longtime_homeowner exe_disabled exe_vet_returning exe_vet_dis_lt50 #> <int> <int> <int> <int> #> 1: 0 0 0 0 #> exe_vet_dis_50_69 exe_vet_dis_ge70 exe_abate #> <int> <int> <int> #> 1: 0 0 0
-
Repeat inputs will yield distinct outputs:
pin <- lookup_pin(2021, c("13264290020000", "13264290020000")) print(pin) #> Key: <year, pin> #> year pin class av eav exe_homeowner exe_senior exe_freeze #> <num> <char> <char> <int> <int> <int> <int> <int> #> 1: 2021 13264290020000 211 62464 187561 0 0 0 #> exe_longtime_homeowner exe_disabled exe_vet_returning exe_vet_dis_lt50 #> <int> <int> <int> <int> #> 1: 0 0 0 0 #> exe_vet_dis_50_69 exe_vet_dis_ge70 exe_abate #> <int> <int> <int> #> 1: 0 0 0
-
By default, will return the final assessed value for the year after all appeals are processed. This can be changed using an additional argument:
stage
. The options forstage
include:-
"mailed"
- Initial assessed values mailed to taxpayers, no appeals -
"certified"
- Assessed values after Assessor appeals are complete -
"board"
- Assessed values after Board of Review appeals are complete -
"clerk"
- Assessed values used by the Clerk to calculate the base and by the Treasurer to calculate bills. Identical to"board"
in the huge majority of cases
pin_mailed <- lookup_pin(2021, "13264290020000", stage = "mailed") pin_board <- lookup_pin(2021, "13264290020000", stage = "board") print(rbind(pin_mailed, pin_board)) #> year pin class av eav exe_homeowner exe_senior exe_freeze #> <num> <char> <char> <int> <int> <int> <int> <int> #> 1: 2021 13264290020000 211 80000 240216 0 0 0 #> 2: 2021 13264290020000 211 62464 187561 0 0 0 #> exe_longtime_homeowner exe_disabled exe_vet_returning exe_vet_dis_lt50 #> <int> <int> <int> <int> #> 1: 0 0 0 0 #> 2: 0 0 0 0 #> exe_vet_dis_50_69 exe_vet_dis_ge70 exe_abate #> <int> <int> <int> #> 1: 0 0 0 #> 2: 0 0 0
-
-
tif_dt
Column Name | Type | Key | Note |
---|---|---|---|
year |
int | 1 | See tax_bill() outputs |
tax_code |
varchar(5) | 2 | See tax_bill() outputs. Unique identifier for the tax situation created by the TIF |
agency_num |
varchar(9) | 3 | See tax_bill() outputs. Unique identifier for the TIF |
agency_name |
varchar | See tax_bill() outputs | |
agency_major_type |
varchar | See tax_bill() outputs | |
agency_minor_type |
varchar | See tax_bill() outputs | |
tif_share |
double | The percentage of this tax code/PIN’s revenue dedicated to the TIF. Increases as the EAV above the frozen amount increases |
- Expects a
data.table
with the columns above: - Each row represents a TIF that covers the specified
tax_code
. Eachtax_code
can only have one TIF, likewise for PINs - The input
data.table
must be keyed on theyear
,tax_code
, andagency_num
columns and must be distinct, e.i. it must not have repeat rows - Filled by
lookup_tif()
:-
Takes years and tax codes as inputs and outputs a keyed
data.table
of TIF districts, including their identifying information and distribution percentage/TIF share. Here is an example:tax_code <- lookup_tax_code(2021, "14172270080000") tif <- lookup_tif(2021, tax_code) print(tif, topn = 3) #> Key: <year, tax_code, agency_num> #> year tax_code agency_num agency_name agency_major_type #> <int> <char> <char> <char> <char> #> 1: 2021 73032 030210610 TIF - CHICAGO - WILSON YA... MUNICIPALITY/TOWNSHIP #> agency_minor_type tif_share #> <char> <num> #> 1: TIF 0.7337937
-
Returns a
data.table
with zero rows if the specifiedtax_code
is not within a TIF district:tax_code <- lookup_tax_code(2021, "13264290020000") tif <- lookup_tif(2021, tax_code) print(tif, topn = 3) #> Key: <year, tax_code, agency_num> #> Empty data.table (0 rows and 7 cols): year,tax_code,agency_num,agency_name,agency_major_type,agency_minor_type...
-
2. Common scenarios
In this section, we’ll look at how to use the tax_bill()
function to model common property tax scenarios. These scenarios are
simple enough that they do not require complicated secondary
tax_bill()
inputs (such as recalculating the tax base).
Let’s see some examples.
Change in assessed value
To recalculate a tax bill with a counterfactual assessed value,
simply change the pin_dt
column eav
. For
example, PIN 13-26-429-002-0000 had a 2020 estimated
market value of $403,710. What if it had a market value of $800,000?
To find out, we’ll first convert the new market value into the equalized assessed value (EAV).
mkt_value <- 800000
# For residential properties, AV is 10% of market value
assmt_value <- mkt_value * 0.1
# Get the final equalization factor for 2020 to get the equalized assessed value
eq_factor <- DBI::dbGetQuery(
ptaxsim_db_conn,
"SELECT * FROM eq_factor WHERE year = 2020"
) %>%
pull(eq_factor_final)
eq_value <- assmt_value * eq_factor
Then we’ll recalculate the bill using the new EAV and compare to the
original. To do so, we’ll create a counterfactual pin_dt
input by replacing the original 2020 eav
column with our
newly calculated EAV.
pin_dt_new <- lookup_pin(2020, "13264290020000") %>%
mutate(av = assmt_value, eav = eq_value) %>%
setDT(key = c("year", "pin")) # convert to data.table or R will complain
# Combine the original and updated bills into one output
rbind(
tax_bill(2020, "13264290020000") %>% mutate(type = "original"),
tax_bill(2020, "13264290020000", pin_dt = pin_dt_new) %>%
mutate(type = "counterfactual")
) %>%
group_by(year, pin, type, av) %>%
summarize(bill_total = sum(final_tax)) %>%
arrange(desc(type))
#> # A tibble: 2 × 5
#> # Groups: year, pin, type [2]
#> year pin type av bill_total
#> <int> <chr> <chr> <dbl> <dbl>
#> 1 2020 13264290020000 original 40371 9026.
#> 2 2020 13264290020000 counterfactual 80000 17886.
What if the market value went down to $300,000?
mkt_value <- 300000
assmt_value <- mkt_value * 0.1
eq_value <- assmt_value * eq_factor
pin_dt_new <- lookup_pin(2020, "13264290020000") %>%
mutate(av = assmt_value, eav = eq_value) %>%
setDT(key = c("year", "pin"))
rbind(
tax_bill(2020, "13264290020000") %>% mutate(type = "original"),
tax_bill(2020, "13264290020000", pin_dt = pin_dt_new) %>%
mutate(type = "counterfactual")
) %>%
group_by(year, pin, type, av) %>%
summarize(bill_total = sum(final_tax)) %>%
arrange(desc(type))
#> # A tibble: 2 × 5
#> # Groups: year, pin, type [2]
#> year pin type av bill_total
#> <int> <chr> <chr> <dbl> <dbl>
#> 1 2020 13264290020000 original 40371 9026.
#> 2 2020 13264290020000 counterfactual 30000 6707.
In this case, the percentage change in total tax roughly mirrors the percentage change in assessed value. However, this isn’t always true. During a reassessment, the base can increase significantly, resulting in bills going down even for properties with increased assessments.
Change in levy
Changes in levies are another common cause of increased tax bills. Calculating a levy change is not as straightforward as a change in AV, but a good rule-of-thumb is that levies can only increase by 5% or the rate of inflation, whichever is less.
NOTE: Correctly calculating a levy increase can be extremely complicated. Most levies are subject to numerous limiting laws (PTELL, rate limits, tax caps) that can vary by municipality/district
To change a levy, we need to alter the agency_total_ext
column of the agency_dt
input. Let’s see what happens if we
increase Chicago’s levy by 5%.
tax_code <- lookup_tax_code(2020, "13264290020000")
# Add 5% to only Chicago's levy for this PIN
agency_dt_new <- lookup_agency(2020, tax_code) %>%
mutate(agency_total_ext = ifelse(
agency_num == "030210000",
agency_total_ext + (agency_total_ext * 0.05),
agency_total_ext
)) %>%
setDT(key = c("year", "tax_code", "agency_num"))
rbind(
tax_bill(2020, "13264290020000") %>% mutate(type = "original"),
tax_bill(2020, "13264290020000", agency_dt = agency_dt_new) %>%
mutate(type = "counterfactual")
) %>%
group_by(year, pin, type, av) %>%
summarize(bill_total = sum(final_tax)) %>%
arrange(desc(type))
#> # A tibble: 2 × 5
#> # Groups: year, pin, type [2]
#> year pin type av bill_total
#> <int> <chr> <chr> <int> <dbl>
#> 1 2020 13264290020000 original 40371 9026.
#> 2 2020 13264290020000 counterfactual 40371 9129.
A 5% increase in the Chicago levy leads to a roughly $100 increase in taxes for this PIN, holding all else constant.
Tax bills over time
We can also use PTAXSIM to look at how tax bills have changed over
time. To do so, simply use tax_bill()
to get multiple
years’ worth of bills. The PTAXSIM database starts in 2006, so we can
use that as our earliest year.
bills <- tax_bill(2006:2021, "13264290020000")
bills_summ <- bills %>%
group_by(pin, year) %>%
summarize(bill_total = sum(final_tax))
We can plot the total bill over time:
Click here to show plot code
And even plot the change of the proportion of tax dollars going to each district:
Click here to show plot code
This PIN’s bill has increased over 3x since 2006. A significant portion of that increase is due to increases in the Chicago Public Schools levy.
Advanced scenarios
PTAXSIM can also be used to perform much more advanced analysis, such
as calculating the impact of appeals or changes to exemptions. These
analyses involve creating multiple counterfacual inputs for the
tax_bill()
function and typically require recalculating the
tax base.
If you’re comfortable with data.table
or
dplyr
and want to dive further into PTAXSIM, check out the
articles section for a list of advanced vignettes.